Skip to main content

teksilo_data/
tree_change.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! TreeChange — change notifications and stable node identifiers for tree collections.
5//!
6//! [`NodeId`] is an opaque, stable handle for a node in a [`crate::TreeModel`].
7//! Because `TreeModel` is backed by a slotmap, `NodeId` values survive arbitrary
8//! insertions, removals, and moves — only deleting the node itself invalidates it.
9//! [`TreeChange`] describes exactly what mutated in the tree so that projections
10//! (`SortFilterTreeModel`, `TreeSlice`) can refresh efficiently and emit
11//! fine-grained divergence hints.
12//!
13//! Consumers typically receive `TreeChange` values through an observer registered
14//! via [`crate::TreeModel::observe_changes`], which fires synchronously (before
15//! the registering call returns) after each mutation. The projections listed above
16//! subscribe internally; app code rarely needs to subscribe directly.
17//!
18//! ```ignore
19//! // TreeModel::observe_changes returns an ObserverHandle whose drop
20//! // unregisters the callback — keep it alive for the observer's lifetime.
21//! use teksilo_data::{TreeModel, TreeChange};
22//! let tree: TreeModel<String> = TreeModel::new();
23//! let _handle = tree.observe_changes(|change| {
24//!     println!("{change:?}");
25//! });
26//! tree.insert_root(0, "root".to_string());
27//! // prints: NodeInserted { parent: None, index: 0, node: NodeId(...) }
28//! ```
29
30/// Opaque identifier for a node in a `TreeModel`.
31///
32/// `NodeId` values are stable across mutations — inserting or removing other
33/// nodes does not invalidate existing `NodeId` handles (they are SlotMap keys).
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub struct NodeId(slotmap::DefaultKey);
36
37impl NodeId {
38    pub(crate) fn from_key(key: slotmap::DefaultKey) -> Self {
39        Self(key)
40    }
41
42    pub(crate) fn key(self) -> slotmap::DefaultKey {
43        self.0
44    }
45}
46
47/// Describes a mutation to a tree structure. Emitted by `TreeModel<T>` automatically.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum TreeChange {
50    /// A node was inserted as a child of `parent` at the given index.
51    /// `parent` is `None` for root-level insertions.
52    NodeInserted {
53        parent: Option<NodeId>,
54        index: usize,
55        node: NodeId,
56    },
57
58    /// A node (and its entire subtree) was removed.
59    /// `parent` is `None` if it was a root-level node.
60    NodeRemoved {
61        parent: Option<NodeId>,
62        node: NodeId,
63    },
64
65    /// A node was moved to a new parent at the given index.
66    NodeMoved {
67        node: NodeId,
68        old_parent: Option<NodeId>,
69        new_parent: Option<NodeId>,
70        new_index: usize,
71    },
72
73    /// A node's data was updated in place.
74    NodeUpdated { node: NodeId },
75
76    /// The entire tree was replaced. Consumers should discard all state and rebuild.
77    Reset,
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn node_id_equality() {
86        use slotmap::SlotMap;
87        let mut sm: SlotMap<slotmap::DefaultKey, ()> = SlotMap::new();
88        let k1 = sm.insert(());
89        let k2 = sm.insert(());
90        let id1 = NodeId::from_key(k1);
91        let id1_clone = NodeId::from_key(k1);
92        let id2 = NodeId::from_key(k2);
93
94        assert_eq!(id1, id1_clone);
95        assert_ne!(id1, id2);
96    }
97}