Skip to main content

bubba_core/runtime/
mod.rs

1//! # Runtime
2//!
3//! The Bubba app runtime: lifecycle hooks, built-in actions (`alert`, `log`),
4//! async task scheduling, and the Android JNI bridge.
5
6use anyhow::Result;
7use std::sync::Arc;
8use tokio::runtime::Runtime as TokioRuntime;
9use crate::navigation::{global_stack, navigate_to};
10use crate::ui::Screen;
11
12/// The Bubba runtime instance. One per application process.
13pub struct Runtime {
14    tokio: Arc<TokioRuntime>,
15}
16
17impl Runtime {
18    /// Boot the Bubba runtime.
19    pub fn new() -> Result<Self> {
20        let tokio = tokio::runtime::Builder::new_multi_thread()
21            .worker_threads(2)
22            .enable_all()
23            .build()?;
24        Ok(Self {
25            tokio: Arc::new(tokio),
26        })
27    }
28
29    /// Launch the app with the given root screen.
30    pub fn launch(&self, root_name: &'static str, root: fn() -> Screen) {
31        log::info!("[Bubba] Runtime launching. Root screen: {}", root_name);
32        navigate_to(root_name, root);
33
34        // Platform dispatch
35        #[cfg(target_os = "android")]
36        self.run_android_loop();
37
38        #[cfg(not(target_os = "android"))]
39        self.run_host_loop();
40    }
41
42    /// Render the current top-of-stack screen into the native view hierarchy.
43    pub fn render_current(&self) {
44        if let Some(screen) = global_stack().current() {
45            log::debug!("[Bubba] Rendering screen:\n{}", screen.root.debug_render(0));
46
47            // On Android: walk the Element tree and call JNI View constructors.
48            // On host (dev/test): print to stdout for visual inspection.
49            #[cfg(not(target_os = "android"))]
50            println!("{}", screen.root.debug_render(0));
51        }
52    }
53
54    /// Spawn an async background task.
55    ///
56    /// ```rust,ignore
57    /// spawn(async {
58    ///     let data = fetch_user_data().await;
59    ///     // update state…
60    /// });
61    /// ```
62    pub fn spawn_task<F>(&self, fut: F)
63    where
64        F: std::future::Future<Output = ()> + Send + 'static,
65    {
66        self.tokio.spawn(fut);
67    }
68
69    // ── Host dev loop ─────────────────────────────────────────────────────────
70
71    #[cfg(not(target_os = "android"))]
72    fn run_host_loop(&self) {
73        log::info!("[Bubba] Running in host mode (no Android runtime).");
74        self.render_current();
75    }
76
77    // ── Android event loop (stub — JNI bridge fills this in) ─────────────────
78
79    #[cfg(target_os = "android")]
80    fn run_android_loop(&self) {
81        log::info!("[Bubba] Android runtime active.");
82        // The actual loop is driven by the JNI callbacks from
83        // `BubbaBridge.java` → `bubba_on_event()` below.
84    }
85}
86
87impl Default for Runtime {
88    fn default() -> Self {
89        Self::new().expect("Failed to boot Bubba runtime")
90    }
91}
92
93// ── Built-in actions ─────────────────────────────────────────────────────────
94
95/// Show a native alert dialog.
96///
97/// ```rust
98/// bubba_core::runtime::alert("You tapped the button!");
99/// ```
100pub fn alert(message: impl Into<String>) {
101    let msg = message.into();
102    log::info!("[Bubba alert] {}", msg);
103
104    #[cfg(target_os = "android")]
105    android_alert(&msg);
106
107    #[cfg(not(target_os = "android"))]
108    println!("[ALERT] {}", msg);
109}
110
111/// Log a message (maps to `android.util.Log.d` on device).
112///
113/// ```rust
114/// bubba_core::runtime::log_msg("Typing…");
115/// ```
116pub fn log_msg(message: impl Into<String>) {
117    log::debug!("[Bubba log] {}", message.into());
118}
119
120/// Spawn a future on the Bubba async executor.
121/// This is the free function form used inside event handlers.
122pub fn spawn<F>(fut: F)
123where
124    F: std::future::Future<Output = ()> + Send + 'static,
125{
126    GLOBAL_RUNTIME.with(|rt| {
127        if let Some(r) = rt.borrow().as_ref() {
128            r.tokio.spawn(fut);
129        }
130    });
131}
132
133// ── Thread-local runtime handle ───────────────────────────────────────────────
134
135use std::cell::RefCell;
136
137thread_local! {
138    static GLOBAL_RUNTIME: RefCell<Option<Arc<Runtime>>> = RefCell::new(None);
139}
140
141/// Set the thread-local runtime reference (called by `cargo bubba` bootstrap).
142pub fn set_global_runtime(rt: Arc<Runtime>) {
143    GLOBAL_RUNTIME.with(|r| {
144        *r.borrow_mut() = Some(rt);
145    });
146}
147
148// ── Android JNI bridge ────────────────────────────────────────────────────────
149
150#[cfg(all(target_os = "android", feature = "android"))]
151mod android {
152    use jni::JNIEnv;
153    use jni::objects::{JClass, JString};
154
155    /// Called from Java: `BubbaBridge.nativeOnEvent(elementId, eventKind, value)`
156    #[no_mangle]
157    pub extern "system" fn Java_rs_bubba_BubbaBridge_nativeOnEvent(
158        env: JNIEnv,
159        _class: JClass,
160        element_id: JString,
161        event_kind: JString,
162        value: JString,
163    ) {
164        let element_id: String = env.get_string(element_id).unwrap().into();
165        let event_kind: String = env.get_string(event_kind).unwrap().into();
166        let value: String = env.get_string(value).unwrap().into();
167
168        log::debug!("[JNI] event: {} on #{}", event_kind, element_id);
169
170        // Route to Bubba event dispatcher
171        // GLOBAL_DISPATCHER.push(element_id, BubbaEvent { kind: &event_kind, value: Some(value), key: None });
172    }
173
174    /// Show a native Android AlertDialog from Rust.
175    pub fn android_alert(message: &str) {
176        // JNI call to `BubbaBridge.showAlert(message)` would go here.
177        log::info!("[Android] Alert: {}", message);
178    }
179}
180
181#[cfg(not(all(target_os = "android", feature = "android")))]
182#[allow(dead_code)]
183fn android_alert(_msg: &str) {}
184
185// ── Build information ─────────────────────────────────────────────────────────
186
187/// Bubba runtime version.
188pub const BUBBA_VERSION: &str = env!("CARGO_PKG_VERSION");
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn runtime_boots() {
196        let rt = Runtime::new().expect("runtime should boot");
197        // Just verify it didn't panic.
198        drop(rt);
199    }
200
201    #[test]
202    fn alert_does_not_panic() {
203        alert("test alert");
204    }
205
206    #[test]
207    fn log_does_not_panic() {
208        log_msg("test log");
209    }
210}