Skip to content

Widgets API

Every Stage widget is addressable from a script by its name (the script identifier you set in the inspector, not the user-visible label). One top-level helper returns a handle:

typescript
widget(name: string): SwWidget

widget(name) always returns a handle. The handle is cheap (a fresh one is created on every call) and holds no state of its own, every read and write goes straight through the engine's widget table. If the name isn't bound to anything, get() simply reads back whatever the engine has for it (0 by default) and set() is a no-op mirror.

SwWidget

typescript
interface SwWidget {
  get(): number;                               // current value (booleans read as 0/1)
  set(value: number): void;                    // write the value
  on(event: 'change', fn: (value: number) => void): void;
}

That is the entire surface. There is no toggle(), setLabel(), setColor(), type, label, or off(), the handle only reads, writes, and subscribes.

get() and set()

Continuous widgets (knob, fader, drawbar, expression pedal): get() returns 0..1 and set(v) takes the same range. Your widget's scaling curve still applies to what the binding sees (see Stage → Scaling curves).

Boolean widgets (button, toggle, LED, LED-button, sustain pedal): get() returns 0 or 1 and set() takes 0 or 1.

typescript
widget('GAIN').set(0.5);                      // half
const tempoOn = widget('TEMPO_LED').get();    // 0 | 1

// No toggle() helper, read-invert-write:
const mute = widget('MUTE_1');
mute.set(mute.get() > 0.5 ? 0 : 1);           // flip

on('change', fn)

Fires when the widget's value changes for any reason: mouse, MIDI, OSC, or a script write. The handler receives the new value (a number). Subscribers are removed automatically on script reload, nothing to clean up by hand.

typescript
const muteOne = widget('MUTE_1');
muteOne.on('change', (v) => {
  console.log(`MUTE_1 is now ${v >= 0.5 ? 'ON' : 'OFF'}`);
});

Loop guard

A handler that writes to its own widget from its own on('change') can re-fire itself. The engine's widget bridge dedupes exact-value writes, but a handler that writes a slightly different value each time can still ping-pong. Prefer writing to a different widget, or use the bind / exclusive helpers, which carry a re-entry guard.

Common patterns

A mute panel driven by hardware

typescript
const mutes = [1, 2, 3, 4, 5, 6, 7, 8].map((i) => widget(`MUTE_${i}`));
const akai = midi.input('MIDI Mix');

akai.on('cc', (m) => {
  if (m.value !== 127) return;                // ignore release
  const i = m.controller - 22;
  if (i >= 0 && i < mutes.length) {
    const w = mutes[i];
    w.set(w.get() > 0.5 ? 0 : 1);             // toggle
  }
});

LED feedback to the controller

typescript
const out = midi.output('MIDI Mix');
const mutes = [1, 2, 3, 4, 5, 6, 7, 8].map((i) => widget(`MUTE_${i}`));

const setLed = (i: number, on: boolean) =>
  out.sendNoteOn(1, 1 + i * 3, on ? 127 : 0);

mutes.forEach((w, i) => {
  w.on('change', (v) => setLed(i, v > 0.5));
});

onInit(() => {
  mutes.forEach((w, i) => setLed(i, w.get() > 0.5));
});

Radio-style switch across ungrouped widgets

set() writes plain values, so a single toggle can drive two mixer strips in opposition:

typescript
// One toggle named PIANO_SRC: ON = audio source, OFF = MIDI source.
const src = widget('PIANO_SRC');
const audioMute = widget('PIANO_AUDIO_MUTE'); // mixer strip 1 mute
const midiMute  = widget('PIANO_MIDI_MUTE');  // mixer strip 2 mute

const apply = (toAudio: boolean) => {
  audioMute.set(toAudio ? 0 : 1);
  midiMute.set(toAudio ? 1 : 0);
};

src.on('change', (v) => apply(v > 0.5));
onInit(() => apply(src.get() > 0.5));

For a plain radio group with no script, set the same radio-group field on the widgets in the inspector, the cascade handles exclusivity. See Stage → Groups. Reach for a script when each target needs its own value or scaling.

Next: MIDI input & output.

Proprietary software, used under the Stagewright Software Licence.