Files
Ely-GPUI-Components/examples/gallery/pages/motion/loading.rs
T

241 lines
8.0 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;
if let Err(error) = cx.update(|cx| set(&state, value, cx)) {
log::error!("gallery: the demo closed before its answer: {error:#}");
}
})
.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."))
}