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
105 lines
5.4 KiB
Markdown
105 lines
5.4 KiB
Markdown
# 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.
|