pub struct ListModel<T: 'static> { /* private fields */ }Expand description
A concrete reactive list that stores items in a Vec<T>.
ListModel<T> is Clone — cloning produces a second handle to the same
data. Multiple widgets can hold clones and all see the same items.
Every mutation method modifies the internal Vec, drops the mutable borrow,
then notifies observers. By the time any observer runs, the borrow is
released and shared borrows (len(), with_item()) are safe.
Implementations§
Source§impl<T: 'static> ListModel<T>
impl<T: 'static> ListModel<T>
Sourcepub fn with_item<R>(&self, index: usize, f: impl FnOnce(&T) -> R) -> Option<R>
pub fn with_item<R>(&self, index: usize, f: impl FnOnce(&T) -> R) -> Option<R>
Access an item by index via a callback. Returns None if out of bounds.
The callback pattern avoids returning a reference that would need to
outlive the RefCell borrow guard.
Sourcepub fn move_item(&self, from: usize, to: usize)
pub fn move_item(&self, from: usize, to: usize)
Move an item from one index to another.
The item at from is removed, then inserted at to (post-removal index).
§Panics
Panics if either index is out of bounds.
Sourcepub fn move_items(&self, indices: &[usize], insert_gap: usize) -> bool
pub fn move_items(&self, indices: &[usize], insert_gap: usize) -> bool
Move a set of items so they land contiguously at a drop gap,
preserving their relative order — the multi-row same-view reorder
commit. indices are the items’ current positions (any order;
out-of-range entries are ignored); insert_gap is the destination in
0..=len expressed in the pre-move indexing (i.e. “land before the item
currently at insert_gap”; len = at the end).
Returns whether anything moved (false if indices held no in-range
entry). A contiguous source block emits a single
DataChange::ItemsMoved — so index-based selection follows the moved
rows; a non-contiguous set emits DataChange::Reset (that permutation
is not expressible as one ItemsMoved, and selection is dropped). For a
single index prefer move_item.
Sourcepub fn replace_all(&self, items: Vec<T>)
pub fn replace_all(&self, items: Vec<T>)
Replace the entire list contents.
Sourcepub fn observe_changes(
&self,
f: impl Fn(&DataChange) + 'static,
) -> ObserverHandle
pub fn observe_changes( &self, f: impl Fn(&DataChange) + 'static, ) -> ObserverHandle
Register an observer that is called on every mutation.
Returns an ObserverHandle — dropping it removes the callback.
Source§impl<T: PartialEq + 'static> ListModel<T>
impl<T: PartialEq + 'static> ListModel<T>
Sourcepub fn reconcile_by_key<K: Eq + Hash>(
&self,
new_items: Vec<T>,
key_fn: impl Fn(&T) -> K,
)
pub fn reconcile_by_key<K: Eq + Hash>( &self, new_items: Vec<T>, key_fn: impl Fn(&T) -> K, )
Reconcile the list’s contents with new_items, matching old and new
rows by key (key_fn) instead of wholesale-replacing them, and
emitting the minimal set of granular DataChanges needed to reach
that state — never DataChange::Reset.
This is the primitive a live view needs when a peer process (or any
other out-of-band writer) reloads a backing file and the merged
result must land in a ListModel that a ListView is currently
displaying, without wiping the user’s selection or keyboard focus
mid-interaction. replace_all/clear always emit Reset, and a
Reset unconditionally clears a positional SelectionModel
(RowSelection::from_index) — reconcile_by_key is how a caller
avoids that.
§Change order
The changes are emitted in two stages, coalescing contiguous runs into a single event each:
- Removals first. One
DataChange::ItemsRemovedper contiguous run of keys present in the old list but absent fromnew_items, emitted back to front so every range is still valid, in the old indices, at the moment it is removed. - One left-to-right walk over the target order. Each surviving
row is handled when the walk reaches it: a
DataChange::ItemsMoved(a single-row block) if it is not yet in its target slot — skipped for rows already in place, so an append-only or remove-only reload emits no moves — then aDataChange::ItemUpdatedif its content differs (T: PartialEq; the stored value is replaced with the incoming one). Brand-new keys are buffered and flushed as oneDataChange::ItemsInsertedrun just before the next survivor, or at the end of the walk.
So after the removals, moves, updates and inserts are interleaved in
walk order, not grouped by kind: reconciling [(1,"a"),(2,"b")]
into [(1,"a"),(2,"B"),(3,"c")] emits ItemUpdated { index: 1 } and
then ItemsInserted { range: 2..3 }.
Every change’s indices are valid against the list as it stands
after the previous change (a sequential index space), not against
the list before reconcile_by_key or after it. A consumer that
applies the changes one at a time, in the order delivered, stays
correct; one that batches them by kind (all removals, then all moves,
…) or replays them out of order does not.
If new_items is identical (same keys, same order, same content, by
PartialEq) to the current contents, no change is emitted and no
observer runs — reconciling with unchanged data is silent.
§Preconditions
key_fn must be a pure, stable function of an item’s identity (not
its content) and keys must be unique within both the current list
and new_items. See # Panics below — an unstable key_fn or a
duplicate within new_items panics; a duplicate within the current
list is silently tolerated and leaves the extra copies behind, which
is worse.
§Panics
Panics (via an internal .expect) if key_fn is not stable — it
returns a different key for the same item across the two calls this
method makes to it (once while snapshotting the current list’s keys,
once while re-deriving a key during the write pass) — or if a key is
duplicated within the current list or within new_items. Both
break the same invariant the write pass relies on: “the item that
was accounted for under this key is still findable at or after the
write cursor.” A duplicate key means two different items raced to
claim one key slot, so by the time the second one is processed the
slot the accounting expected is already gone. This is this crate’s
usual documented-panic-on-contract-violation style (see e.g.
TreeModel::remove) — a caller-side bug
surfaced immediately as a panic, not silently wrong data.
§Complexity
Re-ordering is a straightforward left-to-right pass that moves each
out-of-place survivor into its target slot; it is correct and always
granular, but is not guaranteed to emit the mathematically fewest
possible ItemsMoved events for an adversarial permutation (an
LIS-based scheme could do slightly better there). For the common
case this primitive targets — a peer append/remove/edit merged back
in — the existing relative order of untouched rows is preserved
as-is, so no moves are emitted at all.
Source§impl<T: Debug + 'static> ListModel<T>
impl<T: Debug + 'static> ListModel<T>
Sourcepub fn debug_named(self, _name: impl Into<String>) -> Self
pub fn debug_named(self, _name: impl Into<String>) -> Self
Register this model with the debug inspector under name. In
release builds (!cfg(debug_assertions)) this is a no-op
pass-through so call sites stay free of #[cfg] lines.
Idempotent on repeated calls — the latest registration wins.
The registration drops automatically when the last ListModel
handle is freed (the adapter the registry holds is Weak).
Trait Implementations§
Source§impl<T: 'static> ListDataSource for ListModel<T>
ListModel is the built-in fully-resident, in-memory ListDataSource.
Identity is positional (Key = usize); a SameView drop reorders via
move_item. Into and Foreign drops are rejected (a flat list does not
nest, and a bare model knows no foreign payloads).
impl<T: 'static> ListDataSource for ListModel<T>
ListModel is the built-in fully-resident, in-memory ListDataSource.
Identity is positional (Key = usize); a SameView drop reorders via
move_item. Into and Foreign drops are rejected (a flat list does not
nest, and a bare model knows no foreign payloads).
Source§type Key = usize
type Key = usize
ListModel uses usize (the
index); external sources use their own domain key so keyed selection /
DnD survive reorders without a mirror model.Source§fn len(&self) -> usize
fn len(&self) -> usize
Source§fn with_item<R>(&self, index: usize, f: impl FnOnce(&T) -> R) -> Option<R>
fn with_item<R>(&self, index: usize, f: impl FnOnce(&T) -> R) -> Option<R>
index via a callback. Returns None for an
out-of-bounds index OR an in-bounds index whose data is still
Loading (see row_state).Source§fn key_at(&self, index: usize) -> Option<usize>
fn key_at(&self, index: usize) -> Option<usize>
index. Default None (no identity);
sources that support keyed selection / DnD override it.Source§fn index_of(&self, key: &usize) -> Option<usize>
fn index_of(&self, key: &usize) -> Option<usize>
None.Source§fn observe_changes(&self, f: impl Fn(&DataChange) + 'static) -> ObserverHandle
fn observe_changes(&self, f: impl Fn(&DataChange) + 'static) -> ObserverHandle
ObserverHandle] unregisters the callback automatically.Source§fn drag(&self, _key: &usize) -> DragEligibility
fn drag(&self, _key: &usize) -> DragEligibility
Source§fn can_accept(&self, query: &DropQuery<'_, usize>) -> DropResponse
fn can_accept(&self, query: &DropQuery<'_, usize>) -> DropResponse
Source§fn accept_drop(&self, commit: DropCommit<'_, usize>) -> bool
fn accept_drop(&self, commit: DropCommit<'_, usize>) -> bool
Source§fn reorder_within(
&self,
sources: &[usize],
target: &usize,
position: DropPosition,
) -> bool
fn reorder_within( &self, sources: &[usize], target: &usize, position: DropPosition, ) -> bool
sources are the
dragged rows’ keys in the origin’s visible order; target / position
name the drop gap. Returns whether anything moved. Read moreSource§fn on_drag_out(&self, key: &usize)
fn on_drag_out(&self, key: &usize)
Source§fn first_changed_index(&self) -> Option<usize>
fn first_changed_index(&self) -> Option<usize>
0..index are unchanged. None means unknown (full change).