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}