Skip to main content

bubba_core/runtime/
mod.rs

1//! # Runtime
2//!
3//! The Bubba runtime — bridges Rust UI logic to Android native views,
4//! exactly like React Native's bridge between JS and UIManager.
5//!
6//! ## Architecture
7//!
8//! ```
9//! Rust view! macro  →  Element tree  →  JSON
10//!                                         ↓
11//!                                    JNI bridge
12//!                                         ↓
13//!                                  ViewInflater.java
14//!                                         ↓
15//!                            Real Android Views (TextView, Button…)
16//!                                         ↓
17//!                               User taps button
18//!                                         ↓
19//!                           nativeOnEvent(id, "click", "")
20//!                                         ↓
21//!                          Rust EventDispatcher fires handler
22//!                                         ↓
23//!                          navigate() → new Element tree → JSON
24//!                                         ↓
25//!                               Java re-inflates Views
26//! ```
27
28use anyhow::Result;
29use std::sync::{Arc, Mutex, OnceLock};
30use tokio::runtime::Runtime as TokioRuntime;
31use crate::events::{EventHandler, BubbaEvent};
32use crate::navigation::{global_stack, navigate_to};
33use crate::ui::Screen;
34
35// ── Global state ──────────────────────────────────────────────────────────────
36
37/// All active event handlers from the current screen, keyed by element ID.
38/// Updated on every render. The Java bridge looks up handlers here on events.
39static HANDLERS: OnceLock<Mutex<Vec<(u32, EventHandler)>>> = OnceLock::new();
40
41/// Whether a navigation event occurred since the last render.
42static DID_NAVIGATE: Mutex<bool> = Mutex::new(false);
43
44fn handlers() -> &'static Mutex<Vec<(u32, EventHandler)>> {
45    HANDLERS.get_or_init(|| Mutex::new(Vec::new()))
46}
47
48fn mark_navigated() {
49    *DID_NAVIGATE.lock().unwrap() = true;
50}
51
52// ── Runtime ───────────────────────────────────────────────────────────────────
53
54/// The Bubba runtime. One per process.
55pub struct Runtime {
56    tokio: Arc<TokioRuntime>,
57}
58
59impl Runtime {
60    /// Boot the runtime.
61    pub fn new() -> Result<Self> {
62        let tokio = tokio::runtime::Builder::new_multi_thread()
63            .worker_threads(2)
64            .enable_all()
65            .build()?;
66        Ok(Self { tokio: Arc::new(tokio) })
67    }
68
69    /// Register the root screen and start the platform loop.
70    pub fn launch(&self, root_name: &'static str, root: fn() -> Screen) {
71        log::info!("[Bubba] Launching. Root: {}", root_name);
72        navigate_to(root_name, root);
73
74        #[cfg(target_os = "android")]
75        {
76            // On Android the loop is event-driven from Java callbacks.
77            // We just sit here — Java calls nativeRender() and nativeOnEvent().
78            log::info!("[Bubba] Android bridge ready. Waiting for Java callbacks.");
79        }
80
81        #[cfg(not(target_os = "android"))]
82        {
83            log::info!("[Bubba] Host mode.");
84            self.host_render();
85        }
86    }
87
88    /// Spawn an async task.
89    pub fn spawn_task<F>(&self, fut: F)
90    where F: std::future::Future<Output = ()> + Send + 'static {
91        self.tokio.spawn(fut);
92    }
93
94    #[cfg(not(target_os = "android"))]
95    fn host_render(&self) {
96        if let Some(screen) = global_stack().current() {
97            println!("{}", screen.root.debug_render(0));
98        }
99    }
100}
101
102impl Default for Runtime {
103    fn default() -> Self { Self::new().expect("Failed to boot Bubba runtime") }
104}
105
106// ── Render bridge ─────────────────────────────────────────────────────────────
107
108/// Build the current screen's element tree, register its handlers,
109/// and return the JSON string to send to Java's ViewInflater.
110///
111/// Called by Java: `BubbaBridge.nativeRender()`
112pub fn render_current_to_json() -> String {
113    let screen = match global_stack().current() {
114        Some(s) => s,
115        None => return "{}".to_string(),
116    };
117
118    // Collect all handlers from this render into the global table
119    let mut new_handlers: Vec<(u32, EventHandler)> = Vec::new();
120    screen.root.collect_handlers(&mut new_handlers);
121    *handlers().lock().unwrap() = new_handlers;
122
123    // Clear navigation flag
124    *DID_NAVIGATE.lock().unwrap() = false;
125
126    screen.to_json()
127}
128
129/// Returns true if a navigation event occurred since the last render.
130/// Called by Java after every nativeOnEvent() to know if it should re-render.
131pub fn did_navigate() -> bool {
132    *DID_NAVIGATE.lock().unwrap()
133}
134
135// ── Event bridge ──────────────────────────────────────────────────────────────
136
137/// Dispatch an event from Java into the matching Rust EventHandler.
138///
139/// Called by Java: `BubbaBridge.nativeOnEvent(elementId, eventKind, value)`
140pub fn dispatch_event(element_id: u32, event_kind: &str, value: &str) {
141    log::debug!("[Bubba] Event: {} on element #{}", event_kind, element_id);
142
143    let handlers_guard = handlers().lock().unwrap();
144    let matching: Vec<EventHandler> = handlers_guard
145        .iter()
146        .filter(|(id, h)| *id == element_id && h.event == event_kind)
147        .map(|(_, h)| h.clone())
148        .collect();
149    drop(handlers_guard); // release lock before firing handlers
150
151    let event = BubbaEvent {
152        kind: Box::leak(event_kind.to_string().into_boxed_str()),
153        value: if value.is_empty() { None } else { Some(value.to_string()) },
154        key: None,
155    };
156
157    for handler in matching {
158        handler.dispatch(event.clone());
159    }
160}
161
162// ── Built-in actions ─────────────────────────────────────────────────────────
163
164/// Show a native alert dialog.
165///
166/// ```rust
167/// bubba_core::runtime::alert("Hello!");
168/// ```
169pub fn alert(message: impl Into<String>) {
170    let msg = message.into();
171    log::info!("[Bubba alert] {}", msg);
172    #[cfg(not(target_os = "android"))]
173    println!("[ALERT] {}", msg);
174    // On Android: Java side handles this via nativeAlert() callback
175}
176
177/// Log a debug message.
178///
179/// ```rust
180/// bubba_core::runtime::log_msg("Hello!");
181/// ```
182pub fn log_msg(message: impl Into<String>) {
183    log::debug!("[Bubba] {}", message.into());
184}
185
186/// Spawn a future on the async executor.
187pub fn spawn<F>(fut: F)
188where F: std::future::Future<Output = ()> + Send + 'static {
189    GLOBAL_RUNTIME.with(|rt| {
190        if let Some(r) = rt.borrow().as_ref() {
191            r.tokio.spawn(fut);
192        }
193    });
194}
195
196use std::cell::RefCell;
197thread_local! {
198    static GLOBAL_RUNTIME: RefCell<Option<Arc<Runtime>>> = RefCell::new(None);
199}
200
201/// Register the runtime for use by `spawn()`.
202pub fn set_global_runtime(rt: Arc<Runtime>) {
203    GLOBAL_RUNTIME.with(|r| { *r.borrow_mut() = Some(rt); });
204}
205
206// ── Navigation hook ───────────────────────────────────────────────────────────
207
208/// Called internally when `navigate()` fires so Java knows to re-render.
209pub fn on_navigate() {
210    mark_navigated();
211}
212
213/// Bubba runtime version.
214pub const BUBBA_VERSION: &str = env!("CARGO_PKG_VERSION");
215
216#[allow(dead_code)]
217fn android_alert(_msg: &str) {}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn runtime_boots() {
225        let rt = Runtime::new().expect("runtime should boot");
226        drop(rt);
227    }
228
229    #[test]
230    fn alert_does_not_panic() { alert("test"); }
231
232    #[test]
233    fn log_does_not_panic() { log_msg("test"); }
234
235    #[test]
236    fn render_returns_json() {
237        use crate::ui::{Element, Screen};
238        use crate::navigation::navigate_to;
239
240        fn test_screen() -> Screen {
241            Screen::new(Element::h1().text("Hello"))
242        }
243        navigate_to("Test", test_screen);
244        let json = render_current_to_json();
245        assert!(json.contains("h1") || json.contains("div"));
246    }
247}