Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
sorted_list.hpp
Go to the documentation of this file.
1#pragma once
2
3// SortedList<T> — a live-updating sorted view over an ObservableList<T>.
4//
5// Given a source list and a comparator, SortedList exposes the items of
6// the source, reordered to satisfy `comparator(a, b)` as a strict weak
7// ordering. Equivalent items (neither `cmp(a,b)` nor `cmp(b,a)` is
8// true) retain their source order — SortedList is a STABLE sort, and
9// stable at runtime too: no insert, replace, or ItemChanged ever
10// permutes two already-equivalent items against each other.
11//
12// Like FilteredList, every change to the source is translated into the
13// minimal incremental sequence of derived events; small source
14// mutations never escalate to a full Reset.
15//
16// Usage:
17//
18// auto source = std::make_shared<ObservableList<Task>>();
19// auto sorted = std::make_shared<SortedList<Task>>(
20// source,
21// [](const Task& a, const Task& b) {
22// return a.priority() < b.priority();
23// });
24//
25// source->push_back(std::make_shared<Task>(/*prio=*/5));
26// source->push_back(std::make_shared<Task>(/*prio=*/2));
27// // sorted sees: Insert(0), Insert(0) — the prio=2 ends up at index 0.
28//
29// source->at(0)->set_priority(10);
30// // if Task::on_changed propagates, sorted sees Move then ItemChanged
31// // so downstream views refresh the same object at its new position.
32//
33// Event-translation contract (mandatory; pinned by tests):
34//
35// Source event Derived behaviour
36// ──────────────── ──────────────────────────────────────────
37// Insert(i, x) binary_search → Insert(j, x) (always 1 event)
38// Remove(i) Remove(j) (always 1 event)
39// Replace(i, x_new) d_new == d_old → Replace(d_old, x_new)
40// otherwise → Remove(d_old), Insert(d_new)
41// ItemChanged(i) d_new == d_old → ItemChanged(d_old)
42// otherwise → Move(d_old, d_new, x),
43// ItemChanged(d_new)
44// Move(from, to) no event for distinct keys; equivalent keys
45// follow the new source order using Moves.
46// Reset Reset
47//
48// Objects may mutate before their notifications arrive, including several
49// distinct objects in one graph batch. Binary-search inputs are validated in
50// O(n); an unordered remainder uses a complete stable permutation repair.
51// Repair emits owning Moves (possibly for unchanged rows) plus the primary
52// content/structural event in replay order. It costs O(n log n + n*m), where
53// m is the number of Moves; ordinary ordered edits remain incremental.
54//
55// set_comparator() reshuffles the derived list against the new
56// comparator and emits a single Reset event (rather than trying to
57// emit a minimal-edit sequence — that would be O(n log n) to compute
58// and almost always dominated by the re-sort itself). The `reset`
59// event is the honest signal "layout changed wholesale".
60//
61// Stability contract:
62// Equivalent items (by the comparator) are ordered by ascending
63// source index, including after a source Move changes those indices.
64//
65// Thread-safety:
66// Same policy as FilteredList: internal `shared_mutex`, source
67// listener holds only weak_ptrs, emits happen after the mutation is
68// visible but before the lock is re-taken by a later event.
69
71#include "aria/list_source.hpp"
73#include "aria/subscription.hpp"
74#include "aria/detail/list_signal_mixin.hpp"
75#include "aria/detail/typed_signal.hpp"
76
77#include <algorithm>
78#include <cstddef>
79#include <memory>
80#include <mutex>
81#include <optional>
82#include <shared_mutex>
83#include <utility>
84#include <vector>
85
86namespace aria {
87
88template<typename T, typename Source = ObservableList<T>>
91 : public detail::ListSignalMixin<SortedList<T, Source>, T> {
92 friend detail::ListSignalMixin<SortedList<T, Source>, T>;
93
94public:
95 using value_type = T;
99 using Comparator = aria::inplace_function<bool(const T&, const T&), 32>;
100 using Signal = detail::ListSignal<T>;
101
102 SortedList(std::shared_ptr<Source> source,
103 Comparator comparator)
104 : source_(std::move(source)),
105 signal_(std::make_shared<Signal>()),
106 state_(std::make_shared<SharedState>())
107 {
108 state_->comparator = std::move(comparator);
109
110 // Build the initial mapping from the source snapshot. See the
111 // FilteredList constructor for why we snapshot before
112 // subscribing; concurrent source mutations that arrive
113 // mid-construction are then delivered as normal listener
114 // callbacks on top of the known-good baseline.
115 {
116 std::unique_lock lk(state_->m);
117 rebuild_from_snapshot_unlocked_(*state_, source_->snapshot());
118 }
119
120 // Subscribe on the source. The lambda holds only weak_ptrs.
121 std::weak_ptr<SharedState> weak_state = state_;
122 std::weak_ptr<Signal> weak_signal = signal_;
123 std::weak_ptr<Source> weak_source{source_};
124 source_sub_ = source_->observe(
125 [weak_state, weak_signal, weak_source](const ListChange<T>& ch) {
126 auto st = weak_state.lock();
127 auto sig = weak_signal.lock();
128 auto src = weak_source.lock();
129 if (!st || !sig || !src) return;
130 dispatch_source_change_(*st, *sig, ch);
131 });
132 }
133
134 ~SortedList() = default;
135
136 SortedList(const SortedList&) = delete;
137 SortedList& operator=(const SortedList&) = delete;
138
139 // ── Read surface ──────────────────────────────────────────────────
140 [[nodiscard]] std::size_t size() const {
141 std::shared_lock lk(state_->m);
142 return state_->items.size();
143 }
144
145 [[nodiscard]] bool empty() const { return size() == 0; }
146
147 [[nodiscard]] std::shared_ptr<T> at(std::size_t derived_index) const {
148 std::shared_lock lk(state_->m);
149 return state_->items.at(derived_index);
150 }
151
152 [[nodiscard]] std::vector<std::shared_ptr<T>> snapshot() const {
153 std::shared_lock lk(state_->m);
154 return state_->items;
155 }
156
157 [[nodiscard]] std::optional<std::size_t>
158 source_index_of(std::size_t derived_index) const {
159 std::shared_lock lk(state_->m);
160 if (derived_index >= state_->derived_to_source.size()) return std::nullopt;
161 return state_->derived_to_source[derived_index];
162 }
163
164 // ── Comparator replacement ────────────────────────────────────────
165 //
166 // Re-sorts the derived layout against the new comparator and emits
167 // a single Reset. An O(n log n) minimal-edit sequence could be
168 // computed, but the cost would dominate the re-sort itself and the
169 // wider Qt/AppKit adapter ecosystem handles Reset cleanly anyway.
170 void set_comparator(Comparator new_comparator) {
171 auto signal = signal_;
172 ListChange<T> reset;
173 {
174 std::unique_lock lk(state_->m);
175 state_->comparator = std::move(new_comparator);
176 std::vector<std::shared_ptr<T>> source_items(state_->items.size());
177 for (std::size_t i = 0; i < state_->items.size(); ++i) {
178 source_items[state_->derived_to_source[i]] = state_->items[i];
179 }
180 rebuild_from_snapshot_unlocked_(*state_, std::move(source_items));
181 reset = ListChange<T>::reset(state_->items);
182 }
183 signal->emit(std::move(reset));
184 }
185
186private:
187 // Per-instance state shared with the source-listener lambda.
188 struct SharedState {
189 mutable std::shared_mutex m;
190 Comparator comparator;
191 // length == source.size(); value = derived index of that source slot.
192 std::vector<std::size_t> source_to_derived;
193 // length == derived.size(); value = source index.
194 std::vector<std::size_t> derived_to_source;
195 // parallel to derived_to_source; strong refs for at() / snapshot().
196 std::vector<std::shared_ptr<T>> items;
197 };
198
199 std::shared_ptr<Source> source_;
200 std::shared_ptr<Signal> signal_;
201 std::shared_ptr<SharedState> state_;
202 Subscription source_sub_;
203
204 // Rebuild all three parallel vectors from a fresh source snapshot.
205 // Caller must hold the unique lock on `st.m`. `snap` is taken
206 // from the source, whose lock is independent of ours.
207 static void rebuild_from_snapshot_unlocked_(
208 SharedState& st,
209 std::vector<std::shared_ptr<T>> snap)
210 {
211 std::vector<std::size_t> idx(snap.size());
212 for (std::size_t i = 0; i < snap.size(); ++i) idx[i] = i;
213 std::stable_sort(idx.begin(), idx.end(),
214 [&](std::size_t a, std::size_t b) {
215 return st.comparator(*snap[a], *snap[b]);
216 });
217
218 st.source_to_derived.assign(snap.size(), 0);
219 st.derived_to_source.assign(idx.size(), 0);
220 st.items.clear();
221 st.items.reserve(idx.size());
222 for (std::size_t d = 0; d < idx.size(); ++d) {
223 const std::size_t s = idx[d];
224 st.derived_to_source[d] = s;
225 st.source_to_derived[s] = d;
226 st.items.push_back(snap[s]);
227 }
228 renumber_s2d_(st);
229 }
230
231 // ── Translation: one source event -> zero or more derived events ──
232 static void dispatch_source_change_(SharedState& st,
233 Signal& sig,
234 const ListChange<T>& ch) {
235 switch (ch.kind) {
236 case ListChangeKind::Insert: handle_insert_(st, sig, ch); return;
237 case ListChangeKind::Remove: handle_remove_(st, sig, ch); return;
238 case ListChangeKind::Replace: handle_replace_(st, sig, ch); return;
239 case ListChangeKind::ItemChanged: handle_item_changed_(st, sig, ch); return;
240 case ListChangeKind::Move: handle_move_(st, sig, ch); return;
241 case ListChangeKind::Reset: handle_reset_(st, sig, ch); return;
242 }
243 }
244
266 static std::size_t binary_search_insert_(
267 const SharedState& st, const T& needle, std::size_t needle_src_i,
268 std::optional<std::size_t> skip_derived_idx = std::nullopt)
269 {
270 const auto& cmp = st.comparator;
271 const auto& items = st.items;
272 const auto& d2s = st.derived_to_source;
273
274 // `real` translates from compressed (with d_old skipped) to
275 // real derived-index. Only used to access items[] / d2s[]
276 // during the search — the returned value stays in compressed
277 // coordinates.
278 auto real = [&](std::size_t compressed) -> std::size_t {
279 if (skip_derived_idx && compressed >= *skip_derived_idx)
280 return compressed + 1;
281 return compressed;
282 };
283 const std::size_t n_eff = items.size()
284 - (skip_derived_idx ? 1u : 0u);
285
286 std::size_t lo = 0;
287 std::size_t hi = n_eff;
288 while (lo < hi) {
289 const std::size_t mid = lo + (hi - lo) / 2;
290 const std::size_t r = real(mid);
291 const auto& mid_item = *items[r];
292 if (cmp(needle, mid_item)) {
293 hi = mid;
294 } else if (cmp(mid_item, needle)) {
295 lo = mid + 1;
296 } else {
297 // Equivalent keys — stability tie-breaker by source
298 // index.
299 if (needle_src_i < d2s[r]) hi = mid;
300 else lo = mid + 1;
301 }
302 }
303 return lo;
304 }
305
306 static void handle_insert_(SharedState& st, Signal& sig,
307 const ListChange<T>& ch) {
308 std::unique_lock lk(st.m);
309 const std::size_t src_idx = ch.index;
310
311 // Shift all source indices >= src_idx by +1 (the source just
312 // grew one slot before/at them).
313 if (src_idx != st.items.size()) {
314 for (auto& s : st.derived_to_source) {
315 if (s >= src_idx) ++s;
316 }
317 }
318 st.source_to_derived.insert(st.source_to_derived.begin()
319 + static_cast<std::ptrdiff_t>(src_idx),
320 0);
321
322 // Resolve the current shared_ptr from the source. source has
323 // already released its own lock by the time emit() runs.
324 auto shared = ch.item;
325
326 const bool ordered = ordered_unlocked_(st);
327 const std::size_t d_idx = ordered ? binary_search_insert_(st, *shared, src_idx) : st.items.size();
328
329 st.derived_to_source.insert(st.derived_to_source.begin()
330 + static_cast<std::ptrdiff_t>(d_idx),
331 src_idx);
332 st.items.insert(st.items.begin()
333 + static_cast<std::ptrdiff_t>(d_idx),
334 shared);
335
336 // Rebuild s2d from d2s (the insert shifted all derived
337 // indices >= d_idx by +1).
338 renumber_s2d_(st, d_idx);
339
340 if (!ordered) {
341 auto moves = reorder_unlocked_(st);
342 moves.insert(moves.begin(), {ListChangeKind::Insert, d_idx, ch.item, 0});
343 lk.unlock();
344 sig.emit_batch(std::move(moves));
345 } else {
346 lk.unlock();
347 sig.emit(ListChange<T>{ListChangeKind::Insert, d_idx, ch.item, 0});
348 }
349 }
350
351 static void handle_remove_(SharedState& st, Signal& sig,
352 const ListChange<T>& ch) {
353 std::unique_lock lk(st.m);
354 const std::size_t src_idx = ch.index;
355 if (src_idx >= st.source_to_derived.size()) return;
356
357 const std::size_t d_idx = st.source_to_derived[src_idx];
358 auto removed = st.items[d_idx];
359
360 st.items.erase(st.items.begin() + static_cast<std::ptrdiff_t>(d_idx));
361 st.derived_to_source.erase(
362 st.derived_to_source.begin() + static_cast<std::ptrdiff_t>(d_idx));
363 st.source_to_derived.erase(
364 st.source_to_derived.begin() + static_cast<std::ptrdiff_t>(src_idx));
365
366 // All source indices > src_idx shift down by 1.
367 for (auto& s : st.derived_to_source) {
368 if (s > src_idx) --s;
369 }
370 renumber_s2d_(st);
371
372 if (!ordered_unlocked_(st)) {
373 auto moves = reorder_unlocked_(st);
374 moves.insert(moves.begin(), {ListChangeKind::Remove, d_idx, removed, 0});
375 lk.unlock();
376 sig.emit_batch(std::move(moves));
377 } else {
378 lk.unlock();
379 sig.emit(ListChange<T>{ListChangeKind::Remove, d_idx, removed, 0});
380 }
381 }
382
383 static void handle_replace_(SharedState& st, Signal& sig,
384 const ListChange<T>& ch) {
385 handle_slot_changed_(st, sig, ch,
386 /*new_ptr_from_src=*/true,
387 /*same_slot_kind=*/ListChangeKind::Replace,
388 /*cross_slot_use_move=*/false);
389 }
390
391 static void handle_item_changed_(SharedState& st, Signal& sig,
392 const ListChange<T>& ch) {
393 handle_slot_changed_(st, sig, ch,
394 /*new_ptr_from_src=*/false,
395 /*same_slot_kind=*/ListChangeKind::ItemChanged,
396 /*cross_slot_use_move=*/true);
397 }
398
399 // Objects can change before their notifications arrive (graph batches,
400 // shared handles, or reentrant observers). Never binary-search until the
401 // remaining rows have been checked against their current values.
402 static bool ordered_unlocked_(const SharedState& st,
403 std::optional<std::size_t> skip = std::nullopt) {
404 std::optional<std::size_t> previous;
405 for (std::size_t d = 0; d < st.items.size(); ++d) {
406 if (skip == d) continue;
407 if (previous) {
408 const auto p = *previous;
409 // Source order resolves equal keys. When source indices are
410 // inverted, the preceding key must be strictly smaller;
411 // otherwise it need only be no greater. One comparison is
412 // enough in either case for a strict weak ordering.
413 if (st.derived_to_source[d] < st.derived_to_source[p]) {
414 if (!st.comparator(*st.items[p], *st.items[d])) return false;
415 } else if (st.comparator(*st.items[d], *st.items[p])) {
416 return false;
417 }
418 }
419 previous = d;
420 }
421 return true;
422 }
423
424 // Move one row by shifting its interval once. Unlike a general rotate,
425 // this does not swap shared handles repeatedly around permutation cycles.
426 static void move_row_unlocked_(SharedState& st, std::size_t from, std::size_t to) noexcept {
427 auto item = std::move(st.items[from]);
428 const auto source_index = st.derived_to_source[from];
429 const auto first = static_cast<std::ptrdiff_t>(std::min(from, to));
430 const auto last = static_cast<std::ptrdiff_t>(std::max(from, to));
431 if (from < to) {
432 std::move(st.items.begin() + first + 1, st.items.begin() + last + 1, st.items.begin() + first);
433 std::move(st.derived_to_source.begin() + first + 1, st.derived_to_source.begin() + last + 1,
434 st.derived_to_source.begin() + first);
435 } else {
436 std::move_backward(st.items.begin() + first, st.items.begin() + last, st.items.begin() + last + 1);
437 std::move_backward(st.derived_to_source.begin() + first, st.derived_to_source.begin() + last,
438 st.derived_to_source.begin() + last + 1);
439 }
440 st.items[to] = std::move(item);
441 st.derived_to_source[to] = source_index;
442 for (auto d = std::min(from, to); d <= std::max(from, to); ++d)
443 st.source_to_derived[st.derived_to_source[d]] = d;
444 }
445
449 static std::vector<ListChange<T>> reorder_unlocked_(SharedState& st) {
450 auto before = [&](std::size_t a, std::size_t b) {
451 const auto& lhs = *st.items[st.source_to_derived[a]];
452 const auto& rhs = *st.items[st.source_to_derived[b]];
453 if (st.comparator(lhs, rhs)) return true;
454 if (st.comparator(rhs, lhs)) return false;
455 return a < b;
456 };
457 std::vector<ListChange<T>> events;
458 if (std::is_sorted(st.derived_to_source.begin(), st.derived_to_source.end(), before)) return events;
459 auto target = st.derived_to_source;
460 std::sort(target.begin(), target.end(), before);
461 events.reserve(target.size());
462 // Move the farther-displaced end of the interval into place. This
463 // avoids moving a long repeated block one occurrence at a time when
464 // shifting the few intervening rows represents the same permutation.
465 std::size_t first = 0, last = target.size();
466 while (first < last && target[first] == st.derived_to_source[first]) ++first;
467 while (last > first && target[last - 1] == st.derived_to_source[last - 1]) --last;
468 const bool reverse = st.source_to_derived[target[first]] - first <
469 last - 1 - st.source_to_derived[target[last - 1]];
470 auto place = [&](std::size_t to) {
471 const auto from = st.source_to_derived[target[to]];
472 if (from == to) return;
473 const auto item = st.items[from];
474 events.push_back({ListChangeKind::Move, to, item, from});
475 move_row_unlocked_(st, from, to);
476 };
477 if (reverse) { for (auto i = last; i-- > first;) place(i); }
478 else { for (auto i = first; i < last; ++i) place(i); }
479 return events;
480 }
481
492 static void handle_slot_changed_(SharedState& st, Signal& sig,
493 const ListChange<T>& ch,
494 bool new_ptr_from_src,
495 ListChangeKind same_slot_kind,
496 bool cross_slot_use_move) {
497 std::unique_lock lk(st.m);
498 const std::size_t src_idx = ch.index;
499 if (src_idx >= st.source_to_derived.size()) return;
500
501 const std::size_t d_old = st.source_to_derived[src_idx];
502
503 // Replace carries the new owning handle; ItemChanged keeps identity.
504 auto previous = st.items[d_old];
505 auto fresh = new_ptr_from_src ? ch.item : previous;
506 if (new_ptr_from_src) st.items[d_old] = fresh;
507 if (!ordered_unlocked_(st, d_old)) {
508 auto moves = reorder_unlocked_(st);
509 if (new_ptr_from_src) {
510 // Replace the old logical row before replaying the permutation.
511 moves.insert(moves.begin(), {ListChangeKind::Replace, d_old, fresh, 0});
512 } else {
513 moves.push_back({ListChangeKind::ItemChanged, st.source_to_derived[src_idx], fresh, 0});
514 }
515 lk.unlock();
516 sig.emit_batch(std::move(moves));
517 return;
518 }
519
520 // Where does the (possibly mutated) item belong now? Skip the
521 // old slot in the search so we measure "new position if the
522 // old slot weren't there". Returned `p_small` is in the
523 // compressed (d_old-skipped) view: `p_small == d_old` means
524 // the item belongs right back where it was.
525 const std::size_t p_small = binary_search_insert_(
526 st, *fresh, src_idx, /*skip_derived_idx=*/d_old);
527
528 if (p_small == d_old) {
529 // Item stayed in place — one same-slot event.
530 lk.unlock();
531 sig.emit(ListChange<T>{same_slot_kind, d_old, fresh, 0});
532 return;
533 }
534
535 // Shift only the affected interval. Erase followed by insert would
536 // shift the unchanged suffix twice and renumber unaffected rows.
537 const std::size_t d_insert = p_small;
538 move_row_unlocked_(st, d_old, d_insert);
539
540 lk.unlock();
541 if (cross_slot_use_move) {
542 sig.emit_batch({ListChange<T>{ListChangeKind::Move, d_insert, fresh, d_old},
543 ListChange<T>{ListChangeKind::ItemChanged, d_insert, fresh, 0}});
544 } else {
545 sig.emit_batch({ListChange<T>{ListChangeKind::Remove, d_old, previous, 0},
546 ListChange<T>{ListChangeKind::Insert, d_insert, fresh, 0}});
547 }
548 }
549
553 static void handle_move_(SharedState& st, Signal& sig,
554 const ListChange<T>& ch) {
555 std::unique_lock lk(st.m);
556 const std::size_t from = ch.from_index;
557 const std::size_t to = ch.index;
558 if (from == to) return;
559 if (from >= st.source_to_derived.size()) return;
560 if (to >= st.source_to_derived.size()) return;
561
562 // Shift all source indices that lived in [from+1..to] left by 1
563 // (if from < to) or all indices in [to..from-1] right by 1
564 // (if from > to). The moved item itself gets the new value
565 // `to`.
566 const std::size_t moved_d = st.source_to_derived[from];
567
568 if (from < to) {
569 for (auto& s : st.derived_to_source) {
570 if (s > from && s <= to) --s;
571 }
572 } else {
573 for (auto& s : st.derived_to_source) {
574 if (s >= to && s < from) ++s;
575 }
576 }
577 st.derived_to_source[moved_d] = to;
578 renumber_s2d_(st);
579 // A source Move normally changes only source-index metadata. Pending
580 // object-key writes can also have invalidated the visible key order.
581 if (!ordered_unlocked_(st)) {
582 auto moves = reorder_unlocked_(st);
583 lk.unlock();
584 sig.emit_batch(std::move(moves));
585 }
586 }
587
588 static void handle_reset_(SharedState& st, Signal& sig,
589 const ListChange<T>& ch) {
590 ListChange<T> reset;
591 {
592 std::unique_lock lk(st.m);
593 rebuild_from_snapshot_unlocked_(st, *ch.snapshot);
594 reset = ListChange<T>::reset(st.items);
595 }
596 sig.emit(std::move(reset));
597 }
598
602 static void renumber_s2d_(SharedState& st, std::size_t first = 0) {
603 for (std::size_t d = first; d < st.derived_to_source.size(); ++d) {
604 st.source_to_derived[st.derived_to_source[d]] = d;
605 }
606 }
607};
608
609// ---------------------------------------------------------------------------
610// Factory helper — deduces the source type so pipelines stay readable.
611// See the note on `aria::filtered` in filtered_list.hpp.
612// ---------------------------------------------------------------------------
613template<typename Source,
614 typename Comparator,
615 typename T = list_source_value_t<Source>>
617[[nodiscard]] std::shared_ptr<SortedList<T, Source>>
618sorted(std::shared_ptr<Source> source, Comparator comparator) {
619 return std::make_shared<SortedList<T, Source>>(
620 std::move(source),
621 typename SortedList<T, Source>::Comparator{std::move(comparator)});
622}
623
624} // namespace aria
~SortedList()=default
std::vector< std::shared_ptr< T > > snapshot() const
Definition sorted_list.hpp:152
aria::inplace_function< bool(const T &, const T &), 32 > Comparator
Strict weak ordering on items.
Definition sorted_list.hpp:99
bool empty() const
Definition sorted_list.hpp:145
std::shared_ptr< T > at(std::size_t derived_index) const
Definition sorted_list.hpp:147
void set_comparator(Comparator new_comparator)
Definition sorted_list.hpp:170
SortedList(std::shared_ptr< Source > source, Comparator comparator)
Definition sorted_list.hpp:102
SortedList & operator=(const SortedList &)=delete
detail::ListSignal< T > Signal
Definition sorted_list.hpp:100
std::size_t size() const
Definition sorted_list.hpp:140
SortedList(const SortedList &)=delete
std::optional< std::size_t > source_index_of(std::size_t derived_index) const
Definition sorted_list.hpp:158
T value_type
Definition sorted_list.hpp:95
Definition inplace_function.hpp:106
Definition list_source.hpp:80
@ Source
Definition node.hpp:88
Definition signal.hpp:12
typename detail::list_source_value_impl< std::remove_cvref_t< L > >::type list_source_value_t
Element type for a list source.
Definition list_source.hpp:76
ListChangeKind
Definition list_change.hpp:10
@ Replace
Definition list_change.hpp:10
@ Remove
Definition list_change.hpp:10
@ Reset
Definition list_change.hpp:10
@ Move
Definition list_change.hpp:10
@ ItemChanged
Definition list_change.hpp:10
@ Insert
Definition list_change.hpp:10
std::shared_ptr< SortedList< T, Source > > sorted(std::shared_ptr< Source > source, Comparator comparator)
Definition sorted_list.hpp:618
Definition validation_key.hpp:110
An owning event in a sequential list edit stream.
Definition list_change.hpp:16
static ListChange reset(Snapshot items)
Definition list_change.hpp:24