-
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?
-
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.
-
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.
The benefits of seeing directly your change on screen is interesting, as it should positively influence the result. It's really meant as a tool to lessen the cost to try a graphical change.
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) }
- Plus everything you can normally do in Wren.
See plugin.wren
int he 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 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 have two
@ScriptExport
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:dsp": "~>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.