bubba-core 0.2.2

Core runtime for the Bubba mobile framework
Documentation
//! # Runtime
//!
//! The Bubba runtime — bridges Rust UI logic to Android native views,
//! exactly like React Native's bridge between JS and UIManager.
//!
//! ## Architecture
//!
//! ```
//! Rust view! macro  →  Element tree  →  JSON
//!//!                                    JNI bridge
//!//!                                  ViewInflater.java
//!//!                            Real Android Views (TextView, Button…)
//!//!                               User taps button
//!//!                           nativeOnEvent(id, "click", "")
//!//!                          Rust EventDispatcher fires handler
//!//!                          navigate() → new Element tree → JSON
//!//!                               Java re-inflates Views
//! ```

use anyhow::Result;
use std::sync::{Arc, Mutex, OnceLock};
use tokio::runtime::Runtime as TokioRuntime;
use crate::events::{EventHandler, BubbaEvent};
use crate::navigation::{global_stack, navigate_to};
use crate::ui::Screen;

// ── Global state ──────────────────────────────────────────────────────────────

/// All active event handlers from the current screen, keyed by element ID.
/// Updated on every render. The Java bridge looks up handlers here on events.
static HANDLERS: OnceLock<Mutex<Vec<(u32, EventHandler)>>> = OnceLock::new();

/// Whether a navigation event occurred since the last render.
static DID_NAVIGATE: Mutex<bool> = Mutex::new(false);

fn handlers() -> &'static Mutex<Vec<(u32, EventHandler)>> {
    HANDLERS.get_or_init(|| Mutex::new(Vec::new()))
}

fn mark_navigated() {
    *DID_NAVIGATE.lock().unwrap() = true;
}

// ── Runtime ───────────────────────────────────────────────────────────────────

/// The Bubba runtime. One per process.
pub struct Runtime {
    tokio: Arc<TokioRuntime>,
}

impl Runtime {
    /// Boot the runtime.
    pub fn new() -> Result<Self> {
        let tokio = tokio::runtime::Builder::new_multi_thread()
            .worker_threads(2)
            .enable_all()
            .build()?;
        Ok(Self { tokio: Arc::new(tokio) })
    }

    /// Register the root screen and start the platform loop.
    pub fn launch(&self, root_name: &'static str, root: fn() -> Screen) {
        log::info!("[Bubba] Launching. Root: {}", root_name);
        navigate_to(root_name, root);

        #[cfg(target_os = "android")]
        {
            // On Android the loop is event-driven from Java callbacks.
            // We just sit here — Java calls nativeRender() and nativeOnEvent().
            log::info!("[Bubba] Android bridge ready. Waiting for Java callbacks.");
        }

        #[cfg(not(target_os = "android"))]
        {
            log::info!("[Bubba] Host mode.");
            self.host_render();
        }
    }

    /// Spawn an async task.
    pub fn spawn_task<F>(&self, fut: F)
    where F: std::future::Future<Output = ()> + Send + 'static {
        self.tokio.spawn(fut);
    }

    #[cfg(not(target_os = "android"))]
    fn host_render(&self) {
        if let Some(screen) = global_stack().current() {
            println!("{}", screen.root.debug_render(0));
        }
    }
}

impl Default for Runtime {
    fn default() -> Self { Self::new().expect("Failed to boot Bubba runtime") }
}

// ── Render bridge ─────────────────────────────────────────────────────────────

/// Build the current screen's element tree, register its handlers,
/// and return the JSON string to send to Java's ViewInflater.
///
/// Called by Java: `BubbaBridge.nativeRender()`
pub fn render_current_to_json() -> String {
    let screen = match global_stack().current() {
        Some(s) => s,
        None => return "{}".to_string(),
    };

    // Collect all handlers from this render into the global table
    let mut new_handlers: Vec<(u32, EventHandler)> = Vec::new();
    screen.root.collect_handlers(&mut new_handlers);
    *handlers().lock().unwrap() = new_handlers;

    // Clear navigation flag
    *DID_NAVIGATE.lock().unwrap() = false;

    screen.to_json()
}

/// Returns true if a navigation event occurred since the last render.
/// Called by Java after every nativeOnEvent() to know if it should re-render.
pub fn did_navigate() -> bool {
    *DID_NAVIGATE.lock().unwrap()
}

// ── Event bridge ──────────────────────────────────────────────────────────────

/// Dispatch an event from Java into the matching Rust EventHandler.
///
/// Called by Java: `BubbaBridge.nativeOnEvent(elementId, eventKind, value)`
pub fn dispatch_event(element_id: u32, event_kind: &str, value: &str) {
    log::debug!("[Bubba] Event: {} on element #{}", event_kind, element_id);

    let handlers_guard = handlers().lock().unwrap();
    let matching: Vec<EventHandler> = handlers_guard
        .iter()
        .filter(|(id, h)| *id == element_id && h.event == event_kind)
        .map(|(_, h)| h.clone())
        .collect();
    drop(handlers_guard); // release lock before firing handlers

    let event = BubbaEvent {
        kind: Box::leak(event_kind.to_string().into_boxed_str()),
        value: if value.is_empty() { None } else { Some(value.to_string()) },
        key: None,
    };

    for handler in matching {
        handler.dispatch(event.clone());
    }
}

// ── Built-in actions ─────────────────────────────────────────────────────────

/// Show a native alert dialog.
///
/// ```rust
/// bubba_core::runtime::alert("Hello!");
/// ```
pub fn alert(message: impl Into<String>) {
    let msg = message.into();
    log::info!("[Bubba alert] {}", msg);
    #[cfg(not(target_os = "android"))]
    println!("[ALERT] {}", msg);
    // On Android: Java side handles this via nativeAlert() callback
}

/// Log a debug message.
///
/// ```rust
/// bubba_core::runtime::log_msg("Hello!");
/// ```
pub fn log_msg(message: impl Into<String>) {
    log::debug!("[Bubba] {}", message.into());
}

/// Spawn a future on the async executor.
pub fn spawn<F>(fut: F)
where F: std::future::Future<Output = ()> + Send + 'static {
    GLOBAL_RUNTIME.with(|rt| {
        if let Some(r) = rt.borrow().as_ref() {
            r.tokio.spawn(fut);
        }
    });
}

use std::cell::RefCell;
thread_local! {
    static GLOBAL_RUNTIME: RefCell<Option<Arc<Runtime>>> = RefCell::new(None);
}

/// Register the runtime for use by `spawn()`.
pub fn set_global_runtime(rt: Arc<Runtime>) {
    GLOBAL_RUNTIME.with(|r| { *r.borrow_mut() = Some(rt); });
}

// ── Navigation hook ───────────────────────────────────────────────────────────

/// Called internally when `navigate()` fires so Java knows to re-render.
pub fn on_navigate() {
    mark_navigated();
}

/// Bubba runtime version.
pub const BUBBA_VERSION: &str = env!("CARGO_PKG_VERSION");

#[allow(dead_code)]
fn android_alert(_msg: &str) {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn runtime_boots() {
        let rt = Runtime::new().expect("runtime should boot");
        drop(rt);
    }

    #[test]
    fn alert_does_not_panic() { alert("test"); }

    #[test]
    fn log_does_not_panic() { log_msg("test"); }

    #[test]
    fn render_returns_json() {
        use crate::ui::{Element, Screen};
        use crate::navigation::navigate_to;

        fn test_screen() -> Screen {
            Screen::new(Element::h1().text("Hello"))
        }
        navigate_to("Test", test_screen);
        let json = render_current_to_json();
        assert!(json.contains("h1") || json.contains("div"));
    }
}