feat(input): implement input.pointer as a cursor position source - #229
feat(input): implement input.pointer as a cursor position source#2297086cmd wants to merge 2 commits into
Conversation
The platform contracts have registered `input.pointer` since the desktop
surface landed — "a REAL absolute pointer (mouse/trackpad): position plus
press/drag/release edges, hover resolves focus", explicitly distinct from
`input.cursor`'s nub-synthesized pointer. Nothing in the framework consumed
it: the only worked example, examples/note-widget, routes its mouse through
an app-specific svc channel and maps clicks to CIRCLE by hand, so every host
with a real pointer has to reinvent hover and hit-resolution in app code.
Give the capability a framework implementation by making the cursor's
position source configurable:
enableCursor({ source: "pointer" })
In that mode the host's absolute position drives the existing cursor state
machine and the pointer's own button supplies the press/release edges, so
hover, `focus:`, `active:`, press arming and onPress behave exactly as they
already do — only the position source and the edge source change. `speed`,
`dpadSpeed` and `button` do not apply. The default stays "analog", so every
existing app, host, tape and golden is untouched.
The host passes position and button state as a 4th frame argument, packed
`(down << 20) | (y << 10) | x` (10 bits per axis), mirroring how `touches`
was added. `undefined` means the host has no pointer this frame — a pointer
that left the window — which is deliberately distinguishable from one parked
at the origin: the cursor holds its last position and an in-flight press
stays armed until a real release. Replay scrubs the pointer exactly like
touch, so tapes stay deterministic.
Why a frame argument rather than a polled HostOps method: a pull-based op
would not be captured by the flight recorder, and every input the guest sees
has to be on the tape for `tape:check` to mean anything.
pocket-mod gains `Guest::frame_with_pointer` for native hosts.
Coverage: 7 cursor tests for the pointer source (adoption vs. nub steering,
hover-focus with no button, press/hold/release firing once, cancel on
release away from the armed node, the PSP mask NOT clicking a host-driven
cursor, pointer-left-the-window, clamping), 5 pointer.ts snapshot tests, and
a pocket-mod round-trip. `bun run tape:check` still matches all 180 frames.
Hero marks its button `focusable` with `onPress` but never establishes initial focus, and the framework deliberately does not auto-focus. `firePress` walks up from `focused`, which is null until a d-pad press, so CIRCLE on a freshly mounted hero activates nothing — and a host that reports an absolute point (a Vita/PocketBook panel, or a desktop host presenting a mouse as a contact) had no way to reach the button at all. Resolve the first contact to the focusable under it in onFrame, which runs before input edge detection, so a press arriving in the same frame lands on the node just focused rather than on whatever the d-pad left focused. This is the same hover-IS-focus pattern apps/note already uses for its svc mouse stream, spelled against the portable touch channel instead. Hosts with neither touch nor a pointer report no contact and this is a no-op, so the d-pad remains the portable default and the deterministic tape — which scrubs touch on replay — still matches all 180 frames.
22c0499 to
dd2d172
Compare
|
Thanks for working on this. The underlying goal is valid: give a real mouse or trackpad pointer a framework-owned path for absolute position, hover-to-focus, press, drag, release, and onPress, instead of making each host send private svc mouse messages and synthesize CIRCLE. I am closing this PR because the implementation is based on an input model that current main has superseded, and carrying it forward would lock in the wrong boundaries:
This is a close-and-rework decision, not a rejection of mouse support. Please coordinate the next pointer implementation in #59 and start from current main. A focused replacement should define one versioned frame-input contract, explicit pointer edge and cancellation semantics, tape recording and replay, separation between real-pointer interaction and virtual-cursor rendering, and one real host integration such as note-widget or Linux SDL. It should also cover Solid, Vue Vapor, and Octane, plus resize, high-resolution, fast-click, cancellation, and replay tests. Thanks again for pushing this direction forward. |
Why
The platform contracts have registered
input.pointersince the desktop surface landed — "a REAL absolute pointer (mouse/trackpad): position plus press/drag/release edges, hover resolves focus", explicitly notinput.cursor(contracts/spec/platforms.ts,docs/WIDGET.md). Nothing in the framework consumed it.The only worked example,
examples/note-widget, routes its mouse through an app-specificsvcchannel and maps clicks to CIRCLE by hand:So every host with a real pointer reinvents hover and hit-resolution in app code. This gives the capability a framework implementation.
What
The host's absolute position drives the existing cursor state machine and the pointer's own button supplies the press/release edges. Hover,
focus:,active:, press arming andonPressbehave exactly as they already do — only the position source and the edge source change.speed,dpadSpeedandbuttondo not apply.Default stays
"analog", so every existing app, host, tape and golden is untouched.Position arrives as a 4th frame argument, packed
(down << 20) | (y << 10) | x(10 bits per axis), mirroring howtoucheswas added.undefinedmeans the host has no pointer this frame — a pointer that left the window — deliberately distinguishable from one parked at the origin: the cursor holds its last position and an in-flight press stays armed until a real release.pocket-modgainsGuest::frame_with_pointerfor native hosts.Why a frame argument, not a polled
HostOpsmethodA pull-based op would not be captured by the flight recorder, and every input the guest sees has to be on the tape for
tape:checkto mean anything. Replay scrubs the pointer exactly like touch.Second commit:
apps/heroHero marks its button
focusablewithonPressbut never establishes initial focus, and the framework deliberately does not auto-focus —firePresswalks up fromfocused, which is null until a d-pad press. So CIRCLE on a freshly mounted hero activates nothing, and a host reporting an absolute point (Vita/PocketBook panel, or a desktop host presenting a mouse as a contact) could not reach the button at all.It now resolves the first contact to the focusable under it in
onFrame, which runs before input edge detection — the same hover-IS-focus patternapps/notealready uses, spelled against the portable touch channel. Hosts with neither touch nor a pointer report no contact and it is a no-op.Verification
bunx tsc --noEmitcleanbun run tape:check— 180 frames still match, both commitspointer.tssnapshot tests, registered inpackage.json'stestscriptpocket-modround-trip test forframe_with_pointerbun run test: 242 pass, 1 fail —psp-toolchain.test.ts > doctor treats an invalid explicit LLVM override, which imports onlytools/psp-toolchain.ts, has zero references toframework/orpocket-mod, and fails on a machine with no PSP SDK. Please confirm it's green in your CI.Context
Found while wiring mouse input for a
consortium-hmi-pocketkiosk host (embedded Linux, where there is no OS cursor to fall back on). Happy to adjust the packing, the option name, or split the hero commit out.🤖 Generated with Claude Code