-
Notifications
You must be signed in to change notification settings - Fork 32
Making a Scriptable UI
Making a plug-in UI is quite a lot of work, often more than the DSP part.
Since Dplug v12.3
, you can use Wren as an "imperative CSS" to have live-coding in the UI creation process.
This article explains how to add Wren scripting to your plug-in.
Should you use Wren scripting in your Dplug product? Normally, yes!
-
Pros:
- Live-code UI widgets graphical properties, and see the result immediately without rebuild.
- Your UI code will probably contain large amounts of graphical properties settings. Creating this code is slow, and limited by the speed of your feedback loop. Hence, scripting.
-
Cons:
- About 200kb of additional binary code in final product.
- About 1 to 10 mb of additional memory in final product.
- Slower UI opening and resize, because of scripting overhead.
- Another langage to learn, with significant space, no semicolons, and K&R style. 🤔
The benefits of seeing directly your change on screen is interesting, as it hopefully influence the result in a positive manner.
Here is what you can do from Wren: (as of Dplug v12.3)
-
Set positions of a
UIElement
at UI creation or reflow.static reflow() { var S = UI.width / UI.defaultWidth ($"_inputSlider").position = Rectangle.new(190, 132, 30, 130).scaleByFactor(S) }
-
Set/get values of fields in
UIElement
-derived classes that are marked with@ScriptProperty
, at UI creation or reflow.static reflow() { var S = UI.width / UI.defaultWidth ($"_inputSlider").litTrailDiffuse = RGBA.new(151, 119, 255, 100) }
-
Set/get visibility of a
UIElement
:static reflow() { ($"_slider").visibility = false }
-
Set/get Z-order of a
UIElement
:static reflow() { ($"_slider").zOrder = 4 }
- Plus everything you can normally do in Wren.
See plugin.wren
in the Distort example.
-
The available exposed API is
ui.wren
. This is a Dplug-specific Wren API implemented in thedplug:wren-support
sub-package. -
The other imported Wren module is
widgets
, whose code is auto-generated based on what widgets were exposed to Wren.
-
You need to expose to Wren the final, concrete used
class
not only the base class. Even if the base class has the@ScirptProperty
of interest. This is because Wren has no knowledge of what classes are in a hierarchy, and only see the runtime type. The properties are iterated and repeated for each subclass. - You can not create a
UIElement
(or any kind of D object) from script. You will still need to create widgets in D code, and calladdChild
manually. -
@ScriptExport
classes must derive fromUIElement
. - Calling a
@ScriptProperty
setter just changes the memory. It does not trigger a redraw by itself. You can't expose methods, just assign fields in some D objects. So: you can't add arbitrary Wren APIs for now, just use@ScriptExport
/@ScriptProperty
. - Enums are lowered to integers in Wren, they don't have pretty names.
- You can't expose to Wren two classes that are named the same in different D modules.
- Each
UIElement
you want to modify by script should have a unique ID. Can't traverse UI tree for now.
Add the dplug:wren-support
sub-package as a dependency to you dub.json
.
"dependencies":
{
"dplug:wren-support": "~>12.3"
}
Add a "scripts" string import directory to you dub.json
.
"stringImportPaths": ["scripts"],
Create the minimal file scripts/plugin.wren
.
import "ui" for UI, Element, Point, Size, Rectangle, RGBA
import "widgets" for <all the classes you need to set properties for from Wren>
class Plugin {
static createUI() {
}
static reflow() {
}
}
All Wren output is passed to debugLog
, and thus is available when debugging.
Note that a Wren crash is kept silent.
In your gui.d
:
- Add
import dplug.wren;
at top-level. - Call
context.enableWrenSupport();
in the UI constructor. - Call
context.disableWrenSupport();
in the UI destructor.
Each of your exposed widget should have a unique ID to be queried from Wren.
In your gui.d
main UI class:
- Add
mixin(fieldIdentifiersAreIDs!MyGUI);
in the UI constructor.
Wren needs to know which classes exist and what property exists. Dplug thus defines 2 UDAs: @ScriptExport
and @ScriptProperty
.
- Mark your top-level UI fields with
@ScriptExport
User-Defined Attribute.
@ScriptExport
{
UISlider _inputSlider;
UIKnob _driveKnob;
UISlider _outputSlider;
UIOnOffSwitch _onOffSwitch;
UILevelDisplay _inputLevel, _outputLevel;
UIColorCorrection _colorCorrection;
UIImageKnob _imageKnob;
UIWindowResizer _resizer;
}
- Use
context.wrenSupport.registerScriptExports!MyGUI;
in the UI constructor.
If you don't have such a list of fields, you can register individual classes with context.wrenSupport.registerUIElementClass!UIClass;
instead.
Same if your fields are not of the final concrete type, but a base class. In those cases, @ScriptExport
is not necessary.
What this registering does is retrieve information about all fields marked as @ScriptProperty
in those classes, or their base classes. Thus, you can create your own UI classes with your own @ScriptProperty
fields.
class UIMyButton : UIElement
{
@ScriptProperty RGBA color;
@ScriptProperty RGBA colorPushed; // All that exposed to Wren if it knows about UIMyButton.
@ScriptProperty float animationSpeed;
}
@ScriptProperty
fields can be of the following types: (as of Dplug v12.3)
bool
-
byte
/ubyte
/short
/ushort
/int
/uint
float
double
RGBA
- enums or
L16
(but this is lowered to integers)
Wren needs to know what the "plugin"
module is, and how to find it.
debug
context.wrenSupport.addModuleFileWatch("plugin", `/my/absolute/path/to/plugin.wren`); // debug => live reload, enter absolute path here
else
context.wrenSupport.addModuleSource("plugin", import("plugin.wren"));
WARNING: It is absolutely necessary to disable live-loading on release, and have a static script instead. Do not use addModuleFileWatch
in a released plug-in.
- Call
context.wrenSupport.callCreateUI();
from your UI constructor. - Call
context.wrenSupport.callReflow();
from your UIreflow()
- (optional) Call
context.wrenSupport.callReflowWhenScriptsChange(dt);
from your UIonAnimate()
. This implements live-reload.
That's it! Your plug-in is scripted.
"I save my plugin.wren
but nothing happens."
- Can you read the debug output? Maybe Visual Studio is redirecting it to its Output window.
- If your script properly reloading? Try to break into
callReflowWhenScriptsChange()
. - Any Wren compile-time error? See debug output.
- Is that class registered? See import "widgets", maybe you are missing one class.
"I want syntax coloration in Visual Studio."
Go find it here: https://github.com/AuburnSounds/wren-port