faststep 0.1.0

UIKit-inspired embedded UI framework built on embedded-graphics
Documentation
use crate::{DisplayPort, FsTheme, I18n, TouchEvent, UiSystem, UiView, ViewEvent};

/// OEM-owned timing and touch adapter used by [`run_ui_system`].
pub trait UiRuntimeDriver {
    /// Driver-specific error type.
    type Error;

    /// Poll interval used while the UI is idle.
    fn idle_poll_ms(&self) -> u32;

    /// Poll interval used while the UI is actively animating or tracking touch.
    fn active_poll_ms(&self) -> u32;

    /// Returns `true` when the runtime should use [`Self::active_poll_ms`].
    fn needs_active_poll(&self) -> bool;

    /// Sleeps or yields for the requested duration.
    fn delay_ms(&mut self, delay_ms: u32);

    /// Returns the current monotonic time in milliseconds.
    fn now_ms(&self) -> u32;

    /// Polls the current touch source.
    fn poll_touch(&mut self, now_ms: u32) -> Result<Option<TouchEvent>, Self::Error>;
}

/// Presentation policy used by [`run_ui_system`] to batch redraw requests.
pub trait UiRuntimePresenter<'text, Display, Root, ViewId, Message, const N: usize>
where
    Display: DisplayPort,
    Root: UiView<'text, ViewId, Message, N>,
{
    /// Aggregated pending state used by the presenter between frames.
    type Pending: Copy;
    /// Presenter-specific error type.
    type Error;

    /// Returns the empty pending state.
    fn idle_pending(&self) -> Self::Pending;

    /// Merges two pending redraw states.
    fn merge_pending(&self, current: Self::Pending, next: Self::Pending) -> Self::Pending;

    /// Converts a view event into pending presentation state.
    fn pending_for_event(&mut self, event: ViewEvent<Message>) -> Self::Pending;

    /// Decides whether a frame should be presented now.
    fn should_present(
        &mut self,
        pending: Self::Pending,
        since_present_ms: u32,
        system: &UiSystem<'text, Display, Root, ViewId, Message, N>,
    ) -> bool;

    /// Presents the pending frame.
    fn present(
        &mut self,
        pending: Self::Pending,
        system: &mut UiSystem<'text, Display, Root, ViewId, Message, N>,
    ) -> Result<(), Self::Error>;

    /// Optional callback invoked after a successful presentation.
    fn did_present(
        &mut self,
        _pending: Self::Pending,
        _system: &UiSystem<'text, Display, Root, ViewId, Message, N>,
    ) {
    }
}

/// Error returned by [`run_ui_system`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UiRuntimeError<DriverError, PresentError> {
    /// Runtime driver failure.
    Driver(DriverError),
    /// Presentation failure.
    Present(PresentError),
}

/// Runs the framework-owned update, input, and presentation loop forever.
pub fn run_ui_system<'text, Display, Root, ViewId, Message, Driver, Presenter, const N: usize>(
    display: Display,
    root: Root,
    theme: FsTheme,
    i18n: I18n<'text>,
    mut driver: Driver,
    mut presenter: Presenter,
) -> Result<(), UiRuntimeError<Driver::Error, Presenter::Error>>
where
    Display: DisplayPort,
    Root: UiView<'text, ViewId, Message, N>,
    Driver: UiRuntimeDriver,
    Presenter: UiRuntimePresenter<'text, Display, Root, ViewId, Message, N>,
{
    let mut system = UiSystem::new(display, root, theme, i18n);
    let mut pending = presenter.idle_pending();
    let mut last_tick_ms = driver.now_ms();
    let mut last_present_ms = last_tick_ms;

    loop {
        let poll_delay_ms = if driver.needs_active_poll() {
            driver.active_poll_ms()
        } else {
            driver.idle_poll_ms()
        };
        driver.delay_ms(poll_delay_ms);

        let now_ms = driver.now_ms();
        let dt_ms = now_ms.saturating_sub(last_tick_ms).max(1);
        last_tick_ms = now_ms;

        let update_pending = presenter.pending_for_event(system.update(dt_ms));
        pending = presenter.merge_pending(pending, update_pending);

        if let Some(touch) = driver.poll_touch(now_ms).map_err(UiRuntimeError::Driver)? {
            let touch_pending = presenter.pending_for_event(system.handle_touch(touch));
            pending = presenter.merge_pending(pending, touch_pending);
        }

        let since_present_ms = now_ms.saturating_sub(last_present_ms);
        if presenter.should_present(pending, since_present_ms, &system) {
            presenter
                .present(pending, &mut system)
                .map_err(UiRuntimeError::Present)?;
            presenter.did_present(pending, &system);
            pending = presenter.idle_pending();
            last_present_ms = driver.now_ms();
        }
    }
}