T11c motion: transitions, stagger, presence that folds, flip that glides, drag to reorder; motion keeps each row's identity

This commit is contained in:
ZacharyZhang-NY committed 2026-09-25 18:01:29 -04:00
1 parent ce31c7fb06
commit 8e7e7ccace
10 files changed
+1156 -10

No files matched your search

+16
View File
@@ -1,4 +1,5 @@
mod loading;
mod moves;
mod progress;
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
@@ -42,6 +43,16 @@ const SCRIPT: &[Step] = &[
Step::Click("uploads-tick"),
Step::Wait(400),
Step::Shot("uploads"),
Step::Click("transition-toggle"),
Step::Wait(80),
Step::Shot("transition-leaving"),
Step::Click("transition-toggle"),
Step::Click("presence-add"),
Step::Wait(80),
Step::Shot("presence-arriving"),
Step::Click("flip-shuffle"),
Step::Wait(100),
Step::Shot("flip-gliding"),
Step::Rest,
];
@@ -58,5 +69,10 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
.child(progress::suspense(window, cx))
.child(progress::refresh(window, cx))
.child(progress::uploads(window, cx))
.child(moves::transitions(window, cx))
.child(moves::stagger(window, cx))
.child(moves::presence(window, cx))
.child(moves::flip(window, cx))
.child(moves::reorder(window, cx))
.into_any_element()
}
+251
View File
@@ -0,0 +1,251 @@
use ely_gpui_component::{
buttons::{Button, ButtonVariant, IconButton},
motion::{AnimatePresence, Entrance, Flip, Reorder, Stagger, Transition},
primitives::{Icon, IconName},
theme::{ActiveTheme, ControlSize, IconSize, Radius},
typography::Caption,
};
use gpui::{App, Div, IntoElement, ParentElement, SharedString, Styled, Window, div};
use crate::{
probe::probe,
ui::{keep, row, section, set, specimen, specimens},
};
fn card(text: impl Into<SharedString>, cx: &App) -> Div {
let theme = cx.theme();
div()
.px_4()
.py_3()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border)
.bg(theme.colors.surface)
.child(text.into())
}
/// A row in a list demo: a hairline under it.
fn line(text: impl Into<SharedString>, cx: &App) -> Div {
let theme = cx.theme();
div()
.flex()
.items_center()
.gap_3()
.px_4()
.py_2()
.border_b_1()
.border_color(theme.colors.border)
.bg(theme.colors.surface)
.child(text.into())
}
pub fn transitions(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let shown = keep("transition-shown", || true, window, cx);
let (now, toggle) = (*shown.read(cx), shown.clone());
let entrances = [
(Entrance::Fade, "fade"),
(Entrance::Rise, "rise"),
(Entrance::Slide, "slide"),
];
section(
"Transition",
"Content that fades, rises or slides in, and leaves the way it came.",
cx,
)
.child(specimens().children(entrances.map(|(entrance, name)| {
specimen(
name,
div().w_48().h_12().child(
Transition::new(SharedString::from(format!("transition-{name}")), now)
.entrance(entrance)
.child(card("Hello", cx)),
),
cx,
)
})))
.child(
row().child(probe(
"transition-toggle",
Button::new("transition-toggle", if now { "Hide" } else { "Show" })
.variant(ButtonVariant::Ghost)
.on_click(move |_, _, cx| set(&toggle, !now, cx)),
)),
)
}
pub fn stagger(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let turn = keep("stagger-turn", || 0usize, window, cx);
let (now, replay) = (*turn.read(cx), turn.clone());
section(
"Stagger",
"Children arrive one after another, a few frames apart.",
cx,
)
.child(
div().w_96().child(
Stagger::new(SharedString::from(format!("stagger-{now}"))).children(
["Inbox", "Drafts", "Sent", "Archive", "Trash"].map(|name| line(name, cx)),
),
),
)
.child(
row().child(probe(
"stagger-replay",
Button::new("stagger-replay", "Replay")
.variant(ButtonVariant::Ghost)
.on_click(move |_, _, cx| set(&replay, now + 1, cx)),
)),
)
}
/// A tag in the presence demo: a stable number, its name, and whether it shows.
type Tag = (usize, &'static str, bool);
pub fn presence(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
const MORE: [&str; 4] = ["Typography", "Color", "Layout", "Sound"];
let tags = keep(
"presence-tags",
|| vec![(0, "Design", true), (1, "Motion", true), (2, "Rust", true)],
window,
cx,
);
let list: Vec<Tag> = tags.read(cx).clone();
let (add, gone) = (tags.clone(), tags.clone());
let rows = list.iter().fold(
AnimatePresence::new("presence").on_gone(move |key, _, cx| {
let left: Vec<Tag> = gone
.read(cx)
.iter()
.filter(|(number, _, _)| format!("tag-{number}") != key.as_ref())
.copied()
.collect();
set(&gone, left, cx)
}),
|rows, (number, name, shown)| {
let (hide, number) = (tags.clone(), *number);
rows.row(
SharedString::from(format!("tag-{number}")),
*shown,
line(*name, cx).justify_between().child(
IconButton::new(
SharedString::from(format!("presence-remove-{number}")),
IconName::X,
)
.variant(ButtonVariant::Ghost)
.size(ControlSize::Sm)
.on_click(move |_, _, cx| {
let then: Vec<Tag> = hide
.read(cx)
.iter()
.map(|tag| {
if tag.0 == number {
(tag.0, tag.1, false)
} else {
*tag
}
})
.collect();
set(&hide, then, cx)
}),
),
)
},
);
section(
"AnimatePresence",
"Rows fold open as they arrive and fold away as they go; the rest glide to close the gap.",
cx,
)
.child(div().w_96().child(rows))
.child(
row().child(probe(
"presence-add",
Button::new("presence-add", "Add a tag")
.variant(ButtonVariant::Ghost)
.on_click(move |_, _, cx| {
let mut then = add.read(cx).clone();
let number = then.iter().map(|tag| tag.0 + 1).max().unwrap_or(0);
then.push((number, MORE[number % MORE.len()], true));
set(&add, then, cx);
}),
)),
)
}
pub fn flip(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let order = keep(
"flip-order",
|| vec!["Aurora", "Basalt", "Cinder", "Dune", "Ember"],
window,
cx,
);
let (now, shuffle) = (order.read(cx).clone(), order.clone());
let list = now.iter().fold(Flip::new("flip"), |list, name| {
list.row(*name, line(*name, cx))
});
section(
"Flip",
"When the order changes, each row glides from where it was to where it goes.",
cx,
)
.child(div().w_96().child(list))
.child(
row().child(probe(
"flip-shuffle",
Button::new("flip-shuffle", "Shuffle")
.variant(ButtonVariant::Ghost)
.on_click(move |_, _, cx| {
let mut then = shuffle.read(cx).clone();
then.rotate_left(2);
then.swap(0, 3);
set(&shuffle, then, cx)
}),
)),
)
}
pub fn reorder(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let tasks = keep(
"reorder-tasks",
|| {
vec![
"Write the brief",
"Sketch the flows",
"Pick the type",
"Build the prototype",
"Test with five people",
]
},
window,
cx,
);
let (now, moved) = (tasks.read(cx).clone(), tasks.clone());
let muted = cx.theme().colors.fg_subtle;
let list = now.iter().fold(
Reorder::new("reorder").on_reorder(move |from, to, _, cx| {
let mut then = moved.read(cx).clone();
let task = then.remove(from);
then.insert(to, task);
set(&moved, then, cx)
}),
|list, task| {
list.row(
*task,
line(*task, cx).child(div().flex_1()).child(
Icon::new(IconName::GripVertical)
.size(IconSize::Sm)
.color(muted),
),
)
},
);
section(
"Reorder",
"Drag a row: it follows the pointer and the others glide aside to make room.",
cx,
)
.child(div().w_96().child(list))
.child(Caption::new(
"Motion is gpui's with_animation, timed by motion::duration and eased by the curves in motion. Spring is motion::spring. CountUp is typography::AnimatedNumber::count_up. A collapse is layout::Collapsible. A scale entrance is blocked: gpui 0.2.2 transforms only icons.",
))
}