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    // Tell the Android bridge a navigation happened — Java will call nativeRender() again
117    crate::runtime::on_navigate();
118}
119
120/// Go back one screen.
121pub fn navigate_back() -> bool {
122    let result = global_stack().pop();
123    if result { crate::runtime::on_navigate(); }
124    result
125}
126
127/// The `navigate()` function referenced inside `view!` blocks.
128/// The macro desugars `navigate(Profile)` into a closure that calls this.
129#[macro_export]
130macro_rules! navigate {
131    ($screen:ident) => {
132        $crate::navigation::navigate_to(stringify!($screen), $screen)
133    };
134}
135
136// Re-export for use in prelude
137pub use navigate_to as navigate;
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::ui::{Element, Screen};
143
144    fn dummy_screen() -> Screen {
145        Screen::new(Element::div().text("dummy"))
146    }
147
148    #[test]
149    fn push_and_pop() {
150        let stack = NavigationStack::new();
151        stack.push(ScreenEntry { name: "Home", build: dummy_screen });
152        assert_eq!(stack.depth(), 1);
153        stack.push(ScreenEntry { name: "Profile", build: dummy_screen });
154        assert_eq!(stack.depth(), 2);
155        assert!(stack.pop());
156        assert_eq!(stack.depth(), 1);
157        // Cannot pop root
158        assert!(!stack.pop());
159    }
160}