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
3.8 KiB
3.8 KiB
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:
// 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 followsslide; vertical offset =(1.0 - slide) * 12pxin 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.onfrom the current interpolated value (free with the animator — no special code, just don'tset_visible(false)early). set_visible(false)happens only after the exit completes: on the dismiss timer, playopen.offand start a0.2scleanup timeout that flips visibility; ashowarriving 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_motionflag is set,animator_cutboth 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 atinsurance/src/app.rs:1243–1260, rider:882, koboyo's equivalent) —CraftToastreplaces the RoundedView, inheriting itsdraw_bgcolors via+:merge. - Timer style:
Option<Timer>fields +cx.start_timeout, as all three apps already do.
Steps
- Implement
CraftToastin the craft crate: DSL (slide instance + animator per above), Rust (show(cx, msg),dismiss(cx), internal cleanup timer,reduce_motion: boolprop). - insurance: swap
toast_viewDSL; rewriteshow_toast(app.rs:1313–1321) and the timer arm inhandle_timertotoast.show/dismiss. Delete the rawset_visiblepair. - rider: same at
app.rs:882/:1004/:1279. - koboyo: same at
app.rs:558/:589. - 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.rstoast generation logic. - Do NOT introduce per-app toast forks — one widget, three consumers.
- Headless tests assert visibility/text/model state only, never mid-flight
slidevalues.
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.rsreturns nothing and the matrix is green.