# 001 — Press contract: feedback on down, commit on up-if-over, 0.96 scale - **Status**: TODO - **Commit**: d50006e - **Severity**: HIGH (all three crates) - **Category**: Purpose & frequency / Physicality — the responsiveness contract - **Estimated scope**: `craft/src/widgets.rs` (+~150 lines), `insurance/src/app.rs`, `rider/src/app.rs`, `koboyo/src/app.rs` (DSL + handler edits at ~40 call sites), UI tests ## Problem Every plain-View "button" in all three apps fires its action the moment a finger touches it, with zero visual feedback. Users cannot cancel a mis-touch by dragging away (the standard tap contract on every platform), and the UI never acknowledges a press. ```rust // insurance/src/app.rs:1427 — current (same pattern ~15×: tabs, back btns, quick actions, fab, chips) for (id, screen) in tab_rows { if ui.view(cx, &[id]).finger_down(actions).is_some() { self.flow.go(screen); self.refresh_chrome(cx); return; } } ``` ```rust // rider/src/app.rs:1110 — current (same pattern ~12×; note rider ALSO has real Buttons using // clicked() at :1200/:1215, so identical-looking controls commit at different moments) if ui.view(cx, &[id]).finger_down(actions).is_some() { let fx = self.flow.go(screen); self.apply_fx(cx, fx); return; } ``` ```rust // koboyo/src/app.rs:824 — current (rail tools, t_lock, t_shapes, t_connect, t_all_tools, // drawer_close, zoom_minus/plus, …) for (id, tool) in TOOL_BTNS { if ui.view(cx, &[id]).finger_down(actions).is_some() { self.set_tool(cx, tool); return; } } ``` ## Target A `Pressable` widget in the craft crate; every tappable View becomes one. Exact behavior: - `Hit::FingerDown` → play `hover.down`: `down` reaches 1.0 via `snap(1.0)` (instant), visible as **scale 0.96** computed in the SDF shader (RECIPES.md §1 — scale around center, radius scales with it) plus the widget's existing pressed tint if any. - `Hit::FingerUp{is_over: true}` → play release (`hover.on` for pointer devices / `hover.off` otherwise) with `Forward {duration: 0.15}`, `ease: OutQuad`, **and emit `PressableAction::Clicked`** — the ONLY place the action fires. - `Hit::FingerUp{is_over: false}` → play `hover.off`, emit nothing (tap-cancel). - `Hit::FingerHoverIn/Out` → `hover` 1.0/0.0 over `Forward {duration: 0.1}` / `0.1`, `OutQuad`. - Animator track is exactly the upstream `widgets/src/button.rs` `hover` structure (off/on/down states, `snap(1.0)` entries); durations from `mod.craft.*` tokens. - High-frequency exception (koboyo rail): construct with `feedback: minimal` — color/opacity only, no scale, durations unchanged. Direct-manipulation exception: tools that *arm a drag* (pen/lasso in koboyo) may keep act-on-down; mark those call sites with a comment `// arms a drag: act-on-down is intentional` instead of migrating them. ## Repo conventions to follow - Handler style: apps match on collected `actions` in `handle_actions`; `Pressable` should surface `clicked(actions)` exactly like stock `Button` so migration is mechanical: `ui.view(cx, &[id]).finger_down(actions).is_some()` → `pressable_clicked(ui, cx, &[id], actions)` or per-widget `ui.widget(cx, &[id]).…`. - Dynamic ids in loops MUST use `&[id]`, never `ids!(id)` (README bug 1). - Register with the `let Pressable = #(Pressable::register_widget(vm)) {…}` pattern (bug 3). ## Steps 1. Implement `Pressable` in `craft/src/widgets.rs` per RECIPES.md §1 (struct, DSL, animator, hit handling, `PressableAction::Clicked`). Include `minimal: bool` live prop. 2. insurance: wrap the ~15 tappable Views (`tab_*`, `back_*`, quick actions, `dl_btn`, `fab_btn`, `backdrop_view` stays a plain scrim, chips at `:1507/:1531`) as `Pressable`; change `app.rs:1427–1531` handlers from `finger_down` to clicked-on-up. `backdrop_view` dismiss may remain act-on-down (scrims dismiss on touch — platform convention). 3. rider: same for `app.rs:1108–1214` (nav rows, avatars, `back_found`, `wtc_pill`, suggestion rows, pills). Real `Button`s at `:1200/:1215` keep `clicked()` but gain the 0.96 press track via a `craft`-styled button variant so look == behavior. 4. koboyo: same for `app.rs:822–883+` rail/zoom/drawer-close controls with `minimal: true`; leave pen/lasso arming sites act-on-down with the marker comment. 5. Tests, per crate: - Integration (model untouched — should pass unchanged). - UI (`tests/ui.rs`): add a **tap-cancel test**: synthesize FingerDown on a nav tile, FingerMove outside its rect, FingerUp → assert screen/model did NOT change; then a plain click → assert it did. Assert end-state via model/screen, not animator mid-values. ## Boundaries - Do NOT change what any action does — only *when* it fires (up-if-over) and its feedback. - Do NOT animate keyboard-driven paths. - Do NOT touch canvas gesture code in `koboyo/src/canvas_view.rs`. - If a call site's action deliberately needs act-on-down beyond the listed exceptions, STOP and report rather than guessing. ## Verification - **Mechanical**: full test matrix green; new tap-cancel UI tests pass in `MAKEPAD=headless` mode. - **Feel check** (run each app windowed): press-and-hold any tile → it visibly sinks to 0.96 instantly; drag off and release → nothing happens; release over it → action fires and the tile relaxes over ~0.15s. Spam-tap → no restart-from-zero flicker (animator retargets). - **Done when**: `grep -rn "finger_down(actions)" */src/app.rs` returns only the documented act-on-down exceptions, and all 140+ tests are green.