use crate::widget::TypeId;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum WidgetKey {
String(String),
Index(usize),
}
impl WidgetKey {
#[must_use]
pub fn string(s: impl Into<String>) -> Self {
Self::String(s.into())
}
#[must_use]
pub const fn index(i: usize) -> Self {
Self::Index(i)
}
}
#[derive(Debug, Clone)]
pub struct DiffNode {
pub type_id: TypeId,
pub key: Option<String>,
pub props_hash: u64,
pub children: Vec<Self>,
pub index: usize,
}
impl DiffNode {
#[must_use]
pub const fn new(type_id: TypeId, props_hash: u64) -> Self {
Self {
type_id,
key: None,
props_hash,
children: Vec::new(),
index: 0,
}
}
#[must_use]
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
#[must_use]
pub const fn with_index(mut self, index: usize) -> Self {
self.index = index;
self
}
pub fn add_child(&mut self, child: Self) {
self.children.push(child);
}
#[must_use]
pub fn with_child(mut self, child: Self) -> Self {
self.children.push(child);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiffOp {
Insert {
path: Vec<usize>,
index: usize,
type_id: TypeId,
props_hash: u64,
},
Remove {
path: Vec<usize>,
},
Update {
path: Vec<usize>,
new_props_hash: u64,
},
Move {
from_path: Vec<usize>,
to_path: Vec<usize>,
},
Replace {
path: Vec<usize>,
new_type_id: TypeId,
new_props_hash: u64,
},
}
#[derive(Debug, Clone, Default)]
pub struct DiffResult {
pub operations: Vec<DiffOp>,
}
impl DiffResult {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.operations.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.operations.len()
}
pub fn push(&mut self, op: DiffOp) {
self.operations.push(op);
}
}
#[derive(Debug, Default)]
pub struct TreeDiffer {
current_path: Vec<usize>,
}
impl TreeDiffer {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn diff(&mut self, old: &DiffNode, new: &DiffNode) -> DiffResult {
let mut result = DiffResult::new();
self.current_path.clear();
self.diff_node(old, new, &mut result);
result
}
fn diff_node(&mut self, old: &DiffNode, new: &DiffNode, result: &mut DiffResult) {
if old.type_id != new.type_id {
result.push(DiffOp::Replace {
path: self.current_path.clone(),
new_type_id: new.type_id,
new_props_hash: new.props_hash,
});
return;
}
if old.props_hash != new.props_hash {
result.push(DiffOp::Update {
path: self.current_path.clone(),
new_props_hash: new.props_hash,
});
}
self.diff_children(&old.children, &new.children, result);
}
fn diff_children(
&mut self,
old_children: &[DiffNode],
new_children: &[DiffNode],
result: &mut DiffResult,
) {
let old_keyed: HashMap<&str, usize> = old_children
.iter()
.enumerate()
.filter_map(|(i, c)| c.key.as_deref().map(|k| (k, i)))
.collect();
let _new_keyed: HashMap<&str, usize> = new_children
.iter()
.enumerate()
.filter_map(|(i, c)| c.key.as_deref().map(|k| (k, i)))
.collect();
let mut old_matched = vec![false; old_children.len()];
let mut new_matched = vec![false; new_children.len()];
for (new_idx, new_child) in new_children.iter().enumerate() {
if let Some(key) = &new_child.key {
if let Some(&old_idx) = old_keyed.get(key.as_str()) {
old_matched[old_idx] = true;
new_matched[new_idx] = true;
if old_idx != new_idx {
let mut from_path = self.current_path.clone();
from_path.push(old_idx);
let mut to_path = self.current_path.clone();
to_path.push(new_idx);
result.push(DiffOp::Move { from_path, to_path });
}
self.current_path.push(new_idx);
self.diff_node(&old_children[old_idx], new_child, result);
self.current_path.pop();
}
}
}
let old_unkeyed: Vec<usize> = old_children
.iter()
.enumerate()
.filter(|(i, c)| c.key.is_none() && !old_matched[*i])
.map(|(i, _)| i)
.collect();
let new_unkeyed: Vec<usize> = new_children
.iter()
.enumerate()
.filter(|(i, c)| c.key.is_none() && !new_matched[*i])
.map(|(i, _)| i)
.collect();
let mut old_unkeyed_used = vec![false; old_unkeyed.len()];
for new_pos in &new_unkeyed {
let new_child = &new_children[*new_pos];
let mut found = false;
for (old_pos_idx, old_pos) in old_unkeyed.iter().enumerate() {
if old_unkeyed_used[old_pos_idx] {
continue;
}
let old_child = &old_children[*old_pos];
if old_child.type_id == new_child.type_id {
old_unkeyed_used[old_pos_idx] = true;
old_matched[*old_pos] = true;
new_matched[*new_pos] = true;
found = true;
self.current_path.push(*new_pos);
self.diff_node(old_child, new_child, result);
self.current_path.pop();
break;
}
}
if !found {
new_matched[*new_pos] = true;
result.push(DiffOp::Insert {
path: self.current_path.clone(),
index: *new_pos,
type_id: new_child.type_id,
props_hash: new_child.props_hash,
});
self.current_path.push(*new_pos);
self.insert_subtree(new_child, result);
self.current_path.pop();
}
}
for (i, matched) in old_matched.iter().enumerate().rev() {
if !matched {
let mut path = self.current_path.clone();
path.push(i);
result.push(DiffOp::Remove { path });
}
}
for (i, matched) in new_matched.iter().enumerate() {
if !matched {
let new_child = &new_children[i];
result.push(DiffOp::Insert {
path: self.current_path.clone(),
index: i,
type_id: new_child.type_id,
props_hash: new_child.props_hash,
});
self.current_path.push(i);
self.insert_subtree(new_child, result);
self.current_path.pop();
}
}
}
fn insert_subtree(&mut self, node: &DiffNode, result: &mut DiffResult) {
for (i, child) in node.children.iter().enumerate() {
result.push(DiffOp::Insert {
path: self.current_path.clone(),
index: i,
type_id: child.type_id,
props_hash: child.props_hash,
});
self.current_path.push(i);
self.insert_subtree(child, result);
self.current_path.pop();
}
}
}
#[must_use]
pub fn diff_trees(old: &DiffNode, new: &DiffNode) -> DiffResult {
let mut differ = TreeDiffer::new();
differ.diff(old, new)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_type_id<T: 'static>() -> TypeId {
TypeId::of::<T>()
}
#[test]
fn test_widget_key_string() {
let key = WidgetKey::string("test");
assert_eq!(key, WidgetKey::String("test".to_string()));
}
#[test]
fn test_widget_key_index() {
let key = WidgetKey::index(42);
assert_eq!(key, WidgetKey::Index(42));
}
#[test]
fn test_diff_node_new() {
let type_id = make_type_id::<u32>();
let node = DiffNode::new(type_id, 123);
assert_eq!(node.type_id, type_id);
assert_eq!(node.props_hash, 123);
assert!(node.key.is_none());
assert!(node.children.is_empty());
}
#[test]
fn test_diff_node_with_key() {
let type_id = make_type_id::<u32>();
let node = DiffNode::new(type_id, 123).with_key("my-key");
assert_eq!(node.key, Some("my-key".to_string()));
}
#[test]
fn test_diff_node_with_child() {
let type_id = make_type_id::<u32>();
let child = DiffNode::new(type_id, 456);
let parent = DiffNode::new(type_id, 123).with_child(child);
assert_eq!(parent.children.len(), 1);
assert_eq!(parent.children[0].props_hash, 456);
}
#[test]
fn test_diff_result_empty() {
let result = DiffResult::new();
assert!(result.is_empty());
assert_eq!(result.len(), 0);
}
#[test]
fn test_diff_identical_trees() {
let type_id = make_type_id::<u32>();
let old = DiffNode::new(type_id, 123);
let new = DiffNode::new(type_id, 123);
let result = diff_trees(&old, &new);
assert!(result.is_empty());
}
#[test]
fn test_diff_props_changed() {
let type_id = make_type_id::<u32>();
let old = DiffNode::new(type_id, 123);
let new = DiffNode::new(type_id, 456);
let result = diff_trees(&old, &new);
assert_eq!(result.len(), 1);
assert!(matches!(
&result.operations[0],
DiffOp::Update { path, new_props_hash: 456 } if path.is_empty()
));
}
#[test]
fn test_diff_type_changed() {
let old_type = make_type_id::<u32>();
let new_type = make_type_id::<String>();
let old = DiffNode::new(old_type, 123);
let new = DiffNode::new(new_type, 123);
let result = diff_trees(&old, &new);
assert_eq!(result.len(), 1);
assert!(matches!(
&result.operations[0],
DiffOp::Replace { path, new_type_id, .. } if path.is_empty() && *new_type_id == new_type
));
}
#[test]
fn test_diff_child_added() {
let type_id = make_type_id::<u32>();
let child_type = make_type_id::<String>();
let old = DiffNode::new(type_id, 123);
let new = DiffNode::new(type_id, 123).with_child(DiffNode::new(child_type, 456));
let result = diff_trees(&old, &new);
assert_eq!(result.len(), 1);
assert!(matches!(
&result.operations[0],
DiffOp::Insert { path, index: 0, type_id: t, .. } if path.is_empty() && *t == child_type
));
}
#[test]
fn test_diff_child_removed() {
let type_id = make_type_id::<u32>();
let child_type = make_type_id::<String>();
let old = DiffNode::new(type_id, 123).with_child(DiffNode::new(child_type, 456));
let new = DiffNode::new(type_id, 123);
let result = diff_trees(&old, &new);
assert_eq!(result.len(), 1);
assert!(matches!(
&result.operations[0],
DiffOp::Remove { path } if *path == vec![0]
));
}
#[test]
fn test_diff_keyed_children_reordered() {
let type_id = make_type_id::<u32>();
let old = DiffNode::new(type_id, 0)
.with_child(DiffNode::new(type_id, 1).with_key("a"))
.with_child(DiffNode::new(type_id, 2).with_key("b"));
let new = DiffNode::new(type_id, 0)
.with_child(DiffNode::new(type_id, 2).with_key("b"))
.with_child(DiffNode::new(type_id, 1).with_key("a"));
let result = diff_trees(&old, &new);
let move_ops: Vec<_> = result
.operations
.iter()
.filter(|op| matches!(op, DiffOp::Move { .. }))
.collect();
assert!(!move_ops.is_empty());
}
#[test]
fn test_diff_keyed_child_updated() {
let type_id = make_type_id::<u32>();
let old = DiffNode::new(type_id, 0).with_child(DiffNode::new(type_id, 1).with_key("item"));
let new = DiffNode::new(type_id, 0).with_child(DiffNode::new(type_id, 2).with_key("item"));
let result = diff_trees(&old, &new);
let update_ops: Vec<_> = result
.operations
.iter()
.filter(|op| matches!(op, DiffOp::Update { .. }))
.collect();
assert_eq!(update_ops.len(), 1);
}
#[test]
fn test_diff_nested_changes() {
let type_id = make_type_id::<u32>();
let old = DiffNode::new(type_id, 0)
.with_child(DiffNode::new(type_id, 1).with_child(DiffNode::new(type_id, 2)));
let new = DiffNode::new(type_id, 0)
.with_child(DiffNode::new(type_id, 1).with_child(DiffNode::new(type_id, 3)));
let result = diff_trees(&old, &new);
let update_ops: Vec<_> = result
.operations
.iter()
.filter(|op| matches!(op, DiffOp::Update { path, .. } if *path == vec![0, 0]))
.collect();
assert_eq!(update_ops.len(), 1);
}
#[test]
fn test_diff_multiple_children_mixed() {
let type_id = make_type_id::<u32>();
let string_type = make_type_id::<String>();
let old = DiffNode::new(type_id, 0)
.with_child(DiffNode::new(type_id, 1))
.with_child(DiffNode::new(string_type, 2))
.with_child(DiffNode::new(type_id, 3));
let new = DiffNode::new(type_id, 0)
.with_child(DiffNode::new(type_id, 1))
.with_child(DiffNode::new(type_id, 4));
let result = diff_trees(&old, &new);
let remove_ops: Vec<_> = result
.operations
.iter()
.filter(|op| matches!(op, DiffOp::Remove { .. }))
.collect();
assert!(!remove_ops.is_empty());
}
#[test]
fn test_tree_differ_reuse() {
let type_id = make_type_id::<u32>();
let mut differ = TreeDiffer::new();
let old1 = DiffNode::new(type_id, 1);
let new1 = DiffNode::new(type_id, 2);
let result1 = differ.diff(&old1, &new1);
let old2 = DiffNode::new(type_id, 3);
let new2 = DiffNode::new(type_id, 3);
let result2 = differ.diff(&old2, &new2);
assert_eq!(result1.len(), 1);
assert!(result2.is_empty());
}
#[test]
fn test_diff_empty_to_tree() {
let type_id = make_type_id::<u32>();
let old = DiffNode::new(type_id, 0);
let new = DiffNode::new(type_id, 0)
.with_child(DiffNode::new(type_id, 1))
.with_child(DiffNode::new(type_id, 2));
let result = diff_trees(&old, &new);
let insert_ops: Vec<_> = result
.operations
.iter()
.filter(|op| matches!(op, DiffOp::Insert { .. }))
.collect();
assert_eq!(insert_ops.len(), 2);
}
#[test]
fn test_diff_tree_to_empty() {
let type_id = make_type_id::<u32>();
let old = DiffNode::new(type_id, 0)
.with_child(DiffNode::new(type_id, 1))
.with_child(DiffNode::new(type_id, 2));
let new = DiffNode::new(type_id, 0);
let result = diff_trees(&old, &new);
let remove_ops: Vec<_> = result
.operations
.iter()
.filter(|op| matches!(op, DiffOp::Remove { .. }))
.collect();
assert_eq!(remove_ops.len(), 2);
}
#[test]
fn test_diff_deeply_nested() {
let type_id = make_type_id::<u32>();
let old = DiffNode::new(type_id, 0).with_child(
DiffNode::new(type_id, 1)
.with_child(DiffNode::new(type_id, 2).with_child(DiffNode::new(type_id, 3))),
);
let new = DiffNode::new(type_id, 0).with_child(
DiffNode::new(type_id, 1)
.with_child(DiffNode::new(type_id, 2).with_child(DiffNode::new(type_id, 99))),
);
let result = diff_trees(&old, &new);
let update_ops: Vec<_> = result
.operations
.iter()
.filter(|op| {
matches!(op, DiffOp::Update { path, new_props_hash: 99 } if *path == vec![0, 0, 0])
})
.collect();
assert_eq!(update_ops.len(), 1);
}
#[test]
fn test_diff_op_debug() {
let op = DiffOp::Insert {
path: vec![0, 1],
index: 2,
type_id: make_type_id::<u32>(),
props_hash: 123,
};
let debug_str = format!("{op:?}");
assert!(debug_str.contains("Insert"));
}
#[test]
fn test_diff_result_push() {
let mut result = DiffResult::new();
result.push(DiffOp::Remove { path: vec![0] });
assert_eq!(result.len(), 1);
assert!(!result.is_empty());
}
}