99using System . Linq ;
1010using System . Runtime . CompilerServices ;
1111#if ! HAS_UNO
12+ using System . Diagnostics ;
1213using System . Runtime . InteropServices ;
1314#endif
1415using Windows . Foundation ;
@@ -29,13 +30,35 @@ public static void Tap(this InputInjectorHelper injector, UIElement elt)
2930 if ( injector . CurrentPointerType == PointerDeviceType . Mouse )
3031 {
3132 // On WinUI 3, InputInjector uses relative mouse deltas from the physical OS cursor.
32- // The tracked position does not correspond to the physical cursor location, so
33- // MoveTo (which computes deltas from tracked position) misses the target.
34- // Use Win32 SetCursorPos to position the cursor at the exact element center,
35- // then inject Press/Release at that location.
33+ // Use Win32 SetCursorPos for absolute screen positioning, then inject a zero-delta
34+ // Move so the WinUI input system registers the cursor at the target before button events.
3635 var scale = elt . XamlRoot ? . RasterizationScale ?? 1.0 ;
3736 injector . InjectMouseInput ( injector . Mouse . ReleaseAny ( ) ) ;
38- PositionCursorAtClientCoordinates ( center . X , center . Y , scale ) ;
37+ var screenPt = GetOnScreenPoint ( center . X , center . Y , scale ) ;
38+ SetCursorPos ( screenPt . x , screenPt . y ) ;
39+
40+ // Verify the cursor reached the target. SetCursorPos silently clamps
41+ // to virtual screen bounds; if the cursor drifted, the click would miss.
42+ GetCursorPos ( out var actual ) ;
43+ if ( Math . Abs ( actual . x - screenPt . x ) > 2 || Math . Abs ( actual . y - screenPt . y ) > 2 )
44+ {
45+ var processHwnd = Process . GetCurrentProcess ( ) . MainWindowHandle ;
46+ var diagHwnd = GetActiveWindow ( ) ;
47+ GetWindowRect ( processHwnd , out var procRect ) ;
48+ throw new InvalidOperationException (
49+ $ "Cannot tap element: cursor could not reach screen ({ screenPt . x } ,{ screenPt . y } ). " +
50+ $ "Actual=({ actual . x } ,{ actual . y } ). " +
51+ $ "Center DIPs=({ center . X : F1} ,{ center . Y : F1} ), Scale={ scale } . " +
52+ $ "ProcessHWND=0x{ processHwnd . ToInt64 ( ) : X} , ActiveHWND=0x{ diagHwnd . ToInt64 ( ) : X} . " +
53+ $ "ProcessRect=({ procRect . left } ,{ procRect . top } ,{ procRect . right } ,{ procRect . bottom } ).") ;
54+ }
55+
56+ injector . InjectMouseInput ( new InjectedInputMouseInfo
57+ {
58+ MouseOptions = InjectedInputMouseOptions . Move ,
59+ DeltaX = 0 ,
60+ DeltaY = 0
61+ } ) ;
3962 injector . Mouse . SetTrackedPosition ( center . X , center . Y ) ;
4063 injector . InjectMouseInput ( injector . Mouse . Press ( ) ) ;
4164 injector . InjectMouseInput ( injector . Mouse . Release ( ) ) ;
@@ -185,35 +208,145 @@ private struct POINT
185208 public int y ;
186209 }
187210
211+ [ StructLayout ( LayoutKind . Sequential ) ]
212+ private struct RECT
213+ {
214+ public int left , top , right , bottom ;
215+ }
216+
188217 [ DllImport ( "user32.dll" ) ]
189218 private static extern bool SetCursorPos ( int X , int Y ) ;
190219
191220 [ DllImport ( "user32.dll" ) ]
192221 private static extern bool ClientToScreen ( IntPtr hWnd , ref POINT lpPoint ) ;
193222
223+ [ DllImport ( "user32.dll" ) ]
224+ private static extern bool GetCursorPos ( out POINT lpPoint ) ;
225+
194226 [ DllImport ( "user32.dll" ) ]
195227 private static extern IntPtr GetActiveWindow ( ) ;
196228
229+ [ DllImport ( "user32.dll" ) ]
230+ private static extern IntPtr GetForegroundWindow ( ) ;
231+
232+ [ DllImport ( "user32.dll" ) ]
233+ private static extern bool GetWindowRect ( IntPtr hWnd , out RECT lpRect ) ;
234+
235+ [ DllImport ( "user32.dll" ) ]
236+ private static extern bool SetWindowPos ( IntPtr hWnd , IntPtr hWndInsertAfter ,
237+ int X , int Y , int cx , int cy , uint uFlags ) ;
238+
239+ [ DllImport ( "user32.dll" ) ]
240+ private static extern int GetSystemMetrics ( int nIndex ) ;
241+
242+ [ DllImport ( "user32.dll" ) ]
243+ private static extern IntPtr GetAncestor ( IntPtr hwnd , uint gaFlags ) ;
244+
245+ private const uint GA_ROOT = 2 ;
246+
247+ private const uint SWP_NOSIZE = 0x0001 ;
248+ private const uint SWP_NOZORDER = 0x0004 ;
249+ private const uint SWP_NOACTIVATE = 0x0010 ;
250+ private const int SM_XVIRTUALSCREEN = 76 ;
251+ private const int SM_YVIRTUALSCREEN = 77 ;
252+ private const int SM_CXVIRTUALSCREEN = 78 ;
253+ private const int SM_CYVIRTUALSCREEN = 79 ;
254+
197255 /// <summary>
198- /// Positions the OS cursor at the given window-client-relative DIP coordinates.
199- /// Converts from DIPs to physical pixels using the rasterization scale,
200- /// then from client coordinates to screen coordinates using ClientToScreen.
256+ /// Converts window-client-relative DIP coordinates to screen pixel coordinates.
257+ /// If the computed screen position falls outside the virtual screen bounds
258+ /// (e.g., the window extends off-screen), the window is repositioned first
259+ /// to ensure the point is reachable by cursor and touch input.
201260 /// </summary>
202- private static void PositionCursorAtClientCoordinates ( double dipX , double dipY , double rasterizationScale )
261+ private static POINT GetOnScreenPoint ( double clientDipX , double clientDipY , double scale )
203262 {
204263 var hwnd = GetActiveWindow ( ) ;
205264 if ( hwnd == IntPtr . Zero )
206265 {
207- return ;
266+ hwnd = GetForegroundWindow ( ) ;
267+ }
268+ if ( hwnd == IntPtr . Zero )
269+ {
270+ hwnd = Process . GetCurrentProcess ( ) . MainWindowHandle ;
208271 }
209272
210- var point = new POINT
273+ var clientPixel = new POINT
211274 {
212- x = ( int ) ( dipX * rasterizationScale ) ,
213- y = ( int ) ( dipY * rasterizationScale )
275+ x = ( int ) ( clientDipX * scale ) ,
276+ y = ( int ) ( clientDipY * scale )
214277 } ;
215- ClientToScreen ( hwnd , ref point ) ;
216- SetCursorPos ( point . x , point . y ) ;
278+
279+ if ( hwnd == IntPtr . Zero )
280+ {
281+ return clientPixel ;
282+ }
283+
284+ var screenPoint = clientPixel ;
285+ ClientToScreen ( hwnd , ref screenPoint ) ;
286+
287+ // If the screen position is outside the virtual screen, SetCursorPos
288+ // will clamp the cursor and touch injection will miss the target.
289+ // This happens when the test runner window extends beyond the visible
290+ // screen area (common in automated/CI environments or multi-monitor setups).
291+ if ( ! IsPointOnVirtualScreen ( screenPoint . x , screenPoint . y ) )
292+ {
293+ EnsureClientAreaOnScreen ( hwnd ) ;
294+
295+ // Recompute screen coordinates after the window move.
296+ screenPoint = clientPixel ;
297+ ClientToScreen ( hwnd , ref screenPoint ) ;
298+ }
299+
300+ return screenPoint ;
301+ }
302+
303+ private static bool IsPointOnVirtualScreen ( int x , int y )
304+ {
305+ int left = GetSystemMetrics ( SM_XVIRTUALSCREEN ) ;
306+ int top = GetSystemMetrics ( SM_YVIRTUALSCREEN ) ;
307+ int width = GetSystemMetrics ( SM_CXVIRTUALSCREEN ) ;
308+ int height = GetSystemMetrics ( SM_CYVIRTUALSCREEN ) ;
309+ return x >= left && x < left + width && y >= top && y < top + height ;
310+ }
311+
312+ /// <summary>
313+ /// Moves the top-level ancestor window so the given child HWND's client area
314+ /// origin maps to screen (100, 100), ensuring elements near the top-left
315+ /// of the client area are reachable by the OS cursor.
316+ /// SetWindowPos on child windows only repositions them within their parent,
317+ /// so we must find and move the root ancestor.
318+ /// </summary>
319+ private static void EnsureClientAreaOnScreen ( IntPtr hwnd )
320+ {
321+ // Use Process.MainWindowHandle to reliably get the top-level window.
322+ // GetActiveWindow returns a XAML-island child HWND, and GetAncestor(GA_ROOT)
323+ // may not traverse the WinUI 3 HWND hierarchy correctly.
324+ var rootHwnd = Process . GetCurrentProcess ( ) . MainWindowHandle ;
325+ if ( rootHwnd == IntPtr . Zero )
326+ {
327+ rootHwnd = GetAncestor ( hwnd , GA_ROOT ) ;
328+ }
329+ if ( rootHwnd == IntPtr . Zero )
330+ {
331+ return ; // Cannot determine top-level window; skip repositioning.
332+ }
333+
334+ var clientOrigin = new POINT ( ) ;
335+ ClientToScreen ( hwnd , ref clientOrigin ) ;
336+
337+ GetWindowRect ( rootHwnd , out var windowRect ) ;
338+
339+ // Shift the root window so the child's client area origin
340+ // lands at screen (100, 100).
341+ int newLeft = windowRect . left + ( 100 - clientOrigin . x ) ;
342+ int newTop = windowRect . top + ( 100 - clientOrigin . y ) ;
343+
344+ SetWindowPos ( rootHwnd , IntPtr . Zero , newLeft , newTop , 0 , 0 ,
345+ SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE ) ;
346+
347+ // Allow the OS to fully process the window position change
348+ // before subsequent ClientToScreen calls read updated coordinates.
349+ System . Threading . Thread . Sleep ( 500 ) ;
217350 }
218351#endif
219352}
0 commit comments