1use crate::prelude::*;
2use gpui::{Animation, AnimationExt, FontWeight};
3use std::time::Duration;
4
5/// Different types of spinner animations
6#[derive(Debug, Default, Clone, Copy, PartialEq)]
7pub enum SpinnerVariant {
8 #[default]
9 Dots,
10 DotsVariant,
11}
12
13/// A spinner indication, based on the label component, that loops through
14/// frames of the specified animation. It implements `LabelCommon` as well.
15///
16/// # Default Example
17///
18/// ```
19/// use ui::{SpinnerLabel};
20///
21/// SpinnerLabel::new();
22/// ```
23///
24/// # Variant Example
25///
26/// ```
27/// use ui::{SpinnerLabel};
28///
29/// SpinnerLabel::dots_variant();
30/// ```
31#[derive(IntoElement, RegisterComponent)]
32pub struct SpinnerLabel {
33 base: Label,
34 variant: SpinnerVariant,
35 frames: Vec<&'static str>,
36 duration: Duration,
37}
38
39impl SpinnerVariant {
40 fn frames(&self) -> Vec<&'static str> {
41 match self {
42 SpinnerVariant::Dots => vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
43 SpinnerVariant::DotsVariant => vec!["⣼", "⣹", "⢻", "⠿", "⡟", "⣏", "⣧", "⣶"],
44 }
45 }
46
47 fn duration(&self) -> Duration {
48 match self {
49 SpinnerVariant::Dots => Duration::from_millis(1000),
50 SpinnerVariant::DotsVariant => Duration::from_millis(1000),
51 }
52 }
53
54 fn animation_id(&self) -> &'static str {
55 match self {
56 SpinnerVariant::Dots => "spinner_label_dots",
57 SpinnerVariant::DotsVariant => "spinner_label_dots_variant",
58 }
59 }
60}
61
62impl SpinnerLabel {
63 pub fn new() -> Self {
64 Self::with_variant(SpinnerVariant::default())
65 }
66
67 pub fn with_variant(variant: SpinnerVariant) -> Self {
68 let frames = variant.frames();
69 let duration = variant.duration();
70
71 SpinnerLabel {
72 base: Label::new(frames[0]),
73 variant,
74 frames,
75 duration,
76 }
77 }
78
79 pub fn dots() -> Self {
80 Self::with_variant(SpinnerVariant::Dots)
81 }
82
83 pub fn dots_variant() -> Self {
84 Self::with_variant(SpinnerVariant::DotsVariant)
85 }
86}
87
88impl LabelCommon for SpinnerLabel {
89 fn size(mut self, size: LabelSize) -> Self {
90 self.base = self.base.size(size);
91 self
92 }
93
94 fn weight(mut self, weight: FontWeight) -> Self {
95 self.base = self.base.weight(weight);
96 self
97 }
98
99 fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
100 self.base = self.base.line_height_style(line_height_style);
101 self
102 }
103
104 fn color(mut self, color: Color) -> Self {
105 self.base = self.base.color(color);
106 self
107 }
108
109 fn strikethrough(mut self) -> Self {
110 self.base = self.base.strikethrough();
111 self
112 }
113
114 fn italic(mut self) -> Self {
115 self.base = self.base.italic();
116 self
117 }
118
119 fn alpha(mut self, alpha: f32) -> Self {
120 self.base = self.base.alpha(alpha);
121 self
122 }
123
124 fn underline(mut self) -> Self {
125 self.base = self.base.underline();
126 self
127 }
128
129 fn truncate(mut self) -> Self {
130 self.base = self.base.truncate();
131 self
132 }
133
134 fn single_line(mut self) -> Self {
135 self.base = self.base.single_line();
136 self
137 }
138
139 fn buffer_font(mut self, cx: &App) -> Self {
140 self.base = self.base.buffer_font(cx);
141 self
142 }
143
144 fn inline_code(mut self, cx: &App) -> Self {
145 self.base = self.base.inline_code(cx);
146 self
147 }
148}
149
150impl RenderOnce for SpinnerLabel {
151 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
152 let frames = self.frames.clone();
153 let duration = self.duration;
154
155 self.base.color(Color::Muted).with_animation(
156 self.variant.animation_id(),
157 Animation::new(duration).repeat(),
158 move |mut label, delta| {
159 let frame_index = (delta * frames.len() as f32) as usize % frames.len();
160
161 label.set_text(frames[frame_index]);
162 label
163 },
164 )
165 }
166}
167
168impl Component for SpinnerLabel {
169 fn scope() -> ComponentScope {
170 ComponentScope::Loading
171 }
172
173 fn name() -> &'static str {
174 "Spinner Label"
175 }
176
177 fn sort_name() -> &'static str {
178 "Spinner Label"
179 }
180
181 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
182 let examples = vec![
183 single_example("Default", SpinnerLabel::new().into_any_element()),
184 single_example(
185 "Dots Variant",
186 SpinnerLabel::dots_variant().into_any_element(),
187 ),
188 ];
189
190 Some(example_group(examples).vertical().into_any_element())
191 }
192}