faststep 0.1.0

UIKit-inspired embedded UI framework built on embedded-graphics
Documentation
use embedded_graphics::{prelude::*, primitives::Rectangle};

use super::{Animation, Curve, FsTheme, ModalLayer, lerp_i32, lerp_u8};

const MODAL_ANIMATION_MS: u32 = 320;
const DIM_ALPHA_MAX: u8 = 96;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ModalState<M> {
    Hidden,
    Showing { modal: M, animation: Animation },
    Visible { modal: M },
    Hiding { modal: M, animation: Animation },
}

/// Generic modal transition state machine used by higher-level hosts.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ModalHost<M> {
    state: ModalState<M>,
}

impl<M: Copy> ModalHost<M> {
    /// Creates an empty modal host.
    pub const fn new() -> Self {
        Self {
            state: ModalState::Hidden,
        }
    }

    /// Starts presenting `modal`.
    pub fn show(&mut self, modal: M) {
        self.state = ModalState::Showing {
            modal,
            animation: Animation::new(MODAL_ANIMATION_MS, Curve::EaseInOut),
        };
    }

    /// Starts dismissing the current modal, if any.
    pub fn dismiss(&mut self) {
        let modal = match self.state {
            ModalState::Showing { modal, .. } | ModalState::Visible { modal } => Some(modal),
            ModalState::Hiding { modal, .. } => Some(modal),
            ModalState::Hidden => None,
        };

        if let Some(modal) = modal {
            self.state = ModalState::Hiding {
                modal,
                animation: Animation::new(MODAL_ANIMATION_MS, Curve::EaseInOut),
            };
        }
    }

    /// Returns whether a show or hide animation is in progress.
    pub const fn is_animating(&self) -> bool {
        matches!(
            self.state,
            ModalState::Showing { .. } | ModalState::Hiding { .. }
        )
    }

    /// Returns whether any modal is currently owned by the host.
    pub const fn has_modal(&self) -> bool {
        !matches!(self.state, ModalState::Hidden)
    }

    /// Advances the modal animation state.
    pub fn advance(&mut self, dt_ms: u32) -> bool {
        match &mut self.state {
            ModalState::Hidden | ModalState::Visible { .. } => false,
            ModalState::Showing { modal, animation } => {
                let was_running = animation.is_running();
                let is_running = animation.advance(dt_ms);
                if is_running {
                    true
                } else {
                    self.state = ModalState::Visible { modal: *modal };
                    was_running
                }
            }
            ModalState::Hiding { animation, .. } => {
                let was_running = animation.is_running();
                let is_running = animation.advance(dt_ms);
                if is_running {
                    true
                } else {
                    self.state = ModalState::Hidden;
                    was_running
                }
            }
        }
    }

    /// Returns the current modal layer using the default themed panel.
    pub fn current(&self, bounds: Rectangle, theme: &FsTheme) -> Option<ModalLayer<M>> {
        self.current_with_panel(bounds, modal_panel(bounds, theme))
    }

    /// Returns the current modal layer using an explicit panel rectangle.
    pub fn current_with_panel(&self, bounds: Rectangle, panel: Rectangle) -> Option<ModalLayer<M>> {
        let offscreen = offscreen_offset(bounds, panel);

        match self.state {
            ModalState::Hidden => None,
            ModalState::Visible { modal } => Some(ModalLayer::new(modal, panel, 0, DIM_ALPHA_MAX)),
            ModalState::Showing { modal, animation } => {
                let progress = animation.progress_permille();
                Some(ModalLayer::new(
                    modal,
                    panel,
                    lerp_i32(offscreen, 0, progress),
                    lerp_u8(0, DIM_ALPHA_MAX, progress),
                ))
            }
            ModalState::Hiding { modal, animation } => {
                let progress = animation.progress_permille();
                Some(ModalLayer::new(
                    modal,
                    panel,
                    lerp_i32(0, offscreen, progress),
                    lerp_u8(DIM_ALPHA_MAX, 0, progress),
                ))
            }
        }
    }
}

fn modal_panel(bounds: Rectangle, theme: &FsTheme) -> Rectangle {
    let max_width = bounds
        .size
        .width
        .saturating_sub(theme.modal_margin.saturating_mul(2));
    let width = ((bounds.size.width.saturating_mul(3)) / 5)
        .max(320)
        .min(max_width);
    let height = (bounds.size.height / 3).max(176).min(208);
    let x = bounds.top_left.x + ((bounds.size.width.saturating_sub(width)) / 2) as i32;
    let y = bounds.top_left.y + ((bounds.size.height.saturating_sub(height)) / 2) as i32;
    Rectangle::new(Point::new(x, y), Size::new(width, height))
}

fn offscreen_offset(bounds: Rectangle, panel: Rectangle) -> i32 {
    bounds.top_left.y + bounds.size.height as i32 - panel.top_left.y + 24
}