1use std::time::Duration;
4
5use crate::animation::AnimationName;
6use crate::color::Rgb;
7use crate::geometry::{Rect, Size};
8use crate::style::CellStyle;
9use crate::text;
10use crate::widget::{MeasureCx, PaintCx, Widget};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum SpinnerStyle {
16 #[default]
18 Arc,
19 Dots,
21 Orbit,
23 Pop,
25 Pulse,
27 Quarters,
30 Slices,
33}
34
35impl SpinnerStyle {
36 pub const ALL: [Self; 7] =
38 [Self::Arc, Self::Dots, Self::Orbit, Self::Pop, Self::Pulse, Self::Quarters, Self::Slices];
39
40 #[must_use]
42 pub fn name(self) -> &'static str {
43 match self {
44 Self::Arc => "arc",
45 Self::Dots => "dots",
46 Self::Orbit => "orbit",
47 Self::Pop => "pop",
48 Self::Pulse => "pulse",
49 Self::Quarters => "quarters",
50 Self::Slices => "slices",
51 }
52 }
53
54 #[must_use]
56 pub fn animation(self) -> &'static str {
57 match self {
58 Self::Arc => "spinner-arc",
59 Self::Dots => "spinner-dots",
60 Self::Orbit => "spinner-orbit",
61 Self::Pop => "spinner-pop",
62 Self::Pulse => "spinner-pulse",
63 Self::Quarters => "spinner-quarters",
64 Self::Slices => "spinner-slices",
65 }
66 }
67}
68
69impl From<SpinnerStyle> for AnimationName {
70 fn from(style: SpinnerStyle) -> Self {
71 Self::from(style.animation())
72 }
73}
74
75const DONE_ANIMATION: &str = "spinner-done";
77
78#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct Spinner {
90 animation: AnimationName,
91 label: Option<String>,
92 variant: Option<String>,
93 done: bool,
94}
95
96impl Default for Spinner {
97 fn default() -> Self {
98 Self { animation: SpinnerStyle::default().into(), label: None, variant: None, done: false }
99 }
100}
101
102impl Spinner {
103 #[must_use]
105 pub fn new() -> Self {
106 Self::default()
107 }
108
109 #[must_use]
111 pub fn style(mut self, style: SpinnerStyle) -> Self {
112 self.animation = style.into();
113 self
114 }
115
116 #[must_use]
119 pub fn animation(mut self, name: impl Into<AnimationName>) -> Self {
120 self.animation = name.into();
121 self
122 }
123
124 #[must_use]
126 pub fn label(mut self, label: impl Into<String>) -> Self {
127 self.label = Some(label.into());
128 self
129 }
130
131 #[must_use]
133 pub fn variant(mut self, variant: impl Into<String>) -> Self {
134 self.variant = Some(variant.into());
135 self
136 }
137
138 #[must_use]
144 pub fn done(mut self, done: bool) -> Self {
145 self.done = done;
146 self
147 }
148
149 fn cell_style(&self, cx: &mut PaintCx<'_>) -> CellStyle {
151 let style = cx.style("spinner", self.variant.as_deref(), &[]).text();
152 CellStyle { fg: Some(style.fg.unwrap_or_else(|| cx.color("accent"))), ..style }
153 }
154
155 fn paint_done(&self, cx: &mut PaintCx<'_>, area: Rect) {
157 let style = self.cell_style(cx);
158 let started = match *cx.memory::<Finish>() {
159 Finish::Unseen | Finish::Resting => None,
160 Finish::Since { at, from } => Some((at, from)),
161 Finish::Spinning => {
162 let now = cx.now();
164 let turning = cx.animation(self.animation.as_str(), style, Some(Duration::ZERO));
165 Some((now, turning.style.fg))
166 }
167 };
168 let since = started.filter(|_| !cx.reduced_motion());
169 let from = CellStyle { fg: since.and_then(|(_, from)| from).or(style.fg), ..style };
170 let cell = cx.animation(DONE_ANIMATION, from, since.map(|(at, _)| at));
171 *cx.memory::<Finish>() = match since {
172 Some((at, from)) if !cell.finished => Finish::Since { at, from },
173 _ => Finish::Resting,
174 };
175 cx.text(area.x, area.y, &cell.glyph, cell.style, 1);
176 }
177
178 fn paint_turning(&self, cx: &mut PaintCx<'_>, area: Rect) {
180 *cx.memory::<Finish>() = Finish::Spinning;
181 let style = self.cell_style(cx);
182 let cell = cx.animation(self.animation.as_str(), style, Some(Duration::ZERO));
183 cx.text(area.x, area.y, &cell.glyph, cell.style, 1);
184 }
185}
186
187#[derive(Debug, Clone, Copy, Default)]
189enum Finish {
190 #[default]
192 Unseen,
193 Spinning,
195 Since { at: Duration, from: Option<Rgb> },
197 Resting,
199}
200
201impl<Msg: 'static> Widget<Msg> for Spinner {
202 fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
203 let label = self.label.as_deref().map_or(0, |label| text::width(label).saturating_add(2));
204 Size::new(label.saturating_add(1), 1).min(available)
205 }
206
207 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
208 if self.done {
209 self.paint_done(cx, area);
210 } else {
211 self.paint_turning(cx, area);
212 }
213 if let Some(label) = &self.label {
214 let label_style = cx.style("spinner-label", self.variant.as_deref(), &[]).text();
215 let budget = area.width.saturating_sub(2);
216 let shown = text::truncate(label, budget).into_owned();
217 cx.text(area.x + 2, area.y, &shown, label_style, budget);
218 }
219 }
220}
221
222#[cfg(test)]
223mod tests;