faststep 0.1.0

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

use super::{Animation, Curve, Layer, lerp_i32};

const STACK_ANIMATION_MS: u32 = 320;

/// Error returned by stack navigation operations.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StackError {
    /// A transition is already active.
    Busy,
    /// The stack is at capacity.
    Full,
    /// The root view cannot be popped.
    RootLocked,
}

/// Base and overlay layers used during stack presentation.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StackLayers<K> {
    /// Settled base layer.
    pub base: Layer<K>,
    /// Animated overlay layer, if any.
    pub overlay: Option<Layer<K>>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Transition<K> {
    Push {
        from: K,
        to: K,
        animation: Animation,
    },
    Pop {
        from: K,
        to: K,
        animation: Animation,
    },
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum HeaderTransition<K> {
    Push { from: K, to: K, progress: u16 },
    Pop { from: K, to: K, progress: u16 },
}

/// Stack-based navigation state machine.
#[derive(Debug)]
pub struct StackNav<K, const N: usize> {
    stack: Vec<K, N>,
    transition: Option<Transition<K>>,
}

impl<K: Copy, const N: usize> StackNav<K, N> {
    /// Creates a stack containing only `root`.
    pub fn new(root: K) -> Self {
        let mut stack = Vec::new();
        let _ = stack.push(root);
        Self {
            stack,
            transition: None,
        }
    }

    /// Returns the top-most view identifier.
    pub fn top(&self) -> K {
        *self
            .stack
            .last()
            .expect("stack always contains a root view")
    }

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

    /// Returns whether a push or pop animation is active.
    pub fn is_animating(&self) -> bool {
        self.transition.is_some()
    }

    pub(crate) fn previous(&self) -> Option<K> {
        self.stack.get(self.stack.len().checked_sub(2)?).copied()
    }

    pub(crate) fn header_transition(&self) -> Option<HeaderTransition<K>> {
        match self.transition {
            None => None,
            Some(Transition::Push {
                from,
                to,
                animation,
            }) => Some(HeaderTransition::Push {
                from,
                to,
                progress: animation.progress_permille(),
            }),
            Some(Transition::Pop {
                from,
                to,
                animation,
            }) => Some(HeaderTransition::Pop {
                from,
                to,
                progress: animation.progress_permille(),
            }),
        }
    }

    /// Pushes `key` and starts a transition.
    pub fn push(&mut self, key: K) -> Result<(), StackError> {
        if self.transition.is_some() {
            return Err(StackError::Busy);
        }

        let from = self.top();
        self.stack.push(key).map_err(|_| StackError::Full)?;
        self.transition = Some(Transition::Push {
            from,
            to: key,
            animation: Animation::new(STACK_ANIMATION_MS, Curve::EaseInOut),
        });
        Ok(())
    }

    /// Pops the current view and starts a transition.
    pub fn pop(&mut self) -> Result<K, StackError> {
        if self.transition.is_some() {
            return Err(StackError::Busy);
        }
        if self.stack.len() <= 1 {
            return Err(StackError::RootLocked);
        }

        let from = self.stack.pop().ok_or(StackError::RootLocked)?;
        let to = self.top();
        self.transition = Some(Transition::Pop {
            from,
            to,
            animation: Animation::new(STACK_ANIMATION_MS, Curve::EaseInOut),
        });
        Ok(from)
    }

    /// Advances the active transition, if any.
    pub fn advance(&mut self, dt_ms: u32) -> bool {
        let Some(mut transition) = self.transition.take() else {
            return false;
        };

        let was_running = match &transition {
            Transition::Push { animation, .. } | Transition::Pop { animation, .. } => {
                animation.is_running()
            }
        };
        let is_running = match &mut transition {
            Transition::Push { animation, .. } | Transition::Pop { animation, .. } => {
                animation.advance(dt_ms)
            }
        };

        if is_running {
            self.transition = Some(transition);
        }

        was_running
    }

    /// Returns the base and overlay layers for a frame.
    pub fn layers(&self, frame: Rectangle) -> StackLayers<K> {
        let width = frame.size.width as i32;
        let idle = Layer::new(self.top(), frame, Point::zero());

        match self.transition {
            None => StackLayers {
                base: idle,
                overlay: None,
            },
            Some(Transition::Push {
                from,
                to,
                animation,
            }) => {
                let progress = animation.progress_permille();
                let base = Layer::new(from, frame, Point::zero());
                let overlay = Layer::new(to, frame, Point::new(lerp_i32(width, 0, progress), 0));
                StackLayers {
                    base,
                    overlay: Some(overlay),
                }
            }
            Some(Transition::Pop {
                from,
                to,
                animation,
            }) => {
                let progress = animation.progress_permille();
                let base = Layer::new(to, frame, Point::zero());
                let overlay = Layer::new(from, frame, Point::new(lerp_i32(0, width, progress), 0));
                StackLayers {
                    base,
                    overlay: Some(overlay),
                }
            }
        }
    }
}