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
| Component | One gesture |
|---|---|
| Knob, Fader, NumberBox | A 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. |
| ToggleGroup | A 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.