Skip to main content

teksilo_data/
tree_data_source.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! `TreeDataSource` — read-and-command interface for hierarchical data behind a
5//! `TreeView` / `TreeTableView`.
6//!
7//! `TreeDataSource` is to trees what [`ListDataSource`](crate::ListDataSource)
8//! is to flat lists: a projected, per-view, flattened read API plus the
9//! capability protocol for identity, DnD validation, and lazy loading.
10//! The built-in [`TreeSlice`](crate::TreeSlice) and
11//! [`SortFilterTreeModel`](crate::SortFilterTreeModel) implement it over an
12//! in-memory [`TreeModel`]; an external source of truth
13//! (e.g. a Qleany entity store) implements it directly with its own `Key` type
14//! and so never needs to mirror itself into a `TreeModel`.
15//!
16//! ## When to use
17//!
18//! Implement `TreeDataSource` directly when your data already lives outside an
19//! in-memory tree (a database, a virtual filesystem, a remote store) and you
20//! do not want to mirror it into a `TreeModel`. Use [`TreeSlice`](crate::TreeSlice)
21//! when you have a `TreeModel<T>` and want per-view expand state.
22//!
23//! ## Example
24//!
25//! ```ignore
26//! use teksilo_data::{TreeDataSource, FlatEntry, NodeId};
27//! use teksilo_data::dnd_types::{DragEligibility, DropQuery, DropResponse, DropCommit, RowState};
28//! use teksilo_core::signal::Signal;
29//!
30//! struct MySource { version: Signal<u64> }
31//!
32//! impl TreeDataSource for MySource {
33//!     type Item = String;
34//!     type Key = NodeId;
35//!
36//!     fn visible_count(&self) -> usize { 0 }
37//!     fn with_entry<R>(&self, _i: usize, _f: impl FnOnce(&String, &FlatEntry<NodeId>) -> R) -> Option<R> { None }
38//!     fn key_at(&self, _i: usize) -> Option<NodeId> { None }
39//!     fn flat_index_of(&self, _k: &NodeId) -> Option<usize> { None }
40//!     fn parent(&self, _k: &NodeId) -> Option<NodeId> { None }
41//!     fn child_keys(&self, _k: &NodeId) -> Vec<NodeId> { vec![] }
42//!     fn version_signal(&self) -> Signal<u64> { self.version.clone() }
43//!     fn is_expanded(&self, _k: &NodeId) -> bool { false }
44//!     fn set_expanded(&self, _k: &NodeId, _expanded: bool) {}
45//! }
46//! ```
47
48use teksilo_core::signal::Signal;
49
50use crate::dnd_types::ItemKey;
51use crate::dnd_types::{
52    DragEligibility, DragSource, DropCommit, DropPosition, DropQuery, DropResponse, RowState,
53};
54use crate::tree_change::NodeId;
55use crate::tree_model::TreeModel;
56
57/// A single entry in a tree's flattened, currently-visible row list.
58///
59/// Generic over the key type so external sources carry their own identity
60/// (`K = NodeId` for `TreeModel`-backed sources, `K = i64` for an entity-id
61/// store, …). The default `K = NodeId` keeps every in-tree `FlatEntry` mention
62/// and `entry.node_id` read compiling unchanged.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct FlatEntry<K: ItemKey = NodeId> {
65    /// The row's stable key in its source.
66    pub node_id: K,
67    /// Depth in the tree (0 for roots).
68    pub depth: usize,
69    /// Whether this row has children in the source.
70    pub has_children: bool,
71    /// Whether this row is currently expanded (children visible).
72    pub is_expanded: bool,
73}
74
75/// A per-view flattened, projectable view over hierarchical data.
76///
77/// Not object-safe (associated types + `impl FnOnce`); views consume it
78/// generically and erase it into a closure bundle, exactly as `ListView` does
79/// with `ListDataSource`. The DnD (`drag`/`can_accept`/`accept_drop`/
80/// `on_drag_out`) and lazy (`row_state`/`request_window`/`can_fetch_more`/
81/// `fetch_more`) methods default to inert/fully-resident, so a read-only source
82/// implements only the core read + nav surface.
83pub trait TreeDataSource: 'static {
84    /// The item type stored at each node.
85    type Item: 'static;
86    /// The stable per-node identity (`NodeId` for in-memory trees, an entity id
87    /// for an external store).
88    type Key: ItemKey;
89
90    // ── Core read ─────────────────────────────────────────────────────────
91    /// Number of currently-visible (flattened) rows.
92    fn visible_count(&self) -> usize;
93    /// Access the item + flat metadata at a visible index via callback.
94    fn with_entry<R>(
95        &self,
96        flat_index: usize,
97        f: impl FnOnce(&Self::Item, &FlatEntry<Self::Key>) -> R,
98    ) -> Option<R>;
99    /// The key of the row at a visible index.
100    fn key_at(&self, flat_index: usize) -> Option<Self::Key>;
101    /// The visible index of a key, if currently visible.
102    fn flat_index_of(&self, key: &Self::Key) -> Option<usize>;
103    /// The parent of a node (`None` for a root) — drives sibling nav + the
104    /// drop cycle-guard.
105    fn parent(&self, key: &Self::Key) -> Option<Self::Key>;
106    /// The children of a node, in order.
107    fn child_keys(&self, key: &Self::Key) -> Vec<Self::Key>;
108    /// A version signal that bumps on every structural/projection change — the
109    /// view binds it at `BindingLevel::Rebuild`.
110    fn version_signal(&self) -> Signal<u64>;
111
112    // ── Expand / collapse (per-view) ──────────────────────────────────────
113    /// Whether the node is expanded.
114    fn is_expanded(&self, key: &Self::Key) -> bool;
115    /// Expand (`true`) or collapse (`false`) the node.
116    fn set_expanded(&self, key: &Self::Key, expanded: bool);
117
118    /// First visible index whose content may differ after the latest change —
119    /// rows `0..index` are unchanged, so per-row derived state (e.g. a measured
120    /// height) remains valid. `None` means unknown (treat as a full change).
121    fn first_changed_index(&self) -> Option<usize> {
122        None
123    }
124
125    /// Whether `key` still exists in the source, **independent of visibility** —
126    /// a node hidden under a collapsed ancestor (or scrolled out of a lazy
127    /// window) still exists. Drives keyed-selection pruning, so that a
128    /// collapsed-but-present node keeps its selection and only a *deleted* node
129    /// is dropped. Default: visible-only (`flat_index_of(key).is_some()`);
130    /// sources whose nodes persist while collapsed/scrolled out should override
131    /// this to consult their full store.
132    fn contains_key(&self, key: &Self::Key) -> bool {
133        self.flat_index_of(key).is_some()
134    }
135
136    // ── DnD (default: inert) ──────────────────────────────────────────────
137    /// Whether the node may begin a drag (the transferable gate).
138    fn drag(&self, _key: &Self::Key) -> DragEligibility {
139        DragEligibility::NoDrag
140    }
141    /// Whether a hovered drop is permitted (and where) — the pre-commit verdict.
142    fn can_accept(&self, _query: &DropQuery<'_, Self::Key>) -> DropResponse {
143        DropResponse::Reject
144    }
145    /// Apply a committed drop. Returns whether it was applied.
146    fn accept_drop(&self, _commit: DropCommit<'_, Self::Key>) -> bool {
147        false
148    }
149    /// Reorder a whole set of this source's OWN nodes so they land contiguously
150    /// at a drop gap — the multi-row same-view reorder commit. `sources` are the
151    /// dragged nodes' keys in visible order; `target` / `position` name the drop
152    /// gap. Returns whether anything moved.
153    ///
154    /// The default first drops any `sources` node that is a **descendant of
155    /// another** `sources` node (moving an ancestor already carries its
156    /// subtree), then moves the remaining top-level nodes one at a time,
157    /// re-anchoring each after the previous. Tree keys are stable, so the
158    /// re-anchoring is correct without index bookkeeping.
159    fn reorder_within(
160        &self,
161        sources: &[Self::Key],
162        target: &Self::Key,
163        position: DropPosition,
164    ) -> bool {
165        // Dropping INTO one of the dragged subtrees (target is a dragged node
166        // or a descendant of one) is invalid for the whole gesture — reject
167        // rather than partially apply, matching what the hover verdict shows.
168        let mut t = Some(target.clone());
169        while let Some(node) = t {
170            if sources.iter().any(|s| s == &node) {
171                return false;
172            }
173            t = self.parent(&node);
174        }
175        // Keep only nodes that are not a descendant of another selected node.
176        let top: Vec<Self::Key> = sources
177            .iter()
178            .filter(|k| {
179                let mut p = self.parent(k);
180                while let Some(ancestor) = p {
181                    if sources.iter().any(|s| s == &ancestor) {
182                        return false;
183                    }
184                    p = self.parent(&ancestor);
185                }
186                true
187            })
188            .cloned()
189            .collect();
190        let mut anchor = target.clone();
191        let mut pos = position;
192        let mut moved = false;
193        for key in &top {
194            if key == &anchor {
195                continue;
196            }
197            if self.accept_drop(DropCommit {
198                source: DragSource::SameView { key: key.clone() },
199                target: anchor.clone(),
200                position: pos,
201            }) {
202                moved = true;
203                anchor = key.clone();
204                pos = DropPosition::After;
205            }
206        }
207        moved
208    }
209    /// Called on the *origin* source after one of its rows was accepted by a
210    /// different view (source-side completion). Sources backed by a shared /
211    /// command model no-op this; independent models use it to drop the moved
212    /// row.
213    fn on_drag_out(&self, _key: &Self::Key) {}
214
215    // ── Lazy (default: fully resident) ────────────────────────────────────
216    /// Whether the row at a visible index is loaded.
217    fn row_state(&self, _flat_index: usize) -> RowState {
218        RowState::Ready
219    }
220    /// Nudge the source to load the given visible range (the view calls this
221    /// each build with its visible + buffer window).
222    fn request_window(&self, _range: std::ops::Range<usize>) {}
223    /// Whether more rows can be appended (infinite scroll).
224    fn can_fetch_more(&self) -> bool {
225        false
226    }
227    /// Fetch the next page (append-only growth).
228    fn fetch_more(&self) {}
229}
230
231/// Whether `node` is `ancestor` or one of its descendants — the move cycle
232/// guard (you cannot drop a node into its own subtree).
233pub fn tree_is_desc_or_self<T: 'static>(
234    tree: &TreeModel<T>,
235    node: NodeId,
236    ancestor: NodeId,
237) -> bool {
238    let mut cur = Some(node);
239    while let Some(n) = cur {
240        if n == ancestor {
241            return true;
242        }
243        cur = tree.parent(n);
244    }
245    false
246}
247
248/// Apply a tree reorder by `NodeId`, with the cycle guard and the
249/// remove-then-insert index adjustment `TreeModel::move_node` requires. Shared
250/// by the `TreeSlice` / `SortFilterTreeModel` `accept_drop` impls. Returns
251/// whether the move was applied (false = rejected, e.g. cycle or self-drop).
252pub fn tree_apply_reorder<T: 'static>(
253    tree: &TreeModel<T>,
254    source: NodeId,
255    target: NodeId,
256    position: DropPosition,
257) -> bool {
258    if source == target {
259        return false;
260    }
261    // Reject dropping a node anywhere inside its own subtree (covers Into a
262    // descendant and reorder relative to a descendant).
263    if tree_is_desc_or_self(tree, target, source) {
264        return false;
265    }
266    match position {
267        DropPosition::Into => {
268            // Append as last child. move_node removes `source` first, so if it
269            // was already a child of `target` the post-removal length is one
270            // smaller.
271            let mut idx = tree.child_count(target);
272            if tree.parent(source) == Some(target) {
273                idx -= 1;
274            }
275            tree.move_node(source, target, idx);
276            true
277        }
278        DropPosition::Before | DropPosition::After => {
279            let new_parent = tree.parent(target);
280            let siblings: Vec<NodeId> = match new_parent {
281                Some(p) => tree.children(p),
282                None => (0..tree.root_count()).map(|i| tree.root(i)).collect(),
283            };
284            // `target` must be among its own parent's children. If it isn't,
285            // the tree is inconsistent — reject the drop rather than silently
286            // falling back to index 0, which would reorder the node to the
287            // start and corrupt the sibling order.
288            let Some(pos) = siblings.iter().position(|&s| s == target) else {
289                return false;
290            };
291            let mut idx = if position == DropPosition::After {
292                pos + 1
293            } else {
294                pos
295            };
296            // Same-parent removal shift: move_node removes `source` before
297            // inserting, so an insertion point above the source's old slot
298            // shifts down by one.
299            if tree.parent(source) == new_parent
300                && let Some(sp) = siblings.iter().position(|&s| s == source)
301                && sp < idx
302            {
303                idx -= 1;
304            }
305            match new_parent {
306                Some(p) => tree.move_node(source, p, idx),
307                None => tree.move_to_root(source, idx),
308            }
309            true
310        }
311    }
312}