bubba-core 0.2.2

Core runtime for the Bubba mobile framework
Documentation
//! # Navigation
//!
//! Bubba's built-in navigation system. Screens are pushed and popped from a
//! stack — back-button support is free.
//!
//! ## Usage
//! ```rust,ignore
//! // In a view! block:
//! onclick = navigate(Profile)
//! ```
//! Under the hood, `navigate(Profile)` is sugar for:
//! ```rust,ignore
//! EventHandler::onclick(|_| navigate_to("Profile", Profile))
//! ```

use std::sync::{Arc, Mutex};
use crate::ui::Screen;

/// Signature of a screen constructor.
pub type ScreenFn = fn() -> Screen;

/// A navigation entry on the stack.
#[derive(Clone)]
pub struct ScreenEntry {
    /// Human-readable name of the screen (used for debugging / transitions).
    pub name: &'static str,
    /// The constructor for this screen.
    pub build: ScreenFn,
}

/// The global navigation stack.
///
/// Bubba maintains a single, app-wide stack. You rarely interact with this
/// directly — use the `navigate()` and `navigate_back()` free functions instead.
pub struct NavigationStack {
    stack: Mutex<Vec<ScreenEntry>>,
}

impl NavigationStack {
    /// Create an empty navigation stack.
    pub fn new() -> Self {
        Self {
            stack: Mutex::new(Vec::new()),
        }
    }

    /// Push a new screen onto the stack.
    pub fn push(&self, entry: ScreenEntry) {
        log::info!("[Bubba Nav] → {}", entry.name);
        self.stack.lock().unwrap().push(entry);
    }

    /// Pop the current screen (back navigation).
    /// Returns `false` if the stack has only one screen (can't go further back).
    pub fn pop(&self) -> bool {
        let mut stack = self.stack.lock().unwrap();
        if stack.len() <= 1 {
            log::warn!("[Bubba Nav] Cannot pop root screen.");
            return false;
        }
        let exiting = stack.pop().unwrap();
        log::info!("[Bubba Nav] ← {}", exiting.name);
        true
    }

    /// Build and return the current top-of-stack screen.
    pub fn current(&self) -> Option<Screen> {
        let stack = self.stack.lock().unwrap();
        stack.last().map(|e| (e.build)())
    }

    /// How many screens are on the stack.
    pub fn depth(&self) -> usize {
        self.stack.lock().unwrap().len()
    }

    /// Names of all screens on the stack, bottom → top.
    pub fn breadcrumbs(&self) -> Vec<&'static str> {
        self.stack.lock().unwrap().iter().map(|e| e.name).collect()
    }
}

impl Default for NavigationStack {
    fn default() -> Self {
        Self::new()
    }
}

// ── Global singleton ──────────────────────────────────────────────────────────

use std::sync::OnceLock;

static NAV_STACK: OnceLock<Arc<NavigationStack>> = OnceLock::new();

/// Access the global navigation stack.
pub fn global_stack() -> Arc<NavigationStack> {
    Arc::clone(NAV_STACK.get_or_init(|| Arc::new(NavigationStack::new())))
}

/// Navigate to a screen by pushing it onto the global stack.
///
/// This is what `navigate(ScreenName)` in `view!` desugars to.
///
/// ```rust
/// use bubba_core::ui::{Element, Screen};
/// use bubba_core::navigation::navigate_to;
///
/// fn my_screen() -> Screen {
///     Screen::new(Element::div())
/// }
///
/// navigate_to("MyScreen", my_screen);
/// ```
pub fn navigate_to(name: &'static str, build: ScreenFn) {
    global_stack().push(ScreenEntry { name, build });
    // Tell the Android bridge a navigation happened — Java will call nativeRender() again
    crate::runtime::on_navigate();
}

/// Go back one screen.
pub fn navigate_back() -> bool {
    let result = global_stack().pop();
    if result { crate::runtime::on_navigate(); }
    result
}

/// The `navigate()` function referenced inside `view!` blocks.
/// The macro desugars `navigate(Profile)` into a closure that calls this.
#[macro_export]
macro_rules! navigate {
    ($screen:ident) => {
        $crate::navigation::navigate_to(stringify!($screen), $screen)
    };
}

// Re-export for use in prelude
pub use navigate_to as navigate;

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ui::{Element, Screen};

    fn dummy_screen() -> Screen {
        Screen::new(Element::div().text("dummy"))
    }

    #[test]
    fn push_and_pop() {
        let stack = NavigationStack::new();
        stack.push(ScreenEntry { name: "Home", build: dummy_screen });
        assert_eq!(stack.depth(), 1);
        stack.push(ScreenEntry { name: "Profile", build: dummy_screen });
        assert_eq!(stack.depth(), 2);
        assert!(stack.pop());
        assert_eq!(stack.depth(), 1);
        // Cannot pop root
        assert!(!stack.pop());
    }
}