T48g Docs · Motion: durations and easings, reduced motion, components that move; the motion example compiles
This commit is contained in:
4 files changed
+174
-1
No files matched your search
@@ -103,3 +103,7 @@ path = "examples/docs/theme.rs"
|
|||||||
[[example]]
|
[[example]]
|
||||||
name = "docs_focus"
|
name = "docs_focus"
|
||||||
path = "examples/docs/focus.rs"
|
path = "examples/docs/focus.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "docs_motion"
|
||||||
|
path = "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<Self>) -> 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { Marked, type Tokens } from "marked";
|
|||||||
const at = (relative: string) => fileURLToPath(new URL(relative, import.meta.url));
|
const at = (relative: string) => fileURLToPath(new URL(relative, import.meta.url));
|
||||||
|
|
||||||
/** The guides in reading order, by file name. */
|
/** 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 {
|
export interface Guide {
|
||||||
/** The path segment; the introduction has none. */
|
/** The path segment; the introduction has none. */
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
Reference in new issue
Block a user