morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
workspace.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include <algorithm>
5#include <chrono>
6#include <cstddef>
7#include <cstdint>
8#include <expected>
9#include <functional>
10#include <map>
11#include <memory>
12#include <optional>
13#include <string>
14#include <string_view>
15#include <utility>
16#include <vector>
17
20#include "../reactive/scope.hpp"
21#include "backend.hpp"
22#include "host.hpp"
23#include "mount.hpp"
24#include "route.hpp"
25#include "value.hpp"
26#include "view.hpp"
27#include "workspace_model.hpp"
28
29/// @file
30/// @brief The workspace shell (spec 6 §7, §15): tabs over screens, mounted through a `DocumentScreenHost`.
31///
32/// Specified in `docs/spec/ui/workspace.md`.
33
34namespace morph::ui {
35
36/// @brief One open tab, as an application's tab menu or a test reads it.
38 /// @brief The screen id.
39 std::string screenId;
40 /// @brief The screen's identity; `null` when it has none.
42 /// @brief The caption the bar shows: the title, with ` •` while there are unsaved changes.
43 std::string caption;
44};
45
46/// @brief Where a workspace draws the active screen's head row.
47enum class HeadPlacement : std::uint8_t {
48 /// @brief Above each page, inside the workspace.
50 /// @brief Nowhere: the host mounts `headRowForActive()` where it chooses, and a page holds only its screen.
52};
53
54/// @brief Tabs over screens: a tab bar over a `WorkspaceModel`, one screen instance per tab, mounted into the tab's
55/// page through `DocumentScreenHost::mountInto`, with the screen's head row above it or, when the placement is
56/// `External`, mounted by the host instead.
57///
58/// The model holds the tab logic: tabs keep the order they were opened in, opening a screen whose identity an open
59/// tab already has focuses that tab, a singleton screen has one tab, and closing runs each screen's close protocol
60/// (spec 6 §3), one tab at a time, a walk over several tabs stopping at the first veto. The bar shows the model's tabs
61/// and takes Ctrl+Tab, Ctrl+Shift+Tab and Ctrl+W, which documents may not declare. Neither copyable nor movable;
62/// owned by the owner thread of its runtime.
63class Workspace {
64public:
65 /// @brief The shell, its tab bar appended to @p parent.
66 /// @param runtime The runtime the screens' reactive nodes belong to. Borrowed.
67 /// @param backend The backend the bar and the pages are made with. Borrowed.
68 /// @param host What mounts the screens. Borrowed; it outlives the shell.
69 /// @param options The screens' catalog facts.
70 /// @param parent The container the shell is appended to; null makes it a backend root.
71 /// @param headPlacement Where the active screen's head row is drawn. `External` leaves the host to mount
72 /// `headRowForActive()` itself, so a shell with its own top bar shows the head once.
73 Workspace(reactive::Runtime& runtime MORPH_LIFETIMEBOUND, IViewBackend& backend MORPH_LIFETIMEBOUND,
74 DocumentScreenHost& host MORPH_LIFETIMEBOUND, WorkspaceOptions options = {},
75 ContainerWidget* parent = nullptr, HeadPlacement headPlacement = HeadPlacement::InPage)
76 : _runtime{&runtime},
77 _backend{&backend},
78 _host{&host},
79 _bar{backend.createTabs(parent)},
80 _hosts{options.hosts},
81 _screens{options.screens},
82 _onChanged{std::move(options.onChanged)},
83 _headPlacement{headPlacement} {
84 // The bar and each page fill what they are given, so a screen whose root stretches fills the page.
85 _bar->setLayout({.width = Sizing::stretch(), .height = Sizing::stretch()});
86 _bar->setTabs({});
87 _bar->setSelected(kNone);
88 _bar->setOnSelect([this](std::size_t index) {
89 std::vector<WorkspaceTabId> const order = _model->order();
90 if (index < order.size()) {
91 _model->activate(order.at(index));
92 }
93 });
94 _bar->setKeys({"Ctrl+Tab", "Ctrl+Shift+Tab", "Ctrl+W"}, [this](std::string const& chord) {
95 if (chord == "Ctrl+Tab") {
96 next();
97 } else if (chord == "Ctrl+Shift+Tab") {
98 previous();
99 } else if (std::optional<std::size_t> const shown = active()) {
100 close(*shown, {});
101 }
102 });
103 WorkspaceModelOptions modelOptions{std::move(options), {}, {}};
104 modelOptions.onChanged = [this] { showTabs(); };
105 modelOptions.mountPage = [this](WorkspaceTabId tab, std::string_view screenId, ParamMap const& params) {
106 return mountPage(tab, screenId, params);
107 };
108 modelOptions.unmountPage = [this](WorkspaceTabId tab) {
109 std::erase(_shownOrder, tab);
110 _pages.erase(tab.value);
111 };
112 _model = std::make_unique<WorkspaceModel>(runtime, host, std::move(modelOptions));
113 }
114
115 /// @brief Unmounts every screen without asking, newest first, then the pages.
116 ~Workspace() { _model.reset(); }
117
118 Workspace(Workspace const&) = delete;
119 Workspace& operator=(Workspace const&) = delete;
120 Workspace(Workspace&&) = delete;
121 Workspace& operator=(Workspace&&) = delete;
122
123 /// @brief The tab bar: the shell's root widget, which the host places.
124 /// @return It.
125 [[nodiscard]] TabsWidget& root() noexcept { return *_bar; }
126
127 /// @brief The tab logic the bar shows, for an application that reads or drives it by tab id.
128 /// @return It, valid as long as the shell.
129 [[nodiscard]] WorkspaceModel& model() noexcept { return *_model; }
130
131 /// @brief Opens a screen, or focuses the tab that already shows it (`WorkspaceModel::open`).
132 /// @param screenId The screen id.
133 /// @param params Its params.
134 /// @return The tab's index, or why the screen could not be mounted; a refused mount opens nothing.
135 std::expected<std::size_t, MountError> open(std::string_view screenId, ParamMap const& params) {
136 auto opened = _model->open(screenId, params);
137 if (!opened) {
138 return std::unexpected(std::move(opened.error()));
139 }
140 return indexOf(*opened);
141 }
142
143 /// @brief Opens the start screens (`WorkspaceModel::start`).
144 /// @param how The start screen, the fallback and the record.
145 /// @return The number of tabs opened.
146 std::size_t start(WorkspaceStart const& how) { return _model->start(how); }
147
148 /// @brief Writes the record now, cancelling a pending debounced write: what sign-out does before the session is
149 /// cleared.
150 void flushRecord() { _model->flushRecord(); }
151
152 /// @brief The record as it would be written now: the routes of the open tabs, in order.
153 /// @return The routes.
154 [[nodiscard]] std::vector<std::string> record() const { return _model->record(); }
155
156 /// @brief Shows a tab and makes it the most recently used.
157 /// @param index The tab; past the last one does nothing.
158 void activate(std::size_t index) {
159 if (std::optional<WorkspaceTabId> const tab = tabAt(index)) {
160 _model->activate(*tab);
161 }
162 }
163
164 /// @brief Shows the next tab in most-recently-used order (Ctrl+Tab). Repeated calls walk the order as it was at
165 /// the first of them; any other activation starts a new walk.
166 void next() { _model->next(); }
167
168 /// @brief Shows the previous tab in most-recently-used order (Ctrl+Shift+Tab).
169 void previous() { _model->previous(); }
170
171 /// @brief Closes a tab through its screen's close protocol.
172 /// @param index The tab.
173 /// @param done Called once with whether it closed; may be empty.
174 void close(std::size_t index, std::function<void(bool)> done) {
175 std::optional<WorkspaceTabId> const tab = tabAt(index);
176 if (!tab) {
177 if (done) {
178 done(false);
179 }
180 return;
181 }
182 _model->close(*tab, std::move(done));
183 }
184
185 /// @brief Closes every tab but one, one at a time, stopping at the first veto.
186 /// @param index The tab kept; past the last one closes every tab.
187 /// @param done Called once with whether every other tab closed; may be empty.
188 void closeOthers(std::size_t index, std::function<void(bool)> done) {
189 std::optional<WorkspaceTabId> const tab = tabAt(index);
190 if (!tab) {
191 _model->closeAll(std::move(done));
192 return;
193 }
194 _model->closeOthers(*tab, std::move(done));
195 }
196
197 /// @brief Closes every tab, one at a time, stopping at the first veto.
198 /// @param done Called once with whether every tab closed; may be empty.
199 void closeAll(std::function<void(bool)> done) { _model->closeAll(std::move(done)); }
200
201 /// @brief Closes every tab without unsaved changes, one at a time, stopping at the first veto.
202 /// @param done Called once with whether each of them closed; may be empty.
203 void closeSaved(std::function<void(bool)> done) { _model->closeSaved(std::move(done)); }
204
205 /// @brief The active tab's index.
206 /// @return It, or `std::nullopt` while no tab is open.
207 [[nodiscard]] std::optional<std::size_t> active() const {
208 std::optional<WorkspaceTabId> const tab = _model->active().peek();
209 if (!tab) {
210 return std::nullopt;
211 }
212 return indexOf(*tab);
213 }
214
215 /// @brief The screen handle of a tab: what a client remounts for development reload and inspects.
216 /// @param index The tab's index.
217 /// @return Its handle, valid while the tab is open; null past the end.
218 [[nodiscard]] ScreenHandle* handleOf(std::size_t index) const noexcept {
219 std::optional<WorkspaceTabId> const tab = tabAt(index);
220 return tab ? _model->handleOf(*tab) : nullptr;
221 }
222
223 /// @brief The active tab's screen handle.
224 /// @return It, or null while no tab is open.
225 [[nodiscard]] ScreenHandle* activeHandle() const {
226 std::optional<std::size_t> const shown = active();
227 return shown ? handleOf(*shown) : nullptr;
228 }
229
230 /// @brief The active tab's head row, the same reactive content `headRow` builds for the body-embedded rendering,
231 /// for a host that mounts it elsewhere (a shell's own top bar, say). Never null: an invisible, empty row
232 /// stands in for "no active tab" rather than a null `Node`, since a caller mounting this may not expect to
233 /// handle one.
234 /// @return The row, reactive to the active tab changing and to its head's own content changing.
235 [[nodiscard]] Node headRowForActive() const {
236 std::optional<WorkspaceTabId> const tab = _model->active().peek();
237 if (tab) {
238 return ui::row(buildHeadRow(*tab));
239 }
240 Row empty{.children = {}, .gap = 0, .common = {}, .tone = std::nullopt};
241 empty.common.testId = "head";
242 empty.common.visible = false;
243 return ui::row(std::move(empty));
244 }
245
246 /// @brief Closes every tab without asking, newest first: what signing out does, since no close guard may keep a
247 /// screen of the principal who is leaving.
248 void forceCloseAll() { _model->forceCloseAll(); }
249
250 /// @brief The open tabs, in order.
251 /// @return Each tab's screen, identity and caption.
252 [[nodiscard]] std::vector<WorkspaceTab> tabs() const {
253 std::vector<WorkspaceTab> out;
254 for (WorkspaceTabId const tab : _model->order()) {
255 ScreenHandle const& handle = *_model->handleOf(tab);
256 out.push_back(WorkspaceTab{.screenId = std::string{screenOf(tab)},
257 .identity = handle.identity().peek(),
258 .caption = captionOf(handle)});
259 }
260 return out;
261 }
262
263private:
264 static constexpr std::size_t kNone = static_cast<std::size_t>(-1);
265
266 // A tab's page: the slot of the bar it shows in, the head row above its screen, and the screen's id. The model
267 // destroys the screen's handle before it lets the page go.
268 struct Page {
269 std::string screenId;
270 std::unique_ptr<SlotWidget> slot;
271 // The head row reads the handle through the model, which answers null once the handle is gone.
272 std::optional<Mounted> head;
273
274 Page() = default;
275 Page(Page const&) = delete;
276 Page& operator=(Page const&) = delete;
277 Page(Page&&) = delete;
278 Page& operator=(Page&&) = delete;
279 ~Page() {
280 head.reset();
281 slot.reset();
282 }
283 };
284
285 // A badge of the head row, by its place among the visible badges.
286 struct PlacedBadge {
287 std::size_t index = 0;
288 HeadBadge badge;
289 };
290
291 [[nodiscard]] std::optional<WorkspaceTabId> tabAt(std::size_t index) const {
292 std::vector<WorkspaceTabId> const order = _model->order();
293 return index < order.size() ? std::optional<WorkspaceTabId>{order.at(index)} : std::nullopt;
294 }
295
296 [[nodiscard]] std::size_t indexOf(WorkspaceTabId tab) const {
297 std::vector<WorkspaceTabId> const order = _model->order();
298 auto const found = std::ranges::find(order, tab);
299 return found == order.end() ? kNone : static_cast<std::size_t>(found - order.begin());
300 }
301
302 [[nodiscard]] std::string_view screenOf(WorkspaceTabId tab) const {
303 auto const found = _pages.find(tab.value);
304 return found == _pages.end() ? std::string_view{} : std::string_view{found->second->screenId};
305 }
306
307 [[nodiscard]] static std::string captionOf(ScreenHandle const& handle) {
308 std::string caption = handle.title().peek();
309 if (handle.dirty().peek()) {
310 caption += " •";
311 }
312 return caption;
313 }
314
315 // The model's mountPage: a page slot at the end of the bar, the screen mounted into it, a host screen from the
316 // registry and a document screen from the document host, then the head row above it. The page goes with a
317 // refused mount.
318 std::expected<std::unique_ptr<ScreenHandle>, MountError> mountPage(WorkspaceTabId tab, std::string_view screenId,
319 ParamMap const& params) {
320 auto page = std::make_unique<Page>();
321 page->screenId = std::string{screenId};
322 page->slot = _backend->createSlot(_bar.get());
323 page->slot->setLayout({.width = Sizing::stretch(), .height = Sizing::stretch()});
324 page->slot->setVisible(false);
325 std::expected<std::unique_ptr<ScreenHandle>, MountError> handle;
326 WorkspaceScreen const facts = _screens ? _screens(screenId) : WorkspaceScreen{};
327 if (facts.host.empty()) {
328 handle = _host->mountInto(screenId, params, *page->slot);
329 } else {
330 HostScreenFactory const* const factory = _hosts == nullptr ? nullptr : _hosts->find(facts.host);
331 if (factory == nullptr) {
332 return std::unexpected(MountError{.screenId = std::string{screenId},
333 .reason = "no host screen `" + facts.host + "` is registered"});
334 }
335 handle = (*factory)(params, *page->slot);
336 }
337 if (!handle) {
338 return handle;
339 }
340 if (_headPlacement == HeadPlacement::InPage && (*handle)->head() != nullptr) {
341 // Mounted after the screen, since it reads the handle, and moved above it.
342 page->head.emplace(*_runtime, *_backend, headRow(tab), page->slot.get());
343 page->slot->moveChild(page->head->root(), 0);
344 }
345 _pages.insert_or_assign(tab.value, std::move(page));
346 _shownOrder.push_back(tab);
347 return handle;
348 }
349
350 // The head of a tab's screen, while the tab is open. A tracked read of the model's tabs, so a binding made while
351 // the model is still opening the tab reads the head once it holds it.
352 [[nodiscard]] ScreenHead* headOf(WorkspaceTabId tab) const {
353 static_cast<void>(_model->tabs().get());
354 ScreenHandle* const handle = _model->handleOf(tab);
355 return handle == nullptr ? nullptr : handle->head();
356 }
357
358 // The head row's own content, built but not yet wrapped into an immutable Node: the subtitle (muted), the
359 // badges, a spacer and the actions as buttons. Hidden while the head has none of them, so a screen without a
360 // head looks as it would without the row. Factored out of headRow() so a caller needing to set additional
361 // layout fields on the row -- which a Node, once built, can no longer be mutated to carry -- can do so on the
362 // still-mutable Row this returns, before wrapping it itself.
363 [[nodiscard]] Row buildHeadRow(WorkspaceTabId tab) const {
364 auto const subtitle = [this, tab] {
365 ScreenHead const* const head = headOf(tab);
366 return head == nullptr ? std::string{} : head->subtitle().get();
367 };
368 auto const status = [this, tab] {
369 ScreenHead const* const head = headOf(tab);
370 return head == nullptr ? std::vector<HeadBadge>{} : head->status().get();
371 };
372 auto const actions = [this, tab] {
373 ScreenHead const* const head = headOf(tab);
374 return head == nullptr ? std::vector<HeadAction>{} : head->actions().get();
375 };
376 Row row{.children = {}, .gap = 2, .common = {}, .tone = std::nullopt};
377 row.common.testId = "head";
378 // Full width, so the actions sit at the trailing edge in either placement.
379 row.common.layout.width = Sizing::stretch();
380 row.common.visible = Prop<bool>{
381 [subtitle, status, actions] { return !subtitle().empty() || !status().empty() || !actions().empty(); }};
382 Text text{.text = Prop<std::string>{subtitle}, .role = TextRole::Muted, .field = {}, .common = {}};
383 text.common.testId = "head_subtitle";
384 text.common.visible = Prop<bool>{[subtitle] { return !subtitle().empty(); }};
385 row.children.push_back(ui::text(std::move(text)));
386 row.children.push_back(ui::forEach<PlacedBadge>(
387 [status] {
388 std::vector<PlacedBadge> placed;
389 for (HeadBadge const& badge : status()) {
390 placed.push_back(PlacedBadge{.index = placed.size(), .badge = badge});
391 }
392 return placed;
393 },
394 [](PlacedBadge const& placed) { return Key{static_cast<std::int64_t>(placed.index)}; },
395 [](reactive::Signal<PlacedBadge> const& placed) {
396 Badge badge{.tone = Prop<Tone>{[&placed] { return placed.get().badge.tone; }},
397 .text = Prop<std::string>{[&placed] { return placed.get().badge.text; }},
398 .icon = placed.peek().badge.icon,
399 .common = {}};
400 badge.common.testId = "head_status_" + std::to_string(placed.peek().index);
401 return ui::badge(std::move(badge));
402 },
403 Axis::Horizontal, 1));
404 // The spacer stretches, which is what makes the row lay out at full width and pushes the actions to the end.
405 Spacer filler;
406 filler.common.testId = "head_spacer";
407 filler.common.layout.width = Sizing::stretch();
408 row.children.push_back(ui::spacer(std::move(filler)));
409 row.children.push_back(ui::forEach<HeadAction>(
410 actions, [](HeadAction const& action) { return Key{action.id}; },
411 [this, tab](reactive::Signal<HeadAction> const& action) {
412 Button button{.label = Prop<std::string>{[&action] { return action.get().label; }},
413 .emphasis = action.peek().emphasis,
414 .onClick =
415 [this, tab, actionId = action.peek().id] {
416 ScreenHandle* const handle = _model->handleOf(tab);
417 if (ScreenHead* const head = handle == nullptr ? nullptr : handle->head()) {
418 head->run(actionId);
419 }
420 },
421 .common = {}};
422 button.common.testId = "head_action_" + action.peek().id;
423 button.common.enabled = Prop<bool>{[&action] { return action.get().enabled; }};
424 return ui::button(std::move(button));
425 },
426 Axis::Horizontal, 1));
427 return row;
428 }
429
430 // The head row above a tab's screen, exactly as buildHeadRow() builds it, wrapped into a Node.
431 [[nodiscard]] Node headRow(WorkspaceTabId tab) const { return ui::row(buildHeadRow(tab)); }
432
433 // The model changed: the bar shows its tabs in order, with their captions, and the active tab's page.
434 void showTabs() {
435 if (_model == nullptr) {
436 return;
437 }
438 std::vector<WorkspaceTabId> const order = _model->order();
439 std::optional<WorkspaceTabId> const shown = _model->active().peek();
440 std::vector<std::string> captions;
441 captions.reserve(order.size());
442 for (std::size_t index = 0; index < order.size(); ++index) {
443 auto const found = _pages.find(order.at(index).value);
444 if (found == _pages.end()) {
445 continue;
446 }
447 Page& page = *found->second;
448 // A tab the model moved (a reuseEmpty replacement) moves its page in the bar too; `_shownOrder` is the
449 // bar's own order of the pages.
450 if (index >= _shownOrder.size() || _shownOrder.at(index) != order.at(index)) {
451 std::erase(_shownOrder, order.at(index));
452 _shownOrder.insert(
453 _shownOrder.begin() + static_cast<std::ptrdiff_t>(std::min(index, _shownOrder.size())),
454 order.at(index));
455 _bar->moveChild(*page.slot, index);
456 }
457 page.slot->setVisible(shown == order.at(index));
458 captions.push_back(captionOf(*_model->handleOf(order.at(index))));
459 }
460 _bar->setTabs(captions);
461 _bar->setSelected(shown ? indexOf(*shown) : kNone);
462 // One tab needs no strip to choose it by; the chords still work with the strip hidden.
463 _bar->setBarShown(captions.size() > 1);
464 if (_onChanged) {
465 _onChanged();
466 }
467 }
468
469 reactive::Runtime* _runtime;
470 IViewBackend* _backend;
471 DocumentScreenHost* _host;
472 std::unique_ptr<TabsWidget> _bar;
473 ScreenHostRegistry const* _hosts = nullptr;
474 std::function<WorkspaceScreen(std::string_view)> _screens;
475 std::function<void()> _onChanged;
476 HeadPlacement _headPlacement = HeadPlacement::InPage;
477 std::map<std::uint64_t, std::unique_ptr<Page>> _pages;
478 // The pages in the bar's order.
479 std::vector<WorkspaceTabId> _shownOrder;
480 std::unique_ptr<WorkspaceModel> _model;
481};
482
483} // namespace morph::ui
Node text(TextOptions const &options)
A text node.
Definition author.hpp:3126
Node button(ButtonOptions const &options)
A button node.
Definition author.hpp:3156
Handle< T > row()
The innermost row, {"row": ""}.
Definition author.hpp:1145
Node badge(AnyHandle const &text, AnyHandle const &tone={}, AnyHandle const &icon={})
A badge node.
Definition author.hpp:4018
The owner of a signal graph: every node takes one, and every operation runs on its owner.
Definition runtime.hpp:70
A widget with children. A factory given this container appends the new widget as its last child.
Definition backend.hpp:103
The framework's ScreenHost over UI documents: it mounts them through an Interpreter,...
Definition host.hpp:344
Makes widgets. Each factory appends the new widget to parent, or makes a root when it is null,...
Definition backend.hpp:810
One mounted screen, as its host sees it.
Definition host.hpp:82
virtual reactive::Computed< Value > const & identity() const =0
The screen's identity: the document's identity, null when it declares none. A host re-keys a tab when...
A tab bar over page slots.
Definition backend.hpp:391
The document's typed value (spec 5 §3): what every expression evaluates to.
Definition value.hpp:157
The workspace's tabs with no widgets: which screens are open, in which order, which is active,...
Tabs over screens: a tab bar over a WorkspaceModel, one screen instance per tab, mounted into the tab...
Definition workspace.hpp:63
void activate(std::size_t index)
Shows a tab and makes it the most recently used.
ScreenHandle * handleOf(std::size_t index) const noexcept
The screen handle of a tab: what a client remounts for development reload and inspects.
void closeSaved(std::function< void(bool)> done)
Closes every tab without unsaved changes, one at a time, stopping at the first veto.
std::expected< std::size_t, MountError > open(std::string_view screenId, ParamMap const &params)
Opens a screen, or focuses the tab that already shows it (WorkspaceModel::open).
std::vector< std::string > record() const
The record as it would be written now: the routes of the open tabs, in order.
void forceCloseAll()
Closes every tab without asking, newest first: what signing out does, since no close guard may keep a...
Workspace(reactive::Runtime &runtime, IViewBackend &backend, DocumentScreenHost &host, WorkspaceOptions options={}, ContainerWidget *parent=nullptr, HeadPlacement headPlacement=HeadPlacement::InPage)
The shell, its tab bar appended to parent.
Definition workspace.hpp:73
Node headRowForActive() const
The active tab's head row, the same reactive content headRow builds for the body-embedded rendering,...
TabsWidget & root() noexcept
The tab bar: the shell's root widget, which the host places.
void close(std::size_t index, std::function< void(bool)> done)
Closes a tab through its screen's close protocol.
~Workspace()
Unmounts every screen without asking, newest first, then the pages.
void flushRecord()
Writes the record now, cancelling a pending debounced write: what sign-out does before the session is...
WorkspaceModel & model() noexcept
The tab logic the bar shows, for an application that reads or drives it by tab id.
ScreenHandle * activeHandle() const
The active tab's screen handle.
std::vector< WorkspaceTab > tabs() const
The open tabs, in order.
void next()
Shows the next tab in most-recently-used order (Ctrl+Tab). Repeated calls walk the order as it was at...
void closeAll(std::function< void(bool)> done)
Closes every tab, one at a time, stopping at the first veto.
std::size_t start(WorkspaceStart const &how)
Opens the start screens (WorkspaceModel::start).
void previous()
Shows the previous tab in most-recently-used order (Ctrl+Shift+Tab).
void closeOthers(std::size_t index, std::function< void(bool)> done)
Closes every tab but one, one at a time, stopping at the first veto.
std::optional< std::size_t > active() const
The active tab's index.
Node testId(std::string const &name) const
A stable identifier for automated tests.
Definition author.hpp:1907
Node visible(AnyHandle const &condition) const
Whether the node shows.
Definition author.hpp:1823
Node id(Element const &element) const
The element id, by which relations read its values.
Definition author.hpp:1818
Node enabled(AnyHandle const &condition) const
Whether the node is enabled.
Definition author.hpp:1828
@ Row
On every row; the body is built from the row's bind entries.
The host contract (spec 6 §4): how a host mounts UI documents and closes them, whether it owns its ow...
ValueRecord ParamMap
A screen's params, by name.
Definition host.hpp:38
std::function< std::expected< std::unique_ptr< ScreenHandle >, MountError >( ParamMap const &params, ContainerWidget &page)> HostScreenFactory
Makes a host screen (spec 6 §15): the application's native screen, mounted into a shell's page with p...
Definition host.hpp:175
morph::ui::Mounted: builds a view tree's widgets through a backend once, and keeps them current throu...
morph::reactive::Scheduler: timers a frontend runs on the runtime's owner executor.
Routes (spec 6 §20): the string form of a screen instance, <screen id>?<param>=<value>&…,...
morph::reactive::Runtime: the owner, batching and flush scheduling of a signal graph.
morph::reactive::Scope: owns reactive nodes and objects, and destroys them in reverse.
std::string testId
The stable identifier an automated test finds the widget by; empty means none. Set once.
Definition view.hpp:364
Prop< bool > visible
Whether the widget is shown.
Definition view.hpp:350
Children laid out left to right.
Definition view.hpp:578
Common common
Visibility, enablement, layout, drag-and-drop.
Definition view.hpp:584
static constexpr Sizing stretch(int weight=1) noexcept
A share of the leftover space.
Definition view.hpp:294
How a workspace treats screens.
How a workspace starts (spec 6 §7, app-start).
A tab's identity in a WorkspaceModel: issued when it opens, never reused.
One open tab, as an application's tab menu or a test reads it.
Definition workspace.hpp:37
Value identity
The screen's identity; null when it has none.
Definition workspace.hpp:41
std::string screenId
The screen id.
Definition workspace.hpp:39
std::string caption
The caption the bar shows: the title, with • while there are unsaved changes.
Definition workspace.hpp:43
The backend contract: the retained widgets a mount drives, and the factory that makes them.
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
The toolkit-agnostic view tree: props, node kinds and their builders.
std::shared_ptr< NodeData const > Node
A view-tree node: immutable, shared data.
Definition view.hpp:403
Node row(Row spec)
Children laid out left to right.
Definition view.hpp:1229
std::variant< std::int64_t, std::string > Key
The identity of a Switch case, a Select option, a ForEach or Table row and a drag payload.
Definition view.hpp:38
SlotBinding< std::decay_t< F > > slot(SlotId slotId, F &&read)
A slot with an id.
Definition view.hpp:63
HeadPlacement
Where a workspace draws the active screen's head row.
Definition workspace.hpp:47
@ External
Nowhere: the host mounts headRowForActive() where it chooses, and a page holds only its screen.
Definition workspace.hpp:51
@ InPage
Above each page, inside the workspace.
Definition workspace.hpp:49
The workspace's tab logic with no widgets (spec 6 §7, §15): what an application's own tab strip binds...