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::{Constraint, Layout, Rect},
    style::{Color, Style},
    widgets::{Borders, StatefulWidget},
};

use crate::popup::{Popup, PopupSize};
use crate::popups::AutoSized;
use crate::text_input::{TextInput, TextInputState};
use crate::{BadgeStack, BorderType, DotPattern};

/// Popup que envuelve un `TextInput` para capturar texto del usuario.
///
/// Compone un `Popup` (chrome) con un `TextInput` (campo de texto centrado).
/// Hereda la configuración del `TextInput` a través de builders delegados.
///
/// El popup se auto-dimensiona según el alto requerido por el `TextInput`,
/// y centra verticalmente el campo dentro del área disponible.
#[derive(Clone)]
pub struct TextInputPopup<'a> {
    popup: Popup<'a>,
    text_input: TextInput<'a>,
}

impl<'a> TextInputPopup<'a> {
    /// Crea un `TextInputPopup` con valores predeterminados.
    ///
    /// Por defecto: `Popup` con borde redondeado y padding 0,
    /// `TextInput` con borders invisibles (`Borders::NONE`)
    /// y `BorderType::None` (la entrada se renderiza sin borde propio).
    pub fn new() -> Self {
        Self {
            popup: Popup::new(Color::White)
                .padding(0)
                .border_type(BorderType::Rounded),
            text_input: TextInput::new(),
        }
    }

    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.text_input = self.text_input.cursor_style(style);
        self
    }

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

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

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

    /// Background color for the inner `TextInput` content area.
    /// Use `bg_color` for the popup's background instead.
    pub fn input_bg_color(mut self, color: Color) -> Self {
        self.text_input = self.text_input.bg_color(color);
        self
    }

    /// Background color for the entire popup area (including border).
    /// Delegates to `Popup::bg_color`. See `input_bg_color` for the
    /// inner text input background.
    pub fn bg_color(mut self, color: Color) -> Self {
        self.popup = self.popup.bg_color(color);
        self
    }

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

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

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

    pub fn placeholder(mut self, placeholder: &'a str) -> Self {
        self.text_input = self.text_input.placeholder(placeholder);
        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 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 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 TextInputPopup<'_> {
    type State = TextInputState;

    fn auto_height(&self, _state: &Self::State, _area: Rect) -> u16 {
        let header_extra = if self.popup.has_header() { 2 } else { 0 };
        self.text_input.required_height() + 2 + header_extra
    }
}

impl<'a> TextInputPopup<'a> {
    /// Resolves the final `Rect` for this popup within the given `area`,
    /// using the text input state for Auto-height resolution.
    ///
    /// See [`AutoSized::auto_height`].
    pub fn resolve_rect(&self, area: Rect, state: &TextInputState) -> 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 TextInputPopup<'_> {
    type State = TextInputState;

    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
        let input_height = self.text_input.required_height();

        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);

        // Split inner area to place text_input in center
        let chunks = Layout::vertical([
            Constraint::Min(0),
            Constraint::Length(input_height),
            Constraint::Min(0),
        ]).split(inner);

        self.text_input.render(chunks[1], buf, state);
    }
}

#[cfg(test)]
mod tests;