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:
ZacharyZhang-NY committed 2026-09-25 16:58:05 -04:00
1 parent 866335912a
commit d4066661d5
17 files changed
+1184 -28

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ pub use history::BackForwardNavigation;
pub use menu::NavigationMenu;
pub use nav::{NavGroup, NavItem};
pub use overflow::TabOverflowMenu;
pub use pages::Pagination;
pub use pages::{LoadMore, Pagination};
pub use palette::{
Command, CommandPalette, QuickLauncher, QuickOpen, QuickSwitcher, SearchPalette,
};
+77 -2
View File
@@ -1,13 +1,16 @@
use std::rc::Rc;
use gpui::{
App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, Styled, Window, div,
App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, Styled, Window,
div, prelude::*,
};
use crate::{
buttons::{Button, ButtonVariant, IconButton},
forms::Run,
primitives::IconName,
theme::{ActiveTheme, ControlSize},
theme::{ActiveTheme, ControlSize, TextSize},
typography::format::{self, Separators},
};
/// The first, the last, `current` and `near` pages either side; `None` marks a gap of two or more.
@@ -164,3 +167,75 @@ mod tests {
assert_eq!(shown(3, 5), "1 2 3 4 5");
}
}
/// Ends a list that loads in pages: a button that spins while more come, and how many show. Once all show, only the count stays.
#[derive(IntoElement)]
pub struct LoadMore {
id: ElementId,
loading: bool,
shown: Option<(usize, usize)>,
on_load: Option<Run>,
}
impl LoadMore {
pub fn new(id: impl Into<ElementId>, loading: bool) -> Self {
Self {
id: id.into(),
loading,
shown: None,
on_load: None,
}
}
/// How many show, of how many there are.
pub fn shown(mut self, shown: usize, total: usize) -> Self {
assert!(shown <= total, "{shown} shown of only {total}");
self.shown = Some((shown, total));
self
}
pub fn on_load(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_load = Some(Rc::new(handler));
self
}
}
impl RenderOnce for LoadMore {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let count = |value: usize| format::number(value as f64, 0, Separators::EN);
let done = self.shown.is_some_and(|(shown, total)| shown == total);
let (id, on_load) = (self.id.clone(), self.on_load);
div()
.id(self.id.clone())
.flex()
.flex_col()
.items_center()
.gap_2()
.py_4()
.when(!done, |more| {
more.child(
Button::new((self.id.clone(), "button"), "Load more")
.loading(self.loading)
.on_click(move |_, window, cx| {
log::info!("load more {id:?}: asked");
if let Some(on_load) = &on_load {
on_load(window, cx);
}
}),
)
})
.when_some(self.shown, |more, (shown, total)| {
more.child(
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(theme.colors.fg_subtle)
.child(if done {
format!("All {} shown", count(total))
} else {
format!("Showing {} of {}", count(shown), count(total))
}),
)
})
}
}
+49 -2
View File
@@ -5,8 +5,8 @@ use gpui::{
};
use super::{
BackForwardNavigation, Breadcrumb, Crumb, EditorTab, EditorTabs, GoToLine, NavigationMenu,
Tabs, Wizard,
BackForwardNavigation, Breadcrumb, Crumb, EditorTab, EditorTabs, GoToLine, LoadMore,
NavigationMenu, Tabs, Wizard,
};
use crate::{
forms::{Choice, TextInput},
@@ -391,3 +391,50 @@ fn enter_jumps_to_a_line_and_column(cx: &mut TestAppContext) {
cx.simulate_keystrokes("enter");
assert_eq!(view.read_with(cx, |view, _| view.to), Some((12, Some(4))));
}
/// A list's end that loads more, counting what it asked.
struct More {
loading: bool,
shown: usize,
asks: usize,
}
impl Render for More {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
LoadMore::new("more", self.loading)
.shown(self.shown, 30)
.on_load(move |_, cx| view.update(cx, |more, _| more.asks += 1))
}
}
#[gpui::test]
fn load_more_asks_once_per_press_and_goes_once_all_show(cx: &mut TestAppContext) {
setup(cx);
let (view, cx) = cx.add_window_view(|_, _| More {
loading: false,
shown: 10,
asks: 0,
});
cx.update(|window, _| window.focus_next());
press("enter", cx);
assert_eq!(view.read_with(cx, |more, _| more.asks), 1);
view.update(cx, |more, cx| {
more.loading = true;
cx.notify();
});
press("enter", cx);
assert_eq!(view.read_with(cx, |more, _| more.asks), 1, "busy");
view.update(cx, |more, cx| {
more.loading = false;
more.shown = 30;
cx.notify();
});
cx.update(|window, _| window.focus_next());
press("enter", cx);
assert_eq!(
view.read_with(cx, |more, _| more.asks),
1,
"all shown, no button"
);
}