gpui_cn/spinner.rs
1use gpui_kit::{
2 App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role,
3 SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled, TestSupportExt as _,
4 Window, base::StyledExt as _, div,
5};
6
7use crate::Progress;
8
9/// A shadcn-style spinner: an arc turning in place, for work whose length
10/// is not known. Use a circular [`Progress`] for a known percentage; this
11/// is that ring with no value.
12///
13/// The look is shadcn's `Spinner`: `size-4` (16px at the default font
14/// size), painted with the inherited text color, and turning once a
15/// second at a linear pace, Tailwind's `animate-spin`. `Styled` sizes and
16/// colors it:
17///
18/// ```
19/// use gpui_cn::Spinner;
20/// use gpui_kit::Styled as _;
21///
22/// let _ = Spinner::new("saving");
23/// let _ = Spinner::new("sync").size_6().accessibility_label("Syncing");
24/// ```
25///
26/// Assistive technology reads it as a status named "Loading", as shadcn's
27/// `role="status" aria-label="Loading"`.
28///
29/// The turn repaints at 30 fps on the app's shared loop clock, not at the
30/// display rate: a 16px arc turning once a second moves 12 degrees a
31/// step, which reads as smooth, and every spinner on screen repaints on
32/// the same tick. Under reduced motion it holds still and asks for no
33/// frames. A spinner is a thin wrapper: a [`Progress`] ring with no value
34/// draws the arc.
35#[derive(IntoElement)]
36pub struct Spinner {
37 id: ElementId,
38 style: StyleRefinement,
39 accessibility_label: Option<SharedString>,
40}
41
42impl Spinner {
43 /// A spinner with a stable id.
44 pub fn new(id: impl Into<ElementId>) -> Self {
45 Self {
46 id: id.into(),
47 style: StyleRefinement::default(),
48 accessibility_label: None,
49 }
50 }
51
52 /// The name a screen reader announces. The default is "Loading".
53 pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
54 self.accessibility_label = Some(label.into());
55 self
56 }
57}
58
59impl Styled for Spinner {
60 fn style(&mut self) -> &mut StyleRefinement {
61 &mut self.style
62 }
63}
64
65/// The accessible name when none is given, as shadcn's.
66const DEFAULT_LABEL: &str = "Loading";
67
68impl RenderOnce for Spinner {
69 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
70 let label = self
71 .accessibility_label
72 .unwrap_or_else(|| DEFAULT_LABEL.into());
73 div()
74 .id(self.id.clone())
75 .role(Role::Status)
76 .aria_label(label.clone())
77 .test_support()
78 .flex_shrink_0()
79 .size_4()
80 .refine_style(&self.style)
81 .child(
82 Progress::new(ElementId::NamedChild(self.id.into(), "arc".into()))
83 .circular()
84 .accessibility_label(label)
85 .size_full(),
86 )
87 }
88}