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        let spinner_styled = self
47            .spinner_color
48            .as_ref()
49            .map(|c| format!("{}{}\x1b[0m", c, spinner))
50            .unwrap_or_else(|| spinner.to_string());
51        let message_styled = self
52            .message_color
53            .as_ref()
54            .map(|c| format!("{}{}\x1b[0m", c, self.message))
55            .unwrap_or_else(|| self.message.clone());
56        let line = format!("{} {}", spinner_styled, message_styled);
57        Ok(Rendered {
58            lines: vec![line],
59            cursor: None,
60            images: Vec::new(),
61        })
62    }
63}