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() {