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//! which the Bubba runtime then tranbubbas into native Android View calls.
6
7use std::collections::HashMap;
8use crate::events::EventHandler;
9
10/// A rendered screen — the return type of every screen function.
11///
12/// ```rust
13/// use bubba_core::ui::{Element, Screen};
14///
15/// pub fn home() -> Screen {
16///     Screen::new(Element::h1().class("title").text("Hello"))
17/// }
18/// ```
19pub struct Screen {
20    /// The root element tree for this screen.
21    pub root: Element,
22    /// Optional CSS stylesheet linked to this screen.
23    pub stylesheet: Option<String>,
24}
25
26impl Screen {
27    /// Create a new screen from a root element.
28    pub fn new(root: Element) -> Self {
29        Self { root, stylesheet: None }
30    }
31
32    /// Attach a CSS stylesheet path to this screen.
33    pub fn with_stylesheet(mut self, path: impl Into<String>) -> Self {
34        self.stylesheet = Some(path.into());
35        self
36    }
37}
38
39/// The fundamental UI node. Every tag in a `view!` block becomes one of these.
40#[derive(Debug)]
41pub struct Element {
42    /// Tag name: "h1", "button", "p", "img", "input", "div", etc.
43    pub tag: &'static str,
44    /// CSS class string, e.g. `"title"` or `"btn primary-btn"`.
45    pub class: Option<String>,
46    /// Arbitrary HTML-like attributes (src, alt, placeholder, …).
47    pub attrs: HashMap<&'static str, String>,
48    /// Text content for leaf nodes.
49    pub text: Option<String>,
50    /// Child elements.
51    pub children: Vec<Element>,
52    /// Registered event handlers.
53    pub handlers: Vec<EventHandler>,
54}
55
56impl Element {
57    /// Create a new element with the given tag.
58    pub fn new(tag: &'static str) -> Self {
59        Self {
60            tag,
61            class: None,
62            attrs: HashMap::new(),
63            text: None,
64            children: Vec::new(),
65            handlers: Vec::new(),
66        }
67    }
68
69    /// Set the `class` attribute.
70    pub fn class(mut self, cls: impl Into<String>) -> Self {
71        self.class = Some(cls.into());
72        self
73    }
74
75    /// Set a generic attribute.
76    pub fn attr(mut self, key: &'static str, value: impl Into<String>) -> Self {
77        self.attrs.insert(key, value.into());
78        self
79    }
80
81    /// Set text content.
82    pub fn text(mut self, content: impl Into<String>) -> Self {
83        self.text = Some(content.into());
84        self
85    }
86
87    /// Add a child element.
88    pub fn child(mut self, child: Element) -> Self {
89        self.children.push(child);
90        self
91    }
92
93    /// Attach an event handler.
94    pub fn on(mut self, handler: EventHandler) -> Self {
95        self.handlers.push(handler);
96        self
97    }
98
99    // ── Convenience constructors ──────────────────────────────────────────────
100
101    /// `<h1>`
102    pub fn h1() -> Self { Self::new("h1") }
103    /// `<h2>`
104    pub fn h2() -> Self { Self::new("h2") }
105    /// `<h3>`
106    pub fn h3() -> Self { Self::new("h3") }
107    /// `<p>`
108    pub fn p() -> Self { Self::new("p") }
109    /// `<button>`
110    pub fn button() -> Self { Self::new("button") }
111    /// `<img>`
112    pub fn img() -> Self { Self::new("img") }
113    /// `<input>`
114    pub fn input() -> Self { Self::new("input") }
115    /// `<div>`
116    pub fn div() -> Self { Self::new("div") }
117    /// `<span>`
118    pub fn span() -> Self { Self::new("span") }
119    /// `<a>`
120    pub fn a() -> Self { Self::new("a") }
121
122    /// Render this element tree as a human-readable string (for debugging).
123    pub fn debug_render(&self, indent: usize) -> String {
124        let pad = " ".repeat(indent * 2);
125        let class_str = self
126            .class
127            .as_ref()
128            .map(|c| format!(r#" class="{}""#, c))
129            .unwrap_or_default();
130
131        let attrs_str: String = self
132            .attrs
133            .iter()
134            .map(|(k, v)| format!(r#" {}="{}""#, k, v))
135            .collect();
136
137        let handlers_str: String = self
138            .handlers
139            .iter()
140            .map(|h| format!(" {}=[fn]", h.event))
141            .collect();
142
143        if self.children.is_empty() && self.text.is_none() {
144            return format!("{}<{}{}{}{} />\n", pad, self.tag, class_str, attrs_str, handlers_str);
145        }
146
147        let mut out = format!("{}<{}{}{}{}>", pad, self.tag, class_str, attrs_str, handlers_str);
148        if let Some(text) = &self.text {
149            out.push_str(&format!("{}", text));
150        }
151        if !self.children.is_empty() {
152            out.push('\n');
153            for child in &self.children {
154                out.push_str(&child.debug_render(indent + 1));
155            }
156            out.push_str(&pad);
157        }
158        out.push_str(&format!("</{}>\n", self.tag));
159        out
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn element_builder_chain() {
169        let el = Element::h1()
170            .class("title")
171            .text("Hello, Bubba!");
172        assert_eq!(el.tag, "h1");
173        assert_eq!(el.class.as_deref(), Some("title"));
174        assert_eq!(el.text.as_deref(), Some("Hello, Bubba!"));
175    }
176
177    #[test]
178    fn debug_render_self_closing() {
179        let el = Element::img().attr("src", "avatar.png").class("avatar");
180        let rendered = el.debug_render(0);
181        assert!(rendered.contains("<img"));
182        assert!(rendered.contains("/>"));
183    }
184}