From: Paul Gofman pgofman@codeweavers.com
--- dlls/user32/tests/msg.c | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-)
diff --git a/dlls/user32/tests/msg.c b/dlls/user32/tests/msg.c index c8a12a6f998..2b54e5087ec 100644 --- a/dlls/user32/tests/msg.c +++ b/dlls/user32/tests/msg.c @@ -111,6 +111,7 @@ typedef struct static BOOL test_DestroyWindow_flag; static BOOL test_context_menu; static BOOL ignore_mouse_messages = TRUE; +static BOOL ignore_WM_NCHITTEST = TRUE; static HWINEVENTHOOK hEvent_hook; static HHOOK hKBD_hook; static HHOOK hCBT_hook; @@ -10935,7 +10936,9 @@ static LRESULT MsgCheckProc (BOOL unicode, HWND hwnd, UINT message,
/* test_accelerators() depends on this */ case WM_NCHITTEST: - return HTCLIENT; + if (ignore_WM_NCHITTEST) + return HTCLIENT; + break;
case WM_USER+10: { @@ -14933,6 +14936,12 @@ static const struct message WmMouseLeaveSeq[] = { 0 } };
+static const struct message TrackMouseEventCallSeq[] = +{ + { WM_NCHITTEST, sent | wine_only, 0, 0 }, + { 0 } +}; + static void pump_msg_loop_timeout(DWORD timeout, BOOL inject_mouse_move) { MSG msg; @@ -15157,6 +15166,31 @@ static void test_TrackMouseEvent(void)
DestroyWindow(hwnd);
+ /* Test that TrackMouseEvent() tracking doesn't produce WM_NCHITTEST */ + hwnd2 = CreateWindowA("TestWindowClass", NULL, WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0, 0, 640, 480, + 0, NULL, NULL, 0); + ok(!!hwnd2, "Failed to create window, error %lu.\n", GetLastError()); + + GetCursorPos(&old_pt); + SetCursorPos(150, 150); + + flush_events(); + flush_sequence(); + + tme.cbSize = sizeof(tme); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = hwnd2; + tme.dwHoverTime = HOVER_DEFAULT; + SetLastError(0xdeadbeef); + ignore_WM_NCHITTEST = FALSE; + ret = pTrackMouseEvent(&tme); + ok(ret, "TrackMouseEvent(TME_LEAVE) failed, error %ld\n", GetLastError()); + flush_events(); + ignore_WM_NCHITTEST = TRUE; + ok_sequence(TrackMouseEventCallSeq, "TrackMouseEventCallSeq", FALSE); + SetCursorPos(old_pt.x, old_pt.y); + DestroyWindow(hwnd2); + /* Test that tracking a new window with TME_LEAVE and when the cursor is not in the new window, * WM_MOUSELEAVE is immediately posted to the window */ hwnd = CreateWindowA("static", NULL, WS_OVERLAPPEDWINDOW | WS_VISIBLE, 100, 100, 100,
From: Paul Gofman pgofman@codeweavers.com
--- dlls/user32/tests/msg.c | 8 +------- dlls/win32u/input.c | 39 ++++++++++++++++++++++++++++++------ dlls/win32u/message.c | 5 +++-- dlls/win32u/win32u_private.h | 3 ++- dlls/win32u/window.c | 6 +++--- 5 files changed, 42 insertions(+), 19 deletions(-)
diff --git a/dlls/user32/tests/msg.c b/dlls/user32/tests/msg.c index 2b54e5087ec..a68508ae8f4 100644 --- a/dlls/user32/tests/msg.c +++ b/dlls/user32/tests/msg.c @@ -14936,12 +14936,6 @@ static const struct message WmMouseLeaveSeq[] = { 0 } };
-static const struct message TrackMouseEventCallSeq[] = -{ - { WM_NCHITTEST, sent | wine_only, 0, 0 }, - { 0 } -}; - static void pump_msg_loop_timeout(DWORD timeout, BOOL inject_mouse_move) { MSG msg; @@ -15187,7 +15181,7 @@ static void test_TrackMouseEvent(void) ok(ret, "TrackMouseEvent(TME_LEAVE) failed, error %ld\n", GetLastError()); flush_events(); ignore_WM_NCHITTEST = TRUE; - ok_sequence(TrackMouseEventCallSeq, "TrackMouseEventCallSeq", FALSE); + ok_sequence(WmEmptySeq, "TrackMouseEventCallSeq", FALSE); SetCursorPos(old_pt.x, old_pt.y); DestroyWindow(hwnd2);
diff --git a/dlls/win32u/input.c b/dlls/win32u/input.c index c718bd1cbbc..a07cfd18d33 100644 --- a/dlls/win32u/input.c +++ b/dlls/win32u/input.c @@ -1663,6 +1663,16 @@ struct tracking_list
/* FIXME: move tracking stuff into per-thread data */ static struct tracking_list tracking_info; +static HWND last_mouse_message_hwnd; +static int last_mouse_message_hittest; +static POINT last_mouse_message_pos; + +void update_current_mouse_window( HWND hwnd, INT hittest, POINT pos ) +{ + last_mouse_message_hwnd = hwnd; + last_mouse_message_hittest = hittest; + last_mouse_message_pos = pos; +}
static void check_mouse_leave( HWND hwnd, int hittest ) { @@ -1696,6 +1706,27 @@ static void check_mouse_leave( HWND hwnd, int hittest ) } }
+static HWND get_mouse_window( HWND hwnd, INT *hittest, POINT *ret_pos ) +{ + POINT pos; + HWND ret; + + NtUserGetCursorPos( &pos ); + ret = window_from_point( hwnd, pos, hittest, FALSE ); + if (ret == last_mouse_message_hwnd) + { + *hittest = last_mouse_message_hittest; + *ret_pos = last_mouse_message_pos; + } + else + { + last_mouse_message_hwnd = NULL; + *ret_pos = pos; + } + TRACE( "point %s hwnd %p hittest %d\n", wine_dbgstr_point(&pos), ret, *hittest ); + return ret; +} + void update_mouse_tracking_info( HWND hwnd ) { int hover_width = 0, hover_height = 0, hittest; @@ -1703,10 +1734,7 @@ void update_mouse_tracking_info( HWND hwnd )
TRACE( "hwnd %p\n", hwnd );
- NtUserGetCursorPos( &pos ); - hwnd = window_from_point( hwnd, pos, &hittest ); - - TRACE( "point %s hwnd %p hittest %d\n", wine_dbgstr_point(&pos), hwnd, hittest ); + hwnd = get_mouse_window( hwnd, &hittest, &pos );
NtUserSystemParametersInfo( SPI_GETMOUSEHOVERWIDTH, 0, &hover_width, 0 ); NtUserSystemParametersInfo( SPI_GETMOUSEHOVERHEIGHT, 0, &hover_height, 0 ); @@ -1799,8 +1827,7 @@ BOOL WINAPI NtUserTrackMouseEvent( TRACKMOUSEEVENT *info ) if (hover_time == HOVER_DEFAULT || hover_time == 0) NtUserSystemParametersInfo( SPI_GETMOUSEHOVERTIME, 0, &hover_time, 0 );
- NtUserGetCursorPos( &pos ); - hwnd = window_from_point( info->hwndTrack, pos, &hittest ); + hwnd = get_mouse_window( info->hwndTrack, &hittest, &pos ); TRACE( "point %s hwnd %p hittest %d\n", wine_dbgstr_point(&pos), hwnd, hittest );
if (info->dwFlags & ~(TME_CANCEL | TME_HOVER | TME_LEAVE | TME_NONCLIENT)) diff --git a/dlls/win32u/message.c b/dlls/win32u/message.c index d3ef83785bb..14000da9918 100644 --- a/dlls/win32u/message.c +++ b/dlls/win32u/message.c @@ -2555,16 +2555,17 @@ static BOOL process_mouse_message( MSG *msg, UINT hw_id, ULONG_PTR extra_info, H { HWND orig = msg->hwnd;
- msg->hwnd = window_from_point( msg->hwnd, msg->pt, &hittest ); + msg->hwnd = window_from_point( msg->hwnd, msg->pt, &hittest, TRUE ); if (!msg->hwnd) /* As a heuristic, try the next window if it's the owner of orig */ { HWND next = get_window_relative( orig, GW_HWNDNEXT );
if (next && get_window_relative( orig, GW_OWNER ) == next && is_current_thread_window( next )) - msg->hwnd = window_from_point( next, msg->pt, &hittest ); + msg->hwnd = window_from_point( next, msg->pt, &hittest, TRUE ); } } + update_current_mouse_window( msg->hwnd, hittest, msg->pt );
if (!msg->hwnd || !is_current_thread_window( msg->hwnd )) { diff --git a/dlls/win32u/win32u_private.h b/dlls/win32u/win32u_private.h index 69d7b5b5257..a9b39de6471 100644 --- a/dlls/win32u/win32u_private.h +++ b/dlls/win32u/win32u_private.h @@ -100,6 +100,7 @@ extern BOOL set_active_window( HWND hwnd, HWND *prev, BOOL mouse, BOOL focus, DW extern BOOL set_ime_composition_rect( HWND hwnd, RECT rect ); extern void toggle_caret( HWND hwnd ); extern void update_mouse_tracking_info( HWND hwnd ); +extern void update_current_mouse_window( HWND hwnd, INT hittest, POINT pos ); extern BOOL process_wine_clipcursor( HWND hwnd, UINT flags, BOOL reset ); extern BOOL clip_fullscreen_window( HWND hwnd, BOOL reset ); extern USHORT map_scan_to_kbd_vkey( USHORT scan, HKL layout ); @@ -282,7 +283,7 @@ extern LONG_PTR set_window_long( HWND hwnd, INT offset, UINT size, LONG_PTR newv extern BOOL set_window_pos( WINDOWPOS *winpos, int parent_x, int parent_y ); extern UINT set_window_style_bits( HWND hwnd, UINT set_bits, UINT clear_bits ); extern void update_window_state( HWND hwnd ); -extern HWND window_from_point( HWND hwnd, POINT pt, INT *hittest ); +extern HWND window_from_point( HWND hwnd, POINT pt, INT *hittest, BOOL send_nchittest ); extern HWND get_shell_window(void); extern HWND get_progman_window(void); extern HWND get_taskman_window(void); diff --git a/dlls/win32u/window.c b/dlls/win32u/window.c index 0363a00541f..07d53339a5e 100644 --- a/dlls/win32u/window.c +++ b/dlls/win32u/window.c @@ -2628,7 +2628,7 @@ static HWND *list_children_from_point( HWND hwnd, POINT pt, UINT dpi ) * * Find the window and hittest for a given point. */ -HWND window_from_point( HWND hwnd, POINT pt, INT *hittest ) +HWND window_from_point( HWND hwnd, POINT pt, INT *hittest, BOOL send_nchittest ) { int i, res; HWND ret, *list; @@ -2655,7 +2655,7 @@ HWND window_from_point( HWND hwnd, POINT pt, INT *hittest ) break; } /* Send WM_NCCHITTEST (if same thread) */ - if (!is_current_thread_window( list[i] )) + if (!send_nchittest || !is_current_thread_window( list[i] )) { *hittest = HTCLIENT; break; @@ -2682,7 +2682,7 @@ HWND WINAPI NtUserWindowFromPoint( LONG x, LONG y ) { POINT pt = { .x = x, .y = y }; INT hittest; - return window_from_point( 0, pt, &hittest ); + return window_from_point( 0, pt, &hittest, TRUE ); }
/*******************************************************************
That avoids exceptions in Summoners War: RUSH window procedure. It will except on recursive calls to window procedure which happens on Wine when it calls TrackMouseEvents() on WM_MOUSEMOVE due to TrackMouseEvents() sending WM_NCHITTEST from window_from_point(). The exception currently gets caught but only because user mode callback filter swallows the exception while on Windows that would abort the process.
Overall, the way window_from_point() sends WM_NCHITTEST is not right WRT TrackMouseEvents() anyway. The skip of WM_NCHITTEST for other thread window is correct for WindowFromPoint() implementation but doesn't make sense for TrackMouseEvents purposes. On Windows calling TrackMouseEvents() never results in WM_NCHITTEST. Those will only be sent upon moving the mouse (the same already happens on Wine from process_mouse_message).
Mouse tracking is updated on timer messages which have lowest priorities, so by the time we need to update mouse tracking the latest mouse messages are supposed to be received and we should have update_current_mouse_window() called for the latest mouse updates. We are going to miss the moment the mouse leaves the window (when the hardware mouse messages won't be sent) thus the check in get_mouse_window() for the window_from_point() return for the current cursor position.
Rémi Bernon (@rbernon) commented about dlls/win32u/input.c:
/* FIXME: move tracking stuff into per-thread data */ static struct tracking_list tracking_info; +static HWND last_mouse_message_hwnd; +static int last_mouse_message_hittest; +static POINT last_mouse_message_pos;
+void update_current_mouse_window( HWND hwnd, INT hittest, POINT pos ) +{
- last_mouse_message_hwnd = hwnd;
- last_mouse_message_hittest = hittest;
- last_mouse_message_pos = pos;
+}
This looks racy, if a thread only processes its message late, after another has processed the most recent messages?
On Tue Sep 30 07:54:57 2025 +0000, Rémi Bernon wrote:
This looks racy, if a thread only processes its message late, after another has processed the most recent messages?
Well, yeah, thinking of it, it is probably better to make mouse tracking fully per thread, with tracking data also stored in thread data as comment near tracking_info suggests: "FIXME: move tracking stuff into per-thread data"? But that would probably mean that NtUserTrackMouseEvent() can initialize the tracking info only for the current thread. Do you think we can add a WM_WINE_ message to send it when NtUserTrackMouseEvent is called for the other thread? Then, all the mouse tracking info will be contained per thread and update_current_mouse_window() will be also per-thread. As a side effect, that should make NtUserTrackMouseEvent() work for other process window (the case on Windows, doesn't work on Wine now).
On Tue Sep 30 17:14:36 2025 +0000, Paul Gofman wrote:
Well, yeah, thinking of it, it is probably better to make mouse tracking fully per thread, with tracking data also stored in thread data as comment near tracking_info suggests: "FIXME: move tracking stuff into per-thread data"? But that would probably mean that NtUserTrackMouseEvent() can initialize the tracking info only for the current thread. Do you think we can add a WM_WINE_ message to send it when NtUserTrackMouseEvent is called for the other thread? Then, all the mouse tracking info will be contained per thread and update_current_mouse_window() will be also per-thread. As a side effect, that should make NtUserTrackMouseEvent() work for other process window (the case on Windows, doesn't work on Wine now).
There is a question what to do with TME_QUERY in this case. And I tested that TME_QUERY always returns zero data when called from the other thread than the tracked window belongs to (while TrackMouseEvent works for setting up tracking from other thread and other process).