Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
design_frame_view.hpp
Go to the documentation of this file.
1#pragma once
2
5#include <pulp/view/view.hpp>
6
7#include <functional>
8#include <memory>
9#include <string>
10#include <unordered_map>
11#include <vector>
12
13namespace pulp::view {
14
15// Held by DesignFrameView behind a unique_ptr via forward declaration so this
16// header — included by 15+ others — does not pull in the collaborator's own
17// includes. Defined in design_param_binding.hpp.
19
20// One interactive element overlaid on a faithfully-rendered design frame. The
21// element list is TYPED and supplied by the importer (source-side semantics) —
22// DesignFrameView does NOT guess from the SVG. Bounds/coords are in the SVG's own
23// coordinate space. Real behavior comes from source metadata, not SVG structure.
25 // `knob` is SVG-patch (rotates its needle path in the SVG). `text_field` /
26 // `dropdown` / `tab_group` / `stepper` are NATIVE-OVERLAY: an opaque child
27 // widget is positioned over the element's rect and replaces that baked SVG
28 // region. `stepper` is a header value cycled in place by `< >` chevrons.
29 // `momentary` is a press/release primitive (keys, pads, drum triggers,
30 // sustain, transport): on_gesture_begin(i)=press, on_gesture_end(i)=release;
31 // set_element_value(i,1/0) lights it via a NATIVE overlay, not SVG mutation.
32 // `swap` is a SWAP-LINK button: clicking its rect calls set_active_frame
33 // (target_frame) — the importer's `swap` link (e.g. a mode toggle). It does
34 // not light or emit notes; it changes which frame the view renders.
35 // `action` is a momentary command button: clicking its rect fires
36 // on_action(action) — an in-design control (octave −/+, velocity, sustain,
37 // pitch-bend preset). It does not light, emit notes, or swap frames; it
38 // names an action the consumer maps to its own state.
39 // `value_label` is a live read-only text overlay: it paints `text` over its
40 // rect (replacing a baked readout glyph that build suppresses), updated via
41 // set_element_text — e.g. an "OCTAVE C2" value that must track state.
42 // `fader` is SVG-patch like `knob` but TRANSLATES its thumb element
43 // (needle_d) by value over the track. Orientation follows the track shape:
44 // a wider-than-tall rect (w>h) is a HORIZONTAL slider (value 0→left x, 1→
45 // right x+w; cx = baked center); otherwise vertical (value 1→top y, 0→bottom
46 // y+h; cy = baked center). `toggle` is a click-to-flip button that tints its rect
47 // (bg_color, value>=0.5=on) over the baked chrome so the label shows through.
48 // A toggle with `needle_d` set is a SWITCH: the dot (needle_d) sits at e.cx in
49 // its OFF state and slides to the mirror across the pill center (x + w/2) when
50 // on, in addition to the tint — so the design's rest state is preserved.
51 // `xy_pad` is SVG-patch like `fader` but in 2D: dragging inside its rect
52 // [x,y,w,h] moves the puck element (needle_d) to follow — `value` is the X
53 // position (0→left, 1→right), `value_y` the Y (0→top, 1→bottom). cx/cy = the
54 // puck's baked center.
55 // `custom` is a registered native control: the overlay is built by a factory
56 // looked up under `factory_id` in the design-control registry
57 // (register_design_control_factory). If no factory is registered the element
58 // renders inert (the baked SVG underneath always shows) and the importer
59 // diagnoses it — a custom control never blanks or silently mis-renders.
63
65
66 // ── knob (SVG-patch) ─────────────────────────────────────────────────
67 float cx = 0.0f;
68 float cy = 0.0f;
69 float hit_radius = 0.0f;
70 // Knob: the `d` of its needle path in the source SVG. Dragging rotates that
71 // path around (cx, cy) by the value angle and re-renders — only the needle
72 // moves; the rest of the chrome stays pixel-exact.
73 std::string needle_d;
74 float value = 0.5f;
75 float value_y = 0.5f;
79 bool flash = false;
83 bool enabled = true;
84
85 // ── overlay controls (text_field / dropdown / tab_group / stepper) ────
86 float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
87 std::string placeholder;
88 std::vector<std::string> options;
94 std::string bg_color;
95
96 // ── momentary (keys / pads) ──────────────────────────────────────────
101 int note = -1;
105 int view_group = -1;
106
107 // ── swap (swap-link button) ──────────────────────────────────────────
110 int target_frame = -1;
111
112 // ── action (command button) ──────────────────────────────────────────
115 std::string action;
116
117 // ── value_label (live readout) ───────────────────────────────────────
121 std::string text;
126 bool value_left_align = false;
127
128 // ── custom (registered native control) ───────────────────────────────
131 std::string factory_id;
134 std::string custom_props;
135
136 // ── provenance (design-import) ───────────────────────────────────────
142 std::string source_node_id;
143
144 // ── host-parameter binding (foreign-host embed) ──────────────────────
156 std::string param_key;
157
180 std::string display_text;
184 bool display_text_bound = false;
185};
186
187// ── Custom-control factory registry ──────────────────────────────────────────
188// Runtime name→View table for genuinely novel controls in imported designs. A
189// host or shared package registers a factory under an id; the importer emits a
190// Kind::custom element carrying that id; DesignFrameView builds the overlay by
191// looking the factory up.
192
193// Geometry + opaque props handed to a custom-control factory so it can build and
194// bind its View. Panel coordinates (the same space DesignFrameView positions
195// overlays in).
197 float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
198 std::string factory_id;
199 std::string props;
200 std::string source_node_id;
201 float default_value = 0.5f;
206};
207
208// Builds a native overlay View for a Kind::custom element. Returns the View
209// (ownership moves to the DesignFrameView) or nullptr to render inert.
211 std::function<std::unique_ptr<View>(const DesignControlContext&)>;
212
213// Opt-in value conduit for a custom-control overlay. A factory-built View MAY
214// also inherit DesignControl to join DesignFrameView's normalized-value channel
215// — exactly as the built-in kinds do, so a custom knob behaves like a knob to
216// the host: set_element_value() pushes host->view, element_value() reads back,
217// and user edits route through on_element_changed + host-param binding. A View
218// that does NOT inherit DesignControl is still built and rendered; it simply has
219// no value channel (element_value() stays -1, set_element_value() is a no-op),
220// which is the pre-conduit behavior. Retrieved via dynamic_cast, mirroring the
221// DropReceiver mixin idiom used elsewhere in core/view.
223public:
224 virtual ~DesignControl() = default;
225 // Host -> view: adopt the normalized value (0..1). SILENT — implementations
226 // MUST NOT re-emit through on_control_changed (this is the host pushing a
227 // value in, not the user editing), matching the silent host->view push the
228 // other kinds do in DesignFrameView::set_element_value.
229 virtual void set_control_value(float value) = 0;
230 // View -> host: the control's current normalized value (0..1).
231 virtual float control_value() const = 0;
232 // Installed by DesignFrameView when the overlay is built. Implementations
233 // call it on a USER-driven value change so the edit funnels through the
234 // frame's single change path (on_element_changed, param_key host routing).
235 // Never call it from set_control_value.
236 std::function<void(float)> on_control_changed;
237 // Optional undo / touch-automation bracketing for a CONTINUOUS control (a
238 // knob/fader): call on_gesture_begin at the start of a drag and on_gesture_end
239 // at its end so the host groups one undo step and latches automation write —
240 // exactly what the built-in knob does. A momentary/discrete control may leave
241 // these unset. DesignFrameView wires them to its own gesture callbacks
242 // (→ host begin_gesture / end_gesture). Both are installed AFTER the initial
243 // seed, so they never fire during construction.
244 std::function<void()> on_gesture_begin;
245 std::function<void()> on_gesture_end;
246};
247
248// Register / query a custom-control factory by id. UI-THREAD-ONLY: registration
249// happens at host startup and lookup at overlay build, both on the UI thread, so
250// the registry needs no locking. Re-registering an id replaces the prior factory.
251//
252// LIFETIME: the factory is stored for the process lifetime (until replaced or
253// clear_design_control_factories()). It must remain callable for that whole time,
254// so do NOT capture by reference/pointer any state that outlives the registrant —
255// register a free function or a lambda over owned/static state. A factory
256// capturing stack locals is a latent use-after-free the moment those locals go
257// out of scope. (The factory itself runs on the UI thread, synchronously, from
258// DesignFrameView::build_overlays.)
259void register_design_control_factory(std::string factory_id,
260 DesignControlFactory factory);
261bool has_design_control_factory(const std::string& factory_id);
262// Test/teardown helper: drop all registered factories.
264
265// A Kind::custom element whose `factory_id` had no registered factory when the
266// overlay was built, so it rendered inert (the baked SVG still shows). Surfaced
267// via DesignFrameView's opt-in diagnostic so a developer learns which ids were
268// referenced-but-unregistered (a forgotten register_design_control_factory or a
269// typo'd id) instead of getting a silently missing control.
271 std::string factory_id;
272 std::string source_node_id;
273};
274
275// A control whose VISIBLE option count disagrees with the value cardinality the
276// HOST reports for the parameter it is bound to (HostParamSurface::
277// param_step_count). Surfaced via DesignFrameView's opt-in diagnostic.
278//
279// This is the mis-scale that a hand-written control table produces silently: a
280// radio drawn with 3 positions, wired to a 6-value parameter, normalizes against
281// whichever count the code happened to reach for. The host's count is the
282// authoritative one and DesignFrameView uses it — but the disagreement itself is
283// still a defect in the binding (the design draws 3 of 6 reachable values, so
284// half the parameter's range has no control position), and it is reported rather
285// than silently absorbed. Same never-guess contract the import lane's
286// RecognitionResolver holds for an unmatched component.
287// Read the FIELDS — the arrival of a report is not by itself the diagnosis:
288//
289// ui host knows meaning
290// --- ----- ----- ---------------------------------------------------------
291// 3 6 yes a MIS-SCALED control: the design draws 3 of 6 reachable
292// values. The host's 6 is used; half the range has no
293// control position.
294// 3 0 NO an UNRESOLVED KEY on a LIVE host: the element names a
295// parameter this host does not carry — a stale or renamed
296// key in a ported control table. The element is bound to
297// NOTHING: it never syncs from the host, its edits never
298// route, and it scales by the 3 positions it draws. That
299// last part is the reported porting defect verbatim (a
300// 3-position control emitting idx/2 instead of idx/5), so
301// it is never silent. Check the key's spelling first.
302// 3 0 yes UNANSWERED, and the view had to GUESS by the 3 it draws.
303// Either the parameter is genuinely continuous (a coarse
304// choice control over a smooth range — fine, expected) or
305// the surface cannot answer: do_param_step_count defaults
306// to 0, so a surface that has not wired it reports 0 for
307// every key, discrete ones included. Those two are
308// indistinguishable from here, which is why this is
309// reported instead of silently absorbed.
310// 0 6 yes an UNBOUND key: a commit to a key no element carries.
311// 0 0 no a key nothing knows.
313 std::string param_key;
317 bool host_has_param = false;
324};
325
326// Remove the first <rect> in `svg` whose x/y/width/height match (within `tol`)
327// the given box, returning true if one was erased. Used to suppress a design's
328// BAKED selected-tab highlight so the live overlay's pill is the only one shown
329// (no double-pill when the selection moves). Pure geometry — no per-design data.
330bool suppress_svg_rect(std::string& svg, float x, float y, float w, float h,
331 float tol = 2.0f);
332
333// Remove a filtered group (`<g filter="url(#...)">…</g>`) whose first drawn
334// coordinate falls inside the box [x, x+w] × [y, y+h], returning true if one was
335// erased. Figma bakes the SELECTED tab's digit with a soft glow (a large-blur
336// drop-shadow filter); the live pill moves on click but that baked glow stays
337// stuck on the originally-selected digit. Removing the filtered group at the
338// originally-selected cell lets the live overlay own the digit cleanly. Pure
339// geometry — no per-design data.
340bool suppress_svg_glow_at(std::string& svg, float x, float y, float w, float h);
341
342// Remove the first standalone `<path …/>` whose first drawn coordinate falls
343// inside the box [x, x+w] × [y, y+h], returning true if one was erased. Used to
344// drop a BAKED tab digit glyph so the live DesignTabGroup is the sole renderer
345// of the digits (no faint "doubled" glyph where the live label paints over the
346// baked one). Pure geometry — no per-design data.
347bool suppress_svg_glyph_at(std::string& svg, float x, float y, float w, float h);
348
349// Renders a design's own SVG document pixel-faithfully via Canvas::draw_svg
350// (Skia SkSVGDOM), cropped to its panel, and overlays native interaction from a
351// typed element list. This is the faithful-vector design-import lane's view: the
352// importer materializes one of these per imported frame.
353//
354// Each drag patches only the dragged knob's needle in the SVG and repaints.
355// Canvas::draw_svg parses the SVG into an SkSVGDOM keyed on the document bytes
356// (process-wide SvgDomCache), so a repaint of an UNCHANGED document — static
357// chrome, or a frame whose only motion is a native-overlay child — reuses the
358// parsed DOM instead of rebuilding it. A render-patched document (rotated knob
359// needle, translated fader/xy_pad thumb, toggle-switch slide) is a different
360// string, so it misses the cache and rebuilds, keeping the dragged element
361// live. The rasterized output is byte-identical to the uncached path; only the
362// parse step is skipped on a hit.
363class DesignFrameView : public View {
364public:
365 // `svg` is the full SVG document. The panel (the design body the window
366 // should show edge-to-edge) is auto-detected as the largest <rect>; pass a
367 // positive panel_* to override. `elements` are the interactive overlays.
368 DesignFrameView(std::string svg, std::vector<DesignFrameElement> elements,
369 float panel_x = -1, float panel_y = -1,
370 float panel_w = -1, float panel_h = -1);
371
372 // Declared (not defaulted inline) so the unique_ptr<DesignParamBinding>
373 // member can hold an incomplete type here and be destroyed where the type is
374 // complete (design_frame_view.cpp). Out-of-line = default.
376
377 int element_count() const { return static_cast<int>(elements_.size()); }
378 float panel_width() const { return panel_w_; }
379 float panel_height() const { return panel_h_; }
380 // Kind of element `i` (knob / dropdown / tab_group / stepper / text_field),
381 // or knob if out of range. Lets a binder treat knobs as continuous params
382 // and dropdown/tab/stepper as normalized-index "choice" params.
384 // Raw note number of momentary element `i` (typing = relative semitone 0–15,
385 // piano = absolute MIDI), or -1. Consumers map by this, not positional index.
386 int element_note(int i) const {
387 return (i >= 0 && i < static_cast<int>(elements_.size())) ? elements_[i].note : -1;
388 }
389 // Panel-coord rect (x, y, w, h) of element `i`, or {0,0,0,0}. Lets a subclass
390 // position its own overlay relative to an element (e.g. the piano C-labels
391 // drawn under the C keys, which shift as the window moves).
392 Rect element_rect(int i) const;
393 // The point, in this view's LOCAL coordinates, at which a pointer event lands
394 // on element `i` — writes it to `out` and returns true, or returns false and
395 // leaves `out` untouched when no such point exists. Dispatching on_mouse_down()
396 // at a reported point hits element `i`.
397 //
398 // This is the INVERSE of hit_element(): it forward-maps the element's hit
399 // anchor through the same panel_transform() that hit_element() inverts, and
400 // picks that anchor the way hit_element() matches each kind — a knob by
401 // distance to its pivot (cx, cy), every other kind by containment in its rect.
402 // Both facts live behind this view's private state (the pivot, the panel crop
403 // origin, and the fit itself), so a caller outside the class cannot derive
404 // this point; re-deriving it against a copy of the fit would be a second
405 // source of truth that drifts from paint(). It exists so a foreign-host
406 // adapter or QA harness can drive a NAMED control through the REAL pointer
407 // path — hit-test included — instead of reaching past hit-testing to poke the
408 // element directly, which would pass even with the hit path broken.
409 //
410 // Returns false — rather than a nearby-looking coordinate — whenever the
411 // element is genuinely unclickable: `i` out of range, the panel not laid out
412 // yet, a disabled element, a momentary key outside the active view group, a
413 // degenerate hit radius or rect, an element occluded by a rect-tested one
414 // ahead of it, and the non-interactive kinds (a value_label readout, a custom
415 // control whose factory never registered). Check the return value: a false
416 // means "there is no such point", never "use (0,0)".
417 bool element_hit_point(int i, Point& out) const;
418 // The `action` id of element `i` (for Kind::action command buttons and the
419 // readout tag of Kind::value_label), or empty. Lets a consumer route by id.
420 const std::string& element_action(int i) const {
421 static const std::string kEmpty;
422 return (i >= 0 && i < static_cast<int>(elements_.size())) ? elements_[i].action : kEmpty;
423 }
424 // Number of discrete options element `i` offers (dropdown entries, tab_group
425 // tabs, stepper positions), or 0 when the element is continuous (knob /
426 // fader / xy_pad) or out of range. A toggle reports 2 (off/on) even though it
427 // declares no `options` list. Non-zero exactly when element_is_discrete(i).
428 //
429 // This is what the control DRAWS, which is the element's normalized divisor
430 // only when no host parameter says otherwise. When a HostParamSurface
431 // resolves the element's param_key and reports a non-zero cardinality, THAT
432 // count wins (see choice_to_norm): a parameter owns its own range, and a
433 // control drawn with fewer positions than the parameter has values does not
434 // shrink it. The two disagreeing is reported via set_on_param_scale_mismatch.
435 int element_option_count(int i) const;
436 // Whether element `i` carries a finite set of positions (dropdown / tab_group
437 // / stepper / toggle) rather than a continuous range. A toggle is discrete
438 // with two positions even though it declares no `options` list.
439 bool element_is_discrete(int i) const;
440 // The Y axis (0=top, 1=bottom) of an xy_pad element `i`, or 0.5. The X axis is
441 // element_value(i).
442 float element_value_y(int i) const {
443 return (i >= 0 && i < static_cast<int>(elements_.size())) ? elements_[i].value_y : 0.5f;
444 }
445 // The frame index a Kind::swap element activates on click, or -1 if unset.
446 int element_target_frame(int i) const {
447 return (i >= 0 && i < static_cast<int>(elements_.size())) ? elements_[i].target_frame : -1;
448 }
449 // The readout string of a Kind::value_label element `i`, or empty.
450 const std::string& element_text(int i) const {
451 static const std::string kEmpty;
452 return (i >= 0 && i < static_cast<int>(elements_.size())) ? elements_[i].text : kEmpty;
453 }
454 // ── Paint-safe host readout ──────────────────────────────────────────────
455 // The host-formatted display text of element `i`'s bound parameter — e.g.
456 // "500 ms", "-6.0 dB", "Sine" — as of the last sync_from_host_params().
457 //
458 // This is the READ half of the host readout channel, and the ONLY one legal
459 // from paint(). HostParamSurface::param_display_text() calls the host's own
460 // formatter (arbitrary code, possibly holding locks shared with the audio
461 // thread) and returns a fresh std::string; both make it illegal mid-render,
462 // which is why the surface asserts on a call from a no-alloc scope. So the
463 // one host round-trip happens at tick — sync_from_host_params() caches every
464 // bound element's text — and paint reads the cache.
465 //
466 // Paint-safe: bounds `i` and returns a reference to the cached string. No
467 // host call, no lock, no allocation, no copy. The reference is valid until
468 // the next sync_from_host_params(), a frame swap, or a build_bind_grid()
469 // rebuild — each replaces or reallocates elements_. A painter reads it and
470 // draws; it does not store it, and it does not cache the INDEX across those
471 // either (see element_for_param_key).
472 //
473 // Returning a reference rather than publishing a fixed-capacity frame is the
474 // same call `text` / element_text() already make: both fields live in the
475 // same elements_ vector, are written by the same tick, and are read by the
476 // same paint. Whatever is true of one is true of the other, so a TripleBuffer
477 // on this one and a bare reference on the field beside it would be
478 // incoherent. What makes it safe is not that a tick is somehow guaranteed to
479 // be on the paint thread — nothing here enforces that, which is why
480 // sync_from_host_params() asserts it in debug — but that the ONE writer and
481 // the ONE reader are the same UI thread by contract. (Contrast
482 // MeterSource/ScalarSource, whose producer genuinely IS the audio thread and
483 // which therefore ride a TripleBuffer of fixed-capacity frames. That is the
484 // shape this would need if the tick ever moved off the UI thread — and
485 // display_text is the field that would hurt, since a >SSO string reassigned
486 // under a concurrent reader is a use-after-free, not a torn value.)
487 //
488 // The CACHE does not truncate and carries no length cap: it holds whatever
489 // string the host returned, verbatim, so the cache is never where a readout
490 // becomes a partial lie. That is a claim about this cache only, not about the
491 // whole path — a surface's own formatter may cap before the text ever
492 // arrives (StateStoreHostParamSurface's no-formatter fallback builds through
493 // a char[64], so its default numeric+unit rendering is bounded at 63 chars).
494 //
495 // Every host parameter has an element once build_bind_grid() has run, so
496 // `element_for_param_key(key)` + this is a complete keyed readout for a
497 // parameter the design draws no control for — the shape a rack/chain UI
498 // painting its own per-slot readouts needs.
499 //
500 // Returns empty when `i` is out of range, when the element carries no
501 // param_key, when no HostParamSurface resolves that key, or before the first
502 // sync. Empty is therefore ambiguous on its own — a host may legitimately
503 // format a value AS an empty string. Gate on element_has_display_text(i)
504 // when the difference matters.
505 const std::string& element_display_text(int i) const {
506 static const std::string kEmpty;
507 return (i >= 0 && i < static_cast<int>(elements_.size())) ? elements_[i].display_text
508 : kEmpty;
509 }
510
511 // Whether element `i`'s display text came from a host parameter at the last
512 // sync — i.e. the element carries a param_key that a HostParamSurface
513 // resolved. False for an out-of-range index, an element with no param_key, a
514 // key no surface resolves, and any element before the first
515 // sync_from_host_params(). Paint-safe, same as element_display_text().
516 //
517 // A key the host stops resolving is CLEARED at the next sync rather than
518 // left at its last value: a readout that keeps painting the text of a
519 // parameter that no longer exists is a stale lie, and unbound must read as
520 // unbound.
521 bool element_has_display_text(int i) const {
522 return i >= 0 && i < static_cast<int>(elements_.size()) &&
523 elements_[i].display_text_bound;
524 }
525
526 // Whether a Kind::value_label element `i` left-aligns its readout.
527 bool element_left_align(int i) const {
528 return (i >= 0 && i < static_cast<int>(elements_.size())) && elements_[i].value_left_align;
529 }
530 // The design-source node id of element `i` (e.g. a Figma node id), or empty
531 // when the element wasn't lowered from a design import. The inspector's
532 // Wiring lens reads this to map a live control back to its design node.
533 const std::string& element_source_node_id(int i) const {
534 static const std::string kEmpty;
535 return (i >= 0 && i < static_cast<int>(elements_.size())) ? elements_[i].source_node_id
536 : kEmpty;
537 }
538 // The host-parameter binding key of element `i` (DesignFrameElement::param_key),
539 // or empty when the element declares no binding. A foreign-host binder reads
540 // this to wire the element to a host parameter; see element_for_param_key for
541 // the host→UI reverse lookup.
542 const std::string& element_param_key(int i) const {
543 static const std::string kEmpty;
544 return (i >= 0 && i < static_cast<int>(elements_.size())) ? elements_[i].param_key
545 : kEmpty;
546 }
547 // Index of the first active-frame element whose param_key == `key`, or -1 if
548 // none (a NULL/empty key never matches). The host→UI direction of the bind:
549 // on automation/preset recall the binder looks up the element for a key and
550 // pushes the value with set_element_value (silently, no echo). Linear scan —
551 // the element count is a panel's worth of controls, not a hot path.
552 int element_for_param_key(const std::string& key) const {
553 if (key.empty()) return -1;
554 for (int i = 0; i < static_cast<int>(elements_.size()); ++i)
555 if (elements_[i].param_key == key) return i;
556 return -1;
557 }
558 // ── Live per-element scalars ─────────────────────────────────────────────
559 // Bind a ScalarSource to the element bound to host parameter `param_key` —
560 // the live per-frame value a subclass paints over that element (an LFO's
561 // modulated position for a macro knob's modulation ring, say). The host
562 // publishes from the audio/host thread; `element_scalar` reads paint-safe.
563 // See View::set_meter_source for the shared lifecycle + lifetime contract.
564 //
565 // Why per-element, and why keyed by param_key: a DesignFrameView is ONE view
566 // painting MANY elements, so the view's own single scalar
567 // (View::set_scalar_source) cannot carry a value per ring. Elements are
568 // keyed by param_key rather than index because `elements_` is REPLACED
569 // wholesale on a frame swap (set_active_frame) — an index means a different
570 // control in a different frame, whereas param_key is the element's stable
571 // host-binding identity. A binding therefore survives a frame swap and
572 // re-points at whichever element carries that key in the new frame; a key no
573 // frame declares reads 0 and stays parked — it never subscribes, so a typo'd
574 // key cannot quietly pin the editor at full frame rate. An empty key never
575 // matches.
576 void set_element_scalar_source(const std::string& param_key,
577 std::shared_ptr<ScalarSource> source);
578
583 float element_scalar(int i) const;
584
587
588 // Active view group for per-view momentary keyboards (e.g. typing=0, piano=1).
589 // hit_element only tests momentary elements whose view_group is -1 or equals
590 // this. Switching it releases any held momentary key (note-off) so no notes
591 // stick across a mode change. -1 (default) = all groups active.
593 int active_view_group() const { return active_view_group_; }
594
595 // ── Multi-frame (swap) support ────────────────────────────────────────
596 // A DesignFrameView can hold N alternate frames — each its own SVG, typed
597 // overlay element list, and panel crop — and swap which one renders. This
598 // is the importer's `swap` link target: a control whose job is to replace
599 // the on-screen content with another frame (e.g. a piano⇄typing mode
600 // toggle). set_active_frame swaps the rendered SVG, the overlay set, AND the
601 // reported intrinsic size, then calls invalidate_layout() to REQUEST a
602 // re-layout. Whether the surface actually resizes is up to the host: a host
603 // that sizes to intrinsic_width()/height() follows the new frame; a
604 // fixed-bounds host keeps its size and the new frame is fit (letterboxed)
605 // into it — clicks still map correctly either way. add_frame returns the new
606 // frame's index; frame 0 is the constructor's. Switching releases any held
607 // momentary key (and subclasses can react via on_active_frame_changed).
608 int add_frame(std::string svg, std::vector<DesignFrameElement> elements,
609 float panel_x = -1, float panel_y = -1,
610 float panel_w = -1, float panel_h = -1);
611 void set_active_frame(int index);
612 int active_frame() const { return active_frame_; }
613 int frame_count() const { return static_cast<int>(frames_.size()); }
614 // Normalized [0,1] value of element `i`, or -1 if out of range / not a
615 // value-bearing control (text_field). For a knob this is its turn; for a
616 // dropdown/tab_group/stepper it is the live selection mapped over the
617 // element's value count (the HOST's cardinality when a HostParamSurface
618 // resolves the element's param_key, else its own option count — see
619 // element_option_count). Reads the live overlay widget when one exists.
620 //
621 // CALL CONTEXT: for a choice element this may consult host_params(), which is
622 // legal from tick/update and NEVER from paint() (HostParamSurface's hard
623 // rule; a debug build asserts). A painter wanting a choice's position should
624 // read the value it was pushed at tick, not re-derive it mid-render.
625 float element_value(int i) const;
626 // Set element `i` from a normalized [0,1] value WITHOUT firing
627 // on_element_changed (a host->view push: knob turn, or choice index =
628 // round(v * (count-1)) applied to the live overlay widget silently). Use for
629 // automation/preset application so it doesn't echo back to the host. Same
630 // tick-not-paint call context as element_value for a choice element.
631 void set_element_value(int i, float v);
632 // Set the live text of a Kind::value_label element and repaint. No-op for
633 // other kinds / out of range. Use for readouts that must track state
634 // (octave / velocity / pitch-bend).
635 void set_element_text(int i, std::string text);
636 // The native-overlay child widget for element `i`, or nullptr (e.g. for a
637 // knob, or out of range). For tests/bindings.
638 View* overlay_widget(int i) const;
639
640 // ── Unregistered-custom-control diagnostic (opt-in, default off) ──────
641 // A Kind::custom element whose factory_id has NO registered factory renders
642 // inert (the baked SVG still shows) — by design a custom control never
643 // blanks. But that means a forgotten register_design_control_factory or a
644 // typo'd id is otherwise silent. Set this callback to be told which ids were
645 // referenced-but-unregistered so a developer / a --validate check can flag
646 // them. Fires once per inert Kind::custom element during overlay build, on
647 // the UI thread. Because the constructor builds the initial frame before a
648 // caller can attach this, setting the callback also REPLAYS the unregistered
649 // controls seen in the most recent build — so a callback attached right after
650 // construction still learns about them. Default (no callback set) is EXACTLY
651 // the old behavior: silent inert render, SVG intact. It never changes what
652 // renders — only ADDS the diagnostic signal.
654 std::function<void(const UnregisteredCustomControl&)> cb);
655 // The set of unregistered custom-control factory_ids seen during the last
656 // overlay build (rebuilt on every frame activation), in first-seen order and
657 // de-duplicated. Empty when every Kind::custom element resolved to a factory.
658 // Queryable without a callback — handy for a `--validate` style assertion.
659 const std::vector<UnregisteredCustomControl>&
661 return unregistered_custom_;
662 }
663
664 // Fired when the USER changes an element (knob drag, dropdown/tab/stepper
665 // select) — index + the new normalized value. NOT fired by set_element_value
666 // (that's a programmatic host->view push). A foreign-host binder forwards
667 // this to its parameter system. gesture begin/end bracket a knob drag so the
668 // binder can group an undo step; choice controls fire one changed (no
669 // gesture). All on the UI thread.
670 std::function<void(int index, float value)> on_element_changed;
671 std::function<void(int index)> on_gesture_begin;
672 std::function<void(int index)> on_gesture_end;
673 // Fired when a Kind::action command button is clicked — its `action` id. The
674 // consumer (e.g. MusicalTypingKeyboard) maps the id to its own state
675 // (octave/velocity/sustain/pitch-bend). UI thread.
676 std::function<void(const std::string& action)> on_action;
677
678 // ── Runtime host-parameter surface (the "port once, any host" path) ──
679 // Beyond the bind-once path (a consumer wiring on_element_changed +
680 // element_for_param_key), a DesignFrameView can bind DIRECTLY to the SDK's
681 // framework-agnostic HostParamSurface (View::host_params()). The surface
682 // hides WHICH parameter system is underneath — an embedding plug-in
683 // framework's parameter tree, or Pulp's own StateStore — so one view runs
684 // unchanged in every one of them.
685 //
686 // Enable with route_changes_to_host_params(true): thereafter a user gesture
687 // on a param_key-tagged element drives host_params() directly
688 // (begin_gesture / set_param / end_gesture), and sync_from_host_params()
689 // pulls current values + display text back at tick. A control whose
690 // param_key is empty or unknown to the surface is left to local state
691 // (degrades exactly like a preview with a null surface).
692 //
693 // OFF by default, and that default is load-bearing. on_element_changed keeps
694 // firing when routing is on, which is harmless for a consumer that OBSERVES
695 // and a double-write for one that WRITES. An embed that already carries the
696 // gesture itself — on_element_changed → StateStore → set_param, which is the
697 // existing binder path — must NOT also turn routing on: the host would
698 // receive every value and every gesture bracket twice, which reads as a
699 // doubled automation write and an unbalanced begin/end pair.
700 //
701 // Pick exactly one path per view: the binder (leave this off), or the
702 // surface (turn it on and drop the write side of your on_element_changed
703 // handler, keeping it only for observation).
704 void route_changes_to_host_params(bool enable) { route_to_host_params_ = enable; }
705 bool routes_changes_to_host_params() const { return route_to_host_params_; }
706
707 // Host→UI snapshot, called once per tick (never from paint — see the
708 // HostParamSurface call-context contract). For every active-frame element
709 // with a non-empty param_key that host_params() resolves, pulls the current
710 // normalized value into the element (silently, via set_element_value) and,
711 // for Kind::value_label readouts whose `action` names a param key, pulls the
712 // formatted display text (via set_element_text). No-op when host_params() is
713 // null. This is the snapshot views paint from, so per-frame ABI/host calls
714 // never happen inside paint().
716
717 // Dynamically re-key element `i` to a new host-parameter key (paged racks,
718 // tabbed effect slots). Updates the element's param_key, releases the old
719 // binding, and fires on_param_key_changed so an owning key→index registry
720 // (e.g. the embed facade's) can mark itself dirty and rebuild. The next
721 // sync_from_host_params() re-pulls the element under its new key. No-op if
722 // `i` is out of range or the key is unchanged.
723 void set_element_param_key(int i, std::string key);
724
725 // Fired by set_element_param_key after a successful re-key. A foreign-host
726 // embed uses this to invalidate its cached key→index registry; a native
727 // consumer can ignore it (sync_from_host_params resolves live). UI thread.
728 std::function<void(int index, const std::string& key)> on_param_key_changed;
729
730 // ── Typed commit helpers (UI → host, gesture-bracketed) ──────────────────
731 // One call per user edit, keyed by host parameter. Each brackets the edit in
732 // a gesture (begin → change → end) so it groups as ONE undo step, pushes the
733 // value into the bound element, and routes through the same emit_* funnel a
734 // pointer gesture uses — so a commit and a drag are indistinguishable
735 // downstream, no matter who drives the control.
736 //
737 // WHERE THE EDIT GOES — the same precondition a pointer gesture has, because
738 // it is the same funnel. A commit is NOT a direct line to the host:
739 // * route_changes_to_host_params(true) — emit_* calls the surface's
740 // begin_gesture / set_param / end_gesture. This is what makes the gesture
741 // bracket reach the HOST.
742 // * otherwise (THE DEFAULT — route_to_host_params_ is false) — emit_* fires
743 // on_element_changed / on_gesture_begin / on_gesture_end ONLY, and the
744 // consumer owns the write. This is deliberate and load-bearing: a
745 // consumer with its own store->host funnel (the embed C ABI does exactly
746 // this) must keep routing OFF or every edit is written twice.
747 // With NEITHER wired, a commit updates the element locally and the edit
748 // reaches nothing — a debug build asserts rather than dropping it quietly.
749 // Note the asymmetry: the value COUNT a commit scales against is read from
750 // host_params() regardless of routing (see resolve_value_count), because the
751 // divisor is a question, not a write.
752 //
753 // These exist because the alternative is every consumer hand-writing the
754 // bracket + the normalization per control, which is exactly where a port
755 // accrues transcription bugs. `key` is a host-parameter key; the element it
756 // resolves to (element_for_param_key) may be a real control or a bind-grid
757 // stand-in (see build_bind_grid) — a commit works either way. A key with no
758 // element resolves to nothing and reports a mismatch rather than writing to a
759 // wrong index.
760 //
761 // The index handed to on_element_changed / on_gesture_* for a STAND-IN is its
762 // position in elements_, which is assigned per frame activation and therefore
763 // shifts with the active frame's real-element count. It is a valid index into
764 // this view at the moment it fires and nothing more — do not persist it, and
765 // do not switch on it to identify a control. Switch on the KEY (the commit's
766 // own argument), or gate on element_is_bind_grid_stand_in(index).
767 //
768 // Suited to a discrete edit (a click, a typed value, a step). A continuous
769 // drag should bracket ONCE around the whole drag — call emit_gesture_begin /
770 // emit_element_changed / emit_gesture_end directly for that, or the host sees
771 // one undo step per pixel moved.
772
773 // Commit a normalized [0, 1] value. Clamped.
774 void commit_value(const std::string& key, float normalized);
775
776 // Commit a bipolar [-1, 1] depth, mapped to normalized as (depth + 1) / 2 —
777 // so -1 → 0.0, 0 → 0.5 (center), +1 → 1.0. The mapping every bipolar control
778 // (pan, detune, mod depth) otherwise re-derives by hand. Clamped to [-1, 1].
779 void commit_bipolar(const std::string& key, float depth);
780
781 // Commit a discrete value INDEX. The divisor is the host's own value count
782 // (HostParamSurface::param_step_count(key) - 1) — deliberately NOT a caller-
783 // supplied denominator and NOT the control's visible option count. A caller
784 // that passes its own divisor is re-introducing the mis-scale this helper
785 // exists to remove: the count belongs to the parameter, not to the view.
786 // Falls back to the element's option count when no host surface resolves the
787 // key (preview / screenshot), and reports a ParamScaleMismatch when neither
788 // yields an index domain. `index` is clamped into the resolved domain.
789 void commit_discrete(const std::string& key, int index);
790
791 // ── Bind grid (one element per host parameter) ───────────────────────────
792 // Append an invisible, zero-hit stand-in element for every key in `keys` that
793 // the active frame does not already carry a control for. The result: EVERY
794 // host parameter has an element, so the two directions of the bind need no
795 // per-parameter plumbing —
796 // host → UI: sync_from_host_params() pulls each key's current value at tick,
797 // so automation and preset recall land with no extra wiring;
798 // UI → host: commit_value / commit_bipolar / commit_discrete resolve any
799 // key, whether or not the design drew a control for it.
800 // A stand-in draws nothing (no needle path), never hit-tests (zero hit radius
801 // AND disabled), and costs one struct plus a value copy per tick — a full
802 // plug-in's worth of parameters is not a measurable cost.
803 //
804 // Keys already bound to a real control are SKIPPED, so a design's own control
805 // always wins the element_for_param_key lookup; the grid only fills gaps. The
806 // grid is re-applied per frame on every frame swap, so a key drawn on frame A
807 // but absent on frame B gets a real control on A and a stand-in on B.
808 // Repeated calls REPLACE the grid's key set rather than accumulating.
809 //
810 // The keys are caller-supplied because HostParamSurface exposes no parameter
811 // ENUMERATION — it answers questions about a key you already have
812 // (has_param / get_param / param_step_count) and has no "list every
813 // parameter". A host reaching this surface over a C ABI has that list on its
814 // own side; passing it here is the honest wiring, not a guess.
815 void build_bind_grid(std::vector<std::string> keys);
816
817 // The keys the bind grid was last built with (build_bind_grid's argument),
818 // whether or not each one produced a stand-in element on the active frame.
819 const std::vector<std::string>& bind_grid_keys() const;
820
821 // Whether element `i` is a bind-grid stand-in rather than a design control.
823
824 // ── Param-scale mismatch diagnostic (opt-in, default off) ────────────────
825 // Reports a control whose visible option count disagrees with the host's
826 // value cardinality for the parameter it is bound to, or a commit against a
827 // key with no resolvable index domain. DesignFrameView always normalizes
828 // against the HOST's count when it has one — this only ADDS the signal, it
829 // never changes what is emitted.
830 //
831 // Fires once per distinct param_key (de-duplicated, first-seen order) on the
832 // UI thread. Setting the callback REPLAYS the mismatches already seen, so a
833 // callback attached after the first tick still learns about them — matching
834 // set_on_unregistered_custom_control.
835 void set_on_param_scale_mismatch(std::function<void(const ParamScaleMismatch&)> cb);
836
837 // The distinct param-scale mismatches seen so far, first-seen order,
838 // de-duplicated by param_key. Queryable without a callback — handy for a
839 // `--validate` style assertion over a ported control table.
840 const std::vector<ParamScaleMismatch>& param_scale_mismatches() const;
841
842 // ── Host action/command channel ─────────────────────────────────────────
843 // When enabled, a Kind::action button click is ALSO forwarded to
844 // View::host_actions()->send_host_action(action, "{}") in addition to
845 // on_action. Lets a view trigger structural host commands (insert/remove/
846 // reorder rack slot, load preset) through the same framework-agnostic
847 // channel the import lane uses. OFF by default. args_json is "{}" here;
848 // richer payloads are the consumer's job via on_action.
849 void route_actions_to_host(bool enable) { route_actions_to_host_ = enable; }
850 bool routes_actions_to_host() const { return route_actions_to_host_; }
851
852 // ── Per-element hover + enabled/bypass state ─────────────────────────────
853 // DesignFrameView had no hover concept; every faithful port needs one
854 // (hover affordances, EDIT overlays, bypass dimming). Hover is tracked by
855 // hit-testing pointer moves (drive it with simulate_hover / the host's
856 // mouse-move). One element is hovered at a time; a disabled element is never
857 // hovered and never hit.
858 //
859 // The state is exposed for a subclass or painter to honor visually (e.g.
860 // brighten the hovered element, desaturate a disabled one) — the base view
861 // tracks state + interaction gating; pixel-level SVG restyling rides on the
862 // fragment-handle primitive. on_element_hover fires on the UI thread
863 // with the entered/exited index so a consumer can drive its own affordance.
864 int element_hovered() const { return hovered_element_; }
865 bool element_is_hovered(int i) const { return i >= 0 && i == hovered_element_; }
866 std::function<void(int index, bool entered)> on_element_hover;
867
868 // ── SVG fragment handles ─────────────────────────────────────────────────
869 // Tag a sub-tree of the live SVG once by a unique marker substring (a path
870 // `d`, the same handle the knob-needle patch keys on), then redraw JUST that
871 // fragment on demand — transformed (translate/rotate/scale), composited at an
872 // opacity, and optionally recolored — composited over the already-drawn frame
873 // through the SAME panel fit paint() uses, so it lands on its original spot.
874 // This is the primitive the hover-brighten / bypass-dim visuals ride on,
875 // and what a meter-needle redraw, reorder ghost, or focus glow reuses. The
876 // string/geometry work is the pure svg_fragment.hpp helpers; these methods add
877 // the Canvas::draw_svg call and the panel-fit draw box.
878 void register_fragment(std::string id, std::string marker);
879 bool has_fragment(const std::string& id) const {
880 return fragments_.find(id) != fragments_.end();
881 }
882 // Draw the registered fragment `id`. Returns false if the id is unknown, its
883 // marker isn't in the current SVG, the panel isn't laid out, or the backend
884 // can't render SVG. Never throws / never mutates the live document.
885 bool draw_fragment(canvas::Canvas& canvas, const std::string& id,
886 const FragmentTransform& xform = {}, float opacity = 1.0f,
887 const std::string& recolor_hex = {}) const;
888 // Lower-level: draw an arbitrary (not pre-registered) marker fragment.
889 bool draw_fragment_marker(canvas::Canvas& canvas, const std::string& marker,
890 const FragmentTransform& xform = {},
891 float opacity = 1.0f,
892 const std::string& recolor_hex = {}) const;
893
894 // Enable/disable (bypass) element `i`. A disabled element is skipped by
895 // hit-testing — it cannot be hovered, dragged, clicked, or gesture — and if
896 // it was the hovered element the hover is cleared. Repaints. No-op out of
897 // range.
899 bool element_enabled(int i) const {
900 return i >= 0 && i < static_cast<int>(elements_.size()) && elements_[i].enabled;
901 }
902
903 // The panel is the view's natural size — a host should size its window to
904 // this aspect so the design fills it with no letterbox (see paint()).
905 float intrinsic_width() const override { return panel_w_; }
906 float intrinsic_height() const override { return panel_h_; }
907
908 void paint(canvas::Canvas& canvas) override;
909 void layout_children() override;
910 void on_mouse_down(Point pos) override;
911 void on_mouse_drag(Point pos) override;
912 void on_mouse_up(Point pos) override;
913 void on_hover_move(Point pos) override; // hover-track the element under the pointer
914 void on_mouse_leave() override; // clear hover on exit
915 bool wants_mouse_input() const override { return true; }
916
917protected:
918 // Called after the active frame changes (set_active_frame or the initial
919 // constructor activation). Subclasses override to react to a frame swap —
920 // e.g. release any of their own held input and re-apply external highlight
921 // state to the new frame's elements. Default: no-op. (Invoked from the
922 // constructor's activate_frame(0) too, where virtual dispatch resolves to
923 // this base no-op — subclass state isn't built yet.)
924 virtual void on_active_frame_changed() {}
925
926 // The ONE transform shared by paint() and hit_element(): a uniform fit of the
927 // panel into `bounds`, centered (letterbox when bounds aspect != panel
928 // aspect). `scale` is panel→view; (ox,oy) is the view-space position of the
929 // panel's top-left. paint draws through it; hit_element inverts it — so a
930 // knob is hit exactly where it is drawn, at ANY host window aspect. Protected
931 // so a subclass that paints its own overlay (e.g. MusicalTypingKeyboard's
932 // movable overview-strip highlight) maps panel↔view through the SAME fit.
933 struct PanelTransform { float scale = 0.0f, ox = 0.0f, oy = 0.0f; };
935
936private:
937 // The authoritative number of VALUES element `i`'s domain has — the count
938 // choice<->normalized divides against (as count-1; see
939 // param_index_to_normalized). Resolution order:
940 // 1. the HOST's cardinality for the element's param_key, when a
941 // HostParamSurface resolves it and reports a non-zero step count. The
942 // parameter owns its own range; a control drawn with fewer positions
943 // than the parameter has values does not shrink the parameter.
944 // 2. otherwise the element's visible option count (element_option_count) —
945 // the preview / screenshot / no-surface path, where there is no host to
946 // ask and the control's own positions are the only domain that exists.
947 // Returns 0 when neither yields a domain (continuous, or unknown key with no
948 // options), which param_index_to_normalized reads as "no index domain".
949 //
950 // Reports a ParamScaleMismatch when both counts exist and disagree.
951 int resolve_value_count(int i) const;
952
953 // Map a choice element's selected index to a normalized [0,1] value and back,
954 // using resolve_value_count. Single source of truth for choice<->normalized.
955 float choice_to_norm(int i, int selected) const;
956 int norm_to_choice(int i, float v) const;
957
958 // Sync a user choice change (overlay widget -> element + on_element_changed).
959 void notify_choice(int i, int selected);
960
961protected:
962 // User-gesture emit helpers: route to host_params() (when routing is on and
963 // the element carries a key the surface resolves) AND fire the public
964 // on_element_changed / on_gesture_* callback. Single funnel so every
965 // value-bearing gesture path stays consistent.
966 //
967 // These are the supported subclass extension point. An adapter that drives
968 // the editor from something other than a pointer (an embedding host pushing
969 // its own automation, or a headless QA harness) can call these to push a
970 // synthetic value or bracket
971 // a gesture through the *same* funnel a real pointer gesture uses — so
972 // host-param routing, the public callbacks, and undo bracketing all stay
973 // consistent with hand-driven input. `i` is an active-frame element index:
974 // host-param routing is skipped for an out-of-range index, while the public
975 // on_element_changed / on_gesture_* callback still receives `i` verbatim
976 // (callers own their own index validation). UI thread only.
977 void emit_element_changed(int i, float value);
980
981private:
982 // Build the native-overlay child widgets (TextEditor / ComboBox / tabs) for
983 // the non-knob elements of the active frame; called when a frame activates.
984 void build_overlays();
985
986 // One swappable frame. Holds the panel-detected + baked-tab-suppressed SVG,
987 // its overlay element list, and resolved panel crop. Frames are built once
988 // (build_frame) and copied into the active members by activate_frame.
989 struct Frame {
990 std::string svg;
991 std::vector<DesignFrameElement> elements;
992 float svg_w = 0.0f, svg_h = 0.0f;
993 float panel_x = 0.0f, panel_y = 0.0f, panel_w = 0.0f, panel_h = 0.0f;
994 };
995 // Run panel-detect + baked-tab suppression on raw inputs and return a Frame.
996 // Touches no member state (safe to call before/after activation).
997 Frame build_frame(std::string svg, std::vector<DesignFrameElement> elements,
998 float panel_x, float panel_y, float panel_w, float panel_h) const;
999 // Tear down the active overlay widgets, copy frame `index` into the active
1000 // members (svg_/elements_/panel_*), and rebuild overlays.
1001 void activate_frame(int index);
1002
1003 int hit_element(Point pos) const;
1004
1005 // A native-overlay child widget bound to one element (by index). The widget
1006 // is owned by the View child list; this just maps element -> widget so
1007 // layout_children() can position it via the panel transform.
1008 struct Overlay { int element_index = -1; View* widget = nullptr; };
1009
1010 // Host-parameter binding collaborator (scalar sources, bind grid, scale-
1011 // mismatch diagnostic). Constructed in the ctor before the initial
1012 // activate_frame(0); held by pointer so the header stays additive.
1013 std::unique_ptr<DesignParamBinding> binding_;
1014
1015 std::string svg_;
1016 std::vector<DesignFrameElement> elements_;
1017 std::vector<Overlay> overlays_;
1018 float svg_w_ = 0.0f, svg_h_ = 0.0f; // SVG intrinsic size
1019 float panel_x_ = 0, panel_y_ = 0, panel_w_ = 0, panel_h_ = 0; // crop, SVG coords
1020 int drag_ = -1;
1021 float drag_start_x_ = 0.0f, drag_start_y_ = 0.0f, drag_start_value_ = 0.0f;
1022 int active_view_group_ = -1;
1023 // Opt-in diagnostic for referenced-but-unregistered Kind::custom controls,
1024 // plus the accumulator rebuilt on every overlay build. UI-thread-only.
1025 std::function<void(const UnregisteredCustomControl&)> on_unregistered_custom_;
1026 std::vector<UnregisteredCustomControl> unregistered_custom_;
1027
1028 std::vector<Frame> frames_;
1029 int active_frame_ = 0;
1030 bool route_to_host_params_ = false;
1031 bool route_actions_to_host_ = false;
1032 int hovered_element_ = -1;
1033 std::unordered_map<std::string, std::string> fragments_;
1034
1035 // The panel→view draw box for the current bounds — the exact (x,y,w,h) paint()
1036 // hands Canvas::draw_svg for the full frame. A fragment mini-document drawn at
1037 // this box composites over its original position. Returns false (leaves out-*
1038 // untouched) when the panel isn't laid out.
1039 bool current_svg_draw_box(float& ox, float& oy, float& ow, float& oh) const;
1040
1041 // Set the hovered element (fires on_element_hover on change, repaints).
1042 void set_hovered_element(int i);
1043};
1044
1045// The native-overlay widget for a `tab_group` element: a compact segmented
1046// control drawn opaque over the design's tab strip (so it replaces the baked
1047// tabs + highlight). Clicking a slot selects it and moves the highlight — the
1048// "regular selection state" a static SVG can't provide. Styling approximates the
1049// design's dark strip; it is intentionally an approximation rather than
1050// pixel-exact theming.
1051class DesignTabGroup : public View {
1052public:
1053 DesignTabGroup(std::vector<std::string> labels, int selected);
1054 int selected() const { return selected_; }
1055 int tab_count() const { return static_cast<int>(labels_.size()); }
1056 // Set selection without firing on_select (programmatic host->view push).
1057 void set_selected_silent(int index);
1058 // Fired when the USER taps a different tab (index). Not fired by
1059 // set_selected_silent.
1060 std::function<void(int index)> on_select;
1061
1062 void paint(canvas::Canvas& canvas) override;
1063 void on_mouse_down(Point pos) override;
1064 bool wants_mouse_input() const override { return true; }
1065
1066private:
1067 std::vector<std::string> labels_;
1068 int selected_ = 0;
1069};
1070
1071// The native-overlay widget for a `stepper` element: a header value cycled in
1072// place by `< >` chevrons (the design's section-header preset selectors). It
1073// paints the current option centered with a `<` on the left and `>` on the
1074// right; clicking the left third steps to the previous option, the right third
1075// to the next (clamped). Nothing is drawn behind the text, so the design's
1076// header chrome shows through — only the value text and chevrons are ours.
1077class DesignStepper : public View {
1078public:
1079 DesignStepper(std::vector<std::string> options, int selected);
1080 int selected() const { return selected_; }
1081 int option_count() const { return static_cast<int>(options_.size()); }
1082 const std::string& current() const;
1083 // Set selection without firing on_select (programmatic host->view push).
1084 void set_selected_silent(int index);
1085 // Fired when the USER steps to a different option (index). Not fired by
1086 // set_selected_silent.
1087 std::function<void(int index)> on_select;
1088
1089 void paint(canvas::Canvas& canvas) override;
1090 void on_mouse_down(Point pos) override;
1091 bool wants_mouse_input() const override { return true; }
1092
1093private:
1094 std::vector<std::string> options_;
1095 int selected_ = 0;
1096};
1097
1098} // namespace pulp::view
Definition canvas.hpp:240
Definition design_frame_view.hpp:222
std::function< void(float)> on_control_changed
Definition design_frame_view.hpp:236
std::function< void()> on_gesture_begin
Definition design_frame_view.hpp:244
virtual ~DesignControl()=default
std::function< void()> on_gesture_end
Definition design_frame_view.hpp:245
virtual float control_value() const =0
virtual void set_control_value(float value)=0
int add_frame(std::string svg, std::vector< DesignFrameElement > elements, float panel_x=-1, float panel_y=-1, float panel_w=-1, float panel_h=-1)
bool element_has_scalar_source(int i) const
Whether element i of the active frame has a scalar source bound.
void route_changes_to_host_params(bool enable)
Definition design_frame_view.hpp:704
void layout_children() override
float element_value_y(int i) const
Definition design_frame_view.hpp:442
float intrinsic_width() const override
Intrinsic content size (override in widgets that know their natural size).
Definition design_frame_view.hpp:905
std::function< void(const std::string &action)> on_action
Definition design_frame_view.hpp:676
float intrinsic_height() const override
Definition design_frame_view.hpp:906
bool has_fragment(const std::string &id) const
Definition design_frame_view.hpp:879
void build_bind_grid(std::vector< std::string > keys)
bool routes_changes_to_host_params() const
Definition design_frame_view.hpp:705
void set_element_value(int i, float v)
bool draw_fragment(canvas::Canvas &canvas, const std::string &id, const FragmentTransform &xform={}, float opacity=1.0f, const std::string &recolor_hex={}) const
const std::vector< std::string > & bind_grid_keys() const
void set_active_frame(int index)
bool element_hit_point(int i, Point &out) const
const std::string & element_source_node_id(int i) const
Definition design_frame_view.hpp:533
const std::string & element_action(int i) const
Definition design_frame_view.hpp:420
float element_value(int i) const
void emit_element_changed(int i, float value)
float element_scalar(int i) const
The latest value published for element i of the ACTIVE frame, or 0 when i is out of range or its elem...
Rect element_rect(int i) const
float panel_width() const
Definition design_frame_view.hpp:378
const std::string & element_display_text(int i) const
Definition design_frame_view.hpp:505
const std::vector< UnregisteredCustomControl > & unregistered_custom_controls() const
Definition design_frame_view.hpp:660
void on_mouse_down(Point pos) override
std::function< void(int index)> on_gesture_begin
Definition design_frame_view.hpp:671
bool wants_mouse_input() const override
True if this widget handles mouse down/drag through virtual methods even when it does not expose JS p...
Definition design_frame_view.hpp:915
bool element_left_align(int i) const
Definition design_frame_view.hpp:527
View * overlay_widget(int i) const
void on_hover_move(Point pos) override
Pointer moved over this view with no button held, reported in this view's LOCAL coordinates.
void paint(canvas::Canvas &canvas) override
bool element_is_hovered(int i) const
Definition design_frame_view.hpp:865
int element_count() const
Definition design_frame_view.hpp:377
void set_on_param_scale_mismatch(std::function< void(const ParamScaleMismatch &)> cb)
void set_element_scalar_source(const std::string &param_key, std::shared_ptr< ScalarSource > source)
void commit_bipolar(const std::string &key, float depth)
const std::string & element_text(int i) const
Definition design_frame_view.hpp:450
void set_element_enabled(int i, bool enabled)
bool element_has_display_text(int i) const
Definition design_frame_view.hpp:521
void on_mouse_up(Point pos) override
void commit_discrete(const std::string &key, int index)
DesignFrameElement::Kind element_kind(int i) const
bool draw_fragment_marker(canvas::Canvas &canvas, const std::string &marker, const FragmentTransform &xform={}, float opacity=1.0f, const std::string &recolor_hex={}) const
void on_mouse_leave() override
bool routes_actions_to_host() const
Definition design_frame_view.hpp:850
DesignFrameView(std::string svg, std::vector< DesignFrameElement > elements, float panel_x=-1, float panel_y=-1, float panel_w=-1, float panel_h=-1)
PanelTransform panel_transform(const Rect &bounds) const
void set_element_text(int i, std::string text)
int element_option_count(int i) const
void on_mouse_drag(Point pos) override
int element_for_param_key(const std::string &key) const
Definition design_frame_view.hpp:552
float panel_height() const
Definition design_frame_view.hpp:379
int element_target_frame(int i) const
Definition design_frame_view.hpp:446
void set_on_unregistered_custom_control(std::function< void(const UnregisteredCustomControl &)> cb)
int element_hovered() const
Definition design_frame_view.hpp:864
virtual void on_active_frame_changed()
Definition design_frame_view.hpp:924
std::function< void(int index, const std::string &key)> on_param_key_changed
Definition design_frame_view.hpp:728
std::function< void(int index, bool entered)> on_element_hover
Definition design_frame_view.hpp:866
void commit_value(const std::string &key, float normalized)
bool element_enabled(int i) const
Definition design_frame_view.hpp:899
const std::string & element_param_key(int i) const
Definition design_frame_view.hpp:542
void route_actions_to_host(bool enable)
Definition design_frame_view.hpp:849
int active_view_group() const
Definition design_frame_view.hpp:593
void set_element_param_key(int i, std::string key)
const std::vector< ParamScaleMismatch > & param_scale_mismatches() const
std::function< void(int index)> on_gesture_end
Definition design_frame_view.hpp:672
void set_active_view_group(int group)
bool element_is_bind_grid_stand_in(int i) const
int frame_count() const
Definition design_frame_view.hpp:613
bool element_is_discrete(int i) const
int active_frame() const
Definition design_frame_view.hpp:612
int element_note(int i) const
Definition design_frame_view.hpp:386
std::function< void(int index, float value)> on_element_changed
Definition design_frame_view.hpp:670
void register_fragment(std::string id, std::string marker)
Definition design_param_binding.hpp:27
void set_selected_silent(int index)
void paint(canvas::Canvas &canvas) override
bool wants_mouse_input() const override
True if this widget handles mouse down/drag through virtual methods even when it does not expose JS p...
Definition design_frame_view.hpp:1091
DesignStepper(std::vector< std::string > options, int selected)
int selected() const
Definition design_frame_view.hpp:1080
const std::string & current() const
std::function< void(int index)> on_select
Definition design_frame_view.hpp:1087
void on_mouse_down(Point pos) override
int option_count() const
Definition design_frame_view.hpp:1081
void set_selected_silent(int index)
std::function< void(int index)> on_select
Definition design_frame_view.hpp:1060
int tab_count() const
Definition design_frame_view.hpp:1055
void paint(canvas::Canvas &canvas) override
void on_mouse_down(Point pos) override
bool wants_mouse_input() const override
True if this widget handles mouse down/drag through virtual methods even when it does not expose JS p...
Definition design_frame_view.hpp:1064
DesignTabGroup(std::vector< std::string > labels, int selected)
int selected() const
Definition design_frame_view.hpp:1054
Definition view.hpp:73
float opacity() const
Definition view.hpp:1117
@ group
Definition view.hpp:989
@ text
Definition view.hpp:2742
bool enabled() const
CSS :disabled equivalent — blocks input, reduces opacity.
Definition view.hpp:715
Rect bounds() const
Definition view.hpp:84
Definition affine_transform.hpp:24
Hover-cursor tracking for platform window hosts.
Definition fwd.hpp:27
std::function< std::unique_ptr< View >(const DesignControlContext &)> DesignControlFactory
Definition design_frame_view.hpp:210
bool suppress_svg_rect(std::string &svg, float x, float y, float w, float h, float tol=2.0f)
void register_design_control_factory(std::string factory_id, DesignControlFactory factory)
bool suppress_svg_glow_at(std::string &svg, float x, float y, float w, float h)
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
@ h
Definition input_events.hpp:39
@ y
Definition input_events.hpp:42
@ i
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41
@ v
Definition input_events.hpp:41
@ w
Definition input_events.hpp:41
bool has_design_control_factory(const std::string &factory_id)
void clear_design_control_factories()
bool suppress_svg_glyph_at(std::string &svg, float x, float y, float w, float h)
@ action
Definition design_ir.hpp:490
Definition design_frame_view.hpp:196
std::string factory_id
Definition design_frame_view.hpp:198
std::string props
opaque (typically JSON); Pulp does not parse it
Definition design_frame_view.hpp:199
float w
Definition design_frame_view.hpp:197
float y
Definition design_frame_view.hpp:197
std::string source_node_id
Definition design_frame_view.hpp:200
float x
Definition design_frame_view.hpp:197
float h
Definition design_frame_view.hpp:197
float default_value
opening value.
Definition design_frame_view.hpp:201
Definition design_frame_view.hpp:24
Kind kind
Definition design_frame_view.hpp:64
int selected_index
dropdown / tab selection
Definition design_frame_view.hpp:89
std::string factory_id
For Kind::custom: the id the overlay factory is registered under (register_design_control_factory).
Definition design_frame_view.hpp:131
float h
element rect, SVG coords
Definition design_frame_view.hpp:86
std::string param_key
Optional host-parameter binding id for this control.
Definition design_frame_view.hpp:156
std::string action
For Kind::action: the action id fired (on_action) when clicked, e.g.
Definition design_frame_view.hpp:115
float cy
Definition design_frame_view.hpp:68
std::string needle_d
Definition design_frame_view.hpp:73
std::string source_node_id
Source node id this overlay came from (e.g.
Definition design_frame_view.hpp:142
float x
Definition design_frame_view.hpp:86
bool display_text_bound
Whether display_text came from a host parameter at the last sync, as opposed to being empty because n...
Definition design_frame_view.hpp:184
bool enabled
When false, the element is bypassed/disabled: hit-testing skips it (it cannot be hovered,...
Definition design_frame_view.hpp:83
float value
0..1 (xy_pad: the X axis)
Definition design_frame_view.hpp:74
int note
Raw parsed number: typing keys = relative semitone (0–15), piano keys = absolute MIDI note.
Definition design_frame_view.hpp:101
std::vector< std::string > options
dropdown options / tab labels
Definition design_frame_view.hpp:88
int view_group
View scope for per-view keyboards (e.g.
Definition design_frame_view.hpp:105
std::string text
For Kind::value_label: the live display string painted over the rect (right-aligned to match the desi...
Definition design_frame_view.hpp:121
std::string placeholder
text_field
Definition design_frame_view.hpp:87
float hit_radius
click-target radius, SVG coords
Definition design_frame_view.hpp:69
std::string bg_color
text_field: the design's own field background ("#RRGGBB").
Definition design_frame_view.hpp:94
float w
Definition design_frame_view.hpp:86
bool value_left_align
value_label: left-align the text in the rect instead of right-aligning.
Definition design_frame_view.hpp:126
float cx
pivot / hit center, SVG coords
Definition design_frame_view.hpp:67
Kind
Definition design_frame_view.hpp:60
@ fader
Definition design_frame_view.hpp:60
@ toggle
Definition design_frame_view.hpp:60
@ stepper
Definition design_frame_view.hpp:61
@ custom
Definition design_frame_view.hpp:62
@ momentary
Definition design_frame_view.hpp:61
@ dropdown
Definition design_frame_view.hpp:60
@ knob
Definition design_frame_view.hpp:60
@ xy_pad
Definition design_frame_view.hpp:61
@ text_field
Definition design_frame_view.hpp:60
@ value_label
Definition design_frame_view.hpp:61
@ swap
Definition design_frame_view.hpp:61
@ tab_group
Definition design_frame_view.hpp:60
std::string display_text
Host-formatted readout for param_key, cached by sync_from_host_params() once per tick so paint() can ...
Definition design_frame_view.hpp:180
bool flash
toggle only: press-flash command button (sample next/prev/random, dice).
Definition design_frame_view.hpp:79
int target_frame
For Kind::swap: the frame index to activate when this button is clicked (the swap link target).
Definition design_frame_view.hpp:110
float value_y
0..1 (xy_pad: the Y axis, 0=top)
Definition design_frame_view.hpp:75
std::string custom_props
For Kind::custom: opaque props handed to the factory (typically a JSON string the factory parses).
Definition design_frame_view.hpp:134
float y
Definition design_frame_view.hpp:86
Definition design_frame_view.hpp:933
float oy
Definition design_frame_view.hpp:933
float scale
Definition design_frame_view.hpp:933
float ox
Definition design_frame_view.hpp:933
Definition svg_fragment.hpp:35
Definition design_frame_view.hpp:312
std::string param_key
the bound host-parameter key
Definition design_frame_view.hpp:313
bool host_has_param
whether a live surface resolved param_key at all.
Definition design_frame_view.hpp:317
int host_step_count
values the host's parameter exposes (authoritative); 0 means the surface reported no index domain
Definition design_frame_view.hpp:315
int ui_option_count
positions the control draws (element_option_count)
Definition design_frame_view.hpp:314
Definition geometry.hpp:20
Axis-aligned rectangle in continuous (float) coordinates.
Definition geometry.hpp:52
Definition design_frame_view.hpp:270
std::string factory_id
the id that had no factory
Definition design_frame_view.hpp:271
std::string source_node_id
the design-source node id (e.g. Figma), if any
Definition design_frame_view.hpp:272
View-side bindings for the paint-safe host→view channels in value_source.hpp.