EditorBridge Reference¶
pulp::view::EditorBridge is the renderer-agnostic JSON message
dispatcher that sits between a plugin editor (WebView panel today,
native JS runtime import lane tomorrow) and the C++ processor. Each plugin registers
its own per-message handlers; the framework owns the envelope parse,
the type→handler dispatch, the response builders, and a standard
error vocabulary.
The bridge keeps editor message dispatch shared across plugins so each editor does not need to reinvent envelope parsing, handler routing, and error response formatting.
- Header:
core/view/include/pulp/view/editor_bridge.hpp - Implementation:
core/view/src/editor_bridge.cpp - Tests:
test/test_editor_bridge.cpp
Envelope¶
Inbound JSON envelopes:
payload is optional. When omitted, handlers receive an empty
choc::value::Value object so they don't have to special-case the
absence of fields.
Responses are always one of:
Standard error vocabulary¶
dispatch_json(...) and dispatch(...) are noexcept and always
emit a well-formed response envelope. Envelope-level failures fall
into one of five categories. The on-the-wire error strings are
substring-compatible with existing plugin-editor tests so framework-level
dispatch can be adopted without changing plugin error assertions:
| Category | Trigger | On-the-wire substring |
|---|---|---|
malformed_json |
JSON parse failed, or root is not an object | "malformed JSON" / "envelope must be an object" |
missing_field |
Envelope has no type, or type is non-string / empty |
"envelope missing 'type'" |
unknown_type |
No handler registered for the given type |
"unknown message type" |
wrong_type |
Handler-emitted via err_response("...") for invalid payload values |
(handler chooses) |
internal_error |
Handler threw an exception | "internal error" |
Plugin-level handlers may use err_response(...) with any message;
the framework reserves the substrings above only for envelope-level
failures it emits itself.
API¶
namespace pulp::view {
class EditorBridge {
public:
using Handler = std::function<std::string(const choc::value::ValueView& payload)>;
EditorBridge();
~EditorBridge();
// Non-copyable AND non-movable. attach_webview / attach_native_runtime
// install callbacks that reference this bridge instance, so moving an
// attached bridge would dangle them. Construct in-place; static_asserts
// in the test suite lock this in.
EditorBridge(const EditorBridge&) = delete;
EditorBridge& operator=(const EditorBridge&) = delete;
EditorBridge(EditorBridge&&) = delete;
EditorBridge& operator=(EditorBridge&&) = delete;
// Registration.
void add_handler (std::string_view type, Handler fn);
void remove_handler(std::string_view type);
bool has_handler (std::string_view type) const noexcept;
std::size_t handler_count () const noexcept;
// Dispatch.
std::string dispatch (std::string_view type,
const choc::value::ValueView& payload) const noexcept;
std::string dispatch_json (std::string_view json) const noexcept;
std::string dispatch_webview_message(std::string_view type,
std::string_view payload_json) const noexcept;
// Renderer attach helpers.
void attach_webview (WebViewPanel& panel);
void detach_webview (WebViewPanel& panel);
void attach_native_runtime (JsRuntime& runtime, std::string_view handler_name);
// Static value-coercion helpers (never throw).
static float get_float (const choc::value::ValueView&, const char* key, float dflt) noexcept;
static std::size_t get_uint (const choc::value::ValueView&, const char* key, std::size_t dflt) noexcept;
static std::string get_string(const choc::value::ValueView&, const char* key) noexcept;
// Static response builders.
static std::string ok_response () noexcept;
static std::string ok_response (const choc::value::ValueView& extras) noexcept;
static std::string err_response(std::string_view msg) noexcept;
};
} // namespace pulp::view
attach_webview¶
Routes a WebViewPanel's structured message channel through this
bridge. Equivalent to:
panel.set_message_handler([this](const WebViewMessage& m) {
return dispatch_webview_message(m.type, m.payload_json);
});
dispatch_webview_message treats a payload_json of "null" (the
WebView default for "no payload") as an empty object so handlers see
the same shape regardless of whether the JS side passed a payload.
detach_webview¶
Clears the message handler installed by attach_webview. Call this
before tearing down a panel or detaching its native child view when the
bridge and panel are owned side-by-side and you want explicit teardown
ordering:
Calling detach_webview before an attach is safe; it is a no-op from
the caller's perspective.
attach_native_runtime¶
Stub interface for the Claude Design import lane. The full wiring lands when
JsRuntime exposes a postMessage-equivalent primitive that calls back into
C++. Defining the interface here keeps native-runtime editors on the same
dispatch model as WebView editors.
Usage example¶
#include <pulp/view/editor_bridge.hpp>
class MyEditor {
public:
void wire(pulp::view::WebViewPanel& panel) {
bridge_.add_handler("set_value", [this](const auto& payload) {
const auto v = pulp::view::EditorBridge::get_float(payload, "value", 0.0f);
apply_to_processor(std::clamp(v, 0.0f, 1.0f));
return pulp::view::EditorBridge::ok_response();
});
bridge_.add_handler("save_preset", [this](const auto&) {
const auto preset_json = serialize_state();
auto extras = choc::value::createObject("");
extras.addMember("preset_json", preset_json);
return pulp::view::EditorBridge::ok_response(extras);
});
bridge_.attach_webview(panel);
}
private:
pulp::view::EditorBridge bridge_;
};
The matching JS side (when running inside a Pulp WebView):
const resp = await __pulpPostMessage({ type: "set_value",
payload: { value: 0.42 } });
console.log(resp); // {"ok":true}
Non-goals (v1)¶
- No specific message types — every plugin owns its own schema.
- No drag-state helpers (
std::optional<DragSnapshot>-style). Capture per-session state on[this]in the handler closure instead. ADragBridgeadd-on may follow if the pattern becomes ubiquitous. - No C++ → JS push direction. That's a separate seam
(
panel_->execute_script()for WebView; runtime-specific for native JS) and deserves its own design pass.
Related¶
view-bridgeskill — editor lifecycle (create_view,open → notify_attached → resize → close)import-designskill — Claude Design imports + the CLI bridge-handler scaffold (pulp import-design --from claude --file <path>)core/format/include/pulp/format/view_bridge.hpp— the lifecycle bridge that wrapsProcessor::create_view()