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

3.8 KiB
Raw Permalink Blame History

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 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.