From c41ce2055c0606f1e19e65d32b25824e5111a72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 1 Oct 2026 12:51:23 -0400 Subject: [PATCH] =?UTF-8?q?T48g=20Docs=20=C2=B7=20Motion:=20durations=20an?= =?UTF-8?q?d=20easings,=20reduced=20motion,=20components=20that=20move;=20?= =?UTF-8?q?the=20motion=20example=20compiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 4 ++ examples/docs/motion.rs | 122 ++++++++++++++++++++++++++++++++++++ frontend/src/docs/guides.ts | 2 +- frontend/src/docs/motion.md | 47 ++++++++++++++ 4 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 examples/docs/motion.rs create mode 100644 frontend/src/docs/motion.md diff --git a/Cargo.toml b/Cargo.toml index 031a2ea..ae92a1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,3 +103,7 @@ path = "examples/docs/theme.rs" [[example]] name = "docs_focus" path = "examples/docs/focus.rs" + +[[example]] +name = "docs_motion" +path = "examples/docs/motion.rs" diff --git a/examples/docs/motion.rs b/examples/docs/motion.rs new file mode 100644 index 0000000..ccfb125 --- /dev/null +++ b/examples/docs/motion.rs @@ -0,0 +1,122 @@ +use ely_gpui_component::{ + Assets, + buttons::Button, + motion::{self, Entrance, Flash, Spinner, Transition}, + primitives::FocusScope, + theme::{ActiveTheme, Radius, Theme}, +}; +use gpui::{ + Animation, AnimationExt, App, AppContext, Context, FocusHandle, IntoElement, ParentElement, + Render, Styled, Window, WindowOptions, div, +}; + +struct Moves { + focus: FocusHandle, + shown: bool, + count: usize, +} + +impl Render for Moves { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = cx.theme(); + let still = theme.reduced_motion; + let (toggle, add) = (cx.entity(), cx.entity()); + let dot = div() + .size_4() + .rounded(theme.radius(Radius::Xl)) + .bg(theme.colors.accent); + let dot = if still { + dot.into_any_element() + } else { + dot.with_animation( + "breath", + Animation::new(motion::duration(motion::SLOW * 4, cx)) + .repeat() + .with_easing(motion::ease_in_out_cubic), + |dot, t| dot.opacity(0.4 + 0.6 * (1.0 - (2.0 * t - 1.0).abs())), + ) + .into_any_element() + }; + FocusScope::new(&self.focus) + .root() + .size_full() + .flex() + .flex_col() + .gap_4() + .p_8() + .bg(theme.colors.bg) + .text_color(theme.colors.fg) + .child( + div() + .flex() + .gap_2() + .child( + Button::new("show", if self.shown { "Hide" } else { "Show" }).on_click( + move |_, _, cx| { + toggle.update(cx, |moves, cx| { + moves.shown = !moves.shown; + cx.notify(); + }) + }, + ), + ) + .child(Button::new("add", "Add one").on_click(move |_, _, cx| { + add.update(cx, |moves, cx| { + moves.count += 1; + cx.notify(); + }) + })) + .child( + Button::new( + "still", + if still { + "Allow motion" + } else { + "Reduce motion" + }, + ) + .on_click(move |_, _, cx| { + Theme::update(cx, |theme| theme.reduced_motion = !still) + }), + ), + ) + .child( + Transition::new("panel", self.shown) + .entrance(Entrance::Rise) + .child("A panel that rises in and fades out."), + ) + .child( + Flash::new("count", self.count) + .p_2() + .child(format!("Count: {}", self.count)), + ) + .child( + div() + .flex() + .gap_4() + .child(Spinner::new("spinner")) + .child(dot), + ) + } +} + +fn main() { + gpui_platform::application() + .with_assets(Assets) + .run(|cx: &mut App| { + ely_gpui_component::init(cx); + cx.open_window(WindowOptions::default(), |window, cx| { + cx.new(|cx| { + let focus = cx.focus_handle(); + window.focus(&focus, cx); + Moves { + focus, + shown: true, + count: 0, + } + }) + }) + .expect("window failed to open"); + cx.activate(true); + }); +} diff --git a/frontend/src/docs/guides.ts b/frontend/src/docs/guides.ts index 49108ef..e53b539 100644 --- a/frontend/src/docs/guides.ts +++ b/frontend/src/docs/guides.ts @@ -6,7 +6,7 @@ import { Marked, type Tokens } from "marked"; const at = (relative: string) => fileURLToPath(new URL(relative, import.meta.url)); /** The guides in reading order, by file name. */ -const ORDER = ["introduction", "installation", "getting-started", "theme", "focus"]; +const ORDER = ["introduction", "installation", "getting-started", "theme", "focus", "motion"]; export interface Guide { /** The path segment; the introduction has none. */ diff --git a/frontend/src/docs/motion.md b/frontend/src/docs/motion.md new file mode 100644 index 0000000..dff019d --- /dev/null +++ b/frontend/src/docs/motion.md @@ -0,0 +1,47 @@ +# Motion + +Ely's motion lives in `ely_gpui_component::motion`: four durations, a few easings, and components that move. Every one honors the theme's reduced motion. + +## Durations and easings + +| Item | Value | +| --- | --- | +| `FAST`, `BASE`, `SLOW` | 120, 200 and 320 ms. | +| `THEME` | 280 ms, the theme's cross-fade. | +| `NUDGE` | 4px, the travel of a small entrance. | +| `duration(base, cx)` | `base`, or 1 ms under reduced motion. Pass every duration through it. | +| `ease_out_cubic`, `ease_in_out_cubic`, `lerp` | Plain functions of `t` from 0 to 1. | +| `spring` | A damped spring. It overshoots past 1 before it settles. | + +GPUI hands an easing's value to the animator unclamped. Use `spring` inside the animator, on what may overshoot, such as a thumb's travel; its colors keep a plain easing. + +## Reduced motion + +The theme's `reduced_motion` field starts off. Ely does not read the system's setting; the app sets it: + +```rust +Theme::update(cx, |theme| theme.reduced_motion = true); +``` + +Under it, `duration` returns 1 ms, so a change lands at once. A motion that repeats, such as a spinner or a breathing dot, holds still instead: at 1 ms a turn it would flicker. Ely's spinner checks the flag itself; an app's own repeating animation checks it too, as the example's dot does. + +## Components that move + +| Component | Moves | +| --- | --- | +| `Transition::new(id, shown)` | Its children in and out as `shown` turns: `Entrance::Fade`, `Rise` or `Slide`. | +| `Stagger` | Rows in one after another. | +| `Flash::new(id, key)` | A tint each time `key` changes, fading out. | +| `Shake::new(id, key)` | A shake each time `key` changes; still under reduced motion. | +| `Spinner`, `ProgressBar`, `ProgressRing` | Progress, held still under reduced motion where they repeat. | +| `Skeleton`, `Shimmer` | Placeholders while content loads. | +| `Flip`, `AnimatePresence`, `Reorder` | Keyed rows that glide to new places, enter, leave, or drag. | + +`Flash` and `Shake` read the time since `key` changed; they do not restart an animation, so what they hold keeps its focus and state. + +## A window that moves + +The example shows a panel that rises in, a count that flashes, a spinner, and a breathing dot, with a button that turns reduced motion on and off. + +```rust example=motion +```