windjammer_ui/
vnode_ffi.rs

1//! VNode FFI - Functions for constructing VNodes from Windjammer code
2//!
3//! This module provides a stable FFI interface that Windjammer components can use
4//! to construct VNodes. This enables cross-platform UI rendering:
5//! - Web: VNode → HTML (via simple_renderer)
6//! - Desktop: VNode → egui (via desktop_renderer)
7//!
8//! The design uses opaque handles (u64) to reference VNodes stored in a thread-local
9//! registry. This avoids complex memory management across the FFI boundary.
10
11use crate::simple_vnode::{VAttr, VNode};
12use std::cell::RefCell;
13use std::collections::HashMap;
14
15thread_local! {
16    /// Registry of VNodes created via FFI
17    static VNODE_REGISTRY: RefCell<VNodeRegistry> = RefCell::new(VNodeRegistry::new());
18}
19
20/// Registry for storing VNodes with handles
21struct VNodeRegistry {
22    nodes: HashMap<u64, VNode>,
23    next_handle: u64,
24}
25
26impl VNodeRegistry {
27    fn new() -> Self {
28        Self {
29            nodes: HashMap::new(),
30            next_handle: 1,
31        }
32    }
33
34    fn insert(&mut self, node: VNode) -> u64 {
35        let handle = self.next_handle;
36        self.next_handle += 1;
37        self.nodes.insert(handle, node);
38        handle
39    }
40
41    fn get(&self, handle: u64) -> Option<&VNode> {
42        self.nodes.get(&handle)
43    }
44
45    fn take(&mut self, handle: u64) -> Option<VNode> {
46        self.nodes.remove(&handle)
47    }
48
49    fn clear(&mut self) {
50        self.nodes.clear();
51        self.next_handle = 1;
52    }
53}
54
55// ============================================================================
56// VNode Creation Functions (called from Windjammer)
57// ============================================================================
58
59/// Create a new element VNode with a tag name
60/// Returns a handle to the VNode
61#[inline]
62pub fn vnode_element(tag: impl AsRef<str>) -> u64 {
63    let node = VNode::Element {
64        tag: tag.as_ref().to_string(),
65        attrs: Vec::new(),
66        children: Vec::new(),
67    };
68    VNODE_REGISTRY.with(|registry| registry.borrow_mut().insert(node))
69}
70
71/// Create a text VNode
72/// Returns a handle to the VNode
73#[inline]
74pub fn vnode_text(content: impl AsRef<str>) -> u64 {
75    let node = VNode::Text(content.as_ref().to_string());
76    VNODE_REGISTRY.with(|registry| registry.borrow_mut().insert(node))
77}
78
79/// Add a static attribute to a VNode
80#[inline]
81pub fn vnode_attr(handle: u64, name: impl AsRef<str>, value: impl AsRef<str>) {
82    VNODE_REGISTRY.with(|registry| {
83        let mut reg = registry.borrow_mut();
84        if let Some(VNode::Element { attrs, .. }) = reg.nodes.get_mut(&handle) {
85            attrs.push((
86                name.as_ref().to_string(),
87                VAttr::Static(value.as_ref().to_string()),
88            ));
89        }
90    });
91}
92
93/// Add a child VNode to a parent VNode
94/// The child is consumed (moved from registry to parent's children)
95#[inline]
96pub fn vnode_child(parent_handle: u64, child_handle: u64) {
97    VNODE_REGISTRY.with(|registry| {
98        let mut reg = registry.borrow_mut();
99        // Take the child from the registry
100        if let Some(child) = reg.nodes.remove(&child_handle) {
101            // Add it to the parent's children
102            if let Some(VNode::Element { children, .. }) = reg.nodes.get_mut(&parent_handle) {
103                children.push(child);
104            }
105        }
106    });
107}
108
109/// Add a class to a VNode (convenience for class attribute)
110#[inline]
111pub fn vnode_class(handle: u64, class: impl AsRef<str>) {
112    let class = class.as_ref();
113    VNODE_REGISTRY.with(|registry| {
114        let mut reg = registry.borrow_mut();
115        if let Some(VNode::Element { attrs, .. }) = reg.nodes.get_mut(&handle) {
116            // Find existing class attribute or create new one
117            let mut found = false;
118            for (name, value) in attrs.iter_mut() {
119                if name == "class" {
120                    if let VAttr::Static(existing) = value {
121                        *existing = format!("{} {}", existing, class);
122                        found = true;
123                        break;
124                    }
125                }
126            }
127            if !found {
128                attrs.push(("class".to_string(), VAttr::Static(class.to_string())));
129            }
130        }
131    });
132}
133
134/// Add inline style to a VNode
135#[inline]
136pub fn vnode_style(handle: u64, style: impl AsRef<str>) {
137    let style = style.as_ref();
138    VNODE_REGISTRY.with(|registry| {
139        let mut reg = registry.borrow_mut();
140        if let Some(VNode::Element { attrs, .. }) = reg.nodes.get_mut(&handle) {
141            // Find existing style attribute or create new one
142            let mut found = false;
143            for (name, value) in attrs.iter_mut() {
144                if name == "style" {
145                    if let VAttr::Static(existing) = value {
146                        *existing = format!("{}; {}", existing, style);
147                        found = true;
148                        break;
149                    }
150                }
151            }
152            if !found {
153                attrs.push(("style".to_string(), VAttr::Static(style.to_string())));
154            }
155        }
156    });
157}
158
159/// Get the VNode for a handle and remove it from the registry
160/// This is called when the VNode tree is complete and ready to render
161#[inline]
162pub fn vnode_take(handle: u64) -> Option<VNode> {
163    VNODE_REGISTRY.with(|registry| registry.borrow_mut().take(handle))
164}
165
166/// Get a reference to a VNode (without removing it)
167#[inline]
168pub fn vnode_get(handle: u64) -> Option<VNode> {
169    VNODE_REGISTRY.with(|registry| registry.borrow().get(handle).cloned())
170}
171
172/// Clear all VNodes from the registry (for cleanup between renders)
173#[inline]
174pub fn vnode_clear() {
175    VNODE_REGISTRY.with(|registry| registry.borrow_mut().clear());
176}
177
178// ============================================================================
179// Convenience builders for common elements
180// ============================================================================
181
182/// Create a div element with optional class and style
183#[inline]
184pub fn vnode_div(class: &str, style: &str) -> u64 {
185    let handle = vnode_element("div");
186    if !class.is_empty() {
187        vnode_class(handle, class);
188    }
189    if !style.is_empty() {
190        vnode_style(handle, style);
191    }
192    handle
193}
194
195/// Create a span element with text content
196#[inline]
197pub fn vnode_span(text: &str, class: &str, style: &str) -> u64 {
198    let handle = vnode_element("span");
199    if !class.is_empty() {
200        vnode_class(handle, class);
201    }
202    if !style.is_empty() {
203        vnode_style(handle, style);
204    }
205    let text_handle = vnode_text(text);
206    vnode_child(handle, text_handle);
207    handle
208}
209
210/// Create a button element
211#[inline]
212pub fn vnode_button(label: &str, class: &str, style: &str) -> u64 {
213    let handle = vnode_element("button");
214    if !class.is_empty() {
215        vnode_class(handle, class);
216    }
217    if !style.is_empty() {
218        vnode_style(handle, style);
219    }
220    let text_handle = vnode_text(label);
221    vnode_child(handle, text_handle);
222    handle
223}
224
225// ============================================================================
226// Tests
227// ============================================================================
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_create_element() {
235        vnode_clear();
236        let handle = vnode_element("div");
237        assert!(handle > 0);
238
239        let node = vnode_take(handle);
240        assert!(node.is_some());
241        if let Some(VNode::Element { tag, .. }) = node {
242            assert_eq!(tag, "div");
243        }
244    }
245
246    #[test]
247    fn test_create_text() {
248        vnode_clear();
249        let handle = vnode_text("Hello, World!");
250
251        let node = vnode_take(handle);
252        assert!(node.is_some());
253        if let Some(VNode::Text(content)) = node {
254            assert_eq!(content, "Hello, World!");
255        }
256    }
257
258    #[test]
259    fn test_add_child() {
260        vnode_clear();
261        let parent = vnode_element("div");
262        let child = vnode_text("Child text");
263
264        vnode_child(parent, child);
265
266        let node = vnode_take(parent);
267        if let Some(VNode::Element { children, .. }) = node {
268            assert_eq!(children.len(), 1);
269            if let VNode::Text(content) = &children[0] {
270                assert_eq!(content, "Child text");
271            }
272        }
273    }
274
275    #[test]
276    fn test_add_class() {
277        vnode_clear();
278        let handle = vnode_element("button");
279        vnode_class(handle, "wj-button");
280        vnode_class(handle, "wj-button-primary");
281
282        let node = vnode_take(handle);
283        if let Some(VNode::Element { attrs, .. }) = node {
284            let class_attr = attrs.iter().find(|(n, _)| n == "class");
285            assert!(class_attr.is_some());
286            if let Some((_, VAttr::Static(classes))) = class_attr {
287                assert!(classes.contains("wj-button"));
288                assert!(classes.contains("wj-button-primary"));
289            }
290        }
291    }
292
293    #[test]
294    fn test_convenience_builders() {
295        vnode_clear();
296        let button = vnode_button("Click me", "btn", "color: red");
297
298        let node = vnode_take(button);
299        if let Some(VNode::Element {
300            tag,
301            children,
302            attrs,
303            ..
304        }) = node
305        {
306            assert_eq!(tag, "button");
307            assert_eq!(children.len(), 1);
308
309            // Check class
310            let has_class = attrs
311                .iter()
312                .any(|(n, v)| n == "class" && matches!(v, VAttr::Static(s) if s.contains("btn")));
313            assert!(has_class);
314        }
315    }
316}