Skip to main content

teksilo_widgets/
tree_source.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Type-erased data source adapter for [`TreeView`](crate::TreeView).
5//!
6//! Wraps any [`TreeDataSource`] behind a uniform set of `Rc<dyn Fn(..)>` closures
7//! keyed on the **visible flat index**, so `TreeView<T>` requires no extra type
8//! parameter for the source's `Key`. Each closure resolves index → `Key` (via
9//! `key_at`) before forwarding to the source's `parent`, `set_expanded`,
10//! `can_accept`, etc. The `Key` type is fully captured here and never surfaces
11//! in the view.
12//!
13//! Both built-in and external backings flow through
14//! [`TreeSource::from_data_source`]: the `TreeView::new(TreeModel)` path wraps a
15//! `Rc<TreeSlice<T>>` (which implements `TreeDataSource<Key = NodeId>`), while
16//! `TreeView::from_source` wraps an external `TreeDataSource` with its own `Key`.
17//! The only built-in-vs-external difference — the `NodeId`-typed `TreeRowContext`
18//! handed to the legacy delegate — lives in `tree_view.rs`, not here.
19
20use std::cell::RefCell;
21use std::rc::Rc;
22
23use teksilo_core::drag_payload::DragPayload;
24use teksilo_core::signal::Signal;
25use teksilo_core::widget::{EventContext, Widget};
26use teksilo_data::{
27    DragEligibility, DragSource, DropCommit, DropPosition, DropQuery, DropResponse, RowState,
28    TreeDataSource,
29};
30
31use crate::data_views::{RowDragData, ViewId};
32
33/// Key-erased per-row flat metadata, derived from the source's `FlatEntry`.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct TreeRowMeta {
36    /// Depth in the tree (0 for roots).
37    pub depth: usize,
38    /// Whether this row has children in the source.
39    pub has_children: bool,
40    /// Whether this row is currently expanded.
41    pub is_expanded: bool,
42}
43
44/// Per-row context handed to a [`TreeView::from_source`](crate::TreeView::from_source)
45/// delegate — the key-erased counterpart of the built-in
46/// [`TreeRowContext`](crate::TreeRowContext). Carries the row's flat metadata
47/// plus a one-call chevron toggle that flips the row's expansion through the
48/// source (by index → key → `set_expanded`).
49pub struct TreeRow {
50    /// Depth in the tree (0 for roots).
51    pub depth: usize,
52    /// Whether this row has children in the source.
53    pub has_children: bool,
54    /// Whether this row is currently expanded.
55    pub is_expanded: bool,
56    toggle: Rc<dyn Fn(&mut EventContext)>,
57}
58
59impl TreeRow {
60    /// Toggle callback for this row's chevron. Wires in one line:
61    /// `.on_chevron_toggle_rc(row.toggle_callback())`.
62    pub fn toggle_callback(&self) -> Rc<dyn Fn(&mut EventContext)> {
63        self.toggle.clone()
64    }
65}
66
67/// Cache of the ascending flat indices of every visible depth-0 (root) row,
68/// valid for one source version. Roots have no `parent` to enumerate
69/// siblings through, so both `sibling_pos`'s root branch and Alt+Arrow's
70/// root-sibling reorder fall back to scanning the WHOLE visible range for
71/// `depth == 0` rows — O(realized root rows × visible count) per rebuild for
72/// a flat-ish tree with many roots, since every realized root row repeats
73/// the full scan. Rebuilt once per version bump (shared by all three call
74/// sites) instead, then answered by a binary search (`sibling_pos`) or a
75/// direct re-map (`sibling_move` / `reparent`).
76type RootIndexCache = RefCell<Option<(u64, Rc<Vec<usize>>)>>;
77
78/// The cached root indices for `source`'s current version, rescanning only
79/// when the version has moved on since the last call.
80fn root_indices<S: TreeDataSource>(source: &S, cache: &RootIndexCache) -> Rc<Vec<usize>> {
81    let version = source.version_signal().get();
82    {
83        let cached = cache.borrow();
84        if let Some((v, flat)) = cached.as_ref()
85            && *v == version
86        {
87            return flat.clone();
88        }
89    }
90    let n = source.visible_count();
91    let flat = Rc::new(
92        (0..n)
93            .filter(|&j| source.with_entry(j, |_it, e| e.depth == 0).unwrap_or(false))
94            .collect::<Vec<usize>>(),
95    );
96    *cache.borrow_mut() = Some((version, flat.clone()));
97    flat
98}
99
100/// Erased DnD + lazy capability closures for a tree source. View-facing
101/// arguments are visible flat indices + the view's id; the closures resolve keys
102/// internally. Mirrors [`DndLazy`](crate::list_source::DndLazy) for trees, so the
103/// `Key` never escapes into `TreeView<T>`.
104pub(crate) struct TreeDndLazy {
105    /// Whether the row at `index` may begin a drag.
106    pub(crate) drag_fn: Rc<dyn Fn(usize) -> DragEligibility>,
107    /// `(payload, target_index, position, this_view_id) -> verdict`.
108    pub(crate) can_accept_fn: Rc<dyn Fn(&DragPayload, usize, DropPosition, ViewId) -> DropResponse>,
109    /// `(payload, target_index, position, this_view_id) -> applied`.
110    pub(crate) accept_drop_fn: Rc<dyn Fn(&DragPayload, usize, DropPosition, ViewId) -> bool>,
111    /// Source-side completion: resolve stable node keys for these rows NOW
112    /// (drag-start) and return a thunk that removes them (a foreign move-out).
113    /// Resolving eagerly keeps a Move correct even when the tree's flat indices
114    /// reshuffle mid-drag (spring-load auto-expand), since the stable `NodeId`s
115    /// were already captured.
116    pub(crate) snapshot_out_fn: crate::data_views::SnapshotOutFn,
117    /// Resolve + stash the dragged rows' stable node keys for a **synthetic**
118    /// same-view payload built outside `RowExport::build_payload`. Pointer
119    /// drags stash through [`snapshot_out_fn`](Self::snapshot_out_fn) at
120    /// drag-start; the same-view accept path reads identity exclusively from
121    /// this stash — see [`can_accept_fn`](Self::can_accept_fn).
122    pub(crate) stash_drag_keys_fn: Rc<dyn Fn(&[usize])>,
123    /// Whether the row at `index` is loaded.
124    pub(crate) row_state_fn: Rc<dyn Fn(usize) -> RowState>,
125    /// Nudge the source to load a visible range.
126    pub(crate) request_window_fn: Rc<dyn Fn(std::ops::Range<usize>)>,
127    /// Whether more rows can be appended.
128    pub(crate) can_fetch_more_fn: Rc<dyn Fn() -> bool>,
129    /// Fetch the next page.
130    pub(crate) fetch_more_fn: Rc<dyn Fn()>,
131}
132
133impl TreeDndLazy {
134    fn from_source<T: 'static, S: TreeDataSource<Item = T> + 'static>(s: Rc<S>) -> Self {
135        // Stable node keys of the in-flight same-view drag, resolved from flat
136        // indices ONCE at payload construction (`snapshot_out_fn` for pointer
137        // drags, `stash_drag_keys_fn` for synthetic keyboard payloads). The
138        // accept path reads identity from here rather than re-resolving
139        // `RowDragData::rows` at hover/drop time: a tree's flat indices
140        // reshuffle mid-drag (the spring-load auto-expand is triggered by the
141        // very hover that precedes the drop), and whichever nodes slid into
142        // the stale slots must not stand in for the dragged ones.
143        let drag_keys: Rc<RefCell<Option<Vec<S::Key>>>> = Rc::new(RefCell::new(None));
144        let (keys_ca, keys_ad, keys_snap, keys_stash) = (
145            drag_keys.clone(),
146            drag_keys.clone(),
147            drag_keys.clone(),
148            drag_keys,
149        );
150        let (s1, s2, s3, s4, s5, s6, s7, s8, s9) = (
151            s.clone(),
152            s.clone(),
153            s.clone(),
154            s.clone(),
155            s.clone(),
156            s.clone(),
157            s.clone(),
158            s.clone(),
159            s,
160        );
161        Self {
162            drag_fn: Rc::new(move |index| match s1.key_at(index) {
163                Some(k) => s1.drag(&k),
164                None => DragEligibility::NoDrag,
165            }),
166            can_accept_fn: Rc::new(move |payload, target_index, position, view_id| {
167                let Some(target_key) = s2.key_at(target_index) else {
168                    return DropResponse::Reject;
169                };
170                if let Some(rd) = payload.get_typed::<RowDragData<T>>()
171                    && rd.source == view_id
172                {
173                    let source_key = {
174                        let stash = keys_ca.borrow();
175                        let Some(keys) = stash.as_ref().filter(|k| !k.is_empty()) else {
176                            debug_assert!(false, "same-view drag without a drag-start key stash");
177                            return DropResponse::Reject;
178                        };
179                        // Own-row rejection by key, so it survives a mid-drag
180                        // reflow.
181                        if keys.contains(&target_key) {
182                            return DropResponse::Reject;
183                        }
184                        keys[0].clone()
185                    };
186                    return s2.can_accept(&DropQuery {
187                        source: DragSource::SameView { key: source_key },
188                        target: target_key,
189                        position,
190                    });
191                }
192                s2.can_accept(&DropQuery {
193                    source: DragSource::Foreign { payload },
194                    target: target_key,
195                    position,
196                })
197            }),
198            accept_drop_fn: Rc::new(move |payload, target_index, position, view_id| {
199                let Some(target_key) = s3.key_at(target_index) else {
200                    return false;
201                };
202                if let Some(rd) = payload.get_typed::<RowDragData<T>>()
203                    && rd.source == view_id
204                {
205                    // Consume the drag-start stash (a construction path that
206                    // forgot to stash then fails loudly on its next drop
207                    // instead of silently reusing a previous drag's keys).
208                    let taken = keys_ad.borrow_mut().take();
209                    let Some(keys) = taken.filter(|k| !k.is_empty()) else {
210                        debug_assert!(false, "same-view drop without a drag-start key stash");
211                        return false;
212                    };
213                    if keys.contains(&target_key) {
214                        return false;
215                    }
216                    // `reorder_within` drops descendants-of-selected and keeps
217                    // the remaining nodes contiguous (single- or multi-row).
218                    return s3.reorder_within(&keys, &target_key, position);
219                }
220                s3.accept_drop(DropCommit {
221                    source: DragSource::Foreign { payload },
222                    target: target_key,
223                    position,
224                })
225            }),
226            snapshot_out_fn: Rc::new(move |indices: &[usize]| {
227                // Resolves stable keys NOW: they feed both the same-view accept
228                // path (via the drag-key stash) and the returned removal thunk.
229                let mut pairs: Vec<(usize, S::Key)> = indices
230                    .iter()
231                    .filter_map(|&i| s4.key_at(i).map(|k| (i, k)))
232                    .collect();
233                *keys_snap.borrow_mut() = Some(pairs.iter().map(|(_, k)| k.clone()).collect());
234                pairs.sort_by_key(|&(i, _)| std::cmp::Reverse(i));
235                let s = s4.clone();
236                Box::new(move || {
237                    for (_, k) in &pairs {
238                        s.on_drag_out(k);
239                    }
240                }) as Box<dyn Fn()>
241            }),
242            stash_drag_keys_fn: Rc::new(move |indices: &[usize]| {
243                *keys_stash.borrow_mut() =
244                    Some(indices.iter().filter_map(|&i| s9.key_at(i)).collect());
245            }),
246            row_state_fn: Rc::new(move |index| s5.row_state(index)),
247            request_window_fn: Rc::new(move |range| s6.request_window(range)),
248            can_fetch_more_fn: Rc::new(move || s7.can_fetch_more()),
249            fetch_more_fn: Rc::new(move || s8.fetch_more()),
250        }
251    }
252}
253
254/// Erased tree backing consumed by `TreeView`. All accessors are keyed on the
255/// visible flat index; the `Key` type is captured at construction and never
256/// surfaces in `TreeView<T>`.
257pub(crate) struct TreeSource<T: 'static> {
258    visible_count_fn: Rc<dyn Fn() -> usize>,
259    /// Build a widget for the row at `index`: hands the builder `(&T, &TreeRowMeta)`.
260    /// `None` when the index is out of range OR its data is still `Loading`.
261    with_row_fn:
262        Rc<dyn Fn(usize, &dyn Fn(&T, &TreeRowMeta) -> Box<dyn Widget>) -> Option<Box<dyn Widget>>>,
263    /// String-returning sibling of [`with_row_fn`](Self::with_row_fn) — reads
264    /// an arbitrary `String` from a resident row's item, for type-ahead label
265    /// extraction. `None` when out of range or still loading.
266    with_row_str_fn: Rc<dyn Fn(usize, &dyn Fn(&T) -> String) -> Option<String>>,
267    /// Read `&T` from the resident row at `index` via a side-effecting
268    /// callback, returning whether it ran. Powers export item-cloning
269    /// (`.exportable(..)`) without the delegate's widget-building path.
270    pub(crate) read_item_fn: Rc<dyn Fn(usize, &mut dyn FnMut(&T)) -> bool>,
271    /// Flat metadata for `index` without building a widget (a11y, keyboard).
272    meta_fn: Rc<dyn Fn(usize) -> Option<TreeRowMeta>>,
273    /// Expand (`true`) / collapse (`false`) the row at `index` (index → key).
274    set_expanded_at_fn: Rc<dyn Fn(usize, bool)>,
275    /// Whether the row at `index` is expanded.
276    is_expanded_at_fn: Rc<dyn Fn(usize) -> bool>,
277    /// The visible flat index of the row's parent, if visible (ArrowLeft-to-parent).
278    parent_index_fn: Rc<dyn Fn(usize) -> Option<usize>>,
279    /// `(pos_in_set_1based, set_size)` among the row's siblings (a11y).
280    sibling_pos_fn: Rc<dyn Fn(usize) -> (usize, usize)>,
281    /// Non-drag sibling reorder: `(index, move) -> new flat index` (or `None`
282    /// if the move is unavailable or the source rejected it). Commits through
283    /// the source's own `accept_drop`, so it is the row drag's own path; the
284    /// key-typed sibling logic stays internal.
285    sibling_move_fn: Rc<dyn Fn(usize, crate::common::ordered_move::OrderedMove) -> Option<usize>>,
286    /// Non-drag reparent: `(index, indent | outdent) -> new flat index`. The
287    /// same `DropPosition::Into` / sibling-of-parent drop a drag makes; `None`
288    /// when there is no previous sibling to enter or no parent to leave.
289    reparent_fn: Rc<dyn Fn(usize, crate::common::ordered_move::TreeMove) -> Option<usize>>,
290    /// Resolve `index` to a [`RowAnchor`](crate::data_views::RowAnchor) that
291    /// survives row movement. Captures the source's key at build time; the key
292    /// stays inside the closure, so `TreeSource<T>` remains key-agnostic.
293    anchor_fn: Rc<dyn Fn(usize) -> crate::data_views::RowAnchor>,
294    version_fn: Rc<dyn Fn() -> Signal<u64>>,
295    first_changed_fn: Rc<dyn Fn() -> Option<usize>>,
296    pub(crate) dnd: TreeDndLazy,
297}
298
299impl<T: 'static> TreeSource<T> {
300    /// Erase any concrete [`TreeDataSource`]. The built-in path passes a
301    /// `Rc<TreeSlice<T>>`; an external source passes its own `Rc<S>`.
302    pub(crate) fn from_data_source<S: TreeDataSource<Item = T> + 'static>(s: Rc<S>) -> Self {
303        let dnd = TreeDndLazy::from_source(s.clone());
304        // Shared by `sibling_pos_fn`, `sibling_move_fn` and `reparent_fn` below — all
305        // need "all visible roots, in order" and a version bump invalidates
306        // them alike, so one scan per version serves every caller.
307        let root_cache: Rc<RootIndexCache> = Rc::new(RefCell::new(None));
308        let (root_cache_sib, root_cache_kbd, root_cache_reparent) =
309            (root_cache.clone(), root_cache.clone(), root_cache);
310        let (s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12, s13, s14) = (
311            s.clone(),
312            s.clone(),
313            s.clone(),
314            s.clone(),
315            s.clone(),
316            s.clone(),
317            s.clone(),
318            s.clone(),
319            s.clone(),
320            s.clone(),
321            s.clone(),
322            s.clone(),
323            s.clone(),
324            s,
325        );
326        Self {
327            visible_count_fn: Rc::new(move || s1.visible_count()),
328            anchor_fn: Rc::new(move |index| match s13.key_at(index) {
329                Some(key) => {
330                    let src = s13.clone();
331                    crate::data_views::RowAnchor::new(Rc::new(move || {
332                        // Fast path: the captured slot still holds this row.
333                        if src.key_at(index).as_ref() == Some(&key) {
334                            return Some(index);
335                        }
336                        src.flat_index_of(&key)
337                    }))
338                }
339                None => crate::data_views::RowAnchor::fixed(index),
340            }),
341            with_row_fn: Rc::new(move |index, build| {
342                s2.with_entry(index, |item, entry| {
343                    let meta = TreeRowMeta {
344                        depth: entry.depth,
345                        has_children: entry.has_children,
346                        is_expanded: entry.is_expanded,
347                    };
348                    build(item, &meta)
349                })
350            }),
351            with_row_str_fn: Rc::new(move |index, f| s11.with_entry(index, |item, _entry| f(item))),
352            read_item_fn: Rc::new(move |index, f| {
353                s12.with_entry(index, |item, _entry| f(item)).is_some()
354            }),
355            meta_fn: Rc::new(move |index| {
356                s3.with_entry(index, |_item, entry| TreeRowMeta {
357                    depth: entry.depth,
358                    has_children: entry.has_children,
359                    is_expanded: entry.is_expanded,
360                })
361            }),
362            set_expanded_at_fn: Rc::new(move |index, expanded| {
363                if let Some(k) = s4.key_at(index) {
364                    s4.set_expanded(&k, expanded);
365                }
366            }),
367            is_expanded_at_fn: Rc::new(move |index| {
368                s5.key_at(index)
369                    .map(|k| s5.is_expanded(&k))
370                    .unwrap_or(false)
371            }),
372            parent_index_fn: Rc::new(move |index| {
373                let k = s6.key_at(index)?;
374                let p = s6.parent(&k)?;
375                s6.flat_index_of(&p)
376            }),
377            sibling_pos_fn: Rc::new(move |index| {
378                let Some(k) = s7.key_at(index) else {
379                    return (1, 1);
380                };
381                match s7.parent(&k) {
382                    Some(p) => {
383                        let sibs = s7.child_keys(&p);
384                        let pos = sibs.iter().position(|x| *x == k).unwrap_or(0) + 1;
385                        (pos, sibs.len().max(1))
386                    }
387                    None => {
388                        // Roots are always visible (depth 0). The cached scan
389                        // (one per source version) avoids re-deriving "all
390                        // visible roots" for every realized root row.
391                        let roots = root_indices(&*s7, &root_cache_sib);
392                        let pos = roots.binary_search(&index).map(|p| p + 1).unwrap_or(1);
393                        (pos, roots.len().max(1))
394                    }
395                }
396            }),
397            sibling_move_fn: Rc::new(move |index, mv| {
398                let k = s10.key_at(index)?;
399                // Ordered sibling keys at `k`'s level. Roots are always visible
400                // (depth 0 never collapses out), so the root list is the visible
401                // depth-0 scan — no root-enumeration method needed on the trait.
402                let siblings: Vec<S::Key> = match s10.parent(&k) {
403                    Some(p) => s10.child_keys(&p),
404                    None => root_indices(&*s10, &root_cache_kbd)
405                        .iter()
406                        .filter_map(|&j| s10.key_at(j))
407                        .collect(),
408                };
409                let pos = siblings.iter().position(|x| *x == k)?;
410                // The gap a drag would have released over, computed once for all
411                // four moves — see `common::ordered_move::OrderedMove::as_row_drop`,
412                // whose target is a *position among these siblings* here rather
413                // than a flat index.
414                let drop = mv.as_row_drop(pos, siblings.len())?;
415                let target = siblings[drop.target].clone();
416                let applied = s10.accept_drop(DropCommit {
417                    source: DragSource::SameView { key: k.clone() },
418                    target,
419                    position: drop.position,
420                });
421                if applied { s10.flat_index_of(&k) } else { None }
422            }),
423            reparent_fn: Rc::new(move |index, mv| {
424                use crate::common::ordered_move::TreeMove;
425                let k = s14.key_at(index)?;
426                let (target, position) = match mv {
427                    TreeMove::Indent => {
428                        // The previous sibling becomes the new parent. Roots are
429                        // the visible depth-0 scan, exactly as above.
430                        let siblings: Vec<S::Key> = match s14.parent(&k) {
431                            Some(p) => s14.child_keys(&p),
432                            None => root_indices(&*s14, &root_cache_reparent)
433                                .iter()
434                                .filter_map(|&j| s14.key_at(j))
435                                .collect(),
436                        };
437                        let pos = siblings.iter().position(|x| *x == k)?;
438                        let target = siblings[pos.checked_sub(1)?].clone();
439                        // Reveal the new parent, or the indented row lands
440                        // inside something collapsed and vanishes — the caller
441                        // could then neither follow it nor say where it went. A
442                        // drag reaches the same state by spring-loading the row
443                        // it hovers.
444                        s14.set_expanded(&target, true);
445                        (target, DropPosition::Into)
446                    }
447                    // Land after the parent, at the parent's own level — which is
448                    // what makes outdent the exact inverse of indent.
449                    TreeMove::Outdent => (s14.parent(&k)?, DropPosition::After),
450                };
451                let applied = s14.accept_drop(DropCommit {
452                    source: DragSource::SameView { key: k.clone() },
453                    target,
454                    position,
455                });
456                if applied { s14.flat_index_of(&k) } else { None }
457            }),
458            version_fn: Rc::new(move || s8.version_signal()),
459            first_changed_fn: Rc::new(move || s9.first_changed_index()),
460            dnd,
461        }
462    }
463
464    /// A movement-proof handle to the row at `index`.
465    pub(crate) fn anchor(&self, index: usize) -> crate::data_views::RowAnchor {
466        (self.anchor_fn)(index)
467    }
468
469    pub(crate) fn visible_count(&self) -> usize {
470        (self.visible_count_fn)()
471    }
472
473    pub(crate) fn with_row(
474        &self,
475        index: usize,
476        build: &dyn Fn(&T, &TreeRowMeta) -> Box<dyn Widget>,
477    ) -> Option<Box<dyn Widget>> {
478        (self.with_row_fn)(index, build)
479    }
480
481    pub(crate) fn meta(&self, index: usize) -> Option<TreeRowMeta> {
482        (self.meta_fn)(index)
483    }
484
485    /// Read a `String` from the resident row at `index` (type-ahead label).
486    pub(crate) fn with_row_str(&self, index: usize, f: &dyn Fn(&T) -> String) -> Option<String> {
487        (self.with_row_str_fn)(index, f)
488    }
489
490    /// The tree depth of the visible row at `index`, or `0` when the row is
491    /// out of range or its data is still `Loading`. Drives the drop
492    /// affordance's indent — a missing row reads as root level rather than
493    /// shifting the indicator somewhere arbitrary.
494    pub(crate) fn depth(&self, index: usize) -> usize {
495        self.meta(index).map(|m| m.depth).unwrap_or(0)
496    }
497
498    pub(crate) fn set_expanded_at(&self, index: usize, expanded: bool) {
499        (self.set_expanded_at_fn)(index, expanded)
500    }
501
502    pub(crate) fn is_expanded_at(&self, index: usize) -> bool {
503        (self.is_expanded_at_fn)(index)
504    }
505
506    pub(crate) fn toggle_at(&self, index: usize) {
507        let expanded = (self.is_expanded_at_fn)(index);
508        (self.set_expanded_at_fn)(index, !expanded);
509    }
510
511    pub(crate) fn parent_index(&self, index: usize) -> Option<usize> {
512        (self.parent_index_fn)(index)
513    }
514
515    pub(crate) fn sibling_pos(&self, index: usize) -> (usize, usize) {
516        (self.sibling_pos_fn)(index)
517    }
518
519    /// Move the row at `index` among its siblings as `mv` says, routed through
520    /// the source's own `accept_drop`. Returns the moved row's new flat index,
521    /// or `None` at an edge / if rejected.
522    pub(crate) fn sibling_move(
523        &self,
524        index: usize,
525        mv: crate::common::ordered_move::OrderedMove,
526    ) -> Option<usize> {
527        (self.sibling_move_fn)(index, mv)
528    }
529
530    /// Indent or outdent the row at `index`, returning its new flat index.
531    pub(crate) fn reparent(
532        &self,
533        index: usize,
534        mv: crate::common::ordered_move::TreeMove,
535    ) -> Option<usize> {
536        (self.reparent_fn)(index, mv)
537    }
538
539    /// `(pos_in_set_1based, set_size)` among the row's siblings — which moves
540    /// are available reads off this.
541    pub(crate) fn sibling_position(&self, index: usize) -> (usize, usize) {
542        (self.sibling_pos_fn)(index)
543    }
544
545    pub(crate) fn version_signal(&self) -> Signal<u64> {
546        (self.version_fn)()
547    }
548
549    pub(crate) fn first_changed_index(&self) -> Option<usize> {
550        (self.first_changed_fn)()
551    }
552
553    /// Build a per-row [`TreeRow`] context (key-erased toggle) for the
554    /// `from_source` delegate.
555    pub(crate) fn row_context(self_rc: &Rc<TreeSource<T>>, index: usize) -> TreeRow {
556        let meta = self_rc.meta(index).unwrap_or(TreeRowMeta {
557            depth: 0,
558            has_children: false,
559            is_expanded: false,
560        });
561        let src = self_rc.clone();
562        // Anchored, not index-captured: the chevron keeps toggling ITS row even
563        // if rows above it appear or vanish before the click lands, and no-ops
564        // if the row is gone rather than toggling whoever took its place.
565        let anchor = self_rc.anchor(index);
566        TreeRow {
567            depth: meta.depth,
568            has_children: meta.has_children,
569            is_expanded: meta.is_expanded,
570            toggle: Rc::new(move |_ctx| {
571                if let Some(i) = anchor.index() {
572                    src.toggle_at(i);
573                }
574            }),
575        }
576    }
577}
578
579#[cfg(test)]
580mod drag_identity_tests {
581    use super::*;
582    use std::cell::RefCell;
583    use teksilo_data::{TreeDataSlice, TreeRow};
584
585    use crate::data_views::{RowDragData, ViewId, ViewKind};
586
587    fn slice_of(keys: &[u64]) -> TreeDataSlice<u64, u64> {
588        let slice = TreeDataSlice::<u64, u64>::new();
589        let owned: Vec<u64> = keys.to_vec();
590        slice.set_source(move || owned.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
591        slice.reload();
592        slice
593    }
594
595    fn reshape(slice: &TreeDataSlice<u64, u64>, keys: &[u64]) {
596        let owned: Vec<u64> = keys.to_vec();
597        slice.set_source(move || owned.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
598        slice.reload();
599    }
600
601    fn same_view_payload(view_id: ViewId, rows: Vec<usize>) -> DragPayload {
602        DragPayload::typed(RowDragData::<u64> {
603            source: view_id,
604            rows,
605            items: None,
606        })
607    }
608
609    #[test]
610    fn a_reorder_moves_the_node_dragged_not_the_slot_it_left() {
611        // Node 30 is grabbed at flat index 2, then the tree reflows mid-drag —
612        // the exact shape a spring-load auto-expand produces, since the dwell
613        // that expands a collapsed branch happens during the very drag. The
614        // drop must move node 30, not whichever node now sits at index 2.
615        let slice = slice_of(&[10, 20, 30]);
616        let recorded: Rc<RefCell<Vec<(u64, u64, DropPosition)>>> =
617            Rc::new(RefCell::new(Vec::new()));
618        let rec = recorded.clone();
619        slice.set_reorder(move |dragged, target, pos| {
620            rec.borrow_mut().push((dragged, target, pos));
621            true
622        });
623        let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
624        let vid = ViewId::next(ViewKind::Tree);
625
626        let _thunk = (src.dnd.snapshot_out_fn)(&[2]); // drag-start on node 30
627        let payload = same_view_payload(vid, vec![2]);
628
629        reshape(&slice, &[1, 2, 10, 20, 30]); // rows appear above mid-drag
630
631        assert_eq!(
632            (src.dnd.can_accept_fn)(&payload, 0, DropPosition::Before, vid),
633            DropResponse::Accept
634        );
635        assert!((src.dnd.accept_drop_fn)(
636            &payload,
637            0,
638            DropPosition::Before,
639            vid
640        ));
641        assert_eq!(
642            recorded.borrow().as_slice(),
643            &[(30, 1, DropPosition::Before)],
644            "the dragged node's key must move, not whichever node slid into its old index"
645        );
646    }
647
648    #[test]
649    fn a_reflowed_own_node_still_rejects_a_drop_onto_itself() {
650        // After the mid-drag reflow the dragged node sits at a NEW flat index;
651        // the own-row rejection must follow it there.
652        let slice = slice_of(&[10, 20, 30]);
653        slice.set_reorder(|_, _, _| true);
654        let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
655        let vid = ViewId::next(ViewKind::Tree);
656
657        let _thunk = (src.dnd.snapshot_out_fn)(&[2]); // node 30
658        let payload = same_view_payload(vid, vec![2]);
659
660        reshape(&slice, &[1, 2, 10, 20, 30]); // node 30 now at index 4
661
662        assert_eq!(
663            (src.dnd.can_accept_fn)(&payload, 4, DropPosition::Before, vid),
664            DropResponse::Reject
665        );
666        assert!(!(src.dnd.accept_drop_fn)(
667            &payload,
668            4,
669            DropPosition::Before,
670            vid
671        ));
672    }
673}
674
675#[cfg(test)]
676mod anchor_tests {
677    use super::*;
678    use teksilo_data::{TreeDataSlice, TreeRow};
679
680    fn slice_of(keys: &[u64]) -> TreeDataSlice<u64, u64> {
681        let slice = TreeDataSlice::<u64, u64>::new();
682        let owned: Vec<u64> = keys.to_vec();
683        slice.set_source(move || owned.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
684        slice.reload();
685        slice
686    }
687
688    #[test]
689    fn an_anchor_follows_its_row_when_rows_shift_above_it() {
690        // Row 30 starts at index 2. After two rows are inserted above it, a
691        // captured index would point at a different row entirely; the anchor
692        // resolves to 30's new position.
693        let slice = slice_of(&[10, 20, 30]);
694        let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
695        let anchor = src.anchor(2);
696        assert_eq!(anchor.index(), Some(2));
697
698        let shifted: Vec<u64> = vec![1, 2, 10, 20, 30];
699        slice.set_source(move || shifted.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
700        slice.reload();
701
702        assert_eq!(
703            anchor.index(),
704            Some(4),
705            "the anchor must track row 30 to its new index, not stay at 2"
706        );
707    }
708
709    #[test]
710    fn an_anchor_reports_none_once_its_row_is_gone() {
711        // Deleting the row must make the handler a no-op, not redirect it onto
712        // whichever row slid into the vacated slot.
713        let slice = slice_of(&[10, 20, 30]);
714        let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
715        let anchor = src.anchor(1); // row 20
716
717        let remaining: Vec<u64> = vec![10, 30];
718        slice.set_source(move || remaining.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
719        slice.reload();
720
721        assert_eq!(anchor.index(), None, "row 20 is gone");
722        assert!(!anchor.is_live());
723    }
724
725    #[test]
726    fn a_keyless_source_degrades_to_a_fixed_anchor() {
727        // No identity available: the anchor is no worse than capturing the
728        // index, and must not pretend the row vanished.
729        let anchor = crate::data_views::RowAnchor::fixed(7);
730        assert_eq!(anchor.index(), Some(7));
731        assert!(anchor.is_live());
732    }
733
734    #[test]
735    fn an_editing_reconcile_converges_in_one_pass() {
736        // `reconcile_editing_row` writes `editing_cell` from inside a pane's
737        // build. That is safe only because it settles: once the row index has
738        // been corrected, a second pass must write nothing. Pin that, so the
739        // write-during-build never becomes a rebuild loop.
740        use std::cell::RefCell;
741        use teksilo_core::signal::Signal;
742
743        let slice = slice_of(&[10, 20, 30]);
744        let src = Rc::new(TreeSource::from_data_source(Rc::new(slice.clone())));
745        let editing: Signal<Option<(usize, usize)>> = Signal::new(Some((2, 0)));
746        let slot = Rc::new(RefCell::new(None));
747        let anchor_of = |i: usize| src.anchor(i);
748
749        // Pass 1 captures the anchor for row 30.
750        crate::data_views::reconcile_editing_row(&editing, &slot, &anchor_of);
751        assert_eq!(editing.get(), Some((2, 0)));
752
753        // Row 30 moves to index 4.
754        let shifted: Vec<u64> = vec![1, 2, 10, 20, 30];
755        slice.set_source(move || shifted.iter().map(|k| TreeRow::new(*k, *k, 0)).collect());
756        slice.reload();
757
758        crate::data_views::reconcile_editing_row(&editing, &slot, &anchor_of);
759        assert_eq!(editing.get(), Some((4, 0)), "corrected once");
760
761        // The settling pass must be a no-op.
762        let before = editing.get();
763        crate::data_views::reconcile_editing_row(&editing, &slot, &anchor_of);
764        assert_eq!(editing.get(), before, "second pass must write nothing");
765    }
766}