makepad-ports/plans/001-press-contract.md
andodeki 3cfeaa289c Add implementation plans for makepad-ui-craft (Emil's improve-animations plan format)
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
2026-09-01 18:50:05 +00:00

112 lines
5.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:14271531` 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:11081214` (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:822883+` 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.