Skip to content

Making a Scriptable UI

p0nce edited this page Jan 6, 2022 · 23 revisions

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.


Pros and cons of Wren scripting in Dplug

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.


What can you do exactly from Wren?

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.

Full Example

See plugin.wren int he Distort example.

  • The available exposed API is ui.wren. This is a Dplug-specific Wren API implemented in the dplug:wren-support sub-package.

  • The other imported Wren module is widgets, whose code is auto-generated based on what widgets were exposed to Wren.


Limitations (as of Dplug v12.3)

  • 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 call addChild manually.
  • @ScriptExport classes must derive from UIElement.
  • 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.

How to enable Wren scripting in your UI

STEP 1: Add dplug:wren-support as dependency.

Add the dplug:wren-support sub-package as a dependency to you dub.json.

"dependencies":
{
    "dplug:dsp": "~>12.3"
}

STEP 2: Add an import directory

Add a "scripts" string import directory to you dub.json.

"stringImportPaths": ["scripts"],

STEP 3: Start a script

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.

STEP 4: Adding the Wren VM

In your gui.d:

  1. Add import dplug.wren; at top-level.
  2. Call context.enableWrenSupport(); in the UI constructor.
  3. Call context.disableWrenSupport(); in the UI destructor.

STEP 5: Setting widgets IDs

Each of your exposed widget should have a unique ID to be queried from Wren.

In your gui.d main UI class:

  1. Add mixin(fieldIdentifiersAreIDs!MyGUI); in the UI constructor.

STEP 6: Expose @ScriptProperty to Wren

Wren needs to know which classes exist and what property exists. Dplug thus defines 2 UDAs: @ScriptExport and @ScriptProperty.

  1. 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;
}
  1. 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)

STEP 6: Add module sources

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.

STEP 7: Calling Wren methods

  1. Call context.wrenSupport.callCreateUI(); from your UI constructor.
  2. Call context.wrenSupport.callReflow(); from your UI reflow()
  3. (optional) Call context.wrenSupport.callReflowWhenScriptsChange(dt); from your UI onAnimate(). This implements live-reload.

That's it! Your plug-in is scripted.