use crate::tree::{WidgetId, WidgetNode, WidgetTree};
use std::collections::HashSet;
#[derive(Clone, Debug, PartialEq)]
pub enum DiffOp {
Insert {
id: WidgetId,
parent: WidgetId,
index: usize,
},
Remove {
id: WidgetId,
},
Update {
id: WidgetId,
},
Move {
id: WidgetId,
parent: WidgetId,
index: usize,
},
}
pub fn diff(old: &WidgetTree, new: &WidgetTree) -> Vec<DiffOp> {
let mut ops = Vec::new();
let old_ids: HashSet<WidgetId> = collect_ids(old);
let new_ids: HashSet<WidgetId> = collect_ids(new);
for &id in old_ids.iter() {
if !new_ids.contains(&id) {
let ancestor_removed = ancestor_chain(old, id)
.into_iter()
.any(|anc| anc != id && !new_ids.contains(&anc));
if !ancestor_removed {
ops.push(DiffOp::Remove { id });
}
}
}
let mut visit_order = Vec::new();
new.walk_dfs(|node, _| visit_order.push(node.id));
for id in visit_order {
let new_node = match new.get(id) {
Some(n) => n,
None => continue,
};
match old.get(id) {
Some(old_node) => {
if node_changed(old_node, new_node) {
ops.push(DiffOp::Update { id });
}
reconcile_children(
id,
&old_node.children,
&new_node.children,
&old_ids,
&mut ops,
);
}
None => {
}
}
}
ops
}
fn reconcile_children(
parent: WidgetId,
old_children: &[WidgetId],
new_children: &[WidgetId],
old_ids: &HashSet<WidgetId>,
ops: &mut Vec<DiffOp>,
) {
let lcs = longest_common_subsequence(old_children, new_children);
let kept: HashSet<WidgetId> = lcs.iter().copied().collect();
for (index, &child) in new_children.iter().enumerate() {
if kept.contains(&child) {
continue;
}
if old_ids.contains(&child) {
ops.push(DiffOp::Move {
id: child,
parent,
index,
});
} else {
ops.push(DiffOp::Insert {
id: child,
parent,
index,
});
}
}
}
fn node_changed(old: &WidgetNode, new: &WidgetNode) -> bool {
old.rect != new.rect
|| old.z_index != new.z_index
|| old.hit_testable != new.hit_testable
|| old.focusable != new.focusable
|| old.clip_rect != new.clip_rect
|| old.label != new.label
}
fn collect_ids(tree: &WidgetTree) -> HashSet<WidgetId> {
let mut ids = HashSet::new();
tree.walk_dfs(|node, _| {
ids.insert(node.id);
});
ids
}
fn ancestor_chain(tree: &WidgetTree, id: WidgetId) -> Vec<WidgetId> {
let mut chain = Vec::new();
let mut cur = tree.get(id);
while let Some(node) = cur {
chain.push(node.id);
cur = node.parent.and_then(|p| tree.get(p));
}
chain
}
fn longest_common_subsequence(a: &[WidgetId], b: &[WidgetId]) -> Vec<WidgetId> {
let n = a.len();
let m = b.len();
if n == 0 || m == 0 {
return Vec::new();
}
let mut dp = vec![vec![0u32; m + 1]; n + 1];
for i in (0..n).rev() {
for j in (0..m).rev() {
dp[i][j] = if a[i] == b[j] {
dp[i + 1][j + 1] + 1
} else {
dp[i + 1][j].max(dp[i][j + 1])
};
}
}
let mut result = Vec::new();
let (mut i, mut j) = (0usize, 0usize);
while i < n && j < m {
if a[i] == b[j] {
result.push(a[i]);
i += 1;
j += 1;
} else if dp[i + 1][j] >= dp[i][j + 1] {
i += 1;
} else {
j += 1;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::Rect;
fn r(x: f32) -> Rect {
Rect::new(x, 0.0, 10.0, 10.0)
}
#[test]
fn lcs_basic() {
let a = [WidgetId(1), WidgetId(2), WidgetId(3), WidgetId(4)];
let b = [WidgetId(2), WidgetId(4), WidgetId(1), WidgetId(3)];
let lcs = longest_common_subsequence(&a, &b);
assert_eq!(lcs.len(), 2);
}
#[test]
fn identical_trees_yield_no_ops() {
let mut old = WidgetTree::new(r(0.0));
let a = old.insert(WidgetId::ROOT, r(1.0)).expect("root");
let _b = old.insert(WidgetId::ROOT, r(2.0)).expect("root");
let _c = old.insert(a, r(3.0)).expect("a");
let mut new = WidgetTree::new(r(0.0));
let a2 = new.insert(WidgetId::ROOT, r(1.0)).expect("root");
let _b2 = new.insert(WidgetId::ROOT, r(2.0)).expect("root");
let _c2 = new.insert(a2, r(3.0)).expect("a");
let ops = diff(&old, &new);
assert!(
ops.is_empty(),
"identical trees should diff to nothing: {ops:?}"
);
}
#[test]
fn detects_field_update() {
let mut old = WidgetTree::new(r(0.0));
let a = old.insert(WidgetId::ROOT, r(1.0)).expect("root");
let mut new = WidgetTree::new(r(0.0));
let a2 = new.insert(WidgetId::ROOT, r(1.0)).expect("root");
if let Some(n) = new.get_mut(a2) {
n.rect = r(99.0); }
assert_eq!(a, a2);
let ops = diff(&old, &new);
assert!(
ops.contains(&DiffOp::Update { id: a }),
"expected Update, got {ops:?}"
);
}
#[test]
fn detects_insert_and_remove() {
let mut old = WidgetTree::new(r(0.0));
let a = old.insert(WidgetId::ROOT, r(1.0)).expect("root");
let mut new = WidgetTree::new(r(0.0));
let a2 = new.insert(WidgetId::ROOT, r(1.0)).expect("root");
let b = new.insert(WidgetId::ROOT, r(2.0)).expect("root");
assert_eq!(a, a2);
let ops = diff(&old, &new);
assert!(
ops.iter()
.any(|o| matches!(o, DiffOp::Insert { id, parent, index }
if *id == b && *parent == WidgetId::ROOT && *index == 1)),
"expected Insert of b at index 1, got {ops:?}"
);
let ops_rev = diff(&new, &old);
assert!(
ops_rev.contains(&DiffOp::Remove { id: b }),
"expected Remove of b, got {ops_rev:?}"
);
}
#[test]
fn removed_subtree_emits_single_remove() {
let mut old = WidgetTree::new(r(0.0));
let a = old.insert(WidgetId::ROOT, r(1.0)).expect("root");
let a1 = old.insert(a, r(2.0)).expect("a");
let new = WidgetTree::new(r(0.0));
let ops = diff(&old, &new);
assert!(ops.contains(&DiffOp::Remove { id: a }), "got {ops:?}");
assert!(
!ops.contains(&DiffOp::Remove { id: a1 }),
"child should not get its own Remove: {ops:?}"
);
}
#[test]
fn reorder_emits_minimal_moves() {
let mut old = WidgetTree::new(r(0.0));
let c1 = old.insert(WidgetId::ROOT, r(1.0)).expect("root");
let c2 = old.insert(WidgetId::ROOT, r(2.0)).expect("root");
let c3 = old.insert(WidgetId::ROOT, r(3.0)).expect("root");
let mut new = WidgetTree::new(r(0.0));
let _ = new.insert(WidgetId::ROOT, r(1.0)).expect("root"); let _ = new.insert(WidgetId::ROOT, r(2.0)).expect("root"); let _ = new.insert(WidgetId::ROOT, r(3.0)).expect("root"); if let Some(root) = new.get_mut(WidgetId::ROOT) {
root.children = vec![c3, c1, c2];
}
let ops = diff(&old, &new);
let moves: Vec<_> = ops
.iter()
.filter(|o| matches!(o, DiffOp::Move { .. }))
.collect();
assert_eq!(moves.len(), 1, "exactly one move expected, got {ops:?}");
assert!(
matches!(moves[0], DiffOp::Move { id, index, .. } if *id == c3 && *index == 0),
"c3 should move to index 0, got {:?}",
moves[0]
);
}
}