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},
    widgets::StatefulWidget,
};

use crate::popup::{Popup, PopupSize};
use crate::popups::AutoSized;
use crate::tree::{Tree, TreeNode, TreeState};
use crate::{BadgeStack, BorderType, DotPattern};

/// Popup que envuelve un `Tree` para mostrar una jerarquía expandible.
///
/// Compone un `Popup` (chrome) con un `Tree` (árbol).
/// Hereda la configuración del `Tree` a través de builders delegados.
#[derive(Clone)]
pub struct TreePopup<'a> {
    popup: Popup<'a>,
    tree: Tree<'a>,
}

impl<'a> TreePopup<'a> {
    pub fn new(nodes: Vec<TreeNode<'a>>) -> Self {
        Self {
            popup: Popup::new(Color::White)
                .padding(0)
                .border_type(BorderType::Rounded),
            tree: Tree::new(nodes),
        }
    }

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

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

    pub fn border_color(mut self, color: Color) -> Self {
        self.popup = self.popup.border_color(color);
        self
    }

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

    pub fn bg_color(mut self, color: Color) -> Self {
        self.popup = self.popup.bg_color(color);
        self
    }

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

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

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

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

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

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

    // Tree config delegation


    // Tree config delegation

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

    pub fn expand_icon(mut self, icon: &'a str) -> Self {
        self.tree = self.tree.expand_icon(icon);
        self
    }

    pub fn collapse_icon(mut self, icon: &'a str) -> Self {
        self.tree = self.tree.collapse_icon(icon);
        self
    }

    pub fn leaf_icon(mut self, icon: &'a str) -> Self {
        self.tree = self.tree.leaf_icon(icon);
        self
    }

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

    pub fn highlight_symbol(mut self, symbol: &'a str) -> Self {
        self.tree = self.tree.highlight_symbol(symbol);
        self
    }

    pub fn borders(mut self, borders: ratatui::widgets::Borders) -> Self {
        self.tree = self.tree.borders(borders);
        self
    }

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

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

    pub fn background_dots(mut self, color: Color, symbol: &str, density: u16) -> Self {
        self.popup = self.popup.background_dots(color, symbol, density);
        self
    }

    pub fn background_pattern(mut self, pattern: DotPattern) -> Self {
        self.popup = self.popup.background_pattern(pattern);
        self
    }

    pub fn background_spacing(mut self, density_x: u16, density_y: u16) -> Self {
        self.popup = self.popup.background_spacing(density_x, density_y);
        self
    }

    pub fn badges(mut self, badges: BadgeStack<'a>) -> Self {
        self.popup = self.popup.badges(badges);
        self
    }
}

impl AutoSized for TreePopup<'_> {
    type State = TreeState;

    fn auto_height(&self, state: &Self::State, _area: Rect) -> u16 {
        let header_height: u16 = if self.popup.has_header() { 2 } else { 0 };
        let tree_height = self.tree.visible_count(&state.expanded) as u16 + 2 + header_height;
        tree_height.max(5)
    }
}

impl<'a> TreePopup<'a> {
    /// Map a screen row to the flat visible item index at that position.
    /// Returns `None` if the row is in the border/header area or beyond the
    /// last visible item.
    pub fn item_at(&self, state: &TreeState, popup_rect: Rect, row: u16) -> Option<usize> {
        let content_y = popup_rect.y + self.popup.content_offset();
        if row < content_y {
            return None;
        }
        let list_row = (row - content_y) as usize;
        let idx = list_row + state.scroll_state.list_state.offset();
        let total = self.tree.visible_count(&state.expanded);
        if idx < total {
            Some(idx)
        } else {
            None
        }
    }

    /// Resolves the final `Rect` for this popup within the given `area`,
    /// using the current tree state for Auto-height resolution.
    ///
    /// See [`AutoSized::auto_height`].
    pub fn resolve_rect(&self, area: Rect, state: &TreeState) -> Rect {
        if self.popup.get_height() == PopupSize::Auto {
            self.popup
                .clone()
                .height(PopupSize::Fixed(self.auto_height(state, area)))
                .resolve_rect(area)
        } else {
            self.popup.resolve_rect(area)
        }
    }
}

impl StatefulWidget for TreePopup<'_> {
    type State = TreeState;

    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
        let auto_h = if self.popup.get_height() == PopupSize::Auto {
            Some(self.auto_height(state, area))
        } else {
            None
        };
        let mut popup = self.popup;
        if let Some(h) = auto_h {
            popup = popup.height(PopupSize::Fixed(h));
        }
        let inner = popup.render_inner(area, buf);

        self.tree.render(inner, buf, state);
    }
}

#[cfg(test)]
mod tests;