Skip to main content

teksilo_data/
tree_data_slice.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! `TreeDataSlice` — the reusable [`TreeDataSource`] engine for an **external,
5//! indent-ordered** tree (a Qleany entity store, a database, a virtual
6//! filesystem) that is NOT mirrored into a [`TreeModel`](crate::TreeModel).
7//!
8//! [`TreeSlice`](crate::TreeSlice) gives per-view expand state + flattening +
9//! divergence to a `TreeModel`. `TreeDataSlice` gives the **same machinery** to
10//! a source whose identity is a domain key (`K = i64` entity id, a tagged enum,
11//! …) and whose natural shape is a flat, pre-order, indent-annotated row stream
12//! — the shape an outline is genuinely stored in (Scrivener-class binders /
13//! chapters / scenes, OPML, Markdown headings). The app hands over
14//! `Vec<`[`TreeRow`]`<K, T>>` (`{ key, item, depth }`, document order) on every
15//! (re)load; the engine owns everything else:
16//!
17//! * **tree derivation** — parent links + child index + roots + structural
18//!   depth, derived from the indent sequence (an item's parent is the nearest
19//!   preceding row of strictly smaller depth; depth-0 rows are roots);
20//! * **per-view expand state** — a `K`-keyed set, so two slices over the same
21//!   source expand independently and expand survives a full re-source;
22//! * **collapse-aware flattening** into the visible row list;
23//! * **divergence** ([`first_changed_index`](TreeDataSlice::first_changed_index))
24//!   — the common-prefix of the old vs new visible rows, comparing key + depth +
25//!   has-children + expand **and item content** (hence the `T: PartialEq`
26//!   bound), so a consumer caching per-row state (a measured row height) keeps
27//!   its valid prefix across reloads and expand toggles;
28//! * **DnD mechanism** — the cycle guard + `can_accept`/`accept_drop` plumbing;
29//!   domain *policy* is injected as closures ([`TreeDataSlice::set_drag_policy`],
30//!   [`TreeDataSlice::set_drop_resolver`], [`TreeDataSlice::set_reorder`]).
31//!
32//! It is a cheap `Rc`-handle (clone = share, like `ListModel` / `SceneModel`):
33//! pass one clone to `TreeView::from_source` and keep another to drive
34//! [`reload`](TreeDataSlice::reload) / [`set_rows`](TreeDataSlice::set_rows) from
35//! the app.
36//!
37//! ## Wiring an external source
38//!
39//! ```
40//! use teksilo_data::{TreeDataSlice, TreeRow};
41//! use teksilo_data::dnd_types::{DragEligibility, DropPosition};
42//!
43//! // key = entity id, item = the row's display data
44//! let slice: TreeDataSlice<u64, String> = TreeDataSlice::new();
45//! slice.set_expand_new_nodes(true);             // new nodes appear expanded
46//! slice.set_source(|| vec![                     // your `rows::load`
47//!     TreeRow::new(1, "Binder".to_string(), 0),
48//!     TreeRow::new(2, "Chapter".to_string(), 1),
49//!     TreeRow::new(3, "Scene".to_string(), 2),
50//! ]);
51//! slice.set_drag_policy(|key| if *key == 1 { DragEligibility::NoDrag } else { DragEligibility::CanDrag });
52//! slice.set_reorder(|_dragged, _target, _pos: DropPosition| { /* backend move + undo */ true });
53//! slice.reload();
54//!
55//! assert_eq!(slice.visible_count(), 3);         // all expanded
56//! // let view = TreeView::from_source(slice.clone(), delegate);
57//! ```
58
59use std::cell::{Cell, RefCell};
60use std::collections::{HashMap, HashSet};
61use std::rc::Rc;
62
63use teksilo_core::signal::Signal;
64
65use crate::dnd_types::{
66    DragEligibility, DragSource, DropCommit, DropPosition, DropQuery, DropResponse, ItemKey,
67};
68use crate::tree_data_source::{FlatEntry, TreeDataSource};
69
70/// One row the app hands to a [`TreeDataSlice`], in **document (pre-)order**.
71///
72/// `depth` is the indent level (`0` = a root). The engine derives each row's
73/// parent, children, and structural depth from the `depth` sequence: a row's
74/// parent is the nearest preceding row with a strictly smaller `depth`. The row
75/// stream must be well-formed pre-order (a parent precedes its subtree) — the
76/// shape any indent-stored outline already has.
77#[derive(Debug, Clone)]
78pub struct TreeRow<K, T> {
79    /// The row's stable domain identity (entity id, tagged key, …). Must be
80    /// stable across reloads for expand-state and divergence to survive.
81    pub key: K,
82    /// The row's display payload.
83    pub item: T,
84    /// Indent level; `0` is a root.
85    pub depth: usize,
86    /// **Declares that this row has children the source has not emitted.**
87    ///
88    /// Unset (`None`) is the ordinary case and the historical behaviour: whether a
89    /// row has children is derived structurally, from whether any row in the stream
90    /// names it as parent. That is right for a source that always hands over the
91    /// whole tree, which is what most of them do.
92    ///
93    /// It is a deadlock for a source that wants to materialise a branch only when it
94    /// is opened. Such a source emits no children until the row is expanded, so the
95    /// row is derived childless, so no chevron is drawn, so there is nothing to
96    /// click, so it is never expanded. `StandardTreeItem::on_toggle` exists to hang
97    /// exactly that kind of load off, and without this the callback can never fire.
98    ///
99    /// Set it to `Some(true)` to promise children that are not there yet: the row
100    /// draws its chevron, the toggle reaches the app, and the app re-sources with the
101    /// branch filled in. `Some(false)` promises the opposite — a leaf, even if the
102    /// stream happens to contain rows beneath it.
103    ///
104    /// ⚠ It is a **promise, not a projection**. A row that claims children and then
105    /// produces none on expand opens onto nothing, and the tree cannot detect that
106    /// for you.
107    pub has_children: Option<bool>,
108}
109
110impl<K, T> TreeRow<K, T> {
111    /// Convenience constructor, leaving `has_children` to be derived from the
112    /// stream — see the field, and [`with_children`](Self::with_children) for the
113    /// case where it cannot be.
114    pub fn new(key: K, item: T, depth: usize) -> Self {
115        Self {
116            key,
117            item,
118            depth,
119            has_children: None,
120        }
121    }
122
123    /// Declare whether this row has children, rather than letting the stream say.
124    ///
125    /// For a source that materialises a branch on expand. See
126    /// [`has_children`](Self::has_children) for why a lazy source cannot work
127    /// without it.
128    pub fn with_children(mut self, has_children: bool) -> Self {
129        self.has_children = Some(has_children);
130        self
131    }
132}
133
134/// Reorder command: `(dragged, target, position) -> applied`. Applies the move
135/// through the backend (with undo) and reports whether it took. On `true` the
136/// slice re-sources itself via the [`set_source`](TreeDataSlice::set_source)
137/// closure.
138///
139/// `Rc`, not `Box`: callers (`accept_drop`/`drag`/`resolve`) clone the handle
140/// out of its `RefCell` and drop the borrow *before* invoking the closure, so
141/// a closure that calls back into the slice (e.g. re-registering itself via
142/// `set_reorder`) doesn't hit a `BorrowMutError`.
143type ReorderFn<K> = Rc<dyn Fn(K, K, DropPosition) -> bool>;
144/// Per-row drag gate. Default (unset): every row is `NoDrag`.
145type DragPolicyFn<K> = Rc<dyn Fn(&K) -> DragEligibility>;
146/// Domain drop policy: `(dragged, target, target_item, position) -> effective
147/// position`, or `None` to forbid. The engine applies its own cycle guard first
148/// and looks up the target's payload, so the resolver can encode domain rules
149/// that depend on the target node (e.g. "a drop onto a non-container leaf
150/// becomes `After` it") **without capturing the slice** (which would form an
151/// `Rc` cycle).
152type DropResolverFn<K, T> = Rc<dyn Fn(&K, &K, &T, DropPosition) -> Option<DropPosition>>;
153/// Row source: produces the whole indent-ordered stream for the current state.
154type SourceFn<K, T> = Rc<dyn Fn() -> Vec<TreeRow<K, T>>>;
155
156/// Internal, fully-derived representation of one row.
157struct Row<K, T> {
158    key: K,
159    item: T,
160    /// Structural depth (root = 0), derived from the tree, not the raw indent.
161    depth: usize,
162    parent: Option<K>,
163    has_children: bool,
164    /// Whether [`has_children`](Self::has_children) came from the source rather than
165    /// from the shape of the stream. Kept so the structural pass leaves it alone.
166    declared: bool,
167}
168
169/// The freshly-built structure + projection, staged before commit.
170struct Built<K, T> {
171    rows: Vec<Row<K, T>>,
172    children: HashMap<K, Vec<usize>>,
173    roots: Vec<usize>,
174    row_pos: HashMap<K, usize>,
175    visible: Vec<usize>,
176    vis_pos: HashMap<K, usize>,
177    expanded: HashSet<K>,
178    seen: HashSet<K>,
179}
180
181struct Inner<K: ItemKey, T> {
182    rows: RefCell<Vec<Row<K, T>>>,
183    /// parent key → child **row indices**, in sibling order.
184    children: RefCell<HashMap<K, Vec<usize>>>,
185    /// Root **row indices**, in order.
186    roots: RefCell<Vec<usize>>,
187    /// key → **row index**.
188    row_pos: RefCell<HashMap<K, usize>>,
189    /// **Row indices** currently visible (collapse-aware), in flat order.
190    visible: RefCell<Vec<usize>>,
191    /// key → **flat index** within `visible`.
192    vis_pos: RefCell<HashMap<K, usize>>,
193    expanded: RefCell<HashSet<K>>,
194    /// Every key ever installed — lets a re-source auto-expand only *newly*
195    /// appearing nodes while preserving the user's later collapses.
196    seen: RefCell<HashSet<K>>,
197    expand_new: Cell<bool>,
198    /// Reveal override: when `true`, the flatten treats every node as expanded,
199    /// ignoring `expanded` (which is preserved). Drives "reveal while filtering".
200    all_expanded: Cell<bool>,
201    version: Signal<u64>,
202    version_counter: Cell<u64>,
203    divergence: Cell<Option<usize>>,
204    source: RefCell<Option<SourceFn<K, T>>>,
205    reorder: RefCell<Option<ReorderFn<K>>>,
206    drag_policy: RefCell<Option<DragPolicyFn<K>>>,
207    drop_resolver: RefCell<Option<DropResolverFn<K, T>>>,
208}
209
210/// Per-view flattened projection of an external, indent-ordered tree source.
211/// See the [module documentation](self).
212pub struct TreeDataSlice<K: ItemKey, T> {
213    inner: Rc<Inner<K, T>>,
214}
215
216impl<K: ItemKey, T> Clone for TreeDataSlice<K, T> {
217    fn clone(&self) -> Self {
218        Self {
219            inner: self.inner.clone(),
220        }
221    }
222}
223
224impl<K: ItemKey, T> Default for TreeDataSlice<K, T> {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230impl<K: ItemKey, T> TreeDataSlice<K, T> {
231    /// Create an empty slice. Configure it (`set_source` / `set_reorder` /
232    /// policies / `set_expand_new_nodes`) then populate with
233    /// [`reload`](Self::reload) or [`set_rows`](Self::set_rows).
234    pub fn new() -> Self {
235        Self {
236            inner: Rc::new(Inner {
237                rows: RefCell::new(Vec::new()),
238                children: RefCell::new(HashMap::new()),
239                roots: RefCell::new(Vec::new()),
240                row_pos: RefCell::new(HashMap::new()),
241                visible: RefCell::new(Vec::new()),
242                vis_pos: RefCell::new(HashMap::new()),
243                expanded: RefCell::new(HashSet::new()),
244                seen: RefCell::new(HashSet::new()),
245                expand_new: Cell::new(false),
246                all_expanded: Cell::new(false),
247                version: Signal::new(0),
248                version_counter: Cell::new(0),
249                divergence: Cell::new(None),
250                source: RefCell::new(None),
251                reorder: RefCell::new(None),
252                drag_policy: RefCell::new(None),
253                drop_resolver: RefCell::new(None),
254            }),
255        }
256    }
257
258    // ── Configuration ─────────────────────────────────────────────────────
259
260    /// Install the row source (`rows::load`). [`reload`](Self::reload) and a
261    /// committed drop call it to re-materialise the tree.
262    pub fn set_source(&self, f: impl Fn() -> Vec<TreeRow<K, T>> + 'static) {
263        *self.inner.source.borrow_mut() = Some(Rc::new(f));
264    }
265
266    /// Install the reorder command (`dragged, target, position -> applied`).
267    /// Without one, drops are refused.
268    pub fn set_reorder(&self, f: impl Fn(K, K, DropPosition) -> bool + 'static) {
269        *self.inner.reorder.borrow_mut() = Some(Rc::new(f));
270    }
271
272    /// Install the per-row drag gate. Without one, no row is draggable.
273    pub fn set_drag_policy(&self, f: impl Fn(&K) -> DragEligibility + 'static) {
274        *self.inner.drag_policy.borrow_mut() = Some(Rc::new(f));
275    }
276
277    /// Install the domain drop resolver. The engine's cycle guard (no drop into
278    /// your own subtree, no self-drop) runs first, then hands the resolver
279    /// `(dragged, target, target_item, position)`; return `Some(pos)` to accept
280    /// at `pos` (a different `pos` snaps the indicator, i.e.
281    /// [`DropResponse::Redirect`]) or `None` to forbid. Without one, any
282    /// non-cyclic drop is accepted at the requested position.
283    pub fn set_drop_resolver(
284        &self,
285        f: impl Fn(&K, &K, &T, DropPosition) -> Option<DropPosition> + 'static,
286    ) {
287        *self.inner.drop_resolver.borrow_mut() = Some(Rc::new(f));
288    }
289
290    /// Whether nodes appearing for the first time start expanded (`true`) or
291    /// collapsed (`false`, the default, matching `TreeSlice`). Set this **before**
292    /// the first populate to affect the initial rows.
293    pub fn set_expand_new_nodes(&self, expand: bool) {
294        self.inner.expand_new.set(expand);
295    }
296
297    // ── Population ────────────────────────────────────────────────────────
298
299    /// Build a slice directly from an initial row stream (no version bump / no
300    /// divergence — construction is not a change).
301    pub fn from_rows(rows: Vec<TreeRow<K, T>>) -> Self {
302        let slice = Self::new();
303        let built = slice.build(rows);
304        slice.commit(built);
305        slice
306    }
307
308    /// Re-source the rows via the [`set_source`](Self::set_source) closure and
309    /// reproject. No-op if no source is installed.
310    pub fn reload(&self)
311    where
312        T: PartialEq,
313    {
314        // Clone the handle out and drop the borrow before invoking: the
315        // app-supplied loader may call back into this slice (even re-install
316        // the source), which would otherwise hit a re-entrant borrow.
317        let f = {
318            let src = self.inner.source.borrow();
319            match src.as_ref() {
320                Some(f) => f.clone(),
321                None => return,
322            }
323        };
324        self.set_rows(f());
325    }
326
327    /// Replace the rows with a freshly-sourced stream, preserving per-view
328    /// expand state by key, computing [`first_changed_index`](Self::first_changed_index),
329    /// and bumping the version signal.
330    pub fn set_rows(&self, rows: Vec<TreeRow<K, T>>)
331    where
332        T: PartialEq,
333    {
334        let built = self.build(rows);
335        let all = self.inner.all_expanded.get();
336        let div = {
337            let old_rows = self.inner.rows.borrow();
338            let old_visible = self.inner.visible.borrow();
339            let old_expanded = self.inner.expanded.borrow();
340            common_prefix(
341                &old_rows,
342                &old_visible,
343                &old_expanded,
344                all,
345                &built.rows,
346                &built.visible,
347                &built.expanded,
348                all,
349            )
350        };
351        self.commit(built);
352        self.inner.divergence.set(Some(div));
353        self.bump();
354    }
355
356    // ── Read surface (also exposed via `TreeDataSource`) ──────────────────
357
358    /// Number of currently-visible (flattened) rows.
359    pub fn visible_count(&self) -> usize {
360        self.inner.visible.borrow().len()
361    }
362
363    /// Access the item + flat metadata at a visible index via callback.
364    pub fn with_entry<R>(
365        &self,
366        flat_index: usize,
367        f: impl FnOnce(&T, &FlatEntry<K>) -> R,
368    ) -> Option<R> {
369        let visible = self.inner.visible.borrow();
370        let &row_idx = visible.get(flat_index)?;
371        let rows = self.inner.rows.borrow();
372        let row = rows.get(row_idx)?;
373        let entry = FlatEntry {
374            node_id: row.key.clone(),
375            depth: row.depth,
376            has_children: row.has_children,
377            is_expanded: self.inner.all_expanded.get()
378                || self.inner.expanded.borrow().contains(&row.key),
379        };
380        Some(f(&row.item, &entry))
381    }
382
383    /// Access a node's item by key via callback, **regardless of visibility** (a
384    /// node hidden under a collapsed ancestor is still reachable). Returns `None`
385    /// if the key is absent from the source. The by-key counterpart of
386    /// [`with_entry`](Self::with_entry) (which is by visible index) — use it to
387    /// resolve a key to its domain payload.
388    pub fn with_key<R>(&self, key: &K, f: impl FnOnce(&T) -> R) -> Option<R> {
389        let idx = *self.inner.row_pos.borrow().get(key)?;
390        let rows = self.inner.rows.borrow();
391        rows.get(idx).map(|r| f(&r.item))
392    }
393
394    /// The key of the row at a visible index.
395    pub fn key_at(&self, flat_index: usize) -> Option<K> {
396        let visible = self.inner.visible.borrow();
397        let &row_idx = visible.get(flat_index)?;
398        self.inner.rows.borrow().get(row_idx).map(|r| r.key.clone())
399    }
400
401    /// The `FlatEntry` at a visible index (cloned).
402    pub fn entry_at(&self, flat_index: usize) -> Option<FlatEntry<K>> {
403        let visible = self.inner.visible.borrow();
404        let &row_idx = visible.get(flat_index)?;
405        let rows = self.inner.rows.borrow();
406        let row = rows.get(row_idx)?;
407        Some(FlatEntry {
408            node_id: row.key.clone(),
409            depth: row.depth,
410            has_children: row.has_children,
411            is_expanded: self.inner.all_expanded.get()
412                || self.inner.expanded.borrow().contains(&row.key),
413        })
414    }
415
416    /// Structural depth at a visible index (`0` for a root).
417    pub fn depth_at(&self, flat_index: usize) -> usize {
418        let visible = self.inner.visible.borrow();
419        visible
420            .get(flat_index)
421            .and_then(|&i| self.inner.rows.borrow().get(i).map(|r| r.depth))
422            .unwrap_or(0)
423    }
424
425    /// The visible index of a key, if currently visible.
426    pub fn flat_index_of(&self, key: &K) -> Option<usize> {
427        self.inner.vis_pos.borrow().get(key).copied()
428    }
429
430    /// Whether `key` still exists in the source, independent of visibility (a
431    /// node hidden under a collapsed ancestor still exists).
432    pub fn contains_key(&self, key: &K) -> bool {
433        self.inner.row_pos.borrow().contains_key(key)
434    }
435
436    /// The parent of a node (`None` for a root or an absent key).
437    pub fn parent_of(&self, key: &K) -> Option<K> {
438        let idx = *self.inner.row_pos.borrow().get(key)?;
439        self.inner
440            .rows
441            .borrow()
442            .get(idx)
443            .and_then(|r| r.parent.clone())
444    }
445
446    /// The children of a node, in order (empty for a leaf / absent key). O(children).
447    pub fn child_keys_of(&self, key: &K) -> Vec<K> {
448        let children = self.inner.children.borrow();
449        let rows = self.inner.rows.borrow();
450        children
451            .get(key)
452            .map(|idxs| {
453                idxs.iter()
454                    .filter_map(|&i| rows.get(i).map(|r| r.key.clone()))
455                    .collect()
456            })
457            .unwrap_or_default()
458    }
459
460    // ── Expand / collapse (per-view) ──────────────────────────────────────
461
462    /// Whether the node is *effectively* expanded (its children shown) — `true`
463    /// for every branch while the [`set_all_expanded`](Self::set_all_expanded)
464    /// reveal override is on, otherwise its per-view expand state. Use
465    /// [`expanded_keys`](Self::expanded_keys) for the persistent set.
466    pub fn is_expanded(&self, key: &K) -> bool {
467        self.inner.all_expanded.get() || self.inner.expanded.borrow().contains(key)
468    }
469
470    /// Expand a node (make its children visible).
471    pub fn expand(&self, key: &K)
472    where
473        T: PartialEq,
474    {
475        self.set_expanded_flag(key, true);
476    }
477
478    /// Collapse a node (hide its children).
479    pub fn collapse(&self, key: &K)
480    where
481        T: PartialEq,
482    {
483        self.set_expanded_flag(key, false);
484    }
485
486    /// Toggle a node's expand state.
487    pub fn toggle(&self, key: &K)
488    where
489        T: PartialEq,
490    {
491        // Toggle the persistent per-view state (not the reveal override).
492        let expanded = self.inner.expanded.borrow().contains(key);
493        self.set_expanded_flag(key, !expanded);
494    }
495
496    /// Expand every node that has children.
497    pub fn expand_all(&self)
498    where
499        T: PartialEq,
500    {
501        let target: HashSet<K> = {
502            let rows = self.inner.rows.borrow();
503            rows.iter()
504                .filter(|r| r.has_children)
505                .map(|r| r.key.clone())
506                .collect()
507        };
508        self.replace_expanded(target);
509    }
510
511    /// Collapse every node (only roots remain visible).
512    pub fn collapse_all(&self)
513    where
514        T: PartialEq,
515    {
516        self.replace_expanded(HashSet::new());
517    }
518
519    /// The currently-expanded keys (for persistence).
520    pub fn expanded_keys(&self) -> Vec<K> {
521        self.inner.expanded.borrow().iter().cloned().collect()
522    }
523
524    /// Restore expanded state (for persistence). Keys absent from the source are
525    /// ignored on the next reflatten.
526    pub fn set_expanded_keys(&self, keys: &[K])
527    where
528        T: PartialEq,
529    {
530        self.replace_expanded(keys.iter().cloned().collect());
531    }
532
533    // ── Reactivity ────────────────────────────────────────────────────────
534
535    /// Version signal — bind at `BindingLevel::Rebuild`. Bumps on every
536    /// `set_rows` / expand / collapse.
537    pub fn version_signal(&self) -> Signal<u64> {
538        self.inner.version.clone()
539    }
540
541    /// First visible index whose content may differ after the latest change —
542    /// rows `0..index` are unchanged (same key, depth, has-children, expand, and
543    /// item content), so per-row derived state remains valid for them. Equal to
544    /// `visible_count()` when nothing visible changed; `None` before the first
545    /// change (construction is not a change).
546    pub fn first_changed_index(&self) -> Option<usize> {
547        self.inner.divergence.get()
548    }
549
550    // ── Internal ──────────────────────────────────────────────────────────
551
552    fn set_expanded_flag(&self, key: &K, expanded: bool)
553    where
554        T: PartialEq,
555    {
556        let mut target = self.inner.expanded.borrow().clone();
557        let changed = if expanded {
558            target.insert(key.clone())
559        } else {
560            target.remove(key)
561        };
562        if !changed {
563            return;
564        }
565        self.replace_expanded(target);
566    }
567
568    /// Swap the expand set to `target`, reflatten, compute divergence, bump.
569    /// Structure (`rows`/`children`/`roots`/`row_pos`) is untouched.
570    fn replace_expanded(&self, target: HashSet<K>)
571    where
572        T: PartialEq,
573    {
574        let all = self.inner.all_expanded.get();
575        let (visible, vis_pos) = {
576            let rows = self.inner.rows.borrow();
577            let children = self.inner.children.borrow();
578            let roots = self.inner.roots.borrow();
579            flatten(&rows, &children, &roots, &target, all)
580        };
581        let div = {
582            let rows = self.inner.rows.borrow();
583            let old_visible = self.inner.visible.borrow();
584            let old_expanded = self.inner.expanded.borrow();
585            common_prefix(
586                &rows,
587                &old_visible,
588                &old_expanded,
589                all,
590                &rows,
591                &visible,
592                &target,
593                all,
594            )
595        };
596        *self.inner.visible.borrow_mut() = visible;
597        *self.inner.vis_pos.borrow_mut() = vis_pos;
598        *self.inner.expanded.borrow_mut() = target;
599        self.inner.divergence.set(Some(div));
600        self.bump();
601    }
602
603    /// Reveal override for a filtered view: when `on`, the flatten treats every
604    /// node as expanded, so all rows in the (already sort/filter-narrowed) stream
605    /// are visible — the ancestors `TreeRowFilter::KeepAncestors` keeps no longer
606    /// hide their matching descendants. The per-view expand set is **preserved**
607    /// underneath, so turning it off restores the user's real collapse state.
608    /// Flip it on with the filter and off when it clears. No-op if unchanged.
609    pub fn set_all_expanded(&self, on: bool)
610    where
611        T: PartialEq,
612    {
613        if self.inner.all_expanded.get() == on {
614            return;
615        }
616        let expanded = self.inner.expanded.borrow().clone();
617        let (visible, vis_pos) = {
618            let rows = self.inner.rows.borrow();
619            let children = self.inner.children.borrow();
620            let roots = self.inner.roots.borrow();
621            flatten(&rows, &children, &roots, &expanded, on)
622        };
623        let div = {
624            let rows = self.inner.rows.borrow();
625            let old_visible = self.inner.visible.borrow();
626            common_prefix(
627                &rows,
628                &old_visible,
629                &expanded,
630                !on, // the previous flag value
631                &rows,
632                &visible,
633                &expanded,
634                on,
635            )
636        };
637        self.inner.all_expanded.set(on);
638        *self.inner.visible.borrow_mut() = visible;
639        *self.inner.vis_pos.borrow_mut() = vis_pos;
640        self.inner.divergence.set(Some(div));
641        self.bump();
642    }
643
644    /// Whether the reveal-all override is on (see [`set_all_expanded`](Self::set_all_expanded)).
645    pub fn all_expanded(&self) -> bool {
646        self.inner.all_expanded.get()
647    }
648
649    /// Derive the full structure + projection from a raw row stream, seeding the
650    /// new expand/seen sets from the current ones (preserve expand by key,
651    /// auto-expand newly-seen nodes per policy). Reads current state; commits nothing.
652    fn build(&self, input: Vec<TreeRow<K, T>>) -> Built<K, T> {
653        // 1. Derive parent links + structural depth via the indent stack.
654        let mut rows: Vec<Row<K, T>> = Vec::with_capacity(input.len());
655        // stack entries: (raw indent depth, key, row index)
656        let mut stack: Vec<(usize, K, usize)> = Vec::new();
657        for tr in input {
658            while let Some((d, _, _)) = stack.last() {
659                if *d >= tr.depth {
660                    stack.pop();
661                } else {
662                    break;
663                }
664            }
665            let (parent, struct_depth) = match stack.last() {
666                Some((_, k, pidx)) => (Some(k.clone()), rows[*pidx].depth + 1),
667                None => (None, 0),
668            };
669            let idx = rows.len();
670            let key = tr.key.clone();
671            rows.push(Row {
672                key: tr.key,
673                item: tr.item,
674                depth: struct_depth,
675                parent,
676                // Filled in below for every row that did not declare it.
677                has_children: tr.has_children.unwrap_or(false),
678                declared: tr.has_children.is_some(),
679            });
680            stack.push((tr.depth, key, idx));
681        }
682
683        // 2. Build the child index, roots, and row_pos.
684        let mut children: HashMap<K, Vec<usize>> = HashMap::new();
685        let mut roots: Vec<usize> = Vec::new();
686        let mut row_pos: HashMap<K, usize> = HashMap::with_capacity(rows.len());
687        for (i, r) in rows.iter().enumerate() {
688            row_pos.insert(r.key.clone(), i);
689            match &r.parent {
690                Some(pk) => children.entry(pk.clone()).or_default().push(i),
691                None => roots.push(i),
692            }
693        }
694        // Structural derivation, for every row that did not declare an answer. A row
695        // that did keeps it: that is the whole point of the declaration, and a lazy
696        // source's unopened branch would otherwise be derived childless and could
697        // never be opened.
698        for r in rows.iter_mut() {
699            if !r.declared {
700                r.has_children = children.get(&r.key).is_some_and(|v| !v.is_empty());
701            }
702        }
703
704        // 3. Seed the expand + seen sets from the current ones.
705        let expand_new = self.inner.expand_new.get();
706        let (mut expanded, mut seen) = {
707            let old_exp = self.inner.expanded.borrow();
708            let old_seen = self.inner.seen.borrow();
709            let mut e = HashSet::new();
710            let s = old_seen.clone();
711            for r in &rows {
712                if old_seen.contains(&r.key) {
713                    if old_exp.contains(&r.key) {
714                        e.insert(r.key.clone());
715                    }
716                } else if expand_new && r.has_children {
717                    e.insert(r.key.clone());
718                }
719            }
720            (e, s)
721        };
722        for r in &rows {
723            seen.insert(r.key.clone());
724        }
725        // Drop expand entries whose node vanished.
726        expanded.retain(|k| row_pos.contains_key(k));
727
728        // 4. Flatten to the visible projection.
729        let (visible, vis_pos) = flatten(
730            &rows,
731            &children,
732            &roots,
733            &expanded,
734            self.inner.all_expanded.get(),
735        );
736
737        Built {
738            rows,
739            children,
740            roots,
741            row_pos,
742            visible,
743            vis_pos,
744            expanded,
745            seen,
746        }
747    }
748
749    /// Move a `Built` into `self`. Every interior borrow is released before
750    /// returning; callers bump the version afterwards (never while borrowed).
751    fn commit(&self, built: Built<K, T>) {
752        *self.inner.rows.borrow_mut() = built.rows;
753        *self.inner.children.borrow_mut() = built.children;
754        *self.inner.roots.borrow_mut() = built.roots;
755        *self.inner.row_pos.borrow_mut() = built.row_pos;
756        *self.inner.visible.borrow_mut() = built.visible;
757        *self.inner.vis_pos.borrow_mut() = built.vis_pos;
758        *self.inner.expanded.borrow_mut() = built.expanded;
759        *self.inner.seen.borrow_mut() = built.seen;
760    }
761
762    fn bump(&self) {
763        let next = self.inner.version_counter.get() + 1;
764        self.inner.version_counter.set(next);
765        self.inner.version.set(next);
766    }
767
768    /// Whether `maybe_descendant` is inside the subtree rooted at `ancestor`.
769    fn is_descendant(&self, maybe_descendant: &K, ancestor: &K) -> bool {
770        let rows = self.inner.rows.borrow();
771        let row_pos = self.inner.row_pos.borrow();
772        let mut cur = maybe_descendant.clone();
773        for _ in 0..rows.len() {
774            let Some(&idx) = row_pos.get(&cur) else {
775                return false;
776            };
777            let Some(parent) = rows[idx].parent.clone() else {
778                return false;
779            };
780            if &parent == ancestor {
781                return true;
782            }
783            cur = parent;
784        }
785        false
786    }
787
788    /// Cycle guard (mechanism) + domain resolver (policy). `None` = forbidden.
789    fn resolve(&self, dragged: &K, target: &K, position: DropPosition) -> Option<DropPosition> {
790        if dragged == target || self.is_descendant(target, dragged) {
791            return None;
792        }
793        // Clone the handle out and drop the `drop_resolver` borrow before
794        // calling `f` — a resolver that calls `set_drop_resolver` on the
795        // same slice would otherwise hit a `BorrowMutError`.
796        let resolver = self.inner.drop_resolver.borrow().clone();
797        match resolver {
798            Some(f) => {
799                let row_pos = self.inner.row_pos.borrow();
800                let &idx = row_pos.get(target)?; // absent target → forbid
801                let rows = self.inner.rows.borrow();
802                f(dragged, target, &rows[idx].item, position)
803            }
804            None => Some(position),
805        }
806    }
807}
808
809impl<K: ItemKey, T> std::fmt::Debug for TreeDataSlice<K, T> {
810    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
811        f.debug_struct("TreeDataSlice")
812            .field("visible_count", &self.visible_count())
813            .field("row_count", &self.inner.rows.borrow().len())
814            .field("expanded_count", &self.inner.expanded.borrow().len())
815            .finish()
816    }
817}
818
819/// Recursive collapse-aware flatten: emit each root's subtree, descending only
820/// into expanded nodes. Visits O(visible) rows.
821fn flatten<K: ItemKey, T>(
822    rows: &[Row<K, T>],
823    children: &HashMap<K, Vec<usize>>,
824    roots: &[usize],
825    expanded: &HashSet<K>,
826    all_expanded: bool,
827) -> (Vec<usize>, HashMap<K, usize>) {
828    let mut visible = Vec::with_capacity(rows.len());
829    let mut vis_pos = HashMap::with_capacity(rows.len());
830    for &root in roots {
831        flatten_node(
832            root,
833            rows,
834            children,
835            expanded,
836            all_expanded,
837            &mut visible,
838            &mut vis_pos,
839        );
840    }
841    (visible, vis_pos)
842}
843
844fn flatten_node<K: ItemKey, T>(
845    idx: usize,
846    rows: &[Row<K, T>],
847    children: &HashMap<K, Vec<usize>>,
848    expanded: &HashSet<K>,
849    all_expanded: bool,
850    visible: &mut Vec<usize>,
851    vis_pos: &mut HashMap<K, usize>,
852) {
853    let row = &rows[idx];
854    vis_pos.insert(row.key.clone(), visible.len());
855    visible.push(idx);
856    if row.has_children
857        && (all_expanded || expanded.contains(&row.key))
858        && let Some(kids) = children.get(&row.key)
859    {
860        for &child in kids {
861            flatten_node(
862                child,
863                rows,
864                children,
865                expanded,
866                all_expanded,
867                visible,
868                vis_pos,
869            );
870        }
871    }
872}
873
874/// Length of the common prefix of the old vs new visible lists, comparing key +
875/// depth + has-children + expand state + **item content**. The first index at
876/// which the projection diverges; equals `min(len)` when the shorter list is a
877/// prefix of the longer.
878#[allow(clippy::too_many_arguments)]
879fn common_prefix<K: ItemKey, T: PartialEq>(
880    old_rows: &[Row<K, T>],
881    old_visible: &[usize],
882    old_expanded: &HashSet<K>,
883    old_all: bool,
884    new_rows: &[Row<K, T>],
885    new_visible: &[usize],
886    new_expanded: &HashSet<K>,
887    new_all: bool,
888) -> usize {
889    let n = old_visible.len().min(new_visible.len());
890    for i in 0..n {
891        let o = &old_rows[old_visible[i]];
892        let m = &new_rows[new_visible[i]];
893        let o_exp = old_all || old_expanded.contains(&o.key);
894        let m_exp = new_all || new_expanded.contains(&m.key);
895        if o.key != m.key
896            || o.depth != m.depth
897            || o.has_children != m.has_children
898            || o_exp != m_exp
899            || o.item != m.item
900        {
901            return i;
902        }
903    }
904    n
905}
906
907/// `TreeDataSlice` is a reusable per-view `TreeDataSource` over an external,
908/// indent-ordered source. Identity is the domain key `K`; a `SameView` drop is
909/// resolved by the cycle guard + injected drop resolver and applied via the
910/// injected reorder command (then the slice re-sources). `Foreign` drops are
911/// rejected.
912impl<K: ItemKey, T: PartialEq + 'static> TreeDataSource for TreeDataSlice<K, T> {
913    type Item = T;
914    type Key = K;
915
916    fn visible_count(&self) -> usize {
917        TreeDataSlice::visible_count(self)
918    }
919
920    fn with_entry<R>(
921        &self,
922        flat_index: usize,
923        f: impl FnOnce(&Self::Item, &FlatEntry<Self::Key>) -> R,
924    ) -> Option<R> {
925        TreeDataSlice::with_entry(self, flat_index, f)
926    }
927
928    fn key_at(&self, flat_index: usize) -> Option<K> {
929        TreeDataSlice::key_at(self, flat_index)
930    }
931
932    fn flat_index_of(&self, key: &K) -> Option<usize> {
933        TreeDataSlice::flat_index_of(self, key)
934    }
935
936    fn parent(&self, key: &K) -> Option<K> {
937        TreeDataSlice::parent_of(self, key)
938    }
939
940    fn child_keys(&self, key: &K) -> Vec<K> {
941        TreeDataSlice::child_keys_of(self, key)
942    }
943
944    fn version_signal(&self) -> Signal<u64> {
945        TreeDataSlice::version_signal(self)
946    }
947
948    fn first_changed_index(&self) -> Option<usize> {
949        TreeDataSlice::first_changed_index(self)
950    }
951
952    fn contains_key(&self, key: &K) -> bool {
953        TreeDataSlice::contains_key(self, key)
954    }
955
956    fn is_expanded(&self, key: &K) -> bool {
957        TreeDataSlice::is_expanded(self, key)
958    }
959
960    fn set_expanded(&self, key: &K, expanded: bool) {
961        self.set_expanded_flag(key, expanded);
962    }
963
964    fn drag(&self, key: &K) -> DragEligibility {
965        // Clone the handle out and drop the borrow before calling `f` — see
966        // `resolve`'s matching comment.
967        let policy = self.inner.drag_policy.borrow().clone();
968        match policy {
969            Some(f) => f(key),
970            None => DragEligibility::NoDrag,
971        }
972    }
973
974    fn can_accept(&self, query: &DropQuery<'_, K>) -> DropResponse {
975        let dragged = match &query.source {
976            DragSource::SameView { key } => key,
977            DragSource::Foreign { .. } => return DropResponse::Reject,
978        };
979        match self.resolve(dragged, &query.target, query.position) {
980            Some(p) if p == query.position => DropResponse::Accept,
981            Some(p) => DropResponse::Redirect(p),
982            None => DropResponse::Reject,
983        }
984    }
985
986    fn accept_drop(&self, commit: DropCommit<'_, K>) -> bool {
987        let dragged = match &commit.source {
988            DragSource::SameView { key } => key.clone(),
989            DragSource::Foreign { .. } => return false,
990        };
991        let Some(place) = self.resolve(&dragged, &commit.target, commit.position) else {
992            return false;
993        };
994        // Clone the handle out and drop the `reorder` borrow before calling
995        // `f` — a reorder command that calls back into the slice (e.g.
996        // `set_reorder`, to reconfigure itself after applying the move)
997        // would otherwise hit a `BorrowMutError`.
998        let reorder = self.inner.reorder.borrow().clone();
999        let applied = match reorder {
1000            Some(f) => f(dragged, commit.target.clone(), place),
1001            None => return false,
1002        };
1003        if applied {
1004            self.reload();
1005            true
1006        } else {
1007            false
1008        }
1009    }
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014    use super::*;
1015
1016    /// A sample outline (binder-style: depth-0 roots + indented items):
1017    /// M (binder)
1018    ///   Book        (folder)
1019    ///     Opening
1020    ///     Dawn
1021    ///   Ch2         (folder)
1022    ///     Fight
1023    /// N (binder)
1024    ///   Sketch
1025    fn sample() -> Vec<TreeRow<u64, &'static str>> {
1026        vec![
1027            TreeRow::new(1, "M", 0),
1028            TreeRow::new(101, "Book", 1),
1029            TreeRow::new(102, "Opening", 2),
1030            TreeRow::new(103, "Dawn", 2),
1031            TreeRow::new(104, "Ch2", 1),
1032            TreeRow::new(105, "Fight", 2),
1033            TreeRow::new(2, "N", 0),
1034            TreeRow::new(106, "Sketch", 1),
1035        ]
1036    }
1037
1038    fn expanded_slice() -> TreeDataSlice<u64, &'static str> {
1039        let slice = TreeDataSlice::new();
1040        slice.set_expand_new_nodes(true);
1041        slice.set_source(sample);
1042        slice.reload();
1043        slice
1044    }
1045
1046    /// **A row may promise children the stream has not delivered.**
1047    ///
1048    /// This is what a source that materialises a branch on expand needs, and
1049    /// without it such a source deadlocks: no children emitted, so the row derives
1050    /// childless, so no chevron is drawn, so the toggle it wanted to load from can
1051    /// never fire.
1052    #[test]
1053    fn a_declared_parent_keeps_its_chevron_with_no_children_in_the_stream() {
1054        let slice = TreeDataSlice::from_rows(vec![
1055            TreeRow::new(1, "Scene one", 0).with_children(true),
1056            TreeRow::new(2, "Scene two", 0).with_children(true),
1057        ]);
1058        assert_eq!(slice.visible_count(), 2);
1059        for i in 0..2 {
1060            slice.with_entry(i, |_, e| {
1061                assert!(e.has_children, "a promise the stream cannot corroborate");
1062            });
1063        }
1064    }
1065
1066    /// The declaration overrides the stream in **both** directions, so a source can
1067    /// also say "leaf" about a row that happens to have rows under it.
1068    #[test]
1069    fn a_declared_leaf_stays_a_leaf() {
1070        let slice = TreeDataSlice::from_rows(vec![
1071            TreeRow::new(1, "Parent", 0).with_children(false),
1072            TreeRow::new(2, "Child", 1),
1073        ]);
1074        slice.set_expanded_keys(&[1]);
1075        slice.with_entry(0, |_, e| assert!(!e.has_children));
1076    }
1077
1078    /// **Silence still means "derive it".** Every existing source hands over the
1079    /// whole tree and says nothing, and must keep getting the structural answer.
1080    #[test]
1081    fn an_undeclared_row_is_still_derived_from_the_stream() {
1082        let slice = TreeDataSlice::from_rows(sample());
1083        slice.with_entry(0, |item, e| {
1084            assert_eq!(*item, "M");
1085            assert!(e.has_children, "M has Book and Ch2 beneath it");
1086        });
1087        slice.with_entry(2, |item, e| {
1088            assert_eq!(*item, "Opening");
1089            assert!(!e.has_children, "Opening has nothing beneath it");
1090        });
1091    }
1092
1093    /// The promise survives the reload a lazy source performs when it fills a
1094    /// branch in, and the row keeps its expand state across it — which is the whole
1095    /// sequence: declare, expand, re-source with children, stay open.
1096    #[test]
1097    fn a_declared_parent_can_be_expanded_and_then_filled_in() {
1098        let filled = std::rc::Rc::new(std::cell::Cell::new(false));
1099        let slice = TreeDataSlice::new();
1100        slice.set_source({
1101            let filled = filled.clone();
1102            move || {
1103                let mut rows = vec![TreeRow::new(1, "Scene one", 0).with_children(true)];
1104                if filled.get() {
1105                    rows.push(TreeRow::new(11, "hit at 42", 1));
1106                    rows.push(TreeRow::new(12, "hit at 91", 1));
1107                }
1108                rows
1109            }
1110        });
1111        slice.reload();
1112        slice.with_entry(0, |_, e| assert!(e.has_children));
1113        assert_eq!(slice.visible_count(), 1);
1114
1115        slice.set_expanded_keys(&[1]);
1116        assert!(slice.is_expanded(&1));
1117        // Still nothing under it: the app has not loaded the branch yet.
1118        assert_eq!(slice.visible_count(), 1);
1119
1120        filled.set(true);
1121        slice.reload();
1122        assert!(slice.is_expanded(&1), "the expand survived the reload");
1123        assert_eq!(
1124            slice.visible_count(),
1125            3,
1126            "and the branch is now really there"
1127        );
1128        slice.with_entry(0, |_, e| assert!(e.has_children));
1129    }
1130
1131    #[test]
1132    fn structure_derivation() {
1133        let slice = TreeDataSlice::from_rows(sample());
1134        // parents
1135        assert_eq!(slice.parent_of(&1), None); // binder is a root
1136        assert_eq!(slice.parent_of(&101), Some(1)); // Book under M
1137        assert_eq!(slice.parent_of(&102), Some(101)); // Opening under Book
1138        assert_eq!(slice.parent_of(&104), Some(1)); // Ch2 under M
1139        assert_eq!(slice.parent_of(&105), Some(104)); // Fight under Ch2
1140        assert_eq!(slice.parent_of(&106), Some(2)); // Sketch under N
1141        // children
1142        assert_eq!(slice.child_keys_of(&1), vec![101, 104]);
1143        assert_eq!(slice.child_keys_of(&101), vec![102, 103]);
1144        assert_eq!(slice.child_keys_of(&102), Vec::<u64>::new()); // leaf
1145    }
1146
1147    #[test]
1148    fn collapsed_by_default_shows_roots() {
1149        let slice = TreeDataSlice::from_rows(sample());
1150        assert_eq!(slice.visible_count(), 2); // M, N
1151        assert_eq!(slice.key_at(0), Some(1));
1152        assert_eq!(slice.key_at(1), Some(2));
1153    }
1154
1155    #[test]
1156    fn expand_new_shows_all() {
1157        let slice = expanded_slice();
1158        assert_eq!(slice.visible_count(), 8);
1159        assert_eq!(
1160            slice.with_entry(1, |item, e| {
1161                assert_eq!(*item, "Book");
1162                assert_eq!(e.depth, 1);
1163                assert!(e.has_children);
1164            }),
1165            Some(())
1166        );
1167    }
1168
1169    #[test]
1170    fn collapse_hides_subtree() {
1171        let slice = expanded_slice();
1172        assert_eq!(slice.visible_count(), 8);
1173        slice.collapse(&101); // Book (2 children)
1174        assert_eq!(slice.visible_count(), 6);
1175        slice.expand(&101);
1176        assert_eq!(slice.visible_count(), 8);
1177    }
1178
1179    #[test]
1180    fn toggle_and_flat_index() {
1181        let slice = TreeDataSlice::from_rows(sample());
1182        assert_eq!(slice.flat_index_of(&1), Some(0));
1183        assert_eq!(slice.flat_index_of(&101), None); // hidden
1184        slice.toggle(&1);
1185        assert_eq!(slice.flat_index_of(&101), Some(1));
1186        assert!(slice.is_expanded(&1));
1187    }
1188
1189    #[test]
1190    fn expand_all_collapse_all() {
1191        let slice = TreeDataSlice::from_rows(sample());
1192        slice.expand_all();
1193        assert_eq!(slice.visible_count(), 8);
1194        slice.collapse_all();
1195        assert_eq!(slice.visible_count(), 2);
1196    }
1197
1198    #[test]
1199    fn set_all_expanded_reveals_then_restores() {
1200        let slice = TreeDataSlice::from_rows(sample()); // collapsed → 2 roots
1201        assert_eq!(slice.visible_count(), 2);
1202        assert!(!slice.all_expanded());
1203
1204        slice.set_all_expanded(true);
1205        assert_eq!(slice.visible_count(), 8); // everything revealed
1206        assert!(slice.all_expanded());
1207        assert!(slice.is_expanded(&1)); // effective: shown open
1208
1209        slice.set_all_expanded(false);
1210        assert_eq!(slice.visible_count(), 2); // back to collapsed
1211        assert!(!slice.all_expanded());
1212    }
1213
1214    #[test]
1215    fn reveal_preserves_raw_expand_set() {
1216        let slice = TreeDataSlice::from_rows(sample());
1217        slice.expand(&1); // M expanded (persistent) → M, Book, Ch2, N
1218        assert_eq!(slice.visible_count(), 4);
1219
1220        slice.set_all_expanded(true);
1221        assert_eq!(slice.visible_count(), 8);
1222
1223        slice.set_all_expanded(false);
1224        // M's persistent expand survived the reveal round-trip; N still collapsed.
1225        assert_eq!(slice.visible_count(), 4);
1226        assert_eq!(slice.expanded_keys(), vec![1]);
1227    }
1228
1229    #[test]
1230    fn filter_keepancestors_reveal_shows_matches() {
1231        // The gap this closes: KeepAncestors keeps the ancestor rows, but a
1232        // freshly-collapsed slice hides the match under them — set_all_expanded
1233        // reveals the whole filtered result without touching the persistent set.
1234        use crate::{TreeFilterMode, TreeRowFilter};
1235        let sieve = TreeRowFilter::new()
1236            .filter_mode(TreeFilterMode::KeepAncestors)
1237            .filter(|t: &&str| *t == "Dawn");
1238        let slice = TreeDataSlice::from_rows(sieve.apply(sample()));
1239        // Filtered stream = M → Book → Dawn; collapsed shows only the root M.
1240        assert_eq!(slice.visible_count(), 1);
1241
1242        slice.set_all_expanded(true);
1243        assert_eq!(slice.visible_count(), 3);
1244        let titles: Vec<&str> = (0..3)
1245            .map(|i| slice.with_entry(i, |it, _| *it).unwrap())
1246            .collect();
1247        assert_eq!(titles, vec!["M", "Book", "Dawn"]);
1248    }
1249
1250    #[test]
1251    fn two_slices_independent_expand() {
1252        let a = TreeDataSlice::from_rows(sample());
1253        let b = TreeDataSlice::from_rows(sample());
1254        a.expand(&1);
1255        assert_eq!(a.visible_count(), 4); // M, Book, Ch2, N
1256        assert_eq!(b.visible_count(), 2); // still collapsed
1257    }
1258
1259    #[test]
1260    fn clone_shares_state() {
1261        let a = TreeDataSlice::from_rows(sample());
1262        let b = a.clone();
1263        a.expand(&1);
1264        assert_eq!(b.visible_count(), 4); // b sees a's expand
1265    }
1266
1267    // ── divergence ───────────────────────────────────────────────────────
1268
1269    #[test]
1270    fn divergence_none_before_change() {
1271        let slice = TreeDataSlice::from_rows(sample());
1272        assert_eq!(slice.first_changed_index(), None);
1273    }
1274
1275    #[test]
1276    fn divergence_on_expand_is_toggled_row() {
1277        let slice = TreeDataSlice::from_rows(sample());
1278        // Expanding M (flat 0) changes M's own is_expanded and inserts rows after.
1279        slice.expand(&1);
1280        assert_eq!(slice.first_changed_index(), Some(0));
1281    }
1282
1283    #[test]
1284    fn divergence_on_deep_expand_is_that_row() {
1285        let slice = TreeDataSlice::from_rows(sample());
1286        slice.expand(&1); // M, Book, Ch2, N
1287        // Expanding Book (flat 1) leaves M untouched.
1288        slice.expand(&101);
1289        assert_eq!(slice.first_changed_index(), Some(1));
1290    }
1291
1292    #[test]
1293    fn divergence_on_rename_is_that_row() {
1294        let slice = expanded_slice();
1295        // Rename "Fight" (105) — structure identical, only its item changes.
1296        let renamed: Vec<TreeRow<u64, &'static str>> = sample()
1297            .into_iter()
1298            .map(|mut r| {
1299                if r.key == 105 {
1300                    r.item = "Duel";
1301                }
1302                r
1303            })
1304            .collect();
1305        slice.set_rows(renamed);
1306        // Fight is at flat index 5 (M,Book,Opening,Dawn,Ch2,Fight,...).
1307        assert_eq!(slice.first_changed_index(), Some(5));
1308    }
1309
1310    #[test]
1311    fn divergence_on_append_is_old_len() {
1312        let slice = expanded_slice();
1313        let mut rows = sample();
1314        rows.push(TreeRow::new(107, "Idea", 1)); // new child of N
1315        slice.set_rows(rows);
1316        // Old visible len was 8; the appended row diverges at 8.
1317        assert_eq!(slice.first_changed_index(), Some(8));
1318    }
1319
1320    #[test]
1321    fn reload_preserves_expand_by_key() {
1322        let counter = Rc::new(Cell::new(0u32));
1323        let c = counter.clone();
1324        let slice: TreeDataSlice<u64, &'static str> = TreeDataSlice::new();
1325        slice.set_source(move || {
1326            c.set(c.get() + 1);
1327            sample()
1328        });
1329        slice.reload();
1330        slice.expand(&1);
1331        assert_eq!(slice.visible_count(), 4);
1332        slice.reload(); // re-source; M was expanded and still exists
1333        assert_eq!(slice.visible_count(), 4); // expand survived
1334        assert!(slice.is_expanded(&1));
1335    }
1336
1337    // ── DnD ──────────────────────────────────────────────────────────────
1338
1339    #[test]
1340    fn drag_policy_gate() {
1341        let slice = TreeDataSlice::from_rows(sample());
1342        slice.set_drag_policy(|k| {
1343            if *k < 100 {
1344                DragEligibility::NoDrag // binders
1345            } else {
1346                DragEligibility::CanDrag
1347            }
1348        });
1349        assert_eq!(slice.drag(&1), DragEligibility::NoDrag);
1350        assert_eq!(slice.drag(&102), DragEligibility::CanDrag);
1351    }
1352
1353    #[test]
1354    fn drag_default_is_nodrag() {
1355        let slice = TreeDataSlice::from_rows(sample());
1356        assert_eq!(slice.drag(&102), DragEligibility::NoDrag);
1357    }
1358
1359    #[test]
1360    fn can_accept_rejects_cycle() {
1361        let slice = expanded_slice();
1362        // Drop Book (101) into its own child Opening (102) → cycle.
1363        let q = DropQuery {
1364            source: DragSource::SameView { key: 101 },
1365            target: 102,
1366            position: DropPosition::Into,
1367        };
1368        assert_eq!(slice.can_accept(&q), DropResponse::Reject);
1369    }
1370
1371    #[test]
1372    fn can_accept_default_accepts_sibling() {
1373        let slice = expanded_slice();
1374        let q = DropQuery {
1375            source: DragSource::SameView { key: 102 },
1376            target: 106,
1377            position: DropPosition::Before,
1378        };
1379        assert_eq!(slice.can_accept(&q), DropResponse::Accept);
1380    }
1381
1382    #[test]
1383    fn drop_resolver_redirects() {
1384        let slice = expanded_slice();
1385        // A leaf item (its own item text is inspected here just to exercise the
1386        // target_item param) redirects Into → After.
1387        slice.set_drop_resolver(|_dragged, target, target_item, pos| match pos {
1388            DropPosition::Into if *target == 103 && *target_item == "Dawn" => {
1389                Some(DropPosition::After)
1390            }
1391            p => Some(p),
1392        });
1393        let q = DropQuery {
1394            source: DragSource::SameView { key: 102 },
1395            target: 103,
1396            position: DropPosition::Into,
1397        };
1398        assert_eq!(
1399            slice.can_accept(&q),
1400            DropResponse::Redirect(DropPosition::After)
1401        );
1402    }
1403
1404    #[test]
1405    fn accept_drop_runs_reorder_then_reloads() {
1406        let moved = Rc::new(Cell::new(false));
1407        let m = moved.clone();
1408        let slice = expanded_slice();
1409        slice.set_reorder(move |dragged, target, _pos| {
1410            assert_eq!(dragged, 102);
1411            assert_eq!(target, 106);
1412            m.set(true);
1413            true
1414        });
1415        let ok = slice.accept_drop(DropCommit {
1416            source: DragSource::SameView { key: 102 },
1417            target: 106,
1418            position: DropPosition::Before,
1419        });
1420        assert!(ok);
1421        assert!(moved.get());
1422    }
1423
1424    #[test]
1425    fn reorder_closure_can_call_back_into_the_slice() {
1426        // Regression: `accept_drop` used to hold a `Ref` on the reorder
1427        // closure's `RefCell` for the whole call, so a closure that called
1428        // back into the slice — e.g. `set_reorder`, to swap its own policy
1429        // out right after applying a move — hit a `BorrowMutError`. The
1430        // handle must be cloned out and the borrow dropped before the
1431        // closure runs.
1432        let slice = expanded_slice();
1433        let reentrant_target = slice.clone();
1434        slice.set_reorder(move |_dragged, _target, _pos| {
1435            reentrant_target.set_reorder(|_, _, _| true);
1436            true
1437        });
1438        let ok = slice.accept_drop(DropCommit {
1439            source: DragSource::SameView { key: 102 },
1440            target: 106,
1441            position: DropPosition::Before,
1442        });
1443        assert!(ok);
1444    }
1445
1446    #[test]
1447    fn source_closure_can_call_back_into_the_slice() {
1448        // Same shape as the reorder-closure regression above, on the loader
1449        // path: `reload` used to hold a `Ref` on the source `RefCell` while
1450        // invoking the loader, so a loader that re-installed the source (a
1451        // one-shot loader swapping itself for the steady-state one) hit a
1452        // `BorrowMutError`.
1453        let slice = TreeDataSlice::<u64, &'static str>::new();
1454        let reentrant = slice.clone();
1455        slice.set_source(move || {
1456            reentrant.set_source(Vec::new);
1457            vec![TreeRow::new(1, "root", 0)]
1458        });
1459        slice.reload();
1460        assert_eq!(slice.visible_count(), 1);
1461    }
1462
1463    #[test]
1464    fn accept_drop_without_reorder_is_refused() {
1465        let slice = expanded_slice();
1466        let ok = slice.accept_drop(DropCommit {
1467            source: DragSource::SameView { key: 102 },
1468            target: 106,
1469            position: DropPosition::Before,
1470        });
1471        assert!(!ok);
1472    }
1473
1474    #[test]
1475    fn foreign_drop_rejected() {
1476        let slice = expanded_slice();
1477        slice.set_reorder(|_, _, _| true);
1478        // No Foreign fixture here; can_accept path for Foreign is covered by
1479        // the SameView tests + the explicit early return. Assert accept_drop
1480        // refuses when there is no reorder for a same-view drop into self.
1481        let ok = slice.accept_drop(DropCommit {
1482            source: DragSource::SameView { key: 1 },
1483            target: 1, // self-drop → resolve None
1484            position: DropPosition::Into,
1485        });
1486        assert!(!ok);
1487    }
1488}