windjammer_ui/
vdom.rs

1//! Virtual DOM implementation
2
3use std::collections::HashMap;
4
5/// A virtual DOM node
6#[derive(Debug, Clone, PartialEq)]
7pub enum VNode {
8    /// An element node
9    Element(VElement),
10    /// A text node
11    Text(VText),
12    /// A component node
13    Component(VComponent),
14    /// An empty node
15    Empty,
16}
17
18/// A virtual element
19#[derive(Debug, Clone, PartialEq)]
20pub struct VElement {
21    pub tag: String,
22    pub attrs: HashMap<String, String>,
23    pub children: Vec<VNode>,
24}
25
26impl VElement {
27    /// Create a new virtual element
28    pub fn new(tag: impl Into<String>) -> Self {
29        Self {
30            tag: tag.into(),
31            attrs: HashMap::new(),
32            children: Vec::new(),
33        }
34    }
35
36    /// Add an attribute
37    pub fn attr(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
38        self.attrs.insert(key.into(), value.into());
39        self
40    }
41
42    /// Add children
43    pub fn children(mut self, children: Vec<VNode>) -> Self {
44        self.children = children;
45        self
46    }
47
48    /// Add a single child
49    pub fn child(mut self, child: VNode) -> Self {
50        self.children.push(child);
51        self
52    }
53}
54
55/// A virtual text node
56#[derive(Debug, Clone, PartialEq)]
57pub struct VText {
58    pub content: String,
59}
60
61impl VText {
62    /// Create a new text node
63    pub fn new(content: impl Into<String>) -> Self {
64        Self {
65            content: content.into(),
66        }
67    }
68}
69
70impl From<VElement> for VNode {
71    fn from(element: VElement) -> Self {
72        VNode::Element(element)
73    }
74}
75
76impl From<VText> for VNode {
77    fn from(text: VText) -> Self {
78        VNode::Text(text)
79    }
80}
81
82impl From<String> for VNode {
83    fn from(s: String) -> Self {
84        VNode::Text(VText::new(s))
85    }
86}
87
88impl From<&str> for VNode {
89    fn from(s: &str) -> Self {
90        VNode::Text(VText::new(s))
91    }
92}
93
94/// A virtual component node
95#[derive(Debug, Clone, PartialEq)]
96pub struct VComponent {
97    pub name: String,
98    pub props: HashMap<String, String>,
99}
100
101/// Diff two virtual DOM trees and produce a list of patches
102pub fn diff(old: &VNode, new: &VNode) -> Vec<Patch> {
103    let mut patches = Vec::new();
104    diff_recursive(old, new, &mut patches, vec![0]);
105    patches
106}
107
108fn diff_recursive(old: &VNode, new: &VNode, patches: &mut Vec<Patch>, path: Vec<usize>) {
109    match (old, new) {
110        (VNode::Text(old_text), VNode::Text(new_text)) => {
111            if old_text.content != new_text.content {
112                patches.push(Patch::UpdateText {
113                    path: path.clone(),
114                    content: new_text.content.clone(),
115                });
116            }
117        }
118        (VNode::Element(old_el), VNode::Element(new_el)) => {
119            if old_el.tag != new_el.tag {
120                patches.push(Patch::Replace {
121                    path: path.clone(),
122                    node: VNode::Element(new_el.clone()),
123                });
124                return;
125            }
126
127            // Diff attributes
128            for (key, new_value) in &new_el.attrs {
129                if old_el.attrs.get(key) != Some(new_value) {
130                    patches.push(Patch::SetAttribute {
131                        path: path.clone(),
132                        key: key.clone(),
133                        value: new_value.clone(),
134                    });
135                }
136            }
137
138            // Diff children
139            let max_len = old_el.children.len().max(new_el.children.len());
140            for i in 0..max_len {
141                let mut child_path = path.clone();
142                child_path.push(i);
143
144                match (old_el.children.get(i), new_el.children.get(i)) {
145                    (Some(old_child), Some(new_child)) => {
146                        diff_recursive(old_child, new_child, patches, child_path);
147                    }
148                    (None, Some(new_child)) => {
149                        patches.push(Patch::Append {
150                            path: path.clone(),
151                            node: new_child.clone(),
152                        });
153                    }
154                    (Some(_), None) => {
155                        patches.push(Patch::Remove { path: child_path });
156                    }
157                    (None, None) => unreachable!(),
158                }
159            }
160        }
161        _ => {
162            // Different node types, replace
163            patches.push(Patch::Replace {
164                path,
165                node: new.clone(),
166            });
167        }
168    }
169}
170
171/// A patch to apply to the DOM
172#[derive(Debug, Clone, PartialEq)]
173pub enum Patch {
174    /// Replace a node
175    Replace { path: Vec<usize>, node: VNode },
176    /// Update text content
177    UpdateText { path: Vec<usize>, content: String },
178    /// Set an attribute
179    SetAttribute {
180        path: Vec<usize>,
181        key: String,
182        value: String,
183    },
184    /// Append a child
185    Append { path: Vec<usize>, node: VNode },
186    /// Remove a node
187    Remove { path: Vec<usize> },
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_velement_creation() {
196        let el = VElement::new("div")
197            .attr("class", "container")
198            .child(VNode::Text(VText::new("Hello")));
199
200        assert_eq!(el.tag, "div");
201        assert_eq!(el.attrs.get("class"), Some(&"container".to_string()));
202        assert_eq!(el.children.len(), 1);
203    }
204
205    #[test]
206    fn test_vtext_creation() {
207        let text = VText::new("Hello, World!");
208        assert_eq!(text.content, "Hello, World!");
209    }
210
211    #[test]
212    fn test_diff_text_update() {
213        let old = VNode::Text(VText::new("old"));
214        let new = VNode::Text(VText::new("new"));
215
216        let patches = diff(&old, &new);
217        assert_eq!(patches.len(), 1);
218        assert!(matches!(patches[0], Patch::UpdateText { .. }));
219    }
220
221    #[test]
222    fn test_diff_no_change() {
223        let node = VNode::Text(VText::new("same"));
224        let patches = diff(&node, &node);
225        assert_eq!(patches.len(), 0);
226    }
227}