[PATCH v2 0/4] MR10104: user32/edit: Reset capture on WM_LBUTTONDOWN if already enabled
Edit control does not release mouse capture after WM_LBUTTONDOWN has been processed two times in a row. This MR fixes this issue by releasing capture and setting it again on WM_LBUTTONDOWN if window was already captured. I have stumbled upon the app in which edit control process WM_LBUTTONDOWN twice on one mouse click. While in windows it causes no problem, wine edit does not release capture, so other buttons inside this app become unclickable. It happens because when WM_LBUTTONDOWN is processed twice, WM_CAPTURECHANGED is sent and bCaptureState is set to FALSE, then EDIT_WM_LButtonUp does not release capture because bCaptureState is already FALSE. Thing about tests: I have added test case for this problem to be visible. I also had to use todo_wine because SendMessageA for WM_LBUTTONDOWN and WM_LBUTTONUP returns 0 in wine and 1 in windows (which is not the case for combobox test, where SendMessageА for WM_LBUTTONDOWN also returns 1 in wine). -- v2: comctl32_v6/edit: Reset capture on WM_LBUTTONDOWN if already enabled user32/edit: Reset capture on WM_LBUTTONDOWN if already enabled comctl32/tests/edit.c: Test WM_LBUTTONDOWN processing user32/tests/edit.c: Test WM_LBUTTONDOWN processing https://gitlab.winehq.org/wine/wine/-/merge_requests/10104
From: Ivan Ivlev <iviv@etersoft.ru> Signed-off-by: Ivan Ivlev <iviv@etersoft.ru> --- dlls/user32/tests/edit.c | 69 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/dlls/user32/tests/edit.c b/dlls/user32/tests/edit.c index c71b46495aa..14bd34efd53 100644 --- a/dlls/user32/tests/edit.c +++ b/dlls/user32/tests/edit.c @@ -3518,6 +3518,74 @@ static void test_PASSWORDCHAR(void) DestroyWindow (hwEdit); } +BOOL capture_changed = FALSE; + +LRESULT CALLBACK CaptureEditProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) +{ + WNDPROC origEditProc = (WNDPROC)GetWindowLongPtrA(hwnd, GWLP_USERDATA); + if (msg == WM_CAPTURECHANGED) + capture_changed = TRUE; + return CallWindowProcA(origEditProc, hwnd, msg, wp, lp); +} + +int CheckCapture(void) +{ + if(capture_changed){ + capture_changed = FALSE; + return 1; + } + return 0; +} + +static void test_WM_LBUTTONDOWN(void) +{ + HWND hwEdit; + WNDPROC origEditProc; + + hwEdit = CreateWindowExA(0, "EDIT", "Test", ES_LEFT, + 0, 0, 100, 100, NULL, NULL, NULL, NULL); + origEditProc = (WNDPROC)SetWindowLongPtrA(hwEdit, GWLP_WNDPROC, (LONG_PTR)CaptureEditProc); + SetWindowLongPtrA(hwEdit, GWLP_USERDATA, (LONG_PTR)origEditProc); + // test single WM_LBUTTONDOWN processing + ok(!CheckCapture(), "Unexpected WM_CAPTURECHANGED recieved\n"); + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONDOWN, 1, 0), + "WM_LBUTTONDOWN was not processed. LastError=%ld\n", GetLastError()); + ok(!CheckCapture(), "Unexpected WM_CAPTURECHANGED recieved after WM_LBUTTONDOWN\n"); + ok(GetFocus() == hwEdit, + "Focus is not on Edit Control, instead on %p\n", GetFocus()); + ok(GetCapture() == hwEdit, + "Capture not on Edit Control, instead on %p\n", GetCapture()); + + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONUP, 0, 0), + "WM_LBUTTONUP was not processed. LastError=%ld\n", GetLastError()); + ok(CheckCapture(), "Expected WM_CAPTURECHANGED was not recieved after WM_LBUTTONUP\n"); + ok(GetFocus() == hwEdit, + "Focus is not on Edit Control, instead on %p\n", GetFocus()); + ok(GetCapture() != hwEdit, + "Capture is on Edit Control %p, expected to be released\n", GetCapture()); + + // test double WM_LBUTTONDOWN processing + ok(!CheckCapture(), "Unexpected WM_CAPTURECHANGED recieved\n"); + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONDOWN, 1, 0), + "1/2 WM_LBUTTONDOWN was not processed. LastError=%ld\n", GetLastError()); + ok(!CheckCapture(), "Unexpected WM_CAPTURECHANGED recieved after 1/2 WM_LBUTTONDOWN\n"); + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONDOWN, 1, 0), + "2/2 WM_LBUTTONDOWN was not processed. LastError=%ld\n", GetLastError()); + ok(CheckCapture(), "Expected WM_CAPTURECHANGED was not recieved after 2/2 WM_LBUTTONDOWN\n"); + ok(GetCapture() == hwEdit, + "Capture is not on Edit Control, instead on %p\n", GetCapture()); + + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONUP, 0, 0), + "WM_LBUTTONUP was not processed. LastError=%ld\n", GetLastError()); + ok(CheckCapture(), "Expected WM_CAPTURECHANGED was not recieved after WM_LBUTTONUP\n"); + ok(GetFocus() == hwEdit, + "Focus is not on Edit Control, instead on %p\n", GetFocus()); + ok(GetCapture() != hwEdit, + "Capture is on Edit Control %p, expected to be released\n", GetCapture()); + + DestroyWindow(hwEdit); +} + START_TEST(edit) { BOOL b; @@ -3558,6 +3626,7 @@ START_TEST(edit) test_dbcs_WM_CHAR(); test_format_rect(); test_PASSWORDCHAR(); + test_WM_LBUTTONDOWN(); UnregisterWindowClasses(); } -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10104
From: Ivan Ivlev <iviv@etersoft.ru> Signed-off-by: Ivan Ivlev <iviv@etersoft.ru> --- dlls/comctl32/tests/edit.c | 69 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/dlls/comctl32/tests/edit.c b/dlls/comctl32/tests/edit.c index 45c2a243b70..f9dfd3b98e4 100644 --- a/dlls/comctl32/tests/edit.c +++ b/dlls/comctl32/tests/edit.c @@ -3937,6 +3937,74 @@ static void test_PASSWORDCHAR(void) DestroyWindow (hwEdit); } +BOOL capture_changed = FALSE; + +LRESULT CALLBACK CaptureEditProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) +{ + WNDPROC origEditProc = (WNDPROC)GetWindowLongPtrA(hwnd, GWLP_USERDATA); + if (msg == WM_CAPTURECHANGED) + capture_changed = TRUE; + return CallWindowProcA(origEditProc, hwnd, msg, wp, lp); +} + +int CheckCapture(void) +{ + if(capture_changed){ + capture_changed = FALSE; + return 1; + } + return 0; +} + +static void test_WM_LBUTTONDOWN(void) +{ + HWND hwEdit; + WNDPROC origEditProc; + + hwEdit = CreateWindowExA(0, "EDIT", "Test", ES_LEFT, + 0, 0, 100, 100, NULL, NULL, NULL, NULL); + origEditProc = (WNDPROC)SetWindowLongPtrA(hwEdit, GWLP_WNDPROC, (LONG_PTR)CaptureEditProc); + SetWindowLongPtrA(hwEdit, GWLP_USERDATA, (LONG_PTR)origEditProc); + // test single WM_LBUTTONDOWN processing + ok(!CheckCapture(), "Unexpected WM_CAPTURECHANGED recieved\n"); + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONDOWN, 1, 0), + "WM_LBUTTONDOWN was not processed. LastError=%ld\n", GetLastError()); + ok(!CheckCapture(), "Unexpected WM_CAPTURECHANGED recieved after WM_LBUTTONDOWN\n"); + ok(GetFocus() == hwEdit, + "Focus is not on Edit Control, instead on %p\n", GetFocus()); + ok(GetCapture() == hwEdit, + "Capture not on Edit Control, instead on %p\n", GetCapture()); + + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONUP, 0, 0), + "WM_LBUTTONUP was not processed. LastError=%ld\n", GetLastError()); + ok(CheckCapture(), "Expected WM_CAPTURECHANGED was not recieved after WM_LBUTTONUP\n"); + ok(GetFocus() == hwEdit, + "Focus is not on Edit Control, instead on %p\n", GetFocus()); + ok(GetCapture() != hwEdit, + "Capture is on Edit Control %p, expected to be released\n", GetCapture()); + + // test double WM_LBUTTONDOWN processing + ok(!CheckCapture(), "Unexpected WM_CAPTURECHANGED recieved\n"); + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONDOWN, 1, 0), + "1/2 WM_LBUTTONDOWN was not processed. LastError=%ld\n", GetLastError()); + ok(!CheckCapture(), "Unexpected WM_CAPTURECHANGED recieved after 1/2 WM_LBUTTONDOWN\n"); + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONDOWN, 1, 0), + "2/2 WM_LBUTTONDOWN was not processed. LastError=%ld\n", GetLastError()); + ok(CheckCapture(), "Expected WM_CAPTURECHANGED was not recieved after 2/2 WM_LBUTTONDOWN\n"); + ok(GetCapture() == hwEdit, + "Capture is not on Edit Control, instead on %p\n", GetCapture()); + + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONUP, 0, 0), + "WM_LBUTTONUP was not processed. LastError=%ld\n", GetLastError()); + ok(CheckCapture(), "Expected WM_CAPTURECHANGED was not recieved after WM_LBUTTONUP\n"); + ok(GetFocus() == hwEdit, + "Focus is not on Edit Control, instead on %p\n", GetFocus()); + ok(GetCapture() != hwEdit, + "Capture is on Edit Control %p, expected to be released\n", GetCapture()); + + DestroyWindow(hwEdit); +} + START_TEST(edit) { ULONG_PTR ctx_cookie; @@ -3988,6 +4056,7 @@ START_TEST(edit) test_ime(); test_format_rect(); test_PASSWORDCHAR(); + test_WM_LBUTTONDOWN(); UnregisterWindowClasses(); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10104
From: Ivan Ivlev <iviv@etersoft.ru> Signed-off-by: Ivan Ivlev <iviv@etersoft.ru> --- dlls/user32/edit.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dlls/user32/edit.c b/dlls/user32/edit.c index cad14800995..aa93d12391f 100644 --- a/dlls/user32/edit.c +++ b/dlls/user32/edit.c @@ -3632,6 +3632,8 @@ static LRESULT EDIT_WM_LButtonDown(EDITSTATE *es, DWORD keys, INT x, INT y) INT e; BOOL after_wrap; + if (es->bCaptureState && (GetCapture() == es->hwndSelf)) NtUserReleaseCapture(); + es->bCaptureState = TRUE; NtUserSetCapture(es->hwndSelf); EDIT_ConfinePoint(es, &x, &y); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10104
From: Ivan Ivlev <iviv@etersoft.ru> Signed-off-by: Ivan Ivlev <iviv@etersoft.ru> --- dlls/comctl32_v6/edit.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dlls/comctl32_v6/edit.c b/dlls/comctl32_v6/edit.c index a79e2c0c169..9440deb8c6c 100644 --- a/dlls/comctl32_v6/edit.c +++ b/dlls/comctl32_v6/edit.c @@ -3480,6 +3480,8 @@ static LRESULT EDIT_WM_LButtonDown(EDITSTATE *es, DWORD keys, INT x, INT y) INT e; BOOL after_wrap; + if (es->bCaptureState && (GetCapture() == es->hwndSelf)) ReleaseCapture(); + es->bCaptureState = TRUE; SetCapture(es->hwndSelf); EDIT_ConfinePoint(es, &x, &y); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10104
Again, even if the tests work, you release capture only to capture same window again. The fix as it is now does not make sense to me. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10104#note_134224
On Sat Mar 28 18:12:47 2026 +0000, Nikolay Sivov wrote:
Again, even if the tests work, you release capture only to capture same window again. The fix as it is now does not make sense to me. You asked:
_The real question is whether WM_CAPTURECHANGED should happen at all in this case of repeated WM_LBUTTONDOWN?_ Yes, test case shows that on repeated WM_LBUTTONDOWN happens WM_CAPTURECHANGED. Since it is happening, we have to do ReleaseCapture to be able to then process WM_LBUTTONUP properly. According to the tests, windows has same behavior, releases capture (and sending WM_CAPTURECHANGED) only to capture same window again. Are these tests not enough? Even if it acts identically as windows? -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10104#note_134233
participants (3)
-
Ivan Ivlev -
Ivan Ivlev (@iviv) -
Nikolay Sivov (@nsivov)