presentar_core/
diff.rs

1//! Widget tree diffing for efficient UI updates.
2//!
3//! This module provides algorithms for comparing two widget trees and computing
4//! the minimal set of operations needed to transform one into the other.
5//!
6//! # Algorithm
7//!
8//! The diffing algorithm works in two phases:
9//!
10//! 1. **Matching**: Match widgets between old and new trees by key or position
11//! 2. **Reconciliation**: Generate operations for differences
12//!
13//! Widgets are matched by:
14//! - Explicit key (if set via `key` attribute)
15//! - Type + position (fallback)
16
17use crate::widget::TypeId;
18use std::collections::HashMap;
19
20/// A key used to identify widgets across renders.
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub enum WidgetKey {
23    /// Explicit string key
24    String(String),
25    /// Auto-generated index key
26    Index(usize),
27}
28
29impl WidgetKey {
30    /// Create a string key.
31    #[must_use]
32    pub fn string(s: impl Into<String>) -> Self {
33        Self::String(s.into())
34    }
35
36    /// Create an index key.
37    #[must_use]
38    pub const fn index(i: usize) -> Self {
39        Self::Index(i)
40    }
41}
42
43/// A node in the widget tree for diffing.
44#[derive(Debug, Clone)]
45pub struct DiffNode {
46    /// Widget type
47    pub type_id: TypeId,
48    /// Optional key for matching
49    pub key: Option<String>,
50    /// Widget properties hash (for detecting changes)
51    pub props_hash: u64,
52    /// Child nodes
53    pub children: Vec<Self>,
54    /// Position in parent's children list
55    pub index: usize,
56}
57
58impl DiffNode {
59    /// Create a new diff node.
60    #[must_use]
61    pub const fn new(type_id: TypeId, props_hash: u64) -> Self {
62        Self {
63            type_id,
64            key: None,
65            props_hash,
66            children: Vec::new(),
67            index: 0,
68        }
69    }
70
71    /// Set the key for this node.
72    #[must_use]
73    pub fn with_key(mut self, key: impl Into<String>) -> Self {
74        self.key = Some(key.into());
75        self
76    }
77
78    /// Set the index of this node.
79    #[must_use]
80    pub const fn with_index(mut self, index: usize) -> Self {
81        self.index = index;
82        self
83    }
84
85    /// Add a child node.
86    pub fn add_child(&mut self, child: Self) {
87        self.children.push(child);
88    }
89
90    /// Add a child node with fluent API.
91    #[must_use]
92    pub fn with_child(mut self, child: Self) -> Self {
93        self.children.push(child);
94        self
95    }
96}
97
98/// Operation to apply during reconciliation.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum DiffOp {
101    /// Insert a new widget at path
102    Insert {
103        /// Path to parent (indices from root)
104        path: Vec<usize>,
105        /// Index to insert at
106        index: usize,
107        /// Type of widget to insert
108        type_id: TypeId,
109        /// Props hash of the new widget
110        props_hash: u64,
111    },
112    /// Remove widget at path
113    Remove {
114        /// Path to widget to remove
115        path: Vec<usize>,
116    },
117    /// Update widget properties at path
118    Update {
119        /// Path to widget to update
120        path: Vec<usize>,
121        /// New props hash
122        new_props_hash: u64,
123    },
124    /// Move widget from one position to another
125    Move {
126        /// Old path
127        from_path: Vec<usize>,
128        /// New path
129        to_path: Vec<usize>,
130    },
131    /// Replace widget with different type
132    Replace {
133        /// Path to widget to replace
134        path: Vec<usize>,
135        /// New type ID
136        new_type_id: TypeId,
137        /// New props hash
138        new_props_hash: u64,
139    },
140}
141
142/// Result of diffing two widget trees.
143#[derive(Debug, Clone, Default)]
144pub struct DiffResult {
145    /// List of operations to apply
146    pub operations: Vec<DiffOp>,
147}
148
149impl DiffResult {
150    /// Create an empty diff result.
151    #[must_use]
152    pub fn new() -> Self {
153        Self::default()
154    }
155
156    /// Check if there are no changes.
157    #[must_use]
158    pub fn is_empty(&self) -> bool {
159        self.operations.is_empty()
160    }
161
162    /// Get the number of operations.
163    #[must_use]
164    pub fn len(&self) -> usize {
165        self.operations.len()
166    }
167
168    /// Add an operation.
169    pub fn push(&mut self, op: DiffOp) {
170        self.operations.push(op);
171    }
172}
173
174/// Widget tree differ.
175#[derive(Debug, Default)]
176pub struct TreeDiffer {
177    /// Current path during traversal
178    current_path: Vec<usize>,
179}
180
181impl TreeDiffer {
182    /// Create a new tree differ.
183    #[must_use]
184    pub fn new() -> Self {
185        Self::default()
186    }
187
188    /// Compute the diff between two widget trees.
189    #[must_use]
190    pub fn diff(&mut self, old: &DiffNode, new: &DiffNode) -> DiffResult {
191        let mut result = DiffResult::new();
192        self.current_path.clear();
193        self.diff_node(old, new, &mut result);
194        result
195    }
196
197    fn diff_node(&mut self, old: &DiffNode, new: &DiffNode, result: &mut DiffResult) {
198        // Check if type changed - need full replacement
199        if old.type_id != new.type_id {
200            result.push(DiffOp::Replace {
201                path: self.current_path.clone(),
202                new_type_id: new.type_id,
203                new_props_hash: new.props_hash,
204            });
205            return;
206        }
207
208        // Check if props changed
209        if old.props_hash != new.props_hash {
210            result.push(DiffOp::Update {
211                path: self.current_path.clone(),
212                new_props_hash: new.props_hash,
213            });
214        }
215
216        // Diff children
217        self.diff_children(&old.children, &new.children, result);
218    }
219
220    fn diff_children(
221        &mut self,
222        old_children: &[DiffNode],
223        new_children: &[DiffNode],
224        result: &mut DiffResult,
225    ) {
226        // Build key -> index maps for keyed children
227        let old_keyed: HashMap<&str, usize> = old_children
228            .iter()
229            .enumerate()
230            .filter_map(|(i, c)| c.key.as_deref().map(|k| (k, i)))
231            .collect();
232
233        let _new_keyed: HashMap<&str, usize> = new_children
234            .iter()
235            .enumerate()
236            .filter_map(|(i, c)| c.key.as_deref().map(|k| (k, i)))
237            .collect();
238
239        // Track which old children have been matched
240        let mut old_matched = vec![false; old_children.len()];
241        let mut new_matched = vec![false; new_children.len()];
242
243        // Phase 1: Match by key
244        for (new_idx, new_child) in new_children.iter().enumerate() {
245            if let Some(key) = &new_child.key {
246                if let Some(&old_idx) = old_keyed.get(key.as_str()) {
247                    old_matched[old_idx] = true;
248                    new_matched[new_idx] = true;
249
250                    // Check if moved
251                    if old_idx != new_idx {
252                        let mut from_path = self.current_path.clone();
253                        from_path.push(old_idx);
254                        let mut to_path = self.current_path.clone();
255                        to_path.push(new_idx);
256                        result.push(DiffOp::Move { from_path, to_path });
257                    }
258
259                    // Recursively diff
260                    self.current_path.push(new_idx);
261                    self.diff_node(&old_children[old_idx], new_child, result);
262                    self.current_path.pop();
263                }
264            }
265        }
266
267        // Phase 2: Match unkeyed children by type + position
268        let old_unkeyed: Vec<usize> = old_children
269            .iter()
270            .enumerate()
271            .filter(|(i, c)| c.key.is_none() && !old_matched[*i])
272            .map(|(i, _)| i)
273            .collect();
274
275        let new_unkeyed: Vec<usize> = new_children
276            .iter()
277            .enumerate()
278            .filter(|(i, c)| c.key.is_none() && !new_matched[*i])
279            .map(|(i, _)| i)
280            .collect();
281
282        // Match by type
283        let mut old_unkeyed_used = vec![false; old_unkeyed.len()];
284        for new_pos in &new_unkeyed {
285            let new_child = &new_children[*new_pos];
286            let mut found = false;
287
288            for (old_pos_idx, old_pos) in old_unkeyed.iter().enumerate() {
289                if old_unkeyed_used[old_pos_idx] {
290                    continue;
291                }
292                let old_child = &old_children[*old_pos];
293
294                if old_child.type_id == new_child.type_id {
295                    old_unkeyed_used[old_pos_idx] = true;
296                    old_matched[*old_pos] = true;
297                    new_matched[*new_pos] = true;
298                    found = true;
299
300                    // Recursively diff
301                    self.current_path.push(*new_pos);
302                    self.diff_node(old_child, new_child, result);
303                    self.current_path.pop();
304                    break;
305                }
306            }
307
308            if !found {
309                // New child with no match - insert
310                new_matched[*new_pos] = true;
311                result.push(DiffOp::Insert {
312                    path: self.current_path.clone(),
313                    index: *new_pos,
314                    type_id: new_child.type_id,
315                    props_hash: new_child.props_hash,
316                });
317
318                // Recursively handle new children's children
319                self.current_path.push(*new_pos);
320                self.insert_subtree(new_child, result);
321                self.current_path.pop();
322            }
323        }
324
325        // Phase 3: Remove unmatched old children (in reverse order)
326        for (i, matched) in old_matched.iter().enumerate().rev() {
327            if !matched {
328                let mut path = self.current_path.clone();
329                path.push(i);
330                result.push(DiffOp::Remove { path });
331            }
332        }
333
334        // Phase 4: Insert remaining new children
335        for (i, matched) in new_matched.iter().enumerate() {
336            if !matched {
337                let new_child = &new_children[i];
338                result.push(DiffOp::Insert {
339                    path: self.current_path.clone(),
340                    index: i,
341                    type_id: new_child.type_id,
342                    props_hash: new_child.props_hash,
343                });
344
345                // Recursively handle new children's children
346                self.current_path.push(i);
347                self.insert_subtree(new_child, result);
348                self.current_path.pop();
349            }
350        }
351    }
352
353    fn insert_subtree(&mut self, node: &DiffNode, result: &mut DiffResult) {
354        for (i, child) in node.children.iter().enumerate() {
355            result.push(DiffOp::Insert {
356                path: self.current_path.clone(),
357                index: i,
358                type_id: child.type_id,
359                props_hash: child.props_hash,
360            });
361
362            self.current_path.push(i);
363            self.insert_subtree(child, result);
364            self.current_path.pop();
365        }
366    }
367}
368
369/// Convenience function to diff two trees.
370#[must_use]
371pub fn diff_trees(old: &DiffNode, new: &DiffNode) -> DiffResult {
372    let mut differ = TreeDiffer::new();
373    differ.diff(old, new)
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    fn make_type_id<T: 'static>() -> TypeId {
381        TypeId::of::<T>()
382    }
383
384    #[test]
385    fn test_widget_key_string() {
386        let key = WidgetKey::string("test");
387        assert_eq!(key, WidgetKey::String("test".to_string()));
388    }
389
390    #[test]
391    fn test_widget_key_index() {
392        let key = WidgetKey::index(42);
393        assert_eq!(key, WidgetKey::Index(42));
394    }
395
396    #[test]
397    fn test_diff_node_new() {
398        let type_id = make_type_id::<u32>();
399        let node = DiffNode::new(type_id, 123);
400
401        assert_eq!(node.type_id, type_id);
402        assert_eq!(node.props_hash, 123);
403        assert!(node.key.is_none());
404        assert!(node.children.is_empty());
405    }
406
407    #[test]
408    fn test_diff_node_with_key() {
409        let type_id = make_type_id::<u32>();
410        let node = DiffNode::new(type_id, 123).with_key("my-key");
411
412        assert_eq!(node.key, Some("my-key".to_string()));
413    }
414
415    #[test]
416    fn test_diff_node_with_child() {
417        let type_id = make_type_id::<u32>();
418        let child = DiffNode::new(type_id, 456);
419        let parent = DiffNode::new(type_id, 123).with_child(child);
420
421        assert_eq!(parent.children.len(), 1);
422        assert_eq!(parent.children[0].props_hash, 456);
423    }
424
425    #[test]
426    fn test_diff_result_empty() {
427        let result = DiffResult::new();
428        assert!(result.is_empty());
429        assert_eq!(result.len(), 0);
430    }
431
432    #[test]
433    fn test_diff_identical_trees() {
434        let type_id = make_type_id::<u32>();
435        let old = DiffNode::new(type_id, 123);
436        let new = DiffNode::new(type_id, 123);
437
438        let result = diff_trees(&old, &new);
439        assert!(result.is_empty());
440    }
441
442    #[test]
443    fn test_diff_props_changed() {
444        let type_id = make_type_id::<u32>();
445        let old = DiffNode::new(type_id, 123);
446        let new = DiffNode::new(type_id, 456);
447
448        let result = diff_trees(&old, &new);
449        assert_eq!(result.len(), 1);
450        assert!(matches!(
451            &result.operations[0],
452            DiffOp::Update { path, new_props_hash: 456 } if path.is_empty()
453        ));
454    }
455
456    #[test]
457    fn test_diff_type_changed() {
458        let old_type = make_type_id::<u32>();
459        let new_type = make_type_id::<String>();
460        let old = DiffNode::new(old_type, 123);
461        let new = DiffNode::new(new_type, 123);
462
463        let result = diff_trees(&old, &new);
464        assert_eq!(result.len(), 1);
465        assert!(matches!(
466            &result.operations[0],
467            DiffOp::Replace { path, new_type_id, .. } if path.is_empty() && *new_type_id == new_type
468        ));
469    }
470
471    #[test]
472    fn test_diff_child_added() {
473        let type_id = make_type_id::<u32>();
474        let child_type = make_type_id::<String>();
475
476        let old = DiffNode::new(type_id, 123);
477        let new = DiffNode::new(type_id, 123).with_child(DiffNode::new(child_type, 456));
478
479        let result = diff_trees(&old, &new);
480        assert_eq!(result.len(), 1);
481        assert!(matches!(
482            &result.operations[0],
483            DiffOp::Insert { path, index: 0, type_id: t, .. } if path.is_empty() && *t == child_type
484        ));
485    }
486
487    #[test]
488    fn test_diff_child_removed() {
489        let type_id = make_type_id::<u32>();
490        let child_type = make_type_id::<String>();
491
492        let old = DiffNode::new(type_id, 123).with_child(DiffNode::new(child_type, 456));
493        let new = DiffNode::new(type_id, 123);
494
495        let result = diff_trees(&old, &new);
496        assert_eq!(result.len(), 1);
497        assert!(matches!(
498            &result.operations[0],
499            DiffOp::Remove { path } if *path == vec![0]
500        ));
501    }
502
503    #[test]
504    fn test_diff_keyed_children_reordered() {
505        let type_id = make_type_id::<u32>();
506
507        let old = DiffNode::new(type_id, 0)
508            .with_child(DiffNode::new(type_id, 1).with_key("a"))
509            .with_child(DiffNode::new(type_id, 2).with_key("b"));
510
511        let new = DiffNode::new(type_id, 0)
512            .with_child(DiffNode::new(type_id, 2).with_key("b"))
513            .with_child(DiffNode::new(type_id, 1).with_key("a"));
514
515        let result = diff_trees(&old, &new);
516
517        // Should have move operations
518        let move_ops: Vec<_> = result
519            .operations
520            .iter()
521            .filter(|op| matches!(op, DiffOp::Move { .. }))
522            .collect();
523        assert!(!move_ops.is_empty());
524    }
525
526    #[test]
527    fn test_diff_keyed_child_updated() {
528        let type_id = make_type_id::<u32>();
529
530        let old = DiffNode::new(type_id, 0).with_child(DiffNode::new(type_id, 1).with_key("item"));
531        let new = DiffNode::new(type_id, 0).with_child(DiffNode::new(type_id, 2).with_key("item"));
532
533        let result = diff_trees(&old, &new);
534
535        let update_ops: Vec<_> = result
536            .operations
537            .iter()
538            .filter(|op| matches!(op, DiffOp::Update { .. }))
539            .collect();
540        assert_eq!(update_ops.len(), 1);
541    }
542
543    #[test]
544    fn test_diff_nested_changes() {
545        let type_id = make_type_id::<u32>();
546
547        let old = DiffNode::new(type_id, 0)
548            .with_child(DiffNode::new(type_id, 1).with_child(DiffNode::new(type_id, 2)));
549
550        let new = DiffNode::new(type_id, 0)
551            .with_child(DiffNode::new(type_id, 1).with_child(DiffNode::new(type_id, 3)));
552
553        let result = diff_trees(&old, &new);
554
555        // Should have update at path [0, 0]
556        let update_ops: Vec<_> = result
557            .operations
558            .iter()
559            .filter(|op| matches!(op, DiffOp::Update { path, .. } if *path == vec![0, 0]))
560            .collect();
561        assert_eq!(update_ops.len(), 1);
562    }
563
564    #[test]
565    fn test_diff_multiple_children_mixed() {
566        let type_id = make_type_id::<u32>();
567        let string_type = make_type_id::<String>();
568
569        let old = DiffNode::new(type_id, 0)
570            .with_child(DiffNode::new(type_id, 1))
571            .with_child(DiffNode::new(string_type, 2))
572            .with_child(DiffNode::new(type_id, 3));
573
574        let new = DiffNode::new(type_id, 0)
575            .with_child(DiffNode::new(type_id, 1))
576            .with_child(DiffNode::new(type_id, 4)); // Changed type and removed one
577
578        let result = diff_trees(&old, &new);
579
580        // Should have remove operations for removed children
581        let remove_ops: Vec<_> = result
582            .operations
583            .iter()
584            .filter(|op| matches!(op, DiffOp::Remove { .. }))
585            .collect();
586        assert!(!remove_ops.is_empty());
587    }
588
589    #[test]
590    fn test_tree_differ_reuse() {
591        let type_id = make_type_id::<u32>();
592        let mut differ = TreeDiffer::new();
593
594        let old1 = DiffNode::new(type_id, 1);
595        let new1 = DiffNode::new(type_id, 2);
596        let result1 = differ.diff(&old1, &new1);
597
598        let old2 = DiffNode::new(type_id, 3);
599        let new2 = DiffNode::new(type_id, 3);
600        let result2 = differ.diff(&old2, &new2);
601
602        assert_eq!(result1.len(), 1);
603        assert!(result2.is_empty());
604    }
605
606    #[test]
607    fn test_diff_empty_to_tree() {
608        let type_id = make_type_id::<u32>();
609
610        let old = DiffNode::new(type_id, 0);
611        let new = DiffNode::new(type_id, 0)
612            .with_child(DiffNode::new(type_id, 1))
613            .with_child(DiffNode::new(type_id, 2));
614
615        let result = diff_trees(&old, &new);
616
617        let insert_ops: Vec<_> = result
618            .operations
619            .iter()
620            .filter(|op| matches!(op, DiffOp::Insert { .. }))
621            .collect();
622        assert_eq!(insert_ops.len(), 2);
623    }
624
625    #[test]
626    fn test_diff_tree_to_empty() {
627        let type_id = make_type_id::<u32>();
628
629        let old = DiffNode::new(type_id, 0)
630            .with_child(DiffNode::new(type_id, 1))
631            .with_child(DiffNode::new(type_id, 2));
632        let new = DiffNode::new(type_id, 0);
633
634        let result = diff_trees(&old, &new);
635
636        let remove_ops: Vec<_> = result
637            .operations
638            .iter()
639            .filter(|op| matches!(op, DiffOp::Remove { .. }))
640            .collect();
641        assert_eq!(remove_ops.len(), 2);
642    }
643
644    #[test]
645    fn test_diff_deeply_nested() {
646        let type_id = make_type_id::<u32>();
647
648        let old = DiffNode::new(type_id, 0).with_child(
649            DiffNode::new(type_id, 1)
650                .with_child(DiffNode::new(type_id, 2).with_child(DiffNode::new(type_id, 3))),
651        );
652
653        let new = DiffNode::new(type_id, 0).with_child(
654            DiffNode::new(type_id, 1)
655                .with_child(DiffNode::new(type_id, 2).with_child(DiffNode::new(type_id, 99))),
656        );
657
658        let result = diff_trees(&old, &new);
659
660        // Should update the deeply nested node
661        let update_ops: Vec<_> = result
662            .operations
663            .iter()
664            .filter(|op| {
665                matches!(op, DiffOp::Update { path, new_props_hash: 99 } if *path == vec![0, 0, 0])
666            })
667            .collect();
668        assert_eq!(update_ops.len(), 1);
669    }
670
671    #[test]
672    fn test_diff_op_debug() {
673        let op = DiffOp::Insert {
674            path: vec![0, 1],
675            index: 2,
676            type_id: make_type_id::<u32>(),
677            props_hash: 123,
678        };
679        let debug_str = format!("{op:?}");
680        assert!(debug_str.contains("Insert"));
681    }
682
683    #[test]
684    fn test_diff_result_push() {
685        let mut result = DiffResult::new();
686        result.push(DiffOp::Remove { path: vec![0] });
687        assert_eq!(result.len(), 1);
688        assert!(!result.is_empty());
689    }
690}