hefesto-widgets 0.2.6

Ratatui widgets for the Hefesto TUI toolkit
Documentation
use ratatui::{
    buffer::Buffer,
    layout::{Alignment, Constraint, Layout, Rect},
    text::Line,
    style::{Color, Style},
    prelude::Stylize,
    widgets::{
        Block, Borders, Clear, Padding, Paragraph, Widget, Wrap,
    },
};

use crate::popup::Popup;
use crate::BorderType;

const POPUP_WIDTH: u16 = 44;
const POPUP_HEIGHT: u16 = 9;

#[derive(Clone)]
pub struct ConfirmationPopup<'a> {
    title: Option<&'a str>,
    border_title: Option<&'a str>,
    body: Vec<Line<'a>>,
    border_color: Color,
    border_type: BorderType,
    confirm_bg: Color,
    cancel_bg: Color,
    confirm_label: &'a str,
    cancel_label: &'a str,
    position: Option<Rect>,
}

impl<'a> ConfirmationPopup<'a> {
    pub fn new() -> Self {
        Self {
            title: None,
            border_title: None,
            body: vec![],
            border_color: Color::White,
            border_type: BorderType::Rounded,
            confirm_bg: Color::Green,
            cancel_bg: Color::Gray,
            confirm_label: "Confirmar",
            cancel_label: "Cancelar",
            position: None,
        }
    }

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

    #[allow(dead_code)]
    pub fn border_title(mut self, border_title: &'a str) -> Self {
        self.border_title = Some(border_title);
        self
    }

    pub fn body(mut self, body: Vec<Line<'a>>) -> Self {
        self.body = body;
        self
    }

    #[allow(dead_code)]
    pub fn border_color(mut self, color: Color) -> Self {
        self.border_color = color;
        self
    }

    #[allow(dead_code)]
    pub fn confirm_bg(mut self, color: Color) -> Self {
        self.confirm_bg = color;
        self
    }

    #[allow(dead_code)]
    pub fn cancel_bg(mut self, color: Color) -> Self {
        self.cancel_bg = color;
        self
    }

    #[allow(dead_code)]
    pub fn options(mut self, confirm: &'a str, cancel: &'a str) -> Self {
        self.confirm_label = confirm;
        self.cancel_label = cancel;
        self
    }

    #[allow(dead_code)]
    pub fn border_type(mut self, border_type: BorderType) -> Self {
        self.border_type = border_type;
        self
    }

    #[allow(dead_code)]
    pub fn position(mut self, position: Rect) -> Self {
        self.position = Some(position);
        self
    }

    fn to_ratatui_border(bt: BorderType) -> ratatui::widgets::BorderType {
        match bt {
            BorderType::Plain => ratatui::widgets::BorderType::Plain,
            BorderType::Rounded => ratatui::widgets::BorderType::Rounded,
            BorderType::Double => ratatui::widgets::BorderType::Double,
            BorderType::Thick => ratatui::widgets::BorderType::Thick,
            BorderType::QuadrantInside => ratatui::widgets::BorderType::QuadrantInside,
            BorderType::QuadrantOutside => ratatui::widgets::BorderType::QuadrantOutside,
            BorderType::None => ratatui::widgets::BorderType::Plain,
        }
    }

    fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
        Rect {
            x: (area.width.saturating_sub(width)) / 2,
            y: (area.height.saturating_sub(height)) / 2,
            width: width.min(area.width),
            height: height.min(area.height),
        }
    }
}

impl Widget for ConfirmationPopup<'_> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let popup_rect = self.position.unwrap_or_else(|| Self::centered_rect(area, POPUP_WIDTH, POPUP_HEIGHT));
        Clear.render(popup_rect, buf);

        let border_style = Style::new().bold();
        let rt_border = Self::to_ratatui_border(self.border_type);

        let mut outer_block = Block::bordered()
            .border_type(rt_border)
            .border_style(border_style.fg(self.border_color));

        if let Some(border_title) = self.border_title {
            outer_block = outer_block.title(border_title.to_string());
        }

        let inner = outer_block.inner(popup_rect);
        outer_block.render(popup_rect, buf);

        let chunks = Layout::vertical([
            Constraint::Length(2),
            Constraint::Min(0),
            Constraint::Length(2),
        ]).split(inner);

        let body_block = Block::new()
            .padding(Padding::new(1, 1, 0, 0));

        let options_block = Block::new()
            .borders(Borders::TOP)
            .border_style(border_style.fg(self.border_color))
            .padding(Padding::new(1, 1, 0, 0));

        let option_item = Block::new()
            .padding(Padding::new(1, 1, 0, 0));

        let confirm_style = Style::new().bg(self.confirm_bg).fg(Color::Black);
        let cancel_style = Style::new().bg(self.cancel_bg).fg(Color::Black);

        Popup::render_header(
            chunks[0], buf,
            self.border_color,
            self.title.unwrap_or(""),
        );

        let body_para = Paragraph::new(self.body.clone())
            .block(body_block)
            .wrap(Wrap { trim: false });
        body_para.render(chunks[1], buf);

        let options_inner = options_block.inner(chunks[2]);
        let option_chunks = Layout::horizontal([
            Constraint::Percentage(48),
            Constraint::Length(1),
            Constraint::Percentage(48),
        ]).split(options_inner);

        let confirm_para = Paragraph::new(format!("{}", self.confirm_label).dim())
            .alignment(Alignment::Center)
            .style(confirm_style)
            .block(option_item.clone());
        confirm_para.render(option_chunks[0], buf);

        let cancel_para = Paragraph::new(format!("{}", self.cancel_label).dim())
            .alignment(Alignment::Center)
            .style(cancel_style)
            .block(option_item);
        cancel_para.render(option_chunks[2], buf);

        options_block.render(chunks[2], buf);
    }
}