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;
static HANDLERS: OnceLock<Mutex<Vec<(u32, EventHandler)>>> = OnceLock::new();
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;
}
pub struct Runtime {
tokio: Arc<TokioRuntime>,
}
impl 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) })
}
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")]
{
log::info!("[Bubba] Android bridge ready. Waiting for Java callbacks.");
}
#[cfg(not(target_os = "android"))]
{
log::info!("[Bubba] Host mode.");
self.host_render();
}
}
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") }
}
pub fn render_current_to_json() -> String {
let screen = match global_stack().current() {
Some(s) => s,
None => return "{}".to_string(),
};
let mut new_handlers: Vec<(u32, EventHandler)> = Vec::new();
screen.root.collect_handlers(&mut new_handlers);
*handlers().lock().unwrap() = new_handlers;
*DID_NAVIGATE.lock().unwrap() = false;
screen.to_json()
}
pub fn did_navigate() -> bool {
*DID_NAVIGATE.lock().unwrap()
}
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);
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());
}
}
pub fn alert(message: impl Into<String>) {
let msg = message.into();
log::info!("[Bubba alert] {}", msg);
#[cfg(not(target_os = "android"))]
println!("[ALERT] {}", msg);
}
pub fn log_msg(message: impl Into<String>) {
log::debug!("[Bubba] {}", message.into());
}
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);
}
pub fn set_global_runtime(rt: Arc<Runtime>) {
GLOBAL_RUNTIME.with(|r| { *r.borrow_mut() = Some(rt); });
}
pub fn on_navigate() {
mark_navigated();
}
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"));
}
}