From 0d02460d57d815fbac58bf3aa55461a2335afce9 Mon Sep 17 00:00:00 2001 From: Vladimir Gamalyan Date: Sun, 12 Jul 2026 11:17:05 +0700 Subject: [PATCH] Prevent widget overlap with taskbar apps --- src/native_interop.rs | 63 +++++++++++++++++++- src/window.rs | 130 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 174 insertions(+), 19 deletions(-) diff --git a/src/native_interop.rs b/src/native_interop.rs index c745d08..270c9cb 100644 --- a/src/native_interop.rs +++ b/src/native_interop.rs @@ -77,6 +77,65 @@ pub fn find_child_window(parent: HWND, class_name: &str) -> Option { } } +/// Find a descendant window by class name. +pub fn find_descendant_window(parent: HWND, class_name: &str) -> Option { + struct SearchContext { + class_name: Vec, + result: Option, + } + + unsafe extern "system" fn enum_proc(hwnd: HWND, lparam: LPARAM) -> BOOL { + let context = &mut *(lparam.0 as *mut SearchContext); + let mut candidate = [0u16; 64]; + let len = unsafe { GetClassNameW(hwnd, &mut candidate) }; + if len > 0 && candidate[..len as usize] == context.class_name { + context.result = Some(hwnd); + return BOOL(0); + } + BOOL(1) + } + + let mut context = SearchContext { + class_name: class_name.encode_utf16().collect(), + result: None, + }; + unsafe { + let _ = EnumChildWindows( + parent, + Some(enum_proc), + LPARAM(&mut context as *mut _ as isize), + ); + } + context.result +} + +/// Get the task-switcher bounds used by Windows for pinned and running apps. +pub fn get_task_switcher_rect(taskbar_hwnd: HWND) -> Option { + // MSTaskListWClass is the narrowest match on classic taskbars. Windows 11 + // may expose only its parent or the ReBar container, so keep both as + // fallbacks. Their bounds still track the space occupied by task buttons. + ["MSTaskListWClass", "MSTaskSwWClass", "ReBarWindow32"] + .into_iter() + .find_map(|class_name| { + find_descendant_window(taskbar_hwnd, class_name) + .and_then(get_window_rect_safe) + .filter(|rect| rect.right > rect.left) + }) +} + +pub fn is_task_switcher_window(hwnd: HWND) -> bool { + let mut class_name = [0u16; 64]; + let len = unsafe { GetClassNameW(hwnd, &mut class_name) }; + if len == 0 { + return false; + } + + matches!( + String::from_utf16_lossy(&class_name[..len as usize]).as_str(), + "MSTaskListWClass" | "MSTaskSwWClass" | "ReBarWindow32" + ) +} + /// Get taskbar position via SHAppBarMessage pub fn get_taskbar_rect(taskbar_hwnd: HWND) -> Option { unsafe { @@ -141,8 +200,8 @@ pub fn move_window(hwnd: HWND, x: i32, y: i32, w: i32, h: i32) { } } -/// Set up a WinEvent hook for tray location changes -pub fn set_tray_event_hook( +/// Set up a WinEvent hook for taskbar layout changes +pub fn set_taskbar_event_hook( thread_id: u32, callback: unsafe extern "system" fn(HWINEVENTHOOK, u32, HWND, i32, i32, u32, u32), ) -> Option { diff --git a/src/window.rs b/src/window.rs index f6d261e..64e3417 100644 --- a/src/window.rs +++ b/src/window.rs @@ -473,11 +473,11 @@ fn sync_tray_icons(hwnd: HWND) { } fn toggle_widget_visibility(hwnd: HWND) { - let new_visible = { + let (new_visible, embedded) = { let mut state = lock_state(); if let Some(s) = state.as_mut() { s.widget_visible = !s.widget_visible; - s.widget_visible + (s.widget_visible, s.embedded) } else { return; } @@ -486,7 +486,9 @@ fn toggle_widget_visibility(hwnd: HWND) { unsafe { if new_visible { position_at_taskbar(); - let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE); + if !embedded { + let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE); + } render_layered(); } else { let _ = ShowWindow(hwnd, SW_HIDE); @@ -529,15 +531,23 @@ fn attach_to_taskbar(hwnd: HWND, requested_index: usize) -> bool { } else { diagnose::log("TrayNotifyWnd not found"); } + if let Some(rect) = native_interop::get_task_switcher_rect(taskbar.hwnd) { + diagnose::log(format!( + "task switcher rect=({}, {}, {}, {})", + rect.left, rect.top, rect.right, rect.bottom + )); + } else { + diagnose::log("task switcher window not found"); + } let hook = tray_notify.and_then(|tray_hwnd| { let thread_id = native_interop::get_window_thread_id(tray_hwnd); - native_interop::set_tray_event_hook(thread_id, on_tray_location_changed) + native_interop::set_taskbar_event_hook(thread_id, on_taskbar_layout_changed) }); if hook.is_some() { - diagnose::log("tray event hook installed"); + diagnose::log("taskbar layout event hook installed"); } else { - diagnose::log("tray event hook could not be installed"); + diagnose::log("taskbar layout event hook could not be installed"); } let mut state = lock_state(); @@ -573,9 +583,40 @@ fn tray_left_for_taskbar(taskbar_hwnd: HWND, taskbar_rect: RECT) -> i32 { tray_left } +fn max_safe_tray_offset( + taskbar_left: i32, + tray_left: i32, + task_switcher_right: Option, + widget_width: i32, +) -> Option { + let occupied_right = task_switcher_right + .unwrap_or(taskbar_left) + .clamp(taskbar_left, tray_left); + let available_width = tray_left - occupied_right; + (available_width >= widget_width).then_some(available_width - widget_width) +} + +fn max_offset_for_taskbar( + taskbar_hwnd: HWND, + taskbar_rect: RECT, + tray_left: i32, + widget_width: i32, +) -> Option { + let task_switcher_right = + native_interop::get_task_switcher_rect(taskbar_hwnd).map(|rect| rect.right); + max_safe_tray_offset( + taskbar_rect.left, + tray_left, + task_switcher_right, + widget_width, + ) +} + fn clamp_offset_for_taskbar(taskbar_hwnd: HWND, taskbar_rect: RECT, offset: i32) -> i32 { let tray_left = tray_left_for_taskbar(taskbar_hwnd, taskbar_rect); - let max_offset = (tray_left - taskbar_rect.left - total_widget_width()).max(0); + let max_offset = + max_offset_for_taskbar(taskbar_hwnd, taskbar_rect, tray_left, total_widget_width()) + .unwrap_or(0); offset.clamp(0, max_offset) } @@ -1352,12 +1393,12 @@ pub fn run() { // Register system tray icon(s) sync_tray_icons(hwnd); - // Position and show (only if widget_visible preference is true) + // Position and show when the user wants the widget and the taskbar has room. position_at_taskbar(); - if settings.widget_visible { + if !embedded && settings.widget_visible { let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE); } - diagnose::log("window shown"); + diagnose::log("window initialized"); // Initial render via UpdateLayeredWindow (for embedded) or InvalidateRect (fallback) render_layered(); @@ -2064,7 +2105,7 @@ fn position_at_taskbar() { refresh_dpi(); // Drop the app-state lock before any Win32 call that may synchronously // re-enter our window procedure. - let (hwnd, embedded, tray_offset, taskbar_hwnd) = { + let (hwnd, embedded, tray_offset, taskbar_hwnd, widget_visible) = { let state = lock_state(); let s = match state.as_ref() { Some(s) => s, @@ -2084,7 +2125,13 @@ fn position_at_taskbar() { } }; - (s.hwnd.to_hwnd(), s.embedded, s.tray_offset, taskbar_hwnd) + ( + s.hwnd.to_hwnd(), + s.embedded, + s.tray_offset, + taskbar_hwnd, + s.widget_visible, + ) }; let taskbar_rect = match native_interop::get_taskbar_rect(taskbar_hwnd) { @@ -2107,7 +2154,17 @@ fn position_at_taskbar() { } let widget_width = total_widget_width(); - let max_offset = (tray_left - taskbar_rect.left - widget_width).max(0); + let Some(max_offset) = + max_offset_for_taskbar(taskbar_hwnd, taskbar_rect, tray_left, widget_width) + else { + unsafe { + let _ = ShowWindow(hwnd, SW_HIDE); + } + diagnose::log(format!( + "taskbar has no free region for widget width={widget_width}; hiding widget" + )); + return; + }; let tray_offset = tray_offset.clamp(0, max_offset); let offset_changed = { let mut state = lock_state(); @@ -2144,6 +2201,12 @@ fn position_at_taskbar() { "positioned fallback widget at x={x} y={y} w={widget_width} h={widget_height}" )); } + + if widget_visible { + unsafe { + let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE); + } + } } fn compute_anchor_y(anchor_top: i32, anchor_height: i32, widget_height: i32) -> i32 { @@ -2151,8 +2214,8 @@ fn compute_anchor_y(anchor_top: i32, anchor_height: i32, widget_height: i32) -> (anchor_bottom - widget_height).max(anchor_top) } -/// WinEvent callback for tray icon location changes -unsafe extern "system" fn on_tray_location_changed( +/// WinEvent callback for tray and task-switcher layout changes +unsafe extern "system" fn on_taskbar_layout_changed( _hook: HWINEVENTHOOK, _event: u32, hwnd: HWND, @@ -2171,8 +2234,9 @@ unsafe extern "system" fn on_tray_location_changed( .map(|h| h == hwnd) .unwrap_or(false) }; + let is_taskbar_layout = is_tray || native_interop::is_task_switcher_window(hwnd); - if is_tray { + if is_taskbar_layout { if tray_reposition_is_suppressed() { return; } @@ -2401,7 +2465,13 @@ unsafe extern "system" fn wnd_proc( } } let widget_width = total_widget_width_for_state(s); - let max_offset = (tray_left - taskbar_rect.left - widget_width).max(0); + let max_offset = max_offset_for_taskbar( + taskbar_hwnd, + taskbar_rect, + tray_left, + widget_width, + ) + .unwrap_or(0); if new_offset > max_offset { new_offset = max_offset; } @@ -3290,3 +3360,29 @@ fn draw_rounded_rect(hdc: HDC, rect: &RECT, color: &Color, radius: i32) { let _ = DeleteObject(brush); } } + +#[cfg(test)] +mod taskbar_placement_tests { + use super::max_safe_tray_offset; + + #[test] + fn leaves_task_switcher_space_untouched() { + assert_eq!(max_safe_tray_offset(0, 1800, Some(900), 300), Some(600)); + } + + #[test] + fn uses_whole_pre_tray_area_when_task_switcher_is_unknown() { + assert_eq!(max_safe_tray_offset(100, 1800, None, 300), Some(1400)); + } + + #[test] + fn reports_no_space_when_widget_does_not_fit_after_task_switcher() { + assert_eq!(max_safe_tray_offset(0, 1800, Some(1600), 300), None); + } + + #[test] + fn clamps_out_of_range_task_switcher_bounds() { + assert_eq!(max_safe_tray_offset(100, 1800, Some(50), 300), Some(1400)); + assert_eq!(max_safe_tray_offset(100, 1800, Some(1900), 300), None); + } +}