//! Comparing two widget trees structurally. //! //! A scene comparison says two trees *draw* the same thing, which is most of //! what matters and none of what a registration is. This says they *are* the //! same thing: walk both in lockstep and describe the first place they part //! company, down to what each node has attached to it and what the tree holds //! on their behalf. //! //! It is what makes "a rebuilt tree is indistinguishable from a fresh one" //! checkable — the invariant a hot reload has to meet, and one that a count of //! resources cannot state, because a rearrangement leaves every count alone. //! //! Widget ids belong to the tree that minted them, so nothing here compares //! them; it compares position, shape, and the attributes a node carries. The //! result is a path rather than a bool, because "these trees differ" is not //! useful and "root > child 2 > child 0: 3 handlers vs 4" is. use super::{WidgetId, WidgetTree}; impl WidgetTree { /// Describe the first structural difference between two trees, or `None` /// if they are the same shape throughout. /// /// Compares, at every corresponding position: widget type, style classes, /// tooltip, enabled flag, attached handler count, child count, and laid-out /// rectangle; then the tree-level registrations, the overlay layers, and /// where focus sits as a path. /// /// Two of those are worth naming, because a scene comparison sees neither. /// **Handler count** is how a wire registered twice, or not at all, shows /// up — the widgets are identical and nothing is left over, so only /// counting them at their position finds it. **The tree-level lists** are /// where a subtree's registrations *outside* itself land: a menu's /// accelerator belongs to no node, so a walk alone would never see one /// outliving the menu that declared it. pub fn structural_diff(&self, other: &WidgetTree) -> Option { let registrations = [ ("shortcuts", self.shortcuts.len(), other.shortcuts.len()), ("overlay layers", self.overlays.len(), other.overlays.len()), ("open dialogs", self.dialogs.len(), other.dialogs.len()), ("open menus", self.menu_stack.len(), other.menu_stack.len()), ( "pending wakes", self.deadlines.len() + self.wake_requests.len(), other.deadlines.len() + other.wake_requests.len(), ), ( "accessibility owners", self.a11y_owners.len(), other.a11y_owners.len(), ), ]; for (what, mine, theirs) in registrations { if mine != theirs { return Some(format!("{what}: {mine} vs {theirs}")); } } match (self.root, other.root) { (None, None) => {} (Some(a), Some(b)) => { if let Some(diff) = self.diff_node(other, a, b, "root") { return Some(diff); } } (a, b) => { return Some(format!( "one tree has a root and the other does not ({}, {})", a.is_some(), b.is_some() )); } } for (index, (mine, theirs)) in self.overlays.iter().zip(&other.overlays).enumerate() { if let Some(diff) = self.diff_node(other, mine.root, theirs.root, &format!("overlay {index}")) { return Some(diff); } } let (mine, theirs) = (self.focus_path(), other.focus_path()); if mine != theirs { return Some(format!("focus sits at {mine:?} vs {theirs:?}")); } None } fn diff_node( &self, other: &WidgetTree, mine: WidgetId, theirs: WidgetId, path: &str, ) -> Option { let (Some(a), Some(b)) = (self.nodes.get(mine), other.nodes.get(theirs)) else { return Some(format!("{path}: one side has no node")); }; let differs = |what: &str, x: String, y: String| { (x != y).then(|| format!("{path}: {what} {x} vs {y}")) }; let checks = [ differs( "widget type", a.widget.type_name().to_owned(), b.widget.type_name().to_owned(), ), differs( "classes", format!("{:?}", a.classes), format!("{:?}", b.classes), ), differs( "tooltip", format!("{:?}", a.tooltip), format!("{:?}", b.tooltip), ), differs("enabled", a.enabled.to_string(), b.enabled.to_string()), differs( "handler count", a.handlers.len().to_string(), b.handlers.len().to_string(), ), differs( "child count", a.children.len().to_string(), b.children.len().to_string(), ), differs( "layout", format!("{:?}", a.last_layout), format!("{:?}", b.last_layout), ), ]; if let Some(diff) = checks.into_iter().flatten().next() { return Some(diff); } for (index, (mine, theirs)) in a.children.iter().zip(&b.children).enumerate() { if let Some(diff) = self.diff_node(other, *mine, *theirs, &format!("{path} > child {index}")) { return Some(diff); } } None } /// Where focus sits, as child indices from the root — an id means nothing /// to another tree, but a position does. fn focus_path(&self) -> Option> { let mut current = self.focus?; let mut path = Vec::new(); while let Some(parent) = self.parent(current) { path.push(self.child_index(parent, current)?); current = parent; } path.reverse(); Some(path) } }