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.
This commit is contained in:
2026-08-19 00:41:49 +08:00
parent e0aa597f1e
commit 194d761ade
28 changed files with 409 additions and 2482 deletions
+8 -57
View File
@@ -24,8 +24,8 @@
//! an `oaknode::id::NodeId` in that project's `oaknode::graph::Graph`.
//!
//! Undo commands follow the oaktimeline `undocommon` pattern: a command
//! struct is boxed into an `oakundo::undocommand::UndoCommand` vtable
//! command ([`box_command`]); `redo_now`/`undo_now` dispatch to it.
//! struct is boxed into an `oakundo::undocommand::UndoCommand` value
//! ([`box_command`]); `redo_now`/`undo_now` dispatch to it.
use std::sync::{Arc, Mutex, MutexGuard};
@@ -40,7 +40,7 @@ use oaknode::node::{NodeBehavior, NodeCore};
use oaknode::project::Project;
use oaknode::sequence::SequenceBehavior;
use oaknode::track::{BlockRange, TrackBehavior, TrackListBehavior, TrackRange, TrackType};
use oakundo::undocommand::{OakUndoCommandVtable, UndoCommand};
use oakundo::undocommand::UndoCommand;
/// A project, shared like the C++ `olive::Project` smart pointer.
pub type ProjectRef = Arc<Mutex<Project>>;
@@ -905,62 +905,13 @@ pub fn pixel_format_from_code(code: i32) -> oakcore_rs::PixelFormat {
// Undo commands
// ---------------------------------------------------------------------------
/// Command trait for the task-local undo commands (mirrors the
/// oaktimeline `undocommon::Command` pattern).
trait TaskCommand {
/// Apply the change.
fn redo(&mut self);
/// Revert the change.
fn undo(&mut self);
}
/// Command trait for the task-local undo commands (the oakundo command
/// trait; mirrors the oaktimeline `undocommon::Command` pattern).
pub use oakundo::undocommand::Command as TaskCommand;
/// Generic `redo` callback forwarding to [`TaskCommand::redo`].
///
/// # Safety
/// `userdata` must be the `Box<T>` produced by [`box_command`].
unsafe extern "C" fn redo_cb<T: TaskCommand>(userdata: *mut std::ffi::c_void) {
if userdata.is_null() {
return;
}
// SAFETY: box_command allocated a `Box<T>`; the command still owns it.
(unsafe { &mut *(userdata as *mut T) }).redo();
}
/// Generic `undo` callback forwarding to [`TaskCommand::undo`].
///
/// # Safety
/// As `redo_cb`.
unsafe extern "C" fn undo_cb<T: TaskCommand>(userdata: *mut std::ffi::c_void) {
if userdata.is_null() {
return;
}
// SAFETY: as `redo_cb`.
(unsafe { &mut *(userdata as *mut T) }).undo();
}
/// Generic `free` callback dropping the boxed command.
///
/// # Safety
/// `userdata` must be the `Box<T>` produced by [`box_command`].
unsafe extern "C" fn free_cb<T: TaskCommand>(userdata: *mut std::ffi::c_void) {
if userdata.is_null() {
return;
}
// SAFETY: the command owns this box; dropping it frees the command.
unsafe {
drop(Box::from_raw(userdata as *mut T));
}
}
/// Box a task command into an oakundo vtable command value.
/// Box a task command into an oakundo command value.
fn box_command<T: TaskCommand + 'static>(cmd: T) -> UndoCommand {
let userdata = Box::into_raw(Box::new(cmd)) as *mut std::ffi::c_void;
let vtable = OakUndoCommandVtable {
redo: Some(redo_cb::<T>),
undo: Some(undo_cb::<T>),
free_fn: Some(free_cb::<T>),
};
UndoCommand::from_vtable(vtable, userdata)
UndoCommand::new(cmd)
}
/// `FolderAddChildCommand` — add an item to a bin folder