morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
view_change.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file table/view_change.hpp
6/// @brief What changed between two view orders, as list-model operations.
7///
8/// A renderer's list model applies a `ViewChange` as keyed operations, so
9/// selection, focus and scroll position survive a sort or a filter. The
10/// change is computed from the old and new view orders and is minimal in
11/// moves: rows on a longest increasing subsequence of the new order stay where
12/// they are and only the rest move.
13///
14/// **Replay contract.** Apply the operations in order to the old list:
15/// - `Removed{first, count}` erases `count` rows at `first`;
16/// - `Moved{first, to}` erases the row at `first`, then inserts it at `to`
17/// (an index into the list after the erase);
18/// - `Inserted{first, count}` inserts the new view's rows `[first, first + count)`
19/// at `first`;
20/// - `Changed{first, count}` marks rows `[first, first + count)` of the new
21/// view as having new content, and changes no order;
22/// - `Reset` replaces the whole list with the new view.
23///
24/// All removals come first (back to front), then moves, then insertions
25/// (front to back), then changes. The result is the new view.
26///
27/// See `docs/spec/table/engine.md`.
28
29#include <algorithm>
30#include <cstddef>
31#include <cstdint>
32#include <limits>
33#include <span>
34#include <vector>
35
36namespace morph::table {
37
38// NOLINTBEGIN(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access) -- row and column indices are bounded by the snapshot's own counts, and the sort and filter loops index once per comparison, where at() would check every access
39
40/// @brief One list-model operation.
41struct ViewOp {
42 /// @brief The operation.
43 enum class Kind : std::uint8_t {
44 Removed, ///< `count` rows at `first` were removed.
45 Moved, ///< The row at `first` moved to `to`.
46 Inserted, ///< `count` rows were inserted at `first`.
47 Changed, ///< `count` rows at `first` have new content.
48 Reset, ///< The whole view was replaced.
49 };
50
51 /// @brief The operation.
53 /// @brief First row concerned.
54 std::size_t first = 0;
55 /// @brief Rows concerned (1 for `Moved`, 0 for `Reset`).
56 std::size_t count = 0;
57 /// @brief Destination of a `Moved` row, in the list after its removal.
58 std::size_t to = 0;
59
60 /// @brief Member-wise equality.
61 /// @param other The operation to compare with.
62 /// @return `true` when every member matches.
63 [[nodiscard]] bool operator==(ViewOp const& other) const = default;
64};
65
66/// @brief The operations that turn one view order into another.
67struct ViewChange {
68 /// @brief The operations, in replay order.
69 std::vector<ViewOp> ops;
70
71 /// @brief Whether nothing changed.
72 /// @return `true` when there are no operations.
73 [[nodiscard]] bool empty() const noexcept { return ops.empty(); }
74
75 /// @brief Whether the change is a whole-view reset.
76 /// @return `true` when the only operation is `Reset`.
77 [[nodiscard]] bool isReset() const noexcept { return ops.size() == 1 && ops.front().kind == ViewOp::Kind::Reset; }
78};
79
80namespace detail {
81
82/// @brief A Fenwick tree of slot occupancy: how many occupied slots precede a slot.
84public:
85 /// @brief An empty counter over @p size slots.
86 /// @param size Slot count.
87 explicit SlotCounter(std::size_t size) : _tree(size + 1, 0) {}
88
89 /// @brief Marks a slot occupied.
90 /// @param slot The slot.
91 void occupy(std::size_t slot) { change(slot, 1); }
92
93 /// @brief Marks a slot free.
94 /// @param slot The slot.
95 void release(std::size_t slot) { change(slot, -1); }
96
97 /// @brief Occupied slots before @p slot.
98 /// @param slot The slot.
99 /// @return The count.
100 [[nodiscard]] std::size_t before(std::size_t slot) const {
101 int sum = 0;
102 for (auto index = slot; index > 0; index -= index & (~index + 1)) {
103 sum += _tree[index];
104 }
105 return static_cast<std::size_t>(sum);
106 }
107
108private:
109 void change(std::size_t slot, int delta) { // NOLINT(bugprone-easily-swappable-parameters)
110 for (auto index = slot + 1; index < _tree.size(); index += index & (~index + 1)) {
111 _tree[index] += delta;
112 }
113 }
114
115 std::vector<int> _tree;
116};
117
118/// @brief Marks the elements of a longest strictly increasing subsequence.
119/// @param values The sequence.
120/// @return One flag per element; `true` on the subsequence.
121[[nodiscard]] inline std::vector<bool> longestIncreasing(std::span<std::uint32_t const> values) {
122 constexpr auto kNone = std::numeric_limits<std::size_t>::max();
123 std::vector<std::size_t> tails; // index into values of the smallest tail of each length
124 std::vector<std::size_t> parent(values.size(), kNone);
125 for (std::size_t i = 0; i < values.size(); ++i) {
126 auto const position =
127 std::ranges::lower_bound(tails, values[i], {}, [&](std::size_t index) { return values[index]; });
128 auto const length = static_cast<std::size_t>(position - tails.begin());
129 if (length > 0) {
130 parent[i] = tails[length - 1];
131 }
132 if (position == tails.end()) {
133 tails.push_back(i);
134 } else {
135 *position = i;
136 }
137 }
138 std::vector<bool> marked(values.size(), false);
139 if (!tails.empty()) {
140 for (auto index = tails.back(); index != kNone; index = parent[index]) {
141 marked[index] = true;
142 }
143 }
144 return marked;
145}
146
147/// @brief Marks an identity absent from a view.
148inline constexpr auto kAbsent = std::numeric_limits<std::uint32_t>::max();
149
150/// @brief Where each identity sits in each view.
152 /// @brief Position in the old view, or `kAbsent`.
153 std::vector<std::uint32_t> before;
154 /// @brief Position in the new view, or `kAbsent`.
155 std::vector<std::uint32_t> after;
156};
157
158/// @brief Appends the removals, back to front, coalesced into runs.
159/// @param before The old view.
160/// @param positions Where identities sit.
161/// @param out Receives the operations.
162/// @param limit Most operations @p out may hold; past it the scan stops.
163/// @return `false` when the removals take @p out past @p limit.
164[[nodiscard]] inline bool appendRemovals(std::span<std::uint32_t const> before, ViewPositions const& positions,
165 std::vector<ViewOp>& out, std::size_t limit) {
166 for (std::size_t end = before.size(); end > 0;) {
167 if (positions.after[before[end - 1]] != kAbsent) {
168 --end;
169 continue;
170 }
171 std::size_t start = end - 1;
172 while (start > 0 && positions.after[before[start - 1]] == kAbsent) {
173 --start;
174 }
175 out.push_back(ViewOp{.kind = ViewOp::Kind::Removed, .first = start, .count = end - start, .to = 0});
176 if (out.size() > limit) {
177 return false;
178 }
179 end = start;
180 }
181 return true;
182}
183
184/// @brief Appends the moves that put the kept rows in their new order.
185///
186/// @p ranks lists the kept rows in old order, as their rank in the new order.
187/// Rows on a longest increasing subsequence stay. Between two stationary rows
188/// is a gap; each moving row leaves a slot in the gap it was in and takes a
189/// slot in the gap it belongs to. Within a gap, rows already moved (by rank)
190/// sit before rows not yet moved (by old position), so every row's index is a
191/// count of occupied slots before its own.
192/// @param ranks The kept rows, in old order, as new-order ranks.
193/// @param out Receives the operations.
194/// @param limit Most operations @p out may hold; past it no move is computed.
195/// @return `false` when the moves would take @p out past @p limit.
196[[nodiscard]] inline bool appendMoves(std::span<std::uint32_t const> ranks, std::vector<ViewOp>& out,
197 std::size_t limit) {
198 auto const stationary = longestIncreasing(ranks);
199 auto const stationaryCount = static_cast<std::size_t>(std::ranges::count(stationary, true));
200 // Counting first keeps a full re-sort, which is a reset anyway, from
201 // computing every move before discarding them.
202 if (out.size() + (ranks.size() - stationaryCount) > limit) {
203 return false;
204 }
205 auto const kept = ranks.size();
206 std::vector<std::size_t> oldGap(kept);
207 std::vector<std::size_t> stationaryBeforeRank(kept + 1, 0);
208 std::size_t seen = 0;
209 for (std::size_t i = 0; i < kept; ++i) {
210 oldGap[i] = seen;
211 if (stationary[i]) {
212 stationaryBeforeRank[ranks[i] + 1] = 1;
213 ++seen;
214 }
215 }
216 for (std::size_t rank = 0; rank < kept; ++rank) {
217 stationaryBeforeRank[rank + 1] += stationaryBeforeRank[rank];
218 }
219 // Slot layout per gap: [moved slots][unmoved slots][stationary slot].
220 std::vector<std::size_t> movedInGap(stationaryCount + 1, 0);
221 std::vector<std::size_t> unmovedInGap(stationaryCount + 1, 0);
222 for (std::size_t i = 0; i < kept; ++i) {
223 if (!stationary[i]) {
224 ++movedInGap[stationaryBeforeRank[ranks[i]]];
225 ++unmovedInGap[oldGap[i]];
226 }
227 }
228 std::vector<std::size_t> gapBase(stationaryCount + 2, 0);
229 for (std::size_t gap = 0; gap <= stationaryCount; ++gap) {
230 gapBase[gap + 1] = gapBase[gap] + movedInGap[gap] + unmovedInGap[gap] + (gap < stationaryCount ? 1 : 0);
231 }
232 SlotCounter occupancy{gapBase[stationaryCount + 1]};
233 std::vector<std::size_t> unmovedSlot(kept, 0);
234 std::vector<std::size_t> nextUnmoved(stationaryCount + 1, 0);
235 std::vector<std::size_t> indexOfRank(kept, 0);
236 for (std::size_t i = 0; i < kept; ++i) {
237 auto const gap = oldGap[i];
238 indexOfRank[ranks[i]] = i;
239 if (stationary[i]) {
240 occupancy.occupy(gapBase[gap] + movedInGap[gap] + unmovedInGap[gap]);
241 } else {
242 unmovedSlot[i] = gapBase[gap] + movedInGap[gap] + nextUnmoved[gap]++;
243 occupancy.occupy(unmovedSlot[i]);
244 }
245 }
246 std::vector<std::size_t> nextMoved(stationaryCount + 1, 0);
247 for (std::size_t rank = 0; rank < kept; ++rank) {
248 auto const i = indexOfRank[rank];
249 if (stationary[i]) {
250 continue;
251 }
252 auto const from = occupancy.before(unmovedSlot[i]);
253 occupancy.release(unmovedSlot[i]);
254 auto const gap = stationaryBeforeRank[rank];
255 auto const movedSlot = gapBase[gap] + nextMoved[gap]++;
256 auto const destination = occupancy.before(movedSlot);
257 occupancy.occupy(movedSlot);
258 out.push_back(ViewOp{.kind = ViewOp::Kind::Moved, .first = from, .count = 1, .to = destination});
259 }
260 return true;
261}
262
263/// @brief Appends the insertions, front to back, coalesced into runs.
264/// @param after The new view.
265/// @param positions Where identities sit.
266/// @param out Receives the operations.
267/// @param limit Most operations @p out may hold; past it the scan stops.
268/// @return `false` when the insertions take @p out past @p limit.
269[[nodiscard]] inline bool appendInsertions(std::span<std::uint32_t const> after, ViewPositions const& positions,
270 std::vector<ViewOp>& out, std::size_t limit) {
271 for (std::size_t start = 0; start < after.size();) {
272 if (positions.before[after[start]] != kAbsent) {
273 ++start;
274 continue;
275 }
276 std::size_t end = start + 1;
277 while (end < after.size() && positions.before[after[end]] == kAbsent) {
278 ++end;
279 }
280 out.push_back(ViewOp{.kind = ViewOp::Kind::Inserted, .first = start, .count = end - start, .to = 0});
281 if (out.size() > limit) {
282 return false;
283 }
284 start = end;
285 }
286 return true;
287}
288
289/// @brief Appends the changes of rows in both views, in new positions, coalesced into runs.
290/// @param changed Identities whose content changed.
291/// @param positions Where identities sit.
292/// @param out Receives the operations.
293inline void appendChanges(std::span<std::uint32_t const> changed, ViewPositions const& positions,
294 std::vector<ViewOp>& out) {
295 std::vector<std::uint32_t> changedAt;
296 for (auto const identity : changed) {
297 if (identity < positions.after.size() && positions.after[identity] != kAbsent &&
298 positions.before[identity] != kAbsent) {
299 changedAt.push_back(positions.after[identity]);
300 }
301 }
302 std::ranges::sort(changedAt);
303 auto const [dupFirst, dupLast] = std::ranges::unique(changedAt);
304 changedAt.erase(dupFirst, dupLast);
305 for (std::size_t start = 0; start < changedAt.size();) {
306 std::size_t end = start + 1;
307 while (end < changedAt.size() && changedAt[end] == changedAt[end - 1] + 1) {
308 ++end;
309 }
310 out.push_back(ViewOp{.kind = ViewOp::Kind::Changed, .first = changedAt[start], .count = end - start, .to = 0});
311 start = end;
312 }
313}
314
315} // namespace detail
316
317/// @brief Computes the operations that turn @p before into @p after.
318///
319/// Rows are identified by integers below @p identityLimit, the same in both
320/// orders: a source row index when both views come from one snapshot, or a
321/// mapping through `RowId` when they do not.
322/// @param before The old view order.
323/// @param after The new view order.
324/// @param changed Identities whose content changed; those in both views
325/// are reported as `Changed` in their new position.
326/// @param identityLimit One more than the largest identity.
327/// @param resetThreshold More operations than this give one `Reset`.
328/// @return The change.
329// NOLINTBEGIN(bugprone-easily-swappable-parameters) -- old then new view, then the identity bound and the threshold, as documented above
330[[nodiscard]] inline ViewChange diffViews(std::span<std::uint32_t const> before, std::span<std::uint32_t const> after,
331 std::span<std::uint32_t const> changed, std::size_t identityLimit,
332 std::size_t resetThreshold) {
333 // NOLINTEND(bugprone-easily-swappable-parameters)
334 ViewChange out;
335 detail::ViewPositions positions{.before = std::vector<std::uint32_t>(identityLimit, detail::kAbsent),
336 .after = std::vector<std::uint32_t>(identityLimit, detail::kAbsent)};
337 for (std::size_t i = 0; i < after.size(); ++i) {
338 positions.after[after[i]] = static_cast<std::uint32_t>(i);
339 }
340 for (std::size_t i = 0; i < before.size(); ++i) {
341 positions.before[before[i]] = static_cast<std::uint32_t>(i);
342 }
343 bool fits = detail::appendRemovals(before, positions, out.ops, resetThreshold);
344 if (fits) {
345 // The kept rows in old order, as ranks in the new order.
346 std::vector<std::uint32_t> rankOf(identityLimit, detail::kAbsent);
347 std::uint32_t rank = 0;
348 for (auto const identity : after) {
349 if (positions.before[identity] != detail::kAbsent) {
350 rankOf[identity] = rank++;
351 }
352 }
353 std::vector<std::uint32_t> ranks;
354 ranks.reserve(rank);
355 for (auto const identity : before) {
356 if (rankOf[identity] != detail::kAbsent) {
357 ranks.push_back(rankOf[identity]);
358 }
359 }
360 fits = detail::appendMoves(ranks, out.ops, resetThreshold);
361 }
362 if (fits) {
363 fits = detail::appendInsertions(after, positions, out.ops, resetThreshold);
364 }
365 if (fits) {
366 detail::appendChanges(changed, positions, out.ops);
367 }
368 if (!fits || out.ops.size() > resetThreshold) {
369 out.ops.assign(1, ViewOp{.kind = ViewOp::Kind::Reset, .first = 0, .count = 0, .to = 0});
370 }
371 return out;
372}
373
374/// @brief Applies @p change to @p list, following the replay contract.
375///
376/// The reference implementation of what a renderer's list model does; the
377/// tests replay every change through it.
378/// @tparam T Element type.
379/// @param list The old view's elements; becomes the new view's.
380/// @param change The change.
381/// @param after The new view's elements, where inserted and reset rows come from.
382template <typename T>
383void applyViewChange(std::vector<T>& list, ViewChange const& change, std::span<T const> after) {
384 for (auto const& op : change.ops) {
385 auto const first = static_cast<std::ptrdiff_t>(op.first);
386 switch (op.kind) {
388 list.erase(list.begin() + first, list.begin() + first + static_cast<std::ptrdiff_t>(op.count));
389 break;
390 case ViewOp::Kind::Moved: {
391 T moved = std::move(list[op.first]);
392 list.erase(list.begin() + first);
393 list.insert(list.begin() + static_cast<std::ptrdiff_t>(op.to), std::move(moved));
394 break;
395 }
397 list.insert(list.begin() + first, after.begin() + first,
398 after.begin() + first + static_cast<std::ptrdiff_t>(op.count));
399 break;
401 list.assign(after.begin(), after.end());
402 break;
404 default:
405 break;
406 }
407 }
408}
409
410// NOLINTEND(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
411
412} // namespace morph::table
A Fenwick tree of slot occupancy: how many occupied slots precede a slot.
void release(std::size_t slot)
Marks a slot free.
SlotCounter(std::size_t size)
An empty counter over size slots.
std::size_t before(std::size_t slot) const
Occupied slots before slot.
void occupy(std::size_t slot)
Marks a slot occupied.
The operations that turn one view order into another.
bool isReset() const noexcept
Whether the change is a whole-view reset.
bool empty() const noexcept
Whether nothing changed.
std::vector< ViewOp > ops
The operations, in replay order.
One list-model operation.
std::size_t count
Rows concerned (1 for Moved, 0 for Reset).
std::size_t first
First row concerned.
Kind kind
The operation.
std::size_t to
Destination of a Moved row, in the list after its removal.
bool operator==(ViewOp const &other) const =default
Member-wise equality.
@ Inserted
count rows were inserted at first.
@ Reset
The whole view was replaced.
@ Changed
count rows at first have new content.
@ Removed
count rows at first were removed.
@ Moved
The row at first moved to to.
Where each identity sits in each view.
std::vector< std::uint32_t > after
Position in the new view, or kAbsent.
std::vector< std::uint32_t > before
Position in the old view, or kAbsent.
ViewChange diffViews(std::span< std::uint32_t const > before, std::span< std::uint32_t const > after, std::span< std::uint32_t const > changed, std::size_t identityLimit, std::size_t resetThreshold)
Computes the operations that turn before into after.
bool appendRemovals(std::span< std::uint32_t const > before, ViewPositions const &positions, std::vector< ViewOp > &out, std::size_t limit)
Appends the removals, back to front, coalesced into runs.
bool appendMoves(std::span< std::uint32_t const > ranks, std::vector< ViewOp > &out, std::size_t limit)
Appends the moves that put the kept rows in their new order.
constexpr auto kAbsent
Marks an identity absent from a view.
bool appendInsertions(std::span< std::uint32_t const > after, ViewPositions const &positions, std::vector< ViewOp > &out, std::size_t limit)
Appends the insertions, front to back, coalesced into runs.
std::vector< bool > longestIncreasing(std::span< std::uint32_t const > values)
Marks the elements of a longest strictly increasing subsequence.
void appendChanges(std::span< std::uint32_t const > changed, ViewPositions const &positions, std::vector< ViewOp > &out)
Appends the changes of rows in both views, in new positions, coalesced into runs.
void applyViewChange(std::vector< T > &list, ViewChange const &change, std::span< T const > after)
Applies change to list, following the replay contract.