Files
Ely-GPUI-Components/examples/gallery/pages/motion/moves.rs
T
ZacharyZhang-NY 3f1597fa28 Port · Parity: every page matches gpui 0.2.2's; the rules re-tested
Every page and scripted state was shot with gpui 0.2.2 and with the
port, light and dark, full width and at 280px, judged pair by pair by
agents with skeptics, and every page's log diffed. What differed is
fixed, each with a test that fails without it where the test platform
can show it:

- The gallery's harness takes posted mouse moves (gpui_macos listens
  to a tracking area), posts each drag move twice (it no longer
  replays a held drag), and waits three frames before each shot.
- Rolling digits: a block column, so taffy 0.13 lends the cell no
  baseline, and a line of whole device pixels, since gpui snaps each
  length on its own. DescriptionList's row aligns ends.
- Margins that taffy 0.13 collapses through a plain block: the
  drawer's pull and snap, the stagger and transition rise, the dialog
  and palette rises, the fullscreen dialog's slide.
- An Input keeps room to type in a squeezed row; the field rows drop
  w_full, whose reason was taffy 0.9's.

AGENTS.md's rules that named gpui 0.2.2 or taffy 0.9 were re-tested
against both sources and rewritten, and new ones say what the port
changed. TASKS.md's tags follow: what the new gpui makes possible is
prove. Captions, doc comments and README say what holds now.
2026-09-29 10:39:55 -04:00

252 lines
8.2 KiB
Rust

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 transforms only svgs.",
))
}