morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
host.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include <algorithm>
5#include <cstdint>
6#include <expected>
7#include <functional>
8#include <map>
9#include <memory>
10#include <optional>
11#include <ranges>
12#include <span>
13#include <stdexcept>
14#include <string>
15#include <string_view>
16#include <utility>
17#include <vector>
18
19#include "../core/executor.hpp"
22#include "document.hpp"
23#include "expression.hpp"
24#include "inspector.hpp"
25#include "interpreter.hpp"
26#include "route.hpp"
27#include "value.hpp"
28
29/// @file
30/// @brief The host contract (spec 6 §4): how a host mounts UI documents and closes them, whether it owns its own
31/// shell or is the framework's workspace shell, and `DocumentScreenHost`, the framework's host over documents.
32///
33/// Specified in `docs/spec/ui/host.md`.
34
35namespace morph::ui {
36
37/// @brief A screen's params, by name.
39
40/// @brief Why a screen could not be mounted.
41struct MountError {
42 /// @brief The screen id asked for.
43 std::string screenId;
44 /// @brief Why: an unknown screen, a refused param, a refused mount.
45 std::string reason;
46};
47
48/// @brief A screen's head beside its title: the subtitle, the status badges and the actions a shell shows above the
49/// screen (the document's `subtitle`, `status` and `actions`).
50class ScreenHead {
51public:
52 ScreenHead() = default;
53 ScreenHead(ScreenHead const&) = delete;
54 ScreenHead& operator=(ScreenHead const&) = delete;
55 ScreenHead(ScreenHead&&) = delete;
56 ScreenHead& operator=(ScreenHead&&) = delete;
57 virtual ~ScreenHead() = default;
58
59 /// @brief The subtitle, as text; empty when the screen declares none.
60 /// @return It; it keeps its last value once the screen is closed.
61 [[nodiscard]] virtual reactive::Computed<std::string> const& subtitle() const = 0;
62
63 /// @brief The status badges that are visible, in order.
64 /// @return They; empty once the screen is closed.
65 [[nodiscard]] virtual reactive::Computed<std::vector<HeadBadge>> const& status() const = 0;
66
67 /// @brief The actions that are visible, in order, each with whether it is enabled.
68 /// @return They; empty once the screen is closed.
69 [[nodiscard]] virtual reactive::Computed<std::vector<HeadAction>> const& actions() const = 0;
70
71 /// @brief Runs an action's `onClick` as a user event in the screen scope, in one batch.
72 /// @param actionId The action's id.
73 /// @return Whether it ran: `false`, with a diagnostic, for an id the head does not hold enabled and visible, and
74 /// `false` for a closed screen.
75 virtual bool run(std::string_view actionId) = 0;
76};
77
78/// @brief One mounted screen, as its host sees it.
79///
80/// The host reads `title`, `dirty` and `identity` to show the tab and the unsaved indicator, and closes the screen
81/// through `requestClose`. It never destroys a handle whose close is pending, except after `forceClose`.
82class ScreenHandle {
83public:
84 ScreenHandle() = default;
85 ScreenHandle(ScreenHandle const&) = delete;
86 ScreenHandle& operator=(ScreenHandle const&) = delete;
87 ScreenHandle(ScreenHandle&&) = delete;
88 ScreenHandle& operator=(ScreenHandle&&) = delete;
89 virtual ~ScreenHandle() = default;
90
91 /// @brief The screen's title: the document's `title`, as text.
92 /// @return It; it keeps its last value once the screen is closed.
93 [[nodiscard]] virtual reactive::Computed<std::string> const& title() const = 0;
94
95 /// @brief Whether the screen has unsaved changes: the document's `dirty`.
96 /// @return It; `false` once the screen is closed.
97 [[nodiscard]] virtual reactive::Computed<bool> const& dirty() const = 0;
98
99 /// @brief The screen's identity: the document's `identity`, `null` when it declares none. A host re-keys a tab
100 /// when it changes.
101 /// @return It; it keeps its last value once the screen is closed.
102 [[nodiscard]] virtual reactive::Computed<Value> const& identity() const = 0;
103
104 /// @brief The screen's head beside its title, for a shell to show above it.
105 /// @return It, valid as long as the handle: a document screen's (empty values when the document declares no
106 /// head); null for a host screen unless its handle overrides this.
107 [[nodiscard]] virtual ScreenHead* head() noexcept { return nullptr; }
108
109 /// @brief Asks the screen to close (spec 6 §3).
110 ///
111 /// @p done is called exactly once, posted to the owner, never from inside the document's own handler: with `true`
112 /// when the screen closed, `false` when it stays open. A request while one is pending joins it, and is answered
113 /// with the same outcome.
114 /// @param done Called with the outcome.
115 virtual void requestClose(std::function<void(bool closed)> done) = 0;
116
117 /// @brief Closes the screen without asking: skips `onClose` and the close guard, runs `onUnmount`, and answers
118 /// a pending `requestClose` with `true`. For application quit.
119 virtual void forceClose() = 0;
120
121 /// @brief Whether the screen can save its changes: a document screen that declares `onSave`, or a host screen
122 /// whose handle implements `save`. A close guard offers *save and close* only when this is `true`.
123 /// @return It; `false` unless a derived handle says otherwise.
124 [[nodiscard]] virtual bool canSave() const { return false; }
125
126 /// @brief Saves the screen's changes (spec 6 §15) and answers whether it is then clean.
127 ///
128 /// A document screen runs `onSave`, waits until nothing pending is left, and answers `true` when `dirty` is then
129 /// not `true`. A save while one waits joins it. A screen that cannot save answers `false` at once. @p done is not
130 /// called when the screen closes before the save has finished.
131 /// @param done Called with whether the screen is clean.
132 // NOLINTNEXTLINE(performance-unnecessary-value-param): the base only calls it; overrides keep it.
133 virtual void save(std::function<void(bool clean)> done) {
134 if (done) {
135 done(false);
136 }
137 }
138};
139
140/// @brief What mounts screens for a host that owns its own shell; the workspace shell is one.
141class ScreenHost {
142public:
143 ScreenHost() = default;
144 ScreenHost(ScreenHost const&) = delete;
145 ScreenHost& operator=(ScreenHost const&) = delete;
146 ScreenHost(ScreenHost&&) = delete;
147 ScreenHost& operator=(ScreenHost&&) = delete;
148 virtual ~ScreenHost() = default;
149
150 /// @brief Mounts a screen.
151 /// @param screenId The screen id.
152 /// @param params Its params.
153 /// @return Its handle, or why it could not be mounted.
154 [[nodiscard]] virtual std::expected<std::unique_ptr<ScreenHandle>, MountError> mount(std::string_view screenId,
155 ParamMap params) = 0;
156
157 /// @brief The identity a screen would have with @p params, without mounting it, so a host can focus an open
158 /// screen instead of opening a second.
159 /// @param screenId The screen id.
160 /// @param params The params.
161 /// @return Its canonical text, or `std::nullopt` for a screen that declares none or whose identity reads more
162 /// than its params.
163 [[nodiscard]] virtual std::optional<std::string> identityOf(std::string_view screenId, ParamMap const& params) = 0;
164
165 /// @brief A document asked to open another screen. The host decides whether that is a new tab, a focus or a
166 /// dialog. Implemented by the host.
167 /// @param screenId The screen id.
168 /// @param params Its params.
169 virtual void onNavigate(std::string_view screenId, ParamMap const& params) = 0;
170};
171
172/// @brief Makes a host screen (spec 6 §15): the application's native screen, mounted into a shell's page with
173/// @p params. It returns the screen's handle, whose `title`, `dirty`, `identity` and close protocol the shell
174/// reads as it does a document screen's, or why it could not be mounted.
175using HostScreenFactory = std::function<std::expected<std::unique_ptr<ScreenHandle>, MountError>(
176 ParamMap const& params, ContainerWidget& page)>;
177
178/// @brief The host screens an application registers by name: what a catalog entry's `host` names instead of a
179/// document.
181public:
182 /// @brief Registers a host screen, replacing one of the same name.
183 /// @param name The name a catalog entry's `host` gives.
184 /// @param factory What mounts it.
185 void add(std::string name, HostScreenFactory factory) {
186 _factories.insert_or_assign(std::move(name), std::move(factory));
187 }
188
189 /// @brief The factory registered under @p name.
190 /// @param name The name.
191 /// @return It, or null when none is registered.
192 [[nodiscard]] HostScreenFactory const* find(std::string_view name) const {
193 auto const found = _factories.find(name);
194 return found == _factories.end() ? nullptr : &found->second;
195 }
196
197private:
198 std::map<std::string, HostScreenFactory, std::less<>> _factories;
199};
200
201/// @brief What the user chose for a dirty screen that is being closed (spec 6 §15).
202enum class CloseChoice : std::uint8_t {
203 Save, ///< Run the screen's save, and close it only if it is then clean.
204 Discard, ///< Close it without saving.
205 Cancel, ///< Keep it open.
206};
207
208/// @brief What decides a close the document does not decide itself: a dirty screen that declares no `onClose`.
209///
210/// It asks the user, typically with a dialog, and calls `choose` once. It offers *save and close* only when
211/// `screen.canSave()`; `Save` on a screen that cannot save keeps it open. The handle carries the choice out, so
212/// `choose` may be called after the handle is gone, and then does nothing.
213using CloseGuard = std::function<void(ScreenHandle& screen, std::function<void(CloseChoice choice)> choose)>;
214
215/// @brief How a `DocumentScreenHost` finds documents and mounts them.
217 /// @brief The loaded document of a screen id, or null for an unknown one.
218 std::function<std::shared_ptr<Document const>(std::string_view)> documents;
219 /// @brief The view backend screens mount on. Borrowed: it outlives every screen.
221 /// @brief Decides the close of a dirty screen without `onClose`; empty keeps every such screen open.
223 /// @brief Called when the guard's *save and close* left the screen dirty, before it is kept open, so the host
224 /// shows the failure. Optional.
225 std::function<void(ScreenHandle& screen)> saveFailed;
226 /// @brief The application's side of the commands the host does not answer itself; `nullptr` answers them "not
227 /// supported". Borrowed.
229 /// @brief Called with each diagnostic the screens report.
230 std::function<void(Refusal const&)> diagnostics;
231};
232
233namespace detail::host {
234
235/// The environment `identityOf` evaluates in: the params, and nothing else. Reading anything else marks it unknown.
236class ParamsEnv final : public Environment {
237public:
238 /// @brief An environment over @p params.
239 /// @param params The params; borrowed for the environment's lifetime.
240 explicit ParamsEnv(ParamMap const& params) : _params{&params} {}
241
242 [[nodiscard]] Value ref(std::string_view head, std::span<std::string const> path) const override {
243 auto const found = _params->find(head);
244 if (found == _params->end()) {
245 _unknown = true;
246 return Value{};
247 }
248 Value value = found->second;
249 for (std::string const& step : path) {
250 ValueRecord const* const record = value.asRecord();
251 if (record == nullptr || !record->contains(step)) {
252 return Value{};
253 }
254 value = record->at(step);
255 }
256 return value;
257 }
258
259 [[nodiscard]] Value row(std::span<std::string const> path) const override { return unknown(path); }
260
261 [[nodiscard]] Value control(ExprKind form, std::string_view name) const override {
262 static_cast<void>(form);
263 static_cast<void>(name);
264 _unknown = true;
265 return Value{};
266 }
267
268 [[nodiscard]] Value pref(std::string_view point, std::span<std::string const> path) const override {
269 static_cast<void>(point);
270 return unknown(path);
271 }
272
273 [[nodiscard]] Value client(ExprKind form, std::string_view name) const override {
274 static_cast<void>(form);
275 static_cast<void>(name);
276 _unknown = true;
277 return Value{};
278 }
279
280 [[nodiscard]] FormatContext const& formatting() const override { return _formatting; }
281
282 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): Environment's signature.
283 void report(std::string_view path, std::string_view reason) const override {
284 static_cast<void>(path);
285 static_cast<void>(reason);
286 _unknown = true;
287 }
288
289 /// @brief Whether the evaluation read anything but the params, or failed.
290 /// @return It.
291 [[nodiscard]] bool unknown() const noexcept { return _unknown; }
292
293private:
294 [[nodiscard]] Value unknown(std::span<std::string const> path) const {
295 static_cast<void>(path);
296 _unknown = true;
297 return Value{};
298 }
299
300 ParamMap const* _params;
301 FormatContext _formatting;
302 mutable bool _unknown = false;
303};
304
305/// @brief The text a title value shows.
306/// @param value The title's value.
307/// @return A string as is, `null` as empty, anything else as its canonical JSON.
308[[nodiscard]] inline std::string titleText(Value const& value) {
309 if (std::string const* const text = value.asString()) {
310 return *text;
311 }
312 return value.isNull() ? std::string{} : ::morph::ui::detail::canonicalJson(value);
313}
314
315} // namespace detail::host
316
317/// @brief The canonical text of a screen's identity: what `DocumentScreenHost::identityOf` answers, so
318/// `identityOf(id, params) == identityKey(handle.identity().peek())` says whether a mounted screen is the one
319/// those params would open.
320/// @param identity The identity value.
321/// @return Its JSON with record fields in name order, or `std::nullopt` for `null` (a screen without an identity).
322[[nodiscard]] inline std::optional<std::string> identityKey(Value const& identity) {
323 if (identity.isNull()) {
324 return std::nullopt;
325 }
326 return detail::canonicalJson(identity);
327}
328
329/// @brief Whether two identities name the same screen instance: their keys are equal, and neither is `null`.
330/// @param lhs One identity.
331/// @param rhs The other.
332/// @return True when both have a key and the keys are equal; `null` equals nothing, itself included.
333[[nodiscard]] inline bool sameIdentity(Value const& lhs, Value const& rhs) {
334 std::optional<std::string> const left = identityKey(lhs);
335 return left.has_value() && left == identityKey(rhs);
336}
337
338/// @brief The framework's `ScreenHost` over UI documents: it mounts them through an `Interpreter`, closes them by the
339/// protocol of spec 6 §3, and hands a document's `navigate` to `onNavigate`, which the deriving host
340/// implements.
341///
342/// It is the interpreter's `InterpreterHost` for `navigate`; every other command goes to the application's host, if
343/// one was given. Everything runs on the runtime's owner.
344class DocumentScreenHost : public ScreenHost, private InterpreterHost {
345public:
346 /// @brief A host over @p session and @p dispatcher.
347 /// @param session The session the screens share. Borrowed: it outlives the host.
348 /// @param dispatcher Where model calls go. Borrowed: it outlives the host.
349 /// @param options The documents, the backend, the close guard and the application's host.
350 /// @throws std::invalid_argument without a document lookup or a backend.
351 DocumentScreenHost(Session& session MORPH_LIFETIMEBOUND, Dispatcher& dispatcher MORPH_LIFETIMEBOUND,
353 : _options{std::move(options)},
354 _session{&session},
355 _dispatcher{&dispatcher},
356 _interpreter{
357 session, dispatcher,
358 InterpreterOptions{.host = static_cast<InterpreterHost*>(this), .diagnostics = _options.diagnostics}} {
359 if (!_options.documents || _options.backend == nullptr) {
360 throw std::invalid_argument("DocumentScreenHost: a document lookup and a backend are required");
361 }
362 }
363
364 /// @brief Mounts a document screen.
365 /// @param screenId The screen id.
366 /// @param params Its params.
367 /// @return Its handle, or why it could not be mounted: an unknown screen, or the interpreter's refusal.
368 [[nodiscard]] std::expected<std::unique_ptr<ScreenHandle>, MountError> mount(std::string_view screenId,
369 ParamMap params) override {
370 return mountAt(screenId, params, nullptr);
371 }
372
373 /// @brief Mounts a screen as `mount` does, with its root widget appended to @p parent instead of made a backend
374 /// root: how a shell puts a screen in its own container, such as a tab's page.
375 /// @param screenId The screen id.
376 /// @param params Its params.
377 /// @param parent The container; it outlives the handle.
378 /// @return Its handle, or why it could not be mounted.
379 [[nodiscard]] std::expected<std::unique_ptr<ScreenHandle>, MountError> mountInto(
380 std::string_view screenId, ParamMap const& params, ContainerWidget& parent MORPH_LIFETIMEBOUND) {
381 return mountAt(screenId, params, &parent);
382 }
383
384 /// @brief The identity a document screen would have with @p params: its `identity` evaluated over the params.
385 /// @param screenId The screen id.
386 /// @param params The params.
387 /// @return Its canonical text, or `std::nullopt` for an unknown screen, one without `identity`, or one whose
388 /// identity reads more than its params or is `null`.
389 [[nodiscard]] std::optional<std::string> identityOf(std::string_view screenId, ParamMap const& params) override {
390 std::shared_ptr<Document const> const document = _options.documents(screenId);
391 if (!document || !document->identity) {
392 return std::nullopt;
393 }
394 detail::host::ParamsEnv const env{params};
395 Value const identity = evaluate(*document->identity, env);
396 if (env.unknown()) {
397 return std::nullopt;
398 }
399 return identityKey(identity);
400 }
401
402 /// @brief Starts the requests of a screen's `prefetch` queries before it mounts (spec 5 §5), so the screen mounts
403 /// on a warm cache: what a shell does when the pointer rests on an entry for a screen that is not open.
404 ///
405 /// Each named query's body is evaluated over @p params alone and its entry warmed through the session's cache,
406 /// under the token the mounted screen's alias will have. A keyed alias's token follows from its key, and a
407 /// stateless model's is shared. A private alias gets a fresh token when the screen mounts, so nothing could read
408 /// what a prefetch warmed: its queries are skipped, as are evaluations, a key or body that reads more than the
409 /// params, and an entry that is already fresh or in flight.
410 /// @param screenId The screen id; an unknown one does nothing.
411 /// @param params The params it would mount with.
412 void prefetch(std::string_view screenId, ParamMap const& params) {
413 std::shared_ptr<Document const> const document = _options.documents(screenId);
414 if (!document) {
415 return;
416 }
417 for (std::string const& name : document->prefetch) {
418 auto const query = std::ranges::find(document->scope.queries, name, &QueryDecl::name);
419 if (query == document->scope.queries.end() || query->evaluate) {
420 continue;
421 }
422 auto const model = std::ranges::find(document->scope.models, query->model, &ModelDecl::name);
423 if (model == document->scope.models.end()) {
424 continue;
425 }
426 std::optional<Binding> const binding = bindingOf(*model, params);
427 if (!binding) {
428 continue;
429 }
430 detail::host::ParamsEnv const env{params};
431 Value const body = query->body ? evaluate(*query->body, env) : Value{ValueRecord{}};
432 if (env.unknown()) {
433 continue;
434 }
435 std::shared_ptr<ModelChannel> const channel =
436 _dispatcher->bind(model->type, binding->sharing, binding->instanceKey);
437 _session->prefetch(channel, binding->token, model->type, query->action, body);
438 }
439 }
440
441 /// @brief The route of a screen instance (spec 6 §20): `routeOf` over the screen's declared params.
442 /// @param screenId The screen id.
443 /// @param params Its params.
444 /// @return The route, or why it has none: an unknown screen, or a param with no route form.
445 [[nodiscard]] std::expected<std::string, std::string> routeOf(std::string_view screenId,
446 ParamMap const& params) const {
447 std::shared_ptr<Document const> const document = _options.documents(screenId);
448 if (!document) {
449 return std::unexpected("unknown screen `" + std::string{screenId} + "`");
450 }
451 return ::morph::ui::routeOf(*document, params);
452 }
453
454 /// @brief Reads a route back against the screen it names (`parseRoute` over this host's documents).
455 /// @param route The route.
456 /// @return The screen and its params, or why the route is refused.
457 [[nodiscard]] std::expected<Route, std::string> parseRoute(std::string_view route) const {
458 return ::morph::ui::parseRoute(route, _options.documents);
459 }
460
461 /// @brief A document asked to open another screen, with how (`as`). The default hands the screen and its params
462 /// to `onNavigate(screenId, params)`; a host that shows dialogs, drawers or windows overrides this one.
463 /// @param request The request: screen, params, `as`.
464 virtual void onNavigate(NavigateRequest const& request) {
465 ValueRecord const* const params = request.params.asRecord();
466 onNavigate(request.screen, params == nullptr ? ParamMap{} : *params);
467 }
468
470
471 /// @brief A screen this host mounted closed itself, by `return` or `close: self` (spec 6 §16). Called after the
472 /// screen is closed, posted to the owner; the host drops its tab or dialog. The default does nothing.
473 /// @param screen The handle, closed.
474 virtual void onScreenClosed(ScreenHandle& screen) { static_cast<void>(screen); }
475
476 /// @brief Mounts a screen this host mounted again, on a changed document (spec 5 §12, development reload).
477 ///
478 /// The new screen is mounted with the same params in the same place, and each state of its own scope starts from
479 /// the old screen's value of the state of the same name and type; any other state starts from its init. The old
480 /// screen is then destroyed, which runs its `onUnmount`, and the handle shows the new one: its `title`, `dirty`
481 /// and `identity` follow it, and a pending call stays tied to it. A refused mount keeps the old screen.
482 /// @param handle A handle this host mounted.
483 /// @param document The changed document of the same screen, loaded.
484 /// @return Nothing, or why the old screen was kept: a foreign or closed handle, a close or save in progress, or
485 /// the interpreter's refusal of the new document.
486 [[nodiscard]] std::expected<void, MountError> remount(ScreenHandle& handle,
487 std::shared_ptr<Document const> document) {
488 auto* const own = dynamic_cast<Handle*>(&handle);
489 if (own == nullptr || !document) {
490 return std::unexpected(MountError{.screenId = document ? document->id : std::string{},
491 .reason = "not a document screen of this host"});
492 }
493 return own->remount(_interpreter, std::move(document), *_options.backend);
494 }
495
496 /// @brief The development inspector over a screen this host mounted (spec 6 §21): what a shell's inspector window
497 /// reads. It hands out no access to the screen beyond the inspector's own read-only view and its
498 /// `refetch` command.
499 /// @param handle A handle this host mounted.
500 /// @return The inspector, valid until the screen closes or is remounted; nothing for a closed or foreign handle,
501 /// and nothing for a screen whose session has `DevelopmentTools::Off`.
502 [[nodiscard]] static std::optional<Inspector> inspector(ScreenHandle& handle) {
503 auto const* const own = dynamic_cast<Handle const*>(&handle);
504 Screen* const screen = own == nullptr ? nullptr : own->screen();
505 if (screen == nullptr) {
506 return std::nullopt;
507 }
508 return Inspector::over(*screen);
509 }
510
511 /// @brief The root widget of a handle this host mounted, which a host places in its own layout.
512 /// @param handle A handle this host mounted.
513 /// @return Its root widget, or `nullptr` for a closed or foreign handle.
514 [[nodiscard]] static Widget* rootOf(ScreenHandle const& handle) {
515 auto const* const own = dynamic_cast<Handle const*>(&handle);
516 return own == nullptr ? nullptr : own->root();
517 }
518
519private:
520 // How a prefetch reaches the instance the mounted screen's alias will bind.
521 struct Binding {
522 std::string token;
523 bridge::BindSharing sharing = bridge::BindSharing::Private;
524 std::string instanceKey;
525 };
526
527 // The binding of @p model over @p params, or nothing for an instance the mounted screen will not share.
528 [[nodiscard]] std::optional<Binding> bindingOf(ModelDecl const& model, ParamMap const& params) const {
529 InstanceMode mode = model.instance;
530 if (mode == InstanceMode::Default) {
531 Catalog const& catalog = _session->options().catalog;
532 std::optional<ModelInfo> const info = catalog.model ? catalog.model(model.type) : std::nullopt;
533 mode = info && info->stateless ? InstanceMode::Shared : InstanceMode::Private;
534 }
535 if (mode == InstanceMode::Shared) {
536 return Binding{.token = "shared", .sharing = bridge::BindSharing::Private, .instanceKey = {}};
537 }
538 if (mode != InstanceMode::Keyed || !model.key) {
539 return std::nullopt;
540 }
541 detail::host::ParamsEnv const env{params};
542 std::string const key = detail::instanceKeyText(evaluate(*model.key, env));
543 if (env.unknown() || key.empty()) {
544 return std::nullopt;
545 }
546 return Binding{.token = "shared:" + key, .sharing = bridge::BindSharing::Shared, .instanceKey = key};
547 }
548
549 // The mount itself, under @p parent or as a backend root.
550 std::expected<std::unique_ptr<ScreenHandle>, MountError> mountAt(std::string_view screenId, ParamMap const& params,
551 ContainerWidget* parent) {
552 std::shared_ptr<Document const> document = _options.documents(screenId);
553 if (!document) {
554 return std::unexpected(MountError{.screenId = std::string{screenId}, .reason = "unknown screen"});
555 }
556 auto screen =
557 _interpreter.mount(std::move(document), params, *_options.backend, MountOptions{.parent = parent});
558 if (!screen) {
559 return std::unexpected(mountErrorOf(screenId, screen.error()));
560 }
561 auto handle = std::make_unique<Handle>(std::move(*screen), _options.guard, _options.saveFailed);
562 handle->placed(params, parent);
563 adoptCall(*handle, screenId, params);
564 return handle;
565 }
566
567 // The interpreter's refusal of a mount, as the host reports it.
568 [[nodiscard]] static MountError mountErrorOf(std::string_view screenId, Refusal const& refusal) {
569 std::string reason = refusal.reason;
570 if (!refusal.path.empty()) {
571 reason = refusal.path + ": " + reason;
572 }
573 return MountError{.screenId = std::string{screenId}, .reason = std::move(reason)};
574 }
575
576 /// A screen opened to answer its opener (spec 6 §16): answered exactly once, by `return`, by `close: self`, or by
577 /// the screen closing any other way, which is a cancel.
578 struct Call {
579 std::string screen;
580 ParamMap params;
581 std::function<void(Value)> onResult;
582 std::function<void()> onCancel;
583 bool answered = false;
584
585 void answer(std::optional<Value> result) {
586 if (answered) {
587 return;
588 }
589 answered = true;
590 if (result) {
591 if (onResult) {
592 onResult(std::move(*result));
593 }
594 } else if (onCancel) {
595 onCancel();
596 }
597 }
598 };
599
600 /// The handle of a document screen. Its state is shared with the posts it makes, so a post that runs after the
601 /// handle is gone finds the screen already closed.
602 class Handle final : public ScreenHandle, public ScreenHead {
603 public:
604 Handle(std::unique_ptr<Screen> screen, CloseGuard guard, std::function<void(ScreenHandle&)> saveFailed)
605 : _state{std::make_shared<State>(std::move(screen), std::move(guard), std::move(saveFailed))} {
606 _state->self = this;
607 }
608
609 ~Handle() override {
610 _state->self = nullptr;
611 try {
612 _state->finish(true);
613 } catch (...) { // NOLINT(bugprone-empty-catch): a destructor has no one to report to; the screen is gone.
614 }
615 }
616
617 Handle(Handle const&) = delete;
618 Handle& operator=(Handle const&) = delete;
619 Handle(Handle&&) = delete;
620 Handle& operator=(Handle&&) = delete;
621
622 [[nodiscard]] reactive::Computed<std::string> const& title() const override { return _state->title; }
623 [[nodiscard]] reactive::Computed<bool> const& dirty() const override { return _state->dirty; }
624 [[nodiscard]] reactive::Computed<Value> const& identity() const override { return _state->identity; }
625
626 [[nodiscard]] ScreenHead* head() noexcept override { return this; }
627 [[nodiscard]] reactive::Computed<std::string> const& subtitle() const override { return _state->subtitle; }
628 [[nodiscard]] reactive::Computed<std::vector<HeadBadge>> const& status() const override {
629 return _state->status;
630 }
631 [[nodiscard]] reactive::Computed<std::vector<HeadAction>> const& actions() const override {
632 return _state->actions;
633 }
634 bool run(std::string_view actionId) override {
635 return _state->screen != nullptr && _state->screen->runAction(actionId);
636 }
637
638 void requestClose(std::function<void(bool closed)> done) override { State::request(_state, std::move(done)); }
639
640 void forceClose() override { _state->finish(true); }
641
642 [[nodiscard]] bool canSave() const override { return _state->canSave(); }
643
644 void save(std::function<void(bool clean)> done) override { State::save(_state, std::move(done)); }
645
646 [[nodiscard]] Widget* root() const { return _state->screen ? &_state->screen->root() : nullptr; }
647
648 /// The screen while it is open; null once it closed.
649 [[nodiscard]] Screen* screen() const { return _state->screen.get(); }
650
651 /// Records what the screen was mounted with, so a remount mounts the same instance in the same place.
652 void placed(ParamMap params, ContainerWidget* parent) {
653 _state->params = std::move(params);
654 _state->parent = parent;
655 }
656
657 /// Mounts @p document with the screen's params, keeping its state by name and type, and replaces the
658 /// screen with it; on a refusal the screen stays as it was.
659 std::expected<void, MountError> remount(Interpreter& interpreter, std::shared_ptr<Document const> document,
660 IViewBackend& backend) {
661 State& state = *_state;
662 std::string const screenId = document->id;
663 if (!state.screen) {
664 return std::unexpected(MountError{.screenId = screenId, .reason = "the screen is closed"});
665 }
666 if (state.closing || state.saved.effect) {
667 return std::unexpected(MountError{.screenId = screenId, .reason = "the screen is closing or saving"});
668 }
669 auto next = interpreter.mount(std::move(document), state.params, backend,
670 MountOptions{.parent = state.parent, .keepStateOf = state.screen.get()});
671 if (!next) {
672 return std::unexpected(mountErrorOf(screenId, next.error()));
673 }
674 // The old screen goes after the new one has mounted, so its root widget is never missing in between;
675 // destroying it runs its `onUnmount`.
676 std::unique_ptr<Screen> const replaced = std::exchange(state.screen, std::move(*next));
677 state.runtime->batch([&state] { state.generation.set(state.generation.peek() + 1); });
678 return {};
679 }
680
681 /// The call this screen answers, if it was opened as one.
682 void answers(std::shared_ptr<Call> call) { _state->call = std::move(call); }
683
684 /// Closes the screen after its own `return` or `close: self` has answered: posted, since the command that
685 /// asked for it is still running in the screen.
686 void closeItself(std::function<void(ScreenHandle&)> closed) {
687 std::weak_ptr<State> const weak = _state;
688 _state->runtime->owner().post([weak, closed = std::move(closed)] {
689 auto const held = weak.lock();
690 if (!held || !held->screen) {
691 return;
692 }
693 held->finish(true);
694 if (held->self != nullptr && closed) {
695 closed(*held->self);
696 }
697 });
698 }
699
700 /// Whether this screen answers @p call and is still open.
701 [[nodiscard]] bool answering(std::shared_ptr<Call> const& call) const {
702 return _state->screen && _state->call == call;
703 }
704
705 private:
706 struct State {
707 State(std::unique_ptr<Screen> mounted, CloseGuard closeGuard,
708 std::function<void(ScreenHandle&)> onSaveFailed)
709 : screen{std::move(mounted)},
710 runtime{&screen->runtime()},
711 guard{std::move(closeGuard)},
712 saveFailed{std::move(onSaveFailed)},
713 closed{*runtime, false},
714 generation{*runtime, 0},
715 title{*runtime,
716 [this] {
717 if (!closed.get() && generation.get() >= 0) {
718 lastTitle = detail::host::titleText(screen->title());
719 }
720 return lastTitle;
721 }},
722 dirty{*runtime, [this] { return !closed.get() && generation.get() >= 0 && screen->dirty(); }},
723 identity{*runtime,
724 [this] {
725 if (!closed.get() && generation.get() >= 0) {
726 lastIdentity = screen->identity();
727 }
728 return lastIdentity;
729 }},
730 subtitle{*runtime,
731 [this] {
732 if (!closed.get() && generation.get() >= 0) {
733 lastSubtitle = detail::host::titleText(screen->subtitle());
734 }
735 return lastSubtitle;
736 }},
737 status{*runtime,
738 [this] {
739 return !closed.get() && generation.get() >= 0 ? screen->status()
740 : std::vector<HeadBadge>{};
741 }},
742 actions{*runtime, [this] {
743 return !closed.get() && generation.get() >= 0 ? screen->actions()
744 : std::vector<HeadAction>{};
745 }} {}
746
747 State(State const&) = delete;
748 State& operator=(State const&) = delete;
749 State(State&&) = delete;
750 State& operator=(State&&) = delete;
751 ~State() = default;
752
753 /// Starts a close, or joins the one in progress.
754 static void request(std::shared_ptr<State> const& state, std::function<void(bool)> done) {
755 if (!state->screen) {
756 state->post({std::move(done)}, true);
757 return;
758 }
759 state->waiting.push_back(std::move(done));
760 if (state->closing) {
761 return;
762 }
763 state->closing = true;
764 std::shared_ptr<bool const> const cancelled = state->screen->beginClose();
765 if (cancelled == nullptr) {
766 bool const isDirty = state->screen->dirty();
767 if (!isDirty) {
768 state->finish(true);
769 } else if (state->guard && state->self != nullptr) {
770 std::weak_ptr<State> const weak = state;
771 state->guard(*state->self, [weak](CloseChoice choice) {
772 if (auto const held = weak.lock()) {
773 choose(held, choice);
774 }
775 });
776 } else {
777 state->decide(false);
778 }
779 return;
780 }
781 // Closes once onClose and everything it started has ended, unless it ran cancelClose.
782 whenSettled(state, &State::settled,
783 [cancelled](State& settledState) { settledState.decide(!*cancelled); });
784 }
785
786 /// A wait for the screen to have nothing pending; the id tells a posted task whether it is still current.
787 struct Watch {
788 /// @brief The effect that watches `pending()`; null when nothing is watched.
789 std::unique_ptr<reactive::Effect> effect;
790 /// @brief Which watch this is, among every watch the screen made.
791 std::uint64_t id = 0;
792 };
793
794 /// Runs @p then once the screen has nothing pending, in a task posted to the owner, so the commands that
795 /// settled the work (a mutation's `onSuccess`) have run. @p slot holds the watch; resetting it, or closing
796 /// the screen, cancels it.
797 static void whenSettled(std::shared_ptr<State> const& state, Watch State::* slot,
798 std::function<void(State&)> then) {
799 std::weak_ptr<State> const weak = state;
800 std::uint64_t const watchId = ++state->watches;
801 Watch& watch = state.get()->*slot;
802 watch.id = watchId;
803 watch.effect =
804 std::make_unique<reactive::Effect>(*state->runtime, [weak, slot, watchId, then = std::move(then)] {
805 auto const held = weak.lock();
806 if (!held || !held->screen || held->screen->pending()) {
807 return;
808 }
809 held->runtime->owner().post([weak, slot, watchId, then] {
810 auto const again = weak.lock();
811 if (!again || !again->screen || (again.get()->*slot).id != watchId) {
812 return;
813 }
814 again.get()->*slot = Watch{};
815 then(*again);
816 });
817 });
818 }
819
820 /// Whether the screen is open and declares `onSave`.
821 [[nodiscard]] bool canSave() const { return screen && screen->declares("onSave"); }
822
823 /// Runs `onSave`, or joins the save in progress, and answers once nothing is pending.
824 static void save(std::shared_ptr<State> const& state, std::function<void(bool)> done) {
825 if (!state->canSave()) {
826 if (done) {
827 done(false);
828 }
829 return;
830 }
831 if (state->saved.effect) {
832 state->saving.push_back(std::move(done));
833 return;
834 }
835 state->screen->runLifecycle("onSave");
836 state->saving.push_back(std::move(done));
837 // Answered in the settled task itself rather than posted again, so an answer never reaches a handle
838 // that closed in between; an answer that closes the screen ends the answering.
839 whenSettled(state, &State::saved, [](State& savedState) {
840 bool const clean = !savedState.screen->dirty();
841 for (auto const& answer : std::exchange(savedState.saving, {})) {
842 if (!savedState.screen) {
843 return;
844 }
845 if (answer) {
846 answer(clean);
847 }
848 }
849 });
850 }
851
852 /// Carries out the guard's choice for the close in progress.
853 static void choose(std::shared_ptr<State> const& state, CloseChoice choice) {
854 if (!state->closing) {
855 return;
856 }
857 if (choice != CloseChoice::Save || !state->canSave()) {
858 state->decide(choice == CloseChoice::Discard);
859 return;
860 }
861 std::weak_ptr<State> const weak = state;
862 save(state, [weak](bool clean) {
863 auto const held = weak.lock();
864 if (!held) {
865 return;
866 }
867 if (!clean && held->saveFailed && held->self != nullptr) {
868 held->saveFailed(*held->self);
869 }
870 held->decide(clean);
871 });
872 }
873
874 /// Ends the close in progress: closes the screen or keeps it open, and answers every request.
875 void decide(bool close) {
876 if (!closing) {
877 return;
878 }
879 if (close) {
880 finish(true);
881 return;
882 }
883 closing = false;
884 post(std::exchange(waiting, {}), false);
885 }
886
887 /// Closes the screen now, answering every pending request with `true`.
888 void finish(bool answer) {
889 settled = {};
890 saved = {};
891 saving.clear(); // a save outlived by its screen is never answered
892 closing = false;
893 if (call) {
894 call->answer(std::nullopt); // a no-op once `return` or `close: self` has answered
895 }
896 if (screen) {
897 // The last values a host reads once the screen is gone, whether or not it read them before.
898 lastTitle = detail::host::titleText(screen->title());
899 lastIdentity = screen->identity();
900 lastSubtitle = detail::host::titleText(screen->subtitle());
901 runtime->batch([this] { closed.set(true); });
902 screen.reset();
903 }
904 post(std::exchange(waiting, {}), answer);
905 }
906
907 /// Posts each answer to the owner, so none runs inside the document's own handler.
908 void post(std::vector<std::function<void(bool)>> answers, bool answer) const {
909 if (answers.empty()) {
910 return;
911 }
912 runtime->owner().post([answers = std::move(answers), answer] {
913 for (auto const& done : answers) {
914 if (done) {
915 done(answer);
916 }
917 }
918 });
919 }
920
921 std::unique_ptr<Screen> screen;
922 reactive::Runtime* runtime;
923 CloseGuard guard;
924 Handle* self = nullptr;
925 std::shared_ptr<Call> call;
926 bool closing = false;
927 std::vector<std::function<void(bool)>> waiting;
928 std::function<void(ScreenHandle&)> saveFailed;
929 Watch settled;
930 std::vector<std::function<void(bool)>> saving;
931 Watch saved;
932 std::uint64_t watches = 0;
933 std::string lastTitle;
934 Value lastIdentity;
935 std::string lastSubtitle;
936 // What the screen was mounted with, for a remount.
937 ParamMap params;
938 ContainerWidget* parent = nullptr;
939 reactive::Signal<bool> closed;
940 // Bumped when a remount replaces the screen, so the values below read the new one.
941 reactive::Signal<int> generation;
942 reactive::Computed<std::string> title;
943 reactive::Computed<bool> dirty;
944 reactive::Computed<Value> identity;
945 reactive::Computed<std::string> subtitle;
946 reactive::Computed<std::vector<HeadBadge>> status;
947 reactive::Computed<std::vector<HeadAction>> actions;
948 };
949
950 std::shared_ptr<State> _state;
951 };
952
953 // InterpreterHost: navigation is the host's; everything else is the application's.
954
955 bool navigate(NavigateRequest const& request, std::function<void(Value)> onResult,
956 std::function<void()> onCancel) override {
957 if (request.screen.empty()) {
958 return _options.application != nullptr &&
959 _options.application->navigate(request, std::move(onResult), std::move(onCancel));
960 }
961 if (onResult || onCancel) {
962 ValueRecord const* const params = request.params.asRecord();
963 _unclaimed.push_back(std::make_shared<Call>(Call{.screen = request.screen,
964 .params = params == nullptr ? ParamMap{} : *params,
965 .onResult = std::move(onResult),
966 .onCancel = std::move(onCancel),
967 .answered = false}));
968 }
969 onNavigate(request);
970 return true;
971 }
972
973 bool closeSelf(std::string const& screen, std::optional<Value> result) override {
974 // The innermost open call of that screen answers; a screen opened without one is the application's.
975 for (OpenCall const& open : std::views::reverse(_open)) {
976 std::shared_ptr<Call> const call = open.call.lock();
977 if (call == nullptr || call->screen != screen || call->answered || open.handle == nullptr ||
978 !open.handle->answering(call)) {
979 continue;
980 }
981 call->answer(std::move(result));
982 open.handle->closeItself([this](ScreenHandle& closed) { onScreenClosed(closed); });
983 return true;
984 }
985 return _options.application != nullptr && _options.application->closeSelf(screen, std::move(result));
986 }
987
988 /// Ties a mounted screen to the call that opened it: the oldest unclaimed call of that screen and params.
989 void adoptCall(Handle& handle, std::string_view screenId, ParamMap const& params) {
990 std::erase_if(_open, [](OpenCall const& open) { return open.call.expired(); });
991 auto const found = std::ranges::find_if(_unclaimed, [&](std::shared_ptr<Call> const& call) {
992 return call->screen == screenId && call->params == params;
993 });
994 if (found == _unclaimed.end()) {
995 return;
996 }
997 std::shared_ptr<Call> const call = std::move(*found);
998 _unclaimed.erase(found);
999 handle.answers(call);
1000 _open.push_back(OpenCall{.call = call, .handle = &handle});
1001 }
1002 bool hostCommand(std::string const& name, Value const& args) override {
1003 return _options.application != nullptr && _options.application->hostCommand(name, args);
1004 }
1005 bool switchBackend(std::string const& name, std::function<void(std::expected<void, std::string>)> done) override {
1006 return _options.application != nullptr && _options.application->switchBackend(name, std::move(done));
1007 }
1008 void resetPrefs(std::string const& screen, std::string const& point) override {
1009 if (_options.application != nullptr) {
1010 _options.application->resetPrefs(screen, point);
1011 }
1012 }
1013 void setLocale(std::string const& locale) override {
1014 if (_options.application != nullptr) {
1015 _options.application->setLocale(locale);
1016 }
1017 }
1018 bool notify(std::string const& text, std::string const& role) override {
1019 return _options.application != nullptr && _options.application->notify(text, role);
1020 }
1021 bool signIn(Value const& reply) override {
1022 return _options.application != nullptr && _options.application->signIn(reply);
1023 }
1024 void signOut() override {
1025 if (_options.application != nullptr) {
1026 _options.application->signOut();
1027 }
1028 }
1029 bool setAppValue(std::string const& name, Value const& value) override {
1030 return _options.application != nullptr && _options.application->setAppValue(name, value);
1031 }
1032 bool uploadFor(UploadRequest const& request, std::function<void(Value)> progress,
1033 std::function<void(std::expected<Value, std::string>)> done) override {
1034 return _options.application != nullptr &&
1035 _options.application->uploadFor(request, std::move(progress), std::move(done));
1036 }
1037 bool download(Value const& file, std::string const& option, Value const& name,
1038 std::function<void(std::expected<Value, std::string>)> done) override {
1039 return _options.application != nullptr && _options.application->download(file, option, name, std::move(done));
1040 }
1041 bool readFile(Value const& file, std::size_t maxBytes,
1042 std::function<void(std::expected<InlineFile, std::string>)> done) override {
1043 return _options.application != nullptr && _options.application->readFile(file, maxBytes, std::move(done));
1044 }
1045 bool saveFile(InlineFile const& file, std::string const& option,
1046 std::function<void(std::expected<Value, std::string>)> done) override {
1047 return _options.application != nullptr && _options.application->saveFile(file, option, std::move(done));
1048 }
1049 void preferenceChanged(std::string const& screen, std::string const& point, Value const& value) override {
1050 if (_options.application != nullptr) {
1051 _options.application->preferenceChanged(screen, point, value);
1052 }
1053 }
1054 void permissionsStale() override {
1055 if (_options.application != nullptr) {
1056 _options.application->permissionsStale();
1057 }
1058 }
1059
1060 /// A call a mounted screen answers, newest last.
1061 struct OpenCall {
1062 std::weak_ptr<Call> call;
1063 Handle* handle = nullptr;
1064 };
1065
1066 DocumentScreenHostOptions _options;
1067 Session* _session;
1068 Dispatcher* _dispatcher;
1069 std::vector<std::shared_ptr<Call>> _unclaimed;
1070 std::vector<OpenCall> _open;
1071 Interpreter _interpreter;
1072};
1073
1074} // namespace morph::ui
Command signOut()
signOut.
Definition author.hpp:2960
Command resetPrefs(Point const &point)
resetPrefs: resets a customization point to the document's default.
Definition author.hpp:2887
Command notify(AnyHandle const &message, Role role=Role::Info)
notify: shows a notification.
Definition author.hpp:2910
AnyHandle env(std::string const &name)
A client environment value, {"env": name}: widthClass, locale, platform, online, backend.
Definition author.hpp:1209
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 navigate(std::string const &screenId, NavigateOptions const &options={})
navigate to a screen by id.
Definition author.hpp:2849
Command switchBackend(std::string const &name)
switchBackend: switches to a named backend.
Definition author.hpp:2882
Command open(Element const &dialog)
open: opens a dialog or drawer.
Definition author.hpp:2779
constexpr Self self
The dialog or drawer a command list runs in.
Definition author.hpp:1295
Command setLocale(std::string const &locale)
setLocale: switches the client's locale.
Definition author.hpp:2896
Command signIn(MutationRef const &mutation)
signIn: runs a sign-in mutation and installs the token it returns.
Definition author.hpp:2954
A derived value: computed lazily on first read, cached, and recomputed only after a source it read re...
Definition signal.hpp:168
A widget with children. A factory given this container appends the new widget as its last child.
Definition backend.hpp:103
The interpreter's view of the bridge: binds a model by its type id, privately or to a shared instance...
virtual void onScreenClosed(ScreenHandle &screen)
A screen this host mounted closed itself, by return or close: self (spec 6 §16). Called after the scr...
Definition host.hpp:474
static Widget * rootOf(ScreenHandle const &handle)
The root widget of a handle this host mounted, which a host places in its own layout.
Definition host.hpp:514
DocumentScreenHost(Session &session, Dispatcher &dispatcher, DocumentScreenHostOptions options)
A host over session and dispatcher.
Definition host.hpp:351
std::expected< std::string, std::string > routeOf(std::string_view screenId, ParamMap const &params) const
The route of a screen instance (spec 6 §20): routeOf over the screen's declared params.
Definition host.hpp:445
void prefetch(std::string_view screenId, ParamMap const &params)
Starts the requests of a screen's prefetch queries before it mounts (spec 5 §5), so the screen mounts...
Definition host.hpp:412
static std::optional< Inspector > inspector(ScreenHandle &handle)
The development inspector over a screen this host mounted (spec 6 §21): what a shell's inspector wind...
Definition host.hpp:502
std::optional< std::string > identityOf(std::string_view screenId, ParamMap const &params) override
The identity a document screen would have with params: its identity evaluated over the params.
Definition host.hpp:389
std::expected< Route, std::string > parseRoute(std::string_view route) const
Reads a route back against the screen it names (parseRoute over this host's documents).
Definition host.hpp:457
virtual void onNavigate(NavigateRequest const &request)
A document asked to open another screen, with how (as). The default hands the screen and its params t...
Definition host.hpp:464
std::expected< std::unique_ptr< ScreenHandle >, MountError > mountInto(std::string_view screenId, ParamMap const &params, ContainerWidget &parent)
Mounts a screen as mount does, with its root widget appended to parent instead of made a backend root...
Definition host.hpp:379
std::expected< std::unique_ptr< ScreenHandle >, MountError > mount(std::string_view screenId, ParamMap params) override
Mounts a document screen.
Definition host.hpp:368
std::expected< void, MountError > remount(ScreenHandle &handle, std::shared_ptr< Document const > document)
Mounts a screen this host mounted again, on a changed document (spec 5 §12, development reload).
Definition host.hpp:486
Makes widgets. Each factory appends the new widget to parent, or makes a root when it is null,...
Definition backend.hpp:810
static std::optional< Inspector > over(Screen &screen)
An inspector over screen, when its session records what an inspector reads.
Definition inspector.hpp:75
What the application answers for the commands a document cannot carry out alone (spec 6)....
One mounted screen, as its host sees it.
Definition host.hpp:82
virtual void save(std::function< void(bool clean)> done)
Saves the screen's changes (spec 6 §15) and answers whether it is then clean.
Definition host.hpp:133
virtual reactive::Computed< bool > const & dirty() const =0
Whether the screen has unsaved changes: the document's dirty.
virtual bool canSave() const
Whether the screen can save its changes: a document screen that declares onSave, or a host screen who...
Definition host.hpp:124
virtual void requestClose(std::function< void(bool closed)> done)=0
Asks the screen to close (spec 6 §3).
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...
virtual void forceClose()=0
Closes the screen without asking: skips onClose and the close guard, runs onUnmount,...
virtual ScreenHead * head() noexcept
The screen's head beside its title, for a shell to show above it.
Definition host.hpp:107
virtual reactive::Computed< std::string > const & title() const =0
The screen's title: the document's title, as text.
A screen's head beside its title: the subtitle, the status badges and the actions a shell shows above...
Definition host.hpp:50
virtual reactive::Computed< std::string > const & subtitle() const =0
The subtitle, as text; empty when the screen declares none.
virtual bool run(std::string_view actionId)=0
Runs an action's onClick as a user event in the screen scope, in one batch.
virtual reactive::Computed< std::vector< HeadAction > > const & actions() const =0
The actions that are visible, in order, each with whether it is enabled.
virtual reactive::Computed< std::vector< HeadBadge > > const & status() const =0
The status badges that are visible, in order.
The host screens an application registers by name: what a catalog entry's host names instead of a doc...
Definition host.hpp:180
HostScreenFactory const * find(std::string_view name) const
The factory registered under name.
Definition host.hpp:192
void add(std::string name, HostScreenFactory factory)
Registers a host screen, replacing one of the same name.
Definition host.hpp:185
virtual std::expected< std::unique_ptr< ScreenHandle >, MountError > mount(std::string_view screenId, ParamMap params)=0
Mounts a screen.
virtual std::optional< std::string > identityOf(std::string_view screenId, ParamMap const &params)=0
The identity a screen would have with params, without mounting it, so a host can focus an open screen...
virtual void onNavigate(std::string_view screenId, ParamMap const &params)=0
A document asked to open another screen. The host decides whether that is a new tab,...
A mounted screen: its scopes, its view mounted through Mounted, and what its host asks of it.
What screens of one client session share (spec 5 §5, §12b): the query cache, the mounted queries,...
SessionOptions const & options() const noexcept
The options.
The document's typed value (spec 5 §3): what every expression evaluates to.
Definition value.hpp:157
bool isNull() const noexcept
Whether the value is null.
Definition value.hpp:212
ValueRecord const * asRecord() const noexcept
The record, if held.
Definition value.hpp:247
std::string const * asString() const noexcept
The string, if held.
Definition value.hpp:224
A retained widget. A new widget is visible, enabled, content-sized, not draggable and not a drop targ...
Definition backend.hpp:34
The environment identityOf evaluates in: the params, and nothing else. Reading anything else marks it...
Definition host.hpp:236
Value ref(std::string_view head, std::span< std::string const > path) const override
Reads {"ref": "head.path"}.
Definition host.hpp:242
FormatContext const & formatting() const override
The formatting facts.
Definition host.hpp:280
bool unknown() const noexcept
Whether the evaluation read anything but the params, or failed.
Definition host.hpp:291
Value row(std::span< std::string const > path) const override
Reads {"row": "path"}.
Definition host.hpp:259
Value client(ExprKind form, std::string_view name) const override
Reads env, session or host.
Definition host.hpp:273
ParamsEnv(ParamMap const &params)
An environment over params.
Definition host.hpp:240
Value pref(std::string_view point, std::span< std::string const > path) const override
Reads {"pref": "point.path"}.
Definition host.hpp:268
void report(std::string_view path, std::string_view reason) const override
Reports a runtime error of an expression.
Definition host.hpp:283
Value control(ExprKind form, std::string_view name) const override
Reads query, pending, error, result, errorKind or can.
Definition host.hpp:261
The UI document (spec 5 §3): its model — declarations, commands, view nodes, components and customiza...
The expr/1 expression language of the UI document: parsing a JSON expression into an Expr tree,...
Value evaluate(Expr const &expr, Environment const &env)
Evaluates an expression.
ExprKind
The forms of an expr/1 expression (spec 5 §4).
@ Session
{"session": "name"}: a session attribute.
CloseChoice
What the user chose for a dirty screen that is being closed (spec 6 §15).
Definition host.hpp:202
@ Save
Run the screen's save, and close it only if it is then clean.
Definition host.hpp:203
@ Discard
Close it without saving.
Definition host.hpp:204
@ Cancel
Keep it open.
Definition host.hpp:205
std::optional< std::string > identityKey(Value const &identity)
The canonical text of a screen's identity: what DocumentScreenHost::identityOf answers,...
Definition host.hpp:322
std::function< void(ScreenHandle &screen, std::function< void(CloseChoice choice)> choose)> CloseGuard
What decides a close the document does not decide itself: a dirty screen that declares no onClose.
Definition host.hpp:213
ValueRecord ParamMap
A screen's params, by name.
Definition host.hpp:38
std::string titleText(Value const &value)
The text a title value shows.
Definition host.hpp:308
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
bool sameIdentity(Value const &lhs, Value const &rhs)
Whether two identities name the same screen instance: their keys are equal, and neither is null.
Definition host.hpp:333
The UI document interpreter: Interpreter::mount turns a loaded Document into reactive state and a vie...
std::string canonicalJson(Value const &value)
A value's JSON text, with record fields in name order: a cache key's canonical body.
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.
Signal<T>, Computed<T> and Effect: the nodes of a reactive graph.
void request(Store< ViewState, Msg > &store, bridge::BridgeHandler< Model, Sharing > &handler, async::CallbackScope const &scope, Action action, ToMsg toMsg, ToFailMsg toFailMsg)
Executes action and sends its result, or its failure, to store as one message.
Definition store.hpp:202
How a DocumentScreenHost finds documents and mounts them.
Definition host.hpp:216
std::function< std::shared_ptr< Document const >(std::string_view)> documents
The loaded document of a screen id, or null for an unknown one.
Definition host.hpp:218
std::function< void(ScreenHandle &screen)> saveFailed
Called when the guard's save and close left the screen dirty, before it is kept open,...
Definition host.hpp:225
std::function< void(Refusal const &)> diagnostics
Called with each diagnostic the screens report.
Definition host.hpp:230
CloseGuard guard
Decides the close of a dirty screen without onClose; empty keeps every such screen open.
Definition host.hpp:222
IViewBackend * backend
The view backend screens mount on. Borrowed: it outlives every screen.
Definition host.hpp:220
InterpreterHost * application
The application's side of the commands the host does not answer itself; nullptr answers them "not ...
Definition host.hpp:228
A wait for the screen to have nothing pending; the id tells a posted task whether it is still current...
Definition host.hpp:787
std::unique_ptr< reactive::Effect > effect
The effect that watches pending(); null when nothing is watched.
Definition host.hpp:789
std::uint64_t id
Which watch this is, among every watch the screen made.
Definition host.hpp:791
What the display functions and t format with: the client's locale and its formatting facts.
What an Interpreter is built with.
std::string name
The alias.
Definition document.hpp:157
Why a screen could not be mounted.
Definition host.hpp:41
std::string reason
Why: an unknown screen, a refused param, a refused mount.
Definition host.hpp:45
std::string screenId
The screen id asked for.
Definition host.hpp:43
What navigate asks the host to open (spec 6 §16, §20).
std::string name
The name.
Definition document.hpp:195
Why a document, an expression or a value was refused: where, and the reason.
Definition value.hpp:38
Catalog catalog
The catalog: stateless decides an alias's default instance; an action's secretFields are never cached...
morph::ui::Inspector: the development inspector's read-only view of a mounted screen (spec 6 §21).
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
std::map< std::string, Value, std::less<> > ValueRecord
A record value: field name to value, ordered by name.
Definition value.hpp:129
Node text(Text spec)
Static or bound text.
Definition view.hpp:1194
SlotBinding< std::decay_t< F > > slot(SlotId slotId, F &&read)
A slot with an id.
Definition view.hpp:63