WIP: feat(display): hide the cursor in play once the pads or keys are used (Windows) #245

Draft
jarynclouatre wants to merge 1 commit from jarynclouatre/Longshot:feat/cursor-hidden-in-play into main
Contributor

Why

Playing on a controller in a window or borderless, the mouse cursor stays on top of the picture until you push it off the window by hand. In the game's menus it already hides once the pad or the keys are used and comes back when the mouse moves; in play it did not. Reported on Hookshot as issues 2784 and 1964.

What changed

  • display (host/display.c): the menus' cursor rule now holds whenever the Insert menu is closed, not only while one of the game's menus is open.
    • The cursor is shown when the window opens (gm_shown starts at 1) and hides at the first pad or key input.
    • WM_MOUSEMOVE calls the new gm_mouse_moved. It reads the move in screen coordinates (GetMessagePos). While the cursor is shown, any other point counts as the mouse in use. While it is hidden, only a point more than 4 pixels (at 96 dpi, scaled by the window's dpi) from where it was hidden: a bumped desk does not bring it back, and neither does the WM_MOUSEMOVE Windows sends when the window moves or resizes under a cursor at rest. A move under the Insert menu counts too, so closing that menu with a click leaves the cursor shown.
    • WM_TIMER: pad or key input after the mouse hides it at once (gm_cursor_off), and only while the game's window is the foreground window.
    • WM_SETCURSOR: it stays hidden over the client area while gm_cursor_shown() is 0.
  • One change in the game's menus: a hidden cursor there also needs the move past 4 pixels, where main showed it at one pixel.
  • Not changed: the Insert menu keeps its cursor (menu_open), a captured mouse is hidden as before, and the pointer's part in the game's menus (hos_mouse_game_pointer) is as it was.
  • Windows only. host/posix/display_sdl.c is not touched, so on Linux and the Steam Deck the cursor is as before; the title says so.
  • tests:
    • tests/test_cursor_hidden_in_play.py is new. It compiles the cursor's own functions out of host/display.c (gm_cursor_shown, gm_move_counts, gm_mouse_moved) with stand-ins for the Win32 calls and runs a sequence: shown at the start, hidden by the pad, still hidden after 3 pixels, shown after 5, 8 pixels at 192 dpi, negative screen coordinates. It also reads the three wndproc conditions.
    • tests/screensaver_test.c (the fixture that compiles the whole wndproc): three timer cases. A cursor the pads hid is turned off with the game in front, not with another window in front, and not under the Insert menu.
    • tests/display_pointer_test.cpp, tests/test_display_pointer.py: a stand-in for gm_mouse_moved, since the move case no longer reads lp.
  • docs: none needed. The README's line under "Mouse in the game's menus (Windows)" already says "The cursor stays hidden while you play on a controller or the keyboard, until the mouse moves".

Flags and settings

None.

Measurements

main: 514591a3 (2026-11-13)
this PR: feat/cursor-hidden-in-play @ 9749eea5
Hardware: none used (game-free checks on a Linux x86-64 PC, Python 3.14.4)
Scene: none, the game was not run yet
Method: python3 -I -B tests/run_all.py, two runs each, interleaved (PR, main, PR, main), both runs of a side gave the same counts; the new check also run against main's host/display.c
How to reproduce: the commands above in a checkout of each commit; for the last row, copy this PR's tests/test_cursor_hidden_in_play.py into main

main @ 514591a3 this PR @ 9749eea5
tests/run_all.py (run 1, run 2) 452 passed, 2 failed, 79 skipped (both) 453 passed, 2 failed, 79 skipped (both)
failing checks test_bake_portability.py, test_menu_i18n.py the same two
tests/test_cursor_hidden_in_play.py 5 of its 5 tests fail (copied in, not part of main) 5 of 5 pass

test_bake_portability.py fails here for a missing Python module (zstandard), on main as well. test_menu_i18n.py fails on main itself.

Testing

Tested: the game-free suite on a Linux PC, as in Measurements, with test_screensaver.py and test_display_pointer.py (the two fixtures that compile host/display.c's window code) passing on both sides. A Windows build of this branch at 9749eea5 with llvm-mingw (exe and renderer DLL) compiles and links.

Not tested: the game itself, and a Linux build (the Linux build does not compile host/display.c). The Windows run from the review is still to do, on Windows 11 (RTX 4070 Ti SUPER): in a window and borderless, the first pad or key input hides the cursor and a move shows it; Alt-Tab away and back with the cursor over the game; the Steam overlay opened with a pad; the Insert menu closed by click and by pad; fullscreen toggled with the cursor at rest; Steam Input's Desktop layout. This stays work in progress until then, and the result goes in a comment here.

Needs other testers: None.

Screenshots

Nothing on screen.

Risk and rollback

After the first pad or key input, no cursor is drawn over the window in play until the mouse moves more than a few pixels; someone who looks for it has to move it first. With Steam Input's Desktop layout a stick moves the mouse and stamps the pad at once, so the cursor may show and hide in turn there; that is in the Windows run above. A left click in play still takes the mouse for the camera as on main. Reverting the commit puts the old rule back.

Reviewer notes

  • git diff main...HEAD -- host/display.c shows all of the change: the state's start value, gm_move_counts, gm_mouse_moved, the hide point in menus_mouse kept as a screen point, and the three conditions in wndproc.
  • GetForegroundWindow() == w in the timer keeps a game in the background from hiding a cursor that is over it while another program is in use.
  • SDL is left for a follow-up if wanted (SDL_HideCursor / SDL_ShowCursor, beside hos_sdl_relative_mouse).

Issues

None.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths.
## Why Playing on a controller in a window or borderless, the mouse cursor stays on top of the picture until you push it off the window by hand. In the game's menus it already hides once the pad or the keys are used and comes back when the mouse moves; in play it did not. Reported on Hookshot as issues 2784 and 1964. ## What changed - display (`host/display.c`): the menus' cursor rule now holds whenever the Insert menu is closed, not only while one of the game's menus is open. - The cursor is shown when the window opens (`gm_shown` starts at 1) and hides at the first pad or key input. - `WM_MOUSEMOVE` calls the new `gm_mouse_moved`. It reads the move in screen coordinates (`GetMessagePos`). While the cursor is shown, any other point counts as the mouse in use. While it is hidden, only a point more than 4 pixels (at 96 dpi, scaled by the window's dpi) from where it was hidden: a bumped desk does not bring it back, and neither does the `WM_MOUSEMOVE` Windows sends when the window moves or resizes under a cursor at rest. A move under the Insert menu counts too, so closing that menu with a click leaves the cursor shown. - `WM_TIMER`: pad or key input after the mouse hides it at once (`gm_cursor_off`), and only while the game's window is the foreground window. - `WM_SETCURSOR`: it stays hidden over the client area while `gm_cursor_shown()` is 0. - One change in the game's menus: a hidden cursor there also needs the move past 4 pixels, where main showed it at one pixel. - Not changed: the Insert menu keeps its cursor (`menu_open`), a captured mouse is hidden as before, and the pointer's part in the game's menus (`hos_mouse_game_pointer`) is as it was. - Windows only. `host/posix/display_sdl.c` is not touched, so on Linux and the Steam Deck the cursor is as before; the title says so. - tests: - `tests/test_cursor_hidden_in_play.py` is new. It compiles the cursor's own functions out of `host/display.c` (`gm_cursor_shown`, `gm_move_counts`, `gm_mouse_moved`) with stand-ins for the Win32 calls and runs a sequence: shown at the start, hidden by the pad, still hidden after 3 pixels, shown after 5, 8 pixels at 192 dpi, negative screen coordinates. It also reads the three `wndproc` conditions. - `tests/screensaver_test.c` (the fixture that compiles the whole `wndproc`): three timer cases. A cursor the pads hid is turned off with the game in front, not with another window in front, and not under the Insert menu. - `tests/display_pointer_test.cpp`, `tests/test_display_pointer.py`: a stand-in for `gm_mouse_moved`, since the move case no longer reads `lp`. - docs: none needed. The README's line under "Mouse in the game's menus (Windows)" already says "The cursor stays hidden while you play on a controller or the keyboard, until the mouse moves". ## Flags and settings None. ## Measurements main: 514591a3 (2026-11-13) this PR: feat/cursor-hidden-in-play @ 9749eea5 Hardware: none used (game-free checks on a Linux x86-64 PC, Python 3.14.4) Scene: none, the game was not run yet Method: `python3 -I -B tests/run_all.py`, two runs each, interleaved (PR, main, PR, main), both runs of a side gave the same counts; the new check also run against main's `host/display.c` How to reproduce: the commands above in a checkout of each commit; for the last row, copy this PR's `tests/test_cursor_hidden_in_play.py` into main | | main @ 514591a3 | this PR @ 9749eea5 | |---|---|---| | `tests/run_all.py` (run 1, run 2) | 452 passed, 2 failed, 79 skipped (both) | 453 passed, 2 failed, 79 skipped (both) | | failing checks | `test_bake_portability.py`, `test_menu_i18n.py` | the same two | | `tests/test_cursor_hidden_in_play.py` | 5 of its 5 tests fail (copied in, not part of main) | 5 of 5 pass | `test_bake_portability.py` fails here for a missing Python module (zstandard), on main as well. `test_menu_i18n.py` fails on main itself. ## Testing **Tested:** the game-free suite on a Linux PC, as in Measurements, with `test_screensaver.py` and `test_display_pointer.py` (the two fixtures that compile `host/display.c`'s window code) passing on both sides. A Windows build of this branch at 9749eea5 with llvm-mingw (exe and renderer DLL) compiles and links. **Not tested:** the game itself, and a Linux build (the Linux build does not compile `host/display.c`). The Windows run from the review is still to do, on Windows 11 (RTX 4070 Ti SUPER): in a window and borderless, the first pad or key input hides the cursor and a move shows it; Alt-Tab away and back with the cursor over the game; the Steam overlay opened with a pad; the Insert menu closed by click and by pad; fullscreen toggled with the cursor at rest; Steam Input's Desktop layout. This stays work in progress until then, and the result goes in a comment here. **Needs other testers:** None. ## Screenshots Nothing on screen. ## Risk and rollback After the first pad or key input, no cursor is drawn over the window in play until the mouse moves more than a few pixels; someone who looks for it has to move it first. With Steam Input's Desktop layout a stick moves the mouse and stamps the pad at once, so the cursor may show and hide in turn there; that is in the Windows run above. A left click in play still takes the mouse for the camera as on main. Reverting the commit puts the old rule back. ## Reviewer notes - `git diff main...HEAD -- host/display.c` shows all of the change: the state's start value, `gm_move_counts`, `gm_mouse_moved`, the hide point in `menus_mouse` kept as a screen point, and the three conditions in `wndproc`. - `GetForegroundWindow() == w` in the timer keeps a game in the background from hiding a cursor that is over it while another program is in use. - SDL is left for a follow-up if wanted (`SDL_HideCursor` / `SDL_ShowCursor`, beside `hos_sdl_relative_mouse`). ## Issues None. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths.
feat(display): hide the cursor in play once the pads or keys are used
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 31s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
6da7308764
The cursor over the game's window already hid in the game's menus once the
pads or the keys were used after the mouse, and showed again when the mouse
moved. Outside the menus it stayed on screen, so a player on a controller
had to move it off the window by hand (reported on Hookshot as issues 2784
and 1964).

The same rule now holds in play: hidden until the mouse moves, hidden again
at the next pad or key input. The Insert menu keeps its cursor, and a
captured mouse is hidden as before.
HumanBean added this to the v3.0.0 milestone 2026-11-13 17:51:58 +00:00
HumanBean left a comment

The change itself is sound: three conditions in wndproc, no new ShowCursor call, so Windows' cursor display count is untouched. Only SetCursor(NULL) hides the cursor, it applies over the client area alone (HTCLIENT, and gm_cursor_off's PtInRect on the client rect), and Windows puts the cursor back when the mouse moves over another window, so nothing can stay hidden after exit or a crash. The window border and resize area keep their cursors. A captured mouse (Mouse camera on) works as before: WM_MOUSEMOVE still returns early while captured and WM_SETCURSOR already hid the cursor then. Releasing the capture leaves the cursor invisible until the next move, which is also what main does, because SetCursor(NULL) was the last shape set during the capture. With Mouse camera Off (#178) clicks in play did nothing before and still do nothing. Dialogue choices and the game's menus go through mouse_tick_locked → hos_mouse_game_menu, so game_menu_open is set for them and they behave as on main (#85 is unaffected). The Insert menu keeps its pointer: menu_open is checked in all three places, and its pointer mailbox doesn't read gm_shown. The new GetForegroundWindow() == w in the timer is a good addition. Pads in the background never stamp player_input_ns (nn_hid.c returns before player_used when the game isn't in front), so a background game can't hide the cursor.

Main has moved two commits since the fork (f647a072 → fbd42024). Neither touches host/display.c or the fixtures, so it isn't a conflict, but "Rebased on current main" is stale.

Must change

Nothing blocking in the code. The PR stays WIP for the Windows run named under Testing. "Host code builds on both Windows (llvm-mingw) and Linux" applies here and stays unticked until that build is done.

Should change

  1. Closing the Insert menu with a click hides the cursor at once. Use the pad, open the Insert menu, move the mouse and click Close/Resume. Moves while menu_open don't call gm_mouse_used, so gm_shown is still 0, and the next WM_TIMER (250 ms or less) calls gm_cursor_off right after the player used the mouse. Main had this only with a game menu under the Insert menu; it now happens in play too. Fix: drop !menu_open from the WM_MOUSEMOVE condition, since the gm state isn't read while the Insert menu is open, and update test_the_insert_menu_keeps_it.
  2. There is no dead zone, and the comparison uses client coordinates. Any one-pixel change shows the cursor. In play there is often no pad input for a while (cutscenes, idle), so a bumped desk or a high-DPI mouse's noise shows it until the next pad input. And because gm_mx/gm_my are client coordinates, the synthetic WM_MOUSEMOVE Windows sends after the window moves or resizes under a still cursor (F11, Alt+Enter, the Insert menu's fullscreen row, a DPI change) counts as "the mouse used". Suggestion: compare screen coordinates (GetMessagePos()) against the point where the cursor was hidden, and show it only past a few pixels, scaled by DPI.
  3. The cursor is hidden from the moment the window opens. gm_shown starts at 0, so the cursor is invisible over the window from launch (during shader preparation too), before any pad or key input. The title and the description say it hides "once the pads or keys are used". Either start gm_shown at 1 (call gm_mouse_used() at window creation), or say in the description and the README line that it starts hidden.
  4. The hide path has no behavioural check. The screensaver_test.c stub now always answers "shown", so the new timer branch (hide at once, but only in front and not under the Insert menu) is checked only by the text match in the new test. The fixture already compiles the whole wndproc with a foreground stub, so three cases would cover it: shown=0 with the game in front calls gm_cursor_off once; another window in front doesn't; menu_open doesn't.
  5. Linux and the Steam Deck get nothing. host/posix/display_sdl.c is untouched, so on Linux (X11 and Wayland) the cursor still sits over the picture in play. The README line is under a "(Windows)" heading, so it's accurate. But the commit title becomes the release note: say "(Windows)" in it, or open a follow-up for SDL (SDL_HideCursor/SDL_ShowCursor, which must not fight hos_sdl_relative_mouse).
  6. Nit: the edited comment line in display.c (line 68) is 182 columns; reflow it to the file's width.

For the Windows run (please cover)

  • In a window and borderless: the first pad or key input hides the cursor, and a mouse move shows it.
  • Alt-Tab away and back with the cursor over the game.
  • The Steam overlay (Shift+Tab / Guide) opened with a pad.
  • Close the Insert menu by click and by pad.
  • Toggle fullscreen with the cursor still.
  • With Steam Input's Desktop layout, where a stick or trackpad drives the mouse (see #152): check the cursor doesn't flicker between shown (the mouse move) and hidden (the pad stamp) on every WM_SETCURSOR.

Not checked here (read-only review): any build or run.

The change itself is sound: three conditions in `wndproc`, no new `ShowCursor` call, so Windows' cursor display count is untouched. Only `SetCursor(NULL)` hides the cursor, it applies over the client area alone (`HTCLIENT`, and `gm_cursor_off`'s `PtInRect` on the client rect), and Windows puts the cursor back when the mouse moves over another window, so nothing can stay hidden after exit or a crash. The window border and resize area keep their cursors. A captured mouse (Mouse camera on) works as before: `WM_MOUSEMOVE` still returns early while captured and `WM_SETCURSOR` already hid the cursor then. Releasing the capture leaves the cursor invisible until the next move, which is also what main does, because `SetCursor(NULL)` was the last shape set during the capture. With Mouse camera Off (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/178) clicks in play did nothing before and still do nothing. Dialogue choices and the game's menus go through `mouse_tick_locked` → `hos_mouse_game_menu`, so `game_menu_open` is set for them and they behave as on main (https://git.magicbean.dev/MagicBeanDev/Longshot/issues/85 is unaffected). The Insert menu keeps its pointer: `menu_open` is checked in all three places, and its pointer mailbox doesn't read `gm_shown`. The new `GetForegroundWindow() == w` in the timer is a good addition. Pads in the background never stamp `player_input_ns` (nn_hid.c returns before `player_used` when the game isn't in front), so a background game can't hide the cursor. Main has moved two commits since the fork (f647a072 → fbd42024). Neither touches `host/display.c` or the fixtures, so it isn't a conflict, but "Rebased on current main" is stale. ## Must change Nothing blocking in the code. The PR stays WIP for the Windows run named under Testing. "Host code builds on both Windows (llvm-mingw) and Linux" applies here and stays unticked until that build is done. ## Should change 1. **Closing the Insert menu with a click hides the cursor at once.** Use the pad, open the Insert menu, move the mouse and click Close/Resume. Moves while `menu_open` don't call `gm_mouse_used`, so `gm_shown` is still 0, and the next `WM_TIMER` (250 ms or less) calls `gm_cursor_off` right after the player used the mouse. Main had this only with a game menu under the Insert menu; it now happens in play too. Fix: drop `!menu_open` from the `WM_MOUSEMOVE` condition, since the gm state isn't read while the Insert menu is open, and update `test_the_insert_menu_keeps_it`. 2. **There is no dead zone, and the comparison uses client coordinates.** Any one-pixel change shows the cursor. In play there is often no pad input for a while (cutscenes, idle), so a bumped desk or a high-DPI mouse's noise shows it until the next pad input. And because `gm_mx/gm_my` are client coordinates, the synthetic `WM_MOUSEMOVE` Windows sends after the window moves or resizes under a still cursor (F11, Alt+Enter, the Insert menu's fullscreen row, a DPI change) counts as "the mouse used". Suggestion: compare screen coordinates (`GetMessagePos()`) against the point where the cursor was hidden, and show it only past a few pixels, scaled by DPI. 3. **The cursor is hidden from the moment the window opens.** `gm_shown` starts at 0, so the cursor is invisible over the window from launch (during shader preparation too), before any pad or key input. The title and the description say it hides "once the pads or keys are used". Either start `gm_shown` at 1 (call `gm_mouse_used()` at window creation), or say in the description and the README line that it starts hidden. 4. **The hide path has no behavioural check.** The `screensaver_test.c` stub now always answers "shown", so the new timer branch (hide at once, but only in front and not under the Insert menu) is checked only by the text match in the new test. The fixture already compiles the whole `wndproc` with a `foreground` stub, so three cases would cover it: shown=0 with the game in front calls `gm_cursor_off` once; another window in front doesn't; `menu_open` doesn't. 5. **Linux and the Steam Deck get nothing.** `host/posix/display_sdl.c` is untouched, so on Linux (X11 and Wayland) the cursor still sits over the picture in play. The README line is under a "(Windows)" heading, so it's accurate. But the commit title becomes the release note: say "(Windows)" in it, or open a follow-up for SDL (`SDL_HideCursor`/`SDL_ShowCursor`, which must not fight `hos_sdl_relative_mouse`). 6. Nit: the edited comment line in `display.c` (line 68) is 182 columns; reflow it to the file's width. ## For the Windows run (please cover) - In a window and borderless: the first pad or key input hides the cursor, and a mouse move shows it. - Alt-Tab away and back with the cursor over the game. - The Steam overlay (Shift+Tab / Guide) opened with a pad. - Close the Insert menu by click and by pad. - Toggle fullscreen with the cursor still. - With Steam Input's Desktop layout, where a stick or trackpad drives the mouse (see https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/152): check the cursor doesn't flicker between shown (the mouse move) and hidden (the pad stamp) on every `WM_SETCURSOR`. Not checked here (read-only review): any build or run.
host/display.c Outdated
@ -68,1 +66,3 @@
takes no part (no pointing or clicks while hidden). The wheel works hidden too, and does not show it: a button held
/* The cursor over the game's window, in play and in the game's menus (not the Insert menu), is hidden until the mouse
is used (moved or a button pressed) and again once the pads or the keys are (nn_hid.c: hos_player_input_ns): a player
on a controller never sees it, and a mouse at rest takes no part in the menus (no pointing or clicks while hidden). The wheel works hidden too, and does not show it: a button held
Owner

Nit: 182 columns, reflow to the file's width.

Nit: 182 columns, reflow to the file's width.
host/display.c Outdated
@ -274,3 +274,3 @@
if (mouse_captured) return 0;
const int x = (short)LOWORD(lp), y = (short)HIWORD(lp);
if (game_menu_open && !menu_open && (x != gm_mx || y != gm_my)) { /* (a move: not a repeat at rest) */
if (!menu_open && (x != gm_mx || y != gm_my)) { /* (a move: not a repeat at rest) */
Owner

Two things here. (1) !menu_open means moves in the Insert menu don't count as the mouse used, so after a pad press, opening the Insert menu and closing it with a click hides the cursor at the next WM_TIMER (250 ms or less), right after the mouse was used. The gm state isn't read while the Insert menu is open, so the condition can go (and test_the_insert_menu_keeps_it be updated). (2) Any one-pixel change in client coordinates counts: desk jitter, high-DPI noise, and the synthetic WM_MOUSEMOVE Windows sends when the window moves or resizes under a still cursor (F11, Alt+Enter, a DPI change) all show it. Consider screen coordinates (GetMessagePos()) against the point where it was hidden, with a small DPI-scaled threshold.

Two things here. (1) `!menu_open` means moves in the Insert menu don't count as the mouse used, so after a pad press, opening the Insert menu and closing it with a click hides the cursor at the next WM_TIMER (250 ms or less), right after the mouse was used. The gm state isn't read while the Insert menu is open, so the condition can go (and `test_the_insert_menu_keeps_it` be updated). (2) Any one-pixel change in client coordinates counts: desk jitter, high-DPI noise, and the synthetic WM_MOUSEMOVE Windows sends when the window moves or resizes under a still cursor (F11, Alt+Enter, a DPI change) all show it. Consider screen coordinates (`GetMessagePos()`) against the point where it was hidden, with a small DPI-scaled threshold.
host/display.c Outdated
@ -336,3 +336,3 @@
InterlockedCompareExchange64(&game_menu_seen_ns, 0, seen) == seen)
menus_mouse(w, menu_open, 0);
if (game_menu_open && !menu_open && !gm_cursor_shown()) gm_cursor_off(w); /* (hidden by the pads or keys: at once) */
if (!menu_open && !gm_cursor_shown() && GetForegroundWindow() == w) gm_cursor_off(w); /* (hidden by the pads or keys: at once) */
Owner

The foreground check is a good addition. The branch has no behavioural check, though: the screensaver fixture's gm_cursor_shown stub now always returns 1. That fixture already compiles all of wndproc with a foreground stub; cases for shown=0 with the game in front (gm_cursor_off called), another window in front (not called) and menu_open (not called) would cover it.

The foreground check is a good addition. The branch has no behavioural check, though: the screensaver fixture's `gm_cursor_shown` stub now always returns 1. That fixture already compiles all of `wndproc` with a `foreground` stub; cases for shown=0 with the game in front (gm_cursor_off called), another window in front (not called) and menu_open (not called) would cover it.
host/display.c Outdated
@ -405,3 +405,3 @@
break;
case WM_SETCURSOR:
if ((mouse_captured || (game_menu_open && !menu_open && !gm_cursor_shown())) && LOWORD(lp) == HTCLIENT) {
if ((mouse_captured || (!menu_open && !gm_cursor_shown())) && LOWORD(lp) == HTCLIENT) {
Owner

gm_shown starts at 0, so this now hides the cursor over the client area from the moment the window opens (shader preparation included), before any pad or key input. The title says "once the pads or keys are used". Either start shown (gm_mouse_used() at window creation), or document that it starts hidden.

`gm_shown` starts at 0, so this now hides the cursor over the client area from the moment the window opens (shader preparation included), before any pad or key input. The title says "once the pads or keys are used". Either start shown (`gm_mouse_used()` at window creation), or document that it starts hidden.
@ -133,3 +133,3 @@
static int64_t hos_now_ns(void) { return 0; }
static void gm_mouse_used(void) { unexpected(); }
static int gm_cursor_shown(void) { unexpected(); return 1; }
static int gm_cursor_shown(void) { return 1; } /* (the timer asks in play too: the cursor shown, it is left alone) */
Owner

Fine as a stub for the screensaver cases, but it means the new hide branch is never exercised in this fixture; see the comment on display.c's WM_TIMER.

Fine as a stub for the screensaver cases, but it means the new hide branch is never exercised in this fixture; see the comment on display.c's WM_TIMER.
jarynclouatre force-pushed feat/cursor-hidden-in-play from 6da7308764
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 31s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
to 9749eea590
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
2026-11-13 19:39:11 +00:00
Compare
jarynclouatre changed title from WIP: feat(display): hide the cursor in play once the pads or keys are used to WIP: feat(display): hide the cursor in play once the pads or keys are used (Windows) 2026-11-13 19:43:41 +00:00
Author
Contributor

Pushed 9749eea5: one commit, rebased onto main 514591a3. By the review's points:

  1. Insert menu closed with a click. WM_MOUSEMOVE no longer tests menu_open: a move under the Insert menu counts as the mouse used, so the cursor stays shown after a click on Close. test_the_insert_menu_keeps_it became test_a_move_counts_in_play_and_under_the_insert_menu.
  2. Dead zone and coordinates. The move is read with GetMessagePos() and compared in screen coordinates (gm_move_counts). While the cursor is shown, any other point counts. While it is hidden, only a point more than 4 pixels at 96 dpi (MulDiv(4, window_dpi(w), 96)) from where it was hidden. The hide point menus_mouse stores is a screen point now too. One side effect: in the game's menus a hidden cursor also needs that move, where main showed it at one pixel.
  3. Hidden from launch. gm_shown starts at 1: the cursor is shown until the first pad or key input.
  4. Behavioural checks. The three timer cases are in tests/screensaver_test.c (game in front, another window in front, Insert menu open). tests/test_cursor_hidden_in_play.py now compiles gm_cursor_shown, gm_move_counts and gm_mouse_moved out of display.c and runs them: shown at the start, hidden by the pad, 3 pixels no, 5 pixels yes, 8 at 192 dpi, negative screen coordinates.
  5. Linux. The title ends in "(Windows)" and the description says SDL is untouched. SDL is not in this PR; I can send it as a follow-up.
  6. The long comment line is reflowed.

Suite: 453 passed, 2 failed, 79 skipped, twice; main 514591a3: 452, 2, 79, twice (the same two failing checks, named in the description). A Windows build of 9749eea5 with llvm-mingw compiles and links. The Windows run with your list is still to do, so this stays WIP; its result will go in a comment here.

Pushed 9749eea5: one commit, rebased onto main 514591a3. By the review's points: 1. **Insert menu closed with a click.** `WM_MOUSEMOVE` no longer tests `menu_open`: a move under the Insert menu counts as the mouse used, so the cursor stays shown after a click on Close. `test_the_insert_menu_keeps_it` became `test_a_move_counts_in_play_and_under_the_insert_menu`. 2. **Dead zone and coordinates.** The move is read with `GetMessagePos()` and compared in screen coordinates (`gm_move_counts`). While the cursor is shown, any other point counts. While it is hidden, only a point more than 4 pixels at 96 dpi (`MulDiv(4, window_dpi(w), 96)`) from where it was hidden. The hide point `menus_mouse` stores is a screen point now too. One side effect: in the game's menus a hidden cursor also needs that move, where main showed it at one pixel. 3. **Hidden from launch.** `gm_shown` starts at 1: the cursor is shown until the first pad or key input. 4. **Behavioural checks.** The three timer cases are in `tests/screensaver_test.c` (game in front, another window in front, Insert menu open). `tests/test_cursor_hidden_in_play.py` now compiles `gm_cursor_shown`, `gm_move_counts` and `gm_mouse_moved` out of `display.c` and runs them: shown at the start, hidden by the pad, 3 pixels no, 5 pixels yes, 8 at 192 dpi, negative screen coordinates. 5. **Linux.** The title ends in "(Windows)" and the description says SDL is untouched. SDL is not in this PR; I can send it as a follow-up. 6. The long comment line is reflowed. Suite: 453 passed, 2 failed, 79 skipped, twice; main 514591a3: 452, 2, 79, twice (the same two failing checks, named in the description). A Windows build of 9749eea5 with llvm-mingw compiles and links. The Windows run with your list is still to do, so this stays WIP; its result will go in a comment here.
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
This pull request is marked as a work in progress.
Some workflows are waiting to be reviewed.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'feat/cursor-hidden-in-play:jarynclouatre-feat/cursor-hidden-in-play'
git switch 'jarynclouatre-feat/cursor-hidden-in-play'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!245
No description provided.