# Recipes — the doctrine as `script_mod!` + Rust Copy-paste starting points, written against the Makepad 2.0 dev branch (`b41e740`) API: `AnimatorImpl` (`animator_play`, `animator_cut`, `animator_toggle`, `animator_handle_event`, `animator_in_state`), `AnimatorState`, `Play`, `Ease`, `snap()`. Widget structs need `#[derive(Script, ScriptHook, Widget, Animator)]` and `#[apply_default] animator: Animator` (see `reference/AGENTS.md`). In `handle_event`, always pump the animator: ```rust if self.animator_handle_event(cx, event).must_redraw() { self.draw_bg.redraw(cx); } ``` --- ## 1. Press feedback — 0.96 shader scale, snap in / eased out The asymmetric contract: press feedback is instant (`snap`), release relaxes in 0.15s. Scale happens in SDF space — no layout churn. ```rust // DSL — inside the widget definition draw_bg +: { down: instance(0.0) hover: instance(0.0) color: uniform(#ffffff) color_hover: uniform(#f4f3fa) radius: uniform(12.0) pixel: fn() { let sdf = Sdf2d.viewport(self.pos * self.rect_size) // scale(0.96) around the center while held let s = mix(1.0, 0.96, self.down) let c = self.rect_size * 0.5 sdf.box( c.x - c.x * s, c.y - c.y * s, self.rect_size.x * s, self.rect_size.y * s, self.radius * s ) sdf.fill(self.color.mix(self.color_hover, self.hover)) return sdf.result } } animator: Animator{ hover: { default: @off off: AnimatorState{ ease: OutQuad from: {all: Forward {duration: 0.1}} apply: {draw_bg: {hover: 0.0, down: 0.0}} } on: AnimatorState{ ease: OutQuad from: {all: Forward {duration: 0.08}} apply: {draw_bg: {hover: 1.0, down: 0.0}} } down: AnimatorState{ ease: OutQuad from: {all: Forward {duration: 0.15}} // release path: eased apply: {draw_bg: {down: snap(1.0), hover: 1.0}} // press path: instant } } } ``` ```rust // Rust — feedback on down, COMMIT ON UP-IF-OVER (tap-cancel works) match event.hits(cx, self.draw_bg.area()) { Hit::FingerHoverIn(_) => self.animator_play(cx, ids!(hover.on)), Hit::FingerHoverOut(_) => self.animator_play(cx, ids!(hover.off)), Hit::FingerDown(_) => self.animator_play(cx, ids!(hover.down)), Hit::FingerUp(fe) => { if fe.is_over { self.animator_play(cx, if fe.device.has_hovers() {ids!(hover.on)} else {ids!(hover.off)}); cx.widget_action(uid, &scope.path, ButtonAction::Clicked); // commit here } else { self.animator_play(cx, ids!(hover.off)); // dragged away = cancel } } _ => {} } ``` --- ## 2. Icon / state swap — opacity + scale + blur-substitute Jakub's exact values: scale 0.25↔1, opacity 0↔1 (blur has no cheap SDF analog; widen the edge feather with a `soft` instance instead). Cross-fade both glyphs in one shader, driven by a single `t`: ```rust draw_bg +: { t: instance(0.0) // 0 = icon A, 1 = icon B pixel: fn() { let sdf = Sdf2d.viewport(self.pos * self.rect_size) let sa = mix(1.0, 0.25, self.t) // A shrinks out let sb = mix(0.25, 1.0, self.t) // B grows in // ...draw glyph A with scale sa, alpha (1.0 - self.t) // ...draw glyph B with scale sb, alpha self.t return sdf.result } } animator: Animator{ active: { default: @off off: AnimatorState{ ease: Ease.Bezier {cp0: 0.2, cp1: 0.0, cp2: 0.0, cp3: 1.0} from: {all: Forward {duration: 0.2}} apply: {draw_bg: {t: 0.0}} } on: AnimatorState{ ease: Ease.Bezier {cp0: 0.2, cp1: 0.0, cp2: 0.0, cp3: 1.0} from: {all: Forward {duration: 0.2}} apply: {draw_bg: {t: 1.0}} } } } ``` Boot rule: `animator_toggle(cx, state, Animate::No, ids!(active.on), ids!(active.off))` when first syncing UI to model — animate only on *changes* (`Animate::Yes`), never on startup. --- ## 3. Toast — enter/exit instead of `set_visible` Toasts are occasional → standard animation. Enter: rise + fade, strong ease-out, 0.3s. Exit: same direction back, subtler and faster, 0.2s. Interruptible: a new toast retargets the same track mid-flight (that's free with the animator). ```rust // DSL: drive offset + alpha as instances; shift in the vertex/pixel stage toast_view := RoundedView{ draw_bg +: { slide: instance(0.0) // 0 = hidden below, 1 = resting // use slide to offset the drawn rect / alpha = slide } animator: Animator{ open: { default: @off off: AnimatorState{ // exit: faster, same path down ease: OutQuad from: {all: Forward {duration: 0.2}} apply: {draw_bg: {slide: 0.0}} } on: AnimatorState{ // enter: strong ease-out rise ease: Ease.Bezier {cp0: 0.23, cp1: 1.0, cp2: 0.32, cp3: 1.0} from: {all: Forward {duration: 0.3}} apply: {draw_bg: {slide: 1.0}} } } } } ``` ```rust // Rust — show: make visible, then play (never just set_visible) self.ui.view(cx, ids!(toast_view)).set_visible(cx, true); toast.animator_play(cx, ids!(open.on)); self.toast_timer = Some(cx.start_timeout(TOAST_SECS)); // on timer: play open.off; flip set_visible(false) only after the exit finishes // (a short second timeout at 0.2s, or check animator_in_state on the next frame). ``` If the toast body must physically move (layout, not shader), animating its container `margin` through the animator is acceptable — toasts are rare enough that relayout is fine. --- ## 4. Sheet release — momentum projection + velocity handoff Replace "decide mode → snap chrome" with: project where the flick was going, choose the nearest snap state, then settle from the *current* position at the *release* velocity using a `NextFrame` decay loop (the animator can't take an initial velocity; this can). ```rust const DECEL_RATE: f64 = 0.998; // 0.99 for a snappier feel fn project(v: f64) -> f64 { (v / 1000.0) * DECEL_RATE / (1.0 - DECEL_RATE) } // FingerUp: let projected = drag.pos + project(drag.velocity); let target = self.flow.nearest_snap(projected); // choose target from projection self.settle = Some(Settle { from: drag.pos, v0: drag.velocity, target, start: event_time, }); self.next_frame = cx.new_next_frame(); // NextFrame handler: exponential settle carrying v0 (no-overshoot spring analog) if let Some(s) = &self.settle { let t = time - s.start; let lambda = 12.0; // stiffness/period; raise = snappier let d = s.from - s.target; let x = (d + (s.v0 + lambda * d) * t) * (-lambda * t).exp(); let pos = s.target + x; self.apply_sheet_pos(cx, pos); // write the instance / walk value if x.abs() < 0.5 && (s.v0 * (-lambda * t).exp()).abs() < 10.0 { self.settle = None; // settled self.flow.set_sheet(s.target_mode); } else { self.next_frame = cx.new_next_frame(); } } // FingerDown during settle: grab it — kill the settle, resume 1:1 drag from `pos`. ``` For overscroll bounce, use the stock `rubber_band_bounce(x0, v0, t, touch)` from `widgets/src/scroll_motion.rs`; while dragging past a bound apply `over * dim * 0.55 / (dim + 0.55 * over.abs())`. --- ## 5. Staggered entrance (rare screens only) Per-item delay of 0.03–0.08s. Simplest robust form: one repeating timer stepping an index, playing each row's `enter.on`: ```rust // on screen-enter (first time only): self.stagger_idx = 0; self.stagger_timer = Some(cx.start_interval(0.05)); // on timer tick: if let Some(row) = rows.get(self.stagger_idx) { row.animator_play(cx, ids!(enter.on)); self.stagger_idx += 1; } else { cx.stop_timer(self.stagger_timer.take().unwrap()); } ``` Rows rest at `opacity 0` + `scale 0.97` (never 0) and enter with the strong ease-out at 0.3s. Never replay on back-navigation; never stagger a list the user reopens tens of times a day. --- ## 6. Reduced motion switch ```rust // app-level flag (theme/settings) pub reduce_motion: bool, fn play_or_cut(&self, cx: &mut Cx, w: &mut impl AnimatorImpl, st: &[LiveId; 2]) { if self.reduce_motion { w.animator_cut(cx, st) } // instant, state still lands else { w.animator_play(cx, st) } } ``` Keep opacity/color feedback under reduced motion; drop slides, scales, overshoot, staggers. --- ## 7. What NOT to do (verbatim slop patterns) ```rust // ✗ state change with no transition where one belongs self.ui.view(cx, ids!(panel)).set_visible(cx, true); // ✗ commit on press — breaks tap-cancel if ui.view(cx, &[id]).finger_down(actions).is_some() { do_the_action(); } // ✗ enter from nothing apply: {draw_bg: {scale: 0.0}} // start at 0.95–0.97 // ✗ ease-in on anything user-facing ease: InQuad // OutQuad/OutQuart/Bezier // ✗ symmetric press/release, or animated press // press must be snap(1.0); release 0.1–0.16s eased // ✗ hardcoded per-widget brand hex draw_bg +: { color: #7b5cf6 } // hoist to a shared mod/theme token ```