WIP: feat(display): hide the cursor in play once the pads or keys are used (Windows) #245
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!245
Loading…
Reference in a new issue
No description provided.
Delete branch "jarynclouatre/Longshot:feat/cursor-hidden-in-play"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
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.gm_shownstarts at 1) and hides at the first pad or key input.WM_MOUSEMOVEcalls the newgm_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 theWM_MOUSEMOVEWindows 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 whilegm_cursor_shown()is 0.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.host/posix/display_sdl.cis not touched, so on Linux and the Steam Deck the cursor is as before; the title says so.tests/test_cursor_hidden_in_play.pyis new. It compiles the cursor's own functions out ofhost/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 threewndprocconditions.tests/screensaver_test.c(the fixture that compiles the wholewndproc): 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 forgm_mouse_moved, since the move case no longer readslp.Flags and settings
None.
Measurements
main:
514591a3(2026-11-13)this PR: feat/cursor-hidden-in-play @
9749eea5Hardware: 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'shost/display.cHow 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.pyinto main514591a39749eea5tests/run_all.py(run 1, run 2)test_bake_portability.py,test_menu_i18n.pytests/test_cursor_hidden_in_play.pytest_bake_portability.pyfails here for a missing Python module (zstandard), on main as well.test_menu_i18n.pyfails on main itself.Testing
Tested: the game-free suite on a Linux PC, as in Measurements, with
test_screensaver.pyandtest_display_pointer.py(the two fixtures that compilehost/display.c's window code) passing on both sides. A Windows build of this branch at9749eea5with 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.cshows all of the change: the state's start value,gm_move_counts,gm_mouse_moved, the hide point inmenus_mousekept as a screen point, and the three conditions inwndproc.GetForegroundWindow() == win the timer keeps a game in the background from hiding a cursor that is over it while another program is in use.SDL_HideCursor/SDL_ShowCursor, besidehos_sdl_relative_mouse).Issues
None.
Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
tests/that reads changed source updated in this PR, not weakened.The change itself is sound: three conditions in
wndproc, no newShowCursorcall, so Windows' cursor display count is untouched. OnlySetCursor(NULL)hides the cursor, it applies over the client area alone (HTCLIENT, andgm_cursor_off'sPtInRecton 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_MOUSEMOVEstill returns early while captured andWM_SETCURSORalready hid the cursor then. Releasing the capture leaves the cursor invisible until the next move, which is also what main does, becauseSetCursor(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 throughmouse_tick_locked→hos_mouse_game_menu, sogame_menu_openis set for them and they behave as on main (#85 is unaffected). The Insert menu keeps its pointer:menu_openis checked in all three places, and its pointer mailbox doesn't readgm_shown. The newGetForegroundWindow() == win the timer is a good addition. Pads in the background never stampplayer_input_ns(nn_hid.c returns beforeplayer_usedwhen 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 toucheshost/display.cor 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
menu_opendon't callgm_mouse_used, sogm_shownis still 0, and the nextWM_TIMER(250 ms or less) callsgm_cursor_offright 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_openfrom theWM_MOUSEMOVEcondition, since the gm state isn't read while the Insert menu is open, and updatetest_the_insert_menu_keeps_it.gm_mx/gm_myare client coordinates, the syntheticWM_MOUSEMOVEWindows 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.gm_shownstarts 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 startgm_shownat 1 (callgm_mouse_used()at window creation), or say in the description and the README line that it starts hidden.screensaver_test.cstub 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 wholewndprocwith aforegroundstub, so three cases would cover it: shown=0 with the game in front callsgm_cursor_offonce; another window in front doesn't;menu_opendoesn't.host/posix/display_sdl.cis 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 fighthos_sdl_relative_mouse).display.c(line 68) is 182 columns; reflow it to the file's width.For the Windows run (please cover)
WM_SETCURSOR.Not checked here (read-only review): any build or run.
@ -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 mouseis used (moved or a button pressed) and again once the pads or the keys are (nn_hid.c: hos_player_input_ns): a playeron 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 heldNit: 182 columns, reflow to the file's width.
@ -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) */Two things here. (1)
!menu_openmeans 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 (andtest_the_insert_menu_keeps_itbe 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.@ -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) */The foreground check is a good addition. The branch has no behavioural check, though: the screensaver fixture's
gm_cursor_shownstub now always returns 1. That fixture already compiles all ofwndprocwith aforegroundstub; 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.@ -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) {gm_shownstarts 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) */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.
6da73087649749eea590WIP: feat(display): hide the cursor in play once the pads or keys are usedto WIP: feat(display): hide the cursor in play once the pads or keys are used (Windows)Pushed
9749eea5: one commit, rebased onto main514591a3. By the review's points:WM_MOUSEMOVEno longer testsmenu_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_itbecametest_a_move_counts_in_play_and_under_the_insert_menu.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 pointmenus_mousestores 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.gm_shownstarts at 1: the cursor is shown until the first pad or key input.tests/screensaver_test.c(game in front, another window in front, Insert menu open).tests/test_cursor_hidden_in_play.pynow compilesgm_cursor_shown,gm_move_countsandgm_mouse_movedout ofdisplay.cand runs them: shown at the start, hidden by the pad, 3 pixels no, 5 pixels yes, 8 at 192 dpi, negative screen coordinates.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 of9749eea5with 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.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.