eddacraft-tui 0.2.3

Shared Ratatui component library for the eddacraft product family
Documentation
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::text::{Line, Span};
use ratatui::widgets::Widget;

use crate::theme::Theme;

pub struct StatusBar<'a, T: Theme> {
    theme: &'a T,
    left: Vec<StatusItem<'a>>,
    right: Vec<StatusItem<'a>>,
}

pub struct StatusItem<'a> {
    pub label: &'a str,
    pub kind: StatusKind,
}

#[derive(Clone, Copy)]
pub enum StatusKind {
    Normal,
    Success,
    Error,
    Warning,
    Muted,
}

fn kind_style<T: Theme>(kind: StatusKind, theme: &T) -> ratatui::style::Style {
    match kind {
        StatusKind::Normal => theme.base(),
        StatusKind::Success => theme.status_ok(),
        StatusKind::Error => theme.status_error(),
        StatusKind::Warning => theme.status_warning(),
        StatusKind::Muted => theme.disabled(),
    }
}

impl<'a, T: Theme> StatusBar<'a, T> {
    pub fn new(theme: &'a T) -> Self {
        Self {
            theme,
            left: Vec::new(),
            right: Vec::new(),
        }
    }

    #[must_use]
    pub fn left(mut self, items: impl IntoIterator<Item = StatusItem<'a>>) -> Self {
        self.left = items.into_iter().collect();
        self
    }

    #[must_use]
    pub fn right(mut self, items: impl IntoIterator<Item = StatusItem<'a>>) -> Self {
        self.right = items.into_iter().collect();
        self
    }
}

impl<T: Theme> Widget for StatusBar<'_, T> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        buf.set_style(area, self.theme.base());

        let chunks = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
            .split(area);

        let left_spans: Vec<Span> = self
            .left
            .iter()
            .flat_map(|item| {
                let style = kind_style(item.kind, self.theme);
                vec![Span::styled(item.label, style), Span::raw(" ")]
            })
            .collect();

        let right_spans: Vec<Span> = self
            .right
            .iter()
            .flat_map(|item| {
                let style = kind_style(item.kind, self.theme);
                vec![Span::raw(" "), Span::styled(item.label, style)]
            })
            .collect();

        Line::from(left_spans).render(chunks[0], buf);
        Line::from(right_spans).render(chunks[1], buf);
    }
}

#[cfg(test)]
mod tests {
    use ratatui::widgets::Widget;

    use super::*;
    use crate::theme::EddaCraftTheme;

    fn item(label: &str, kind: StatusKind) -> StatusItem<'_> {
        StatusItem { label, kind }
    }

    #[test]
    fn empty_status_bar_renders_base_style() {
        let theme = EddaCraftTheme;
        let area = Rect::new(0, 0, 20, 1);
        let mut buf = Buffer::empty(area);

        StatusBar::new(&theme).render(area, &mut buf);

        for x in 0..20 {
            assert_eq!(buf[(x, 0)].symbol(), " ");
        }
    }

    #[test]
    fn left_items_appear_in_left_half() {
        let theme = EddaCraftTheme;
        let area = Rect::new(0, 0, 20, 1);
        let mut buf = Buffer::empty(area);

        StatusBar::new(&theme)
            .left(vec![item("OK", StatusKind::Normal)])
            .render(area, &mut buf);

        let text: String = (0..10).map(|x| buf[(x, 0)].symbol().to_string()).collect();
        assert!(text.starts_with("OK"));
    }

    #[test]
    fn right_items_appear_in_right_half() {
        let theme = EddaCraftTheme;
        let area = Rect::new(0, 0, 20, 1);
        let mut buf = Buffer::empty(area);

        StatusBar::new(&theme)
            .right(vec![item("v1", StatusKind::Muted)])
            .render(area, &mut buf);

        let text: String = (10..20).map(|x| buf[(x, 0)].symbol().to_string()).collect();
        assert!(text.contains("v1"));
    }

    #[test]
    fn success_kind_uses_status_ok_style() {
        let theme = EddaCraftTheme;
        let area = Rect::new(0, 0, 20, 1);
        let mut buf = Buffer::empty(area);

        StatusBar::new(&theme)
            .left(vec![item("pass", StatusKind::Success)])
            .render(area, &mut buf);

        let expected = theme.status_ok();
        assert_eq!(buf[(0, 0)].fg, expected.fg.unwrap());
    }

    #[test]
    fn error_kind_uses_status_error_style() {
        let theme = EddaCraftTheme;
        let area = Rect::new(0, 0, 20, 1);
        let mut buf = Buffer::empty(area);

        StatusBar::new(&theme)
            .left(vec![item("fail", StatusKind::Error)])
            .render(area, &mut buf);

        let expected = theme.status_error();
        assert_eq!(buf[(0, 0)].fg, expected.fg.unwrap());
    }

    #[test]
    fn warning_kind_uses_status_warning_style() {
        let theme = EddaCraftTheme;
        let area = Rect::new(0, 0, 20, 1);
        let mut buf = Buffer::empty(area);

        StatusBar::new(&theme)
            .left(vec![item("warn", StatusKind::Warning)])
            .render(area, &mut buf);

        let expected = theme.status_warning();
        assert_eq!(buf[(0, 0)].fg, expected.fg.unwrap());
    }
}