windjammer_ui/
vnode_ffi.rs1use crate::simple_vnode::{VAttr, VNode};
12use std::cell::RefCell;
13use std::collections::HashMap;
14
15thread_local! {
16 static VNODE_REGISTRY: RefCell<VNodeRegistry> = RefCell::new(VNodeRegistry::new());
18}
19
20struct 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#[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#[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#[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#[inline]
96pub fn vnode_child(parent_handle: u64, child_handle: u64) {
97 VNODE_REGISTRY.with(|registry| {
98 let mut reg = registry.borrow_mut();
99 if let Some(child) = reg.nodes.remove(&child_handle) {
101 if let Some(VNode::Element { children, .. }) = reg.nodes.get_mut(&parent_handle) {
103 children.push(child);
104 }
105 }
106 });
107}
108
109#[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 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#[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 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#[inline]
162pub fn vnode_take(handle: u64) -> Option<VNode> {
163 VNODE_REGISTRY.with(|registry| registry.borrow_mut().take(handle))
164}
165
166#[inline]
168pub fn vnode_get(handle: u64) -> Option<VNode> {
169 VNODE_REGISTRY.with(|registry| registry.borrow().get(handle).cloned())
170}
171
172#[inline]
174pub fn vnode_clear() {
175 VNODE_REGISTRY.with(|registry| registry.borrow_mut().clear());
176}
177
178#[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#[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#[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#[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 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}