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:1424
text label widget.
Definition ui.cpp:984
table bounds
widget bounding rect {x,y,width,height}
Definition ui.cpp:766
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:931

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:804

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

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:534
@ Exponential
Exponential mapper, the parameter's range should be strictly positive.
Definition ui.cpp:546
Numeric input widget.
Definition ui.cpp:1297
Horizontal or vertical slider widget.
Definition ui.cpp:1356
Predefined unit types.
Definition ui.cpp:592
@ Hertz
display Hz symbol.
Definition ui.cpp:619
@ PercentNormalized
display % symbol but actual value is in the 0..1 range
Definition ui.cpp:607

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:1164
2 states boolean button.
Definition ui.cpp:864
function changed
callback function used by child widgets to be notified of changes
Definition ui.cpp:776

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:1040
string backgroundColour
background colour: colour string that defines the desired colour.
Definition ui.cpp:1096

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:922

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:1502

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:1231

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:1640

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:1677
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:946
FileFormat::Type acceptedFileFormat
accepted file format
Definition ui.cpp:954
Predefined format file types.
Definition ui.cpp:693
@ Audio
Audio file type (wav, aiff, FLAC, ...)
Definition ui.cpp:704

Mappers and Units

Knob, Slider, and NumBox accept a Mapper and a Unit to control the value curve and display formatting.

A Mapper defines the curve between the widget's visual position and the parameter value:

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:1465
string backgroundColour
background colour.
Definition ui.cpp:1512

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:883
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:773

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.