Skip to main content

teksilo_data/
tree_slice.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! `TreeSlice` — per-view flattened projection of a [`TreeModel`].
5//!
6//! `TreeSlice<T>` wraps a `TreeModel<T>` and maintains an independent
7//! expand/collapse set so two `TreeView` widgets sharing the same model have
8//! independent visible rows — dual-pane file managers, overview/detail splits,
9//! and search results panels are each one `TreeSlice::new(model.clone())`. The
10//! slice re-flattens automatically whenever the underlying model emits a
11//! [`TreeChange`], and bumps a [`version_signal`](TreeSlice::version_signal)
12//! `Signal<u64>` that views bind at `BindingLevel::Rebuild`.
13//!
14//! A lightweight [`TreeSliceHandle`] (created via [`TreeSlice::handle`]) shares
15//! all `Rc`-based internals and is usable in closures without keeping the
16//! tree-change observer alive.
17//!
18//! `TreeSlice` implements [`TreeDataSource`] and is the
19//! built-in source for `TreeView` / `TreeTableView`.
20//!
21//! ## Example
22//!
23//! ```rust
24//! # use teksilo_data::{TreeModel, TreeSlice};
25//! let tree = TreeModel::new();
26//! let root = tree.insert_root(0, "root");
27//! let child = tree.insert_child(root, 0, "child");
28//!
29//! let slice1 = TreeSlice::new(tree.clone());
30//! let slice2 = TreeSlice::new(tree.clone());
31//!
32//! slice1.expand(root);
33//! assert_eq!(slice1.visible_count(), 2); // root + child visible
34//! assert_eq!(slice2.visible_count(), 1); // still collapsed in slice2
35//!
36//! // Inserting into the model notifies both slices.
37//! tree.insert_child(root, 1, "child2");
38//! assert_eq!(slice1.visible_count(), 3); // child2 also visible in the expanded slice
39//! ```
40
41use std::cell::RefCell;
42use std::collections::{HashMap, HashSet};
43use std::rc::Rc;
44
45use teksilo_core::ObserverHandle;
46use teksilo_core::signal::Signal;
47
48use crate::TreeModel;
49use crate::dnd_types::{DragEligibility, DragSource, DropCommit, DropQuery, DropResponse};
50use crate::tree_change::{NodeId, TreeChange};
51use crate::tree_data_source::{
52    FlatEntry, TreeDataSource, tree_apply_reorder, tree_is_desc_or_self,
53};
54
55/// Per-view flattened projection of a [`TreeModel<T>`](crate::TreeModel).
56///
57/// Owns an independent expand/collapse set and re-flattens automatically on
58/// every [`TreeChange`] from the underlying model. Two slices
59/// over the same model have completely independent expand state. See the
60/// [module documentation](self) for the full picture.
61pub struct TreeSlice<T: 'static> {
62    tree: TreeModel<T>,
63    expanded: Rc<RefCell<HashSet<NodeId>>>,
64    flattened: Rc<RefCell<Vec<FlatEntry>>>,
65    /// `NodeId` → flat index, rebuilt alongside `flattened` on every
66    /// reflatten. Keeps `flat_index_of` O(1) (mirrors `TreeDataSlice`'s
67    /// `vis_pos`) instead of a linear scan over `flattened`.
68    positions: Rc<RefCell<HashMap<NodeId, usize>>>,
69    version: Signal<u64>,
70    version_counter: Rc<std::cell::Cell<u64>>,
71    /// First flat index whose content may differ after the latest
72    /// reflatten. See [`first_changed_index`](Self::first_changed_index).
73    divergence: Rc<std::cell::Cell<Option<usize>>>,
74    _tree_observer: ObserverHandle,
75}
76
77impl<T: 'static> TreeSlice<T> {
78    /// Create a new `TreeSlice` for the given `TreeModel`.
79    /// All nodes start collapsed (only roots are visible).
80    pub fn new(tree: TreeModel<T>) -> Self {
81        let expanded: Rc<RefCell<HashSet<NodeId>>> = Rc::new(RefCell::new(HashSet::new()));
82        let flattened: Rc<RefCell<Vec<FlatEntry>>> = Rc::new(RefCell::new(Vec::new()));
83        let positions: Rc<RefCell<HashMap<NodeId, usize>>> = Rc::new(RefCell::new(HashMap::new()));
84        let version = Signal::new(0_u64);
85        let version_counter = Rc::new(std::cell::Cell::new(0_u64));
86        let divergence = Rc::new(std::cell::Cell::new(None));
87
88        // Initial flatten
89        Self::rebuild_flat_list(
90            &tree,
91            &expanded.borrow(),
92            &mut flattened.borrow_mut(),
93            &mut positions.borrow_mut(),
94        );
95
96        // Observe tree changes
97        let exp = expanded.clone();
98        let flat = flattened.clone();
99        let pos = positions.clone();
100        let tree_for_obs = tree.clone();
101        let ver = version.clone();
102        let vc = version_counter.clone();
103        let div = divergence.clone();
104        let observer = tree.observe_changes(move |change| {
105            let mut d = Self::rebuild_flat_list(
106                &tree_for_obs,
107                &exp.borrow(),
108                &mut flat.borrow_mut(),
109                &mut pos.borrow_mut(),
110            );
111            // A NodeUpdated leaves the flat structure identical, but the
112            // updated node's content (and thus any per-row derived state
113            // such as a measured height) changed — fold its flat position
114            // into the divergence.
115            if let TreeChange::NodeUpdated { node } = change
116                && let Some(&p) = pos.borrow().get(node)
117            {
118                d = d.min(p);
119            }
120            div.set(Some(d));
121            let next = vc.get() + 1;
122            vc.set(next);
123            ver.set(next);
124        });
125
126        Self {
127            tree,
128            expanded,
129            flattened,
130            positions,
131            version,
132            version_counter,
133            divergence,
134            _tree_observer: observer,
135        }
136    }
137
138    /// Number of currently visible (flattened) rows.
139    pub fn visible_count(&self) -> usize {
140        self.flattened.borrow().len()
141    }
142
143    /// Access a flat entry by index via callback.
144    /// The callback receives `(&T, &FlatEntry)`.
145    pub fn with_entry<R>(
146        &self,
147        flat_index: usize,
148        f: impl FnOnce(&T, &FlatEntry) -> R,
149    ) -> Option<R> {
150        let flat = self.flattened.borrow();
151        let entry = flat.get(flat_index)?;
152        let node_id = entry.node_id;
153        // We need to access tree data while holding the flat borrow.
154        // Since tree.with_item borrows the tree's inner RefCell (separate from ours), this is safe.
155        self.tree.with_item(node_id, |item| f(item, entry))
156    }
157
158    /// Get the `NodeId` at the given flat index.
159    pub fn visible_node_id(&self, flat_index: usize) -> Option<NodeId> {
160        self.flattened.borrow().get(flat_index).map(|e| e.node_id)
161    }
162
163    /// Get the `FlatEntry` at the given flat index (cloned).
164    pub fn entry_at(&self, flat_index: usize) -> Option<FlatEntry> {
165        self.flattened.borrow().get(flat_index).cloned()
166    }
167
168    /// Get the depth at the given flat index.
169    pub fn depth_at(&self, flat_index: usize) -> usize {
170        self.flattened
171            .borrow()
172            .get(flat_index)
173            .map(|e| e.depth)
174            .unwrap_or(0)
175    }
176
177    /// Find the flat index for a given `NodeId`, or `None` if not visible.
178    /// O(1) — backed by a position map rebuilt on every reflatten.
179    pub fn flat_index_of(&self, node: NodeId) -> Option<usize> {
180        self.positions.borrow().get(&node).copied()
181    }
182
183    // --- Expand / Collapse ---
184
185    /// Whether the given node is expanded.
186    pub fn is_expanded(&self, node: NodeId) -> bool {
187        self.expanded.borrow().contains(&node)
188    }
189
190    /// Expand a node (make its children visible).
191    pub fn expand(&self, node: NodeId) {
192        {
193            let mut exp = self.expanded.borrow_mut();
194            if !exp.insert(node) {
195                return; // Already expanded
196            }
197        }
198        self.reflatten_and_notify();
199    }
200
201    /// Collapse a node (hide its children).
202    pub fn collapse(&self, node: NodeId) {
203        {
204            let mut exp = self.expanded.borrow_mut();
205            if !exp.remove(&node) {
206                return; // Already collapsed
207            }
208        }
209        self.reflatten_and_notify();
210    }
211
212    /// Toggle expand/collapse state of a node.
213    pub fn toggle(&self, node: NodeId) {
214        {
215            let mut exp = self.expanded.borrow_mut();
216            if exp.contains(&node) {
217                exp.remove(&node);
218            } else {
219                exp.insert(node);
220            }
221        }
222        self.reflatten_and_notify();
223    }
224
225    /// Expand all nodes in the tree.
226    pub fn expand_all(&self) {
227        {
228            let mut exp = self.expanded.borrow_mut();
229            self.expand_all_recursive(&mut exp);
230        }
231        self.reflatten_and_notify();
232    }
233
234    /// Collapse all nodes in the tree.
235    pub fn collapse_all(&self) {
236        {
237            let mut exp = self.expanded.borrow_mut();
238            exp.clear();
239        }
240        self.reflatten_and_notify();
241    }
242
243    /// Get all expanded node IDs (for persistence).
244    pub fn expanded_nodes(&self) -> Vec<NodeId> {
245        self.expanded.borrow().iter().copied().collect()
246    }
247
248    /// Restore expanded state (for persistence).
249    pub fn set_expanded_nodes(&self, nodes: &[NodeId]) {
250        {
251            let mut exp = self.expanded.borrow_mut();
252            exp.clear();
253            for &node in nodes {
254                exp.insert(node);
255            }
256        }
257        self.reflatten_and_notify();
258    }
259
260    /// Get the version signal for binding to `BindingLevel::Rebuild`.
261    pub fn version_signal(&self) -> Signal<u64> {
262        self.version.clone()
263    }
264
265    /// First flat index whose content may differ from before the latest
266    /// reflatten — the rows `0..index` are the same nodes, at the same
267    /// depths, with the same expand state as before, so any per-row
268    /// derived state (e.g. a measured row height) remains valid for them.
269    /// Equal to `visible_count()` when the visible list is unchanged.
270    ///
271    /// `None` means unknown (no reflatten observed yet) — treat as a full
272    /// change. The value describes the **latest** reflatten only; read it
273    /// synchronously from a `version_signal()` observer (observers fire
274    /// inline on every bump, so per-change reads cannot miss a value).
275    pub fn first_changed_index(&self) -> Option<usize> {
276        self.divergence.get()
277    }
278
279    /// Access the underlying `TreeModel`.
280    pub fn tree(&self) -> &TreeModel<T> {
281        &self.tree
282    }
283
284    /// Create a lightweight handle for use in closures.
285    /// Shares all Rc-based internals but does not keep the observer alive.
286    pub fn handle(&self) -> TreeSliceHandle<T> {
287        TreeSliceHandle {
288            tree: self.tree.clone(),
289            expanded: self.expanded.clone(),
290            flattened: self.flattened.clone(),
291            positions: self.positions.clone(),
292            version: self.version.clone(),
293            version_counter: self.version_counter.clone(),
294            divergence: self.divergence.clone(),
295        }
296    }
297
298    // --- Internal ---
299
300    fn reflatten_and_notify(&self) {
301        let d = Self::rebuild_flat_list(
302            &self.tree,
303            &self.expanded.borrow(),
304            &mut self.flattened.borrow_mut(),
305            &mut self.positions.borrow_mut(),
306        );
307        self.divergence.set(Some(d));
308        let next = self.version_counter.get() + 1;
309        self.version_counter.set(next);
310        self.version.set(next);
311    }
312
313    fn expand_all_recursive(&self, expanded: &mut HashSet<NodeId>) {
314        // Walk the full tree to find all nodes with children
315        let root_count = self.tree.root_count();
316        for i in 0..root_count {
317            let root = self.tree.root(i);
318            Self::expand_subtree_recursive(&self.tree, root, expanded);
319        }
320    }
321
322    /// Explicit-stack walk — `expand_all` is the natural companion of a deep
323    /// `flatten_node` walk, so it needs the same depth-bounded traversal.
324    fn expand_subtree_recursive(tree: &TreeModel<T>, root: NodeId, expanded: &mut HashSet<NodeId>) {
325        let mut stack = vec![root];
326        while let Some(node) = stack.pop() {
327            if tree.has_children(node) {
328                expanded.insert(node);
329                for child in tree.children(node) {
330                    stack.push(child);
331                }
332            }
333        }
334    }
335
336    /// Rebuild `out` (and the `pos` position map alongside it) from scratch
337    /// and return the length of the common prefix with the previous flat
338    /// list — the first flat index at which the projection diverges
339    /// (`out.len()` when nothing visible changed). `NodeId`s are stable
340    /// slotmap keys, so equal entries denote the same node at the same
341    /// depth/expand state.
342    fn rebuild_flat_list(
343        tree: &TreeModel<T>,
344        expanded: &HashSet<NodeId>,
345        out: &mut Vec<FlatEntry>,
346        pos: &mut HashMap<NodeId, usize>,
347    ) -> usize {
348        let old = std::mem::take(out);
349        out.reserve(old.len());
350        let root_count = tree.root_count();
351        for i in 0..root_count {
352            let root = tree.root(i);
353            Self::flatten_node(tree, root, 0, expanded, out);
354        }
355        pos.clear();
356        pos.extend(out.iter().enumerate().map(|(i, e)| (e.node_id, i)));
357        old.iter()
358            .zip(out.iter())
359            .take_while(|(a, b)| a == b)
360            .count()
361    }
362
363    /// Explicit-stack pre-order walk (children pushed in reverse so `pop()`
364    /// yields them in source order) — depth-bounded by tree size, not the
365    /// call stack.
366    fn flatten_node(
367        tree: &TreeModel<T>,
368        root: NodeId,
369        depth: usize,
370        expanded: &HashSet<NodeId>,
371        out: &mut Vec<FlatEntry>,
372    ) {
373        let mut stack = vec![(root, depth)];
374        while let Some((node, depth)) = stack.pop() {
375            let has_children = tree.has_children(node);
376            let is_expanded = expanded.contains(&node);
377
378            out.push(FlatEntry {
379                node_id: node,
380                depth,
381                has_children,
382                is_expanded,
383            });
384
385            if is_expanded && has_children {
386                for child in tree.children(node).into_iter().rev() {
387                    stack.push((child, depth + 1));
388                }
389            }
390        }
391    }
392}
393
394impl<T: 'static> std::fmt::Debug for TreeSlice<T> {
395    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
396        f.debug_struct("TreeSlice")
397            .field("visible_count", &self.visible_count())
398            .field("expanded_count", &self.expanded.borrow().len())
399            .finish()
400    }
401}
402
403/// Lightweight handle to a [`TreeSlice`]'s shared state, usable in closures.
404///
405/// Created via [`TreeSlice::handle`]. Shares all `Rc`-based internals with its
406/// parent `TreeSlice` but does **not** keep the tree-change observer alive —
407/// the `TreeSlice` that owns the observer must outlive all handles that rely on
408/// automatic re-flattening on model changes.
409pub struct TreeSliceHandle<T: 'static> {
410    tree: TreeModel<T>,
411    expanded: Rc<RefCell<HashSet<NodeId>>>,
412    flattened: Rc<RefCell<Vec<FlatEntry>>>,
413    positions: Rc<RefCell<HashMap<NodeId, usize>>>,
414    version: Signal<u64>,
415    version_counter: Rc<std::cell::Cell<u64>>,
416    divergence: Rc<std::cell::Cell<Option<usize>>>,
417}
418
419impl<T: 'static> TreeSliceHandle<T> {
420    /// Number of currently-visible (flattened) rows.
421    pub fn visible_count(&self) -> usize {
422        self.flattened.borrow().len()
423    }
424
425    /// Get the [`FlatEntry`] at `flat_index` (cloned), or `None` if out of bounds.
426    pub fn entry_at(&self, flat_index: usize) -> Option<FlatEntry> {
427        self.flattened.borrow().get(flat_index).cloned()
428    }
429
430    /// Get the [`NodeId`] at `flat_index`, or `None` if out of bounds.
431    pub fn visible_node_id(&self, flat_index: usize) -> Option<NodeId> {
432        self.flattened.borrow().get(flat_index).map(|e| e.node_id)
433    }
434
435    /// Expand `node` (make its children visible) and bump the version signal.
436    /// No-op if already expanded.
437    pub fn expand(&self, node: NodeId) {
438        let inserted = self.expanded.borrow_mut().insert(node);
439        if inserted {
440            self.reflatten_and_notify();
441        }
442    }
443
444    /// Collapse `node` (hide its children) and bump the version signal.
445    /// No-op if already collapsed.
446    pub fn collapse(&self, node: NodeId) {
447        let removed = self.expanded.borrow_mut().remove(&node);
448        if removed {
449            self.reflatten_and_notify();
450        }
451    }
452
453    /// Returns `true` if `node` is currently expanded.
454    pub fn is_expanded(&self, node: NodeId) -> bool {
455        self.expanded.borrow().contains(&node)
456    }
457
458    /// Toggle `node`'s expand/collapse state and bump the version signal.
459    pub fn toggle_expand(&self, node: NodeId) {
460        {
461            let mut exp = self.expanded.borrow_mut();
462            if exp.contains(&node) {
463                exp.remove(&node);
464            } else {
465                exp.insert(node);
466            }
467        }
468        self.reflatten_and_notify();
469    }
470
471    /// Access the underlying [`TreeModel`].
472    pub fn tree(&self) -> &TreeModel<T> {
473        &self.tree
474    }
475
476    /// Expand every node with children — see [`TreeSlice::expand_all`]. Useful
477    /// after a model rebuild reassigns `NodeId`s (the old expand set no longer
478    /// matches), to keep the view fully expanded.
479    pub fn expand_all(&self) {
480        {
481            let mut exp = self.expanded.borrow_mut();
482            let root_count = self.tree.root_count();
483            for i in 0..root_count {
484                let root = self.tree.root(i);
485                TreeSlice::<T>::expand_subtree_recursive(&self.tree, root, &mut exp);
486            }
487        }
488        self.reflatten_and_notify();
489    }
490
491    /// See [`TreeSlice::first_changed_index`].
492    pub fn first_changed_index(&self) -> Option<usize> {
493        self.divergence.get()
494    }
495
496    fn reflatten_and_notify(&self) {
497        let d = TreeSlice::<T>::rebuild_flat_list(
498            &self.tree,
499            &self.expanded.borrow(),
500            &mut self.flattened.borrow_mut(),
501            &mut self.positions.borrow_mut(),
502        );
503        self.divergence.set(Some(d));
504        let next = self.version_counter.get() + 1;
505        self.version_counter.set(next);
506        self.version.set(next);
507    }
508}
509
510impl<T: 'static> Clone for TreeSliceHandle<T> {
511    fn clone(&self) -> Self {
512        Self {
513            tree: self.tree.clone(),
514            expanded: self.expanded.clone(),
515            flattened: self.flattened.clone(),
516            positions: self.positions.clone(),
517            version: self.version.clone(),
518            version_counter: self.version_counter.clone(),
519            divergence: self.divergence.clone(),
520        }
521    }
522}
523
524/// `TreeSlice` is the built-in per-view `TreeDataSource` over an in-memory
525/// `TreeModel`. Identity is `NodeId`; a `SameView` drop reorders via
526/// `move_node`/`move_to_root` (with the cycle guard). `Foreign` drops are
527/// rejected — a bare slice knows no foreign payloads.
528impl<T: 'static> TreeDataSource for TreeSlice<T> {
529    type Item = T;
530    type Key = NodeId;
531
532    fn visible_count(&self) -> usize {
533        TreeSlice::visible_count(self)
534    }
535
536    fn with_entry<R>(
537        &self,
538        flat_index: usize,
539        f: impl FnOnce(&Self::Item, &FlatEntry<Self::Key>) -> R,
540    ) -> Option<R> {
541        TreeSlice::with_entry(self, flat_index, f)
542    }
543
544    fn key_at(&self, flat_index: usize) -> Option<NodeId> {
545        self.visible_node_id(flat_index)
546    }
547
548    fn flat_index_of(&self, key: &NodeId) -> Option<usize> {
549        TreeSlice::flat_index_of(self, *key)
550    }
551
552    fn parent(&self, key: &NodeId) -> Option<NodeId> {
553        self.tree().parent(*key)
554    }
555
556    fn child_keys(&self, key: &NodeId) -> Vec<NodeId> {
557        self.tree().children(*key)
558    }
559
560    fn version_signal(&self) -> Signal<u64> {
561        TreeSlice::version_signal(self)
562    }
563
564    fn first_changed_index(&self) -> Option<usize> {
565        TreeSlice::first_changed_index(self)
566    }
567
568    fn contains_key(&self, key: &NodeId) -> bool {
569        // Existence against the backing tree, not the visible projection, so a
570        // node hidden under a collapsed ancestor keeps its keyed selection.
571        self.tree().with_item(*key, |_| ()).is_some()
572    }
573
574    fn is_expanded(&self, key: &NodeId) -> bool {
575        TreeSlice::is_expanded(self, *key)
576    }
577
578    fn set_expanded(&self, key: &NodeId, expanded: bool) {
579        if expanded {
580            self.expand(*key);
581        } else {
582            self.collapse(*key);
583        }
584    }
585
586    fn drag(&self, _key: &NodeId) -> DragEligibility {
587        DragEligibility::CanDrag
588    }
589
590    fn can_accept(&self, query: &DropQuery<'_, NodeId>) -> DropResponse {
591        match &query.source {
592            DragSource::SameView { key: source } => {
593                if *source == query.target
594                    || tree_is_desc_or_self(self.tree(), query.target, *source)
595                {
596                    DropResponse::Reject
597                } else {
598                    DropResponse::Accept
599                }
600            }
601            DragSource::Foreign { .. } => DropResponse::Reject,
602        }
603    }
604
605    fn accept_drop(&self, commit: DropCommit<'_, NodeId>) -> bool {
606        match commit.source {
607            DragSource::SameView { key: source } => {
608                tree_apply_reorder(self.tree(), source, commit.target, commit.position)
609            }
610            DragSource::Foreign { .. } => false,
611        }
612    }
613
614    fn on_drag_out(&self, key: &NodeId) {
615        // Source-side completion for a foreign move: drop the node (and its
616        // subtree) that was accepted elsewhere. Re-check existence first — a
617        // reactive observer reacting to an earlier removal in the same batch
618        // (or any unrelated mutation) could have already freed this node, and
619        // `TreeModel::remove` panics on a stale key.
620        if self.tree().with_item(*key, |_| ()).is_some() {
621            self.tree().remove(*key);
622        }
623    }
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629    use crate::dnd_types::DropPosition;
630
631    /// Build a sample tree:
632    /// A
633    ///   A1
634    ///     A1a
635    ///   A2
636    /// B
637    ///   B1
638    /// C
639    fn sample_tree() -> TreeModel<&'static str> {
640        let tree = TreeModel::new();
641        let a = tree.insert_root(0, "A");
642        let a1 = tree.insert_child(a, 0, "A1");
643        tree.insert_child(a1, 0, "A1a");
644        tree.insert_child(a, 1, "A2");
645        let b = tree.insert_root(1, "B");
646        tree.insert_child(b, 0, "B1");
647        tree.insert_root(2, "C");
648        tree
649    }
650
651    #[test]
652    fn initial_state_shows_only_roots() {
653        let tree = sample_tree();
654        let slice = TreeSlice::new(tree);
655
656        assert_eq!(slice.visible_count(), 3); // A, B, C
657        assert_eq!(
658            slice.with_entry(0, |item, entry| {
659                assert_eq!(*item, "A");
660                assert_eq!(entry.depth, 0);
661                assert!(entry.has_children);
662                assert!(!entry.is_expanded);
663            }),
664            Some(())
665        );
666        assert_eq!(slice.with_entry(1, |item, _| *item), Some("B"));
667        assert_eq!(slice.with_entry(2, |item, _| *item), Some("C"));
668    }
669
670    #[test]
671    fn expand_shows_children() {
672        let tree = sample_tree();
673        let a = tree.root(0);
674        let slice = TreeSlice::new(tree);
675
676        assert_eq!(slice.visible_count(), 3);
677
678        slice.expand(a);
679        assert_eq!(slice.visible_count(), 5); // A, A1, A2, B, C
680
681        assert_eq!(slice.with_entry(0, |item, _| *item), Some("A"));
682        assert_eq!(
683            slice.with_entry(1, |item, entry| {
684                assert_eq!(*item, "A1");
685                assert_eq!(entry.depth, 1);
686                assert!(entry.has_children); // A1 has A1a
687            }),
688            Some(())
689        );
690        assert_eq!(slice.with_entry(2, |item, _| *item), Some("A2"));
691        assert_eq!(slice.with_entry(3, |item, _| *item), Some("B"));
692    }
693
694    #[test]
695    fn collapse_hides_children() {
696        let tree = sample_tree();
697        let a = tree.root(0);
698        let slice = TreeSlice::new(tree);
699
700        slice.expand(a);
701        assert_eq!(slice.visible_count(), 5);
702
703        slice.collapse(a);
704        assert_eq!(slice.visible_count(), 3);
705    }
706
707    #[test]
708    fn deep_expand() {
709        let tree = sample_tree();
710        let a = tree.root(0);
711        let slice = TreeSlice::new(tree.clone());
712
713        slice.expand(a);
714        let a1 = slice.visible_node_id(1).unwrap();
715        slice.expand(a1);
716
717        // A, A1, A1a, A2, B, C
718        assert_eq!(slice.visible_count(), 6);
719        assert_eq!(
720            slice.with_entry(2, |item, entry| {
721                assert_eq!(*item, "A1a");
722                assert_eq!(entry.depth, 2);
723            }),
724            Some(())
725        );
726    }
727
728    #[test]
729    fn toggle() {
730        let tree = sample_tree();
731        let a = tree.root(0);
732        let slice = TreeSlice::new(tree);
733
734        slice.toggle(a);
735        assert_eq!(slice.visible_count(), 5); // expanded
736        assert!(slice.is_expanded(a));
737
738        slice.toggle(a);
739        assert_eq!(slice.visible_count(), 3); // collapsed
740        assert!(!slice.is_expanded(a));
741    }
742
743    #[test]
744    fn expand_all() {
745        let tree = sample_tree();
746        let slice = TreeSlice::new(tree);
747
748        slice.expand_all();
749        // A, A1, A1a, A2, B, B1, C
750        assert_eq!(slice.visible_count(), 7);
751    }
752
753    #[test]
754    fn collapse_all() {
755        let tree = sample_tree();
756        let slice = TreeSlice::new(tree);
757
758        slice.expand_all();
759        assert_eq!(slice.visible_count(), 7);
760
761        slice.collapse_all();
762        assert_eq!(slice.visible_count(), 3);
763    }
764
765    #[test]
766    fn handle_expand_all_matches_slice() {
767        let tree = sample_tree();
768        let slice = TreeSlice::new(tree);
769        let handle = slice.handle();
770
771        assert_eq!(slice.visible_count(), 3); // roots collapsed
772        handle.expand_all();
773        // A, A1, A1a, A2, B, B1, C — visible through the shared slice.
774        assert_eq!(slice.visible_count(), 7);
775    }
776
777    #[test]
778    fn two_slices_independent_expand() {
779        let tree = sample_tree();
780        let a = tree.root(0);
781        let b = tree.root(1);
782
783        let slice1 = TreeSlice::new(tree.clone());
784        let slice2 = TreeSlice::new(tree);
785
786        slice1.expand(a);
787        slice2.expand(b);
788
789        // Slice 1: A expanded, B collapsed
790        assert_eq!(slice1.visible_count(), 5); // A, A1, A2, B, C
791        assert!(slice1.is_expanded(a));
792        assert!(!slice1.is_expanded(b));
793
794        // Slice 2: A collapsed, B expanded
795        assert_eq!(slice2.visible_count(), 4); // A, B, B1, C
796        assert!(!slice2.is_expanded(a));
797        assert!(slice2.is_expanded(b));
798    }
799
800    #[test]
801    fn tree_mutation_updates_slice() {
802        let tree = sample_tree();
803        let a = tree.root(0);
804        let slice = TreeSlice::new(tree.clone());
805
806        slice.expand(a);
807        assert_eq!(slice.visible_count(), 5); // A, A1, A2, B, C
808
809        // Insert a new child under A
810        tree.insert_child(a, 2, "A3");
811        assert_eq!(slice.visible_count(), 6); // A, A1, A2, A3, B, C
812    }
813
814    #[test]
815    fn tree_remove_updates_slice() {
816        let tree = sample_tree();
817        let a = tree.root(0);
818        let slice = TreeSlice::new(tree.clone());
819
820        slice.expand(a);
821        let a1 = slice.visible_node_id(1).unwrap();
822        tree.remove(a1);
823
824        assert_eq!(slice.visible_count(), 4); // A, A2, B, C
825    }
826
827    #[test]
828    fn version_signal_increments() {
829        let tree = sample_tree();
830        let a = tree.root(0);
831        let slice = TreeSlice::new(tree.clone());
832
833        let v0 = slice.version_signal().get();
834
835        slice.expand(a);
836        let v1 = slice.version_signal().get();
837        assert!(v1 > v0, "version should increment on expand");
838
839        tree.insert_root(3, "D");
840        let v2 = slice.version_signal().get();
841        assert!(v2 > v1, "version should increment on tree mutation");
842    }
843
844    #[test]
845    fn flat_index_of() {
846        let tree = sample_tree();
847        let a = tree.root(0);
848        let b = tree.root(1);
849        let slice = TreeSlice::new(tree);
850
851        assert_eq!(slice.flat_index_of(a), Some(0));
852        assert_eq!(slice.flat_index_of(b), Some(1));
853    }
854
855    /// The position map underlying `flat_index_of` must agree with iteration
856    /// order (`visible_node_id`) after every kind of reflatten-triggering
857    /// mutation — expand, collapse, and an upstream model change (a filter
858    /// pass on `TreeSlice` would be the model-level equivalent).
859    fn assert_positions_match_iteration_order<T>(slice: &TreeSlice<T>) {
860        for i in 0..slice.visible_count() {
861            let node = slice.visible_node_id(i).unwrap();
862            assert_eq!(
863                slice.flat_index_of(node),
864                Some(i),
865                "flat_index_of({node:?}) should be the iteration position {i}"
866            );
867        }
868    }
869
870    #[test]
871    fn flat_index_of_matches_iteration_order_across_mutations() {
872        let tree = sample_tree();
873        let a = tree.root(0);
874        let b = tree.root(1);
875        let slice = TreeSlice::new(tree.clone());
876
877        assert_positions_match_iteration_order(&slice);
878
879        slice.expand(a);
880        assert_positions_match_iteration_order(&slice);
881
882        slice.expand(b);
883        assert_positions_match_iteration_order(&slice);
884
885        slice.collapse(a);
886        assert_positions_match_iteration_order(&slice);
887
888        tree.insert_root(3, "D");
889        assert_positions_match_iteration_order(&slice);
890
891        tree.remove(b);
892        assert_positions_match_iteration_order(&slice);
893    }
894
895    #[test]
896    fn persistence_save_restore() {
897        let tree = sample_tree();
898        let a = tree.root(0);
899        let slice = TreeSlice::new(tree.clone());
900
901        slice.expand(a);
902        let saved = slice.expanded_nodes();
903        assert_eq!(saved.len(), 1);
904
905        slice.collapse_all();
906        assert_eq!(slice.visible_count(), 3);
907
908        slice.set_expanded_nodes(&saved);
909        assert_eq!(slice.visible_count(), 5); // A expanded again
910    }
911
912    #[test]
913    fn out_of_bounds_returns_none() {
914        let tree = sample_tree();
915        let slice = TreeSlice::new(tree);
916        assert_eq!(slice.with_entry(99, |_, _| ()), None);
917        assert_eq!(slice.visible_node_id(99), None);
918    }
919
920    // ── first_changed_index (divergence) ────────────────────────────────
921
922    #[test]
923    fn divergence_unknown_before_first_reflatten() {
924        let tree = sample_tree();
925        let slice = TreeSlice::new(tree);
926        assert_eq!(slice.first_changed_index(), None);
927    }
928
929    #[test]
930    fn divergence_on_expand_is_the_toggled_row() {
931        let tree = sample_tree();
932        let b = tree.root(1);
933        let slice = TreeSlice::new(tree);
934
935        // Expanding B (flat index 1) changes B's own entry (is_expanded)
936        // and inserts B1 after it — A (flat 0) is untouched.
937        slice.expand(b);
938        assert_eq!(slice.first_changed_index(), Some(1));
939
940        slice.collapse(b);
941        assert_eq!(slice.first_changed_index(), Some(1));
942    }
943
944    #[test]
945    fn divergence_on_append_is_old_len() {
946        let tree = sample_tree();
947        let slice = TreeSlice::new(tree.clone());
948
949        tree.insert_root(3, "D"); // old visible: A, B, C
950        assert_eq!(slice.first_changed_index(), Some(3));
951    }
952
953    #[test]
954    fn divergence_on_remove_is_removed_position() {
955        let tree = sample_tree();
956        let b = tree.root(1);
957        let slice = TreeSlice::new(tree.clone());
958
959        tree.remove(b); // old: A, B, C → new: A, C
960        assert_eq!(slice.first_changed_index(), Some(1));
961    }
962
963    #[test]
964    fn divergence_on_node_update_is_its_flat_index() {
965        let tree = sample_tree();
966        let c = tree.root(2);
967        let slice = TreeSlice::new(tree.clone());
968
969        // Structure unchanged, but C's content (flat index 2) changed.
970        tree.update(c, "C-updated");
971        assert_eq!(slice.first_changed_index(), Some(2));
972    }
973
974    #[test]
975    fn divergence_on_invisible_update_is_visible_count() {
976        let tree = sample_tree();
977        let a = tree.root(0);
978        let a1 = tree.children(a)[0];
979        let slice = TreeSlice::new(tree.clone());
980
981        // A1 is hidden (A collapsed) — nothing visible changed.
982        tree.update(a1, "A1-updated");
983        assert_eq!(slice.first_changed_index(), Some(slice.visible_count()));
984    }
985
986    #[test]
987    fn divergence_via_handle_toggle() {
988        let tree = sample_tree();
989        let b = tree.root(1);
990        let slice = TreeSlice::new(tree);
991        let handle = slice.handle();
992
993        handle.toggle_expand(b);
994        assert_eq!(handle.first_changed_index(), Some(1));
995        assert_eq!(slice.first_changed_index(), Some(1));
996    }
997
998    // ── TreeDataSource capability protocol ──────────────────────────────
999
1000    #[test]
1001    fn tree_source_accept_drop_reparents_into() {
1002        // Move B (root 1) Into A (root 0). Roots become A, C; B's parent is A.
1003        let tree = sample_tree();
1004        let a = tree.root(0);
1005        let b = tree.root(1);
1006        let slice = TreeSlice::new(tree.clone());
1007        assert!(slice.accept_drop(DropCommit {
1008            source: DragSource::SameView { key: b },
1009            target: a,
1010            position: DropPosition::Into,
1011        }));
1012        assert_eq!(tree.root_count(), 2);
1013        assert_eq!(tree.parent(b), Some(a));
1014    }
1015
1016    #[test]
1017    fn tree_source_can_accept_rejects_cycle_and_refuses_drop() {
1018        // Cannot drop A into its own descendant A1.
1019        let tree = sample_tree();
1020        let a = tree.root(0);
1021        let slice = TreeSlice::new(tree.clone());
1022        slice.expand(a);
1023        let a1 = slice.visible_node_id(1).unwrap();
1024        assert_eq!(
1025            slice.can_accept(&DropQuery {
1026                source: DragSource::SameView { key: a },
1027                target: a1,
1028                position: DropPosition::Into,
1029            }),
1030            DropResponse::Reject
1031        );
1032        // accept_drop refuses rather than panicking in TreeModel::move_node.
1033        assert!(!slice.accept_drop(DropCommit {
1034            source: DragSource::SameView { key: a },
1035            target: a1,
1036            position: DropPosition::Into,
1037        }));
1038    }
1039
1040    #[test]
1041    fn tree_source_reorders_root_siblings() {
1042        // Move C (root 2) Before A (root 0) → C, A, B at the root level.
1043        let tree = sample_tree();
1044        let a = tree.root(0);
1045        let c = tree.root(2);
1046        let slice = TreeSlice::new(tree.clone());
1047        assert!(slice.accept_drop(DropCommit {
1048            source: DragSource::SameView { key: c },
1049            target: a,
1050            position: DropPosition::Before,
1051        }));
1052        assert_eq!(slice.with_entry(0, |v, _| *v), Some("C"));
1053        assert_eq!(slice.with_entry(1, |v, _| *v), Some("A"));
1054        assert_eq!(slice.with_entry(2, |v, _| *v), Some("B"));
1055    }
1056
1057    #[test]
1058    fn tree_reorder_within_filters_descendants_of_selected() {
1059        // Dragging {A, A1} (A1 is A's child) after C must move only A — A1
1060        // rides along inside A's subtree, it is not relocated independently.
1061        let tree = sample_tree();
1062        let a = tree.root(0);
1063        let c = tree.root(2);
1064        let slice = TreeSlice::new(tree.clone());
1065        let a1 = slice.child_keys(&a)[0];
1066        assert!(slice.reorder_within(&[a, a1], &c, DropPosition::After));
1067        assert_eq!(slice.with_entry(0, |v, _| *v), Some("B"));
1068        assert_eq!(slice.with_entry(1, |v, _| *v), Some("C"));
1069        assert_eq!(slice.with_entry(2, |v, _| *v), Some("A"));
1070        // A still owns both its children (A1 stayed put under A).
1071        assert_eq!(slice.child_keys(&a).len(), 2);
1072    }
1073
1074    #[test]
1075    fn tree_on_drag_out_removes_node_and_subtree() {
1076        let tree = sample_tree();
1077        let b = tree.root(1);
1078        let slice = TreeSlice::new(tree.clone());
1079        slice.on_drag_out(&b);
1080        // B (and B1) gone → roots A, C remain.
1081        assert_eq!(slice.visible_count(), 2);
1082        assert_eq!(slice.with_entry(0, |v, _| *v), Some("A"));
1083        assert_eq!(slice.with_entry(1, |v, _| *v), Some("C"));
1084    }
1085
1086    /// `flatten_node` and `expand_subtree_recursive` are both explicit-stack
1087    /// walks; a 50,000-deep single-child chain must flatten (and fully
1088    /// expand) without overflowing the call stack.
1089    #[test]
1090    fn deep_chain_flattens_and_expands_without_overflow() {
1091        const DEPTH: usize = 50_000;
1092        let tree = TreeModel::new();
1093        let root = tree.insert_root(0, 0usize);
1094        let mut leaf = root;
1095        for i in 1..DEPTH {
1096            leaf = tree.insert_child(leaf, 0, i);
1097        }
1098        let slice = TreeSlice::new(tree);
1099
1100        // expand_all walks the whole tree (expand_subtree_recursive) then
1101        // reflattens once (flatten_node walks the full DEPTH) — both
1102        // explicit-stack, so this exercises both in one shot instead of
1103        // one `expand()` reflatten per node (which would be O(n^2)).
1104        slice.expand_all();
1105
1106        assert_eq!(slice.visible_count(), DEPTH);
1107        assert_eq!(slice.flat_index_of(leaf), Some(DEPTH - 1));
1108        assert_eq!(slice.depth_at(DEPTH - 1), DEPTH - 1);
1109    }
1110}