diff options
| author | Andre Weissflog <floooh@gmail.com> | 2025-03-26 16:03:32 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-03-26 16:03:32 +0100 |
| commit | 1b7c8e72c679aaf407dc26db5695f04cdcc84a42 (patch) | |
| tree | 22cf6b50ac8e51bbc47469532687735f0b20d74f | |
| parent | bcdf25ae58c4fe82cd444ea1ce3f1b8f2532c7ed (diff) | |
sokol_app.h: more robust mouse-lock behaviour on Windows (fixes #1221) (#1230)
* sokol_app.h win32: rewrite mouse lock to be more robust around edge cases
* sokol_app.h win32: add back ShowCursor calls in lock/unlock mouse code
* sokol_app.h: update doc section on mouse lock
* update changelog (https://github.com/floooh/sokol/pull/1230)
* sokol_app.h win32: also release mouse capture in _sapp_win32_do_unlock_mouse
| -rw-r--r-- | CHANGELOG.md | 15 | ||||
| -rw-r--r-- | sokol_app.h | 227 |
2 files changed, 149 insertions, 93 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 02e504dc..5645f9a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ ## Updates +### 26-Mar-2025 + +- sokol_app.h win32: Mouse lock behaviour is now more robust in edge cases + (like stealing the window focus by opening the Windows task manager): + Calling sapp_lock_mouse() will now only set a flag with the new + intended mouse lock state instead of changing the mouse-lock state immediately. + Then once per frame the sokol_app.h win32 run-loop will check if the intended + state differs from the current state and will change the mouse lock state + accordingly. + + Also note the updated `MOUSE LOCK` documentation section in sokol_app.h. + + Related issue: https://github.com/floooh/sokol/issues/1221 + Implemented in PR: https://github.com/floooh/sokol/pull/1230 + ### 20-Mar-2025 - sokol_app.h macOS: A small fix for Ctrl-Tab key down. So far this wasn't forwarded diff --git a/sokol_app.h b/sokol_app.h index 64ac34e0..efb647d2 100644 --- a/sokol_app.h +++ b/sokol_app.h @@ -432,14 +432,15 @@ if (sapp_mouse_locked()) { ... } - On native platforms, the sapp_lock_mouse() and sapp_mouse_locked() - functions work as expected (mouse lock is activated or deactivated - immediately when sapp_lock_mouse() is called, and sapp_mouse_locked() - also immediately returns the new state after sapp_lock_mouse() - is called. + Note that mouse-lock state may not change immediately after sapp_lock_mouse(true/false) + is called, instead on some platforms the actual state switch may be delayed + to the end of the current frame or even to a later frame. - On the web platform, sapp_lock_mouse() and sapp_mouse_locked() behave - differently, as dictated by the limitations of the HTML5 Pointer Lock API: + The mouse may also be unlocked automatically without calling sapp_lock_mouse(false), + most notably when the application window becomes inactive. + + On the web platform there are further restrictions to be aware of, caused + by the limitations of the HTML5 Pointer Lock API: - sapp_lock_mouse(true) can be called at any time, but it will only take effect in a 'short-lived input event handler of a specific @@ -491,6 +492,13 @@ } } + For a 'first person shooter mouse' the following code inside the sokol-app event handler + is recommended somewhere in your frame callback: + + if (!sapp_mouse_locked()) { + sapp_lock_mouse(true); + } + CLIPBOARD SUPPORT ================= Applications can send and receive UTF-8 encoded text data from and to the @@ -2631,18 +2639,24 @@ typedef struct { HICON small_icon; HCURSOR cursors[_SAPP_MOUSECURSOR_NUM]; UINT orig_codepage; - LONG mouse_locked_x, mouse_locked_y; - bool mouse_locked_pos_valid; RECT stored_window_rect; // used to restore window pos/size when toggling fullscreen => windowed bool is_win10_or_greater; bool in_create_window; bool iconified; - bool mouse_tracked; - uint8_t mouse_capture_mask; _sapp_win32_dpi_t dpi; - bool raw_input_mousepos_valid; - LONG raw_input_mousepos_x; - LONG raw_input_mousepos_y; + struct { + struct { + LONG pos_x, pos_y; + bool pos_valid; + } lock; + struct { + LONG pos_x, pos_y; + bool pos_valid; + } raw_input; + bool requested_lock; + bool tracked; + uint8_t capture_mask; + } mouse; uint8_t raw_input_data[256]; } _sapp_win32_t; @@ -7194,16 +7208,16 @@ _SOKOL_PRIVATE void _sapp_win32_update_cursor(sapp_mouse_cursor cursor, bool sho } _SOKOL_PRIVATE void _sapp_win32_capture_mouse(uint8_t btn_mask) { - if (0 == _sapp.win32.mouse_capture_mask) { + if (0 == _sapp.win32.mouse.capture_mask) { SetCapture(_sapp.win32.hwnd); } - _sapp.win32.mouse_capture_mask |= btn_mask; + _sapp.win32.mouse.capture_mask |= btn_mask; } _SOKOL_PRIVATE void _sapp_win32_release_mouse(uint8_t btn_mask) { - if (0 != _sapp.win32.mouse_capture_mask) { - _sapp.win32.mouse_capture_mask &= ~btn_mask; - if (0 == _sapp.win32.mouse_capture_mask) { + if (0 != _sapp.win32.mouse.capture_mask) { + _sapp.win32.mouse.capture_mask &= ~btn_mask; + if (0 == _sapp.win32.mouse.capture_mask) { ReleaseCapture(); } } @@ -7214,76 +7228,107 @@ _SOKOL_PRIVATE bool _sapp_win32_is_foreground_window(void) { } _SOKOL_PRIVATE void _sapp_win32_lock_mouse(bool lock) { - if (lock == _sapp.mouse.locked) { - return; + _sapp.win32.mouse.requested_lock = lock; +} + +_SOKOL_PRIVATE void _sapp_win32_do_lock_mouse(void) { + _sapp.mouse.locked = true; + + // hide mouse cursor (NOTE: this maintains a hidden counter, but since + // only mouse-lock uses ShowCursor this doesn't matter) + ShowCursor(FALSE); + + // reset dx/dy and release any active mouse capture + _sapp.mouse.dx = 0.0f; + _sapp.mouse.dy = 0.0f; + _sapp_win32_release_mouse(0xFF); + + // store current mouse position so that it can be restored when unlocked + POINT pos; + if (GetCursorPos(&pos)) { + _sapp.win32.mouse.lock.pos_valid = true; + _sapp.win32.mouse.lock.pos_x = pos.x; + _sapp.win32.mouse.lock.pos_y = pos.y; + } else { + _sapp.win32.mouse.lock.pos_valid = false; + } + + // while mouse is locked, restrict cursor movement to the client + // rectangle so that we don't loose any mouse movement events + RECT client_rect; + GetClientRect(_sapp.win32.hwnd, &client_rect); + POINT mid_point; + mid_point.x = (client_rect.right - client_rect.left) / 2; + mid_point.y = (client_rect.bottom - client_rect.top) / 2; + ClientToScreen(_sapp.win32.hwnd, &mid_point); + RECT clip_rect; + clip_rect.left = clip_rect.right = mid_point.x; + clip_rect.top = clip_rect.bottom = mid_point.y; + ClipCursor(&clip_rect); + + // enable raw input for mouse, starts sending WM_INPUT messages to WinProc (see GLFW) + const RAWINPUTDEVICE rid = { + 0x01, // usUsagePage: HID_USAGE_PAGE_GENERIC + 0x02, // usUsage: HID_USAGE_GENERIC_MOUSE + 0, // dwFlags + _sapp.win32.hwnd // hwndTarget + }; + if (!RegisterRawInputDevices(&rid, 1, sizeof(rid))) { + _SAPP_ERROR(WIN32_REGISTER_RAW_INPUT_DEVICES_FAILED_MOUSE_LOCK); } + // in case the raw mouse device only supports absolute position reporting, + // we need to skip the dx/dy compution for the first WM_INPUT event + _sapp.win32.mouse.raw_input.pos_valid = false; +} + +_SOKOL_PRIVATE void _sapp_win32_do_unlock_mouse(void) { + _sapp.mouse.locked = false; + + // make mouse cursor visible + ShowCursor(TRUE); + + // reset dx/dy and release any active mouse capture _sapp.mouse.dx = 0.0f; _sapp.mouse.dy = 0.0f; _sapp_win32_release_mouse(0xFF); - if (lock) { - // don't allow locking the mouse unless we're the active window - if (!_sapp_win32_is_foreground_window()) { - return; - } - _sapp.mouse.locked = true; - /* store the current mouse position, so it can be restored when unlocked */ - POINT pos; - BOOL res = GetCursorPos(&pos); - if (res) { - _sapp.win32.mouse_locked_x = pos.x; - _sapp.win32.mouse_locked_y = pos.y; - _sapp.win32.mouse_locked_pos_valid = true; + // disable raw input for mouse + const RAWINPUTDEVICE rid = { 0x01, 0x02, RIDEV_REMOVE, NULL }; + if (!RegisterRawInputDevices(&rid, 1, sizeof(rid))) { + _SAPP_ERROR(WIN32_REGISTER_RAW_INPUT_DEVICES_FAILED_MOUSE_UNLOCK); + } - /* while the mouse is locked, make the mouse cursor invisible and - confine the mouse movement to a small rectangle inside our window - (so that we don't miss any mouse up events) - */ - RECT client_rect = { - _sapp.win32.mouse_locked_x, - _sapp.win32.mouse_locked_y, - _sapp.win32.mouse_locked_x, - _sapp.win32.mouse_locked_y - }; - ClipCursor(&client_rect); - } else { - _sapp.win32.mouse_locked_pos_valid = false; - } + // unrestrict mouse movement + ClipCursor(NULL); - /* make the mouse cursor invisible, this will stack with sapp_show_mouse() */ - ShowCursor(FALSE); + // restore the 'pre-locked' mouse position + if (_sapp.win32.mouse.lock.pos_valid) { + SetCursorPos(_sapp.win32.mouse.lock.pos_x, _sapp.win32.mouse.lock.pos_y); + _sapp.win32.mouse.lock.pos_valid = false; + } +} - /* enable raw input for mouse, starts sending WM_INPUT messages to WinProc (see GLFW) */ - const RAWINPUTDEVICE rid = { - 0x01, // usUsagePage: HID_USAGE_PAGE_GENERIC - 0x02, // usUsage: HID_USAGE_GENERIC_MOUSE - 0, // dwFlags - _sapp.win32.hwnd // hwndTarget - }; - if (!RegisterRawInputDevices(&rid, 1, sizeof(rid))) { - _SAPP_ERROR(WIN32_REGISTER_RAW_INPUT_DEVICES_FAILED_MOUSE_LOCK); - } - /* in case the raw mouse device only supports absolute position reporting, - we need to skip the dx/dy compution for the first WM_INPUT event - */ - _sapp.win32.raw_input_mousepos_valid = false; - } else { - _sapp.mouse.locked = false; - /* disable raw input for mouse */ - const RAWINPUTDEVICE rid = { 0x01, 0x02, RIDEV_REMOVE, NULL }; - if (!RegisterRawInputDevices(&rid, 1, sizeof(rid))) { - _SAPP_ERROR(WIN32_REGISTER_RAW_INPUT_DEVICES_FAILED_MOUSE_UNLOCK); +_SOKOL_PRIVATE void _sapp_win32_update_mouse_lock(void) { + // mouse lock can only be active when we're the active window + if (!_sapp_win32_is_foreground_window()) { + // unlock mouse if currently locked + if (_sapp.mouse.locked) { + _sapp_win32_do_unlock_mouse(); } + return; + } - /* let the mouse roam freely again */ - ClipCursor(NULL); - ShowCursor(TRUE); + // nothing to do if requested lock state matches current lock state + const bool lock = _sapp.win32.mouse.requested_lock; + if (lock == _sapp.mouse.locked) { + return; + } - /* restore the 'pre-locked' mouse position */ - if (_sapp.win32.mouse_locked_pos_valid) { - SetCursorPos(_sapp.win32.mouse_locked_x, _sapp.win32.mouse_locked_y); - _sapp.win32.mouse_locked_pos_valid = false; - } + // otherwise change into desired state + if (lock) { + _sapp_win32_do_lock_mouse(); + } else { + _sapp_win32_do_unlock_mouse(); } } @@ -7582,8 +7627,8 @@ _SOKOL_PRIVATE LRESULT CALLBACK _sapp_win32_wndproc(HWND hWnd, UINT uMsg, WPARAM case WM_MOUSEMOVE: if (!_sapp.mouse.locked) { _sapp_win32_mouse_update(lParam); - if (!_sapp.win32.mouse_tracked) { - _sapp.win32.mouse_tracked = true; + if (!_sapp.win32.mouse.tracked) { + _sapp.win32.mouse.tracked = true; TRACKMOUSEEVENT tme; _sapp_clear(&tme, sizeof(tme)); tme.cbSize = sizeof(tme); @@ -7617,13 +7662,13 @@ _SOKOL_PRIVATE LRESULT CALLBACK _sapp_win32_wndproc(HWND hWnd, UINT uMsg, WPARAM */ LONG new_x = raw_mouse_data->data.mouse.lLastX; LONG new_y = raw_mouse_data->data.mouse.lLastY; - if (_sapp.win32.raw_input_mousepos_valid) { - _sapp.mouse.dx = (float) (new_x - _sapp.win32.raw_input_mousepos_x); - _sapp.mouse.dy = (float) (new_y - _sapp.win32.raw_input_mousepos_y); + if (_sapp.win32.mouse.raw_input.pos_valid) { + _sapp.mouse.dx = (float) (new_x - _sapp.win32.mouse.raw_input.pos_x); + _sapp.mouse.dy = (float) (new_y - _sapp.win32.mouse.raw_input.pos_y); } - _sapp.win32.raw_input_mousepos_x = new_x; - _sapp.win32.raw_input_mousepos_y = new_y; - _sapp.win32.raw_input_mousepos_valid = true; + _sapp.win32.mouse.raw_input.pos_x = new_x; + _sapp.win32.mouse.raw_input.pos_y = new_y; + _sapp.win32.mouse.raw_input.pos_valid = true; } else { /* mouse reports movement delta (this seems to be the common case) */ @@ -7638,7 +7683,7 @@ _SOKOL_PRIVATE LRESULT CALLBACK _sapp_win32_wndproc(HWND hWnd, UINT uMsg, WPARAM if (!_sapp.mouse.locked) { _sapp.mouse.dx = 0.0f; _sapp.mouse.dy = 0.0f; - _sapp.win32.mouse_tracked = false; + _sapp.win32.mouse.tracked = false; _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_LEAVE, SAPP_MOUSEBUTTON_INVALID); } break; @@ -8133,12 +8178,8 @@ _SOKOL_PRIVATE void _sapp_win32_run(const sapp_desc* desc) { if (_sapp.quit_requested) { PostMessage(_sapp.win32.hwnd, WM_CLOSE, 0, 0); } - // unlock mouse if window doesn't have focus - if (_sapp.mouse.locked) { - if (!_sapp_win32_is_foreground_window()) { - _sapp_win32_lock_mouse(false); - } - } + // update mouse-lock state + _sapp_win32_update_mouse_lock(); } _sapp_call_cleanup(); |