rtlibs-tui 0.1.5

rtools library: ratatui widgets
Documentation
use std::marker::PhantomData;

use crossterm::event::KeyCode;
use crossterm::event::KeyModifiers;
use ratatui::buffer::Buffer;
use ratatui::layout::Alignment;
use ratatui::layout::Position;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;

use crate::widgets::Modal;
use crate::widgets::ModalConfig;
use crate::Result;

pub trait ModalSelectResponse<EVENT>
{
    fn selected(
        event: EVENT,
        value: String,
    ) -> Self;
    fn waiting_input() -> Self;
    fn canceled(event: EVENT) -> Self;
}

#[derive(Debug)]
pub struct ModalSelect<RESPONSE, EVENT>
{
    pub message: String,
    pub options: Vec<String>,
    selected: usize,
    phantom_r: PhantomData<RESPONSE>,
    phantom_e: PhantomData<EVENT>,
}

impl<RESPONSE, EVENT> ModalSelect<RESPONSE, EVENT>
{
    pub fn new<S>(
        message: S,
        options: Vec<String>,
    ) -> Self
    where
        S: AsRef<str>,
    {
        Self {
            message: message
                .as_ref()
                .to_string(),
            options,
            selected: 0,
            phantom_r: PhantomData,
            phantom_e: PhantomData,
        }
    }
}

impl<RESPONSE, EVENT> Modal<RESPONSE, EVENT> for ModalSelect<RESPONSE, EVENT>
where
    EVENT: std::fmt::Debug,
    RESPONSE: std::fmt::Debug + ModalSelectResponse<EVENT>,
{
    fn config(&self) -> ModalConfig
    {
        ModalConfig::new().title(" SELECT ")
    }

    fn render(
        &mut self,
        area: Rect,
        buf: &mut Buffer,
    )
    {
        let mut inner_area = area;
        inner_area.y += 1;
        inner_area.height -= 1;

        let lines = textwrap::wrap(
            &self.message,
            inner_area
                .width
                .saturating_sub(4) as usize,
        )
        .iter()
        .map(|s| Line::raw(s.to_string()))
        .collect::<Vec<_>>();

        let count = lines.len();

        Paragraph::new(lines)
            .alignment(Alignment::Center)
            // .wrap(Wrap { trim: true })
            .render(
                inner_area, buf,
            );

        inner_area.y += u16::try_from(count).unwrap_or_default();
        inner_area.height = 1;

        for (index, option) in self
            .options
            .iter()
            .enumerate()
        {
            inner_area.y += 1;

            let mut paragraph =
                Paragraph::new(vec![Line::raw(option)]).alignment(Alignment::Center);
            // TODO set into style
            if index == self.selected
            {
                paragraph = paragraph.style(Style::new().on_dark_gray());
            }
            else
            {
                paragraph = paragraph.style(Style::new().reset());
            }
            paragraph.render(
                inner_area, buf,
            );
        }

        let mut commands_area = area;
        commands_area.y = commands_area.y + commands_area.height - 2;
        commands_area.height = 1;

        Paragraph::new("[O]k   [C]ancel")
            .alignment(Alignment::Center)
            .render(
                commands_area,
                buf,
            );
    }

    fn handle_key_event(
        &mut self,
        code: KeyCode,
        _modifiers: KeyModifiers,
        event: EVENT,
    ) -> Result<RESPONSE>
    {
        let skip = RESPONSE::waiting_input();

        let response = match code
        {
            KeyCode::Down =>
            {
                if self.selected
                    == self
                        .options
                        .len()
                        - 1
                {
                    self.selected = 0;
                }
                else
                {
                    self.selected += 1;
                }
                skip
            }
            KeyCode::Up =>
            {
                if self.selected == 0
                {
                    self.selected = self
                        .options
                        .len()
                        - 1;
                }
                else
                {
                    self.selected -= 1;
                }
                skip
            }
            KeyCode::Char('c') | KeyCode::Esc => RESPONSE::canceled(event),
            KeyCode::Char('o') | KeyCode::Enter =>
            {
                if let Some(value) = self
                    .options
                    .get(self.selected)
                    .cloned()
                {
                    RESPONSE::selected(
                        event, value,
                    )
                }
                else
                {
                    skip
                }
            }
            _ => skip,
        };

        Ok(response)
    }

    fn cursor(&self) -> Option<Position>
    {
        None
    }
}