79 _bar{backend.createTabs(parent)},
80 _hosts{options.hosts},
81 _screens{options.screens},
82 _onChanged{std::move(options.onChanged)},
83 _headPlacement{headPlacement} {
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));
94 _bar->setKeys({
"Ctrl+Tab",
"Ctrl+Shift+Tab",
"Ctrl+W"}, [
this](std::string
const& chord) {
95 if (chord ==
"Ctrl+Tab") {
97 }
else if (chord ==
"Ctrl+Shift+Tab") {
99 }
else if (std::optional<std::size_t>
const shown =
active()) {
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);
108 modelOptions.unmountPage = [
this](WorkspaceTabId tab) {
109 std::erase(_shownOrder, tab);
110 _pages.erase(tab.value);
112 _model = std::make_unique<WorkspaceModel>(runtime, host, std::move(modelOptions));
135 std::expected<std::size_t, MountError>
open(std::string_view screenId,
ParamMap const& params) {
136 auto opened = _model->open(screenId, params);
138 return std::unexpected(std::move(opened.error()));
140 return indexOf(*opened);
154 [[nodiscard]] std::vector<std::string>
record()
const {
return _model->record(); }
159 if (std::optional<WorkspaceTabId>
const tab = tabAt(index)) {
160 _model->activate(*tab);
166 void next() { _model->next(); }
174 void close(std::size_t index, std::function<
void(
bool)> done) {
175 std::optional<WorkspaceTabId>
const tab = tabAt(index);
182 _model->close(*tab, std::move(done));
188 void closeOthers(std::size_t index, std::function<
void(
bool)> done) {
189 std::optional<WorkspaceTabId>
const tab = tabAt(index);
191 _model->closeAll(std::move(done));
194 _model->closeOthers(*tab, std::move(done));
199 void closeAll(std::function<
void(
bool)> done) { _model->closeAll(std::move(done)); }
203 void closeSaved(std::function<
void(
bool)> done) { _model->closeSaved(std::move(done)); }
207 [[nodiscard]] std::optional<std::size_t>
active()
const {
208 std::optional<WorkspaceTabId>
const tab = _model->active().peek();
212 return indexOf(*tab);
219 std::optional<WorkspaceTabId>
const tab = tabAt(index);
220 return tab ? _model->handleOf(*tab) :
nullptr;
226 std::optional<std::size_t>
const shown =
active();
227 return shown ?
handleOf(*shown) :
nullptr;
236 std::optional<WorkspaceTabId>
const tab = _model->active().peek();
238 return ui::row(buildHeadRow(*tab));
240 Row empty{.children = {}, .gap = 0, .common = {}, .tone = std::nullopt};
243 return ui::row(std::move(empty));
252 [[nodiscard]] std::vector<WorkspaceTab>
tabs()
const {
253 std::vector<WorkspaceTab> out;
256 out.push_back(
WorkspaceTab{.screenId = std::string{screenOf(tab)},
257 .identity = handle.
identity().peek(),
258 .caption = captionOf(handle)});
264 static constexpr std::size_t kNone =
static_cast<std::size_t
>(-1);
269 std::string screenId;
270 std::unique_ptr<SlotWidget>
slot;
272 std::optional<Mounted> head;
275 Page(Page
const&) =
delete;
276 Page& operator=(Page
const&) =
delete;
277 Page(Page&&) =
delete;
278 Page& operator=(Page&&) =
delete;
287 std::size_t index = 0;
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;
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());
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};
307 [[nodiscard]]
static std::string captionOf(ScreenHandle
const& handle) {
308 std::string caption = handle.title().peek();
309 if (handle.dirty().peek()) {
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());
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);
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"});
335 handle = (*factory)(params, *page->slot);
340 if (_headPlacement == HeadPlacement::InPage && (*handle)->head() !=
nullptr) {
342 page->head.emplace(*_runtime, *_backend, headRow(tab), page->slot.get());
343 page->slot->moveChild(page->head->root(), 0);
345 _pages.insert_or_assign(tab.value, std::move(page));
346 _shownOrder.push_back(tab);
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();
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();
368 auto const status = [
this, tab] {
369 ScreenHead
const*
const head = headOf(tab);
370 return head ==
nullptr ? std::vector<HeadBadge>{} : head->status().get();
372 auto const actions = [
this, tab] {
373 ScreenHead
const*
const head = headOf(tab);
374 return head ==
nullptr ? std::vector<HeadAction>{} : head->actions().get();
376 Row row{.children = {}, .gap = 2, .common = {}, .tone = std::nullopt};
377 row.common.testId =
"head";
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 = {}};
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>(
388 std::vector<PlacedBadge> placed;
389 for (HeadBadge
const& badge : status()) {
390 placed.push_back(PlacedBadge{.index = placed.size(), .badge =
badge});
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,
400 badge.common.
testId =
"head_status_" + std::to_string(placed.peek().index);
401 return ui::badge(std::move(badge));
403 Axis::Horizontal, 1));
406 filler.common.testId =
"head_spacer";
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,
415 [
this, tab, actionId = action.peek().id] {
416 ScreenHandle*
const handle = _model->handleOf(tab);
417 if (ScreenHead*
const head = handle ==
nullptr ?
nullptr : handle->head()) {
423 button.common.
enabled = Prop<bool>{[&action] {
return action.get().enabled; }};
424 return ui::button(std::move(button));
426 Axis::Horizontal, 1));
431 [[nodiscard]]
Node headRow(WorkspaceTabId tab)
const {
return ui::row(buildHeadRow(tab)); }
435 if (_model ==
nullptr) {
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()) {
447 Page& page = *found->second;
450 if (index >= _shownOrder.size() || _shownOrder.at(index) != order.at(index)) {
451 std::erase(_shownOrder, order.at(index));
453 _shownOrder.begin() +
static_cast<std::ptrdiff_t
>(std::min(index, _shownOrder.size())),
455 _bar->moveChild(*page.slot, index);
457 page.slot->setVisible(shown == order.at(index));
458 captions.push_back(captionOf(*_model->handleOf(order.at(index))));
460 _bar->setTabs(captions);
461 _bar->setSelected(shown ? indexOf(*shown) : kNone);
463 _bar->setBarShown(captions.size() > 1);
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;
477 std::map<std::uint64_t, std::unique_ptr<Page>> _pages;
479 std::vector<WorkspaceTabId> _shownOrder;
480 std::unique_ptr<WorkspaceModel> _model;
Node text(TextOptions const &options)
A text node.
Node button(ButtonOptions const &options)
A button node.
Handle< T > row()
The innermost row, {"row": ""}.
Node badge(AnyHandle const &text, AnyHandle const &tone={}, AnyHandle const &icon={})
A badge node.
The owner of a signal graph: every node takes one, and every operation runs on its owner.
The framework's ScreenHost over UI documents: it mounts them through an Interpreter,...
Makes widgets. Each factory appends the new widget to parent, or makes a root when it is null,...
One mounted screen, as its host sees it.
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...
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,...
Tabs over screens: a tab bar over a WorkspaceModel, one screen instance per tab, mounted into the tab...
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 ¶ms)
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.
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.
Node visible(AnyHandle const &condition) const
Whether the node shows.
Node id(Element const &element) const
The element id, by which relations read its values.
Node enabled(AnyHandle const &condition) const
Whether the node is enabled.
@ 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.
std::function< std::expected< std::unique_ptr< ScreenHandle >, MountError >( ParamMap const ¶ms, ContainerWidget &page)> HostScreenFactory
Makes a host screen (spec 6 §15): the application's native screen, mounted into a shell's page with p...
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.
Prop< bool > visible
Whether the widget is shown.
Children laid out left to right.
Common common
Visibility, enablement, layout, drag-and-drop.
static constexpr Sizing stretch(int weight=1) noexcept
A share of the leftover space.
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.
Value identity
The screen's identity; null when it has none.
std::string screenId
The screen id.
std::string caption
The caption the bar shows: the title, with • while there are unsaved changes.
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.
Node row(Row spec)
Children laid out left to right.
std::variant< std::int64_t, std::string > Key
The identity of a Switch case, a Select option, a ForEach or Table row and a drag payload.
SlotBinding< std::decay_t< F > > slot(SlotId slotId, F &&read)
A slot with an id.
HeadPlacement
Where a workspace draws the active screen's head row.
@ External
Nowhere: the host mounts headRowForActive() where it chooses, and a page holds only its screen.
@ InPage
Above each page, inside the workspace.
The workspace's tab logic with no widgets (spec 6 §7, §15): what an application's own tab strip binds...