faststep 0.1.0

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

use crate::{
    AlertSpec, ButtonTouchResponse, ButtonTouchState, FsTheme, I18n, Localized, NavHeaderAction,
    NavView, StackError, StackMotion, StackNav, TabBar, TabSpec, TouchEvent,
};

/// Axis used by [`SplitView`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SplitAxis {
    /// Split left and right.
    Horizontal,
    /// Split top and bottom.
    Vertical,
}

/// Output rectangles produced by [`SplitView::layout`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SplitLayout {
    /// Primary pane frame.
    pub primary: Rectangle,
    /// Secondary pane frame.
    pub secondary: Rectangle,
}

/// Simple proportional split container.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SplitView {
    axis: SplitAxis,
    primary_permille: u16,
    spacing: u32,
}

impl SplitView {
    /// Creates a split view.
    pub const fn new(axis: SplitAxis, primary_permille: u16, spacing: u32) -> Self {
        Self {
            axis,
            primary_permille,
            spacing,
        }
    }

    /// Returns the split axis.
    pub const fn axis(&self) -> SplitAxis {
        self.axis
    }

    /// Returns the primary-pane proportion in permille.
    pub const fn primary_permille(&self) -> u16 {
        self.primary_permille
    }

    /// Updates the primary-pane proportion in permille.
    pub fn set_primary_permille(&mut self, primary_permille: u16) {
        self.primary_permille = primary_permille.min(1000);
    }

    /// Returns spacing between panes.
    pub const fn spacing(&self) -> u32 {
        self.spacing
    }

    /// Updates spacing between panes.
    pub fn set_spacing(&mut self, spacing: u32) {
        self.spacing = spacing;
    }

    /// Computes primary and secondary pane frames.
    pub fn layout(&self, frame: Rectangle) -> SplitLayout {
        match self.axis {
            SplitAxis::Horizontal => horizontal_split(frame, self.primary_permille, self.spacing),
            SplitAxis::Vertical => vertical_split(frame, self.primary_permille, self.spacing),
        }
    }
}

/// Stack navigation container built on [`StackNav`].
pub struct StackView<'a, ViewId, TitleFor, const N: usize>
where
    TitleFor: Fn(ViewId) -> Localized<'a>,
{
    nav: StackNav<ViewId, N>,
    title_for: TitleFor,
    header_touch: ButtonTouchState<NavHeaderAction>,
}

impl<'a, ViewId, TitleFor, const N: usize> StackView<'a, ViewId, TitleFor, N>
where
    ViewId: Copy,
    TitleFor: Fn(ViewId) -> Localized<'a>,
{
    /// Creates a stack view with a root view and title callback.
    pub fn new(root: ViewId, title_for: TitleFor) -> Self {
        Self {
            nav: StackNav::new(root),
            title_for,
            header_touch: ButtonTouchState::new(),
        }
    }

    /// Replaces the whole stack with a new root.
    pub fn set_root(&mut self, root: ViewId) {
        self.nav = StackNav::new(root);
        self.header_touch.clear();
    }

    /// Pushes a new view onto the stack.
    pub fn push_view(&mut self, view: ViewId) -> Result<(), StackError> {
        self.nav.push(view)
    }

    /// Pops the current view off the stack.
    pub fn pop_view(&mut self) -> Result<ViewId, StackError> {
        self.nav.pop()
    }

    /// Returns the top-most view identifier.
    pub fn top_view(&self) -> ViewId {
        self.nav.top()
    }

    /// Returns the current stack depth.
    pub fn depth(&self) -> usize {
        self.nav.depth()
    }

    /// Returns whether the stack is animating.
    pub fn is_animating(&self) -> bool {
        self.nav.is_animating()
    }

    /// Advances stack animation state.
    pub fn advance(&mut self, dt_ms: u32) -> bool {
        self.nav.advance(dt_ms)
    }

    /// Builds a navigation view for the supplied frame.
    pub fn nav_view(&self, frame: Rectangle) -> NavView<'a, ViewId> {
        self.nav.nav_view(frame, &self.title_for)
    }

    /// Clears transient header touch state.
    pub fn clear_touch_state(&mut self) {
        self.header_touch.clear();
    }

    /// Routes one touch event to the navigation chrome.
    pub fn handle_touch(
        &mut self,
        touch: TouchEvent,
        frame: Rectangle,
    ) -> ButtonTouchResponse<NavHeaderAction> {
        self.nav_view(frame)
            .handle_touch(&mut self.header_touch, touch)
    }

    /// Draws stack navigation chrome.
    pub fn draw_chrome<D>(
        &self,
        display: &mut D,
        frame: Rectangle,
        theme: &FsTheme,
        i18n: &I18n<'a>,
    ) where
        D: embedded_graphics::draw_target::DrawTarget<
                Color = embedded_graphics::pixelcolor::Rgb565,
            >,
    {
        self.nav_view(frame)
            .draw_chrome(display, theme, i18n, &self.header_touch);
    }

    /// Returns stack-motion geometry for animated presentation.
    pub fn motion(&self, frame: Rectangle) -> Option<StackMotion> {
        let nav = self.nav_view(frame);
        nav.layers.overlay.map(|overlay| StackMotion {
            header: nav.header,
            body: nav.body,
            overlay: overlay.translated_frame(),
        })
    }
}

/// Tab container built on [`TabBar`].
pub struct TabView<'a, TabId, const N: usize> {
    tabs: TabBar<'a, TabId, N>,
}

impl<'a, TabId, const N: usize> TabView<'a, TabId, N>
where
    TabId: Copy,
{
    /// Creates a tab view from tab specs and an initial active index.
    pub const fn new(specs: [TabSpec<'a, TabId>; N], active_index: usize) -> Self {
        Self {
            tabs: TabBar::new(specs, active_index),
        }
    }

    /// Replaces the tab set and active index.
    pub fn set_tabs(&mut self, specs: [TabSpec<'a, TabId>; N], active_index: usize) {
        self.tabs = TabBar::new(specs, active_index);
    }

    /// Returns the active tab identifier.
    pub fn active_tab(&self) -> TabId {
        self.tabs.active()
    }

    /// Selects a tab by index.
    pub fn select_tab(&mut self, index: usize) {
        self.tabs.select(index);
    }

    /// Returns the content frame above the tab bar.
    pub fn content_frame(&self, bounds: Rectangle, theme: &FsTheme) -> Rectangle {
        self.tabs.content_frame(bounds, theme)
    }

    /// Routes one touch event to the tab bar.
    pub fn handle_touch(
        &mut self,
        touch: TouchEvent,
        bounds: Rectangle,
        theme: &FsTheme,
    ) -> Option<TabId> {
        self.tabs.handle_touch(touch, bounds, theme)
    }

    /// Draws the tab bar.
    pub fn draw_bar<D>(&self, display: &mut D, bounds: Rectangle, theme: &FsTheme, i18n: &I18n<'a>)
    where
        D: embedded_graphics::draw_target::DrawTarget<
                Color = embedded_graphics::pixelcolor::Rgb565,
            >,
    {
        self.tabs.draw_bar(display, bounds, theme, i18n);
    }
}

/// Lightweight alert presentation configuration.
pub struct AlertConfiguration<'a, const N: usize> {
    /// Alert specification to present.
    pub spec: AlertSpec<'a, N>,
    /// Backdrop alpha for the presentation.
    pub dim_alpha: u8,
}

fn horizontal_split(frame: Rectangle, primary_permille: u16, spacing: u32) -> SplitLayout {
    let available = frame.size.width.saturating_sub(spacing);
    let primary_width = ((available as u64 * primary_permille.min(1000) as u64) / 1000) as u32;
    let secondary_width = available.saturating_sub(primary_width);
    let secondary_x = frame.top_left.x + primary_width as i32 + spacing as i32;
    SplitLayout {
        primary: Rectangle::new(
            frame.top_left,
            embedded_graphics::prelude::Size::new(primary_width, frame.size.height),
        ),
        secondary: Rectangle::new(
            embedded_graphics::prelude::Point::new(secondary_x, frame.top_left.y),
            embedded_graphics::prelude::Size::new(secondary_width, frame.size.height),
        ),
    }
}

fn vertical_split(frame: Rectangle, primary_permille: u16, spacing: u32) -> SplitLayout {
    let available = frame.size.height.saturating_sub(spacing);
    let primary_height = ((available as u64 * primary_permille.min(1000) as u64) / 1000) as u32;
    let secondary_height = available.saturating_sub(primary_height);
    let secondary_y = frame.top_left.y + primary_height as i32 + spacing as i32;
    SplitLayout {
        primary: Rectangle::new(
            frame.top_left,
            embedded_graphics::prelude::Size::new(frame.size.width, primary_height),
        ),
        secondary: Rectangle::new(
            embedded_graphics::prelude::Point::new(frame.top_left.x, secondary_y),
            embedded_graphics::prelude::Size::new(frame.size.width, secondary_height),
        ),
    }
}