Skip to main content

envision/component/router/
mod.rs

1//! A component for multi-screen navigation with history.
2//!
3//! `Router` provides type-safe navigation between screens with back navigation
4//! support. Unlike most components, Router is state-only and doesn't implement
5//! a view - the parent application renders based on the current screen.
6//!
7//! # Example
8//!
9//! ```rust
10//! use envision::component::{Router, RouterState, RouterMessage, Component};
11//!
12//! #[derive(Clone, Debug, PartialEq, Eq)]
13//! enum Screen {
14//!     Home,
15//!     Settings,
16//!     Profile,
17//! }
18//!
19//! // Create router starting at Home screen
20//! let mut state = RouterState::new(Screen::Home);
21//!
22//! // Navigate to Settings
23//! Router::update(&mut state, RouterMessage::Navigate(Screen::Settings));
24//! assert_eq!(state.current(), &Screen::Settings);
25//! assert!(state.can_go_back());
26//!
27//! // Go back to Home
28//! Router::update(&mut state, RouterMessage::Back);
29//! assert_eq!(state.current(), &Screen::Home);
30//! ```
31//!
32//! # Usage Pattern
33//!
34//! ```rust
35//! use envision::component::RouterState;
36//! use ratatui::Frame;
37//!
38//! #[derive(Clone, Debug, PartialEq, Eq)]
39//! enum Screen { Home, Settings, Profile }
40//!
41//! struct AppState {
42//!     router: RouterState<Screen>,
43//! }
44//!
45//! // In your app's view function:
46//! fn view(state: &AppState, frame: &mut Frame) {
47//!     match state.router.current() {
48//!         Screen::Home => { /* render home */ }
49//!         Screen::Settings => { /* render settings */ }
50//!         Screen::Profile => { /* render profile */ }
51//!     }
52//! }
53//! ```
54
55use ratatui::prelude::*;
56
57use super::Component;
58use crate::theme::Theme;
59
60/// Navigation mode for screen transitions.
61#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
62pub enum NavigationMode {
63    /// Push the new screen onto the history stack.
64    #[default]
65    Push,
66    /// Replace the current screen without adding to history.
67    Replace,
68}
69
70/// Messages for the Router component.
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub enum RouterMessage<S: Clone + PartialEq> {
73    /// Navigate to a new screen (pushes to history).
74    Navigate(S),
75    /// Navigate with a specific mode.
76    NavigateWith(S, NavigationMode),
77    /// Replace the current screen without adding to history.
78    Replace(S),
79    /// Go back to the previous screen.
80    Back,
81    /// Clear all navigation history.
82    ClearHistory,
83    /// Reset to a specific screen, clearing all history.
84    Reset(S),
85}
86
87/// Output messages from the Router component.
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub enum RouterOutput<S: Clone + PartialEq> {
90    /// Screen changed (from, to).
91    ScreenChanged {
92        /// The previous screen.
93        from: S,
94        /// The new current screen.
95        to: S,
96    },
97    /// Successfully navigated back.
98    NavigatedBack {
99        /// The screen we navigated to.
100        to: S,
101    },
102    /// Tried to go back but there's no history.
103    NoPreviousScreen,
104    /// Router was reset to a new screen.
105    Reset(S),
106    /// History was cleared.
107    HistoryCleared,
108}
109
110/// State for the Router component.
111///
112/// The type parameter `S` is your screen enum. It must implement `Clone` and `PartialEq`.
113///
114/// # Example
115///
116/// ```rust
117/// use envision::component::RouterState;
118///
119/// #[derive(Clone, Debug, PartialEq, Eq)]
120/// enum Screen {
121///     Home,
122///     Settings,
123/// }
124///
125/// let state = RouterState::new(Screen::Home);
126/// assert_eq!(state.current(), &Screen::Home);
127/// assert!(!state.can_go_back());
128/// ```
129#[derive(Clone, Debug)]
130#[cfg_attr(
131    feature = "serialization",
132    derive(serde::Serialize, serde::Deserialize)
133)]
134pub struct RouterState<S: Clone + PartialEq> {
135    /// The current screen.
136    current: S,
137    /// Navigation history (most recent last).
138    history: Vec<S>,
139    /// Maximum history size (0 = unlimited).
140    max_history: usize,
141}
142
143impl<S: Default + Clone + PartialEq> Default for RouterState<S> {
144    fn default() -> Self {
145        Self::new(S::default())
146    }
147}
148
149impl<S: Clone + PartialEq> PartialEq for RouterState<S> {
150    fn eq(&self, other: &Self) -> bool {
151        self.current == other.current
152            && self.history == other.history
153            && self.max_history == other.max_history
154    }
155}
156
157impl<S: Clone + PartialEq> RouterState<S> {
158    /// Creates a new router state starting at the given screen.
159    pub fn new(initial: S) -> Self {
160        Self {
161            current: initial,
162            history: Vec::new(),
163            max_history: 0,
164        }
165    }
166
167    /// Sets the maximum history size.
168    ///
169    /// When the history exceeds this limit, the oldest entries are removed.
170    /// Set to 0 for unlimited history (default).
171    pub fn with_max_history(mut self, max: usize) -> Self {
172        self.max_history = max;
173        self
174    }
175
176    /// Returns the current screen.
177    pub fn current(&self) -> &S {
178        &self.current
179    }
180
181    /// Returns true if we can navigate back.
182    pub fn can_go_back(&self) -> bool {
183        !self.history.is_empty()
184    }
185
186    /// Returns the number of screens in history.
187    pub fn history_len(&self) -> usize {
188        self.history.len()
189    }
190
191    /// Returns the history stack (oldest first).
192    pub fn history(&self) -> &[S] {
193        &self.history
194    }
195
196    /// Returns the maximum history size (0 = unlimited).
197    pub fn max_history(&self) -> usize {
198        self.max_history
199    }
200
201    /// Sets the maximum history size.
202    pub fn set_max_history(&mut self, max: usize) {
203        self.max_history = max;
204        self.enforce_history_limit();
205    }
206
207    /// Returns the previous screen if available.
208    pub fn previous(&self) -> Option<&S> {
209        self.history.last()
210    }
211
212    /// Checks if the current screen is the given screen.
213    pub fn is_at(&self, screen: &S) -> bool {
214        &self.current == screen
215    }
216
217    /// Clears all navigation history.
218    pub fn clear_history(&mut self) {
219        self.history.clear();
220    }
221
222    /// Enforces the max history limit.
223    fn enforce_history_limit(&mut self) {
224        if self.max_history > 0 && self.history.len() > self.max_history {
225            let excess = self.history.len() - self.max_history;
226            self.history.drain(0..excess);
227        }
228    }
229}
230
231/// A component for multi-screen navigation with history.
232///
233/// Router manages screen navigation with a history stack for back navigation.
234/// It's designed to be used with an enum representing your application's screens.
235///
236/// # Note
237///
238/// Router doesn't implement `view()` - it's a state-only component. Your
239/// application should render based on `state.current()`.
240///
241/// # Example
242///
243/// ```rust
244/// use envision::component::{Router, RouterState, RouterMessage, RouterOutput, Component};
245///
246/// #[derive(Clone, Debug, PartialEq, Eq)]
247/// enum Screen {
248///     Home,
249///     Settings,
250///     About,
251/// }
252///
253/// let mut state = RouterState::new(Screen::Home);
254///
255/// // Navigate to Settings
256/// let output = Router::update(&mut state, RouterMessage::Navigate(Screen::Settings));
257/// assert!(matches!(output, Some(RouterOutput::ScreenChanged { .. })));
258/// assert_eq!(state.current(), &Screen::Settings);
259///
260/// // Navigate to About
261/// Router::update(&mut state, RouterMessage::Navigate(Screen::About));
262/// assert_eq!(state.history_len(), 2);
263///
264/// // Go back twice
265/// Router::update(&mut state, RouterMessage::Back);
266/// assert_eq!(state.current(), &Screen::Settings);
267/// Router::update(&mut state, RouterMessage::Back);
268/// assert_eq!(state.current(), &Screen::Home);
269/// ```
270pub struct Router<S: Clone + PartialEq>(std::marker::PhantomData<S>);
271
272impl<S: Clone + PartialEq> Router<S> {
273    /// Updates the router state.
274    ///
275    /// This inherent method is available for all screen types that implement
276    /// `Clone + PartialEq`. Screen types that also implement `Default` can
277    /// use the [`Component`] trait methods instead.
278    pub fn update(state: &mut RouterState<S>, msg: RouterMessage<S>) -> Option<RouterOutput<S>> {
279        match msg {
280            RouterMessage::Navigate(screen) => {
281                if state.current == screen {
282                    return None; // Already at this screen
283                }
284
285                let from = state.current.clone();
286                state.history.push(state.current.clone());
287                state.current = screen.clone();
288                state.enforce_history_limit();
289
290                Some(RouterOutput::ScreenChanged { from, to: screen })
291            }
292
293            RouterMessage::NavigateWith(screen, mode) => match mode {
294                NavigationMode::Push => Self::update(state, RouterMessage::Navigate(screen)),
295                NavigationMode::Replace => Self::update(state, RouterMessage::Replace(screen)),
296            },
297
298            RouterMessage::Replace(screen) => {
299                if state.current == screen {
300                    return None;
301                }
302
303                let from = state.current.clone();
304                state.current = screen.clone();
305
306                Some(RouterOutput::ScreenChanged { from, to: screen })
307            }
308
309            RouterMessage::Back => {
310                if let Some(previous) = state.history.pop() {
311                    state.current = previous.clone();
312                    Some(RouterOutput::NavigatedBack { to: previous })
313                } else {
314                    Some(RouterOutput::NoPreviousScreen)
315                }
316            }
317
318            RouterMessage::ClearHistory => {
319                if state.history.is_empty() {
320                    None
321                } else {
322                    state.history.clear();
323                    Some(RouterOutput::HistoryCleared)
324                }
325            }
326
327            RouterMessage::Reset(screen) => {
328                state.history.clear();
329                state.current = screen.clone();
330                Some(RouterOutput::Reset(screen))
331            }
332        }
333    }
334
335    /// Renders the router view.
336    ///
337    /// Router is state-only, so this is a no-op. The parent application
338    /// should render based on `state.current()`.
339    pub fn view(_state: &RouterState<S>, _frame: &mut Frame, _area: Rect, _theme: &Theme) {
340        // Router is state-only - no view implementation.
341        // The parent application should render based on state.current()
342    }
343}
344
345impl<S: Clone + PartialEq + Default> Component for Router<S> {
346    type State = RouterState<S>;
347    type Message = RouterMessage<S>;
348    type Output = RouterOutput<S>;
349
350    fn init() -> Self::State {
351        RouterState::new(S::default())
352    }
353
354    fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
355        Router::update(state, msg)
356    }
357
358    fn view(state: &Self::State, frame: &mut Frame, area: Rect, theme: &Theme) {
359        Router::view(state, frame, area, theme)
360    }
361}
362
363#[cfg(test)]
364mod tests;