faststep 0.1.0

UIKit-inspired embedded UI framework built on embedded-graphics
Documentation
mod presented;

use embedded_graphics::primitives::Rectangle;

use crate::{AlertSpec, FsTheme, I18n, Localized, ModalHost, ModalLayer, TouchEvent, UiCanvas};

use presented::PresentedAlert;

/// Response returned by [`AlertHost::handle_touch`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AlertHostResponse<Modal> {
    /// Activated alert action, if one was selected.
    pub action: Option<(Modal, u8)>,
    /// Whether the alert host captured the touch.
    pub captured: bool,
    /// Whether the alert host visual state changed.
    pub redraw: bool,
}

/// Error returned when an alert cannot be accepted by the host.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AlertHostError {
    /// The supplied alert had more actions than the host capacity.
    TooManyActions,
}

/// High-level modal alert presenter owned by the framework.
pub struct AlertHost<'a, Modal, const MAX_ACTIONS: usize> {
    host: ModalHost<Modal>,
    alert: Option<PresentedAlert<'a, Modal, MAX_ACTIONS>>,
}

impl<'a, Modal, const MAX_ACTIONS: usize> AlertHost<'a, Modal, MAX_ACTIONS>
where
    Modal: Copy + Eq,
{
    /// Creates an empty alert host.
    pub const fn new() -> Self {
        Self {
            host: ModalHost::new(),
            alert: None,
        }
    }

    /// Presents a new alert.
    pub fn present<const N: usize>(
        &mut self,
        key: Modal,
        spec: AlertSpec<'a, N>,
    ) -> Result<(), AlertHostError> {
        self.alert = Some(PresentedAlert::new(key, spec)?);
        self.host.show(key);
        Ok(())
    }

    /// Dismisses the currently presented alert, if any.
    pub fn dismiss_presented(&mut self) {
        self.host.dismiss();
    }

    /// Alias for [`Self::dismiss_presented`].
    pub fn dismiss(&mut self) {
        self.dismiss_presented();
    }

    /// Clears transient button state.
    pub fn clear_touch_state(&mut self) {
        if let Some(alert) = self.alert.as_mut() {
            alert.clear_touch_state();
        }
    }

    /// Returns whether the host is animating a modal transition.
    pub const fn is_animating(&self) -> bool {
        self.host.is_animating()
    }

    /// Advances modal animation state.
    pub fn advance(&mut self, dt_ms: u32) -> bool {
        let changed = self.host.advance(dt_ms);
        if !self.host.has_modal() {
            self.alert = None;
        }
        changed
    }

    /// Returns the currently presented modal key, if any.
    pub fn presented_key(&self) -> Option<Modal> {
        self.alert.as_ref().map(PresentedAlert::key)
    }

    /// Returns the title of the currently presented modal, if any.
    pub fn presented_title(&self) -> Option<Localized<'a>> {
        self.alert.as_ref().map(PresentedAlert::title)
    }

    /// Returns the current modal layer geometry.
    pub fn layer(
        &self,
        bounds: Rectangle,
        theme: &FsTheme,
        i18n: &I18n<'a>,
    ) -> Option<ModalLayer<Modal>> {
        let alert = self.alert.as_ref()?;
        let panel = alert.panel(bounds, theme, i18n);
        self.host.current_with_panel(bounds, panel)
    }

    /// Routes one touch event to the alert host.
    pub fn handle_touch(
        &mut self,
        touch: TouchEvent,
        bounds: Rectangle,
        theme: &FsTheme,
        i18n: &I18n<'a>,
    ) -> AlertHostResponse<Modal> {
        let Some(alert) = self.alert.as_mut() else {
            return AlertHostResponse {
                action: None,
                captured: false,
                redraw: false,
            };
        };

        let panel = alert.panel(bounds, theme, i18n);
        let Some(layer) = self.host.current_with_panel(bounds, panel) else {
            return AlertHostResponse {
                action: None,
                captured: false,
                redraw: false,
            };
        };

        let response = alert.handle_touch(touch, layer.translated_panel());
        if let Some(action) = response.action {
            self.host.dismiss();
            return AlertHostResponse {
                action: Some((alert.key(), action)),
                captured: true,
                redraw: true,
            };
        }

        AlertHostResponse {
            action: None,
            captured: true,
            redraw: response.redraw,
        }
    }

    /// Draws the current alert and dimming layer.
    pub fn draw<D>(&self, display: &mut D, bounds: Rectangle, theme: &FsTheme, i18n: &I18n<'a>)
    where
        D: UiCanvas,
    {
        let Some(alert) = self.alert.as_ref() else {
            return;
        };
        let Some(layer) = self.layer(bounds, theme, i18n) else {
            return;
        };

        display.dim_rect(bounds, layer.dim_alpha);
        alert.draw(display, layer.translated_panel(), theme, i18n);
    }
}