Motion in gpui is not CSS with a different syntax. gpui 0.2.x has no fluent transform on div — no .scale(), no .translate(), no .rotate(), no transform property to interpolate, no spring on layout, no AnimatePresence, no compositor thread to offload to. Every "lift / slide / zoom / spring" you internalized from Framer Motion and View Transitions must be re-derived from the four primitives gpui actually ships: opacity, BoxShadow, hover/color interpolation, and animated layout fractions driven by the delta closure inside with_animation. A .repeat() animation is a standing invitation to re-render the whole window forever, and every re-render walks the entire element tree top-to-bottom — so the frame budget here is an architecture concern, not a styling concern. This skill is the field manual for doing that right in pd-console (native gpui on Metal). It is the gpui counterpart to the web-focused animation-system-architect.
Activation Triggers
Activate on:with_animation, Animation::new, gpui easing (ease_in_out, bounce, pulsating_between, linear), BoxShadow/glow, breathing/pulsing dots, pane expand / list reflow / tab-slide / fullscreen-zoom transitions, "lift/slide/zoom/spring in gpui", custom paint/canvas(), PathBuilder, Vello/wgpu surfaces, "animation never stops re-rendering", reduced-motion in a native Rust app, two owners fighting one element.
NOT for: web/React/Framer/GSAP/View-Transitions motion → animation-system-architect | terminal/CLI/TUI output → beautiful-cli-design | general GUI layout, color, theming, typography (the non-motion design system) → beautiful-gui-design.
Quick Start
Classify the surface — a micro-interaction (glow, breathing dot, hover lift), a transition (a surface changes shape or identity), or bespoke graphics (the element tree genuinely can't express it). Different primitives, different owners.
Gate on reduced-motion and frame budget FIRST. There is no @media fallback and no compositor thread. Decide the static/instant path before the animated one.
Animate compositor-friendly properties only — opacity, BoxShadow/glow, color; never width/height/px/insets in a hot render. Layout animation costs a full tree walk per frame.
One motion owner per surface. A transition state machine lives in the view and is retargetable mid-flight. Never let two with_animations or a repeat() and a state machine drive the same element.
Audit the rendered frame, not the snippet. Build it, run it, watch for runaway re-renders (.repeat()), label overlap, and motion that outlives its job.
Drop here only when div().bg().shadow().child() truly can't express it (dither, shimmer, particles, beziers).
Heaviest surfaces
Vello / wgpu surface (ADR-0086)
Custom-render path for scanlines/playheads/particle fields; highest cost, last resort.
Decision Points
flowchart TD
A[Need motion on a gpui surface] --> B{Reduced motion requested?}
B -->|Yes| C[Resolve to final state instantly; keep orientation cue, drop travel]
B -->|No| D{Does the surface change SHAPE or IDENTITY?}
D -->|"No — atom: glow, lift, breathing, hover"| E{Loops forever?}
E -->|"Yes — life/pulse"| F["Animation::new(..).repeat()<br/>pulsating_between — ONE owner, budgeted"]
E -->|"No — one-shot ack"| G["with_animation: map delta→opacity/BoxShadow"]
D -->|"Yes — pane expand / list reflow / tab slide / fullscreen zoom"| H{Can the element tree express it?<br/>opacity + shadow + layout-fraction}
H -->|Yes| I["Interruptible transition state machine in the view<br/>retarget mid-flight, one owner"]
H -->|"No — dither / shimmer / particles / beziers / scanlines"| J{How heavy?}
J -->|"Path / quad / shader-ish in one frame"| K["gpui paint API: canvas(), paint_quad, PathBuilder"]
J -->|"Continuous custom-rendered surface"| L["Vello / wgpu surface (ADR-0086) — last resort"]
Expert cue: if you reach for "I just need transform: scale()", stop — gpui has no such thing. Re-derive the intent ("emphasize this pane") as opacity + BoxShadow growth + an animated layout fraction. The transform vocabulary does not port; the intent does.
Core Rules
No-transform constraint is law. There is no scale/translate/rotate to interpolate. Compose every "lift/slide/zoom/spring" from opacity, BoxShadow, color, and the delta closure. (references/02-the-no-transform-constraint.md is the full translation table.)
Compositor-friendly only in a hot render. Animate opacity, BoxShadow, and color. Animating width/height/insets/px forces a full element-tree re-layout every frame. If you must animate layout, animate a fraction with eyes open, never as a convenience default.
One owner per surface. Exactly one thing drives a given element's motion — one with_animation, or one transition state machine. Two owners fighting one element is the native equivalent of the web ownership conflict and produces irreproducible snapping.
Reduced-motion-first, and it must preserve orientation. Decide the instant/static path before the animated one. Reducing motion means less travel, not deleted feedback — keep the orientation cue (final position, a fade, a state hint), drop only the journey.
Frame budget is architecture, not polish. A .repeat() animation re-renders the window forever; every re-render walks the whole tree. Budget loops deliberately, scope them to the smallest view, and never leave one running for a surface that's off-screen or idle.
Transitions are interruptible state machines that live in the view. A surface that changes shape/identity gets a retargetable state, not a fire-and-forget keyframe. Mid-flight interruptions retarget the current value; they don't restart from zero.
Drop below the widget layer only when the tree can't express it.paint/canvas/Vello are escape hatches with real cost. Earn them; don't default to them for things div().shadow() already does.
Failure Modes
Layout animation in a hot render
Symptom: A pane "expands" by animating width/height (or px(...) insets) and the whole window stutters as more content loads.
Detection:delta closure writes to size/inset fields inside a frequently-rendered view; frame time climbs with tree depth, not with the animated element.
Fix: Animate opacity + BoxShadow growth for the "lift", and if shape must change, animate a single layout fraction at the smallest enclosing node — not raw pixels on every child.
repeat() that never stops re-rendering
Symptom: CPU/GPU stays hot on an idle screen; the window re-renders continuously even with no input.
Detection: An Animation::new(..).repeat() (e.g. a breathing dot) mounted high in the tree or left running while its view is off-screen/inactive.
Fix: Scope the loop to the smallest leaf view that needs it; pause/unmount it when the surface is idle or hidden; never stack two loops on one element.
Reduced-motion deletes orientation
Symptom: With reduced motion on, a transition just snaps and the user loses track of what moved where (which pane expanded, which tab is now active).
Detection: The reduced-motion branch sets duration to 0 / removes the animation entirely with no replacement cue.
Fix: Replace travel with a minimal fade or an explicit final-state hint. Reduced motion = smaller motion that still communicates, not silence.
Two owners fighting one element
Symptom: Flicker, value snapping, stale shadow/opacity, "impossible" bugs only after an interrupted interaction.
Detection: Both a with_animation and a state-machine field (or two with_animations) write the same property on the same element.
Fix: Make one owner authoritative. Let the transition state machine own the value and have everything else read it; remove the competing with_animation.
Reaching below the widget layer too early
Symptom: A hand-rolled paint/Vello surface re-implements a glow or a rounded card that BoxShadow + div already does, now untestable and off the layout grid.
Detection: Custom paint code whose output is expressible as div().bg().shadow().rounded().
Fix: Return to the element tree. Reserve paint/Vello for genuinely inexpressible work (dither, shimmer sweep, particles, anti-aliased causal-thread beziers, scanlines).
Easing that doesn't exist
Symptom: Build fails or motion falls back to linear because you reached for a web curve name.
Detection:ease_out (not exported in 0.2.x) or other CSS-named curves in with_easing(..).
Fix: Use confirmed gpui curves: ease_in_out, bounce, pulsating_between, linear. For deceleration, ease_in_out reads like a heavy door settling on a good hinge — that's the "buttery" target.
Worked Example — pd-console spawn → fullscreen → diff, done right
Goal. An operator spawns an agent; the spawn card expands to advanced view, the fleet renders as a list, the selected pane zooms to fullscreen, and the diff slides in as a tab. Buttery, interruptible, reduced-motion-safe — with zero transform usage.
One owner. A single TransitionState field on the view drives the whole flow. It holds the current phase, a target phase, and a 0.0→1.0 progress value. Every frame reads from it; nothing else animates these elements.
// In the view struct — the single motion owner for this surface.
enum Phase { Spawn, Advanced, Fleet, Fullscreen { pane: PaneId }, Diff }
struct TransitionState {
from: Phase,
to: Phase,
progress: f32, // 0.0..=1.0, driven by with_animation's delta
}
spawn → advanced (expand). Do not animate the card's width/height. Drive one with_animation whose delta maps onto: rising opacity for the newly-revealed controls, a growing BoxShadow (blur + spread) to read as "lift", and a single layout fraction at the card's container if reflow is unavoidable. Easing: ease_in_out.
advanced → fleet (list reflow). The fleet appears as a list. Stagger entries by feeding each row a phase-offset into the same delta (row i starts at i * 0.04), animating opacity only. No per-row layout animation — the list lays out once; rows fade in over the settled layout.
fleet → fullscreen (zoom). "Zoom" is faked: the chosen pane's container animates a layout fraction from its grid cell toward full bounds while siblings animate opacity down. The apparent scale comes from the fraction + shadow, never scale().
fullscreen → diff (tab-slide). The diff tab slides in by animating a single horizontal layout fraction (offset as a fraction of width, computed once), with opacity rising. One axis, one owner.
Interruption. If the operator clicks away mid-zoom, set to to the new phase and retargetprogress from its current value — never restart at 0. Because one state owns it, the in-flight value is the new animation's start.
Reduced motion. When reduced motion is on, the same state machine resolves progress to 1.0 immediately and skips the loops — but it still applies the final opacity/shadow/fraction so the operator sees which pane expanded and which tab is active. Orientation preserved; travel dropped.
Why it's correct: one owner, interruptible, compositor-friendly (opacity + shadow + a single fraction), reduced-motion keeps orientation, and not one byte of transform. See references/03-transition-architecture.md for the full state-machine scaffold and references/01-gpui-animation-primitives.md for the with_animation signatures.
Quality Gates
Zero scale/translate/rotate/transform usage — all "lift/slide/zoom/spring" composed from opacity, BoxShadow, color, layout-fraction.
No width/height/px/inset animation inside a hot render; any layout animation is a deliberate single fraction at the smallest node.
Exactly one motion owner per surface (one with_animation or one transition state machine); no element written by two animators.
Reduced-motion path resolves to final state and preserves orientation (fade/hint), never a bare snap or deleted feedback.
Every .repeat() loop is scoped to its smallest leaf view and paused/unmounted when idle or off-screen.
Transitions are interruptible: mid-flight changes retarget the current value rather than restarting at 0.
Only confirmed gpui easings used (ease_in_out, bounce, pulsating_between, linear); no ease_out or CSS curve names.
paint/canvas/Vello/wgpu used only for work the element tree genuinely can't express; nothing reimplements div().shadow().
Built and run, not just read: confirmed no runaway re-render on idle, no label overlap, motion ends the instant its job is done.
Fork Guidance
Fork to a subagent when the task is a repo-wide motion audit of a gpui app: inventory every with_animation/repeat(), flag layout-in-hot-render and multi-owner elements, and check reduced-motion coverage across views.
Keep inline when choosing a primitive for one surface, designing a single transition, or reviewing one view's motion.
Good subagent payload: target view/module, surface class (micro | transition | bespoke), the Phases involved, whether reduced motion must be honored, and the frame-budget expectation (idle must be 0 re-renders).
Reference Map
The six grounding docs in references/ — all keyed to real pd-console/src code (gpui 0.2.x, Metal):
references/01-gpui-animation-primitives.md — the four primitives, with_animation / Animation signatures, the delta closure, easing names. Read first.
references/02-the-no-transform-constraint.md — the translation table: how to re-derive lift/slide/zoom/spring without scale/translate/rotate. Read before composing any motion.
references/03-transition-architecture.md — one owner per surface; the interruptible, retargetable transition state machine for spawn → expand → list → zoom → tab-slide.
references/04-frame-budget-and-reduced-motion.md — why .repeat() re-renders forever, scoping loops, and the native reduced-motion discipline (no @media, no compositor thread).
references/05-bespoke-graphics-vello-wgpu.md — the three escape hatches below the widget layer: gpui paint/canvas, PathBuilder, and Vello/wgpu surfaces (ADR-0086).
references/06-motion-aesthetics-and-vocabulary.md — the "splendid, retro-futuristic, buttery" vocabulary: swoosh, glow, hard_offset, breathing dots, control flash, and curves that decelerate like a heavy door settling.
Skill Bundle Index
Every file in this skill, and when to open it. Auto-generated; run scripts/index_references.py --fix.
references/
references/01-gpui-animation-primitives.md — gpui Animation Primitives — > Scope. This is the field manual for motion in pd-console — a native gpui > 0.2.2 app (core/pd-console/Cargo.toml:32: `gpui = {
references/02-the-no-transform-constraint.md — 02 — The No-Fluent-Transform Constraint — > *gpui 0.2.x has no scale, translate, or rotate on elements.
references/03-transition-architecture.md — Transition Architecture — > One motion owner per surface.
references/05-bespoke-graphics-vello-wgpu.md — Bespoke Graphics in gpui — Custom Painting, Vello/wgpu Surfaces, and the Cassette-Futurism Layer — > Scope. When the element tree (div().bg().shadow().child()) genuinely cannot express what you need — ordered dithering, a shimmer swe
references/06-motion-aesthetics-and-vocabulary.md — Motion Aesthetics & Vocabulary — > "Splendid, retro-futuristic, buttery." Buttery is not a feeling you bolt on at the end — it is honest 60fps, motion that ends the instan
1---2name: rust-gpui-motion3description: Design motion, transitions, and bespoke graphics for native Rust gpui apps — the framework behind Zed and the pd-console operator console. Use for with_animation, easing curves, BoxShadow/glow, breathing dots, pane expand/zoom/slide transitions, and custom paint/Vello/wgpu surfaces. Trigger on: gpui animation, with_animation, gpui transition, gpui easing, BoxShadow, gpui shadow, pd-console motion, gpui paint/canvas, Vello, wgpu, "lift/slide/zoom/spring in gpui", reduced-motion in a native Rust UI, "animation re-renders forever", repeat() never stops. NOT for: web/React motion (→ animation-system-architect), CLI/TUI output (→ beautiful-cli-design), general non-motion GUI layout/color/typography (→ beautiful-gui-design).4license: Apache-2.05---67# Rust gpui Motion89Motion in gpui is not CSS with a different syntax. gpui **0.2.x has no fluent transform on `div`** — no `.scale()`, no `.translate()`, no `.rotate()`, no `transform` property to interpolate, no spring on layout, no `AnimatePresence`, no compositor thread to offload to. Every "lift / slide / zoom / spring" you internalized from Framer Motion and View Transitions must be **re-derived** from the four primitives gpui actually ships: `opacity`, `BoxShadow`, hover/color interpolation, and animated **layout fractions** driven by the `delta` closure inside `with_animation`. A `.repeat()` animation is a standing invitation to re-render the whole window forever, and every re-render walks the entire element tree top-to-bottom — so the frame budget here is an architecture concern, not a styling concern. This skill is the field manual for doing that right in `pd-console` (native gpui on Metal). It is the gpui counterpart to the web-focused `animation-system-architect`.1011## Activation Triggers1213**Activate on:** `with_animation`, `Animation::new`, gpui easing (`ease_in_out`, `bounce`, `pulsating_between`, `linear`), `BoxShadow`/glow, breathing/pulsing dots, pane expand / list reflow / tab-slide / fullscreen-zoom transitions, "lift/slide/zoom/spring in gpui", custom `paint`/`canvas()`, `PathBuilder`, Vello/wgpu surfaces, "animation never stops re-rendering", reduced-motion in a native Rust app, two owners fighting one element.1415**NOT for:** web/React/Framer/GSAP/View-Transitions motion → `animation-system-architect` | terminal/CLI/TUI output → `beautiful-cli-design` | general GUI layout, color, theming, typography (the non-motion design system) → `beautiful-gui-design`.1617## Quick Start18191. **Classify the surface** — a micro-interaction (glow, breathing dot, hover lift), a *transition* (a surface changes shape or identity), or *bespoke graphics* (the element tree genuinely can't express it). Different primitives, different owners.202. **Gate on reduced-motion and frame budget FIRST.** There is no `@media` fallback and no compositor thread. Decide the static/instant path before the animated one.213. **Animate compositor-friendly properties only** — `opacity`, `BoxShadow`/glow, color; never `width`/`height`/`px`/insets in a hot render. Layout animation costs a full tree walk per frame.224. **One motion owner per surface.** A transition state machine lives *in the view* and is retargetable mid-flight. Never let two `with_animation`s or a `repeat()` and a state machine drive the same element.235. **Audit the rendered frame, not the snippet.** Build it, run it, watch for runaway re-renders (`.repeat()`), label overlap, and motion that outlives its job.2425## Core Capabilities2627| Domain | gpui primitive | Expert cue |28|---|---|---|29| One-shot micro-motion | `with_animation(id, Animation::new(dur).with_easing(ease_in_out), delta_closure)` | The `delta` closure (0.0→1.0) is your only interpolator — map it onto opacity/shadow/layout-fraction by hand. |30| Looping life (breathing dot) | `Animation::new(..).repeat().with_easing(pulsating_between(a, b))` | A loop re-renders forever. Budget it; never stack two on one element. |31| Depth & emphasis | `BoxShadow { blur_radius, spread_radius, color }`, glow helpers | Shadow + opacity is the compositor-friendly substitute for `scale()`. This is how you fake "lift". |32| Shape/identity change | An interruptible **transition state machine** in the view | `spawn → expand → list → zoom → tab-slide`: retarget mid-flight, don't restart. |33| Below the widget layer | `canvas()`, `window.paint_quad(quad(..))`, `PathBuilder::fill()/stroke()`, `window.paint_path` | Drop here only when `div().bg().shadow().child()` truly can't express it (dither, shimmer, particles, beziers). |34| Heaviest surfaces | Vello / wgpu surface (ADR-0086) | Custom-render path for scanlines/playheads/particle fields; highest cost, last resort. |3536## Decision Points3738```mermaid39flowchart TD40 A[Need motion on a gpui surface] --> B{Reduced motion requested?}41 B -->|Yes| C[Resolve to final state instantly; keep orientation cue, drop travel]42 B -->|No| D{Does the surface change SHAPE or IDENTITY?}43 D -->|"No — atom: glow, lift, breathing, hover"| E{Loops forever?}44 E -->|"Yes — life/pulse"| F["Animation::new(..).repeat()<br/>pulsating_between — ONE owner, budgeted"]45 E -->|"No — one-shot ack"| G["with_animation: map delta→opacity/BoxShadow"]46 D -->|"Yes — pane expand / list reflow / tab slide / fullscreen zoom"| H{Can the element tree express it?<br/>opacity + shadow + layout-fraction}47 H -->|Yes| I["Interruptible transition state machine in the view<br/>retarget mid-flight, one owner"]48 H -->|"No — dither / shimmer / particles / beziers / scanlines"| J{How heavy?}49 J -->|"Path / quad / shader-ish in one frame"| K["gpui paint API: canvas(), paint_quad, PathBuilder"]50 J -->|"Continuous custom-rendered surface"| L["Vello / wgpu surface (ADR-0086) — last resort"]51```5253**Expert cue:** if you reach for "I just need `transform: scale()`", stop — gpui has no such thing. Re-derive the *intent* ("emphasize this pane") as opacity + `BoxShadow` growth + an animated layout fraction. The transform vocabulary does not port; the intent does.5455## Core Rules56571. **No-transform constraint is law.** There is no `scale/translate/rotate` to interpolate. Compose every "lift/slide/zoom/spring" from opacity, `BoxShadow`, color, and the `delta` closure. (`references/02-the-no-transform-constraint.md` is the full translation table.)582. **Compositor-friendly only in a hot render.** Animate `opacity`, `BoxShadow`, and color. Animating `width`/`height`/insets/`px` forces a full element-tree re-layout every frame. If you must animate layout, animate a **fraction** with eyes open, never as a convenience default.593. **One owner per surface.** Exactly one thing drives a given element's motion — one `with_animation`, or one transition state machine. Two owners fighting one element is the native equivalent of the web ownership conflict and produces irreproducible snapping.604. **Reduced-motion-first, and it must preserve orientation.** Decide the instant/static path before the animated one. Reducing motion means *less travel*, not *deleted feedback* — keep the orientation cue (final position, a fade, a state hint), drop only the journey.615. **Frame budget is architecture, not polish.** A `.repeat()` animation re-renders the window forever; every re-render walks the whole tree. Budget loops deliberately, scope them to the smallest view, and never leave one running for a surface that's off-screen or idle.626. **Transitions are interruptible state machines that live in the view.** A surface that changes shape/identity gets a retargetable state, not a fire-and-forget keyframe. Mid-flight interruptions retarget the current value; they don't restart from zero.637. **Drop below the widget layer only when the tree can't express it.** `paint`/`canvas`/Vello are escape hatches with real cost. Earn them; don't default to them for things `div().shadow()` already does.6465## Failure Modes6667### Layout animation in a hot render68- **Symptom:** A pane "expands" by animating `width`/`height` (or `px(...)` insets) and the whole window stutters as more content loads.69- **Detection:** `delta` closure writes to size/inset fields inside a frequently-rendered view; frame time climbs with tree depth, not with the animated element.70- **Fix:** Animate `opacity` + `BoxShadow` growth for the "lift", and if shape must change, animate a single **layout fraction** at the smallest enclosing node — not raw pixels on every child.7172### `repeat()` that never stops re-rendering73- **Symptom:** CPU/GPU stays hot on an idle screen; the window re-renders continuously even with no input.74- **Detection:** An `Animation::new(..).repeat()` (e.g. a breathing dot) mounted high in the tree or left running while its view is off-screen/inactive.75- **Fix:** Scope the loop to the smallest leaf view that needs it; pause/unmount it when the surface is idle or hidden; never stack two loops on one element.7677### Reduced-motion deletes orientation78- **Symptom:** With reduced motion on, a transition just *snaps* and the user loses track of what moved where (which pane expanded, which tab is now active).79- **Detection:** The reduced-motion branch sets duration to 0 / removes the animation entirely with no replacement cue.80- **Fix:** Replace travel with a minimal fade or an explicit final-state hint. Reduced motion = smaller motion that still communicates, not silence.8182### Two owners fighting one element83- **Symptom:** Flicker, value snapping, stale shadow/opacity, "impossible" bugs only after an interrupted interaction.84- **Detection:** Both a `with_animation` and a state-machine field (or two `with_animation`s) write the same property on the same element.85- **Fix:** Make one owner authoritative. Let the transition state machine own the value and have everything else read it; remove the competing `with_animation`.8687### Reaching below the widget layer too early88- **Symptom:** A hand-rolled `paint`/Vello surface re-implements a glow or a rounded card that `BoxShadow` + `div` already does, now untestable and off the layout grid.89- **Detection:** Custom paint code whose output is expressible as `div().bg().shadow().rounded()`.90- **Fix:** Return to the element tree. Reserve `paint`/Vello for genuinely inexpressible work (dither, shimmer sweep, particles, anti-aliased causal-thread beziers, scanlines).9192### Easing that doesn't exist93- **Symptom:** Build fails or motion falls back to linear because you reached for a web curve name.94- **Detection:** `ease_out` (not exported in 0.2.x) or other CSS-named curves in `with_easing(..)`.95- **Fix:** Use confirmed gpui curves: `ease_in_out`, `bounce`, `pulsating_between`, `linear`. For deceleration, `ease_in_out` reads like a heavy door settling on a good hinge — that's the "buttery" target.9697## Worked Example — pd-console `spawn → fullscreen → diff`, done right9899**Goal.** An operator spawns an agent; the spawn card *expands* to advanced view, the fleet renders as a list, the selected pane *zooms* to fullscreen, and the diff *slides* in as a tab. Buttery, interruptible, reduced-motion-safe — with **zero** transform usage.100101**One owner.** A single `TransitionState` field on the view drives the whole flow. It holds the current phase, a target phase, and a `0.0→1.0` progress value. Every frame reads from it; nothing else animates these elements.102103```rust104// In the view struct — the single motion owner for this surface.105enum Phase { Spawn, Advanced, Fleet, Fullscreen { pane: PaneId }, Diff }106107struct TransitionState {108 from: Phase,109 to: Phase,110 progress: f32, // 0.0..=1.0, driven by with_animation's delta111}112```1131141. **spawn → advanced (expand).** Do **not** animate the card's `width`/`height`. Drive one `with_animation` whose `delta` maps onto: rising `opacity` for the newly-revealed controls, a growing `BoxShadow` (blur + spread) to read as "lift", and a single **layout fraction** at the card's container if reflow is unavoidable. Easing: `ease_in_out`.1151162. **advanced → fleet (list reflow).** The fleet appears as a list. Stagger entries by feeding each row a phase-offset into the same `delta` (row *i* starts at `i * 0.04`), animating `opacity` only. No per-row layout animation — the list lays out once; rows fade in over the settled layout.1171183. **fleet → fullscreen (zoom).** "Zoom" is faked: the chosen pane's container animates a layout fraction from its grid cell toward full bounds while siblings animate `opacity` down. The *apparent* scale comes from the fraction + shadow, never `scale()`.1191204. **fullscreen → diff (tab-slide).** The diff tab slides in by animating a single horizontal layout fraction (offset as a fraction of width, computed once), with `opacity` rising. One axis, one owner.1211225. **Interruption.** If the operator clicks away mid-zoom, set `to` to the new phase and **retarget** `progress` from its current value — never restart at 0. Because one state owns it, the in-flight value is the new animation's start.1231246. **Reduced motion.** When reduced motion is on, the same state machine resolves `progress` to `1.0` immediately and skips the loops — but it still applies the **final** opacity/shadow/fraction so the operator sees *which* pane expanded and *which* tab is active. Orientation preserved; travel dropped.125126**Why it's correct:** one owner, interruptible, compositor-friendly (opacity + shadow + a single fraction), reduced-motion keeps orientation, and not one byte of transform. See `references/03-transition-architecture.md` for the full state-machine scaffold and `references/01-gpui-animation-primitives.md` for the `with_animation` signatures.127128## Quality Gates129130- [ ] Zero `scale`/`translate`/`rotate`/`transform` usage — all "lift/slide/zoom/spring" composed from opacity, `BoxShadow`, color, layout-fraction.131- [ ] No `width`/`height`/`px`/inset animation inside a hot render; any layout animation is a deliberate single fraction at the smallest node.132- [ ] Exactly one motion owner per surface (one `with_animation` or one transition state machine); no element written by two animators.133- [ ] Reduced-motion path resolves to final state **and** preserves orientation (fade/hint), never a bare snap or deleted feedback.134- [ ] Every `.repeat()` loop is scoped to its smallest leaf view and paused/unmounted when idle or off-screen.135- [ ] Transitions are interruptible: mid-flight changes retarget the current value rather than restarting at 0.136- [ ] Only confirmed gpui easings used (`ease_in_out`, `bounce`, `pulsating_between`, `linear`); no `ease_out` or CSS curve names.137- [ ] `paint`/`canvas`/Vello/wgpu used only for work the element tree genuinely can't express; nothing reimplements `div().shadow()`.138- [ ] Built and **run**, not just read: confirmed no runaway re-render on idle, no label overlap, motion ends the instant its job is done.139140## Fork Guidance141142- **Fork to a subagent** when the task is a repo-wide motion audit of a gpui app: inventory every `with_animation`/`repeat()`, flag layout-in-hot-render and multi-owner elements, and check reduced-motion coverage across views.143- **Keep inline** when choosing a primitive for one surface, designing a single transition, or reviewing one view's motion.144- **Good subagent payload:** target view/module, surface class (micro | transition | bespoke), the `Phase`s involved, whether reduced motion must be honored, and the frame-budget expectation (idle must be 0 re-renders).145146## Reference Map147148The six grounding docs in `references/` — all keyed to real `pd-console/src` code (gpui 0.2.x, Metal):149150- `references/01-gpui-animation-primitives.md` — the four primitives, `with_animation` / `Animation` signatures, the `delta` closure, easing names. Read first.151- `references/02-the-no-transform-constraint.md` — the translation table: how to re-derive lift/slide/zoom/spring without `scale/translate/rotate`. Read before composing any motion.152- `references/03-transition-architecture.md` — one owner per surface; the interruptible, retargetable transition state machine for `spawn → expand → list → zoom → tab-slide`.153- `references/04-frame-budget-and-reduced-motion.md` — why `.repeat()` re-renders forever, scoping loops, and the native reduced-motion discipline (no `@media`, no compositor thread).154- `references/05-bespoke-graphics-vello-wgpu.md` — the three escape hatches below the widget layer: gpui `paint`/`canvas`, `PathBuilder`, and Vello/wgpu surfaces (ADR-0086).155- `references/06-motion-aesthetics-and-vocabulary.md` — the "splendid, retro-futuristic, buttery" vocabulary: `swoosh`, `glow`, `hard_offset`, breathing dots, control flash, and curves that decelerate like a heavy door settling.156157<!-- BEGIN BUNDLE INDEX (auto: index_references.py) -->158159## Skill Bundle Index160161*Every file in this skill, and when to open it. Auto-generated; run `scripts/index_references.py --fix`.*162163**`references/`**164- [`references/01-gpui-animation-primitives.md`](references/01-gpui-animation-primitives.md) — gpui Animation Primitives — > **Scope.** This is the field manual for motion in `pd-console` — a native gpui > **0.2.2** app (`core/pd-console/Cargo.toml:32`: `gpui = {165- [`references/02-the-no-transform-constraint.md`](references/02-the-no-transform-constraint.md) — 02 — The No-Fluent-Transform Constraint — > *gpui 0.2.x has no `scale`, `translate`, or `rotate` on elements.166- [`references/03-transition-architecture.md`](references/03-transition-architecture.md) — Transition Architecture — > One motion **owner** per surface.167- [`references/04-frame-budget-and-reduced-motion.md`](references/04-frame-budget-and-reduced-motion.md) — Frame Budget & Reduced Motion in gpui — > Target file: `references/04-frame-budget-and-reduced-motion.md`.168- [`references/05-bespoke-graphics-vello-wgpu.md`](references/05-bespoke-graphics-vello-wgpu.md) — Bespoke Graphics in gpui — Custom Painting, Vello/wgpu Surfaces, and the Cassette-Futurism Layer — > **Scope.** When the element tree (`div().bg().shadow().child()`) genuinely cannot express what you need — ordered dithering, a shimmer swe169- [`references/06-motion-aesthetics-and-vocabulary.md`](references/06-motion-aesthetics-and-vocabulary.md) — Motion Aesthetics & Vocabulary — > *"Splendid, retro-futuristic, buttery."* Buttery is not a feeling you bolt on at the end — it is honest 60fps, motion that ends the instan170171<!-- END BUNDLE INDEX -->
Run npx skillmds@latest add curiositech/rust-gpui-motion in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Design motion, transitions, and bespoke graphics for native Rust gpui apps — the framework behind Zed and the pd-console operator console. Use for with_animation, easing curves, BoxShadow/glow, breathing dots, pane expand/zoom/slide transitions, and custom paint/Vello/wgpu surfaces. Trigger on: gpui animation, with_animation, gpui transition, gpui easing, BoxShadow, gpui shadow, pd-console motion, gpui paint/canvas, Vello, wgpu, "lift/slide/zoom/spring in gpui", reduced-motion in a native Rust UI, "animation re-renders forever", repeat() never stops. NOT for: web/React motion (→ animation-system-architect), CLI/TUI output (→ beautiful-cli-design), general non-motion GUI layout/color/typography (→ beautiful-gui-design). It is listed under Design & Media on SkillMD.
SkillMD's automated safety review verdict for this skill is PASS. Independent scanners report: SkillSpector: PASS, Skill Scanner: PASS. Capability flags: makes network calls. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free. This skill is licensed under Apache-2.
curiositech (@curiositech) published this skill. Their other Agent Skills are listed on their SkillMD profile.