ltrender 0.1.9

a diff based terminal rendering engine
Documentation
use std::{
    sync::{
        Arc,
        atomic::{AtomicU16, Ordering},
    },
    time::{SystemTime, UNIX_EPOCH},
};

use ascii_assets::{Color, TerminalChar};
use common_stdx::{Point, Rect};
use crossterm::event::KeyCode;

use crate::{
    DrawError, Drawable, ScreenFitting, ScreenKey,
    error::AppError,
    input_handler::{
        hook::EventHook,
        manager::{
            KeyAction, KeyMessage, KeySubscriptionTypes, SubscriptionMessage, SubscriptionType,
        },
    },
    rendering::{render_handle::RenderHandle, renderer::RenderModeBehavior},
    terminal_buffer::{
        RectDrawable,
        standard_drawables::rect_drawable::{BorderStyle, ScreenFitType},
    },
};

#[derive(Debug)]
pub enum SelectMenuFitting {
    FitToContents,
    FitToScreen,
}

#[derive(Debug)]
pub struct SelectMenuDrawable {
    fields: Vec<Box<dyn Drawable>>,
    selected_field: Arc<AtomicU16>,
    pub border_rect: RectDrawable,
    fitting: SelectMenuFitting,

    _event_hook: EventHook,
}

impl SelectMenuDrawable {
    pub fn new<M>(
        fields: Vec<Box<dyn Drawable>>,
        border_rect: RectDrawable,
        fitting: SelectMenuFitting,
        menu_screen: ScreenKey,
        mut event_hook: EventHook,
        r: RenderHandle<M>,
    ) -> Result<Self, AppError>
    where
        M: RenderModeBehavior + Sized + Send + 'static,
    {
        let global_selected_field = Arc::new(AtomicU16::new(0));
        let selected_field = global_selected_field.clone();
        let amount_of_fields = Arc::new(AtomicU16::new(fields.len() as u16));
        event_hook.subscribe(
            SubscriptionType::Key(KeySubscriptionTypes::All, KeyAction::Pressed),
            move |k| {
                if let SubscriptionMessage::Key { msg, screen } = k
                    && screen.targeting(menu_screen)
                {
                    let current = global_selected_field.load(Ordering::Relaxed);
                    let max = amount_of_fields.load(Ordering::Relaxed);

                    if let KeyMessage::Pressed(KeyCode::Char('a'), _) = msg {
                        if current < max - 1 {
                            global_selected_field.fetch_add(1, Ordering::Relaxed);
                        }
                        let _ = r.render_screen(menu_screen);
                    } else if let KeyMessage::Pressed(KeyCode::Char('d'), _) = msg
                        && current > 0
                    {
                        // todo why does it only update when i resize the terminal?
                        global_selected_field.fetch_sub(1, Ordering::Relaxed);
                        let _ = r.render_screen(menu_screen);
                    }
                }
            },
        )?;
        Ok(SelectMenuDrawable {
            fields,
            selected_field,
            border_rect,
            fitting,
            _event_hook: event_hook,
        })
    }
}

impl Drawable for SelectMenuDrawable {
    fn size(&self, sprites: &crate::SpriteRegistry) -> Result<(u16, u16), crate::DrawError> {
        let mut max_width: u16 = 0;
        let mut total_height: u16 = 0;

        for obj in &self.fields {
            let (w, h) = obj.size(sprites)?;
            if w > max_width {
                max_width = w;
            }
            total_height = total_height.saturating_add(h + 1)
        }

        Ok((max_width + 1, total_height.saturating_sub(1)))
    }
    fn draw(
        &mut self,
        sprites: &crate::SpriteRegistry,
    ) -> Result<crate::drawable_traits::basic_draw_creator::BasicDrawCreator, crate::DrawError>
    {
        let mut current_offset = Point::from((1, 1));

        if let SelectMenuFitting::FitToContents = self.fitting {
            let size = self.size(sprites)?;
            self.border_rect.rect = Rect::from_coords(0, 0, size.0 as i32 + 1, size.1 as i32)
        }

        let mut initial = self
            .border_rect
            .draw(sprites)
            .map_err(|_| DrawError::FailedDrawing("unable to draw rect drawable".into()))?;
        for (idx, obj) in self.fields.iter_mut().enumerate() {
            let mut bdc = if let Ok(bdc) = obj.draw(sprites) {
                bdc
            } else {
                return Err(DrawError::FailedDrawing(format!(
                    "unable to draw field with idx: {} in SelectMenuDrawable",
                    idx
                )));
            };
            let bbox = bdc.get_bounding_box();
            let height = bbox.p2.y - bbox.p1.y + 1;
            bdc.align_to_origin(current_offset);
            if self.selected_field.load(Ordering::Relaxed) == idx as u16 {
                initial.draw_line(
                    current_offset + Point::from((-1, 0)),
                    current_offset + Point::from((-1, height - 1)),
                    TerminalChar::from_char('>').set_fg(Color::rgb(255, 50, 50)),
                );
            }
            current_offset.y += height;
            initial.merge_creator(bdc);
        }
        Ok(initial)
    }
    fn as_screen_fitting_mut(&mut self) -> Option<&mut dyn crate::ScreenFitting> {
        Some(self)
    }
    fn on_screen_select(
        &mut self,
        _selected_screen: crate::input_handler::manager::TargetScreen,
    ) -> Result<(), DrawError> {
        let time = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs_f64();

        let shade = ((time % 1.0) * 255.0) as u8;
        let color = Color::rgb(shade, shade, shade);

        self.border_rect.border_style =
            BorderStyle::AllRound(TerminalChar::from_char('a').set_bg(color));

        Ok(())
    }
}

impl ScreenFitting for SelectMenuDrawable {
    fn fit_to_screen(&mut self, rect: common_stdx::Rect<i32>) {
        if let SelectMenuFitting::FitToScreen = self.fitting {
            self.border_rect.screen_fit = Some(ScreenFitType::Full);
            self.border_rect.fit_to_screen(rect);
        };
    }
}