Skip to main content

teksilo_data/
list_data_source.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! `ListDataSource` — read-and-command interface for a flat collection behind a `ListView` /
5//! `TableView`.
6//!
7//! `ListDataSource` is the flat-list peer of
8//! [`TreeDataSource`](crate::TreeDataSource): a positional read API plus the
9//! capability protocol (identity, DnD validation, lazy loading). It is the
10//! input every flat data view reads through. The built-in [`ListModel<T>`](crate::ListModel) and
11//! [`SortFilterListModel<T>`](crate::SortFilterListModel) implement it; an external/huge source
12//! (a paged database cursor, a 1M-row windowed feed) implements it directly and owns its
13//! own paging behind `row_state`/`request_window`/`fetch_more`.
14//!
15//! Not object-safe (associated types + generic `with_item`); `ListView`
16//! consumes it generically via `ListView::from_source` and erases it into a
17//! closure bundle. The DnD and lazy methods default to inert / fully-resident,
18//! so a read-only in-memory source implements only `len` + `with_item` +
19//! `observe_changes`.
20//!
21//! ## When to use
22//!
23//! Prefer [`ListModel<T>`](crate::ListModel) when your data fits in memory and you want
24//! automatic `DataChange` notifications with no extra work. Implement `ListDataSource`
25//! directly when the source is external, huge, or requires lazy window-based loading —
26//! the view calls `request_window` each build pass and `fetch_more` near the end.
27//!
28//! ```rust
29//! # use teksilo_data::{ListModel, ListDataSource};
30//! // ListModel<T> implements ListDataSource — pass it directly to any flat view.
31//! let model = ListModel::from_vec(vec!["alpha", "beta", "gamma"]);
32//! // Access via the ListDataSource interface:
33//! let _len = model.len();
34//! let _first = model.with_item(0, |s| *s);
35//! assert_eq!(_len, 3);
36//! assert_eq!(_first, Some("alpha"));
37//! ```
38
39use std::ops::Range;
40
41use teksilo_core::ObserverHandle;
42
43use crate::data_change::DataChange;
44use crate::dnd_types::{
45    DragEligibility, DragSource, DropCommit, DropPosition, DropQuery, DropResponse, ItemKey,
46    RowState,
47};
48
49/// A data source for a flat collection viewed by `ListView`, `TableView`, and `GridView`.
50///
51/// The trait separates the read interface (`len`, `with_item`) from the capability
52/// protocol: identity (`key_at`/`index_of`), drag-and-drop validation
53/// (`drag`/`can_accept`/`accept_drop`/`on_drag_out`), and lazy loading
54/// (`row_state`/`request_window`/`can_fetch_more`/`fetch_more`). All capability
55/// methods have inert defaults, so a minimal implementation only needs `len`,
56/// `with_item`, and `observe_changes`.
57pub trait ListDataSource: 'static {
58    /// The item type exposed by this data source.
59    type Item: 'static;
60    /// The stable per-row identity. In-memory `ListModel` uses `usize` (the
61    /// index); external sources use their own domain key so keyed selection /
62    /// DnD survive reorders without a mirror model.
63    type Key: ItemKey;
64
65    /// Number of rows (the **total**, including not-yet-loaded ones for a
66    /// windowed source — the scrollbar needs it).
67    fn len(&self) -> usize;
68
69    /// Whether the source is empty.
70    fn is_empty(&self) -> bool {
71        self.len() == 0
72    }
73
74    /// Access the item at `index` via a callback. Returns `None` for an
75    /// out-of-bounds index OR an in-bounds index whose data is still
76    /// `Loading` (see `row_state`).
77    fn with_item<R>(&self, index: usize, f: impl FnOnce(&Self::Item) -> R) -> Option<R>;
78
79    /// The stable key of the row at `index`. Default `None` (no identity);
80    /// sources that support keyed selection / DnD override it.
81    fn key_at(&self, _index: usize) -> Option<Self::Key> {
82        None
83    }
84
85    /// The index of a key, if currently present. Default `None`.
86    fn index_of(&self, _key: &Self::Key) -> Option<usize> {
87        None
88    }
89
90    /// Register an observer that is called on every mutation; dropping the
91    /// returned [`ObserverHandle`] unregisters the callback automatically.
92    fn observe_changes(&self, f: impl Fn(&DataChange) + 'static) -> ObserverHandle;
93
94    /// First index whose content may differ after the change just delivered —
95    /// rows `0..index` are unchanged. `None` means unknown (full change).
96    fn first_changed_index(&self) -> Option<usize> {
97        None
98    }
99
100    // ── DnD (default: inert) ──────────────────────────────────────────────
101    /// Whether the row may begin a drag (the transferable gate).
102    fn drag(&self, _key: &Self::Key) -> DragEligibility {
103        DragEligibility::NoDrag
104    }
105    /// Whether a hovered drop is permitted (and where) — the pre-commit verdict.
106    fn can_accept(&self, _query: &DropQuery<'_, Self::Key>) -> DropResponse {
107        DropResponse::Reject
108    }
109    /// Apply a committed drop. Returns whether it was applied.
110    fn accept_drop(&self, _commit: DropCommit<'_, Self::Key>) -> bool {
111        false
112    }
113    /// Reorder a whole set of this source's OWN rows so they land contiguously
114    /// at a drop gap — the multi-row same-view reorder commit. `sources` are the
115    /// dragged rows' keys in the origin's visible order; `target` / `position`
116    /// name the drop gap. Returns whether anything moved.
117    ///
118    /// The default moves them one at a time, re-anchoring each after the
119    /// previous so they stay contiguous and keep their relative order — correct
120    /// for a source with **stable** keys. [`ListModel`](crate::ListModel), whose
121    /// key *is* the index (so a single move renumbers everything), overrides
122    /// this with a direct block move; a single-row drag needs neither and just
123    /// falls through to one [`accept_drop`](Self::accept_drop).
124    fn reorder_within(
125        &self,
126        sources: &[Self::Key],
127        target: &Self::Key,
128        position: DropPosition,
129    ) -> bool {
130        let mut anchor = target.clone();
131        let mut pos = position;
132        let mut moved = false;
133        for key in sources {
134            if key == &anchor {
135                continue;
136            }
137            if self.accept_drop(DropCommit {
138                source: DragSource::SameView { key: key.clone() },
139                target: anchor.clone(),
140                position: pos,
141            }) {
142                moved = true;
143                anchor = key.clone();
144                pos = DropPosition::After;
145            }
146        }
147        moved
148    }
149    /// Called on the *origin* source after one of its rows was accepted by a
150    /// different view (source-side completion). Shared/command-backed sources
151    /// no-op this; independent models use it to drop the moved row.
152    fn on_drag_out(&self, _key: &Self::Key) {}
153
154    // ── Lazy (default: fully resident) ────────────────────────────────────
155    /// Whether the row at `index` is loaded.
156    fn row_state(&self, _index: usize) -> RowState {
157        RowState::Ready
158    }
159    /// Nudge the source to load the given range (the view calls this each build
160    /// with its visible + buffer window).
161    fn request_window(&self, _range: Range<usize>) {}
162    /// Whether more rows can be appended (infinite scroll).
163    fn can_fetch_more(&self) -> bool {
164        false
165    }
166    /// Fetch the next page (append-only growth).
167    fn fetch_more(&self) {}
168}