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}