From 4be9ff0468b0a9de09164a4d0f1f0d8c7a8bc597 Mon Sep 17 00:00:00 2001 From: jonghooneun <25918149+jonghooneun@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:37:50 +0900 Subject: [PATCH] fix(window): leave an already hosted taskbar surface in place on render Every routine render re-embedded the taskbar surface: it rewrote the window styles and called SetWindowPos with SWP_FRAMECHANGED even though the surface already was a child of that taskbar. A frame change counts as a window placement change, and Windows answers each one by posting a WM_MOUSEMOVE with unchanged coordinates to whichever window is under the pointer. Applications that hide the pointer while typing therefore showed it again on every widget render: once per poll, or every second with a theme that uses a seconds-resolution clock. embed_as_child now returns early when the surface already has this parent and the hosted styles, and taskbar positioning only calls SetWindowPos when the surface is not already the top sibling at the target rectangle. Parent changes, popup-to-child conversion and real moves are unchanged. Co-Authored-By: Claude Fable 5.2 --- src/native_interop.rs | 19 +++++++----- src/window/layered_window_tests.rs | 34 +++++++++++++++++++++ src/window/positioning.rs | 48 ++++++++++++++++++++++++------ 3 files changed, 85 insertions(+), 16 deletions(-) diff --git a/src/native_interop.rs b/src/native_interop.rs index b7ac478d..3bd6d407 100644 --- a/src/native_interop.rs +++ b/src/native_interop.rs @@ -315,15 +315,20 @@ pub fn embed_as_child(hwnd: HWND, parent: HWND) { unsafe { let current_parent = GetParent(hwnd).ok(); let ex_style = GetWindowLongW(hwnd, GWL_EXSTYLE); - let _ = SetWindowLongW( - hwnd, - GWL_EXSTYLE, - (ex_style | WS_EX_TOOLWINDOW.0 as i32 | WS_EX_NOACTIVATE.0 as i32) - & !(WS_EX_TOPMOST.0 as i32), - ); - + let new_ex_style = (ex_style | WS_EX_TOOLWINDOW.0 as i32 | WS_EX_NOACTIVATE.0 as i32) + & !(WS_EX_TOPMOST.0 as i32); let style = GetWindowLongW(hwnd, GWL_STYLE) as u32; let new_style = (style & !WS_POPUP_STYLE) | WS_CHILD_STYLE | WS_CLIPSIBLINGS_STYLE; + // Already hosted by this parent with the hosted styles: nothing to do. + // Rewriting the styles and forcing SWP_FRAMECHANGED on every render is + // a window placement change each time, and Windows answers every + // placement change by posting an unmoved WM_MOUSEMOVE to the window + // under the pointer (apps that hide the pointer while typing then + // show it again on every widget refresh). + if current_parent == Some(parent) && new_ex_style == ex_style && new_style == style { + return; + } + let _ = SetWindowLongW(hwnd, GWL_EXSTYLE, new_ex_style); let _ = SetWindowLongW(hwnd, GWL_STYLE, new_style as i32); if current_parent != Some(parent) { diff --git a/src/window/layered_window_tests.rs b/src/window/layered_window_tests.rs index a6d1c8c9..d8227837 100644 --- a/src/window/layered_window_tests.rs +++ b/src/window/layered_window_tests.rs @@ -3,6 +3,7 @@ use std::cell::RefCell; thread_local! { static LAYERED_TRANSITIONS: RefCell> = const { RefCell::new(Vec::new()) }; + static FRAME_CHANGES: std::cell::Cell = const { std::cell::Cell::new(0) }; } unsafe extern "system" fn observe_layered_style( @@ -21,6 +22,12 @@ unsafe extern "system" fn observe_layered_style( }); } } + if message == WM_WINDOWPOSCHANGING { + let position = &*(lparam.0 as *const WINDOWPOS); + if position.flags.contains(SWP_FRAMECHANGED) { + FRAME_CHANGES.with(|changes| changes.set(changes.get() + 1)); + } + } DefWindowProcW(hwnd, message, wparam, lparam) } @@ -89,6 +96,10 @@ fn take_transitions() -> Vec { LAYERED_TRANSITIONS.with(|transitions| std::mem::take(&mut *transitions.borrow_mut())) } +fn take_frame_changes() -> usize { + FRAME_CHANGES.with(|changes| changes.replace(0)) +} + #[test] fn watchdog_tray_requests_leave_a_delayed_reposition_pending() { let window = TestWindow::new(WINDOW_EX_STYLE::default()); @@ -175,3 +186,26 @@ fn reparenting_desktop_composition_windows_does_not_enable_layering() { assert_eq!(ex_style & WS_EX_LAYERED.0, 0); assert_ne!(ex_style & WS_EX_NOREDIRECTIONBITMAP.0, 0); } + +#[test] +fn re_embedding_a_hosted_surface_does_not_change_its_frame() { + let parent = TestWindow::new(WINDOW_EX_STYLE::default()); + let window = TestWindow::new(WS_EX_LAYERED); + native_interop::embed_as_child(window.0, parent.0); + assert!(take_frame_changes() > 0); + + // Routine renders re-embed into the same host. A frame change there is a + // window placement change, which makes Windows post an unmoved + // WM_MOUSEMOVE to whichever window is under the pointer. + for _ in 0..3 { + native_interop::embed_as_child(window.0, parent.0); + } + assert_eq!(take_frame_changes(), 0); + + // A surface that left the hosted form is still converted back. + native_interop::make_popup(window.0, true); + take_frame_changes(); + native_interop::embed_as_child(window.0, parent.0); + assert!(take_frame_changes() > 0); + assert_eq!(unsafe { GetParent(window.0) }.unwrap(), parent.0); +} diff --git a/src/window/positioning.rs b/src/window/positioning.rs index addb3c7b..59df6dc8 100644 --- a/src/window/positioning.rs +++ b/src/window/positioning.rs @@ -245,6 +245,32 @@ pub(super) fn render_custom_window( } } +/// True when a hosted child is already the top sibling at exactly this +/// rectangle (origin in `parent` client coordinates). +unsafe fn hosted_child_is_placed( + hwnd: HWND, + parent: HWND, + origin: POINT, + width: i32, + height: i32, +) -> bool { + if GetTopWindow(Some(parent)).ok() != Some(hwnd) { + return false; + } + let Some(rect) = native_interop::get_window_rect_safe(hwnd) else { + return false; + }; + let mut current = [POINT { + x: rect.left, + y: rect.top, + }]; + MapWindowPoints(None, Some(parent), &mut current); + current[0].x == origin.x + && current[0].y == origin.y + && rect.right - rect.left == width + && rect.bottom - rect.top == height +} + pub(super) fn position_custom_theme(hwnd: HWND, theme: &ThemeDocument, scale: f64) -> bool { position_custom_theme_internal(hwnd, theme, scale) } @@ -320,15 +346,19 @@ pub(super) fn position_custom_theme_internal( ensure_tray_event_hook_for_taskbar(hwnd, taskbar.hwnd); let mut point = [POINT { x, y }]; MapWindowPoints(None, Some(taskbar.hwnd), &mut point); - let _ = SetWindowPos( - hwnd, - Some(HWND_TOP), - point[0].x, - point[0].y, - width, - height, - SWP_NOACTIVATE, - ); + // Routine renders land on the same rectangle; only touch the + // window when its place or z-order actually has to change. + if !hosted_child_is_placed(hwnd, taskbar.hwnd, point[0], width, height) { + let _ = SetWindowPos( + hwnd, + Some(HWND_TOP), + point[0].x, + point[0].y, + width, + height, + SWP_NOACTIVATE, + ); + } } SurfaceNest::Desktop => { if let Some(desktop) = native_interop::find_desktop_host() {