bubba_core/navigation/
mod.rs1use std::sync::{Arc, Mutex};
17use crate::ui::Screen;
18
19pub type ScreenFn = fn() -> Screen;
21
22#[derive(Clone)]
24pub struct ScreenEntry {
25 pub name: &'static str,
27 pub build: ScreenFn,
29}
30
31pub struct NavigationStack {
36 stack: Mutex<Vec<ScreenEntry>>,
37}
38
39impl NavigationStack {
40 pub fn new() -> Self {
42 Self {
43 stack: Mutex::new(Vec::new()),
44 }
45 }
46
47 pub fn push(&self, entry: ScreenEntry) {
49 log::info!("[Bubba Nav] → {}", entry.name);
50 self.stack.lock().unwrap().push(entry);
51 }
52
53 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 pub fn current(&self) -> Option<Screen> {
68 let stack = self.stack.lock().unwrap();
69 stack.last().map(|e| (e.build)())
70 }
71
72 pub fn depth(&self) -> usize {
74 self.stack.lock().unwrap().len()
75 }
76
77 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
89use std::sync::OnceLock;
92
93static NAV_STACK: OnceLock<Arc<NavigationStack>> = OnceLock::new();
94
95pub fn global_stack() -> Arc<NavigationStack> {
97 Arc::clone(NAV_STACK.get_or_init(|| Arc::new(NavigationStack::new())))
98}
99
100pub fn navigate_to(name: &'static str, build: ScreenFn) {
115 global_stack().push(ScreenEntry { name, build });
116}
117
118pub fn navigate_back() -> bool {
120 global_stack().pop()
121}
122
123#[macro_export]
126macro_rules! navigate {
127 ($screen:ident) => {
128 $crate::navigation::navigate_to(stringify!($screen), $screen)
129 };
130}
131
132pub 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 assert!(!stack.pop());
155 }
156}