bubba_core/runtime/
mod.rs1use 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
35static HANDLERS: OnceLock<Mutex<Vec<(u32, EventHandler)>>> = OnceLock::new();
40
41static 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
52pub struct Runtime {
56 tokio: Arc<TokioRuntime>,
57}
58
59impl Runtime {
60 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 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 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 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
106pub 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 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 *DID_NAVIGATE.lock().unwrap() = false;
125
126 screen.to_json()
127}
128
129pub fn did_navigate() -> bool {
132 *DID_NAVIGATE.lock().unwrap()
133}
134
135pub 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); 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
162pub 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 }
176
177pub fn log_msg(message: impl Into<String>) {
183 log::debug!("[Bubba] {}", message.into());
184}
185
186pub 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
201pub fn set_global_runtime(rt: Arc<Runtime>) {
203 GLOBAL_RUNTIME.with(|r| { *r.borrow_mut() = Some(rt); });
204}
205
206pub fn on_navigate() {
210 mark_navigated();
211}
212
213pub 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}