From 97b02ac404638e866cf60998a706fc0bfaa8bd1f Mon Sep 17 00:00:00 2001 From: electricmessiah Date: Thu, 9 Jul 2026 01:40:13 -0400 Subject: [PATCH] Fix: fully theme Find/Replace, Goto Line, and Find in Files dialogs in dark mode - Dialog backgrounds (the space between controls) were never explicitly painted; the window class's null hbrBackground means DefWindowProc's default WM_ERASEBKGND handling is a no-op, so add an explicit handler that fills with the theme color (dark) or COLOR_BTNFACE (light). - Checkbox (BS_AUTOCHECKBOX) label text ignored WM_CTLCOLORBTN's color while visual styles were active, rendering black-on-dark and unreadable; strip visual styles on checkboxes in dark mode (reversible) so the color takes effect. - All theming (titlebar, child-control theme, checkbox visual style) was only ever applied once at dialog creation. Since these dialogs are cached (shown/hidden, not recreated), toggling dark mode after a dialog was already open left it permanently on the stale theme, and once dark had disabled a checkbox's visual style, going back to light never restored it. Re-apply full theming on every dialog show and immediately when dark mode is toggled while a dialog is already open. - Fixed a pre-existing layout bug surfaced by the above: the Wrap around checkbox's bounding box (x=330..410) overlapped the button column (x=360..450) by 50px in the Find/Replace dialog; widened the dialog and shifted the button column clear of it. --- src/platform/dark_mode.rs | 26 +++++++++ src/platform/win32.rs | 118 +++++++++++++++++++++++++++++++++----- 2 files changed, 130 insertions(+), 14 deletions(-) diff --git a/src/platform/dark_mode.rs b/src/platform/dark_mode.rs index c7caf24..2ce19fd 100644 --- a/src/platform/dark_mode.rs +++ b/src/platform/dark_mode.rs @@ -146,6 +146,32 @@ pub fn disable_visual_styles(hwnd: HWND) { } } +/// Reverses [`disable_visual_styles`]: restores the control's normal themed +/// (visual-styles) rendering. Passing real null pointers (as opposed to +/// empty-string pointers, which is what disables theming) resets the +/// control to its default theme. +pub fn restore_visual_styles(hwnd: HWND) { + if hwnd.0 == 0 { + return; + } + unsafe { + let _ = SetWindowTheme(hwnd, PCWSTR::null(), PCWSTR::null()); + } +} + +/// Sets a checkbox/radio-style `BS_AUTOCHECKBOX` control's visual style for +/// the given dark-mode state: disabled (classic rendering, so +/// `WM_CTLCOLORBTN`'s text color actually takes effect) when dark, restored +/// (normal themed rendering) when light. Idempotent — safe to call on every +/// theme change, not just once at creation. +pub fn set_checkbox_dark_mode(hwnd: HWND, dark: bool) { + if dark { + disable_visual_styles(hwnd); + } else { + restore_visual_styles(hwnd); + } +} + /// Applies the dark or light Explorer visual style to a control. Used on /// the vertical tab `ListView` so the system-drawn selection rectangle /// matches our dark theme instead of flashing the light Explorer chrome on diff --git a/src/platform/win32.rs b/src/platform/win32.rs index db63363..e3b8850 100644 --- a/src/platform/win32.rs +++ b/src/platform/win32.rs @@ -10,10 +10,11 @@ use windows::Win32::Foundation::{ POINT, RECT, WPARAM, }; use windows::Win32::Graphics::Gdi::{ - BeginPaint, CreatePen, CreateSolidBrush, DT_CENTER, DT_END_ELLIPSIS, DT_HIDEPREFIX, - DT_NOPREFIX, DT_SINGLELINE, DT_VCENTER, DeleteObject, DrawTextW, EndPaint, FillRect, HBRUSH, - HDC, HGDIOBJ, InvalidateRect, LineTo, MONITOR_DEFAULTTONULL, MonitorFromRect, MoveToEx, - PAINTSTRUCT, PS_SOLID, ScreenToClient, SelectObject, SetBkMode, SetTextColor, TRANSPARENT, + BeginPaint, COLOR_BTNFACE, CreatePen, CreateSolidBrush, DT_CENTER, DT_END_ELLIPSIS, + DT_HIDEPREFIX, DT_NOPREFIX, DT_SINGLELINE, DT_VCENTER, DeleteObject, DrawTextW, EndPaint, + FillRect, GetSysColorBrush, HBRUSH, HDC, HGDIOBJ, InvalidateRect, LineTo, + MONITOR_DEFAULTTONULL, MonitorFromRect, MoveToEx, PAINTSTRUCT, PS_SOLID, ScreenToClient, + SelectObject, SetBkMode, SetTextColor, TRANSPARENT, }; use windows::Win32::System::Com::CoTaskMemFree; use windows::Win32::System::DataExchange::COPYDATASTRUCT; @@ -3224,6 +3225,15 @@ fn show_find_dialog(hwnd: HWND, state: &mut AppState, mode: SearchDialogMode) -> state.search_dialog_mode = mode; seed_search_text_from_selection(state); if let Some(dialog) = &state.find_dialog { + apply_dialog_dark_mode( + dialog.hwnd, + &[ + dialog.match_case, + dialog.whole_word, + dialog.regex, + dialog.wrap, + ], + ); unsafe { ShowWindow(dialog.hwnd, SW_SHOW); let _ = SetFocus(dialog.find_edit); @@ -3233,7 +3243,7 @@ fn show_find_dialog(hwnd: HWND, state: &mut AppState, mode: SearchDialogMode) -> } let instance = module_instance()?; - let width = scale_for_dpi(hwnd, 460); + let width = scale_for_dpi(hwnd, 520); let height = scale_for_dpi(hwnd, 240); let hwnd_dialog = unsafe { CreateWindowExW( @@ -3290,6 +3300,7 @@ fn show_go_to_line_dialog(hwnd: HWND, state: &mut AppState) -> Result<()> { return Ok(()); } if let Some(dialog) = &state.go_to_line_dialog { + apply_dialog_dark_mode(dialog.hwnd, &[]); unsafe { ShowWindow(dialog.hwnd, SW_SHOW); let _ = SetFocus(dialog.line_edit); @@ -3382,6 +3393,15 @@ fn close_go_to_line_dialog(main_hwnd: HWND, state: &mut AppState) { fn show_find_in_files_dialog(hwnd: HWND, state: &mut AppState) -> Result<()> { if let Some(dialog) = &state.find_in_files { + apply_dialog_dark_mode( + dialog.hwnd, + &[ + dialog.match_case, + dialog.whole_word, + dialog.regex, + dialog.recurse, + ], + ); unsafe { ShowWindow(dialog.hwnd, SW_SHOW); } @@ -5991,13 +6011,51 @@ fn dialog_ctl_color(dlg_hwnd: HWND, hdc: HDC, edit_like: bool) -> Option LRESULT { + let mut rect = windows::Win32::Foundation::RECT::default(); + unsafe { + let _ = GetClientRect(dlg_hwnd, &mut rect); + } + // The window class's `hbrBackground` is null, so `DefWindowProc` is a + // no-op on `WM_ERASEBKGND` — it does NOT paint white, it leaves + // whatever was already there. We must always paint an explicit brush + // here (dark or light), never rely on falling through to the default. + let brush = match dialog_dark_theme(dlg_hwnd) { + Some(theme) => dark_mode::cached_solid_brush(theme.bg), + None => unsafe { GetSysColorBrush(COLOR_BTNFACE) }, + }; + unsafe { + let _ = FillRect(hdc, &rect, brush); + } + LRESULT(1) +} + /// Called from dialog `WM_CREATE` once all child controls exist. Applies the /// title-bar dark attribute and re-themes child controls so scrollbars and /// borders pick up the dark variant. -fn apply_dialog_dark_mode(dlg_hwnd: HWND) { +/// `checkboxes` are `BS_AUTOCHECKBOX` controls, which ignore `WM_CTLCOLORBTN` +/// (and thus our dark text color) while visual styles are active — Windows +/// theme-draws their label using the light-mode color regardless of what +/// the app returns. Stripping their visual style makes them fall back to +/// classic owner-color-respecting rendering, at the cost of the modern +/// checkbox glyph. +fn apply_dialog_dark_mode(dlg_hwnd: HWND, checkboxes: &[HWND]) { let dark = dialog_dark_theme(dlg_hwnd).is_some(); dark_mode::apply_to_window(dlg_hwnd, dark); dark_mode::theme_child_controls(dlg_hwnd, dark); + for &checkbox in checkboxes { + dark_mode::set_checkbox_dark_mode(checkbox, dark); + } + unsafe { + let _ = InvalidateRect(dlg_hwnd, None, true); + } } fn loword(value: usize) -> u16 { @@ -6772,6 +6830,35 @@ fn set_editor_dark_mode(hwnd: HWND, state: &mut AppState, enabled: bool) { SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE, ); } + // Find/Replace, Goto Line, and Find in Files are cached windows (shown + // and hidden, not recreated), so they need to be re-themed here too — + // otherwise a dialog opened before this toggle stays on the old theme + // until it happens to be closed and reopened. + if let Some(dialog) = &state.find_dialog { + apply_dialog_dark_mode( + dialog.hwnd, + &[ + dialog.match_case, + dialog.whole_word, + dialog.regex, + dialog.wrap, + ], + ); + } + if let Some(dialog) = &state.go_to_line_dialog { + apply_dialog_dark_mode(dialog.hwnd, &[]); + } + if let Some(dialog) = &state.find_in_files { + apply_dialog_dark_mode( + dialog.hwnd, + &[ + dialog.match_case, + dialog.whole_word, + dialog.regex, + dialog.recurse, + ], + ); + } persist_ui_settings(state); } @@ -7363,7 +7450,7 @@ unsafe extern "system" fn find_wndproc( w!("Button"), w!("Find Next"), button_style, - scale(360), + scale(420), scale(10), scale(90), scale(22), @@ -7377,7 +7464,7 @@ unsafe extern "system" fn find_wndproc( w!("Button"), w!("Find Prev"), button_style, - scale(360), + scale(420), scale(40), scale(90), scale(22), @@ -7391,7 +7478,7 @@ unsafe extern "system" fn find_wndproc( w!("Button"), w!("Replace"), button_style, - scale(360), + scale(420), scale(70), scale(90), scale(22), @@ -7405,7 +7492,7 @@ unsafe extern "system" fn find_wndproc( w!("Button"), w!("Replace All"), button_style, - scale(360), + scale(420), scale(100), scale(90), scale(22), @@ -7433,7 +7520,7 @@ unsafe extern "system" fn find_wndproc( w!("Button"), w!("Close"), button_style, - scale(360), + scale(420), scale(130), scale(90), scale(22), @@ -7487,9 +7574,10 @@ unsafe extern "system" fn find_wndproc( let _ = SetFocus(find_edit); } } - apply_dialog_dark_mode(hwnd); + apply_dialog_dark_mode(hwnd, &[match_case, whole_word, regex, wrap]); LRESULT(0) } + WM_ERASEBKGND => dialog_erase_background(hwnd, HDC(wparam.0 as isize)), WM_CTLCOLORDLG | WM_CTLCOLORSTATIC | WM_CTLCOLORBTN => { if let Some(r) = dialog_ctl_color(hwnd, HDC(wparam.0 as isize), false) { return r; @@ -7657,9 +7745,10 @@ unsafe extern "system" fn goto_line_wndproc( let _ = SetFocus(line_edit); } } - apply_dialog_dark_mode(hwnd); + apply_dialog_dark_mode(hwnd, &[]); LRESULT(0) } + WM_ERASEBKGND => dialog_erase_background(hwnd, HDC(wparam.0 as isize)), WM_CTLCOLORDLG | WM_CTLCOLORSTATIC | WM_CTLCOLORBTN => { if let Some(r) = dialog_ctl_color(hwnd, HDC(wparam.0 as isize), false) { return r; @@ -8063,9 +8152,10 @@ unsafe extern "system" fn find_in_files_wndproc( }); } - apply_dialog_dark_mode(hwnd); + apply_dialog_dark_mode(hwnd, &[match_case, whole_word, regex, recurse]); LRESULT(0) } + WM_ERASEBKGND => dialog_erase_background(hwnd, HDC(wparam.0 as isize)), WM_CTLCOLORDLG | WM_CTLCOLORSTATIC | WM_CTLCOLORBTN => { if let Some(r) = dialog_ctl_color(hwnd, HDC(wparam.0 as isize), false) { return r;