faststep 0.1.0

UIKit-inspired embedded UI framework built on embedded-graphics
Documentation
use embedded_graphics::primitives::Rectangle;

use crate::{ButtonTouchState, FsTheme, I18n, NavHeaderAction, StackNav, TouchEvent};

use super::{AppNavigation, AppRedraw, AppTouchResult, ViewDelegate, ViewResponse};

/// Compatibility app wrapper retained for pre-`UiSystem` consumers.
pub struct UiApp<'a, ViewId, Delegate, const STACK_DEPTH: usize> {
    pub(super) theme: FsTheme,
    pub(super) i18n: I18n<'a>,
    pub(super) stack: StackNav<ViewId, STACK_DEPTH>,
    pub(super) nav_touch: ButtonTouchState<NavHeaderAction>,
    pub(super) delegate: Delegate,
}

impl<'a, ViewId, Delegate, const STACK_DEPTH: usize> UiApp<'a, ViewId, Delegate, STACK_DEPTH>
where
    ViewId: Copy,
    Delegate: ViewDelegate<'a, ViewId>,
{
    /// Creates a compatibility app rooted at `root`.
    pub fn new(root: ViewId, delegate: Delegate, theme: FsTheme, i18n: I18n<'a>) -> Self {
        Self {
            theme,
            i18n,
            stack: StackNav::new(root),
            nav_touch: ButtonTouchState::new(),
            delegate,
        }
    }

    /// Returns the delegate.
    pub const fn delegate(&self) -> &Delegate {
        &self.delegate
    }

    /// Returns the delegate mutably.
    pub fn delegate_mut(&mut self) -> &mut Delegate {
        &mut self.delegate
    }

    /// Returns the active theme.
    pub const fn theme(&self) -> &FsTheme {
        &self.theme
    }

    /// Returns the active theme mutably.
    pub fn theme_mut(&mut self) -> &mut FsTheme {
        &mut self.theme
    }

    /// Returns the active localization tables.
    pub const fn i18n(&self) -> &I18n<'a> {
        &self.i18n
    }

    /// Returns the active localization tables mutably.
    pub fn i18n_mut(&mut self) -> &mut I18n<'a> {
        &mut self.i18n
    }

    /// Returns the currently visible view identifier.
    pub fn active_view(&self) -> ViewId {
        self.stack.top()
    }

    /// Returns whether a stack animation is active.
    pub fn is_animating(&self) -> bool {
        self.stack.is_animating()
    }

    /// Returns the body frame for the active view.
    pub fn content_frame(&self, bounds: Rectangle) -> Rectangle {
        self.nav_view(bounds).body
    }

    /// Returns the dirty region for the active view.
    pub fn active_view_dirty(&self, bounds: Rectangle) -> Rectangle {
        self.content_frame(bounds)
    }

    /// Advances the compatibility app by one tick.
    pub fn tick(&mut self, dt_ms: u32, bounds: Rectangle) -> AppRedraw {
        if self.stack.advance(dt_ms) {
            return AppRedraw::StackMotion;
        }
        let body = self.content_frame(bounds);
        let response = self
            .delegate
            .tick(self.stack.top(), dt_ms, body, &self.theme, &self.i18n);
        self.apply_response(response)
    }

    /// Handles one touch event.
    pub fn handle_touch(&mut self, touch: TouchEvent, bounds: Rectangle) -> AppRedraw {
        self.handle_touch_result(touch, bounds).redraw
    }

    /// Handles one touch event and returns capture status.
    pub fn handle_touch_result(&mut self, touch: TouchEvent, bounds: Rectangle) -> AppTouchResult {
        if self.is_animating() {
            return AppTouchResult {
                redraw: AppRedraw::None,
                captured: false,
            };
        }

        let nav = self.nav_view(bounds);
        let chrome = nav.handle_touch(&mut self.nav_touch, touch);
        if chrome.action == Some(NavHeaderAction::Back) {
            self.clear_touch_state();
            return AppTouchResult {
                redraw: self
                    .stack
                    .pop()
                    .map(|_| AppRedraw::StackMotion)
                    .unwrap_or(AppRedraw::None),
                captured: true,
            };
        }
        if chrome.captured {
            return AppTouchResult {
                redraw: if chrome.redraw {
                    AppRedraw::Interactive
                } else {
                    AppRedraw::None
                },
                captured: true,
            };
        }

        let response = self.delegate.handle_view_touch(
            self.stack.top(),
            touch,
            nav.body,
            &self.theme,
            &self.i18n,
        );
        AppTouchResult {
            redraw: self.apply_response(response),
            captured: response.captured || response.navigation.is_some(),
        }
    }

    /// Clears all compatibility touch state.
    pub fn clear_touch_state(&mut self) {
        self.nav_touch.clear();
        self.delegate.clear_touch_state();
    }

    fn apply_response(&mut self, response: ViewResponse<ViewId>) -> AppRedraw {
        let mut redraw = response.redraw;
        match response.navigation {
            Some(AppNavigation::Push(view)) => {
                self.clear_touch_state();
                if self.stack.push(view).is_ok() {
                    redraw = redraw.merge(AppRedraw::StackMotion);
                }
            }
            Some(AppNavigation::Pop) => {
                self.clear_touch_state();
                if self.stack.pop().is_ok() {
                    redraw = redraw.merge(AppRedraw::StackMotion);
                }
            }
            None => {}
        }
        redraw
    }
}