hefesto-widgets 0.7.3

Ratatui widgets for the Hefesto TUI toolkit: popups, scrollable lists, trees, text input and spinners
Documentation
use ratatui::{
    buffer::Buffer,
    layout::Rect,
    style::{Color, Style},
    text::{Line, Span},
    widgets::{Paragraph, StatefulWidget, Widget},
};

use crate::scroll_list::ScrollListState;
use crate::{CHECK, CROSS, DEFAULT_FRAMES};

/// State for the spinner widget.
///
/// `scroll_list` is included for use by [`SpinPopup`](crate::SpinPopup),
/// which renders output lines in a scrollable region. If you use [`Spin`]
/// directly, you can ignore this field.
#[derive(Clone)]
pub struct SpinState {
    /// Current animation frame index.
    pub frame: usize,
    /// Whether the spinner has finished.
    pub finished: bool,
    /// Exit code (only meaningful when `finished == true`).
    pub exit_code: Option<i32>,
    /// Scrollable output state. Used by [`SpinPopup`](crate::SpinPopup).
    pub scroll_list: ScrollListState,
}

impl Default for SpinState {
    fn default() -> Self {
        Self {
            frame: 0,
            finished: false,
            exit_code: None,
            scroll_list: ScrollListState::default(),
        }
    }
}

impl SpinState {
    /// Advances the animation by one frame.
    ///
    /// `frames_len` should be the length of the frames array of the
    /// associated [`Spin`] (i.e. `spin.frames.len()`).
    ///
    /// Does nothing when `finished` is `true` or `frames_len` is `0`.
    pub fn tick(&mut self, frames_len: usize) {
        if !self.finished && frames_len > 0 {
            self.frame = (self.frame + 1) % frames_len;
        }
    }

    pub fn output_next(&mut self, line_count: usize) {
        self.scroll_list.next(line_count);
    }

    pub fn output_previous(&mut self) {
        self.scroll_list.previous();
    }

    pub fn output_last(&mut self, line_count: usize) {
        self.scroll_list.last(line_count);
    }
}

#[derive(Clone)]
pub struct Spin {
    frames: &'static [&'static str],
    spinner_style: Style,
}

impl Spin {
    pub fn new() -> Self {
        Self {
            frames: DEFAULT_FRAMES,
            spinner_style: Style::new().fg(Color::Cyan),
        }
    }

    pub fn frames(mut self, frames: &'static [&'static str]) -> Self {
        self.frames = frames;
        self
    }

    pub fn spinner_style(mut self, style: Style) -> Self {
        self.spinner_style = style;
        self
    }
}

impl Default for Spin {
    fn default() -> Self {
        Self::new()
    }
}

impl StatefulWidget for Spin {
    type State = SpinState;

    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
        let (ch, style) = if state.finished {
            let ok = state.exit_code.map_or(true, |c| c == 0);
            (
                if ok { CHECK } else { CROSS },
                Style::new().fg(if ok { Color::Green } else { Color::Red }),
            )
        } else {
            let frame = if self.frames.is_empty() {
                " "
            } else {
                self.frames[state.frame % self.frames.len()]
            };
            (frame, self.spinner_style)
        };

        Paragraph::new(Line::from(Span::styled(ch.to_string(), style))).render(area, buf);
    }
}
#[cfg(test)]
mod tests;