Skip to main content

rusty_bubbles/
spinner.rs

1//! Cleanroom Rust port of upstream Go source file: `spinner/spinner.go`
2//! Upstream Target Tag / Version: `v2.1.0`
3//!
4//! <public-docs>
5//! # Spinner
6//!
7//! A spinner component for Bubble Tea applications.
8//! </public-docs>
9
10use rusty_bubbletea::commands;
11use rusty_bubbletea::model::{Cmd, Msg};
12use rusty_lipgloss::Style;
13use std::sync::atomic::{AtomicI64, Ordering};
14use std::time::{Duration, SystemTime};
15
16/// Internal ID management. Used during animating to ensure that frame messages
17/// are received only by spinner components that sent them.
18static LAST_ID: AtomicI64 = AtomicI64::new(0);
19
20fn next_id() -> i32 {
21    (LAST_ID.fetch_add(1, Ordering::SeqCst)) as i32
22}
23
24/// Spinner is a set of frames used in animating the spinner.
25#[derive(Debug, Clone)]
26pub struct Spinner {
27    /// The frames of the spinner animation.
28    pub frames: Vec<String>,
29    /// The frames-per-second rate at which the spinner animates.
30    pub fps: Duration,
31}
32
33/// Some spinners to choose from. You could also make your own.
34pub fn line() -> Spinner {
35    Spinner {
36        frames: vec![
37            "|".to_string(),
38            "/".to_string(),
39            "-".to_string(),
40            "\\".to_string(),
41        ],
42        fps: Duration::from_millis(100),
43    }
44}
45
46/// A spinner of braille dots.
47pub fn dot() -> Spinner {
48    Spinner {
49        frames: vec![
50            "⣾ ".to_string(),
51            "⣽ ".to_string(),
52            "⣻ ".to_string(),
53            "⢿ ".to_string(),
54            "⡿ ".to_string(),
55            "⣟ ".to_string(),
56            "⣯ ".to_string(),
57            "⣷ ".to_string(),
58        ],
59        fps: Duration::from_millis(100),
60    }
61}
62
63/// A mini braille-dot spinner.
64pub fn mini_dot() -> Spinner {
65    Spinner {
66        frames: vec![
67            "⠋".to_string(),
68            "⠙".to_string(),
69            "⠹".to_string(),
70            "⠸".to_string(),
71            "⠼".to_string(),
72            "⠴".to_string(),
73            "⠦".to_string(),
74            "⠧".to_string(),
75            "⠇".to_string(),
76            "⠏".to_string(),
77        ],
78        fps: Duration::from_millis(83),
79    }
80}
81
82/// A jumping spinner.
83pub fn jump() -> Spinner {
84    Spinner {
85        frames: vec![
86            "⢄".to_string(),
87            "⢂".to_string(),
88            "⢁".to_string(),
89            "⡁".to_string(),
90            "⡈".to_string(),
91            "⡐".to_string(),
92            "⡠".to_string(),
93        ],
94        fps: Duration::from_millis(100),
95    }
96}
97
98/// A pulsing block spinner.
99pub fn pulse() -> Spinner {
100    Spinner {
101        frames: vec![
102            "█".to_string(),
103            "▓".to_string(),
104            "▒".to_string(),
105            "░".to_string(),
106        ],
107        fps: Duration::from_millis(125),
108    }
109}
110
111/// A points spinner.
112pub fn points() -> Spinner {
113    Spinner {
114        frames: vec![
115            "∙∙∙".to_string(),
116            "●∙∙".to_string(),
117            "∙●∙".to_string(),
118            "∙∙●".to_string(),
119        ],
120        fps: Duration::from_millis(142),
121    }
122}
123
124/// A globe spinner.
125pub fn globe() -> Spinner {
126    Spinner {
127        frames: vec!["🌍".to_string(), "🌎".to_string(), "🌏".to_string()],
128        fps: Duration::from_millis(250),
129    }
130}
131
132/// A moon spinner.
133pub fn moon() -> Spinner {
134    Spinner {
135        frames: vec![
136            "🌑".to_string(),
137            "🌒".to_string(),
138            "🌓".to_string(),
139            "🌔".to_string(),
140            "🌕".to_string(),
141            "🌖".to_string(),
142            "🌗".to_string(),
143            "🌘".to_string(),
144        ],
145        fps: Duration::from_millis(125),
146    }
147}
148
149/// A monkey spinner.
150pub fn monkey() -> Spinner {
151    Spinner {
152        frames: vec!["🙈".to_string(), "🙉".to_string(), "🙊".to_string()],
153        fps: Duration::from_millis(333),
154    }
155}
156
157/// A meter spinner.
158pub fn meter() -> Spinner {
159    Spinner {
160        frames: vec![
161            "▱▱▱".to_string(),
162            "▰▱▱".to_string(),
163            "▰▰▱".to_string(),
164            "▰▰▰".to_string(),
165            "▰▰▱".to_string(),
166            "▰▱▱".to_string(),
167            "▱▱▱".to_string(),
168        ],
169        fps: Duration::from_millis(142),
170    }
171}
172
173/// A hamburger spinner.
174pub fn hamburger() -> Spinner {
175    Spinner {
176        frames: vec![
177            "☱".to_string(),
178            "☲".to_string(),
179            "☴".to_string(),
180            "☲".to_string(),
181        ],
182        fps: Duration::from_millis(333),
183    }
184}
185
186/// An ellipsis spinner.
187pub fn ellipsis() -> Spinner {
188    Spinner {
189        frames: vec![
190            "".to_string(),
191            ".".to_string(),
192            "..".to_string(),
193            "...".to_string(),
194        ],
195        fps: Duration::from_millis(333),
196    }
197}
198
199/// Model contains the state for the spinner. Use [`new`] to create new models
200/// rather than using Model as a struct literal.
201pub struct Model {
202    /// Spinner settings to use. See type [`Spinner`].
203    pub spinner: Spinner,
204
205    /// Style sets the styling for the spinner. Most of the time you'll just
206    /// want foreground and background coloring, and potentially some padding.
207    pub style: Style,
208
209    frame: usize,
210    id: i32,
211    tag: i32,
212}
213
214impl std::fmt::Debug for Model {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        f.debug_struct("spinner::Model")
217            .field("id", &self.id)
218            .field("frame", &self.frame)
219            .finish()
220    }
221}
222
223/// ID returns the spinner's unique ID.
224impl Model {
225    /// ID returns the spinner's unique ID.
226    pub fn id(&self) -> i32 {
227        self.id
228    }
229
230    /// Update is the Tea update function.
231    pub fn update(&mut self, msg: &dyn Msg) -> Cmd {
232        if let Some(m) = msg.as_any().downcast_ref::<TickMsg>() {
233            // If an ID is set, and the ID doesn't belong to this spinner,
234            // reject the message.
235            if m.id > 0 && m.id != self.id {
236                return None;
237            }
238
239            // If a tag is set, and it's not the one we expect, reject the
240            // message. This prevents the spinner from receiving too many
241            // messages and thus spinning too fast.
242            if m.tag > 0 && m.tag != self.tag {
243                return None;
244            }
245
246            self.frame += 1;
247            if self.frame >= self.spinner.frames.len() {
248                self.frame = 0;
249            }
250
251            self.tag += 1;
252            return self.tick(self.id, self.tag);
253        }
254        None
255    }
256
257    /// View renders the model's view.
258    pub fn view(&self) -> String {
259        if self.frame >= self.spinner.frames.len() {
260            return "(error)".to_string();
261        }
262
263        self.style.render(&self.spinner.frames[self.frame])
264    }
265
266    /// Tick is the command used to advance the spinner one frame. Use this
267    /// command to effectively start the spinner.
268    pub fn tick_msg(&self) -> TickMsg {
269        TickMsg {
270            // The time at which the tick occurred.
271            time: SystemTime::now(),
272
273            // The ID of the spinner that this message belongs to. This can
274            // be helpful when routing messages, however bear in mind that
275            // spinners will ignore messages that don't contain ID by
276            // default.
277            id: self.id,
278
279            tag: self.tag,
280        }
281    }
282
283    fn tick(&self, id: i32, tag: i32) -> Cmd {
284        let fps = self.spinner.fps;
285        commands::tick(fps, move |t| Some(Box::new(TickMsg { time: t, id, tag })))
286    }
287}
288
289/// TickMsg indicates that the timer has ticked and we should render a frame.
290#[derive(Debug, Clone)]
291pub struct TickMsg {
292    /// The time at which the tick occurred.
293    pub time: SystemTime,
294    tag: i32,
295    /// The ID of the spinner that this message belongs to.
296    pub id: i32,
297}
298
299/// Option is used to set options in [`new`]. For example:
300///
301/// ```rust
302/// # use rusty_bubbles::spinner;
303/// let spinner = spinner::new(vec![spinner::with_spinner(spinner::dot())]);
304/// ```
305pub type Option = Box<dyn FnOnce(&mut Model)>;
306
307/// WithSpinner is an option to set the spinner. Pass this to [`new`].
308pub fn with_spinner(spinner: Spinner) -> Option {
309    Box::new(move |m: &mut Model| {
310        m.spinner = spinner;
311    })
312}
313
314/// WithStyle is an option to set the spinner style. Pass this to [`new`].
315pub fn with_style(style: Style) -> Option {
316    Box::new(move |m: &mut Model| {
317        m.style = style;
318    })
319}
320
321/// New returns a model with default values.
322pub fn new(opts: Vec<Option>) -> Model {
323    let mut m = Model {
324        spinner: line(),
325        id: next_id(),
326        frame: 0,
327        tag: 0,
328        style: Style::new(),
329    };
330
331    for opt in opts {
332        opt(&mut m);
333    }
334
335    m
336}