Skip to main content

bubba_core/navigation/
mod.rs

1//! # Navigation
2//!
3//! Bubba's built-in navigation system. Screens are pushed and popped from a
4//! stack — back-button support is free.
5//!
6//! ## Usage
7//! ```rust,ignore
8//! // In a view! block:
9//! onclick = navigate(Profile)
10//! ```
11//! Under the hood, `navigate(Profile)` is sugar for:
12//! ```rust,ignore
13//! EventHandler::onclick(|_| navigate_to("Profile", Profile))
14//! ```
15
16use std::sync::{Arc, Mutex};
17use crate::ui::Screen;
18
19/// Signature of a screen constructor.
20pub type ScreenFn = fn() -> Screen;
21
22/// A navigation entry on the stack.
23#[derive(Clone)]
24pub struct ScreenEntry {
25    /// Human-readable name of the screen (used for debugging / transitions).
26    pub name: &'static str,
27    /// The constructor for this screen.
28    pub build: ScreenFn,
29}
30
31/// The global navigation stack.
32///
33/// Bubba maintains a single, app-wide stack. You rarely interact with this
34/// directly — use the `navigate()` and `navigate_back()` free functions instead.
35pub struct NavigationStack {
36    stack: Mutex<Vec<ScreenEntry>>,
37}
38
39impl NavigationStack {
40    /// Create an empty navigation stack.
41    pub fn new() -> Self {
42        Self {
43            stack: Mutex::new(Vec::new()),
44        }
45    }
46
47    /// Push a new screen onto the stack.
48    pub fn push(&self, entry: ScreenEntry) {
49        log::info!("[Bubba Nav] → {}", entry.name);
50        self.stack.lock().unwrap().push(entry);
51    }
52
53    /// Pop the current screen (back navigation).
54    /// Returns `false` if the stack has only one screen (can't go further back).
55    pub fn pop(&self) -> bool {
56        let mut stack = self.stack.lock().unwrap();
57        if stack.len() <= 1 {
58            log::warn!("[Bubba Nav] Cannot pop root screen.");
59            return false;
60        }
61        let exiting = stack.pop().unwrap();
62        log::info!("[Bubba Nav] ← {}", exiting.name);
63        true
64    }
65
66    /// Build and return the current top-of-stack screen.
67    pub fn current(&self) -> Option<Screen> {
68        let stack = self.stack.lock().unwrap();
69        stack.last().map(|e| (e.build)())
70    }
71
72    /// How many screens are on the stack.
73    pub fn depth(&self) -> usize {
74        self.stack.lock().unwrap().len()
75    }
76
77    /// Names of all screens on the stack, bottom → top.
78    pub fn breadcrumbs(&self) -> Vec<&'static str> {
79        self.stack.lock().unwrap().iter().map(|e| e.name).collect()
80    }
81}
82
83impl Default for NavigationStack {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89// ── Global singleton ──────────────────────────────────────────────────────────
90
91use std::sync::OnceLock;
92
93static NAV_STACK: OnceLock<Arc<NavigationStack>> = OnceLock::new();
94
95/// Access the global navigation stack.
96pub fn global_stack() -> Arc<NavigationStack> {
97    Arc::clone(NAV_STACK.get_or_init(|| Arc::new(NavigationStack::new())))
98}
99
100/// Navigate to a screen by pushing it onto the global stack.
101///
102/// This is what `navigate(ScreenName)` in `view!` desugars to.
103///
104/// ```rust
105/// use bubba_core::ui::{Element, Screen};
106/// use bubba_core::navigation::navigate_to;
107///
108/// fn my_screen() -> Screen {
109///     Screen::new(Element::div())
110/// }
111///
112/// navigate_to("MyScreen", my_screen);
113/// ```
114pub fn navigate_to(name: &'static str, build: ScreenFn) {
115    global_stack().push(ScreenEntry { name, build });
116}
117
118/// Go back one screen.
119pub fn navigate_back() -> bool {
120    global_stack().pop()
121}
122
123/// The `navigate()` function referenced inside `view!` blocks.
124/// The macro desugars `navigate(Profile)` into a closure that calls this.
125#[macro_export]
126macro_rules! navigate {
127    ($screen:ident) => {
128        $crate::navigation::navigate_to(stringify!($screen), $screen)
129    };
130}
131
132// Re-export for use in prelude
133pub use navigate_to as navigate;
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::ui::{Element, Screen};
139
140    fn dummy_screen() -> Screen {
141        Screen::new(Element::div().text("dummy"))
142    }
143
144    #[test]
145    fn push_and_pop() {
146        let stack = NavigationStack::new();
147        stack.push(ScreenEntry { name: "Home", build: dummy_screen });
148        assert_eq!(stack.depth(), 1);
149        stack.push(ScreenEntry { name: "Profile", build: dummy_screen });
150        assert_eq!(stack.depth(), 2);
151        assert!(stack.pop());
152        assert_eq!(stack.depth(), 1);
153        // Cannot pop root
154        assert!(!stack.pop());
155    }
156}