chargrid_menu 0.3.1

chargrid menus
Documentation
use crate::{
    MenuEntryString, MenuEntryToRender, MenuIndexFromScreenCoord, MenuInstance, MenuInstanceChoose,
    MenuInstanceMouseTracker, Selected,
};
use chargrid_event_routine::{
    common_event, event_or_peek_with_handled, EventOrPeek, EventRoutine, Handled,
};
use chargrid_render::{ColModify, Coord, Frame, Rgb24, Style, View, ViewContext};
use chargrid_text::StringViewSingleLine;
use std::collections::HashMap;
use std::marker::PhantomData;
use std::time::Duration;

fn duration_ratio_u8(delta: Duration, period: Duration) -> Result<u8, Duration> {
    if let Some(remaining) = delta.checked_sub(period) {
        Err(remaining)
    } else {
        Ok(((delta.as_micros() * 255) / period.as_micros()) as u8)
    }
}

struct FadeInstance {
    from: Rgb24,
    to: Rgb24,
    started_at_since_epoch: Duration,
    total_duration: Duration,
}

impl FadeInstance {
    fn new(from: Rgb24, to: Rgb24, total_duration: Duration, since_epoch: Duration) -> Self {
        Self {
            from,
            to,
            started_at_since_epoch: since_epoch,
            total_duration,
        }
    }

    fn constant(col: Rgb24) -> Self {
        Self::new(col, col, Duration::from_millis(0), Duration::from_millis(0))
    }

    fn current(&self, since_epoch: Duration) -> Rgb24 {
        if let Some(time_delta) = since_epoch.checked_sub(self.started_at_since_epoch) {
            match duration_ratio_u8(time_delta, self.total_duration) {
                Ok(ratio) => self.from.linear_interpolate(self.to, ratio),
                Err(_) => self.to,
            }
        } else {
            self.from
        }
    }

    fn transform_foreground(&self, style: &fade_spec::Style, since_epoch: Duration) -> Self {
        let from = match style.from.foreground {
            fade_spec::FromCol::Current => self.current(since_epoch),
            fade_spec::FromCol::Rgb24(rgb24) => rgb24,
        };
        Self::new(
            from,
            style.to.foreground,
            style.durations.foreground,
            since_epoch,
        )
    }

    fn transform_background(&self, style: &fade_spec::Style, since_epoch: Duration) -> Self {
        let from = match style.from.background {
            fade_spec::FromCol::Current => self.current(since_epoch),
            fade_spec::FromCol::Rgb24(rgb24) => rgb24,
        };
        Self::new(
            from,
            style.to.background,
            style.durations.background,
            since_epoch,
        )
    }
}

struct MenuEntryChange {
    change_to: Option<Selected>,
    foreground: FadeInstance,
    background: FadeInstance,
}

pub mod fade_spec {
    pub use chargrid_render::Rgb24;
    pub use std::time::Duration;

    #[derive(Debug, Clone)]
    pub enum FromCol {
        Current,
        Rgb24(Rgb24),
    }

    #[derive(Debug, Clone)]
    pub struct From {
        pub foreground: FromCol,
        pub background: FromCol,
    }
    impl From {
        pub fn current() -> Self {
            Self {
                foreground: FromCol::Current,
                background: FromCol::Current,
            }
        }
    }

    #[derive(Debug, Clone)]
    pub struct To {
        pub foreground: Rgb24,
        pub background: Rgb24,
        pub bold: bool,
        pub underline: bool,
    }

    #[derive(Debug, Clone)]
    pub struct Durations {
        pub foreground: Duration,
        pub background: Duration,
    }

    #[derive(Debug, Clone)]
    pub struct Style {
        pub to: To,
        pub from: From,
        pub durations: Durations,
    }

    #[derive(Debug, Clone)]
    pub struct Spec {
        pub selected: Style,
        pub normal: Style,
    }
}

pub struct FadeMenuInstanceView {
    last_change: HashMap<usize, MenuEntryChange>,
    previous_view_since_epoch: Duration,
    spec: fade_spec::Spec,
    mouse_tracker: MenuInstanceMouseTracker,
    buf: String,
}

impl FadeMenuInstanceView {
    pub fn new(spec: fade_spec::Spec) -> Self {
        Self {
            last_change: HashMap::new(),
            previous_view_since_epoch: Duration::from_millis(0),
            spec,
            mouse_tracker: Default::default(),
            buf: String::new(),
        }
    }
    pub fn clear(&mut self) {
        self.last_change.clear();
    }
}

pub struct FadeMenuInstanceModel<'a, E, S>
where
    E: Clone,
    S: MenuEntryString<Entry = E>,
{
    menu_instance: &'a MenuInstance<E>,
    menu_entry_string: &'a S,
    since_epoch: Duration,
}

impl<'a, E, S> View<FadeMenuInstanceModel<'a, E, S>> for FadeMenuInstanceView
where
    E: Clone,
    S: MenuEntryString<Entry = E>,
{
    fn view<F: Frame, C: ColModify>(
        &mut self,
        FadeMenuInstanceModel {
            menu_instance,
            menu_entry_string,
            since_epoch,
        }: FadeMenuInstanceModel<'a, E, S>,
        context: ViewContext<C>,
        frame: &mut F,
    ) {
        self.mouse_tracker.new_frame(context.offset);
        let spec = &self.spec;
        for (i, entry, maybe_selected) in menu_instance.enumerate() {
            let current_style = if let Some(Selected) = maybe_selected {
                &spec.selected
            } else {
                &spec.normal
            };
            let current = self
                .last_change
                .entry(i)
                .or_insert_with(|| MenuEntryChange {
                    change_to: maybe_selected,
                    foreground: FadeInstance::constant(current_style.to.foreground),
                    background: FadeInstance::constant(current_style.to.background),
                });
            match (current.change_to, maybe_selected) {
                (None, None) | (Some(Selected), Some(Selected)) => (),
                (Some(Selected), None) => {
                    current.change_to = None;
                    current.foreground = current
                        .foreground
                        .transform_foreground(&spec.normal, since_epoch);
                    current.background = current
                        .background
                        .transform_background(&spec.normal, since_epoch);
                }
                (None, Some(Selected)) => {
                    current.change_to = Some(Selected);
                    current.foreground = current
                        .foreground
                        .transform_foreground(&spec.selected, since_epoch);
                    current.background = current
                        .background
                        .transform_background(&spec.selected, since_epoch);
                }
            }
            let foreground = current.foreground.current(since_epoch);
            let background = current.background.current(since_epoch);
            let entry_to_render = MenuEntryToRender {
                index: i,
                entry,
                selected: maybe_selected.is_some(),
            };
            self.buf.clear();
            menu_entry_string.render_string(entry_to_render, &mut self.buf);
            let (bold, underline) = if maybe_selected.is_some() {
                (spec.selected.to.bold, spec.selected.to.underline)
            } else {
                (spec.normal.to.bold, spec.normal.to.underline)
            };
            let mut view = StringViewSingleLine::new(
                Style::new()
                    .with_bold(bold)
                    .with_underline(underline)
                    .with_foreground(foreground)
                    .with_background(background),
            );
            let size = view.view_size(
                &self.buf,
                context.add_offset(Coord::new(0, i as i32)),
                frame,
            );
            self.mouse_tracker.on_entry_view_size(size);
        }
    }
}

impl MenuIndexFromScreenCoord for FadeMenuInstanceView {
    fn menu_index_from_screen_coord(&self, len: usize, coord: Coord) -> Option<usize> {
        self.mouse_tracker.menu_index_from_screen_coord(len, coord)
    }
}

pub struct FadeMenuInstanceRoutine<C, S> {
    choose: PhantomData<C>,
    since_epoch: Duration,
    menu_entry_string: S,
}

impl<C, S> FadeMenuInstanceRoutine<C, S>
where
    C: MenuInstanceChoose,
    S: MenuEntryString<Entry = C::Entry>,
{
    pub fn new(menu_entry_string: S) -> Self {
        Self {
            choose: PhantomData,
            since_epoch: Duration::from_millis(0),
            menu_entry_string,
        }
    }
}

impl<C, T> Clone for FadeMenuInstanceRoutine<C, T>
where
    C: MenuInstanceChoose,
    T: MenuEntryString<Entry = C::Entry> + Clone,
{
    fn clone(&self) -> Self {
        Self {
            choose: PhantomData,
            since_epoch: self.since_epoch,
            menu_entry_string: self.menu_entry_string.clone(),
        }
    }
}

impl<C, S> EventRoutine for FadeMenuInstanceRoutine<C, S>
where
    C: MenuInstanceChoose,
    C::Entry: Clone,
    S: MenuEntryString<Entry = C::Entry>,
{
    type Return = C::Output;
    type Data = C;
    type View = FadeMenuInstanceView;
    type Event = common_event::CommonEvent;

    fn handle<EP>(
        self,
        data: &mut Self::Data,
        view: &Self::View,
        event_or_peek: EP,
    ) -> Handled<Self::Return, Self>
    where
        EP: EventOrPeek<Event = Self::Event>,
    {
        event_or_peek_with_handled(event_or_peek, self, |s, event| match event {
            common_event::CommonEvent::Input(input) => {
                if let Some(menu_output) = data.choose(view, input) {
                    Handled::Return(menu_output)
                } else {
                    Handled::Continue(s)
                }
            }
            common_event::CommonEvent::Frame(duration) => {
                let Self {
                    choose,
                    since_epoch,
                    menu_entry_string,
                } = s;
                Handled::Continue(Self {
                    choose,
                    since_epoch: since_epoch + duration,
                    menu_entry_string,
                })
            }
        })
    }

    fn view<F, CM>(
        &self,
        data: &Self::Data,
        view: &mut Self::View,
        context: ViewContext<CM>,
        frame: &mut F,
    ) where
        F: Frame,
        CM: ColModify,
    {
        if self.since_epoch < view.previous_view_since_epoch {
            view.clear();
        }
        let model = FadeMenuInstanceModel {
            menu_instance: data.menu_instance(),
            menu_entry_string: &self.menu_entry_string,
            since_epoch: self.since_epoch,
        };
        view.view(model, context, frame);
        view.previous_view_since_epoch = self.since_epoch;
    }
}