Skip to main content

gpui_component/
spinner.rs

1use crate::{Icon, IconName, Sizable, Size};
2use gpui::{
3    Animation, AnimationExt as _, App, Hsla, IntoElement, ParentElement, RenderOnce, Styled as _,
4    Transformation, Window, div, ease_in_out, percentage, prelude::FluentBuilder as _,
5};
6use instant::Duration;
7
8/// A cycling loading spinner.
9#[derive(IntoElement)]
10pub struct Spinner {
11    size: Size,
12    icon: Icon,
13    speed: Duration,
14    easing: Box<dyn Fn(f32) -> f32>,
15    color: Option<Hsla>,
16}
17
18impl Spinner {
19    /// Create a new loading spinner.
20    pub fn new() -> Self {
21        Self {
22            size: Size::Medium,
23            speed: Duration::from_secs_f64(0.8),
24            easing: Box::new(ease_in_out),
25            icon: Icon::new(IconName::Loader),
26            color: None,
27        }
28    }
29
30    /// Set specified icon for the spinner.
31    ///
32    /// Default is [`IconName::Loader`].
33    ///
34    /// Please ensure the icon used is suitable for a loading spinner.
35    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
36        self.icon = icon.into();
37        self
38    }
39
40    /// Set the icon color.
41    pub fn color(mut self, color: Hsla) -> Self {
42        self.color = Some(color);
43        self
44    }
45
46    /// Set the easing function.
47    pub fn ease(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
48        self.easing = Box::new(easing);
49        self
50    }
51}
52
53impl Sizable for Spinner {
54    fn with_size(mut self, size: impl Into<Size>) -> Self {
55        self.size = size.into();
56        self
57    }
58}
59
60impl RenderOnce for Spinner {
61    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
62        div()
63            .child(
64                self.icon
65                    .with_size(self.size)
66                    .when_some(self.color, |this, color| this.text_color(color))
67                    .with_animation(
68                        "circle",
69                        Animation::new(self.speed).repeat().with_easing(self.easing),
70                        |this, delta| this.transform(Transformation::rotate(percentage(delta))),
71                    ),
72            )
73            .into_element()
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use gpui::{Render, TestAppContext, px, size};
81
82    struct SpinnerHost;
83
84    impl Render for SpinnerHost {
85        fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
86            Spinner::new()
87        }
88    }
89
90    #[gpui::test]
91    fn reduced_motion_spinner_is_static_and_requests_no_frame(cx: &mut TestAppContext) {
92        cx.update(|cx| cx.set_reduce_motion(true));
93        let window = cx.open_window(size(px(100.), px(100.)), |_, _| SpinnerHost);
94        cx.run_until_parked();
95
96        assert_eq!(
97            window
98                .update(cx, |_, window, cx| window.simulate_next_frame(cx))
99                .unwrap(),
100            0
101        );
102    }
103}