plans/README.md: index, execution order 000->005, executor ground rules (test gate, DSL landmines) 000: shared craft crate (easing/duration/color tokens, Pressable, CraftToast, motion math + units) 001: HIGH press contract - feedback on down, commit on up-if-over, 0.96 scale (all 3 crates) 002: MEDIUM toast enter/exit, interruptible, reduced-motion aware (all 3 crates) 003: MEDIUM insurance sheet momentum projection + velocity-handoff settle, rubber-band 004: MEDIUM koboyo drawer slide, origin-aware popovers, animated camera fit 005: LOW polish - color tokens, concentric radii, real icons, tabular figures
112 lines
5.5 KiB
Markdown
112 lines
5.5 KiB
Markdown
# 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.
|