10#include <initializer_list>
17#include <unordered_map>
18#include <unordered_set>
33namespace morph::ui::testing {
38using PropList = std::vector<std::pair<std::string, std::string>>;
43[[nodiscard]]
inline std::string
formatBool(
bool value) {
return value ?
"true" :
"false"; }
51template <
typename Special>
52[[nodiscard]] std::string
escape(std::string_view
text, Special
const& isSpecial) {
54 escaped.reserve(
text.size());
55 for (
char const character :
text) {
56 if (character ==
'\n') {
58 }
else if (character ==
'\r') {
61 if (character ==
'\\' || isSpecial(character)) {
75 return escape(
text, [](
char character) {
return character ==
'='; });
83 return escape(
text, [](
char character) {
return std::string_view{
"=,[]"}.contains(character); });
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); });
96 std::string formatted =
"[";
98 formatted += formatted.size() == 1 ?
"" :
",";
99 formatted +=
text(entry.label);
100 if (!entry.icon.empty()) {
101 formatted +=
";icon=" +
text(entry.icon);
103 if (!entry.keys.empty()) {
104 formatted +=
";keys=" +
text(entry.keys);
106 if (entry.checked.has_value()) {
107 formatted += std::string{
";checked="} + (*entry.checked ?
"true" :
"false");
109 if (!entry.enabled) {
110 formatted +=
";enabled=false";
112 if (!entry.items.empty()) {
116 return formatted +
"]";
124 if (
auto const*
const number = std::get_if<std::int64_t>(&key); number !=
nullptr) {
125 return std::to_string(*number);
128 escape(std::get<std::string>(key),
129 [](
char character) {
return std::string_view{
"=,[]\""}.contains(character); }) +
137 return key.has_value() ?
formatKey(*key) : std::string{
"none"};
144 switch (sizing.
kind) {
148 return "fixed(" + std::to_string(sizing.
amount) +
")";
150 return "stretch(" + std::to_string(sizing.
amount) +
")";
195 auto const bounds = [](std::optional<int> minimum, std::optional<int> maximum) {
198 text +=
" min=" + std::to_string(*minimum);
201 text +=
" max=" + std::to_string(*maximum);
213[[nodiscard]]
inline std::string
formatTimestamp(std::optional<morph::time::Timestamp>
const& value) {
214 if (!value.has_value()) {
217 if (!value->hasValue()) {
220 return (**value).toIso8601();
229template <
typename T,
typename F>
230[[nodiscard]] std::string
formatList(std::vector<T>
const& items, F
const& each) {
231 std::string
text =
"[";
233 for (T
const& item : items) {
373 if (!value.has_value()) {
376 return std::format(
"{}", *value);
454 std::function<void(std::size_t)>
index;
456 std::function<void(std::optional<morph::time::Timestamp>)>
dateTime;
458 std::function<void(std::int64_t)>
slide;
474 std::function<void(std::string, std::string)>
component;
476 std::function<void(std::string)>
chord;
480 std::function<void(std::vector<std::size_t>)>
path;
484 std::function<void(std::vector<int>)>
resize;
486 std::function<void(std::vector<std::string>)>
files;
500 std::map<std::string, std::string, std::less<>>
props;
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));
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));
546 void set(
int widgetId, std::string_view name, std::string value) {
548 _log.push_back(
"set " + label(record) +
" " + std::string{name} +
"=" + value);
549 record.props.insert_or_assign(std::string{name}, std::move(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));
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));
577 auto const found = _records.find(widgetId);
578 if (found == _records.end()) {
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);
591 if (_focused == widgetId) {
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) {
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)};
612 return found->second;
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)};
624 return found->second;
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"};
636 return found->second;
641 [[nodiscard]] std::map<int, Record>
const&
records() const noexcept {
return _records; }
645 [[nodiscard]] std::vector<std::string>
const&
log() const noexcept {
return _log; }
654 _log.push_back(
"focus " + label(
at(widgetId)));
660 [[nodiscard]]
int focused() const noexcept {
return _focused; }
664 [[nodiscard]] std::string
dump()
const {
666 for (
int const root : _roots) {
667 dumpInto(
text, root, 0);
673 [[nodiscard]]
static std::string label(
Record const& record) {
674 return record.kind +
"#" + std::to_string(record.id);
677 std::vector<int>& siblingsOf(
int parentId) {
return parentId == 0 ? _roots :
at(parentId).
children; }
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) {
693 for (
int const child : record.children) {
694 dumpInto(text, child, depth + 1);
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;
709template <
typename Interface>
719 : _store{std::move(
store)}, _id{_store->create(std::move(kind), parent, *this, std::move(initial))} {}
724 _store->destroy(_id);
750 _store->at(_id).dragKey = key;
759 handlers.
accepts = std::move(accepts);
760 handlers.
drop = std::move(onDrop);
761 set(
"drop",
"handler");
788 void setKeys(std::vector<std::string>
const& chords, std::function<
void(std::string)> onChord)
override {
789 Record& record = _store->at(_id);
791 record.chords = chords;
793 for (std::string
const& chord : chords) {
794 joined += joined.empty() ?
"" :
",";
801 void focus()
override { _store->focus(_id); }
805 void setOnHover(std::function<
void()> onHover)
override { _store->at(_id).callbacks.hover = std::move(onHover); }
811 void set(std::string_view name, std::string value) { _store->set(_id, name, std::move(value)); }
823 [[nodiscard]] std::shared_ptr<RecordStore>
const&
sharedStore() const noexcept {
return _store; }
827 [[nodiscard]]
int id() const noexcept {
return _id; }
830 std::shared_ptr<RecordStore> _store;
836template <
typename Interface>
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"};
851 records.
move(childId, index);
857template <
typename Interface>
872 void setErrors(std::vector<std::string>
const& errors)
override {
898 void setSelected(
bool selected)
override {
set(
"selected", selected ?
"true" :
"false"); }
913 if (
text != _shown) {
914 _shown = std::string{
text};
915 _cursor = _shown.size();
924 _shown = std::move(
text);
925 _cursor = std::min(
cursor, _shown.size());
935 [[nodiscard]] std::size_t
cursor() const noexcept {
return _cursor; }
939 [[nodiscard]] std::string
const&
text() const noexcept {
return _shown; }
943 void setDecimals(
int decimals)
override {
set(
"decimals", std::to_string(decimals)); }
945 void setCaption(std::string_view label,
bool shown)
override {
955 std::size_t _cursor = 0;
978 void setOptions(std::vector<SelectOption>
const& options)
override {
983 if (_requested.has_value()) {
991 _requested.emplace(key);
992 set(
"selected", marked());
1011 return std::ranges::any_of(_options, [&key](
SelectOption const& option) {
return option.
key == key; });
1017 _requested.emplace(key);
1022 [[nodiscard]] std::string marked()
const {
1023 if (!_requested.has_value() || !_requested->has_value() || !
offers(**_requested)) {
1026 return formatKey(**_requested);
1029 std::vector<SelectOption> _options;
1031 std::optional<std::optional<Key>> _requested;
1038 void setItems(std::vector<MenuEntry>
const& entries)
override {
1042 void setOnActivate(std::function<
void(std::vector<std::size_t>)> onActivate)
override {
1051 void setGap(
int gap)
override {
set(
"gap", std::to_string(gap)); }
1060 void setColumns(
int columns)
override {
set(
"columns", std::to_string(columns)); }
1061 void setGap(
int gap)
override {
set(
"gap", std::to_string(gap)); }
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"};
1073 store().
set(childId,
"span", std::to_string(span));
1082 void setPadding(
int padding)
override {
set(
"padding", std::to_string(padding)); }
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"};
1093 set(
"header", std::to_string(headerId));
1111 void setValue(std::optional<morph::time::Timestamp>
const& value)
override {
1114 void setOnChange(std::function<
void(std::optional<morph::time::Timestamp>)> onChange)
override {
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));
1130 void setValue(std::int64_t value)
override {
set(
"value", std::to_string(value)); }
1132 void setOnCommit(std::function<
void(std::int64_t)> onCommit)
override {
1149 void setTabs(std::vector<std::string>
const& labels)
override {
1152 void setSelected(std::size_t index)
override {
set(
"selected", std::to_string(index)); }
1160template <
typename Interface>
1168 if (_footer ==
nullptr) {
1169 _footer = std::make_unique<FakeContainer<SlotWidget>>(this->
sharedStore(),
"Footer",
this);
1175 std::unique_ptr<FakeContainer<SlotWidget>> _footer;
1207 set(
"shown", std::to_string(++_shown));
1240 set(
"current", index.has_value() ? std::to_string(*index) : std::string{
"none"});
1248 void setItems(std::vector<KeyValueItem>
const& items)
override {
1279 void setSizes(std::vector<int>
const& sizes)
override {
1280 set(
"sizes",
formatList(sizes, [](
int size) {
return std::to_string(size); }));
1282 void setOnResize(std::function<
void(std::vector<int>)> onResize)
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"};
1301 set(
"header", std::to_string(headerId));
1311 void setAccept(std::vector<std::string>
const& extensions)
override {
1312 _accept = extensions;
1316 _multiple = multiple;
1319 void setOnDrop(std::function<
void(std::vector<std::string>)> onDrop)
override {
1327 [[nodiscard]]
bool takes(std::vector<std::string>
const& paths)
const {
1328 if (paths.size() > 1 && !_multiple) {
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);
1339 [[nodiscard]]
static bool endsWithIgnoringCase(std::string_view
text, std::string_view suffix) {
1340 if (suffix.size() >
text.size()) {
1343 auto const lower = [](
char character) {
1344 return static_cast<char>(std::tolower(
static_cast<unsigned char>(character)));
1346 return std::ranges::equal(
text.substr(
text.size() - suffix.size()), suffix, {}, lower, lower);
1349 std::vector<std::string> _accept;
1350 bool _multiple =
false;
1357 void setTextProp(std::string_view name, std::string_view value)
override {
1364 set(
"prop." + std::string{name}, std::format(
"{}", value));
1366 void setOnEvent(std::function<
void(std::string event, std::string payload)> onEvent)
override {
1385 _rowKeys.erase(childId);
1407 void setColumns(std::vector<TableColumn>
const& columns)
override {
1430 if (
store().at(rowId).parent !=
id()) {
1431 throw std::logic_error{
"RecordingBackend: setRowKey of a widget that is not this table's row"};
1434 _rowKeys.insert_or_assign(rowId, key);
1444 if (
store().at(rowId).parent !=
id()) {
1445 throw std::logic_error{
"RecordingBackend: setRowTone of a widget that is not this table's row"};
1454 set(
"selection", marked());
1472 set(
"realizedWindow", std::to_string(first) +
"+" + std::to_string(count) +
"/" + std::to_string(total));
1478 _onVisibleRangeChanged = std::move(onVisibleRangeChanged);
1488 if (_onVisibleRangeChanged) {
1489 _onVisibleRangeChanged(first, count);
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) {
1506 return std::nullopt;
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);
1524 return formatList(keys, [](
Key const& key) {
return formatKey(key); });
1528 if (_requested.has_value()) {
1534 std::map<int, Key> _rowKeys;
1536 std::optional<std::vector<Key>> _requested;
1537 std::function<void(std::size_t, std::size_t)> _onVisibleRangeChanged;
1556 return std::make_unique<detail::FakeText>(_store,
"Text", parent);
1559 return std::make_unique<detail::FakeButton>(_store,
"Button", parent);
1563 return std::make_unique<detail::FakeTextInput>(_store,
"TextInput", parent,
1567 return std::make_unique<detail::FakeCheckbox>(_store,
"Checkbox", parent);
1570 return std::make_unique<detail::FakeSelect>(_store,
"Select", parent,
1574 return std::make_unique<detail::FakeMenu>(_store,
"Menu", parent);
1577 return std::make_unique<detail::FakeStack>(_store, axis ==
Axis::Vertical ?
"Column" :
"Row", parent);
1580 return std::make_unique<detail::FakeGrid>(_store,
"Grid", parent);
1583 return std::make_unique<detail::FakeLeaf<SpacerWidget>>(_store,
"Spacer", parent);
1586 return std::make_unique<detail::FakePanel>(_store,
"Panel", parent);
1589 return std::make_unique<detail::FakeContainer<ScrollWidget>>(
1593 return std::make_unique<detail::FakeContainer<SlotWidget>>(_store,
"Slot", parent);
1596 return std::make_unique<detail::FakeTabs>(_store,
"Tabs", parent);
1599 return std::make_unique<detail::FakeDialog>(_store,
"Dialog", parent);
1602 return std::make_unique<detail::FakeToast>(_store,
"Toast", parent);
1605 return std::make_unique<detail::FakeBusy>(_store,
"Busy", parent);
1608 return std::make_unique<detail::FakeTable>(_store,
"Table", parent);
1611 int offsetMinutes)
override {
1612 return std::make_unique<detail::FakeDateTimeInput>(
1613 _store,
"DateTimeInput", parent,
1617 return std::make_unique<detail::FakeSlider>(_store,
"Slider", parent);
1621 return std::make_unique<detail::FakeFilePicker>(_store,
"FilePicker", parent,
1625 return std::make_unique<detail::FakeBanner>(_store,
"Banner", parent);
1628 return std::make_unique<detail::FakeBadge>(_store,
"Badge", parent);
1631 return std::make_unique<detail::FakeProgress>(_store,
"Progress", parent);
1634 return std::make_unique<detail::FakeSteps>(_store,
"Steps", parent);
1637 return std::make_unique<detail::FakeKeyValue>(_store,
"KeyValue", parent);
1640 return std::make_unique<detail::FakeEmptyState>(_store,
"EmptyState", parent);
1643 return std::make_unique<detail::FakeDrawer>(_store,
"Drawer", parent,
1647 return std::make_unique<detail::FakeSplitter>(_store,
"Splitter", parent,
1651 return std::make_unique<detail::FakeCollapsible>(_store,
"Collapsible", parent);
1654 return std::make_unique<detail::FakeDropZone>(_store,
"DropZone", parent);
1664 if (!_components.contains(request.name)) {
1667 return std::make_unique<detail::FakeCustom>(
1668 _store,
"Custom", parent,
1670 {
"minVersion", std::to_string(request.minVersion)}});
1676 _components.clear();
1677 for (std::string& name : names) {
1678 _components.insert(std::move(name));
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));
1697 [[nodiscard]] std::string
dump()
const {
return _store->dump(); }
1702 [[nodiscard]] std::vector<std::string>
const&
log()
const {
return _store->log(); }
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) {
1719 if (
auto const found = record.props.find(name); found != record.props.end() && found->second == value) {
1723 return std::nullopt;
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);
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;
1755 [[nodiscard]]
bool hasProp(
int widgetId, std::string_view name)
const {
1756 return _store->at(widgetId).props.contains(name);
1762 [[nodiscard]]
bool exists(
int widgetId)
const {
return _store->records().contains(widgetId); }
1774 [[nodiscard]] std::string
kindOf(
int widgetId)
const {
return _store->at(widgetId).kind; }
1780 [[nodiscard]] std::vector<int>
children(
int widgetId)
const {
return _store->at(widgetId).children; }
1786 [[nodiscard]]
Widget*
widget(
int widgetId)
const {
return _store->at(widgetId).widget; }
1790 [[nodiscard]]
int focused() const noexcept {
return _store->focused(); }
1799 bool press(
int widgetId, std::string
const& chord) {
1800 if (!reachable(_store->at(widgetId))) {
1803 for (
int candidate = widgetId; candidate != 0;) {
1805 if (std::ranges::find(record.chords, chord) != record.chords.end()) {
1806 invoke(record.callbacks.chord, chord);
1809 candidate = record.
parent;
1818 if (reachable(record)) {
1819 invoke(record.callbacks.hover);
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"};
1833 if (reachable(record)) {
1834 invoke(record.kind ==
"Button" ? record.callbacks.click : record.callbacks.action);
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));
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));
1870 if (
detail::Record const*
const record = actionable(widgetId,
"moveCursor", {
"TextInput"});
1871 record !=
nullptr) {
1872 textInput(*record).moveCursorByUser(position);
1880 [[nodiscard]] std::size_t
cursor(
int widgetId) {
1881 return textInput(expectKind(widgetId,
"cursor", {
"TextInput"})).
cursor();
1888 if (
detail::Record const*
const record = editable(widgetId,
"commit", {
"TextInput"}); record !=
nullptr) {
1889 invoke(record->callbacks.commit, textInput(*record).text());
1897 if (
detail::Record const*
const record = editable(widgetId,
"toggle", {
"Checkbox"}); record !=
nullptr) {
1898 bool const checked =
prop(widgetId,
"checked") !=
"true";
1900 invoke(record->callbacks.toggle, checked);
1909 detail::Record const& record = expectKind(widgetId,
"open", {
"Select"});
1910 if (reachable(record) && !flagIs(record,
"readonly",
true)) {
1911 invoke(record.callbacks.open);
1921 detail::Record const& record = expectKind(widgetId,
"choose", {
"Select"});
1924 throw std::logic_error{
"RecordingBackend: choose of " +
detail::formatKey(key) +
", which Select#" +
1925 std::to_string(widgetId) +
" does not offer"};
1927 if (reachable(record) && !flagIs(record,
"readonly",
true)) {
1928 select->chooseByUser(key);
1929 invoke(record.callbacks.select, std::move(key));
1941 detail::Record const*
const record = actionable(widgetId,
"chooseMenuEntry", {
"Menu"});
1942 if (record ==
nullptr) {
1945 std::vector<MenuEntry>
const* level = &record->
menu;
1947 for (std::size_t
const index : path) {
1948 if (level ==
nullptr || index >= level->size() || !level->at(index).enabled) {
1951 entry = &level->at(index);
1952 level = &entry->
items;
1954 if (entry !=
nullptr && entry->
items.empty()) {
1955 invoke(record->callbacks.path, path);
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);
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"};
1987 if (reachable(record)) {
1989 invoke(record.callbacks.toggle, collapsed);
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"};
2003 if (reachable(record)) {
2004 if (record.kind !=
"Banner") {
2007 invoke(record.callbacks.dismiss);
2017 if (
detail::Record const*
const record = actionable(widgetId,
"expand", {
"Collapsible"}); record !=
nullptr) {
2019 invoke(record->callbacks.toggle,
open);
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"};
2034 if (reachable(record)) {
2035 _store->setByUser(widgetId,
"sizes",
2037 invoke(record.callbacks.resize, std::move(sizes));
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"};
2052 if (!reachable(record) || !zone.takes(paths)) {
2055 invoke(record.callbacks.files, std::move(paths));
2065 detail::Record const& record = expectKind(widgetId,
"selectRows", {
"Table"});
2067 std::string
const where =
" of Table#" + std::to_string(widgetId);
2069 throw std::logic_error{
"RecordingBackend: selectRows" + where +
", which selects nothing"};
2072 throw std::logic_error{
"RecordingBackend: selectRows of several rows" + where +
", which selects one"};
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"};
2081 if (reachable(record)) {
2082 table->selectByUser(keys);
2083 invoke(record.callbacks.selection, std::move(keys));
2092 detail::Record const& record = expectKind(widgetId,
"activateRow", {
"Table"});
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"};
2099 if (reachable(_store->at(*rowId))) {
2100 invoke(record.callbacks.activate, std::move(key));
2113 void scrollTable(
int widgetId, std::size_t first, std::size_t count) {
2114 detail::Record const& record = expectKind(widgetId,
"scrollTable", {
"Table"});
2116 if (
table ==
nullptr) {
2117 throw std::logic_error{
"RecordingBackend: scrollTable of Table#" + std::to_string(widgetId) +
2118 ", which is not a table"};
2120 table->scrollTo(first, count);
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) {
2131 invoke(record->callbacks.dateTime, value);
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));
2143 auto const commit = record->callbacks.slideCommit;
2144 invoke(record->callbacks.slide, 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);
2165 void pick(
int widgetId, std::string path) {
2166 if (
detail::Record const*
const record = editable(widgetId,
"pick", {
"FilePicker"}); record !=
nullptr) {
2168 invoke(record->callbacks.picked, std::move(path));
2180 bool drag(
int sourceId,
int targetId) {
2181 if (sourceId == targetId) {
2182 throw std::logic_error{
"RecordingBackend: drag of a widget onto itself"};
2186 if (!reachable(source) || !reachable(target)) {
2189 std::optional<Key>
const key = source.
dragKey;
2192 if (!key.has_value() || !drop || !accepts || !accepts(*key)) {
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)};
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;
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;
2232 [[nodiscard]]
bool reachable(detail::Record
const& record)
const {
2233 if (flagIs(record,
"visible",
false) || flagIs(record,
"enabled",
false) || isClosedDialog(record)) {
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)) {
2243 outerId = outer.parent;
2249 [[nodiscard]]
static bool isClosedDialog(detail::Record
const& record) {
2250 return (record.kind ==
"Dialog" || record.kind ==
"Drawer") && !flagIs(record,
"open",
true);
2254 [[nodiscard]]
static bool closedAround(detail::Record
const& outer,
int childId) {
2255 if (outer.kind !=
"Collapsible" || !flagIs(outer,
"open",
false)) {
2258 auto const header = outer.props.find(
"header");
2259 return header == outer.props.end() || header->second != std::to_string(childId);
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);
2268 template <
typename... Args,
typename... Values>
2269 static void invoke(std::function<
void(Args...)>
const& handler, Values&&... values) {
2270 auto const copy = handler;
2272 copy(std::forward<Values>(values)...);
2276 std::shared_ptr<detail::RecordStore> _store = std::make_shared<detail::RecordStore>();
2277 std::unordered_set<std::string> _components;
Justify
Where a stack places its spare length along its axis, while no shown child stretches.
@ Center
Packed in the middle.
@ Between
In equal gaps between the children, never less than the stack's gap.
@ Start
Packed at the start.
Align
Where a child is placed across its container's axis: a column's horizontally, a row's vertically,...
@ End
At the end: right in a column, bottom in a row or grid.
@ Start
At the start: left in a column, top in a row or grid.
@ Stretch
A content child fills the cross extent; one its maximum stops sits at the start.
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 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 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 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.
What IViewBackend::createCustom asks for.
One row of a KeyValue list.
std::string label
The caption.
std::string value
The value, as text.
A widget's size request in both dimensions, and the bounds its extent is clamped into.
Sizing height
Along the vertical axis.
std::optional< int > minHeight
The least height, in backend units; nullopt for none.
std::optional< int > minWidth
The least width, in backend units; nullopt for none.
std::optional< int > maxWidth
The most width, in backend units; nullopt for none.
std::optional< int > maxHeight
The most height, in backend units; nullopt for none.
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...
Sizing width
Along the horizontal axis.
Key key
The option's identity.
std::string label
The caption.
How much space a widget asks for along one dimension, in backend units (a character cell on the TUI).
int amount
Units for Fixed, weight for Stretch, unused for Content.
@ Fixed
Exactly amount units.
@ Content
As much as the content needs.
@ Stretch
A share of the leftover space, weighted by amount.
One step of a Steps indicator.
StepState state
Where it stands.
std::string label
The caption.
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.
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.
@ Warn
Something to check.
@ Neutral
Nothing in particular.
Node collapsible(Collapsible spec)
A titled section the user opens and closes.
TextInputMode
How a TextInput edits.
@ SingleLine
One line; Enter submits.
@ Multiline
Several lines.
@ Password
One line, masked.
TextRole
How a Text is styled; each backend maps a role to its theme.
@ Heading
A section heading.
@ Section
The heading of a group within a list: body size, bold and muted, below a Heading.
DateMode
What a DateTimeInput edits.
@ Date
A calendar date; the time of day is midnight in the display zone.
@ DateTime
A date and a time of day.
std::function< void()> Action
A widget event handler taking no arguments.
Node textInput(TextInput spec)
An editable text field.
Node column(Column spec)
Children stacked top to bottom.
SelectionMode
How many rows of a Table the user can select.
@ None
Rows can be activated, not selected.
@ Multiple
Any number of rows.
Side
The edge a Drawer slides in from, in reading direction.
@ End
The right edge in a left-to-right layout.
@ Start
The left edge in a left-to-right layout.
FilePickerMode
Whether a FilePicker chooses an existing file or a file to write.
@ Save
A file to create or overwrite.
Node text(Text spec)
Static or bound text.
Node select(Select spec)
A choice of one option.
SelectStyle
How a Select presents its options.
@ Radio
Every option visible, one marked.
@ Dropdown
A closed field that opens a list.
Node row(Row spec)
Children laid out left to right.
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.
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.
Node steps(Steps spec)
The steps of a process.
Axis
The direction a stack arranges, or a scroll area scrolls, its content.
@ Horizontal
Left to right.
StepState
Where one step of a Steps indicator stands.
@ Failed
Stopped by a failure.