[PATCH 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). -- 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 | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/dlls/user32/tests/edit.c b/dlls/user32/tests/edit.c index c71b46495aa..de37200b6f8 100644 --- a/dlls/user32/tests/edit.c +++ b/dlls/user32/tests/edit.c @@ -3518,6 +3518,46 @@ static void test_PASSWORDCHAR(void) DestroyWindow (hwEdit); } +static void test_WM_LBUTTONDOWN(void) +{ + HWND hwEdit; + + hwEdit = CreateWindowExA(0, "EDIT", "Test", ES_LEFT, + 0, 0, 100, 100, NULL, NULL, NULL, NULL); + + // test single WM_LBUTTONDOWN processing + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONDOWN, 1, 0), + "WM_LBUTTONDOWN was not processed. LastError=%ld\n", GetLastError()); + 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(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 + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONDOWN, 1, 0), + "1/2 WM_LBUTTONDOWN was not processed. LastError=%ld\n", GetLastError()); + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONDOWN, 1, 0), + "2/2 WM_LBUTTONDOWN was not processed. LastError=%ld\n", GetLastError()); + 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(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 +3598,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 | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/dlls/comctl32/tests/edit.c b/dlls/comctl32/tests/edit.c index 45c2a243b70..b607b3e694d 100644 --- a/dlls/comctl32/tests/edit.c +++ b/dlls/comctl32/tests/edit.c @@ -3937,6 +3937,46 @@ static void test_PASSWORDCHAR(void) DestroyWindow (hwEdit); } +static void test_WM_LBUTTONDOWN(void) +{ + HWND hwEdit; + + hwEdit = CreateWindowExA(0, "EDIT", "Test", ES_LEFT, + 0, 0, 100, 100, NULL, NULL, NULL, NULL); + + // test single WM_LBUTTONDOWN processing + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONDOWN, 1, 0), + "WM_LBUTTONDOWN was not processed. LastError=%ld\n", GetLastError()); + 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(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 + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONDOWN, 1, 0), + "1/2 WM_LBUTTONDOWN was not processed. LastError=%ld\n", GetLastError()); + todo_wine ok(SendMessageA(hwEdit, WM_LBUTTONDOWN, 1, 0), + "2/2 WM_LBUTTONDOWN was not processed. LastError=%ld\n", GetLastError()); + 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(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 +4028,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
Nikolay Sivov (@nsivov) commented about dlls/comctl32_v6/edit.c:
INT e; BOOL after_wrap;
+ if (es->bCaptureState && (GetCapture() == es->hwndSelf)) ReleaseCapture(); + es->bCaptureState = TRUE; SetCapture(es->hwndSelf); This looks very much like a workaround for WM_CAPTURECHANGED that you mentioned. The real question is whether WM_CAPTURECHANGED should happen at all in this case of repeated WM_LBUTTONDOWN. And more generally, should WM_CAPTURECHANGED happen on repeated SetCapture() with same window handle.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10104#note_129699
On Sat Feb 14 16:45:59 2026 +0000, Nikolay Sivov wrote:
This looks very much like a workaround for WM_CAPTURECHANGED that you mentioned. The real question is whether WM_CAPTURECHANGED should happen at all in this case of repeated WM_LBUTTONDOWN. And more generally, should WM_CAPTURECHANGED happen on repeated SetCapture() with same window handle. I wrote a simple test program with edit control, which prints in console every time WM_CAPTURECHANGED is processed and process WM_LBUTTONDOWN twice. I did this by subclassing default edit control and calling original edit procedure twice on WM_LBUTTONDOWN.
Running this test on windows I see this sequence: WM_LBUTTONDOWN -\> WM_CAPTURECHANGED -\> WM_LBUTTONUP -\> WM_CAPTURECHANGED So, I assume, WM_CAPTURECHANGED should happen there. Should i make a bug to attach this test program? -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10104#note_129736
On Mon Feb 16 14:09:01 2026 +0000, Ivan Ivlev wrote:
I wrote a simple test program with edit control, which prints in console every time WM_CAPTURECHANGED is processed and process WM_LBUTTONDOWN twice. I did this by subclassing default edit control and calling original edit procedure twice on WM_LBUTTONDOWN. Running this test on windows I see this sequence: WM_LBUTTONDOWN -\> WM_CAPTURECHANGED -\> WM_LBUTTONUP -\> WM_CAPTURECHANGED So, I assume, WM_CAPTURECHANGED should happen there. Should i make a bug to attach this test program? It would be best if you could add the test to Wine's test suite for edit control.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10104#note_131088
participants (4)
-
Ivan Ivlev -
Ivan Ivlev (@iviv) -
Nikolay Sivov (@nsivov) -
Zhiyi Zhang (@zhiyi)