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)
    }
}