Skip to main content

photon_ui/components/
loader.rs

1use crate::{
2    Component,
3    RenderError,
4    Rendered,
5};
6
7/// Braille spinner frames used by [`Loader`].
8const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
9
10/// An animated loading spinner with a message.
11///
12/// Call [`tick`](Loader::tick) to advance the spinner frame. The component
13/// does not auto-animate; the application must drive it via a timer loop.
14#[derive(Clone)]
15pub struct Loader {
16    message: String,
17    frame: usize,
18    spinner_color: Option<String>,
19    message_color: Option<String>,
20}
21
22impl Loader {
23    /// Create a new loader with the given message and optional color codes.
24    pub fn new(
25        message: impl Into<String>,
26        spinner_color: Option<String>,
27        message_color: Option<String>,
28    ) -> Self {
29        Self {
30            message: message.into(),
31            frame: 0,
32            spinner_color,
33            message_color,
34        }
35    }
36
37    /// Advance to the next spinner frame.
38    pub fn tick(&mut self) {
39        self.frame = (self.frame + 1) % SPINNER_FRAMES.len();
40    }
41}
42
43impl Component for Loader {
44    fn render(&self, _width: u16) -> Result<Rendered, RenderError> {
45        let spinner = SPINNER_FRAMES[self.frame];
46        // Defensive: emit an explicit intensity reset before the full reset in
47        // case the supplied color code turned on bold/faint.
48        let spinner_styled = self
49            .spinner_color
50            .as_ref()
51            .map(|c| format!("{}{}\x1b[22m\x1b[0m", c, spinner))
52            .unwrap_or_else(|| spinner.to_string());
53        let message_styled = self
54            .message_color
55            .as_ref()
56            .map(|c| format!("{}{}\x1b[22m\x1b[0m", c, self.message))
57            .unwrap_or_else(|| self.message.clone());
58        let line = format!("{} {}", spinner_styled, message_styled);
59        Ok(Rendered {
60            lines: vec![line],
61            cursor: None,
62            images: Vec::new(),
63        })
64    }
65}