docs
Loading...
Searching...
No Matches
Binding.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#include <Ultralight/CAPI/CAPI_DOMData.h>
10#include <Ultralight/dom/data/detail/Delivery.h>
11#include <Ultralight/dom/data/detail/Emission.h>
12#include <Ultralight/dom/data/detail/Receivers.h>
13
14#include <string>
15#include <string_view>
16#include <tuple>
17#include <type_traits>
18#include <utility>
19#include <vector>
20
21namespace ultralight {
22namespace dom {
23namespace data {
24
25/// \cond INTERNAL
26namespace detail {
27
28// The parameter list of a non-generic callable (a lambda or function object with one
29// non-template operator()). Generic lambdas and overloaded function objects have no single
30// signature, so their forms stay checked by invocability alone.
31template <typename Fn, typename = void>
32struct HandlerSignature {
33 static constexpr bool kKnown = false;
34};
35template <typename Fn>
36struct HandlerSignature<Fn, std::void_t<decltype(&Fn::operator())>> {
37 static constexpr bool kKnown = true;
38 using args = typename MemberCallableSig<decltype(&Fn::operator())>::args;
39};
40
41// Whether a one-parameter non-generic handler's parameter decays to one of `Forms`. The
42// delivery code tests invocability, which any implicit conversion passes (a `bool` handler
43// on a double field); this pins the parameter to the documented forms. Handlers of any other
44// arity, and generic ones, are left to the delivery code's own checks.
45template <typename Fn, typename... Forms>
46consteval bool HandlerParamIsOneOf() {
47 using Plain = std::remove_cvref_t<Fn>;
48 if constexpr (!HandlerSignature<Plain>::kKnown) {
49 return true;
50 } else {
51 using Args = typename HandlerSignature<Plain>::args;
52 if constexpr (std::tuple_size_v<Args> != 1)
53 return true;
54 else
55 return (std::is_same_v<std::decay_t<std::tuple_element_t<0, Args>>, Forms> || ...);
56 }
57}
58
59template <typename V, ValueKind kKind, typename Fn>
60consteval void CheckChangeHandlerForm() {
61 if constexpr (kKind == ValueKind::Color) {
62 static_assert(HandlerParamIsOneOf<Fn, Color>(),
63 "dom::data: an OnChange handler for a color field takes an "
64 "ultralight::Color");
65 } else if constexpr (kKind == ValueKind::String && std::is_enum_v<V>) {
66 static_assert(HandlerParamIsOneOf<Fn, V, Value>(),
67 "dom::data: an OnChange handler for an enum field takes the enum value "
68 "(or a dd::Value)");
69 } else if constexpr (kKind == ValueKind::String) {
70 static_assert(HandlerParamIsOneOf<Fn, V, ultralight::String, std::string_view, Value>(),
71 "dom::data: an OnChange handler for a string field takes the field's "
72 "value type, a const String&, or a dd::Value");
73 } else if constexpr (kKind == ValueKind::Bool) {
74 static_assert(HandlerParamIsOneOf<Fn, V, bool, Value>(),
75 "dom::data: an OnChange handler takes the field's value type (or its "
76 "slot type, or a dd::Value)");
77 } else {
78 static_assert(HandlerParamIsOneOf<Fn, V, int64_t, double, Value>(),
79 "dom::data: an OnChange handler takes the field's value type (or its "
80 "slot type, or a dd::Value)");
81 }
82}
83
84template <typename P, typename Fn>
85consteval void CheckRootActionHandlerForm() {
86 constexpr bool kOk = [] {
87 if constexpr (std::is_void_v<P>)
88 return HandlerParamIsOneOf<Fn, Value>();
89 else
90 return HandlerParamIsOneOf<Fn, std::decay_t<P>, Value>();
91 }();
92 static_assert(kOk,
93 "dom::data: an OnAction handler takes nothing, the action's declared "
94 "payload (its struct by const reference, or the scalar by value), a "
95 "dd::Value, or (dd::Value, dd::ActionInfo)");
96}
97
98template <typename R, ValueKind kKeyKind, typename Fn>
99consteval void CheckRowActionHandlerForm() {
100 using Key = std::conditional_t<kKeyKind == ValueKind::String, std::string_view, int64_t>;
101 static_assert(HandlerParamIsOneOf<Fn, R, Key, Value>(),
102 "dom::data: a row OnAction handler takes nothing, the row type by const "
103 "reference, the row key (int64_t, or std::string_view for string-keyed "
104 "lists), a dd::Value, or (dd::Value, dd::ActionInfo)");
105}
106
107} // namespace detail
108/// \endcond
109
110///
111/// A handle that keeps an instance bound to pages and registers its input handlers.
112///
113/// A Binding keeps a native instance bound under the name page markup uses for as long as you keep
114/// the handle. It's where you register the handlers for that instance's edits and actions from the
115/// page.
116///
117/// Store the handle alongside your data to keep the binding active for the lifetime of your
118/// component.
119///
120/// This HUD component stores its binding as a member variable and routes an action to a member
121/// function:
122///
123/// ```
124/// class Hud {
125/// public:
126/// explicit Hud(dd::Context& ctx)
127/// : binding_(ctx.Bind("player", player_)
128/// .OnAction<"usePotion">(&Player::UsePotion)) {}
129///
130/// private:
131/// Player player_;
132/// dd::Binding<Player> binding_; // unbinds when the Hud is destroyed
133/// };
134/// ```
135///
136/// ## Handling Input
137///
138/// Call OnChange() to handle edits to an Editable field from a `ul-value` form control on the page.
139/// The handler receives the new value, but the library never writes directly to your data. Store
140/// the value in your instance to accept the edit, or leave the field unchanged to decline it and
141/// let the control snap back at the end of the sync.
142///
143/// This handler accepts volume edits by writing the new value back to the settings struct:
144///
145/// ```
146/// dd::Binding settings_binding = ctx.Bind("settings", settings);
147/// settings_binding.OnChange<"volume">([&](double volume) {
148/// settings.volume = volume; // storing the value accepts the edit
149/// });
150/// ```
151///
152/// Call OnAction() to receive actions triggered by `ul-on` markup on the page or posted through
153/// PostAction(). A dotted name like `"items.drop"` routes actions from list rows. Handlers run
154/// during Context::Sync() on the context's home thread, delivering all value edits before running
155/// any actions. Registering a handler for a field or action that already has one replaces the
156/// previous handler.
157///
158/// ## Handler Forms
159///
160/// Both OnChange() and OnAction() route events to any of these targets:
161///
162/// - **A callable receives the event directly.** Pass a lambda or function object that accepts the
163/// field value, the action payload, or no arguments.
164/// - **A borrowed receiver calls a member function on an existing object.** The library doesn't
165/// copy the object, so you must keep it alive while the binding exists.
166/// - **A smart pointer manages receiver lifetime.** An owning pointer like `std::shared_ptr` keeps
167/// the target alive, while a `std::weak_ptr` skips deliveries once its object is gone.
168/// - **A member function of the bound type runs on the bound instance.** It does nothing if you
169/// bound a `const` instance.
170///
171/// ## Posting Actions from Native Code
172///
173/// Call PostAction() to send an action from native code as if page markup triggered it. The action
174/// enters the context's action queue and reaches its handler during the next Context::Sync().
175/// PostAction() is safe to call from any thread, which lets background threads hand work to the
176/// thread that owns the data. It returns `false` if the queue is full or if the binding stopped
177/// working.
178///
179/// This queues an action for delivery at the next sync:
180///
181/// ```
182/// // From any thread (eg, the input thread's potion hotkey):
183/// binding.PostAction<"usePotion">();
184/// ```
185///
186/// ## Binding Lifetime
187///
188/// Destroying a Binding unbinds the instance and releases its handlers.
189///
190/// The unbind timing depends on which thread destroys the handle:
191///
192/// - **On the home thread, unbinding happens immediately.** The context stops reading the instance
193/// right away.
194/// - **From any other thread, unbinding takes effect at the start of the next Context::Sync().**
195/// The binding receives no further input during that synchronization cycle.
196///
197/// Binding the same name again replaces the earlier binding, and destroying the Context ends all
198/// its bindings. A replaced binding or one whose Context was destroyed remains safe to hold, but
199/// its handlers are dropped and PostAction() returns `false`.
200///
201/// Inspect the state of a handle using these methods:
202///
203/// - **IsAlive() checks whether the binding is active.** It returns `false` when the binding was
204/// replaced or its Context was destroyed, even though the `bool` test stays `true`.
205/// - **IsEmpty() checks whether the handle holds nothing.** It returns `true` after a move or when
206/// Context::Bind() fails.
207///
208/// This checks whether a binding remains active after another component bound the same name:
209///
210/// ```
211/// dd::Binding hud = ctx.Bind("player", player);
212/// dd::Binding menu = ctx.Bind("player", player); // replaces hud's binding
213///
214/// if (!hud.IsAlive())
215/// Log("hud's binding was replaced");
216/// ```
217///
218/// @note Actions are requests that can arrive after the state that prompted them changed, so your
219/// handler should verify that prerequisites still hold before applying the request.
220///
221/// @warning Calling ulDOMDataBindingSetChangeCallback() or ulDOMDataBindingSetActionCallback() on
222/// raw() replaces this Binding's typed handlers, and registering typed handlers replaces
223/// any C callbacks.
224///
225/// @see dom::data::Context::Bind(), dom::data::Context::PostAction(), dom::data::ActionInfo,
226/// dom::data::Value, dom::data::Editable
227///
228template <typename T>
229class [[nodiscard]] Binding {
230 public:
231 ///
232 /// The bound type.
233 ///
234 using described_type = T;
235
236 ///
237 /// Create an empty Binding.
238 ///
239 Binding() = default;
240
241 Binding(const Binding&) = delete;
242 Binding& operator=(const Binding&) = delete;
243
244 ///
245 /// Move constructor (`other` becomes empty).
246 ///
247 /// @param other The Binding to move from.
248 ///
249 Binding(Binding&& other) noexcept
250 : handle_(other.handle_), handlers_(other.handlers_),
251 access_(std::move(other.access_)) {
252 other.handle_ = nullptr;
253 other.handlers_ = nullptr;
254 other.access_ = nullptr;
255 }
256
257 ///
258 /// Move assignment (releases the binding this one held, then takes over `other`'s).
259 ///
260 /// @param other The Binding to move from.
261 ///
262 /// @return Returns this Binding.
263 ///
264 Binding& operator=(Binding&& other) noexcept {
265 if (this != &other) {
266 ulDestroyDOMDataBinding(handle_);
267 handle_ = other.handle_;
268 handlers_ = other.handlers_;
269 access_ = std::move(other.access_);
270 other.handle_ = nullptr;
271 other.handlers_ = nullptr;
272 other.access_ = nullptr;
273 }
274 return *this;
275 }
276
277 ///
278 /// Destroy this Binding (see Binding Lifetime in the class description).
279 ///
280 ~Binding() { ulDestroyDOMDataBinding(handle_); }
281
282 ///
283 /// Whether or not this Binding holds a binding (false for an empty Binding, eg, after a move
284 /// or a failed Context::Bind()).
285 ///
286 /// @note It stays true after the binding name is bound again (see Binding Lifetime in the
287 /// class description). Use IsAlive() to check whether the binding still works.
288 ///
289 explicit operator bool() const { return handle_ != nullptr; }
290
291 ///
292 /// Whether or not this Binding holds nothing (eg, after a move or a failed Context::Bind()).
293 ///
294 /// @return Returns true for an empty Binding.
295 ///
296 /// @note Safe to call from any thread.
297 ///
298 bool IsEmpty() const { return handle_ == nullptr; }
299
300 ///
301 /// Whether or not this Binding still works.
302 ///
303 /// @return Returns false for an empty Binding, or one that stopped working (its binding name was
304 /// bound again, or its Context was destroyed).
305 ///
306 /// @note Safe to call from any thread.
307 ///
308 bool IsAlive() const { return ulDOMDataBindingIsAlive(handle_); }
309
310 ///
311 /// Register the handler for edits to an Editable field (replacing any earlier one).
312 ///
313 /// The handler runs during Context::Sync() after the user edits a `ul-value` control bound to
314 /// the field. It runs at most once per field per Sync (with the latest value) and before any
315 /// action. Store the value in your instance to accept it:
316 ///
317 /// ```
318 /// b.OnChange<"volume">([&](float v) { settings.volume = v; audio.SetVolume(v); });
319 /// ```
320 ///
321 /// The handler takes the value as one of these:
322 ///
323 /// - **The field's type** (or `int64_t` or `double` for a number field).
324 /// - **`const String&`** for a string field, or `std::string_view` for a std::string field
325 /// (valid only during the call).
326 /// - **ultralight::Color** for a color field (its only form).
327 /// - **dd::Value** for any field except a color.
328 ///
329 /// A parameter of another type doesn't compile, even one the value converts to (eg, `bool`
330 /// for a number field).
331 ///
332 /// If the field has a validator (see Validate()), the handler gets the value it accepted.
333 ///
334 /// @param fn The handler.
335 ///
336 /// @return Returns this Binding (for chaining).
337 ///
338 /// @note The field name and the handler are checked at compile time. The field must be an
339 /// Editable bool, number, string, enum, or color field of the bound type itself (not a
340 /// row or a nested object).
341 ///
342 /// @warning For a field whose type has your own ValueTraits specialization, a handler that takes
343 /// that type still compiles when the type can be constructed from the kind's C++ type
344 /// (eg, `double`). The handler then receives the raw value converted by that
345 /// constructor rather than your ValueTraits specialization. Take the kind's C++ type or
346 /// a dd::Value instead (see ValueTraits).
347 ///
348 template <ultralight::detail::FixedString Field, typename Fn>
349 requires(!std::is_member_pointer_v<std::decay_t<Fn>>)
350 Binding& OnChange(Fn&& fn) & {
351 constexpr std::string_view path = Field.view();
352 static_assert(path.find('.') == std::string_view::npos,
353 "dom::data: nested and row change delivery is not yet available; "
354 "OnChange registers root-level Editable fields");
355 if constexpr (path.find('.') == std::string_view::npos) {
356 constexpr size_t I = detail::SchemaIndexOf<T>(path);
357 static_assert(I != detail::kNpos,
358 "dom::data: the bound type's schema has no entry with this name");
359 if constexpr (I != detail::kNpos) {
360 using SchemaType = std::remove_cvref_t<decltype(TypeTraits<T>::schema)>;
361 using Tok = std::tuple_element_t<I, SchemaType>;
362 static_assert(detail::IsFieldToken<Tok>::value
363 && detail::EntryInfo<Tok>::kClass == detail::EntryClass::kLeaf,
364 "dom::data: OnChange registers leaf Field entries");
365 if constexpr (detail::IsFieldToken<Tok>::value
366 && detail::EntryInfo<Tok>::kClass == detail::EntryClass::kLeaf) {
367 static_assert(
368 (TypeTraits<T>::schema.template get<I>().flags & kEntryFlags_Editable) != 0,
369 "dom::data: OnChange registers Editable fields (changes are never staged "
370 "for a field the UI cannot edit)");
371 detail::CheckChangeHandlerForm<std::remove_cvref_t<typename Tok::value_type>,
372 detail::EntryInfo<Tok>::kKind, Fn>();
373 detail::WithBindingOwnerTurn(handle_, [&] {
374 if (auto* table = EnsureHandlers()) {
375 table->changes[static_cast<uint32_t>(I)]
376 = detail::MakeChangeHandler<T, I>(std::forward<Fn>(fn));
377 }
378 });
379 }
380 }
381 }
382 return *this;
383 }
384
385 ///
386 /// Register a member function of `receiver` as the handler for edits to an Editable field.
387 ///
388 /// Works like the callable form of OnChange().
389 ///
390 /// @param receiver The object to call `method` on. It isn't copied, so keep it alive while
391 /// the handler can run.
392 ///
393 /// @param method The member function to call.
394 ///
395 /// @return Returns this Binding (for chaining).
396 ///
397 template <ultralight::detail::FixedString Field, typename C, typename M>
398 requires(std::is_member_function_pointer_v<M> && !LockableHolder<C>)
399 Binding& OnChange(C& receiver, M method) & {
400 return OnChange<Field>(detail::WrapBorrowedMember(receiver, method));
401 }
402
403 ///
404 /// Register a member function as the handler for edits to an Editable field, called through
405 /// a smart pointer.
406 ///
407 /// Works like the callable form of OnChange().
408 ///
409 /// @param holder The smart pointer to the object (copied). A std::shared_ptr keeps its object
410 /// alive while the handler is registered. A std::weak_ptr skips deliveries once
411 /// its object is gone.
412 ///
413 /// @param method The member function to call.
414 ///
415 /// @return Returns this Binding (for chaining).
416 ///
417 template <ultralight::detail::FixedString Field, typename H, typename M>
418 requires(LockableHolder<H> && std::is_member_function_pointer_v<M>)
419 Binding& OnChange(H holder, M method) & {
420 return OnChange<Field>(detail::WrapHolderMember(std::move(holder), method));
421 }
422
423 ///
424 /// Register a member function of the bound type as the handler for edits to an Editable
425 /// field, called on the bound instance.
426 ///
427 /// Works like the callable form of OnChange(). The handler runs only when Context::Bind() got
428 /// an instance it can change (see Handler Forms in the class description).
429 ///
430 /// @param method The member function to call.
431 ///
432 /// @return Returns this Binding (for chaining).
433 ///
434 template <ultralight::detail::FixedString Field, typename M>
435 requires std::is_member_function_pointer_v<M>
436 Binding& OnChange(M method) & {
437 return OnChange<Field>(detail::WrapImplicitMember<T>(access_, method));
438 }
439
440 ///
441 /// Register the handler for edits to an Editable field on a Binding you just created.
442 ///
443 /// This lets a chain start at Context::Bind() and end in the variable you keep. It takes the
444 /// same arguments as the other forms of OnChange().
445 ///
446 /// @param args The handler (a callable, an object or smart pointer and a member function, or
447 /// a member function of the bound type).
448 ///
449 /// @return Returns this Binding by value.
450 ///
451 template <ultralight::detail::FixedString Field, typename... A>
452 [[nodiscard]] Binding OnChange(A&&... args) && {
453 this->template OnChange<Field>(std::forward<A>(args)...);
454 return std::move(*this);
455 }
456
457 ///
458 /// Register the handler for an action (replacing any earlier one).
459 ///
460 /// The handler runs during Context::Sync() after all changes, in the order the actions fired.
461 /// Its parameter list decides what it receives:
462 ///
463 /// ```
464 /// b.OnAction<"voteSkip">([&] { match.VoteSkip(); }); // nothing
465 /// b.OnAction<"moveItem">([&](const MoveItem& move) { ... }); // the payload
466 /// b.OnAction<"roster.focusPlayer">([&](const ScoreRow& row) { ... }); // the row
467 /// b.OnAction<"roster.focusPlayer">([&](int64_t key) { ... }); // the row key
468 /// b.OnAction<"save">([&](dd::Value payload, dd::ActionInfo info) { ... });
469 /// ```
470 ///
471 /// - **No parameters** works for every action (any payload is ignored).
472 /// - **The declared payload** (a struct by const reference, or a scalar like `double` by value).
473 /// An action whose payload doesn't match is dropped. Actions from `ul-on:*` markup have no
474 /// payload, so this form never receives them.
475 /// - **dd::Value** holds the payload. You can add a dd::ActionInfo parameter after it (the
476 /// action name and row key).
477 /// - **The row type** (by const reference, for a row action) holds the row's values as of the
478 /// last Context::Sync(). The action is dropped if no row has that key anymore (or, in a list
479 /// without keys, that position).
480 /// - **The row key** (for a row action) is an `int64_t` (in a list without keys, the row's
481 /// position when the page fired the action) or a `std::string_view` for string keys (valid
482 /// only during the call).
483 ///
484 /// A row action on a list without keys addresses the row by its position when the page fired
485 /// it, so rows added or removed before the next Sync() make it reach another row (the page
486 /// warns when it compiles one). Declare a key for a list whose rows can move.
487 ///
488 /// The row type form rebuilds the row, so the row type must be default-constructible and every
489 /// field in its schema must read a data member (a member pointer or a Reflect() field) of a
490 /// bool, number, enum (only when ULTRALIGHT_REFLECTION is 1), StyleValue, or Color type, or of a
491 /// string type constructible from a `const char*` and a length (eg, `std::string`). A row with
492 /// a list, a nested object, a Var(), a getter or lambda accessor, or a `std::string_view` member
493 /// doesn't compile in this form, so take the row key instead.
494 ///
495 /// A dotted name (`list.action`) addresses an action of a list's row type. Only one level is
496 /// supported, so actions of a nested object or of a list inside a row can't be registered. The
497 /// name and the handler are checked at compile time, and a parameter must have one of these
498 /// types exactly (eg, a `bool` parameter for a `double` payload doesn't compile).
499 ///
500 /// @param fn The handler.
501 ///
502 /// @return Returns this Binding (for chaining).
503 ///
504 template <ultralight::detail::FixedString Name, typename Fn>
505 requires(!std::is_member_pointer_v<std::decay_t<Fn>>)
506 Binding& OnAction(Fn&& fn) & {
507 constexpr std::string_view path = Name.view();
508 constexpr size_t dot = path.find('.');
509 if constexpr (dot == std::string_view::npos) {
510 constexpr size_t I = detail::SchemaIndexOf<T>(path);
511 static_assert(I != detail::kNpos,
512 "dom::data: the bound type's schema has no entry with this name");
513 if constexpr (I != detail::kNpos) {
514 using SchemaType = std::remove_cvref_t<decltype(TypeTraits<T>::schema)>;
515 using Tok = std::tuple_element_t<I, SchemaType>;
516 static_assert(detail::IsActionToken<Tok>::value,
517 "dom::data: OnAction registers Action entries (the named entry is "
518 "not an Action)");
519 if constexpr (detail::IsActionToken<Tok>::value) {
520 detail::CheckRootActionHandlerForm<typename Tok::payload_type, Fn>();
521 detail::WithBindingOwnerTurn(handle_, [&] {
522 if (auto* table = EnsureHandlers()) {
523 table->actions[std::make_pair(detail::kRootActionSlot,
524 static_cast<uint32_t>(I))]
525 = detail::MakeRootActionHandler<T, I>(std::forward<Fn>(fn));
526 }
527 });
528 }
529 }
530 } else {
531 static_assert(path.find('.', dot + 1) == std::string_view::npos,
532 "dom::data: one dotted level is supported (list.action); deeper "
533 "paths are not yet deliverable");
534 constexpr size_t L = detail::SchemaIndexOf<T>(path.substr(0, dot));
535 static_assert(L != detail::kNpos,
536 "dom::data: the bound type's schema has no entry named by the first "
537 "path segment");
538 if constexpr (L != detail::kNpos && path.find('.', dot + 1) == std::string_view::npos) {
539 using SchemaType = std::remove_cvref_t<decltype(TypeTraits<T>::schema)>;
540 using LTok = std::tuple_element_t<L, SchemaType>;
541 static_assert(detail::EntryInfo<LTok>::kClass != detail::EntryClass::kObject
542 && detail::EntryInfo<LTok>::kClass
543 != detail::EntryClass::kNullableObject,
544 "dom::data: actions behind object fields are not yet deliverable");
545 static_assert(detail::EntryInfo<LTok>::kClass == detail::EntryClass::kList,
546 "dom::data: OnAction's dotted form addresses an action of a list's "
547 "row type");
548 if constexpr (detail::EntryInfo<LTok>::kClass == detail::EntryClass::kList) {
549 using R = typename detail::EntryInfo<LTok>::child_type;
550 constexpr size_t A = detail::SchemaIndexOf<R>(path.substr(dot + 1));
551 static_assert(A != detail::kNpos,
552 "dom::data: the row type's schema has no entry named by the "
553 "second path segment");
554 if constexpr (A != detail::kNpos) {
555 using RTok = std::tuple_element_t<
556 A, std::remove_cvref_t<decltype(TypeTraits<R>::schema)>>;
557 static_assert(detail::IsActionToken<RTok>::value,
558 "dom::data: OnAction registers Action entries (the named row "
559 "entry is not an Action)");
560 if constexpr (detail::IsActionToken<RTok>::value) {
561 constexpr ValueKind kKeyKind = [] {
562 if constexpr (detail::IsListToken<LTok>::value)
563 return detail::EntryInfo<LTok>::KeyKind();
564 else
565 return ValueKind::Int64; // A Field-declared list is matched by position.
566 }();
567 detail::CheckRowActionHandlerForm<R, kKeyKind, Fn>();
568 detail::WithBindingOwnerTurn(handle_, [&] {
569 if (auto* table = EnsureHandlers()) {
570 table->actions[std::make_pair(static_cast<uint32_t>(L),
571 static_cast<uint32_t>(A))]
572 = detail::MakeRowActionHandler<T, L, A>(std::forward<Fn>(fn));
573 }
574 });
575 }
576 }
577 }
578 }
579 }
580 return *this;
581 }
582
583 ///
584 /// Register a member function of `receiver` as the handler for an action.
585 ///
586 /// Works like the callable form of OnAction().
587 ///
588 /// @param receiver The object to call `method` on. It isn't copied, so keep it alive while
589 /// the handler can run.
590 ///
591 /// @param method The member function to call.
592 ///
593 /// @return Returns this Binding (for chaining).
594 ///
595 template <ultralight::detail::FixedString Name, typename C, typename M>
596 requires(std::is_member_function_pointer_v<M> && !LockableHolder<C>)
597 Binding& OnAction(C& receiver, M method) & {
598 return OnAction<Name>(detail::WrapBorrowedMember(receiver, method));
599 }
600
601 ///
602 /// Register a member function as the handler for an action, called through a smart pointer.
603 ///
604 /// Works like the callable form of OnAction().
605 ///
606 /// @param holder The smart pointer to the object (copied). A std::shared_ptr keeps its object
607 /// alive while the handler is registered. A std::weak_ptr skips deliveries once
608 /// its object is gone.
609 ///
610 /// @param method The member function to call.
611 ///
612 /// @return Returns this Binding (for chaining).
613 ///
614 template <ultralight::detail::FixedString Name, typename H, typename M>
615 requires(LockableHolder<H> && std::is_member_function_pointer_v<M>)
616 Binding& OnAction(H holder, M method) & {
617 return OnAction<Name>(detail::WrapHolderMember(std::move(holder), method));
618 }
619
620 ///
621 /// Register a member function of the bound type as the handler for an action, called on the
622 /// bound instance.
623 ///
624 /// Works like the callable form of OnAction(). The handler runs only when Context::Bind() got
625 /// an instance it can change (see Handler Forms in the class description).
626 ///
627 /// @param method The member function to call.
628 ///
629 /// @return Returns this Binding (for chaining).
630 ///
631 template <ultralight::detail::FixedString Name, typename M>
632 requires std::is_member_function_pointer_v<M>
633 Binding& OnAction(M method) & {
634 return OnAction<Name>(detail::WrapImplicitMember<T>(access_, method));
635 }
636
637 ///
638 /// Register the handler for an action on a Binding you just created.
639 ///
640 /// This lets a chain start at Context::Bind() and end in the variable you keep. It takes the
641 /// same arguments as the other forms of OnAction().
642 ///
643 /// @param args The handler (a callable, an object or smart pointer and a member function, or
644 /// a member function of the bound type).
645 ///
646 /// @return Returns this Binding by value.
647 ///
648 template <ultralight::detail::FixedString Name, typename... A>
649 [[nodiscard]] Binding OnAction(A&&... args) && {
650 this->template OnAction<Name>(std::forward<A>(args)...);
651 return std::move(*this);
652 }
653
654 ///
655 /// Remove the handler for edits to an Editable field (if there is one).
656 ///
657 /// The field name is checked at compile time like OnChange().
658 ///
659 /// @return Returns this Binding (for chaining).
660 ///
661 template <ultralight::detail::FixedString Field>
663 constexpr std::string_view path = Field.view();
664 static_assert(path.find('.') == std::string_view::npos,
665 "dom::data: nested and row change delivery is not yet available; "
666 "change handlers register root-level Editable fields");
667 if constexpr (path.find('.') == std::string_view::npos) {
668 constexpr size_t I = detail::SchemaIndexOf<T>(path);
669 static_assert(I != detail::kNpos,
670 "dom::data: the bound type's schema has no entry with this name");
671 if constexpr (I != detail::kNpos) {
672 using SchemaType = std::remove_cvref_t<decltype(TypeTraits<T>::schema)>;
673 using Tok = std::tuple_element_t<I, SchemaType>;
674 static_assert(detail::IsFieldToken<Tok>::value
675 && detail::EntryInfo<Tok>::kClass == detail::EntryClass::kLeaf,
676 "dom::data: change handlers address leaf Field entries");
677 if constexpr (detail::IsFieldToken<Tok>::value
678 && detail::EntryInfo<Tok>::kClass == detail::EntryClass::kLeaf) {
679 static_assert(
680 (TypeTraits<T>::schema.template get<I>().flags & kEntryFlags_Editable) != 0,
681 "dom::data: change handlers address Editable fields (changes are never "
682 "staged for a field the UI cannot edit)");
683 detail::WithBindingOwnerTurn(handle_, [&] {
684 if (auto* table = FindHandlers())
685 table->changes.erase(static_cast<uint32_t>(I));
686 });
687 }
688 }
689 }
690 return *this;
691 }
692
693 ///
694 /// Remove the handler for edits to an Editable field on a Binding you just created.
695 ///
696 /// @return Returns this Binding by value.
697 ///
698 template <ultralight::detail::FixedString Field>
699 [[nodiscard]] Binding RemoveChangeHandler() && {
700 this->template RemoveChangeHandler<Field>();
701 return std::move(*this);
702 }
703
704 ///
705 /// Remove the handler for an action (if there is one).
706 ///
707 /// The action name is checked at compile time like OnAction() (a dotted `list.action` name
708 /// works too).
709 ///
710 /// @return Returns this Binding (for chaining).
711 ///
712 template <ultralight::detail::FixedString Name>
714 constexpr std::string_view path = Name.view();
715 constexpr size_t dot = path.find('.');
716 if constexpr (dot == std::string_view::npos) {
717 constexpr size_t I = detail::SchemaIndexOf<T>(path);
718 static_assert(I != detail::kNpos,
719 "dom::data: the bound type's schema has no entry with this name");
720 if constexpr (I != detail::kNpos) {
721 using SchemaType = std::remove_cvref_t<decltype(TypeTraits<T>::schema)>;
722 using Tok = std::tuple_element_t<I, SchemaType>;
723 static_assert(detail::IsActionToken<Tok>::value,
724 "dom::data: action handlers address Action entries (the named "
725 "entry is not an Action)");
726 if constexpr (detail::IsActionToken<Tok>::value) {
727 detail::WithBindingOwnerTurn(handle_, [&] {
728 if (auto* table = FindHandlers()) {
729 table->actions.erase(
730 std::make_pair(detail::kRootActionSlot, static_cast<uint32_t>(I)));
731 }
732 });
733 }
734 }
735 } else {
736 static_assert(path.find('.', dot + 1) == std::string_view::npos,
737 "dom::data: one dotted level is supported (list.action); deeper "
738 "paths are not yet deliverable");
739 constexpr size_t L = detail::SchemaIndexOf<T>(path.substr(0, dot));
740 static_assert(L != detail::kNpos,
741 "dom::data: the bound type's schema has no entry named by the first "
742 "path segment");
743 if constexpr (L != detail::kNpos && path.find('.', dot + 1) == std::string_view::npos) {
744 using SchemaType = std::remove_cvref_t<decltype(TypeTraits<T>::schema)>;
745 using LTok = std::tuple_element_t<L, SchemaType>;
746 static_assert(detail::EntryInfo<LTok>::kClass == detail::EntryClass::kList,
747 "dom::data: the dotted form addresses an action of a list's row "
748 "type");
749 if constexpr (detail::EntryInfo<LTok>::kClass == detail::EntryClass::kList) {
750 using R = typename detail::EntryInfo<LTok>::child_type;
751 constexpr size_t A = detail::SchemaIndexOf<R>(path.substr(dot + 1));
752 static_assert(A != detail::kNpos,
753 "dom::data: the row type's schema has no entry named by the "
754 "second path segment");
755 if constexpr (A != detail::kNpos) {
756 using RTok = std::tuple_element_t<
757 A, std::remove_cvref_t<decltype(TypeTraits<R>::schema)>>;
758 static_assert(detail::IsActionToken<RTok>::value,
759 "dom::data: action handlers address Action entries (the named "
760 "row entry is not an Action)");
761 if constexpr (detail::IsActionToken<RTok>::value) {
762 detail::WithBindingOwnerTurn(handle_, [&] {
763 if (auto* table = FindHandlers()) {
764 table->actions.erase(
765 std::make_pair(static_cast<uint32_t>(L), static_cast<uint32_t>(A)));
766 }
767 });
768 }
769 }
770 }
771 }
772 }
773 return *this;
774 }
775
776 ///
777 /// Remove the handler for an action on a Binding you just created.
778 ///
779 /// @return Returns this Binding by value.
780 ///
781 template <ultralight::detail::FixedString Name>
782 [[nodiscard]] Binding RemoveActionHandler() && {
783 this->template RemoveActionHandler<Name>();
784 return std::move(*this);
785 }
786
787 ///
788 /// Send one of this binding's actions that has no payload, as if the page fired it.
789 ///
790 /// ```
791 /// b.PostAction<"voteSkip">();
792 /// ```
793 ///
794 /// The action reaches its handler at the next Context::Sync(), like an action from the page.
795 /// The name is checked at compile time and must be an action of the bound type itself (not a
796 /// `list.action` name).
797 ///
798 /// @return Returns true if the action was queued. Returns false for an empty Binding, one that
799 /// stopped working (see IsAlive()), or a full action queue.
800 ///
801 /// @note Safe to call from any thread.
802 ///
803 /// @see Context::PostAction()
804 ///
805 template <ultralight::detail::FixedString Name>
806 bool PostAction() {
807 constexpr std::string_view path = Name.view();
808 static_assert(path.find('.') == std::string_view::npos,
809 "dom::data: PostAction addresses root actions (row-scoped actions fire "
810 "from bound markup with their row's identity)");
811 if constexpr (path.find('.') == std::string_view::npos) {
812 constexpr size_t I = detail::SchemaIndexOf<T>(path);
813 static_assert(I != detail::kNpos,
814 "dom::data: the bound type's schema has no entry with this name");
815 if constexpr (I != detail::kNpos) {
816 using SchemaType = std::remove_cvref_t<decltype(TypeTraits<T>::schema)>;
817 using Tok = std::tuple_element_t<I, SchemaType>;
818 static_assert(detail::IsActionToken<Tok>::value,
819 "dom::data: PostAction emits Action entries (the named entry is "
820 "not an Action)");
821 if constexpr (detail::IsActionToken<Tok>::value) {
822 static_assert(std::is_void_v<typename Tok::payload_type>,
823 "dom::data: this action declares a payload; pass it as "
824 "PostAction's argument");
825 if constexpr (std::is_void_v<typename Tok::payload_type>) {
826 return detail::PostBindingAction(handle_, static_cast<unsigned int>(I));
827 }
828 }
829 }
830 }
831 return false;
832 }
833
834 ///
835 /// Send one of this binding's actions with its payload, as if the page fired it.
836 ///
837 /// ```
838 /// b.PostAction<"moveItem">({ .from = a, .to = b }); // dd::Action<MoveItem>
839 /// b.PostAction<"seek">(0.5f); // dd::Action<double>
840 /// ```
841 ///
842 /// Works like the form without a payload.
843 ///
844 /// @param payload The payload (of the action's declared type).
845 ///
846 /// @return Returns true if the action was queued. Returns false for an empty Binding, one that
847 /// stopped working (see IsAlive()), or a full action queue.
848 ///
849 /// @note Safe to call from any thread.
850 ///
851 template <ultralight::detail::FixedString Name>
852 bool PostAction(const detail::ActionPayloadArg<T, Name>& payload) {
853 constexpr std::string_view path = Name.view();
854 static_assert(path.find('.') == std::string_view::npos,
855 "dom::data: PostAction addresses root actions (row-scoped actions fire "
856 "from bound markup with their row's identity)");
857 if constexpr (path.find('.') == std::string_view::npos) {
858 constexpr size_t I = detail::SchemaIndexOf<T>(path);
859 static_assert(I != detail::kNpos,
860 "dom::data: the bound type's schema has no entry with this name");
861 if constexpr (I != detail::kNpos) {
862 using SchemaType = std::remove_cvref_t<decltype(TypeTraits<T>::schema)>;
863 using Tok = std::tuple_element_t<I, SchemaType>;
864 static_assert(detail::IsActionToken<Tok>::value,
865 "dom::data: PostAction emits Action entries (the named entry is "
866 "not an Action)");
867 if constexpr (detail::IsActionToken<Tok>::value) {
868 static_assert(!std::is_void_v<typename Tok::payload_type>,
869 "dom::data: this action declares no payload; call PostAction "
870 "with no argument");
871 if constexpr (!std::is_void_v<typename Tok::payload_type>) {
873 static_assert(
874 detail::PayloadMarshalable<typename Tok::payload_type>(),
875 "dom::data: a payload type carries plain data fields only (readable "
876 "leaf members of boolean, integral, floating-point, string, or "
877 "reflected enum type)");
878 std::vector<std::string> strings;
879 std::vector<ULDOMDataPayloadEntry> entries;
880 detail::MarshalPayload(payload, strings, entries);
881 return detail::PostBindingAction(handle_, static_cast<unsigned int>(I),
882 entries.empty() ? nullptr : entries.data(),
883 entries.size());
884 } else {
885 std::string backing;
886 ULDOMDataPayloadEntry entry {};
887 detail::MarshalScalarPayload(payload, backing, entry);
888 return detail::PostBindingAction(handle_, static_cast<unsigned int>(I), &entry,
889 1);
890 }
891 }
892 }
893 }
894 }
895 return false;
896 }
897
898 ///
899 /// Get the binding name.
900 ///
901 /// @return Returns the binding name, or an empty string for an empty Binding or one whose
902 /// name was bound again.
903 ///
904 /// @note Call this on the home thread (unlike PostAction(), which is safe from any thread).
905 ///
906 std::string name() const {
907 if (!handle_)
908 return std::string();
909 ULString text = ulDOMDataBindingGetName(handle_);
910 if (!text)
911 return std::string();
912 std::string result(ulStringGetData(text), ulStringGetLength(text));
913 ulDestroyString(text);
914 return result;
915 }
916
917 // --- Interop with the C API (most embedders never touch raw handles) -------------------
918
919 ///
920 /// Wrap a C handle you own, taking ownership of it.
921 ///
922 /// @param handle A handle from the C API that you would otherwise destroy with
923 /// ulDestroyDOMDataBinding() (NULL gives an empty Binding).
924 ///
925 /// @return Returns a Binding that releases `handle` when it's done.
926 ///
927 static Binding Adopt(ULDOMDataBinding handle) { return Binding(handle); }
928
929 ///
930 /// Wrap a C handle someone else owns (eg, another Binding's raw()), adding a reference.
931 ///
932 /// @param handle The borrowed handle (NULL gives an empty Binding).
933 ///
934 /// @return Returns a Binding with its own reference.
935 ///
936 static Binding FromBorrowed(ULDOMDataBinding handle) {
937 return Binding(handle ? ulCreateDOMDataBindingRef(handle) : nullptr);
938 }
939
940 ///
941 /// Get the C handle, for passing to the `<Ultralight/CAPI/CAPI_DOMData.h>` functions.
942 ///
943 /// @return Returns the handle (NULL for an empty Binding). This Binding still owns it, so
944 /// don't destroy it.
945 ///
946 ULDOMDataBinding raw() const { return handle_; }
947
948 ///
949 /// Give up ownership of the C handle and return it. This Binding becomes empty.
950 ///
951 /// @return Returns the handle. You must call ulDestroyDOMDataBinding() when finished.
952 ///
953 ULDOMDataBinding LeakRef() {
954 ULDOMDataBinding handle = handle_;
955 handle_ = nullptr;
956 handlers_ = nullptr;
957 access_ = nullptr;
958 return handle;
959 }
960
961 private:
962 friend class Context;
963 explicit Binding(ULDOMDataBinding handle) : handle_(handle) {}
964
965 // Find or install the type-erased handler table behind both C callback slots. Returns
966 // null for an empty or stale guard (its name was rebound, or the binding is gone), so
967 // registration degrades to a benign no-op and a freed table is never touched.
968 //
969 // The table's identity lives on the BINDING (its installed change slot), never in this
970 // guard alone: every guard to one underlying binding probes the slot and adopts the
971 // table it already carries, so a second guard extends the shared registration set
972 // instead of replacing (and freeing) the first guard's table.
973 detail::HandlerTable* EnsureHandlers() {
974 if (!handle_ || !ulDOMDataBindingGetStagingTable(handle_))
975 return nullptr;
976 ULDOMDataChangeCallback current = nullptr;
977 void* current_data = nullptr;
978 ulDOMDataBindingGetChangeCallback(handle_, &current, &current_data);
979 if (current == &detail::HandlerTableChangeThunk) {
980 handlers_ = static_cast<detail::HandlerTable*>(current_data);
981 return handlers_;
982 }
983 auto* table = new detail::HandlerTable();
984 table->refs = 3; // Both callback slots, plus this frame's registration probe.
985 table->context = detail::BorrowBindingContext(handle_);
986 ulDOMDataBindingSetChangeCallback(handle_, &detail::HandlerTableChangeThunk, table,
987 &detail::ReleaseHandlerTable);
988 ulDOMDataBindingSetActionCallback(handle_, &detail::HandlerTableActionThunk, table,
989 &detail::ReleaseHandlerTable);
990 const bool registered = table->refs == 3;
991 detail::ReleaseHandlerTable(table);
992 if (!registered)
993 return nullptr;
994 handlers_ = table;
995 return handlers_;
996 }
997
998 // Find the installed handler table WITHOUT installing one (the removal path): null
999 // when the guard is empty or its page is gone, or when no typed registration exists yet.
1000 detail::HandlerTable* FindHandlers() {
1001 if (!handle_)
1002 return nullptr;
1003 ULDOMDataChangeCallback current = nullptr;
1004 void* current_data = nullptr;
1005 ulDOMDataBindingGetChangeCallback(handle_, &current, &current_data);
1006 if (current == &detail::HandlerTableChangeThunk) {
1007 handlers_ = static_cast<detail::HandlerTable*>(current_data);
1008 return handlers_;
1009 }
1010 return nullptr;
1011 }
1012
1013 ULDOMDataBinding handle_ = nullptr;
1014 detail::HandlerTable* handlers_ = nullptr;
1015 // The implicit-receiver forms' route to the bound instance; empty when the bind cannot
1016 // grant mutable access (const binds, const-element holders, raw-handle guards).
1017 detail::InstanceAccess<T> access_;
1018};
1019
1020} // namespace data
1021} // namespace dom
1022} // namespace ultralight
ULDOMDataBinding raw() const
Get the C handle, for passing to the <Ultralight/CAPI/CAPI_DOMData.h> functions.
Definition Binding.h:946
Binding & OnAction(Fn &&fn) &
Register the handler for an action (replacing any earlier one).
Definition Binding.h:506
Binding & operator=(const Binding &)=delete
std::string name() const
Get the binding name.
Definition Binding.h:906
bool PostAction(const detail::ActionPayloadArg< T, Name > &payload)
Send one of this binding's actions with its payload, as if the page fired it.
Definition Binding.h:852
Binding()=default
Create an empty Binding.
Binding OnChange(A &&... args) &&
Register the handler for edits to an Editable field on a Binding you just created.
Definition Binding.h:452
bool PostAction()
Send one of this binding's actions that has no payload, as if the page fired it.
Definition Binding.h:806
Binding & OnChange(M method) &
Register a member function of the bound type as the handler for edits to an Editable field,...
Definition Binding.h:436
Binding & OnAction(H holder, M method) &
Register a member function as the handler for an action, called through a smart pointer.
Definition Binding.h:616
static Binding FromBorrowed(ULDOMDataBinding handle)
Wrap a C handle someone else owns (eg, another Binding's raw()), adding a reference.
Definition Binding.h:936
Binding RemoveActionHandler() &&
Remove the handler for an action on a Binding you just created.
Definition Binding.h:782
T described_type
The bound type.
Definition Binding.h:234
Binding & OnChange(H holder, M method) &
Register a member function as the handler for edits to an Editable field, called through a smart poin...
Definition Binding.h:419
Binding & RemoveActionHandler() &
Remove the handler for an action (if there is one).
Definition Binding.h:713
static Binding Adopt(ULDOMDataBinding handle)
Wrap a C handle you own, taking ownership of it.
Definition Binding.h:927
ULDOMDataBinding LeakRef()
Give up ownership of the C handle and return it.
Definition Binding.h:953
bool IsEmpty() const
Whether or not this Binding holds nothing (eg, after a move or a failed Context::Bind()).
Definition Binding.h:298
bool IsAlive() const
Whether or not this Binding still works.
Definition Binding.h:308
Binding & OnAction(M method) &
Register a member function of the bound type as the handler for an action, called on the bound instan...
Definition Binding.h:633
Binding(const Binding &)=delete
Binding & operator=(Binding &&other) noexcept
Move assignment (releases the binding this one held, then takes over other's).
Definition Binding.h:264
Binding & OnChange(Fn &&fn) &
Register the handler for edits to an Editable field (replacing any earlier one).
Definition Binding.h:350
Binding & OnChange(C &receiver, M method) &
Register a member function of receiver as the handler for edits to an Editable field.
Definition Binding.h:399
Binding(Binding &&other) noexcept
Move constructor (other becomes empty).
Definition Binding.h:249
Binding OnAction(A &&... args) &&
Register the handler for an action on a Binding you just created.
Definition Binding.h:649
friend class Context
Definition Binding.h:962
Binding & OnAction(C &receiver, M method) &
Register a member function of receiver as the handler for an action.
Definition Binding.h:597
Binding RemoveChangeHandler() &&
Remove the handler for edits to an Editable field on a Binding you just created.
Definition Binding.h:699
~Binding()
Destroy this Binding (see Binding Lifetime in the class description).
Definition Binding.h:280
Binding & RemoveChangeHandler() &
Remove the handler for edits to an Editable field (if there is one).
Definition Binding.h:662
Whether or not the DOM API can hold an object through H (ignoring const and references),...
Definition Holders.h:41
Whether or not T has a schema (ignoring const, volatile, and references), ie.
Definition TypeTraits.h:163
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
@ Int64
A signed 64-bit integer (from any integer type).
Definition ValueTraits.h:29
consteval auto Field(const char *name, A accessor, Tags... tags)
Declare a field.
Definition Schema.h:551
@ kEntryFlags_Editable
The page may request changes to this field.
Definition Schema.h:100
Direct C++ access to modify page elements and handle events.
Root namespace for every public Ultralight type, function, and enumeration.
Traits template for describing a type to data bindings.
Definition TypeTraits.h:156