daw-ui

Gestures, undo and automation

Every user action is one gesture, so it becomes one undo step or one automation pass.

A drag on a fader sends dozens of values. An undo history should get one step for it, and an automation lane should know when the user grabbed the fader and when they let go. Plugins call this a change gesture; every daw-ui control reports it:

<Fader.Root
  onGestureStart={() => history.begin('Volume')}
  onValueChange={(db) => track.setVolume(db)}
  onGestureEnd={() => history.commit()}
/>

What one gesture is

ComponentOne gesture
Knob, Fader, NumberBoxA drag, from press to release. A key press. A burst of wheel events, ending 400 ms after the last one. A reset by double-click or Delete. A typed value.
ToggleGroupA press, including the toggles an exclusive press turns off. A paint stroke. A Shift+arrow paint step.

A gesture starts with its first change, not with the press: a click that changes nothing leaves no empty undo step.

Playing and recording automation

read plays automation: the knob calls it once per frame and shows the value without rendering. While the user holds the knob, read is ignored and the user's value wins, which is what DAWs call touch mode. Write what the user does into the lane while the gesture is open:

<Knob.Root
  read={() => automation.valueAt('cutoff', transport.time)}
  onGestureStart={() => automation.touch('cutoff')}
  onValueChange={(hz) => automation.write('cutoff', transport.time, hz)}
  onGestureEnd={() => automation.release('cutoff')}
/>

When the gesture ends, read takes over again and returns what was just recorded.

Why a value changed

onValueChange and onPressedChange receive details.reason: "drag", "keyboard", "wheel", "reset" or "input" for values, and "press", "release", "keyboard", "paint" or "exclusive" for toggles, with the native event.

How much a value changed

onValueChange also receives details.delta, the change from the previous value. Relative uses read it instead of the value: an endless knob that steps through presets, or a control that sends MIDI in relative mode.

<Knob.Root endless max={24} step={1} onValueChange={(_, { delta }) => browser.move(delta)} />

On a control that wraps, delta goes the way the user moved: turning up from 350° to 10° is +20, not −340.

On this page