fyodor 0.4.0

Double buffered terminal renderer for Rust
Documentation
use std::io;

use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
use thiserror::Error;

use crate::{
    input::Keylist,
    layout::{
        align::Align,
        sized::{FullyKnown, KnownHeight, KnownWidth},
        Pos,
    },
    CanvasLike, CanvasLikeExt, Drawable, Renderer,
};

use super::{menu::Menu, Window};

#[derive(Debug, Error)]
pub enum MenuError {
    #[error("menu is empty")]
    Empty,
}

#[derive(Debug)]
pub struct MenuResult<'a, T> {
    pub code: KeyCode,
    pub index: usize,
    pub data: &'a T,
}

pub struct FullscreenMenu<T> {
    pub menu: Menu<T>,
    pub select_keys: Keylist,
    pub up_keys: Keylist,
    pub down_keys: Keylist,
}

impl<T> FullscreenMenu<T> {
    pub fn new(menu: Menu<T>) -> Self {
        Self {
            menu,
            select_keys: Keylist::new(false)
                .except_chars(&['s', 'S', 'k'])
                .except_chars(&['w', 'W', 'j']),
            up_keys: Keylist::new(false)
                .with_chars(&['s', 'S', 'k'])
                .with_keys(&[KeyCode::Up]),
            down_keys: Keylist::new(false)
                .with_chars(&['w', 'W', 'j'])
                .with_keys(&[KeyCode::Down]),
        }
    }
}

fn dok<T, E1, E2>(v: T) -> Result<Result<T, E1>, E2> {
    Ok(Ok(v))
}

impl<T> Window for FullscreenMenu<T>
where
    Self: Drawable<X = Align, Y = Align>,
{
    type Output<'a> = Result<MenuResult<'a, T>, MenuError> where T: 'a;

    fn run(&mut self, renderer: &mut Renderer) -> io::Result<Self::Output<'_>> {
        if self.menu.items().is_empty() {
            return Ok(Err(MenuError::Empty));
        }

        let mut canvas = renderer.get_render_space();
        loop {
            canvas.show((Align::Center, Align::Center), self);
            renderer.render()?;

            let event = crossterm::event::read()?;
            if let Event::Key(KeyEvent { code, kind, .. }) = event {
                if kind != KeyEventKind::Release {
                    if self.up_keys.contains(code) {
                        self.menu.up(1);
                    } else if self.down_keys.contains(code) {
                        self.menu.down(1);
                    } else if self.select_keys.contains(code) {
                        break dok(MenuResult {
                            code,
                            index: self.menu.selected_index().unwrap(),
                            data: self.menu.selected().unwrap(),
                        });
                    }
                }
            }

            renderer.on_event(&event)?;
        }
    }
}

impl<T> Drawable for FullscreenMenu<T>
where
    Menu<T>: Drawable<X = i32, Y = i32> + FullyKnown,
{
    type X = Align;
    type Y = Align;

    fn draw(&self, pos: impl Into<Pos<Align, Align>>, canvas: &mut impl CanvasLike) {
        let pos: Pos<_, _> = pos.into();
        let pos = pos.calc_both(self.menu.dims(), canvas.size());
        self.menu.draw(pos, canvas);
    }
}

impl<T> KnownWidth for FullscreenMenu<T>
where
    Menu<T>: KnownWidth,
{
    fn w(&self) -> i32 {
        self.menu.w()
    }
}

impl<T> KnownHeight for FullscreenMenu<T>
where
    Menu<T>: KnownHeight,
{
    fn h(&self) -> i32 {
        self.menu.h()
    }
}