11//! Windows implementation of Win32 wrappers.
22
33use crate :: rect:: Rect ;
4- use windows:: Win32 :: Foundation :: { CloseHandle , RECT as WinRect , BOOL , HWND , LPARAM , POINT } ;
4+ use windows:: Win32 :: Foundation :: { CloseHandle , BOOL , HWND , LPARAM , POINT , RECT as WinRect } ;
55use windows:: Win32 :: Graphics :: Dwm :: { DwmGetWindowAttribute , DWMWA_EXTENDED_FRAME_BOUNDS } ;
66use windows:: Win32 :: Graphics :: Gdi :: {
7- EnumDisplayMonitors , GetMonitorInfoW , HMONITOR , MonitorFromPoint , MonitorFromWindow ,
7+ EnumDisplayMonitors , GetMonitorInfoW , MonitorFromPoint , MonitorFromWindow , HMONITOR ,
88 MONITORINFOEXW , MONITOR_DEFAULTTONEAREST ,
99} ;
1010use windows:: Win32 :: System :: Threading :: {
11- OpenProcess , PROCESS_QUERY_LIMITED_INFORMATION , PROCESS_NAME_NATIVE ,
12- QueryFullProcessImageNameW ,
11+ OpenProcess , QueryFullProcessImageNameW , PROCESS_NAME_NATIVE , PROCESS_QUERY_LIMITED_INFORMATION ,
1312} ;
1413use windows:: Win32 :: UI :: WindowsAndMessaging :: {
15- GetAncestor , GetCursorPos , GetForegroundWindow , GetWindowRect , GetWindowThreadProcessId ,
16- IsWindow , SetCursorPos , SetForegroundWindow , SetWindowPos ,
17- GA_ROOT , SET_WINDOW_POS_FLAGS ,
14+ GetAncestor , GetCursorPos , GetForegroundWindow , GetWindowLongW , GetWindowRect ,
15+ GetWindowThreadProcessId , IsWindow , IsWindowVisible , SetCursorPos , SetForegroundWindow ,
16+ SetWindowPos , GA_ROOT , GWL_EXSTYLE , GWL_STYLE , SET_WINDOW_POS_FLAGS , WS_CAPTION ,
17+ WS_EX_TOOLWINDOW , WS_MAXIMIZEBOX , WS_MINIMIZEBOX , WS_POPUP , WS_THICKFRAME , WS_VISIBLE ,
1818} ;
1919
2020const SWP_NOZORDER_U : u32 = 0x0004 ;
@@ -38,24 +38,69 @@ fn rect_to_win_rect(r: &Rect) -> WinRect {
3838 }
3939}
4040
41- /// Get foreground window handle (root/top-level), or None if invalid.
42- /// Uses GetAncestor(GA_ROOT) so we always move the top-level window, not a child
43- /// (e.g. when the Tauri/WebView window is focused, the foreground may be the WebView child).
44- pub fn get_foreground_window ( ) -> Option < HWND > {
41+ fn has_style ( style : u32 , flag : u32 ) -> bool {
42+ ( style & flag) == flag
43+ }
44+
45+ fn is_non_processable_popup ( style : u32 ) -> bool {
46+ let is_popup = has_style ( style, WS_POPUP . 0 ) ;
47+ let has_thick_frame = has_style ( style, WS_THICKFRAME . 0 ) ;
48+ let has_caption = has_style ( style, WS_CAPTION . 0 ) ;
49+ let has_minimize_or_maximize =
50+ has_style ( style, WS_MINIMIZEBOX . 0 ) || has_style ( style, WS_MAXIMIZEBOX . 0 ) ;
51+
52+ is_popup && !( has_thick_frame && ( has_caption || has_minimize_or_maximize) )
53+ }
54+
55+ fn is_snap_target_window ( hwnd : HWND ) -> bool {
56+ unsafe {
57+ if hwnd. 0 . is_null ( ) || !IsWindow ( hwnd) . as_bool ( ) || !IsWindowVisible ( hwnd) . as_bool ( ) {
58+ return false ;
59+ }
60+
61+ let style = GetWindowLongW ( hwnd, GWL_STYLE ) as u32 ;
62+ let ex_style = GetWindowLongW ( hwnd, GWL_EXSTYLE ) as u32 ;
63+
64+ if !has_style ( style, WS_VISIBLE . 0 ) {
65+ return false ;
66+ }
67+ if has_style ( ex_style, WS_EX_TOOLWINDOW . 0 ) {
68+ return false ;
69+ }
70+ if is_non_processable_popup ( style) {
71+ return false ;
72+ }
73+
74+ true
75+ }
76+ }
77+
78+ /// Resolve a caller-provided HWND to a snap target window, filtering out shell popups
79+ /// and utility surfaces like the Start menu or tool windows.
80+ pub fn resolve_snap_target_window ( hwnd : HWND ) -> Option < HWND > {
4581 unsafe {
46- let hwnd = GetForegroundWindow ( ) ;
4782 if hwnd. 0 . is_null ( ) || !IsWindow ( hwnd) . as_bool ( ) {
4883 return None ;
4984 }
85+
5086 let root = GetAncestor ( hwnd, GA_ROOT ) ;
51- if root. 0 . is_null ( ) || !IsWindow ( root) . as_bool ( ) {
52- Some ( hwnd)
87+ let candidate = if root. 0 . is_null ( ) || !IsWindow ( root) . as_bool ( ) {
88+ hwnd
5389 } else {
54- Some ( root)
55- }
90+ root
91+ } ;
92+
93+ is_snap_target_window ( candidate) . then_some ( candidate)
5694 }
5795}
5896
97+ /// Get foreground window handle (root/top-level), or None if invalid.
98+ /// Uses GetAncestor(GA_ROOT) so we always move the top-level window, not a child
99+ /// (e.g. when the Tauri/WebView window is focused, the foreground may be the WebView child).
100+ pub fn get_foreground_window ( ) -> Option < HWND > {
101+ unsafe { resolve_snap_target_window ( GetForegroundWindow ( ) ) }
102+ }
103+
59104/// Get window bounds (prefer DWM extended frame bounds).
60105pub fn try_get_window_bounds ( hwnd : HWND , use_window_rect : bool ) -> Option < Rect > {
61106 unsafe {
@@ -269,4 +314,71 @@ pub fn get_process_image_name(hwnd: HWND) -> Option<String> {
269314 . and_then ( |n| n. to_str ( ) )
270315 . map ( String :: from)
271316 }
272- }
317+ }
318+
319+ #[ cfg( all( test, windows) ) ]
320+ mod tests {
321+ use super :: { is_non_processable_popup, is_snap_target_window, resolve_snap_target_window} ;
322+ use windows:: core:: w;
323+ use windows:: Win32 :: Foundation :: { HINSTANCE , HWND } ;
324+ use windows:: Win32 :: UI :: WindowsAndMessaging :: {
325+ CreateWindowExW , DefWindowProcW , DestroyWindow , RegisterClassW , HMENU , WNDCLASSW ,
326+ WS_CAPTION , WS_CLIPCHILDREN , WS_CLIPSIBLINGS , WS_EX_TOOLWINDOW , WS_MAXIMIZEBOX ,
327+ WS_MINIMIZEBOX , WS_OVERLAPPED , WS_POPUP , WS_THICKFRAME , WS_VISIBLE ,
328+ } ;
329+
330+ #[ test]
331+ fn popup_menu_styles_are_rejected ( ) {
332+ let style = ( WS_POPUP | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_VISIBLE ) . 0 ;
333+ assert ! ( is_non_processable_popup( style) ) ;
334+ }
335+
336+ #[ test]
337+ fn popup_app_styles_are_allowed ( ) {
338+ let style =
339+ ( WS_POPUP | WS_THICKFRAME | WS_CAPTION | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_VISIBLE )
340+ . 0 ;
341+ assert ! ( !is_non_processable_popup( style) ) ;
342+ }
343+
344+ #[ test]
345+ fn tool_windows_are_not_snap_targets ( ) {
346+ unsafe {
347+ let class_name = w ! ( "RectangleWinImplTests" ) ;
348+ let mut wc = WNDCLASSW :: default ( ) ;
349+ wc. lpfnWndProc = Some ( wndproc) ;
350+ wc. lpszClassName = class_name;
351+ let _ = RegisterClassW ( & wc) ;
352+
353+ let hwnd = CreateWindowExW (
354+ WS_EX_TOOLWINDOW ,
355+ class_name,
356+ w ! ( "Tool" ) ,
357+ WS_OVERLAPPED | WS_VISIBLE ,
358+ 0 ,
359+ 0 ,
360+ 100 ,
361+ 100 ,
362+ HWND :: default ( ) ,
363+ HMENU :: default ( ) ,
364+ HINSTANCE :: default ( ) ,
365+ None ,
366+ )
367+ . expect ( "create tool window" ) ;
368+
369+ assert ! ( !is_snap_target_window( hwnd) ) ;
370+ assert_eq ! ( resolve_snap_target_window( hwnd) , None ) ;
371+
372+ let _ = DestroyWindow ( hwnd) ;
373+ }
374+ }
375+
376+ unsafe extern "system" fn wndproc (
377+ hwnd : HWND ,
378+ msg : u32 ,
379+ wparam : windows:: Win32 :: Foundation :: WPARAM ,
380+ lparam : windows:: Win32 :: Foundation :: LPARAM ,
381+ ) -> windows:: Win32 :: Foundation :: LRESULT {
382+ DefWindowProcW ( hwnd, msg, wparam, lparam)
383+ }
384+ }
0 commit comments