compare.rs
raw
//! 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<String> {
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<String> {
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<Vec<usize>> {
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)
}
}