Primitives
primitives::undo
Coalescing undo/redo command stack for memory edits.
Purpose
primitives::undo sits in front of any memory type T (for a session,
superb::primitives::session::Memory) on the UI thread. It knows nothing
about audio: mutations happen through Command::apply / revert, and nothing
here is real-time safe.
Depends only on the primitives root (for Result / SuperbError). No third-party
deps.
Key types
Command<T>— reversible edit:apply,revert, optionalCoalesceKey, optionalmergeso a run of small edits collapses into one step. Downcasting helpers (is/downcast_ref/downcast_mut/downcast) live ondyn Command<T>.CoalesceKey { kind, target }— identifies a mergeable run (e.g. kind = "clip move", target = clip raw ID).UndoStack<T>— history:push/push_boxed/push_at,undo/redo,seal(end a coalesce run, e.g. on mouse-up),begin_group/end_group/cancel_group,mark_clean/is_clean, depthlimit, optional coalesce timewindow.PropertyEdit<T, V>— ready-made "set this value" command from getter/setter closures; merges by keeping the first previous value and the latest new value.
Usage
use superb::primitives::undo::{CoalesceKey, PropertyEdit, UndoStack};
struct Doc { gain_db: f32 }
let mut doc = Doc { gain_db: 0.0 };
let mut history = UndoStack::new();
let key = CoalesceKey::new(1, 0);
let edit = |db: f32| PropertyEdit::new(
"Set gain",
Some(key),
db,
|d: &Doc| d.gain_db,
|d: &mut Doc, v| { d.gain_db = v; Ok(()) },
);
history.push(&mut doc, edit(-1.0)).unwrap();
history.push(&mut doc, edit(-2.0)).unwrap();
history.push(&mut doc, edit(-3.0)).unwrap();
assert_eq!(history.undo_len(), 1); // coalesced
assert_eq!(doc.gain_db, -3.0);
assert!(history.undo(&mut doc).unwrap());
assert_eq!(doc.gain_db, 0.0);
assert!(history.redo(&mut doc).unwrap());
assert_eq!(doc.gain_db, -3.0);Contract notes:
- Capture "previous" state during
apply, not at construction, so redo after intervening edits stays correct. - A failed
applyinpushrecords nothing. A failedrevert/applyduring undo/redo clears the whole history (memory state is unknown). - Groups nest; only the outermost label becomes the undo step. Undo/redo while a group is open is refused.
Real-time notes
Not RT-safe. Pair with RT-safe dirty flags later if the audio thread needs to know the memory changed; the stack itself stays off-thread.
Limitations / next
- No persistent undo history across process restarts (session snapshots are the memory; the stack is ephemeral).
- The module has no unsafe code: downcasting goes through
dyn Any(trait upcasting, Rust 1.86+). - Session-facing commands live in
superb::primitives::session::commands, not here.