Skip to content

feat(framework): gesture layer over the touch snapshot#161

Draft
doodlewind wants to merge 1 commit into
mainfrom
feat/gesture-layer
Draft

feat(framework): gesture layer over the touch snapshot#161
doodlewind wants to merge 1 commit into
mainfrom
feat/gesture-layer

Conversation

@doodlewind

Copy link
Copy Markdown
Collaborator

Part 1/6 of the touch-input infrastructure series (→ Vita YouTube with modern mobile UX, graceful PSP degradation).

What

  • framework/src/gesture.ts (new, @pocketjs/framework/gesture): turns the stateless touches() snapshot into contact lifecycles with recognizers:
    • down / move / up / cancel edges, per-contact Int16Array history, release velocity (x[t]−x[t−k])·hz/k — integer deltas + one IEEE division, bit-identical on every host.
    • tap (slop-gated), long-press (virtual-frame deadline from simulationHz()), axis-lockable pan with release velocity for flings.
    • Claim semantics (the UIKit model): the first recognizer whose pan crosses slop claims the contact and every other owner is cancelled — a list pan cancels a row's press highlight. Priority = registration order, last first (the focus-controller convention).
    • Region ownership via the ink-claiming hitTest op with rect as both fallback (no op 27) and complement (ink misses inside the region); occluding ink outside the subtree wins.
    • pushTouchBlock() — the touch mirror of pushButtonHandlerBlock: synchronously cancels non-exempt in-flight contacts and mutes new downs (OSK modality).
    • Steady state allocates nothing; on PSP (touches() always empty) the pump is two comparisons.
  • input.ts: setActiveNode / pressNode / hitNode exports so touch, cursor, and d-pad route focus:/active: through one authority (no stranded pressed visuals).
  • index.ts: __runGestures() between effect delivery and app frame hooks; resetGestures() on mount/dispose.
  • Subpath registered in all three maps (package.json exports / tsconfig paths / jsx-plugin FRAMEWORKS.solid.subpaths).

Determinism

Pump order is fixed (after __setTouches/__drainEffects, before runFrameHooks), velocities and deadlines are pure functions of the input tape and simulationHz(). No Math.pow/transcendentals anywhere.

Tests

  • tests/gesture.test.ts: 19 cases — lifecycle edges, tap slop boundaries, single-fire priority, exact velocities at 60/30 Hz, long-press deadlines at 60/30 Hz, axis-lock rejection, claim ordering, pushTouchBlock cancel+exempt, dispose/cancel, 8-contact cap.
  • Byte-exact sim suites (tests/sim.test.ts, tests/im-sim.test.ts) green over the modified frame pump; bunx tsc --noEmit clean.

Follow-ups in this series

kinetic scroller → tape v2 touch track → VirtualList → OSK touch model → scripted touch goldens.

🤖 Generated with Claude Code

touches() was a stateless per-frame snapshot with exactly one consumer (the
OSK's contact-edge tap). This adds framework/src/gesture.ts — contact
lifecycles (down/move/up/cancel), per-contact history with deterministic
release velocity, and recognizers for tap, long-press, and axis-lockable pan
with UIKit-style claim semantics (a list pan cancels the row's press
highlight). Steady state allocates nothing: 8 pooled tracks (the wire cap),
Int16Array history rings, one flag byte per recognizer×slot.

The pump runs in the frame handler between effect delivery and app frame
hooks, so app code always observes the frame's completed gesture output; on
hosts without touch (PSP) it costs two comparisons.

The gesture layer never touches focus itself — input.ts grows three exports
(setActiveNode, pressNode, hitNode) so touch, cursor, and d-pad share one
authority over focus:/active: variants, and pushTouchBlock() mirrors
pushButtonHandlerBlock for modals (opening the OSK cancels list gestures the
same frame).

New @pocketjs/framework/gesture subpath registered in all three maps
(package.json exports, tsconfig paths, jsx-plugin FRAMEWORKS.solid.subpaths).

Tests: 19 unit cases driving __setTouches/__runGestures directly — edges,
slop, claim priority, hz-scaled long-press deadlines and velocities, block/
dispose cancellation. Sim byte-exact suites (cafe, im) stay green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant