gpui_component/
spinner.rs1use 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#[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 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 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
36 self.icon = icon.into();
37 self
38 }
39
40 pub fn color(mut self, color: Hsla) -> Self {
42 self.color = Some(color);
43 self
44 }
45
46 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}