# 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 ```rust // 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 } } ``` ```rust // 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); ``` ```rust // 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 1. **Drawer** (`drawer` at `app.rs:268`): slide + fade via a `slide` instance, enter `Forward {duration: 0.25}` `ease: mod.craft.ease_sheet` (`Bezier {cp0: 0.32, cp1: 0.72, cp2: 0.0, cp3: 1.0}`), exit `Forward {duration: 0.18}` `OutQuad`, same edge both directions. `set_key_focus` moves to *after* the enter starts (same frame is fine — after `animator_play`, not before `set_visible`). 2. **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, not `rect_size*0.5`). Exit 0.12s fade `OutQuad`. Never start from scale 0. 3. **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 by `NextFrame` in `canvas_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. Under `reduce_motion`: jump (current behavior). 4. 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_stationary` test style); the tween targets should be computed by the existing `fit_to_bbox`/`reset_zoom` into a *pending target* rather than mutating live camera state directly. - `canvas_view.rs` already 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 1. `model.rs`: add `CameraTween {from: Cam, to: Cam, start: f64}` + `pub fn camera_tween_eval(tw, t) -> Cam` (geometric zoom, content-space pan) + units: endpoint equality at `t≥1`, anchor stability for pure-zoom tweens, cancel semantics. 2. `canvas_view.rs`: route Fit/Reset through the tween; NextFrame step; cancel on any `Hit::FingerDown`/scroll; keep instant path under `reduce_motion`. 3. `app.rs`: drawer animator track + focus reorder; popover animator tracks with per-panel anchor; replace the five `set_visible` lines with `animator_play` + deferred hide (0.12–0.18s cleanup timer, as in the toast pattern). 4. 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.rs` shows no drawer/popover pops (toast handled by plan 002), and the matrix is green.