Building a plugin editor with the bridge
A jig:ui page binds to ports with attributes, and a small script does the messaging. Without it, an
editor has to implement the whole handshake in messaging.md section 2 by hand, which is about a
hundred lines before the first slider (plugins/tremolo/ui/index.html is that version). With it, a slider is
<input type="range" data-param="threshold">and the script reads the port's range and unit from the profile, sends the gestures, and draws the value the host sends back. The idea comes from APF, a Rust plugin framework whose editors are HTML pages bound the same way. The sandbox, the message protocol and the contract are unchanged: this is a convenience for the author of a page, not a new kind of interface.
An editor is still optional and still sits beside the generated panel, which reaches every port (host-plugin-contract.md section 9.1). Use it for what a skin cannot draw: curves, displays, anything that needs code. Dynamix's editor is the worked example, a compressor transfer curve with a live dot on it (plugins/dynamix/ui/src.html).
Attributes
| Attribute | On | Effect |
|---|---|---|
data-param="symbol" |
input range, input checkbox, select |
The control follows the port, both ways. Range, step and, for a select, the options come from the port |
data-param-name="symbol" |
any | Its text becomes the port's lv2:name |
data-param-text="symbol" |
any | Its text becomes the value with its unit, as -18 dB |
data-param-help |
any | Shows what the hovered or focused control does, from the port's rdfs:comment. Hidden from assistive technology |
The symbol is the port's lv2:symbol, the same name the wire format uses. A binding to a symbol the plugin
does not declare is hidden and reported to the console, not left disabled.
What the bridge does for a bound control, so the page does not:
- A port with a comment gets it as the control's description, said on focus, whether or not the page has a help line. The help line follows the pointer and the focus, returns to the focused control when the pointer leaves, and Escape puts it away, as the generated panel's does.
- A drag is one
gesturebegin and end, and each move is aparameterrequest. A key press or a tap has no pointer to bracket it and is sent as one whole gesture. - The control draws the value in the host's
parameterreply and never the one it asked for, so a clamped or automated value cannot leave the page disagreeing with the sound (messaging.md section 2.3). - The value text is set as
aria-valuetext, in words: "-18 decibels", not "-18". A control with no label gets the port's name as itsaria-label. - The host's origin is learnt from
initand every later message is addressed to it. A message from another window or origin is ignored. - The frame asks the host for the height its content needs whenever that changes.
Script
The script defines jig.
| Call | Purpose |
|---|---|
jig.ready |
A promise, resolved with { ports, parameters } once the host has sent init |
jig.on(symbol, fn(value, text)) |
Called now if the value is known and on every change. text is { text, spoken } |
jig.value(symbol) |
The last value the host applied |
jig.begin(symbol), jig.set(symbol, value), jig.end(symbol) |
For a control the page draws itself, such as a point on a curve |
jig.change(symbol, value) |
One whole edit |
jig.bind(root) |
Bind data-param* elements the page added later |
jig.onPlugin(fn(payload)), jig.sendPlugin(payload) |
The opaque relay to and from the processor (messaging.md section 2.4) |
Building a page
The page ships as one HTML file with its script inline, because the profile's jig:integrity digest covers one
file and a script fetched separately would be code the digest does not cover. So the author writes
ui/src.html, marks the place for the bridge with <script data-jig-bridge></script>, and
bin/inline-ui.js writes ui/index.html:
node bin/inline-ui.js plugins/dynamixRun it in the plugin's build.sh before bin/write-profile.js, which digests the result. The generated file
says so at the top. tests/bin/inline-ui.test.js fails when a committed index.html is not what the tool makes
of its src.html and the current bridge, so a change to the bridge cannot leave a plugin quietly running the
old one.
The bridge is src/plugin-ui/JigUi.js. It shares the unit text with the generated panel
(src/ui/PortText.js), so a port reads the same in both.
What the frame is given
The init profile is the plugin's own, as JSON-LD, and carries what an editor draws from: each port's
symbol, name, minimum, maximum and default, and also its comment, its unit, its portProperty values (so a
toggle is known to be one) and its scalePoints (so an enumeration has its names). The widget rule is the host's
(host-plugin-contract.md section 5.3). Built by src/rdf/ProfileJsonLd.js.
Worked example: Dynamix
Dynamix is a compressor/expander, limiter and clipper in Rust. Its module exports two optional meters,
jig_gain_reduction_db and jig_detector_db, written once per block. The processor posts them about 23 times a
second as a plugin message, reusing one object so the audio path allocates nothing. The page draws the transfer
curve from the threshold, ratio, knee, make-up gain and mode, puts a dot on it at the live level, and shows the
gain reduction as text and a bar.
The curve is held to the module. tests/ui/dynamix-ui.test.js gives the same settings to the wasm and to the
page's curve function, for a compressor above and inside its knee, below its threshold with make-up gain, and an
expander on both sides, and requires the levels to agree within 0.3 dB. A curve that draws a law the compressor
does not apply would be worse than none.
The gain reduction text is not a live region. Twenty updates a second would drown everything else a screen reader has to say.
Second example: Canticle, which needed no script and became a skin
Canticle, a Rust instrument with sixteen ports, was given an editor made only of
markup bound with data-param and a stylesheet, to find out whether an interface of controls and labels needs code. It did
not, so it ships as an HTML skin, which the host draws in its own document, and has no jig:ui. A plugin
whose interface needs code, such as Dynamix with its curve, stays an editor.
Not done
- Meters in skins. svg-skins.md still has no way to name a signal. The relay above is the interim answer for a page.
- Reset by double click and MIDI learn on bound controls, which APF gives for free. The host has a context menu and a MIDI map; the bridge has no message to ask for either.