Files
Ely-GPUI-Components/examples/gallery/pages/motion/loading.rs
T
ZacharyZhang-NY 4c3ed1bbb9 Port · Build: Zed's gpui at 1a28cff
gpui and gpui_platform now come from Zed's repo by git at 1a28cff,
the code gpui-pre 0.3.7 republishes, so the gallery can later build
for the browser. gpui takes stacker, as 0.2.2 always did, and
gpui_platform takes font-kit, without which macOS draws no text;
runtime_shaders moves to gpui_platform.

The API moved: focus, blur and Tab steps take cx, Corner is Anchor,
shaped lines paint with an alignment, defer_draw takes a content
mask, the inspector registers a factory, async updates return their
value, and wrapped lines are shared, not cloned. Updates through a
window context go through the window, so a closed window still ends
them as it did.

Where the new gpui behaves differently, Ely keeps its old intent.
Holds and reveals under a resting pointer survive typing through
gpui's input-modality-independent hover; previews the keyboard also
moves keep the default, and the calendar's leave ignores a key press.
Enter reaches a button inside the code editor, since gpui now arms a
key click on the press. The event logger skips a lone modifier. A
video and an image grid keep their shape in a flex column under
taffy 0.13, as a picture does.

AGENTS.md records the new stack and the rules this port changes; the
rest of the old gpui and taffy rules are re-tested in T44c.
2026-09-28 20:30:17 -04:00

239 lines
7.9 KiB
Rust

use std::time::Duration;
use ely_gpui_component::{
buttons::{Button, ButtonVariant},
motion::{
Shimmer, Skeleton, SkeletonAvatar, SkeletonCard, SkeletonTable, SkeletonText, Spinner,
SpinnerStyle,
},
navigation::LoadMore,
theme::{ActiveTheme, IconSize, Radius, TextSize},
typography::Caption,
};
use gpui::{
App, Entity, FontWeight, IntoElement, ParentElement, SharedString, Styled, Window, div,
};
use crate::{
probe::probe,
ui::{keep, row, section, set, specimen, specimens},
};
/// Sets `state` to `value` after `wait`, as a slow server would answer.
pub fn later<T: 'static>(state: &Entity<T>, value: T, wait: Duration, cx: &mut App) {
let state = state.clone();
cx.spawn(async move |cx| {
cx.background_executor().timer(wait).await;
cx.update(|cx| set(&state, value, cx));
})
.detach();
}
pub fn spinners(cx: &mut App) -> impl IntoElement + use<> {
const STYLES: [(SpinnerStyle, &str); 6] = [
(SpinnerStyle::Ring, "ring"),
(SpinnerStyle::Dots, "dots"),
(SpinnerStyle::Bars, "bars"),
(SpinnerStyle::Pulse, "pulse"),
(SpinnerStyle::Orbit, "orbit"),
(SpinnerStyle::Wave, "wave"),
];
const SIZES: [(IconSize, &str); 5] = [
(IconSize::Xs, "xs"),
(IconSize::Sm, "sm"),
(IconSize::Md, "md"),
(IconSize::Lg, "lg"),
(IconSize::Xl, "xl"),
];
section(
"Spinner / DotsLoader / BarsLoader / PulseLoader / RingLoader / OrbitLoader / WaveLoader / InlineLoader",
"Six small motions for waiting, one component. Under reduced motion each rests on one frame.",
cx,
)
.child(specimens().children(STYLES.map(|(style, name)| {
specimen(
name,
Spinner::new(SharedString::from(format!("spinner-{name}"))).style(style).size(IconSize::Xl),
cx,
)
})))
.child(specimens().children(SIZES.map(|(size, name)| {
specimen(name, Spinner::new(SharedString::from(format!("spinner-size-{name}"))).size(size), cx)
})))
}
pub fn skeletons(cx: &mut App) -> impl IntoElement + use<> {
section(
"Skeleton / SkeletonText / SkeletonAvatar / SkeletonCard / SkeletonTable",
"Placeholders in the shape of what is coming. They breathe together, slowly.",
cx,
)
.child(
div()
.flex()
.items_start()
.gap_6()
.child(div().w_80().child(SkeletonCard::new("skeleton-card")))
.child(
div()
.flex_1()
.flex()
.flex_col()
.gap_6()
.child(
div()
.flex()
.items_center()
.gap_3()
.child(SkeletonAvatar::new("skeleton-avatar"))
.child(div().flex_1().child(SkeletonText::new("skeleton-text", 3))),
)
.child(SkeletonTable::new("skeleton-table", 3, 4))
.child(Skeleton::new("skeleton-block").w_full().h_16()),
),
)
}
pub fn shimmer(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let loaded = keep("shimmer-loaded", || false, window, cx);
let (now, toggle) = (*loaded.read(cx), loaded.clone());
let theme = cx.theme();
let card = if now {
div()
.w_80()
.flex()
.flex_col()
.gap_3()
.p_4()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border)
.bg(theme.colors.surface)
.child(
div()
.font_weight(FontWeight::SEMIBOLD)
.child("Quarterly review"),
)
.child(
div()
.text_size(theme.text_size(TextSize::Sm))
.text_color(theme.colors.fg_muted)
.child("Revenue rose 12% on the quarter. Churn held at 2.1%; three new markets opened."),
)
.into_any_element()
} else {
Shimmer::new("shimmer")
.w_80()
.rounded(theme.radius(Radius::Lg))
.child(SkeletonCard::new("shimmer-card"))
.into_any_element()
};
section(
"Shimmer",
"A soft band of light sweeps what is loading. Load swaps in the content.",
cx,
)
.child(card)
.child(
row().child(probe(
"shimmer-load",
Button::new("shimmer-load", if now { "Reload" } else { "Load" })
.variant(ButtonVariant::Ghost)
.on_click(move |_, _, cx| set(&toggle, !now, cx)),
)),
)
}
pub fn buttons(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let saving = keep("button-saving", || false, window, cx);
let exporting = keep("button-exporting", || false, window, cx);
let (save_now, export_now) = (*saving.read(cx), *exporting.read(cx));
section(
"LoadingButton",
"A button spins in place at the same width and ignores presses until the work is done.",
cx,
)
.child(
row()
.child(probe(
"save",
Button::new("save", "Save changes")
.primary()
.loading(save_now)
.on_click(move |_, _, cx| {
set(&saving, true, cx);
later(&saving, false, Duration::from_millis(1600), cx);
}),
))
.child(
Button::new("export", "Export")
.loading(export_now)
.on_click(move |_, _, cx| {
set(&exporting, true, cx);
later(&exporting, false, Duration::from_millis(2400), cx);
}),
),
)
}
pub fn more(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
const PEOPLE: [&str; 18] = [
"Ada Lovelace",
"Alan Turing",
"Grace Hopper",
"Katherine Johnson",
"Tim Berners-Lee",
"Margaret Hamilton",
"Donald Knuth",
"Barbara Liskov",
"Ken Thompson",
"Radia Perlman",
"Edsger Dijkstra",
"Frances Allen",
"Dennis Ritchie",
"Hedy Lamarr",
"John McCarthy",
"Sophie Wilson",
"Yukihiro Matsumoto",
"Fei-Fei Li",
];
let shown = keep("more-shown", || 5usize, window, cx);
let busy = keep("more-busy", || false, window, cx);
let (count, loading) = (*shown.read(cx), *busy.read(cx));
let theme = cx.theme();
section(
"LoadMore",
"A list that loads in pages ends in one button. It spins while the next page comes, and counts what shows.",
cx,
)
.child(
div()
.w_96()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border)
.bg(theme.colors.surface)
.children(PEOPLE[..count].iter().map(|name| {
div()
.px_4()
.py_2()
.border_b_1()
.border_color(theme.colors.border)
.child(*name)
}))
.child(probe(
"load-more",
div().w_96().child(
LoadMore::new("more", loading)
.shown(count, PEOPLE.len())
.on_load(move |_, cx| {
set(&busy, true, cx);
later(&busy, false, Duration::from_millis(900), cx);
later(&shown, (count + 5).min(PEOPLE.len()), Duration::from_millis(900), cx);
}),
),
)),
)
.child(Caption::new("Five at a time, eighteen in all."))
}