docs
Loading...
Searching...
No Matches
Context.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
6// X11's headers define None as a macro, which would break the None enumerators.
7#pragma push_macro("None")
8#undef None
9
10#include <Ultralight/CAPI/CAPI_DOMData.h>
12#include <Ultralight/View.h>
18#include <Ultralight/dom/data/detail/ContextImpl.h>
19#include <Ultralight/dom/data/detail/Descriptors.h>
20#include <Ultralight/dom/data/detail/Emission.h>
21
22#include <cstdint>
23#include <cstring>
24#include <initializer_list>
25#include <memory>
26#include <string>
27#include <string_view>
28#include <type_traits>
29#include <utility>
30#include <vector>
31
32namespace ultralight {
33namespace dom {
34
35///
36/// @namespace ultralight::dom::data
37///
38/// Data-binding API that connects native C++ data to HTML and CSS markup.
39///
40/// `#include <Ultralight/dom/data/Context.h>`
41///
42/// @note This API is a preview and may still change after 2.0.
43///
44/// Data bindings connect native C++ objects directly to page markup. Your HTML and CSS declare
45/// where values appear and which elements dispatch actions, while your data and application logic
46/// remain in native code.
47///
48/// You update your native objects during your normal update loop and call
49/// dom::data::Context::Sync() once per frame. The library reads every bound instance through its
50/// schema and sends only the values that changed in a single batch.
51///
52/// A data context binds native instances and synchronizes their fields with an attached View:
53///
54/// ```
55/// struct Player {
56/// int health = 100;
57/// int64_t gold = 0;
58/// };
59///
60/// Player player;
61/// dd::Context ctx = dd::Context::Create();
62/// dd::Binding binding = ctx.Bind("player", player); // "player" in markup
63///
64/// if (ctx.AttachTo(view.get()))
65/// view->LoadURL("file:///hud.html");
66///
67/// // Once per frame:
68/// player.health -= 10;
69/// ctx.Sync();
70/// ```
71///
72/// The page markup displays those values using placeholder syntax:
73///
74/// ```html
75/// <p>Health: {{player.health}}</p>
76/// <p>Gold: {{player.gold}}</p>
77/// ```
78///
79/// ## Common Tasks
80///
81/// Select a type based on the task you need to perform:
82///
83/// | Task | Type |
84/// |---------------------------------|------------------------|
85/// | Bind instances and sync pages | dom::data::Context |
86/// | Describe a type for markup | dom::data::TypeTraits |
87/// | Handle actions and edits | dom::data::Binding |
88/// | Skip unchanged data during sync | dom::data::Model |
89/// | Support custom value types | dom::data::ValueTraits |
90///
91/// ## Header and Namespace
92///
93/// The data-binding types live in `<Ultralight/dom/data/Context.h>` and aren't included by
94/// `<Ultralight/DOM.h>`. Including this header provides the full data-binding API.
95///
96/// The library provides `dd` as a namespace alias for `ultralight::dom::data`. Reference
97/// documentation and code examples use this alias for brevity (`dd::Context`, `dd::Binding`).
98///
99/// ## Core Rules
100///
101/// - **Most operations belong to the Context's home thread.** You bind instances, register
102/// handlers, modify bound objects, and call dom::data::Context::Sync() on the thread that created
103/// the Context. Members safe to call from any thread say so in their documentation.
104/// - **The library reads bound objects only during dom::data::Context::Sync() and never writes
105/// them.** You can modify your instances between sync calls.
106/// - **Page input arrives as requests that your handlers apply.** User edits and button clicks
107/// don't modify your objects directly. Your dom::data::Binding::OnChange() and
108/// dom::data::Binding::OnAction() callbacks decide whether to store or reject each change.
109/// - **The API reports errors through return values rather than C++ exceptions.** Functions return
110/// false or empty handles when an operation can't be completed. If a handler, formatter,
111/// validator, or posted task throws, the library catches it, drops that call, and logs a warning.
112///
113/// @see dom::data::Context, dom::data::Binding, dom::data::TypeTraits, dom::data::Schema()
114///
115namespace data {
116
117///
118/// When Context::DumpSchema() writes its file.
119///
120enum class DumpAt {
121 NextSync, ///< At the end of the next Context::Sync().
122 Now, ///< Right away.
123};
124
125static_assert(static_cast<int>(DumpAt::NextSync) == kULDOMDataDumpAt_NextSync
126 && static_cast<int>(DumpAt::Now) == kULDOMDataDumpAt_Now,
127 "DumpAt must mirror ULDOMDataDumpAt");
128
129///
130/// The flags for Context::AttachTo() (the typed form of ULDOMDataContextAttachFlags). None are
131/// defined yet.
132///
133enum class AttachFlags : unsigned {
134 None = 0, ///< No options.
135};
136
137static_assert(static_cast<unsigned>(AttachFlags::None) == kULDOMDataContextAttachFlags_None,
138 "dom::data::AttachFlags must mirror ULDOMDataContextAttachFlags");
139
140///
141/// Options for Context::AttachTo().
142///
144 ///
145 /// The attach flags (see AttachFlags).
146 ///
148
149 ///
150 /// The origin rules for the pages that get the bindings. Leave it empty for the default
151 /// policy (your application's own content). See ultralight::OriginRules.
152 ///
154};
155
156///
157/// Data-binding context for attached View%s.
158///
159/// A Context registers your C++ instances under names that HTML markup can reference, exposing
160/// their fields and actions to the pages loaded by each attached View. Your markup declares where
161/// values appear and which interactions trigger native actions, keeping page presentation separate
162/// from your application's data.
163///
164/// Calling Sync() once per frame delivers input from the page to your native handlers and
165/// broadcasts changed values in a single batch to keep those pages current.
166///
167/// This example binds a player instance to a context and attaches it to a View:
168///
169/// ```
170/// struct Player {
171/// int health = 80;
172/// int potions = 3;
173/// void UsePotion();
174///
175/// static constexpr auto schema = dd::Schema(
176/// dd::Field("health", &Player::health),
177/// dd::Field("potions", &Player::potions),
178/// dd::Action("usePotion"));
179/// };
180///
181/// Player player;
182/// dd::Context ctx = dd::Context::Create();
183/// dd::Binding binding = ctx.Bind("player", player) // the page calls it "player"
184/// .OnAction<"usePotion">(&Player::UsePotion); // the button below calls this
185///
186/// if (!ctx.AttachTo(view.get()))
187/// Log("couldn't attach the data context");
188/// view->LoadURL("file:///hud.html");
189///
190/// // Once per frame:
191/// ctx.Sync(); // runs UsePotion() if clicked, then updates the page
192/// ```
193///
194/// The loaded page references bound fields and click actions directly in markup:
195///
196/// ```html
197/// <p>Health: {{player.health}}</p>
198/// <button type="button" ul-on:click="player.usePotion">
199/// Drink ({{player.potions}} left)
200/// </button>
201/// ```
202///
203/// ## Binding Instances
204///
205/// Calling Bind() registers an instance under a binding name that forms the root of every markup
206/// path on the page.
207///
208/// Always store the returned dom::data::Binding handle alongside your data, because destroying it
209/// unbinds the instance immediately (see dom::data::Binding).
210///
211/// A context can bind an instance by reference, by value, or through a smart pointer:
212///
213/// ```
214/// auto ally = std::make_shared<Player>();
215///
216/// dd::Binding b1 = ctx.Bind("player", player); // borrows player
217/// dd::Binding b2 = ctx.Bind("preview", Player {}); // owns its own Player
218/// dd::Binding b3 = ctx.Bind("ally", ally); // shares ownership of *ally
219/// ```
220///
221/// Each binding form defines who keeps the bound instance alive:
222///
223/// - **Binding by reference borrows the instance.** Native code retains ownership and must keep the
224/// instance alive while it remains bound.
225/// - **Binding by value moves ownership into the binding.** The dom::data::Binding owns the
226/// instance and destroys it when it unbinds.
227/// - **Binding through a smart pointer manages lifetime automatically.** An owning pointer keeps
228/// the instance alive, while an expired weak pointer skips updates and leaves the last values on
229/// the page.
230///
231/// ## Attaching to Views
232///
233/// Call AttachTo() to connect a Context to a View. You only attach once-- every page that View
234/// loads receives the bindings automatically, including pages restored from the back-forward cache.
235///
236/// A single Context can attach to multiple View%s, and a View can hold bindings from multiple
237/// Context%s. One call to Sync() synchronizes all attached View%s at once.
238///
239/// Data bindings work only in a View's main frame.
240///
241/// ### Origin Rules
242///
243/// By default, only local `file://` pages and pages loaded with View::LoadHTML() receive bindings.
244///
245/// Passing custom origin rules in AttachOptions replaces the default policy completely. To allow
246/// local files alongside remote origins, include `file://*` in the rule list (see OriginRules for
247/// pattern syntax).
248///
249/// Pages loaded with View::LoadHTML() without a URL have an opaque origin that no custom rule
250/// matches, so they receive bindings only under the default policy. When loaded with an explicit
251/// URL, they match against that URL's origin.
252///
253/// ## Home Thread
254///
255/// The thread that calls Create() becomes the home thread for that Context. In a single-threaded
256/// application, this is the Renderer's thread. You bind instances, register handlers, and call
257/// Sync() on this thread. When you modify bound data, make those changes on the home thread between
258/// calls to Sync().
259///
260/// When multiple worker threads need to modify bound data, give each thread its own Context. If the
261/// thread that calls Sync() changes over time (such as across worker threads in a job system), call
262/// CreateThreadSafe() instead so any thread can bind data, register handlers, and call Sync().
263///
264/// ## Finding Mistakes
265///
266/// A mistake in markup drops only the broken binding, allowing the rest of the page to bind
267/// normally.
268///
269/// When developer mode is on (`Config::diagnostics`), the library outputs warnings for markup
270/// mistakes to the native Logger and the page's console.
271///
272/// \parblock
273/// @note Keep calling Sync() while the page is visible, even when your game is paused, so actions
274/// and change requests from the page continue to reach native handlers.
275/// \endparblock
276///
277/// \parblock
278/// @note dom::data::Context is unrelated to js::Context.
279/// \endparblock
280///
281/// @see dom::data::Binding, dom::data::TypeTraits, dom::data::Schema(), PostTask(), DefineFormat()
282///
283class Context {
284 public:
285 ///
286 /// Create a Context whose home thread is the calling thread.
287 ///
288 /// @return Returns the new Context.
289 ///
290 static Context Create() { return Context(ulCreateDOMDataContext()); }
291
292 ///
293 /// Create a Context that any thread can use.
294 ///
295 /// Use this when the thread that calls Sync() changes over time (eg, a job system that syncs
296 /// on different worker threads). Any thread can then bind, sync, and register handlers. The
297 /// calls take turns (each one waits until the running one finishes), and handlers run on the
298 /// thread that called Sync().
299 ///
300 /// @warning
301 /// \parblock
302 /// Only the Context's own calls are thread-safe, **not** your bound instances. Sync() reads
303 /// them without any lock you can see, so don't change an instance while another thread may be
304 /// syncing. To hand an instance to another thread, use your own synchronization.
305 ///
306 /// A handler that waits on another thread that needs this Context deadlocks. Calling back into
307 /// the Context from the handler itself is fine.
308 /// \endparblock
309 ///
310 /// @return Returns the new Context.
311 ///
312 static Context CreateThreadSafe() { return Context(ulCreateDOMDataContextThreadSafe()); }
313
314 Context(const Context&) = delete;
315 Context& operator=(const Context&) = delete;
316
317 ///
318 /// Move constructor (`other` becomes empty).
319 ///
320 /// @param other The Context to move from.
321 ///
322 Context(Context&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; }
323
324 ///
325 /// Move assignment (`other` becomes empty).
326 ///
327 /// This Context releases its own handle first.
328 ///
329 /// @param other The Context to move from.
330 ///
331 /// @return Returns this Context.
332 ///
333 Context& operator=(Context&& other) noexcept {
334 if (this != &other) {
335 ulDestroyDOMDataContext(handle_);
336 handle_ = other.handle_;
337 other.handle_ = nullptr;
338 }
339 return *this;
340 }
341
342 ///
343 /// Destroy this handle.
344 ///
345 /// Destroying the Context detaches it from every View (the pages drop its bindings the same
346 /// way DetachFrom() does) and destroys its bindings, handlers, and formatters (and anything
347 /// they capture) right here, on the calling thread. So destroy it on its home thread. A
348 /// Binding that outlives it stays safe to use, but its instance is no longer read.
349 ///
350 /// @note A Context from FromBorrowed() doesn't own the context, so destroying it changes
351 /// nothing, and neither does destroying one while another owning handle exists.
352 ///
353 ~Context() { ulDestroyDOMDataContext(handle_); }
354
355 ///
356 /// Whether or not this Context is valid (false after a move or LeakRef()).
357 ///
358 explicit operator bool() const { return handle_ != nullptr; }
359
360 ///
361 /// Bind an instance of a described type under a binding name.
362 ///
363 /// Pages reach the instance's fields through the binding name (eg, `hud.health`), and each
364 /// Sync() sends your changes to them. The Binding borrows `instance`, so keep it alive while
365 /// it's bound.
366 ///
367 /// Binding a name again replaces the earlier binding (see Binding).
368 ///
369 /// @param name The binding name pages use. It starts with a letter or an underscore,
370 /// followed by letters, digits, and underscores (eg, `hud` or
371 /// `main_menu`).
372 ///
373 /// @param instance The instance to bind. Ownership remains with the caller.
374 ///
375 /// @return Returns the Binding (empty if `name` isn't a valid binding name, and the library
376 /// logs a warning).
377 ///
378 /// @warning Destroying the Binding unbinds the instance, so a `ctx.Bind(...)` statement whose
379 /// result you don't keep binds and unbinds at once.
380 ///
381 template <typename T>
382 requires(Described<T> && !std::is_const_v<T>)
383 [[nodiscard]] Binding<T> Bind(std::string_view name, T& instance) {
384 Binding<T> binding(BindHandle<T>(name, static_cast<void*>(std::addressof(instance)),
385 &detail::BorrowedAcquire, nullptr, nullptr));
386 if (binding)
387 binding.access_ = detail::MakeBorrowedAccess<T>(std::addressof(instance));
388 return binding;
389 }
390
391 ///
392 /// Bind a const instance under a binding name.
393 ///
394 /// This works like the non-const overload, except that handlers which are member functions
395 /// of the bound type (eg, `OnAction<"respawn">(&HUD::Respawn)`) do nothing, since they would
396 /// change the instance. Register a lambda or another receiver instead.
397 ///
398 /// @param name The binding name pages use.
399 ///
400 /// @param instance The instance to bind. Ownership remains with the caller.
401 ///
402 /// @return Returns the Binding (empty if `name` isn't a valid binding name).
403 ///
404 template <typename T>
405 requires Described<T>
406 [[nodiscard]] Binding<T> Bind(std::string_view name, const T& instance) {
407 return Binding<T>(
408 BindHandle<T>(name, const_cast<void*>(static_cast<const void*>(std::addressof(instance))),
409 &detail::BorrowedAcquire, nullptr, nullptr));
410 }
411
412 ///
413 /// Bind an instance by value under a binding name.
414 ///
415 /// The Binding takes ownership of the moved-in instance and destroys it when it unbinds. Use
416 /// this for data you build once and don't change afterwards.
417 ///
418 /// Handlers that are member functions of the bound type (eg,
419 /// `OnAction<"respawn">(&HUD::Respawn)`) run on the owned instance. They're the only way to
420 /// change it after binding.
421 ///
422 /// @param name The binding name pages use.
423 ///
424 /// @param instance The instance to move into the Binding.
425 ///
426 /// @return Returns the Binding (empty if `name` isn't a valid binding name).
427 ///
428 template <typename T>
429 requires(Described<std::remove_cvref_t<T>> && !std::is_lvalue_reference_v<T>)
430 [[nodiscard]] Binding<std::remove_cvref_t<T>> Bind(std::string_view name, T&& instance) {
431 using Plain = std::remove_cvref_t<T>;
432 using Box = detail::ValueHolderBox<Plain>;
433 auto* box = new Box { std::forward<T>(instance) };
434 Binding<Plain> binding(
435 BindHandle<Plain>(name, box, &Box::Acquire, nullptr, &Box::Destroy));
436 if (binding)
437 binding.access_ = detail::MakeValueAccess<Plain>(box);
438 return binding;
439 }
440
441 ///
442 /// Bind an instance through a smart pointer under a binding name.
443 ///
444 /// `holder` can be a std::shared_ptr, RefPtr, or moved std::unique_ptr. It can also be a
445 /// std::weak_ptr, WeakPtr, or your own smart pointer type with an ultralight::HolderTraits
446 /// specialization. The Binding keeps the holder (a copy or the moved std::unique_ptr)-- an owning
447 /// pointer keeps the instance alive while it's bound. Sync() locks the holder while it reads the
448 /// instance.
449 ///
450 /// Once a weak holder expires, Sync() skips the binding and pages keep its last values.
451 /// Handlers that are member functions of the bound type skip it too, and do nothing at all
452 /// when the holder points to a const type.
453 ///
454 /// @param name The binding name pages use.
455 ///
456 /// @param holder The smart pointer to the instance.
457 ///
458 /// @return Returns the Binding (empty if `name` isn't a valid binding name).
459 ///
460 template <typename H>
461 requires BindableHolder<H>
462 [[nodiscard]] Binding<
463 std::remove_cv_t<typename HolderTraits<std::remove_cvref_t<H>>::element_type>>
464 Bind(std::string_view name, H&& holder) {
465 using Plain = std::remove_cvref_t<H>;
466 using T = std::remove_cv_t<typename HolderTraits<Plain>::element_type>;
467 using Box = detail::SmartHolderBox<Plain>;
468 auto* box = new Box { std::forward<H>(holder), std::nullopt };
469 Binding<T> binding(
470 BindHandle<T>(name, box, &Box::Acquire, &Box::Release, &Box::Destroy));
471 if (binding) {
472 using LockedRef
473 = decltype(*ultralight::detail::LockHolder(std::declval<const Plain&>()));
474 if constexpr (!std::is_const_v<std::remove_reference_t<LockedRef>>)
475 binding.access_ = detail::MakeHolderAccess<Plain, T>(box);
476 }
477 return binding;
478 }
479
480 ///
481 /// Attach this Context to a View.
482 ///
483 /// Every page the View loads then gets this Context's bindings, starting with the current page
484 /// during a later Renderer::Update(). The View keeps the Context attached until you detach it
485 /// or destroy its last owning Context.
486 ///
487 /// A page gets the bindings only if the origin rules allow it. With no rules, only your
488 /// application's own content does (local pages and pages loaded with View::LoadHTML()). Rules
489 /// have the `scheme://host[:port]` form (see OriginRules for the full syntax), and they replace
490 /// that default, so add `file://*` if you still want local pages:
491 ///
492 /// ```
493 /// if (!ctx.AttachTo(view.get(), { .origin_rules = { "https://*.mygame.com", "file://*" } }))
494 /// Log("an origin rule failed to parse");
495 /// ```
496 ///
497 /// A page whose origin isn't allowed shows its authored markup, and its controls send no input. A
498 /// page loaded with View::LoadHTML() without a URL has an opaque origin-- no rule matches it and
499 /// no filter allows one, so it gets bindings only with the default rules. Attaching an already
500 /// attached Context replaces its flags and rules.
501 ///
502 /// @param view The View to attach to.
503 ///
504 /// @param options The attach flags and the origin rules (see AttachOptions).
505 ///
506 /// @return Returns true on success, or false if `view` is nullptr, this Context is empty, a
507 /// flag is unknown, or a rule failed to parse. An earlier attachment stays as it was.
508 /// An unknown flag or a rule that failed to parse also logs a warning that says why.
509 ///
510 /// @note Safe to call from any thread.
511 ///
512 /// @see DetachFrom()
513 ///
514 [[nodiscard]] bool AttachTo(View* view, const AttachOptions& options = {}) {
515 return view && handle_
516 && view->AttachDOMDataContext(handle_, static_cast<unsigned>(options.flags),
517 options.origin_rules.data(), options.origin_rules.size());
518 }
519
520 ///
521 /// Detach this Context from a View.
522 ///
523 /// During a later Renderer::Update(), the View's page rebuilds its bindings from the Contexts
524 /// still attached. When none are left, the page gets its authored markup back (`{{path}}` text,
525 /// `ul-if` templates, and no rows). Values already written by `ul-text`, classes, attributes,
526 /// and styles stay.
527 ///
528 /// @param view The View to detach from (nullptr does nothing).
529 ///
530 /// @note Safe to call from any thread.
531 ///
532 void DetachFrom(View* view) {
533 if (view && handle_)
534 view->DetachDOMDataContext(handle_);
535 }
536
537 ///
538 /// Deliver input from the page to your handlers and send your changes to the pages.
539 ///
540 /// Call this once per tick on the home thread. Each call does three things in order:
541 ///
542 /// 1. **Delivers input from the page** to your handlers (changes first, then actions).
543 /// 2. **Reads every bound instance** through its schema.
544 /// 3. **Sends what changed** to the pages as one update.
545 ///
546 /// Pages show the update during a later Renderer::Update(), and they never show part of one.
547 /// A Sync with nothing to send is cheap.
548 ///
549 /// A type can control how its instances are read with a static `Sync` function, which reads
550 /// fields with Model::Sync() (see "Three Kinds of Sync" in the Model class description).
551 ///
552 /// @note A Sync() called from inside one of your handlers does nothing (the library logs a
553 /// warning).
554 ///
555 void Sync() { ulDOMDataContextSync(handle_); }
556
557 ///
558 /// Send an action to a bound instance's handler as if the page fired it.
559 ///
560 /// Use this where you have no Binding at hand, eg, in a DOM listener that handles an
561 /// interaction the markup can't express. Where you have the Binding, use
562 /// Binding::PostAction() instead (it checks the action name at compile time).
563 ///
564 /// The action reaches its handler at the next Sync(), in order with the actions from the page.
565 /// An action with an unknown binding or action name is dropped there, with a warning when
566 /// DOM diagnostics are on (see "Finding Mistakes" in the class overview).
567 ///
568 /// @param name The binding name and the action name joined by a dot (eg, `"inv.sort"`).
569 /// Only actions of the bound type itself can be sent (not a row's actions).
570 ///
571 /// @return Returns true if the action was queued, or false if `name` isn't in that form or
572 /// the action queue is full.
573 ///
574 /// \parblock
575 /// @note Safe to call from any thread.
576 /// \endparblock
577 ///
578 /// \parblock
579 /// @note Each call takes one entry in the action queue, even for an action declared
580 /// `dd::OnlyLatest` (repeats merge only when Sync() delivers them).
581 /// \endparblock
582 ///
583 /// @see set_action_queue_capacity()
584 ///
585 bool PostAction(std::string_view name) { return PostActionByName(name, nullptr, 0); }
586
587 ///
588 /// Send an action with a payload struct (see the payloadless overload).
589 ///
590 /// ```
591 /// ctx.PostAction("inv.moveItem", MoveItem { .from = a, .to = b });
592 /// ```
593 ///
594 /// The payload is a described struct whose fields are all bool, integer, floating-point,
595 /// string, or reflected enum values (checked at compile time). The action's handler receives
596 /// it as its declared payload type (a handler whose payload type doesn't match doesn't receive
597 /// it).
598 ///
599 /// @param name The binding name and the action name joined by a dot.
600 ///
601 /// @param payload The payload to send.
602 ///
603 /// @return Returns true if the action was queued, or false if `name` isn't in that form or
604 /// the action queue is full.
605 ///
606 template <typename P>
608 bool PostAction(std::string_view name, const P& payload) {
609 using Plain = std::remove_cvref_t<P>;
610 static_assert(detail::PayloadMarshalable<Plain>(),
611 "dom::data: a payload type carries plain data fields only (readable "
612 "leaf members of boolean, integral, floating-point, string, or "
613 "reflected enum type)");
614 std::vector<std::string> strings;
615 std::vector<ULDOMDataPayloadEntry> entries;
616 detail::MarshalPayload(payload, strings, entries);
617 return PostActionByName(name, entries.empty() ? nullptr : entries.data(),
618 entries.size());
619 }
620
621 ///
622 /// Send an action with a single-value payload (see the payloadless overload).
623 ///
624 /// Use this for an action declared with one bool, number, string, or reflected enum value (eg,
625 /// `dd::Action<double>("seek")`):
626 ///
627 /// ```
628 /// ctx.PostAction("player.seek", 0.5);
629 /// ```
630 ///
631 /// @param name The binding name and the action name joined by a dot.
632 ///
633 /// @param payload The value to send.
634 ///
635 /// @return Returns true if the action was queued, or false if `name` isn't in that form or
636 /// the action queue is full.
637 ///
638 template <typename P>
640 bool PostAction(std::string_view name, const P& payload) {
641 std::string backing;
642 ULDOMDataPayloadEntry entry {};
643 detail::MarshalScalarPayload(payload, backing, entry);
644 return PostActionByName(name, &entry, 1);
645 }
646
647 ///
648 /// Run a callable on the Renderer's thread once the pages show your latest data.
649 ///
650 /// The callable runs during a later Renderer::Update(), after the pages have applied the data
651 /// from:
652 ///
653 /// - **This Sync**, when you post from a handler inside Sync().
654 /// - **The last Sync**, when you post from anywhere else.
655 ///
656 /// How often it runs depends on its parameters:
657 ///
658 /// - **No parameters** runs once after every attached View has applied the data. When no View is
659 /// attached, it runs during a later Renderer::Update() without waiting for a page.
660 /// - **A dom::Document** runs once for each attached View and gets that View's document, so it
661 /// can read the page your data produced. With no attached Views, it runs once with an empty
662 /// Document.
663 ///
664 /// ```
665 /// ctx.PostTask([] { SignalUIReady(); }); // once
666 /// ctx.PostTask([](dom::Document doc) { // once per View
667 /// dom::Element row = doc.querySelector("#roster li");
668 /// // ...
669 /// });
670 /// ```
671 ///
672 /// A View whose page is still loading holds it up until that page shows the data. A page that
673 /// doesn't use this Context (eg, its origin isn't allowed) doesn't hold it up.
674 ///
675 /// @param callback The callable to run. Capture by value, since it runs later (and on
676 /// another thread unless your home thread is the Renderer's thread).
677 ///
678 /// \parblock
679 /// @note Safe to call from any thread.
680 /// \endparblock
681 ///
682 /// \parblock
683 /// @note The library destroys the callable after its last run. If no Renderer exists, or the
684 /// Renderer shuts down first, it's destroyed without running.
685 /// \endparblock
686 ///
687 template <typename F>
688 requires(std::is_invocable_v<std::decay_t<F>&>
689 || std::is_invocable_v<std::decay_t<F>&, Document>)
690 void PostTask(F&& callback) {
691 using Fn = std::decay_t<F>;
692 auto* fn = new Fn(std::forward<F>(callback));
693 if constexpr (std::is_invocable_v<Fn&, Document>) {
694 ulDOMDataContextPostTask(handle_, &detail::InvokePostedCallable<Fn>, fn,
695 &detail::DeletePostedCallable<Fn>);
696 } else {
697 ulDOMDataContextPostTaskOnce(handle_, &detail::InvokePostedCallable<Fn>, fn,
698 &detail::DeletePostedCallable<Fn>);
699 }
700 }
701
702 ///
703 /// Define a formatter for `{{path|name}}` text.
704 ///
705 /// A formatter turns a bool, number, or string value into the text the page shows:
706 ///
707 /// ```
708 /// ctx.DefineFormat("comma", [](dd::Value v) {
709 /// return GroupDigits(v.Or(int64_t(0)));
710 /// });
711 /// ```
712 ///
713 /// ```html
714 /// <span>{{hud.gold|comma}} gold</span>
715 /// ```
716 ///
717 /// Only `{{path}}` text uses formatters (`ul-text` doesn't take one), and only for the
718 /// bindings of this Context. Defining a name again replaces its formatter. A new formatter
719 /// shows the next time the text changes.
720 ///
721 /// @param name The formatter name, used after the pipe. It starts with a letter or an
722 /// underscore, followed by letters, digits, and underscores. A name that
723 /// doesn't defines nothing (the library logs a warning).
724 ///
725 /// @param fn The formatter. It takes the value as a dd::Value and returns the text (a
726 /// String, a std::string, or a C string).
727 ///
728 /// @return Returns this Context, for chaining.
729 ///
730 /// @warning Formatters run on the **Renderer's thread**. They must return the same text for
731 /// the same value, must not throw, and must not call the data-binding API.
732 ///
733 template <typename Fn>
734 Context& DefineFormat(std::string_view name, Fn&& fn) {
735 using Plain = std::decay_t<Fn>;
736 static_assert(std::is_invocable_v<Plain&, Value>,
737 "dom::data: a formatter takes the published value as a dd::Value");
738 if constexpr (std::is_invocable_v<Plain&, Value>) {
739 using R = std::remove_cvref_t<std::invoke_result_t<Plain&, Value>>;
740 static_assert(std::is_same_v<R, ultralight::String> || std::is_same_v<R, std::string>
741 || std::is_constructible_v<ultralight::String, R>,
742 "dom::data: a formatter returns display text (String, std::string, "
743 "or a C string)");
744 auto* boxed = new Plain(std::forward<Fn>(fn));
745 ulDOMDataContextDefineFormat(handle_, std::string(name).c_str(),
746 &detail::InvokeFormatCallable<Plain>, boxed,
747 &detail::DeletePostedCallable<Plain>);
748 }
749 return *this;
750 }
751
752 ///
753 /// Remove a formatter.
754 ///
755 /// Text that uses it shows the plain value (and logs a warning) from the next time it changes.
756 ///
757 /// @param name The formatter name.
758 ///
759 /// @return Returns this Context, for chaining.
760 ///
761 Context& UndefineFormat(std::string_view name) {
762 ulDOMDataContextDefineFormat(handle_, std::string(name).c_str(), nullptr, nullptr, nullptr);
763 return *this;
764 }
765
766 ///
767 /// Get the generation of the last update Sync() sent to the pages.
768 ///
769 /// The generation goes up by one with each update.
770 ///
771 /// @return Returns the generation (0 before the first update or for an empty Context).
772 ///
773 uint64_t generation() const { return ulDOMDataContextGetGeneration(handle_); }
774
775 ///
776 /// Set how many actions can wait for the next Sync(). (Default: 1024)
777 ///
778 /// When the queue is full, new actions are dropped (the library logs a warning). For an action
779 /// the page fires continuously (eg, a scroll position), declare it `dd::OnlyLatest` so each new
780 /// one replaces the waiting one instead of raising the limit.
781 ///
782 /// @param capacity The number of actions (values below 1 become 1).
783 ///
784 /// @note Safe to call from any thread.
785 ///
786 void set_action_queue_capacity(uint32_t capacity) {
787 ulDOMDataContextSetActionQueueCapacity(handle_, capacity);
788 }
789
790 ///
791 /// Get how many actions can wait for the next Sync().
792 ///
793 /// @return Returns the limit (0 for an empty Context).
794 ///
795 /// @note Safe to call from any thread.
796 ///
797 uint32_t action_queue_capacity() const {
798 return ulDOMDataContextGetActionQueueCapacity(handle_);
799 }
800
801 ///
802 /// Get the schema of every bound instance as JSON, with their current values.
803 ///
804 /// The mock data tools read this JSON, so you can build and test pages in a browser with your
805 /// app's real model shapes (see DumpSchema() to write it to a file). It has:
806 ///
807 /// - **`format`, `api`, and `version`**: "ul-schema", "data", and 1.
808 /// - **`models`**: maps each binding name to its type in `types`.
809 /// - **`types`**: lists each type's entries in schema order (with their names, value types,
810 /// flags, and child types).
811 /// - **`values`**: maps each binding name to its current values, read through its schema in
812 /// the form mock data uses. A list holds at most its first 100 rows, and actions and
813 /// dd::Internal entries have no values.
814 ///
815 /// A reader should ignore keys and value types it doesn't know. The `version` changes only for
816 /// a change that would break an existing reader.
817 ///
818 /// Since the values are your app's real data, you should only write them out from development
819 /// builds.
820 ///
821 /// @return Returns the JSON text (empty for an empty Context).
822 ///
823 /// @note This reads every bound instance (like Sync() does), so it isn't free. Call it on the
824 /// home thread.
825 ///
826 std::string schema() const {
827 ULString text = ulDOMDataContextGetSchema(handle_);
828 if (!text)
829 return std::string();
830 std::string result(ulStringGetData(text), ulStringGetLength(text));
831 ulDestroyString(text);
832 return result;
833 }
834
835 ///
836 /// Write the JSON from schema() to a file.
837 ///
838 /// With DumpAt::NextSync (the default), the library writes the file at the end of the next
839 /// Sync() on the home thread, so you can call this from any thread (eg, from a debug hotkey).
840 /// A write that fails then logs a warning with the path.
841 ///
842 /// With DumpAt::Now, the library writes the file before this returns. Call it on the home
843 /// thread.
844 ///
845 /// ```
846 /// if (debug_key_pressed)
847 /// ctx.DumpSchema("ui/schema.json");
848 /// ```
849 ///
850 /// @param utf8_path The file's path as a UTF-8 string (on Windows too). An existing file is
851 /// replaced.
852 ///
853 /// @param when When to write the file.
854 ///
855 /// @return Returns false if `utf8_path` is NULL or empty, or for an empty Context. With
856 /// DumpAt::Now, also returns false if the write fails.
857 ///
858 bool DumpSchema(const char* utf8_path, DumpAt when = DumpAt::NextSync) {
859 return ulDOMDataContextDumpSchema(handle_, utf8_path, static_cast<ULDOMDataDumpAt>(when));
860 }
861
862 // --- Interop with the C API (most embedders never touch raw handles) -------------------
863
864 ///
865 /// Wrap a C handle you own, taking ownership of it.
866 ///
867 /// @param handle A handle from the C API that you would otherwise destroy with
868 /// ulDestroyDOMDataContext() (NULL gives an empty Context).
869 ///
870 /// @return Returns a Context that destroys `handle` when it's done.
871 ///
872 static Context Adopt(ULDOMDataContext handle) { return Context(handle); }
873
874 ///
875 /// Wrap a C handle someone else owns, without owning the context.
876 ///
877 /// The result works like any Context, but destroying it never detaches the context or
878 /// destroys its bindings, and it doesn't keep the context attached once its owners are gone.
879 ///
880 /// @param handle The borrowed handle (NULL gives an empty Context).
881 ///
882 /// @return Returns a Context with its own (non-owning) reference, so you can keep it. Its
883 /// raw() is a different handle than `handle`.
884 ///
886 return Context(handle ? ulCreateDOMDataContextBorrowedRef(handle) : nullptr);
887 }
888
889 ///
890 /// Get the C handle, for passing to the `<Ultralight/CAPI/CAPI_DOMData.h>` functions.
891 ///
892 /// @return Returns the handle (NULL for an empty Context). This Context still owns it, so
893 /// don't destroy it.
894 ///
895 ULDOMDataContext raw() const { return handle_; }
896
897 ///
898 /// Give up ownership of the C handle and return it. This Context becomes empty.
899 ///
900 /// @return Returns the handle. You must call ulDestroyDOMDataContext() when finished.
901 ///
903 ULDOMDataContext handle = handle_;
904 handle_ = nullptr;
905 return handle;
906 }
907
908 private:
909 explicit Context(ULDOMDataContext handle) : handle_(handle) {}
910
911 template <typename T>
912 ULDOMDataBinding BindHandle(std::string_view name, void* holder_state,
913 ULDOMDataAcquireCallback acquire, ULDOMDataReleaseCallback release,
914 ULUserDataDestroyCallback destroy_holder) {
915 return ulDOMDataContextBind(handle_, std::string(name).c_str(),
916 reinterpret_cast<ULDOMDataType>(detail::Descriptors<T>()),
917 &detail::SyncWalkThunk<T>, holder_state, acquire, release,
918 destroy_holder);
919 }
920
921 bool PostActionByName(std::string_view name, const ULDOMDataPayloadEntry* entries,
922 size_t count) {
923 const size_t dot = name.find('.');
924 if (dot == std::string_view::npos || dot == 0 || dot + 1 == name.size())
925 return false;
926 const std::string binding_name(name.substr(0, dot));
927 const std::string action_name(name.substr(dot + 1));
928 return ulDOMDataContextPostActionByName(handle_, binding_name.c_str(), action_name.c_str(),
929 entries, count);
930 }
931
932 ULDOMDataContext handle_ = nullptr;
933};
934
935} // namespace data
936} // namespace dom
937
938///
939/// Short alias for the Data Bindings namespace (`dd::Context`, `dd::Binding`, ...).
940///
941namespace dd = dom::data;
942
943} // namespace ultralight
944
945#pragma pop_macro("None")
struct C_DOMDataContext * ULDOMDataContext
Opaque handle to a data-binding context.
Definition View.h:53
A list of origin patterns that controls which pages get an attached API.
Definition OriginRules.h:90
Web-page container rendered to an offscreen surface.
Definition View.h:483
virtual void DetachDOMDataContext(ULDOMDataContext context)=0
Detach a data-binding context from this View.
virtual bool AttachDOMDataContext(ULDOMDataContext context, unsigned flags=0, const char *const *origin_rules=nullptr, size_t num_origin_rules=0)=0
Attach a data-binding context to this View.
The root of a page's DOM tree in a View or frame.
Definition Document.h:106
A handle that keeps an instance bound to pages and registers its input handlers.
Definition Binding.h:229
Binding()=default
Create an empty Binding.
Context & operator=(Context &&other) noexcept
Move assignment (other becomes empty).
Definition Context.h:333
Binding< T > Bind(std::string_view name, T &instance)
Bind an instance of a described type under a binding name.
Definition Context.h:383
uint64_t generation() const
Get the generation of the last update Sync() sent to the pages.
Definition Context.h:773
static Context Adopt(ULDOMDataContext handle)
Wrap a C handle you own, taking ownership of it.
Definition Context.h:872
bool PostAction(std::string_view name)
Send an action to a bound instance's handler as if the page fired it.
Definition Context.h:585
bool DumpSchema(const char *utf8_path, DumpAt when=DumpAt::NextSync)
Write the JSON from schema() to a file.
Definition Context.h:858
Context & operator=(const Context &)=delete
Context & UndefineFormat(std::string_view name)
Remove a formatter.
Definition Context.h:761
bool PostAction(std::string_view name, const P &payload)
Send an action with a payload struct (see the payloadless overload).
Definition Context.h:608
static Context FromBorrowed(ULDOMDataContext handle)
Wrap a C handle someone else owns, without owning the context.
Definition Context.h:885
Binding< std::remove_cvref_t< T > > Bind(std::string_view name, T &&instance)
Bind an instance by value under a binding name.
Definition Context.h:430
bool AttachTo(View *view, const AttachOptions &options={})
Attach this Context to a View.
Definition Context.h:514
ULDOMDataContext LeakRef()
Give up ownership of the C handle and return it.
Definition Context.h:902
static Context Create()
Create a Context whose home thread is the calling thread.
Definition Context.h:290
Context(Context &&other) noexcept
Move constructor (other becomes empty).
Definition Context.h:322
void set_action_queue_capacity(uint32_t capacity)
Set how many actions can wait for the next Sync().
Definition Context.h:786
Binding< std::remove_cv_t< typename HolderTraits< std::remove_cvref_t< H > >::element_type > > Bind(std::string_view name, H &&holder)
Bind an instance through a smart pointer under a binding name.
Definition Context.h:464
void Sync()
Deliver input from the page to your handlers and send your changes to the pages.
Definition Context.h:555
ULDOMDataContext raw() const
Get the C handle, for passing to the <Ultralight/CAPI/CAPI_DOMData.h> functions.
Definition Context.h:895
bool PostAction(std::string_view name, const P &payload)
Send an action with a single-value payload (see the payloadless overload).
Definition Context.h:640
static Context CreateThreadSafe()
Create a Context that any thread can use.
Definition Context.h:312
void PostTask(F &&callback)
Run a callable on the Renderer's thread once the pages show your latest data.
Definition Context.h:690
~Context()
Destroy this handle.
Definition Context.h:353
Context(const Context &)=delete
uint32_t action_queue_capacity() const
Get how many actions can wait for the next Sync().
Definition Context.h:797
void DetachFrom(View *view)
Detach this Context from a View.
Definition Context.h:532
Context & DefineFormat(std::string_view name, Fn &&fn)
Define a formatter for {{path|name}} text.
Definition Context.h:734
Binding< T > Bind(std::string_view name, const T &instance)
Bind a const instance under a binding name.
Definition Context.h:406
std::string schema() const
Get the schema of every bound instance as JSON, with their current values.
Definition Context.h:826
Whether or not Context::Bind() can bind an instance through H.
Definition Holders.h:37
Whether or not T has a schema (ignoring const, volatile, and references), ie.
Definition TypeTraits.h:163
Whether or not P can be an action's payload without a payload struct (ignoring const,...
Definition ValueTraits.h:268
Data-binding API that connects native C++ data to HTML and CSS markup.
Definition ActionInfo.h:13
DumpAt
When Context::DumpSchema() writes its file.
Definition Context.h:120
@ Now
Right away.
Definition Context.h:122
@ NextSync
At the end of the next Context::Sync().
Definition Context.h:121
AttachFlags
The flags for Context::AttachTo() (the typed form of ULDOMDataContextAttachFlags).
Definition Context.h:133
@ None
No options.
Definition Context.h:134
Direct C++ access to modify page elements and handle events.
@ None
Add the listeners to the main frame only.
Definition Triggers.h:49
Root namespace for every public Ultralight type, function, and enumeration.
@ None
Definition Anchor.h:36
Options for Context::AttachTo().
Definition Context.h:143
OriginRules origin_rules
The origin rules for the pages that get the bindings.
Definition Context.h:153
AttachFlags flags
The attach flags (see AttachFlags).
Definition Context.h:147