glyph_ui 0.1.0

TUI library utilizing the Elm architecture
Documentation
//! Displays multiple views in a row or column

use std::marker::PhantomData;

use euclid::Size2D;
use keyboard_types::Key;

use crate::{
    event::Event, unit::Cell, view::element, Printer, View as ViewTrait,
};

mod flex;
pub mod layout;

use flex::Flex;

/// Type-state indicating a row layout
pub enum Row {}

/// Type-state indicating a column layout
pub enum Column {}

/// Persistent state for this view
#[derive(Default)]
pub struct State {
    focused: Option<usize>,
}

impl State {
    /// Move UI focus to the next element
    pub fn focus_prev(&mut self) {
        self.focused = self.focused.map(|x| x.wrapping_sub(1));
    }

    /// Move UI focus to the previous element
    pub fn focus_next(&mut self) {
        self.focused = self.focused.map(|x| x.wrapping_add(1));
    }
}

/// The view itself
pub struct View<'a, T, M, D> {
    inners: Vec<(element::View<'a, T, M>, layout::Item)>,
    state: &'a mut State,
    layout: layout::Container,
    direction: PhantomData<D>,
}

impl<'a, T, M, D> View<'a, T, M, D>
where
    Self: Flex<T, M>,
{
    pub fn new(state: &'a mut State) -> Self {
        Self {
            inners: Vec::new(),
            state,
            layout: Default::default(),
            direction: PhantomData,
        }
    }

    /// Change the layout behavior of this container
    pub fn layout(mut self, layout: layout::Container) -> Self {
        self.layout = layout;

        self
    }

    /// Add a new view below (column) or to the right (row) of any existing
    /// views
    pub fn push<V>(self, view: V) -> Self
    where
        V: ViewTrait<T, M> + 'a,
    {
        self.push_with_layout(view, Default::default())
    }

    /// Same as [`push`](Self::push) but allows setting the layout for this item
    pub fn push_with_layout<V>(mut self, view: V, layout: layout::Item) -> Self
    where
        V: ViewTrait<T, M> + 'a,
    {
        self.inners.push((element::new(view), layout));

        if self.state.focused.is_none() {
            self.state.focused = Some(0);
        }

        self
    }

    /// Translates State::focused into an index into View::inners
    fn focused_child(&self) -> Option<usize> {
        let mut interactive_idx = None;
        let interactive_count =
            self.inners.iter().filter(|(i, _)| i.interactive()).count();

        if interactive_count == 0 {
            return None;
        }

        for (i, inner) in self.inners.iter().map(|(i, _)| i).enumerate() {
            if inner.interactive() {
                match interactive_idx.as_mut() {
                    None => interactive_idx = Some(0),
                    Some(i) => *i += 1,
                }
            }

            let state_calc_agree = self
                .state
                .focused
                .map(|x| x % interactive_count)
                .eq(&interactive_idx);

            if interactive_idx.is_some() && state_calc_agree {
                return Some(i);
            }
        }

        None
    }

    /// Calculate the main and cross axis length for all children
    fn flex_bases(&self, main_axis_length: u16) -> Vec<(u16, u16)> {
        let mut bases: Vec<_> = self
            .inners
            .iter()
            .map(|(inner, _layout)| {
                Self::axes_from_size(Self::inner_main_axis(inner))
            })
            .collect();

        let diff = i32::from(main_axis_length)
            - i32::from(bases.iter().map(|(m, _c)| m).sum::<u16>());

        match diff {
            // Grow some items
            d if d > 0 => {
                let all_growable =
                    self.inners.iter().all(|(_, l)| l.grow.is_some());

                let growable_space = if all_growable {
                    main_axis_length
                } else {
                    let consumed_by_basis: u16 = bases
                        .iter()
                        .zip(self.inners.iter())
                        .filter_map(|((m, _), (_, l))| match l.grow {
                            Some(_) => None,
                            None => Some(m),
                        })
                        .sum();

                    main_axis_length - consumed_by_basis
                };

                let total_grow_factor: u8 =
                    self.inners.iter().filter_map(|(_, l)| l.grow).sum();

                let grow_unit =
                    growable_space.checked_div(total_grow_factor as u16);

                let basis_grow_factor_iter = bases
                    .iter_mut()
                    .map(|(m, _c)| m)
                    .zip(self.inners.iter().map(|(_, l)| l.grow));

                for (basis, grow_factor) in basis_grow_factor_iter {
                    if let (Some(grow_unit), Some(grow_factor)) =
                        (grow_unit, grow_factor)
                    {
                        *basis = grow_unit * grow_factor as u16;
                    }
                }
            }

            // Everything fits
            //
            // This case mostly gets hit when attempting to calculate our
            // "sensible minimum" main axis length when we're inside another
            // container
            //
            // TODO this may not be the right thing to do, but I'm not sure
            // flex-grow and flex-shrink should or even could take effect
            // when all the flex-bases add up to the exact width of the
            // container
            0 => (),

            // Shrink items?
            _ => todo!(),
        }

        bases
    }
}

/// Shorthand for [`View::new()`] with a [`Row`] layout
///
/// [`View::new()`]: View::new
/// [`Row`]: Row
pub fn row<T, M>(state: &mut State) -> View<'_, T, M, Row> {
    View::new(state)
}

/// Shorthand for [`View::new()`] with a [`Column`] layout
///
/// [`View::new()`]: View::new
/// [`Column`]: Column
pub fn column<T, M>(state: &mut State) -> View<'_, T, M, Column> {
    View::new(state)
}

impl<T, M, D> ViewTrait<T, M> for View<'_, T, M, D>
where
    Self: Flex<T, M>,
    M: 'static,
{
    fn draw(&self, printer: &Printer, focused: bool) {
        let focused_idx = self.focused_child();

        // Start position accumulator
        let mut main_axis_pos_acc = 0;

        let (printer_main_axis, printer_cross_axis) =
            Self::axes_from_size(printer.size());

        let bases = self.flex_bases(printer_main_axis);

        let i_inner_basis_iter =
            self.inners.iter().map(|(i, _)| i).zip(bases.iter()).enumerate();

        for (i, (inner, (basis_main, basis_cross))) in i_inner_basis_iter {
            let focused = focused_idx
                .filter(|_| focused)
                .map(|x| x == i)
                .unwrap_or(false);

            inner.draw(
                &printer
                    .to_sub_area(Self::new_element_rect(
                        main_axis_pos_acc,
                        *basis_main,
                        match self.layout.align_items {
                            layout::AlignItems::Stretch => printer_cross_axis,
                            layout::AlignItems::Start => *basis_cross,
                        },
                    ))
                    .unwrap(),
                focused,
            );

            main_axis_pos_acc += *basis_main;
        }
    }

    fn width(&self) -> Size2D<u16, Cell> {
        Flex::width(self)
    }

    fn height(&self) -> Size2D<u16, Cell> {
        Flex::height(self)
    }

    fn layout(&self, constraint: Size2D<u16, Cell>) -> Size2D<u16, Cell> {
        let (constraint_main_axis, _constraint_cross_axis) =
            Self::axes_from_size(constraint);

        let mut min_main_axis = 0;
        let mut min_cross_axis = 0;

        for (basis_main, basis_cross) in self.flex_bases(constraint_main_axis) {
            // The minimum main axis will be the sum of the minimum main axes of
            // all children
            min_main_axis += basis_main;

            // The minimum cross axis will be the maximum minimum of all
            // children
            min_cross_axis = min_cross_axis.max(basis_cross);
        }

        Self::axes_to_size(min_main_axis, min_cross_axis)
    }

    fn event(
        &mut self,
        event: &Event<T>,
        focused: bool,
    ) -> Box<dyn Iterator<Item = M>> {
        if focused {
            if let Event::Key(k) = event {
                if k.key == Key::Tab {
                    self.state.focus_next();
                }
            }
        }

        let mut messages = Vec::new();

        let focused_idx = self.focused_child();

        for (i, inner) in self.inners.iter_mut().map(|(i, _)| i).enumerate() {
            let focused = focused_idx
                .filter(|_| focused)
                .map(|x| x == i)
                .unwrap_or(false);

            messages.extend(inner.event(event, focused));
        }

        Box::new(messages.into_iter())
    }

    fn interactive(&self) -> bool {
        // If any children are interactive, so are we
        self.inners.iter().any(|(inner, _)| inner.interactive())
    }
}