Cursor capture: hide the OS cursor and lock/confine the mouse to the window (Input.cursor_mode) #89

Closed
opened 2026-09-02 06:38:22 +02:00 by orkun · 1 comment
Owner

A windowed action game aiming with the mouse needs to (a) hide the OS cursor while playing and draw its own reticle, and (b) keep the mouse from leaving the window (confine or lock+relative deltas), releasing both in pause/menus and on focus loss. The platform layer (runtime/native/cocoa.ll) has no cursor support at all — no NSCursor hide/unhide, no CGAssociateMouseAndMouseCursorPosition / CGWarpMouseCursorPosition — and there is no Input/Window builtin for it.

Proposal: Input.cursor_mode(mode) with

  • 0 normal (default): OS cursor visible, free.
  • 1 hidden: [NSCursor hide] while the window is key; unhide on focus loss / mode change.
  • 2 locked: hidden + CGAssociateMouseAndMouseCursorPosition(false) + warp to the window centre each frame, feeding Input.mouse_dx/dy as relative motion (and clamping mouse_x/y to the framebuffer) — the FPS/twin-stick capture mode.
  • 3 confined: visible or hidden, but position clamped to the window rect (warp back when it leaves).
    Auto-release: any mode returns to 0 while the window is not key (Cmd-Tab), and on win_close. Headless: no-op. Windows/Linux backends implement the same contract (SetCursor/ClipCursor, XGrabPointer / SDL_SetRelativeMouseMode).

Needs a new win_* intrinsic (emit_intrin2) + the cocoa.ll implementation + Input.cursor_mode in emit_call. Motivated by Emberdepths: the cursor wanders off the window mid-fight and the OS arrow sits over the game's own reticle.

A windowed action game aiming with the mouse needs to (a) hide the OS cursor while playing and draw its own reticle, and (b) keep the mouse from leaving the window (confine or lock+relative deltas), releasing both in pause/menus and on focus loss. The platform layer (runtime/native/cocoa.ll) has no cursor support at all — no NSCursor hide/unhide, no CGAssociateMouseAndMouseCursorPosition / CGWarpMouseCursorPosition — and there is no Input/Window builtin for it. Proposal: `Input.cursor_mode(mode)` with - 0 normal (default): OS cursor visible, free. - 1 hidden: `[NSCursor hide]` while the window is key; unhide on focus loss / mode change. - 2 locked: hidden + `CGAssociateMouseAndMouseCursorPosition(false)` + warp to the window centre each frame, feeding `Input.mouse_dx/dy` as relative motion (and clamping `mouse_x/y` to the framebuffer) — the FPS/twin-stick capture mode. - 3 confined: visible or hidden, but position clamped to the window rect (warp back when it leaves). Auto-release: any mode returns to 0 while the window is not key (Cmd-Tab), and on `win_close`. Headless: no-op. Windows/Linux backends implement the same contract (SetCursor/ClipCursor, XGrabPointer / SDL_SetRelativeMouseMode). Needs a new win_* intrinsic (emit_intrin2) + the cocoa.ll implementation + `Input.cursor_mode` in emit_call. Motivated by Emberdepths: the cursor wanders off the window mid-fight and the OS arrow sits over the game's own reticle.
Author
Owner

Shipped in 3eb5447 (full suite 115/0, fixpoint intact).

Input.cursor_mode(mode) with the four modes from the proposal:

  • 0 normal — cursor visible and free (default)
  • 1 hidden — [NSCursor hide] while the window is key; draw your own reticle
  • 2 locked — hidden + CGAssociateMouseAndMouseCursorPosition(false); the mouse feeds relative motion through Input.mouse_dx/dy (via CGGetLastMouseDelta), and Input.mouse_x/y is a clamped virtual cursor — the FPS / twin-stick capture mode
  • 3 confined — dissociated but visible; the mouse can't leave the window

Auto-release is handled every frame from win_poll: while the window is not key (Cmd-Tab) the cursor is shown + reconnected, and it re-acquires on refocus — and win_close restores it, so a game never leaves the cursor hidden/locked. Headless / non-windowed it is a no-op (the win_cursor_mode call is DCE'd behind is_windowed()).

Implementation is all in the platform layer as the issue laid out: a new win_cursor_mode intrinsic (emit_intrin2 + the emit_head declare) + the cocoa.ll implementation (NSCursor hide/unhide — ref-counted, so I track the applied state and toggle only on a change to keep the hide count balanced across focus changes; CGAssociateMouseAndMouseCursorPosition; CGGetLastMouseDelta) + Input.cursor_mode in emit_call.

This is windowed-only behaviour, so it's not in the headless golden suite; examples/library/cursor_capture.ludic is wired as a compile smoke (it draws a reticle at the captured mouse position) and I verified a windowed build links cleanly against the new cocoa.ll. A Windows/Linux backend implements the same win_cursor_mode contract (SetCursor/ClipCursor, XGrabPointer / SDL_SetRelativeMouseMode).

One honest note on mode 3: it currently dissociates + clamps the virtual cursor (so the mouse can't leave), rather than warping the visible OS cursor back at the window edge each frame — a precise visible-confine warp needs the window/screen frame math and can be a follow-up if you want the visible arrow to slide along the edge.

Shipped in 3eb5447 (full suite 115/0, fixpoint intact). **`Input.cursor_mode(mode)`** with the four modes from the proposal: - `0` normal — cursor visible and free (default) - `1` hidden — `[NSCursor hide]` while the window is key; draw your own reticle - `2` locked — hidden + `CGAssociateMouseAndMouseCursorPosition(false)`; the mouse feeds **relative** motion through `Input.mouse_dx/dy` (via `CGGetLastMouseDelta`), and `Input.mouse_x/y` is a clamped virtual cursor — the FPS / twin-stick capture mode - `3` confined — dissociated but visible; the mouse can't leave the window **Auto-release** is handled every frame from `win_poll`: while the window is not key (Cmd-Tab) the cursor is shown + reconnected, and it re-acquires on refocus — and `win_close` restores it, so a game never leaves the cursor hidden/locked. Headless / non-windowed it is a no-op (the `win_cursor_mode` call is DCE'd behind `is_windowed()`). Implementation is all in the platform layer as the issue laid out: a new `win_cursor_mode` intrinsic (emit_intrin2 + the emit_head `declare`) + the `cocoa.ll` implementation (`NSCursor hide/unhide` — ref-counted, so I track the applied state and toggle only on a change to keep the hide count balanced across focus changes; `CGAssociateMouseAndMouseCursorPosition`; `CGGetLastMouseDelta`) + `Input.cursor_mode` in emit_call. This is windowed-only behaviour, so it's not in the headless golden suite; `examples/library/cursor_capture.ludic` is wired as a compile smoke (it draws a reticle at the captured mouse position) and I verified a windowed build links cleanly against the new cocoa.ll. A Windows/Linux backend implements the same `win_cursor_mode` contract (SetCursor/ClipCursor, XGrabPointer / SDL_SetRelativeMouseMode). One honest note on mode 3: it currently dissociates + clamps the virtual cursor (so the mouse can't leave), rather than warping the *visible* OS cursor back at the window edge each frame — a precise visible-confine warp needs the window/screen frame math and can be a follow-up if you want the visible arrow to slide along the edge.
orkun closed this issue 2026-09-02 07:06:56 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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: workshopsoft/ludic#89
No description provided.