bubba-core 0.2.2

Core runtime for the Bubba mobile framework
Documentation
//! # UI Elements
//!
//! Core UI types produced by the `view!` macro. Every declarative tag
//! (`<button>`, `<h1>`, `<p>`, etc.) compiles down to an [`Element`] node.
//!
//! The Bubba runtime serialises the element tree to JSON and sends it to the
//! Android Java layer via JNI — exactly like React Native's UIManager.

use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, Ordering};
use crate::events::EventHandler;

/// Global element ID counter — each Element gets a unique ID for event routing.
static ELEMENT_ID_COUNTER: AtomicU32 = AtomicU32::new(1);

fn next_id() -> u32 {
    ELEMENT_ID_COUNTER.fetch_add(1, Ordering::Relaxed)
}

/// A rendered screen — the return type of every screen function.
///
/// ```rust
/// use bubba_core::ui::{Element, Screen};
///
/// pub fn home() -> Screen {
///     Screen::new(Element::h1().class("title").text("Hello"))
/// }
/// ```
pub struct Screen {
    /// The root element tree for this screen.
    pub root: Element,
    /// Optional CSS stylesheet path.
    pub stylesheet: Option<String>,
}

impl Screen {
    /// Create a new screen from a root element.
    pub fn new(root: Element) -> Self {
        Self { root, stylesheet: None }
    }

    /// Attach a CSS stylesheet path to this screen.
    pub fn with_stylesheet(mut self, path: impl Into<String>) -> Self {
        self.stylesheet = Some(path.into());
        self
    }

    /// Serialise this screen's element tree to JSON.
    /// This is what gets sent over the JNI bridge to Android.
    pub fn to_json(&self) -> String {
        self.root.to_json()
    }
}

/// The fundamental UI node. Every tag in a `view!` block becomes one of these.
#[derive(Debug)]
pub struct Element {
    /// Unique ID for event routing back from Java → Rust.
    pub id: u32,
    /// Tag name: "h1", "button", "p", "img", "input", "div", etc.
    pub tag: &'static str,
    /// CSS class string.
    pub class: Option<String>,
    /// Generic attributes (src, alt, placeholder, href, …).
    pub attrs: HashMap<&'static str, String>,
    /// Text content for leaf nodes.
    pub text: Option<String>,
    /// Child elements.
    pub children: Vec<Element>,
    /// Registered event handlers.
    pub handlers: Vec<EventHandler>,
}

impl Element {
    /// Create a new element with a unique ID.
    pub fn new(tag: &'static str) -> Self {
        Self {
            id: next_id(),
            tag,
            class: None,
            attrs: HashMap::new(),
            text: None,
            children: Vec::new(),
            handlers: Vec::new(),
        }
    }

    /// Set the `class` attribute.
    pub fn class(mut self, cls: impl Into<String>) -> Self {
        self.class = Some(cls.into());
        self
    }

    /// Set a generic attribute.
    pub fn attr(mut self, key: &'static str, value: impl Into<String>) -> Self {
        self.attrs.insert(key, value.into());
        self
    }

    /// Set text content.
    pub fn text(mut self, content: impl Into<String>) -> Self {
        self.text = Some(content.into());
        self
    }

    /// Add a child element.
    pub fn child(mut self, child: Element) -> Self {
        self.children.push(child);
        self
    }

    /// Attach an event handler.
    pub fn on(mut self, handler: EventHandler) -> Self {
        self.handlers.push(handler);
        self
    }

    // ── Convenience constructors ──────────────────────────────────────────────

    /// `<h1>`
    pub fn h1() -> Self { Self::new("h1") }
    /// `<h2>`
    pub fn h2() -> Self { Self::new("h2") }
    /// `<h3>`
    pub fn h3() -> Self { Self::new("h3") }
    /// `<p>`
    pub fn p() -> Self { Self::new("p") }
    /// `<button>`
    pub fn button() -> Self { Self::new("button") }
    /// `<img>`
    pub fn img() -> Self { Self::new("img") }
    /// `<input>`
    pub fn input() -> Self { Self::new("input") }
    /// `<div>`
    pub fn div() -> Self { Self::new("div") }
    /// `<span>`
    pub fn span() -> Self { Self::new("span") }
    /// `<a>`
    pub fn a() -> Self { Self::new("a") }

    /// Serialise this element tree to JSON for the JNI bridge.
    ///
    /// Produces:
    /// ```json
    /// {
    ///   "id": 1,
    ///   "tag": "button",
    ///   "class": "primary-btn",
    ///   "text": "Tap me",
    ///   "attrs": {},
    ///   "handlers": ["click"],
    ///   "children": []
    /// }
    /// ```
    pub fn to_json(&self) -> String {
        serde_json::to_string(&self.to_value()).unwrap_or_default()
    }

    /// Convert to a `serde_json::Value` recursively.
    pub fn to_value(&self) -> serde_json::Value {
        use serde_json::json;

        let children: Vec<serde_json::Value> = self.children
            .iter()
            .map(|c| c.to_value())
            .collect();

        // Only include event names — callbacks stay in Rust
        let handlers: Vec<&str> = self.handlers
            .iter()
            .map(|h| h.event)
            .collect();

        let attrs: serde_json::Map<String, serde_json::Value> = self.attrs
            .iter()
            .map(|(k, v)| (k.to_string(), json!(v)))
            .collect();

        json!({
            "id":       self.id,
            "tag":      self.tag,
            "class":    self.class,
            "text":     self.text,
            "attrs":    attrs,
            "handlers": handlers,
            "children": children,
        })
    }

    /// Collect all elements with their handlers into a flat list.
    /// Used by the event dispatcher to find the right handler by element ID.
    pub fn collect_handlers(&self, out: &mut Vec<(u32, EventHandler)>) {
        for handler in &self.handlers {
            out.push((self.id, handler.clone()));
        }
        for child in &self.children {
            child.collect_handlers(out);
        }
    }

    /// Debug render as an indented tag tree (host mode only).
    pub fn debug_render(&self, indent: usize) -> String {
        let pad = "  ".repeat(indent);
        let class_str = self.class.as_ref()
            .map(|c| format!(r#" class="{}""#, c))
            .unwrap_or_default();
        let handler_str: String = self.handlers.iter()
            .map(|h| format!(" on{}=[fn]", h.event))
            .collect();

        if self.children.is_empty() && self.text.is_none() {
            return format!("{}<{}#{}{}{} />\n", pad, self.tag, self.id, class_str, handler_str);
        }

        let mut out = format!("{}<{}#{}{}{}>", pad, self.tag, self.id, class_str, handler_str);
        if let Some(t) = &self.text { out.push_str(t); }
        if !self.children.is_empty() {
            out.push('\n');
            for child in &self.children {
                out.push_str(&child.debug_render(indent + 1));
            }
            out.push_str(&pad);
        }
        out.push_str(&format!("</{}>\n", self.tag));
        out
    }
}

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

    #[test]
    fn element_gets_unique_ids() {
        let a = Element::div();
        let b = Element::div();
        assert_ne!(a.id, b.id);
    }

    #[test]
    fn element_builder_chain() {
        let el = Element::h1().class("title").text("Hello, Bubba!");
        assert_eq!(el.tag, "h1");
        assert_eq!(el.class.as_deref(), Some("title"));
        assert_eq!(el.text.as_deref(), Some("Hello, Bubba!"));
    }

    #[test]
    fn to_json_contains_expected_fields() {
        let el = Element::button()
            .class("primary-btn")
            .text("Tap me");
        let json = el.to_json();
        assert!(json.contains("\"tag\":\"button\""));
        assert!(json.contains("\"class\":\"primary-btn\""));
        assert!(json.contains("\"text\":\"Tap me\""));
    }

    #[test]
    fn collect_handlers_finds_nested() {
        use crate::events::EventHandler;
        let el = Element::div()
            .child(
                Element::button()
                    .on(EventHandler::onclick(|_| {}))
            );
        let mut handlers = Vec::new();
        el.collect_handlers(&mut handlers);
        assert_eq!(handlers.len(), 1);
    }
}