Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
widget_bridge.hpp
Go to the documentation of this file.
1#pragma once
2
3// Windows: system headers before any namespace block, or CHOC/QuickJS
4// intrinsics (_InterlockedIncrement etc.) get declared in pulp::view:: and
5// conflict with winbase.h. win32_sane.hpp, never raw <windows.h> — it pre-sets
6// NOMINMAX, else min/max break widgets.hpp's std::max below (MSVC C2589).
7#if defined(_WIN32)
9#endif
10
13#include <pulp/view/view.hpp>
15#include <pulp/view/theme.hpp>
20#include <pulp/state/store.hpp>
21
22namespace pulp::view { struct ClaudeBundle; }
23#include <algorithm>
24#include <chrono>
25#include <cstdint>
26#include <filesystem>
27#include <optional>
28#include <functional>
29#include <limits>
30#include <string>
31#include <string_view>
32#include <vector>
33#include <unordered_map>
34#include <unordered_set>
35#include <memory>
36#include <mutex>
37#include <atomic>
38
40
41namespace pulp::view {
42
43class QueryService;
44struct BridgeCallbackState;
45// Widget value snapshot for hot reload preservation
47 std::unordered_map<std::string, float> scalar_values;
48 struct XYValue { float x = 0; float y = 0; };
49 std::unordered_map<std::string, XYValue> xy_values;
50 // Custom widget-declared state (item 1.4b): opaque per-widget blobs from
51 // View::save_reload_state, keyed by script id, restored via restore_reload_state.
52 std::unordered_map<std::string, std::string> custom_state;
53 // Legacy compat
54 std::unordered_map<std::string, float> values;
55};
56// Bridges JS scripts to the Pulp widget system.
57// Registers native functions that JS code calls to create, configure,
58// layout, style, and interact with widgets.
60public:
61 using DeadlineCheck = std::function<void()>;
62 // `granted_capabilities` scopes the EFFECTFUL bridge-API groups (exec,
63 // clipboard, filesystem, storage, ai, runtime_import). Ungranted groups are
64 // never registered — the JS symbol is absent. The default all() leaves every
65 // group present (the full-featured posture for trusted/local UIs); a host that
66 // runs untrusted UI constructs with only the declared set (empty = most
67 // restrictive). Pure-UI groups ignore this. See reload_capabilities.hpp.
72
73 // Attach (or replace) the GPU surface AFTER construction. Mirrors the
74 // 4th constructor argument but for the common case where the WidgetBridge
75 // is built before the host's GpuSurface exists (e.g. AUv3 editor lifecycle:
76 // the format adapter opens ViewBridge / ScriptedUiSession first, then
77 // builds the PluginViewHost — only THEN is `host->gpu_surface()` non-null).
78 //
79 // Without this attach call, the JS-side navigator.gpu / canvas.getContext
80 // ('webgpu') bridge falls through to mocks and Three.js renders black.
81 //
82 // Lifetime: the GpuSurface must outlive this WidgetBridge. Pass `nullptr`
83 // to detach (e.g. during host teardown). Idempotent — calling with the
84 // same surface twice is a no-op.
86
87 // Read-back accessor for diagnostics + tests.
88 render::GpuSurface* gpu_surface() const noexcept { return gpu_surface_; }
89
94 void visit_value_channels(const ValueChannelVisitor& visitor) const;
95
108 void set_imported_text_scale(float scale) noexcept {
109 if (!(scale > 0.0f)) return; // NaN-safe: rejects 0, negatives and NaN
110 imported_text_scale_ = std::clamp(scale, 0.5f, 4.0f);
111 }
112 float imported_text_scale() const noexcept { return imported_text_scale_; }
113
114 CapabilitySet granted_capabilities() const noexcept { return granted_capabilities_; }
115 // True iff a GpuSurface is attached AND its adapter reports
116 // `native_bridge=true` (i.e. JS navigator.gpu / canvas.getContext('webgpu')
117 // will route through Pulp's Dawn).
118 bool has_native_gpu_bridge() const noexcept;
119
124 void load_script(const std::string& code);
126 static std::string loaded_script_source(const std::string& code);
127 bool frame_pump_live() const noexcept { return frame_pump_live_; }
128
137 std::string_view receiver,
138 std::string_view type,
139 const choc::value::Value& payload,
140 std::string_view id = {},
141 std::string_view context = "native message dispatch");
142
146 void set_asset_roots(std::vector<std::filesystem::path> roots) {
147 asset_roots_.clear();
148 asset_roots_.reserve(roots.size());
149 for (auto& root : roots) {
150 if (root.empty()) continue;
151 std::error_code ec;
152 auto abs = std::filesystem::absolute(root, ec).lexically_normal();
153 if (ec) abs = root.lexically_normal();
154 asset_roots_.push_back(std::move(abs));
155 }
156 }
157 const std::vector<std::filesystem::path>& asset_roots() const noexcept { return asset_roots_; }
158
163 std::string name;
164 const unsigned char* data = nullptr;
165 std::size_t size = 0;
166 };
167
172 void set_embedded_assets(std::vector<EmbeddedAsset> assets);
173 const std::vector<EmbeddedAsset>& embedded_assets() const noexcept {
174 return embedded_assets_;
175 }
176
182 const EmbeddedAsset* embedded_asset(std::string_view path) const noexcept;
185 std::string embedded_asset_data_uri(const EmbeddedAsset& asset) const;
186
191 void set_storage_identity(std::string identity) {
192 storage_identity_ = std::move(identity);
193 }
194
196 std::string storage_identity() const;
199 std::string font_weight_scope() const;
200
206 void set_script_base_dir(std::filesystem::path dir) {
207 if (dir.empty()) { script_base_dir_.clear(); return; }
208 std::error_code ec;
209 auto abs = std::filesystem::absolute(dir, ec).lexically_normal();
210 script_base_dir_ = ec ? dir.lexically_normal() : abs;
211 }
212 const std::filesystem::path& script_base_dir() const noexcept { return script_base_dir_; }
213
223 void set_allow_unreviewed_file_paths(bool allow) noexcept {
224 allow_unreviewed_file_paths_ = allow;
225 }
226 bool allow_unreviewed_file_paths() const noexcept {
227 return allow_unreviewed_file_paths_;
228 }
229
234 bool permits_file_path(const std::filesystem::path& path) const;
235
246 std::string resolve_script_relative(const std::string& path) const;
247
254 void load_script(const std::string& code, const std::string& script_id);
255
260 void set_active_script_id(const std::string& script_id);
261
262 const std::string& active_script_id() const noexcept { return active_script_id_; }
263
264 // Get a widget by its JS-assigned ID
265 View* widget(const std::string& id);
266
267 // ScrollView wrappers created for retained DOM containers are kept
268 // separate from the authored widget registry so normal ID APIs continue
269 // to address the original content View.
270 ScrollView* scroll_wrapper(const std::string& id) const noexcept;
271
272 // Return the native style/hit-test surface for an authored id. Retained
273 // scroll containers use a wrapper for clipping and geometry; ordinary
274 // widgets continue to resolve through the authored registry.
275 View* style_target(const std::string& id) noexcept;
276
277 // Sync all widget values from the parameter store
279
280 // Hot reload support: clear all JS-created widgets
281 void clear(const DeadlineCheck& deadline_check = {});
282 // Runtime.evaluate realm replacement must never execute cleanup code in the
283 // evaluated realm: user code can replace those helpers and wedge teardown.
284 void clear_for_realm_replacement(const DeadlineCheck& deadline_check = {});
287 std::string_view bounded_realm_retirement_denial() const noexcept;
288 // Owner-teardown cleanup for a quarantined realm. Removes only this
289 // bridge's root children so a replacement session already attached to the
290 // shared root remains intact.
292 // Allocation-free destructor fallback when selective quarantined cleanup
293 // could not be prepared. Retires the complete attached root rather than
294 // leave any View callback borrowing a realm that is about to be destroyed.
296 // Constant-work fail-close used when the realm-replacement deadline has
297 // already expired. The retained realm is hidden and removed from every
298 // host pump/fan-out path, then reclaimed with the owning session.
299 void quarantine_realm() noexcept;
300
301 // Forward a key event to JS (called by host for global shortcuts).
302 // Per-instance entry; usually invoked by `dispatch_global_key` below.
303 void forward_key_event(int key_code, uint16_t modifiers, bool is_down);
304
309 static bool dispatch_key_for_root(View& root, int key_code,
310 uint16_t modifiers, bool is_down);
311
312 // Static fan-out: forward a key event to every live WidgetBridge.
313 //
314 // Platform hosts call this from their key-event paths so that any
315 // app using @pulp/react / WidgetBridge gets `window.addEventListener
316 // ('keydown', ...)` and `registerShortcut(...)` delivery without the
317 // app having to install its own `View::on_global_key` lambda.
318 // Bridges register themselves in the static `all_bridges_` set in
319 // their constructor and unregister in their destructor; the set is
320 // mutex-protected because bridge construction can happen off the
321 // platform main thread in some embedding contexts.
322 static void dispatch_global_key(int key_code, uint16_t modifiers, bool is_down);
323
324 // Static fan-out: fire a document-level event (`mousedown`,
325 // `pointerdown`, etc.) into every live WidgetBridge's JS document.
326 // Used by platform hosts to fire synthetic outside-click events on
327 // Esc so React popovers that use the
328 // `document.addEventListener('mousedown', onDoc)` click-outside
329 // idiom close automatically. `event_json_literal` is a JS object
330 // literal expression INCLUDING braces, e.g. `{clientX:-1,clientY:-1,target:null}`.
331 static void dispatch_document_event(const std::string& event_type,
332 const std::string& event_json_literal);
333
334 // Snapshot widget values for preservation across hot reload
336 const DeadlineCheck& deadline_check = {},
337 bool include_custom_state = true) const;
338 void snapshot_values(std::unordered_map<std::string, float>& out) const;
339
340 // Restore widget values after hot reload rebuild
342 const DeadlineCheck& deadline_check = {},
343 bool include_custom_state = true);
344 void restore_values(const std::unordered_map<std::string, float>& snapshot);
345
346 // Deliver any pending async shell results back onto the JS thread.
348
349 // Flush requestAnimationFrame-style callbacks and pending microtasks from
350 // the host frame loop.
352
353 // Host frames a mounted runtime document still asked for to settle its
354 // commits (`__pulpRuntimeSettle__`). Each service_frame_callbacks() pass
355 // consumes one. Zero once the document has settled, or for a document that
356 // never asked.
357 int pending_runtime_settle_rounds() const noexcept {
358 return pending_runtime_settle_rounds_;
359 }
360
361 // ── Declarative native→widget bindings (params / meters) ──────────────
362 //
363 // A binding is registered ONCE from JS via `bindWidgetToParam(widgetId,
364 // paramName, transform?)` or `bindMeter(widgetId, paramName, transform?)`.
365 // Thereafter the widget's value is pushed from the atomic param store to
366 // the widget every frame in C++ — off the host FrameClock, with ZERO
367 // per-frame JS bridge crossing — by service_param_bindings(), which
368 // service_frame_callbacks() drives on the host idle cadence. For native
369 // gesture-capable value widgets, the binding also routes begin/end to the
370 // StateStore so a JS `change` handler's writes remain a balanced host
371 // automation gesture.
372 //
373 // This replaces the requestAnimationFrame loop a live-metering / param-
374 // following UI would otherwise run (the one thing that guarantees QuickJS
375 // executes every vsync). Precedence: a value widget (knob/fader/slider/
376 // toggle/progress) is re-asserted from the store EVERY frame, so a stray
377 // explicit setValue is corrected on the next frame; a bound Meter is updated
378 // whenever its source changes (Meter::set_level repaints unconditionally, so
379 // it is not touched on a static frame). Either way the binding yields while
380 // the user is dragging that widget — the gesture wins (View::is_gesture_active()).
381 // See bind_* in state_binding_api.cpp.
384
385 // Number of live param/meter bindings (diagnostics + tests).
386 std::size_t param_binding_count() const noexcept { return param_bindings_.size(); }
387 // Exact live creation identities retained for scoped realm cleanup.
388 std::size_t owned_widget_identity_count() const noexcept {
389 return owned_widgets_.size();
390 }
391 std::size_t widget_cache_count() const noexcept {
392 return widgets_.size();
393 }
394
395 // Number of JS-to-native bridge dispatches since the last reset. This is
396 // per bridge so deterministic import benchmarks can measure mount and
397 // step work without a process-global counter.
398 std::uint64_t bridge_call_count() const noexcept {
399 return bridge_call_count_->load(std::memory_order_relaxed);
400 }
401 void reset_bridge_call_count() noexcept {
402 bridge_call_count_->store(0, std::memory_order_relaxed);
403 }
404
405 // Deliver `paramchange` to JS subscriptions whose param moved since the
406 // last frame. Polled, not pushed — see state_binding_api.cpp for why that
407 // is what makes delivery origin-blind, coalesced, and off the audio thread.
409
410 // Live paramchange subscriptions, excluding mid-dispatch tombstones.
411 std::size_t param_subscription_count() const noexcept;
412
413 // Deliver event-channel blocks to JS handlers on the frame tick.
415
416 // Live event-channel bindings, excluding mid-dispatch tombstones.
417 std::size_t event_binding_count() const noexcept;
418
419 // ── Runtime design import ─────────────────────────────────────
420 //
421 // Registers the `__pulpRuntimeImport__(html, source)` and
422 // `__pulpRuntimeSettle__(rounds)` native functions on the bridge's
423 // JS engine. These are the C++ side of `@pulp/react/runtime-import`'s
424 // `renderFromDesign()`: they evaluate a design bundle's HTML in
425 // the LIVE bridge engine (not a fresh sandbox) and pump the
426 // settle loop using this bridge's own service_frame_callbacks.
427 //
428 // Call ONCE after constructing the bridge, before consumer code
429 // calls renderFromDesign(). Subsequent calls are no-ops.
430 //
432
433private:
434 bool forward_key_event_handled(int key_code, uint16_t modifiers,
435 bool is_down, View* focus_root = nullptr);
436 // Internal helper used by __pulpRuntimeImport__. Forward-declared in
437 // the design_import unit so the implementation lives next to the
438 // related offline boot code. Installs navigator/HTML*Element/etc.
439 // sandbox shims on engine_ (idempotent), evaluates the bundle's
440 // asset payloads (React/ReactDOM/Babel) preserving any host React
441 // already installed on globalThis, evaluates inline text/javascript
442 // and text/babel scripts, dispatches DOMContentLoaded. Skips
443 // buildDom + walkDomJson — the runtime path is reconciler-owned.
444 void evaluate_claude_bundle_in_live_engine(const ClaudeBundle& bundle);
445public:
446
447 // Override the repaint invalidator used after JS-driven UI / style
448 // changes and to drain `requestAnimationFrame` callbacks.
449 //
450 // By default the bridge's constructor wires this to call
451 // `root.request_repaint()`, which walks up to the host's `repaint()`.
452 // Override only when the bridge is part of a larger composite UI whose
453 // top-level invalidator is not the same as the root view passed in
454 // (e.g. the standalone window's editor uses the WindowHost directly).
455 //
456 // Without a wired callback, JS `requestAnimationFrame` callbacks queue
457 // but never schedule a second paint.
458 void set_repaint_callback(std::function<void()> cb);
459
464 std::uint64_t damage_requests = 0;
465 std::uint64_t unmarked_mutations = 0;
466 std::uint64_t layout_generation = 0;
467 bool self_damaged(const View& root) const;
468 bool untouched(const View& root) const;
469 };
476
477 // Override the AI CLI command used by the design tool chat.
478 void set_ai_cli_command(std::string cmd);
479
480 // hostKind(): "plugin" | "standalone" | "unknown"; see ScriptedUiSession::set_host_kind.
481 void set_host_kind(std::string kind) { host_kind_ = std::move(kind); }
482 const std::string& host_kind() const { return host_kind_; }
483
486 using TimeSource = std::function<std::chrono::steady_clock::time_point()>;
488 std::chrono::steady_clock::time_point time_now() const;
489
490 // Inspect the native texture backing a canvas element when the native GPU
491 // bridge is active. Used by low-level native WebGPU/Skia validation.
493
494 // Inspect a native texture allocated through the GPU bridge by texture id.
495 // Used by low-level native WebGPU/Skia validation when the source is not a
496 // canvas-backed presentation texture.
498
499#ifdef PULP_BENCHMARK
500 // Install (or clear) the zero-copy benchmark perf-counter sink. The
501 // counters are accumulated inline around WriteBuffer calls on the
502 // JS-driven GPU resource setup path in widget_bridge/gpu_api.cpp.
503 // Tooling-only; compiled out unless PULP_BENCHMARK is defined.
504 void set_bench_counters(render::bench::PerfCounters* counters);
505#endif
506
507private:
508 struct NativeGpuBridgeState;
509 // One capability-gated entry in register_api()'s registration table.
510 // Defined in widget_bridge.cpp — the table is an implementation detail.
511 struct ApiGroup;
512
513 ScriptEngine& engine_;
514 View& root_;
515 state::StateStore& store_;
516 // Effectful-API-group gate. See the constructor doc + reload_capabilities.hpp.
517 CapabilitySet granted_capabilities_;
518 render::GpuSurface* gpu_surface_ = nullptr;
519 std::unique_ptr<NativeGpuBridgeState> native_gpu_bridge_state_;
520 std::vector<std::filesystem::path> asset_roots_;
521 std::filesystem::path script_base_dir_;
522 bool allow_unreviewed_file_paths_ = false;
523 std::vector<EmbeddedAsset> embedded_assets_;
524 std::string storage_identity_;
525
530 struct PrivateStoreDirs {
531 std::string identity;
532 std::optional<std::filesystem::path> widget_storage;
533 std::optional<std::filesystem::path> style_presets;
534 };
535 PrivateStoreDirs private_store_dirs_;
536
537 // Per-id cache entry: the resolved View plus the tree structure generation
538 // at which that View was last CONFIRMED to live under `root_`. A fresh entry
539 // starts unvalidated (generation 0, the reserved sentinel), so the first
540 // widget() lookup always runs the authoritative subtree walk; once confirmed,
541 // repeat lookups short-circuit to O(1) while the root-local generation is
542 // unchanged (no remove_child has detached anything since). The implicit
543 // View* conversions keep this a drop-in for the former
544 // `unordered_map<string, View*>` at every call site; assignment resets the
545 // validation because a replacement pointer has not yet been confirmed.
546 struct BridgeWidgetState {
547 View* view = nullptr;
548 std::uint64_t validated_generation = 0;
549 std::uint64_t instance_id = 0;
550 BridgeWidgetState() = default;
551 BridgeWidgetState(View* v) // NOLINT: intentional implicit
552 : view(v),
553 instance_id(v ? v->import_binding_instance_id() : 0) {}
554 BridgeWidgetState& operator=(View* v) {
555 view = v;
556 validated_generation = 0;
557 instance_id = v ? v->import_binding_instance_id() : 0;
558 return *this;
559 }
560 operator View*() const { return view; }
561 View* operator->() const { return view; }
562 bool identifies(const View* candidate) const {
563 return view == candidate && candidate &&
564 instance_id == candidate->import_binding_instance_id();
565 }
566 };
567
568 class WidgetRegistry
569 : public std::unordered_map<std::string, BridgeWidgetState> {
570 using Base = std::unordered_map<std::string, BridgeWidgetState>;
571
572 public:
573 explicit WidgetRegistry(std::vector<BridgeWidgetState>& owned_widgets)
574 : owned_widgets_(owned_widgets) {}
575
577 public:
578 AssignmentSlot(BridgeWidgetState& state,
579 std::vector<BridgeWidgetState>& owned_widgets)
580 : state_(state), owned_widgets_(owned_widgets) {}
581
583 if (view)
584 owned_widgets_.emplace_back(view);
585 state_ = view;
586 return *this;
587 }
588
589 private:
590 BridgeWidgetState& state_;
591 std::vector<BridgeWidgetState>& owned_widgets_;
592 };
593
594 AssignmentSlot operator[](const std::string& id) {
595 return AssignmentSlot(Base::operator[](id), owned_widgets_);
596 }
597
598 void cache(const std::string& id, View* view) {
599 Base::operator[](id) = view;
600 }
601
602 private:
603 std::vector<BridgeWidgetState>& owned_widgets_;
604 };
605
606 // Track widgets by ID for JS access
607 // Exact live creation identities, independent of the mutable ID registry,
608 // so duplicate IDs cannot lose delayed-quarantine ownership.
609 std::vector<BridgeWidgetState> owned_widgets_;
610 WidgetRegistry widgets_;
611 std::unordered_map<std::string, ScrollView*> scroll_wrappers_;
612
613 // Idempotency guards for native-event registrations, one record per widget
614 // id. Re-running a registrar on every React commit must not replace or stack
615 // its View callback. The claim_* helpers below gate registrations so each
616 // (widget id, channel) wires the native hook exactly once, and forgetting an
617 // id is a single erase.
618 //
619 // Keyed independently of `widgets_`: a registrar claims its channel BEFORE
620 // resolving the widget, so an id can carry registration state with no live
621 // widget behind it.
622 struct WidgetRegistrations {
623 bool pointer = false;
624 bool wheel = false;
625 // Recognizer keys already added to this widget (`tap`, `pinch`, …).
626 // A vector, not a set: a widget carries a handful of recognizers, so a
627 // linear scan beats hashing.
628 std::vector<std::string> gesture_recognizers;
629 };
630 std::unordered_map<std::string, WidgetRegistrations> registrations_;
631
632 // Claim a native-event channel for `id`. Returns true the first time the
633 // channel is claimed — the caller then wires the native hook — and false
634 // once it is already registered.
635 bool claim_pointer_registration(const std::string& id);
636 bool claim_wheel_registration(const std::string& id);
637 bool claim_gesture_registration(const std::string& id,
638 const std::string& recognizer_key);
639
640 // Registered keyboard shortcuts from JS
641 struct ShortcutBinding {
642 KeyCode key;
643 uint16_t modifiers;
644 std::string callback;
645 };
646 std::vector<ShortcutBinding> shortcuts_;
647
652 CssKeyframesRegistry css_keyframes_registry_;
653public:
654 CssKeyframesRegistry& css_keyframes_registry() { return css_keyframes_registry_; }
655 const CssKeyframesRegistry& css_keyframes_registry() const { return css_keyframes_registry_; }
656private:
657
658 // Model-agnostic AI CLI command (default: Claude)
659 std::string ai_cli_command_ = "claude --print --model {model}";
660 std::string host_kind_ = "unknown";
661 TimeSource time_source_;
662 std::chrono::steady_clock::time_point performance_epoch_ = std::chrono::steady_clock::now();
663
664 struct AsyncExecResult {
665 std::string callback_id;
666 std::string output;
667 };
668 // Retirement of a view removed by a JS handler is owned by the tree root
669 // (View::retire), not by the bridge, so it obeys the same contract as a
670 // removal from a native lifecycle hook. callback_alive_ carries the root
671 // pointer because most callback closures capture only this state.
672 float imported_text_scale_ = 1.0f;
673 std::shared_ptr<BridgeCallbackState> callback_alive_;
674 std::shared_ptr<std::mutex> async_exec_mutex_ = std::make_shared<std::mutex>();
675 std::shared_ptr<std::vector<AsyncExecResult>> async_exec_results_ =
676 std::make_shared<std::vector<AsyncExecResult>>();
677
678 // Returns a thread-safe sink that a background worker calls with
679 // (callback_id, payload) to queue a result for delivery to JS via
680 // poll_async_results() → __dispatch__. Captures the async_exec_results_
681 // plumbing by shared_ptr (not `this`), so a worker that outlives the bridge
682 // safely no-ops via the callback_alive_ flag. Shared by execAsync and the
683 // query service.
684 std::function<void(const std::string& callback_id, std::string payload)>
685 make_async_result_sink();
686
687 // Off-UI-thread index/search service (R7). Lazily created on first use by
688 // register_query_service_api(); its worker delivers results through the same
689 // async_exec_results_ queue that poll_async_results() drains, so no extra
690 // per-frame pump is needed. Held by shared_ptr (not unique_ptr) so its
691 // type-erased deleter lets widget_bridge.cpp destroy it with only a forward
692 // declaration — avoiding a query_service.hpp include in that hotspot file.
693 std::shared_ptr<QueryService> query_service_;
694
695 std::vector<int> pending_frame_ids_;
696 // Single-slot handshake between the two frame-draining entry points.
697 // `service_frame_callbacks()` sets it when it drains pending_frame_ids_;
698 // `poll_async_results()` consumes it (exchange-to-false) and skips its own
699 // drain for that one pass. A rAF callback that re-arms itself refills the
700 // queue while it is being drained, so without this both entry points fire
701 // the same self-rearming callback in one host tick and the scene is drawn
702 // twice per display period. Browser semantics agree: a frame requested from
703 // inside a frame callback belongs to the NEXT frame, never the current one.
704 //
705 // Liveness is bounded by construction: every poll clears the slot, so two
706 // consecutive polls can never both skip, and a skip only ever follows a
707 // service pass that already drew. A host that drives poll alone therefore
708 // keeps drawing on every poll.
709 bool frames_drained_by_service_ = false;
710 bool frame_pump_live_ = false; // set by the first host frame service
711 std::shared_ptr<std::atomic<std::uint64_t>> bridge_call_count_ =
712 std::make_shared<std::atomic<std::uint64_t>>(0);
713 // Requested by __pulpRuntimeSettle__ while QuickJS is inside a native
714 // callback. Drained only from the outer host-frame boundary; this is a
715 // budget, not a synchronous recursion request.
716 int pending_runtime_settle_rounds_ = 0;
717 bool frame_preamble_loaded_ = false;
718 // Identity of the most-recently loaded script (via
719 // `load_script(code, script_id)` or `set_active_script_id`). When
720 // non-empty, rAF callbacks and `motion.publishValue` bindings use
721 // it as the `source_id` prefix on emitted motion events.
722 std::string active_script_id_;
723 // install_runtime_import_handlers() is idempotent. Tracked per-bridge
724 // (not as a static thread_local on `this`, because heap reuse across tests
725 // gave duplicate addresses → spurious skip).
726 bool runtime_import_installed_ = false;
727 bool realm_quarantined_ = false;
728 bool realm_retired_ = false;
729 // Complete root-owned state transferred by the one retirement commit.
730 // The emergency instance is preconstructed so destructor fail-close does
731 // not allocate even when selective realm extraction could not be prepared.
732 struct RetiredRealmState {
733 std::vector<std::unique_ptr<View>> children;
734 std::unique_ptr<ActiveDrag> drag;
735 std::unique_ptr<View::RootInteractionState> interaction;
736 std::function<void(const std::string&, uint16_t)> root_click_callback;
737 };
738 RetiredRealmState retired_realm_;
739 RetiredRealmState emergency_retired_realm_;
740
741 // Native-side timer queue for setTimeout / setInterval. Callbacks
742 // themselves live in JS (`__timerCallbacks__`); native tracks (id,
743 // deadline, repeat, interval) so service_frame_callbacks() can fire expired
744 // timers regardless of which JS-side scheduler the consumer wants to
745 // feature-detect against.
746 struct PendingTimer {
747 int id;
748 std::chrono::steady_clock::time_point deadline;
749 std::chrono::milliseconds interval;
750 bool repeating;
751 };
752 std::vector<PendingTimer> pending_timers_;
753
754 // Declarative native→widget binding (see service_param_bindings()).
755 // Transform applied to the source before it reaches the widget, in order:
756 // optional dB→linear map → scale → offset → clamp. The default is the
757 // identity on the store's already-normalized [0,1] value, which is the
758 // common case (a knob/fader/meter tracking a normalized param 1:1).
759 struct BindingTransform {
760 bool db = false;
761 float db_min = -60.0f;
762 float db_max = 0.0f;
763 float scale = 1.0f;
764 float offset = 0.0f;
765 bool clamp = true;
766 float clamp_min = 0.0f;
767 float clamp_max = 1.0f;
768 float apply(float v) const;
769 };
770 struct ParamBinding {
771 using Target = BindingTarget;
772 std::string widget_id;
773 state::ParamID param_id = 0;
777 std::string value_channel;
779 std::string uniform_name;
784 std::uint32_t last_publish_seq = 0;
785 std::uint64_t value_generation_identity = 0;
786 std::chrono::steady_clock::time_point last_publish_at{};
787 float neutral = 0.0f;
788 Target target = Target::value;
789 BindingTransform transform;
793 bool derive_from_param = false;
794 float last_applied = std::numeric_limits<float>::quiet_NaN();
795 };
796 std::vector<ParamBinding> param_bindings_;
797 ValueChannelAccess value_channel_access_;
798 std::vector<ParamSubscription> param_subscriptions_;
799 std::uint32_t next_param_subscription_id_ = 1;
800 // True while service_param_subscriptions() is dispatching into JS, so an
801 // unsubscribe from inside a handler tombstones instead of erasing.
802 bool in_param_dispatch_ = false;
803 struct EventBinding {
804 std::uint32_t id = 0;
805 std::string channel_name;
806 std::uint32_t last_publication = 0;
807 std::uint64_t value_generation_identity = 0;
808 };
809 std::vector<EventBinding> event_bindings_;
810 std::uint32_t next_event_binding_id_ = 1;
811 bool in_event_dispatch_ = false;
812 struct ParamGestureRoute {
813 state::ParamID active_param_id = 0;
814 bool active = false;
815 };
816 std::unordered_map<std::string, std::shared_ptr<ParamGestureRoute>>
817 param_gesture_routes_;
818 // Register (or replace) a binding for `widget_id`. Resolves `param_name` to
819 // its id; returns false (no binding added) if the store has no such param.
820 // `transform` is the optional JS transform object (may be null → identity).
821 bool add_param_binding(const std::string& widget_id,
822 const std::string& param_name,
823 ParamBinding::Target target,
824 const choc::value::Value* transform);
825 bool add_shader_uniform_binding(const std::string& widget_id,
826 const std::string& uniform_name,
827 const std::string& source,
828 const choc::value::Value* transform);
829 // Parse the optional JS transform object (`{db,dbMin,dbMax,scale,offset,
830 // min,max,clamp}`) into a BindingTransform. Null / non-object → identity.
831 static BindingTransform parse_transform(const choc::value::Value* v);
833 static bool transform_requests_derivation(const choc::value::Value* v);
834 // Resolve a param NAME to its id via the store. Returns false when the
835 // store has no param with that name (the binding is then not registered).
836 bool resolve_param_id(const std::string& name, state::ParamID& out) const;
837 // Drop bindings whose widget id no longer resolves in widgets_ (after a
838 // removeWidget subtree teardown) so a later widget reusing that id is not
839 // silently re-bound.
840 void prune_dangling_bindings();
841 // Route a gesture-capable widget through its CURRENT declarative binding.
842 // The route retains the parameter that began an active gesture so rebinding
843 // mid-drag still ends the original parameter exactly once.
844 void wire_parameter_gestures(const std::string& widget_id, View* widget);
849 void begin_param_gesture(const std::string& widget_id);
850 void end_param_gesture(const std::string& widget_id);
851 void finish_param_gesture_route(
852 const std::shared_ptr<ParamGestureRoute>& route);
853 void release_param_gesture_route(const std::string& widget_id) noexcept;
854 void release_all_param_gesture_routes() noexcept;
855 void defer_all_param_gesture_routes();
856 // Forget every native bridge reference to the removed subtree. This is
857 // stronger than erasing widgets_: native event-registration guards are also
858 // keyed by id and must be cleared before recycled ids can be rewired.
859 void forget_widget_subtree(View* node, bool preserve_js_dom_state = false,
860 const DeadlineCheck& deadline_check = {},
861 bool notify_js = true);
862 void retire_removed_widget(std::unique_ptr<View> widget);
863 void clear_realm(const DeadlineCheck& deadline_check);
864 void retire_realm(const DeadlineCheck& deadline_check);
865 void commit_realm_retirement(RetiredRealmState& destination) noexcept;
866 void unregister_global_dispatch() noexcept;
867 void unregister_global_dispatch(const DeadlineCheck& deadline_check);
868 std::vector<BridgeWidgetState> foreign_owned_widget_states() const;
869 void begin_root_quarantine() noexcept;
870 void end_root_quarantine();
871 void invalidate_cached_subtrees_everywhere(
872 const std::vector<View*>& nodes,
873 const DeadlineCheck& deadline_check = {});
874 // Clear event/callback state for a widget that remains alive, such as a
875 // recycled VirtualList row container before it is rebound to a new index.
876 void forget_widget_event_state(View& view);
877 // Push one binding's transformed source value onto the resolved widget.
878 // Writes the widget only when the transformed value changed since the last
879 // frame; returns true on a change so the caller schedules one repaint.
880 bool apply_param_binding(ParamBinding& binding, View* w,
881 const MeterFrame* value_frame = nullptr,
882 std::uint32_t publish_seq = 0);
884 void derive_binding_transform(ParamBinding& binding, View* w);
886 bool apply_scope_binding(ParamBinding& binding, View* w,
887 const VectorFrame* value_frame,
888 std::uint32_t publish_seq);
891 static bool value_channel_is_stale(ParamBinding& binding, std::uint32_t seq);
892
893public:
896 const std::vector<BindingAttempt>& binding_attempts() const noexcept {
897 return binding_attempts_;
898 }
899
901 std::vector<std::string> unbound_params() const;
902
906 bool record_binding_attempt(const std::string& widget_id,
907 const std::string& param_name,
908 BindingTarget target,
909 BindingOutcome outcome);
910 bool parameter_id_for_name(const std::string& name, state::ParamID& out) const;
911 void clear_shader_uniform_bindings(const std::string& widget_id);
912
913private:
914 std::vector<BindingAttempt> binding_attempts_;
915
916 std::function<void()> repaint_callback_;
919 bool default_repaint_callback_ = true;
920 std::uint64_t repaint_request_generation_ = 0;
921
922#ifdef PULP_BENCHMARK
923 render::bench::PerfCounters* bench_counters_ = nullptr;
924#endif
925
926 // Resolve parent: returns view for parentId, or &root_ if empty
927 // The view a script named as a parent, or the root for an unknown id. With
928 // `child`, a parent that is the child itself or one of its descendants also
929 // resolves to the root: a script registers a widget under its id before
930 // attaching it, so naming that id as the parent must not build a cycle.
931 View* resolve_parent(const std::string& parent_id, const View* child = nullptr);
932 // Attaches a freshly created widget already registered under `id`. A
933 // refused attach drops that registration before the view is destroyed,
934 // so the registry never holds a pointer to a view the tree rejected.
935 bool attach_created(const std::string& id, const std::string& parent_id,
936 std::unique_ptr<View> view);
937
938 // Install on_change/on_toggle callbacks that dispatch to JS
939 void wire_callbacks(const std::string& id, View* w);
940
941 // Remove native event registration guards so recycled ids wire fresh views.
942 void forget_widget_registrations(const std::string& id);
943
944 // Single source-of-truth table that maps a lowercase widget tag
945 // (`knob`/`fader`/`toggle`/`combo`/`checkbox`/`spectrum`/`waveform`/
946 // `meter`/`xypad`/`listbox`/`icon`, plus the `select`/`progress`/`img`
947 // HTML aliases) to a freshly constructed, id-set, callback-wired native
948 // widget. Returns nullptr for non-widget tags so callers fall through to
949 // their container default. Used by the `__domAppend` React-commit fast path;
950 // mirrors the `createX` factory functions and the JS `_ensureNative` /
951 // `@pulp/react` host-config maps so the four widget-routing surfaces can't
952 // drift again (asserted by the routing-parity test).
953 std::unique_ptr<View> make_widget_for_tag(const std::string& tag,
954 const std::string& id);
955
956 // Called by the JS `__requestFrame__` / async-result chain whenever
957 // pending work needs the surface to repaint. Routes through
958 // `repaint_callback_`, which is wired in the constructor to
959 // `root.request_repaint()` by default and may be replaced via
960 // `set_repaint_callback`.
961 void request_repaint();
962
963 // Temporarily lend this bridge's owning root to a materialized document
964 // interaction that handles only the host navigation-key allowlist. The
965 // implementation transfers/commits any prior text focus, records a single
966 // owner per root, and releases only its own claim.
967 bool claim_document_navigation_focus();
968 void release_document_navigation_focus() noexcept;
969
970 // The ~75 bridge sub-API registrars used to be private member
971 // declarations here. Every added API changed this PUBLIC header and forced
972 // a recompile of the whole view/render/host surface that includes it.
973 // They now live as static methods of BridgeRegistrars (an internal friend,
974 // declared in the src-only widget_bridge/registrars.hpp), each taking the
975 // owning WidgetBridge& — so adding or removing a bridge API touches only
976 // that internal header, the owning .cpp, and register_api()'s data table
977 // in widget_bridge.cpp, never this header. The friend grants those statics
978 // access to WidgetBridge's private state.
979 // ── Layout-pass elision ─────────────────────────────────────────────
980 //
981 // The geometry queries (getLayoutRect / getLayoutBoxMetrics /
982 // getLayoutAncestorRects) each used to force `root_.layout_children()`
983 // UNCONDITIONALLY. That is correct but quadratic in practice: a shipping
984 // scripted UI calls getBoundingClientRect and getLayoutBoxMetrics dozens of
985 // times per frame, and `valueFromEvent` calls one PER POINTER EVENT — so a
986 // drag paid a full tree layout per sample, on the UI thread, ahead of paint.
987 //
988 // `ensure_layout()` keeps the guarantee those call sites actually need —
989 // "the geometry you are about to read is current" — while paying for it
990 // only when something has changed since the last pass.
991 void ensure_layout();
992 std::uint64_t last_layout_generation_ = 0; // 0 => always lay out once
993
994 // The press this bridge's script was offered through the navigation-claim
995 // hook, pending a match in the global fan-out. dispatch_global_key
996 // explains why the offer, and not the claim, is the discriminator.
997 struct NavigationOffer {
998 int key_code = 0;
999 std::uint16_t modifiers = 0;
1000 bool is_down = false;
1001 bool pending = false;
1002 };
1003 NavigationOffer last_navigation_offer_{};
1004
1005 friend struct BridgeRegistrars;
1006 void register_api();
1007
1008 // Lazily creates the off-UI-thread query service (spawns its worker thread
1009 // only on first use) with a deliver hook that funnels results into the
1010 // async_exec_results_ queue drained by poll_async_results(). Stays a
1011 // WidgetBridge member (not a registrar) — register_query_service_api()
1012 // calls it through `self`.
1013 QueryService& ensure_query_service();
1014};
1015
1016} // namespace pulp::view
Why a widget→parameter binding attempt did or did not take effect.
A View that replays recorded draw commands in paint().
A processor's declared channels.
Definition value_channel_set.hpp:61
Abstract GPU surface — a presentable rendering target backed by a native window/view.
Definition gpu_surface.hpp:112
Centralized parameter storage with lock-free value access.
Definition store.hpp:164
A granted-capability set.
Definition reload_capabilities.hpp:74
static CapabilitySet all()
Definition reload_capabilities.hpp:78
Definition query_service.hpp:51
Definition script_engine.hpp:22
Scrollable container that clips and scrolls its content.
Definition ui_components.hpp:494
Definition view.hpp:73
AssignmentSlot & operator=(View *view)
Definition widget_bridge.hpp:582
AssignmentSlot(BridgeWidgetState &state, std::vector< BridgeWidgetState > &owned_widgets)
Definition widget_bridge.hpp:578
void set_time_source(TimeSource source)
const std::filesystem::path & script_base_dir() const noexcept
Definition widget_bridge.hpp:212
DamageLedger damage_ledger() const
static std::string loaded_script_source(const std::string &code)
What load_script() evaluates for code – what a prewarm must compile.
void clear_for_realm_replacement(const DeadlineCheck &deadline_check={})
void set_active_script_id(const std::string &script_id)
Set the active script identifier without re-evaluating a script.
void clear_shader_uniform_bindings(const std::string &widget_id)
void set_allow_unreviewed_file_paths(bool allow) noexcept
Whether path-taking bridge APIs (setImageSource, registerFont, loadFont, sprite strips,...
Definition widget_bridge.hpp:223
void snapshot_values(std::unordered_map< std::string, float > &out) const
void install_runtime_import_handlers()
void set_repaint_callback(std::function< void()> cb)
void attach_gpu_surface(render::GpuSurface *gpu_surface)
const EmbeddedAsset * embedded_asset(std::string_view path) const noexcept
The embedded file a script-relative path names, or nullptr.
void restore_values(const std::unordered_map< std::string, float > &snapshot)
void set_ai_cli_command(std::string cmd)
void request_frame()
Ask for a frame without marking damage (View::request_frame()).
WidgetBridge(ScriptEngine &engine, View &root, state::StateStore &store, render::GpuSurface *gpu_surface=nullptr, CapabilitySet granted_capabilities=CapabilitySet::all())
void set_embedded_assets(std::vector< EmbeddedAsset > assets)
Install the files embedded beside the script.
std::string storage_identity() const
The identity the persisted stores hash: set_storage_identity()'s value when set, else the script base...
bool record_binding_attempt(const std::string &widget_id, const std::string &param_name, BindingTarget target, BindingOutcome outcome)
Record a binding attempt from an extension API such as shader uniforms.
std::function< std::chrono::steady_clock::time_point()> TimeSource
Clock for script timers, performance.now() and the rAF timestamp (default steady_clock::now; Date....
Definition widget_bridge.hpp:486
std::size_t event_binding_count() const noexcept
std::size_t param_subscription_count() const noexcept
const CssKeyframesRegistry & css_keyframes_registry() const
Definition widget_bridge.hpp:655
void set_value_channel_access(ValueChannelAccess access)
std::string_view bounded_realm_retirement_denial() const noexcept
Empty when the current tree can be retired by Runtime.evaluate's bounded reset; otherwise describes t...
std::vector< std::string > unbound_params() const
Declared parameters no attempt reached — the UI cannot change them.
void force_retire_root_for_owner_teardown() noexcept
static bool dispatch_key_for_root(View &root, int key_code, uint16_t modifiers, bool is_down)
Deliver a key only to the live bridge attached to root.
std::size_t owned_widget_identity_count() const noexcept
Definition widget_bridge.hpp:388
const std::string & active_script_id() const noexcept
Definition widget_bridge.hpp:262
void load_script(const std::string &code)
Load and execute a UI script.
const std::vector< BindingAttempt > & binding_attempts() const noexcept
Binding attempts in call order, bound or not (binding_diagnostics.hpp).
Definition widget_bridge.hpp:896
void clear(const DeadlineCheck &deadline_check={})
CssKeyframesRegistry & css_keyframes_registry()
Definition widget_bridge.hpp:654
static void dispatch_global_key(int key_code, uint16_t modifiers, bool is_down)
View * widget(const std::string &id)
bool allow_unreviewed_file_paths() const noexcept
Definition widget_bridge.hpp:226
CanvasWidget::NativeGpuTextureFrame describe_native_canvas_frame(const std::string &canvas_id) const
void set_imported_text_scale(float scale) noexcept
Multiplies every font size an imported UI asks for through setFontSize.
Definition widget_bridge.hpp:108
void set_host_kind(std::string kind)
Definition widget_bridge.hpp:481
std::chrono::steady_clock::time_point time_now() const
void forward_key_event(int key_code, uint16_t modifiers, bool is_down)
void restore_values(const WidgetReloadSnapshot &snapshot, const DeadlineCheck &deadline_check={}, bool include_custom_state=true)
const std::string & host_kind() const
Definition widget_bridge.hpp:482
bool permits_file_path(const std::filesystem::path &path) const
Whether a script may open path, an absolute filesystem path.
std::string embedded_asset_data_uri(const EmbeddedAsset &asset) const
data:<mime>;base64,... for an embedded file, which is how an image widget draws bytes that have no fi...
void set_asset_roots(std::vector< std::filesystem::path > roots)
Set reviewed local roots used by JS asset loading for relative URLs.
Definition widget_bridge.hpp:146
std::string font_weight_scope() const
Process-local identity for declarations owned by this bridge lifetime.
View * style_target(const std::string &id) noexcept
void set_storage_identity(std::string identity)
Identity of this bridge's persisted stores (widget storage, style presets).
Definition widget_bridge.hpp:191
void quarantine_realm() noexcept
std::size_t param_binding_count() const noexcept
Definition widget_bridge.hpp:386
void load_script(const std::string &code, const std::string &script_id)
load_script overload that retains a script identifier.
void request_repaint_after(const DamageLedger &ledger)
The repaint owed after a batch of script work that requested none itself, decided from what it marked...
void snapshot_values(WidgetReloadSnapshot &out, const DeadlineCheck &deadline_check={}, bool include_custom_state=true) const
void dispatch_native_message(std::string_view receiver, std::string_view type, const choc::value::Value &payload, std::string_view id={}, std::string_view context="native message dispatch")
Deliver one typed host-owned message to a named receiver in this UI realm.
float imported_text_scale() const noexcept
Definition widget_bridge.hpp:112
const std::vector< std::filesystem::path > & asset_roots() const noexcept
Definition widget_bridge.hpp:157
render::GpuSurface * gpu_surface() const noexcept
Definition widget_bridge.hpp:88
bool frame_pump_live() const noexcept
Definition widget_bridge.hpp:127
CanvasWidget::NativeGpuTextureFrame describe_native_texture_frame(const std::string &texture_id) const
std::size_t widget_cache_count() const noexcept
Definition widget_bridge.hpp:391
void set_value_channels(ValueChannelSet *channels)
Attach stable channels, or leased access for a reloadable processor.
int pending_runtime_settle_rounds() const noexcept
Definition widget_bridge.hpp:357
bool parameter_id_for_name(const std::string &name, state::ParamID &out) const
static void dispatch_document_event(const std::string &event_type, const std::string &event_json_literal)
CapabilitySet granted_capabilities() const noexcept
Definition widget_bridge.hpp:114
const std::vector< EmbeddedAsset > & embedded_assets() const noexcept
Definition widget_bridge.hpp:173
std::uint64_t bridge_call_count() const noexcept
Definition widget_bridge.hpp:398
std::string resolve_script_relative(const std::string &path) const
Resolve a file a script named against the script directory or a reviewed asset root,...
std::function< void()> DeadlineCheck
Definition widget_bridge.hpp:61
void set_script_base_dir(std::filesystem::path dir)
Preferred directory for resolving RELATIVE filesystem paths handed to path-taking bridge APIs.
Definition widget_bridge.hpp:206
bool has_native_gpu_bridge() const noexcept
friend struct BridgeRegistrars
Definition widget_bridge.hpp:1005
void visit_value_channels(const ValueChannelVisitor &visitor) const
The visitor must not retain a channel set or source after it returns.
ScrollView * scroll_wrapper(const std::string &id) const noexcept
void reset_bridge_call_count() noexcept
Definition widget_bridge.hpp:401
Input event types with modifier keys and pointer IDs.
std::function< void(ValueChannelSet *)> ValueChannelVisitor
Runs a value-channel operation while the hosting processor guarantees that the exposed set and all of...
Definition value_channel_set.hpp:52
std::function< void(const ValueChannelVisitor &)> ValueChannelAccess
Definition value_channel_set.hpp:53
SequencerStateChannel is the audio-safe transport for a plugin's structured NON-parameter state: the ...
Definition audio_device_manager.hpp:48
uint32_t ParamID
Unique parameter identifier, stable across plugin versions.
Definition parameter.hpp:55
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
@ path
Definition anchor_strategy.hpp:42
@ type
Built-in / common type name (e.g. int, Number).
Definition code_editor_tokenizer.hpp:39
BindingOutcome
The outcome of one bindWidgetToParam / bindMeter call.
Definition binding_diagnostics.hpp:26
BindingTarget
What a binding drives on its widget.
Definition binding_diagnostics.hpp:18
KeyCode
Definition input_events.hpp:35
@ v
Definition input_events.hpp:41
@ w
Definition input_events.hpp:41
@ candidate
Definition waveform_editor_primitives.hpp:193
@ scale
Definition css_animation.hpp:126
STL namespace.
Definition canvas_widget.hpp:222
Result of unpacking a Claude Design bundle's <script type="__bundler/manifest">.
Definition design_source_types.hpp:113
Application-wide registry for @keyframes blocks.
Definition css_animation.hpp:300
What the tree had marked when a batch of script work started, so the bridge can tell whether that wor...
Definition widget_bridge.hpp:463
std::uint64_t unmarked_mutations
Definition widget_bridge.hpp:465
std::uint64_t layout_generation
Definition widget_bridge.hpp:466
bool self_damaged(const View &root) const
std::uint64_t damage_requests
Definition widget_bridge.hpp:464
bool untouched(const View &root) const
One file compiled into the plug-in beside its editor script.
Definition widget_bridge.hpp:162
const unsigned char * data
Definition widget_bridge.hpp:164
std::string name
Definition widget_bridge.hpp:163
std::size_t size
Definition widget_bridge.hpp:165
Definition widget_bridge.hpp:48
float y
Definition widget_bridge.hpp:48
float x
Definition widget_bridge.hpp:48
Definition widget_bridge.hpp:46
std::unordered_map< std::string, std::string > custom_state
Definition widget_bridge.hpp:52
std::unordered_map< std::string, XYValue > xy_values
Definition widget_bridge.hpp:49
std::unordered_map< std::string, float > values
Definition widget_bridge.hpp:54
std::unordered_map< std::string, float > scalar_values
Definition widget_bridge.hpp:47
Source-compatible spelling of <pulp/dataplane/value_channel_set.hpp>.