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