eddacraft-tui 0.2.3

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

use crate::theme::Theme;

pub struct Confirm<'a, T: Theme> {
    theme: &'a T,
    message: &'a str,
    block: Option<Block<'a>>,
}

#[derive(Debug)]
pub struct ConfirmState {
    pub selected: bool,
    pub confirmed: Option<bool>,
}

impl Default for ConfirmState {
    fn default() -> Self {
        Self {
            selected: true,
            confirmed: None,
        }
    }
}

impl ConfirmState {
    pub fn toggle(&mut self) {
        self.selected = !self.selected;
    }

    pub fn confirm(&mut self) {
        self.confirmed = Some(self.selected);
    }

    pub fn confirm_yes(&mut self) {
        self.confirmed = Some(true);
    }

    pub fn confirm_no(&mut self) {
        self.confirmed = Some(false);
    }

    #[must_use]
    pub fn is_confirmed(&self) -> Option<bool> {
        self.confirmed
    }

    pub fn reset(&mut self) {
        self.confirmed = None;
    }
}

impl<'a, T: Theme> Confirm<'a, T> {
    pub fn new(message: &'a str, theme: &'a T) -> Self {
        Self {
            theme,
            message,
            block: None,
        }
    }

    #[must_use]
    pub fn block(mut self, block: Block<'a>) -> Self {
        self.block = block.into();
        self
    }
}

impl<T: Theme> StatefulWidget for Confirm<'_, T> {
    type State = ConfirmState;

    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
        let inner =
            super::render_block(self.block.as_ref(), self.theme.border_focused(), area, buf);

        if inner.height == 0 || inner.width == 0 {
            return;
        }

        let yes_style = if state.selected {
            self.theme.title().add_modifier(Modifier::BOLD)
        } else {
            self.theme.disabled()
        };
        let no_style = if state.selected {
            self.theme.disabled()
        } else {
            self.theme.status_error().add_modifier(Modifier::BOLD)
        };

        let line = Line::from(vec![
            Span::styled(format!("{} ", self.message), self.theme.base()),
            Span::styled("Yes", yes_style),
            Span::styled(" / ", self.theme.disabled()),
            Span::styled("No", no_style),
            Span::styled(" (y/n)", self.theme.disabled()),
        ]);
        line.render(Rect::new(inner.x, inner.y, inner.width, 1), buf);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn toggle_switches_selection() {
        let mut state = ConfirmState::default();
        assert!(state.selected);
        state.toggle();
        assert!(!state.selected);
    }

    #[test]
    fn confirm_yes_and_no_set_values() {
        let mut state = ConfirmState::default();
        state.confirm_yes();
        assert_eq!(state.is_confirmed(), Some(true));

        state.confirm_no();
        assert_eq!(state.is_confirmed(), Some(false));
    }

    #[test]
    fn reset_clears_confirmation() {
        let mut state = ConfirmState::default();
        state.confirm();
        assert_eq!(state.is_confirmed(), Some(true));
        state.reset();
        assert_eq!(state.is_confirmed(), None);
    }
}