Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/platform/dark_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
118 changes: 104 additions & 14 deletions src/platform/win32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -5991,13 +6011,51 @@ fn dialog_ctl_color(dlg_hwnd: HWND, hdc: HDC, edit_like: bool) -> Option<LRESULT
Some(LRESULT(brush.0))
}

/// Handles `WM_ERASEBKGND` for a dialog window. These dialogs are plain
/// `CreateWindowExW` windows with a null class background brush (not real
/// dialog-box windows), so nothing ever paints the client area outside of
/// child controls; without this, the gaps between controls stay
/// (light-themed) white even when `WM_CTLCOLOR*` has already darkened the
/// controls themselves. Returns `None` when dark mode is off (caller should
/// fall through to `DefWindowProc`).
fn dialog_erase_background(dlg_hwnd: HWND, hdc: HDC) -> 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 {
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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),
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down