From 87b49d639f0520343c9087268a890a717921b105 Mon Sep 17 00:00:00 2001 From: andreescocard Date: Wed, 24 Jun 2026 18:09:34 -0300 Subject: [PATCH 1/2] fix(ui): preserve taskbar monitor selection across display changes Store the selected taskbar's monitor device name in settings and prefer it when reattaching the widget. This keeps the widget anchored to the same physical monitor when taskbar ordering changes after monitor connect, disconnect, or reconfiguration, while still falling back to the saved index for existing settings and compatibility. --- src/native_interop.rs | 31 +++++++++++++++++++++++++++++-- src/window.rs | 30 ++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/native_interop.rs b/src/native_interop.rs index c745d08..638a9dc 100644 --- a/src/native_interop.rs +++ b/src/native_interop.rs @@ -1,5 +1,8 @@ use windows::core::PCWSTR; use windows::Win32::Foundation::{BOOL, HWND, LPARAM, RECT}; +use windows::Win32::Graphics::Gdi::{ + GetMonitorInfoW, MonitorFromWindow, MONITORINFOEXW, MONITOR_DEFAULTTONEAREST, +}; use windows::Win32::UI::Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK}; use windows::Win32::UI::Shell::{SHAppBarMessage, ABM_GETTASKBARPOS, APPBARDATA}; use windows::Win32::UI::WindowsAndMessaging::*; @@ -24,10 +27,33 @@ pub const WM_APP: u32 = 0x8000; pub const WM_APP_USAGE_UPDATED: u32 = WM_APP + 1; pub const WM_APP_TRAY: u32 = WM_APP + 3; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub struct TaskbarWindow { pub hwnd: HWND, pub rect: RECT, + /// Stable monitor identity (e.g. "\\\\.\\DISPLAY1"). Used to keep the widget + /// anchored to the same physical monitor across topology changes, where the + /// geometric ordering (and therefore the index) of taskbars can shift. + pub device: String, +} + +/// Stable identifier of the monitor a window currently lives on. +pub fn monitor_device_name(hwnd: HWND) -> String { + unsafe { + let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + let mut info = MONITORINFOEXW::default(); + info.monitorInfo.cbSize = std::mem::size_of::() as u32; + if GetMonitorInfoW(monitor, &mut info as *mut _ as *mut _).as_bool() { + let len = info + .szDevice + .iter() + .position(|&c| c == 0) + .unwrap_or(info.szDevice.len()); + String::from_utf16_lossy(&info.szDevice[..len]) + } else { + String::new() + } + } } pub fn find_taskbars() -> Vec { @@ -39,7 +65,8 @@ pub fn find_taskbars() -> Vec { let class_name = String::from_utf16_lossy(&class_name[..len as usize]); if class_name == "Shell_TrayWnd" || class_name == "Shell_SecondaryTrayWnd" { if let Some(rect) = get_taskbar_rect(hwnd).or_else(|| get_window_rect_safe(hwnd)) { - taskbars.push(TaskbarWindow { hwnd, rect }); + let device = monitor_device_name(hwnd); + taskbars.push(TaskbarWindow { hwnd, rect, device }); } } } diff --git a/src/window.rs b/src/window.rs index f6d261e..fe73d66 100644 --- a/src/window.rs +++ b/src/window.rs @@ -84,6 +84,7 @@ struct AppState { last_update_check_unix: Option, taskbar_index: usize, + taskbar_device: Option, tray_offset: i32, dragging: bool, drag_start_mouse_x: i32, @@ -302,6 +303,8 @@ struct SettingsFile { tray_offset: i32, #[serde(default)] taskbar_index: usize, + #[serde(default, skip_serializing_if = "Option::is_none")] + taskbar_device: Option, #[serde(default = "default_poll_interval")] poll_interval_ms: u32, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -323,6 +326,7 @@ impl Default for SettingsFile { Self { tray_offset: 0, taskbar_index: 0, + taskbar_device: None, poll_interval_ms: default_poll_interval(), language: None, last_update_check_unix: None, @@ -382,6 +386,7 @@ fn save_state_settings() { save_settings(&SettingsFile { tray_offset: s.tray_offset, taskbar_index: s.taskbar_index, + taskbar_device: s.taskbar_device.clone(), poll_interval_ms: s.poll_interval_ms, language: s .language_override @@ -494,17 +499,28 @@ fn toggle_widget_visibility(hwnd: HWND) { } } -fn attach_to_taskbar(hwnd: HWND, requested_index: usize) -> bool { +fn attach_to_taskbar( + hwnd: HWND, + requested_index: usize, + requested_device: Option<&str>, +) -> bool { let taskbars = native_interop::find_taskbars(); if taskbars.is_empty() { diagnose::log("taskbar not found; using fallback popup window"); return false; } - let index = requested_index.min(taskbars.len().saturating_sub(1)); - let taskbar = taskbars[index]; + // Prefer the taskbar on the same physical monitor we were last anchored to. + // The geometric index is unstable across monitor connect/disconnect/reorder, + // so matching by device name keeps the widget on the user's chosen monitor. + let index = requested_device + .filter(|d| !d.is_empty()) + .and_then(|device| taskbars.iter().position(|t| t.device == device)) + .unwrap_or_else(|| requested_index.min(taskbars.len().saturating_sub(1))); + let taskbar = taskbars[index].clone(); diagnose::log(format!( - "taskbar selected index={index} count={} hwnd={:?} rect=({}, {}, {}, {})", + "taskbar selected index={index} device={} count={} hwnd={:?} rect=({}, {}, {}, {})", + taskbar.device, taskbars.len(), taskbar.hwnd, taskbar.rect.left, @@ -546,6 +562,7 @@ fn attach_to_taskbar(hwnd: HWND, requested_index: usize) -> bool { s.tray_notify_hwnd = tray_notify; s.win_event_hook = hook; s.taskbar_index = index; + s.taskbar_device = Some(taskbar.device.clone()); s.embedded = true; } true @@ -1321,6 +1338,7 @@ pub fn run() { update_status: UpdateStatus::Idle, last_update_check_unix: settings.last_update_check_unix, taskbar_index: settings.taskbar_index, + taskbar_device: settings.taskbar_device.clone(), tray_offset: settings.tray_offset, dragging: false, drag_start_mouse_x: 0, @@ -1331,7 +1349,7 @@ pub fn run() { } // Try to embed in taskbar - if attach_to_taskbar(hwnd, settings.taskbar_index) { + if attach_to_taskbar(hwnd, settings.taskbar_index, settings.taskbar_device.as_deref()) { embedded = true; } @@ -2487,7 +2505,7 @@ unsafe extern "system" fn wnd_proc( s.tray_offset = new_offset; } } - if attach_to_taskbar(hwnd, target_index) { + if attach_to_taskbar(hwnd, target_index, Some(&target_taskbar.device)) { position_at_taskbar(); render_layered(); } From b85b9318f1fcaf238144ef95fbe3806e1d2a71db Mon Sep 17 00:00:00 2001 From: andreescocard Date: Fri, 26 Jun 2026 19:44:58 -0300 Subject: [PATCH 2/2] fix(taskbar): stop widget vanishing when the Start menu opens The widget embeds as a child of the taskbar, which keeps it painted above the shell's Start/Search acrylic backdrop. The actual bug was the watchdog: opening the Start menu makes the taskbar briefly drop out of EnumWindows (~5s) even though its HWND is still valid, and the watchdog read that as an explorer restart and relaunched the whole process, wiping the widget. Guard the watchdog with IsWindow on the stored taskbar handle: if the handle still exists it is a transient Start-menu enumeration blip, so skip. Only relaunch when the handle is truly destroyed (a real explorer restart) and only after TASKBAR_MISSING_TICKS consecutive confirmations. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_017m74C6QgGGawTbeTb8NkBG --- src/native_interop.rs | 4 ++++ src/window.rs | 42 +++++++++++++++++++++++++++++------------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/native_interop.rs b/src/native_interop.rs index 638a9dc..c35ec77 100644 --- a/src/native_interop.rs +++ b/src/native_interop.rs @@ -196,6 +196,10 @@ pub fn get_window_thread_id(hwnd: HWND) -> u32 { unsafe { GetWindowThreadProcessId(hwnd, None) } } +pub fn window_exists(hwnd: HWND) -> bool { + unsafe { IsWindow(hwnd).as_bool() } +} + /// Unhook a WinEvent hook pub fn unhook_win_event(hook: HWINEVENTHOOK) { unsafe { diff --git a/src/window.rs b/src/window.rs index fe73d66..82ca80c 100644 --- a/src/window.rs +++ b/src/window.rs @@ -141,6 +141,8 @@ const TRAY_ICON_UPDATE_REPOSITION_SUPPRESS_MS: u64 = 750; /// recreates the taskbar and wipes our tray-icon registration). const TASKBAR_WATCH_INTERVAL_SECS: u64 = 2; +const TASKBAR_MISSING_TICKS: u32 = 2; + static SUPPRESS_TRAY_REPOSITION_UNTIL: Mutex> = Mutex::new(None); /// Current system DPI (96 = 100% scaling, 144 = 150%, 192 = 200%, etc.) @@ -233,21 +235,35 @@ fn relaunch_self() { /// dedicated thread (independent of the dead message loop) polls the taskbar /// handle and, when it changes, relaunches the widget as a fresh process. fn spawn_taskbar_watchdog() { - std::thread::spawn(move || loop { - std::thread::sleep(Duration::from_secs(TASKBAR_WATCH_INTERVAL_SECS)); - let stored = { - let state = lock_state(); - state.as_ref().and_then(|s| s.taskbar_hwnd) - }; - // Only relevant once we have embedded into a taskbar at least once. - let Some(old) = stored else { - continue; - }; - let taskbars = native_interop::find_taskbars(); - if !taskbars.is_empty() && !taskbars.iter().any(|taskbar| taskbar.hwnd == old) { + std::thread::spawn(move || { + let mut missing_streak = 0u32; + loop { + std::thread::sleep(Duration::from_secs(TASKBAR_WATCH_INTERVAL_SECS)); + let stored = { + let state = lock_state(); + state.as_ref().and_then(|s| s.taskbar_hwnd) + }; + let Some(old) = stored else { + missing_streak = 0; + continue; + }; + if native_interop::window_exists(old) { + missing_streak = 0; + continue; + } + let taskbars = native_interop::find_taskbars(); + if taskbars.is_empty() || taskbars.iter().any(|taskbar| taskbar.hwnd == old) { + missing_streak = 0; + continue; + } + missing_streak += 1; + if missing_streak < TASKBAR_MISSING_TICKS { + continue; + } + missing_streak = 0; let new = taskbars[0].hwnd; diagnose::log(format!( - "watchdog: taskbar changed old={:?} new={:?} -> relaunching", + "watchdog: taskbar destroyed old={:?} new={:?} -> relaunching", old.0, new.0 )); relaunch_self();