Skip to content

Making a Scriptable UI

p0nce edited this page May 5, 2024 · 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? 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.


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)
    }
  • 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.

Full Example

See plugin.wren in the 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 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 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 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.

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:wren-support": "~>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.


Common errors

"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

Clone this wiki locally