20#include "../attributes.hpp"
68 std::function<void(std::vector<std::string>
const&)>
onRecord;
110 std::function<std::expected<std::unique_ptr<ScreenHandle>,
MountError>(
135 : _runtime{&runtime},
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"};
149 while (!_tabs.empty()) {
164 std::expected<WorkspaceTabId, MountError>
open(std::string_view screenId,
ParamMap const& params) {
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);
173 std::optional<WorkspaceTabId> replaced;
175 for (
auto const& tab : _tabs) {
176 if (tab->screenId == screenId && tab->handle->identity().peek().isNull() &&
177 !tab->handle->dirty().peek()) {
183 auto opened = mountTab(screenId, params);
185 return std::unexpected(std::move(opened.error()));
189 moveTab(tabId, indexOf(*replaced));
190 removeTab(*replaced,
true);
192 activateTab(tabId,
false);
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)) {
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);
219 if (opened == 0 && !how.
fallback.empty()) {
223 openOne(how.
start, {});
225 if (first && indexOf(*first) != kNone) {
226 activateTab(*first,
false);
247 void close(
WorkspaceTabId tab, std::function<
void(
bool)> done) { closeEach({held(tab)}, std::move(done)); }
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);
262 closeEach(std::move(ids), std::move(done));
267 void closeAll(std::function<
void(
bool)> done) { closeEach(
order(), std::move(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);
279 closeEach(std::move(ids), std::move(done));
285 while (!_tabs.empty()) {
286 removeTab(_tabs.back()->id,
true);
293 _recordTimer.cancel();
294 if (_options.onRecord) {
295 _options.onRecord(
record());
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);
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);
336 std::size_t
const where = indexOf(tab);
337 return where == kNone ? nullptr : _tabs.at(where)->handle.get();
341 static constexpr std::size_t kNone =
static_cast<std::size_t
>(-1);
346 std::string screenId;
349 std::unique_ptr<ScreenHandle> handle;
350 std::unique_ptr<reactive::Scope> scope;
354 Tab& operator=(
Tab const&) =
delete;
356 Tab& operator=(
Tab&&) =
delete;
364 std::vector<WorkspaceTabId> order;
365 std::size_t position = 0;
368 [[nodiscard]]
static std::string identityText(
Tab const& tab) {
369 return identityKey(tab.handle->identity().peek()).value_or(std::string{});
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) {
378 .screenId = tab->screenId,
379 .identity = tab->handle->identity().peek(),
380 .title = tab->handle->title().peek(),
381 .dirty = tab->handle->dirty().peek()});
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());
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)"};
400 [[nodiscard]]
WorkspaceScreen factsOf(std::string_view screenId)
const {
401 return _options.screens ? _options.screens(screenId) :
WorkspaceScreen{};
405 [[nodiscard]] std::optional<Route> readRoute(std::string
const& route)
const {
406 if (!factsOf(route).
host.empty()) {
407 return Route{.screen = route, .params = {}};
409 auto read = _host->parseRoute(route);
413 return std::move(*read);
417 std::expected<WorkspaceTabId, MountError> mountTab(std::string_view screenId,
ParamMap const& params) {
419 std::expected<std::unique_ptr<ScreenHandle>,
MountError> handle;
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()});
425 handle = std::unexpected(
MountError{.screenId = std::string{screenId}, .reason =
"mountPage threw"});
427 if (handle && *handle ==
nullptr) {
428 handle = std::unexpected(
429 MountError{.screenId = std::string{screenId}, .reason =
"mountPage answered no handle"});
432 return std::unexpected(std::move(handle.error()));
434 auto tab = std::make_unique<Tab>();
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);
442 tab->handle = std::move(*handle);
443 tab->scope = std::make_unique<reactive::Scope>(*_runtime);
445 tab->scope->effect([
this, screen] {
449 _runtime->untracked([
this] { changed(); });
451 _tabs.push_back(std::move(tab));
458 void recordChanged() {
459 if (!_options.onRecord) {
462 if (_options.scheduler ==
nullptr) {
463 _options.onRecord(record());
466 std::weak_ptr<bool>
const alive = _alive;
467 _recordTimer = _options.scheduler->after(_options.recordDelay, [
this, alive] {
468 if (!alive.expired()) {
475 void moveTab(WorkspaceTabId tabId, std::size_t target) {
476 std::size_t
const from = indexOf(tabId);
477 if (from == kNone || from == target) {
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));
488 WorkspaceTabId
const tabId = _tabs.back()->id;
490 if (_options.unmountPage) {
491 _options.unmountPage(tabId);
495 void removeTab(WorkspaceTabId tabId,
bool force) {
496 std::size_t
const where = indexOf(tabId);
497 if (where == kNone) {
501 _tabs.at(where)->handle->forceClose();
503 _tabs.erase(_tabs.begin() +
static_cast<std::ptrdiff_t
>(where));
504 if (_options.unmountPage) {
505 _options.unmountPage(tabId);
507 std::erase(_recent, tabId);
510 if (_active == tabId) {
511 setActive(std::nullopt);
512 if (!_recent.empty()) {
513 activateTab(_recent.front(),
false);
520 void activateTab(WorkspaceTabId tabId,
bool walking) {
525 std::erase(_recent, tabId);
526 _recent.insert(_recent.begin(), tabId);
530 void walk(
int step) {
531 if (_tabs.size() < 2 || !_active) {
535 _walk = Walk{.order = _recent, .position = 0};
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);
541 activateTab(_walk->order.at(_walk->position),
true);
545 void closeEach(std::vector<WorkspaceTabId> ids, std::function<
void(
bool)> done) {
546 std::erase_if(ids, [
this](WorkspaceTabId tabId) {
return indexOf(tabId) == kNone; });
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()) {
568 removeTab(tabId,
false);
569 closeEach(std::move(rest), std::move(done));
573 void setActive(std::optional<WorkspaceTabId> tab) {
575 _runtime->batch([
this, tab] { _activeTab.set(tab); });
580 _runtime->batch([
this] { _changes.set(_changes.peek() + 1); });
581 if (_options.onChanged) {
582 _options.onChanged();
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;
Screen screen(Screen::Builder builder)
A screen with its id as a template argument, as spec 5 §11 writes it.
Command host(std::string const &name, AnyHandle const &args={})
host: a command the application registered with the client.
Command open(Element const &dialog)
open: opens a dialog or drawer.
Handle< std::int64_t > count(AnyHandle const &list)
count: a list's length.
A derived value: computed lazily on first read, cached, and recomputed only after a source it read re...
The owner of a signal graph: every node takes one, and every operation runs on its owner.
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,...
One mounted screen, as its host sees it.
The host screens an application registers by name: what a catalog entry's host names instead of a doc...
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.
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 ¶ms)
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,...
ValueRecord ParamMap
A screen's params, by name.
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.
A route read back: the screen and its params.
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 ¶ms)> 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,...