docs
Loading...
Searching...
No Matches
Model.h
Go to the documentation of this file.
1///
2/// Copyright (C) 2026 Ultralight, Inc. All rights reserved.
3/// A license is required for commercial use. https://ultralig.ht
4///
5#pragma once
7#include <Ultralight/dom/data/detail/EntryInfo.h>
8#include <Ultralight/dom/data/detail/Record.h>
9
10#include <iterator>
11
12namespace ultralight {
13namespace dom {
14namespace data {
15
16template <typename T>
17class Model;
18
19/// \cond INTERNAL
20namespace detail {
21
22template <typename T>
23inline constexpr bool kHasCustomSync = requires(const T& instance, Model<T> model) {
24 TypeTraits<std::remove_cvref_t<T>>::Sync(instance, model);
25};
26
27// The walk entry for one described type: the type's own Sync when declared, the
28// synthesized full walk otherwise. Recursion into sub-objects and rows funnels through
29// here so each type's skip logic owns its subtree.
30template <typename T>
31void SyncType(RecordOps* ops, const T& instance);
32
33} // namespace detail
34/// \endcond
35
36///
37/// Read-only access to the synchronized values of a described type.
38///
39/// Snapshot lets you inspect the field values and CSS variables recorded for a described type
40/// without touching the underlying C++ instance. Calling Get() with an entry name returns its
41/// current synchronized value.
42///
43/// This schema uses a validator to check an edited bid against another field on the page:
44///
45/// ```
46/// struct Shop {
47/// int64_t gold = 500;
48/// int64_t bid = 0;
49///
50/// static constexpr auto schema = dd::Schema(
51/// dd::Field("gold", &Shop::gold),
52/// dd::Field("bid", &Shop::bid, dd::Editable,
53/// dd::Validate([](int64_t bid, auto s) -> std::optional<int64_t> {
54/// if (bid > s.template Get<"gold">())
55/// return std::nullopt; // can't bid more than you have
56/// return bid;
57/// })));
58/// };
59/// ```
60///
61/// ## Receiving a Snapshot
62///
63/// The library passes a Snapshot to functions that inspect page state:
64///
65/// - **A validator receives a Snapshot through its second parameter on the Renderer's thread.** It
66/// provides the values the page currently shows, letting you inspect related fields before
67/// accepting an edit.
68/// - **A type's static `Sync` function receives a dom::data::Model, which inherits from Snapshot.**
69/// Calling Get() returns the last synced values, including any fields that call has already
70/// synced, so you can compare against native data and skip unchanged fields (see
71/// dom::data::Model).
72///
73/// @see dom::data::Model, dom::data::Validate()
74///
75template <typename T>
76class Snapshot {
77 public:
78 using described_type = T;
79
80 ///
81 /// Get the type's schema.
82 ///
83 static constexpr const auto& schema() { return TypeTraits<T>::schema; }
84
85 /// \cond INTERNAL
86 Snapshot(detail::RecordOps* ops) : ops_(ops) {}
87 /// \endcond
88
89 ///
90 /// Get the value of a field or Var by name.
91 ///
92 /// A misspelled name is a compile error. So is an object, a list, or an action (they have no
93 /// single value).
94 ///
95 /// @return Returns the value as a `bool`, `int64_t`, `double`, String, StyleValue, or Color.
96 /// Integer fields read as `int64_t` and floating-point fields as `double`. String
97 /// fields read as a String, and so do enum fields (as the enumerator's name).
98 ///
99 /// @note Most fields compare directly with the value in your object (eg,
100 /// `s.rev != m.Get<"rev">()`). A `std::string`, enum, or StyleValue field can't be
101 /// compared that way, so gate on an integer revision counter instead.
102 ///
103 template <ultralight::detail::FixedString Name>
104 auto Get() const {
105 constexpr size_t idx = IndexOfChecked<Name>();
106 using SchemaT = std::remove_cvref_t<decltype(schema())>;
107 using Info = detail::EntryInfo<std::tuple_element_t<idx, SchemaT>>;
108 static_assert(Info::kClass == detail::EntryClass::kLeaf
109 || Info::kClass == detail::EntryClass::kVar,
110 "Get reads leaf values; objects, lists, and actions have no slot value");
111 return ReadSlot<Info::kKind>(static_cast<uint32_t>(idx));
112 }
113
114 protected:
115 template <ultralight::detail::FixedString Name>
116 static consteval size_t IndexOfChecked() {
117 constexpr size_t idx = schema().IndexOf(Name.view());
118 static_assert(idx != detail::kNpos, "no entry with this name in the type's schema");
119 return idx;
120 }
121
122 template <ValueKind K>
123 auto ReadSlot(uint32_t slot) const {
124 if constexpr (K == ValueKind::Bool) {
125 return (ops_ && ops_->get_bool) ? ops_->get_bool(ops_->state, slot) : false;
126 } else if constexpr (K == ValueKind::Int64) {
127 return (ops_ && ops_->get_int64) ? ops_->get_int64(ops_->state, slot) : int64_t(0);
128 } else if constexpr (K == ValueKind::Double) {
129 return (ops_ && ops_->get_double) ? ops_->get_double(ops_->state, slot) : 0.0;
130 } else if constexpr (K == ValueKind::String) {
131 String out;
132 if (ops_ && ops_->get_string)
133 ops_->get_string(ops_->state, slot, &out);
134 return out;
135 } else if constexpr (K == ValueKind::StyleValue) {
136 return (ops_ && ops_->get_style) ? ops_->get_style(ops_->state, slot)
137 : StyleValue { 0.0 };
138 } else if constexpr (K == ValueKind::Color) {
139 return (ops_ && ops_->get_color) ? ops_->get_color(ops_->state, slot) : Color {};
140 } else {
141 static_assert(detail::kAlwaysFalse<Snapshot>, "unsupported slot kind");
142 }
143 }
144
145 detail::RecordOps* ops_;
146};
147
148///
149/// Handle passed to a type's static `Sync` function to control which fields are read during
150/// synchronization.
151///
152/// When Context::Sync() runs, the library normally reads every field of every bound instance
153/// through its schema. You can customize this process for any described type by declaring a static
154/// `Sync` function that takes your instance alongside a Model.
155///
156/// It provides access to previously synchronized values and lets you choose which fields to read.
157/// This avoids reading large containers or calling expensive accessors on frames where the
158/// underlying data hasn't changed.
159///
160/// This leaderboard reads its row collection only when its revision counter changes:
161///
162/// ```
163/// struct Score {
164/// int64_t player_id = 0;
165/// int points = 0;
166/// };
167///
168/// struct Leaderboard {
169/// int rev = 1; // bump it whenever rows change
170/// std::string heading;
171/// std::vector<Score> rows;
172///
173/// static constexpr auto schema = dd::Schema(
174/// dd::Field("rev", &Leaderboard::rev, dd::Internal),
175/// dd::Field("heading", &Leaderboard::heading),
176/// dd::List("rows", &Leaderboard::rows, &Score::player_id));
177///
178/// static void Sync(const Leaderboard& b, dd::Model<Leaderboard> m) {
179/// if (b.rev != m.Get<"rev">()) // compare before syncing rev
180/// m.Sync<"rows">();
181/// m.SyncAllExcept<"rows">(); // syncs rev and heading
182/// }
183/// };
184/// ```
185///
186/// ## Writing a Sync Function
187///
188/// You declare a static `Sync` function beside the type's schema, either inside the type itself or
189/// within its dom::data::TypeTraits specialization.
190///
191/// The library never resends unchanged values to attached pages, with or without a static `Sync`
192/// function. Skipping a field saves only the work of reading it in native code.
193///
194/// When you skip syncing a field, the page keeps its last synchronized value. For collections gated
195/// on a revision counter, tag the counter with dom::data::Internal in the schema. Markup can't bind
196/// an Internal entry, and schema tools leave it out.
197///
198/// @note Compare a revision counter before syncing it, and start the counter at 1 rather than 0.
199/// Calling Sync() on the counter updates the recorded value, so comparing afterwards always
200/// finds them equal. Before the first sync, Get() returns 0, so starting at 0 skips the
201/// initial sync and leaves the list blank.
202///
203/// ## Three Kinds of Sync
204///
205/// Each function plays a distinct role during synchronization:
206///
207/// - **Context::Sync() coordinates the overall update cycle.** You call it once per frame on the
208/// home thread to deliver page events to handlers and broadcast changed values to attached
209/// View%s.
210/// - **A type's static `Sync` function customizes the read step for that type.** The library
211/// invokes it while inspecting an instance, passing the instance alongside a Model.
212/// - **Model::Sync() reads a specific field from the instance and sends it if it changed.** You
213/// call it inside your type's static `Sync` function, alongside SyncAll() to read every field and
214/// SyncAllExcept() to read every field except the ones you list.
215///
216/// @warning A Model is valid only during the call to your type's static `Sync` function. Never
217/// store a Model or use it after the function returns.
218///
219/// @see dom::data::Snapshot, dom::data::TypeTraits, dom::data::Internal,
220/// dom::data::Context::Sync()
221///
222template <typename T>
223class Model : public Snapshot<T> {
224 public:
225 /// \cond INTERNAL
226 Model(detail::RecordOps* ops, const T* instance)
227 : Snapshot<T>(ops), instance_(instance) {}
228 /// \endcond
229
230 ///
231 /// Sync one field by name.
232 ///
233 /// Reads the field from your instance and sends it to the page if it changed. A nested object
234 /// or a list syncs each object or row through its own type's Sync function.
235 ///
236 /// @note Sync() on an action or on a field without an accessor is a compile error. Send a
237 /// field without an accessor with Set().
238 ///
239 template <ultralight::detail::FixedString Name>
240 void Sync() {
241 constexpr size_t idx = Base::template IndexOfChecked<Name>();
242 using Tok = std::tuple_element_t<idx, std::remove_cvref_t<decltype(Base::schema())>>;
243 static_assert(detail::EntryInfo<Tok>::kClass != detail::EntryClass::kAction,
244 "actions have no value to sync");
245 static_assert(detail::EntryInfo<Tok>::kHasAccessor,
246 "accessorless fields are written with Set, not synced");
247 SyncEntry<idx>();
248 }
249
250 ///
251 /// Sync every field (what Context::Sync() does for a type without a Sync function).
252 ///
253 /// Actions and fields without an accessor are skipped.
254 ///
255 void SyncAll() { SyncRange(std::make_index_sequence<Base::schema().kCount> {}); }
256
257 ///
258 /// Sync every field except the ones you list (eg, `m.SyncAllExcept<"rows", "log">()`).
259 ///
260 /// Actions and fields without an accessor are skipped. A name that isn't in the schema is a
261 /// compile error.
262 ///
263 template <ultralight::detail::FixedString... Names>
265 ((void)Base::template IndexOfChecked<Names>(), ...);
266 SyncRangeExcept<Names...>(std::make_index_sequence<Base::schema().kCount> {});
267 }
268
269 ///
270 /// Set a field's value by name.
271 ///
272 /// This is how you send a field declared without an accessor (eg, `dd::Field<int>("score")`).
273 /// It also works on any other field or Var with a single value.
274 ///
275 /// @param value The value to send. It must convert to the field's declared type.
276 ///
277 template <ultralight::detail::FixedString Name, typename Arg>
278 void Set(const Arg& value) {
279 constexpr size_t idx = Base::template IndexOfChecked<Name>();
280 using Tok = std::tuple_element_t<idx, std::remove_cvref_t<decltype(Base::schema())>>;
281 using Info = detail::EntryInfo<Tok>;
282 static_assert(Info::kClass == detail::EntryClass::kLeaf
283 || Info::kClass == detail::EntryClass::kVar,
284 "Set writes leaf values; objects, lists, and actions have no slot value");
285 using Value = typename Tok::value_type;
286 static_assert(std::is_convertible_v<const Arg&, Value>,
287 "value is not convertible to the field's declared value type");
288 WriteSlot<Info::kKind>(static_cast<uint32_t>(idx),
289 ValueTraits<Value>::ToSlot(static_cast<Value>(value)));
290 }
291
292 private:
293 using Base = Snapshot<T>;
294
295 template <size_t... Is>
296 void SyncRange(std::index_sequence<Is...>) {
297 (SyncEntryIfSyncable<Is>(), ...);
298 }
299
300 template <ultralight::detail::FixedString... Names, size_t... Is>
301 void SyncRangeExcept(std::index_sequence<Is...>) {
302 (SyncEntryIfIncluded<Is, Names...>(), ...);
303 }
304
305 template <size_t I, ultralight::detail::FixedString... Names>
306 void SyncEntryIfIncluded() {
307 constexpr bool excluded = ((Base::schema().IndexOf(Names.view()) == I) || ...);
308 if constexpr (!excluded)
309 SyncEntryIfSyncable<I>();
310 }
311
312 template <size_t I>
313 void SyncEntryIfSyncable() {
314 using SchemaT = std::remove_cvref_t<decltype(Base::schema())>;
315 using Info = detail::EntryInfo<std::tuple_element_t<I, SchemaT>>;
316 if constexpr (Info::kClass != detail::EntryClass::kAction && Info::kHasAccessor)
317 SyncEntry<I>();
318 }
319
320 template <size_t I>
321 void SyncEntry() {
322 const auto& token = Base::schema().template get<I>();
323 using Tok = std::remove_cvref_t<decltype(token)>;
324 using Info = detail::EntryInfo<Tok>;
325 static_assert(requires(const T& t, const Tok& tk) { detail::ReadAccessor(tk.accessor, t); },
326 "schema accessor does not read this described type (accessor owner "
327 "mismatch at the TypeTraits pairing)");
328 if (!instance_)
329 return;
330 constexpr uint32_t slot = static_cast<uint32_t>(I);
331 detail::RecordOps* ops = this->ops_;
332 if constexpr (Info::kClass == detail::EntryClass::kLeaf
333 || Info::kClass == detail::EntryClass::kVar) {
334 using Value = typename Tok::value_type;
335 WriteSlot<Info::kKind>(slot,
336 ValueTraits<Value>::ToSlot(
337 detail::ReadAccessor(token.accessor, *instance_)));
338 } else if constexpr (Info::kClass == detail::EntryClass::kNullableObject) {
339 decltype(auto) value = detail::ReadAccessor(token.accessor, *instance_);
340 const bool present = static_cast<bool>(value);
341 if (ops && ops->set_present)
342 ops->set_present(ops->state, slot, present);
343 if (present) {
344 if (detail::RecordOps* child = (ops && ops->child) ? ops->child(ops->state, slot)
345 : nullptr)
346 detail::SyncType(child, *value);
347 }
348 } else if constexpr (Info::kClass == detail::EntryClass::kObject) {
349 decltype(auto) value = detail::ReadAccessor(token.accessor, *instance_);
350 if (detail::RecordOps* child = (ops && ops->child) ? ops->child(ops->state, slot)
351 : nullptr)
352 detail::SyncType(child, value);
353 } else if constexpr (Info::kClass == detail::EntryClass::kList) {
354 SyncList<I>(token);
355 }
356 }
357
358 template <size_t I, typename Tok>
359 void SyncList(const Tok& token) {
360 detail::RecordOps* ops = this->ops_;
361 if (!ops)
362 return;
363 constexpr uint32_t slot = static_cast<uint32_t>(I);
364 decltype(auto) rows = detail::ReadAccessor(token.accessor, *instance_);
365 const uint64_t count
366 = static_cast<uint64_t>(std::distance(std::begin(rows), std::end(rows)));
367 if (ops->list_begin)
368 ops->list_begin(ops->state, slot, count);
369 constexpr bool keyed = [] {
370 if constexpr (detail::IsListToken<Tok>::value)
371 return Tok::kKeyed;
372 else
373 return false;
374 }();
375 uint64_t ordinal = 0;
376 for (const auto& row : rows) {
377 detail::RecordOps* row_ops = nullptr;
378 if constexpr (keyed) {
379 using Key = detail::AccessorValue<typename Tok::key_pointer_type>;
380 constexpr ValueKind key_kind = ValueTraits<Key>::kind;
381 if constexpr (key_kind == ValueKind::Int64) {
382 if (ops->list_row_key_int64)
383 row_ops = ops->list_row_key_int64(ops->state, slot, ordinal,
384 ValueTraits<Key>::ToSlot(row.*token.key));
385 } else {
386 const String key = ValueTraits<Key>::ToSlot(row.*token.key);
387 if (ops->list_row_key_string)
388 row_ops = ops->list_row_key_string(ops->state, slot, ordinal, &key);
389 }
390 } else {
391 if (ops->list_row)
392 row_ops = ops->list_row(ops->state, slot, ordinal);
393 }
394 if (row_ops)
395 detail::SyncType(row_ops, row);
396 ordinal++;
397 }
398 if (ops->list_end)
399 ops->list_end(ops->state, slot);
400 }
401
402 template <ValueKind K, typename SlotV>
403 void WriteSlot(uint32_t slot, const SlotV& value) {
404 detail::RecordOps* ops = this->ops_;
405 if (!ops)
406 return;
407 if constexpr (K == ValueKind::Bool) {
408 if (ops->set_bool)
409 ops->set_bool(ops->state, slot, value);
410 } else if constexpr (K == ValueKind::Int64) {
411 if (ops->set_int64)
412 ops->set_int64(ops->state, slot, value);
413 } else if constexpr (K == ValueKind::Double) {
414 if (ops->set_double)
415 ops->set_double(ops->state, slot, value);
416 } else if constexpr (K == ValueKind::String) {
417 const String& stored = value;
418 if (ops->set_string)
419 ops->set_string(ops->state, slot, &stored);
420 } else if constexpr (K == ValueKind::StyleValue) {
421 if (ops->set_style)
422 ops->set_style(ops->state, slot, value);
423 } else if constexpr (K == ValueKind::Color) {
424 if (ops->set_color)
425 ops->set_color(ops->state, slot, value);
426 } else {
427 static_assert(detail::kAlwaysFalse<SlotV>, "unsupported slot kind");
428 }
429 }
430
431 const T* instance_;
432};
433
434/// \cond INTERNAL
435namespace detail {
436
437template <typename T>
438void SyncType(RecordOps* ops, const T& instance) {
439 using Plain = std::remove_cvref_t<T>;
440 Model<Plain> model(ops, &instance);
441 if constexpr (kHasCustomSync<Plain>)
442 TypeTraits<Plain>::Sync(instance, model);
443 else
444 model.SyncAll();
445}
446
447} // namespace detail
448/// \endcond
449
450} // namespace data
451} // namespace dom
452} // namespace ultralight
An RGBA color value in a certain color space (with CSS parsing helpers).
Definition Color.h:69
Unicode string container with conversions for UTF-8, UTF-16, and UTF-32.
Definition String.h:31
Handle passed to a type's static Sync function to control which fields are read during synchronizatio...
Definition Model.h:223
void Sync()
Sync one field by name.
Definition Model.h:240
void SyncAllExcept()
Sync every field except the ones you list (eg, m.SyncAllExcept<"rows", "log">()).
Definition Model.h:264
void Set(const Arg &value)
Set a field's value by name.
Definition Model.h:278
void SyncAll()
Sync every field (what Context::Sync() does for a type without a Sync function).
Definition Model.h:255
Read-only access to the synchronized values of a described type.
Definition Model.h:76
detail::RecordOps * ops_
Definition Model.h:145
T described_type
Definition Model.h:78
auto ReadSlot(uint32_t slot) const
Definition Model.h:123
static constexpr const auto & schema()
Get the type's schema.
Definition Model.h:83
auto Get() const
Get the value of a field or Var by name.
Definition Model.h:104
static consteval size_t IndexOfChecked()
Definition Model.h:116
A generic value passed to data-binding formatters and handlers.
Definition Value.h:95
Data-binding API that connects native C++ data to HTML and CSS markup.
Definition ActionInfo.h:13
ValueKind
The kinds of value a schema entry holds.
Definition ValueTraits.h:27
@ String
UTF-8 text (from strings and enums).
Definition ValueTraits.h:31
@ StyleValue
A CSS number with a unit (see dom::StyleValue).
Definition ValueTraits.h:32
@ Bool
A boolean.
Definition ValueTraits.h:28
@ Color
A color (see ultralight::Color).
Definition ValueTraits.h:33
@ Double
A double-precision float (from any floating-point type).
Definition ValueTraits.h:30
@ Int64
A signed 64-bit integer (from any integer type).
Definition ValueTraits.h:29
Direct C++ access to modify page elements and handle events.
Root namespace for every public Ultralight type, function, and enumeration.
ContainerSpec< Children... > row(ContainerOptions options, Children... children)
Describe a row container with child elements.
Definition Builder.h:125
@ Info
Information icon.
Definition Dialogs.h:55
A numeric CSS value and its unit.
Definition StyleValue.h:147
Traits template for describing a type to data bindings.
Definition TypeTraits.h:156
Conversion rules that expose a C++ type as a primitive value in data bindings.
Definition ValueTraits.h:248