Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
mapped_list.hpp
Go to the documentation of this file.
1#pragma once
2
3// MappedList<Source, Target> — a 1:1 projection of an ObservableList.
4//
5// Given a source list `ObservableList<Source>` and a mapper
6// `Target(const Source&)` (returning `shared_ptr<Target>`), MappedList
7// exposes a parallel list of Targets that tracks the source:
8//
9// source: [Person("alice"), Person("bob"), ...]
10// mapper: [](const Person& p) { return std::make_shared<PersonVM>(p); }
11// ↓
12// mapped: [PersonVM("alice"), PersonVM("bob"), ...]
13//
14// A MappedList is the canonical way to turn a domain-level list into
15// a view-model-level list without wiring per-row subscription glue by
16// hand. The derived list emits the same ListChange<Target> vocabulary
17// ObservableList / FilteredList / SortedList use, so adapters
18// (QtListModel etc.) can consume it identically.
19//
20// Identity preservation:
21// `st.targets[i]` IS the authoritative cache — there is no
22// separate `Source* → Target` map. Move / ItemChanged just leave
23// the slot alone (or move it), so the Target pointer that was
24// live before is still live after. Replace / Remove drop the
25// slot; external shared_ptr refs keep the Target alive if the
26// caller still holds one.
27//
28// ItemChanged policy:
29// By default, ItemChanged propagates as `ItemChanged(j, target_ptr)`
30// *without* re-running the mapper. Rationale: Targets are usually
31// long-lived ViewModels that already subscribe to the Source
32// themselves (via `Source::on_changed` inside `PersonVM`). A
33// re-map would churn the identity for no benefit.
34//
35// Call `MappedList<S,T>(src, mapper, /*remap_on_change=*/true)` to
36// invalidate the slot and re-invoke the mapper on every
37// ItemChanged — suitable when Target is a cheap, immutable
38// snapshot that must rebuild on each update.
39//
40// Event-translation contract:
41//
42// Source event Derived behaviour
43// ──────────────── ──────────────────────────────────────────
44// Insert(i, x) Insert(i, mapper(x))
45// Remove(i) Remove(i, old_target.get())
46// Replace(i, x_new) Replace(i, mapper(x_new))
47// ItemChanged(i) ItemChanged(i, target_ptr) by default;
48// Replace(i, new_target) with remap_on_change
49// Move(from, to) Move(from, to, target_ptr)
50// Reset Reset (fully rebuilt from the post-reset
51// source snapshot)
52//
53// Thread-safety and lifetime: same shape as FilteredList / SortedList.
54
56#include "aria/list_source.hpp"
58#include "aria/subscription.hpp"
59#include "aria/detail/list_signal_mixin.hpp"
60#include "aria/detail/typed_signal.hpp"
61
62#include <cstddef>
63#include <memory>
64#include <mutex>
65#include <shared_mutex>
66#include <utility>
67#include <vector>
68
69namespace aria {
70
71template<typename Source, typename Target,
72 typename SourceList = ObservableList<Source>>
75 : public detail::ListSignalMixin<MappedList<Source, Target, SourceList>,
76 Target> {
77 friend detail::ListSignalMixin<MappedList<Source, Target, SourceList>,
78 Target>;
79
80public:
83 using value_type = Target;
86 using Signal = detail::ListSignal<Target>;
87
99 MappedList(std::shared_ptr<SourceList> source,
100 Mapper mapper,
101 bool remap_on_change = false)
102 : source_(std::move(source)),
103 signal_(std::make_shared<Signal>()),
104 state_(std::make_shared<SharedState>())
105 {
106 state_->mapper = std::move(mapper);
107 state_->remap_on_change = remap_on_change;
108
109 // Initial snapshot — map each source item once.
110 {
111 std::unique_lock lk(state_->m);
112 auto snap = source_->snapshot();
113 state_->targets.reserve(snap.size());
114 for (const auto& s : snap) {
115 state_->targets.push_back(state_->mapper(*s));
116 }
117 }
118
119 // Subscribe on source. Listener holds only weak_ptrs.
120 std::weak_ptr<SharedState> weak_state = state_;
121 std::weak_ptr<Signal> weak_signal = signal_;
122 std::weak_ptr<SourceList> weak_source{source_};
123 source_sub_ = source_->observe(
124 [weak_state, weak_signal, weak_source](const ListChange<Source>& ch) {
125 auto st = weak_state.lock();
126 auto sig = weak_signal.lock();
127 auto src = weak_source.lock();
128 if (!st || !sig || !src) return;
129 dispatch_source_change_(*st, *sig, ch);
130 });
131 }
132
133 ~MappedList() = default;
134
135 MappedList(const MappedList&) = delete;
136 MappedList& operator=(const MappedList&) = delete;
137
138 // ── Read surface ──────────────────────────────────────────────────
139 [[nodiscard]] std::size_t size() const {
140 std::shared_lock lk(state_->m);
141 return state_->targets.size();
142 }
143
144 [[nodiscard]] bool empty() const { return size() == 0; }
145
146 [[nodiscard]] std::shared_ptr<Target> at(std::size_t idx) const {
147 std::shared_lock lk(state_->m);
148 return state_->targets.at(idx);
149 }
150
151 [[nodiscard]] std::vector<std::shared_ptr<Target>> snapshot() const {
152 std::shared_lock lk(state_->m);
153 return state_->targets;
154 }
155
156private:
157 struct SharedState {
158 mutable std::shared_mutex m;
159 Mapper mapper;
160 bool remap_on_change{false};
161 std::vector<std::shared_ptr<Target>> targets;
162 };
163
164 std::shared_ptr<SourceList> source_;
165 std::shared_ptr<Signal> signal_;
166 std::shared_ptr<SharedState> state_;
167 Subscription source_sub_;
168
169 static void dispatch_source_change_(SharedState& st,
170 Signal& sig,
171 const ListChange<Source>& ch) {
172 switch (ch.kind) {
173 case ListChangeKind::Insert: handle_insert_(st, sig, ch); return;
174 case ListChangeKind::Remove: handle_remove_(st, sig, ch); return;
175 case ListChangeKind::Replace: handle_replace_(st, sig, ch); return;
176 case ListChangeKind::ItemChanged: handle_item_changed_(st, sig, ch); return;
177 case ListChangeKind::Move: handle_move_(st, sig, ch); return;
178 case ListChangeKind::Reset: handle_reset_(st, sig, ch); return;
179 }
180 }
181
182 static void handle_insert_(SharedState& st, Signal& sig,
183 const ListChange<Source>& ch) {
184 std::shared_ptr<Target> t;
185 {
186 std::unique_lock lk(st.m);
187 const std::size_t idx = ch.index;
188 auto shared_src = ch.item;
189 t = st.mapper(*shared_src);
190 st.targets.insert(st.targets.begin()
191 + static_cast<std::ptrdiff_t>(idx), t);
192 }
193 sig.emit(ListChange<Target>{ListChangeKind::Insert, ch.index, t, 0});
194 }
195
196 static void handle_remove_(SharedState& st, Signal& sig,
197 const ListChange<Source>& ch) {
198 std::shared_ptr<Target> removed;
199 {
200 std::unique_lock lk(st.m);
201 const std::size_t idx = ch.index;
202 if (idx >= st.targets.size()) return;
203 removed = st.targets[idx];
204 st.targets.erase(st.targets.begin()
205 + static_cast<std::ptrdiff_t>(idx));
206 }
207 sig.emit(ListChange<Target>{ListChangeKind::Remove, ch.index,
208 removed, 0});
209 }
210
211 static void handle_replace_(SharedState& st, Signal& sig,
212 const ListChange<Source>& ch) {
213 std::shared_ptr<Target> t;
214 {
215 std::unique_lock lk(st.m);
216 const std::size_t idx = ch.index;
217 if (idx >= st.targets.size()) return;
218 auto shared_new = ch.item;
219 t = st.mapper(*shared_new);
220 // Overwriting st.targets[idx] drops the old Target; any
221 // external shared_ptr keeps it alive.
222 st.targets[idx] = t;
223 }
224 sig.emit(ListChange<Target>{ListChangeKind::Replace, ch.index,
225 t, 0});
226 }
227
228 static void handle_item_changed_(SharedState& st, Signal& sig,
229 const ListChange<Source>& ch) {
230 std::shared_ptr<Target> t;
231 {
232 std::unique_lock lk(st.m);
233 const std::size_t idx = ch.index;
234 if (idx >= st.targets.size()) return;
235
236 if (st.remap_on_change) {
237 auto shared_new = ch.item;
238 t = st.mapper(*shared_new);
239 st.targets[idx] = t;
240 } else {
241 // Preserve Target identity; downstream observers that
242 // want "refresh" semantics should subscribe to the
243 // Source inside their Target.
244 t = st.targets[idx];
245 }
246 }
247 sig.emit(ListChange<Target>{st.remap_on_change ? ListChangeKind::Replace
249 ch.index, t, 0});
250 }
251
252 static void handle_move_(SharedState& st, Signal& sig,
253 const ListChange<Source>& ch) {
254 std::shared_ptr<Target> moved;
255 {
256 std::unique_lock lk(st.m);
257 const std::size_t from = ch.from_index;
258 const std::size_t to = ch.index;
259 if (from == to) return;
260 if (from >= st.targets.size() || to >= st.targets.size()) return;
261
262 moved = st.targets[from];
263 st.targets.erase(st.targets.begin()
264 + static_cast<std::ptrdiff_t>(from));
265 st.targets.insert(st.targets.begin()
266 + static_cast<std::ptrdiff_t>(to), moved);
267 }
268 sig.emit(ListChange<Target>{ListChangeKind::Move, ch.index,
269 moved, ch.from_index});
270 }
271
272 static void handle_reset_(SharedState& st, Signal& sig,
273 const ListChange<Source>& ch) {
274 ListChange<Target> reset;
275 {
276 std::unique_lock lk(st.m);
277 st.targets.clear();
278 const auto& snap = *ch.snapshot;
279 st.targets.reserve(snap.size());
280 for (const auto& s : snap) {
281 st.targets.push_back(st.mapper(*s));
282 }
283 reset = ListChange<Target>::reset(st.targets);
284 }
285 sig.emit(std::move(reset));
286 }
287};
288
289// ---------------------------------------------------------------------------
290// Factory helper — deduces the source type so pipelines stay readable.
291// See the note on `aria::filtered` in filtered_list.hpp.
292// ---------------------------------------------------------------------------
293template<typename Target,
294 typename SourceList,
295 typename MapFn,
298[[nodiscard]] std::shared_ptr<MappedList<Source, Target, SourceList>>
299mapped(std::shared_ptr<SourceList> source,
300 MapFn mapper,
301 bool remap_on_change = false) {
303 return std::make_shared<Derived>(
304 std::move(source),
305 typename Derived::Mapper{std::move(mapper)},
306 remap_on_change);
307}
308
309} // namespace aria
Definition mapped_list.hpp:76
MappedList & operator=(const MappedList &)=delete
MappedList(std::shared_ptr< SourceList > source, Mapper mapper, bool remap_on_change=false)
Construct a MappedList.
Definition mapped_list.hpp:99
std::vector< std::shared_ptr< Target > > snapshot() const
Definition mapped_list.hpp:151
aria::inplace_function< std::shared_ptr< Target >(const Source &), 32 > Mapper
Owning, heap-free mapper handle (capacity 32 bytes).
Definition mapped_list.hpp:85
~MappedList()=default
bool empty() const
Definition mapped_list.hpp:144
Target value_type
Element type of the derived list — i.e.
Definition mapped_list.hpp:83
detail::ListSignal< Target > Signal
Definition mapped_list.hpp:86
MappedList(const MappedList &)=delete
std::size_t size() const
Definition mapped_list.hpp:139
std::shared_ptr< Target > at(std::size_t idx) const
Definition mapped_list.hpp:146
Observable sequence of owning element handles.
Definition observable_list.hpp:40
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
@ 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< MappedList< Source, Target, SourceList > > mapped(std::shared_ptr< SourceList > source, MapFn mapper, bool remap_on_change=false)
Definition mapped_list.hpp:299
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