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

+43 -9
View File
@@ -4,6 +4,7 @@ use gpui::{
};
use crate::{
motion::Spinner,
primitives::{FocusRing, Icon, IconName},
theme::{ActiveTheme, ControlSize, IconSize, Mix, Palette, Radius, TextSize},
typography::keys::{Platform, keystroke_labels},
@@ -119,6 +120,7 @@ pub struct Button {
variant: ButtonVariant,
size: ControlSize,
disabled: bool,
loading: bool,
full_width: bool,
pub(crate) slot: Option<Slot>,
on_click: Option<ClickHandler>,
@@ -135,6 +137,7 @@ impl Button {
variant: ButtonVariant::default(),
size: ControlSize::default(),
disabled: false,
loading: false,
full_width: false,
slot: None,
on_click: None,
@@ -178,6 +181,12 @@ impl Button {
self
}
/// Spins in place of its content, at the same width, and ignores presses. Tab still reaches it.
pub fn loading(mut self, loading: bool) -> Self {
self.loading = loading;
self
}
pub fn full_width(mut self) -> Self {
self.full_width = true;
self
@@ -201,6 +210,23 @@ impl RenderOnce for Button {
let icon = |name| Icon::new(name).size(icon_size).color(tone.fg);
let radius = theme.radius(Radius::Md);
let hint = self.shortcut.as_ref().map(shortcut_text);
let spinner = (self.id.clone(), "spinner");
let parts = div()
.flex()
.items_center()
.gap_1p5()
.when_some(self.icon, |el, name| el.child(icon(name)))
.when(!self.label.is_empty(), |el| el.child(self.label))
.when_some(self.trailing_icon, |el, name| el.child(icon(name)))
.when_some(hint, |el, hint| {
el.child(
div()
.pl_1()
.font_weight(FontWeight::NORMAL)
.text_color(tone.fg.opacity(0.55))
.child(hint),
)
});
div()
.id(self.id)
@@ -224,22 +250,30 @@ impl RenderOnce for Button {
.text_color(tone.fg)
.bg(tone.bg)
.when(self.full_width, |el| el.w_full())
.when_some(self.icon, |el, name| el.child(icon(name)))
.when(!self.label.is_empty(), |el| el.child(self.label))
.when_some(self.trailing_icon, |el, name| el.child(icon(name)))
.when_some(hint, |el, hint| {
el.child(
.map(|el| {
if !self.loading {
return el.child(parts);
}
el.relative().child(parts.invisible()).child(
div()
.pl_1()
.font_weight(FontWeight::NORMAL)
.text_color(tone.fg.opacity(0.55))
.child(hint),
.absolute()
.inset_0()
.flex()
.items_center()
.justify_center()
.child(Spinner::new(spinner).size(icon_size).color(tone.fg)),
)
})
.map(|el| {
if self.disabled {
return el.opacity(0.45).cursor_not_allowed();
}
if self.loading {
return el
.tab_index(0)
.focus_ring(cx)
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default());
}
el.cursor_pointer()
.tab_index(0)
.hover(|style| {
+3
View File
@@ -11,6 +11,9 @@ mod segmented;
mod share;
mod sheet;
#[cfg(all(test, feature = "test-support"))]
mod tests;
pub use bars::{ActionBar, BulkActionBar};
pub use button::{Button, ButtonVariant};
pub use confirm::{ConfirmButton, ConfirmMode};
+65
View File
@@ -0,0 +1,65 @@
use gpui::{
Context, FocusHandle, IntoElement, KeyBinding, KeyUpEvent, Keystroke, ParentElement, Render,
Styled, TestAppContext, VisualTestContext, Window,
};
use super::Button;
use crate::{
primitives::{FocusNext, FocusScope},
theme::Theme,
};
/// One button that counts its presses.
struct Bench {
root: FocusHandle,
loading: bool,
presses: usize,
}
impl Render for Bench {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
FocusScope::new(&self.root).root().size_full().child(
Button::new("save", "Save")
.loading(self.loading)
.on_click(move |_, _, cx| view.update(cx, |bench, _| bench.presses += 1)),
)
}
}
fn press(key: &str, cx: &mut VisualTestContext) {
cx.simulate_keystrokes(key);
cx.simulate_event(KeyUpEvent {
keystroke: Keystroke::parse(key).expect("a key"),
});
cx.run_until_parked();
}
#[gpui::test]
fn a_loading_button_ignores_presses_and_keeps_its_tab_stop(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::init(cx);
cx.bind_keys([KeyBinding::new("tab", FocusNext, None)]);
});
let (view, cx) = cx.add_window_view(|_, cx| Bench {
root: cx.focus_handle(),
loading: true,
presses: 0,
});
let root = view.read_with(cx, |bench, _| bench.root.clone());
cx.update(|window, _| window.focus(&root));
press("tab", cx);
press("enter", cx);
assert_eq!(view.read_with(cx, |bench, _| bench.presses), 0);
view.update(cx, |bench, cx| {
bench.loading = false;
cx.notify();
});
cx.run_until_parked();
press("enter", cx);
assert_eq!(
view.read_with(cx, |bench, _| bench.presses),
1,
"focus stayed on it"
);
}