makepad-ports/plans/002-toast-motion.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

76 lines
3.8 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.

# 002 — Toast enter/exit motion (kill the `set_visible` pop)
- **Status**: TODO
- **Commit**: d50006e
- **Severity**: MEDIUM (all three crates)
- **Category**: Physicality & interruptibility
- **Estimated scope**: `craft/src/widgets.rs` (+~80 lines), 3 × `app.rs` (replace ~10 lines each), UI tests
## Problem
All three apps pop toasts in and out with `set_visible` — appearance from nothing, hard cut
on dismiss, mid-lifecycle re-shows snap:
```rust
// insurance/src/app.rs:1313 — current (rider:1004/:1279 and koboyo:558/:589 are equivalent)
fn show_toast(&mut self, cx: &mut Cx, msg: &str) {
self.toast_gen = self.flow.show_toast(msg);
self.ui.label(cx, ids!(toast_lbl)).set_text(cx, msg);
self.ui.view(cx, ids!(toast_view)).set_visible(cx, true);
if let Some(t) = self.toast_timer.take() { cx.stop_timer(t); }
self.toast_timer = Some(cx.start_timeout(TOAST_MS as f64 / 1000.0));
}
```
## Target
One `CraftToast` widget in the craft crate (RECIPES.md §3), adopted by all three apps:
- **Enter**: rise + fade driven by a `slide: instance(0.0→1.0)`,
`Forward {duration: 0.3}`, `ease: mod.craft.ease_out_strong`
(`Bezier {cp0: 0.23, cp1: 1.0, cp2: 0.32, cp3: 1.0}`). Rest alpha follows `slide`;
vertical offset = `(1.0 - slide) * 12px` in shader space. Nothing starts at scale/offset zero.
- **Exit**: same path downward, `Forward {duration: 0.2}`, `ease: OutQuad` — subtler and
faster than the enter, same direction (spatial consistency).
- **Interruptible**: a re-show mid-exit plays `open.on` from the current interpolated value
(free with the animator — no special code, just don't `set_visible(false)` early).
- `set_visible(false)` happens only after the exit completes: on the dismiss timer, play
`open.off` and start a `0.2s` cleanup timeout that flips visibility; a `show` arriving
in that window cancels the cleanup.
- Existing TTL generations (`toast_gen`) and durations (insurance 2200 ms etc.) unchanged.
- Reduced motion: when the app's `reduce_motion` flag is set, `animator_cut` both ways
(opacity still lands — state never depends on motion).
## Repo conventions to follow
- Keep each app's toast placement/styling DSL (`toast_view := RoundedView{…}` overlay at
`insurance/src/app.rs:12431260`, rider `:882`, koboyo's equivalent) — `CraftToast`
replaces the RoundedView, inheriting its `draw_bg` colors via `+:` merge.
- Timer style: `Option<Timer>` fields + `cx.start_timeout`, as all three apps already do.
## Steps
1. Implement `CraftToast` in the craft crate: DSL (slide instance + animator per above),
Rust (`show(cx, msg)`, `dismiss(cx)`, internal cleanup timer, `reduce_motion: bool` prop).
2. insurance: swap `toast_view` DSL; rewrite `show_toast` (`app.rs:13131321`) and the timer
arm in `handle_timer` to `toast.show/dismiss`. Delete the raw `set_visible` pair.
3. rider: same at `app.rs:882/:1004/:1279`.
4. koboyo: same at `app.rs:558/:589`.
5. UI tests per crate: trigger a toast action → assert toast text + visible; advance past
TTL + 0.3s → assert hidden. Add a **re-show mid-exit** test: show, wait past TTL so exit
starts, show again immediately → assert visible with the new text (no flash-to-hidden).
## Boundaries
- Do NOT change toast copy, TTLs, or `model.rs` toast generation logic.
- Do NOT introduce per-app toast forks — one widget, three consumers.
- Headless tests assert visibility/text/model state only, never mid-flight `slide` values.
## Verification
- **Mechanical**: full matrix green + new toast lifecycle tests.
- **Feel check**: trigger a toast → rises in with a fast start that eases out (~0.3s); expires
→ slips down faster (~0.2s); spam the triggering button → the toast retargets smoothly,
never blinks or restarts from the bottom.
- **Done when**: `grep -n "toast_view)).set_visible" */src/app.rs` returns nothing and the
matrix is green.