uvi-script
Musical event scripting with Lua
Loading...
Searching...
No Matches
User Interface

This guide provides a comprehensive overview of how to create custom user interfaces for your scripts. For a detailed reference of all available widgets and functions, see the User Interface Classes and Functions group.

Overview

The UVIScript UI system allows you to build powerful, interactive control surfaces for your instruments and effects. You can create a variety of widgets, from simple buttons and knobs to complex tables and XY pads.

Key features include:

  • A rich library of over 20 widgets.
  • A flexible layout system with both automatic and manual positioning.
  • An intuitive event-handling system using per-widget callbacks.
  • A convenient table-based syntax for creating and configuring widgets.
Note
All UI widgets must be created during the main script execution. They cannot be created within real-time callbacks like onNote or onRelease.

Quick Start: A Simple Knob

Let's start with a complete example. This script creates a single knob that controls the script's gain and a label to display the current value.

-- Create a Knob and a Label
local gainKnob = Knob{"Gain", 0.5, 0.0, 1.0}
local valueLabel = Label{"Value"}
-- position label below the knob
valueLabel.bounds = {gainKnob.x, gainKnob.y + gainKnob.height, gainKnob.width, 20}
-- react to user input
function gainKnob:changed()
valueLabel.text = string.format("%.1f %%", self.value * 100)
end
gainKnob:changed()
-- use the value in your script
function onNote(e)
local velocity = e.velocity * gainKnob.value
playNote(e.note, velocity)
end
Knob widget.
Definition ui.cpp:1520
text label widget.
Definition ui.cpp:1080
table bounds
widget bounding rect {x,y,width,height}
Definition ui.cpp:862
void onNote(table e)
event callback that will receive all incoming note-on events if defined.
function playNote(note, vel, duration, layer, channel, input, vol, pan, tune, slice, oscIndex)
helper function to generate a note event.
Definition api.lua:942

This example demonstrates the three core steps: creating widgets, defining a changed callback to react to user input, and using the widget's value in your script's logic.

Creating Widgets: The Table Constructor

All widgets are created using a convenient table constructor syntax. This allows you to set properties like position, size, and the changed callback at the moment of creation.

Instead of writing this:

local button = Button("My Button")
button.x = 5
button.y = 20
button.width = 120
button.height = 20
button.changed = function(self) print(self.name .. " clicked") end
stateless transient button.
Definition ui.cpp:900

You can write this, using curly brackets {}:

local button = Button{"My Button",
x = 5, y = 20, width = 120, height = 20,
changed = function(self) print(self.name .. " clicked") end
}

The Layout System

The UI canvas has a fixed width of 720px. By default, its height adjusts automatically to fit the widgets, but you can also specify a custom height. You can arrange widgets using either the automatic grid system or by setting pixel-perfect manual coordinates.

Automatic Layout

By default, widgets are placed on a grid in their creation order. Each new widget is placed in the first available empty cell, flowing from left-to-right, top-to-bottom. This is useful for quickly creating simple layouts.

The grid consists of:

  • 6 columns, each 120px wide.
  • 19 rows, each 20px high.
  • A 5px margin around cells.

Some widgets, like Knobs and Sliders, are larger and may span multiple rows or columns by default.

Manual Layout

For precise control, you can manually position and size widgets using their properties. You can set these during creation or modify them later.

  • x, y: The top-left coordinate of the widget.
  • width, height: The dimensions of the widget.
  • position: A table {x, y}.
  • size: A table {width, height}.
  • bounds: A table {x, y, width, height}.
-- Position a label at x=10, y=50 with a size of 100x20 pixels
local myLabel = Label{"Info", bounds = {10, 50, 100, 20}}

You can also use the moveControl() function to snap a widget to a specific cell in the auto-layout grid.

Script Size and Performance View

Use setSize to set the overall dimensions of your script UI, and setHeight if you only need to adjust the height (the width defaults to 720px):

setSize(650, 380) -- custom width and height
setHeight(200) -- adjust height only (720px width)
function setHeight(h)
sets the desired height of the user interface.
Definition ui.lua:69
void setSize(number w, number h)
set the script UI dimensions explicitly.

To make your script UI visible in the host's performance view, call makePerformanceView at the end of your script. This is what allows end users to see and interact with your UI during performance:

-- create all widgets and panels first ...
setSize(650, 380)
void makePerformanceView()
make this script User Interface visible in performance view.
Definition ui.lua:107

Handling User Input: The changed Callback

To respond to user interaction, you assign a function to a widget's changed property. This function is called whenever the widget's value changes.

The callback function always receives the widget instance as its first argument, conventionally named self.

local myKnob = Knob{"Frequency", 500, 20, 20000}
-- Assign a function to the 'changed' property
myKnob.changed = function(self)
print("New frequency: " .. self.value)
end

You can also use Lua's colon syntax, which provides the self argument implicitly:

function myKnob:changed()
print("New frequency: " .. self.value) -- self is implicit
end

For Table widgets, the callback receives a second argument: the index of the value that changed.

function myTable:changed(index)
print("Table value at index " .. index .. " changed to: " .. self:getValue(index))
end
Note
Widget callbacks are not coroutines — you cannot call wait, waitBeat, or waitForRelease directly inside a changed callback. To perform delayed operations, use spawn to launch a separate thread from within the callback.

Temporary Value Display

A common pattern is to flash a knob's value on a Label, then revert to the default name after a delay. Since wait cannot be called in a widget callback, use spawn :

local flashCounters = {}
function flashLabel(label, text, defaultText, ms)
label.text = text
flashCounters[label] = (flashCounters[label] or 0) + 1
local myId = flashCounters[label]
spawn(function()
wait(ms or 1000)
if flashCounters[label] == myId then -- only revert if no newer update
label.text = defaultText
end
end)
end
local resLabel = Label{"RES"}
local resKnob = Knob{"Resonance", 0, 0, 1}
resKnob.changed = function(self)
flashLabel(resLabel, string.format("%.0f%%", self.value * 100), "RES", 1500)
end
function changed
callback function used by child widgets to be notified of changes
Definition ui.cpp:872
function wait(ms)
suspend the current thread callback execution for the given number of samples.
Definition wrapper.lua:39
function spawn(fun,...)
Launch a function in a separate parallel execution thread (deferred execution)
Definition api.lua:1176

The counter ensures that only the last update reverts the label, so rapid knob turns don't cause flickering. See the TemporaryDisplay example for a complete working script.

Widget Catalogue

Value Controls

Knob — Rotary control, ideal for continuous parameters. Supports strip images for custom graphics via setStripImage().

Slider — Horizontal or vertical fader. Set vertical to true in the constructor for a vertical slider. Supports background and handle images.

NumBox — Numeric text input. Users can click and drag or type a value directly.

local cutoff = Knob{"Cutoff", 20000, 20, 20000,
mapper = Mapper.Exponential, unit = Unit.Hertz}
local mix = Slider{"Mix", 0.5, 0, 1, unit = Unit.PercentNormalized}
local voices = NumBox{"Voices", 4, 1, 16, true} -- integer mode
Predefined mapper types.
Definition ui.cpp:630
@ Exponential
Exponential mapper, the parameter's range should be strictly positive.
Definition ui.cpp:642
Numeric input widget.
Definition ui.cpp:1393
Horizontal or vertical slider widget.
Definition ui.cpp:1452
Predefined unit types.
Definition ui.cpp:688
@ Hertz
display Hz symbol.
Definition ui.cpp:715
@ PercentNormalized
display % symbol but actual value is in the 0..1 range
Definition ui.cpp:703

Buttons

Button — Stateless push button. Triggers its callback once per click.

OnOffButton — Toggle button with on/off state. self.value is a boolean.

MultiStateButton — Cycles through multiple named states. Alternative to Menu for small option sets.

local trigger = Button{"Play"}
trigger.changed = function(self) playNote(60, 100, 500) end
local bypass = OnOffButton{"Bypass", false}
bypass.changed = function(self) print("Bypass:", self.value) end
local mode = MultiStateButton{"Mode", {"Poly", "Mono", "Legato"}}
mode.changed = function(self) print("Mode:", self.selectedText) end
MultiStateButton widget.
Definition ui.cpp:1260
2 states boolean button.
Definition ui.cpp:960

Selection

Menu — Dropdown selector for longer lists of options.

local scale = Menu{"Scale", {"Major", "Minor", "Dorian", "Mixolydian"}}
scale.backgroundColour = "black"
scale.textColour = "white"
Menu widget.
Definition ui.cpp:1136
string backgroundColour
background colour: colour string that defines the desired colour.
Definition ui.cpp:1192

Set hierarchical to true to create nested sub-menus from path-like entries. Use "/" as separator — the menu builds the tree automatically:

local irMenu = Menu{"IR", {
"Halls/Large Hall", "Halls/Small Hall",
"Plates/Bright", "Plates/Dark",
"Rooms/Studio A", "Rooms/Studio B"
}, hierarchical = true}
irMenu.changed = function(self)
print("Selected:", self.selectedText) -- e.g. "Halls/Large Hall"
end

See the IRLoader example for a complete hierarchical menu with impulse response loading.

Display

Label — Text display with custom font, alignment, and colour.

Image — Displays a static image (PNG, JPG). Supports an overImage for mouse-over state.

SVG — Displays a Scalable Vector Graphics image.

local title = Label{"title", text = "My Instrument", align = "centred"}
local logo = Image("resources/logo.png")
logo.pos = {10, 10}
Image widget.
Definition ui.cpp:1018

Containers

Panel — Groups widgets together. Create sub-widgets using method syntax:

local panel = Panel{"Controls"}
panel.bounds = {0, 0, 720, 100}
panel.backgroundColour = "3f000000"
-- sub-widgets belong to the panel
local k = panel:Knob("Gain", 0.5, 0, 1)
local s = panel:Slider("Pan", 0, -1, 1)
local b = panel:OnOffButton("Mute", false)
Panel widget.
Definition ui.cpp:1806

Viewport — Scrollable container. Useful when content exceeds available space.

Panel Switching (Tabs)

A common pattern for multi-page UIs: use OnOffButton as tab selectors and toggle Panel visibility. Each tab button deselects the others and shows only the matching panel:

local tabNames = {"Main", "FX", "Seq"}
local tabButtons, tabPanels = {}, {}
for i = 1, #tabNames do
tabButtons[i] = OnOffButton{tabNames[i], i == 1,
bounds = {(i - 1) * 80, 0, 78, 25},
backgroundColourOff = "333333", backgroundColourOn = "FF8800",
persistent = false}
tabPanels[i] = Panel{bounds = {0, 30, 500, 100}}
end
tabPanels[1]:Knob("Volume", 0.8, 0, 1)
tabPanels[2]:Knob("Cutoff", 20000, 20, 20000)
tabPanels[3]:Table("Steps", 8, 0, -12, 12, true)
for i = 1, #tabNames do
tabButtons[i].changed = function()
for j = 1, #tabNames do
tabButtons[j]:setValue(j == i, false) -- deselect others
tabPanels[j].visible = (j == i) -- show matching panel
end
end
end
tabButtons[1]:changed() -- show first tab
Table widget.
Definition ui.cpp:1327

See the PanelSwitcher example for a complete working script.

Data

Table — Multi-value bar editor. Commonly used for sequencer steps, velocity curves, or envelope shapes.

XY — Two-dimensional pad returning X and Y values.

local steps = Table{"Steps", 16, 0, -12, 12, true} -- 16 steps, int, range -12..12
steps.changed = function(self, index)
print("Step", index, "=", self:getValue(index))
end
local pad = XY{"Pad", 0.5, 0.5}
pad.changed = function(self)
print("X:", self.x, "Y:", self.y)
end
XY widget.
Definition ui.cpp:1944

Monitoring

AudioMeter — Real-time level meter bound to an element's audio bus.

WaveView — Displays the waveform of a sample.

local meter = AudioMeter("out", Program, true, 0, true)
meter.bounds = {680, 0, 40, 100}
AudioMeter widget.
Definition ui.cpp:1981
A Patch that represents a monotimbral instrument.
Definition Engine.cpp:238

File

FileSelector — Embedded file browser for selecting samples or presets.

DnDArea — Drag-and-drop zone. Fires a callback when a file is dropped.

local dnd = DnDArea{"Drop"}
dnd.fileDropped = function(self)
print("Dropped:", self.filepath)
end
DnDArea widget.
Definition ui.cpp:1042
FileFormat::Type acceptedFileFormat
accepted file format
Definition ui.cpp:1050
Predefined format file types.
Definition ui.cpp:789
@ Audio
Audio file type (wav, aiff, FLAC, ...)
Definition ui.cpp:800

Parameter-Bound Widgets

Parameter-bound widgets drive an existing Element parameter directly instead of carrying their own script value. This is the inverse of parameterexport": rather than publishing a widget value to the host, the widget reads from and writes to a parameter that already lives on an engine @ref Element — an oscillator, effect, modulation, etc. Range, default, @ref Unit and @ref Mapper all come from the target parameter, so no min/max/default is passed — only the element and the parameter name. The first argument is an @ref Element reference (or a path string identifying one); the second is the parameter name, the same name accepted by @ref Element::setParameter "setParameter". The main benefit over a plain widget is that the control adopts the parameter's @ref Unit @b and @ref Mapper. From the unit, values are both @b formatted for display @b and @b parsed back from typed text for free: the user can type @c "1.5 kHz", @c "-6 dB" or @c "50 %" and it is converted to the correct underlying value, with the right suffix shown. From the mapper, the control follows the parameter's own response curve (e.g. exponential for a frequency), so the knob travel matches the engine. You get all of this without setting @c unit, @c mapper or @c displayText yourself, and without writing a @c changed handler. @ref ParamKnob, @ref ParamSlider, @ref ParamOnOffButton, @ref ParamMenu and @ref ParamNumBox mirror their standard counterparts but bind to a parameter: @code{.lua} local flt = Program.layers[1].keygroups[1].inserts[1] local cutoff = ParamKnob(flt, "Freq") -- knob follows the filter cutoff local bypass = ParamOnOffButton(flt, "Bypass") -- toggle follows the bypass state -- table form also accepts widget options (bounds, colours, ...) local vol = ParamKnob{Program.auxs[1].inserts[4], "Volume", bounds = {0, 0, 80, 80}} @endcode Moving the widget writes the parameter; changing the parameter elsewhere (host automation, modulation, preset load) updates the widget. No @c changed callback is required to keep them in sync. These five are also available as @ref Panel methods, for placing them inside a container: @code{.lua} local panel = Panel{"Filter"} panel:ParamKnob(flt, "Freq") panel:ParamSlider(flt, "Q") @endcode @ref ParameterValue is a non-visual, @b logical binding to a parameter — it draws nothing. Use it to read or drive a parameter from script code without placing a control on the UI. It is a global function only (not a @ref Panel method) and does not accept layout options: @code{.lua} local vol = ParameterValue(Program.auxs[1].inserts[4], "Volume") vol.value = -6 -- write the parameter print(vol.value) -- read it back @endcode @section UIMappersUnits Mappers and Units @ref Knob, @ref Slider, and @ref NumBox accept a @ref Mapper and a @ref Unit to control the value curve and display formatting. A @b Mapper defines the curve between the widget's visual position and the parameter value: <table class="markdownTable"> <tr class="markdownTableHead"> <th class="markdownTableHeadNone"> Mapper

Formula

Typical use

Linear (default)

min + (max-min) × pos

General purpose

Exponential

min × (max/min)^pos

Frequency, time

Quadratic

min + (max-min) × pos²

Gentle curve

Cubic

min + (max-min) × pos³

Volume (perceptual)

SquareRoot

min + (max-min) × √pos

Inverse gentle

See Mapper for the full list (QuarticRoot, CubeRoot, Quartic, Quintic, etc.).

A Unit tells the engine how to format the displayed value:

Unit Display Auto-scale
Hertz Hz → kHz above 1000
Seconds s → ms below 1
MilliSeconds ms → s above 1000
Cents ct → st above 100
LinearGain dB linear 0..1 → dB display
PercentNormalized % 0..1 → 0%..100%
Decibels dB Direct dB value
SemiTones st Semitones
Pan L/R -1..1 pan display
MidiKey C3, D#4... MIDI note name

See Unit for the complete list.

When no built-in unit fits, use the displayText property to override:

gate.changed = function(self)
self.displayText = string.format("%d%%", self.value * 100)
end

Colours

All colour properties accept a string in one of these formats:

Format Example Description
Named "red", "darkgrey", "blue" CSS-style named colours
RGB hex "#FF00CC" 6-digit hex (opaque)
ARGB hex "#3C00FECD" 8-digit hex (with alpha)

Common colour properties: backgroundColour, textColour, fillColour, outlineColour, thumbColour, trackColour.

local k = Knob{"Gain", 0.5, 0, 1}
k.fillColour = "lightgrey"
k.outlineColour = "#FF8800" -- orange
local panel = Panel{"bg"}
panel.backgroundColour = "3f000000" -- semi-transparent black
string fillColour
fill colour.
Definition ui.cpp:1561
string backgroundColour
background colour.
Definition ui.cpp:1816

Images and Retina

Many widgets accept image paths for custom graphics. Supported formats: PNG and JPG.

setBackground("resources/background.png")
local btn = OnOffButton{"Play", false}
btn.normalImage = "resources/play_off.png"
btn.pressedImage = "resources/play_on.png"
local knob = Knob{"Filter", 0.5, 0, 1}
knob:setStripImage("resources/knob_strip.png", 128) -- 128 frames
string normalImage
image path for normal buttonState
Definition ui.cpp:979
void setBackground(string imagePath)
set the script background Image.

For Retina / HiDPI displays, place a @2x variant alongside the standard file:

  • resources/knob.png — standard resolution
  • resources/knob@2x.png — 2× pixel dimensions

The engine automatically picks the appropriate variant.

State and Persistence

Automatic Persistence

Set persistent to true (the default) to have widget values automatically saved and restored with the preset:

local mix = Knob{"Mix", 0.5, 0, 1}
mix.persistent = true -- value saved/restored automatically (default)
bool persistent
flag to tell if the widget values should be serialized when saving.
Definition ui.cpp:869

Parameter Export

Set exported to true to expose a widget as a host-automatable parameter. The paramId is assigned automatically but can be set manually for stable mapping:

local cutoff = Knob{"Cutoff", 20000, 20, 20000}
cutoff.exported = true -- visible to the host for automation

For custom data beyond widget values, use onSave / onLoad callbacks. See Asynchronous Operations for file-based persistence with saveData / loadData.

Keyboard Colours

Use setKeyColour and resetKeyColour to colorize the on-screen keyboard. This is useful for indicating key ranges, keyswitches, or active notes:

-- mark keyswitch range in red
for i = 36, 43 do
setKeyColour(i, "FF0000")
end
-- mark valid key range with transparent white
for i = 48, 84 do
setKeyColour(i, "#00FFFFFF")
end
function setKeyColour(note, colour)
customize the keyboard colours.
Definition ui.lua:50

Next Steps

Explore the full widget reference with all properties and methods in the User Interface Classes and Functions module documentation.