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
35 changes: 33 additions & 2 deletions src/native_interop.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand All @@ -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::<MONITORINFOEXW>() 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<TaskbarWindow> {
Expand All @@ -39,7 +65,8 @@ pub fn find_taskbars() -> Vec<TaskbarWindow> {
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 });
}
}
}
Expand Down Expand Up @@ -169,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 {
Expand Down
72 changes: 53 additions & 19 deletions src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ struct AppState {
last_update_check_unix: Option<u64>,

taskbar_index: usize,
taskbar_device: Option<String>,
tray_offset: i32,
dragging: bool,
drag_start_mouse_x: i32,
Expand Down Expand Up @@ -140,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<Option<Instant>> = Mutex::new(None);

/// Current system DPI (96 = 100% scaling, 144 = 150%, 192 = 200%, etc.)
Expand Down Expand Up @@ -232,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();
Expand Down Expand Up @@ -302,6 +319,8 @@ struct SettingsFile {
tray_offset: i32,
#[serde(default)]
taskbar_index: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
taskbar_device: Option<String>,
#[serde(default = "default_poll_interval")]
poll_interval_ms: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
Expand All @@ -323,6 +342,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,
Expand Down Expand Up @@ -382,6 +402,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
Expand Down Expand Up @@ -494,17 +515,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,
Expand Down Expand Up @@ -546,6 +578,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
Expand Down Expand Up @@ -1321,6 +1354,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,
Expand All @@ -1331,7 +1365,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;
}

Expand Down Expand Up @@ -2487,7 +2521,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();
}
Expand Down