218 : _rt{&runtime}, _backend{&backend} {}
233 [&](
auto const& spec) ->
Widget* {
234 if constexpr (
requires { spec.common.layout.alignSelf; }) {
235 checkPlacement(spec.common, parent);
237 return &this->mountKind(scope, spec, parent);
249 return mountPass([&] {
return mount(scope, node, parent); });
258 bool focus(std::string_view focusId) {
260 auto const found = _focusable.lower_bound(focusId);
261 if (found == _focusable.end() || found->first != focusId) {
265 found->second->focus();
275 template <
typename F>
276 auto underOverlay(
ContainerWidget* overlay, F
const& body) -> std::invoke_result_t<F const&> {
279 Restore(
Mounter& self,
ContainerWidget* next) : _self{&self}, _saved{std::exchange(self._overlay, next)} {}
280 Restore(Restore
const&) =
delete;
281 Restore& operator=(Restore
const&) =
delete;
282 Restore(Restore&&) =
delete;
283 Restore& operator=(Restore&&) =
delete;
284 ~Restore() { _self->_overlay = _saved; }
288 ContainerWidget* _saved;
290 Restore
const restore{*
this, overlay};
295 [[nodiscard]] ContainerWidget* overlayFooter()
const {
296 if (
auto*
const dialog =
dynamic_cast<DialogWidget*
>(_overlay)) {
299 if (
auto*
const drawer =
dynamic_cast<DrawerWidget*
>(_overlay)) {
309 template <
typename F>
310 auto mountPass(F
const& body) -> std::invoke_result_t<F const&> {
311 Widget*
const before = _autofocus;
314 auto result = body();
316 if (_passDepth == 0) {
317 if (Widget*
const target = std::exchange(_autofocus,
nullptr); target !=
nullptr) {
335 template <
typename T,
typename Apply>
336 reactive::Computed<T>* bind(reactive::Scope& scope, Prop<T>
const& prop, Apply apply) {
337 if (!
prop.isBound()) {
341 auto& value = scope.make<reactive::Computed<T>>(*_rt,
prop.binding());
342 scope.effect([&value, apply = std::move(apply)] {
apply(value.get()); });
353 template <
typename F>
354 static bool runCallback(reactive::Runtime& runtime, F
const& body) {
355 return runtime.widgetEvent([&] {
356 return runtime.untracked([&] {
368 template <
typename... Args>
369 [[nodiscard]] std::function<void(Args...)> event(std::function<
void(Args...)>
const& handler)
const {
376 return [runtime = _rt, handler](Args... args) {
377 static_cast<void>(runCallback(*runtime, [&] { handler(std::move(args)...); }));
384 template <
typename T>
388 std::optional<T>* shown;
390 std::function<T()> read;
392 std::function<void(T
const&)> apply;
403 template <
typename T,
typename Read,
typename Apply>
404 [[nodiscard]] std::function<void(T
const&)> reasserter(reactive::Scope& scope, std::optional<T>& shown, Read read,
406 auto const check = std::make_shared<Reassertion<T>>(
407 Reassertion<T>{.shown = &shown, .read = std::move(read), .apply = std::move(apply)});
408 scope.make<std::shared_ptr<Reassertion<T>>>(check);
409 return [runtime = _rt, weak = std::weak_ptr{check}](T
const& requested) {
410 if (
auto const live = weak.lock()) {
411 *live->shown = requested;
418 runtime->owner().post([runtime, weak] {
419 if (
auto const live = weak.lock()) {
420 runtime->untracked([&live] { showSlot(*live); });
430 template <
typename T>
431 static void showSlot(Reassertion<T>
const& check)
noexcept {
433 T
const current = check.read();
434 if (!(check.shown->has_value() && **check.shown == current)) {
435 check.apply(current);
444 template <
typename T,
typename Apply>
445 [[nodiscard]] std::function<void(T
const&)> controlled(reactive::Scope& scope, Prop<T>
const& prop, Apply apply) {
446 auto& shown = scope.make<std::optional<T>>();
448 auto show = [&shown,
apply = std::move(apply)](T
const& value) {
449 if (shown.has_value() && *shown == value) {
455 reactive::Computed<T>
const*
const value = bind(scope, prop, show);
456 if (value ==
nullptr) {
459 return reasserter<T>(scope, shown, [value] {
return value->get(); }, std::move(show));
464 template <
typename T,
typename Arg>
465 [[nodiscard]] std::function<void(Arg)> inputEvent(std::function<
void(Arg)>
const& handler,
466 std::function<
void(T
const&)> reassert)
const {
468 return event(handler);
470 return [inner = event(handler), reassert = std::move(reassert)](Arg arg) {
471 T
const requested(arg);
473 inner(std::move(arg));
481 static void checkLayout(Common
const& common) {
482 LayoutHints
const& hints = common.layout;
483 auto const refuse = [&common](std::string
const& what) {
484 throw std::logic_error{
"morph::ui::mount: " + what +
" on the node" +
485 (common.testId.empty() ? std::string{} :
" `" + common.testId +
"`")};
487 auto const check = [&
refuse](Sizing sizing, std::optional<int> minimum, std::optional<int> maximum,
488 std::string
const& axis) {
489 if (minimum.value_or(0) < 0 || maximum.value_or(0) < 0) {
490 refuse(
"a negative " + axis +
" bound");
492 if (minimum && maximum && *minimum > *maximum) {
493 refuse(
"a minimum " + axis +
" above its maximum");
496 (sizing.amount < minimum.value_or(sizing.amount) || sizing.amount > maximum.value_or(sizing.amount))) {
497 refuse(
"a fixed " + axis +
" outside its bounds");
500 check(hints.width, hints.minWidth, hints.maxWidth,
"width");
501 check(hints.height, hints.minHeight, hints.maxHeight,
"height");
509 static void checkOverlayLayout(Common
const& common, std::string_view kind) {
510 LayoutHints
const& hints = common.layout;
511 auto const refuse = [&common, kind](std::string
const& axis) {
512 throw std::logic_error{
"morph::ui::mount: a " + std::string{kind} +
513 " is sized by the frontend, not by its container; its " + axis +
" cannot stretch" +
514 (common.testId.empty() ? std::string{} :
" (node `" + common.testId +
"`)")};
526 static void checkPlacement(Common
const& common, ContainerWidget
const* parent) {
527 if (common.layout.alignSelf &&
dynamic_cast<StackWidget const*
>(parent) ==
nullptr &&
528 dynamic_cast<GridWidget const*
>(parent) ==
nullptr) {
529 std::string
const named = common.testId.empty() ? std::string{} :
" `" + common.testId +
"`";
530 throw std::logic_error{
531 "morph::ui::mount: `alignSelf` places a node in its row, column or grid, and the node" + named +
536 void applyCommon(reactive::Scope& scope, Widget& widget, Common
const& common) {
537 if (common.visible.isBound() || !common.visible.constant()) {
538 bind(scope, common.visible, [&widget](
bool shown) { widget.setVisible(shown); });
540 if (common.enabled.isBound() || !common.enabled.constant()) {
541 bind(scope, common.enabled, [&widget](
bool enabled) { widget.setEnabled(enabled); });
543 if (common.layout != LayoutHints{}) {
545 widget.setLayout(common.layout);
547 if (common.dragKey.isBound() || common.dragKey.constant().has_value()) {
548 bind(scope, common.dragKey, [&widget](std::optional<Key>
const& key) { widget.setDragKey(key); });
551 applyDrop(widget, common);
553 if (common.a11y.name.isBound() || !common.a11y.name.constant().empty()) {
554 bind(scope, common.a11y.name, [&widget](std::string
const& name) { widget.setAccessibleName(name); });
556 if (!common.a11y.role.empty()) {
557 widget.setAccessibleRole(common.a11y.role);
559 if (!common.testId.empty()) {
560 widget.setTestId(common.testId);
562 if (common.tooltip.isBound() || !common.tooltip.constant().empty()) {
563 bind(scope, common.tooltip, [&widget](std::string
const& text) { widget.setTooltip(text); });
565 if (!common.surface.empty()) {
566 widget.setSurface(common.surface);
568 if (!common.keys.empty()) {
569 applyKeys(widget, common.keys);
571 if (common.autofocus && _autofocus ==
nullptr) {
572 _autofocus = &widget;
574 if (!common.focusId.empty()) {
575 scope.make<Focusable>(_focusable, _focusable.emplace(common.focusId, &widget));
579 using FocusMap = std::multimap<std::string, Widget*, std::less<>>;
584 Focusable(FocusMap& owner, FocusMap::iterator added) noexcept : map{&owner}, entry{added} {}
585 Focusable(Focusable
const&) =
delete;
586 Focusable& operator=(Focusable
const&) =
delete;
587 Focusable(Focusable&&) =
delete;
588 Focusable& operator=(Focusable&&) =
delete;
589 ~Focusable() { map->erase(entry); }
592 FocusMap::iterator entry;
596 void applyDrop(Widget& widget, Common
const& common) {
597 std::function<bool(Key
const&)> accepts = [](
Key const&) {
return true; };
598 if (common.accepts) {
599 accepts = [runtime = _rt, predicate = common.accepts](
Key const& key) {
600 bool accepted =
false;
601 static_cast<void>(runCallback(*runtime, [&] { accepted = predicate(key); }));
605 widget.setDropHandler(std::move(accepts), event(common.onDrop));
609 void applyKeys(Widget& widget, std::vector<KeyBinding>
const& keys) {
610 std::vector<std::string> chords;
611 chords.reserve(keys.size());
612 for (KeyBinding
const& binding : keys) {
613 chords.push_back(binding.chord);
615 widget.setKeys(chords, [runtime = _rt, keys](std::string
const& chord) {
617 if (bound != keys.end() && bound->onPress) {
618 static_cast<void>(runCallback(*runtime, bound->onPress));
623 void mountChildren(reactive::Scope& scope, std::vector<Node>
const& children, ContainerWidget& container) {
624 for (Node
const& child : children) {
625 static_cast<void>(
mount(scope, child, &container));
631 Widget& mountStack(reactive::Scope& scope, Axis axis, std::vector<Node>
const& children,
int gap,
632 Common
const& common, ContainerWidget* parent, Align align = Align::Stretch,
633 Justify justify = Justify::Start) {
634 StackWidget& widget = scope.adopt(_backend->createStack(parent, axis));
635 applyCommon(scope, widget, common);
637 if (align != Align::Stretch) {
638 widget.setAlign(align);
640 if (justify != Justify::Start) {
641 widget.setJustify(justify);
643 mountChildren(scope, children, widget);
649 [[nodiscard]]
static std::vector<MenuEntry> entriesOf(std::vector<MenuItem>
const& items) {
650 std::vector<MenuEntry> entries;
651 entries.reserve(items.size());
652 for (MenuItem
const& item : items) {
653 entries.push_back(MenuEntry{.label = item.label.evaluate(),
656 .checked = item.checked.evaluate(),
657 .enabled = item.enabled.evaluate(),
658 .items = entriesOf(item.items)});
665 [[nodiscard]]
static bool anyBound(std::vector<MenuItem>
const& items) {
666 return std::ranges::any_of(items, entryBound);
671 [[nodiscard]]
static bool entryBound(MenuItem
const& item) {
672 return item.label.isBound() || item.checked.isBound() || item.enabled.isBound() || anyBound(item.items);
676 [[nodiscard]]
static MenuItem
const* itemAt(std::vector<MenuItem>
const& items,
677 std::vector<std::size_t>
const& path) {
678 std::vector<MenuItem>
const* level = &items;
679 MenuItem
const* found =
nullptr;
680 for (std::size_t
const index : path) {
681 if (level ==
nullptr || index >= level->size()) {
684 found = &level->at(index);
685 level = &found->items;
692 template <
class W
idget>
693 void applyCaption(reactive::Scope& scope, Widget& widget, Prop<std::string>
const& label,
bool shown) {
694 if (!shown && !label.isBound() && label.constant().empty()) {
697 bind(scope, label, [&widget, shown](std::string
const& text) { widget.setCaption(text, shown); });
701 void applyField(reactive::Scope& scope, FieldWidget& widget, FieldState
const& field) {
702 if (field.readonly.isBound() || field.readonly.constant()) {
703 bind(scope, field.readonly, [&widget](
bool readonly) { widget.setReadOnly(readonly); });
705 if (field.required.isBound() || field.required.constant()) {
706 bind(scope, field.required, [&widget](
bool required) { widget.setRequired(required); });
708 if (field.errors.isBound() || !field.errors.constant().empty()) {
709 bind(scope, field.errors, [&widget](std::vector<std::string>
const& errors) { widget.setErrors(errors); });
711 if (field.stale.isBound() || field.stale.constant()) {
712 bind(scope, field.stale, [&widget](
bool stale) { widget.setStale(stale); });
716 Widget& mountKind(reactive::Scope& scope, Text
const& spec, ContainerWidget* parent) {
717 TextWidget& widget = scope.adopt(_backend->createText(parent));
718 applyCommon(scope, widget, spec.common);
719 applyField(scope, widget, spec.field);
720 bind(scope, spec.text, [&widget](std::string
const& text) { widget.setText(text); });
721 bind(scope, spec.role, [&widget](TextRole role) { widget.setRole(role); });
725 Widget& mountKind(reactive::Scope& scope, Button
const& spec, ContainerWidget* parent) {
726 if (spec.style == ButtonStyle::Push && (spec.selected.isBound() || spec.selected.constant())) {
727 throw std::logic_error{
"morph::ui: `selected` marks a list item; give the button ButtonStyle::Item"};
729 ButtonWidget& widget = scope.adopt(_backend->createButton(parent));
730 applyCommon(scope, widget, spec.common);
731 bind(scope, spec.label, [&widget](std::string
const& label) { widget.setLabel(label); });
732 if (spec.emphasis != ButtonEmphasis::Normal) {
733 widget.setEmphasis(spec.emphasis);
735 if (!spec.icon.empty()) {
736 widget.setIcon(spec.icon);
738 if (spec.style == ButtonStyle::Item) {
739 widget.setStyle(spec.style);
740 if (spec.selected.isBound() || spec.selected.constant()) {
741 bind(scope, spec.selected, [&widget](
bool selected) { widget.setSelected(selected); });
744 widget.setOnClick(event(spec.onClick));
748 Widget& mountKind(reactive::Scope& scope, TextInput
const& spec, ContainerWidget* parent) {
749 bool const hasUnit = spec.unit.isBound() || !spec.unit.constant().empty();
750 if ((hasUnit || spec.decimals >= 0 || !spec.icon.empty()) && spec.mode == TextInputMode::Multiline) {
751 throw std::logic_error{
752 "morph::ui: a multiline text input holds no number, so it takes no unit, places or icon"};
754 TextInputWidget& widget = scope.adopt(_backend->createTextInput(parent, spec.mode));
755 applyCommon(scope, widget, spec.common);
756 applyField(scope, widget, spec.field);
757 auto reassert = controlled(scope, spec.value, [&widget](std::string
const& value) { widget.setText(value); });
758 bind(scope, spec.placeholder, [&widget](std::string
const& text) { widget.setPlaceholder(text); });
761 bind(scope, spec.unit, [&widget](std::string
const& unit) { widget.setUnit(unit); });
763 if (spec.decimals >= 0) {
764 widget.setDecimals(spec.decimals);
766 if (!spec.icon.empty()) {
767 widget.setIcon(spec.icon);
769 applyCaption(scope, widget, spec.label, spec.showLabel);
770 widget.setOnChange(inputEvent(spec.onChange, reassert));
771 widget.setOnSubmit(inputEvent(spec.onSubmit, std::move(reassert)));
772 widget.setOnCommit(event(spec.onCommit));
776 Widget& mountKind(reactive::Scope& scope, Checkbox
const& spec, ContainerWidget* parent) {
777 CheckboxWidget& widget = scope.adopt(_backend->createCheckbox(parent));
778 applyCommon(scope, widget, spec.common);
779 applyField(scope, widget, spec.field);
780 bind(scope, spec.label, [&widget](std::string
const& label) { widget.setLabel(label); });
781 widget.setOnToggle(inputEvent(
782 spec.onToggle, controlled(scope, spec.checked, [&widget](
bool checked) { widget.setChecked(checked); })));
786 Widget& mountKind(reactive::Scope& scope, Select
const& spec, ContainerWidget* parent) {
787 SelectWidget& widget = scope.adopt(_backend->createSelect(parent, spec.style));
788 applyCommon(scope, widget, spec.common);
789 applyField(scope, widget, spec.field);
790 bind(scope, spec.options, [&widget](std::vector<SelectOption>
const& options) { widget.setOptions(options); });
791 widget.setOnSelect(inputEvent(
793 controlled(scope, spec.selected, [&widget](std::optional<Key>
const& key) { widget.setSelected(key); })));
794 widget.setOnOpen(event(spec.onOpen));
795 applyCaption(scope, widget, spec.label, spec.showLabel);
799 Widget& mountKind(reactive::Scope& scope, Menu
const& spec, ContainerWidget* parent) {
800 MenuWidget& widget = scope.adopt(_backend->createMenu(parent));
801 applyCommon(scope, widget, spec.common);
802 std::vector<MenuItem>
const& items = spec.items;
805 Prop<std::vector<MenuEntry>>
const entries =
806 anyBound(items) ? Prop<std::vector<MenuEntry>>([items = items] {
return entriesOf(items); })
807 : Prop<std::vector<MenuEntry>>(entriesOf(items));
808 bind(scope, entries, [&widget](std::vector<MenuEntry>
const& shown) { widget.setItems(shown); });
809 widget.setOnActivate(
810 event(std::function<
void(std::vector<std::size_t>)>{[items = items](std::vector<std::size_t>
const& path) {
811 MenuItem
const*
const item = itemAt(items, path);
812 if (item !=
nullptr && item->onSelect) {
819 Widget& mountKind(reactive::Scope& scope, Column
const& spec, ContainerWidget* parent) {
820 return mountStack(scope, Axis::Vertical, spec.children, spec.gap, spec.common, parent, spec.align,
824 Widget& mountKind(reactive::Scope& scope, Row
const& spec, ContainerWidget* parent) {
825 if (spec.tone &&
dynamic_cast<TableWidget*
>(parent) ==
nullptr) {
826 throw std::logic_error{
"morph::ui: a row's tone is a table row's; this row is not in a table"};
828 return mountStack(scope, Axis::Horizontal, spec.children, spec.gap, spec.common, parent, spec.align,
832 Widget& mountKind(reactive::Scope& scope, Grid
const& spec, ContainerWidget* parent) {
833 GridWidget& widget = scope.adopt(_backend->createGrid(parent));
834 applyCommon(scope, widget, spec.common);
835 widget.setColumns(spec.columns);
837 if (spec.align != Align::Stretch) {
838 widget.setAlign(spec.align);
840 for (GridCell
const& cell : spec.cells) {
841 Widget*
const child =
mount(scope, cell.node, &widget);
842 if (child !=
nullptr && cell.span != 1) {
843 widget.setSpan(*child, cell.span);
849 Widget& mountKind(reactive::Scope& scope, Spacer
const& spec, ContainerWidget* parent) {
850 SpacerWidget& widget = scope.adopt(_backend->createSpacer(parent));
851 applyCommon(scope, widget, spec.common);
855 Widget& mountKind(reactive::Scope& scope, Panel
const& spec, ContainerWidget* parent) {
856 PanelWidget& widget = scope.adopt(_backend->createPanel(parent));
857 applyCommon(scope, widget, spec.common);
858 bind(scope, spec.title, [&widget](std::string
const& title) { widget.setTitle(title); });
859 if (Widget*
const header =
mount(scope, spec.header, &widget); header !=
nullptr) {
860 widget.setHeader(*header);
863 widget.setCollapsible(spec.collapsible);
864 if (spec.collapsible) {
865 widget.setOnToggle(inputEvent(spec.onToggle, controlled(scope, spec.collapsed, [&widget](
bool collapsed) {
866 widget.setCollapsed(collapsed);
869 static_cast<void>(
mount(scope, spec.child, &widget));
873 Widget& mountKind(reactive::Scope& scope, Scroll
const& spec, ContainerWidget* parent) {
874 ScrollWidget& widget = scope.adopt(_backend->createScroll(parent, spec.axis));
875 applyCommon(scope, widget, spec.common);
876 static_cast<void>(
mount(scope, spec.child, &widget));
884 [[nodiscard]] std::unique_ptr<reactive::Scope> mountContent(std::size_t depth, Node
const& node,
885 ContainerWidget& host) {
886 return mountPass([&] {
887 auto content = std::make_unique<reactive::Scope>(*_rt, depth);
888 _rt->untracked([&] {
static_cast<void>(
mount(*content, node, &host)); });
896 Widget& mountKind(reactive::Scope& scope, Switch
const& spec, ContainerWidget* parent) {
897 SlotWidget& widget = scope.adopt(_backend->createSlot(parent));
898 applyCommon(scope, widget, spec.common);
899 auto&
content = scope.make<std::unique_ptr<reactive::Scope>>();
900 bind(scope, spec.selector,
901 [
this, &widget, &content, depth = scope.depth() + 1, cases = spec.cases, fallback = spec.fallback,
902 overlay = _overlay](Key
const& key) {
904 auto const chosen = std::ranges::find(cases, key, &SwitchCase::key);
905 Node const& node = chosen == cases.end() ? fallback : chosen->node;
907 content = underOverlay(overlay, [&] { return mountContent(depth, node, widget); });
923 Widget& mountKind(reactive::Scope& scope, Tabs
const& spec, ContainerWidget* parent) {
924 TabsWidget& widget = scope.adopt(_backend->createTabs(parent));
925 applyCommon(scope, widget, spec.common);
926 std::vector<std::string> labels;
927 labels.reserve(spec.tabs.size());
928 for (Tab
const& tab : spec.tabs) {
929 labels.push_back(tab.label);
931 widget.setTabs(labels);
932 auto& pageScope = scope.child();
933 auto& pages = scope.make<
Pages>();
934 pages.
pageSlots.resize(spec.tabs.size(),
nullptr);
937 std::function<void(std::size_t
const&)> reassert;
938 if (spec.selected.isBound()) {
939 reassert = reasserter<std::size_t>(
940 scope, pages.highlighted,
941 [&pages] { return pages.shown.value_or(std::numeric_limits<std::size_t>::max()); },
942 [&widget, &pages](std::size_t index) { highlight(widget, pages, index); });
944 widget.setOnSelect(inputEvent(spec.onSelect, reassert));
945 pages.onSelect = inputEvent(spec.onSelect, std::move(reassert));
946 auto show = [
this, &widget, &pageScope, &pages, pageSpecs = spec.tabs, overlay = _overlay](std::size_t index) {
947 _rt->untracked([&] { showPage(widget, pageScope, pages, pageSpecs, index, overlay); });
949 if (!spec.selected.isBound()) {
950 show(spec.selected.constant());
952 scope.effect([binding = spec.selected.binding(), show = std::move(show)] { show(binding()); });
964 void showPage(TabsWidget& widget, reactive::Scope& pageScope,
Pages& pages, std::vector<Tab>
const& pageSpecs,
965 std::size_t index, ContainerWidget* overlay) {
966 bool const exists = index < pageSpecs.size();
967 bool const isNew = exists && pages.pageSlots.at(index) ==
nullptr;
970 underOverlay(overlay, [&] { mountPage(widget, pageScope, pages, pageSpecs.at(index).node, index); });
972 if (pages.shown.has_value()) {
973 restoreSelection(widget, pages, *pages.shown);
978 if (pages.shown != index) {
979 if (pages.shown.has_value() && *pages.shown < pages.pageSlots.size()) {
980 if (SlotWidget*
const previous = pages.pageSlots.at(*pages.shown); previous !=
nullptr) {
981 previous->setVisible(
false);
984 if (exists && !isNew) {
985 pages.pageSlots.at(index)->setVisible(
true);
989 highlight(widget, pages, index);
993 static void highlight(TabsWidget& widget,
Pages& pages, std::size_t index) {
994 widget.setSelected(index);
995 pages.highlighted = index;
1007 static void restoreSelection(TabsWidget& widget,
Pages& pages, std::size_t shown)
noexcept {
1009 highlight(widget, pages, shown);
1014 std::function<void(std::size_t)>
const onSelect = pages.onSelect;
1025 void mountPage(TabsWidget& widget, reactive::Scope& pageScope,
Pages& pages, Node
const& node, std::size_t index) {
1026 auto page = mountPass([&] {
1027 auto built = std::make_unique<reactive::Scope>(*_rt, pageScope.depth());
1028 SlotWidget&
slot = built->adopt(_backend->createSlot(&widget));
1029 static_cast<void>(mount(*built, node, &slot));
1030 return std::pair{std::move(built), &
slot};
1032 pageScope.adopt(std::move(page.first));
1033 pages.pageSlots.at(index) = page.second;
1044 Widget& mountKind(reactive::Scope& scope, Dialog
const& spec, ContainerWidget* parent) {
1045 checkOverlayLayout(spec.common,
"dialog");
1046 DialogWidget& widget = scope.adopt(_backend->createDialog(parent));
1047 applyCommon(scope, widget, spec.common);
1048 return mountOverlay(scope, spec, widget);
1051 Widget& mountKind(reactive::Scope& scope, Drawer
const& spec, ContainerWidget* parent) {
1052 checkOverlayLayout(spec.common,
"drawer");
1053 DrawerWidget& widget = scope.adopt(_backend->createDrawer(parent, spec.side));
1054 applyCommon(scope, widget, spec.common);
1055 return mountOverlay(scope, spec, widget);
1059 template <
typename Spec,
typename OverlayW
idget>
1060 Widget& mountOverlay(reactive::Scope& scope, Spec
const& spec, OverlayWidget& widget) {
1061 bind(scope, spec.title, [&widget](std::string
const& title) { widget.setTitle(title); });
1062 auto&
content = scope.make<std::unique_ptr<reactive::Scope>>();
1063 auto& shownOpen = scope.make<std::optional<bool>>();
1066 std::function<void()> onDismiss =
event(spec.onDismiss);
1067 if (spec.open.isBound()) {
1069 reasserter<bool>(scope, shownOpen, spec.open.binding(), [&widget, &content, &shownOpen](
bool open) {
1070 if (!open || content != nullptr) {
1071 widget.setOpen(open);
1075 onDismiss = [inner = std::move(onDismiss), reassert = std::move(reassert)] {
1082 widget.setOnDismiss(onDismiss);
1083 auto show = [
this, &widget, &
content, &shownOpen, depth = scope.depth() + 1, child = spec.child](
1084 bool open, std::function<void()>
const& dismiss) {
1087 }
else if (content ==
nullptr) {
1089 content = underOverlay(&widget, [&] {
return mountContent(depth, child, widget); });
1091 dismissFailed(dismiss);
1095 widget.setOpen(open);
1098 if (!spec.open.isBound()) {
1100 show(spec.open.constant(), {});
1102 scope.effect([binding = spec.open.binding(), show = std::move(show), onDismiss = std::move(onDismiss)] {
1103 show(binding(), onDismiss);
1113 static void dismissFailed(std::function<
void()>
const& onDismiss)
noexcept {
1115 std::function<void()>
const dismiss = onDismiss;
1125 StackWidget& widget = scope.adopt(_backend->createStack(parent, spec.axis));
1126 applyCommon(scope, widget, spec.common);
1128 mountRows(scope, spec.model, widget, {},
nullptr);
1135 TableWidget& widget = scope.adopt(_backend->createTable(parent));
1136 applyCommon(scope, widget, spec.common);
1137 widget.setColumns(spec.columns);
1138 widget.setShowHeader(spec.showHeader);
1139 widget.setSelectionMode(spec.selectionMode);
1140 widget.setOnActivate(
event(spec.onActivate));
1142 scope, spec.rows, widget,
1143 [
this, &widget](reactive::Scope& rowScope,
Node const& node,
Widget& rowWidget,
Key const& key) {
1144 widget.setRowKey(rowWidget, key);
1146 Row const* const view = node ? std::get_if<Row>(&node->kind) : nullptr;
1147 if (view != nullptr && view->tone) {
1148 bind(rowScope, *view->tone, [&widget, &rowWidget](std::optional<Tone> const& tone) {
1149 widget.setRowTone(rowWidget, tone);
1154 widget.setOnSelectionChange(inputEvent(
1155 spec.onSelectionChange, controlled(scope, spec.selection, [&widget](std::vector<Key>
const& keys) {
1156 widget.setSelection(keys);
1167 void mountRows(reactive::Scope& scope, std::shared_ptr<ForEachModel const>
const& model,
1168 ContainerWidget& container, RowHook onRow, Virtualization
const* virtualize) {
1172 scope.make<std::shared_ptr<ForEachModel const>>(model);
1173 ForEachSession&
session = scope.adopt(model->open(*_rt));
1174 auto& rows = scope.make<KeyedRows>();
1175 TableWidget*
const table = virtualize !=
nullptr ?
dynamic_cast<TableWidget*
>(&container) : nullptr;
1176 if (table ==
nullptr) {
1177 scope.effect([
this, &session, &rows, &container, depth = scope.depth() + 1, onRow, overlay = _overlay] {
1178 std::vector<Key> const keys = session.pull();
1181 underOverlay(overlay, [&] {
1182 _rt->untracked([&] { reconcile(depth, session, rows.list, container, keys, onRow); });
1191 auto& range = scope.make<reactive::Signal<VisibleRange>>(*_rt, VisibleRange{});
1192 table->setOnVisibleRangeChanged([
this, &range](std::size_t first, std::size_t count) {
1193 runCallback(*_rt, [&] { range.set({.first = first, .count =
count}); });
1195 scope.effect([
this, &session, &rows, &container, &range, threshold = virtualize->threshold,
1196 overscan = virtualize->overscan, depth = scope.depth() + 1, onRow,
table, overlay = _overlay] {
1197 std::vector<Key>
const keys =
session.pull();
1198 VisibleRange
const window = range.peek();
1199 underOverlay(overlay, [&] {
1200 _rt->untracked([&] {
1201 if (keys.size() < threshold) {
1202 reconcile(depth, session, rows.list, container, keys, onRow);
1205 std::pair<VisibleRange, std::size_t>
const windowAndOverscan{window, overscan};
1206 reconcile(depth, session, rows.list, container, keys, onRow, &windowAndOverscan);
1210 table->setExistingKeys(keys);
1214 scope.effect([
this, &rows, &container, &range, threshold = virtualize->threshold,
1215 overscan = virtualize->overscan, depth = scope.depth() + 1, onRow,
table, overlay = _overlay] {
1216 VisibleRange
const window = range.get();
1221 if (rows.list.size() < threshold) {
1224 underOverlay(overlay, [&] { applyWindow(depth, rows.list, container, window, overscan, onRow); });
1225 auto const [first,
count] = realizedExtent(rows.list);
1226 table->setRealizedWindow(first, count, rows.list.size());
1231 [[nodiscard]] std::vector<std::size_t> firstOccurrences(std::vector<Key>
const& keys) {
1232 std::unordered_set<Key> seen;
1233 std::vector<std::size_t> order;
1234 order.reserve(keys.size());
1235 for (std::size_t index = 0; index < keys.size(); ++index) {
1236 if (seen.insert(keys.at(index)).second) {
1237 order.push_back(index);
1239 _rt->core()->report(site::kDuplicateKey);
1250 [[nodiscard]] KeyedRow mountRow(std::size_t depth, ForEachSession& session, ContainerWidget& container,
1251 Key
const& key, std::size_t index, RowHook
const& onRow) {
1252 return mountPass([&] {
1253 KeyedRow
row{.key = key, .stateScope = std::make_unique<reactive::Scope>(*_rt, depth)};
1255 row.widgetScope = std::make_unique<reactive::Scope>(*_rt, depth);
1256 row.widget = mount(*
row.widgetScope,
row.slot->view(), &container);
1257 row.widgetState =
row.widget !=
nullptr ? RowWidgetState::Realized : RowWidgetState::NullView;
1258 if (
row.widget !=
nullptr && onRow) {
1259 onRow(*
row.widgetScope,
row.slot->view(), *
row.widget, key);
1271 void realizeRowWidget(std::size_t depth, KeyedRow& row, ContainerWidget& container, RowHook
const& onRow) {
1272 row.widgetScope = std::make_unique<reactive::Scope>(*_rt, depth);
1273 row.widget = mount(*
row.widgetScope,
row.slot->view(), &container);
1274 row.widgetState =
row.widget !=
nullptr ? RowWidgetState::Realized : RowWidgetState::NullView;
1275 if (
row.widget !=
nullptr && onRow) {
1276 onRow(*
row.widgetScope,
row.slot->view(), *
row.widget,
row.key);
1283 static void dewindowRowWidget(KeyedRow& row) {
1284 row.widgetScope.reset();
1285 row.widget =
nullptr;
1286 row.widgetState = RowWidgetState::Dewindowed;
1293 [[nodiscard]] KeyedRow mountRowState(std::size_t depth, ForEachSession& session, Key
const& key,
1294 std::size_t index) {
1295 KeyedRow
row{.key = key, .stateScope = std::make_unique<reactive::Scope>(*_rt, depth)};
1297 row.widgetState = RowWidgetState::Dewindowed;
1303 static void reorder(std::vector<KeyedRow>& rows, ContainerWidget& container, std::vector<Key>
const& keys,
1304 std::vector<std::size_t>
const& order, std::exception_ptr& failure) {
1305 std::unordered_map<Key, std::size_t> position;
1306 std::unordered_map<Widget const*, std::size_t> rowOf;
1308 for (std::size_t i = 0; i < rows.size(); ++i) {
1309 KeyedRow
const&
row = rows.at(i);
1310 position.emplace(
row.key, i);
1311 if (
row.widget !=
nullptr) {
1312 rowOf.emplace(
row.widget, i);
1316 std::vector<Widget*> target;
1317 target.reserve(
present.size());
1318 for (std::size_t
const index : order) {
1319 if (
auto const found = position.find(keys.at(index)); found != position.end()) {
1320 if (KeyedRow
const& row = rows.at(found->second);
row.widget !=
nullptr) {
1321 target.push_back(
row.widget);
1325 std::vector<KeyedRow> ordered;
1326 ordered.reserve(rows.size());
1331 failure = std::current_exception();
1336 for (Widget
const*
const widget : present) {
1337 ordered.push_back(std::move(rows.at(rowOf.at(widget))));
1339 for (KeyedRow& row : rows) {
1340 if (
row.widget ==
nullptr) {
1341 ordered.push_back(std::move(row));
1344 rows = std::move(ordered);
1356 void reconcile(std::size_t depth, ForEachSession& session, std::vector<KeyedRow>& rows, ContainerWidget& container,
1357 std::vector<Key>
const& keys, RowHook
const& onRow,
1358 std::pair<VisibleRange, std::size_t>
const* windowAndOverscan =
nullptr) {
1359 std::vector<std::size_t>
const order = firstOccurrences(keys);
1360 std::unordered_map<Key, std::size_t> wanted;
1361 std::unordered_map<Key, std::size_t> viewPositionOf;
1362 for (std::size_t viewPos = 0; viewPos < order.size(); ++viewPos) {
1363 wanted.emplace(keys.at(order.at(viewPos)), order.at(viewPos));
1364 viewPositionOf.emplace(keys.at(order.at(viewPos)), viewPos);
1370 for (KeyedRow& row : std::views::reverse(rows)) {
1371 if (!wanted.contains(
row.key)) {
1372 row.widgetScope.reset();
1373 row.stateScope.reset();
1376 std::erase_if(rows, [](KeyedRow
const& row) {
return row.stateScope ==
nullptr; });
1377 std::unordered_set<Key> kept;
1378 for (KeyedRow& row : rows) {
1379 kept.insert(
row.key);
1380 row.slot->assign(wanted.at(
row.key));
1381 row.viewPosition = viewPositionOf.at(
row.key);
1384 std::size_t
const bootstrapFirst = 0;
1385 std::size_t
const bootstrapCount = windowAndOverscan !=
nullptr ? windowAndOverscan->second : 0;
1386 VisibleRange
const window = windowAndOverscan !=
nullptr && windowAndOverscan->first.count > 0
1387 ? windowAndOverscan->first
1388 : VisibleRange{.first = bootstrapFirst, .count = bootstrapCount};
1393 std::exception_ptr failure;
1394 rows.reserve(order.size());
1395 for (std::size_t viewPos = 0; viewPos < order.size(); ++viewPos) {
1396 std::size_t
const index = order.at(viewPos);
1397 if (kept.contains(keys.at(index))) {
1400 bool const inWindow =
1401 windowAndOverscan ==
nullptr || (viewPos >= window.first && viewPos < window.first + window.count);
1403 KeyedRow
row = inWindow ? mountRow(depth, session, container, keys.at(index), index, onRow)
1404 : mountRowState(depth,
session, keys.at(index), index);
1405 row.viewPosition = viewPos;
1406 rows.push_back(std::move(row));
1409 failure = std::current_exception();
1414 reorder(rows, container, keys, order, failure);
1416 std::rethrow_exception(failure);
1427 void applyWindow(std::size_t depth, std::vector<KeyedRow>& rows, ContainerWidget& container,
1428 VisibleRange
const& window, std::size_t overscan, RowHook
const& onRow) {
1429 std::size_t
const lo = window.first > overscan ? window.first - overscan : 0;
1430 std::size_t
const hi =
1431 window.first + window.count + overscan;
1432 std::exception_ptr failure;
1433 for (KeyedRow& row : rows) {
1434 bool const shouldBeRealized =
row.viewPosition >= lo &&
row.viewPosition < hi;
1435 if (shouldBeRealized &&
row.widgetState != RowWidgetState::Realized &&
1436 row.widgetState != RowWidgetState::NullView) {
1438 realizeRowWidget(depth, row, container, onRow);
1441 failure = std::current_exception();
1444 }
else if (!shouldBeRealized &&
row.widgetState == RowWidgetState::Realized) {
1445 dewindowRowWidget(row);
1452 std::vector<Key>
const keys = [&] {
1453 std::vector<Key> out;
1454 out.reserve(rows.size());
1455 for (KeyedRow
const& row : rows) {
1456 out.push_back(
row.key);
1460 std::vector<std::size_t> order(rows.size());
1461 std::iota(order.begin(), order.end(), std::size_t{0});
1462 std::ranges::sort(order, {}, [&rows](std::size_t index) {
return rows.at(index).viewPosition; });
1463 reorder(rows, container, keys, order, failure);
1465 std::rethrow_exception(failure);
1474 [[nodiscard]]
static std::pair<std::size_t, std::size_t> realizedExtent(std::vector<KeyedRow>
const& rows) {
1475 std::optional<std::size_t> first;
1476 std::size_t last = 0;
1477 for (KeyedRow
const& row : rows) {
1478 if (
row.widgetState == RowWidgetState::Realized) {
1479 first = first ? std::min(*first,
row.viewPosition) :
row.viewPosition;
1480 last = std::max(last,
row.viewPosition);
1483 return first ? std::pair{*first, last - *first + 1} : std::pair{std::size_t{0}, std::size_t{0}};
1486 Widget& mountKind(reactive::Scope& scope, Busy
const& spec, ContainerWidget* parent) {
1487 BusyWidget& widget = scope.adopt(_backend->createBusy(parent));
1488 applyCommon(scope, widget, spec.common);
1489 bind(scope, spec.active, [&widget](
bool active) { widget.setActive(active); });
1490 bind(scope, spec.label, [&widget](std::string
const& label) { widget.setLabel(label); });
1494 Widget& mountKind(reactive::Scope& scope, Banner
const& spec, ContainerWidget* parent) {
1495 BannerWidget& widget = scope.adopt(_backend->createBanner(parent));
1496 applyCommon(scope, widget, spec.common);
1497 bind(scope, spec.tone, [&widget](Tone tone) { widget.setTone(tone); });
1498 bind(scope, spec.text, [&widget](std::string
const& text) { widget.setText(text); });
1499 bind(scope, spec.action.label, [&widget](std::string
const& label) { widget.setActionLabel(label); });
1500 widget.setOnAction(
event(spec.action.onClick));
1501 widget.setDismissible(spec.dismissible);
1502 widget.setOnDismiss(
event(spec.onDismiss));
1506 Widget& mountKind(reactive::Scope& scope, Badge
const& spec, ContainerWidget* parent) {
1507 BadgeWidget& widget = scope.adopt(_backend->createBadge(parent));
1508 applyCommon(scope, widget, spec.common);
1509 bind(scope, spec.tone, [&widget](Tone tone) { widget.setTone(tone); });
1510 bind(scope, spec.text, [&widget](std::string
const& text) { widget.setText(text); });
1511 widget.setIcon(spec.icon);
1515 Widget& mountKind(reactive::Scope& scope, Progress
const& spec, ContainerWidget* parent) {
1516 ProgressWidget& widget = scope.adopt(_backend->createProgress(parent));
1517 applyCommon(scope, widget, spec.common);
1518 bind(scope, spec.value, [&widget](std::optional<double> value) { widget.setValue(value); });
1519 bind(scope, spec.label, [&widget](std::string
const& label) { widget.setLabel(label); });
1523 Widget& mountKind(reactive::Scope& scope, Steps
const& spec, ContainerWidget* parent) {
1524 StepsWidget& widget = scope.adopt(_backend->createSteps(parent));
1525 applyCommon(scope, widget, spec.common);
1526 bind(scope, spec.items, [&widget](std::vector<Step>
const& steps) { widget.setSteps(steps); });
1527 bind(scope, spec.current, [&widget](std::optional<std::size_t> index) { widget.setCurrent(index); });
1531 Widget& mountKind(reactive::Scope& scope, KeyValue
const& spec, ContainerWidget* parent) {
1532 KeyValueWidget& widget = scope.adopt(_backend->createKeyValue(parent));
1533 applyCommon(scope, widget, spec.common);
1534 bind(scope, spec.items, [&widget](std::vector<KeyValueItem>
const& items) { widget.setItems(items); });
1538 Widget& mountKind(reactive::Scope& scope, EmptyState
const& spec, ContainerWidget* parent) {
1539 EmptyStateWidget& widget = scope.adopt(_backend->createEmptyState(parent));
1540 applyCommon(scope, widget, spec.common);
1541 bind(scope, spec.title, [&widget](std::string
const& title) { widget.setTitle(title); });
1542 bind(scope, spec.text, [&widget](std::string
const& text) { widget.setText(text); });
1543 widget.setIcon(spec.icon);
1544 bind(scope, spec.action.label, [&widget](std::string
const& label) { widget.setActionLabel(label); });
1545 widget.setOnAction(
event(spec.action.onClick));
1549 Widget& mountKind(reactive::Scope& scope, Splitter
const& spec, ContainerWidget* parent) {
1550 SplitterWidget& widget = scope.adopt(_backend->createSplitter(parent, spec.orientation));
1551 applyCommon(scope, widget, spec.common);
1552 mountChildren(scope, spec.children, widget);
1553 widget.setOnResize(inputEvent(
1555 controlled(scope, spec.sizes, [&widget](std::vector<int>
const& sizes) { widget.setSizes(sizes); })));
1560 Widget& mountKind(reactive::Scope& scope, Collapsible
const& spec, ContainerWidget* parent) {
1561 CollapsibleWidget& widget = scope.adopt(_backend->createCollapsible(parent));
1562 applyCommon(scope, widget, spec.common);
1563 bind(scope, spec.title, [&widget](std::string
const& title) { widget.setTitle(title); });
1564 if (Widget*
const header = mount(scope, spec.header, &widget); header !=
nullptr) {
1565 widget.setHeader(*header);
1567 static_cast<void>(mount(scope, spec.child, &widget));
1569 inputEvent(spec.onToggle, controlled(scope, spec.open, [&widget](
bool open) { widget.setOpen(open); })));
1573 Widget& mountKind(reactive::Scope& scope, DropZone
const& spec, ContainerWidget* parent) {
1574 DropZoneWidget& widget = scope.adopt(_backend->createDropZone(parent));
1575 applyCommon(scope, widget, spec.common);
1576 widget.setAccept(spec.accept);
1577 widget.setMultiple(spec.multiple);
1578 widget.setOnDrop(
event(spec.onDrop));
1579 static_cast<void>(mount(scope, spec.child, &widget));
1584 void bindCustomProp(reactive::Scope& scope, CustomWidget& widget, CustomProp
const& prop) {
1585 std::string
const& name =
prop.name;
1586 if (
auto const*
const text = std::get_if<Prop<std::string>>(&
prop.value)) {
1587 bind(scope, *text, [&widget, name](std::string
const& value) { widget.setTextProp(name, value); });
1588 }
else if (
auto const*
const flag = std::get_if<Prop<bool>>(&
prop.value)) {
1589 bind(scope, *flag, [&widget, name](
bool value) { widget.setBoolProp(name, value); });
1590 }
else if (
auto const*
const number = std::get_if<Prop<double>>(&
prop.value)) {
1591 bind(scope, *number, [&widget, name](
double value) { widget.setNumberProp(name, value); });
1597 Widget& mountKind(reactive::Scope& scope, ActionBar
const& spec, ContainerWidget* parent) {
1598 static_cast<void>(parent);
1599 ContainerWidget*
const footer = overlayFooter();
1600 if (footer ==
nullptr) {
1601 throw std::logic_error{
"morph::ui: an action bar mounts only in a dialog's or drawer's content"};
1603 StackWidget& widget = scope.adopt(_backend->createStack(footer, Axis::Horizontal));
1604 applyCommon(scope, widget, spec.common);
1606 for (Node
const& child : spec.children) {
1607 static_cast<void>(mount(scope, child, &widget));
1612 Widget& mountKind(reactive::Scope& scope, Custom
const& spec, ContainerWidget* parent) {
1613 CustomRequest
request{.name = spec.name, .props = {}, .minVersion = spec.minVersion};
1614 request.props.reserve(spec.props.size());
1615 for (CustomProp
const& prop : spec.props) {
1618 if (std::unique_ptr<CustomWidget> loaded = _backend->createCustom(parent, request)) {
1619 CustomWidget& widget = scope.adopt(std::move(loaded));
1620 for (CustomProp
const& prop : spec.props) {
1621 bindCustomProp(scope, widget, prop);
1623 widget.setOnEvent(spec.onEvent);
1626 Widget*
const fallback = mount(scope, spec.fallback, parent);
1627 if (fallback ==
nullptr) {
1628 throw std::invalid_argument{
"morph::ui::Mounted: the custom node " + spec.name +
" has no fallback"};
1633 Widget& mountKind(reactive::Scope& scope, DateTimeInput
const& spec, ContainerWidget* parent) {
1634 DateTimeInputWidget& widget =
1635 scope.adopt(_backend->createDateTimeInput(parent, spec.mode, spec.offsetMinutes));
1636 applyCommon(scope, widget, spec.common);
1637 applyField(scope, widget, spec.field);
1638 widget.setOnChange(inputEvent(
1640 controlled(scope, spec.value,
1641 [&widget](std::optional<morph::time::Timestamp>
const& value) { widget.setValue(value); })));
1642 applyCaption(scope, widget, spec.label, spec.showLabel);
1646 Widget& mountKind(reactive::Scope& scope, Slider
const& spec, ContainerWidget* parent) {
1647 SliderWidget& widget = scope.adopt(_backend->createSlider(parent));
1648 applyCommon(scope, widget, spec.common);
1649 applyField(scope, widget, spec.field);
1650 widget.setRange(spec.minimum, spec.maximum, spec.step);
1651 widget.setOnChange(inputEvent(
1652 spec.onChange, controlled(scope, spec.value, [&widget](std::int64_t value) { widget.setValue(value); })));
1653 widget.setOnCommit(
event(spec.onCommit));
1657 Widget& mountKind(reactive::Scope& scope, FilePicker
const& spec, ContainerWidget* parent) {
1658 FilePickerWidget& widget = scope.adopt(_backend->createFilePicker(parent, spec.mode));
1659 applyCommon(scope, widget, spec.common);
1660 applyField(scope, widget, spec.field);
1661 widget.setOnPicked(inputEvent(spec.onPicked, controlled(scope, spec.path, [&widget](std::string
const& path) {
1662 widget.setPath(path);
1667 reactive::Runtime* _rt;
1668 IViewBackend* _backend;
1672 ContainerWidget* _overlay =
nullptr;
1674 Widget* _autofocus =
nullptr;
1675 std::size_t _passDepth = 0;
1678 FocusMap _focusable;