Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
scripted_ui.hpp
Go to the documentation of this file.
1#pragma once
2
8#include <pulp/view/theme.hpp>
9#include <pulp/view/view.hpp>
11#include <cstdint>
12#include <filesystem>
13#include <functional>
14#include <memory>
15#include <optional>
16#include <string>
17#include <string_view>
18#include <unordered_map>
19
21
22namespace pulp::view {
23
24
26 std::filesystem::path script_path;
27 std::filesystem::path theme_path;
28 std::vector<std::filesystem::path> asset_roots;
29 bool enable_hot_reload = false;
52 std::string script_source;
55 std::string theme_source;
58 std::string script_name;
63 std::vector<WidgetBridge::EmbeddedAsset> embedded_assets;
67 std::string storage_identity;
76};
77
81#define PULP_VIEW_HAS_DEFERRED_DOCUMENT_LOAD 1
82
98 public:
99 ScopedDeferredDocumentLoad() noexcept : previous_(flag()) {
100 flag() = true;
101 }
103 flag() = previous_;
104 }
108 static bool active() noexcept {
109 return flag();
110 }
111
112 private:
113 // Inline: hosts open every embedded editor inside a guard, including
114 // AutoUi editors, and an out-of-line guard would link the script runtime
115 // into each of those plug-ins.
116 static bool& flag() noexcept {
117 static thread_local bool active = false;
118 return active;
119 }
120 bool previous_ = false;
121};
122
123// Manages a JS-driven widget tree, optional theme.json overrides, and
124// standalone hot reload semantics with widget value preservation.
126 public:
128 std::function<std::string(std::string_view envelope_json)>;
129
132
139 void release_root() noexcept override;
141 bool borrows_root(const View& view) const noexcept override {
142 return &root_ == &view;
143 }
144
147
152 bool load(std::string* error = nullptr);
159 bool load_deferrable(std::string* error = nullptr);
161 bool document_load_pending() const noexcept override {
162 return document_load_pending_;
163 }
164
169 bool settling() const noexcept override;
172 bool complete_pending_load(std::string* error = nullptr) override;
178 std::function<void(bool loaded, const std::string& error)> cb) {
179 document_loaded_callback_ = std::move(cb);
180 }
181
192 bool poll(std::string* error = nullptr) override;
193
194 // Explicitly reload the current script in place: rebuilds the widget bridge
195 // under the SAME root + GPU surface, preserving widget state, and probes the
196 // new code first so a bad reload keeps the last-good UI. The on-demand
197 // counterpart to enable_hot_reload's file-watched poll() — for a host/editor
198 // that wants to reload a just-edited bundle without a file watcher.
199 bool reload(std::string* error = nullptr);
200 // Repoint at a different script file and reload it (e.g. swap to another
201 // design bundle's ui.js). Updates script_path()/theme_path(); same in-place,
202 // last-good semantics as reload(). Does not re-arm the hot-reload watcher.
203 // An empty path returns a session built from `script_source` to that
204 // embedded source, which is how a swap transaction rolls it back.
205 bool reload_from(std::filesystem::path script_path, std::string* error = nullptr);
206
207 void set_repaint_callback(std::function<void()> cb);
213 void set_host_kind(std::string kind) override;
214 const std::string& host_kind() const { return host_kind_; }
230 void set_post_evaluation_reset_callback(std::function<void()> cb);
241 void attach_native_message_handler(std::string handler_name,
242 NativeMessageHandler handler);
247 void detach_native_message_handler(std::string_view handler_name);
255 std::uint64_t add_log_callback(LogCallback callback);
256 void remove_log_callback(std::uint64_t token);
258 return runtime_realm_quarantined_ ? nullptr : bridge_.get();
259 }
260
264 return bridge_ ? bridge_->granted_capabilities() : granted_capabilities_;
265 }
266
284 return inspector_port_.get();
285 }
286
287 std::uint64_t identity() const noexcept { return identity_; }
288
295 double probe_ms = 0.0;
296 double snapshot_ms = 0.0;
297 double rebuild_ms = 0.0;
298 double restore_ms = 0.0;
299 double total_ms = 0.0;
300 };
301 const ReloadMetrics& last_reload_metrics() const { return last_reload_metrics_; }
307 std::uint64_t probe_realm_evaluations() const noexcept {
308 return probe_realm_evaluations_;
309 }
310
311 double last_reload_ms() const { return last_reload_metrics_.total_ms; }
312
313 // Attach the host's GpuSurface so the JS-side navigator.gpu /
314 // canvas.getContext('webgpu') bridge routes through Pulp's live Dawn
315 // instance. The format adapters open this session
316 // BEFORE the PluginViewHost exists, so the surface arrives via this
317 // setter once the host is built (e.g. inside `au_view_controller_ios.mm`
318 // after `PluginViewHost::create`).
319 //
320 // Stored so that a hot-reload-triggered bridge rebuild reattaches the
321 // same surface to the new bridge. Pass `nullptr` to detach (called from
322 // the host's teardown before the bridge is destroyed).
324 render::GpuSurface* gpu_surface() const noexcept { return gpu_surface_; }
325
326 const std::filesystem::path& script_path() const { return script_path_; }
327 const std::filesystem::path& theme_path() const { return theme_path_; }
330 bool uses_embedded_source() const noexcept {
331 return script_path_.empty();
332 }
333
335 std::string script_label() const;
336 bool hot_reload_enabled() const { return hot_reload_enabled_; }
340 bool hot_reload_watching() const noexcept {
341 return reloader_ != nullptr;
342 }
343 bool hot_reload_pending() const {
344 return reloader_ && reloader_->has_pending_reload();
345 }
346 bool theme_reload_enabled() const { return theme_reload_enabled_; }
347
348private:
349 struct NativeMessageAttachment {
350 NativeMessageHandler handler;
351 };
352
353 const std::uint64_t identity_;
354 View& root_;
355 state::StateStore& store_;
356 std::filesystem::path script_path_;
357 std::filesystem::path theme_path_;
358 std::string script_source_;
359 std::string theme_source_;
360 std::string script_name_;
361 std::vector<WidgetBridge::EmbeddedAsset> embedded_assets_;
362 std::string storage_identity_;
363 std::string last_good_code_;
364 std::filesystem::path last_good_script_path_;
365 std::vector<std::filesystem::path> asset_roots_;
366 CapabilitySet granted_capabilities_ = CapabilitySet::all();
367 bool allow_unreviewed_file_paths_ = false;
368 bool hot_reload_enabled_ = false;
369 bool theme_reload_enabled_ = false;
370 bool runtime_import_enabled_ = false;
371 std::size_t js_memory_limit_bytes_ = kDefaultJsMemoryLimitBytes;
372 ValueChannelAccess value_channel_access_;
373
374 std::unique_ptr<ScriptEngine> engine_;
375 std::unique_ptr<WidgetBridge> bridge_;
376 struct RetiredRuntimeRealm {
377 std::unique_ptr<ScriptEngine> engine;
378 std::unique_ptr<WidgetBridge> bridge;
379 };
380 struct RuntimeRealmTeardownOwner {
381 std::vector<RetiredRuntimeRealm> retired;
382 std::unique_ptr<ScriptEngine> current_engine;
383 std::unique_ptr<WidgetBridge> current_bridge;
384 };
385 // Owner-thread evaluate may run inline more than once between UI polls, so
386 // retain every old realm until the next poll rather than overwriting one
387 // slot inside a later response fence.
388 std::vector<RetiredRuntimeRealm> retired_runtime_realms_;
389 // Allocated with the session rather than during its noexcept destructor.
390 // A reentrant native accessibility callback may retain this owner after the
391 // ScriptedUiSession itself has gone away.
392 std::shared_ptr<RuntimeRealmTeardownOwner> runtime_realm_teardown_owner_ =
393 std::make_shared<RuntimeRealmTeardownOwner>();
394 // Marshals off-thread inspector evaluate/interrupt requests onto the engine
395 // thread. Re-attached to the live engine after every rebuild_from_code().
396 // Null when no port factory was installed at construction.
397 std::unique_ptr<ScriptInspectorPort> inspector_port_;
398 std::unique_ptr<HotReloader> reloader_;
399 bool root_released_ = false;
400 std::function<void()> repaint_callback_;
401 std::string host_kind_ = "unknown";
402 WidgetBridge::TimeSource time_source_;
403 std::function<void()> post_evaluation_reset_callback_;
404 bool post_evaluation_reset_callback_pending_ = false;
405 LogCallback log_callback_;
406 std::unordered_map<std::uint64_t, LogCallback> log_subscribers_;
407 std::unordered_map<std::string, std::shared_ptr<NativeMessageAttachment>>
408 native_message_handlers_;
409 std::uint64_t next_log_subscription_ = 0;
410 render::GpuSurface* gpu_surface_ = nullptr;
411
412 Theme base_theme_;
413 Theme last_good_effective_theme_;
414 bool runtime_realm_quarantined_ = false;
415 bool accessibility_retirement_pending_ = false;
416 ReloadMetrics last_reload_metrics_{}; // JS-axis reload timings (item 1.2)
417 std::uint64_t probe_realm_evaluations_ = 0;
418 bool document_load_pending_ = false;
419 int deferred_load_polls_ = 0;
420 std::function<void(bool, const std::string&)> document_loaded_callback_;
421 bool load_now(std::string* error);
422 static constexpr double kBlockingLoadWarningMs = 50.0;
423 bool last_theme_exists_ = false;
424 std::optional<std::filesystem::file_time_type> last_theme_write_time_;
425
428 std::string current_script_code(std::string* error) const;
429 bool rebuild_from_code(
430 const std::string& code, const std::filesystem::path& source_path, bool preserve_state,
431 std::string* error,
432 std::optional<ScriptInspectorPort::EvaluationDeadline> deadline = std::nullopt);
433 bool apply_theme_override(std::string* error);
434 // Read + parse the sibling theme override onto `base` WITHOUT mutating any
435 // live state — the FALLIBLE half of a theme apply, split out so a reload can
436 // validate the theme BEFORE the irreversible bridge commit (rollback-safety,
437 // item 1.5). Fills out_* and returns false (with `error`) on a bad theme file.
438 bool resolve_theme_override(const Theme& base, Theme& out_merged, bool& out_exists,
439 std::optional<std::filesystem::file_time_type>& out_write_time,
440 std::string* error) const;
441 bool poll_theme_reload(std::string* error);
442 void attach_inspector_port(ScriptEngine* engine) {
443 if (!inspector_port_)
444 return;
445 // The grant precedes the engine so the port never reports an
446 // evaluable realm it has not yet been told about.
447 inspector_port_->set_realm_capabilities(granted_capabilities_);
448 inspector_port_->attach(engine);
449 }
450 void detach_inspector_port() {
451 if (inspector_port_)
452 inspector_port_->detach();
453 }
454 std::string reset_after_runtime_evaluation(ScriptInspectorPort::EvaluationDeadline deadline);
455 LogCallback engine_log_callback();
456 void install_native_message_handlers(ScriptEngine& engine,
457 bool validation_realm) const;
458 static void install_native_message_handler(
459 ScriptEngine& engine, const std::string& name,
460 std::shared_ptr<NativeMessageAttachment> attachment,
461 bool validation_realm);
462 void dispatch_log(std::string_view level, std::string_view message);
463
464 static std::string read_text_file(const std::filesystem::path& path);
465 static std::string describe_exception();
466};
467
468} // namespace pulp::view
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
static bool active() noexcept
Whether a guard is alive on this thread.
Definition scripted_ui.hpp:108
~ScopedDeferredDocumentLoad()
Definition scripted_ui.hpp:102
ScopedDeferredDocumentLoad() noexcept
Definition scripted_ui.hpp:99
ScopedDeferredDocumentLoad(const ScopedDeferredDocumentLoad &)=delete
ScopedDeferredDocumentLoad & operator=(const ScopedDeferredDocumentLoad &)=delete
The engine-thread half of a runtime inspector, as ScriptedUiSession drives it.
Definition script_inspector_port.hpp:22
std::chrono::steady_clock::time_point EvaluationDeadline
Definition script_inspector_port.hpp:24
bool theme_reload_enabled() const
Definition scripted_ui.hpp:346
const ReloadMetrics & last_reload_metrics() const
Definition scripted_ui.hpp:301
void set_log_callback(LogCallback callback)
Replace the live JS console sink and retain it across hot reloads.
std::uint64_t add_log_callback(LogCallback callback)
Add a secondary console observer without replacing the primary sink.
bool uses_embedded_source() const noexcept
True when the document comes from ScriptedUiOptions::script_source rather than a file on disk.
Definition scripted_ui.hpp:330
bool document_load_pending() const noexcept override
True between a deferred load() and the poll() that evaluates it.
Definition scripted_ui.hpp:161
void set_document_loaded_callback(std::function< void(bool loaded, const std::string &error)> cb)
Called when a load() – immediate or deferred – finishes, with whether the document mounted and the er...
Definition scripted_ui.hpp:177
ScriptedUiSession(const ScriptedUiSession &)=delete
ScriptInspectorPort * script_inspector_port()
The runtime-inspector port for this session's JS engine, or nullptr.
Definition scripted_ui.hpp:283
bool borrows_root(const View &view) const noexcept override
True when view is this session's root.
Definition scripted_ui.hpp:141
bool reload_from(std::filesystem::path script_path, std::string *error=nullptr)
std::uint64_t probe_realm_evaluations() const noexcept
How many times this session evaluated a script on a throwaway probe realm.
Definition scripted_ui.hpp:307
std::string script_label() const
What log lines call this document: the script path, or the embedded script's name.
void set_time_source(WidgetBridge::TimeSource source)
The clock script timers and performance.now() read (see WidgetBridge::set_time_source).
ScriptedUiSession(View &root, state::StateStore &store, ScriptedUiOptions options)
void attach_gpu_surface(render::GpuSurface *gpu_surface) override
See ScriptedUiSession::attach_gpu_surface().
WidgetBridge * bridge() const
Definition scripted_ui.hpp:257
bool reload(std::string *error=nullptr)
bool hot_reload_pending() const
Definition scripted_ui.hpp:343
bool complete_pending_load(std::string *error=nullptr) override
Evaluate a deferred document now (a harness that needs the mounted tree without pumping frames).
void release_root() noexcept override
Tear down everything that borrows the root – the realm, its engine and bridge, and the accessibility ...
bool load(std::string *error=nullptr)
Evaluate the script and mount it into the root now; bridge() is live when it returns true.
ScriptedUiSession & operator=(const ScriptedUiSession &)=delete
void attach_native_message_handler(std::string handler_name, NativeMessageHandler handler)
Attach a renderer-neutral native message endpoint to this session's JS realm.
const std::filesystem::path & theme_path() const
Definition scripted_ui.hpp:327
bool poll(std::string *error=nullptr) override
Advance one frame: discharge the realm reset a completed Runtime.evaluate owes, retire and destroy re...
void remove_log_callback(std::uint64_t token)
void detach_native_message_handler(std::string_view handler_name)
Remove a named native message endpoint from this and future realms.
bool hot_reload_watching() const noexcept
True once a loaded session owns a file watcher.
Definition scripted_ui.hpp:340
bool hot_reload_enabled() const
Definition scripted_ui.hpp:336
CapabilitySet granted_capabilities() const noexcept
Actual effectful API grants installed in the live bridge, or the grants that will be installed before...
Definition scripted_ui.hpp:263
bool load_deferrable(std::string *error=nullptr)
View-first load.
bool settling() const noexcept override
True while the mounted document still owes host frames to settle its commits (see WidgetBridge::pendi...
void set_host_kind(std::string kind) override
Declare where this editor lives, as its scripts read it through hostKind() ("plugin",...
const std::string & host_kind() const
Definition scripted_ui.hpp:214
double last_reload_ms() const
Convenience: total wall-clock of the last reload, ms.
Definition scripted_ui.hpp:311
std::function< std::string(std::string_view envelope_json)> NativeMessageHandler
Definition scripted_ui.hpp:127
void set_post_evaluation_reset_callback(std::function< void()> cb)
Install the host-owned rebind run after a successful deferred Runtime.evaluate realm reset.
std::uint64_t identity() const noexcept
Process-unique identity for borrowed-source lifetime/ABA checks.
Definition scripted_ui.hpp:287
void set_repaint_callback(std::function< void()> cb)
const std::filesystem::path & script_path() const
Definition scripted_ui.hpp:326
render::GpuSurface * gpu_surface() const noexcept
Definition scripted_ui.hpp:324
Definition view.hpp:73
Definition widget_bridge.hpp:59
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::function< void(const ValueChannelVisitor &)> ValueChannelAccess
Definition value_channel_set.hpp:53
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
@ path
Definition anchor_strategy.hpp:42
constexpr std::size_t kDefaultJsMemoryLimitBytes
Definition js_engine.hpp:79
std::function< void(std::string_view level, std::string_view message)> LogCallback
Definition js_engine.hpp:25
@ error
Definition design_ir.hpp:697
STL namespace.
Definition scripted_ui.hpp:25
std::vector< WidgetBridge::EmbeddedAsset > embedded_assets
Files compiled into the image beside an in-memory script.
Definition scripted_ui.hpp:63
std::vector< std::filesystem::path > asset_roots
Definition scripted_ui.hpp:28
std::size_t js_memory_limit_bytes
Heap ceiling for every realm this session creates, in bytes (0 = unlimited).
Definition scripted_ui.hpp:71
bool enable_runtime_import
Install the native runtime-design import endpoint before evaluating the session bundle.
Definition scripted_ui.hpp:46
ValueChannelSet * value_channels
Compatibility adapter for stable, non-reloadable processors.
Definition scripted_ui.hpp:33
bool allow_unreviewed_file_paths
Let path-taking bridge APIs open files outside the script directory and asset_roots (WidgetBridge::se...
Definition scripted_ui.hpp:75
std::filesystem::path script_path
Definition scripted_ui.hpp:26
std::filesystem::path theme_path
Definition scripted_ui.hpp:27
std::string theme_source
Theme override JSON applied over the root theme when theme_path is empty and script_source is in use.
Definition scripted_ui.hpp:55
bool enable_theme_reload
Definition scripted_ui.hpp:30
std::string script_source
In-memory script source.
Definition scripted_ui.hpp:52
std::string storage_identity
Identity of the session's persisted stores (widget storage, style presets) when the script has no dir...
Definition scripted_ui.hpp:67
CapabilitySet granted_capabilities
Native bridge APIs granted to this realm.
Definition scripted_ui.hpp:41
bool enable_hot_reload
Definition scripted_ui.hpp:29
ValueChannelAccess value_channel_access
Leased access to the hosting processor's named value channels.
Definition scripted_ui.hpp:36
std::string script_name
Diagnostic name for an in-memory script (ui.js); log lines and script_label() use it where a path-bac...
Definition scripted_ui.hpp:58
JS-axis reload timings, ms.
Definition scripted_ui.hpp:294
double snapshot_ms
snapshot the live widget values before the rebuild
Definition scripted_ui.hpp:296
double probe_ms
parse + build a probe bridge to validate the code
Definition scripted_ui.hpp:295
double restore_ms
restore preserved widget values into the new tree
Definition scripted_ui.hpp:298
double total_ms
end-to-end
Definition scripted_ui.hpp:299
double rebuild_ms
build the live bridge from the new code + apply theme
Definition scripted_ui.hpp:297