Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
selection.hpp
Go to the documentation of this file.
1#pragma once
2
3// ============================================================================
4// selection.hpp
5// ----------------------------------------------------------------------------
6// Reactive selection models layered on top of `Property` and
7// `ObservableList`.
8//
9// Selection<T> — at most one selected element (nullable).
10// MultiSelection<T> — an ordered set of selected elements
11// (ordered by pick time).
12//
13// Both store `std::shared_ptr<T>` element handles (matching
14// `ObservableList<T>`) and expose reactive state:
15//
16// Selection<Item> sel;
17// sel.bind_to(list); // follow the source list
18// auto sub = sel.selected().on_changed([](auto& p){ highlight(p); });
19// sel.select(item); // -> selected() emits
20//
21// `bind_to(list)` keeps the selection consistent under source mutations:
22// * an element removed from the list (Remove / Reset) drops out of the
23// selection;
24// * Move / Insert / Replace of *other* elements leaves the selection
25// intact (a selected element that is merely repositioned stays
26// selected);
27// * a Replace at the selected element's slot drops it (the logical
28// element changed identity).
29//
30// Threading: same single-graph-thread contract as `Property`. The
31// `ObservableList` change stream is delivered on whatever thread mutates
32// the list; if that is not the graph thread, marshal via a Dispatcher
33// before calling `select` / `clear` (identical rule to writing any
34// Property).
35//
36// Contract IDs (see docs): SE-1 single, SE-2 multi, SE-3 bind-follow,
37// SE-4 derived-list consumption (documented idiom), SE-5 memory/teardown.
38// ============================================================================
39
41#include "aria/detail/list_replay.hpp"
42#include "aria/property.hpp"
43#include "aria/subscription.hpp"
44
45#include <algorithm>
46#include <cstddef>
47#include <memory>
48#include <vector>
49
50namespace aria {
51
52// ----------------------------------------------------------------------------
53// Selection<T> — single (optional) selection.
54// ----------------------------------------------------------------------------
55template<typename T>
56class Selection {
57public:
58 using value_type = T;
59 using handle = std::shared_ptr<T>;
60
61 Selection() = default;
62 ~Selection() { *alive_ = false; }
63
64 Selection(const Selection&) = delete;
65 Selection& operator=(const Selection&) = delete;
66 Selection(Selection&&) = delete;
68
70 [[nodiscard]] reactive::Property<handle>& selected() noexcept { return selected_; }
71 [[nodiscard]] const reactive::Property<handle>& selected() const noexcept { return selected_; }
72
74 [[nodiscard]] handle value() const { return selected_.get(); }
75 [[nodiscard]] bool has_value() const { return static_cast<bool>(selected_.peek()); }
76
79 void select(handle item) { selected_.set(std::move(item)); }
80
82 void clear() { selected_.set(nullptr); }
83
85 [[nodiscard]] bool is_selected(const handle& item) const {
86 return selected_.peek() == item;
87 }
88
94 void bind_to(ObservableList<T>& source) {
95 std::weak_ptr<bool> weak_alive = alive_;
96 auto rows = std::make_shared<std::vector<handle>>(source.snapshot());
97 source_sub_ = source.observe([this, rows, weak_alive](const ListChange<T>& ch) {
98 auto alive = weak_alive.lock();
99 if (!alive || !*alive) return;
100 detail::replay_list_change(*rows, ch);
102 ch.kind != ListChangeKind::Reset) return;
103 auto current = selected_.peek();
104 if (current && std::find(rows->begin(), rows->end(), current) == rows->end()) clear();
105 });
106 }
107
109 void unbind() noexcept { source_sub_.release(); }
110
111private:
112 // Signal emission snapshots may retain a disconnected callback after
113 // this object is destroyed by an earlier observer on the same thread.
114 std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
115 reactive::Property<handle> selected_{nullptr};
116 Subscription source_sub_;
117};
118
119// ----------------------------------------------------------------------------
120// MultiSelection<T> — ordered set of selected elements.
121// ----------------------------------------------------------------------------
122template<typename T>
124public:
125 using value_type = T;
126 using handle = std::shared_ptr<T>;
127
128 MultiSelection() = default;
129 ~MultiSelection() { *alive_ = false; }
130
135
140 return selected_;
141 }
142 [[nodiscard]] const reactive::Property<std::vector<handle>>& selected() const noexcept {
143 return selected_;
144 }
145
146 [[nodiscard]] std::vector<handle> values() const { return selected_.get(); }
147 [[nodiscard]] std::size_t size() const { return selected_.peek().size(); }
148 [[nodiscard]] bool empty() const { return selected_.peek().empty(); }
149
150 [[nodiscard]] bool is_selected(const handle& item) const {
151 const auto& v = selected_.peek();
152 return std::find(v.begin(), v.end(), item) != v.end();
153 }
154
157 void add(handle item) {
158 if (!item) return;
159 auto v = selected_.peek();
160 if (std::find(v.begin(), v.end(), item) != v.end()) return;
161 v.push_back(std::move(item));
162 selected_.set(std::move(v));
163 }
164
166 void remove(const handle& item) {
167 auto v = selected_.peek();
168 auto it = std::find(v.begin(), v.end(), item);
169 if (it == v.end()) return;
170 v.erase(it);
171 selected_.set(std::move(v));
172 }
173
175 void toggle(handle item) {
176 if (!item) return;
177 if (is_selected(item)) remove(item);
178 else add(std::move(item));
179 }
180
181 void clear() {
182 if (selected_.peek().empty()) return;
183 selected_.set({});
184 }
185
189 std::weak_ptr<bool> weak_alive = alive_;
190 auto rows = std::make_shared<std::vector<handle>>(source.snapshot());
191 source_sub_ = source.observe([this, rows, weak_alive](const ListChange<T>& ch) {
192 auto alive = weak_alive.lock();
193 if (!alive || !*alive) return;
194 detail::replay_list_change(*rows, ch);
196 ch.kind != ListChangeKind::Reset) return;
197 auto selected = selected_.peek();
198 const auto end = std::remove_if(selected.begin(), selected.end(), [&](const handle& item) {
199 return std::find(rows->begin(), rows->end(), item) == rows->end();
200 });
201 if (end == selected.end()) return;
202 selected.erase(end, selected.end());
203 selected_.set(std::move(selected));
204 });
205 }
206
207 void unbind() noexcept { source_sub_.release(); }
208
209private:
210 std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
212 Subscription source_sub_;
213};
214
215} // namespace aria
const reactive::Property< std::vector< handle > > & selected() const noexcept
Definition selection.hpp:142
MultiSelection & operator=(const MultiSelection &)=delete
void unbind() noexcept
Definition selection.hpp:207
std::size_t size() const
Definition selection.hpp:147
void add(handle item)
Add item to the selection (appended at the end of pick order).
Definition selection.hpp:157
std::vector< handle > values() const
Definition selection.hpp:146
bool is_selected(const handle &item) const
Definition selection.hpp:150
MultiSelection & operator=(MultiSelection &&)=delete
MultiSelection()=default
void toggle(handle item)
Toggle membership.
Definition selection.hpp:175
std::shared_ptr< T > handle
Definition selection.hpp:126
void bind_to(ObservableList< T > &source)
Keep selected handles that survive each replayed Remove/Replace/Reset.
Definition selection.hpp:188
void remove(const handle &item)
Remove item from the selection. No-op if not selected.
Definition selection.hpp:166
bool empty() const
Definition selection.hpp:148
~MultiSelection()
Definition selection.hpp:129
T value_type
Definition selection.hpp:125
reactive::Property< std::vector< handle > > & selected() noexcept
Reactive snapshot of selected handles, in pick order.
Definition selection.hpp:139
MultiSelection(const MultiSelection &)=delete
void clear()
Definition selection.hpp:181
MultiSelection(MultiSelection &&)=delete
Observable sequence of owning element handles.
Definition observable_list.hpp:40
std::vector< std::shared_ptr< T > > snapshot() const
Definition observable_list.hpp:72
T peek() const noexcept(std::is_nothrow_copy_constructible_v< T >)
Snapshot read that does NOT register a dependency.
Definition property.hpp:164
void set(const T &new_val)
Commit a new value.
Definition property.hpp:179
bool has_value() const
Definition selection.hpp:75
void select(handle item)
Select item (null clears).
Definition selection.hpp:79
T value_type
Definition selection.hpp:58
void bind_to(ObservableList< T > &source)
Follow source membership by replaying its owning events.
Definition selection.hpp:94
void unbind() noexcept
Detach from the bound source list (idempotent).
Definition selection.hpp:109
Selection & operator=(const Selection &)=delete
void clear()
Clear the selection.
Definition selection.hpp:82
reactive::Property< handle > & selected() noexcept
Reactive selected handle (null when nothing is selected).
Definition selection.hpp:70
~Selection()
Definition selection.hpp:62
handle value() const
Current selected element (may be null).
Definition selection.hpp:74
Selection(const Selection &)=delete
Selection(Selection &&)=delete
std::shared_ptr< T > handle
Definition selection.hpp:59
bool is_selected(const handle &item) const
True iff item is the current selection (by shared_ptr identity).
Definition selection.hpp:85
const reactive::Property< handle > & selected() const noexcept
Definition selection.hpp:71
Selection & operator=(Selection &&)=delete
Selection()=default
Definition property.hpp:103
Definition signal.hpp:12
@ Replace
Definition list_change.hpp:10
@ Remove
Definition list_change.hpp:10
@ Reset
Definition list_change.hpp:10
An owning event in a sequential list edit stream.
Definition list_change.hpp:16
ListChangeKind kind
Definition list_change.hpp:18