Skip to main content

faststep/
stack.rs

1use embedded_graphics::{prelude::Point, primitives::Rectangle};
2use heapless::Vec;
3
4use super::{Animation, Curve, Layer, lerp_i32};
5
6const STACK_ANIMATION_MS: u32 = 320;
7
8/// Error returned by stack navigation operations.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum StackError {
11    /// A transition is already active.
12    Busy,
13    /// The stack is at capacity.
14    Full,
15    /// The root view cannot be popped.
16    RootLocked,
17}
18
19/// Base and overlay layers used during stack presentation.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct StackLayers<K> {
22    /// Settled base layer.
23    pub base: Layer<K>,
24    /// Animated overlay layer, if any.
25    pub overlay: Option<Layer<K>>,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29enum Transition<K> {
30    Push {
31        from: K,
32        to: K,
33        animation: Animation,
34    },
35    Pop {
36        from: K,
37        to: K,
38        animation: Animation,
39    },
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub(crate) enum HeaderTransition<K> {
44    Push { from: K, to: K, progress: u16 },
45    Pop { from: K, to: K, progress: u16 },
46}
47
48/// Stack-based navigation state machine.
49#[derive(Debug)]
50pub struct StackNav<K, const N: usize> {
51    stack: Vec<K, N>,
52    transition: Option<Transition<K>>,
53}
54
55impl<K: Copy, const N: usize> StackNav<K, N> {
56    /// Creates a stack containing only `root`.
57    pub fn new(root: K) -> Self {
58        let mut stack = Vec::new();
59        let _ = stack.push(root);
60        Self {
61            stack,
62            transition: None,
63        }
64    }
65
66    /// Returns the top-most view identifier.
67    pub fn top(&self) -> K {
68        *self
69            .stack
70            .last()
71            .expect("stack always contains a root view")
72    }
73
74    /// Returns the current stack depth.
75    pub fn depth(&self) -> usize {
76        self.stack.len()
77    }
78
79    /// Returns whether a push or pop animation is active.
80    pub fn is_animating(&self) -> bool {
81        self.transition.is_some()
82    }
83
84    pub(crate) fn previous(&self) -> Option<K> {
85        self.stack.get(self.stack.len().checked_sub(2)?).copied()
86    }
87
88    pub(crate) fn header_transition(&self) -> Option<HeaderTransition<K>> {
89        match self.transition {
90            None => None,
91            Some(Transition::Push {
92                from,
93                to,
94                animation,
95            }) => Some(HeaderTransition::Push {
96                from,
97                to,
98                progress: animation.progress_permille(),
99            }),
100            Some(Transition::Pop {
101                from,
102                to,
103                animation,
104            }) => Some(HeaderTransition::Pop {
105                from,
106                to,
107                progress: animation.progress_permille(),
108            }),
109        }
110    }
111
112    /// Pushes `key` and starts a transition.
113    pub fn push(&mut self, key: K) -> Result<(), StackError> {
114        if self.transition.is_some() {
115            return Err(StackError::Busy);
116        }
117
118        let from = self.top();
119        self.stack.push(key).map_err(|_| StackError::Full)?;
120        self.transition = Some(Transition::Push {
121            from,
122            to: key,
123            animation: Animation::new(STACK_ANIMATION_MS, Curve::EaseInOut),
124        });
125        Ok(())
126    }
127
128    /// Pops the current view and starts a transition.
129    pub fn pop(&mut self) -> Result<K, StackError> {
130        if self.transition.is_some() {
131            return Err(StackError::Busy);
132        }
133        if self.stack.len() <= 1 {
134            return Err(StackError::RootLocked);
135        }
136
137        let from = self.stack.pop().ok_or(StackError::RootLocked)?;
138        let to = self.top();
139        self.transition = Some(Transition::Pop {
140            from,
141            to,
142            animation: Animation::new(STACK_ANIMATION_MS, Curve::EaseInOut),
143        });
144        Ok(from)
145    }
146
147    /// Advances the active transition, if any.
148    pub fn advance(&mut self, dt_ms: u32) -> bool {
149        let Some(mut transition) = self.transition.take() else {
150            return false;
151        };
152
153        let was_running = match &transition {
154            Transition::Push { animation, .. } | Transition::Pop { animation, .. } => {
155                animation.is_running()
156            }
157        };
158        let is_running = match &mut transition {
159            Transition::Push { animation, .. } | Transition::Pop { animation, .. } => {
160                animation.advance(dt_ms)
161            }
162        };
163
164        if is_running {
165            self.transition = Some(transition);
166        }
167
168        was_running
169    }
170
171    /// Returns the base and overlay layers for a frame.
172    pub fn layers(&self, frame: Rectangle) -> StackLayers<K> {
173        let width = frame.size.width as i32;
174        let idle = Layer::new(self.top(), frame, Point::zero());
175
176        match self.transition {
177            None => StackLayers {
178                base: idle,
179                overlay: None,
180            },
181            Some(Transition::Push {
182                from,
183                to,
184                animation,
185            }) => {
186                let progress = animation.progress_permille();
187                let base = Layer::new(from, frame, Point::zero());
188                let overlay = Layer::new(to, frame, Point::new(lerp_i32(width, 0, progress), 0));
189                StackLayers {
190                    base,
191                    overlay: Some(overlay),
192                }
193            }
194            Some(Transition::Pop {
195                from,
196                to,
197                animation,
198            }) => {
199                let progress = animation.progress_permille();
200                let base = Layer::new(to, frame, Point::zero());
201                let overlay = Layer::new(from, frame, Point::new(lerp_i32(0, width, progress), 0));
202                StackLayers {
203                    base,
204                    overlay: Some(overlay),
205                }
206            }
207        }
208    }
209}