Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
AppKitTableSource.hpp
Go to the documentation of this file.
1#pragma once
2
3// AppKitTableSource.hpp — bridge any aria list source onto NSTableView.
4//
5// AppKit counterpart of `qt_list_model_adapter.hpp`. Accepts any source
6// satisfying `aria::ListSourceOf<L, T>` (ObservableList / FilteredList
7// / SortedList / MappedList) and turns `ListChange<T>` events into:
8//
9// Insert -> [NSTableView insertRowsAtIndexes:withAnimation:]
10// Remove -> [NSTableView removeRowsAtIndexes:withAnimation:]
11// Replace -> [NSTableView reloadDataForRowIndexes:columnIndexes:]
12// ItemChanged -> [NSTableView reloadDataForRowIndexes:columnIndexes:]
13// Move -> [NSTableView moveRowAtIndex:toIndex:]
14// Reset -> [NSTableView reloadData]
15//
16// Header is .mm-only (Cocoa imports). Header-only template — same
17// distribution model as `qt_list_model_adapter.hpp`.
18//
19// Construct and read on the main thread. Events own their payloads and
20// enter one FIFO queue, so a worker event cannot be overtaken by a later
21// main-thread event. Idle main-thread delivery remains synchronous.
22// Destruction retires pending work immediately; native data-source cleanup
23// is transferred to the main queue when destroyed from another thread.
24
26#include "aria/list_source.hpp"
28#include "aria/subscription.hpp"
29
30#import <Cocoa/Cocoa.h>
31
32#include <atomic>
33#include <cstddef>
34#include <functional>
35#include <deque>
36#include <mutex>
37#include <stdexcept>
38#include <memory>
39#include <utility>
40#include <vector>
41
42// ─── ObjC data-source / delegate ────────────────────────────────────────
43
44@interface AriaTableDataSource : NSObject <NSTableViewDataSource, NSTableViewDelegate>
45- (instancetype)initWithRowCount:(std::function<NSInteger()>)rowCountFn
46 viewForFn:(std::function<NSView*(NSTableView*,
47 NSTableColumn*,
48 NSInteger)>)viewForFn;
49@end
50
51namespace aria::adapters::appkit {
52
53template<typename T>
55public:
60 using ViewForRowFn = std::function<NSView*(NSTableView*,
61 NSTableColumn*,
62 std::shared_ptr<T>,
63 NSInteger /*row*/)>;
64
68 template<class L>
69 requires ::aria::ListSourceOf<L, T>
70 ObservableTableSource(NSTableView* tableView,
71 L& source,
72 ViewForRowFn view_for_row)
73 : state_(std::make_shared<State>()) {
74 if (![NSThread isMainThread]) throw std::logic_error("table bridge construction requires the main thread");
75 state_->events = std::make_shared<EventQueue>();
76 state_->events->target = state_;
77 state_->table = tableView;
78 state_->view_for_row = std::move(view_for_row);
79 state_->snapshot = source.snapshot();
80
81 // Native callbacks retain the state only for the duration of a call.
82 std::weak_ptr<State> weak_state = state_;
83
84 auto row_count_fn = [weak_state]() -> NSInteger {
85 if (auto s = weak_state.lock(); s && !s->detached.load(std::memory_order_acquire)) {
86 return static_cast<NSInteger>(s->snapshot.size());
87 }
88 return 0;
89 };
90 auto view_for_fn = [weak_state](NSTableView* tv,
91 NSTableColumn* col,
92 NSInteger row) -> NSView* {
93 auto s = weak_state.lock();
94 if (!s || s->detached.load(std::memory_order_acquire)) return nil;
95 if (row < 0
96 || static_cast<std::size_t>(row) >= s->snapshot.size()) {
97 return nil;
98 }
99 auto item = s->snapshot[static_cast<std::size_t>(row)];
100 if (!item) return nil;
101 return s->view_for_row(tv, col, item, row);
102 };
103
104 state_->ds = [[AriaTableDataSource alloc]
105 initWithRowCount:std::move(row_count_fn)
106 viewForFn:std::move(view_for_fn)];
107 sub_ = source.observe([events = state_->events](const ::aria::ListChange<T>& change) {
108 enqueue_(events, change);
109 });
110 tableView.dataSource = state_->ds;
111 tableView.delegate = state_->ds;
112 [tableView reloadData];
113 }
114
116 state_->detached.store(true, std::memory_order_release);
117 {
118 std::lock_guard lock(state_->events->mutex);
119 state_->events->stopped = true;
120 }
121 sub_.release();
122 if ([NSThread isMainThread]) {
123 cleanup_(*state_);
124 } else {
125 // Transfer the sole wrapper owner, not a temporary shared copy:
126 // the native data-source and renderer captures are released on main.
127 auto* owner = new std::shared_ptr<State>(std::move(state_));
128 dispatch_async_f(dispatch_get_main_queue(), owner, [](void* context) {
129 std::unique_ptr<std::shared_ptr<State>> state{static_cast<std::shared_ptr<State>*>(context)};
130 cleanup_(**state);
131 });
132 }
133 }
134
137
139 [[nodiscard]] std::size_t row_count() const noexcept {
140 return state_ ? state_->snapshot.size() : 0;
141 }
142
144 [[nodiscard]] std::shared_ptr<T> at(std::size_t i) const {
145 if (!state_) return nullptr;
146 if (i >= state_->snapshot.size()) return nullptr;
147 return state_->snapshot[i];
148 }
149
150private:
151 struct State;
152 struct EventQueue {
153 std::mutex mutex;
154 std::deque<::aria::ListChange<T>> changes;
155 std::weak_ptr<State> target;
156 bool scheduled = false;
157 bool stopped = false;
158 };
159 struct State {
160 NSTableView* __weak table = nil;
161 AriaTableDataSource* __strong ds = nil;
162 ViewForRowFn view_for_row;
163 std::vector<std::shared_ptr<T>> snapshot;
164 std::atomic<bool> detached{false};
165 std::shared_ptr<EventQueue> events;
166 };
167
168 static void cleanup_(State& state) {
169 std::deque<::aria::ListChange<T>> discarded;
170 {
171 std::lock_guard lock(state.events->mutex);
172 discarded.swap(state.events->changes);
173 }
174 NSTableView* table = state.table;
175 if (table.dataSource == state.ds) table.dataSource = nil;
176 if (table.delegate == state.ds) table.delegate = nil;
177 }
178
179 static void enqueue_(const std::shared_ptr<EventQueue>& events, const ::aria::ListChange<T>& change) {
180 {
181 std::lock_guard lock(events->mutex);
182 if (events->stopped) return;
183 events->changes.push_back(change);
184 if (events->scheduled) return;
185 events->scheduled = true;
186 }
187 if ([NSThread isMainThread]) drain_(events);
188 else {
189 auto pending = events; // Copy ownership into the block, not the reference parameter.
190 dispatch_async(dispatch_get_main_queue(), ^{ drain_(pending); });
191 }
192 }
193
194 static void drain_(const std::shared_ptr<EventQueue>& events) {
195 auto state = events->target.lock(); // Native State is only retained on main.
196 if (!state) return;
197 for (;;) {
198 ::aria::ListChange<T> change{};
199 {
200 std::lock_guard lock(events->mutex);
201 if (events->stopped || events->changes.empty()) {
202 events->scheduled = false;
203 return;
204 }
205 change = std::move(events->changes.front());
206 events->changes.pop_front();
207 }
208 if (state->detached.load(std::memory_order_acquire)) return;
209 try { apply_change_(*state, change); }
210 catch (...) { ::aria::report_callback_failure("appkit.table", std::current_exception()); }
211 }
212 }
213
214 static void apply_change_(State& s,
215 const ::aria::ListChange<T>& ch) {
216 using K = ::aria::ListChangeKind;
217 switch (ch.kind) {
218 case K::Insert: apply_insert_(s, ch.index, ch.item); return;
219 case K::Remove: apply_remove_(s, ch.index); return;
220 case K::Replace: apply_replace_(s, ch.index, ch.item); return;
221 case K::ItemChanged: apply_item_changed_(s, ch.index); return;
222 case K::Move: apply_move_(s, ch.from_index, ch.index); return;
223 case K::Reset: apply_reset_(s, ch); return;
224 }
225 }
226
227 static void apply_insert_(State& s,
228 std::size_t idx,
229 const std::shared_ptr<T>& item) {
230 if (idx > s.snapshot.size()) idx = s.snapshot.size();
231 s.snapshot.insert(s.snapshot.begin() + static_cast<std::ptrdiff_t>(idx),
232 item);
233 if (!s.table) return;
234 NSIndexSet* set = [NSIndexSet indexSetWithIndex:idx];
235 [s.table insertRowsAtIndexes:set
236 withAnimation:NSTableViewAnimationEffectFade];
237 }
238
239 static void apply_remove_(State& s, std::size_t idx) {
240 if (idx >= s.snapshot.size()) return;
241 s.snapshot.erase(s.snapshot.begin() + static_cast<std::ptrdiff_t>(idx));
242 if (!s.table) return;
243 NSIndexSet* set = [NSIndexSet indexSetWithIndex:idx];
244 [s.table removeRowsAtIndexes:set
245 withAnimation:NSTableViewAnimationEffectFade];
246 }
247
248 static void apply_replace_(State& s,
249 std::size_t idx,
250 const std::shared_ptr<T>& item) {
251 if (idx >= s.snapshot.size()) return;
252 s.snapshot[idx] = item;
253 if (!s.table) return;
254 NSIndexSet* rowSet = [NSIndexSet indexSetWithIndex:idx];
255 NSIndexSet* colSet = [NSIndexSet
256 indexSetWithIndexesInRange:NSMakeRange(0, s.table.numberOfColumns)];
257 [s.table reloadDataForRowIndexes:rowSet columnIndexes:colSet];
258 }
259
260 static void apply_item_changed_(State& s, std::size_t idx) {
261 if (idx >= s.snapshot.size()) return;
262 if (!s.table) return;
263 NSIndexSet* rowSet = [NSIndexSet indexSetWithIndex:idx];
264 NSIndexSet* colSet = [NSIndexSet
265 indexSetWithIndexesInRange:NSMakeRange(0, s.table.numberOfColumns)];
266 [s.table reloadDataForRowIndexes:rowSet columnIndexes:colSet];
267 }
268
269 static void apply_move_(State& s, std::size_t from, std::size_t to) {
270 if (from == to) return;
271 if (from >= s.snapshot.size() || to >= s.snapshot.size()) return;
272 auto moved = s.snapshot[from];
273 s.snapshot.erase(s.snapshot.begin() + static_cast<std::ptrdiff_t>(from));
274 s.snapshot.insert(s.snapshot.begin() + static_cast<std::ptrdiff_t>(to),
275 std::move(moved));
276 if (!s.table) return;
277 [s.table moveRowAtIndex:static_cast<NSInteger>(from)
278 toIndex:static_cast<NSInteger>(to)];
279 }
280
281 static void apply_reset_(State& s, const ::aria::ListChange<T>& change) {
282 if (!change.snapshot) throw std::logic_error("Reset requires an owned snapshot");
283 s.snapshot = *change.snapshot;
284 if (s.table) [s.table reloadData];
285 }
286
287 std::shared_ptr<State> state_;
288 ::aria::Subscription sub_;
289};
290
291} // namespace aria::adapters::appkit
Definition AppKitTableSource.hpp:54
ObservableTableSource(const ObservableTableSource &)=delete
std::function< NSView *(NSTableView *, NSTableColumn *, std::shared_ptr< T >, NSInteger)> ViewForRowFn
Render callback: given the row's shared_ptr<T>, the table view and the column being asked about,...
Definition AppKitTableSource.hpp:60
ObservableTableSource & operator=(const ObservableTableSource &)=delete
requires ::aria::ListSourceOf< L, T > ObservableTableSource(NSTableView *tableView, L &source, ViewForRowFn view_for_row)
Construct a binding between an aria list source and an NSTableView.
Definition AppKitTableSource.hpp:70
~ObservableTableSource()
Definition AppKitTableSource.hpp:115
std::shared_ptr< T > at(std::size_t i) const
Return the bridge's local snapshot at row i (or nullptr).
Definition AppKitTableSource.hpp:144
std::size_t row_count() const noexcept
Read the bridge's current row count (mainly for tests).
Definition AppKitTableSource.hpp:139
AriaTableDataSource
Definition AppKitTableSource.hpp:45
Definition AppKitAdapter.hpp:77
void report_callback_failure(std::string_view category, std::exception_ptr exception, std::string_view message={}) noexcept
Report a callback failure.
ListChangeKind
Definition list_change.hpp:10
Definition validation_key.hpp:110