Skip to main content

bubba_core/ui/
mod.rs

1//! # UI Elements
2//!
3//! Core UI types produced by the `view!` macro. Every declarative tag
4//! (`<button>`, `<h1>`, `<p>`, etc.) compiles down to an [`Element`] node.
5//!
6//! The Bubba runtime serialises the element tree to JSON and sends it to the
7//! Android Java layer via JNI — exactly like React Native's UIManager.
8
9use std::collections::HashMap;
10use std::sync::atomic::{AtomicU32, Ordering};
11use crate::events::EventHandler;
12
13/// Global element ID counter — each Element gets a unique ID for event routing.
14static ELEMENT_ID_COUNTER: AtomicU32 = AtomicU32::new(1);
15
16fn next_id() -> u32 {
17    ELEMENT_ID_COUNTER.fetch_add(1, Ordering::Relaxed)
18}
19
20/// A rendered screen — the return type of every screen function.
21///
22/// ```rust
23/// use bubba_core::ui::{Element, Screen};
24///
25/// pub fn home() -> Screen {
26///     Screen::new(Element::h1().class("title").text("Hello"))
27/// }
28/// ```
29pub struct Screen {
30    /// The root element tree for this screen.
31    pub root: Element,
32    /// Optional CSS stylesheet path.
33    pub stylesheet: Option<String>,
34}
35
36impl Screen {
37    /// Create a new screen from a root element.
38    pub fn new(root: Element) -> Self {
39        Self { root, stylesheet: None }
40    }
41
42    /// Attach a CSS stylesheet path to this screen.
43    pub fn with_stylesheet(mut self, path: impl Into<String>) -> Self {
44        self.stylesheet = Some(path.into());
45        self
46    }
47
48    /// Serialise this screen's element tree to JSON.
49    /// This is what gets sent over the JNI bridge to Android.
50    pub fn to_json(&self) -> String {
51        self.root.to_json()
52    }
53}
54
55/// The fundamental UI node. Every tag in a `view!` block becomes one of these.
56#[derive(Debug)]
57pub struct Element {
58    /// Unique ID for event routing back from Java → Rust.
59    pub id: u32,
60    /// Tag name: "h1", "button", "p", "img", "input", "div", etc.
61    pub tag: &'static str,
62    /// CSS class string.
63    pub class: Option<String>,
64    /// Generic attributes (src, alt, placeholder, href, …).
65    pub attrs: HashMap<&'static str, String>,
66    /// Text content for leaf nodes.
67    pub text: Option<String>,
68    /// Child elements.
69    pub children: Vec<Element>,
70    /// Registered event handlers.
71    pub handlers: Vec<EventHandler>,
72}
73
74impl Element {
75    /// Create a new element with a unique ID.
76    pub fn new(tag: &'static str) -> Self {
77        Self {
78            id: next_id(),
79            tag,
80            class: None,
81            attrs: HashMap::new(),
82            text: None,
83            children: Vec::new(),
84            handlers: Vec::new(),
85        }
86    }
87
88    /// Set the `class` attribute.
89    pub fn class(mut self, cls: impl Into<String>) -> Self {
90        self.class = Some(cls.into());
91        self
92    }
93
94    /// Set a generic attribute.
95    pub fn attr(mut self, key: &'static str, value: impl Into<String>) -> Self {
96        self.attrs.insert(key, value.into());
97        self
98    }
99
100    /// Set text content.
101    pub fn text(mut self, content: impl Into<String>) -> Self {
102        self.text = Some(content.into());
103        self
104    }
105
106    /// Add a child element.
107    pub fn child(mut self, child: Element) -> Self {
108        self.children.push(child);
109        self
110    }
111
112    /// Attach an event handler.
113    pub fn on(mut self, handler: EventHandler) -> Self {
114        self.handlers.push(handler);
115        self
116    }
117
118    // ── Convenience constructors ──────────────────────────────────────────────
119
120    /// `<h1>`
121    pub fn h1() -> Self { Self::new("h1") }
122    /// `<h2>`
123    pub fn h2() -> Self { Self::new("h2") }
124    /// `<h3>`
125    pub fn h3() -> Self { Self::new("h3") }
126    /// `<p>`
127    pub fn p() -> Self { Self::new("p") }
128    /// `<button>`
129    pub fn button() -> Self { Self::new("button") }
130    /// `<img>`
131    pub fn img() -> Self { Self::new("img") }
132    /// `<input>`
133    pub fn input() -> Self { Self::new("input") }
134    /// `<div>`
135    pub fn div() -> Self { Self::new("div") }
136    /// `<span>`
137    pub fn span() -> Self { Self::new("span") }
138    /// `<a>`
139    pub fn a() -> Self { Self::new("a") }
140
141    /// Serialise this element tree to JSON for the JNI bridge.
142    ///
143    /// Produces:
144    /// ```json
145    /// {
146    ///   "id": 1,
147    ///   "tag": "button",
148    ///   "class": "primary-btn",
149    ///   "text": "Tap me",
150    ///   "attrs": {},
151    ///   "handlers": ["click"],
152    ///   "children": []
153    /// }
154    /// ```
155    pub fn to_json(&self) -> String {
156        serde_json::to_string(&self.to_value()).unwrap_or_default()
157    }
158
159    /// Convert to a `serde_json::Value` recursively.
160    pub fn to_value(&self) -> serde_json::Value {
161        use serde_json::json;
162
163        let children: Vec<serde_json::Value> = self.children
164            .iter()
165            .map(|c| c.to_value())
166            .collect();
167
168        // Only include event names — callbacks stay in Rust
169        let handlers: Vec<&str> = self.handlers
170            .iter()
171            .map(|h| h.event)
172            .collect();
173
174        let attrs: serde_json::Map<String, serde_json::Value> = self.attrs
175            .iter()
176            .map(|(k, v)| (k.to_string(), json!(v)))
177            .collect();
178
179        json!({
180            "id":       self.id,
181            "tag":      self.tag,
182            "class":    self.class,
183            "text":     self.text,
184            "attrs":    attrs,
185            "handlers": handlers,
186            "children": children,
187        })
188    }
189
190    /// Collect all elements with their handlers into a flat list.
191    /// Used by the event dispatcher to find the right handler by element ID.
192    pub fn collect_handlers(&self, out: &mut Vec<(u32, EventHandler)>) {
193        for handler in &self.handlers {
194            out.push((self.id, handler.clone()));
195        }
196        for child in &self.children {
197            child.collect_handlers(out);
198        }
199    }
200
201    /// Debug render as an indented tag tree (host mode only).
202    pub fn debug_render(&self, indent: usize) -> String {
203        let pad = "  ".repeat(indent);
204        let class_str = self.class.as_ref()
205            .map(|c| format!(r#" class="{}""#, c))
206            .unwrap_or_default();
207        let handler_str: String = self.handlers.iter()
208            .map(|h| format!(" on{}=[fn]", h.event))
209            .collect();
210
211        if self.children.is_empty() && self.text.is_none() {
212            return format!("{}<{}#{}{}{} />\n", pad, self.tag, self.id, class_str, handler_str);
213        }
214
215        let mut out = format!("{}<{}#{}{}{}>", pad, self.tag, self.id, class_str, handler_str);
216        if let Some(t) = &self.text { out.push_str(t); }
217        if !self.children.is_empty() {
218            out.push('\n');
219            for child in &self.children {
220                out.push_str(&child.debug_render(indent + 1));
221            }
222            out.push_str(&pad);
223        }
224        out.push_str(&format!("</{}>\n", self.tag));
225        out
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn element_gets_unique_ids() {
235        let a = Element::div();
236        let b = Element::div();
237        assert_ne!(a.id, b.id);
238    }
239
240    #[test]
241    fn element_builder_chain() {
242        let el = Element::h1().class("title").text("Hello, Bubba!");
243        assert_eq!(el.tag, "h1");
244        assert_eq!(el.class.as_deref(), Some("title"));
245        assert_eq!(el.text.as_deref(), Some("Hello, Bubba!"));
246    }
247
248    #[test]
249    fn to_json_contains_expected_fields() {
250        let el = Element::button()
251            .class("primary-btn")
252            .text("Tap me");
253        let json = el.to_json();
254        assert!(json.contains("\"tag\":\"button\""));
255        assert!(json.contains("\"class\":\"primary-btn\""));
256        assert!(json.contains("\"text\":\"Tap me\""));
257    }
258
259    #[test]
260    fn collect_handlers_finds_nested() {
261        use crate::events::EventHandler;
262        let el = Element::div()
263            .child(
264                Element::button()
265                    .on(EventHandler::onclick(|_| {}))
266            );
267        let mut handlers = Vec::new();
268        el.collect_handlers(&mut handlers);
269        assert_eq!(handlers.len(), 1);
270    }
271}