morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
workspace_model.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 <compare>
7#include <cstddef>
8#include <cstdint>
9#include <exception>
10#include <expected>
11#include <functional>
12#include <memory>
13#include <optional>
14#include <stdexcept>
15#include <string>
16#include <string_view>
17#include <utility>
18#include <vector>
19
20#include "../attributes.hpp"
23#include "../reactive/scope.hpp"
25#include "host.hpp"
26#include "route.hpp"
27#include "value.hpp"
28
29/// @file
30/// @brief The workspace's tab logic with no widgets (spec 6 §7, §15): what an application's own tab strip binds to,
31/// and what `ui::Workspace` draws as a tab bar.
32///
33/// Specified in `docs/spec/ui/workspace.md`, "Model".
34
35namespace morph::ui {
36
37/// @brief What the shell needs to know of one screen id: the catalog's `singleton` and `reuseEmpty`.
39 /// @brief The screen has at most one tab.
40 bool singleton = false;
41 /// @brief A tab of the screen with no identity and no unsaved changes is replaced, not stacked, when the screen
42 /// is opened with an identity.
43 bool reuseEmpty = false;
44 /// @brief The host screen the catalog entry names instead of a document; empty for a document screen.
45 std::string host;
46 /// @brief Whether the entry can be opened now; `resume` skips a recorded screen that cannot.
47 bool available = true;
48};
49
50/// @brief How a workspace starts (spec 6 §7, `app-start`).
52 /// @brief `resume` reopens the recorded tabs; anything else is the screen id to open.
53 std::string start = "resume";
54 /// @brief The screen opened when `resume` opens nothing; empty opens nothing.
55 std::string fallback;
56 /// @brief The record `resume` reads: the routes of the person's tabs when they were last written, in order.
57 std::vector<std::string> recorded;
58};
59
60/// @brief How a workspace treats screens.
62 /// @brief The catalog facts of a screen id; empty treats every screen as plain.
63 std::function<WorkspaceScreen(std::string_view)> screens;
64 /// @brief The host screens a catalog entry's `host` names. Borrowed; null mounts none.
65 ScreenHostRegistry const* hosts = nullptr;
66 /// @brief Receives the record: the routes of the open tabs, in order, each time the tabs change. A tab with no
67 /// route (a param with no route form) is left out. Empty writes no record.
68 std::function<void(std::vector<std::string> const&)> onRecord;
69 /// @brief When given, a burst of changes writes the record once, `recordDelay` after the last of them. Borrowed.
71 /// @brief The quiet period before the record is written, with a scheduler.
72 std::chrono::milliseconds recordDelay{500};
73 /// @brief Called after the open tabs, their titles, unsaved state or identities, or the active tab changed;
74 /// empty calls nothing.
75 std::function<void()> onChanged;
76};
77
78/// @brief A tab's identity in a `WorkspaceModel`: issued when it opens, never reused.
80 /// @brief The id's number; 0 is never issued.
81 std::uint64_t value = 0;
82
83 /// @brief Ordered by issue.
84 /// @return The ordering of the two values.
85 auto operator<=>(WorkspaceTabId const&) const = default;
86};
87
88/// @brief One open tab, as an application's tab strip binds to it.
90 /// @brief The tab.
92 /// @brief The screen id.
93 std::string screenId;
94 /// @brief The screen's identity; `null` when it has none.
96 /// @brief The screen's title.
97 std::string title;
98 /// @brief Whether the screen has unsaved changes.
99 bool dirty = false;
100
101 /// @brief Memberwise equality.
102 /// @return Whether every member matches.
103 bool operator==(WorkspaceTabState const&) const = default;
104};
105
106/// @brief A `WorkspaceModel`'s options: the workspace's, and how a tab's page is made and unmade.
108 /// @brief Makes a tab's page and mounts the screen in it, a document screen or a host screen alike; the model owns
109 /// the handle it answers. Required.
110 std::function<std::expected<std::unique_ptr<ScreenHandle>, MountError>(
111 WorkspaceTabId tab, std::string_view screenId, ParamMap const& params)>
113 /// @brief Called after a tab's handle is destroyed, so the application destroys its page; may be empty.
114 std::function<void(WorkspaceTabId tab)> unmountPage;
115};
116
117/// @brief The workspace's tabs with no widgets: which screens are open, in which order, which is active, and how
118/// they open, close and are recorded (spec 6 §7, §15).
119///
120/// Opening a screen whose identity an open tab already has focuses that tab, a singleton screen has one tab, and a
121/// `reuseEmpty` screen opened with an identity replaces its tab with no identity and no unsaved changes. Closing runs
122/// each screen's close protocol (spec 6 §3), one tab at a time, and a walk over several tabs stops at the first veto.
123/// `next` and `previous` walk the most recently used order. The page each tab shows is the application's:
124/// `mountPage` makes it and mounts the screen there, `unmountPage` is told when it goes. Neither copyable nor movable;
125/// owned by the owner thread of its runtime.
127public:
128 /// @brief An empty workspace.
129 /// @param runtime The runtime the screens' reactive nodes belong to. Borrowed.
130 /// @param host Answers identities and routes. Borrowed; it outlives the model.
131 /// @param options The screens' catalog facts, the record, and the page callbacks.
132 /// @throws std::invalid_argument when `mountPage` is empty.
133 WorkspaceModel(reactive::Runtime& runtime MORPH_LIFETIMEBOUND, DocumentScreenHost& host MORPH_LIFETIMEBOUND,
134 WorkspaceModelOptions options)
135 : _runtime{&runtime},
136 _host{&host},
137 _options{std::move(options)},
138 _changes{runtime, 0},
139 _activeTab{runtime, std::nullopt},
140 _tabStates{runtime, [this] { return tabStates(); }},
141 _activeState{runtime, [this] { return _activeTab.get(); }} {
142 if (!_options.mountPage) {
143 throw std::invalid_argument{"morph::ui::WorkspaceModel: mountPage is empty; it makes each tab's page"};
144 }
145 }
146
147 /// @brief Unmounts every screen without asking, newest first.
149 while (!_tabs.empty()) {
150 unmountLast();
151 }
152 }
153
154 WorkspaceModel(WorkspaceModel const&) = delete;
155 WorkspaceModel& operator=(WorkspaceModel const&) = delete;
156 WorkspaceModel(WorkspaceModel&&) = delete;
157 WorkspaceModel& operator=(WorkspaceModel&&) = delete;
158
159 /// @brief Opens a screen, or focuses the tab that already shows it; the tab opened or focused is the active one.
160 /// @param screenId The screen id.
161 /// @param params Its params.
162 /// @return The tab, or why the screen could not be mounted; a refused mount, or a `mountPage` that throws, opens
163 /// nothing.
164 std::expected<WorkspaceTabId, MountError> open(std::string_view screenId, ParamMap const& params) {
165 WorkspaceScreen const facts = factsOf(screenId);
166 std::optional<std::string> const identity = _host->identityOf(screenId, params);
167 for (auto const& tab : _tabs) {
168 if (tab->screenId == screenId && (facts.singleton || (identity && identityText(*tab) == *identity))) {
169 activateTab(tab->id, false);
170 return tab->id;
171 }
172 }
173 std::optional<WorkspaceTabId> replaced;
174 if (facts.reuseEmpty && identity) {
175 for (auto const& tab : _tabs) {
176 if (tab->screenId == screenId && tab->handle->identity().peek().isNull() &&
177 !tab->handle->dirty().peek()) {
178 replaced = tab->id;
179 break;
180 }
181 }
182 }
183 auto opened = mountTab(screenId, params);
184 if (!opened) {
185 return std::unexpected(std::move(opened.error()));
186 }
187 WorkspaceTabId const tabId = *opened;
188 if (replaced) {
189 moveTab(tabId, indexOf(*replaced));
190 removeTab(*replaced, true);
191 }
192 activateTab(tabId, false);
193 return tabId;
194 }
195
196 /// @brief Opens the start screens: with `resume`, every recorded route whose screen is known and available, in
197 /// order, and the fallback when none of them opened; otherwise the `start` screen. The first tab opened is
198 /// the active one.
199 /// @param how The start screen, the fallback and the record.
200 /// @return The number of tabs opened.
201 std::size_t start(WorkspaceStart const& how) {
202 std::size_t opened = 0;
203 std::optional<WorkspaceTabId> first;
204 auto const openOne = [&](std::string_view screenId, ParamMap const& params) {
205 if (auto const tab = open(screenId, params)) {
206 ++opened;
207 if (!first) {
208 first = *tab;
209 }
210 }
211 };
212 if (how.start == "resume") {
213 for (std::string const& route : how.recorded) {
214 std::optional<Route> const read = readRoute(route);
215 if (read && factsOf(read->screen).available) {
216 openOne(read->screen, read->params);
217 }
218 }
219 if (opened == 0 && !how.fallback.empty()) {
220 openOne(how.fallback, {});
221 }
222 } else {
223 openOne(how.start, {});
224 }
225 if (first && indexOf(*first) != kNone) {
226 activateTab(*first, false);
227 }
228 return opened;
229 }
230
231 /// @brief Shows a tab and makes it the most recently used.
232 /// @param tab The tab.
233 /// @throws std::out_of_range for a tab the model does not hold.
234 void activate(WorkspaceTabId tab) { activateTab(held(tab), false); }
235
236 /// @brief Shows the next tab in most-recently-used order (Ctrl+Tab). Repeated calls walk the order as it was at
237 /// the first of them; any other activation starts a new walk.
238 void next() { walk(1); }
239
240 /// @brief Shows the previous tab in most-recently-used order (Ctrl+Shift+Tab).
241 void previous() { walk(-1); }
242
243 /// @brief Closes a tab through its screen's close protocol.
244 /// @param tab The tab.
245 /// @param done Called once with whether it closed; may be empty.
246 /// @throws std::out_of_range for a tab the model does not hold.
247 void close(WorkspaceTabId tab, std::function<void(bool)> done) { closeEach({held(tab)}, std::move(done)); }
248
249 /// @brief Closes every tab but one, one at a time, stopping at the first veto.
250 /// @param tab The tab kept.
251 /// @param done Called once with whether every other tab closed; may be empty.
252 /// @throws std::out_of_range for a tab the model does not hold.
253 void closeOthers(WorkspaceTabId tab, std::function<void(bool)> done) {
254 WorkspaceTabId const kept = held(tab);
255 std::vector<WorkspaceTabId> ids;
256 ids.reserve(_tabs.size());
257 for (auto const& open : _tabs) {
258 if (open->id != kept) {
259 ids.push_back(open->id);
260 }
261 }
262 closeEach(std::move(ids), std::move(done));
263 }
264
265 /// @brief Closes every tab, one at a time, stopping at the first veto.
266 /// @param done Called once with whether every tab closed; may be empty.
267 void closeAll(std::function<void(bool)> done) { closeEach(order(), std::move(done)); }
268
269 /// @brief Closes every tab without unsaved changes, one at a time, stopping at the first veto.
270 /// @param done Called once with whether each of them closed; may be empty.
271 void closeSaved(std::function<void(bool)> done) {
272 std::vector<WorkspaceTabId> ids;
273 ids.reserve(_tabs.size());
274 for (auto const& tab : _tabs) {
275 if (!tab->handle->dirty().peek()) {
276 ids.push_back(tab->id);
277 }
278 }
279 closeEach(std::move(ids), std::move(done));
280 }
281
282 /// @brief Closes every tab without asking, newest first: what signing out does, since no close guard may keep a
283 /// screen of the principal who is leaving.
285 while (!_tabs.empty()) {
286 removeTab(_tabs.back()->id, true);
287 }
288 }
289
290 /// @brief Writes the record now, cancelling a pending debounced write: what sign-out does before the session is
291 /// cleared.
292 void flushRecord() {
293 _recordTimer.cancel();
294 if (_options.onRecord) {
295 _options.onRecord(record());
296 }
297 }
298
299 /// @brief The record as it would be written now: the routes of the open tabs, in order.
300 /// @return The routes.
301 [[nodiscard]] std::vector<std::string> record() const {
302 std::vector<std::string> routes;
303 routes.reserve(_tabs.size());
304 for (auto const& tab : _tabs) {
305 if (!tab->route.empty()) {
306 routes.push_back(tab->route);
307 }
308 }
309 return routes;
310 }
311
312 /// @brief The open tabs, in order, each with its screen, identity, title and unsaved state. A tab's values follow
313 /// its handle.
314 /// @return The tabs, equality-gated: an Effect over it runs once per change.
315 [[nodiscard]] reactive::Computed<std::vector<WorkspaceTabState>> const& tabs() const { return _tabStates; }
316
317 /// @brief The active tab.
318 /// @return It, or `std::nullopt` while no tab is open.
319 [[nodiscard]] reactive::Computed<std::optional<WorkspaceTabId>> const& active() const { return _activeState; }
320
321 /// @brief The open tabs' ids, in order, read without tracking: the order `tabs()` lists them in.
322 /// @return The ids.
323 [[nodiscard]] std::vector<WorkspaceTabId> order() const {
324 std::vector<WorkspaceTabId> ids;
325 ids.reserve(_tabs.size());
326 for (auto const& tab : _tabs) {
327 ids.push_back(tab->id);
328 }
329 return ids;
330 }
331
332 /// @brief A tab's screen handle: what a client remounts for development reload and inspects.
333 /// @param tab The tab.
334 /// @return Its handle, valid while the tab is open; null for a tab the model does not hold.
335 [[nodiscard]] ScreenHandle* handleOf(WorkspaceTabId tab) const noexcept {
336 std::size_t const where = indexOf(tab);
337 return where == kNone ? nullptr : _tabs.at(where)->handle.get();
338 }
339
340private:
341 static constexpr std::size_t kNone = static_cast<std::size_t>(-1);
342
343 // One tab: the effect over its handle, then its handle, destroyed in that order.
344 struct Tab {
346 std::string screenId;
347 // The route the record lists; empty for an instance with none.
348 std::string route;
349 std::unique_ptr<ScreenHandle> handle;
350 std::unique_ptr<reactive::Scope> scope;
351
352 Tab() = default;
353 Tab(Tab const&) = delete;
354 Tab& operator=(Tab const&) = delete;
355 Tab(Tab&&) = delete;
356 Tab& operator=(Tab&&) = delete;
357 ~Tab() {
358 scope.reset();
359 handle.reset();
360 }
361 };
362
363 struct Walk {
364 std::vector<WorkspaceTabId> order;
365 std::size_t position = 0;
366 };
367
368 [[nodiscard]] static std::string identityText(Tab const& tab) {
369 return identityKey(tab.handle->identity().peek()).value_or(std::string{});
370 }
371
372 [[nodiscard]] std::vector<WorkspaceTabState> tabStates() const {
373 static_cast<void>(_changes.get());
374 std::vector<WorkspaceTabState> states;
375 states.reserve(_tabs.size());
376 for (auto const& tab : _tabs) {
377 states.push_back(WorkspaceTabState{.id = tab->id,
378 .screenId = tab->screenId,
379 .identity = tab->handle->identity().peek(),
380 .title = tab->handle->title().peek(),
381 .dirty = tab->handle->dirty().peek()});
382 }
383 return states;
384 }
385
386 [[nodiscard]] std::size_t indexOf(WorkspaceTabId tab) const noexcept {
387 auto const found = std::ranges::find_if(_tabs, [tab](auto const& open) { return open->id == tab; });
388 return found == _tabs.end() ? kNone : static_cast<std::size_t>(found - _tabs.begin());
389 }
390
391 // A tab the caller names must be one the model holds.
392 [[nodiscard]] WorkspaceTabId held(WorkspaceTabId tab) const {
393 if (indexOf(tab) == kNone) {
394 throw std::out_of_range{"morph::ui::WorkspaceModel: tab " + std::to_string(tab.value) +
395 " is not open (never issued, or closed)"};
396 }
397 return tab;
398 }
399
400 [[nodiscard]] WorkspaceScreen factsOf(std::string_view screenId) const {
401 return _options.screens ? _options.screens(screenId) : WorkspaceScreen{};
402 }
403
404 // A recorded route read back: a host screen's route is its id, a document screen's is read against its params.
405 [[nodiscard]] std::optional<Route> readRoute(std::string const& route) const {
406 if (!factsOf(route).host.empty()) {
407 return Route{.screen = route, .params = {}};
408 }
409 auto read = _host->parseRoute(route);
410 if (!read) {
411 return std::nullopt;
412 }
413 return std::move(*read);
414 }
415
416 // Asks for a new page at the end and mounts the screen in it; a refusal, or a throw, opens nothing.
417 std::expected<WorkspaceTabId, MountError> mountTab(std::string_view screenId, ParamMap const& params) {
418 WorkspaceTabId const tabId{++_lastId};
419 std::expected<std::unique_ptr<ScreenHandle>, MountError> handle;
420 try {
421 handle = _options.mountPage(tabId, screenId, params);
422 } catch (std::exception const& failure) {
423 handle = std::unexpected(MountError{.screenId = std::string{screenId}, .reason = failure.what()});
424 } catch (...) {
425 handle = std::unexpected(MountError{.screenId = std::string{screenId}, .reason = "mountPage threw"});
426 }
427 if (handle && *handle == nullptr) {
428 handle = std::unexpected(
429 MountError{.screenId = std::string{screenId}, .reason = "mountPage answered no handle"});
430 }
431 if (!handle) {
432 return std::unexpected(std::move(handle.error()));
433 }
434 auto tab = std::make_unique<Tab>();
435 tab->id = tabId;
436 tab->screenId = std::string{screenId};
437 if (!factsOf(screenId).host.empty()) {
438 tab->route = std::string{screenId};
439 } else if (auto route = _host->routeOf(screenId, params)) {
440 tab->route = std::move(*route);
441 }
442 tab->handle = std::move(*handle);
443 tab->scope = std::make_unique<reactive::Scope>(*_runtime);
444 ScreenHandle const* const screen = tab->handle.get();
445 tab->scope->effect([this, screen] {
446 static_cast<void>(screen->title().get());
447 static_cast<void>(screen->dirty().get());
448 static_cast<void>(screen->identity().get());
449 _runtime->untracked([this] { changed(); });
450 });
451 _tabs.push_back(std::move(tab));
452 changed();
453 recordChanged();
454 return tabId;
455 }
456
457 // The open tabs changed: the record is written now, or once the burst is over.
458 void recordChanged() {
459 if (!_options.onRecord) {
460 return;
461 }
462 if (_options.scheduler == nullptr) {
463 _options.onRecord(record());
464 return;
465 }
466 std::weak_ptr<bool> const alive = _alive;
467 _recordTimer = _options.scheduler->after(_options.recordDelay, [this, alive] {
468 if (!alive.expired()) {
469 flushRecord();
470 }
471 });
472 }
473
474 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): a tab, then the index it moves to.
475 void moveTab(WorkspaceTabId tabId, std::size_t target) {
476 std::size_t const from = indexOf(tabId);
477 if (from == kNone || from == target) {
478 return;
479 }
480 std::unique_ptr<Tab> tab = std::move(_tabs.at(from));
481 _tabs.erase(_tabs.begin() + static_cast<std::ptrdiff_t>(from));
482 _tabs.insert(_tabs.begin() + static_cast<std::ptrdiff_t>(target), std::move(tab));
483 changed();
484 }
485
486 // Destroys the last tab's handle, then tells the application its page can go.
487 void unmountLast() {
488 WorkspaceTabId const tabId = _tabs.back()->id;
489 _tabs.pop_back();
490 if (_options.unmountPage) {
491 _options.unmountPage(tabId);
492 }
493 }
494
495 void removeTab(WorkspaceTabId tabId, bool force) {
496 std::size_t const where = indexOf(tabId);
497 if (where == kNone) {
498 return;
499 }
500 if (force) {
501 _tabs.at(where)->handle->forceClose();
502 }
503 _tabs.erase(_tabs.begin() + static_cast<std::ptrdiff_t>(where));
504 if (_options.unmountPage) {
505 _options.unmountPage(tabId);
506 }
507 std::erase(_recent, tabId);
508 _walk.reset();
509 recordChanged();
510 if (_active == tabId) {
511 setActive(std::nullopt);
512 if (!_recent.empty()) {
513 activateTab(_recent.front(), false);
514 return;
515 }
516 }
517 changed();
518 }
519
520 void activateTab(WorkspaceTabId tabId, bool walking) {
521 if (!walking) {
522 _walk.reset();
523 }
524 setActive(tabId);
525 std::erase(_recent, tabId);
526 _recent.insert(_recent.begin(), tabId);
527 changed();
528 }
529
530 void walk(int step) {
531 if (_tabs.size() < 2 || !_active) {
532 return;
533 }
534 if (!_walk) {
535 _walk = Walk{.order = _recent, .position = 0};
536 }
537 auto const count = static_cast<std::ptrdiff_t>(_walk->order.size());
538 std::ptrdiff_t const next = (((static_cast<std::ptrdiff_t>(_walk->position) + step) % count) + count) % count;
539 _walk->position = static_cast<std::size_t>(next);
540 // A walk keeps its own order; the tab it shows moves to the front of the recent list.
541 activateTab(_walk->order.at(_walk->position), true);
542 }
543
544 // Closes the tabs one at a time, stopping at the first that stays open.
545 void closeEach(std::vector<WorkspaceTabId> ids, std::function<void(bool)> done) {
546 std::erase_if(ids, [this](WorkspaceTabId tabId) { return indexOf(tabId) == kNone; });
547 if (ids.empty()) {
548 if (done) {
549 done(true);
550 }
551 return;
552 }
553 WorkspaceTabId const tabId = ids.front();
554 ids.erase(ids.begin());
555 std::weak_ptr<bool> const alive = _alive;
556 _tabs.at(indexOf(tabId))
557 ->handle->requestClose(
558 [this, alive, tabId, rest = std::move(ids), done = std::move(done)](bool closed) mutable {
559 if (alive.expired()) {
560 return;
561 }
562 if (!closed) {
563 if (done) {
564 done(false);
565 }
566 return;
567 }
568 removeTab(tabId, false);
569 closeEach(std::move(rest), std::move(done));
570 });
571 }
572
573 void setActive(std::optional<WorkspaceTabId> tab) {
574 _active = tab;
575 _runtime->batch([this, tab] { _activeTab.set(tab); });
576 }
577
578 // Something a tab strip shows changed: `tabs()` follows, and the application is told.
579 void changed() {
580 _runtime->batch([this] { _changes.set(_changes.peek() + 1); });
581 if (_options.onChanged) {
582 _options.onChanged();
583 }
584 }
585
586 reactive::Runtime* _runtime;
587 DocumentScreenHost* _host;
588 WorkspaceModelOptions _options;
589 std::vector<std::unique_ptr<Tab>> _tabs;
590 std::vector<WorkspaceTabId> _recent;
591 std::optional<WorkspaceTabId> _active;
592 std::optional<Walk> _walk;
593 std::uint64_t _lastId = 0;
594 reactive::TimerHandle _recordTimer;
595 std::shared_ptr<bool> _alive = std::make_shared<bool>(true);
596 reactive::Signal<std::uint64_t> _changes;
597 reactive::Signal<std::optional<WorkspaceTabId>> _activeTab;
598 reactive::Computed<std::vector<WorkspaceTabState>> _tabStates;
599 reactive::Computed<std::optional<WorkspaceTabId>> _activeState;
600};
601
602} // namespace morph::ui
Screen screen(Screen::Builder builder)
A screen with its id as a template argument, as spec 5 §11 writes it.
Definition author.hpp:4556
Command host(std::string const &name, AnyHandle const &args={})
host: a command the application registered with the client.
Definition author.hpp:2870
Command open(Element const &dialog)
open: opens a dialog or drawer.
Definition author.hpp:2779
Handle< std::int64_t > count(AnyHandle const &list)
count: a list's length.
Definition author.hpp:1000
A derived value: computed lazily on first read, cached, and recomputed only after a source it read re...
Definition signal.hpp:168
The owner of a signal graph: every node takes one, and every operation runs on its owner.
Definition runtime.hpp:70
Runs callbacks after a delay or on a period, on the runtime's owner executor.
The framework's ScreenHost over UI documents: it mounts them through an Interpreter,...
Definition host.hpp:344
One mounted screen, as its host sees it.
Definition host.hpp:82
The host screens an application registers by name: what a catalog entry's host names instead of a doc...
Definition host.hpp:180
bool dirty() const
Evaluates the document's dirty.
Value identity() const
Evaluates the document's identity.
Value title() const
Evaluates the document's title.
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,...
~WorkspaceModel()
Unmounts every screen without asking, newest first.
ScreenHandle * handleOf(WorkspaceTabId tab) const noexcept
A tab's screen handle: what a client remounts for development reload and inspects.
std::vector< WorkspaceTabId > order() const
The open tabs' ids, in order, read without tracking: the order tabs() lists them in.
void activate(WorkspaceTabId tab)
Shows a tab and makes it the most recently used.
WorkspaceModel(reactive::Runtime &runtime, DocumentScreenHost &host, WorkspaceModelOptions options)
An empty workspace.
void flushRecord()
Writes the record now, cancelling a pending debounced write: what sign-out does before the session is...
void forceCloseAll()
Closes every tab without asking, newest first: what signing out does, since no close guard may keep a...
void next()
Shows the next tab in most-recently-used order (Ctrl+Tab). Repeated calls walk the order as it was at...
std::size_t start(WorkspaceStart const &how)
Opens the start screens: with resume, every recorded route whose screen is known and available,...
std::expected< WorkspaceTabId, MountError > open(std::string_view screenId, ParamMap const &params)
Opens a screen, or focuses the tab that already shows it; the tab opened or focused is the active one...
reactive::Computed< std::vector< WorkspaceTabState > > const & tabs() const
The open tabs, in order, each with its screen, identity, title and unsaved state. A tab's values foll...
void closeOthers(WorkspaceTabId tab, std::function< void(bool)> done)
Closes every tab but one, one at a time, stopping at the first veto.
void closeSaved(std::function< void(bool)> done)
Closes every tab without unsaved changes, one at a time, stopping at the first veto.
reactive::Computed< std::optional< WorkspaceTabId > > const & active() const
The active tab.
std::vector< std::string > record() const
The record as it would be written now: the routes of the open tabs, in order.
void close(WorkspaceTabId tab, std::function< void(bool)> done)
Closes a tab through its screen's close protocol.
void closeAll(std::function< void(bool)> done)
Closes every tab, one at a time, stopping at the first veto.
void previous()
Shows the previous tab in most-recently-used order (Ctrl+Shift+Tab).
Value walk(Value const &value, std::span< std::string const > path)
Reads a path out of a value: a record's field by name, a list's element by index.
The host contract (spec 6 §4): how a host mounts UI documents and closes them, whether it owns its ow...
std::optional< std::string > identityKey(Value const &identity)
The canonical text of a screen's identity: what DocumentScreenHost::identityOf answers,...
Definition host.hpp:322
ValueRecord ParamMap
A screen's params, by name.
Definition host.hpp:38
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.
Signal<T>, Computed<T> and Effect: the nodes of a reactive graph.
Why a screen could not be mounted.
Definition host.hpp:41
A route read back: the screen and its params.
Definition route.hpp:32
One page of Tabs.
Definition view.hpp:674
A WorkspaceModel's options: the workspace's, and how a tab's page is made and unmade.
std::function< void(WorkspaceTabId tab)> unmountPage
Called after a tab's handle is destroyed, so the application destroys its page; may be empty.
std::function< std::expected< std::unique_ptr< ScreenHandle >, MountError >(WorkspaceTabId tab, std::string_view screenId, ParamMap const &params)> mountPage
Makes a tab's page and mounts the screen in it, a document screen or a host screen alike; the model o...
How a workspace treats screens.
std::function< void()> onChanged
Called after the open tabs, their titles, unsaved state or identities, or the active tab changed; emp...
std::function< WorkspaceScreen(std::string_view)> screens
The catalog facts of a screen id; empty treats every screen as plain.
reactive::Scheduler * scheduler
When given, a burst of changes writes the record once, recordDelay after the last of them....
std::function< void(std::vector< std::string > const &)> onRecord
Receives the record: the routes of the open tabs, in order, each time the tabs change....
ScreenHostRegistry const * hosts
The host screens a catalog entry's host names. Borrowed; null mounts none.
std::chrono::milliseconds recordDelay
The quiet period before the record is written, with a scheduler.
What the shell needs to know of one screen id: the catalog's singleton and reuseEmpty.
bool singleton
The screen has at most one tab.
std::string host
The host screen the catalog entry names instead of a document; empty for a document screen.
bool available
Whether the entry can be opened now; resume skips a recorded screen that cannot.
bool reuseEmpty
A tab of the screen with no identity and no unsaved changes is replaced, not stacked,...
How a workspace starts (spec 6 §7, app-start).
std::string start
resume reopens the recorded tabs; anything else is the screen id to open.
std::string fallback
The screen opened when resume opens nothing; empty opens nothing.
std::vector< std::string > recorded
The record resume reads: the routes of the person's tabs when they were last written,...
A tab's identity in a WorkspaceModel: issued when it opens, never reused.
std::uint64_t value
The id's number; 0 is never issued.
auto operator<=>(WorkspaceTabId const &) const =default
Ordered by issue.
One open tab, as an application's tab strip binds to it.
bool dirty
Whether the screen has unsaved changes.
WorkspaceTabId id
The tab.
std::string screenId
The screen id.
Value identity
The screen's identity; null when it has none.
std::string title
The screen's title.
bool operator==(WorkspaceTabState const &) const =default
Memberwise equality.
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...