Skip to main content

limit_tui/
vdom.rs

1// Virtual DOM implementation for terminal UI rendering
2
3use std::collections::HashMap;
4
5/// Virtual DOM node
6#[derive(Debug, Clone, PartialEq)]
7pub enum VNode {
8    Text(String),
9    Element {
10        tag: String,
11        attrs: HashMap<String, String>,
12        children: Vec<VNode>,
13    },
14}
15
16/// Patch operation for VNode tree updates
17#[derive(Debug, Clone, PartialEq)]
18pub enum Patch {
19    Replace(VNode),
20    UpdateAttrs {
21        add: HashMap<String, String>,
22        remove: Vec<String>,
23    },
24    InsertChild(usize, VNode),
25    RemoveChild(usize),
26}
27
28/// Render VNode to terminal string representation
29pub fn render(vnode: &VNode) -> String {
30    match vnode {
31        VNode::Text(text) => text.clone(),
32        VNode::Element {
33            tag,
34            attrs,
35            children,
36        } => {
37            let mut result = format!("[{}]", tag);
38            if !attrs.is_empty() {
39                result.push(' ');
40                let attr_str: Vec<String> =
41                    attrs.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
42                result.push_str(&attr_str.join(" "));
43            }
44            result.push('\n');
45
46            for child in children {
47                let child_str = render(child);
48                for line in child_str.lines() {
49                    result.push_str("  ");
50                    result.push_str(line);
51                    result.push('\n');
52                }
53            }
54
55            result
56        }
57    }
58}
59
60/// Compute minimal patches between two VNode trees
61pub fn diff(old: &VNode, new: &VNode) -> Vec<Patch> {
62    match (old, new) {
63        (VNode::Text(old_text), VNode::Text(new_text)) => {
64            if old_text == new_text {
65                vec![]
66            } else {
67                vec![Patch::Replace(new.clone())]
68            }
69        }
70        (
71            VNode::Element {
72                tag: old_tag,
73                attrs: old_attrs,
74                children: old_children,
75            },
76            VNode::Element {
77                tag: new_tag,
78                attrs: new_attrs,
79                children: new_children,
80            },
81        ) => {
82            let mut patches = vec![];
83
84            // Tag change requires full replacement
85            if old_tag != new_tag {
86                return vec![Patch::Replace(new.clone())];
87            }
88
89            // Attribute diff
90            let mut add_attrs = HashMap::new();
91            let mut remove_attrs = Vec::new();
92
93            // Check for removed or changed attributes
94            for (key, old_val) in old_attrs.iter() {
95                match new_attrs.get(key) {
96                    Some(new_val) => {
97                        if old_val != new_val {
98                            add_attrs.insert(key.clone(), new_val.clone());
99                        }
100                    }
101                    None => {
102                        remove_attrs.push(key.clone());
103                    }
104                }
105            }
106
107            // Check for new attributes
108            for (key, new_val) in new_attrs.iter() {
109                if !old_attrs.contains_key(key) {
110                    add_attrs.insert(key.clone(), new_val.clone());
111                }
112            }
113
114            if !add_attrs.is_empty() || !remove_attrs.is_empty() {
115                patches.push(Patch::UpdateAttrs {
116                    add: add_attrs,
117                    remove: remove_attrs,
118                });
119            }
120
121            // Children diff
122            let old_len = old_children.len();
123            let _new_len = new_children.len();
124
125            // Find common prefix
126            let common_prefix_len = old_children
127                .iter()
128                .zip(new_children.iter())
129                .take_while(|(o, n)| o == n)
130                .count();
131
132            // Process additions
133            for (i, child) in new_children.iter().enumerate().skip(common_prefix_len) {
134                patches.push(Patch::InsertChild(i, child.clone()));
135            }
136
137            // Process removals
138            for _i in common_prefix_len..old_len {
139                patches.push(Patch::RemoveChild(common_prefix_len));
140            }
141
142            patches
143        }
144        (_, _) => vec![Patch::Replace(new.clone())],
145    }
146}
147
148/// Apply patches to a VNode tree
149pub fn apply(node: &mut VNode, patches: Vec<Patch>) {
150    let node_ref = node;
151
152    for patch in patches {
153        match patch {
154            Patch::Replace(new_node) => {
155                *node_ref = new_node;
156            }
157            Patch::UpdateAttrs { add, remove } => {
158                if let VNode::Element { attrs, .. } = node_ref {
159                    for key in remove {
160                        attrs.remove(&key);
161                    }
162                    for (key, value) in add {
163                        attrs.insert(key, value);
164                    }
165                }
166            }
167            Patch::InsertChild(index, child) => {
168                if let VNode::Element { children, .. } = node_ref {
169                    children.insert(index, child);
170                }
171            }
172            Patch::RemoveChild(index) => {
173                if let VNode::Element { children, .. } = node_ref {
174                    if index < children.len() {
175                        children.remove(index);
176                    }
177                }
178            }
179        }
180    }
181}
182
183// Helper methods for VNode
184impl VNode {
185    pub fn children(&self) -> Option<&Vec<VNode>> {
186        match self {
187            VNode::Element { children, .. } => Some(children),
188            _ => None,
189        }
190    }
191
192    pub fn attrs(&self) -> Option<&HashMap<String, String>> {
193        match self {
194            VNode::Element { attrs, .. } => Some(attrs),
195            _ => None,
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_render_text() {
206        let node = VNode::Text("Hello, World!".to_string());
207        assert_eq!(render(&node), "Hello, World!");
208    }
209
210    #[test]
211    fn test_render_element() {
212        let node = VNode::Element {
213            tag: "div".to_string(),
214            attrs: {
215                let mut map = HashMap::new();
216                map.insert("id".to_string(), "test".to_string());
217                map
218            },
219            children: vec![VNode::Text("Content".to_string())],
220        };
221
222        let result = render(&node);
223        assert!(result.contains("[div]"));
224        assert!(result.contains("id=test"));
225        assert!(result.contains("Content"));
226    }
227
228    #[test]
229    fn test_diff_no_change() {
230        let node = VNode::Text("Same".to_string());
231        let patches = diff(&node, &node);
232        assert!(patches.is_empty());
233    }
234
235    #[test]
236    fn test_diff_text_change() {
237        let old = VNode::Text("Old".to_string());
238        let new = VNode::Text("New".to_string());
239        let patches = diff(&old, &new);
240
241        assert_eq!(patches.len(), 1);
242        assert_eq!(patches[0], Patch::Replace(new.clone()));
243    }
244
245    #[test]
246    fn test_diff_add_child() {
247        let old = VNode::Element {
248            tag: "div".to_string(),
249            attrs: HashMap::new(),
250            children: vec![VNode::Text("First".to_string())],
251        };
252
253        let new = VNode::Element {
254            tag: "div".to_string(),
255            attrs: HashMap::new(),
256            children: vec![
257                VNode::Text("First".to_string()),
258                VNode::Text("Second".to_string()),
259            ],
260        };
261
262        let patches = diff(&old, &new);
263
264        assert_eq!(patches.len(), 1);
265        assert_eq!(
266            patches[0],
267            Patch::InsertChild(1, VNode::Text("Second".to_string()))
268        );
269    }
270
271    #[test]
272    fn test_diff_remove_child() {
273        let old = VNode::Element {
274            tag: "div".to_string(),
275            attrs: HashMap::new(),
276            children: vec![
277                VNode::Text("First".to_string()),
278                VNode::Text("Second".to_string()),
279            ],
280        };
281
282        let new = VNode::Element {
283            tag: "div".to_string(),
284            attrs: HashMap::new(),
285            children: vec![VNode::Text("First".to_string())],
286        };
287
288        let patches = diff(&old, &new);
289
290        assert_eq!(patches.len(), 1);
291        assert_eq!(patches[0], Patch::RemoveChild(1));
292    }
293
294    #[test]
295    fn test_apply_patch() {
296        let mut node = VNode::Text("Old".to_string());
297        let patches = vec![Patch::Replace(VNode::Text("New".to_string()))];
298        apply(&mut node, patches);
299
300        assert_eq!(node, VNode::Text("New".to_string()));
301    }
302
303    #[test]
304    fn test_apply_insert_child() {
305        let mut node = VNode::Element {
306            tag: "div".to_string(),
307            attrs: HashMap::new(),
308            children: vec![VNode::Text("First".to_string())],
309        };
310
311        let patches = vec![Patch::InsertChild(1, VNode::Text("Second".to_string()))];
312        apply(&mut node, patches);
313
314        assert_eq!(node.children().unwrap().len(), 2);
315        assert_eq!(
316            node.children().unwrap()[1],
317            VNode::Text("Second".to_string())
318        );
319    }
320
321    #[test]
322    fn test_apply_remove_child() {
323        let mut node = VNode::Element {
324            tag: "div".to_string(),
325            attrs: HashMap::new(),
326            children: vec![
327                VNode::Text("First".to_string()),
328                VNode::Text("Second".to_string()),
329            ],
330        };
331
332        let patches = vec![Patch::RemoveChild(1)];
333        apply(&mut node, patches);
334
335        assert_eq!(node.children().unwrap().len(), 1);
336    }
337
338    #[test]
339    fn test_apply_update_attrs() {
340        let mut node = VNode::Element {
341            tag: "div".to_string(),
342            attrs: {
343                let mut map = HashMap::new();
344                map.insert("id".to_string(), "old".to_string());
345                map
346            },
347            children: vec![],
348        };
349
350        let patches = vec![Patch::UpdateAttrs {
351            add: {
352                let mut map = HashMap::new();
353                map.insert("class".to_string(), "test".to_string());
354                map
355            },
356            remove: vec!["id".to_string()],
357        }];
358
359        apply(&mut node, patches);
360
361        let attrs = node.attrs().unwrap();
362        assert!(!attrs.contains_key("id"));
363        assert_eq!(attrs.get("class"), Some(&"test".to_string()));
364    }
365}