Skip to main content

egui_cha/
router.rs

1//! Router for page navigation in TEA applications
2//!
3//! # Example
4//! ```ignore
5//! use egui_cha::router::Router;
6//!
7//! #[derive(Clone, PartialEq, Default)]
8//! enum Page {
9//!     #[default]
10//!     Home,
11//!     Settings,
12//!     Profile(u64),
13//! }
14//!
15//! struct Model {
16//!     router: Router<Page>,
17//!     // page-specific models...
18//! }
19//!
20//! enum Msg {
21//!     Router(RouterMsg<Page>),
22//!     // ...
23//! }
24//!
25//! fn update(model: &mut Model, msg: Msg) -> Cmd<Msg> {
26//!     match msg {
27//!         Msg::Router(router_msg) => {
28//!             model.router.handle(router_msg);
29//!         }
30//!         // ...
31//!     }
32//!     Cmd::none()
33//! }
34//!
35//! fn view(model: &Model, ctx: &mut ViewCtx<Msg>) {
36//!     match model.router.current() {
37//!         Page::Home => { /* render home */ }
38//!         Page::Settings => { /* render settings */ }
39//!         Page::Profile(id) => { /* render profile */ }
40//!     }
41//! }
42//! ```
43
44use std::collections::VecDeque;
45
46/// Router for managing page navigation with history
47#[derive(Debug, Clone)]
48pub struct Router<P> {
49    current: P,
50    history: VecDeque<P>,
51    forward_stack: Vec<P>,
52    max_history: usize,
53}
54
55/// Messages for router operations
56#[derive(Debug, Clone, PartialEq)]
57pub enum RouterMsg<P> {
58    /// Navigate to a new page
59    Navigate(P),
60    /// Go back in history
61    Back,
62    /// Go forward in history
63    Forward,
64    /// Replace current page without adding to history
65    Replace(P),
66    /// Clear all history
67    ClearHistory,
68}
69
70impl<P: Clone + PartialEq> Router<P> {
71    /// Create a new router with an initial page
72    pub fn new(initial: P) -> Self {
73        Self {
74            current: initial,
75            history: VecDeque::new(),
76            forward_stack: Vec::new(),
77            max_history: 50,
78        }
79    }
80
81    /// Set maximum history size
82    pub fn with_max_history(mut self, max: usize) -> Self {
83        self.max_history = max;
84        self
85    }
86
87    /// Get the current page
88    pub fn current(&self) -> &P {
89        &self.current
90    }
91
92    /// Check if current page matches
93    pub fn is_at(&self, page: &P) -> bool {
94        &self.current == page
95    }
96
97    /// Navigate to a new page
98    pub fn navigate(&mut self, page: P) {
99        if self.current == page {
100            return; // Already on this page
101        }
102
103        // Push current to history
104        self.history.push_back(self.current.clone());
105        if self.history.len() > self.max_history {
106            self.history.pop_front();
107        }
108
109        // Clear forward stack on new navigation
110        self.forward_stack.clear();
111
112        self.current = page;
113    }
114
115    /// Replace current page without affecting history
116    pub fn replace(&mut self, page: P) {
117        self.current = page;
118    }
119
120    /// Go back in history
121    pub fn back(&mut self) -> bool {
122        if let Some(prev) = self.history.pop_back() {
123            self.forward_stack.push(self.current.clone());
124            self.current = prev;
125            true
126        } else {
127            false
128        }
129    }
130
131    /// Go forward in history
132    pub fn forward(&mut self) -> bool {
133        if let Some(next) = self.forward_stack.pop() {
134            self.history.push_back(self.current.clone());
135            self.current = next;
136            true
137        } else {
138            false
139        }
140    }
141
142    /// Check if can go back
143    pub fn can_back(&self) -> bool {
144        !self.history.is_empty()
145    }
146
147    /// Check if can go forward
148    pub fn can_forward(&self) -> bool {
149        !self.forward_stack.is_empty()
150    }
151
152    /// Get history length
153    pub fn history_len(&self) -> usize {
154        self.history.len()
155    }
156
157    /// Clear all history
158    pub fn clear_history(&mut self) {
159        self.history.clear();
160        self.forward_stack.clear();
161    }
162
163    /// Handle a router message
164    pub fn handle(&mut self, msg: RouterMsg<P>) {
165        match msg {
166            RouterMsg::Navigate(page) => self.navigate(page),
167            RouterMsg::Back => {
168                self.back();
169            }
170            RouterMsg::Forward => {
171                self.forward();
172            }
173            RouterMsg::Replace(page) => self.replace(page),
174            RouterMsg::ClearHistory => self.clear_history(),
175        }
176    }
177}
178
179impl<P: Default + Clone + PartialEq> Default for Router<P> {
180    fn default() -> Self {
181        Self::new(P::default())
182    }
183}
184
185// ============================================================
186// ViewCtx integration
187// ============================================================
188
189use crate::ViewCtx;
190
191impl<'a, Msg> ViewCtx<'a, Msg> {
192    /// Navigate to a page (convenience method)
193    pub fn navigate<P>(&mut self, page: P, to_msg: impl FnOnce(RouterMsg<P>) -> Msg) {
194        self.emit(to_msg(RouterMsg::Navigate(page)));
195    }
196
197    /// Go back in router history
198    pub fn router_back<P>(&mut self, to_msg: impl FnOnce(RouterMsg<P>) -> Msg) {
199        self.emit(to_msg(RouterMsg::Back));
200    }
201
202    /// Go forward in router history
203    pub fn router_forward<P>(&mut self, to_msg: impl FnOnce(RouterMsg<P>) -> Msg) {
204        self.emit(to_msg(RouterMsg::Forward));
205    }
206}
207
208// ============================================================
209// Navigation helpers
210// ============================================================
211
212/// A navigation link that renders as a button
213pub struct NavLink<'a, P> {
214    label: &'a str,
215    page: P,
216    active_style: bool,
217}
218
219impl<'a, P: Clone + PartialEq> NavLink<'a, P> {
220    pub fn new(label: &'a str, page: P) -> Self {
221        Self {
222            label,
223            page,
224            active_style: true,
225        }
226    }
227
228    /// Disable active styling
229    pub fn no_active_style(mut self) -> Self {
230        self.active_style = false;
231        self
232    }
233
234    /// Show the nav link
235    pub fn show<Msg>(
236        self,
237        ctx: &mut ViewCtx<'_, Msg>,
238        router: &Router<P>,
239        to_msg: impl FnOnce(RouterMsg<P>) -> Msg,
240    ) -> bool {
241        let is_active = router.is_at(&self.page);
242
243        let response = if self.active_style && is_active {
244            // Active style - could customize this more
245            ctx.ui
246                .add(egui::Button::new(self.label).fill(egui::Color32::from_rgb(59, 130, 246)))
247        } else {
248            ctx.ui.button(self.label)
249        };
250
251        if response.clicked() && !is_active {
252            ctx.emit(to_msg(RouterMsg::Navigate(self.page)));
253            true
254        } else {
255            false
256        }
257    }
258}
259
260/// Back button helper
261pub struct BackButton<'a> {
262    label: &'a str,
263}
264
265impl<'a> BackButton<'a> {
266    pub fn new() -> Self {
267        Self { label: "Back" }
268    }
269
270    pub fn label(mut self, label: &'a str) -> Self {
271        self.label = label;
272        self
273    }
274
275    pub fn show<P, Msg>(
276        self,
277        ctx: &mut ViewCtx<'_, Msg>,
278        router: &Router<P>,
279        to_msg: impl FnOnce(RouterMsg<P>) -> Msg,
280    ) -> bool
281    where
282        P: Clone + PartialEq,
283    {
284        let enabled = router.can_back();
285        let response = ctx.ui.add_enabled(enabled, egui::Button::new(self.label));
286
287        if response.clicked() && enabled {
288            ctx.emit(to_msg(RouterMsg::Back));
289            true
290        } else {
291            false
292        }
293    }
294}
295
296impl<'a> Default for BackButton<'a> {
297    fn default() -> Self {
298        Self::new()
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[derive(Clone, PartialEq, Debug, Default)]
307    enum TestPage {
308        #[default]
309        Home,
310        Settings,
311        Profile(u64),
312    }
313
314    #[test]
315    fn test_basic_navigation() {
316        let mut router = Router::new(TestPage::Home);
317
318        assert!(router.is_at(&TestPage::Home));
319
320        router.navigate(TestPage::Settings);
321        assert!(router.is_at(&TestPage::Settings));
322        assert!(router.can_back());
323    }
324
325    #[test]
326    fn test_back_forward() {
327        let mut router = Router::new(TestPage::Home);
328
329        router.navigate(TestPage::Settings);
330        router.navigate(TestPage::Profile(42));
331
332        assert!(router.back());
333        assert!(router.is_at(&TestPage::Settings));
334
335        assert!(router.forward());
336        assert!(router.is_at(&TestPage::Profile(42)));
337    }
338
339    #[test]
340    fn test_navigate_clears_forward() {
341        let mut router = Router::new(TestPage::Home);
342
343        router.navigate(TestPage::Settings);
344        router.back();
345
346        // New navigation should clear forward stack
347        router.navigate(TestPage::Profile(1));
348        assert!(!router.can_forward());
349    }
350
351    #[test]
352    fn test_navigate_same_page() {
353        let mut router = Router::new(TestPage::Home);
354
355        router.navigate(TestPage::Home); // Same page
356        assert!(!router.can_back()); // Should not add to history
357    }
358
359    #[test]
360    fn test_replace() {
361        let mut router = Router::new(TestPage::Home);
362
363        router.navigate(TestPage::Settings);
364        router.replace(TestPage::Profile(1));
365
366        assert!(router.is_at(&TestPage::Profile(1)));
367        assert_eq!(router.history_len(), 1); // Only Home in history
368
369        router.back();
370        assert!(router.is_at(&TestPage::Home));
371    }
372
373    #[test]
374    fn test_handle_msg() {
375        let mut router = Router::new(TestPage::Home);
376
377        router.handle(RouterMsg::Navigate(TestPage::Settings));
378        assert!(router.is_at(&TestPage::Settings));
379
380        router.handle(RouterMsg::Back);
381        assert!(router.is_at(&TestPage::Home));
382    }
383}