makepad-ports/plans/004-koboyo-chrome.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

105 lines
5.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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