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
76 lines
3.8 KiB
Markdown
76 lines
3.8 KiB
Markdown
# 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:1243–1260`, 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:1313–1321`) 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.
|