T11a loading: spinner in six motions, skeletons that breathe, a shimmer cut to its corners, loading buttons and load more
This commit is contained in:
17 files changed
+1184
-28
No files matched your search
@@ -3,6 +3,7 @@ mod feedback;
|
||||
mod forms;
|
||||
mod layout;
|
||||
mod menus;
|
||||
mod motion;
|
||||
mod navigation;
|
||||
mod overlays;
|
||||
mod primitives;
|
||||
@@ -35,6 +36,7 @@ pub const ALL: &[Page] = &[
|
||||
menus::PAGE,
|
||||
overlays::PAGE,
|
||||
feedback::PAGE,
|
||||
motion::PAGE,
|
||||
theme::PAGE,
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
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.
|
||||
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."))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
mod loading;
|
||||
|
||||
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
|
||||
|
||||
use super::Page;
|
||||
use crate::script::Step;
|
||||
|
||||
pub const PAGE: Page = Page {
|
||||
number: 11,
|
||||
slug: "motion",
|
||||
title: "Loading & Motion",
|
||||
summary: "Waiting, shown honestly, and the motion everything else moves with.",
|
||||
render,
|
||||
script: SCRIPT,
|
||||
};
|
||||
|
||||
const SCRIPT: &[Step] = &[
|
||||
Step::Rest,
|
||||
Step::Click("save"),
|
||||
Step::Wait(250),
|
||||
Step::Shot("loading-button"),
|
||||
Step::Click("load-more"),
|
||||
Step::Wait(300),
|
||||
Step::Shot("load-more-busy"),
|
||||
Step::Wait(1200),
|
||||
Step::Shot("load-more-done"),
|
||||
Step::Click("shimmer-load"),
|
||||
Step::Wait(500),
|
||||
Step::Shot("shimmer-loaded"),
|
||||
Step::Click("shimmer-load"),
|
||||
Step::Rest,
|
||||
];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
div()
|
||||
.child(loading::spinners(cx))
|
||||
.child(loading::skeletons(cx))
|
||||
.child(loading::shimmer(window, cx))
|
||||
.child(loading::buttons(window, cx))
|
||||
.child(loading::more(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
Reference in new issue
Block a user