## Why The app UI has five overlapping hand-rolled gesture recognizers (`GestureProcessor`, `ChatView::TouchInfo`, `Menu::TouchInfo`+`DragInfo`, `BaseEdit::TouchStateAction`, `EmojiPicker::TouchInfo`) plus a dead pinch node, each re-deriving movement, dwell time, and velocity with wildly divergent thresholds (tap strictness ranges from 0.05px to 10px across widgets). Touch dispatch is split-brained: a sync Stage-thread path that can swallow events before the async path — and before the window gesture processor — ever sees them, no hit-testing, and `handle_gesture` cannot propagate past the first `Layer`. Widget touch code is the largest block of incidental complexity in `bin/app/src/ui/`. ## What Changes - New gesture subsystem in `bin/app/src/ui/gesture/`: a window-level `GestureSession` owning the touch stream, target resolution, long-press timers, version-guarded cancellation, move throttling, and recognizer arbitration; plus a recognizer library (tap, long-press, drag lifecycle, flick) that exists exactly once. - Unified gesture constants (touch slop, tap duration, long-press timeout from the system `long_press_timeout()`, flick velocity, move delivery period) replacing the per-widget scatter. Per-widget config survives only where semantic: axis lock, drag direction, `min_travel: 0.` for precision drags. - `UIObject` gains `gesture_set()` and `gesture_hit_test()`; the existing (dead-dispatch) `handle_gesture()` is re-pointed to the new `GestureAction`; `Layer`/`ScrollLayer` forward gestures with coordinate translation. - New `GestureAction` stream: `Down`/`Up` passthrough for immediate feedback, `Tap`, `LongPress`, `DragStart`/`DragMove`/`DragEnd { vel }` (flick is derived from `DragEnd` velocity by the consumer). - The session is fed from the Stage thread (`gfx` touch entry) so it observes all touches regardless of sync claiming; `EMULATE_TOUCH` mouse emulation routes through the same session so desktop development produces gestures. - All touch widgets migrate to the new model: Button, TokenTable, EmojiPicker, Menu, ChatView, BaseEdit (hybrid: `Down`-armed precision drags for selection handles). Recognition semantics live in recognizers; controller physics (scroll inertia, grab-to-stop) stay widget-side. - **BREAKING** (internal app API): `handle_touch` and `handle_touch_sync` are removed from `UIObject` once every widget is migrated; the four widget `TouchInfo` state machines and `win/gesture.rs` (`GestureProcessor` and the old `GestureAction`) are deleted at the end; the dead `ui/gesture.rs` pinch node is removed up front — the new `ui/gesture/` directory takes its module path (pinch returns later as a recognizer if wanted). - Accepted behavior deltas toward platform-standard feel: slop-bounded taps everywhere (replaces 0.05px strictness in ChatView/Menu), slop dead-zone before scroll starts, long-press fires once during hold by timer, touch ownership sticks to the Started target, EmojiPicker may gain flick inertia, all gesture delivery is async (the sync path dies; on-device latency to be verified during migration with a sync hatch as fallback). ## Capabilities ### New Capabilities - `gesture`: the gesture recognition and delivery system for the app UI — session semantics (stream ownership, targeting, timers, throttling, arbitration), recognizer contracts and unified constants, the `GestureAction` event stream, the `UIObject` gesture contract (`gesture_set`/`gesture_hit_test`/`handle_gesture`), coordinate-translation forwarding, and the migrated behavior of each widget under the new system (button taps, scrollers, chat selection/scroll/flick, edit hybrid handling, menu reorder/edit-mode) including the accepted behavior deltas above. ### Modified Capabilities (none — no main specs exist yet; widget behavior deltas are captured as requirements of the new `gesture` capability) ## Impact - Code: `bin/app/src/ui/` (new `gesture/` module; rewrites of `win/mod.rs` touch entry, `layer.rs`, `scroll_layer.rs`, `button.rs`, `tokentable/`, `emoji_picker/`, `menu/`, `chatview/`, `edit/`; deletion of the dead `ui/gesture.rs` and `win/gesture.rs` up front), and `bin/app/src/gfx/mod.rs` (Stage-thread session feed). No workspace crates, no dependencies, no consensus/ZK surfaces. - Parallel work: `app-chatview` (chatview2) is specified to consume drag/flick as scroll-controller inputs — this change should land its session and recognizers first so chatview2 builds on it; ChatView's own migration is the proof of composition (or is absorbed by chatview2 if it lands first). - Verification: `make compile-dev` (desktop), `make compile-apk` (Android), and on-device touch-feel verification for the async-only delivery (edit selection handles are the latency-sensitive case).