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
5.4 KiB
5.4 KiB
004 — Koboyo chrome: drawer slide, origin-aware popovers, animated camera-fit
- Status: TODO
- Commit:
d50006e - Severity: MEDIUM
- Category: Physicality & origin / missed opportunities
- Estimated scope:
koboyo/src/app.rs(~120 lines),koboyo/src/model.rs(+~40 lines),koboyo/src/canvas_view.rs(camera tween step), tests
Problem
// koboyo/src/app.rs:608 — current: the 112-tool drawer pops
fn set_drawer(&mut self, cx: &mut Cx, open: bool) {
self.drawer_open = open;
self.ui.view(cx, ids!(drawer)).set_visible(cx, open);
if open {
self.ui.text_input(cx, ids!(search_input)).set_text(cx, "");
self.rebuild_drawer(cx);
let area = self.ui.text_input(cx, ids!(search_input)).area();
cx.set_key_focus(area); // focus lands before the surface visually exists
}
}
// koboyo/src/app.rs:601 — current: five popover panels pop from nothing
self.ui.view(cx, ids!(fly_shapes)).set_visible(cx, false);
self.ui.view(cx, ids!(fly_connect)).set_visible(cx, false);
self.ui.view(cx, ids!(menu_panel)).set_visible(cx, false);
self.ui.view(cx, ids!(exp_panel)).set_visible(cx, false);
self.ui.view(cx, ids!(view_panel)).set_visible(cx, false);
// koboyo/src/model.rs:407 and :722 — current: button-triggered camera jumps snap
pub fn reset_zoom(&mut self) { … }
pub fn zoom_to_fit(&mut self, viewport: (f64, f64)) -> bool { … }
Wheel/pinch zoom and pan are 1:1 and must stay instant; but Reset/Fit are occasional, button-triggered spatial jumps — snapping teleports the user's mental map.
Target
- Drawer (
draweratapp.rs:268): slide + fade via aslideinstance, enterForward {duration: 0.25}ease: mod.craft.ease_sheet(Bezier {cp0: 0.32, cp1: 0.72, cp2: 0.0, cp3: 1.0}), exitForward {duration: 0.18}OutQuad, same edge both directions.set_key_focusmoves to after the enter starts (same frame is fine — afteranimator_play, not beforeset_visible). - Popovers (
fly_shapes,fly_connect,menu_panel,exp_panel,view_panel): enter 0.18s, scale 0.97→1.0 + fade 0→1,ease: mod.craft.ease_out_strong, SDF scale anchored at the edge nearest the triggering rail button (origin-aware: anchor point in the shader is the rail-side edge midpoint, notrect_size*0.5). Exit 0.12s fadeOutQuad. Never start from scale 0. - Camera fit/reset: tween the camera (pan + zoom together) from current to target over
0.25s with
Ease.ExpDecay {d1: 0.82, d2: 0.97}evaluated in Rust (Ease::map(t)), stepped byNextFrameincanvas_view.rs. Interpolate zoom geometrically (z = z0·(z1/z0)^k) and pan linearly in content space so the motion doesn't swim. Any gesture (pointer down, wheel) cancels the tween instantly and takes over 1:1. Underreduce_motion: jump (current behavior). - Rail buttons already gain minimal press feedback in plan 001 — no double-animation here.
Repo conventions to follow
- Camera math lives in
model.rs(zoom_at_keeps_anchor_stationarytest style); the tween targets should be computed by the existingfit_to_bbox/reset_zoominto a pending target rather than mutating live camera state directly. canvas_view.rsalready owns gesture routing + redraw; put the NextFrame step there, following its existing pan/zoom apply path.- Dynamic widget lookups with
&[id](README bug 1); custom-widget borrows via.widget(cx, …)(bug 5).
Steps
model.rs: addCameraTween {from: Cam, to: Cam, start: f64}+pub fn camera_tween_eval(tw, t) -> Cam(geometric zoom, content-space pan) + units: endpoint equality att≥1, anchor stability for pure-zoom tweens, cancel semantics.canvas_view.rs: route Fit/Reset through the tween; NextFrame step; cancel on anyHit::FingerDown/scroll; keep instant path underreduce_motion.app.rs: drawer animator track + focus reorder; popover animator tracks with per-panel anchor; replace the fiveset_visiblelines withanimator_play+ deferred hide (0.12–0.18s cleanup timer, as in the toast pattern).- UI tests: drawer open → search input focused and a row clickable after ≤ 0.3s; zoom-to-fit with content → camera equals fit target after ≤ 0.5s (assert model camera, not pixels); popover open/close leaves no invisible-but-hit-testable panel (README bug 6/8 — center-clickability of what's underneath after close).
Boundaries
- Do NOT smooth wheel/pinch zoom, pan, draw, lasso, or eraser — direct manipulation stays 1:1.
- Do NOT change drawer contents, search, recents, or the 112-tool registry.
- Do NOT animate the canvas dot-grid shader.
- If popover anchoring requires layout info not available at draw time, anchor to the rail-side edge center and report — don't invent trigger-position plumbing.
Verification
- Mechanical:
cargo test -p makepad-example-koboyo --lib --test integration(29+14 + new),MAKEPAD=headless … --test ui(8 + new; ~183 s budget — keep new UI tests ≤ 2). - Feel check: open the drawer — it slides from its edge, search is focused, typing works immediately; open a rail popover — it grows out of the rail, not from screen center; press Fit — the canvas glides to frame the content and a mid-glide drag grabs it instantly.
- Done when:
grep -n "set_visible" koboyo/src/app.rsshows no drawer/popover pops (toast handled by plan 002), and the matrix is green.