gpui_ui_kit/
spinner.rs

1//! Spinner component
2//!
3//! Loading indicators and spinners.
4
5use gpui::prelude::*;
6use gpui::*;
7
8/// Spinner size
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum SpinnerSize {
11    /// Extra small (12px)
12    Xs,
13    /// Small (16px)
14    Sm,
15    /// Medium (24px, default)
16    #[default]
17    Md,
18    /// Large (32px)
19    Lg,
20    /// Extra large (48px)
21    Xl,
22}
23
24impl SpinnerSize {
25    fn size(&self) -> Pixels {
26        match self {
27            SpinnerSize::Xs => px(12.0),
28            SpinnerSize::Sm => px(16.0),
29            SpinnerSize::Md => px(24.0),
30            SpinnerSize::Lg => px(32.0),
31            SpinnerSize::Xl => px(48.0),
32        }
33    }
34
35    fn border_width(&self) -> Pixels {
36        match self {
37            SpinnerSize::Xs => px(1.5),
38            SpinnerSize::Sm => px(2.0),
39            SpinnerSize::Md => px(2.5),
40            SpinnerSize::Lg => px(3.0),
41            SpinnerSize::Xl => px(4.0),
42        }
43    }
44}
45
46/// A spinner/loading indicator component
47/// Note: True animation requires GPUI animation support
48pub struct Spinner {
49    size: SpinnerSize,
50    color: Option<Rgba>,
51    label: Option<SharedString>,
52}
53
54impl Spinner {
55    /// Create a new spinner
56    pub fn new() -> Self {
57        Self {
58            size: SpinnerSize::default(),
59            color: None,
60            label: None,
61        }
62    }
63
64    /// Set size
65    pub fn size(mut self, size: SpinnerSize) -> Self {
66        self.size = size;
67        self
68    }
69
70    /// Set custom color
71    pub fn color(mut self, color: Rgba) -> Self {
72        self.color = Some(color);
73        self
74    }
75
76    /// Set loading label
77    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
78        self.label = Some(label.into());
79        self
80    }
81
82    /// Build into element
83    pub fn build(self) -> Div {
84        let size = self.size.size();
85        let border_width = self.size.border_width();
86        let color = self.color.unwrap_or(rgb(0x007acc));
87
88        let mut container = div().flex().items_center().gap_2();
89
90        // Spinner circle
91        // Note: This is a static representation.
92        // True spinning animation requires GPUI animation APIs
93        let spinner = div()
94            .w(size)
95            .h(size)
96            .rounded_full()
97            .border(border_width)
98            .border_color(color);
99
100        container = container.child(spinner);
101
102        // Label
103        if let Some(label) = self.label {
104            let label_el = match self.size {
105                SpinnerSize::Xs | SpinnerSize::Sm => div().text_xs(),
106                SpinnerSize::Md => div().text_sm(),
107                SpinnerSize::Lg | SpinnerSize::Xl => div(),
108            };
109            container = container.child(label_el.text_color(rgb(0xcccccc)).child(label));
110        }
111
112        container
113    }
114}
115
116impl Default for Spinner {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122impl IntoElement for Spinner {
123    type Element = Div;
124
125    fn into_element(self) -> Self::Element {
126        self.build()
127    }
128}
129
130/// A dots loading indicator
131pub struct LoadingDots {
132    size: SpinnerSize,
133    color: Option<Rgba>,
134}
135
136impl LoadingDots {
137    /// Create new loading dots
138    pub fn new() -> Self {
139        Self {
140            size: SpinnerSize::default(),
141            color: None,
142        }
143    }
144
145    /// Set size
146    pub fn size(mut self, size: SpinnerSize) -> Self {
147        self.size = size;
148        self
149    }
150
151    /// Set custom color
152    pub fn color(mut self, color: Rgba) -> Self {
153        self.color = Some(color);
154        self
155    }
156
157    /// Build into element
158    pub fn build(self) -> Div {
159        let color = self.color.unwrap_or(rgb(0x007acc));
160        let dot_size = match self.size {
161            SpinnerSize::Xs => px(4.0),
162            SpinnerSize::Sm => px(6.0),
163            SpinnerSize::Md => px(8.0),
164            SpinnerSize::Lg => px(10.0),
165            SpinnerSize::Xl => px(12.0),
166        };
167
168        div()
169            .flex()
170            .items_center()
171            .gap_1()
172            .child(div().w(dot_size).h(dot_size).rounded_full().bg(color))
173            .child(
174                div()
175                    .w(dot_size)
176                    .h(dot_size)
177                    .rounded_full()
178                    .bg(color)
179                    .opacity(0.7),
180            )
181            .child(
182                div()
183                    .w(dot_size)
184                    .h(dot_size)
185                    .rounded_full()
186                    .bg(color)
187                    .opacity(0.4),
188            )
189    }
190}
191
192impl Default for LoadingDots {
193    fn default() -> Self {
194        Self::new()
195    }
196}
197
198impl IntoElement for LoadingDots {
199    type Element = Div;
200
201    fn into_element(self) -> Self::Element {
202        self.build()
203    }
204}