28#include "../core/backend.hpp"
29#include "../core/bridge.hpp"
30#include "../core/completion.hpp"
31#include "../core/wire.hpp"
77 ModelChannel() =
default;
78 ModelChannel(ModelChannel
const&) =
delete;
79 ModelChannel& operator=(ModelChannel
const&) =
delete;
80 ModelChannel(ModelChannel&&) =
delete;
81 ModelChannel& operator=(ModelChannel&&) =
delete;
82 virtual ~ModelChannel() =
default;
90 std::string
const& bodyJson,
CallOptions options) = 0;
96 Dispatcher() =
default;
97 Dispatcher(Dispatcher
const&) =
delete;
98 Dispatcher& operator=(Dispatcher
const&) =
delete;
99 Dispatcher(Dispatcher&&) =
delete;
100 Dispatcher& operator=(Dispatcher&&) =
delete;
101 virtual ~Dispatcher() =
default;
108 [[nodiscard]]
virtual std::shared_ptr<ModelChannel>
bind(std::string
const& typeId, bridge::BindSharing sharing,
109 std::string
const& instanceKey) = 0;
114using Evaluator = std::function<async::Completion<std::string>(std::string
const& typeId, std::string
const& actionId,
115 std::string
const& bodyJson)>;
133 : _bridge{&bridge}, _cbExec{cbExec}, _evaluator{std::move(evaluator)} {}
140 [[nodiscard]] std::shared_ptr<ModelChannel>
bind(std::string
const& typeId, bridge::BindSharing sharing,
141 std::string
const& instanceKey)
override {
142 return std::make_shared<Channel>(*_bridge, _cbExec, _evaluator, typeId, sharing, instanceKey);
150 bridge::BindSharing sharing, std::string
const& instanceKey)
152 _evaluator{std::move(evaluator)},
154 _handler{bridge, cbExec, typeId, sharing, instanceKey} {}
157 CallOptions options)
override {
158 if (options.evaluate) {
160 return _evaluator(_typeId, actionId, bodyJson);
163 promise.reject(std::make_exception_ptr(std::runtime_error{
"evaluate needs the UI service"}));
164 return std::move(refused);
166 return _handler.execute(actionId, bodyJson);
170 exec::IExecutor* _cbExec;
173 bridge::RawHandler _handler;
176 bridge::Bridge* _bridge;
177 exec::IExecutor* _cbExec;
223inline constexpr std::size_t
kMaxInlineFileBytes = (wire::kMaxEnvelopeBytes - (std::size_t{64} * 1024)) * 3 / 4;
227class InterpreterHost {
229 InterpreterHost() =
default;
230 InterpreterHost(InterpreterHost
const&) =
delete;
231 InterpreterHost& operator=(InterpreterHost
const&) =
delete;
232 InterpreterHost(InterpreterHost&&) =
delete;
233 InterpreterHost& operator=(InterpreterHost&&) =
delete;
234 virtual ~InterpreterHost() =
default;
245 std::function<
void()> onCancel) {
246 static_cast<void>(request);
247 static_cast<void>(onResult);
248 static_cast<void>(onCancel);
256 virtual bool closeSelf(std::string
const& screen, std::optional<Value> result) {
257 static_cast<void>(screen);
258 static_cast<void>(result);
267 static_cast<void>(name);
268 static_cast<void>(args);
280 virtual bool switchBackend(std::string
const& name, std::function<
void(std::expected<void, std::string>)> done) {
281 static_cast<void>(name);
282 static_cast<void>(done);
289 virtual void resetPrefs(std::string
const& screen, std::string
const& point) {
290 static_cast<void>(screen);
291 static_cast<void>(point);
296 virtual void setLocale(std::string
const& locale) {
static_cast<void>(locale); }
302 virtual bool notify(std::string
const&
text, std::string
const& role) {
303 static_cast<void>(
text);
304 static_cast<void>(role);
312 static_cast<void>(reply);
324 static_cast<void>(name);
325 static_cast<void>(value);
335 std::function<
void(std::expected<Value, std::string>)> done) {
336 static_cast<void>(file);
338 static_cast<void>(done);
350 std::function<
void(std::expected<Value, std::string>)> done) {
351 return upload(request.file, std::move(
progress), std::move(done));
361 std::function<
void(std::expected<Value, std::string>)> done) {
362 static_cast<void>(file);
363 static_cast<void>(option);
364 static_cast<void>(name);
365 static_cast<void>(done);
376 std::function<
void(std::expected<InlineFile, std::string>)> done) {
377 static_cast<void>(file);
378 static_cast<void>(maxBytes);
379 static_cast<void>(done);
389 std::function<
void(std::expected<Value, std::string>)> done) {
390 static_cast<void>(file);
391 static_cast<void>(option);
392 static_cast<void>(done);
401 static_cast<void>(screen);
402 static_cast<void>(point);
403 static_cast<void>(value);
418using Watcher = std::function<async::Completion<std::string>(std::string
const& kind, std::string
const& bodyJson)>;
426 std::function<std::chrono::milliseconds()>
clock;
428 std::chrono::milliseconds
keepUnused{std::chrono::minutes{5}};
434 std::function<std::string(std::exception_ptr
const&)>
classify;
450[[nodiscard]]
inline std::string
failureText(std::exception_ptr
const& error) {
452 std::rethrow_exception(error);
453 }
catch (std::exception
const& failure) {
454 return failure.what();
456 return "unknown error";
465 std::rethrow_exception(error);
473 return "backendChanged";
483 return glz::write_json(
valueToJson(value)).value_or(std::string{});
501 fields.insert_or_assign(path.front(),
withPath(inner, path.subspan(1), std::move(value)));
502 return Value{std::move(fields)};
509 Value const*
const found =
row.field(
"id");
510 return found ==
nullptr ?
Value{} : *found;
520 if (fields ==
nullptr || over ==
nullptr) {
524 for (
auto const& [name, field] : *over) {
525 merged.insert_or_assign(name, field);
527 return Value{std::move(merged)};
543[[nodiscard]]
inline std::expected<Value, PatchMiss>
patchRows(
Value const& value, std::string_view listMember,
547 if (!listMember.empty() && (record ==
nullptr || !record->contains(listMember))) {
550 Value const*
const rows = record ==
nullptr ? &value : &record->find(listMember)->second;
552 if (items ==
nullptr) {
569 if (record ==
nullptr) {
570 return Value{std::move(out)};
573 patched.insert_or_assign(std::string{listMember},
Value{std::move(out)});
574 return Value{std::move(patched)};
581[[nodiscard]]
inline std::string
patchMissReason(std::string_view query, std::string_view listMember) {
582 if (listMember.empty()) {
583 return "the value of `" + std::string{query} +
584 "` is not a list, so the patch changes nothing; `at` names the record member that holds the list";
586 return "the value of `" + std::string{query} +
"` has no list at `" + std::string{listMember} +
587 "`, so the patch changes nothing";
596 if (fields.empty()) {
601 for (std::string
const& field : fields) {
604 return Value{std::move(kept)};
608 kept.reserve(items->size());
609 for (
Value const& item : *items) {
612 return Value{std::move(kept)};
620[[nodiscard]]
inline std::vector<std::string>
pathMembers(std::string_view path) {
621 std::vector<std::string> members;
622 for (
auto const part : std::views::split(path,
'.')) {
624 members.emplace_back(part.begin(), part.end());
639inline void keyedRowsAt(
Value const& value, std::span<std::string const> path, std::string_view keyField,
640 std::vector<Value const*>& out) {
642 for (
Value const& item : *items) {
648 if (record ==
nullptr) {
652 if (
auto const found = record->find(path.front()); found != record->end()) {
653 keyedRowsAt(found->second, path.subspan(1), keyField, out);
657 if (record->contains(keyField)) {
658 out.push_back(&value);
661 for (
auto const& [name, item] : *record) {
662 static_cast<void>(name);
663 if (
ValueRecord const*
const row = item.asRecord();
row !=
nullptr &&
row->contains(keyField)) {
664 out.push_back(&item);
670using RowsByKey = std::map<std::string, Value const*, std::less<>>;
679[[nodiscard]]
inline std::optional<Value>
patchKeyedRows(
Value const& value, std::span<std::string const> path,
680 std::string_view keyField,
RowsByKey const& rows) {
683 bool changed =
false;
684 for (
Value& item : out) {
686 item = std::move(*patched);
690 return changed ? std::optional<Value>{
Value{std::move(out)}} : std::nullopt;
693 if (record ==
nullptr) {
697 auto const found = record->find(path.front());
698 if (found == record->end()) {
701 auto patched =
patchKeyedRows(found->second, path.subspan(1), keyField, rows);
706 out.insert_or_assign(path.front(), std::move(*patched));
707 return Value{std::move(out)};
709 if (
auto const key = record->find(keyField); key != record->end()) {
711 return row == rows.end() ? std::nullopt : std::optional<Value>{
mergeRow(value, *
row->second)};
714 bool changed =
false;
715 for (
auto& [name, item] : out) {
716 static_cast<void>(name);
717 if (
ValueRecord const*
const row = item.asRecord();
row !=
nullptr &&
row->contains(keyField)) {
719 item = std::move(*patched);
724 return changed ? std::optional<Value>{
Value{std::move(out)}} : std::nullopt;
796 return found ==
records.end() ? nullptr : &*found;
805[[nodiscard]]
inline std::vector<Value const*>
keyedRowsOf(
Value const& value, std::vector<std::string>
const& paths,
806 std::string_view keyField) {
807 std::vector<Value const*> rows;
808 for (std::string
const& path : paths) {
809 std::vector<std::string>
const members =
pathMembers(path);
821[[nodiscard]]
inline std::optional<Value>
patchHeldRows(
Value const& value, std::vector<std::string>
const& paths,
822 std::string_view keyField,
RowsByKey const& rows) {
823 std::optional<Value> patched;
824 for (std::string
const& path : paths) {
825 std::vector<std::string>
const members =
pathMembers(path);
826 auto next = patched ?
patchKeyedRows(*patched, members, keyField, rows)
829 patched = std::move(next);
864 enum class Kind : std::uint8_t {
896 _options{checkedOptions(std::move(
options))},
908 ~Session() =
default;
920 void setPermitted(std::set<std::string, std::less<>> permitted) { _permitted.set(std::move(permitted)); }
928 [[nodiscard]]
bool can(std::string_view action)
const {
929 static constexpr std::string_view kHostPrefix =
"host:";
930 if (action.starts_with(kHostPrefix)) {
931 return _hostCommandNames.get().contains(action.substr(kHostPrefix.size()));
933 return _permitted.get().contains(action);
948 next.
locale = std::move(locale);
976 _hostCommandRunner = std::move(commands.
run);
977 _hostCommandNames.set(std::move(commands.
available));
984 return _hostCommandRunner;
998 auto const found = values.find(name);
999 return found == values.end() ?
Value{} : found->second;
1013 _runtime->batch([
this] {
1014 for (
auto const& [key, entry] : _entries) {
1015 static_cast<void>(key);
1016 ++entry->generation;
1017 entry->evict.cancel();
1018 countTopics(*entry, -1);
1022 detachQuery(*query);
1048 void prefetch(std::shared_ptr<ModelChannel>
const& channel, std::string
const& token, std::string
const& typeId,
1049 std::string
const& action,
Value const& body) {
1050 auto entry =
acquire(token, typeId, action,
false, body);
1051 if (!entry->pending.peek() && (!entry->hasValue.peek() || entry->stale)) {
1052 fetch(entry, channel);
1059 [[nodiscard]] std::size_t
cacheSize() const noexcept {
return _entries.size(); }
1063 [[nodiscard]] std::string
privateToken() {
return "private#" + std::to_string(++_privateTokens); }
1070 void noteWrite() noexcept { _lastWriteUser = _userDepth > 0; }
1074 void asUser(std::function<
void()>
const& body) {
1092 [[nodiscard]]
static std::string
cacheKey(std::string
const& token, std::string
const& typeId,
1093 std::string
const& action,
bool evaluate, std::string
const& body) {
1094 return typeId +
'\x1f' + token +
'\x1f' + (
evaluate ?
"evaluate:" :
"") + action +
'\x1f' + body;
1104 std::shared_ptr<detail::CacheEntry>
acquire(std::string
const& token, std::string
const& typeId,
1105 std::string
const& action,
bool evaluate,
Value const& body) {
1108 auto found = _entries.find(key);
1109 if (found == _entries.end()) {
1110 auto entry = std::make_shared<detail::CacheEntry>(*_runtime);
1112 entry->typeId = typeId;
1113 entry->action = action;
1116 entry->instanceToken = token;
1117 if (_options.catalog.action) {
1118 if (
auto const info = _options.catalog.action(action)) {
1119 entry->secretFields = info->secretFields;
1121 entry->records = info->records;
1125 countTopics(*entry, +1);
1126 found = _entries.emplace(std::move(key), std::move(entry)).first;
1128 ++found->second->users;
1129 found->second->evict.cancel();
1130 return found->second;
1135 void release(std::shared_ptr<detail::CacheEntry>
const& entry) {
1136 if (--entry->users > 0 || _options.scheduler ==
nullptr) {
1139 std::weak_ptr<detail::CacheEntry>
const weak = entry;
1140 entry->evict = _options.scheduler->after(_options.keepUnused, [
this, weak] {
1141 auto const held = weak.lock();
1142 if (held && held->users == 0) {
1143 countTopics(*held, -1);
1144 _entries.erase(held->key);
1153 void fetch(std::shared_ptr<detail::CacheEntry>
const& entry, std::shared_ptr<ModelChannel>
const& channel) {
1157 std::uint64_t
const generation = ++entry->generation;
1158 if (channel ==
nullptr) {
1159 settleFailure(*entry,
"the model alias has no instance",
"error");
1162 entry->pending.set(
true);
1163 std::weak_ptr<detail::CacheEntry>
const weak = entry;
1165 auto completion = channel->execute(entry->action, entry->body,
CallOptions{.evaluate = entry->evaluate});
1167 .then([
this, weak, generation](std::string
const& reply) {
1168 auto const held = weak.lock();
1169 if (held && held->generation == generation) {
1170 settleReply(*held, reply);
1173 .onError([
this, weak, generation](std::exception_ptr
const& error) {
1174 auto const held = weak.lock();
1175 if (held && held->generation == generation) {
1180 std::exception_ptr
const error = std::current_exception();
1199 shown = std::move(*patched);
1203 entry.
value.set(std::move(shown));
1209 [[nodiscard]] std::string
classify(std::exception_ptr
const& error)
const {
1215 [[nodiscard]] std::chrono::milliseconds
now()
const {
1216 if (_options.clock) {
1217 return _options.clock();
1219 return std::chrono::duration_cast<std::chrono::milliseconds>(
1220 std::chrono::steady_clock::now().time_since_epoch());
1244 void actionSucceeded(std::string
const& typeId, std::string
const& actionId,
Value const& reply,
ReplyUse use,
1245 std::string
const& instanceToken);
1290 throw std::invalid_argument{
"morph::ui::Session: maxInlineFileBytes is " +
1299 auto value = json ?
valueFromJson(*json) : std::unexpected(json.error());
1301 settleFailure(entry,
"the reply is not a document value: " + value.error(),
"error");
1304 _runtime->batch([&] {
1305 confirm(entry, *value);
1306 entry.hasValue.set(
true);
1307 entry.error.set({});
1308 entry.errorKind.set({});
1309 entry.pending.set(
false);
1310 entry.fetchedAt = now();
1311 entry.stale = entry.refetchAfter;
1312 if (entry.refetchAfter) {
1313 entry.refetchAfter =
false;
1314 refetchMounted(entry);
1329 void applyReplyEffects(std::string
const& typeId, std::string
const& actionId,
Value const& reply,
ReplyUse use,
1330 std::string
const& instanceToken);
1340 void flushWatches();
1345 void sendWatch(std::string_view kind, wire::WatchBody
const& body)
const;
1348 void markAllStale();
1368 void markRecordChange(effects::RecordChange
const& change,
detail::Marks& marks);
1373 void markInstanceChange(wire::InstanceTopic
const& instance,
detail::Marks& marks);
1376 _runtime->batch([&] {
1378 entry.error.set(std::move(
text));
1379 entry.errorKind.set(std::move(kind));
1380 entry.pending.set(
false);
1386 reactive::Runtime* _runtime;
1388 reactive::Signal<std::set<std::string, std::less<>>> _permitted;
1389 std::map<std::string, int, std::less<>> _recordRefs;
1390 std::map<std::pair<std::string, std::string>,
int> _instanceRefs;
1391 std::set<std::string, std::less<>> _watchRecords;
1392 std::set<std::string, std::less<>> _unwatchRecords;
1393 std::set<std::pair<std::string, std::string>> _watchInstances;
1394 std::set<std::pair<std::string, std::string>> _unwatchInstances;
1395 reactive::Signal<std::shared_ptr<FormatContext const>> _format;
1396 reactive::Signal<ValueRecord> _env;
1397 reactive::Signal<ValueRecord> _sessionValues;
1398 reactive::Signal<ValueRecord> _hostValues;
1400 std::vector<detail::ScreenCore*> _screens;
1401 reactive::Signal<std::set<std::string, std::less<>>> _hostCommandNames;
1403 std::map<std::string, std::shared_ptr<detail::CacheEntry>, std::less<>> _entries;
1404 std::vector<detail::QueryRuntime*> _mounted;
1406 bool _lastWriteUser =
false;
1407 std::uint64_t _privateTokens = 0;
1451 std::map<std::string, BorrowedModel, std::less<>>
borrowed;
1486 std::unique_ptr<reactive::Signal<Value>>
value;
1530 open{runtime, false},
1535 inner{runtime, nullptr} {}
1575 std::unique_ptr<forms::FormSession>
form;
1581 std::unique_ptr<forms::WizardSession>
wizard;
1585struct CheckRuntime {
1595 CheckRuntime() =
default;
1596 CheckRuntime(CheckRuntime
const&) =
delete;
1597 CheckRuntime& operator=(CheckRuntime
const&) =
delete;
1598 CheckRuntime(CheckRuntime&&) =
delete;
1599 CheckRuntime& operator=(CheckRuntime&&) =
delete;
1604 std::erase(target->checks,
this);
1605 target->checksVersion.set(target->checksVersion.peek() + 1);
1644class RtScope :
public std::enable_shared_from_this<RtScope> {
1666 for (
RtScope const* scope =
this; scope !=
nullptr; scope = scope->parent.get()) {
1667 auto const found = scope->names.find(name);
1668 if (found != scope->names.end()) {
1669 return &found->second;
1678 for (
RtScope const* scope =
this; scope !=
nullptr; scope = scope->parent.get()) {
1679 if (scope->row !=
nullptr) {
1691 scope = scope->
parent.get();
1698 [[nodiscard]]
bool ready()
const;
1713 std::map<std::string, NameRef, std::less<>>
names;
1715 std::vector<std::unique_ptr<StateSlot>>
states;
1721 std::vector<std::unique_ptr<ModelAlias>>
models;
1723 std::vector<std::unique_ptr<QueryRuntime>>
queries;
1727 std::vector<std::unique_ptr<CheckRuntime>>
checks;
1740 : _scope{&scope}, _bindings{bindings}, _row{
row} {}
1746 [[nodiscard]]
Value ref(std::string_view head, std::span<std::string const> path)
const override;
1751 [[nodiscard]]
Value row(std::span<std::string const> path)
const override {
1752 if (_row !=
nullptr) {
1753 return walk(*_row, path);
1756 return current ==
nullptr ?
Value{} :
walk(current->get(), path);
1763 [[nodiscard]]
Value control(
ExprKind form, std::string_view name)
const override;
1769 [[nodiscard]]
Value pref(std::string_view point, std::span<std::string const> path)
const override;
1775 [[nodiscard]]
Value client(
ExprKind form, std::string_view name)
const override;
1779 [[nodiscard]]
FormatContext const& formatting()
const override;
1784 void report(std::string_view path, std::string_view reason)
const override;
1801 ScopeEnv const env{scope, bindings};
1812 if (std::string
const*
const text = value.
asString()) {
1813 return !
text->empty();
1816 return !items->empty();
1830 if (std::string
const*
const text = value.
asString()) {
1833 if (
bool const*
const flag = value.
asBool()) {
1834 return *flag ?
"true" :
"false";
1837 return formatNumber(value, format.numeric).value_or(std::string{}) +
" " +
1840 if (
auto number =
formatNumber(value, format.numeric)) {
1841 return *std::move(number);
1844 return instant->toIso8601();
1853 if (std::int64_t
const*
const number = value.
asInt()) {
1854 return Key{*number};
1856 if (std::string
const*
const text = value.
asString()) {
1866 if (std::int64_t
const*
const number = std::get_if<std::int64_t>(&key)) {
1867 return Value{*number};
1869 return Value{std::get<std::string>(key)};
1876 std::vector<std::string> out;
1877 if (std::string
const*
const text = value.
asString()) {
1878 out.push_back(*
text);
1880 for (
Value const& item : *items) {
1881 if (std::string
const*
const element = item.asString()) {
1882 out.push_back(*element);
1894 items.reserve(texts.size());
1895 for (std::string
const&
text : texts) {
1896 items.emplace_back(
text);
1898 return Value{std::move(items)};
1927 [[nodiscard]]
bool pending()
const;
1940 [[nodiscard]]
bool settled()
const;
1953 if (
auto const& current = _entry.peek()) {
1954 current->stale =
true;
1961 _entry.set(
nullptr);
1962 _carried.set(
Value{});
1968 [[nodiscard]] std::shared_ptr<CacheEntry>
const&
entry()
const {
return _entry.get(); }
1976 [[nodiscard]] std::string
const&
typeId() const noexcept {
return _typeId; }
1980 [[nodiscard]] std::shared_ptr<ModelChannel> channel()
const;
1988 std::string bodyText;
1989 bool operator==(Target
const& other)
const {
1990 return idle == other.idle && token == other.token && bodyText == other.bodyText;
1994 void switchTo(Target
const& target);
1996 QueryDecl
const* _decl;
1997 std::string _typeId;
2004 Target _pendingTarget;
2009 std::shared_ptr<bool> _alive = std::make_shared<bool>(
true);
2036 void run(
Value body, std::shared_ptr<Bindings const> bindings,
RunDone done);
2040 [[nodiscard]]
bool pending()
const {
return _inFlight.get() > 0; }
2060 [[nodiscard]] std::string
const&
typeId() const noexcept {
return _typeId; }
2064 std::uint64_t
id = 0;
2066 std::shared_ptr<Bindings const> bindings;
2068 std::string serialKey;
2069 std::vector<std::shared_ptr<CacheEntry>> layered;
2072 void send(std::shared_ptr<Run>
const& run);
2073 void settle(std::shared_ptr<Run>
const& run, std::optional<Value> reply, std::exception_ptr
const& error);
2074 void next(std::string
const& serialKey);
2075 void applyOptimistic(Run& run);
2076 void dropLayers(Run& run);
2081 std::string _typeId;
2087 std::uint64_t _runs = 0;
2088 std::uint64_t _latest = 0;
2089 std::map<std::string, std::deque<std::shared_ptr<Run>>, std::less<>> _queues;
2090 std::set<std::string, std::less<>> _busy;
2091 std::shared_ptr<bool> _alive = std::make_shared<bool>(
true);
2125 kept.
scope =
nullptr;
2130using Done = std::function<void(
bool)>;
2184 if (spec.
key == key) {
2196 static constexpr std::array<std::string_view, 12> kStateful = {
2197 "textInput",
"checkbox",
"select",
"dateTimeInput",
"slider",
"filePicker",
2198 "dialog",
"drawer",
"table",
"form",
"collection",
"wizard"};
2199 return listed(kStateful, kind);
2234 return "switchBackend";
2236 return "resetPrefs";
2248 return "cancelClose";
2286 _doc{std::move(
doc)} {
2288 for (
auto const& [name, point] : _doc->customize) {
2289 defaults.insert_or_assign(name, point.defaultValue);
2292 prefs.set(std::move(defaults));
2306 auto const found = _doc->lifecycle.find(key);
2308 if (found == _doc->lifecycle.end() || !scope) {
2312 .bindings = std::make_shared<Bindings>(),
2313 .cancelClose = std::make_shared<bool>(
false),
2314 .boundary =
nullptr};
2315 runList(found->second, ctx, {});
2329 void report(std::string_view path, std::string_view reason) {
2333 std::string key = std::string{path} +
'\x1f' + std::string{reason};
2334 if (!
reported.insert(std::move(key)).second) {
2337 Refusal const refusal{.document = _doc->id, .path = std::string{path}, .reason = std::string{reason}};
2355 void declare(
RtScope& scope,
ScopeDecl const* decl, std::vector<DocNode const*>
const& content,
2356 std::vector<Commands const*>
const& commands);
2362 if (
slot.value ||
slot.initializing) {
2365 if (std::optional<Value> keptValue =
keptState(scope, *
slot.decl)) {
2366 slot.value = std::make_unique<reactive::Signal<Value>>(
runtime(), std::move(*keptValue));
2369 slot.initializing =
true;
2371 slot.value = std::make_unique<reactive::Signal<Value>>(
runtime(), std::move(init));
2372 slot.initializing =
false;
2379 [[nodiscard]]
Value elementValue(
ElementState const& element, std::string_view name)
const;
2405 if (commands ==
nullptr || commands->empty()) {
2408 auto bindings = std::make_shared<Bindings>(*ctx.
bindings);
2409 bindings->push_back(
Binding{.name =
"event", .read = [
event = std::move(event)] {
return event; }});
2411 .bindings = std::move(bindings),
2412 .cancelClose = std::make_shared<bool>(
false),
2422 [[nodiscard]]
ActionModels lowerActionParts(std::span<DocNodePtr const> parts,
LowerCtx const& ctx);
2434 [[nodiscard]] std::vector<BadgeModel> lowerBadgeParts(std::span<DocNodePtr const> parts,
LowerCtx const& ctx);
2450 if (nodes.size() == 1) {
2451 return lower(*nodes.front(), ctx);
2474 std::map<std::string, BorrowedModel, std::less<>>
borrowed;
2533 return std::nullopt;
2538 return std::nullopt;
2540 return found->second.value;
2546 void collect(
DocNode const& node,
RtScope& scope, std::vector<Commands const*>& commands);
2553 std::optional<Tone> rowToneOf(
Value const& value, std::string
const& path,
bool& noted);
2560 CustomProp componentProp(std::string
const& prop, std::string
const& type, std::function<
Value()> read);
2564 std::string_view valueType, std::string
const& payload);
2566 [[nodiscard]] std::string arrangedBy(std::string
const& elementId)
const;
2568 Axis axis,
int gap);
2569 Node lowerArrangeEditor(
DocNode const& node,
LowerCtx const& ctx, std::string
const& point,
2576 void wireComposite(ElementState& element);
2577 Node lowerComposite(
DocNode const& node, LowerCtx
const& ctx, ElementState* element);
2580 std::vector<DocNode const*>
const& content);
2581 void commonOf(
DocNode const& node, LowerCtx
const& ctx, ElementState* element,
Common& common);
2583 void fieldOf(
DocNode const& node, LowerCtx
const& ctx, ElementState* element,
FieldState& field);
2584 std::vector<MenuItem> menuItems(std::span<DocNodePtr const> parts, LowerCtx
const& ctx);
2585 void ownQueries(
DocNode const& node, RtScope
const& scope, BoundaryRuntime& boundary,
2586 std::set<std::string, std::less<>>& seen);
2587 void ownQueriesOf(
ExprPtr const& expr, RtScope
const& scope, BoundaryRuntime& boundary,
2588 std::set<std::string, std::less<>>& seen);
2589 void runMutation(
Command const& command, CommandCtx
const& ctx, Done done)
const;
2590 void runSet(
Command const& command, CommandCtx
const& ctx, Done
const& done);
2591 void runSetApp(
Command const& command, std::span<std::string const> rest,
Value value, Done
const& done);
2592 void writeSlot(StateSlot&
slot, std::span<std::string const> within,
Value value,
Command const& command,
2594 void runReset(
Command const& command, CommandCtx
const& ctx, Done
const& done);
2595 void runHostCommand(
Command const& command, CommandCtx
const& ctx, Done
const& done);
2596 void runTransfer(
Command const& command, CommandCtx
const& ctx, Done done);
2597 void runDownload(
Command const& command, CommandCtx
const& ctx,
Value const& file, Done done);
2599 std::optional<InlineFile> inlineDownload(
Command const& command, CommandCtx
const& ctx,
Value const& file,
2601 void runUpload(
Command const& command, CommandCtx
const& ctx,
Value const& file, Done done);
2602 bool unsupported(
Command const& command, std::string_view what) {
2603 report(command.path, std::string{what} +
" is not supported by the host");
2608 template <
typename T,
typename Convert>
2609 Prop<T> bindProp(LowerCtx
const& ctx, ExprPtr
const& expr, T fallback, Convert convert) {
2611 return Prop<T>{std::move(fallback)};
2613 if (expr->kind == ExprKind::Literal) {
2616 return Prop<T>{ui::slot(
2617 nextSlot(), [scope = ctx.scope, bindings = ctx.bindings, expr, convert, session = &session]() -> T {
2618 return convert(evalIn(*scope, bindings.get(), expr), session->formatting());
2624 Prop<TextRole> roleProp(LowerCtx
const& ctx, ExprPtr
const& expr);
2627 template <
typename T,
typename Read>
2628 Prop<T> slotProp(Read read) {
2629 return Prop<T>{ui::slot(nextSlot(), std::move(read))};
2633 [[nodiscard]] Value once(LowerCtx
const& ctx, ExprPtr
const& expr)
const {
2634 return runtime().untracked([&] {
return evalIn(*ctx.scope, ctx.bindings.get(), expr); });
2638 void logList(Commands
const& commands) {
2639 CommandLogEntry entry{.event = {}, .commands = {}, .batch = runtime().core()->currentBatch()};
2640 std::string_view
const first = commands.front().path;
2641 entry.event = std::string{first.substr(0, first.rfind(
'/') == std::string_view::npos ? 0 : first.rfind(
'/'))};
2642 for (Command
const& command : commands) {
2643 entry.commands.push_back(
2646 if (commandLog.size() == kCommandLogSize) {
2647 commandLog.pop_front();
2649 commandLog.push_back(std::move(entry));
2652 std::shared_ptr<Document const> _doc;
2655 std::shared_ptr<bool> _alive = std::make_shared<bool>(
true);
2698 Screen(Screen
const&) =
delete;
2699 Screen& operator=(Screen
const&) =
delete;
2700 Screen(Screen&&) =
delete;
2701 Screen& operator=(Screen&&) =
delete;
2706 if (
Commands const*
const commands = lifecycleList(
"onUnmount")) {
2707 runCommands(*commands);
2711 _core->focusElement = {};
2713 _headActions.clear();
2714 _headStatus.clear();
2722 throw std::logic_error{
"the screen is not mounted"};
2724 return _mounted->root();
2729 [[nodiscard]] std::string
const&
id() const noexcept {
return _core->doc().id; }
2757 [[nodiscard]] std::vector<HeadBadge>
status()
const {
2758 std::vector<HeadBadge> shown;
2760 if (
badge.visible.evaluate()) {
2762 .text =
badge.text.evaluate(), .tone =
badge.tone.evaluate(), .icon =
badge.icon.evaluate()});
2770 [[nodiscard]] std::vector<HeadAction>
actions()
const {
2771 std::vector<HeadAction> shown;
2773 if (action.visible.evaluate()) {
2775 .label = action.label.evaluate(),
2776 .icon = action.icon.evaluate(),
2777 .emphasis = action.emphasis,
2778 .enabled = action.enabled.evaluate()});
2790 if (found == _headActions.end()) {
2791 _core->report(
"/actions",
"head action " + std::string{actionId} +
" is unknown");
2794 auto const [visible, enabled] =
2795 runtime().untracked([&] {
return std::pair{found->visible.evaluate(), found->enabled.evaluate()}; });
2796 if (!visible || !enabled) {
2797 _core->report(found->path,
"head action " + found->id + (visible ?
" is disabled" :
" is hidden"));
2810 bool const*
const flag = value.
asBool();
2811 return flag !=
nullptr && *flag;
2818 [[nodiscard]]
bool pending()
const {
return _core->pendingWork.get() > 0; }
2827 std::shared_ptr<bool const>
const cancelled =
beginClose();
2828 return cancelled ==
nullptr || !*cancelled;
2837 Commands const*
const commands = lifecycleList(
"onClose");
2838 if (commands ==
nullptr) {
2841 return runCommandsFlag(*commands);
2847 [[nodiscard]]
bool declares(std::string_view key)
const {
return lifecycleList(key) !=
nullptr; }
2852 if (
Commands const*
const commands = lifecycleList(key)) {
2853 runCommands(*commands);
2859 [[nodiscard]] std::vector<Refusal>
const&
diagnostics() const noexcept {
return _core->diagnostics; }
2867 auto const found = _core->doc().customize.find(point);
2868 if (found == _core->doc().customize.end() || !fits(found->second, value)) {
2872 prefs.insert_or_assign(point, std::move(value));
2873 _core->prefs.set(std::move(prefs));
2884 [[nodiscard]]
BorrowedModel lend(std::string_view alias)
const;
2893 [[nodiscard]] std::map<std::string, detail::ScreenCore::KeptState, std::less<>> ownState()
const {
2894 std::map<std::string, detail::ScreenCore::KeptState, std::less<>> states;
2895 for (
auto const&
slot : _scope->states) {
2897 states.insert_or_assign(
2905 [[nodiscard]]
Commands const* lifecycleList(std::string_view key)
const {
2906 auto const found = _core->doc().lifecycle.find(key);
2907 return found == _core->doc().lifecycle.end() ? nullptr : &found->second;
2911 bool runCommands(Commands
const& commands) {
return *runCommandsFlag(commands); }
2914 std::shared_ptr<bool const> runCommandsFlag(Commands
const& commands) {
2915 detail::CommandCtx
const ctx{.scope = _scope,
2916 .bindings = std::make_shared<detail::Bindings>(),
2917 .cancelClose = std::make_shared<bool>(
false),
2918 .boundary =
nullptr};
2919 _core->runtime().batch([&] { _core->runList(commands, ctx, {}); });
2920 return ctx.cancelClose;
2923 [[nodiscard]]
static bool fits(CustomizePoint
const& point, Value
const& value) {
2924 if (point.kind ==
"choice") {
2925 std::string
const*
const text = value.asString();
2926 return text !=
nullptr &&
listed(std::span<std::string const>{point.options}, *
text);
2928 if (point.kind ==
"value") {
2929 return point.type.admits(value);
2934 [[nodiscard]]
static bool listed(std::span<std::string const> options, std::string
const& text) {
2935 return std::ranges::find(options, text) != options.end();
2938 std::unique_ptr<detail::ScreenCore> _core;
2939 std::shared_ptr<detail::RtScope> _scope;
2940 std::optional<Mounted> _mounted;
2942 std::vector<detail::BadgeModel> _headStatus;
2943 detail::ActionModels _headActions;
2954 AppRoot(AppRoot
const&) =
delete;
2955 AppRoot& operator=(AppRoot
const&) =
delete;
2956 AppRoot(AppRoot&&) =
delete;
2957 AppRoot& operator=(AppRoot&&) =
delete;
2962 if (
auto const found = _core->doc().lifecycle.find(
"onUnmount"); found != _core->doc().lifecycle.end()) {
2963 runCommands(found->second);
2967 _core->session.setAppScope(
nullptr);
2984 [[nodiscard]] std::vector<Refusal>
const&
diagnostics() const noexcept {
return _core->diagnostics; }
2991 void runCommands(
Commands const& commands) {
2993 .bindings = std::make_shared<detail::Bindings>(),
2994 .cancelClose = std::make_shared<bool>(
false),
2995 .boundary =
nullptr};
2996 _core->runtime().batch([&] { _core->runList(commands, ctx, {}); });
2999 std::unique_ptr<detail::ScreenCore> _core;
3000 std::shared_ptr<detail::RtScope> _scope;
3012 : _session{&session}, _dispatcher{&dispatcher}, _options{std::move(options)} {}
3024 [[nodiscard]] std::expected<std::unique_ptr<Screen>, Refusal> mount(std::shared_ptr<Document const> doc,
3025 ValueRecord
const& params,
3026 IViewBackend& backend,
3027 MountOptions
const& options = {});
3034 [[nodiscard]] std::expected<std::unique_ptr<AppRoot>, Refusal> mountApp(std::shared_ptr<Document const> doc);
3038 Dispatcher* _dispatcher;
3039 InterpreterOptions _options;
3047 _runtime->batch([
this, &backend] {
3048 std::vector<detail::ScreenCore*>
const screens = _screens;
3050 if (std::ranges::find(_screens, screen) != _screens.end()) {
3051 screen->runLifecycleOf(
"onBackendChange");
3055 env.insert_or_assign(
"backend",
Value{backend});
3056 _env.set(std::move(env));
3057 for (
auto const& [key, entry] : _entries) {
3058 static_cast<void>(key);
3061 std::vector<detail::QueryRuntime*>
const mounted = _mounted;
3063 query->refetchCurrent();
3069 std::vector<detail::QueryRuntime*>
const mounted = _mounted;
3071 if (_runtime->untracked([query] { return query->entry(); }).get() == &entry) {
3072 query->refetchCurrent();
3078inline void Session::applyReplyEffects(std::string
const& typeId, std::string
const& actionId, Value
const& reply,
3079 ReplyUse use, std::string
const& instanceToken) {
3080 if (!_options.catalog.action) {
3083 std::optional<ActionInfo>
const info = _options.catalog.action(actionId);
3084 if (!info || !info->effects) {
3087 detail::Marks marks;
3088 for (ActionEffect
const& effect : *info->effects) {
3089 if (effect.kind ==
"changesInstance") {
3090 for (
auto const& [key, entry] : _entries) {
3091 static_cast<void>(key);
3096 }
else if (effect.record && (effect.kind ==
"updates" || effect.kind ==
"changes")) {
3097 applyRecordEffect(effect, *effect.record, *info, reply, use, marks);
3100 markAndRefetch(marks);
3103inline void Session::applyRecordEffect(ActionEffect
const& effect, std::string
const& record, ActionInfo
const& info,
3104 Value
const& reply, ReplyUse use, detail::Marks& marks) {
3106 auto const own = std::ranges::find(info.records, record, &ActionRecordInfo::record);
3107 std::optional<std::string>
const ownKey =
3108 effect.kind ==
"updates" && use == ReplyUse::Applied && own != info.records.end() ? own->keyField
3110 detail::RowsByKey rows;
3112 for (Value
const*
const row : detail::keyedRowsOf(reply, own->paths, *ownKey)) {
3113 rows.insert_or_assign(detail::canonicalJson(*
row->field(*ownKey)), row);
3116 for (
auto const& [key, entry] : _entries) {
3117 static_cast<void>(key);
3118 ActionRecordInfo
const*
const held = entry->
holding(record);
3119 if (held ==
nullptr) {
3122 if (!ownKey || !held->keyField) {
3126 if (
auto patched = detail::patchHeldRows(entry->
confirmed, held->paths, *held->keyField, rows)) {
3127 confirm(*entry, *patched);
3136inline void Session::markAndRefetch(detail::Marks
const& marks) {
3137 for (detail::CacheEntry*
const entry : marks.entries) {
3140 for (detail::CacheEntry
const*
const entry : marks.entries) {
3141 refetchMounted(*entry);
3145inline void Session::countTopics(detail::CacheEntry
const& entry,
int delta) {
3146 if (entry.evaluate) {
3150 auto const count = [delta](
auto& refs,
auto const& topic,
auto& watches,
auto& unwatches) {
3151 int& refsOf = refs[topic];
3153 if (delta > 0 && refsOf == 1 && unwatches.erase(topic) == 0) {
3154 watches.insert(topic);
3155 }
else if (refsOf <= 0) {
3157 if (watches.erase(topic) == 0) {
3158 unwatches.insert(topic);
3162 if (!entry.records.empty()) {
3163 for (ActionRecordInfo
const& record : entry.records) {
3164 count(_recordRefs, record.record, _watchRecords, _unwatchRecords);
3168 constexpr std::string_view kShared =
"shared:";
3169 if (entry.instanceToken.starts_with(kShared)) {
3170 count(_instanceRefs, std::pair{entry.typeId, entry.instanceToken.substr(kShared.size())}, _watchInstances,
3175inline void Session::sendWatch(std::string_view kind, wire::WatchBody
const& body)
const {
3176 auto completion = _options.watcher(std::string{kind}, glz::write_json(body).value_or(std::string{}));
3177 completion.then([](std::string
const&) {}).onError([](std::exception_ptr
const&) {});
3180inline void Session::flushWatches() {
3181 if (!_options.watcher) {
3182 _watchRecords.clear();
3183 _unwatchRecords.clear();
3184 _watchInstances.clear();
3185 _unwatchInstances.clear();
3188 auto const take = [](
auto& records,
auto& instances) {
3189 wire::WatchBody body;
3190 body.records.assign(records.begin(), records.end());
3191 for (
auto const& [modelType, primary] : instances) {
3192 body.instances.push_back(wire::InstanceTopic{.modelType = modelType, .primary = primary, .instance = 0});
3198 if (!_watchRecords.empty() || !_watchInstances.empty()) {
3199 sendWatch(wire::kWatchKind, take(_watchRecords, _watchInstances));
3201 if (!_unwatchRecords.empty() || !_unwatchInstances.empty()) {
3202 sendWatch(wire::kUnwatchKind, take(_unwatchRecords, _unwatchInstances));
3208 for (
auto const& [record, refs] : _recordRefs) {
3209 static_cast<void>(refs);
3210 body.
records.push_back(record);
3212 for (
auto const& [topic, refs] : _instanceRefs) {
3213 static_cast<void>(refs);
3220inline void Session::markAllStale() {
3221 _runtime->batch([
this] {
3223 for (
auto const& [key, entry] : _entries) {
3224 static_cast<void>(key);
3225 if (!entry->evaluate) {
3229 markAndRefetch(marks);
3235 _watchRecords.clear();
3236 _unwatchRecords.clear();
3237 _watchInstances.clear();
3238 _unwatchInstances.clear();
3239 if (_options.watcher) {
3242 sendWatch(wire::kWatchKind, body);
3248 if (event.topic == wire::kResyncTopic) {
3252 if (event.topic != wire::kChangesTopic) {
3255 if (
auto changes = wire::readChangesBody(event.payload.str)) {
3261 _runtime->batch([&] {
3264 markRecordChange(change, marks);
3267 markInstanceChange(instance, marks);
3269 markAndRefetch(marks);
3275 std::set<std::string, std::less<>> keys;
3276 for (glz::raw_json
const& key : change.
keys.value_or(std::vector<glz::raw_json>{})) {
3278 auto value = json ?
valueFromJson(*json) : std::unexpected(json.error());
3283 for (
auto const& [name, entry] : _entries) {
3284 static_cast<void>(name);
3285 ActionRecordInfo
const*
const held = entry->holding(change.
record);
3286 if (held ==
nullptr) {
3290 if (!change.
keys || !held->keyField || !entry->hasValue.peek()) {
3294 std::string
const& keyField = *held->keyField;
3295 bool const holdsKey = std::ranges::any_of(
3296 detail::keyedRowsOf(entry->confirmed, held->paths, keyField),
3297 [&](Value
const* row) { return keys.contains(detail::canonicalJson(*row->field(keyField))); });
3304inline void Session::markInstanceChange(wire::InstanceTopic
const& instance, detail::Marks& marks) {
3305 std::string
const shared =
"shared:" + instance.primary;
3306 for (
auto const& [name, entry] : _entries) {
3307 static_cast<void>(name);
3308 if (entry->evaluate || entry->typeId != instance.modelType) {
3313 instance.primary.empty() ? !entry->instanceToken.starts_with(
"shared") : entry->instanceToken == shared;
3321 ReplyUse use, std::string
const& instanceToken) {
3322 _runtime->batch([&] { applyReplyEffects(typeId, actionId, reply, use, instanceToken); });
3336 if (std::int64_t
const*
const number = key.
asInt()) {
3337 return std::to_string(*number);
3350 std::string
const token = core.runtime().untracked([&] {
return alias.
token->
get(); });
3351 if (token.empty()) {
3357 std::string
const& type = alias.
decl->
type;
3358 switch (alias.
mode) {
3360 auto const found = core.borrowed.find(alias.
decl->
name);
3361 alias.
channel = found == core.borrowed.end() ? nullptr : found->second.channel;
3367 alias.
channel = core.dispatcher.bind(type, bridge::BindSharing::Private, {});
3370 alias.
channel = core.dispatcher.bind(type, bridge::BindSharing::Shared, token.substr(7));
3375 alias.
channel = core.dispatcher.bind(type, bridge::BindSharing::Private, {});
3384 :
core{screen},
parent{std::move(enclosing)},
row{current},
nodes{screen.runtime(), depth} {
3385 if (
core.recording) {
3386 core.scopes.push_back(this);
3391 if (
core.recording) {
3392 std::erase(
core.scopes,
this);
3394 if (
owner !=
nullptr) {
3395 core.addPending(-1);
3396 if (
owner->inner.peek() ==
this) {
3397 owner->inner.set(
nullptr);
3404 for (
auto const& element :
elements) {
3405 Value const shown =
core.elementValue(*element,
"visible");
3406 bool const*
const visible = shown.
asBool();
3407 ValueList const*
const errors = element->errors->
get().asList();
3408 if ((visible ==
nullptr || *visible) && errors !=
nullptr && !errors->empty()) {
3412 for (
auto const& check :
checks) {
3413 if (check->decl->blocking && check->failing->get()) {
3422 if (head ==
"app") {
3424 if (app ==
nullptr || path.empty()) {
3427 return ScopeEnv{*app,
nullptr}.ref(path.front(), path.subspan(1));
3429 if (_bindings !=
nullptr) {
3430 for (
Binding const& bound : std::views::reverse(*_bindings)) {
3431 if (bound.name == head) {
3432 return walk(bound.read(), path);
3436 NameRef const*
const name = _scope->find(head);
3437 if (name ==
nullptr) {
3448 return walk(name->
let->get(), path);
3455 return walk(_scope->core.elementValue(*name->
element, path.front()), path.subspan(1));
3462 if (path.front() ==
"progress") {
3464 }
else if (path.front() ==
"pending") {
3466 }
else if (path.front() ==
"error") {
3467 value = transfer.
error.get();
3469 return walk(value, path.subspan(1));
3480 Session const& session = _scope->core.session;
3481 NameRef const*
const found = _scope->find(name);
3489 return session.can(name);
3494 return query.
value();
3500 return query.
error();
3507 return mutation.
result();
3513 return mutation.
error();
3522 auto const found = prefs.find(point);
3523 return found == prefs.end() ?
Value{} :
walk(found->second, path);
3527 return _scope->core.session.client(form, name);
3533 _scope->core.report(path, reason);
3540 _typeId{alias->
decl->type},
3543 _entry{scope.core.runtime()},
3544 _carried{scope.core.runtime()},
3545 _waiting{scope.core.runtime(), false} {
3552 _scope->core.session.forgetQuery(*
this);
3553 if (
auto const& current = _entry.peek()) {
3554 _scope->core.session.release(current);
3566 Value const when =
evalIn(*_scope,
nullptr, _decl->when);
3567 bool const*
const flag = when.
asBool();
3568 if (flag ==
nullptr || !*flag) {
3572 std::string token = _alias->token->get();
3573 if (token.empty()) {
3580 target.idle =
false;
3581 target.token = std::move(token);
3583 target.body = std::move(body);
3588 std::weak_ptr<bool>
const alive = _alive;
3589 scope.
nodes.
effect([
this, &key, scheduler, alive, first =
true]()
mutable {
3590 Target target = key.get();
3591 _scope->core.runtime().untracked([&] {
3592 if (first || target.idle || _decl->debounce <= 0 || scheduler ==
nullptr) {
3595 _waiting.set(
false);
3599 _pendingTarget = std::move(target);
3601 _debounce = scheduler->
after(std::chrono::milliseconds{_decl->debounce}, [
this, alive] {
3602 if (alive.expired()) {
3605 _scope->core.runtime().batch([
this] {
3606 _waiting.set(
false);
3607 switchTo(_pendingTarget);
3612 if (_decl->refreshEvery > 0 && scheduler !=
nullptr) {
3613 _refresh = scheduler->
every(std::chrono::milliseconds{_decl->refreshEvery}, [
this, alive] {
3614 if (alive.expired()) {
3617 auto const current = _entry.peek();
3618 if (!current || _waiting.peek() || current->pending.peek()) {
3621 _scope->core.runtime().batch([&] { _scope->core.session.fetch(current,
channel()); });
3626inline void QueryRuntime::switchTo(Target
const& target) {
3628 std::shared_ptr<CacheEntry>
const old = _entry.peek();
3629 if (old && target == _target) {
3632 Value shown = old && old->hasValue.peek() ? old->value.peek() : _carried.peek();
3635 _carried.set(
Value{});
3636 _entry.set(
nullptr);
3638 session.release(old);
3642 auto fresh =
session.acquire(target.token, _typeId, _decl->action, _decl->evaluate, target.body);
3643 _carried.set(std::move(shown));
3648 bool const young = fresh->fetchedAt.has_value() &&
3649 session.now() - *fresh->fetchedAt < std::chrono::milliseconds{_decl->staleTime};
3650 if (!fresh->pending.peek() && (!fresh->hasValue.peek() || fresh->stale || !young)) {
3656 _scope->core.runtime().batch([
this] {
3657 bool switched =
false;
3658 if (_waiting.peek()) {
3660 _waiting.set(
false);
3661 switchTo(_pendingTarget);
3664 auto const current = _entry.peek();
3665 if (current && !(switched && current->pending.peek())) {
3666 _scope->core.session.fetch(current,
channel());
3672 if (_key ==
nullptr) {
3676 _scope->core.runtime().batch([
this] {
3678 _waiting.set(
false);
3679 Target
const target = _key->peek();
3680 if (!(_entry.peek() && target == _target)) {
3684 if (
auto const current = _entry.peek(); current && !current->pending.peek()) {
3685 _scope->core.session.fetch(current,
channel());
3691 auto const& current = _entry.get();
3693 if (current->hasValue.get()) {
3694 return current->value.get();
3696 if (!current->error.get().empty()) {
3700 return _carried.get();
3704 if (_waiting.get()) {
3707 auto const& current = _entry.get();
3708 return current && current->pending.get();
3712 auto const& current = _entry.get();
3713 if (!current || current->error.get().empty()) {
3716 return Value{current->error.get()};
3720 auto const& current = _entry.get();
3721 if (!current || current->errorKind.get().empty()) {
3724 return Value{current->errorKind.get()};
3728 auto const& current = _entry.get();
3729 return !current || current->hasValue.get() || !current->error.get().empty();
3738 _typeId{alias->
decl->type},
3740 _inFlight{scope.core.runtime(), 0},
3741 _result{scope.core.runtime()},
3742 _error{scope.core.runtime()},
3743 _errorKind{scope.core.runtime()} {}
3748 if (
int const running = _inFlight.peek(); running > 0) {
3749 _core->addPending(-running);
3756 auto run = std::make_shared<Run>();
3757 run->body = std::move(body);
3758 run->bindings = std::move(bindings);
3759 run->done = std::move(done);
3764 _inFlight.set(_inFlight.peek() + 1);
3765 _core->addPending(1);
3769 withBody.push_back(
Binding{.name =
"body", .read = [body =
run->body] {
return body; }});
3770 Value const key =
evalIn(*_scope, &withBody, _decl->serialKey);
3772 if (_busy.contains(
run->serialKey)) {
3773 _queues[
run->serialKey].push_back(
run);
3776 _busy.insert(
run->serialKey);
3781inline void MutationRuntime::send(std::shared_ptr<Run>
const& run) {
3784 applyOptimistic(*
run);
3785 std::shared_ptr<ModelChannel>
const channel =
channelOf(*_alias, _scope->
core);
3786 if (channel ==
nullptr) {
3787 settle(
run, std::nullopt, std::make_exception_ptr(std::runtime_error{
"the model alias has no instance"}));
3790 std::weak_ptr<bool>
const alive = _alive;
3792 auto completion = channel->execute(_decl->
action, canonicalJson(
run->body),
3793 CallOptions{.queueOffline = _decl->offlineQueue});
3795 .then([
this, alive,
run](std::string
const& reply) {
3796 if (alive.expired()) {
3799 auto json = parseJson(reply);
3800 auto value = json ? valueFromJson(*json) : std::unexpected(json.
error());
3802 settle(
run, std::nullopt,
3803 std::make_exception_ptr(
3804 std::runtime_error{
"the reply is not a document value: " + value.error()}));
3807 settle(
run, std::move(*value),
nullptr);
3809 .onError([
this, alive,
run](std::exception_ptr
const&
error) {
3810 if (!alive.expired()) {
3815 settle(
run, std::nullopt, std::current_exception());
3819inline void MutationRuntime::applyOptimistic(Run& run) {
3820 if (!_decl->optimistic) {
3823 NameRef
const*
const target = _scope->find(_decl->optimistic->query);
3824 if (target ==
nullptr || target->category != NameCategory::Query) {
3827 auto entry = _scope->core.runtime().untracked([&] {
return target->query->entry(); });
3832 bindings.push_back(Binding{.name =
"body", .read = [body =
run.body] {
return body; }});
3833 Value key = _scope->core.runtime().untracked([&] {
return evalIn(*_scope, &bindings, _decl->optimistic->key); });
3834 Value with = _scope->core.runtime().untracked([&] {
return evalIn(*_scope, &bindings, _decl->optimistic->with); });
3836 if (entry->hasValue.peek() &&
3837 patchRows(entry->confirmed, _decl->optimistic->at, key, with).error_or(PatchMiss::NoRow) ==
3838 PatchMiss::NoList) {
3839 _scope->core.report(
pointerAppend(_decl->path,
"optimistic"),
3842 entry->layers.push_back(
3843 OptimisticLayer{.run =
run.id, .at = _decl->optimistic->at, .key = std::move(key), .with = std::move(with)});
3844 _scope->core.session.showLayers(*entry);
3845 run.layered.push_back(std::move(entry));
3848inline void MutationRuntime::dropLayers(Run& run) {
3849 for (
auto const& entry :
run.layered) {
3850 std::erase_if(entry->layers, [&](OptimisticLayer
const& layer) { return layer.run == run.id; });
3851 _scope->core.session.showLayers(*entry);
3853 run.layered.clear();
3857inline void MutationRuntime::settle(std::shared_ptr<Run>
const& run, std::optional<Value> reply,
3858 std::exception_ptr
const& error) {
3859 ScreenCore& core = _scope->core;
3861 bool const applied = _decl->concurrency != Concurrency::Latest ||
run->id == _latest;
3862 std::shared_ptr<RtScope>
const scope = _scope->shared_from_this();
3863 auto commandCtx = [&](
Bindings extra) {
3864 return CommandCtx{.scope = scope,
3865 .bindings = std::make_shared<Bindings>(std::move(extra)),
3866 .cancelClose = std::make_shared<bool>(
false),
3867 .boundary =
nullptr};
3870 bool succeeded =
false;
3872 core.runtime().batch([&] {
3873 _inFlight.set(_inFlight.peek() - 1);
3874 _core->addPending(-1);
3878 for (
auto const& entry :
run->layered) {
3879 Value confirmed = entry->confirmed;
3880 for (OptimisticLayer
const& layer : entry->layers) {
3881 if (layer.run ==
run->id) {
3882 if (
auto patched =
patchRows(confirmed, layer.at, layer.key, layer.with)) {
3883 confirmed = std::move(*patched);
3887 std::erase_if(entry->layers, [&](OptimisticLayer
const& layer) { return layer.run == run->id; });
3888 session.confirm(*entry, confirmed);
3890 run->layered.clear();
3891 std::vector<std::string> secret;
3892 if (
session.options().catalog.action) {
3893 if (
auto const info =
session.options().catalog.action(_decl->action)) {
3894 secret = info->secretFields;
3898 std::string
const token =
3899 _alias ==
nullptr ? std::string{} : core.runtime().untracked([&] {
return _alias->token->get(); });
3901 session.actionSucceeded(_typeId, _decl->action, *reply, ReplyUse::Discarded, token);
3904 _result.set(*reply);
3905 _error.set(Value{});
3906 _errorKind.set(Value{});
3907 session.actionSucceeded(_typeId, _decl->action, *reply, ReplyUse::Applied, token);
3908 core.runList(_decl->onSuccess, commandCtx(*
run->bindings), {});
3909 if (!secret.empty()) {
3910 _result.set(delivered);
3918 _errorKind.set(Value{kind});
3919 core.runList(_decl->onError, commandCtx(*
run->bindings), {});
3921 if (kind ==
"denied" && core.host !=
nullptr) {
3922 core.host->permissionsStale();
3925 if (_decl->concurrency == Concurrency::Serial) {
3926 next(
run->serialKey);
3928 run->done(succeeded, delivered);
3931inline void MutationRuntime::next(std::string
const& serialKey) {
3932 auto const found = _queues.find(serialKey);
3933 if (found == _queues.end() || found->second.empty()) {
3934 _queues.erase(serialKey);
3935 _busy.erase(serialKey);
3938 std::shared_ptr<Run>
const queued = found->second.front();
3939 found->second.pop_front();
3940 _scope->core.runtime().batch([&] { send(queued); });
3955 for (
auto const& element : scope.
elements) {
3956 if (element->node == &node) {
3957 return element.
get();
3968 for (
Command const& command : commands) {
3970 out.push_back(command.transfer);
3979inline void ScreenCore::collect(
DocNode const& node, RtScope& scope, std::vector<Commands const*>& commands) {
3980 KindSpec
const*
const spec = findKind(node.
kind,
false);
3981 if (!node.
id.empty() || statefulKind(node.
kind)) {
3982 auto element = std::make_unique<ElementState>(
runtime());
3983 element->node = &node;
3984 element->scope = &scope;
3985 if (!node.
id.empty()) {
3987 name.category = NameCategory::Element;
3988 name.element = element.
get();
3989 scope.names.insert_or_assign(node.
id, name);
3991 wireErrors(*element);
3992 scope.elements.push_back(std::move(element));
3994 bool const createsScope = spec !=
nullptr && (spec->flags & kScope) != 0;
3995 for (
auto const& [key, list] : node.
events) {
3996 KeySpec
const*
const keyed = spec ==
nullptr ? nullptr : keySpec(*spec, key);
3997 if (!createsScope || keyed ==
nullptr || (keyed->flags & kInner) == 0) {
3998 commands.push_back(&list);
4001 for (
auto const& [chord, list] : node.
keys) {
4002 static_cast<void>(chord);
4003 commands.push_back(&list);
4005 for (
auto const& [key, nodes] : node.
children) {
4006 KeySpec
const*
const keyed = spec ==
nullptr ? nullptr :
keySpec(*spec, key);
4007 if (createsScope && keyed !=
nullptr && (keyed->flags & kInner) != 0) {
4010 for (DocNodePtr
const& child : nodes) {
4011 collect(*child, scope, commands);
4014 for (
auto const& [key, parts] : node.
parts) {
4015 static_cast<void>(key);
4016 for (DocNodePtr
const& part : parts) {
4017 KindSpec
const*
const partSpec =
findKind(part->kind,
true);
4018 if (partSpec !=
nullptr && (partSpec->flags & kScope) != 0) {
4021 for (
auto const& [event, list] : part->events) {
4022 static_cast<void>(
event);
4023 commands.push_back(&list);
4025 for (
auto const& [childKey, nodes] : part->children) {
4026 KeySpec
const*
const keyed = partSpec ==
nullptr ? nullptr :
keySpec(*partSpec, childKey);
4027 if (keyed !=
nullptr && (keyed->flags & kInner) != 0) {
4030 for (DocNodePtr
const& child : nodes) {
4031 collect(*child, scope, commands);
4041 std::vector<Commands const*>
const& commands) {
4042 std::vector<Commands const*> lists = commands;
4043 for (
DocNode const*
const node : content) {
4044 collect(*node, scope, lists);
4046 std::vector<std::string> transferIds;
4047 for (
Commands const*
const list : lists) {
4050 for (std::string
const& transferId : transferIds) {
4051 if (scope.
names.contains(transferId)) {
4058 scope.
names.insert_or_assign(transferId, name);
4060 if (decl !=
nullptr) {
4062 auto alias = std::make_unique<ModelAlias>();
4063 alias->
decl = &model;
4064 alias->
mode = model.instance;
4066 std::optional<ModelInfo>
const info =
4067 session.options().catalog.model ?
session.options().catalog.model(model.type) : std::nullopt;
4071 switch (raw->
mode) {
4075 return key.empty() ? std::string{} :
"shared:" + key;
4082 auto const found =
borrowed.find(model.name);
4083 std::string token = found ==
borrowed.end() ? std::string{} : found->second.token;
4090 std::string token =
session.privateToken();
4098 scope.
names.insert_or_assign(model.name, name);
4099 scope.
models.push_back(std::move(alias));
4105 name.
let = &scope.
nodes.
computed([&scope, expr = let.
value] { return evalIn(scope, nullptr, expr); });
4106 scope.
names.insert_or_assign(let.
name, name);
4109 auto slot = std::make_unique<StateSlot>();
4110 slot->decl = &state;
4111 slot->scope = &scope;
4115 scope.
names.insert_or_assign(state.
name, name);
4123 scope.
queries.push_back(std::make_unique<QueryRuntime>(query, scope,
aliasOf(query.
model)));
4127 scope.
names.insert_or_assign(query.
name, name);
4130 scope.
mutations.push_back(std::make_unique<MutationRuntime>(mutation, scope,
aliasOf(mutation.
model)));
4134 scope.
names.insert_or_assign(mutation.
name, name);
4140 for (
auto const& element : scope.
elements) {
4141 wireElement(*element);
4143 if (decl ==
nullptr) {
4147 auto made = std::make_unique<CheckRuntime>();
4148 made->decl = ✓
4150 Value const asserted = evalIn(scope, nullptr, expr);
4151 bool const* const flag = asserted.asBool();
4152 return flag != nullptr && !*flag;
4155 return textOf(evalIn(scope, nullptr, expr), session->formatting());
4157 for (std::string
const& target : check.
targets) {
4161 made->targets.push_back(found->
element);
4165 scope.
checks.push_back(std::move(made));
4167 for (
auto const& query : scope.
queries) {
4171 auto& source = scope.
nodes.
computed([&scope, expr = watch.
on] { return evalIn(scope, nullptr, expr); });
4172 scope.
nodes.
effect([
this, &scope, &source, watched = &watch, first =
true]()
mutable {
4173 static_cast<void>(source.get());
4178 bool const user =
session.lastWriteByUser();
4183 CommandCtx const ctx{.scope = scope.shared_from_this(),
4184 .bindings = std::make_shared<Bindings>(),
4185 .cancelClose = std::make_shared<bool>(
false),
4186 .boundary =
nullptr};
4190 runList(watched->body, ctx, {});
4218 return {.instant = *instant};
4221 if (
text ==
nullptr) {
4222 return {.instant = std::nullopt, .readable =
false};
4224 if (
text->empty()) {
4227 if (forms::detail::engine::isIsoDate(*
text)) {
4228 auto const wall = forms::detail::engine::parseWallClock(std::string{*
text} +
"T00:00");
4230 return {.instant = std::nullopt, .readable =
false};
4233 std::chrono::time_point_cast<std::chrono::milliseconds>(*wall - std::chrono::minutes{offsetMinutes});
4237 bool const readable = parsed.has_value();
4238 return {.instant = std::move(parsed), .readable = readable};
4246 auto const local = instant.
value + std::chrono::minutes{offsetMinutes};
4247 std::chrono::year_month_day
const date{std::chrono::floor<std::chrono::days>(local)};
4248 return std::format(
"{:04}-{:02}-{:02}",
static_cast<int>(date.year()),
static_cast<unsigned>(date.month()),
4249 static_cast<unsigned>(date.day()));
4261 if (kind ==
"textInput" || kind ==
"filePicker") {
4264 if (kind ==
"dateTimeInput") {
4265 return dateInstantOf(lhs, format.utcOffsetMinutes).instant ==
4275 if (option.
asRecord() ==
nullptr) {
4278 for (std::string_view
const field : {
"key",
"id",
"value"}) {
4279 if (
Value const*
const found = option.
field(field)) {
4287inline void ScreenCore::wireErrors(ElementState& element) {
4288 RtScope& scope = *element.scope;
4289 element.errors = &scope.nodes.computed([
this, &element]() ->
Value {
4290 static_cast<void>(element.checksVersion.
get());
4291 DocNode const& field = *element.node;
4292 RtScope
const& owner = *element.scope;
4293 std::vector<std::string> errors;
4294 if (
ExprPtr const required = field.
prop(
"required")) {
4295 Value const requiredNow = evalIn(owner,
nullptr, required);
4296 bool const*
const flag = requiredNow.
asBool();
4297 if (flag !=
nullptr && *flag && !engaged(
inputValue(element))) {
4298 errors.emplace_back(
"Required");
4301 for (std::string& text : stringsOf(evalIn(owner,
nullptr, field.
prop(
"errors")))) {
4302 errors.push_back(std::move(text));
4304 if (!field.
id.empty()) {
4305 std::vector<std::string>
const path{
"errors", field.
id};
4306 for (
auto const& query : owner.queries) {
4308 errors.push_back(std::move(text));
4311 for (
auto const& mutation : owner.mutations) {
4312 for (std::string& text :
stringsOf(
walk(mutation->result(), path))) {
4313 errors.push_back(std::move(text));
4317 for (CheckRuntime
const*
const check :
element.checks) {
4318 if (check->failing->get()) {
4319 errors.push_back(check->message->get());
4327inline void ScreenCore::wireElement(
ElementState& element) {
4328 DocNode
const& node = *
element.node;
4329 RtScope& scope = *
element.scope;
4330 std::string_view
const kind = node.kind;
4331 if (kind ==
"form") {
4335 if (kind ==
"collection" || kind ==
"wizard") {
4336 wireComposite(element);
4339 bool const input = kind ==
"textInput" || kind ==
"checkbox" || kind ==
"select" || kind ==
"dateTimeInput" ||
4340 kind ==
"slider" || kind ==
"filePicker";
4342 std::string_view key =
"value";
4343 if (kind ==
"checkbox") {
4345 }
else if (kind ==
"select") {
4348 ExprPtr const expr = kind ==
"filePicker" ? nullptr : node.prop(key);
4349 bool const controlled = expr && (expr->kind != ExprKind::Literal || !expr->literal.isNull());
4351 element.bound = &scope.nodes.computed([&scope, expr] {
return evalIn(scope,
nullptr, expr); });
4353 scope.nodes.effect([
this, &element, kind] {
4363 }
else if (kind ==
"checkbox") {
4367 if (kind ==
"select") {
4369 &scope.nodes.computed([&scope, expr = node.prop(
"options")] { return evalIn(scope, nullptr, expr); });
4370 if (
element.bound ==
nullptr && node.
name(
"onStale") ==
"clear") {
4371 scope.nodes.effect([
this, &element] {
4375 if (selected.isNull()) {
4378 ValueList const*
const items = options.asList();
4379 bool const kept = items !=
nullptr && std::ranges::any_of(*items, [&](Value
const& option) {
4390 if (kind ==
"table" && node.prop(
"rows")) {
4392 &scope.nodes.computed([&scope, expr = node.prop(
"rows")] { return evalIn(scope, nullptr, expr); });
4405 return value ? *std::move(value) :
Value{};
4416 if (encoding.has_value() && encoding->has_value()) {
4417 value = (*encoding)->value.
isNull() ?
jsonTextValue((*encoding)->json) : (*encoding)->value;
4419 std::optional<forms::FieldError>
const& error = field.
error().get();
4420 record.insert_or_assign(
"value", std::move(value));
4425 record.insert_or_assign(
"valid",
Value{!error.has_value()});
4429 record.insert_or_assign(
"errors",
4431 return Value{std::move(record)};
4441 if (form ==
nullptr) {
4444 if (name ==
"ready") {
4445 return form->ready();
4447 if (name ==
"body") {
4448 std::optional<std::string>
const body = form->body();
4451 if (name ==
"submitted") {
4452 return form->submitAttempts() > 0;
4454 if (name ==
"pending") {
4455 return form->pending();
4457 for (
auto const& field : form->fields()) {
4458 if (field->spec().
name == name) {
4466 std::string_view
const kind = element.node->kind;
4467 if (kind ==
"checkbox") {
4470 if (kind ==
"select") {
4478 DocNode const& node = *element.node;
4479 RtScope const& scope = *element.scope;
4480 std::string_view
const kind = node.
kind;
4481 auto flagOf = [&](std::string_view key,
bool fallback) ->
Value {
4487 bool const*
const flag = value.
asBool();
4488 return flag !=
nullptr && *flag;
4490 auto errors = [&]() ->
Value {
return element.errors->
get(); };
4491 auto count = [](
Value const& list) ->
Value {
4492 ValueList const*
const items = list.asList();
4493 return static_cast<std::int64_t
>(items ==
nullptr ? 0 : items->size());
4495 if (name ==
"visible" || name ==
"enabled") {
4496 return flagOf(name,
true);
4498 if (name ==
"errors") {
4501 if (name ==
"valid") {
4502 Value const current = errors();
4503 ValueList const*
const items = current.asList();
4504 return items ==
nullptr || items->empty();
4506 if (name ==
"shownErrors") {
4510 if (mode ==
"always" || (mode ==
"submit" && submittedNow) ||
4511 (mode !=
"submit" && (submittedNow || element.touched.
get()))) {
4516 if (kind ==
"dialog" || kind ==
"drawer") {
4517 if (name ==
"open") {
4518 return node.
prop(
"open") ? flagOf(
"open",
false) :
Value{element.
open.get()};
4520 if (name ==
"ready") {
4521 RtScope const*
const inner = element.inner.
get();
4522 return inner !=
nullptr && inner->
ready();
4524 if (name ==
"submitted") {
4525 return element.submitted.
get();
4529 if (kind ==
"table") {
4530 if (name ==
"selection") {
4533 if (name ==
"activeKey") {
4534 return element.activeKey.
get();
4536 if (name ==
"selectedCount") {
4539 if (name ==
"viewCount" || name ==
"sourceCount") {
4540 return element.rows ==
nullptr ?
Value{std::int64_t{0}} : count(element.rows->
get());
4543 if ((name ==
"pending" || name ==
"error") && rows && rows->kind ==
ExprKind::Query) {
4544 ScopeEnv const env{scope,
nullptr};
4547 return name ==
"pending" ?
Value{
false} :
Value{};
4549 if (kind ==
"form") {
4552 if (name ==
"draft") {
4553 return element.
draft.get();
4555 if (name ==
"dirty") {
4556 return element.edited.
get();
4558 if (name ==
"focused") {
4559 return element.focused.
get();
4561 if (name ==
"touched") {
4562 return element.touched.
get();
4564 if (name ==
"checked" || name ==
"selected") {
4565 return element.bound !=
nullptr ? element.bound->
get() : element.
draft.get();
4567 if (name ==
"selectedOption") {
4569 ValueList const*
const options = element.rows ==
nullptr ? nullptr : element.rows->
get().asList();
4570 if (options !=
nullptr && !selected.
isNull()) {
4571 for (
Value const& option : *options) {
4579 if (name ==
"value") {
4580 if (element.bound !=
nullptr) {
4581 return element.bound->
get();
4583 return element.committed.
get();
4595 if (commands.empty()) {
4600 std::size_t left = 0;
4607 auto join = std::make_shared<Join>(Join{.left = commands.size() + 1, .ok =
true, .done = std::move(done)});
4608 auto finish = [join](
bool succeeded) {
4609 join->ok = join->ok && succeeded;
4610 if (--join->left == 0) {
4611 join->done(join->ok);
4614 for (
Command const& command : commands) {
4615 runOne(command, ctx, finish);
4627 auto finish = [&done](
bool succeeded) { done(succeeded); };
4628 switch (command.
kind) {
4630 runSet(command, ctx, done);
4633 runReset(command, ctx, done);
4636 runMutation(command, ctx, std::move(done));
4648 finish((
host !=
nullptr &&
host->closeSelf(_doc->id, std::nullopt)) ||
4649 unsupported(command,
"`close: self`"));
4653 if (element.node->prop(
"open")) {
4654 report(command.
path,
"the dialog's `open` is bound to an expression");
4659 if (opening && !element.
open.peek()) {
4660 element.submitted.set(
false);
4663 element.
open.set(opening);
4669 .route = eval(command.
extra),
4670 .params = eval(command.
value),
4672 if (
host ==
nullptr) {
4673 finish(unsupported(command,
"`navigate`"));
4676 std::weak_ptr<RtScope>
const weak = ctx.
scope;
4678 auto onResult = [
this, weak, kept, list = &command.
commands](
Value const& result) {
4679 if (
auto const held = weak.lock()) {
4680 auto bindings = std::make_shared<Bindings>(*kept.
bindings);
4681 bindings->push_back(
Binding{.name =
"event", .read = [result] {
return result; }});
4683 .bindings = bindings,
4689 auto onCancel = [
this, weak, kept, list = &command.
alternative] {
4690 if (
auto const held = weak.lock()) {
4696 finish(
host->navigate(request, std::move(onResult), std::move(onCancel)) ||
4697 unsupported(command,
"`navigate`"));
4701 finish(
host !=
nullptr &&
host->closeSelf(_doc->id, eval(command.
value))
4703 : unsupported(command,
"`return`"));
4714 runHostCommand(command, ctx, done);
4717 std::weak_ptr<RtScope>
const weak = ctx.
scope;
4718 auto committed = [
this, weak, target = command.
target, path = command.
path,
4719 done](std::expected<void, std::string>
const& outcome) {
4724 report(path,
"backendSwitchFailed: " + outcome.error());
4728 session.backendChanged(target);
4731 if (
host !=
nullptr &&
host->switchBackend(command.
target, std::move(committed))) {
4740 if (command.
target ==
"all") {
4743 prefsNow.insert_or_assign(command.
target, found->second);
4745 prefs.set(std::move(prefsNow));
4746 if (
host !=
nullptr) {
4754 if (
host !=
nullptr) {
4761 finish(
host !=
nullptr &&
host->notify(
text, command.
option) ?
true : unsupported(command,
"`notify`"));
4768 .bindings = std::make_shared<Bindings>(*ctx.
bindings),
4771 std::weak_ptr<RtScope>
const weak = ctx.
scope;
4772 auto step = std::make_shared<std::function<void(std::size_t)>>();
4773 *step = [
this, inner, weak, list = &command.
commands, done = std::move(done),
4774 weakStep = std::weak_ptr<std::function<void(std::size_t)>>{step}](std::size_t index) {
4775 auto const held = weak.lock();
4776 if (index == list->size() || !held) {
4777 done(held !=
nullptr);
4780 auto const self = weakStep.lock();
4782 current.scope = held;
4783 runOne(list->
at(index), current, [self, index, done](
bool succeeded) {
4796 .bindings = std::make_shared<Bindings>(*ctx.
bindings),
4804 bool const*
const flag = condition.
asBool();
4805 if (flag ==
nullptr) {
4819 if (!
runtime().untracked([&] {
return query->
error(); }).isNull()) {
4831 finish(
host !=
nullptr &&
host->signIn(reply) ?
true : unsupported(command,
"`signIn`"));
4836 if (
host !=
nullptr) {
4844 runTransfer(command, ctx, std::move(done));
4850inline void ScreenCore::runMutation(
Command const& command, CommandCtx
const& ctx, Done done)
const {
4851 RtScope
const& scope = *ctx.scope;
4852 MutationRuntime& mutation = *scope.find(command.
target)->mutation;
4853 RtScope
const& policy = scope.policyScope();
4854 if (policy.submitted !=
nullptr) {
4855 policy.submitted->set(
true);
4857 if (policy.owner !=
nullptr && !runtime().untracked([&] {
return policy.ready(); })) {
4861 RtScope
const& declaring = *[&]() -> RtScope
const* {
4862 for (RtScope
const* candidate = &scope; candidate !=
nullptr; candidate = candidate->parent.get()) {
4869 auto bodyFor = [&](
Bindings const& bindings) {
4873 if (mutation.decl().body) {
4874 return evalIn(declaring, &bindings, mutation.decl().body);
4879 Value body = bodyFor(*ctx.bindings);
4880 mutation.run(std::move(body), std::make_shared<Bindings const>(*ctx.bindings),
4881 [bindings = ctx.bindings, bindAs =
command.
binding, done = std::move(done)](
bool succeeded,
4882 Value const& reply) {
4883 if (succeeded && !bindAs.empty()) {
4884 bindings->push_back(Binding{.name = bindAs, .read = [reply] { return reply; }});
4892 if (items.empty()) {
4897 std::size_t left = 0;
4901 auto state = std::make_shared<Each>(Each{.left = items.size(), .ok =
true, .done = std::move(done)});
4902 std::vector<Value> bodies;
4903 std::vector<std::shared_ptr<Bindings const>> bound;
4904 for (
Value const& item : items) {
4907 bindings.push_back(Binding{.name =
command.
binding, .read = [item] {
return item; }});
4909 bodies.push_back(bodyFor(bindings));
4910 bound.push_back(std::make_shared<Bindings const>(std::move(bindings)));
4913 for (std::size_t index = 0; index < bodies.size(); ++index) {
4914 mutation.run(bodies.at(index), bound.at(index), [state](
bool succeeded,
Value const&) {
4915 state->ok = state->ok && succeeded;
4916 if (--state->left == 0) {
4917 state->done(state->ok);
4924 auto step = std::make_shared<std::function<void(std::size_t)>>();
4925 *step = [&mutation, bodies, bound, state,
4926 weakStep = std::weak_ptr<std::function<void(std::size_t)>>{step}](std::size_t index) {
4927 if (index == bodies.size()) {
4928 state->done(state->ok);
4931 auto const self = weakStep.lock();
4932 mutation.run(bodies.at(index), bound.at(index), [self, state, index](
bool succeeded,
Value const&) {
4944inline void ScreenCore::writeSlot(StateSlot&
slot, std::span<std::string const> within,
Value value,
4947 Type const& type =
slot.decl->type;
4948 if (within.empty() && !type.isAny() && !type.admits(value)) {
4949 report(
command.
path,
"`set` writes a value that is not a " + type.name());
4955 slot.value->set(std::move(next));
4959inline void ScreenCore::runSetApp(
Command const& command, std::span<std::string const> rest,
Value value,
4961 if (RtScope
const*
const app =
session.appScope(); app !=
nullptr && !rest.empty()) {
4962 NameRef
const*
const named = app->find(rest.front());
4963 if (named !=
nullptr && named->category == NameCategory::State) {
4964 writeSlot(*named->state, rest.subspan(1), std::move(value), command, done);
4967 report(
command.
path,
"`app." + rest.front() +
"` is not state of the app document");
4972 for (std::string
const& segment : rest) {
4973 name += name.empty() ? segment :
"." + segment;
4975 done(host !=
nullptr &&
host->setAppValue(name, value) ?
true : unsupported(command,
"`set` of `app`"));
4978inline void ScreenCore::runSet(
Command const& command, CommandCtx
const& ctx,
Done const& done) {
4979 RtScope
const& scope = *ctx.scope;
4981 if (!path || path->empty()) {
4985 std::string
const head = path->front();
4986 std::span<std::string const>
const rest = std::span<std::string const>{*path}.subspan(1);
4988 if (head ==
"app") {
4989 runSetApp(command, rest, std::move(value), done);
4992 NameRef
const*
const found = scope.find(head);
4993 if (found !=
nullptr && found->category == NameCategory::State) {
4994 writeSlot(*found->state, rest, std::move(value), command, done);
4997 if (found !=
nullptr && found->category == NameCategory::Element) {
4998 ElementState&
element = *found->element;
5001 if (
element.bound ==
nullptr) {
5002 element.committed.set(std::move(value));
5007 report(
command.
path,
"`set` names nothing writable here");
5011inline void ScreenCore::runReset(
Command const& command, CommandCtx
const& ctx,
Done const& done) {
5012 RtScope
const& scope = *ctx.scope;
5014 auto resetStates = [
this](RtScope
const& owner) {
5015 for (
auto const&
slot : owner.states) {
5017 Value init = runtime().untracked([&] {
return evalIn(owner,
nullptr,
slot->decl->init); });
5018 slot->value->set(std::move(init));
5021 for (
auto const& element : owner.elements) {
5031 if (found !=
nullptr && found->category == NameCategory::State) {
5032 StateSlot&
slot = *found->state;
5034 slot.value->set(runtime().untracked([&] {
return evalIn(*
slot.scope,
nullptr,
slot.decl->init); }));
5035 }
else if (found !=
nullptr && found->category == NameCategory::Element) {
5036 ElementState&
element = *found->element;
5038 if (RtScope
const*
const inner =
element.inner.peek()) {
5039 resetStates(*inner);
5042 report(
command.
path,
"`reset` names no state or dialog here");
5048inline void ScreenCore::runHostCommand(
Command const& command, CommandCtx
const& ctx,
Done const& done) {
5050 if (
auto const& run =
session.hostCommandRunner()) {
5084 if (_state == State::NotTaken) {
5085 _state = State::Taken;
5086 _core->addPending(1);
5092 if (_state == State::Taken && !_alive.expired()) {
5093 _core->addPending(-1);
5095 _state = State::Released;
5099 enum class State : std::uint8_t { NotTaken, Taken, Released };
5102 std::weak_ptr<bool> _alive;
5103 State _state = State::NotTaken;
5110 static constexpr std::string_view kAlphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
5112 out.reserve(((bytes.size() + 2) / 3) * 4);
5113 for (std::size_t index = 0; index < bytes.size(); index += 3) {
5114 std::size_t
const left = std::min<std::size_t>(3, bytes.size() - index);
5115 std::uint32_t group = 0;
5116 for (std::size_t offset = 0; offset < 3; ++offset) {
5118 if (offset < left) {
5119 group |=
static_cast<unsigned char>(bytes.at(index + offset));
5122 for (std::size_t digit = 0; digit < 4; ++digit) {
5123 out.push_back(digit <= left ? kAlphabet.at((group >> (18U - (6U * digit))) & 0x3FU) :
'=');
5133 while (!
text.empty() &&
text.back() ==
'=') {
5134 text.remove_suffix(1);
5136 if (
text.size() % 4 == 1) {
5137 return std::nullopt;
5139 auto digitOf = [](
char chr) ->
int {
5140 if (chr >=
'A' && chr <=
'Z') {
5143 if (chr >=
'a' && chr <=
'z') {
5144 return chr -
'a' + 26;
5146 if (chr >=
'0' && chr <=
'9') {
5147 return chr -
'0' + 52;
5152 return chr ==
'/' ? 63 : -1;
5155 out.reserve((
text.size() * 3) / 4);
5156 std::uint32_t group = 0;
5158 for (
char const chr :
text) {
5159 int const digit = digitOf(chr);
5161 return std::nullopt;
5163 group = (group << 6U) | static_cast<std::uint32_t>(digit);
5167 out.push_back(
static_cast<char>((group >>
static_cast<std::uint32_t
>(bits)) & 0xFFU));
5177 while (!
text.empty() &&
text.back() ==
'=') {
5178 text.remove_suffix(1);
5180 return (
text.size() * 3) / 4;
5186[[nodiscard]]
inline std::string
sizeText(std::size_t bytes) {
5187 constexpr std::size_t kKiB = 1024;
5189 return std::to_string(bytes) +
" bytes";
5191 bool const mebi = bytes >= kKiB * kKiB;
5192 std::size_t
const unit = mebi ? kKiB * kKiB : kKiB;
5194 std::size_t
const tenths = ((bytes * 10) + (unit / 2)) / unit;
5195 std::string
text = std::to_string(tenths / 10);
5196 if (tenths % 10 != 0) {
5197 text +=
"." + std::to_string(tenths % 10);
5199 return text + (mebi ?
" MiB" :
" KiB");
5206 if (std::string
const*
const text = value.
asString()) {
5208 return *std::move(bytes);
5210 return std::unexpected(std::string{
"the text is not base64"});
5214 bytes.reserve(list->size());
5215 for (
Value const& item : *list) {
5216 std::int64_t
const*
const byte = item.asInt();
5217 if (
byte ==
nullptr || *byte < 0 || *byte > 255) {
5218 return std::unexpected(std::string{
"a byte list holds integers 0 to 255"});
5220 bytes.push_back(
static_cast<char>(
static_cast<unsigned char>(*
byte)));
5224 return std::unexpected(std::string{
"inline bytes are base64 text or a list of bytes"});
5227inline void ScreenCore::runTransfer(
Command const& command, CommandCtx
const& ctx, Done done) {
5228 Value const file = evalIn(*ctx.scope, ctx.bindings.get(), command.
value);
5230 runDownload(command, ctx, file, std::move(done));
5232 runUpload(command, ctx, file, std::move(done));
5236inline void ScreenCore::runDownload(
Command const& command, CommandCtx
const& ctx,
Value const& file,
Done done) {
5238 auto const hold = std::make_shared<TransferHold>(*
this, _alive);
5239 auto const settled = [
this, weak = std::weak_ptr<RtScope>{ctx.scope}, target =
command.
target,
5241 hold](std::expected<Value, std::string>
const& reply) {
5242 auto const held = weak.lock();
5247 runtime().batch([&] {
5248 bool const succeeded = reply.has_value();
5249 NameRef
const*
const found = succeeded && preview ? held->find(target) :
nullptr;
5250 if (found !=
nullptr && found->category == NameCategory::State) {
5251 initState(*found->state->scope, *found->state);
5253 found->state->value->set(*reply);
5260 std::optional<InlineFile>
const inlined = inlineDownload(command, ctx, file, name);
5266 unsupported(command,
"`download`");
5274 unsupported(command,
"`download`");
5282inline std::optional<InlineFile> ScreenCore::inlineDownload(
5283 Command const& command, CommandCtx
const& ctx,
5286 std::size_t
const cap =
session.options().maxInlineFileBytes;
5287 std::size_t size = 0;
5288 if (std::string
const*
const text = file.asString()) {
5290 }
else if (
ValueList const*
const list = file.asList()) {
5291 size =
list->size();
5293 std::expected<std::string, std::string> bytes = size > cap ? std::unexpected(std::string{}) :
inlineBytes(file);
5294 if (size > cap || (bytes && bytes->size() > cap)) {
5295 report(command.path,
"download: " + sizeText(bytes ? bytes->size() : size) +
" is over the " + sizeText(cap) +
5296 " inline cap; serve it through the file store");
5297 return std::nullopt;
5300 report(
command.
path,
"download: " + bytes.error());
5301 return std::nullopt;
5308 .bytes = *std::move(bytes)};
5312inline void ScreenCore::runUpload(
Command const& command, CommandCtx
const& ctx,
Value const& file,
Done done) {
5313 RtScope
const& scope = *ctx.scope;
5314 std::weak_ptr<RtScope>
const weak = ctx.scope;
5315 auto const hold = std::make_shared<TransferHold>(*
this, _alive);
5317 TransferState*
const transfer =
5318 transferName !=
nullptr && transferName->category == NameCategory::Transfer ? transferName->transfer :
nullptr;
5320 if (weak.lock() && transfer !=
nullptr) {
5321 transfer->progress.set(std::move(fraction));
5327 hold](std::expected<ValueRecord, std::string> reply) {
5328 auto const held = weak.lock();
5333 runtime().batch([&] {
5334 if (transfer !=
nullptr) {
5335 transfer->pending.set(
false);
5336 transfer->error.set(reply ?
Value{} :
Value{reply.error()});
5346 MutationRuntime& mutation = *found->mutation;
5347 Value const base =
evalIn(*held, kept.bindings.get(), mutation.decl().body);
5349 for (
auto& [member, value] : *reply) {
5350 fields.insert_or_assign(member, std::move(value));
5353 mutation.run(
Value{std::move(fields)}, std::make_shared<Bindings const>(*kept.bindings),
5354 [done](
bool succeeded,
Value const&) { done(succeeded); });
5358 if (host ==
nullptr) {
5359 unsupported(command,
"`upload`");
5363 if (transfer !=
nullptr) {
5364 transfer->pending.set(
true);
5365 transfer->progress.set(
Value{std::int64_t{0}});
5366 transfer->error.set(
Value{});
5370 if (target !=
nullptr && target->category == NameCategory::Mutation && target->mutation !=
nullptr) {
5371 request.modelType = target->mutation->typeId();
5372 request.action = target->mutation->decl().action;
5375 bool started =
false;
5377 std::size_t
const cap =
session.options().maxInlineFileBytes;
5378 started =
host->readFile(
5380 [
this, settled,
progress, cap, member, command = &command](std::expected<InlineFile, std::string> read) {
5382 settled(std::unexpected(read.error()));
5385 if (read->bytes.size() > cap) {
5387 std::string why =
"upload: " +
sizeText(read->bytes.size()) +
" is over the " +
sizeText(cap) +
5388 " inline cap; send it through the file store";
5390 settled(std::unexpected(std::move(why)));
5402 settled(std::move(added));
5405 started =
host->uploadFor(request,
progress, [settled, member](std::expected<Value, std::string> reference) {
5407 settled(std::unexpected(reference.error()));
5411 added.insert_or_assign(member, *std::move(reference));
5412 settled(std::move(added));
5416 if (transfer !=
nullptr) {
5417 transfer->pending.set(
false);
5418 transfer->progress.set(
Value{});
5420 unsupported(command,
"`upload`");
5438 static_cast<void>(format);
5439 bool const*
const flag = value.
asBool();
5440 return flag !=
nullptr && *flag;
5448 return textOf(value, format);
5459 if (type ==
"json") {
5462 if (type ==
"int" || type ==
"exact") {
5463 if (std::int64_t
const*
const whole = value.
asInt()) {
5464 return std::to_string(*whole);
5467 return detail::decimalText(number->value,
static_cast<std::uint32_t
>(number->value.decimalPlaces.value));
5471 static_cast<std::uint32_t
>(quantity->value.decimalPlaces.value)) +
5472 " " + quantity->unit;
5475 return textOf(value, format);
5482 if (std::int64_t
const*
const whole = value.
asInt()) {
5483 return static_cast<double>(*whole);
5486 return number->value.toDouble();
5489 return quantity->value.toDouble();
5500 std::string_view
const text{payload};
5501 if (
text.starts_with(
"i:")) {
5502 std::int64_t whole = 0;
5503 std::string_view
const digits =
text.substr(2);
5504 char const*
const last = std::to_address(digits.end());
5505 auto const [end, error] = std::from_chars(std::to_address(digits.begin()), last, whole);
5506 if (error == std::errc{} && end == last) {
5507 return Value{whole};
5510 if (
text.starts_with(
"s:")) {
5511 return Value{std::string{
text.substr(2)}};
5515 return std::move(*value);
5518 return Value{payload};
5525[[nodiscard]]
inline std::int64_t
intOf(
Value const& value, std::int64_t fallback) {
5526 if (std::int64_t
const*
const number = value.
asInt()) {
5530 return static_cast<std::int64_t
>(number->value.toDouble());
5538[[nodiscard]]
inline std::optional<TextRole>
roleNamed(std::string_view name) {
5539 if (name ==
"normal") {
5542 if (name ==
"muted") {
5545 if (name ==
"heading" || name ==
"title") {
5548 if (name ==
"section") {
5551 if (name ==
"error") {
5554 if (name ==
"success") {
5557 return std::nullopt;
5573 std::string
const text =
textOf(value, format);
5574 if (
text ==
"info") {
5577 if (
text ==
"ok" ||
text ==
"success") {
5580 if (
text ==
"warn" ||
text ==
"warning") {
5583 if (
text ==
"err" ||
text ==
"error") {
5593 if (
Value const*
const fixed = value.
field(
"fixed")) {
5596 if (
Value const*
const stretch = value.
field(
"stretch")) {
5606 if (std::string
const*
const text = value.
asString()) {
5607 return *
text ==
"content";
5610 return record !=
nullptr && record->size() == 1 && (record->contains(
"fixed") || record->contains(
"stretch")) &&
5611 record->begin()->second.asInt() !=
nullptr;
5618 if (name ==
"start") {
5621 if (name ==
"center") {
5624 if (name ==
"end") {
5634 if (name ==
"center") {
5637 if (name ==
"end") {
5640 if (name ==
"between") {
5658 if (name ==
"active") {
5661 if (name ==
"done") {
5672 std::vector<SelectOption> out;
5674 for (
Value const& option : *items) {
5676 Value const*
const label = option.field(
"label");
5688 return ui::forEach<int>([] {
return std::vector<int>{0}; }, [](
int const&) {
return Key{std::int64_t{0}}; },
5698[[nodiscard]]
inline Node holding(std::vector<Node> children, std::shared_ptr<RtScope> scope,
5699 std::vector<KeyBinding> keys = {}) {
5701 column.common.
visible = Prop<bool>{[held = std::move(scope)] {
return held !=
nullptr; }};
5702 column.common.keys = std::move(keys);
5703 return ui::column(std::move(column));
5721 return slotProp<TextRole>([
this, scope = ctx.
scope, bindings = ctx.
bindings, expr] {
5722 Value const value = evalIn(*scope, bindings.get(), expr);
5723 std::optional<TextRole> const role = roleNamed(textOf(value, session.formatting()));
5725 report(expr->path,
"role is " + canonicalJson(value) +
5726 "; a role is normal, muted, heading, section, error or success");
5733inline void ScreenCore::layoutOf(DocNode
const& node, LowerCtx
const& ctx, LayoutHints& layout) {
5734 if (ExprPtr
const sizing = node.prop(
"sizing")) {
5737 Value
const value = once(ctx, sizing);
5738 Value
const*
const width = value.field(
"width");
5739 Value
const*
const height = value.field(
"height");
5740 bool const record = width !=
nullptr || height !=
nullptr;
5741 bool const valid = record ? (width ==
nullptr || isSizing(*width)) && (height ==
nullptr || isSizing(*height))
5744 report(sizing->path,
"sizing is " + canonicalJson(value) +
5745 "; a sizing is \"content\", {\"fixed\": n}, {\"stretch\": w}, or a record of "
5746 "width and height of them");
5747 }
else if (record) {
5754 if (std::string_view
const placed = node.name(
"alignSelf"); !placed.empty()) {
5755 layout.alignSelf =
alignOf(placed);
5758 for (
auto const& [key, bound] :
5761 if (
ExprPtr const expr = node.prop(key); expr !=
nullptr && expr->literal.asInt() !=
nullptr) {
5762 layout.*bound =
static_cast<int>(*expr->literal.asInt());
5767inline void ScreenCore::commonOf(
DocNode const& node, LowerCtx
const& ctx, ElementState* element,
Common& common) {
5769 common.visible = bindProp<bool>(ctx, node.prop(
"visible"),
true, boolOf);
5770 common.enabled = bindProp<bool>(ctx, node.prop(
"enabled"),
true, boolOf);
5771 common.tooltip = bindProp<std::string>(ctx, node.prop(
"tooltip"), std::string{}, stringOf);
5772 layoutOf(node, ctx, common.layout);
5773 common.testId = std::string{node.name(
"testId")};
5774 common.focusId = node.id;
5775 common.surface = std::string{node.name(
"surface")};
5776 if (
ExprPtr const autofocus = node.prop(
"autofocus")) {
5777 common.autofocus =
boolOf(once(ctx, autofocus),
session.formatting());
5779 for (
DocNodePtr const& a11y : node.partList(
"a11y")) {
5780 common.a11y.name = bindProp<std::string>(ctx, a11y->prop(
"name"), std::string{}, stringOf);
5781 common.a11y.role = std::string{a11y->name(
"role")};
5783 for (
DocNodePtr const& drag : node.partList(
"drag")) {
5784 ExprPtr const payload = drag->prop(
"payload");
5785 common.dragKey = slotProp<std::optional<Key>>(
5786 [scope = ctx.scope, bindings = ctx.bindings, payload]() -> std::optional<Key> {
5787 Value const value =
evalIn(*scope, bindings.get(), payload);
5788 return value.isNull() ? std::nullopt : std::optional<Key>{
keyOf(value)};
5791 for (
DocNodePtr const& drop : node.partList(
"drop")) {
5792 ExprPtr const accepts = drop->prop(
"accepts");
5793 common.accepts = [
this, scope = ctx.scope, bindings = ctx.bindings, accepts](
Key const& key) {
5797 Value const accepted = runtime().untracked([&] {
return evalIn(*scope, bindings.get(), accepts); });
5798 if (
ValueList const*
const items = accepted.asList()) {
5799 return std::ranges::any_of(*items, [&](
Value const& item) {
return keyOf(item) == key; });
5801 return accepted.
isNull() ||
keyOf(accepted) == key;
5803 common.onDrop = [
this, ctx, commands = drop->event(
"onDrop")](
Key const& key) {
5807 for (
auto const& [chord, commands] : node.keys) {
5808 common.keys.push_back(
5809 KeyBinding{.chord = chord, .onPress = [
this, ctx,
list = &commands] { runEvent(list, ctx,
Value{}); }});
5813inline void ScreenCore::fieldOf(
DocNode const& node, LowerCtx
const& ctx, ElementState* element,
FieldState& field) {
5814 field.readonly = bindProp<bool>(ctx, node.prop(
"readonly"),
false, boolOf);
5815 field.required = bindProp<bool>(ctx, node.prop(
"required"),
false, boolOf);
5816 field.stale = bindProp<bool>(ctx, node.prop(
"stale"),
false, boolOf);
5817 if (element !=
nullptr) {
5818 field.errors = slotProp<std::vector<std::string>>(
5819 [
this, scope = ctx.scope,
element] {
return stringsOf(elementValue(*element,
"shownErrors")); });
5820 }
else if (
ExprPtr const errors = node.prop(
"errors")) {
5821 field.errors = slotProp<std::vector<std::string>>([scope = ctx.scope, bindings = ctx.bindings, errors] {
5827inline std::vector<MenuItem> ScreenCore::menuItems(std::span<DocNodePtr const> parts, LowerCtx
const& ctx) {
5828 std::vector<MenuItem> items;
5831 item.
label = bindProp<std::string>(ctx, part->prop(
"label"), std::string{}, stringOf);
5832 if (
Commands const*
const commands = part->event(
"onClick")) {
5833 item.onSelect = [
this, ctx, commands] { runEvent(commands, ctx,
Value{}); };
5835 if (
ExprPtr const icon = part->prop(
"icon")) {
5838 item.keys = std::string{part->name(
"keys")};
5839 if (
ExprPtr const checked = part->prop(
"checked")) {
5840 item.checked = slotProp<std::optional<bool>>(
5841 [scope = ctx.scope, bindings = ctx.bindings, checked]() -> std::optional<bool> {
5842 Value const value =
evalIn(*scope, bindings.get(), checked);
5843 bool const*
const flag = value.asBool();
5844 return flag ==
nullptr ? std::nullopt : std::optional<bool>{*flag};
5847 item.enabled = bindProp<bool>(ctx, part->prop(
"enabled"),
true, boolOf);
5848 item.items = menuItems(part->partList(
"items"), ctx);
5849 items.push_back(std::move(item));
5856 models.reserve(parts.size());
5860 .label = bindProp<std::string>(ctx, part->prop(
"label"), std::string{},
stringOf),
5861 .icon = bindProp<std::string>(ctx, part->prop(
"icon"), std::string{},
stringOf),
5863 .enabled = bindProp<bool>(ctx, part->prop(
"enabled"),
true,
boolOf),
5864 .visible = bindProp<bool>(ctx, part->prop(
"visible"),
true,
boolOf),
5866 if (part->name(
"emphasis") ==
"primary") {
5868 }
else if (part->name(
"emphasis") ==
"danger") {
5870 }
else if (part->name(
"emphasis") ==
"ghost") {
5873 if (
Commands const*
const commands = part->event(
"onClick")) {
5874 model.run = [
this, ctx, commands] {
runEvent(commands, ctx,
Value{}); };
5876 models.push_back(std::move(model));
5885 .emphasis = action.emphasis,
5886 .onClick = std::move(action.run),
5888 button.common.visible = std::move(action.visible);
5889 button.common.enabled = std::move(action.enabled);
5890 button.common.testId =
"action_" + action.id;
5897 std::vector<BadgeModel> badges;
5898 badges.reserve(parts.size());
5900 badges.push_back(
BadgeModel{.text = bindProp<std::string>(ctx, part->prop(
"text"), std::string{},
stringOf),
5902 .icon = bindProp<std::string>(ctx, part->prop(
"icon"), std::string{},
stringOf),
5903 .visible = bindProp<bool>(ctx, part->prop(
"visible"),
true,
boolOf)});
5910 std::string_view
const kind = node.
kind;
5912 auto children = [&](std::string_view key) {
5913 std::vector<Node> out;
5915 out.push_back(lower(*child, ctx));
5919 auto child = [&](std::string_view key) -> Node {
5920 auto const nodes = node.
nodes(key);
5921 return nodes.empty() ? Node{} : lowerAll(nodes, ctx);
5923 auto onceInt = [&](std::string_view key, std::int64_t fallback) {
5925 return expr ?
intOf(once(ctx, expr), fallback) : fallback;
5927 auto onceBool = [&](std::string_view key,
bool fallback) {
5929 return expr ?
boolOf(once(ctx, expr), format) : fallback;
5931 auto onceText = [&](std::string_view key) {
5933 return expr ?
textOf(once(ctx, expr), format) : std::string{};
5935 auto textProp = [&](std::string_view key) {
5936 return bindProp<std::string>(ctx, node.
prop(key), std::string{}, stringOf);
5940 auto nameByLabel = [&](
Common& common) {
5941 bool const named = std::ranges::any_of(node.
partList(
"a11y"),
5942 [](
DocNodePtr const& a11y) { return a11y->prop(
"name") != nullptr; });
5943 if (node.
prop(
"label") && !named) {
5944 common.a11y.name = textProp(
"label");
5948 auto captionOf = [&](
auto& spec) {
5949 if (node.
prop(
"label")) {
5950 spec.label = textProp(
"label");
5952 spec.showLabel = node.
flag(
"showLabel");
5954 auto action = [&](std::string_view key) ->
Action {
5956 if (commands ==
nullptr) {
5959 return [
this, ctx, commands] { runEvent(commands, ctx,
Value{}); };
5964 button.label = bindProp<std::string>(ctx, part->prop(
"label"), std::string{}, stringOf);
5965 if (
Commands const*
const commands = part->event(
"onClick")) {
5966 button.onClick = [
this, ctx, commands] { runEvent(commands, ctx,
Value{}); };
5971 auto read = [
element,
this](std::string_view name) {
5972 return [
element,
this, name = std::string{name}] {
return elementValue(*element, name); };
5975 if (kind ==
"text") {
5976 Text spec{.text = textProp(
"text"), .role = roleProp(ctx, node.
prop(
"role"))};
5977 fieldOf(node, ctx, element, spec.field);
5978 commonOf(node, ctx, element, spec.common);
5979 return ui::text(std::move(spec));
5981 if (kind ==
"button") {
5982 Button spec{.label = textProp(
"label")};
5983 spec.
icon = onceText(
"icon");
5984 if (node.
name(
"emphasis") ==
"primary") {
5986 }
else if (node.
name(
"emphasis") ==
"danger") {
5988 }
else if (node.
name(
"emphasis") ==
"ghost") {
5991 if (node.
name(
"style") ==
"item") {
5993 spec.selected = bindProp<bool>(ctx, node.
prop(
"selected"),
false, boolOf);
5996 Commands const*
const onActivate = node.
event(
"onActivate");
5997 if (onClick !=
nullptr || onActivate !=
nullptr) {
5998 spec.onClick = [
this, ctx, onClick, onActivate] {
5999 runEvent(onClick, ctx,
Value{});
6000 runEvent(onActivate, ctx,
Value{});
6003 commonOf(node, ctx, element, spec.common);
6004 return ui::button(std::move(spec));
6006 if (kind ==
"textInput") {
6007 std::string
const commitOn = node.
name(
"commitOn").empty() ?
"change" : std::string{node.
name(
"commitOn")};
6008 auto commit = [
this, ctx,
element, commitOn, onChange = node.
event(
"onChange")](std::string
const& when,
6009 std::string
const&
text) {
6010 if (commitOn != when) {
6013 if (
element->bound ==
nullptr) {
6022 spec.onChange = [
this,
element, commit](std::string
text) {
6028 commit(
"change",
text);
6030 spec.onCommit = [
this, ctx,
element, commit, onCommit = node.
event(
"onCommit")](std::string
const&
text) {
6032 commit(
"blur",
text);
6035 spec.onSubmit = [
this, ctx, commit, onSubmit = node.
event(
"onSubmit")](std::string
const&
text) {
6036 commit(
"enter",
text);
6039 spec.placeholder = textProp(
"placeholder");
6040 std::string
const mode = onceText(
"mode");
6042 if (mode ==
"multiline") {
6044 }
else if (mode ==
"password") {
6047 spec.icon = onceText(
"icon");
6048 fieldOf(node, ctx, element, spec.field);
6049 commonOf(node, ctx, element, spec.common);
6050 nameByLabel(spec.common);
6052 return ui::textInput(std::move(spec));
6054 if (kind ==
"checkbox") {
6055 Checkbox spec{.label = textProp(
"label"), .checked = slotProp<bool>([read = read(
"checked")] {
6056 Value const value = read();
6057 bool const*
const flag = value.asBool();
6058 return flag !=
nullptr && *flag;
6061 onChange = node.
event(
"onChange")](
bool checked) {
6062 if (
element->bound ==
nullptr) {
6068 runEvent(onToggle, ctx,
Value{checked});
6069 runEvent(onChange, ctx,
Value{checked});
6071 fieldOf(node, ctx, element, spec.field);
6072 commonOf(node, ctx, element, spec.common);
6073 return ui::checkbox(std::move(spec));
6075 if (kind ==
"select") {
6077 spec.
options = slotProp<std::vector<SelectOption>>(
6079 spec.selected = slotProp<std::optional<Key>>([read = read(
"selected")]() -> std::optional<Key> {
6080 Value const selected = read();
6081 return selected.isNull() ? std::nullopt : std::optional<Key>{
keyOf(selected)};
6083 spec.onSelect = [
this, ctx,
element, onChoose = node.
event(
"onChoose"),
6084 onChange = node.
event(
"onChange")](
Key const& key) {
6086 if (
element->bound ==
nullptr) {
6092 runEvent(onChoose, ctx, chosen);
6093 runEvent(onChange, ctx, chosen);
6096 fieldOf(node, ctx, element, spec.field);
6097 commonOf(node, ctx, element, spec.common);
6098 nameByLabel(spec.common);
6100 return ui::select(std::move(spec));
6102 if (kind ==
"slider") {
6104 spec.
value = slotProp<std::int64_t>([read = read(
"value"), element] {
6107 spec.minimum = onceInt(
"min", 0);
6108 spec.maximum = onceInt(
"max", 100);
6109 spec.step = onceInt(
"step", 1);
6110 spec.onChange = [
this, ctx,
element, onChange = node.
event(
"onChange")](std::int64_t value) {
6115 if (
element->bound ==
nullptr) {
6116 element->committed.set(value);
6119 runEvent(onChange, ctx,
Value{value});
6121 spec.onCommit = [
this, ctx,
element, onCommit = node.
event(
"onCommit")](std::int64_t value) {
6123 runEvent(onCommit, ctx,
Value{value});
6125 fieldOf(node, ctx, element, spec.field);
6126 commonOf(node, ctx, element, spec.common);
6127 nameByLabel(spec.common);
6128 return ui::slider(std::move(spec));
6130 if (kind ==
"dateTimeInput") {
6133 std::optional<TypeKind> heldAtLoad;
6134 if (
auto const found = _doc->dateValueKinds.find(&node); found != _doc->dateValueKinds.end()) {
6135 heldAtLoad = found->second;
6138 std::int32_t
const offsetMinutes =
format.utcOffsetMinutes;
6139 spec.value = slotProp<std::optional<time::Timestamp>>([
this,
element, offsetMinutes,
6140 valuePath]() -> std::optional<time::Timestamp> {
6142 if (!reading.readable) {
6143 report(valuePath,
"`dateTimeInput` shows a timestamp or an ISO-8601 date or date-time string");
6145 return reading.instant ? std::optional<time::Timestamp>{time::Timestamp{*reading.instant}} : std::nullopt;
6147 spec.onChange = [
this, ctx,
element, heldAtLoad, mode, offsetMinutes, onChange = node.
event(
"onChange"),
6148 onCommit = node.
event(
"onCommit")](std::optional<time::Timestamp> value) {
6149 bool asString =
element->bound !=
nullptr &&
element->bound->
get().asString() !=
nullptr;
6153 std::optional<time::DateTime>
const instant = value ? value->value : std::nullopt;
6157 entered =
Value{*instant};
6159 }
else if (!instant) {
6160 entered =
Value{std::string{}};
6164 entered =
Value{instant->toIso8601()};
6171 if (
element->bound ==
nullptr) {
6172 element->committed.set(entered);
6175 runEvent(onChange, ctx, entered);
6176 runEvent(onCommit, ctx, entered);
6179 spec.offsetMinutes = offsetMinutes;
6180 fieldOf(node, ctx, element, spec.field);
6181 commonOf(node, ctx, element, spec.common);
6182 nameByLabel(spec.common);
6184 return ui::dateTimeInput(std::move(spec));
6186 if (kind ==
"filePicker") {
6190 spec.onPicked = [
this, ctx,
element, onPick = node.
event(
"onPick")](std::string path) {
6196 runEvent(onPick, ctx,
Value{path});
6199 fieldOf(node, ctx, element, spec.field);
6200 commonOf(node, ctx, element, spec.common);
6201 nameByLabel(spec.common);
6202 return ui::filePicker(std::move(spec));
6204 if (kind ==
"menu") {
6205 Menu spec{.items = menuItems(node.
partList(
"items"), ctx)};
6206 commonOf(node, ctx, element, spec.common);
6207 return ui::menu(std::move(spec));
6209 if (kind ==
"column" || kind ==
"row") {
6210 int const gap =
static_cast<int>(onceInt(
"gap", 0));
6211 if (std::string
const arrangement = arrangedBy(node.
id); !arrangement.empty()) {
6215 if (kind ==
"column") {
6216 Column spec{.children = children(
"children"),
6220 commonOf(node, ctx, element, spec.common);
6221 return ui::column(std::move(spec));
6223 Row spec{.children = children(
"children"),
6227 commonOf(node, ctx, element, spec.common);
6228 return ui::row(std::move(spec));
6230 if (kind ==
"grid") {
6231 Grid spec{.columns =
static_cast<int>(onceInt(
"columns", 1)),
6232 .gap =
static_cast<int>(onceInt(
"gap", 0)),
6235 ExprPtr const span = cell->prop(
"span");
6236 spec.cells.push_back(
6237 GridCell{.node = lower(*cell, ctx), .span =
static_cast<int>(span ?
intOf(once(ctx, span), 1) : 1)});
6239 commonOf(node, ctx, element, spec.common);
6240 return ui::grid(std::move(spec));
6242 if (kind ==
"spacer") {
6244 commonOf(node, ctx, element, spec.common);
6245 return ui::spacer(std::move(spec));
6247 if (kind ==
"panel") {
6248 Panel spec{.title = textProp(
"title"), .padding =
static_cast<int>(onceInt(
"padding", 0))};
6249 spec.
header = child(
"header");
6250 spec.child = child(
"children");
6251 spec.collapsible = onceBool(
"collapsible",
false);
6252 spec.collapsed = bindProp<bool>(ctx, node.
prop(
"collapsed"),
false, boolOf);
6253 if (
Commands const*
const commands = node.
event(
"onToggle")) {
6254 spec.onToggle = [
this, ctx, commands](
bool collapsed) { runEvent(commands, ctx,
Value{collapsed}); };
6256 commonOf(node, ctx, element, spec.common);
6257 return ui::panel(std::move(spec));
6259 if (kind ==
"scroll") {
6262 commonOf(node, ctx, element, spec.common);
6263 return ui::scroll(std::move(spec));
6265 if (kind ==
"switch") {
6266 return lowerSwitch(node, ctx);
6268 if (kind ==
"tabs") {
6271 ExprPtr const label = tab->prop(
"label");
6272 auto const nodes = tab->nodes(
"children");
6273 spec.tabs.push_back(
Tab{.label = label ?
textOf(once(ctx, label), format) : std::string{},
6274 .node = nodes.empty() ?
Node{} : lowerAll(nodes, ctx)});
6278 auto const picked = std::make_shared<reactive::Signal<std::size_t>>(runtime(), std::size_t{0});
6279 spec.selected = [picked] {
return picked->get(); };
6280 spec.onSelect = [picked](std::size_t index) { picked->set(index); };
6281 commonOf(node, ctx, element, spec.common);
6282 return ui::tabs(std::move(spec));
6284 if (kind ==
"dialog" || kind ==
"drawer") {
6285 return lowerDialog(node, ctx, element);
6287 if (kind ==
"busy") {
6288 Busy spec{.active = bindProp<bool>(ctx, node.
prop(
"active"),
false, boolOf), .label = textProp(
"label")};
6289 commonOf(node, ctx, element, spec.common);
6290 return ui::busy(std::move(spec));
6292 if (kind ==
"forEach" || kind ==
"table") {
6293 return lowerRows(node, ctx, element);
6295 if (kind ==
"banner") {
6296 Banner spec{.tone = bindProp<Tone>(ctx, node.
prop(
"tone"),
Tone::Info, toneOf), .text = textProp(
"text")};
6298 spec.dismissible = onceBool(
"dismissible",
false);
6299 spec.onDismiss = action(
"onDismiss");
6300 commonOf(node, ctx, element, spec.common);
6301 return ui::banner(std::move(spec));
6303 if (kind ==
"badge") {
6304 Badge spec{.tone = bindProp<Tone>(ctx, node.
prop(
"tone"),
Tone::Neutral, toneOf), .text = textProp(
"text")};
6305 spec.
icon = onceText(
"icon");
6306 commonOf(node, ctx, element, spec.common);
6307 return ui::badge(std::move(spec));
6309 if (kind ==
"progress") {
6311 spec.
value = slotProp<std::optional<double>>(
6312 [scope = ctx.
scope, bindings = ctx.
bindings, expr = node.
prop(
"value")]() -> std::optional<double> {
6313 Value const value =
evalIn(*scope, bindings.get(), expr);
6314 if (std::int64_t
const*
const number = value.asInt()) {
6315 return static_cast<double>(*number);
6317 if (
Decimal const*
const number = value.asDecimal()) {
6318 return number->value.toDouble();
6320 return std::nullopt;
6322 spec.label = textProp(
"label");
6323 commonOf(node, ctx, element, spec.common);
6324 return ui::progress(std::move(spec));
6326 if (kind ==
"steps") {
6329 slotProp<std::vector<Step>>([
this, scope = ctx.
scope, bindings = ctx.
bindings, expr = node.
prop(
"items")] {
6330 std::vector<Step>
steps;
6331 Value const items =
evalIn(*scope, bindings.get(), expr);
6332 if (
ValueList const*
const list = items.asList()) {
6333 for (
Value const& item : *list) {
6334 Value const*
const label = item.
field(
"label");
6335 Value const*
const state = item.
field(
"state");
6336 std::string
const name = state ==
nullptr ? std::string{} :
textOf(*state, {});
6338 Step{.label = label ==
nullptr ? std::string{} :
textOf(*label,
session.formatting()),
6344 spec.current = slotProp<std::optional<std::size_t>>(
6345 [scope = ctx.
scope, bindings = ctx.
bindings, expr = node.
prop(
"current")]() -> std::optional<std::size_t> {
6346 std::int64_t
const index =
intOf(
evalIn(*scope, bindings.get(), expr), -1);
6347 return index < 0 ? std::nullopt : std::optional<std::size_t>{
static_cast<std::size_t
>(index)};
6349 commonOf(node, ctx, element, spec.common);
6350 return ui::steps(std::move(spec));
6352 if (kind ==
"keyValue") {
6354 spec.
items = slotProp<std::vector<KeyValueItem>>(
6356 std::vector<KeyValueItem> out;
6357 Value const items =
evalIn(*scope, bindings.get(), expr);
6358 if (
ValueList const*
const list = items.asList()) {
6359 for (
Value const& item : *list) {
6360 Value const*
const label = item.
field(
"label");
6361 Value const*
const value = item.
field(
"value");
6363 .label = label ==
nullptr ? std::string{} :
textOf(*label,
session.formatting()),
6364 .value = value ==
nullptr ? std::string{} :
textOf(*value,
session.formatting())});
6369 commonOf(node, ctx, element, spec.common);
6370 return ui::keyValue(std::move(spec));
6372 if (kind ==
"emptyState") {
6373 EmptyState spec{.title = textProp(
"title"), .text = textProp(
"text")};
6374 spec.
icon = onceText(
"icon");
6376 commonOf(node, ctx, element, spec.common);
6377 return ui::emptyState(std::move(spec));
6379 if (kind ==
"splitter") {
6380 ExprPtr const orientation = node.
prop(
"orientation");
6381 Splitter spec{.children = children(
"children"),
6385 commonOf(node, ctx, element, spec.common);
6386 return ui::splitter(std::move(spec));
6388 if (kind ==
"collapsible") {
6389 Collapsible spec{.title = textProp(
"title"), .open = bindProp<bool>(ctx, node.
prop(
"open"),
true, boolOf)};
6390 if (
Commands const*
const commands = node.
event(
"onToggle")) {
6391 spec.onToggle = [
this, ctx, commands](
bool open) { runEvent(commands, ctx,
Value{
open}); };
6393 spec.
header = child(
"header");
6394 spec.child = child(
"children");
6395 commonOf(node, ctx, element, spec.common);
6396 return ui::collapsible(std::move(spec));
6398 if (kind ==
"dropZone") {
6401 spec.multiple = onceBool(
"multiple",
false);
6402 if (
Commands const*
const commands = node.
event(
"onDrop")) {
6403 spec.onDrop = [
this, ctx, commands](std::vector<std::string>
const& paths) {
6407 spec.child = child(
"children");
6408 commonOf(node, ctx, element, spec.common);
6409 return ui::dropZone(std::move(spec));
6411 if (kind ==
"boundary") {
6412 return lowerBoundary(node, ctx);
6414 if (kind ==
"custom") {
6415 return lowerCustom(node, ctx);
6417 if (kind ==
"customize") {
6418 return lowerCustomize(node, ctx);
6420 if (kind ==
"form") {
6421 return lowerForm(node, ctx, element);
6423 if (kind ==
"collection" || kind ==
"wizard") {
6424 return lowerComposite(node, ctx, element);
6426 report(node.
path,
"`" + node.
kind +
"` is a node kind this interpreter does not lower");
6437 auto const read = [&](std::string_view key) -> std::optional<Value> {
6440 return std::nullopt;
6444 auto const text = [&](std::string_view key) -> std::optional<std::string> {
6445 auto const value = read(key);
6446 return value.has_value() && !value->isNull() ? std::optional<std::string>{
textOf(*value, format)}
6449 auto const flag = [&](std::string_view key) {
6450 auto const value = read(key);
6451 bool const*
const set = value.has_value() ? value->asBool() :
nullptr;
6452 return set !=
nullptr && *set;
6458 if (
auto const decimals = read(
"decimals");
6459 decimals.has_value() && decimals->asInt() !=
nullptr && *decimals->asInt() >= 0) {
6460 overlay.
decimals =
static_cast<std::uint32_t
>(std::min<std::int64_t>(*decimals->asInt(), 18));
6462 if (
auto const blankAs =
text(
"blankAs")) {
6494inline void ScreenCore::wireForm(ElementState& element) {
6495 DocNode const& node = *element.node;
6496 RtScope& scope = *element.scope;
6497 std::string
const action{node.
name(
"action")};
6498 Catalog
const& catalog = session.options().catalog;
6499 std::optional<std::string>
const schema = catalog.schema ? catalog.schema(action) : std::nullopt;
6500 if (!schema.has_value()) {
6501 report(node.
path,
"no schema for `" + action +
"`: the catalog has none for a `form` to build from");
6504 NameRef
const*
const alias = scope.find(node.
name(
"model"));
6505 if (alias ==
nullptr || alias->model ==
nullptr) {
6506 report(node.
path,
"`" + std::string{node.name(
"model")} +
"` is not a model alias");
6509 forms::FieldOverlays overlays;
6512 for (
DocNodePtr const& part : record->partList(
"fields")) {
6513 overlays.insert_or_assign(part->id,
overlayOf(scope, *part,
session.formatting()));
6519 "the form of `" + action +
"` does not build: " + model.error().path +
": " + model.error().message);
6522 if (node.
name(
"submit") ==
"explicit") {
6523 model = model->withSubmitMode(forms::SubmitMode::Explicit);
6524 }
else if (node.
name(
"submit") ==
"auto") {
6525 model = model->withSubmitMode(forms::SubmitMode::Automatic);
6527 element.form = std::make_unique<forms::FormSession>(runtime(), *std::move(model), aliasSubmits(alias->model),
6528 aliasCalls(alias->model), engineOptions());
6529 if (
ExprPtr const prefill = node.
prop(
"prefill")) {
6535 bool loaded =
false;
6538 auto const followed = std::make_shared<Followed>();
6539 scope.nodes.effect([
this, &scope, prefill, key = node.
prop(
"key"), form, followed] {
6540 Value const value = evalIn(scope, nullptr, prefill);
6541 if (value.isNull()) {
6544 runtime().untracked([&] {
6546 bool const same = !followed->loaded || (key && current == followed->key);
6547 followed->loaded =
true;
6548 followed->key = current;
6549 std::string
const json = glz::write_json(
valueToJson(value)).value_or(std::string{});
6553 form->prefill(json);
6560inline Node ScreenCore::lowerForm(
DocNode const& node, LowerCtx
const& ctx, ElementState* element) {
6562 if (form ==
nullptr) {
6566 auto const watch = [
this, &ctx](
Commands const* commands,
auto count,
auto payload) {
6567 if (commands ==
nullptr || commands->empty()) {
6570 auto const seen = std::make_shared<std::uint64_t>(runtime().untracked(count));
6571 ctx.scope->nodes.effect([
this, held =
detachedLower(ctx), commands, count, payload, seen] {
6572 std::uint64_t
const now =
count();
6577 runtime().untracked([&] { runEvent(commands,
attached(held), payload()); });
6581 node.event(
"onSubmit"), [form] { return form->submitAttempts(); },
6583 std::optional<std::string> const body = form->body();
6584 return body.has_value() ? jsonTextValue(*body) : Value{};
6587 node.event(
"onSuccess"), [form] { return form->successCount(); },
6589 std::optional<std::string> const& reply = form->lastReply();
6590 return reply.has_value() ? jsonTextValue(*reply) : Value{};
6592 if (
Commands const*
const onError = node.event(
"onError"); onError !=
nullptr && !onError->empty()) {
6595 std::make_shared<std::exception_ptr>(runtime().untracked([form] {
return form->lastError(); }));
6596 ctx.scope->nodes.effect([
this, held =
detachedLower(ctx), onError, form, seen] {
6597 std::exception_ptr
const error =
form->lastError();
6598 if (error ==
nullptr || error == *seen) {
6605 forms::FormViewOptions options{.showTitle =
false};
6606 presentForm(node, ctx, options);
6607 options.title = bindProp<std::string>(ctx, node.prop(
"title"), std::string{}, stringOf);
6609 for (
DocNodePtr const& record : node.partList(
"fields")) {
6610 for (
DocNodePtr const& overlay : record->partList(
"fields")) {
6611 if (!overlay->name(
"component").empty()) {
6612 options.overrides.byField(overlay->id,
6613 [
this, ctx, field = overlay.get()](forms::FieldView
const& view) {
6614 return fieldComponent(view, *field, ctx);
6619 Column column{.children = {forms::formView(*form, std::move(options))}};
6620 commonOf(node, ctx, element,
column.common);
6621 return ui::column(std::move(
column));
6629[[nodiscard]]
inline std::expected<forms::detail::engine::Json, std::string>
changeWire(std::string
const& payload,
6630 std::string_view type) {
6631 std::string_view
text{payload};
6632 if (type ==
"string") {
6633 if (
text.starts_with(
"s:")) {
6634 text.remove_prefix(2);
6637 return forms::detail::engine::Json(std::string{
text});
6639 if (
text.starts_with(
"i:")) {
6640 text.remove_prefix(2);
6642 std::optional<forms::detail::engine::Json> json = forms::detail::engine::parseJson(
text);
6644 return std::unexpected(std::string{
"it is not JSON"});
6646 bool const fits = type ==
"json" || json->is_null() || (type ==
"bool" && json->is_boolean()) ||
6647 ((type ==
"int" || type ==
"number") && json->is_number()) ||
6648 (type ==
"exact" && (json->is_number() || json->is_string()));
6650 bool const vowel = type ==
"int" || type ==
"exact";
6651 return std::unexpected(
"it is not " + std::string{vowel ?
"an " :
"a "} + std::string{type});
6653 return *std::move(json);
6662 if (prop ==
"value") {
6665 if (!encoding.has_value() || !encoding->has_value()) {
6668 return (*encoding)->value.isNull() ?
jsonTextValue((*encoding)->json) : (*encoding)->value;
6671 if (prop ==
"errors") {
6673 std::optional<forms::FieldError>
const& error = state->
error().get();
6677 if (prop ==
"readonly") {
6680 if (prop ==
"required") {
6683 if (prop ==
"label") {
6684 return [state] {
return Value{state->
label()}; };
6690inline CustomProp ScreenCore::componentProp(std::string
const& prop, std::string
const& type,
6691 std::function<
Value()> read) {
6692 if (type ==
"bool") {
6693 return CustomProp{.name = prop, .value = slotProp<bool>([
this, read = std::move(read)] {
6694 return boolOf(read(), session.formatting());
6697 if (type ==
"number") {
6699 .value = slotProp<double>([read = std::move(read)] {
return componentNumber(read()); })};
6701 return CustomProp{.name =
prop, .value = slotProp<std::string>([
this, read = std::move(read), type] {
6706inline void ScreenCore::changeField(forms::FieldState& state, forms::FieldSpec
const& spec, std::string
const& path,
6707 std::string_view valueType, std::string
const& payload) {
6710 if (std::size_t
const cap =
session.options().maxInlineFileBytes;
6711 wire && spec.bytes && wire->is_string() &&
base64Size(wire->get_string()) > cap) {
6712 report(path,
"field " + spec.name +
": " + std::to_string(
base64Size(wire->get_string())) +
6713 " bytes is over the " + std::to_string(cap) +
" inline cap");
6717 if (
auto edited = state.edit(*wire); !edited) {
6718 wire = std::unexpected(edited.error());
6722 report(path,
"field " + spec.name +
": the component's change does not decode: " + wire.error());
6726inline Node ScreenCore::fieldComponent(forms::FieldView
const& view,
DocNode const& overlay, LowerCtx
const& ctx) {
6727 std::string
const name{overlay.name(
"component")};
6729 Component const& component = _doc->components.at(name);
6730 forms::FieldState*
const state = &view.state();
6731 Custom spec{.name = name,
6732 .fallback = view.defaultControl(),
6735 static_cast<int>(std::clamp<std::int64_t>(component.version, 0, std::numeric_limits<int>::max())),
6737 for (
auto const& [prop, type] : component.props) {
6738 if (prop ==
"value" && type ==
"json") {
6740 spec.props.push_back(
CustomProp{.name =
prop, .value = slotProp<std::string>([state] {
6741 forms::Encoding
const& encoding = state->encoded().get();
6742 return encoding.has_value() && encoding->has_value()
6744 : std::string{
"null"};
6751 for (
DocNodePtr const& record : overlay.partList(
"props")) {
6752 if (
ExprPtr const expr = record->prop(prop)) {
6756 read = [scope = ctx.scope, bindings = ctx.bindings, given] {
6757 return given ?
evalIn(*scope, bindings.get(), given) :
Value{};
6760 spec.props.push_back(componentProp(prop, type, std::move(read)));
6762 spec.
onEvent = [
this, state, path = overlay.path, fieldSpec = &view.spec(),
6763 valueType = component.props.at(
"value")](std::string
const&
event, std::string
const& payload) {
6764 if (event ==
"change") {
6765 changeField(*state, *fieldSpec, path, valueType, payload);
6768 return ui::custom(std::move(spec));
6772inline forms::Submitter ScreenCore::aliasCalls(ModelAlias* alias) {
6773 return [
this, alias](std::string_view actionId, std::string
const& body) -> async::Completion<std::string> {
6774 std::shared_ptr<ModelChannel>
const channel =
channelOf(*alias, *
this);
6775 if (channel ==
nullptr) {
6777 promise.reject(std::make_exception_ptr(std::runtime_error{
"the node's model alias is not bound"}));
6778 return std::move(failed);
6780 return channel->execute(std::string{actionId}, body,
CallOptions{});
6786inline forms::Submitter ScreenCore::aliasSubmits(ModelAlias* alias, std::string read) {
6787 return [
this, alias, call = aliasCalls(alias), read = std::move(read)](
6788 std::string_view actionId, std::string body) -> async::Completion<std::string> {
6789 async::Completion<std::string> completion = call(actionId, std::move(body));
6790 if (actionId != read) {
6791 completion.then([
this, alive = std::weak_ptr<bool>{_alive}, alias, typeId = alias->decl->type,
6792 action = std::string{actionId}](std::string
const& reply) {
6793 if (alive.expired()) {
6797 auto value = json ?
valueFromJson(*json) : std::unexpected(json.error());
6798 std::string
const token = runtime().untracked([&] {
return alias->token->get(); });
6807inline forms::FormSessionOptions ScreenCore::engineOptions()
const {
6809 forms::FormSessionOptions options{};
6810 options.locale =
format.numeric;
6811 options.bcp47 =
format.locale;
6812 options.translations =
format.translations;
6813 options.displayOffsetMinutes =
format.utcOffsetMinutes;
6820inline void ScreenCore::wireComposite(ElementState& element) {
6822 RtScope
const& scope = *
element.scope;
6823 bool const collection = node.kind ==
"collection";
6824 std::string
const action{node.name(
"action")};
6826 auto const& lookup =
collection ? catalog.view : catalog.wizard;
6827 std::optional<std::string>
const document = lookup ? lookup(action) : std::nullopt;
6828 if (!document.has_value()) {
6829 report(node.path, std::string{
"no "} + (collection ?
"view" :
"wizard") +
" document for `" + action +
6830 "`: the catalog has none for a `" + node.kind +
"` to build from");
6833 NameRef
const*
const alias = scope.find(node.name(
"model"));
6834 if (alias ==
nullptr || alias->model ==
nullptr) {
6835 report(node.path,
"`" + std::string{node.name(
"model")} +
"` is not a model alias");
6838 forms::SchemaLookup
const schemas = catalog.schema ? forms::SchemaLookup{catalog.schema} : forms::SchemaLookup{};
6839 forms::Submitter
const call = aliasCalls(alias->model);
6843 report(node.path,
"the collection of `" + action +
"` does not build: " + model.error().path +
": " +
6844 model.error().message);
6847 if (model->query() != action) {
6848 report(node.path,
"the view document of `" + action +
"` lists `" + std::string{model->query()} +
"`");
6851 std::string query{model->query()};
6852 element.collection = std::make_unique<forms::CollectionSession>(runtime(), *std::move(model), schemas,
6853 aliasSubmits(alias->model, std::move(query)),
6854 call, engineOptions());
6861 "the wizard of `" + action +
"` does not build: " + model.error().path +
": " + model.error().message);
6864 if (model->steps().front().action != action) {
6866 "the wizard document of `" + action +
"` starts with `" + model->steps().front().action +
"`");
6870 element.wizard = std::make_unique<forms::WizardSession>(runtime(), *std::move(model), schemas,
6871 aliasSubmits(alias->model), call, engineOptions());
6872 }
catch (std::invalid_argument
const& error) {
6873 report(node.path, error.what());
6880inline void ScreenCore::presentForm(
DocNode const& node, LowerCtx
const& ctx, forms::FormViewOptions& options) {
6881 if (
ExprPtr const label = node.prop(
"submitLabel")) {
6882 options.submitLabel = bindProp<std::string>(ctx, label, std::string{},
stringOf);
6884 if (
auto const shown = node.flags.find(
"showStatus"); shown != node.flags.end()) {
6885 options.showStatus = shown->second;
6887 if (
ExprPtr const columns = node.prop(
"columns"); columns !=
nullptr && columns->literal.asInt() !=
nullptr) {
6888 options.gridColumns =
static_cast<int>(*columns->literal.asInt());
6889 options.flatGridColumns = options.gridColumns;
6891 if (node.name(
"fieldLayout") ==
"inline") {
6892 options.fieldLayout = forms::FieldLayout::Inline;
6896inline Node ScreenCore::lowerComposite(
DocNode const& node, LowerCtx
const& ctx, ElementState* element) {
6899 forms::FormViewOptions
form{};
6900 form.showTitle =
false;
6901 presentForm(node, ctx, form);
6902 Prop<std::string> title = bindProp<std::string>(ctx, node.prop(
"title"), std::string{}, stringOf);
6904 if (element !=
nullptr &&
element->collection !=
nullptr) {
6905 forms::CollectionViewOptions options{};
6906 options.form = std::move(form);
6907 options.title = std::move(title);
6908 view = forms::collectionView(*
element->collection, std::move(options));
6909 }
else if (element !=
nullptr &&
element->wizard !=
nullptr) {
6910 forms::WizardViewOptions options{};
6911 options.form = std::move(form);
6912 options.title = std::move(title);
6913 view = forms::wizardView(*
element->wizard, options);
6918 commonOf(node, ctx, element,
column.common);
6919 return ui::column(std::move(
column));
6924inline Node ScreenCore::lowerDialog(
DocNode const& node, LowerCtx
const& ctx, ElementState* element) {
6925 Prop<bool> open = node.prop(
"open") ? bindProp<bool>(ctx, node.prop(
"open"),
false, boolOf)
6927 Node body =
lazy([
this, ctx, &node, element]() ->
Node {
6928 auto inner = std::make_shared<RtScope>(*
this, ctx.scope,
nullptr, ctx.scope->nodes.depth() + 2);
6931 inner->submitted = &
element->submitted;
6932 inner->showErrors = std::string{node.name(
"showErrors")};
6933 std::vector<DocNode const*>
content;
6934 for (
DocNodePtr const& child : node.nodes(
"children")) {
6935 content.push_back(child.get());
6937 std::vector<Commands const*> lists;
6938 for (std::string_view
const key : {
"default",
"cancel"}) {
6939 if (
Commands const*
const commands = node.event(key)) {
6940 lists.push_back(commands);
6943 for (
DocNodePtr const& action : node.partList(
"actions")) {
6944 if (
Commands const*
const commands = action->event(
"onClick")) {
6945 lists.push_back(commands);
6948 declare(*inner, node.scope ? &*node.scope :
nullptr, content, lists);
6949 element->inner.set(inner.get());
6950 LowerCtx
const innerCtx{.scope = inner, .bindings = ctx.bindings, .boundary = ctx.boundary};
6951 std::vector<Node> children;
6952 children.reserve(
content.size());
6953 for (
DocNode const*
const child : content) {
6954 children.push_back(lower(*child, innerCtx));
6957 if (ActionModels actions = lowerActionParts(node.partList(
"actions"), innerCtx); !actions.empty()) {
6958 children.push_back(actionBarOf(std::move(actions)));
6960 std::vector<KeyBinding> keys;
6961 if (
Commands const*
const commands = node.event(
"default")) {
6963 .chord =
"Enter", .onPress = [
this, innerCtx, commands] { runEvent(commands, innerCtx,
Value{}); }});
6965 if (
Commands const*
const commands = node.event(
"cancel")) {
6967 .chord =
"Esc", .onPress = [
this, innerCtx, commands] { runEvent(commands, innerCtx,
Value{}); }});
6969 return holding(std::move(children), inner, std::move(keys));
6971 Action onDismiss = [
this, ctx,
element, commands = node.event(
"onDismiss")] {
6976 runEvent(commands, ctx,
Value{});
6978 if (node.kind ==
"drawer") {
6979 ExprPtr const side = node.prop(
"side");
6981 .open = std::move(open),
6982 .title = bindProp<std::string>(ctx, node.prop(
"title"), std::string{}, stringOf),
6983 .child = std::move(body),
6984 .onDismiss = std::move(onDismiss),
6986 commonOf(node, ctx, element, spec.common);
6987 return ui::drawer(std::move(spec));
6989 Dialog spec{.open = std::move(open),
6990 .title = bindProp<std::string>(ctx, node.prop(
"title"), std::string{}, stringOf),
6991 .child = std::move(body),
6992 .onDismiss = std::move(onDismiss)};
6993 commonOf(node, ctx, element, spec.common);
6994 return ui::dialog(std::move(spec));
6997inline Node ScreenCore::lowerSwitch(
DocNode const& node, LowerCtx
const& ctx) {
6998 std::vector<ExprPtr> whens;
7000 std::int64_t index = 0;
7001 for (
DocNodePtr const& part : node.partList(
"cases")) {
7002 whens.push_back(part->prop(
"when"));
7003 DocNode const*
const casePart = part.get();
7005 .key =
Key{index++},
7006 .node =
lazy([
this, ctx, casePart]() ->
Node {
7007 auto inner = std::make_shared<RtScope>(*
this, ctx.scope,
nullptr, ctx.scope->nodes.depth() + 2);
7008 std::vector<DocNode const*>
content;
7009 for (
DocNodePtr const& child : casePart->nodes(
"children")) {
7010 content.push_back(child.get());
7012 declare(*inner, casePart->scope ? &*casePart->scope :
nullptr, content, {});
7013 LowerCtx
const innerCtx{.scope = inner, .bindings = ctx.bindings, .boundary = ctx.boundary};
7014 std::vector<Node> children;
7015 children.reserve(
content.size());
7016 for (
DocNode const*
const child : content) {
7017 children.push_back(lower(*child, innerCtx));
7019 return holding(std::move(children), inner);
7023 [scope = ctx.scope, bindings = ctx.bindings, selected = node.prop(
"on"), whens = std::move(whens)] {
7024 Value const value = evalIn(*scope, bindings.get(), selected);
7025 for (std::size_t at = 0; at < whens.size(); ++at) {
7026 if (evalIn(*scope, bindings.get(), whens.at(at)) == value) {
7027 return Key{static_cast<std::int64_t>(at)};
7030 return Key{std::int64_t{-1}};
7032 auto const fallback = node.nodes(
"default");
7033 spec.fallback = fallback.empty() ?
Node{} : lowerAll(fallback, ctx);
7034 commonOf(node, ctx,
nullptr, spec.common);
7035 return ui::switchOf(std::move(spec));
7038inline std::shared_ptr<RtScope> ScreenCore::rowScope(
DocNode const& node, LowerCtx
const& ctx,
7039 reactive::Signal<Value>
const&
row,
7040 std::vector<DocNode const*>
const& content) {
7041 auto scope = std::make_shared<RtScope>(*
this, ctx.scope, &
row, ctx.scope->nodes.depth() + 2);
7042 declare(*scope, node.scope ? &*node.scope :
nullptr, content, {});
7047inline Node ScreenCore::lowerRows(
DocNode const& node, LowerCtx
const& ctx, ElementState* element) {
7048 ExprPtr const rowsExpr = node.prop(
"rows");
7050 report(node.path,
"a model-backed `table` needs the table engine, which this interpreter does not lower yet");
7053 auto rows = [scope = ctx.scope, bindings = ctx.bindings, rowsExpr] {
7054 Value const value =
evalIn(*scope, bindings.get(), rowsExpr);
7055 ValueList const*
const items = value.asList();
7056 return items ==
nullptr ? std::vector<Value>{} : std::vector<Value>{items->begin(), items->end()};
7058 auto keyFn = [scope = ctx.scope, bindings = ctx.bindings, keyExpr = node.prop(
"key")](
Value const&
row) ->
Key {
7061 return keyOf(rowKey.isNull() ?
row : rowKey);
7063 ScopeEnv
const env{*scope, bindings.get(), &
row};
7066 if (node.kind ==
"forEach") {
7067 std::vector<DocNode const*>
content;
7068 for (
DocNodePtr const& child : node.nodes(
"children")) {
7069 content.push_back(child.get());
7071 auto rowView = [
this, ctx, &node,
content](reactive::Signal<Value>
const&
row) ->
Node {
7072 auto scope = rowScope(node, ctx,
row, content);
7073 LowerCtx
const rowCtx{.scope = scope, .bindings = ctx.bindings, .boundary = ctx.boundary};
7074 std::vector<Node> children;
7075 children.reserve(
content.size());
7076 for (
DocNode const*
const child : content) {
7077 children.push_back(lower(*child, rowCtx));
7079 return holding(std::move(children), scope);
7081 ExprPtr const axis = node.prop(
"axis");
7082 ExprPtr const gap = node.prop(
"gap");
7083 return ui::forEach<Value>(std::move(rows), std::move(keyFn), std::move(rowView),
7085 gap ?
static_cast<int>(
intOf(once(ctx, gap), 0)) : 0);
7087 std::vector<TableColumn> columns;
7088 std::vector<DocNode const*>
cells;
7089 std::vector<std::string> fields;
7093 std::vector<TextRole> roles;
7095 if (
ExprPtr const derived = node.prop(
"columns")) {
7098 for (
Value const& item : *items) {
7100 Value const*
const label = item.
field(
"label");
7101 fields.push_back(field ==
nullptr ? std::string{} :
textOf(*field, format));
7104 cells.push_back(
nullptr);
7109 for (
DocNodePtr const& part : node.partList(
"columns")) {
7110 ExprPtr const label = part->prop(
"label");
7111 ExprPtr const width = part->prop(
"width");
7112 ExprPtr const role = part->prop(
"role");
7114 columns.push_back(
TableColumn{.label = label ?
textOf(once(ctx, label), format) : std::string{},
7116 .role = columnRole});
7117 cells.push_back(part.get());
7118 fields.emplace_back(part->id);
7119 roles.push_back(columnRole);
7121 auto registry = std::make_shared<std::map<std::string, std::weak_ptr<RtScope>, std::less<>>>();
7123 auto toneReported = std::make_shared<bool>(
false);
7124 auto cellsOf = [
this, ctx, &node,
cells, fields, roles, registry, keyFn,
7125 toneReported](reactive::Signal<Value>
const&
row) {
7126 std::vector<DocNode const*>
content;
7130 content.push_back(cell.get());
7134 auto scope = rowScope(node, ctx,
row, content);
7136 LowerCtx
const rowCtx{.scope = scope, .bindings = ctx.bindings, .boundary = ctx.boundary};
7137 std::vector<Node> out;
7138 for (std::size_t index = 0; index <
cells.size(); ++index) {
7140 auto const nodes =
column ==
nullptr ? std::span<DocNodePtr const>{} :
column->nodes(
"cell");
7141 if (!nodes.empty()) {
7142 out.push_back(lowerAll(nodes, rowCtx));
7145 out.push_back(ui::text(
Text{.text = slotProp<std::string>([
this, &
row, field = fields.at(index)] {
7146 Value const* const value = row.get().field(field);
7147 return value == nullptr ? std::string{}
7150 .
role = roles.at(index)}));
7153 out.front() =
holding({out.front()}, scope);
7155 TableRowView view{.cells = std::move(out), .tone = std::nullopt};
7156 if (
ExprPtr const toneExpr = node.prop(
"rowTone")) {
7157 view.tone = slotProp<std::optional<Tone>>(
7158 [
this, scope, bindings = ctx.bindings, toneExpr, toneReported]() -> std::optional<Tone> {
7159 return rowToneOf(
evalIn(*scope, bindings.get(), toneExpr), toneExpr->path, *toneReported);
7165 options.
showHeader = node.prop(
"showHeader") ?
boolOf(once(ctx, node.prop(
"showHeader")), format) : true;
7166 if (
ExprPtr const threshold = node.prop(
"virtualizeThreshold")) {
7167 options.virtualize.threshold =
7168 static_cast<std::size_t
>(std::max<std::int64_t>(0,
intOf(once(ctx, threshold), 2000)));
7170 if (
ExprPtr const overscan = node.prop(
"virtualizeOverscan")) {
7171 options.virtualize.overscan =
7172 static_cast<std::size_t
>(std::max<std::int64_t>(0,
intOf(once(ctx, overscan), 10)));
7174 std::string
const mode =
7175 node.prop(
"selectionMode") ?
textOf(once(ctx, node.prop(
"selectionMode")), format) : std::string{};
7177 if (mode ==
"single") {
7179 }
else if (mode ==
"multiple") {
7182 options.selection = slotProp<std::vector<Key>>([
element] {
7183 std::vector<Key> keys;
7185 for (
Value const& item : *items) {
7186 keys.push_back(
keyOf(item));
7191 options.onSelectionChange = [
this,
element](std::vector<Key>
const& keys) {
7193 for (
Key const& key : keys) {
7201 options.onActivate = [
this, ctx,
element, registry, commands = node.event(
"onActivate")](
Key const& key) {
7205 element->activeKey.set(activated);
7207 auto const found = registry->find(
canonicalJson(activated));
7208 std::shared_ptr<RtScope>
const scope = found == registry->end() ? nullptr : found->second.lock();
7209 LowerCtx
const rowCtx{.scope = scope ? scope : ctx.scope, .bindings = ctx.bindings, .boundary = ctx.boundary};
7210 runEvent(commands, rowCtx, activated);
7212 commonOf(node, ctx, element, options.common);
7213 std::span<DocNodePtr const>
const empty = node.nodes(
"empty");
7214 if (empty.empty()) {
7215 return ui::table<Value>(std::move(columns), std::move(rows), std::move(keyFn), std::move(cellsOf),
7216 std::move(options));
7222 auto const listed = [scope = ctx.scope, bindings = ctx.bindings, rowsExpr] {
7224 return items ==
nullptr || !items->empty();
7228 wrapper.common.layout = options.common.layout;
7229 options.common.visible = slotProp<bool>(listed);
7231 content.common.visible = slotProp<bool>([listed] {
return !
listed(); });
7232 wrapper.children.push_back(ui::table<Value>(std::move(columns), std::move(rows), std::move(keyFn),
7233 std::move(cellsOf), std::move(options)));
7234 wrapper.children.push_back(ui::column(std::move(content)));
7235 return ui::column(std::move(wrapper));
7238inline std::optional<Tone> ScreenCore::rowToneOf(
Value const& value, std::string
const& path,
bool& noted) {
7239 if (value.isNull()) {
7240 return std::nullopt;
7242 static constexpr std::array<std::pair<std::string_view, Tone>, 5> kTones = {{
7249 if (std::string
const*
const text = value.asString()) {
7252 for (
auto const& [name, tone] : kTones) {
7253 if (name == *
text) {
7260 report(path,
"rowTone is " +
canonicalJson(value) +
"; a tone is neutral, info, ok, warn or err");
7262 return std::nullopt;
7265inline void ScreenCore::ownQueriesOf(
ExprPtr const& expr, RtScope
const& scope, BoundaryRuntime& boundary,
7266 std::set<std::string, std::less<>>& seen) {
7273 NameRef
const*
const found = scope.find(expr->name);
7274 if (found !=
nullptr && found->category == NameCategory::Query && seen.insert(expr->name).second) {
7275 boundary.queries.push_back(found->query);
7277 if (expr->kind ==
ExprKind::Ref && found !=
nullptr && found->category == NameCategory::Let &&
7278 seen.insert(
"let:" + expr->name).second) {
7279 ownQueriesOf(found->letExpr, scope, boundary, seen);
7282 for (
ExprPtr const& arg : expr->args) {
7283 ownQueriesOf(arg, scope, boundary, seen);
7285 for (
auto const& [name, field] : expr->fields) {
7286 static_cast<void>(name);
7287 ownQueriesOf(field, scope, boundary, seen);
7291inline void ScreenCore::ownQueries(
DocNode const& node, RtScope
const& scope, BoundaryRuntime& boundary,
7292 std::set<std::string, std::less<>>& seen) {
7293 if (node.kind ==
"boundary") {
7296 for (
auto const& [key, expr] : node.props) {
7297 static_cast<void>(key);
7298 ownQueriesOf(expr, scope, boundary, seen);
7300 for (
auto const& [key, nodes] : node.children) {
7301 static_cast<void>(key);
7303 ownQueries(*child, scope, boundary, seen);
7306 for (
auto const& [key, parts] : node.parts) {
7307 static_cast<void>(key);
7309 ownQueries(*part, scope, boundary, seen);
7314inline Node ScreenCore::lowerBoundary(
DocNode const& node, LowerCtx
const& ctx) {
7315 auto boundary = std::make_shared<BoundaryRuntime>();
7316 std::set<std::string, std::less<>> seen;
7317 for (
DocNodePtr const& child : node.nodes(
"children")) {
7318 ownQueries(*child, *ctx.scope, *boundary, seen);
7320 auto bindings = std::make_shared<Bindings>(*ctx.bindings);
7321 bindings->push_back(Binding{.name =
"boundary", .read = [
boundary] {
7322 for (QueryRuntime
const*
const query :
boundary->queries) {
7323 Value const error = query->error();
7324 if (!error.isNull() && query->value().isNull()) {
7330 LowerCtx
const errorCtx{.scope = ctx.scope, .bindings = bindings, .boundary =
boundary};
7331 LowerCtx
const childCtx{.scope = ctx.scope, .bindings = ctx.bindings, .boundary =
boundary};
7332 auto group = [
this](std::span<DocNodePtr const> nodes, LowerCtx
const& where) ->
Node {
7333 return nodes.empty() ? ui::spacer() : lowerAll(nodes, where);
7336 spec.
cases.push_back(
SwitchCase{.key =
Key{std::int64_t{0}}, .node = group(node.nodes(
"loading"), childCtx)});
7337 spec.cases.push_back(
SwitchCase{.key =
Key{std::int64_t{1}}, .node = group(node.nodes(
"error"), errorCtx)});
7338 spec.cases.push_back(
SwitchCase{.key =
Key{std::int64_t{2}}, .node = group(node.nodes(
"children"), childCtx)});
7339 spec.selector = slotProp<Key>([boundary] {
7340 bool loading =
false;
7341 for (QueryRuntime
const*
const query :
boundary->queries) {
7342 if (!query->settled()) {
7344 }
else if (query->value().isNull() && !query->error().isNull()) {
7345 return Key{std::int64_t{1}};
7348 return Key{std::int64_t{loading ? 0 : 2}};
7350 commonOf(node, ctx,
nullptr, spec.common);
7351 return ui::switchOf(std::move(spec));
7354inline Node ScreenCore::lowerCustom(
DocNode const& node, LowerCtx
const& ctx) {
7355 std::string
const name{node.name(
"name")};
7357 Component const& component = _doc->components.at(name);
7358 std::vector<std::pair<std::string, ExprPtr>> props;
7359 for (
DocNodePtr const& record : node.partList(
"props")) {
7360 for (
auto const& [key, expr] : record->props) {
7361 props.emplace_back(key, expr);
7364 auto bindings = std::make_shared<Bindings>();
7365 bindings->push_back(
7366 Binding{.name =
"props", .read = [scope = ctx.scope, outer = ctx.bindings, props = std::move(props)] {
7368 for (
auto const& [key, expr] : props) {
7369 record.insert_or_assign(key,
evalIn(*scope, outer.get(), expr));
7371 return Value{std::move(record)};
7373 auto scope = std::make_shared<RtScope>(*
this,
nullptr,
nullptr, ctx.scope->nodes.depth() + 1);
7374 DocNode const& fallback = *component.fallback;
7375 declare(*scope,
nullptr, {&fallback}, {});
7376 Node lowered = lower(fallback, LowerCtx{.scope = scope, .bindings = bindings, .boundary =
nullptr});
7377 Custom spec{.name = name,
7378 .fallback = std::move(lowered),
7381 static_cast<int>(std::clamp<std::int64_t>(component.version, 0, std::numeric_limits<int>::max())),
7384 for (
auto const& [prop, type] : component.props) {
7386 for (
DocNodePtr const& record : node.partList(
"props")) {
7387 if (
ExprPtr const given = record->prop(prop)) {
7391 if (type ==
"bool") {
7392 spec.props.push_back(
CustomProp{.name =
prop, .value = bindProp<bool>(ctx, expr,
false, boolOf)});
7393 }
else if (type ==
"number") {
7399 spec.props.push_back(
7401 .value = bindProp<std::string>(ctx, expr, std::string{},
7407 spec.
onEvent = [
this, ctx, site = &node, declared = &component](std::string
const&
event,
7408 std::string
const& payload) {
7409 Commands const*
const commands = site->event(
"on." + event);
7410 if (commands ==
nullptr) {
7413 auto const type = declared->events.find(event);
7414 runEvent(commands, ctx,
componentPayload(payload, type == declared->events.end() ?
nullptr : &type->second));
7416 return ui::custom(std::move(spec));
7436 std::span<DocNode const* const> declared) {
7437 auto const isDeclared = [declared](std::string
const& childId) {
7438 return std::ranges::find(declared, childId, &
DocNode::id) != declared.end();
7440 std::vector<ArrangedChild> out;
7441 std::set<std::string, std::less<>> placed;
7443 for (
Value const& entry : *entries) {
7444 Value const*
const named = entry.
field(
"id");
7445 std::string
const*
const name = named ==
nullptr ? nullptr : named->
asString();
7446 if (name ==
nullptr || !isDeclared(*name) || !placed.insert(*name).second) {
7449 Value const*
const hidden = entry.
field(
"hidden");
7450 bool const*
const flag = hidden ==
nullptr ? nullptr : hidden->
asBool();
7451 out.push_back(
ArrangedChild{.id = *name, .hidden = flag !=
nullptr && *flag});
7454 for (
DocNode const*
const child : declared) {
7455 if (!placed.contains(child->id)) {
7466[[nodiscard]]
inline std::vector<Value>
arrangedOrder(
Value const& stored, std::span<DocNode const* const> declared) {
7467 std::vector<Value> order;
7469 if (!child.hidden) {
7470 order.emplace_back(child.id);
7481 entries.reserve(children.size());
7485 return Value{std::move(entries)};
7494 if (node.
id == elementId) {
7497 for (
auto const*
const groups : {&node.
children, &node.
parts}) {
7498 for (
auto const& [
slot, nodes] : *groups) {
7499 static_cast<void>(
slot);
7510inline std::string ScreenCore::arrangedBy(std::string
const& elementId)
const {
7511 if (elementId.empty()) {
7514 for (
auto const& [name, point] : _doc->customize) {
7515 if (point.kind ==
"arrange" && point.of == elementId) {
7525inline Node ScreenCore::lowerArranged(
DocNode const& node, LowerCtx
const& ctx, ElementState* element,
7526 std::string
const& point,
Axis axis,
int gap) {
7527 std::vector<DocNode const*> declared;
7528 for (
DocNodePtr const& child : node.nodes(
"children")) {
7529 declared.push_back(child.get());
7531 auto order = [
this, point, declared] {
return arrangedOrder(prefs.
get().at(point), declared); };
7532 auto key = [](
Value const& childId) {
return keyOf(childId); };
7533 auto view = [
this, ctx, declared](reactive::Signal<Value>
const&
row) ->
Node {
7536 return lower(**found, ctx);
7539 .model = std::make_shared<TypedForEach<Value>
const>(std::move(order), std::move(key), std::move(view)),
7542 commonOf(node, ctx, element, spec.common);
7548inline Node ScreenCore::lowerArrangeEditor(
DocNode const& node, LowerCtx
const& ctx, std::string
const& point,
7552 std::vector<DocNode const*> declared;
7553 for (
DocNodePtr const& child : container->nodes(
"children")) {
7554 declared.push_back(child.get());
7556 auto const current = [
this, point, declared] {
return arrangedChildren(prefs.
get().at(point), declared); };
7557 auto const place = [current](std::string
const& childId) {
7558 auto const children = current();
7559 return static_cast<std::size_t
>(std::ranges::find(children, childId, &ArrangedChild::id) - children.begin());
7562 auto const edit = [
this, current, choose](std::string
const& childId,
auto const& change) {
7563 auto children = runtime().untracked(current);
7564 auto const found = std::ranges::find(children, childId, &ArrangedChild::id);
7565 change(children,
static_cast<std::size_t
>(found - children.begin()));
7568 auto rows = [current] {
7569 std::vector<Value> ids;
7570 for (ArrangedChild
const& child : current()) {
7571 ids.emplace_back(child.id);
7575 auto key = [](
Value const& childId) {
return keyOf(childId); };
7576 auto view = [
this, current, place, edit](reactive::Signal<Value>
const&
row) ->
Node {
7579 shown.checked = slotProp<bool>([current, childId] {
7580 auto const children = current();
7581 return !std::ranges::find(children, childId, &ArrangedChild::id)->hidden;
7583 shown.onToggle = [edit, childId](
bool checked) {
7584 edit(childId, [checked](std::vector<ArrangedChild>& children, std::size_t index) {
7585 children.at(index).hidden = !checked;
7588 Button moveUp{.label = std::string{
"\xE2\x86\x91"}};
7589 moveUp.
onClick = [edit, childId] {
7591 edit(childId, [](std::vector<ArrangedChild>& children, std::size_t index) {
7592 std::swap(children.at(index), children.at(std::max(index, std::size_t{1}) - 1));
7595 moveUp.common.enabled = slotProp<bool>([place, childId] {
return place(childId) > 0; });
7596 moveUp.common.a11y.name =
"Move " + childId +
" up";
7597 moveUp.common.testId =
"arrange-up-" + childId;
7598 Button moveDown{.label = std::string{
"\xE2\x86\x93"}};
7599 moveDown.
onClick = [edit, childId] {
7600 edit(childId, [](std::vector<ArrangedChild>& children, std::size_t index) {
7601 std::swap(children.at(index), children.at(std::min(index + 1, children.size() - 1)));
7604 moveDown.common.enabled =
7605 slotProp<bool>([current, place, childId] {
return place(childId) + 1 < current().size(); });
7606 moveDown.common.a11y.name =
"Move " + childId +
" down";
7607 moveDown.common.testId =
"arrange-down-" + childId;
7608 shown.common.testId =
"arrange-shown-" + childId;
7609 return ui::row(
Row{.children = {ui::checkbox(std::move(shown)), ui::button(std::move(moveUp)),
7610 ui::button(std::move(moveDown))},
7614 std::make_shared<TypedForEach<Value>
const>(std::move(rows), std::move(key), std::move(view))};
7615 commonOf(node, ctx,
nullptr, editor.common);
7616 return makeNode(std::move(editor));
7619inline Node ScreenCore::lowerCustomize(
DocNode const& node, LowerCtx
const& ctx) {
7620 std::string
const point{node.name(
"point")};
7623 auto choose = [
this, point](
Value value) {
7626 next.insert_or_assign(point, value);
7627 prefs.
set(std::move(next));
7629 if (host !=
nullptr) {
7630 host->preferenceChanged(_doc->id, point, value);
7633 if (spec.kind ==
"arrange") {
7634 return lowerArrangeEditor(node, ctx, point, spec, choose);
7636 if (spec.kind ==
"choice") {
7638 std::vector<SelectOption> options;
7639 options.reserve(spec.options.size());
7640 for (std::string
const& option : spec.options) {
7643 select.options = std::move(options);
7644 select.selected = slotProp<std::optional<Key>>([
this, point]() -> std::optional<Key> {
7645 Value const& chosen = prefs.
get().at(point);
7646 return chosen.isNull() ? std::nullopt : std::optional<Key>{
keyOf(chosen)};
7649 commonOf(node, ctx,
nullptr,
select.common);
7650 return ui::select(std::move(
select));
7652 if (spec.kind ==
"value" && (spec.type.kind ==
TypeKind::String || spec.type.isAny())) {
7655 slotProp<std::string>([
this, point] {
return textOf(prefs.
get().at(point),
session.formatting()); });
7656 input.onChange = [choose](std::string
text) { choose(
Value{std::move(
text)}); };
7657 commonOf(node, ctx,
nullptr, input.common);
7658 return ui::textInput(std::move(input));
7661 commonOf(node, ctx,
nullptr, placeholder.common);
7662 return ui::text(std::move(placeholder));
7675 .token = _core->runtime().untracked([&] {
return model.token->get(); })};
7682 auto refuse = [&](std::string path, std::string reason) {
7683 return std::unexpected(
Refusal{.document = doc->id, .path = std::move(path), .reason = std::move(reason)});
7686 return refuse(
"",
"the document has no view");
7689 for (
auto const& [name, type] : doc->params) {
7690 auto const found = params.find(name);
7691 Value value = found == params.end() ?
Value{} : found->second;
7692 if (!type.admits(value)) {
7694 ?
"param `" + name +
"` is missing"
7695 :
"param `" + name +
"` is not a " + type.name());
7697 given.insert_or_assign(name, std::move(value));
7699 for (
ModelDecl const& model : doc->scope.models) {
7701 return refuse(model.path,
"alias `" + model.name +
"` borrows a model its opener did not lend");
7705 std::unique_ptr<Screen> screen{
new Screen{}};
7706 screen->_core = std::make_unique<detail::ScreenCore>(doc, *_session, *_dispatcher, _options, options);
7709 core.keptStates = options.
keepStateOf->ownState();
7711 screen->_scope = std::make_shared<detail::RtScope>(core,
nullptr,
nullptr, options.
depth);
7712 core.rootScope = screen->_scope;
7716 for (
auto const& [name, value] : given) {
7720 scope.
names.insert_or_assign(name, std::move(ref));
7722 std::vector<Commands const*> lists;
7723 for (
auto const& [key, commands] : doc->lifecycle) {
7724 static_cast<void>(key);
7725 lists.push_back(&commands);
7727 for (
auto const& [chord, commands] : doc->keys) {
7728 static_cast<void>(chord);
7729 lists.push_back(&commands);
7731 for (
DocNodePtr const& action : doc->actions) {
7732 if (
Commands const*
const commands = action->event(
"onClick")) {
7733 lists.push_back(commands);
7737 core.declare(scope, &doc->scope, {doc->view.get()}, lists);
7739 .scope = screen->_scope, .bindings = std::make_shared<detail::Bindings const>(), .boundary =
nullptr};
7740 Node lowered = core.lower(*doc->view, ctx);
7741 screen->_headStatus = core.lowerBadgeParts(doc->status, ctx);
7742 screen->_headActions = core.lowerActionParts(doc->actions, ctx);
7743 if (!doc->keys.empty()) {
7744 std::vector<KeyBinding> keys;
7745 for (
auto const& [chord, commands] : doc->keys) {
7747 .chord = chord, .onPress = [&core, ctx, list = &commands] { core.runEvent(list, ctx,
Value{}); }});
7750 column.common.keys = std::move(keys);
7755 screen->_mounted.emplace(runtime, backend, std::move(root), options.
parent, options.
depth + 1);
7756 screen->_core->focusElement = [mounted = &*screen->_mounted](std::string_view element) {
7757 return mounted->focus(element);
7759 screen->runLifecycle(
"onMount");
7764 if (doc->id !=
"app") {
7765 return std::unexpected(
7766 Refusal{.document = doc->id, .path =
"/id", .reason =
"the app document's id is `app`"});
7768 if (_session->appScope() !=
nullptr) {
7769 return std::unexpected(
7770 Refusal{.document = doc->id, .path = {}, .reason =
"an app document is already mounted on the session"});
7772 std::unique_ptr<AppRoot> root{
new AppRoot{}};
7773 root->_core = std::make_unique<detail::ScreenCore>(doc, *_session, *_dispatcher, _options,
MountOptions{});
7775 root->_scope = std::make_shared<detail::RtScope>(core,
nullptr,
nullptr, 0);
7776 core.rootScope = root->_scope;
7777 std::vector<Commands const*> lists;
7778 for (
auto const& [key, commands] : doc->lifecycle) {
7779 static_cast<void>(key);
7780 lists.push_back(&commands);
7782 _session->runtime().untracked([&] { core.declare(*root->_scope, &doc->scope, {}, lists); });
7783 _session->setAppScope(root->_scope.get());
7784 if (
auto const found = doc->lifecycle.find(
"onMount"); found != doc->lifecycle.end()) {
7785 root->runCommands(found->second);
Justify
Where a stack places its spare length along its axis, while no shown child stretches.
@ Center
Packed in the middle.
@ Between
In equal gaps between the children, never less than the stack's gap.
@ Start
Packed at the start.
Align
Where a child is placed across its container's axis: a column's horizontally, a row's vertically,...
@ End
At the end: right in a column, bottom in a row or grid.
@ Start
At the start: left in a column, top in a row or grid.
@ Stretch
A content child fills the cross extent; one its maximum stops sits at the start.
Node text(TextOptions const &options)
A text node.
Element element(std::string name)
An element of the view.
AnyHandle list(std::vector< AnyHandle > const &items)
{"list": [...]}: a list built from expressions.
AnyHandle env(std::string const &name)
A client environment value, {"env": name}: widthClass, locale, platform, online, backend.
Command command(std::string_view key, Json value)
A command with one key.
Node column(Nodes const &children, ContainerOptions const &options={})
A column node.
Handle< std::string > format(AnyHandle const &pattern, std::vector< AnyHandle > args)
format: a template with {0}, {1}, … placeholders.
Handle< T > row()
The innermost row, {"row": ""}.
Command host(std::string const &name, AnyHandle const &args={})
host: a command the application registered with the client.
Json fieldOf(Json const &json, std::string_view field)
A field read below a named read.
Command run(MutationHandle< Action > const &mutation, Body< Action > const &body={}, RunOptions const &options={})
run: runs a mutation.
AnyHandle prop(std::string const &name)
A prop of a component, inside the component's fallback.
AnyHandle session(std::string const &name)
A session attribute, {"session": name}.
AnyHandle content()
The content sizing, "content": as much as the content needs.
std::vector< TableColumn > cells(std::vector< Node > const &cells)
Columns from cells alone, without ids or labels.
Node collection(ModelHandle< Model > const &model, FormPresentation const &presentation={})
A collection node: the forms engine's list screen over an action.
Command open(Element const &dialog)
open: opens a dialog or drawer.
Node boundary(Nodes const &children, Nodes const &loading={}, Nodes const &error={})
A boundary node: loading until its queries have values, error when one failed without one.
Handle< std::int64_t > count(AnyHandle const &list)
count: a list's length.
AnyHandle event()
The payload of the event whose command list runs.
void actionPart(NodeWriter &node, std::optional< ActionPart > const &action)
Writes an action part.
constexpr Self self
The dialog or drawer a command list runs in.
Node form(ModelHandle< Model > const &model, FormOptions< Action > const &options={})
A form node: a schema form of the forms engine over an action (spec 2); each field takes its label fr...
@ Omit
Blank is "no value": the member is left out of the payload (the default).
Move-only handle representing the eventual result of an asynchronous operation.
static std::pair< Completion< T >, Promise > makeSettleable(::morph::exec::IExecutor *execPtr)
Constructs a Completion<T>/Promise<T> pair sharing one settleable state.
Central dispatcher that routes typed actions to an IBackend.
A derived value: computed lazily on first read, cached, and recomputed only after a source it read re...
T const & get() const
Reads the value, bringing it up to date first, and subscribes the innermost tracked run.
The owner of a signal graph: every node takes one, and every operation runs on its owner.
decltype(auto) untracked(F &&fn) const
Runs fn with tracking off: reads inside it subscribe nothing.
Runs callbacks after a delay or on a period, on the runtime's owner executor.
virtual TimerHandle every(std::chrono::milliseconds period, std::function< void()> fn)=0
Runs fn every period, starting one period from now.
virtual TimerHandle after(std::chrono::milliseconds delay, std::function< void()> fn)=0
Runs fn once, delay from now.
Owns effects, computeds and adopted objects; destroys them in reverse creation order.
auto & computed(F &&fn)
Makes a Computed owned by this scope; its type is what fn returns.
Effect & effect(F &&fn)
Makes an Effect owned by this scope, at this scope's depth.
A source value. Reading it with get() inside a tracked run subscribes that run.
T const & get() const
Reads the value, subscribing the innermost tracked run.
void set(T value)
Replaces the value and notifies observers, unless it equals the current one and the policy is Equalit...
T const & peek() const noexcept
Reads the value without subscribing anything.
Owns one scheduled timer and cancels it when destroyed. Move-only.
RuntimeCore & core() const noexcept
The runtime core.
The app document mounted as the root scope every screen of the session shares (spec 6 §5).
~AppRoot()
Runs onUnmount, takes the scope off the session, and releases everything.
std::vector< Refusal > const & diagnostics() const noexcept
The diagnostics reported so far.
Value read(Json const &expression) const
Evaluates an expression in the app document's scope.
std::shared_ptr< ModelChannel > bind(std::string const &typeId, bridge::BindSharing sharing, std::string const &instanceKey) override
Binds through a RawHandler.
BridgeDispatcher(bridge::Bridge &bridge, exec::IExecutor *cbExec, Evaluator evaluator={})
Dispatches through bridge, and evaluates through evaluator.
The interpreter's view of the bridge: binds a model by its type id, privately or to a shared instance...
virtual std::shared_ptr< ModelChannel > bind(std::string const &typeId, bridge::BindSharing sharing, std::string const &instanceKey)=0
Binds a model.
Makes widgets. Each factory appends the new widget to parent, or makes a root when it is null,...
The development inspector over one mounted screen: its mounted nodes, its queries and mutations,...
What the application answers for the commands a document cannot carry out alone (spec 6)....
virtual bool saveFile(InlineFile const &file, std::string const &option, std::function< void(std::expected< Value, std::string >)> done)
Saves a file a document holds the bytes of, for download from bytes.
virtual void setLocale(std::string const &locale)
Stores the active locale as a preference. The interpreter has already re-resolved every t.
virtual void signOut()
Ends the session. The interpreter has already cleared the session's query cache.
virtual void permissionsStale()
A call was refused with error kind denied: the permitted set is a snapshot, so the host asks the serv...
virtual bool hostCommand(std::string const &name, Value const &args)
Runs a command the application registered (host/1).
virtual bool uploadFor(UploadRequest const &request, std::function< void(Value)> progress, std::function< void(std::expected< Value, std::string >)> done)
Sends a file through the server's file side channel, knowing the mutation that will reference it....
virtual bool setAppValue(std::string const &name, Value const &value)
Writes app.name with set (spec 6 §5).
virtual bool navigate(NavigateRequest const &request, std::function< void(Value)> onResult, std::function< void()> onCancel)
Opens another screen.
virtual bool download(Value const &file, std::string const &option, Value const &name, std::function< void(std::expected< Value, std::string >)> done)
Fetches a file by reference to save it, or to a local temporary reference for a viewer.
virtual bool readFile(Value const &file, std::size_t maxBytes, std::function< void(std::expected< InlineFile, std::string >)> done)
Reads a file's bytes for upload into bytes. A host reads the file's size first and refuses one over m...
virtual void resetPrefs(std::string const &screen, std::string const &point)
The document reset a customization point, or all of them; the host forgets the stored choice.
virtual void preferenceChanged(std::string const &screen, std::string const &point, Value const &value)
The user chose at a customization point through a customize editor; the host stores the choice.
virtual bool upload(Value const &file, std::function< void(Value)> progress, std::function< void(std::expected< Value, std::string >)> done)
Sends a file through the server's file side channel (files/1).
virtual bool closeSelf(std::string const &screen, std::optional< Value > result)
Ends the screen that ran return or close: self.
virtual bool signIn(Value const &reply)
Installs the session the reply of a sign-in mutation carries, and reloads the catalog.
virtual bool notify(std::string const &text, std::string const &role)
Shows a transient message.
virtual bool switchBackend(std::string const &name, std::function< void(std::expected< void, std::string >)> done)
Switches to a backend the application's source lists (AppSource::switchBackend, spec 6 §9).
Mounts UI documents (spec 5): one per client process or window, over a session and a dispatcher.
std::expected< std::unique_ptr< AppRoot >, Refusal > mountApp(std::shared_ptr< Document const > doc)
Mounts the app document as the session's root scope (spec 6 §5).
Interpreter(Session &session, Dispatcher &dispatcher, InterpreterOptions options={})
An interpreter.
std::expected< std::unique_ptr< Screen >, Refusal > mount(std::shared_ptr< Document const > doc, ValueRecord const ¶ms, IViewBackend &backend, MountOptions const &options={})
Mounts a screen.
One bound model alias: the instance a document's alias names, and the calls made through it.
virtual async::Completion< std::string > execute(std::string const &actionId, std::string const &bodyJson, CallOptions options)=0
Sends one action.
A node property: a constant, or a slot — a reactive read, with a stable id when it was given one.
A mounted screen: its scopes, its view mounted through Mounted, and what its host asks of it.
bool dirty() const
Evaluates the document's dirty.
bool setPreference(std::string const &point, Value value)
Sets the user's choice at a customization point, as the preferences store loaded or the user made it....
~Screen()
Runs onUnmount, then unmounts.
std::vector< HeadAction > actions() const
The head's actions that are visible now, in order, each with whether it is enabled....
bool runAction(std::string_view actionId)
Runs a head action's onClick as a user event in the screen scope, in one batch.
Document const & document() const noexcept
The screen's document, whose customize block names the points a preferences store loads.
std::string const & id() const noexcept
The document's id.
Value read(Json const &expression) const
Evaluates an expression in the screen's scope: what a host reads (title, identity),...
BorrowedModel lend(std::string_view alias) const
What an alias of this screen lends to a screen it opens that borrows it.
bool requestClose()
Runs onClose, as the host does before it closes the screen.
Value identity() const
Evaluates the document's identity.
Value subtitle() const
Evaluates the document's subtitle, the head's line under the title.
Value title() const
Evaluates the document's title.
bool pending() const
Whether the screen has work a close waits for: an open dialog or drawer, a mutation run,...
std::vector< Refusal > const & diagnostics() const noexcept
The diagnostics reported so far, each path and reason once.
void runLifecycle(std::string_view key)
Runs a lifecycle command list: onSave, onBackendChange, or any other the document declares.
std::vector< HeadBadge > status() const
The head's status badges that are visible now, in order. A tracked read.
Widget & root() const
The root widget.
reactive::Runtime & runtime() const noexcept
The reactive runtime the screen runs on.
bool declares(std::string_view key) const
Whether the document declares a lifecycle command list.
std::shared_ptr< bool const > beginClose()
Runs onClose, and hands back the flag its cancelClose sets.
What screens of one client session share (spec 5 §5, §12b): the query cache, the mounted queries,...
std::function< HostCommandResult(std::string const &, Value const &)> const & hostCommandRunner() const noexcept
What runs host commands.
void fetch(std::shared_ptr< detail::CacheEntry > const &entry, std::shared_ptr< ModelChannel > const &channel)
Sends an entry's request under a new generation; a reply of an older generation is dropped.
static std::string cacheKey(std::string const &token, std::string const &typeId, std::string const &action, bool evaluate, std::string const &body)
The cache key of a query.
detail::RtScope const * appScope() const noexcept
The app document's scope, which {"ref": "app.name"} reads and {"set": "app.name"} writes; set by Inte...
void prefetch(std::shared_ptr< ModelChannel > const &channel, std::string const &token, std::string const &typeId, std::string const &action, Value const &body)
Starts a query's request before its screen mounts, so the screen mounts on a warm cache.
void setHostValues(ValueRecord values)
Sets the host values the application registered.
void forgetQuery(detail::QueryRuntime &query)
Unregisters a mounted query.
void showLayers(detail::CacheEntry &entry)
Recomputes what an entry shows from its confirmed value and its layers.
void setHostCommands(HostCommands commands)
Sets the client's host commands: the names {"can": "host:name"} answers for, and what runs them....
void attachQuery(detail::QueryRuntime &query)
Registers a mounted query, which a reconnect and a backend switch refetch.
void asUser(std::function< void()> const &body)
Runs body as a user event: what it writes counts as the user's.
void reconnected()
The connection to the server was made again (spec 8 §7, "Reconnect"): sends the whole watch set with ...
FormatContext const & formatting() const
The formatting facts. A tracked read.
void setAppScope(detail::RtScope const *scope) noexcept
Installs or clears the app document's scope. Called by AppRoot, nothing else.
static void markStale(detail::CacheEntry &entry)
Marks an entry stale. While its fetch is in flight, the entry also refetches once that fetch settles,...
void setPermitted(std::set< std::string, std::less<> > permitted)
Sets the action ids the principal may run, as the catalog reply lists them; {"can": ....
std::string classify(std::exception_ptr const &error) const
The error kind of a failure.
std::string privateToken()
A fresh instance token for a private alias: what the cache key holds for its instance.
void deliverEvent(wire::EventBody const &event)
Applies a server event to the cache (spec 8 §9).
std::shared_ptr< detail::CacheEntry > acquire(std::string const &token, std::string const &typeId, std::string const &action, bool evaluate, Value const &body)
Finds or makes an entry and counts one more user.
void noteWrite() noexcept
Records a write to a document source.
void release(std::shared_ptr< detail::CacheEntry > const &entry)
Counts one user less; an entry nobody uses is dropped after keepUnused.
wire::WatchBody watchSet() const
The topics the cache holds now: every record an entry holds, and the keyed shared instance of every e...
void actionSucceeded(std::string const &typeId, std::string const &actionId, Value const &reply, ReplyUse use, std::string const &instanceToken)
A call of actionId on a model of typeId succeeded: applies the action's effects to the cache (spec 8 ...
void setSessionValues(ValueRecord values)
Sets the session values (principal, userName, …).
SessionOptions const & options() const noexcept
The options.
void detachScreen(detail::ScreenCore &screen)
Forgets a screen or app document.
void attachScreen(detail::ScreenCore &screen)
Registers a mounted screen or app document, whose onBackendChange a backend switch runs.
void clear()
Drops every cache entry: signOut and a principal change (spec 5 §5, "Isolation").
void setLocale(std::string locale)
Changes the active locale, keeping the other formatting facts.
std::size_t cacheSize() const noexcept
The number of entries in the cache.
Session(reactive::Runtime &runtime, SessionOptions options={})
An empty session.
void setFormatting(FormatContext format)
Replaces the formatting facts; every t and display function re-evaluates.
bool lastWriteByUser() const noexcept
Whether the newest write to a document source was made by a user event (spec 5 §6,...
bool can(std::string_view action) const
Whether the principal may run an action, or the client can run a host command now....
void setEnvironment(ValueRecord values)
Sets the env values (widthClass, locale, platform, online, backend).
void applyChanges(wire::ChangesBody const &changes)
Applies a changes event's payload to the cache, in one reactive batch.
void confirm(detail::CacheEntry &entry, Value const &confirmed)
Writes an entry's confirmed value and re-applies its optimistic layers over it.
void setTranslations(render::TranslationProvider translations)
Replaces the message catalogue, keeping the other formatting facts; every t re-resolves.
reactive::Runtime & runtime() const noexcept
The runtime.
void backendChanged(std::string const &backend)
Carries out a committed backend switch (spec 6 §9), in one batch: every mounted screen's onBackendCha...
Value client(ExprKind form, std::string_view name) const
Reads env, session or host. A tracked read.
std::chrono::milliseconds now() const
The clock.
The document's typed value (spec 5 §3): what every expression evaluates to.
Quantity const * asQuantity() const noexcept
The quantity, if held.
Decimal const * asDecimal() const noexcept
The decimal, if held.
bool isNull() const noexcept
Whether the value is null.
Value const * field(std::string_view name) const
A field of a record.
ValueRecord const * asRecord() const noexcept
The record, if held.
time::DateTime const * asTimestamp() const noexcept
The timestamp, if held.
bool const * asBool() const noexcept
The bool, if held.
std::int64_t const * asInt() const noexcept
The int, if held.
std::string const * asString() const noexcept
The string, if held.
ValueList const * asList() const noexcept
The list, if held.
AnyHandle get(std::string const &value) const
One of the element's values.
Handle< bool > open() const
Whether a dialog or drawer is open.
std::string const & name() const noexcept
The element's id.
AnyHandle draft() const
An input's text while it is edited.
AnyHandle selection() const
A table's selection.
Handle< Member > at(Member Class::*member) const
A field of the value, by member pointer: a renamed field is a compile error.
Node visible(AnyHandle const &condition) const
Whether the node shows.
A mutation at run time (spec 5 §5): concurrency, optimistic writes and the reply's effects.
void run(Value body, std::shared_ptr< Bindings const > bindings, RunDone done)
Runs it, queues the run or refuses it, as its concurrency says.
Value result() const
The last applied result. A tracked read.
Value error() const
The last applied failure's text, or null. A tracked read.
bool pending() const
Whether a run is in flight or queued. A tracked read.
~MutationRuntime()
Drops the replies still to come.
MutationDecl const & decl() const noexcept
The declaration.
Value errorKind() const
The last applied failure's kind, or null. A tracked read.
std::string const & typeId() const noexcept
The model type.
MutationRuntime(MutationDecl const &decl, RtScope &scope, ModelAlias *alias)
A mutation of scope.
A query at run time (spec 5 §5): a key over the scope, an entry of the session's cache,...
std::string const & typeId() const noexcept
The model type.
bool settled() const
Whether the query has a value or a failure for its current key, or is idle. A tracked read,...
std::shared_ptr< ModelChannel > channel() const
The channel its alias is bound to now.
bool pending() const
Whether a request is in flight or waiting out its debounce. A tracked read.
QueryRuntime(QueryDecl const &decl, RtScope &scope, ModelAlias *alias)
A query of scope, not yet started.
void detach()
Forgets its entry without releasing it: the session dropped the cache.
Value value() const
The value: the entry's, kept from the previous key while the new one has none. A tracked read.
Value error() const
The failure's text, null when none. A tracked read.
void refetch()
Refetches the current key now, ending a debounce wait.
std::shared_ptr< CacheEntry > const & entry() const
The current entry. A tracked read.
void invalidate()
Marks its entry stale and refetches it.
void refetchCurrent()
Fetches the key its body has now, inside the batch that changed the body: switches to that key's entr...
QueryDecl const & decl() const noexcept
The declaration.
Value errorKind() const
The failure's kind, null when none. A tracked read.
~QueryRuntime()
Stops its timers and releases its entry.
void start()
Starts it: makes its key and the Effect that follows it, and its refresh timer.
One document scope at run time: the screen, a dialog's or drawer's content, a switch case or a row.
reactive::Signal< Value > const * innermostRow() const
The innermost row, from this scope outwards.
std::vector< std::unique_ptr< MutationRuntime > > mutations
Its mutations.
RtScope(ScreenCore &screen, std::shared_ptr< RtScope > enclosing, reactive::Signal< Value > const *current, std::size_t depth)
An empty scope.
reactive::Signal< Value > const * row
The row of a row scope.
ScreenCore & core
The screen.
RtScope const & policyScope() const
The innermost scope that decides when errors show and is submitted: a dialog's, or the screen's.
std::vector< std::unique_ptr< QueryRuntime > > queries
Its queries.
std::vector< std::unique_ptr< StateSlot > > states
Its state.
reactive::Signal< bool > * submitted
submitted of a scope that decides when errors show.
~RtScope()
Releases what it owns, and tells the dialog it belongs to.
ElementState * owner
The dialog or drawer whose content it is.
std::map< std::string, NameRef, std::less<> > names
Its names.
std::string showErrors
touched, submit or always, for a scope that decides it.
std::vector< std::unique_ptr< TransferState > > transfers
Its transfers.
NameRef const * find(std::string_view name) const
Resolves a name, innermost scope first.
reactive::Scope nodes
The reactive nodes it owns; declared last, so they go first.
std::vector< std::unique_ptr< CheckRuntime > > checks
Its checks.
bool ready() const
Whether every element of the scope is valid and no blocking check of it fails. A tracked read.
std::shared_ptr< RtScope > parent
The enclosing scope.
std::vector< std::unique_ptr< ElementState > > elements
Its elements, in document order.
std::vector< std::unique_ptr< ModelAlias > > models
Its model aliases.
The Environment the evaluator reads through: a scope chain and the names bound at one place.
void report(std::string_view path, std::string_view reason) const override
Reports to the screen's diagnostics.
ScopeEnv(RtScope const &scope, Bindings const *bindings, Value const *row=nullptr)
Reads scope.
Value pref(std::string_view point, std::span< std::string const > path) const override
Reads a customization choice.
Value ref(std::string_view head, std::span< std::string const > path) const override
Reads a name: a binding, then the scope chain.
Value control(ExprKind form, std::string_view name) const override
Reads a query or mutation form.
Value row(std::span< std::string const > path) const override
Reads the innermost row.
Value client(ExprKind form, std::string_view name) const override
Reads env, session or host.
FormatContext const & formatting() const override
The session's formatting facts.
One mounted screen's interpreter: its document, diagnostics, slots and preferences,...
std::vector< BadgeModel > lowerBadgeParts(std::span< DocNodePtr const > parts, LowerCtx const &ctx)
Lowers a screen head's headBadge parts, bound in ctx's scope.
std::function< bool(std::string_view)> focusElement
Moves focus to a mounted element by its id; empty until the screen is mounted, and again once it is u...
void runOne(Command const &command, CommandCtx const &ctx, Done done)
Runs one command.
void addPending(int delta)
Counts pending work in or out.
void runEvent(Commands const *commands, LowerCtx const &ctx, Value event)
Runs a command list for an event of the user's.
void declare(RtScope &scope, ScopeDecl const *decl, std::vector< DocNode const * > const &content, std::vector< Commands const * > const &commands)
Declares a scope's names and starts its reactive parts.
reactive::Signal< bool > submitted
The screen's submitted.
Session & session
The session.
InterpreterHost * host
The host; may be nullptr.
Value inputValue(ElementState const &element) const
The current value of an input, checkbox or select: what required tests.
ValueRecord prefDefaults
The points' defaults.
reactive::Runtime & runtime() const noexcept
The runtime.
reactive::Signal< ValueRecord > prefs
The customization choices, by point.
Value elementValue(ElementState const &element, std::string_view name) const
Reads an element value. A tracked read.
bool const recording
Whether this screen records what the inspector reads: its session's developmentTools,...
std::weak_ptr< RtScope > rootScope
The document's own scope, once mounted.
ActionModels lowerActionParts(std::span< DocNodePtr const > parts, LowerCtx const &ctx)
Lowers a list of actionButton parts into reactive models: label, icon, enabled and visible bound in c...
static Node actionBarOf(ActionModels actions)
An overlay's actions as the action bar its footer shows: a button per action, test id action_<id>,...
std::set< std::string, std::less<> > reported
The paths and reasons already reported.
Document const & doc() const noexcept
The document.
std::vector< LoweredNode > loweredNodes
Every node lowered, in lowering order; an entry whose scope is gone is pruned as more are added.
std::vector< Refusal > diagnostics
The diagnostics reported, in order.
void runList(Commands const &commands, CommandCtx const &ctx, Done done)
Runs a command list: each command in order in one batch; done when every command in it is.
std::size_t depth
The screen scope's reactive depth.
Dispatcher & dispatcher
The dispatcher.
Node lower(DocNode const &node, LowerCtx const &ctx)
Lowers a document node to a view node.
std::deque< CommandLogEntry > commandLog
The latest command lists run, oldest first, at most kCommandLogSize.
bool inspecting
Set while the inspector evaluates, so that an expression error it meets is not reported.
ScreenCore(std::shared_ptr< Document const > doc, Session &owner, Dispatcher &calls, InterpreterOptions const &options, MountOptions const &mount)
The core of a screen about to mount.
void report(std::string_view path, std::string_view reason)
Reports a diagnostic once per path.
std::map< std::string, BorrowedModel, std::less<> > borrowed
The models the opener lends.
void initState(RtScope const &scope, StateSlot &slot) const
Makes a state's signal from its init, once.
std::function< void(Refusal const &)> onDiagnostic
The diagnostic callback.
std::map< std::string, KeptState, std::less<> > keptStates
The states of the own scope of the screen this one replaces, by name (MountOptions::keepStateOf).
reactive::Signal< int > pendingWork
The open dialogs and drawers, the mutation runs and the transfers in flight, across every scope of th...
~ScreenCore()
Takes the screen off the session, so a later backend switch does not run its onBackendChange.
SlotId nextSlot() noexcept
The next slot id: the interpreter numbers every bound property of the document.
std::optional< Value > keptState(RtScope const &scope, StateDecl const &decl) const
The value a state of the screen's own scope keeps from the screen it replaces: that screen's value of...
void runLifecycleOf(std::string_view key)
Runs a lifecycle list in the document's own scope, as a screen's or app document's own command list.
std::vector< RtScope * > scopes
The screen's live scopes, in creation order.
Node lowerAll(std::span< DocNodePtr const > nodes, LowerCtx const &ctx)
Lowers a node list into one node: the node itself, or a column of them.
void take()
Counts the transfer, unless it has already settled.
TransferHold(ScreenCore &core, std::weak_ptr< bool > alive)
A hold that counts nothing yet.
void release()
Gives the count back, if one was taken and the screen is still there; the transfer counts no more.
Collections, headless: a v-* view document as a reactive list of rows, the table source the table eng...
collectionView: a CollectionSession rendered as a view tree (ui::Node): the title and collection acti...
The UI document (spec 5 §3): its model — declarations, commands, view nodes, components and customiza...
@ Exclusive
The run is refused.
@ Serial
Runs queue, one queue per serialKey.
InstanceMode
How a model alias binds its instance (spec 5 §5).
@ Borrowed
The opener's binding of an alias ({"from": alias}), for a screen opened as a dialog.
@ Private
An instance of the scope's own, released when the scope unmounts.
@ Keyed
The shared instance an expression's key selects.
@ Default
None named: shared when the catalog marks the model stateless, private otherwise.
@ Shared
A stateless model's instance, its queries cached with other screens'; refused for a stateful model.
NameCategory
What a name in a scope declares.
@ Transfer
A transfer id of upload.
std::vector< Command > Commands
A command list, run as one batch.
@ User
Only changes caused by the user's own edits.
KindSpec const * findKind(std::string_view kind, bool part)
Looks up a node kind or a part kind.
bool listed(std::span< std::string_view const > names, std::string_view name)
Whether a list of names holds one.
std::shared_ptr< DocNode const > DocNodePtr
A shared, immutable view node.
CommandKind
The commands of ui/1 (spec 5 §8, spec 6).
@ ResetPrefs
{"resetPrefs": "point" | "all"}
@ SignIn
{"signIn": {"from": "m"}}
@ Navigate
{"navigate": "screen" | {"route": e}, "params": e, "as": ..., "onResult": [...], ....
@ Set
{"set": "name.path", "value": e}
@ If
{"if": [cond, [...], [...]]}
@ Refetch
{"refetch": "q"}
@ Notify
{"notify": e, "role": "success"}
@ SignOut
{"signOut": true}
@ Host
{"host": "name", "args": e?}
@ Run
{"run": "m", "body": e?, "as": "x"?, "each": e?}
@ CancelClose
{"cancelClose": true}
@ SwitchBackend
{"switchBackend": "name"}
@ Close
{"close": "dialog" | "self"}
@ SetLocale
{"setLocale": "de"}
The expr/1 expression language of the UI document: parsing a JSON expression into an Expr tree,...
std::string unitDisplay(std::string const &unit, UnitTable const &units)
The display text of a unit.
std::expected< ExprPtr, Refusal > parseExpression(Json const &json, std::string const &path="")
Parses one expression.
std::expected< std::vector< std::string >, std::string > splitPath(std::string_view text)
Splits head.a.b into its segments.
Value evaluate(Expr const &expr, Environment const &env)
Evaluates an expression.
ExprKind
The forms of an expr/1 expression (spec 5 §4).
@ ErrorKind
{"errorKind": "m"}: the kind of a query's or mutation's error.
@ Pending
{"pending": "q"}: whether a query or mutation is in flight.
@ Ref
{"ref": "name.path"}.
@ Query
{"query": "q"}: a query's value.
@ Session
{"session": "name"}: a session attribute.
@ Result
{"result": "m"}: a mutation's last result.
@ Error
{"error": "q"}: a query's or mutation's error text.
@ Env
{"env": "name"}: the client environment.
@ Can
{"can": "m"}: whether the principal may run a mutation or query.
@ Literal
A JSON scalar, or {"lit": v}.
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.
std::shared_ptr< Expr const > ExprPtr
A shared, immutable expression node.
FormModel: the immutable runtime model of one action's form, read from the schema JSON schemaJson<A>(...
@ ObjectArray
A list of nested objects.
void transfersOf(Commands const &commands, std::vector< std::string > &out)
The transfers of the upload commands in a list, nested lists included.
std::expected< std::string, std::string > inlineBytes(Value const &value)
A document's inline file content as bytes: base64 text, or a list of integers 0 to 255.
Key keyOf(Value const &value)
A value as a view key: an int or a string as itself, anything else as its JSON text.
LowerCtx attached(DetachedLower const &held)
Puts a detached context's scope back.
std::string sizeText(std::size_t bytes)
A byte count as a person reads it: 512 bytes, 2.4 KiB, 1 MiB.
Node holding(std::vector< Node > children, std::shared_ptr< RtScope > scope, std::vector< KeyBinding > keys={})
A column of content that holds its scope for as long as it is mounted: its visible is a binding that ...
Value componentPayload(std::string const &payload, Type const *type)
A custom component's event payload as a value: i:<int64> an int, s:<text> a string,...
ElementState * elementFor(RtScope const &scope, DocNode const &node)
The element a node of a scope keeps its state in.
Value mergeRow(Value const &row, Value const &with)
Writes with over a row: the fields of a record over a record's, otherwise the whole row.
std::vector< Value > arrangedOrder(Value const &stored, std::span< DocNode const *const > declared)
The ids of the children an arrange choice shows, in its order (arrangedChildren, without the hidden).
Axis axisOf(Value const &value)
An axis by name: horizontal, or vertical for anything else.
Value jsonTextValue(std::string_view text)
A JSON text as a document value; null when it is not JSON or holds what a value cannot (a fraction).
std::vector< std::string > stringsOf(Value const &value)
A list of strings out of a value: a string, a list of strings, or nothing.
std::optional< TextRole > roleNamed(std::string_view name)
A text role by name.
bool statefulKind(std::string_view kind)
Whether a node kind keeps per-element state even without an id: the inputs, dialogs,...
Value formFieldValue(forms::FieldState const &field)
One field of a form as a record: value (the encoded value, null while blank or invalid),...
Justify justifyOf(std::string_view name)
A justification by name, as the loader checked it: center, end, between, or start for none.
Value valueOfKey(Key const &key)
A view key as a value.
std::function< Value()> formFilled(std::string const &prop, forms::FieldState *state)
What a form fills of a field component's props: the field's value, errors, read-only and required sta...
Node lazy(std::function< Node()> build)
Content whose scope is made when it mounts and released when it unmounts: a one-row forEach whose row...
Value formValue(ElementState const &element, std::string_view name)
A form's element values (spec 5 §6): ready, body (the record the form would submit,...
std::string dateTextOf(time::DateTime const &instant, std::int32_t offsetMinutes)
A date input's date text for instant in a display zone offsetMinutes from UTC.
std::string instanceKeyText(Value const &key)
The text of an instance key, as the bridge's string-keyed attach takes it.
std::string textOf(Value const &value, FormatContext const &format)
A value as text for a widget: a string as itself, a number in the locale, a timestamp in ISO 8601,...
DateReading dateInstantOf(Value const &value, std::int32_t offsetMinutes)
Reads what a date input shows for value, in a display zone offsetMinutes from UTC.
std::string patchMissReason(std::string_view query, std::string_view listMember)
What a patch that found no list reports.
std::shared_ptr< ModelChannel > channelOf(ModelAlias &alias, ScreenCore &core)
The channel an alias is bound to for its current token, binding it when the token changed.
DetachedLower detachedLower(LowerCtx const &ctx)
Detaches a lowering context's scope.
std::optional< Value > patchKeyedRows(Value const &value, std::span< std::string const > path, std::string_view keyField, RowsByKey const &rows)
Merges new rows over the rows with the same key that a value holds at a path.
Value listOfStrings(std::vector< std::string > const &texts)
A list of strings as a value.
Value withoutFields(Value const &value, std::vector< std::string > const &fields)
Removes the named fields from a record, and from each record of a list.
KeySpec const * keySpec(KindSpec const &kind, std::string_view key)
A kind's key spec, from the kind table.
double componentNumber(Value const &value)
A value as a custom component's number prop: the one kind that crosses as a double.
std::vector< Value const * > keyedRowsOf(Value const &value, std::vector< std::string > const &paths, std::string_view keyField)
The rows of a keyed record a value holds, at each of the record's paths.
std::size_t base64Size(std::string_view text)
How many bytes base64 text decodes to, read from its length alone.
std::string commandKey(CommandKind kind)
The key a command is written with.
std::function< void(bool)> Done
Called once when a command finishes: whether it succeeded.
void keyedRowsAt(Value const &value, std::span< std::string const > path, std::string_view keyField, std::vector< Value const * > &out)
The rows of a keyed record that a value holds at a path.
std::optional< std::string > base64Decode(std::string_view text)
Base64 text as bytes (RFC 4648, with or without padding).
std::string base64Encode(std::string_view bytes)
Bytes as base64 (RFC 4648, with padding): how a document carries an inline file.
bool engaged(Value const &value)
Whether a value counts as given for required: not null, not an empty string or list.
constexpr std::size_t kCommandLogSize
How many command lists the inspector's log keeps.
constexpr std::size_t kPruneLoweredAt
How often, in lowered nodes, the inspector's node list drops the nodes whose scope is gone.
std::string failureText(std::exception_ptr const &error)
The text of a failure.
Value withPath(Value const &base, std::span< std::string const > path, Value value)
A value with the field at path replaced, creating records along the way.
std::vector< SelectOption > selectOptions(Value const &options, FormatContext const &format)
The options of a select, from a list of scalars or of records with a key and a label.
Value optionKey(Value const &option)
The key of a select option: a record's key, id or value field, or the option itself.
PatchMiss
Why a patch left a query's value as it was.
@ NoList
The value, or its at member, is not a list: the patch can never apply, and is reported.
@ NoRow
No row's id equals the key: the row is not loaded, which is not a fault.
std::vector< ActionModel > ActionModels
A list of lowered actionButton parts, in document order.
std::string canonicalJson(Value const &value)
A value's JSON text, with record fields in name order: a cache key's canonical body.
StepState stepStateOf(std::string_view name)
A step's state by name: active, done, failed, or pending for anything else.
std::string defaultErrorKind(std::exception_ptr const &error)
The default error kinds: the bridge's own failures, and error for the rest.
Align alignOf(std::string_view name)
An alignment by name, as the loader checked it: start, center, end, or stretch for none.
std::optional< Value > patchHeldRows(Value const &value, std::vector< std::string > const &paths, std::string_view keyField, RowsByKey const &rows)
Merges new rows into a value at each of the record's paths.
bool sameShown(std::string_view kind, Value const &lhs, Value const &rhs, FormatContext const &format)
Whether two values show the same in an input: by text for a text input, by instant for a date input,...
std::int64_t intOf(Value const &value, std::int64_t fallback)
A value as an integer: an int, or a decimal truncated toward zero.
std::function< async::Completion< std::string >(std::string const &kind, std::string const &bodyJson)> Watcher
Sends a watch or unwatch request for a session's cache (spec 8 §7): the kind, and the WatchBody as JS...
std::vector< std::string > pathMembers(std::string_view path)
The members of a catalog record path: "header.rows" is header, then rows; "" is none.
bool isSizing(Value const &value)
Whether a value is a sizing of one dimension: "content", {"fixed": n} or {"stretch": w}...
Value evalIn(RtScope const &scope, Bindings const *bindings, ExprPtr const &expr)
Evaluates an expression in a scope.
forms::FieldOverlay overlayOf(RtScope const &scope, DocNode const &part, FormatContext const &format)
A document field overlay's expressions, evaluated once, as the engine's overlay.
std::vector< Binding > Bindings
The names bound at one place, innermost last.
Sizing sizingOf(Value const &value)
A sizing by its document form: "content", {"fixed": n} or {"stretch": w}.
ReplyUse
How a mutation's reply was used.
@ Discarded
A newer run's reply was applied first, so its values are older and only mark.
@ Applied
The reply is the latest: its values may patch the cache.
bool boolOf(Value const &value, FormatContext const &format)
A value as a bool property: true only for true.
std::function< async::Completion< std::string >(std::string const &typeId, std::string const &actionId, std::string const &bodyJson)> Evaluator
Evaluates an action body without running it: the model's type id, the action id and the JSON body in,...
DocNode const * elementNode(DocNode const &node, std::string_view elementId)
The element of a document tree with elementId.
std::function< void(bool succeeded, Value const &reply)> RunDone
One run of a mutation, as a command sees it: whether it succeeded, and the reply.
std::string componentText(Value const &value, std::string_view type, FormatContext const &format)
A value as a custom component's text prop (spec 5 §10): an int or exact as plain decimal text,...
std::vector< ArrangedChild > arrangedChildren(Value const &stored, std::span< DocNode const *const > declared)
A container's children in the order a stored arrange choice gives, each marked shown or hidden.
std::string stringOf(Value const &value, FormatContext const &format)
A value as a text property.
std::expected< Value, PatchMiss > patchRows(Value const &value, std::string_view listMember, Value const &key, Value const &with)
Replaces the row whose id equals key in a query's list of rows.
constexpr std::size_t kMaxInlineFileBytes
The largest inline cap a session takes: base64 grows bytes by 4/3, and 64 KiB of an envelope are left...
std::expected< forms::detail::engine::Json, std::string > changeWire(std::string const &payload, std::string_view type)
A component's change payload as the wire value of the field it draws, read as the value prop's declar...
constexpr std::size_t kDefaultInlineFileBytes
The inline cap a session has unless it is given another: 1 MiB.
Value arrangementValue(std::vector< ArrangedChild > const &children)
An arrangement as the choice an arrange point stores.
Tone toneOf(Value const &value, FormatContext const &format)
A tone by name.
Value rowId(Value const &row)
A row's identity for patch and optimistic: its id field.
CommandCtx detached(CommandCtx const &ctx)
A command context without its scope: what a callback the host keeps may hold, so that the host never ...
std::map< std::string, Value const *, std::less<> > RowsByKey
New rows by the canonical text of their key.
TextRole roleOf(Value const &value, FormatContext const &format)
A text role by name, as roleNamed reads it.
The MessageFormat 2 subset ui/1 accepts for t(key, fallback, args), and the CLDR plural rules its ....
std::optional< std::string > formatNumber(Value const &value, render::NumericLocale const &numeric)
Formats a number for display in a locale: at its own places, rounded half to even.
std::string decimalText(math::Rational const &value, std::uint32_t places)
The decimal text of an exact value at a number of places: rounded half to even, then written with exa...
morph::ui::Mounted: builds a view tree's widgets through a backend once, and keeps them current throu...
void convert(const Quantity< From > &in, Quantity< To > &out)
Auto-generated within-dimension conversion (ratio + chaining).
morph::reactive::Scheduler: timers a frontend runs on the runtime's owner executor.
std::function< std::optional< std::string >(std::string_view key, std::string_view bcp47Locale)> TranslationProvider
A host-supplied catalog lookup: key/bcp47Locale -> translated text, or std::nullopt on a miss.
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.
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.
Thrown to in-flight Completions when Bridge::switchBackend() runs.
Thrown to a pending Completion when Bridge::setExecuteDeadline's duration elapses before any reply ar...
Thrown to in-flight Completions when a transport drops mid-call (e.g. a Qt WebSocket disconnect)....
Thrown to a pending Completion when the server-side morph::backend::LimitPolicy::executeTimeout elaps...
A change to the rows of one record type.
std::string record
The record's wire name.
std::optional< std::vector< glz::raw_json > > keys
The keys of the rows that changed, each as raw JSON; absent when the whole type changed.
Abstract executor interface.
A UTC instant with millisecond precision.
static std::optional< DateTime > fromIso8601(std::string_view text) noexcept
Strictly parses ISO-8601 UTC: YYYY-MM-DDTHH:MM:SS, optionally followed by .m/.mm/....
std::chrono::sys_time< std::chrono::milliseconds > value
The underlying UTC instant (Unix time, leap seconds ignored).
A row of buttons in the footer of the dialog or drawer whose content holds it: what a document's over...
std::vector< Node > children
The buttons, in order; a null child is skipped.
One effect an action declares, as the catalog states it.
What the catalog says about an action.
A record an action's result holds, as the catalog states it.
std::string record
The record's wire name.
A short label marking a state, such as a count or a status.
std::string icon
The name of an icon in the theme's icon map; empty shows none. Set once.
A message across the top of a region, with an optional action and an optional dismissal.
ActionButton action
The button beside the message; an empty label shows none.
A model the opener lends to a screen whose alias says {"from": alias} (spec 6 §16).
std::string token
The opener's instance token, so the two share cache entries.
std::shared_ptr< ModelChannel > channel
The opener's channel.
How one model call is sent.
bool evaluate
Ask the server to evaluate the action's body instead of executing it (spec 5 §7, ui-evaluate).
bool queueOffline
The mutation declares "offline": "queue": a dispatcher with an offline queue holds a call that cannot...
The facts load-time validation needs from the application's catalog. Local mode answers them from the...
A validation across elements (spec 5 §6).
std::vector< std::string > targets
The elements the message attaches to.
ExprPtr message
The message added to each target's errors.
ExprPtr assertion
The condition; false fails the check, null does not.
std::function< void(bool)> onToggle
Called with the new state when the user toggles it.
A titled section the user opens and closes. Its header stays usable while it is closed.
Node header
Shown beside the title, open or closed; may be null.
Children stacked top to bottom.
Common common
Visibility, enablement, layout, drag-and-drop.
One command list as it ran: the event that ran it, its commands, and the reactive batch they ran in.
std::vector< LoggedCommand > commands
Its commands, in order.
std::string event
The JSON Pointer of the list: the event, lifecycle key or step that holds it.
std::uint64_t batch
The number of the reactive batch it ran in (reactive::detail::RuntimeCore::currentBatch); 0 outside e...
One command. Which members a kind uses is listed with each member.
std::string option
A closed option: navigate's as, notify's role, download's save or to.
std::string transfer
The transfer id of upload, whose progress is an element value.
Commands commands
Nested commands: seq and all; if's then branch; navigate's onResult.
ExprPtr value
The main expression: set's value, run's body, navigate's params, return's value, host's args,...
std::string nameAs
upload's body members for an inline file's name and media type; empty for none.
bool bytes
Whether the file moves inline: download's "from": "bytes", upload's "into": "bytes".
std::string typeAs
See nameAs.
CommandKind kind
The kind.
ExprPtr extra
The second expression: run's each, navigate's route, download's suggested name.
std::string path
The command's JSON Pointer.
std::string target
The name it acts on: set's name.path; the state of reset; the mutation of run, signIn and upload; the...
std::string binding
A binding name: run's and upload's as.
ExprPtr mediaType
download's media type of inline bytes.
Commands alternative
The alternative: if's else branch; navigate's onCancel.
The part every node carries: visibility, enablement, layout, drag-and-drop, accessibility,...
Prop< bool > visible
Whether the widget is shown.
A custom component declaration (spec 5 §10).
One prop a loaded custom component is given.
A component the backend may load (spec 5 §10), with the built-in nodes shown when it does not.
std::function< void(std::string event, std::string payload)> onEvent
Called with an event a loaded component raises: its name and its payload as the backend reports it.
A customization point (spec 5 §12b).
An exact decimal: a math::Rational whose DecimalPlaces are the value's declared places.
A modal overlay; its content is mounted only while it is open.
A view node of the document (spec 5 §9), or a structured part of one (a table column,...
std::map< std::string, Commands, std::less<> > keys
Key bindings: chord to commands.
std::map< std::string, std::vector< DocNodePtr >, std::less<> > children
Child node lists, by key (children, header, loading, error, default, cell, fallback).
std::string path
The node's JSON Pointer.
std::optional< ScopeDecl > scope
The declarations of the scope the node creates (dialog, drawer, switch case, forEach or table row); e...
std::string_view name(std::string_view key) const
A plain string property.
Commands const * event(std::string_view key) const
A command list.
std::map< std::string, std::vector< DocNodePtr >, std::less<> > parts
Structured parts, by key (columns, tabs, cases, items, a11y, action, fields, …).
std::string kind
The node kind (text, table, …), or for a part its part kind (column, tab, case, …).
std::map< std::string, Commands, std::less<> > events
Command lists, by key (onClick, …), including a custom node's on events as on.<event>.
bool flag(std::string_view key) const
A literal flag.
std::span< DocNodePtr const > partList(std::string_view key) const
A list of parts.
ExprPtr prop(std::string_view key) const
An expression property.
std::string id
The element id; empty when none is given.
std::span< DocNodePtr const > nodes(std::string_view key) const
A child node list.
A loaded and validated UI document: a screen, or the app document (id app, spec 6 §5).
A panel that slides in from one edge over the content: a Dialog with a side, and the same rules.
A region that takes files dropped from the operating system.
std::vector< std::string > accept
The extensions it takes, such as ".csv", compared without regard to case; empty takes every file....
What a region shows when it has nothing to show: a title, an explanation and an optional action.
std::string icon
The name of an icon in the theme's icon map; empty shows none. Set once.
The state of a value a user edits or reads: whether it may be changed, must be given,...
A file path, typed or chosen in the platform's file dialog.
Prop< std::string > path
The path shown.
One widget per row of a keyed collection, updated in place when a row with the same key changes.
What the display functions and t format with: the client's locale and its formatting facts.
render::TranslationProvider translations
The message catalogue; empty shows every fallback.
std::string locale
The active BCP 47 locale.
Cells laid out row-major in a fixed number of columns.
An action of a screen's head, as its handle answers it.
ButtonEmphasis emphasis
How much it stands out.
bool enabled
Whether it can be run now.
bool operator==(HeadAction const &) const =default
Memberwise equality.
std::string id
The action's id, what ScreenHead::run takes.
std::string label
The label.
std::string icon
The name of an icon; empty for none.
A status badge of a screen's head, as its handle answers it.
bool operator==(HeadBadge const &) const =default
Memberwise equality.
std::string icon
The name of an icon; empty for none.
std::string text
The badge's text.
What running a host command (host/1) did.
@ Missing
The client has no such command; nothing ran.
@ Refused
The command exists and did not run: its arguments, or a connection that is not verified.
std::string reason
Why it was refused; empty otherwise.
The client's host commands, as a session runs them (spec 6 §14).
std::function< HostCommandResult(std::string const &name, Value const &args)> run
Runs a command with its evaluated args (null when the document gives none).
std::set< std::string, std::less<> > available
The commands the client can run now; {"can": "host:name"} is true for these.
A file whose bytes a document moves inline: download from bytes, upload into bytes.
std::string name
The file's name.
std::string bytes
Its bytes.
std::string mediaType
Its media type; empty when unknown.
What an Interpreter is built with.
std::function< void(Refusal const &)> diagnostics
Called with each diagnostic as it is reported: a runtime expression error, a command the host does no...
InterpreterHost * host
The application's side of the commands; nullptr answers every one "not supported"....
A keyboard chord a node handles while focus is inside it.
One row of a KeyValue list.
Captions beside their values, one pair per row.
Prop< std::vector< KeyValueItem > > items
The pairs, in order.
A widget's size request in both dimensions, and the bounds its extent is clamped into.
std::optional< int > minHeight
The least height, in backend units; nullopt for none.
std::optional< int > minWidth
The least width, in backend units; nullopt for none.
std::optional< int > maxWidth
The most width, in backend units; nullopt for none.
std::optional< int > maxHeight
The most height, in backend units; nullopt for none.
ExprPtr value
The expression.
std::string name
The name.
One command of a command list the inspector's log shows (spec 6 §21).
std::string path
The command's JSON Pointer in the document.
std::string target
The name it acts on (Command::target); empty for a command that names none.
std::string kind
The command's key: set, run, refetch, …
std::string name
The alias.
std::string type
The model's registered type id.
ExprPtr key
The key expression of a Keyed instance.
What Interpreter::mount is given beside the document.
std::map< std::string, BorrowedModel, std::less<> > borrowed
The models the opener lends, by the alias of this screen that borrows them.
std::size_t depth
The reactive scope depth of the screen's own scope.
ContainerWidget * parent
The container the screen's root widget is appended to; nullptr makes a backend root.
Screen const * keepStateOf
The screen this mount replaces, such as one whose document changed; nullptr for a fresh mount....
std::string model
The model alias it goes through.
std::string action
The action id it runs.
std::string name
The name.
What navigate asks the host to open (spec 6 §16, §20).
std::string screen
The screen id; empty when route is given.
Value route
The route, when the command named one instead of a screen.
std::string as
tab, dialog, drawer or window; empty for the shell's choice.
A titled frame around one child; a collapsible one is an accordion section.
Node header
Shown beside the title, collapsed or not; may be null.
Prop< std::optional< double > > value
How far along, from 0 to 1; a value outside is shown at the nearer end. nullopt: indeterminate.
An exact decimal with a unit: the id of a unit of the server's unit systems (x-unitAlternatives).
std::string name
The name.
std::string model
The model alias it goes through.
std::string action
The action id it runs, or evaluates.
Why a document, an expression or a value was refused: where, and the reason.
Children laid out left to right.
The declarations of one scope, in declaration order.
std::vector< ModelDecl > models
Model aliases.
std::vector< StateDecl > state
State.
std::vector< LetDecl > lets
Derived values.
std::vector< MutationDecl > mutations
Mutations.
std::vector< QueryDecl > queries
Queries.
std::vector< CheckDecl > checks
Checks.
std::vector< WatchDecl > watches
Watches.
Prop< std::vector< SelectOption > > options
The options, in order.
What a Session is built with.
std::function< std::chrono::milliseconds()> clock
The clock staleTime is measured on; empty uses std::chrono::steady_clock.
std::function< std::string(std::exception_ptr const &)> classify
Maps a failed call to its error kind (denied, offline, timeout, …); empty uses the default,...
std::size_t maxInlineFileBytes
The most bytes a document moves inline in one file (download from bytes, upload into bytes,...
reactive::Scheduler * scheduler
Timers for debounce, refreshEvery and the cache's eviction. Without one, debounce fetches at once,...
Catalog catalog
The catalog: stateless decides an alias's default instance; an action's secretFields are never cached...
Watcher watcher
Sends the cache's watch changes to the server. Empty: the session still counts what its cache holds,...
std::chrono::milliseconds keepUnused
How long an entry no mounted query uses is kept (spec 5 §5: five minutes).
DevelopmentTools developmentTools
Development mode: whether this session's screens record what the inspector reads. Off by default.
How much space a widget asks for along one dimension, in backend units (a character cell on the TUI).
static constexpr Sizing content() noexcept
Size to the content.
static constexpr Sizing stretch(int weight=1) noexcept
A share of the leftover space.
static constexpr Sizing fixed(int units) noexcept
A fixed size.
An integer chosen on a range.
Prop< std::int64_t > value
The value shown.
Empty space that takes what its layout hints ask for.
Panes side by side, or stacked, with handles the user drags to share the space between them.
std::string name
The name.
Type type
The declared type; any when the document gives none.
One step of a Steps indicator.
The steps of a process in order, each with its state.
Prop< std::vector< Step > > items
The steps.
Shows the case whose key the selector yields; remounts only when that key changes.
std::vector< SwitchCase > cases
The cases; the first one with the selected key wins.
Prop< Key > selector
Which case to show.
Everything about a table beyond its columns and rows.
bool showHeader
Whether the header row is drawn.
What a table's row view builds for one row: its cells, one per column, and its tone.
An editable text field. The backend never reports its own setText back through onChange.
Prop< std::string > value
The text the field shows.
Prop< TextRole > role
The style.
A document type (spec 5 §3): a kind, whether null is allowed, and a list's element type.
std::string name() const
The type as a document spells it.
bool admits(Value const &value) const
Whether a value conforms to the type.
What an upload command asks the host to send, and the mutation that will reference the file,...
std::string modelType
The model type of the mutation that will reference the file.
std::string action
That mutation's action.
Value file
What the user picked or dropped.
A cascade: commands that run when a value changes (spec 5 §6).
ExprPtr on
The value watched.
An actionButton part, lowered in the scope of the list it belongs to: what a screen's head shows and ...
std::string id
The action's id, unique in its list.
Prop< std::string > icon
The name of an icon; empty for none.
Prop< std::string > label
The label.
ButtonEmphasis emphasis
How much it stands out.
std::string path
The part's JSON Pointer, where a diagnostic about it is reported.
Prop< bool > visible
Whether it is shown.
Prop< bool > enabled
Whether it can be run.
Action run
Runs its onClick as a user event, in one batch; empty when it declares none.
One child of an arranged container, as the user's choice places it.
std::string id
The child's id.
bool hidden
Whether the choice hides it.
A headBadge part, lowered in the screen scope.
Prop< std::string > icon
The name of an icon; empty for none.
Prop< std::string > text
The badge's text.
Prop< bool > visible
Whether it is shown.
Prop< Tone > tone
Its tone.
A name bound for the expressions of one place: a command's event, a step's as, result in a patch,...
std::function< Value()> read
Reads its value; a tracked read for a reactive one.
std::string name
The name.
A boundary at run time: the queries it owns (spec 5 §9).
std::vector< QueryRuntime * > queries
The queries read inside its children.
One entry of the session's query cache (spec 5 §5): one request in flight, one value,...
bool evaluate
Whether it is an evaluate query.
reactive::Signal< bool > hasValue
Whether a reply has been applied.
std::string instanceToken
The alias's instance token the entry was read through.
std::uint64_t generation
The generation of the request in flight; a reply of an older one is dropped.
reactive::Signal< bool > pending
Whether a request is in flight.
ActionRecordInfo const * holding(std::string_view record) const
The catalog entry of a record the entry holds.
Value confirmed
The value the server last confirmed.
bool refetchAfter
Marked stale while a fetch was in flight: that fetch may have read before the change,...
std::vector< std::string > secretFields
The fields never cached.
std::optional< std::chrono::milliseconds > fetchedAt
When the last reply was applied.
std::string body
The body's JSON text.
std::string action
The action.
std::string typeId
The model type.
reactive::TimerHandle evict
The eviction timer, while no query uses it.
reactive::Signal< Value > value
The value shown: the confirmed value with the optimistic layers over it.
reactive::Signal< std::string > error
The last failure's text; empty when none.
reactive::Signal< std::string > errorKind
The last failure's kind; empty when none.
std::string key
The cache key: model type, instance, action and canonical body.
std::vector< ActionRecordInfo > records
The records the entry's action returns, with the key field and paths of each; empty for an evaluate e...
bool stale
Marked stale by a refetch, a backend switch or a change.
int users
The mounted queries using it.
std::vector< OptimisticLayer > layers
The optimistic writes still waiting, oldest first.
CacheEntry(reactive::Runtime &runtime)
An empty entry.
~CheckRuntime()
Leaves its targets.
std::vector< ElementState * > targets
The elements it targets.
reactive::Computed< std::string > * message
The message, as text.
CheckDecl const * decl
The declaration.
reactive::Computed< bool > * failing
Whether assert is false.
Where a command list runs: its scope, the names bound there, and what it may answer.
std::shared_ptr< RtScope > scope
The scope.
std::shared_ptr< BoundaryRuntime > boundary
The innermost boundary, for retry.
std::shared_ptr< bool > cancelClose
Set by cancelClose.
std::shared_ptr< Bindings > bindings
The names bound here; a seq step's as adds to them.
What a date input reads from a value: the instant it shows, and whether the value names one at all.
bool readable
Whether the value names a date, an instant or nothing; false for text that names no date.
std::optional< time::DateTime > instant
The instant shown; empty for an empty field.
A lowering context whose scope is held weakly: what an Effect the scope itself owns may keep,...
std::shared_ptr< Bindings const > bindings
The names bound there.
std::weak_ptr< RtScope > scope
The scope, weakly.
std::shared_ptr< BoundaryRuntime > boundary
The innermost boundary.
An element's live values (spec 5 §6).
reactive::Signal< Value > draft
What the user is editing: a text input's text, a checkbox's state, a select's key,...
std::vector< CheckRuntime * > checks
The checks targeting it.
reactive::Computed< Value > * rows
A table's rows or a select's options, for its counts and selectedOption.
std::unique_ptr< forms::CollectionSession > collection
A collection's session (spec 2), made when the element is wired; null when its view document is unkno...
reactive::Signal< Value > activeKey
A table's last activated key.
ElementState(reactive::Runtime &runtime)
The element's state before any user action.
reactive::Signal< bool > edited
Whether the user changed the draft since it last followed the bound value.
DocNode const * node
The node.
reactive::Signal< int > checksVersion
Bumped when a check targeting it comes or goes.
std::unique_ptr< forms::WizardSession > wizard
A wizard's session (spec 2), made when the element is wired; null when its wizard document or a step'...
std::unique_ptr< forms::FormSession > form
A form's session (spec 2), made when the element is wired; null when its schema is unknown.
reactive::Signal< RtScope * > inner
A dialog's or drawer's scope while its content is mounted.
reactive::Computed< Value > * bound
The value an expression binds (value, checked, selected); nullptr when the element is uncontrolled.
reactive::Computed< Value > * errors
What is wrong now, as a list of strings: required unmet, the node's errors, the reply's field errors ...
reactive::Signal< Value > committed
The committed value of an input whose value no expression binds.
reactive::Signal< bool > submitted
A dialog's or drawer's submitted.
RtScope * scope
The scope that declares it.
reactive::Signal< bool > focused
Whether it has keyboard focus. The view tree reports no focus, so it stays false.
reactive::Signal< bool > open
A dialog's or drawer's open, set by open and close.
reactive::Signal< bool > touched
Whether it has been committed at least once (Enter or focus leaving after an edit).
reactive::Signal< Value > selection
A table's selected keys.
std::string_view key
The key.
A node kind or part kind: its keys and its element values.
std::span< KeySpec const > keys
Its own keys.
Where a node is lowered: its scope, the names bound there and the boundary that owns its queries.
std::shared_ptr< BoundaryRuntime > boundary
The innermost boundary.
std::shared_ptr< Bindings const > bindings
The names bound here.
std::shared_ptr< RtScope > scope
The scope.
The entries one change marks stale, each once, in the order they were found.
std::vector< CacheEntry * > entries
The entries.
void add(CacheEntry &entry)
Adds an entry, unless it is already there.
One model alias at run time: the instance it names, and the channel bound to it.
std::string boundToken
That token.
reactive::Computed< std::string > * token
The instance token: what the cache key holds for the instance; empty while unbound (a null key).
std::shared_ptr< ModelChannel > channel
The channel of the token it was last bound for.
InstanceMode mode
Private, Shared, Keyed or Borrowed, the default resolved through the catalog.
ModelDecl const * decl
The declaration.
One declared name of a run-time scope.
ExprPtr letExpr
The let's expression, for the boundary's ownership walk.
QueryRuntime * query
The query.
reactive::Computed< Value > * let
The let.
MutationRuntime * mutation
The mutation.
TransferState * transfer
The transfer.
NameCategory category
What it declares.
Value param
A param's value.
ModelAlias * model
The model alias.
ElementState * element
The element.
StateSlot * state
The state.
One optimistic write still waiting for its reply.
Value with
What was written.
std::string at
The record member holding the list; empty when the value is the list.
std::uint64_t run
The run that made it.
A state of the screen this one replaces: its declared type's name and its value.
std::string type
The declared type's name.
A node as it was lowered, which the inspector lists while its scope lives.
std::weak_ptr< Bindings const > bindings
The names bound where it was lowered.
DocNode const * node
The node.
std::weak_ptr< RtScope > scope
The scope it was lowered in.
One state declaration at run time.
RtScope const * scope
The scope that declares it, where its init is evaluated.
bool initializing
Set while the init is evaluated, so a cyclic read gives null instead of recursing.
StateDecl const * decl
The declaration.
std::unique_ptr< reactive::Signal< Value > > value
The value, made when the init is evaluated.
The values of an upload transfer id (spec 6 §18).
reactive::Signal< bool > pending
Whether it is running.
TransferState(reactive::Runtime &runtime)
An idle transfer.
reactive::Signal< Value > progress
0 to 1, or null.
reactive::Signal< Value > error
The failure's text, or null.
The payload of a changes event.
std::vector< effects::RecordChange > records
Record changes; a record with no keys changed as a whole.
std::vector< InstanceTopic > instances
Instance changes.
The decoded body of an event envelope.
One instance topic: a model type, its directory key text and its instance id.
The body of a watch or unwatch request.
std::vector< InstanceTopic > instances
Instance topics to watch (or stop watching).
bool replace
For watch: replace the connection's whole watch set instead of adding to it.
std::vector< std::string > records
Record wire names to watch (or stop watching).
The backend contract: the retained widgets a mount drives, and the factory that makes them.
std::optional< std::string_view > aliasOf(std::string_view text)
Whether a string is an alias, {a.b}.
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
std::expected< Json, std::string > parseJson(std::string_view text)
Parses JSON text into the document layer's JSON tree.
glz::generic_u64 Json
The JSON tree the document layer reads and writes: glaze's generic value in its integer-preserving mo...
Json valueToJson(Value const &value)
Writes a value in its JSON form: the inverse of valueFromJson, with a timestamp as ISO-8601 text.
std::vector< Value > ValueList
A list value.
std::expected< Value, std::string > valueFromJson(Json const &json)
Reads a value from its JSON form, structurally.
std::map< std::string, Value, std::less<> > ValueRecord
A record value: field name to value, ordered by name.
std::string pointerAppend(std::string_view base, std::string_view token)
Appends one reference token to a JSON Pointer, escaping ~ and / as RFC 6901 requires.
std::optional< std::expected< WireValue, FieldError > > Encoding
A draft's encoding: std::nullopt when blank, else the value or why it does not encode.
The toolkit-agnostic view tree: props, node kinds and their builders.
Tone
What a message or a mark conveys, mapped to the theme's colours.
@ Warn
Something to check.
@ Neutral
Nothing in particular.
Node actionBar(ActionBar spec)
An action bar node.
@ SingleLine
One line; Enter submits.
@ Multiline
Several lines.
@ Password
One line, masked.
TextRole
How a Text is styled; each backend maps a role to its theme.
@ Heading
A section heading.
@ Section
The heading of a group within a list: body size, bold and muted, below a Heading.
DateMode
What a DateTimeInput edits.
@ Date
A calendar date; the time of day is midnight in the display zone.
@ DateTime
A date and a time of day.
Node forEach(std::function< std::vector< RowT >()> rows, std::function< Key(RowT const &)> keyOf, std::function< Node(reactive::Signal< RowT > const &)> rowView, Axis axis=Axis::Vertical, int gap=0)
One widget per row of a keyed collection read through a binding (a Query's value() included).
Node badge(Badge spec)
A short label marking a state.
std::function< void()> Action
A widget event handler taking no arguments.
Node column(Column spec)
Children stacked top to bottom.
@ None
Rows can be activated, not selected.
@ Multiple
Any number of rows.
@ End
The right edge in a left-to-right layout.
@ Start
The left edge in a left-to-right layout.
@ Save
A file to create or overwrite.
Node text(Text spec)
Static or bound text.
Node select(Select spec)
A choice of one option.
@ Radio
Every option visible, one marked.
@ Dropdown
A closed field that opens a list.
Node progress(Progress spec)
A progress bar.
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.
Node steps(Steps spec)
The steps of a process.
Axis
The direction a stack arranges, or a scroll area scrolls, its content.
@ Horizontal
Left to right.
std::uint32_t SlotId
The stable identity of a slot: a bound property a renderer can refer to by number.
StepState
Where one step of a Steps indicator stands.
@ Failed
Stopped by a failure.
Node makeNode(Kind spec)
Wraps one node aggregate into a shared node.
Node button(Button spec)
A push button.
SlotBinding< std::decay_t< F > > slot(SlotId slotId, F &&read)
A slot with an id.
Wizards, headless: a w-* document as one FormSession per step, sequenced by successful submissions,...
wizardView: a WizardSession rendered as a view tree (ui::Node): the title and step indicator,...