hefesto-widgets 0.6.0

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

use crate::{BorderType, ConfirmationPopup, DotGridConfig, DotPattern, PopupSize};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfirmationVariant {
    Success,
    Warning,
    Danger,
    None,
}

#[derive(Clone)]
pub struct ThemedConfirmationPopup<'a> {
    title: &'a str,
    body: Vec<Line<'a>>,
    variant: ConfirmationVariant,
    confirm_label: &'a str,
    cancel_label: &'a str,
    border_type: BorderType,
    position: Option<(u16, u16)>,
    origin: Option<(u16, u16)>,
    width: Option<PopupSize>,
    height: Option<PopupSize>,
    header: bool,
    padding: u16,
    bg_color: Option<Color>,
    no_dot_grid: bool,
    dot_grid: Option<DotGridConfig>,
}

impl<'a> ThemedConfirmationPopup<'a> {
    fn border_color(variant: ConfirmationVariant) -> Color {
        match variant {
            ConfirmationVariant::Success => Color::Green,
            ConfirmationVariant::Warning => Color::Yellow,
            ConfirmationVariant::Danger => Color::Red,
            ConfirmationVariant::None => Color::Gray,
        }
    }

    fn confirm_color(variant: ConfirmationVariant) -> Color {
        match variant {
            ConfirmationVariant::Success => Color::Green,
            ConfirmationVariant::Warning => Color::Yellow,
            ConfirmationVariant::Danger => Color::Red,
            ConfirmationVariant::None => Color::Blue,
        }
    }

    fn build_inner(&self) -> ConfirmationPopup<'a> {
        let mut popup = ConfirmationPopup::new()
            .title(self.title)
            .body(self.body.clone())
            .border_color(Self::border_color(self.variant))
            .confirm_style(Style::new().bg(Self::confirm_color(self.variant)).fg(Color::Black))
            .cancel_style(Style::new().bg(Color::Gray).fg(Color::Black))
            .border_type(self.border_type)
            .options(self.confirm_label, self.cancel_label)
            .padding(self.padding);

        if let Some((x, y)) = self.position {
            popup = popup.position(x, y);
        }
        if let Some((x, y)) = self.origin {
            popup = popup.origin(x, y);
        }
        if let Some(w) = self.width {
            popup = popup.width(w);
        }
        if let Some(h) = self.height {
            popup = popup.height(h);
        }
        if self.header {
            popup = popup.header();
        }
        if let Some(bg) = self.bg_color {
            popup = popup.bg_color(bg);
        }
        if self.no_dot_grid {
            popup = popup.no_background();
        }
        if let Some(ref dg) = self.dot_grid {
            popup = popup
                .background_dots(dg.color, &dg.symbol, dg.density_x)
                .background_spacing(dg.density_x, dg.density_y);
        }

        popup
    }

    pub fn new(title: &'a str, body: Vec<Line<'a>>, variant: ConfirmationVariant) -> Self {
        Self {
            title,
            body,
            variant,
            confirm_label: "Confirmar",
            cancel_label: "Cancelar",
            border_type: BorderType::Rounded,
            position: None,
            origin: None,
            width: None,
            height: None,
            header: false,
            padding: 0,
            bg_color: None,
            no_dot_grid: false,
            dot_grid: None,
        }
    }

    pub fn options(mut self, confirm_label: &'a str, cancel_label: &'a str) -> Self {
        self.confirm_label = confirm_label;
        self.cancel_label = cancel_label;
        self
    }

    pub fn border_type(mut self, border_type: BorderType) -> Self {
        self.border_type = border_type;
        self
    }

    pub fn position(mut self, x: u16, y: u16) -> Self {
        self.position = Some((x, y));
        self
    }

    pub fn origin(mut self, x: u16, y: u16) -> Self {
        self.origin = Some((x, y));
        self
    }

    pub fn width(mut self, width: PopupSize) -> Self {
        self.width = Some(width);
        self
    }

    pub fn height(mut self, height: PopupSize) -> Self {
        self.height = Some(height);
        self
    }

    pub fn header(mut self) -> Self {
        self.header = true;
        self
    }

    pub fn padding(mut self, padding: u16) -> Self {
        self.padding = padding;
        self
    }

    pub fn resolve_rect(&self, area: Rect) -> Rect {
        self.build_inner().resolve_rect(area)
    }

    /// Background color for the entire popup area (including border).
    /// Delegates to `ConfirmationPopup::bg_color` → `Popup::bg_color`.
    pub fn bg_color(mut self, color: Color) -> Self {
        self.bg_color = Some(color);
        self
    }

    pub fn no_background(mut self) -> Self {
        self.no_dot_grid = true;
        self
    }

    pub fn background_dots(mut self, color: Color, symbol: &str, density: u16) -> Self {
        self.dot_grid = Some(DotGridConfig { color, symbol: symbol.to_string(), density_x: density, density_y: density, pattern: DotPattern::default() });
        self
    }

    pub fn background_pattern(mut self, pattern: DotPattern) -> Self {
        if let Some(ref mut dg) = self.dot_grid {
            dg.pattern = pattern;
        } else {
            self.no_dot_grid = false;
            self.dot_grid = Some(DotGridConfig { pattern, ..DotGridConfig::default() });
        }
        self
    }

    pub fn background_spacing(mut self, density_x: u16, density_y: u16) -> Self {
        if let Some(ref mut dg) = self.dot_grid {
            dg.density_x = density_x;
            dg.density_y = density_y;
        } else {
            self.no_dot_grid = false;
            self.dot_grid = Some(DotGridConfig {
                density_x,
                density_y,
                ..DotGridConfig::default()
            });
        }
        self
    }
}

impl Widget for ThemedConfirmationPopup<'_> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        self.build_inner().render(area, buf);
    }
}

#[cfg(test)]
mod tests;