morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
recording_backend.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <algorithm>
5#include <cctype>
6#include <cstddef>
7#include <cstdint>
8#include <format>
9#include <functional>
10#include <initializer_list>
11#include <map>
12#include <memory>
13#include <optional>
14#include <stdexcept>
15#include <string>
16#include <string_view>
17#include <unordered_map>
18#include <unordered_set>
19#include <utility>
20#include <variant>
21#include <vector>
22
24#include "../backend.hpp"
25#include "../view.hpp"
26
27/// @file
28/// @brief `morph::ui::testing::RecordingBackend`: a headless widget tree with an operation log, a golden dump and
29/// user-interaction helpers — the test double and the reference for every real backend.
30///
31/// Specified in `docs/spec/ui/backend_contract.md`, "RecordingBackend".
32
33namespace morph::ui::testing {
34
35namespace detail {
36
37/// @brief Properties fixed when a widget is created, as name and formatted value.
38using PropList = std::vector<std::pair<std::string, std::string>>;
39
40/// @brief Formats a flag.
41/// @param value The flag.
42/// @return `true` or `false`.
43[[nodiscard]] inline std::string formatBool(bool value) { return value ? "true" : "false"; }
44
45/// @brief Escapes text with a backslash: a line break becomes backslash and `n`, a carriage return backslash and `r`,
46/// and a backslash or a character @p isSpecial selects is preceded by a backslash.
47/// @tparam Special A callable taking a `char` and returning whether to escape it.
48/// @param text The text.
49/// @param isSpecial Selects the characters to escape besides the backslash and the two line-ending characters.
50/// @return The escaped text.
51template <typename Special>
52[[nodiscard]] std::string escape(std::string_view text, Special const& isSpecial) {
53 std::string escaped;
54 escaped.reserve(text.size());
55 for (char const character : text) {
56 if (character == '\n') {
57 escaped += "\\n";
58 } else if (character == '\r') {
59 escaped += "\\r";
60 } else {
61 if (character == '\\' || isSpecial(character)) {
62 escaped += '\\';
63 }
64 escaped += character;
65 }
66 }
67 return escaped;
68}
69
70/// @brief Formats free text as a property value. Line breaks and `=` are escaped, so the value stays on its own line
71/// and no part of it reads as another `name=value`.
72/// @param text The text, UTF-8.
73/// @return The escaped text.
74[[nodiscard]] inline std::string formatText(std::string_view text) {
75 return escape(text, [](char character) { return character == '='; });
76}
77
78/// @brief Formats free text as an element of a `formatList` list. Also escapes `,`, `[` and `]`, so the text reads as
79/// one element.
80/// @param text The text, UTF-8.
81/// @return The escaped text.
82[[nodiscard]] inline std::string formatItem(std::string_view text) {
83 return escape(text, [](char character) { return std::string_view{"=,[]"}.contains(character); });
84}
85
86/// @brief Formats a menu's entries: `[a,b]`, each entry its label followed by the attributes that differ from the
87/// default, as `;icon=x`, `;keys=x`, `;checked=true` or `;checked=false`, and `;enabled=false`, then its
88/// submenu in brackets. A flat menu of plain entries reads `[Open,Quit]`.
89/// @param entries The entries.
90/// @return Their text; `=`, `,`, `[`, `]` and `;` in a label or attribute are escaped.
91// NOLINTNEXTLINE(misc-no-recursion): a submenu is formatted as a menu one level down.
92[[nodiscard]] inline std::string formatMenu(std::vector<MenuEntry> const& entries) {
93 auto const text = [](std::string_view value) {
94 return escape(value, [](char character) { return std::string_view{"=,[];"}.contains(character); });
95 };
96 std::string formatted = "[";
97 for (MenuEntry const& entry : entries) {
98 formatted += formatted.size() == 1 ? "" : ",";
99 formatted += text(entry.label);
100 if (!entry.icon.empty()) {
101 formatted += ";icon=" + text(entry.icon);
102 }
103 if (!entry.keys.empty()) {
104 formatted += ";keys=" + text(entry.keys);
105 }
106 if (entry.checked.has_value()) {
107 formatted += std::string{";checked="} + (*entry.checked ? "true" : "false");
108 }
109 if (!entry.enabled) {
110 formatted += ";enabled=false";
111 }
112 if (!entry.items.empty()) {
113 formatted += formatMenu(entry.items);
114 }
115 }
116 return formatted + "]";
117}
118
119/// @brief Formats a key: an integer in decimal, a string in double quotes, so `7` and `"7"` differ.
120/// @param key The key.
121/// @return Its text. A string key is escaped as `formatItem` escapes, and a double quote in it is preceded by a
122/// backslash.
123[[nodiscard]] inline std::string formatKey(Key const& key) {
124 if (auto const* const number = std::get_if<std::int64_t>(&key); number != nullptr) {
125 return std::to_string(*number);
126 }
127 return '"' +
128 escape(std::get<std::string>(key),
129 [](char character) { return std::string_view{"=,[]\""}.contains(character); }) +
130 '"';
131}
132
133/// @brief Formats an optional key.
134/// @param key The key, or none.
135/// @return `formatKey(*key)`, or `none`.
136[[nodiscard]] inline std::string formatOptionalKey(std::optional<Key> const& key) {
137 return key.has_value() ? formatKey(*key) : std::string{"none"};
138}
139
140/// @brief Formats one dimension's sizing.
141/// @param sizing The sizing.
142/// @return `content`, `fixed(n)` or `stretch(n)`.
143[[nodiscard]] inline std::string formatSizing(Sizing sizing) {
144 switch (sizing.kind) {
146 return "content";
148 return "fixed(" + std::to_string(sizing.amount) + ")";
150 return "stretch(" + std::to_string(sizing.amount) + ")";
151 default:
152 return "unknown";
153 }
154}
155
156/// @brief Formats an alignment.
157/// @param align The alignment.
158/// @return `stretch`, `start`, `center` or `end`.
159[[nodiscard]] inline std::string formatAlign(Align align) {
160 switch (align) {
161 case Align::Start:
162 return "start";
163 case Align::Center:
164 return "center";
165 case Align::End:
166 return "end";
167 case Align::Stretch:
168 default:
169 return "stretch";
170 }
171}
172
173/// @brief Formats a justification.
174/// @param justify The justification.
175/// @return `start`, `center`, `end` or `between`.
176[[nodiscard]] inline std::string formatJustify(Justify justify) {
177 switch (justify) {
178 case Justify::Center:
179 return "center";
180 case Justify::End:
181 return "end";
182 case Justify::Between:
183 return "between";
184 case Justify::Start:
185 default:
186 return "start";
187 }
188}
189
190/// @brief Formats a size request.
191/// @param hints The request.
192/// @return `width/height`, each as `formatSizing` gives it and followed by ` min=<n>` and ` max=<n>` for the bounds
193/// it has, then ` self=<align>` when the node places itself, such as `stretch(1) max=44/content self=center`.
194[[nodiscard]] inline std::string formatLayout(LayoutHints const& hints) {
195 auto const bounds = [](std::optional<int> minimum, std::optional<int> maximum) {
196 std::string text;
197 if (minimum) {
198 text += " min=" + std::to_string(*minimum);
199 }
200 if (maximum) {
201 text += " max=" + std::to_string(*maximum);
202 }
203 return text;
204 };
205 return formatSizing(hints.width) + bounds(hints.minWidth, hints.maxWidth) + "/" + formatSizing(hints.height) +
206 bounds(hints.minHeight, hints.maxHeight) +
207 (hints.alignSelf ? " self=" + formatAlign(*hints.alignSelf) : std::string{});
208}
209
210/// @brief Formats a date-time value.
211/// @param value The value.
212/// @return `none` for `nullopt`, `empty` for an empty `Timestamp`, else the ISO-8601 UTC instant.
213[[nodiscard]] inline std::string formatTimestamp(std::optional<morph::time::Timestamp> const& value) {
214 if (!value.has_value()) {
215 return "none";
216 }
217 if (!value->hasValue()) {
218 return "empty";
219 }
220 return (**value).toIso8601();
221}
222
223/// @brief Formats a list as `[a,b,c]`.
224/// @tparam T The element type.
225/// @tparam F A callable turning a `T const&` into a `std::string`.
226/// @param items The elements.
227/// @param each Formats one element.
228/// @return The list's text.
229template <typename T, typename F>
230[[nodiscard]] std::string formatList(std::vector<T> const& items, F const& each) {
231 std::string text = "[";
232 bool first = true;
233 for (T const& item : items) {
234 if (!first) {
235 text += ',';
236 }
237 first = false;
238 text += each(item);
239 }
240 return text + "]";
241}
242
243/// @brief A button emphasis's name.
244/// @param emphasis The emphasis.
245/// @return The enumerator's name.
246[[nodiscard]] inline std::string enumName(ButtonEmphasis emphasis) {
247 switch (emphasis) {
249 return "Primary";
251 return "Danger";
253 return "Ghost";
255 default:
256 return "Normal";
257 }
258}
259
260/// @brief A button style's name.
261/// @param style The style.
262/// @return The enumerator's name.
263[[nodiscard]] inline std::string enumName(ButtonStyle style) { return style == ButtonStyle::Item ? "Item" : "Push"; }
264
265/// @brief A text role's name.
266/// @param role The role.
267/// @return The enumerator's name.
268[[nodiscard]] inline std::string enumName(TextRole role) {
269 switch (role) {
270 case TextRole::Normal:
271 return "Normal";
272 case TextRole::Muted:
273 return "Muted";
275 return "Heading";
276 case TextRole::Error:
277 return "Error";
279 return "Success";
281 return "Section";
282 default:
283 return "unknown";
284 }
285}
286
287/// @brief A text input mode's name.
288/// @param mode The mode.
289/// @return The enumerator's name.
290[[nodiscard]] inline std::string enumName(TextInputMode mode) {
291 switch (mode) {
293 return "SingleLine";
295 return "Multiline";
297 return "Password";
298 default:
299 return "unknown";
300 }
301}
302
303/// @brief A select style's name.
304/// @param style The style.
305/// @return The enumerator's name.
306[[nodiscard]] inline std::string enumName(SelectStyle style) {
307 switch (style) {
309 return "Dropdown";
311 return "Radio";
312 default:
313 return "unknown";
314 }
315}
316
317/// @brief A tone's name.
318/// @param tone The tone.
319/// @return The enumerator's name.
320[[nodiscard]] inline std::string enumName(Tone tone) {
321 switch (tone) {
322 case Tone::Neutral:
323 return "Neutral";
324 case Tone::Info:
325 return "Info";
326 case Tone::Ok:
327 return "Ok";
328 case Tone::Warn:
329 return "Warn";
330 case Tone::Err:
331 return "Err";
332 default:
333 return "unknown";
334 }
335}
336
337/// @brief A side's name.
338/// @param side The side.
339/// @return The enumerator's name.
340[[nodiscard]] inline std::string enumName(Side side) {
341 switch (side) {
342 case Side::Start:
343 return "Start";
344 case Side::End:
345 return "End";
346 default:
347 return "unknown";
348 }
349}
350
351/// @brief A step state's name.
352/// @param state The state.
353/// @return The enumerator's name.
354[[nodiscard]] inline std::string enumName(StepState state) {
355 switch (state) {
357 return "Pending";
359 return "Active";
360 case StepState::Done:
361 return "Done";
363 return "Failed";
364 default:
365 return "unknown";
366 }
367}
368
369/// @brief A fraction as its shortest decimal text, or `none`.
370/// @param value The fraction.
371/// @return The text, such as `0.25`.
372[[nodiscard]] inline std::string formatFraction(std::optional<double> value) {
373 if (!value.has_value()) {
374 return "none";
375 }
376 return std::format("{}", *value);
377}
378
379/// @brief An axis's name.
380/// @param axis The axis.
381/// @return The enumerator's name.
382[[nodiscard]] inline std::string enumName(Axis axis) {
383 switch (axis) {
384 case Axis::Vertical:
385 return "Vertical";
386 case Axis::Horizontal:
387 return "Horizontal";
388 default:
389 return "unknown";
390 }
391}
392
393/// @brief A date mode's name.
394/// @param mode The mode.
395/// @return The enumerator's name.
396[[nodiscard]] inline std::string enumName(DateMode mode) {
397 switch (mode) {
398 case DateMode::Date:
399 return "Date";
401 return "DateTime";
402 default:
403 return "unknown";
404 }
405}
406
407/// @brief A file picker mode's name.
408/// @param mode The mode.
409/// @return The enumerator's name.
410[[nodiscard]] inline std::string enumName(FilePickerMode mode) {
411 switch (mode) {
413 return "Open";
415 return "Save";
416 default:
417 return "unknown";
418 }
419}
420
421/// @brief A selection mode's name.
422/// @param mode The mode.
423/// @return The enumerator's name.
424[[nodiscard]] inline std::string enumName(SelectionMode mode) {
425 switch (mode) {
427 return "None";
429 return "Single";
431 return "Multiple";
432 default:
433 return "unknown";
434 }
435}
436
437/// @brief The handlers a widget was given, which the interaction helpers call.
438struct Callbacks {
439 /// @brief From `ButtonWidget::setOnClick`.
441 /// @brief From `TextInputWidget::setOnChange`.
442 std::function<void(std::string)> change;
443 /// @brief From `TextInputWidget::setOnSubmit`.
444 std::function<void(std::string)> submit;
445 /// @brief From `TextInputWidget::setOnCommit`.
446 std::function<void(std::string)> commit;
447 /// @brief From `CheckboxWidget::setOnToggle`, `PanelWidget::setOnToggle` or `CollapsibleWidget::setOnToggle`.
448 std::function<void(bool)> toggle;
449 /// @brief From `SelectWidget::setOnSelect`.
450 std::function<void(Key)> select;
451 /// @brief From `SelectWidget::setOnOpen`.
453 /// @brief From `TabsWidget::setOnSelect`.
454 std::function<void(std::size_t)> index;
455 /// @brief From `DateTimeInputWidget::setOnChange`.
456 std::function<void(std::optional<morph::time::Timestamp>)> dateTime;
457 /// @brief From `SliderWidget::setOnChange`.
458 std::function<void(std::int64_t)> slide;
459 /// @brief From `SliderWidget::setOnCommit`.
460 std::function<void(std::int64_t)> slideCommit;
461 /// @brief From `FilePickerWidget::setOnPicked`.
462 std::function<void(std::string)> picked;
463 /// @brief From `DialogWidget::setOnDismiss`, `DrawerWidget::setOnDismiss` or `BannerWidget::setOnDismiss`.
465 /// @brief From `TableWidget::setOnSelectionChange`.
466 std::function<void(std::vector<Key>)> selection;
467 /// @brief From `TableWidget::setOnActivate`.
468 std::function<void(Key)> activate;
469 /// @brief The drop handler's predicate, from `Widget::setDropHandler`.
470 std::function<bool(Key const&)> accepts;
471 /// @brief The drop handler, from `Widget::setDropHandler`.
472 std::function<void(Key)> drop;
473 /// @brief From `CustomWidget::setOnEvent`: the event's name and its payload.
474 std::function<void(std::string, std::string)> component;
475 /// @brief The chord handler, from `Widget::setKeys`.
476 std::function<void(std::string)> chord;
477 /// @brief From `Widget::setOnHover`.
478 std::function<void()> hover;
479 /// @brief A menu's activation handler, from `MenuWidget::setOnActivate`.
480 std::function<void(std::vector<std::size_t>)> path;
481 /// @brief An action button's handler, from `BannerWidget::setOnAction` or `EmptyStateWidget::setOnAction`.
483 /// @brief From `SplitterWidget::setOnResize`.
484 std::function<void(std::vector<int>)> resize;
485 /// @brief From `DropZoneWidget::setOnDrop`.
486 std::function<void(std::vector<std::string>)> files;
487};
488
489/// @brief One fake widget's state.
490struct Record {
491 /// @brief The widget's id: its creation number, from 1.
492 int id = 0;
493 /// @brief The kind shown in the log and the dump.
494 std::string kind;
495 /// @brief The parent's id; 0 for a root.
496 int parent = 0;
497 /// @brief The children's ids, in order.
498 std::vector<int> children;
499 /// @brief Every property set so far, formatted, in name order.
500 std::map<std::string, std::string, std::less<>> props;
501 /// @brief The handlers set so far.
503 /// @brief The drag key, as `setDragKey` last set it.
504 std::optional<Key> dragKey;
505 /// @brief The chords, as `setKeys` last set them.
506 std::vector<std::string> chords;
507 /// @brief A menu's entries, as `setItems` last set them.
508 std::vector<MenuEntry> menu;
509 /// @brief The fake widget object.
510 Widget* widget = nullptr;
511 /// @brief Called with a child's id once that child is destroyed; a fake table re-marks its selection with it.
512 std::function<void(int)> onChildDestroyed;
513};
514
515/// @brief The records, the roots and the operation log that every fake widget of one backend writes to.
516///
517/// Held by `shared_ptr` from the backend and from every fake, so a fake that outlives its backend still has
518/// somewhere to record its destruction.
520public:
521 /// @brief Records a new widget appended to @p parent and logs its creation.
522 /// @param kind The kind shown in the log and the dump.
523 /// @param parent The container it is appended to, or null for a root.
524 /// @param widget The fake widget.
525 /// @param initial Properties fixed at creation; recorded without a log line.
526 /// @return The new widget's id.
527 int create(std::string kind, ContainerWidget const* parent, Widget& widget, PropList initial) {
528 int const widgetId = ++_nextId;
529 int const parentId = parent == nullptr ? 0 : idOf(*parent);
530 Record record{.id = widgetId, .kind = std::move(kind), .parent = parentId, .widget = &widget};
531 for (auto& [name, value] : initial) {
532 record.props.insert_or_assign(std::move(name), std::move(value));
533 }
534 _log.push_back("create " + label(record) + " in " +
535 (parentId == 0 ? std::string{"root"} : label(at(parentId))));
536 siblingsOf(parentId).push_back(widgetId);
537 _ids.insert_or_assign(&widget, widgetId);
538 _records.insert_or_assign(widgetId, std::move(record));
539 return widgetId;
540 }
541
542 /// @brief Sets a property through a widget setter, and logs it.
543 /// @param widgetId The widget.
544 /// @param name The property.
545 /// @param value Its formatted value.
546 void set(int widgetId, std::string_view name, std::string value) {
547 Record& record = at(widgetId);
548 _log.push_back("set " + label(record) + " " + std::string{name} + "=" + value);
549 record.props.insert_or_assign(std::string{name}, std::move(value));
550 }
551
552 /// @brief Sets a property the user changed through the widget itself; no setter ran, so nothing is logged.
553 /// @param widgetId The widget.
554 /// @param name The property.
555 /// @param value Its formatted value.
556 void setByUser(int widgetId, std::string_view name, std::string value) {
557 at(widgetId).props.insert_or_assign(std::string{name}, std::move(value));
558 }
559
560 /// @brief Moves a widget among its siblings, as `ContainerWidget::moveChild` defines it, and logs it.
561 /// @param widgetId The widget.
562 /// @param index The new position; past the end means last.
563 // An id and a position: an `int` and a `std::size_t`, which convert into each other.
564 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
565 void move(int widgetId, std::size_t index) {
566 Record const& record = at(widgetId);
567 std::vector<int>& siblings = siblingsOf(record.parent);
568 std::erase(siblings, widgetId);
569 auto const position = static_cast<std::ptrdiff_t>(std::min(index, siblings.size()));
570 siblings.insert(siblings.begin() + position, widgetId);
571 _log.push_back("move " + label(record) + " to " + std::to_string(index));
572 }
573
574 /// @brief Forgets a destroyed widget and logs it. Its remaining children become roots.
575 /// @param widgetId The widget; an unknown id does nothing.
576 void destroy(int widgetId) {
577 auto const found = _records.find(widgetId);
578 if (found == _records.end()) {
579 return;
580 }
581 Record const& record = found->second;
582 int const parentId = record.parent;
583 _log.push_back("destroy " + label(record));
584 std::erase(siblingsOf(parentId), widgetId);
585 for (int const child : record.children) {
586 if (auto const orphan = _records.find(child); orphan != _records.end()) {
587 orphan->second.parent = 0;
588 _roots.push_back(child);
589 }
590 }
591 if (_focused == widgetId) {
592 _focused = 0;
593 }
594 _ids.erase(record.widget);
595 _records.erase(found);
596 if (auto const parent = _records.find(parentId); parent != _records.end()) {
597 if (auto const notify = parent->second.onChildDestroyed; notify) {
598 notify(widgetId);
599 }
600 }
601 }
602
603 /// @brief A widget's record.
604 /// @param widgetId The widget.
605 /// @return The record.
606 /// @throws std::out_of_range for an id that names no live widget.
607 [[nodiscard]] Record& at(int widgetId) {
608 auto const found = _records.find(widgetId);
609 if (found == _records.end()) {
610 throw std::out_of_range{"RecordingBackend: no widget #" + std::to_string(widgetId)};
611 }
612 return found->second;
613 }
614
615 /// @brief A widget's record.
616 /// @param widgetId The widget.
617 /// @return The record.
618 /// @throws std::out_of_range for an id that names no live widget.
619 [[nodiscard]] Record const& at(int widgetId) const {
620 auto const found = _records.find(widgetId);
621 if (found == _records.end()) {
622 throw std::out_of_range{"RecordingBackend: no widget #" + std::to_string(widgetId)};
623 }
624 return found->second;
625 }
626
627 /// @brief The id of a live widget made by this store's backend.
628 /// @param widget The widget.
629 /// @return Its id.
630 /// @throws std::out_of_range when @p widget is not one of them.
631 [[nodiscard]] int idOf(Widget const& widget) const {
632 auto const found = _ids.find(&widget);
633 if (found == _ids.end()) {
634 throw std::out_of_range{"RecordingBackend: not a live widget of this backend"};
635 }
636 return found->second;
637 }
638
639 /// @brief Every live widget's record.
640 /// @return The records, by id.
641 [[nodiscard]] std::map<int, Record> const& records() const noexcept { return _records; }
642
643 /// @brief The operation log.
644 /// @return One line per operation, oldest first.
645 [[nodiscard]] std::vector<std::string> const& log() const noexcept { return _log; }
646
647 /// @brief Empties the operation log.
648 void clearLog() noexcept { _log.clear(); }
649
650 /// @brief Records that keyboard focus moved to a widget, and logs `focus`.
651 /// @param widgetId The widget that took focus.
652 /// @throws std::out_of_range for an id that names no live widget.
653 void focus(int widgetId) {
654 _log.push_back("focus " + label(at(widgetId)));
655 _focused = widgetId;
656 }
657
658 /// @brief The widget that last took focus.
659 /// @return Its id, or 0 when none has, or it has since been destroyed.
660 [[nodiscard]] int focused() const noexcept { return _focused; }
661
662 /// @brief The golden tree.
663 /// @return One line per live widget, children indented two spaces per depth.
664 [[nodiscard]] std::string dump() const {
665 std::string text;
666 for (int const root : _roots) {
667 dumpInto(text, root, 0);
668 }
669 return text;
670 }
671
672private:
673 [[nodiscard]] static std::string label(Record const& record) {
674 return record.kind + "#" + std::to_string(record.id);
675 }
676
677 std::vector<int>& siblingsOf(int parentId) { return parentId == 0 ? _roots : at(parentId).children; }
678
679 // A widget tree is dumped by descending it. The id and the depth are an `int` and a `std::size_t`, which convert
680 // into each other.
681 // NOLINTNEXTLINE(misc-no-recursion,bugprone-easily-swappable-parameters)
682 void dumpInto(std::string& text, int widgetId, std::size_t depth) const {
683 Record const& record = at(widgetId);
684 text.append(depth * 2, ' ');
685 text += label(record);
686 for (auto const& [name, value] : record.props) {
687 text += ' ';
688 text += name;
689 text += '=';
690 text += value;
691 }
692 text += '\n';
693 for (int const child : record.children) {
694 dumpInto(text, child, depth + 1);
695 }
696 }
697
698 std::map<int, Record> _records;
699 std::unordered_map<Widget const*, int> _ids;
700 std::vector<int> _roots;
701 std::vector<std::string> _log;
702 int _nextId = 0;
703 int _focused = 0;
704};
705
706/// @brief The `Widget` half of every fake: records itself on construction, records `Common`'s setters, and records
707/// its destruction.
708/// @tparam Interface The widget interface the fake implements.
709template <typename Interface>
710class FakeLeaf : public Interface {
711public:
712 /// @brief Records the new widget, appended to @p parent, and logs its creation.
713 /// @param store Where the widget records itself.
714 /// @param kind The kind shown in the log and the dump.
715 /// @param parent The container it is appended to, or null for a root.
716 /// @param initial Properties fixed at creation.
717 FakeLeaf(std::shared_ptr<RecordStore> store, std::string kind, ContainerWidget const* parent,
718 PropList initial = {})
719 : _store{std::move(store)}, _id{_store->create(std::move(kind), parent, *this, std::move(initial))} {}
720
721 /// @brief Forgets the widget and logs its destruction; its remaining children become roots.
722 ~FakeLeaf() override {
723 try {
724 _store->destroy(_id);
725 } catch (...) { // NOLINT(bugprone-empty-catch)
726 // Logging the destruction failed to allocate; the record stays, and a test reads it as a leak.
727 }
728 }
729
730 FakeLeaf(FakeLeaf const&) = delete;
731 FakeLeaf& operator=(FakeLeaf const&) = delete;
732 FakeLeaf(FakeLeaf&&) = delete;
733 FakeLeaf& operator=(FakeLeaf&&) = delete;
734
735 /// @brief Records property `visible`.
736 /// @param visible Whether it is shown.
737 void setVisible(bool visible) override { set("visible", formatBool(visible)); }
738
739 /// @brief Records property `enabled`.
740 /// @param enabled Whether it accepts input.
741 void setEnabled(bool enabled) override { set("enabled", formatBool(enabled)); }
742
743 /// @brief Records property `layout`, as `formatLayout` gives it.
744 /// @param hints The request in both dimensions.
745 void setLayout(LayoutHints const& hints) override { set("layout", formatLayout(hints)); }
746
747 /// @brief Records property `dragKey` and keeps the key for `RecordingBackend::drag`.
748 /// @param key Engaged: the key a drop delivers. `nullopt`: not draggable.
749 void setDragKey(std::optional<Key> const& key) override {
750 _store->at(_id).dragKey = key;
751 set("dragKey", formatOptionalKey(key));
752 }
753
754 /// @brief Records property `drop=handler` and keeps both handlers for `RecordingBackend::drag`.
755 /// @param accepts Whether a dragged key may be dropped here.
756 /// @param onDrop Called with the key on a drop that `accepts` allowed.
757 void setDropHandler(std::function<bool(Key const&)> accepts, std::function<void(Key)> onDrop) override {
758 Callbacks& handlers = callbacks();
759 handlers.accepts = std::move(accepts);
760 handlers.drop = std::move(onDrop);
761 set("drop", "handler");
762 }
763
764 /// @brief Records property `a11yName`.
765 /// @param name The accessible name.
766 void setAccessibleName(std::string_view name) override { set("a11yName", formatText(name)); }
767
768 /// @brief Records property `a11yRole`.
769 /// @param role The role.
770 void setAccessibleRole(std::string_view role) override { set("a11yRole", formatText(role)); }
771
772 /// @brief Records property `testId`.
773 /// @param testId The identifier.
774 void setTestId(std::string_view testId) override { set("testId", formatText(testId)); }
775
776 /// @brief Records property `tooltip`.
777 /// @param text The tooltip.
778 void setTooltip(std::string_view text) override { set("tooltip", formatText(text)); }
779
780 /// @brief Records property `surface`.
781 /// @param surface The surface's name.
782 void setSurface(std::string_view surface) override { set("surface", formatText(surface)); }
783
784 /// @brief Records property `keys`, the chords joined by commas, and keeps the chords and the handler for
785 /// `RecordingBackend::press`.
786 /// @param chords The chords.
787 /// @param onChord Called with the chord that was pressed.
788 void setKeys(std::vector<std::string> const& chords, std::function<void(std::string)> onChord) override {
789 Record& record = _store->at(_id);
790 record.callbacks.chord = std::move(onChord);
791 record.chords = chords;
792 std::string joined;
793 for (std::string const& chord : chords) {
794 joined += joined.empty() ? "" : ",";
795 joined += chord;
796 }
797 set("keys", formatText(joined));
798 }
799
800 /// @brief Moves the backend's recorded focus here, and logs `focus`.
801 void focus() override { _store->focus(_id); }
802
803 /// @brief Keeps the handler for `RecordingBackend::hover`.
804 /// @param onHover The handler.
805 void setOnHover(std::function<void()> onHover) override { _store->at(_id).callbacks.hover = std::move(onHover); }
806
807protected:
808 /// @brief Records a property set through a setter.
809 /// @param name The property.
810 /// @param value Its formatted value.
811 void set(std::string_view name, std::string value) { _store->set(_id, name, std::move(value)); }
812
813 /// @brief This widget's handlers.
814 /// @return The handlers, for a setter to store into.
815 [[nodiscard]] Callbacks& callbacks() { return _store->at(_id).callbacks; }
816
817 /// @brief The store this widget records into.
818 /// @return The store.
819 [[nodiscard]] RecordStore& store() const noexcept { return *_store; }
820
821 /// @brief The store, shared: what a part this widget makes (an overlay's footer) records into.
822 /// @return The store.
823 [[nodiscard]] std::shared_ptr<RecordStore> const& sharedStore() const noexcept { return _store; }
824
825 /// @brief This widget's id.
826 /// @return The id.
827 [[nodiscard]] int id() const noexcept { return _id; }
828
829private:
830 std::shared_ptr<RecordStore> _store;
831 int _id;
832};
833
834/// @brief The `ContainerWidget` half of every fake container.
835/// @tparam Interface The container interface the fake implements.
836template <typename Interface>
837class FakeContainer : public FakeLeaf<Interface> {
838public:
839 using FakeLeaf<Interface>::FakeLeaf;
840
841 /// @brief Moves a child among its siblings and logs it.
842 /// @param child One of this container's children.
843 /// @param index The new position; past the end means last.
844 /// @throws std::logic_error when @p child is not this container's child.
845 void moveChild(Widget& child, std::size_t index) override {
846 RecordStore& records = this->store();
847 int const childId = records.idOf(child);
848 if (records.at(childId).parent != this->id()) {
849 throw std::logic_error{"RecordingBackend: moveChild of a widget that is not this container's child"};
850 }
851 records.move(childId, index);
852 }
853};
854
855/// @brief The `FieldWidget` half of every fake field: properties `readonly`, `required`, `errors`, `stale`.
856/// @tparam Interface The field interface the fake implements.
857template <typename Interface>
858class FakeField : public FakeLeaf<Interface> {
859public:
860 using FakeLeaf<Interface>::FakeLeaf;
861
862 /// @brief Records property `readonly`; `RecordingBackend`'s edits leave a read-only field alone.
863 /// @param readonly Whether the user may not change the value.
864 void setReadOnly(bool readonly) override { this->set("readonly", formatBool(readonly)); }
865
866 /// @brief Records property `required`.
867 /// @param required Whether the field is marked required.
868 void setRequired(bool required) override { this->set("required", formatBool(required)); }
869
870 /// @brief Records property `errors`, as `formatList` gives the messages.
871 /// @param errors The messages.
872 void setErrors(std::vector<std::string> const& errors) override {
873 this->set("errors", formatList(errors, [](std::string const& error) { return formatItem(error); }));
874 }
875
876 /// @brief Records property `stale`.
877 /// @param stale Whether the value is being recomputed.
878 void setStale(bool stale) override { this->set("stale", formatBool(stale)); }
879};
880
881/// @brief A fake `TextWidget`: properties `text`, `role`.
882class FakeText final : public FakeField<TextWidget> {
883public:
885 void setText(std::string_view text) override { set("text", formatText(text)); }
886 void setRole(TextRole role) override { set("role", enumName(role)); }
887};
888
889/// @brief A fake `ButtonWidget`: properties `label`, `emphasis`, `icon`, `style` and `selected`.
890class FakeButton final : public FakeLeaf<ButtonWidget> {
891public:
893 void setLabel(std::string_view label) override { set("label", formatText(label)); }
894 void setOnClick(Action onClick) override { callbacks().click = std::move(onClick); }
895 void setEmphasis(ButtonEmphasis emphasis) override { set("emphasis", enumName(emphasis)); }
896 void setIcon(std::string_view icon) override { set("icon", formatText(icon)); }
897 void setStyle(ButtonStyle style) override { set("style", enumName(style)); }
898 void setSelected(bool selected) override { set("selected", selected ? "true" : "false"); }
899};
900
901/// @brief A fake `TextInputWidget`: properties `mode`, `text`, `placeholder`, `unit`, `decimals`, `icon`, `caption`,
902/// `captionShown`, the field state, and a text cursor.
903///
904/// The cursor is a byte offset into the text. A user's edit puts it where the edit left it; `setText` with the text
905/// the field shows leaves it alone, as the contract requires, and `setText` with another text puts it at the end.
906class FakeTextInput final : public FakeField<TextInputWidget> {
907public:
909
910 /// @brief Records property `text`; moves the cursor to the end only when the text changes.
911 /// @param text The text, UTF-8.
912 void setText(std::string_view text) override {
913 if (text != _shown) {
914 _shown = std::string{text};
915 _cursor = _shown.size();
916 }
917 set("text", formatText(text));
918 }
919
920 /// @brief The text the user typed: the field shows it, and the cursor is at @p cursor; nothing is logged.
921 /// @param text The whole new text.
922 /// @param cursor Where the cursor ends up; clamped to the text's size.
923 void typedByUser(std::string text, std::size_t cursor) {
924 _shown = std::move(text);
925 _cursor = std::min(cursor, _shown.size());
926 store().setByUser(id(), "text", formatText(_shown));
927 }
928
929 /// @brief Moves the cursor as the user's arrow keys or a click would.
930 /// @param cursor The new position; clamped to the text's size.
931 void moveCursorByUser(std::size_t cursor) noexcept { _cursor = std::min(cursor, _shown.size()); }
932
933 /// @brief Where the cursor is.
934 /// @return A byte offset into the text shown.
935 [[nodiscard]] std::size_t cursor() const noexcept { return _cursor; }
936
937 /// @brief The text the field shows.
938 /// @return The text, unformatted.
939 [[nodiscard]] std::string const& text() const noexcept { return _shown; }
940
941 void setPlaceholder(std::string_view placeholder) override { set("placeholder", formatText(placeholder)); }
942 void setUnit(std::string_view unit) override { set("unit", formatText(unit)); }
943 void setDecimals(int decimals) override { set("decimals", std::to_string(decimals)); }
944 void setIcon(std::string_view icon) override { set("icon", formatText(icon)); }
945 void setCaption(std::string_view label, bool shown) override {
946 set("caption", formatText(label));
947 set("captionShown", formatBool(shown));
948 }
949 void setOnChange(std::function<void(std::string)> onChange) override { callbacks().change = std::move(onChange); }
950 void setOnSubmit(std::function<void(std::string)> onSubmit) override { callbacks().submit = std::move(onSubmit); }
951 void setOnCommit(std::function<void(std::string)> onCommit) override { callbacks().commit = std::move(onCommit); }
952
953private:
954 std::string _shown;
955 std::size_t _cursor = 0;
956};
957
958/// @brief A fake `CheckboxWidget`: properties `label`, `checked`.
959class FakeCheckbox final : public FakeField<CheckboxWidget> {
960public:
962 void setLabel(std::string_view label) override { set("label", formatText(label)); }
963 void setChecked(bool checked) override { set("checked", formatBool(checked)); }
964 void setOnToggle(std::function<void(bool)> onToggle) override { callbacks().toggle = std::move(onToggle); }
965};
966
967/// @brief A fake `SelectWidget`: properties `style`, `options`, `selected`, `caption`, `captionShown`.
968///
969/// `selected` shows the option the widget marks: the requested key while the options contain it, else `none`. The
970/// requested key is kept, so options that bring it back mark it again; that re-marking is the widget's own doing and
971/// is not logged.
972class FakeSelect final : public FakeField<SelectWidget> {
973public:
975
976 /// @brief Records property `options`, and re-marks the requested key against them.
977 /// @param options The options, in order.
978 void setOptions(std::vector<SelectOption> const& options) override {
979 _options = options;
980 set("options", formatList(options, [](SelectOption const& option) {
981 return formatKey(option.key) + ":" + formatItem(option.label);
982 }));
983 if (_requested.has_value()) {
984 store().setByUser(id(), "selected", marked());
985 }
986 }
987
988 /// @brief Requests the option with @p key, and records property `selected` as the option it marks.
989 /// @param key The key, or `nullopt` for no selection.
990 void setSelected(std::optional<Key> const& key) override {
991 _requested.emplace(key);
992 set("selected", marked());
993 }
994
995 /// @brief Stores the handler `RecordingBackend::choose` calls.
996 /// @param onSelect The handler.
997 void setOnSelect(std::function<void(Key)> onSelect) override { callbacks().select = std::move(onSelect); }
998
999 /// @brief Stores the handler `RecordingBackend::open` calls.
1000 /// @param onOpen The handler.
1001 void setOnOpen(Action onOpen) override { callbacks().open = std::move(onOpen); }
1002 void setCaption(std::string_view label, bool shown) override {
1003 set("caption", formatText(label));
1004 set("captionShown", formatBool(shown));
1005 }
1006
1007 /// @brief Whether one of the current options has @p key.
1008 /// @param key The key.
1009 /// @return True when an option has it.
1010 [[nodiscard]] bool offers(Key const& key) const {
1011 return std::ranges::any_of(_options, [&key](SelectOption const& option) { return option.key == key; });
1012 }
1013
1014 /// @brief Marks the option the user chose, as the requested key; nothing is logged.
1015 /// @param key The key of one of the current options.
1016 void chooseByUser(Key const& key) {
1017 _requested.emplace(key);
1018 store().setByUser(id(), "selected", marked());
1019 }
1020
1021private:
1022 [[nodiscard]] std::string marked() const {
1023 if (!_requested.has_value() || !_requested->has_value() || !offers(**_requested)) {
1024 return "none";
1025 }
1026 return formatKey(**_requested);
1027 }
1028
1029 std::vector<SelectOption> _options;
1030 // Engaged once `setSelected` ran or the user chose; the inner value is the requested key, or none.
1031 std::optional<std::optional<Key>> _requested;
1032};
1033
1034/// @brief A fake `MenuWidget`: property `items`.
1035class FakeMenu final : public FakeLeaf<MenuWidget> {
1036public:
1038 void setItems(std::vector<MenuEntry> const& entries) override {
1039 store().at(id()).menu = entries;
1040 set("items", formatMenu(entries));
1041 }
1042 void setOnActivate(std::function<void(std::vector<std::size_t>)> onActivate) override {
1043 callbacks().path = std::move(onActivate);
1044 }
1045};
1046
1047/// @brief A fake `StackWidget`, of kind `Column` or `Row`: properties `gap`, `align`, `justify`.
1048class FakeStack final : public FakeContainer<StackWidget> {
1049public:
1051 void setGap(int gap) override { set("gap", std::to_string(gap)); }
1052 void setAlign(Align align) override { set("align", formatAlign(align)); }
1053 void setJustify(Justify justify) override { set("justify", formatJustify(justify)); }
1054};
1055
1056/// @brief A fake `GridWidget`: properties `columns`, `gap`, `align`, and `span` on a child.
1057class FakeGrid final : public FakeContainer<GridWidget> {
1058public:
1060 void setColumns(int columns) override { set("columns", std::to_string(columns)); }
1061 void setGap(int gap) override { set("gap", std::to_string(gap)); }
1062 void setAlign(Align align) override { set("align", formatAlign(align)); }
1063
1064 /// @brief Records property `span` on @p child.
1065 /// @param child One of this grid's children.
1066 /// @param span The column count it occupies.
1067 /// @throws std::logic_error when @p child is not this grid's child.
1068 void setSpan(Widget& child, int span) override {
1069 int const childId = store().idOf(child);
1070 if (store().at(childId).parent != id()) {
1071 throw std::logic_error{"RecordingBackend: setSpan of a widget that is not this grid's child"};
1072 }
1073 store().set(childId, "span", std::to_string(span));
1074 }
1075};
1076
1077/// @brief A fake `PanelWidget`: properties `title`, `header` (the child's id), `padding`, `collapsible`, `collapsed`.
1078class FakePanel final : public FakeContainer<PanelWidget> {
1079public:
1081 void setTitle(std::string_view title) override { set("title", formatText(title)); }
1082 void setPadding(int padding) override { set("padding", std::to_string(padding)); }
1083 void setCollapsible(bool collapsible) override { set("collapsible", formatBool(collapsible)); }
1084
1085 /// @brief Records property `header`, the child's id.
1086 /// @param header One of this widget's children.
1087 /// @throws std::logic_error when @p header is not this widget's child.
1088 void setHeader(Widget& header) override {
1089 int const headerId = store().idOf(header);
1090 if (store().at(headerId).parent != id()) {
1091 throw std::logic_error{"RecordingBackend: setHeader with a widget that is not this panel's child"};
1092 }
1093 set("header", std::to_string(headerId));
1094 }
1095 void setCollapsed(bool collapsed) override { set("collapsed", formatBool(collapsed)); }
1096 void setOnToggle(std::function<void(bool)> onToggle) override { callbacks().toggle = std::move(onToggle); }
1097};
1098
1099/// @brief A fake `BusyWidget`: properties `active`, `label`.
1100class FakeBusy final : public FakeLeaf<BusyWidget> {
1101public:
1103 void setActive(bool active) override { set("active", formatBool(active)); }
1104 void setLabel(std::string_view label) override { set("label", formatText(label)); }
1105};
1106
1107/// @brief A fake `DateTimeInputWidget`: properties `mode`, `offset`, `value`, `caption`, `captionShown`.
1108class FakeDateTimeInput final : public FakeField<DateTimeInputWidget> {
1109public:
1111 void setValue(std::optional<morph::time::Timestamp> const& value) override {
1112 set("value", formatTimestamp(value));
1113 }
1114 void setOnChange(std::function<void(std::optional<morph::time::Timestamp>)> onChange) override {
1115 callbacks().dateTime = std::move(onChange);
1116 }
1117 void setCaption(std::string_view label, bool shown) override {
1118 set("caption", formatText(label));
1119 set("captionShown", formatBool(shown));
1120 }
1121};
1122
1123/// @brief A fake `SliderWidget`: properties `range` (`minimum..maximum/step`), `value`.
1124class FakeSlider final : public FakeField<SliderWidget> {
1125public:
1127 void setRange(std::int64_t minimum, std::int64_t maximum, std::int64_t step) override {
1128 set("range", std::to_string(minimum) + ".." + std::to_string(maximum) + "/" + std::to_string(step));
1129 }
1130 void setValue(std::int64_t value) override { set("value", std::to_string(value)); }
1131 void setOnChange(std::function<void(std::int64_t)> onChange) override { callbacks().slide = std::move(onChange); }
1132 void setOnCommit(std::function<void(std::int64_t)> onCommit) override {
1133 callbacks().slideCommit = std::move(onCommit);
1134 }
1135};
1136
1137/// @brief A fake `FilePickerWidget`: properties `mode`, `path`.
1138class FakeFilePicker final : public FakeField<FilePickerWidget> {
1139public:
1141 void setPath(std::string_view path) override { set("path", formatText(path)); }
1142 void setOnPicked(std::function<void(std::string)> onPicked) override { callbacks().picked = std::move(onPicked); }
1143};
1144
1145/// @brief A fake `TabsWidget`: properties `tabs`, `selected`, `barShown`.
1146class FakeTabs final : public FakeContainer<TabsWidget> {
1147public:
1149 void setTabs(std::vector<std::string> const& labels) override {
1150 set("tabs", formatList(labels, [](std::string const& label) { return formatItem(label); }));
1151 }
1152 void setSelected(std::size_t index) override { set("selected", std::to_string(index)); }
1153 void setOnSelect(std::function<void(std::size_t)> onSelect) override { callbacks().index = std::move(onSelect); }
1154 void setBarShown(bool shown) override { set("barShown", shown ? "true" : "false"); }
1155};
1156
1157/// @brief A fake overlay's footer: a `Footer` child, made when it is first asked for, so an overlay that never shows
1158/// actions logs none.
1159/// @tparam Interface `DialogWidget` or `DrawerWidget`.
1160template <typename Interface>
1161class FakeOverlay : public FakeContainer<Interface> {
1162public:
1163 using FakeContainer<Interface>::FakeContainer;
1164
1165 /// @brief The footer, a `Footer` container child of the overlay.
1166 /// @return It.
1168 if (_footer == nullptr) {
1169 _footer = std::make_unique<FakeContainer<SlotWidget>>(this->sharedStore(), "Footer", this);
1170 }
1171 return *_footer;
1172 }
1173
1174private:
1175 std::unique_ptr<FakeContainer<SlotWidget>> _footer;
1176};
1177
1178/// @brief A fake `DialogWidget`: properties `open`, `title`; a `Footer` child once its footer is asked for.
1179class FakeDialog final : public FakeOverlay<DialogWidget> {
1180public:
1182 void setOpen(bool open) override { set("open", formatBool(open)); }
1183 void setTitle(std::string_view title) override { set("title", formatText(title)); }
1184 void setOnDismiss(Action onDismiss) override { callbacks().dismiss = std::move(onDismiss); }
1185};
1186
1187/// @brief A fake `BannerWidget`: properties `tone`, `text`, `actionLabel`, `dismissible`.
1188class FakeBanner final : public FakeLeaf<BannerWidget> {
1189public:
1191 void setTone(Tone tone) override { set("tone", enumName(tone)); }
1192 void setText(std::string_view text) override { set("text", formatText(text)); }
1193 void setActionLabel(std::string_view label) override { set("actionLabel", formatText(label)); }
1194 void setOnAction(Action onAction) override { callbacks().action = std::move(onAction); }
1195 void setDismissible(bool dismissible) override { set("dismissible", formatBool(dismissible)); }
1196 void setOnDismiss(Action onDismiss) override { callbacks().dismiss = std::move(onDismiss); }
1197};
1198
1199/// @brief A fake `ToastWidget`: properties `text`, `tone`, and `shown` (incremented on every `show`, so a test can
1200/// tell a replace from a no-op).
1201class FakeToast final : public FakeLeaf<ToastWidget> {
1202public:
1204 void show(std::string_view text, Tone tone) override {
1205 set("text", formatText(text));
1206 set("tone", enumName(tone));
1207 set("shown", std::to_string(++_shown));
1208 }
1209
1210private:
1211 int _shown = 0;
1212};
1213
1214/// @brief A fake `BadgeWidget`: properties `tone`, `text`, `icon`.
1215class FakeBadge final : public FakeLeaf<BadgeWidget> {
1216public:
1218 void setTone(Tone tone) override { set("tone", enumName(tone)); }
1219 void setText(std::string_view text) override { set("text", formatText(text)); }
1220 void setIcon(std::string_view icon) override { set("icon", formatText(icon)); }
1221};
1222
1223/// @brief A fake `ProgressWidget`: properties `value` (as `formatFraction` gives it), `label`.
1224class FakeProgress final : public FakeLeaf<ProgressWidget> {
1225public:
1227 void setValue(std::optional<double> value) override { set("value", formatFraction(value)); }
1228 void setLabel(std::string_view label) override { set("label", formatText(label)); }
1229};
1230
1231/// @brief A fake `StepsWidget`: properties `steps` (`[label:State,…]`), `current` (a position, or `none`).
1232class FakeSteps final : public FakeLeaf<StepsWidget> {
1233public:
1235 void setSteps(std::vector<Step> const& steps) override {
1236 set("steps",
1237 formatList(steps, [](Step const& step) { return formatItem(step.label) + ":" + enumName(step.state); }));
1238 }
1239 void setCurrent(std::optional<std::size_t> index) override {
1240 set("current", index.has_value() ? std::to_string(*index) : std::string{"none"});
1241 }
1242};
1243
1244/// @brief A fake `KeyValueWidget`: property `items` (`[label:value,…]`).
1245class FakeKeyValue final : public FakeLeaf<KeyValueWidget> {
1246public:
1248 void setItems(std::vector<KeyValueItem> const& items) override {
1249 set("items", formatList(items, [](KeyValueItem const& item) {
1250 return formatItem(item.label) + ":" + formatItem(item.value);
1251 }));
1252 }
1253};
1254
1255/// @brief A fake `EmptyStateWidget`: properties `title`, `text`, `icon`, `actionLabel`.
1256class FakeEmptyState final : public FakeLeaf<EmptyStateWidget> {
1257public:
1259 void setTitle(std::string_view title) override { set("title", formatText(title)); }
1260 void setText(std::string_view text) override { set("text", formatText(text)); }
1261 void setIcon(std::string_view icon) override { set("icon", formatText(icon)); }
1262 void setActionLabel(std::string_view label) override { set("actionLabel", formatText(label)); }
1263 void setOnAction(Action onAction) override { callbacks().action = std::move(onAction); }
1264};
1265
1266/// @brief A fake `DrawerWidget`: properties `side`, `open`, `title`; a `Footer` child once its footer is asked for.
1267class FakeDrawer final : public FakeOverlay<DrawerWidget> {
1268public:
1270 void setOpen(bool open) override { set("open", formatBool(open)); }
1271 void setTitle(std::string_view title) override { set("title", formatText(title)); }
1272 void setOnDismiss(Action onDismiss) override { callbacks().dismiss = std::move(onDismiss); }
1273};
1274
1275/// @brief A fake `SplitterWidget`: properties `axis`, `sizes` (`[n,…]`).
1276class FakeSplitter final : public FakeContainer<SplitterWidget> {
1277public:
1279 void setSizes(std::vector<int> const& sizes) override {
1280 set("sizes", formatList(sizes, [](int size) { return std::to_string(size); }));
1281 }
1282 void setOnResize(std::function<void(std::vector<int>)> onResize) override {
1283 callbacks().resize = std::move(onResize);
1284 }
1285};
1286
1287/// @brief A fake `CollapsibleWidget`: properties `title`, `header` (the header child's id), `open`.
1288class FakeCollapsible final : public FakeContainer<CollapsibleWidget> {
1289public:
1291 void setTitle(std::string_view title) override { set("title", formatText(title)); }
1292
1293 /// @brief Records property `header`, the child's id.
1294 /// @param header One of this widget's children.
1295 /// @throws std::logic_error when @p header is not this widget's child.
1296 void setHeader(Widget& header) override {
1297 int const headerId = store().idOf(header);
1298 if (store().at(headerId).parent != id()) {
1299 throw std::logic_error{"RecordingBackend: setHeader with a widget that is not this collapsible's child"};
1300 }
1301 set("header", std::to_string(headerId));
1302 }
1303 void setOpen(bool open) override { set("open", formatBool(open)); }
1304 void setOnToggle(std::function<void(bool)> onToggle) override { callbacks().toggle = std::move(onToggle); }
1305};
1306
1307/// @brief A fake `DropZoneWidget`: properties `accept` (`[.ext,…]`), `multiple`.
1308class FakeDropZone final : public FakeContainer<DropZoneWidget> {
1309public:
1311 void setAccept(std::vector<std::string> const& extensions) override {
1312 _accept = extensions;
1313 set("accept", formatList(extensions, [](std::string const& extension) { return formatItem(extension); }));
1314 }
1315 void setMultiple(bool multiple) override {
1316 _multiple = multiple;
1317 set("multiple", formatBool(multiple));
1318 }
1319 void setOnDrop(std::function<void(std::vector<std::string>)> onDrop) override {
1320 callbacks().files = std::move(onDrop);
1321 }
1322
1323 /// @brief Whether the zone takes a drop of @p paths: one file unless several are allowed, each with an accepted
1324 /// extension.
1325 /// @param paths The paths dropped; not empty.
1326 /// @return True when the drop is taken.
1327 [[nodiscard]] bool takes(std::vector<std::string> const& paths) const {
1328 if (paths.size() > 1 && !_multiple) {
1329 return false;
1330 }
1331 return std::ranges::all_of(paths, [this](std::string const& path) {
1332 return _accept.empty() || std::ranges::any_of(_accept, [&path](std::string const& extension) {
1333 return endsWithIgnoringCase(path, extension);
1334 });
1335 });
1336 }
1337
1338private:
1339 [[nodiscard]] static bool endsWithIgnoringCase(std::string_view text, std::string_view suffix) {
1340 if (suffix.size() > text.size()) {
1341 return false;
1342 }
1343 auto const lower = [](char character) {
1344 return static_cast<char>(std::tolower(static_cast<unsigned char>(character)));
1345 };
1346 return std::ranges::equal(text.substr(text.size() - suffix.size()), suffix, {}, lower, lower);
1347 }
1348
1349 std::vector<std::string> _accept;
1350 bool _multiple = false;
1351};
1352
1353/// @brief A fake loaded `CustomWidget`: property `component`, and each prop as `prop.<name>`.
1354class FakeCustom final : public FakeLeaf<CustomWidget> {
1355public:
1357 void setTextProp(std::string_view name, std::string_view value) override {
1358 set("prop." + std::string{name}, formatText(value));
1359 }
1360 void setBoolProp(std::string_view name, bool value) override {
1361 set("prop." + std::string{name}, formatBool(value));
1362 }
1363 void setNumberProp(std::string_view name, double value) override {
1364 set("prop." + std::string{name}, std::format("{}", value));
1365 }
1366 void setOnEvent(std::function<void(std::string event, std::string payload)> onEvent) override {
1367 callbacks().component = std::move(onEvent);
1368 }
1369};
1370
1371/// @brief A fake `TableWidget`: properties `columns`, `selectionMode`, `selection`, and `rowKey` on a row.
1372///
1373/// `selection` shows the rows the widget marks: the requested keys, in the order requested, that one of its current
1374/// rows has. The requested keys are kept, so a row that arrives with one of them is marked and a row that goes is
1375/// unmarked; that re-marking is the widget's own doing and is not logged.
1376class FakeTable final : public FakeContainer<TableWidget> {
1377public:
1378 /// @brief Records the new table, appended to @p parent, and logs its creation.
1379 /// @param store Where the widget records itself.
1380 /// @param kind The kind shown in the log and the dump.
1381 /// @param parent The container it is appended to, or null for a root.
1382 FakeTable(std::shared_ptr<RecordStore> store, std::string kind, ContainerWidget const* parent)
1383 : FakeContainer<TableWidget>{std::move(store), std::move(kind), parent} {
1384 this->store().at(id()).onChildDestroyed = [this](int childId) {
1385 _rowKeys.erase(childId);
1386 remark();
1387 };
1388 }
1389
1390 /// @brief Removes the hook that points at this table, so nothing calls it once the table is gone, even if the
1391 /// record outlives it because recording the destruction failed.
1392 ~FakeTable() override {
1393 try {
1394 store().at(id()).onChildDestroyed = nullptr;
1395 } catch (...) { // NOLINT(bugprone-empty-catch)
1396 // No record left, so no hook to remove.
1397 }
1398 }
1399
1400 FakeTable(FakeTable const&) = delete;
1401 FakeTable& operator=(FakeTable const&) = delete;
1402 FakeTable(FakeTable&&) = delete;
1403 FakeTable& operator=(FakeTable&&) = delete;
1404
1405 /// @brief Records property `columns`, each as its escaped caption, a colon and its width.
1406 /// @param columns The columns, in order.
1407 void setColumns(std::vector<TableColumn> const& columns) override {
1408 set("columns", formatList(columns, [](TableColumn const& column) {
1409 return formatItem(column.label) + ":" + formatSizing(column.width);
1410 }));
1411 }
1412
1413 /// @brief Records property `showHeader`.
1414 /// @param shown Whether the header row is drawn.
1415 void setShowHeader(bool shown) override { set("showHeader", formatBool(shown)); }
1416
1417 /// @brief Records property `selectionMode`, which `RecordingBackend::selectRows` enforces.
1418 /// @param mode None, one, or any number.
1420 _mode = mode;
1421 set("selectionMode", enumName(mode));
1422 }
1423
1424 /// @brief Records property `rowKey` on @p row, and marks the row when its key is requested.
1425 /// @param row One of this table's rows.
1426 /// @param key The row's key.
1427 /// @throws std::logic_error when @p row is not this table's child.
1428 void setRowKey(Widget& row, Key const& key) override {
1429 int const rowId = store().idOf(row);
1430 if (store().at(rowId).parent != id()) {
1431 throw std::logic_error{"RecordingBackend: setRowKey of a widget that is not this table's row"};
1432 }
1433 store().set(rowId, "rowKey", formatKey(key));
1434 _rowKeys.insert_or_assign(rowId, key);
1435 remark();
1436 }
1437
1438 /// @brief Records the row's property `tone` (`none` for no tone).
1439 /// @param row One of this table's rows.
1440 /// @param tone The tone, or none.
1441 /// @throws std::logic_error when @p row is not this table's child.
1442 void setRowTone(Widget& row, std::optional<Tone> tone) override {
1443 int const rowId = store().idOf(row);
1444 if (store().at(rowId).parent != id()) {
1445 throw std::logic_error{"RecordingBackend: setRowTone of a widget that is not this table's row"};
1446 }
1447 store().set(rowId, "tone", tone ? enumName(*tone) : std::string{"none"});
1448 }
1449
1450 /// @brief Requests the rows with @p keys, and records property `selection` as the rows it marks.
1451 /// @param keys The selected keys.
1452 void setSelection(std::vector<Key> const& keys) override {
1453 _requested = keys;
1454 set("selection", marked());
1455 }
1456
1457 /// @brief Stores the handler `RecordingBackend::selectRows` calls.
1458 /// @param onSelectionChange The handler.
1459 void setOnSelectionChange(std::function<void(std::vector<Key>)> onSelectionChange) override {
1460 callbacks().selection = std::move(onSelectionChange);
1461 }
1462
1463 /// @brief Stores the handler `RecordingBackend::activateRow` calls.
1464 /// @param onActivate The handler.
1465 void setOnActivate(std::function<void(Key)> onActivate) override { callbacks().activate = std::move(onActivate); }
1466
1467 /// @brief Records property `realizedWindow` as `<first>+<count>/<total>`.
1468 /// @param first The view index of the first realized row.
1469 /// @param count How many rows are realized, starting at `first`.
1470 /// @param total How many rows the table's data has, realized or not.
1471 void setRealizedWindow(std::size_t first, std::size_t count, std::size_t total) override {
1472 set("realizedWindow", std::to_string(first) + "+" + std::to_string(count) + "/" + std::to_string(total));
1473 }
1474
1475 /// @brief Stores the handler `RecordingBackend::scrollTable` calls.
1476 /// @param onVisibleRangeChanged The handler.
1477 void setOnVisibleRangeChanged(std::function<void(std::size_t, std::size_t)> onVisibleRangeChanged) override {
1478 _onVisibleRangeChanged = std::move(onVisibleRangeChanged);
1479 }
1480
1481 /// @brief Simulates the backend reporting a scroll: calls the handler `setOnVisibleRangeChanged` stored, as a
1482 /// real backend's own scroll callback would. Does nothing if nothing is subscribed (a table below its
1483 /// virtualization threshold never calls `setOnVisibleRangeChanged`, so this is a silent no-op for it,
1484 /// matching a real backend that never virtualizes).
1485 /// @param first The view index of the first row now wanted.
1486 /// @param count How many rows from `first` are wanted.
1487 void scrollTo(std::size_t first, std::size_t count) {
1488 if (_onVisibleRangeChanged) {
1489 _onVisibleRangeChanged(first, count);
1490 }
1491 }
1492
1493 /// @brief The selection mode last set.
1494 /// @return The mode; `None` until `setSelectionMode` runs.
1495 [[nodiscard]] SelectionMode mode() const noexcept { return _mode; }
1496
1497 /// @brief The current row with @p key.
1498 /// @param key The key.
1499 /// @return The row's id, or `nullopt` when no current row has @p key.
1500 [[nodiscard]] std::optional<int> rowOf(Key const& key) const {
1501 for (int const child : store().at(id()).children) {
1502 if (auto const found = _rowKeys.find(child); found != _rowKeys.end() && found->second == key) {
1503 return child;
1504 }
1505 }
1506 return std::nullopt;
1507 }
1508
1509 /// @brief Marks the rows the user selected, as the requested keys; nothing is logged.
1510 /// @param keys Keys of current rows.
1511 void selectByUser(std::vector<Key> const& keys) {
1512 _requested = keys;
1513 remark();
1514 }
1515
1516private:
1517 [[nodiscard]] std::string marked() const {
1518 std::vector<Key> keys;
1519 for (Key const& key : _requested.value_or(std::vector<Key>{})) {
1520 if (rowOf(key).has_value()) {
1521 keys.push_back(key);
1522 }
1523 }
1524 return formatList(keys, [](Key const& key) { return formatKey(key); });
1525 }
1526
1527 void remark() {
1528 if (_requested.has_value()) {
1529 store().setByUser(id(), "selection", marked());
1530 }
1531 }
1532
1533 SelectionMode _mode = SelectionMode::None;
1534 std::map<int, Key> _rowKeys; // row id -> key, as setRowKey told it
1535 // Engaged once `setSelection` ran or the user selected.
1536 std::optional<std::vector<Key>> _requested;
1537 std::function<void(std::size_t, std::size_t)> _onVisibleRangeChanged;
1538};
1539
1540} // namespace detail
1541
1542/// @brief A headless `IViewBackend`: fake widgets that record every operation, a golden dump of the live tree, and
1543/// helpers that act as the user would.
1544///
1545/// Widget ids are creation numbers from 1. A widget ignores every interaction helper, as a real one would, while it
1546/// is hidden or disabled, or while a container around it is hidden, disabled or collapsed. A dialog, and everything
1547/// inside it, ignores them until it is opened. A helper used on a kind of widget it does not apply to throws
1548/// `std::logic_error`: that is a mistake in the test, not something a user can do. A helper copies the handler before
1549/// calling it, so a handler may destroy its own widget.
1550///
1551/// Free text in a property value is escaped as `detail::formatText` and `detail::formatItem` describe, so every
1552/// value stays on its line and reads as one field, or one list element.
1553class RecordingBackend final : public IViewBackend {
1554public:
1555 [[nodiscard]] std::unique_ptr<TextWidget> createText(ContainerWidget* parent) override {
1556 return std::make_unique<detail::FakeText>(_store, "Text", parent);
1557 }
1558 [[nodiscard]] std::unique_ptr<ButtonWidget> createButton(ContainerWidget* parent) override {
1559 return std::make_unique<detail::FakeButton>(_store, "Button", parent);
1560 }
1561 [[nodiscard]] std::unique_ptr<TextInputWidget> createTextInput(ContainerWidget* parent,
1562 TextInputMode mode) override {
1563 return std::make_unique<detail::FakeTextInput>(_store, "TextInput", parent,
1564 detail::PropList{{"mode", detail::enumName(mode)}});
1565 }
1566 [[nodiscard]] std::unique_ptr<CheckboxWidget> createCheckbox(ContainerWidget* parent) override {
1567 return std::make_unique<detail::FakeCheckbox>(_store, "Checkbox", parent);
1568 }
1569 [[nodiscard]] std::unique_ptr<SelectWidget> createSelect(ContainerWidget* parent, SelectStyle style) override {
1570 return std::make_unique<detail::FakeSelect>(_store, "Select", parent,
1571 detail::PropList{{"style", detail::enumName(style)}});
1572 }
1573 [[nodiscard]] std::unique_ptr<MenuWidget> createMenu(ContainerWidget* parent) override {
1574 return std::make_unique<detail::FakeMenu>(_store, "Menu", parent);
1575 }
1576 [[nodiscard]] std::unique_ptr<StackWidget> createStack(ContainerWidget* parent, Axis axis) override {
1577 return std::make_unique<detail::FakeStack>(_store, axis == Axis::Vertical ? "Column" : "Row", parent);
1578 }
1579 [[nodiscard]] std::unique_ptr<GridWidget> createGrid(ContainerWidget* parent) override {
1580 return std::make_unique<detail::FakeGrid>(_store, "Grid", parent);
1581 }
1582 [[nodiscard]] std::unique_ptr<SpacerWidget> createSpacer(ContainerWidget* parent) override {
1583 return std::make_unique<detail::FakeLeaf<SpacerWidget>>(_store, "Spacer", parent);
1584 }
1585 [[nodiscard]] std::unique_ptr<PanelWidget> createPanel(ContainerWidget* parent) override {
1586 return std::make_unique<detail::FakePanel>(_store, "Panel", parent);
1587 }
1588 [[nodiscard]] std::unique_ptr<ScrollWidget> createScroll(ContainerWidget* parent, Axis axis) override {
1589 return std::make_unique<detail::FakeContainer<ScrollWidget>>(
1590 _store, "Scroll", parent, detail::PropList{{"axis", detail::enumName(axis)}});
1591 }
1592 [[nodiscard]] std::unique_ptr<SlotWidget> createSlot(ContainerWidget* parent) override {
1593 return std::make_unique<detail::FakeContainer<SlotWidget>>(_store, "Slot", parent);
1594 }
1595 [[nodiscard]] std::unique_ptr<TabsWidget> createTabs(ContainerWidget* parent) override {
1596 return std::make_unique<detail::FakeTabs>(_store, "Tabs", parent);
1597 }
1598 [[nodiscard]] std::unique_ptr<DialogWidget> createDialog(ContainerWidget* parent) override {
1599 return std::make_unique<detail::FakeDialog>(_store, "Dialog", parent);
1600 }
1601 [[nodiscard]] std::unique_ptr<ToastWidget> createToast(ContainerWidget* parent) override {
1602 return std::make_unique<detail::FakeToast>(_store, "Toast", parent);
1603 }
1604 [[nodiscard]] std::unique_ptr<BusyWidget> createBusy(ContainerWidget* parent) override {
1605 return std::make_unique<detail::FakeBusy>(_store, "Busy", parent);
1606 }
1607 [[nodiscard]] std::unique_ptr<TableWidget> createTable(ContainerWidget* parent) override {
1608 return std::make_unique<detail::FakeTable>(_store, "Table", parent);
1609 }
1610 [[nodiscard]] std::unique_ptr<DateTimeInputWidget> createDateTimeInput(ContainerWidget* parent, DateMode mode,
1611 int offsetMinutes) override {
1612 return std::make_unique<detail::FakeDateTimeInput>(
1613 _store, "DateTimeInput", parent,
1614 detail::PropList{{"mode", detail::enumName(mode)}, {"offset", std::to_string(offsetMinutes)}});
1615 }
1616 [[nodiscard]] std::unique_ptr<SliderWidget> createSlider(ContainerWidget* parent) override {
1617 return std::make_unique<detail::FakeSlider>(_store, "Slider", parent);
1618 }
1619 [[nodiscard]] std::unique_ptr<FilePickerWidget> createFilePicker(ContainerWidget* parent,
1620 FilePickerMode mode) override {
1621 return std::make_unique<detail::FakeFilePicker>(_store, "FilePicker", parent,
1622 detail::PropList{{"mode", detail::enumName(mode)}});
1623 }
1624 [[nodiscard]] std::unique_ptr<BannerWidget> createBanner(ContainerWidget* parent) override {
1625 return std::make_unique<detail::FakeBanner>(_store, "Banner", parent);
1626 }
1627 [[nodiscard]] std::unique_ptr<BadgeWidget> createBadge(ContainerWidget* parent) override {
1628 return std::make_unique<detail::FakeBadge>(_store, "Badge", parent);
1629 }
1630 [[nodiscard]] std::unique_ptr<ProgressWidget> createProgress(ContainerWidget* parent) override {
1631 return std::make_unique<detail::FakeProgress>(_store, "Progress", parent);
1632 }
1633 [[nodiscard]] std::unique_ptr<StepsWidget> createSteps(ContainerWidget* parent) override {
1634 return std::make_unique<detail::FakeSteps>(_store, "Steps", parent);
1635 }
1636 [[nodiscard]] std::unique_ptr<KeyValueWidget> createKeyValue(ContainerWidget* parent) override {
1637 return std::make_unique<detail::FakeKeyValue>(_store, "KeyValue", parent);
1638 }
1639 [[nodiscard]] std::unique_ptr<EmptyStateWidget> createEmptyState(ContainerWidget* parent) override {
1640 return std::make_unique<detail::FakeEmptyState>(_store, "EmptyState", parent);
1641 }
1642 [[nodiscard]] std::unique_ptr<DrawerWidget> createDrawer(ContainerWidget* parent, Side side) override {
1643 return std::make_unique<detail::FakeDrawer>(_store, "Drawer", parent,
1644 detail::PropList{{"side", detail::enumName(side)}});
1645 }
1646 [[nodiscard]] std::unique_ptr<SplitterWidget> createSplitter(ContainerWidget* parent, Axis axis) override {
1647 return std::make_unique<detail::FakeSplitter>(_store, "Splitter", parent,
1648 detail::PropList{{"axis", detail::enumName(axis)}});
1649 }
1650 [[nodiscard]] std::unique_ptr<CollapsibleWidget> createCollapsible(ContainerWidget* parent) override {
1651 return std::make_unique<detail::FakeCollapsible>(_store, "Collapsible", parent);
1652 }
1653 [[nodiscard]] std::unique_ptr<DropZoneWidget> createDropZone(ContainerWidget* parent) override {
1654 return std::make_unique<detail::FakeDropZone>(_store, "DropZone", parent);
1655 }
1656
1657 /// @brief Loads a component `loadsComponents` named, as a `Custom` widget with properties `component` and
1658 /// `minVersion`; any other is not loaded, and the mount shows its fallback.
1659 /// @param parent The container to append it to; null makes a root.
1660 /// @param request The component and its props.
1661 /// @return The widget, or null.
1662 [[nodiscard]] std::unique_ptr<CustomWidget> createCustom(ContainerWidget* parent,
1663 CustomRequest const& request) override {
1664 if (!_components.contains(request.name)) {
1665 return nullptr;
1666 }
1667 return std::make_unique<detail::FakeCustom>(
1668 _store, "Custom", parent,
1669 detail::PropList{{"component", detail::formatText(request.name)},
1670 {"minVersion", std::to_string(request.minVersion)}});
1671 }
1672
1673 /// @brief Names the components `createCustom` loads; none until called, so a custom node shows its fallback.
1674 /// @param names The components.
1675 void loadsComponents(std::vector<std::string> names) {
1676 _components.clear();
1677 for (std::string& name : names) {
1678 _components.insert(std::move(name));
1679 }
1680 }
1681
1682 /// @brief Raises an event of a loaded component, as the component would.
1683 /// @param widgetId The component.
1684 /// @param event The event's name.
1685 /// @param payload Its payload, as the backend reports it (`i:<int64>`, `s:<text>`, or the component's text).
1686 /// @throws std::logic_error when the widget is not a loaded component.
1687 void raiseEvent(int widgetId, std::string event, std::string payload) {
1688 detail::Record const& record = expectKind(widgetId, "raiseEvent", {"Custom"});
1689 if (reachable(record)) {
1690 invoke(record.callbacks.component, std::move(event), std::move(payload));
1691 }
1692 }
1693
1694 /// @brief The golden tree of the live widgets.
1695 /// @return One line per widget, `Kind#id name=value …` in property-name order, children indented two spaces
1696 /// per depth, each line ending in a newline.
1697 [[nodiscard]] std::string dump() const { return _store->dump(); }
1698
1699 /// @brief The operation log since construction or the last `clearLog()`.
1700 /// @return `create Kind#id in Parent#id` (`in root` for a root), `set Kind#id name=value`,
1701 /// `move Kind#id to n`, `destroy Kind#id`, oldest first. Interaction helpers add nothing.
1702 [[nodiscard]] std::vector<std::string> const& log() const { return _store->log(); }
1703
1704 /// @brief Empties the operation log.
1705 void clearLog() { _store->clearLog(); }
1706
1707 /// @brief The first live widget, by id, of a kind whose property has a value.
1708 /// @param kind The kind, as the dump shows it.
1709 /// @param name The property.
1710 /// @param value Its formatted value.
1711 /// @return The widget's id, or `nullopt`.
1712 // The contract's lookup: kind, property name and value, all text.
1713 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
1714 [[nodiscard]] std::optional<int> find(std::string_view kind, std::string_view name, std::string_view value) const {
1715 for (auto const& [widgetId, record] : _store->records()) {
1716 if (record.kind != kind) {
1717 continue;
1718 }
1719 if (auto const found = record.props.find(name); found != record.props.end() && found->second == value) {
1720 return widgetId;
1721 }
1722 }
1723 return std::nullopt;
1724 }
1725
1726 /// @brief Every live widget of a kind.
1727 /// @param kind The kind, as the dump shows it.
1728 /// @return Their ids, in creation order.
1729 [[nodiscard]] std::vector<int> all(std::string_view kind) const {
1730 std::vector<int> ids;
1731 for (auto const& [widgetId, record] : _store->records()) {
1732 if (record.kind == kind) {
1733 ids.push_back(widgetId);
1734 }
1735 }
1736 return ids;
1737 }
1738
1739 /// @brief A property's formatted value, escaped as the dump shows it.
1740 /// @param widgetId The widget.
1741 /// @param name The property.
1742 /// @return The value, or an empty string when it was never set; `hasProp` tells the two apart.
1743 /// @throws std::out_of_range for an id that names no live widget.
1744 [[nodiscard]] std::string prop(int widgetId, std::string_view name) const {
1745 auto const& props = _store->at(widgetId).props;
1746 auto const found = props.find(name);
1747 return found == props.end() ? std::string{} : found->second;
1748 }
1749
1750 /// @brief Whether a property was ever set, by a setter, at creation or by an interaction helper.
1751 /// @param widgetId The widget.
1752 /// @param name The property.
1753 /// @return True once it was set, even to an empty value.
1754 /// @throws std::out_of_range for an id that names no live widget.
1755 [[nodiscard]] bool hasProp(int widgetId, std::string_view name) const {
1756 return _store->at(widgetId).props.contains(name);
1757 }
1758
1759 /// @brief Whether a widget is alive.
1760 /// @param widgetId The widget.
1761 /// @return False once its destructor ran.
1762 [[nodiscard]] bool exists(int widgetId) const { return _store->records().contains(widgetId); }
1763
1764 /// @brief The id of one of this backend's live widgets.
1765 /// @param widget The widget.
1766 /// @return Its id.
1767 /// @throws std::out_of_range when @p widget is not one.
1768 [[nodiscard]] int idOf(Widget const& widget) const { return _store->idOf(widget); }
1769
1770 /// @brief A live widget's kind.
1771 /// @param widgetId The widget.
1772 /// @return The kind, as the dump shows it.
1773 /// @throws std::out_of_range for an id that names no live widget.
1774 [[nodiscard]] std::string kindOf(int widgetId) const { return _store->at(widgetId).kind; }
1775
1776 /// @brief A live widget's children.
1777 /// @param widgetId The widget.
1778 /// @return Their ids, in order.
1779 /// @throws std::out_of_range for an id that names no live widget.
1780 [[nodiscard]] std::vector<int> children(int widgetId) const { return _store->at(widgetId).children; }
1781
1782 /// @brief A live widget object.
1783 /// @param widgetId The widget.
1784 /// @return The widget.
1785 /// @throws std::out_of_range for an id that names no live widget.
1786 [[nodiscard]] Widget* widget(int widgetId) const { return _store->at(widgetId).widget; }
1787
1788 /// @brief The widget that last took keyboard focus.
1789 /// @return Its id, or 0 when no widget has, or it has since been destroyed.
1790 [[nodiscard]] int focused() const noexcept { return _store->focused(); }
1791
1792 /// @brief Presses a keyboard chord with focus on a widget.
1793 ///
1794 /// The chord goes to the innermost widget, from @p widgetId outwards, that declares it through `setKeys`. Like
1795 /// every helper it acts only when a user could reach @p widgetId.
1796 /// @param widgetId The widget focus is on.
1797 /// @param chord The chord, as declared, such as `"Ctrl+S"`.
1798 /// @return True when a widget took the chord.
1799 bool press(int widgetId, std::string const& chord) {
1800 if (!reachable(_store->at(widgetId))) {
1801 return false;
1802 }
1803 for (int candidate = widgetId; candidate != 0;) {
1804 detail::Record const& record = _store->at(candidate);
1805 if (std::ranges::find(record.chords, chord) != record.chords.end()) {
1806 invoke(record.callbacks.chord, chord);
1807 return true;
1808 }
1809 candidate = record.parent;
1810 }
1811 return false;
1812 }
1813
1814 /// @brief Rests the pointer over a widget: calls its `setOnHover` handler, if it has one and can be reached.
1815 /// @param widgetId The widget.
1816 void hover(int widgetId) {
1817 detail::Record const& record = _store->at(widgetId);
1818 if (reachable(record)) {
1819 invoke(record.callbacks.hover);
1820 }
1821 }
1822
1823 /// @brief Activates a button, or the action button of a banner or an empty state.
1824 /// @param widgetId The button, banner or empty state.
1825 /// @throws std::logic_error when the widget is none of those, or is a banner or empty state that shows no action
1826 /// button.
1827 void click(int widgetId) {
1828 detail::Record const& record = expectKind(widgetId, "click", {"Button", "Banner", "EmptyState"});
1829 if (record.kind != "Button" && prop(widgetId, "actionLabel").empty()) {
1830 throw std::logic_error{"RecordingBackend: click of " + record.kind + "#" + std::to_string(widgetId) +
1831 ", which shows no action button"};
1832 }
1833 if (reachable(record)) {
1834 invoke(record.kind == "Button" ? record.callbacks.click : record.callbacks.action);
1835 }
1836 }
1837
1838 /// @brief Types into a text field: the field shows @p text with the cursor at its end, then `onChange` gets it.
1839 /// @param widgetId The field.
1840 /// @param text The whole new text.
1841 /// @throws std::logic_error when the widget is not a text field.
1842 void edit(int widgetId, std::string text) {
1843 if (detail::Record const* const record = editable(widgetId, "edit", {"TextInput"}); record != nullptr) {
1844 textInput(*record).typedByUser(text, text.size());
1845 invoke(record->callbacks.change, std::move(text));
1846 }
1847 }
1848
1849 /// @brief Submits a text field with @p text, as Enter does: the field shows it with the cursor at its end,
1850 /// `onCommit` gets it, then `onSubmit`.
1851 /// @param widgetId The field.
1852 /// @param text The text submitted.
1853 /// @throws std::logic_error when the widget is not a text field.
1854 void submit(int widgetId, std::string text) {
1855 if (detail::Record const* const record = editable(widgetId, "submit", {"TextInput"}); record != nullptr) {
1856 textInput(*record).typedByUser(text, text.size());
1857 invoke(record->callbacks.commit, text);
1858 invoke(record->callbacks.submit, std::move(text));
1859 }
1860 }
1861
1862 /// @brief Moves a text field's cursor, as the arrow keys or a click would; no handler runs. A read-only field
1863 /// takes it too, since moving the cursor changes nothing.
1864 /// @param widgetId The field.
1865 /// @param position A byte offset into the text it shows; clamped to the text's size.
1866 /// @throws std::logic_error when the widget is not a text field.
1867 // An id and a position: an `int` and a `std::size_t`, which convert into each other.
1868 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
1869 void moveCursor(int widgetId, std::size_t position) {
1870 if (detail::Record const* const record = actionable(widgetId, "moveCursor", {"TextInput"});
1871 record != nullptr) {
1872 textInput(*record).moveCursorByUser(position);
1873 }
1874 }
1875
1876 /// @brief Where a text field's cursor is.
1877 /// @param widgetId The field.
1878 /// @return A byte offset into the text it shows.
1879 /// @throws std::logic_error when the widget is not a text field.
1880 [[nodiscard]] std::size_t cursor(int widgetId) {
1881 return textInput(expectKind(widgetId, "cursor", {"TextInput"})).cursor();
1882 }
1883
1884 /// @brief Commits a text field as focus leaving it after an edit does: `onCommit` gets the text it shows.
1885 /// @param widgetId The field.
1886 /// @throws std::logic_error when the widget is not a text field.
1887 void commit(int widgetId) {
1888 if (detail::Record const* const record = editable(widgetId, "commit", {"TextInput"}); record != nullptr) {
1889 invoke(record->callbacks.commit, textInput(*record).text());
1890 }
1891 }
1892
1893 /// @brief Toggles a check box: it flips its own `checked`, then `onToggle` gets the new state.
1894 /// @param widgetId The check box.
1895 /// @throws std::logic_error when the widget is not a check box.
1896 void toggle(int widgetId) {
1897 if (detail::Record const* const record = editable(widgetId, "toggle", {"Checkbox"}); record != nullptr) {
1898 bool const checked = prop(widgetId, "checked") != "true";
1899 _store->setByUser(widgetId, "checked", detail::formatBool(checked));
1900 invoke(record->callbacks.toggle, checked);
1901 }
1902 }
1903
1904 /// @brief Opens a dropdown select, as a user would, whether or not it has options: `onOpen` runs. Nothing is
1905 /// marked or logged.
1906 /// @param widgetId The select.
1907 /// @throws std::logic_error when the widget is not a select.
1908 void open(int widgetId) {
1909 detail::Record const& record = expectKind(widgetId, "open", {"Select"});
1910 if (reachable(record) && !flagIs(record, "readonly", true)) {
1911 invoke(record.callbacks.open);
1912 }
1913 }
1914
1915 /// @brief Chooses an option of a select: it marks the option, then `onSelect` gets the key.
1916 /// @param widgetId The select.
1917 /// @param key The key of one of its current options.
1918 /// @throws std::logic_error when the widget is not a select, or none of its options has @p key: a user can only
1919 /// choose what is offered.
1920 void choose(int widgetId, Key key) {
1921 detail::Record const& record = expectKind(widgetId, "choose", {"Select"});
1922 auto* const select = dynamic_cast<detail::FakeSelect*>(record.widget);
1923 if (select == nullptr || !select->offers(key)) {
1924 throw std::logic_error{"RecordingBackend: choose of " + detail::formatKey(key) + ", which Select#" +
1925 std::to_string(widgetId) + " does not offer"};
1926 }
1927 if (reachable(record) && !flagIs(record, "readonly", true)) {
1928 select->chooseByUser(key);
1929 invoke(record.callbacks.select, std::move(key));
1930 }
1931 }
1932
1933 /// @brief Chooses an entry of a menu by its path, as a user opening each submenu on the way would.
1934 ///
1935 /// Nothing happens for a path that names no entry, an entry with a submenu (it opens rather than runs), or an
1936 /// entry that is disabled or inside a disabled entry.
1937 /// @param widgetId The menu.
1938 /// @param path The entry's index at each level, from the top.
1939 /// @throws std::logic_error when the widget is not a menu.
1940 void chooseMenuEntry(int widgetId, std::vector<std::size_t> const& path) {
1941 detail::Record const* const record = actionable(widgetId, "chooseMenuEntry", {"Menu"});
1942 if (record == nullptr) {
1943 return;
1944 }
1945 std::vector<MenuEntry> const* level = &record->menu;
1946 MenuEntry const* entry = nullptr;
1947 for (std::size_t const index : path) {
1948 if (level == nullptr || index >= level->size() || !level->at(index).enabled) {
1949 return;
1950 }
1951 entry = &level->at(index);
1952 level = &entry->items;
1953 }
1954 if (entry != nullptr && entry->items.empty()) {
1955 invoke(record->callbacks.path, path);
1956 }
1957 }
1958
1959 /// @brief Chooses an entry of a menu's top level, or a tab of a tab bar (which marks it selected).
1960 /// @param widgetId The menu or tab bar.
1961 /// @param index The entry or tab.
1962 /// @throws std::logic_error when the widget is neither a menu nor a tab bar.
1963 void chooseIndex(int widgetId, std::size_t index) {
1964 if (detail::Record const* const record = actionable(widgetId, "chooseIndex", {"Menu", "Tabs"});
1965 record != nullptr) {
1966 if (record->kind == "Tabs") {
1967 _store->setByUser(widgetId, "selected", std::to_string(index));
1968 invoke(record->callbacks.index, index);
1969 } else {
1970 chooseMenuEntry(widgetId, {index});
1971 }
1972 }
1973 }
1974
1975 /// @brief Collapses or expands a collapsible panel with its own collapse control, which works while the panel is
1976 /// collapsed.
1977 /// @param widgetId The panel.
1978 /// @param collapsed The requested state.
1979 /// @throws std::logic_error when the widget is not a panel, or the panel is not collapsible and so has no
1980 /// collapse control.
1981 void collapse(int widgetId, bool collapsed) {
1982 detail::Record const& record = expectKind(widgetId, "collapse", {"Panel"});
1983 if (prop(widgetId, "collapsible") != "true") {
1984 throw std::logic_error{"RecordingBackend: collapse of Panel#" + std::to_string(widgetId) +
1985 ", which is not collapsible"};
1986 }
1987 if (reachable(record)) {
1988 _store->setByUser(widgetId, "collapsed", detail::formatBool(collapsed));
1989 invoke(record.callbacks.toggle, collapsed);
1990 }
1991 }
1992
1993 /// @brief Dismisses an open dialog or drawer, as Esc does on the TUI: it closes, then `onDismiss` runs. Or
1994 /// dismisses a dismissible banner, which stays shown: the application hides it.
1995 /// @param widgetId The dialog, drawer or banner.
1996 /// @throws std::logic_error when the widget is none of those, or is a banner that is not dismissible.
1997 void dismiss(int widgetId) {
1998 detail::Record const& record = expectKind(widgetId, "dismiss", {"Dialog", "Drawer", "Banner"});
1999 if (record.kind == "Banner" && !flagIs(record, "dismissible", true)) {
2000 throw std::logic_error{"RecordingBackend: dismiss of Banner#" + std::to_string(widgetId) +
2001 ", which is not dismissible"};
2002 }
2003 if (reachable(record)) {
2004 if (record.kind != "Banner") {
2005 _store->setByUser(widgetId, "open", detail::formatBool(false));
2006 }
2007 invoke(record.callbacks.dismiss);
2008 }
2009 }
2010
2011 /// @brief Opens or closes a collapsible section with its own control, which works while it is closed: it shows
2012 /// the state asked for, then `onToggle` gets it.
2013 /// @param widgetId The section.
2014 /// @param open The requested state.
2015 /// @throws std::logic_error when the widget is not a collapsible section.
2016 void expand(int widgetId, bool open) {
2017 if (detail::Record const* const record = actionable(widgetId, "expand", {"Collapsible"}); record != nullptr) {
2018 _store->setByUser(widgetId, "open", detail::formatBool(open));
2019 invoke(record->callbacks.toggle, open);
2020 }
2021 }
2022
2023 /// @brief Drags a splitter's handles: it shows the sizes, then `onResize` gets them.
2024 /// @param widgetId The splitter.
2025 /// @param sizes Every pane's size, in order.
2026 /// @throws std::logic_error when the widget is not a splitter, or @p sizes does not name one size per pane.
2027 void resize(int widgetId, std::vector<int> sizes) {
2028 detail::Record const& record = expectKind(widgetId, "resize", {"Splitter"});
2029 if (sizes.size() != record.children.size()) {
2030 throw std::logic_error{"RecordingBackend: resize of Splitter#" + std::to_string(widgetId) + " with " +
2031 std::to_string(sizes.size()) + " sizes for " +
2032 std::to_string(record.children.size()) + " panes"};
2033 }
2034 if (reachable(record)) {
2035 _store->setByUser(widgetId, "sizes",
2036 detail::formatList(sizes, [](int size) { return std::to_string(size); }));
2037 invoke(record.callbacks.resize, std::move(sizes));
2038 }
2039 }
2040
2041 /// @brief Drops files from the operating system onto a drop zone.
2042 /// @param widgetId The drop zone.
2043 /// @param paths The files' paths, in the order dropped.
2044 /// @return True when the zone took the drop and `onDrop` ran; false when it is unreachable or refused the drop.
2045 /// @throws std::logic_error when the widget is not a drop zone, or @p paths is empty.
2046 bool dropFiles(int widgetId, std::vector<std::string> paths) {
2047 detail::Record const& record = expectKind(widgetId, "dropFiles", {"DropZone"});
2048 if (paths.empty()) {
2049 throw std::logic_error{"RecordingBackend: dropFiles of no files"};
2050 }
2051 auto const& zone = dynamic_cast<detail::FakeDropZone const&>(*record.widget);
2052 if (!reachable(record) || !zone.takes(paths)) {
2053 return false;
2054 }
2055 invoke(record.callbacks.files, std::move(paths));
2056 return true;
2057 }
2058
2059 /// @brief Selects rows of a table by key: the table marks them, then `onSelectionChange` gets the keys.
2060 /// @param widgetId The table.
2061 /// @param keys The keys of every row the user now selects, in the order selected.
2062 /// @throws std::logic_error when the widget is not a table, or the selection is not one a user could make: a key
2063 /// that names no current row, a row twice, any row in a `None` table, or more than one in a `Single` one.
2064 void selectRows(int widgetId, std::vector<Key> keys) {
2065 detail::Record const& record = expectKind(widgetId, "selectRows", {"Table"});
2066 auto* const table = dynamic_cast<detail::FakeTable*>(record.widget);
2067 std::string const where = " of Table#" + std::to_string(widgetId);
2068 if (table == nullptr || table->mode() == SelectionMode::None) {
2069 throw std::logic_error{"RecordingBackend: selectRows" + where + ", which selects nothing"};
2070 }
2071 if (table->mode() == SelectionMode::Single && keys.size() > 1) {
2072 throw std::logic_error{"RecordingBackend: selectRows of several rows" + where + ", which selects one"};
2073 }
2074 std::unordered_set<Key> seen;
2075 for (Key const& key : keys) {
2076 if (!table->rowOf(key).has_value() || !seen.insert(key).second) {
2077 throw std::logic_error{"RecordingBackend: selectRows of " + detail::formatKey(key) + where +
2078 ", which has no such row or selects it twice"};
2079 }
2080 }
2081 if (reachable(record)) {
2082 table->selectByUser(keys);
2083 invoke(record.callbacks.selection, std::move(keys));
2084 }
2085 }
2086
2087 /// @brief Activates a row of a table (Enter or a double click).
2088 /// @param widgetId The table.
2089 /// @param key The row's key.
2090 /// @throws std::logic_error when the widget is not a table, or none of its current rows has @p key.
2091 void activateRow(int widgetId, Key key) {
2092 detail::Record const& record = expectKind(widgetId, "activateRow", {"Table"});
2093 auto const* const table = dynamic_cast<detail::FakeTable const*>(record.widget);
2094 std::optional<int> const rowId = table == nullptr ? std::nullopt : table->rowOf(key);
2095 if (!rowId.has_value()) {
2096 throw std::logic_error{"RecordingBackend: activateRow of " + detail::formatKey(key) + ", which Table#" +
2097 std::to_string(widgetId) + " has no row for"};
2098 }
2099 if (reachable(_store->at(*rowId))) {
2100 invoke(record.callbacks.activate, std::move(key));
2101 }
2102 }
2103
2104 /// @brief Simulates the backend reporting a scroll on a virtualized table, as a real Qt Quick table's own
2105 /// scroll handling would: calls the handler `IViewBackend::TableWidget::setOnVisibleRangeChanged`
2106 /// subscribed, so the mount realizes/releases rows to match. A table whose row count is below its
2107 /// `Virtualization::threshold` never subscribes one, so this call does nothing for it, matching a real
2108 /// backend that never virtualizes a small table.
2109 /// @param widgetId The table.
2110 /// @param first The view index of the first row now wanted.
2111 /// @param count How many rows from `first` are wanted.
2112 /// @throws std::logic_error when the widget is not a table.
2113 void scrollTable(int widgetId, std::size_t first, std::size_t count) {
2114 detail::Record const& record = expectKind(widgetId, "scrollTable", {"Table"});
2115 auto* const table = dynamic_cast<detail::FakeTable*>(record.widget);
2116 if (table == nullptr) {
2117 throw std::logic_error{"RecordingBackend: scrollTable of Table#" + std::to_string(widgetId) +
2118 ", which is not a table"};
2119 }
2120 table->scrollTo(first, count);
2121 }
2122
2123 /// @brief Enters a date-time, or clears the field with `nullopt`.
2124 /// @param widgetId The field.
2125 /// @param value The instant entered.
2126 /// @throws std::logic_error when the widget is not a date-time field.
2127 void setDateTime(int widgetId, std::optional<morph::time::Timestamp> value) {
2128 if (detail::Record const* const record = editable(widgetId, "setDateTime", {"DateTimeInput"});
2129 record != nullptr) {
2130 _store->setByUser(widgetId, "value", detail::formatTimestamp(value));
2131 invoke(record->callbacks.dateTime, value);
2132 }
2133 }
2134
2135 /// @brief Moves a slider to @p value, a finished move: `onChange` gets it, then `onCommit`.
2136 /// @param widgetId The slider.
2137 /// @param value The value moved to.
2138 /// @throws std::logic_error when the widget is not a slider.
2139 void slide(int widgetId, std::int64_t value) {
2140 if (detail::Record const* const record = editable(widgetId, "slide", {"Slider"}); record != nullptr) {
2141 _store->setByUser(widgetId, "value", std::to_string(value));
2142 // The change handler may destroy the slider, so the commit handler is copied before either runs.
2143 auto const commit = record->callbacks.slideCommit;
2144 invoke(record->callbacks.slide, value);
2145 invoke(commit, value);
2146 }
2147 }
2148
2149 /// @brief Moves a slider to @p value while the move is still in progress: `onChange` gets it, and `onCommit` does
2150 /// not run until the move finishes. Named apart from `drag`, which drags one widget onto another.
2151 /// @param widgetId The slider.
2152 /// @param value The value moved to.
2153 /// @throws std::logic_error when the widget is not a slider.
2154 void dragSlider(int widgetId, std::int64_t value) {
2155 if (detail::Record const* const record = editable(widgetId, "dragSlider", {"Slider"}); record != nullptr) {
2156 _store->setByUser(widgetId, "value", std::to_string(value));
2157 invoke(record->callbacks.slide, value);
2158 }
2159 }
2160
2161 /// @brief Picks a path in a file picker.
2162 /// @param widgetId The picker.
2163 /// @param path The path picked.
2164 /// @throws std::logic_error when the widget is not a file picker.
2165 void pick(int widgetId, std::string path) {
2166 if (detail::Record const* const record = editable(widgetId, "pick", {"FilePicker"}); record != nullptr) {
2167 _store->setByUser(widgetId, "path", detail::formatText(path));
2168 invoke(record->callbacks.picked, std::move(path));
2169 }
2170 }
2171
2172 /// @brief Drags one widget onto another. Any kind of widget can be a source or a target.
2173 ///
2174 /// An empty `accepts` refuses every key: the mount always passes a predicate, so a drop that lands without
2175 /// one shows the mount's accept-everything default at work rather than this backend's leniency.
2176 /// @param sourceId The dragged widget; it must have a drag key.
2177 /// @param targetId The widget dropped on; it must be a drop target whose `accepts` holds for the key.
2178 /// @return True when the drop was accepted and `onDrop` ran.
2179 /// @throws std::logic_error when @p sourceId and @p targetId are the same widget.
2180 bool drag(int sourceId, int targetId) {
2181 if (sourceId == targetId) {
2182 throw std::logic_error{"RecordingBackend: drag of a widget onto itself"};
2183 }
2184 detail::Record const& source = _store->at(sourceId);
2185 detail::Record const& target = _store->at(targetId);
2186 if (!reachable(source) || !reachable(target)) {
2187 return false;
2188 }
2189 std::optional<Key> const key = source.dragKey;
2190 auto const accepts = target.callbacks.accepts;
2191 auto const drop = target.callbacks.drop;
2192 if (!key.has_value() || !drop || !accepts || !accepts(*key)) {
2193 return false;
2194 }
2195 drop(*key);
2196 return true;
2197 }
2198
2199private:
2200 // The fake behind a text field's record.
2201 static detail::FakeTextInput& textInput(detail::Record const& record) {
2202 return dynamic_cast<detail::FakeTextInput&>(*record.widget);
2203 }
2204
2205 // A helper applies to some kinds only; using it on another kind is a mistake in the test, not a user action.
2206 detail::Record& expectKind(int widgetId, std::string_view helper, std::initializer_list<std::string_view> kinds) {
2207 detail::Record& record = _store->at(widgetId);
2208 if (std::ranges::find(kinds, std::string_view{record.kind}) == kinds.end()) {
2209 throw std::logic_error{"RecordingBackend: " + std::string{helper} + " does not apply to " + record.kind +
2210 "#" + std::to_string(widgetId)};
2211 }
2212 return record;
2213 }
2214
2215 // The widget's record when a user could reach it now, else null.
2216 [[nodiscard]] detail::Record* actionable(int widgetId, std::string_view helper,
2217 std::initializer_list<std::string_view> kinds) {
2218 detail::Record& record = expectKind(widgetId, helper, kinds);
2219 return reachable(record) ? &record : nullptr;
2220 }
2221
2222 // The widget's record when a user could reach it now and change its value, else null.
2223 [[nodiscard]] detail::Record* editable(int widgetId, std::string_view helper,
2224 std::initializer_list<std::string_view> kinds) {
2225 detail::Record* const record = actionable(widgetId, helper, kinds);
2226 return record != nullptr && !flagIs(*record, "readonly", true) ? record : nullptr;
2227 }
2228
2229 // A user reaches a widget that is shown and enabled, inside containers that are all shown, enabled and not
2230 // collapsed. A collapsed panel's own collapse control stays reachable, so only an outer panel's state counts. A
2231 // dialog shows nothing, itself included, until it is opened.
2232 [[nodiscard]] bool reachable(detail::Record const& record) const {
2233 if (flagIs(record, "visible", false) || flagIs(record, "enabled", false) || isClosedDialog(record)) {
2234 return false;
2235 }
2236 for (int innerId = record.id, outerId = record.parent; outerId != 0;) {
2237 detail::Record const& outer = _store->at(outerId);
2238 if (flagIs(outer, "visible", false) || flagIs(outer, "enabled", false) ||
2239 flagIs(outer, "collapsed", true) || isClosedDialog(outer) || closedAround(outer, innerId)) {
2240 return false;
2241 }
2242 innerId = outerId;
2243 outerId = outer.parent;
2244 }
2245 return true;
2246 }
2247
2248 // A dialog or drawer shows nothing until it is opened.
2249 [[nodiscard]] static bool isClosedDialog(detail::Record const& record) {
2250 return (record.kind == "Dialog" || record.kind == "Drawer") && !flagIs(record, "open", true);
2251 }
2252
2253 // A closed collapsible section hides its content, the child on the way in, unless that child is its header.
2254 [[nodiscard]] static bool closedAround(detail::Record const& outer, int childId) {
2255 if (outer.kind != "Collapsible" || !flagIs(outer, "open", false)) {
2256 return false;
2257 }
2258 auto const header = outer.props.find("header");
2259 return header == outer.props.end() || header->second != std::to_string(childId);
2260 }
2261
2262 [[nodiscard]] static bool flagIs(detail::Record const& record, std::string_view name, bool value) {
2263 auto const found = record.props.find(name);
2264 return found != record.props.end() && found->second == detail::formatBool(value);
2265 }
2266
2267 // The copy keeps the handler alive while it runs, even when it destroys the widget it was stored on.
2268 template <typename... Args, typename... Values>
2269 static void invoke(std::function<void(Args...)> const& handler, Values&&... values) {
2270 auto const copy = handler;
2271 if (copy) {
2272 copy(std::forward<Values>(values)...);
2273 }
2274 }
2275
2276 std::shared_ptr<detail::RecordStore> _store = std::make_shared<detail::RecordStore>();
2277 std::unordered_set<std::string> _components;
2278};
2279
2280} // namespace morph::ui::testing
Justify
Where a stack places its spare length along its axis, while no shown child stretches.
Definition alignment.hpp:23
@ Center
Packed in the middle.
Definition alignment.hpp:25
@ Between
In equal gaps between the children, never less than the stack's gap.
Definition alignment.hpp:27
@ End
Packed at the end.
Definition alignment.hpp:26
@ Start
Packed at the start.
Definition alignment.hpp:24
Align
Where a child is placed across its container's axis: a column's horizontally, a row's vertically,...
Definition alignment.hpp:15
@ Center
Centred.
Definition alignment.hpp:18
@ End
At the end: right in a column, bottom in a row or grid.
Definition alignment.hpp:19
@ Start
At the start: left in a column, top in a row or grid.
Definition alignment.hpp:17
@ Stretch
A content child fills the cross extent; one its maximum stops sits at the start.
Definition alignment.hpp:16
ButtonEmphasis
How much a button stands out; each backend maps it to its theme.
@ Primary
The screen's main action.
@ Ghost
An action embedded in other content, with no chrome of its own.
@ Normal
An ordinary action.
@ Danger
An action that destroys or cannot be undone.
ButtonStyle
How a button is drawn; it behaves the same either way.
@ Item
An entry of a list: as wide as its line, its label at the start, unframed, and marked while selected.
A short label marking a state.
Definition backend.hpp:626
A message across the top of a region, with an optional action button and an optional dismissal.
Definition backend.hpp:586
A progress indicator.
Definition backend.hpp:436
A labelled check box.
Definition backend.hpp:226
A titled section the user opens and closes. A new collapsible is open.
Definition backend.hpp:735
A widget with children. A factory given this container appends the new widget as its last child.
Definition backend.hpp:103
A component a renderer loaded in a custom node's place (spec 5 §10): a bundle's verified QML or a hos...
Definition backend.hpp:773
A date or date-and-time field, in the mode and display zone fixed at creation.
Definition backend.hpp:533
A modal overlay around its children, with a focus trap while open. A new dialog is closed.
Definition backend.hpp:415
A panel over the content that slides in from the side fixed at creation; it follows a dialog's rules....
Definition backend.hpp:699
A region that takes files dropped from the operating system.
Definition backend.hpp:755
What a region shows when it has nothing to show.
Definition backend.hpp:674
A file path field, in the mode fixed at creation.
Definition backend.hpp:574
Children laid out row-major in columns.
Definition backend.hpp:326
Captions beside their values.
Definition backend.hpp:666
A menu of commands, with nested submenus.
Definition backend.hpp:296
A titled frame around its children, optionally collapsible.
Definition backend.hpp:351
A choice of one keyed option.
Definition backend.hpp:246
An integer on a range.
Definition backend.hpp:551
Panes along the axis fixed at creation, with handles between them that the user drags.
Definition backend.hpp:720
Children stacked along one axis, fixed at creation.
Definition backend.hpp:310
The steps of a process, each with its state.
Definition backend.hpp:654
Keyed rows under column headers, with selection and activation by key.
Definition backend.hpp:462
A tab bar over page slots.
Definition backend.hpp:391
An editable text field.
Definition backend.hpp:178
Read-only text.
Definition backend.hpp:137
A passive, transient message outside the normal flow: non-modal, dims nothing, and never takes keyboa...
Definition backend.hpp:615
A retained widget. A new widget is visible, enabled, content-sized, not draggable and not a drop targ...
Definition backend.hpp:34
A headless IViewBackend: fake widgets that record every operation, a golden dump of the live tree,...
void setDateTime(int widgetId, std::optional< morph::time::Timestamp > value)
Enters a date-time, or clears the field with nullopt.
void selectRows(int widgetId, std::vector< Key > keys)
Selects rows of a table by key: the table marks them, then onSelectionChange gets the keys.
int idOf(Widget const &widget) const
The id of one of this backend's live widgets.
bool press(int widgetId, std::string const &chord)
Presses a keyboard chord with focus on a widget.
std::string prop(int widgetId, std::string_view name) const
A property's formatted value, escaped as the dump shows it.
std::unique_ptr< PanelWidget > createPanel(ContainerWidget *parent) override
Makes a titled frame.
std::unique_ptr< SelectWidget > createSelect(ContainerWidget *parent, SelectStyle style) override
Makes a choice of one option.
std::vector< int > children(int widgetId) const
A live widget's children.
void pick(int widgetId, std::string path)
Picks a path in a file picker.
void dismiss(int widgetId)
Dismisses an open dialog or drawer, as Esc does on the TUI: it closes, then onDismiss runs....
std::unique_ptr< ToastWidget > createToast(ContainerWidget *parent) override
Makes a transient message.
std::unique_ptr< BannerWidget > createBanner(ContainerWidget *parent) override
Makes a banner.
std::unique_ptr< GridWidget > createGrid(ContainerWidget *parent) override
Makes a grid.
void slide(int widgetId, std::int64_t value)
Moves a slider to value, a finished move: onChange gets it, then onCommit.
std::unique_ptr< TabsWidget > createTabs(ContainerWidget *parent) override
Makes a tab bar.
bool hasProp(int widgetId, std::string_view name) const
Whether a property was ever set, by a setter, at creation or by an interaction helper.
void clearLog()
Empties the operation log.
void chooseIndex(int widgetId, std::size_t index)
Chooses an entry of a menu's top level, or a tab of a tab bar (which marks it selected).
std::unique_ptr< SliderWidget > createSlider(ContainerWidget *parent) override
Makes a slider.
bool drag(int sourceId, int targetId)
Drags one widget onto another. Any kind of widget can be a source or a target.
std::unique_ptr< DropZoneWidget > createDropZone(ContainerWidget *parent) override
Makes a drop zone.
int focused() const noexcept
The widget that last took keyboard focus.
std::unique_ptr< TextInputWidget > createTextInput(ContainerWidget *parent, TextInputMode mode) override
Makes a text field.
void commit(int widgetId)
Commits a text field as focus leaving it after an edit does: onCommit gets the text it shows.
void activateRow(int widgetId, Key key)
Activates a row of a table (Enter or a double click).
std::unique_ptr< MenuWidget > createMenu(ContainerWidget *parent) override
Makes a command list.
std::unique_ptr< CheckboxWidget > createCheckbox(ContainerWidget *parent) override
Makes a check box.
std::unique_ptr< DateTimeInputWidget > createDateTimeInput(ContainerWidget *parent, DateMode mode, int offsetMinutes) override
Makes a date or date-and-time field.
std::unique_ptr< StackWidget > createStack(ContainerWidget *parent, Axis axis) override
Makes a stack.
std::unique_ptr< ScrollWidget > createScroll(ContainerWidget *parent, Axis axis) override
Makes a scroll area.
std::unique_ptr< CollapsibleWidget > createCollapsible(ContainerWidget *parent) override
Makes a collapsible section.
std::optional< int > find(std::string_view kind, std::string_view name, std::string_view value) const
The first live widget, by id, of a kind whose property has a value.
void choose(int widgetId, Key key)
Chooses an option of a select: it marks the option, then onSelect gets the key.
void chooseMenuEntry(int widgetId, std::vector< std::size_t > const &path)
Chooses an entry of a menu by its path, as a user opening each submenu on the way would.
std::vector< int > all(std::string_view kind) const
Every live widget of a kind.
bool exists(int widgetId) const
Whether a widget is alive.
std::unique_ptr< DrawerWidget > createDrawer(ContainerWidget *parent, Side side) override
Makes a drawer.
void expand(int widgetId, bool open)
Opens or closes a collapsible section with its own control, which works while it is closed: it shows ...
std::unique_ptr< SplitterWidget > createSplitter(ContainerWidget *parent, Axis axis) override
Makes a splitter.
std::string dump() const
The golden tree of the live widgets.
std::unique_ptr< SlotWidget > createSlot(ContainerWidget *parent) override
Makes a slot.
std::unique_ptr< TableWidget > createTable(ContainerWidget *parent) override
Makes a table.
void loadsComponents(std::vector< std::string > names)
Names the components createCustom loads; none until called, so a custom node shows its fallback.
std::unique_ptr< BadgeWidget > createBadge(ContainerWidget *parent) override
Makes a badge.
std::size_t cursor(int widgetId)
Where a text field's cursor is.
std::vector< std::string > const & log() const
The operation log since construction or the last clearLog().
void open(int widgetId)
Opens a dropdown select, as a user would, whether or not it has options: onOpen runs....
bool dropFiles(int widgetId, std::vector< std::string > paths)
Drops files from the operating system onto a drop zone.
void scrollTable(int widgetId, std::size_t first, std::size_t count)
Simulates the backend reporting a scroll on a virtualized table, as a real Qt Quick table's own scrol...
void dragSlider(int widgetId, std::int64_t value)
Moves a slider to value while the move is still in progress: onChange gets it, and onCommit does not ...
void raiseEvent(int widgetId, std::string event, std::string payload)
Raises an event of a loaded component, as the component would.
void collapse(int widgetId, bool collapsed)
Collapses or expands a collapsible panel with its own collapse control, which works while the panel i...
void click(int widgetId)
Activates a button, or the action button of a banner or an empty state.
void hover(int widgetId)
Rests the pointer over a widget: calls its setOnHover handler, if it has one and can be reached.
std::unique_ptr< DialogWidget > createDialog(ContainerWidget *parent) override
Makes a modal overlay.
std::unique_ptr< StepsWidget > createSteps(ContainerWidget *parent) override
Makes a steps indicator.
void submit(int widgetId, std::string text)
Submits a text field with text, as Enter does: the field shows it with the cursor at its end,...
std::unique_ptr< FilePickerWidget > createFilePicker(ContainerWidget *parent, FilePickerMode mode) override
Makes a file path field.
std::unique_ptr< BusyWidget > createBusy(ContainerWidget *parent) override
Makes a progress indicator.
void edit(int widgetId, std::string text)
Types into a text field: the field shows text with the cursor at its end, then onChange gets it.
std::unique_ptr< SpacerWidget > createSpacer(ContainerWidget *parent) override
Makes empty space.
std::unique_ptr< EmptyStateWidget > createEmptyState(ContainerWidget *parent) override
Makes an empty state.
std::unique_ptr< KeyValueWidget > createKeyValue(ContainerWidget *parent) override
Makes a key-value list.
std::unique_ptr< ProgressWidget > createProgress(ContainerWidget *parent) override
Makes a progress bar.
void moveCursor(int widgetId, std::size_t position)
Moves a text field's cursor, as the arrow keys or a click would; no handler runs. A read-only field t...
void toggle(int widgetId)
Toggles a check box: it flips its own checked, then onToggle gets the new state.
std::string kindOf(int widgetId) const
A live widget's kind.
std::unique_ptr< TextWidget > createText(ContainerWidget *parent) override
Makes read-only text.
Widget * widget(int widgetId) const
A live widget object.
std::unique_ptr< ButtonWidget > createButton(ContainerWidget *parent) override
Makes a push button.
void resize(int widgetId, std::vector< int > sizes)
Drags a splitter's handles: it shows the sizes, then onResize gets them.
std::unique_ptr< CustomWidget > createCustom(ContainerWidget *parent, CustomRequest const &request) override
Loads a component loadsComponents named, as a Custom widget with properties component and minVersion;...
A fake BadgeWidget: properties tone, text, icon.
void setTone(Tone tone) override
Sets what the mark conveys.
void setIcon(std::string_view icon) override
Sets the icon; called once.
void setText(std::string_view text) override
Replaces the label.
A fake BannerWidget: properties tone, text, actionLabel, dismissible.
void setActionLabel(std::string_view label) override
Replaces the action button's caption.
void setDismissible(bool dismissible) override
Sets whether the user can dismiss the banner; called once, before the banner is shown.
void setOnAction(Action onAction) override
Sets what activating the action button calls.
void setTone(Tone tone) override
Sets what the message conveys.
void setText(std::string_view text) override
Replaces the message.
void setOnDismiss(Action onDismiss) override
Sets what a dismissal calls. A dismissal leaves the banner shown: the application hides it.
A fake BusyWidget: properties active, label.
void setActive(bool active) override
Starts or stops the animation.
void setLabel(std::string_view label) override
Replaces the caption.
A fake ButtonWidget: properties label, emphasis, icon, style and selected.
void setStyle(ButtonStyle style) override
Sets how the button is drawn. A new button is a push button; the mount calls it once,...
void setEmphasis(ButtonEmphasis emphasis) override
Sets how much the button stands out. The mount calls it only for another value than Normal.
void setOnClick(Action onClick) override
Sets what activation calls (Enter, Space or a click).
void setLabel(std::string_view label) override
Replaces the caption.
void setIcon(std::string_view icon) override
Sets the icon drawn beside the caption; called once, and only for a button that has one.
void setSelected(bool selected) override
Marks a list entry as the current one, or clears the mark. A new button is not selected; the mount ca...
A fake CheckboxWidget: properties label, checked.
void setChecked(bool checked) override
Sets the state. Never calls the onToggle handler.
void setOnToggle(std::function< void(bool)> onToggle) override
Sets what a user toggle calls, with the new state.
void setLabel(std::string_view label) override
Replaces the caption.
A fake CollapsibleWidget: properties title, header (the header child's id), open.
void setTitle(std::string_view title) override
Replaces the title.
void setOpen(bool open) override
Opens or closes it. Never calls the onToggle handler.
void setOnToggle(std::function< void(bool)> onToggle) override
Sets what the user opening or closing it calls.
void setHeader(Widget &header) override
Records property header, the child's id.
The ContainerWidget half of every fake container.
void moveChild(Widget &child, std::size_t index) override
Moves a child among its siblings and logs it.
A fake loaded CustomWidget: property component, and each prop as prop.<name>.
void setOnEvent(std::function< void(std::string event, std::string payload)> onEvent) override
Hands over the handler of the component's events.
void setTextProp(std::string_view name, std::string_view value) override
Sets a text prop: string, and int, exact and json, which cross as text so that no value loses precisi...
void setNumberProp(std::string_view name, double value) override
Sets a number prop: the one kind of value that crosses as a double.
void setBoolProp(std::string_view name, bool value) override
Sets a bool prop.
A fake DateTimeInputWidget: properties mode, offset, value, caption, captionShown.
void setOnChange(std::function< void(std::optional< morph::time::Timestamp >)> onChange) override
Sets what a user edit calls, with the instant entered or nullopt for a cleared field.
void setValue(std::optional< morph::time::Timestamp > const &value) override
Shows an instant. Never calls the onChange handler.
void setCaption(std::string_view label, bool shown) override
Sets the field's label, and whether the input draws it as a visible caption above its control....
A fake DialogWidget: properties open, title; a Footer child once its footer is asked for.
void setOnDismiss(Action onDismiss) override
Sets what a user dismissal (Esc on the TUI) of the open dialog calls.
void setOpen(bool open) override
Shows or hides the overlay.
void setTitle(std::string_view title) override
Replaces the title.
A fake DrawerWidget: properties side, open, title; a Footer child once its footer is asked for.
void setOnDismiss(Action onDismiss) override
Sets what a user dismissal (Esc on the TUI) calls.
void setTitle(std::string_view title) override
Replaces the title.
void setOpen(bool open) override
Shows or hides it. Never calls the onDismiss handler.
A fake DropZoneWidget: properties accept ([.ext,…]), multiple.
bool takes(std::vector< std::string > const &paths) const
Whether the zone takes a drop of paths: one file unless several are allowed, each with an accepted ex...
void setAccept(std::vector< std::string > const &extensions) override
Sets the extensions it takes; called once.
void setOnDrop(std::function< void(std::vector< std::string >)> onDrop) override
Sets what a drop it takes calls. A drop with a file outside the accepted extensions,...
void setMultiple(bool multiple) override
Sets whether a drop may carry more than one file; called once.
A fake EmptyStateWidget: properties title, text, icon, actionLabel.
void setIcon(std::string_view icon) override
Sets the icon; called once.
void setActionLabel(std::string_view label) override
Replaces the action button's caption.
void setOnAction(Action onAction) override
Sets what activating the action button calls.
void setTitle(std::string_view title) override
Replaces the title.
void setText(std::string_view text) override
Replaces the explanation.
The FieldWidget half of every fake field: properties readonly, required, errors, stale.
void setStale(bool stale) override
Records property stale.
void setErrors(std::vector< std::string > const &errors) override
Records property errors, as formatList gives the messages.
void setRequired(bool required) override
Records property required.
void setReadOnly(bool readonly) override
Records property readonly; RecordingBackend's edits leave a read-only field alone.
A fake FilePickerWidget: properties mode, path.
void setOnPicked(std::function< void(std::string)> onPicked) override
Sets what a user pick calls, with the path.
void setPath(std::string_view path) override
Shows a path. Never calls the onPicked handler.
A fake GridWidget: properties columns, gap, align, and span on a child.
void setSpan(Widget &child, int span) override
Records property span on child.
void setColumns(int columns) override
Sets the column count.
void setGap(int gap) override
Sets the space between cells.
void setAlign(Align align) override
Places each cell's content vertically in its row. A new grid stretches it; a child's own alignSelf wi...
A fake KeyValueWidget: property items ([label:value,…]).
void setItems(std::vector< KeyValueItem > const &items) override
Replaces the pairs.
The Widget half of every fake: records itself on construction, records Common's setters,...
void set(std::string_view name, std::string value)
Records a property set through a setter.
std::shared_ptr< RecordStore > const & sharedStore() const noexcept
The store, shared: what a part this widget makes (an overlay's footer) records into.
FakeLeaf(std::shared_ptr< RecordStore > store, std::string kind, ContainerWidget const *parent, PropList initial={})
Records the new widget, appended to parent, and logs its creation.
void setTooltip(std::string_view text) override
Records property tooltip.
RecordStore & store() const noexcept
The store this widget records into.
void setAccessibleName(std::string_view name) override
Records property a11yName.
int id() const noexcept
This widget's id.
void setKeys(std::vector< std::string > const &chords, std::function< void(std::string)> onChord) override
Records property keys, the chords joined by commas, and keeps the chords and the handler for Recordin...
void setEnabled(bool enabled) override
Records property enabled.
~FakeLeaf() override
Forgets the widget and logs its destruction; its remaining children become roots.
void setDragKey(std::optional< Key > const &key) override
Records property dragKey and keeps the key for RecordingBackend::drag.
void setAccessibleRole(std::string_view role) override
Records property a11yRole.
void setVisible(bool visible) override
Records property visible.
void setLayout(LayoutHints const &hints) override
Records property layout, as formatLayout gives it.
void focus() override
Moves the backend's recorded focus here, and logs focus.
Callbacks & callbacks()
This widget's handlers.
void setTestId(std::string_view testId) override
Records property testId.
void setOnHover(std::function< void()> onHover) override
Keeps the handler for RecordingBackend::hover.
void setDropHandler(std::function< bool(Key const &)> accepts, std::function< void(Key)> onDrop) override
Records property drop=handler and keeps both handlers for RecordingBackend::drag.
void setSurface(std::string_view surface) override
Records property surface.
A fake MenuWidget: property items.
void setOnActivate(std::function< void(std::vector< std::size_t >)> onActivate) override
Sets what choosing an entry calls, with its path.
void setItems(std::vector< MenuEntry > const &entries) override
Replaces the entries.
A fake overlay's footer: a Footer child, made when it is first asked for, so an overlay that never sh...
ContainerWidget & footer() override
The footer, a Footer container child of the overlay.
A fake PanelWidget: properties title, header (the child's id), padding, collapsible,...
void setTitle(std::string_view title) override
Replaces the title.
void setCollapsible(bool collapsible) override
Sets whether the user can collapse the panel.
void setOnToggle(std::function< void(bool)> onToggle) override
Sets what a user collapse or expand calls, with the requested state.
void setHeader(Widget &header) override
Records property header, the child's id.
void setCollapsed(bool collapsed) override
Collapses or expands the panel. A collapsed panel hides its content, which takes no input; its own co...
void setPadding(int padding) override
Sets the space between frame and content.
A fake ProgressWidget: properties value (as formatFraction gives it), label.
void setLabel(std::string_view label) override
Replaces the caption beside the bar.
void setValue(std::optional< double > value) override
Sets how far along it is.
A fake SelectWidget: properties style, options, selected, caption, captionShown.
void chooseByUser(Key const &key)
Marks the option the user chose, as the requested key; nothing is logged.
void setOnSelect(std::function< void(Key)> onSelect) override
Stores the handler RecordingBackend::choose calls.
bool offers(Key const &key) const
Whether one of the current options has key.
void setOnOpen(Action onOpen) override
Stores the handler RecordingBackend::open calls.
void setOptions(std::vector< SelectOption > const &options) override
Records property options, and re-marks the requested key against them.
void setSelected(std::optional< Key > const &key) override
Requests the option with key, and records property selected as the option it marks.
void setCaption(std::string_view label, bool shown) override
Sets the field's label, and whether the input draws it as a visible caption above its control....
A fake SliderWidget: properties range (minimum..maximum/step), value.
void setValue(std::int64_t value) override
Shows a value. Never calls the onChange handler.
void setOnChange(std::function< void(std::int64_t)> onChange) override
Sets what a user move calls, with the new value.
void setRange(std::int64_t minimum, std::int64_t maximum, std::int64_t step) override
Sets the range and the increment.
void setOnCommit(std::function< void(std::int64_t)> onCommit) override
Sets what a finished move calls, with the value it ended on: a release after a drag,...
A fake SplitterWidget: properties axis, sizes ([n,…]).
void setSizes(std::vector< int > const &sizes) override
Sets each pane's size. Never calls the onResize handler.
void setOnResize(std::function< void(std::vector< int >)> onResize) override
Sets what dragging a handle calls, with every pane's size once the user lets go.
A fake StackWidget, of kind Column or Row: properties gap, align, justify.
void setGap(int gap) override
Sets the space between children.
void setJustify(Justify justify) override
Places the spare length along the axis while no shown child stretches. A new stack packs at the start...
void setAlign(Align align) override
Places the children across the axis. A new stack stretches them; a child's own alignSelf wins.
A fake StepsWidget: properties steps ([label:State,…]), current (a position, or none).
void setSteps(std::vector< Step > const &steps) override
Replaces the steps.
void setCurrent(std::optional< std::size_t > index) override
Highlights one step.
A fake TableWidget: properties columns, selectionMode, selection, and rowKey on a row.
void selectByUser(std::vector< Key > const &keys)
Marks the rows the user selected, as the requested keys; nothing is logged.
void setRowTone(Widget &row, std::optional< Tone > tone) override
Records the row's property tone (none for no tone).
void setRowKey(Widget &row, Key const &key) override
Records property rowKey on row, and marks the row when its key is requested.
void setRealizedWindow(std::size_t first, std::size_t count, std::size_t total) override
Records property realizedWindow as <first>+<count>/<total>.
void setOnSelectionChange(std::function< void(std::vector< Key >)> onSelectionChange) override
Stores the handler RecordingBackend::selectRows calls.
void setSelection(std::vector< Key > const &keys) override
Requests the rows with keys, and records property selection as the rows it marks.
void setSelectionMode(SelectionMode mode) override
Records property selectionMode, which RecordingBackend::selectRows enforces.
FakeTable(std::shared_ptr< RecordStore > store, std::string kind, ContainerWidget const *parent)
Records the new table, appended to parent, and logs its creation.
void setShowHeader(bool shown) override
Records property showHeader.
void setColumns(std::vector< TableColumn > const &columns) override
Records property columns, each as its escaped caption, a colon and its width.
~FakeTable() override
Removes the hook that points at this table, so nothing calls it once the table is gone,...
void scrollTo(std::size_t first, std::size_t count)
Simulates the backend reporting a scroll: calls the handler setOnVisibleRangeChanged stored,...
void setOnActivate(std::function< void(Key)> onActivate) override
Stores the handler RecordingBackend::activateRow calls.
std::optional< int > rowOf(Key const &key) const
The current row with key.
void setOnVisibleRangeChanged(std::function< void(std::size_t, std::size_t)> onVisibleRangeChanged) override
Stores the handler RecordingBackend::scrollTable calls.
SelectionMode mode() const noexcept
The selection mode last set.
A fake TabsWidget: properties tabs, selected, barShown.
void setSelected(std::size_t index) override
Highlights a tab. Never calls the onSelect handler.
void setBarShown(bool shown) override
Shows or hides the strip of tab buttons. The selected page shows either way. A new tabs widget shows ...
void setTabs(std::vector< std::string > const &labels) override
Replaces the tab captions.
void setOnSelect(std::function< void(std::size_t)> onSelect) override
Sets what a user's tab choice calls, with its index.
A fake TextInputWidget: properties mode, text, placeholder, unit, decimals, icon, caption,...
void setIcon(std::string_view icon) override
Sets the icon drawn at the field's leading edge; empty shows none. Called once, and only for a single...
std::string const & text() const noexcept
The text the field shows.
void setDecimals(int decimals) override
Sets the fraction digits a number is entered with: what a backend or a kit may use to align or format...
void setOnChange(std::function< void(std::string)> onChange) override
Sets what a user edit calls, with the whole new text.
void setOnCommit(std::function< void(std::string)> onCommit) override
Sets what a commit calls, with the text: Enter in single-line mode, before the submit,...
void setPlaceholder(std::string_view placeholder) override
Sets the text shown while the field is empty.
void setCaption(std::string_view label, bool shown) override
Sets the field's label, and whether the input draws it as a visible caption above its control....
void moveCursorByUser(std::size_t cursor) noexcept
Moves the cursor as the user's arrow keys or a click would.
void typedByUser(std::string text, std::size_t cursor)
The text the user typed: the field shows it, and the cursor is at cursor; nothing is logged.
void setOnSubmit(std::function< void(std::string)> onSubmit) override
Sets what a submit (Enter in single-line mode) calls, with the text.
std::size_t cursor() const noexcept
Where the cursor is.
void setUnit(std::string_view unit) override
Sets the unit the input shows, muted, at its trailing edge, inside the field; empty shows none....
void setText(std::string_view text) override
Records property text; moves the cursor to the end only when the text changes.
A fake TextWidget: properties text, role.
void setRole(TextRole role) override
Sets the style.
void setText(std::string_view text) override
Replaces the text.
A fake ToastWidget: properties text, tone, and shown (incremented on every show, so a test can tell a...
void show(std::string_view text, Tone tone) override
Shows the message, replacing any shown or dismissing, and (re)starts the dismissal interval regardles...
The records, the roots and the operation log that every fake widget of one backend writes to.
void focus(int widgetId)
Records that keyboard focus moved to a widget, and logs focus.
Record const & at(int widgetId) const
A widget's record.
int idOf(Widget const &widget) const
The id of a live widget made by this store's backend.
void setByUser(int widgetId, std::string_view name, std::string value)
Sets a property the user changed through the widget itself; no setter ran, so nothing is logged.
int focused() const noexcept
The widget that last took focus.
std::string dump() const
The golden tree.
void destroy(int widgetId)
Forgets a destroyed widget and logs it. Its remaining children become roots.
int create(std::string kind, ContainerWidget const *parent, Widget &widget, PropList initial)
Records a new widget appended to parent and logs its creation.
void clearLog() noexcept
Empties the operation log.
std::vector< std::string > const & log() const noexcept
The operation log.
void move(int widgetId, std::size_t index)
Moves a widget among its siblings, as ContainerWidget::moveChild defines it, and logs it.
void set(int widgetId, std::string_view name, std::string value)
Sets a property through a widget setter, and logs it.
std::map< int, Record > const & records() const noexcept
Every live widget's record.
Record & at(int widgetId)
A widget's record.
UTC timestamps for morph actions.
std::string enumName(ButtonEmphasis emphasis)
A button emphasis's name.
std::string formatJustify(Justify justify)
Formats a justification.
std::string formatMenu(std::vector< MenuEntry > const &entries)
Formats a menu's entries: [a,b], each entry its label followed by the attributes that differ from the...
std::string formatLayout(LayoutHints const &hints)
Formats a size request.
std::string formatItem(std::string_view text)
Formats free text as an element of a formatList list. Also escapes ,, [ and ], so the text reads as o...
std::vector< std::pair< std::string, std::string > > PropList
Properties fixed when a widget is created, as name and formatted value.
std::string escape(std::string_view text, Special const &isSpecial)
Escapes text with a backslash: a line break becomes backslash and n, a carriage return backslash and ...
std::string formatAlign(Align align)
Formats an alignment.
std::string formatBool(bool value)
Formats a flag.
std::string formatKey(Key const &key)
Formats a key: an integer in decimal, a string in double quotes, so 7 and "7" differ.
std::string formatSizing(Sizing sizing)
Formats one dimension's sizing.
std::string formatOptionalKey(std::optional< Key > const &key)
Formats an optional key.
std::string formatList(std::vector< T > const &items, F const &each)
Formats a list as [a,b,c].
std::string formatFraction(std::optional< double > value)
A fraction as its shortest decimal text, or none.
std::string formatText(std::string_view text)
Formats free text as a property value. Line breaks and = are escaped, so the value stays on its own l...
std::string formatTimestamp(std::optional< morph::time::Timestamp > const &value)
Formats a date-time value.
SelectionMode
How many rows a selection may hold.
Definition selection.hpp:43
What IViewBackend::createCustom asks for.
Definition backend.hpp:799
One row of a KeyValue list.
Definition view.hpp:1038
std::string label
The caption.
Definition view.hpp:1040
std::string value
The value, as text.
Definition view.hpp:1042
A widget's size request in both dimensions, and the bounds its extent is clamped into.
Definition view.hpp:308
Sizing height
Along the vertical axis.
Definition view.hpp:312
std::optional< int > minHeight
The least height, in backend units; nullopt for none.
Definition view.hpp:318
std::optional< int > minWidth
The least width, in backend units; nullopt for none.
Definition view.hpp:314
std::optional< int > maxWidth
The most width, in backend units; nullopt for none.
Definition view.hpp:316
std::optional< int > maxHeight
The most height, in backend units; nullopt for none.
Definition view.hpp:320
std::optional< Align > alignSelf
Where the node sits across its row's, column's or grid row's axis; nullopt takes the container's alig...
Definition view.hpp:323
Sizing width
Along the horizontal axis.
Definition view.hpp:310
One entry of a menu as a MenuWidget shows it.
Definition backend.hpp:276
std::vector< MenuEntry > items
The submenu; empty for an entry that is chosen.
Definition backend.hpp:288
One option of a Select.
Definition view.hpp:487
Key key
The option's identity.
Definition view.hpp:489
std::string label
The caption.
Definition view.hpp:491
How much space a widget asks for along one dimension, in backend units (a character cell on the TUI).
Definition view.hpp:270
Kind kind
The rule.
Definition view.hpp:278
int amount
Units for Fixed, weight for Stretch, unused for Content.
Definition view.hpp:280
@ Fixed
Exactly amount units.
Definition view.hpp:274
@ Content
As much as the content needs.
Definition view.hpp:273
@ Stretch
A share of the leftover space, weighted by amount.
Definition view.hpp:275
One step of a Steps indicator.
Definition view.hpp:1016
StepState state
Where it stands.
Definition view.hpp:1020
std::string label
The caption.
Definition view.hpp:1018
One column of a Table.
Definition view.hpp:876
The handlers a widget was given, which the interaction helpers call.
std::function< void(std::vector< int >)> resize
From SplitterWidget::setOnResize.
std::function< void(std::string)> submit
From TextInputWidget::setOnSubmit.
std::function< void(bool)> toggle
From CheckboxWidget::setOnToggle, PanelWidget::setOnToggle or CollapsibleWidget::setOnToggle.
Action dismiss
From DialogWidget::setOnDismiss, DrawerWidget::setOnDismiss or BannerWidget::setOnDismiss.
std::function< void(Key)> select
From SelectWidget::setOnSelect.
Action click
From ButtonWidget::setOnClick.
Action action
An action button's handler, from BannerWidget::setOnAction or EmptyStateWidget::setOnAction.
std::function< void(std::string)> chord
The chord handler, from Widget::setKeys.
std::function< bool(Key const &)> accepts
The drop handler's predicate, from Widget::setDropHandler.
std::function< void(std::string)> change
From TextInputWidget::setOnChange.
std::function< void(std::string)> picked
From FilePickerWidget::setOnPicked.
std::function< void(std::int64_t)> slide
From SliderWidget::setOnChange.
std::function< void()> hover
From Widget::setOnHover.
std::function< void(std::vector< Key >)> selection
From TableWidget::setOnSelectionChange.
std::function< void(std::string)> commit
From TextInputWidget::setOnCommit.
std::function< void(Key)> activate
From TableWidget::setOnActivate.
std::function< void(Key)> drop
The drop handler, from Widget::setDropHandler.
std::function< void(std::int64_t)> slideCommit
From SliderWidget::setOnCommit.
Action open
From SelectWidget::setOnOpen.
std::function< void(std::optional< morph::time::Timestamp >)> dateTime
From DateTimeInputWidget::setOnChange.
std::function< void(std::string, std::string)> component
From CustomWidget::setOnEvent: the event's name and its payload.
std::function< void(std::size_t)> index
From TabsWidget::setOnSelect.
std::function< void(std::vector< std::size_t >)> path
A menu's activation handler, from MenuWidget::setOnActivate.
std::function< void(std::vector< std::string >)> files
From DropZoneWidget::setOnDrop.
std::function< void(int)> onChildDestroyed
Called with a child's id once that child is destroyed; a fake table re-marks its selection with it.
Widget * widget
The fake widget object.
std::optional< Key > dragKey
The drag key, as setDragKey last set it.
std::vector< std::string > chords
The chords, as setKeys last set them.
int parent
The parent's id; 0 for a root.
std::vector< MenuEntry > menu
A menu's entries, as setItems last set them.
Callbacks callbacks
The handlers set so far.
std::string kind
The kind shown in the log and the dump.
std::vector< int > children
The children's ids, in order.
std::map< std::string, std::string, std::less<> > props
Every property set so far, formatted, in name order.
The backend contract: the retained widgets a mount drives, and the factory that makes them.
@ Record
A record of named fields.
Definition value.hpp:141
The toolkit-agnostic view tree: props, node kinds and their builders.
Tone
What a message or a mark conveys, mapped to the theme's colours.
Definition view.hpp:240
@ Info
Information.
Definition view.hpp:242
@ Warn
Something to check.
Definition view.hpp:244
@ Ok
Success.
Definition view.hpp:243
@ Err
A failure.
Definition view.hpp:245
@ Neutral
Nothing in particular.
Definition view.hpp:241
Node collapsible(Collapsible spec)
A titled section the user opens and closes.
Definition view.hpp:1334
TextInputMode
How a TextInput edits.
Definition view.hpp:209
@ SingleLine
One line; Enter submits.
Definition view.hpp:210
@ Multiline
Several lines.
Definition view.hpp:211
@ Password
One line, masked.
Definition view.hpp:212
TextRole
How a Text is styled; each backend maps a role to its theme.
Definition view.hpp:199
@ Heading
A section heading.
Definition view.hpp:202
@ Success
A confirmation.
Definition view.hpp:204
@ Muted
Secondary text.
Definition view.hpp:201
@ Error
An error message.
Definition view.hpp:203
@ Normal
Body text.
Definition view.hpp:200
@ Section
The heading of a group within a list: body size, bold and muted, below a Heading.
Definition view.hpp:205
DateMode
What a DateTimeInput edits.
Definition view.hpp:228
@ Date
A calendar date; the time of day is midnight in the display zone.
Definition view.hpp:229
@ DateTime
A date and a time of day.
Definition view.hpp:230
std::function< void()> Action
A widget event handler taking no arguments.
Definition view.hpp:33
Node textInput(TextInput spec)
An editable text field.
Definition view.hpp:1204
Node column(Column spec)
Children stacked top to bottom.
Definition view.hpp:1224
SelectionMode
How many rows of a Table the user can select.
Definition view.hpp:263
@ Single
At most one row.
Definition view.hpp:265
@ None
Rows can be activated, not selected.
Definition view.hpp:264
@ Multiple
Any number of rows.
Definition view.hpp:266
Side
The edge a Drawer slides in from, in reading direction.
Definition view.hpp:249
@ End
The right edge in a left-to-right layout.
Definition view.hpp:251
@ Start
The left edge in a left-to-right layout.
Definition view.hpp:250
FilePickerMode
Whether a FilePicker chooses an existing file or a file to write.
Definition view.hpp:234
@ Open
An existing file.
Definition view.hpp:235
@ Save
A file to create or overwrite.
Definition view.hpp:236
Node text(Text spec)
Static or bound text.
Definition view.hpp:1194
Node select(Select spec)
A choice of one option.
Definition view.hpp:1214
SelectStyle
How a Select presents its options.
Definition view.hpp:216
@ Radio
Every option visible, one marked.
Definition view.hpp:218
@ Dropdown
A closed field that opens a list.
Definition view.hpp:217
Node row(Row spec)
Children laid out left to right.
Definition view.hpp:1229
std::variant< std::int64_t, std::string > Key
The identity of a Switch case, a Select option, a ForEach or Table row and a drag payload.
Definition view.hpp:38
Node table(std::vector< TableColumn > columns, std::function< std::vector< RowT >()> rows, std::function< Key(RowT const &)> keyOf, std::function< std::vector< Node >(reactive::Signal< RowT > const &)> cells, TableOptions options={})
A table of keyed rows: one Row of cells per row, under the column headers.
Definition view.hpp:1464
Node steps(Steps spec)
The steps of a process.
Definition view.hpp:1304
Axis
The direction a stack arranges, or a scroll area scrolls, its content.
Definition view.hpp:222
@ Vertical
Top to bottom.
Definition view.hpp:223
@ Horizontal
Left to right.
Definition view.hpp:224
StepState
Where one step of a Steps indicator stands.
Definition view.hpp:255
@ Pending
Not started.
Definition view.hpp:256
@ Active
In progress.
Definition view.hpp:257
@ Failed
Stopped by a failure.
Definition view.hpp:259
@ Done
Completed.
Definition view.hpp:258