Files
oak-editor/crates/oakundo/README.md
T
Mike-Solar 194d761ade refactor(oakundo): replace the CHandle vtable layer with owned trait objects
With the C ABI facade (oakengine) retired, the frozen-ABI rationale is
gone. UndoCommand now boxes a Send Command trait (new/from_closures/
multi), dropping OakUndoCommandVtable, the userdata trampolines, the
refcount shell, the handle module, and all undostack_* handle exports.
The global facade loses its raw-pointer out-params (can_undo/can_redo
return bool, command_name returns String). oaktimeline/oaknode/
oakplugin/oaktask construct commands directly via UndoCommand::new.
oakundo src is now free of unsafe; behavior (ordering, idempotence,
done flags, groups, observers, 200-row cap) is unchanged and pinned by
the rewritten tests.
2026-08-19 00:41:49 +08:00

69 lines
3.1 KiB
Markdown

# oakundo Rust crate
> Status: **implemented**. Ports the C++ oakundo module
> (`src/undo/src`) to Rust. Template follows `crates/oakplugin`.
## Scope
Replaces the C++ oakundo module (`src/undo/src`): undoable commands
and the undo/redo history stack. The frozen C ABI (`include/undo/*.h`)
and the engine facade that consumed it are gone (see the root
`Cargo.toml` note on `crates/oakengine.bk`): every consumer links the
crate as a plain rlib and uses the value-typed API below.
## Architectural decisions
1. **Trait-object commands replace the vtable pattern.** In C++ other
modules *subclass* `olive::UndoCommand` (`redo()`/`undo()` overrides)
and plug themselves in polymorphically. Rust models the same
polymorphism with a boxed [`undocommand::Command`] trait object:
one-off edits arrive as closure commands
([`undocommand::UndoCommand::from_closures`]) and whole-struct
commands implement the trait and are boxed with
[`undocommand::UndoCommand::new`]; composites are
[`undocommand::MultiUndoCommand`]. The former
`OakUndoCommandVtable` callback table, its `extern "C"` trampolines
and the refcounted `CHandle` layer were deleted with the C ABI —
domain logic dispatches through the trait the same way C++ virtual
dispatch does.
2. **Modified-state callbacks are intentionally omitted.** The C++
`UndoCommand::redo_and_set_modified` pair records/restores a project
dirty flag via `std::function` accessors. The public headers exposed
none of this; the stack drives state via `done_` on the safe type
instead, and the flag callbacks are left as a documented future
extension.
3. **`UndoStack` state machine** is modeled directly on the C++:
two deques — `commands_` (done, oldest at front) and
`undone_commands_` (most-recently-undone at front); `push` clears any
redoable tail, executes redo, and drops the oldest when the cap
(200) is exceeded; `jump` clamps and walks via `undo`/`redo`. The
fresh stack holds a single "New/Open Project" empty command so
`can_undo` is false at the bottom (per `undostack.cpp`).
4. **No merge semantics.** `src/undo/src/*` defines no
`merge_with`/`can_merge`; commands are never coalesced. Tests
reflect this (no merge tests).
## Layout
```
src/
lib.rs crate doc + module map
error.rs error codes (mirrors include/undo/error.h values)
undocommand.rs UndoCommand / Command trait / MultiUndoCommand
undostack.rs UndoStack + empty bottom command
global.rs process-wide stack, groups, observers
tests/ contract tests per module
```
The module has no `unsafe` code and no `extern "C"` surface; panics in
command callbacks propagate as normal process-internal panics (the
process-wide stack recovers a poisoned mutex the same way the former
`guard*` FFI wrappers did).
## Dependency policy
Prefer mature third-party crates (MIT/Apache-2.0/BSD, GPL-compatible)
over hand-rolling; register each addition (name + reason) here. Large
existing C++ libraries (OTIO, OCIO, OIIO, FFmpeg) are NEVER rewritten
— they are consumed through their C ABI / bridge layers.