refactor: workspace layout — crates/, app at root, legacy C++ removed

Single mechanical restructure commit:
- root Cargo.toml = oakapp bin + workspace; one cargo build produces
  oakapp, oak-cli, oak-worker, liboakengine.dylib
- app/rust/src -> src/ (app at repo root, no rust/ nesting)
- src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore;
  src/bindings/oakotio -> crates/oakotio; src/engine/rust ->
  crates/oakengine (keeps cdylib+staticlib+rlib)
- public C headers include/<mod>/ -> crates/oakengine/include/<mod>/
- OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone)
- legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/,
  app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets
  submodule, otio-install, all build-* output (~40GB)
- oakstorage kept but excluded from the workspace (skeleton w/ todos);
  gpui excluded (own workspace)
- verified: cargo build green, cargo test --workspace 1845/0
  (with the documented OCIO_RS_* env override for the homebrew OCIO)
This commit is contained in:
2026-08-10 20:24:25 +08:00
parent f8540e3892
commit 013a175707
4212 changed files with 8331 additions and 2274987 deletions
+62
View File
@@ -0,0 +1,62 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Error codes, mirroring `include/undo/error.h`; project-wide
//! -MMCCCC scheme (module 02), pass-through untranslated.
/// Success.
pub const OAKUNDO_OK: i32 = 0;
/// Empty handle or invalid argument.
pub const OAKUNDO_E_INVALID: i32 = -20001;
/// Call not valid in the current state.
pub const OAKUNDO_E_STATE: i32 = -20002;
/// The underlying operation failed.
pub const OAKUNDO_E_FAILED: i32 = -20003;
/// Index out of range / entry not found.
pub const OAKUNDO_E_NOT_FOUND: i32 = -20004;
/// Allocation failed.
pub const OAKUNDO_E_NOMEM: i32 = -20005;
/// Crate-internal result type.
pub type Result<T> = std::result::Result<T, Error>;
/// Crate-internal error.
#[derive(Debug)]
pub enum Error {
/// Empty handle or invalid argument.
Invalid,
/// Wrong state.
State,
/// Operation failed (context string is log-only).
Failed(String),
/// Not found.
NotFound,
/// Out of memory.
NoMem,
}
impl Error {
/// Map to the public error code.
pub fn code(&self) -> i32 {
match self {
Error::Invalid => OAKUNDO_E_INVALID,
Error::State => OAKUNDO_E_STATE,
Error::Failed(_) => OAKUNDO_E_FAILED,
Error::NotFound => OAKUNDO_E_NOT_FOUND,
Error::NoMem => OAKUNDO_E_NOMEM,
}
}
}
+432
View File
@@ -0,0 +1,432 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! C ABI export layer: implements `include/undo/*.h` verbatim.
//!
//! Organization: one submodule per public header. The authoritative
//! function list is the header itself; each submodule below carries a
//! complete inventory comment plus export stubs. Bodies only unwrap
//! handles, call the safe Rust domains, and map results through
//! [`crate::handle::guard*`]. `include/undo/error.h` exports macros
//! only, so it is folded into this module's preamble instead of getting
//! its own submodule.
//!
//! Handles cross the boundary by field copy between the public
//! [`OakUndoCommand`]/[`OakUndoStack`] structs and the crate-internal
//! [`CHandle`]. Stacks live behind a `Mutex` (shared state), commands in
//! a `CommandBox`.
use std::ffi::{c_char, c_int, c_void, CStr};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Mutex;
use crate::error::{Error, Result};
use crate::handle::{guard, guard_handle, guard_void, make_owned, CHandle};
use crate::undocommand::{
command_from_borrowed, command_from_owned, command_take, command_to_mut, command_to_ref,
UndoCommand, OakUndoCommandVtable,
};
use crate::undostack::UndoStack;
/// `include/undo/error.h` — macros only, no exported functions.
///
/// `OAKUNDO_OK` and the `OAKUNDO_E_*` codes are mirrored as
/// [`crate::error`] constants; `OAKUNDO_ABI_VERSION` lives in
/// [`crate::handle`].
/// `include/undo/undocommand.h` handle type — `OakUndoCommand`
/// (`{ctx, addref, release, abi_version}`). Single-lib unification:
/// aliases the shared [`CHandle`] (identical layout; the C ABI is
/// unchanged).
pub type OakUndoCommand = CHandle;
/// `include/undo/undostack.h` handle type — `OakUndoStack`. Single-lib
/// unification: aliases the shared [`CHandle`].
pub type OakUndoStack = CHandle;
/// Lock the stack behind `stack` and run `f` on it. `E_INVALID` for an
/// empty handle. A poisoned mutex is recovered (its inner value is
/// still valid).
fn with_stack<R>(stack: OakUndoStack, f: impl FnOnce(&mut UndoStack) -> Result<R>) -> Result<R> {
// Keep a local handle so `get`'s returned reference lives for the
// whole call (its lifetime ties to the `&CHandle` argument).
let ch = stack;
let m = unsafe { crate::handle::get::<Mutex<UndoStack>>(&ch) }.ok_or(Error::Invalid)?;
let mut guard = m.lock().unwrap_or_else(|e| e.into_inner());
f(&mut guard)
}
/// Read a NUL-terminated C string; `NULL` yields an empty string
/// (mirrors the C++ `name ? name : ""`).
fn read_name(name: *const c_char) -> String {
if name.is_null() {
String::new()
} else {
// SAFETY: `name` is a valid NUL-terminated string supplied by the
// caller, or NULL (already handled).
unsafe { CStr::from_ptr(name).to_string_lossy().into_owned() }
}
}
/// `include/undo/undocommand.h` exports (complete inventory):
/// command_init / command_init_multi / command_multi_add_child /
/// command_multi_child_count / command_multi_child / command_redo_now /
/// command_undo_now / command_free.
pub mod command {
use super::*;
/// `oakundo_command_init`: vtable-backed command, refcount 1.
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_init(
vtable: *const OakUndoCommandVtable,
userdata: *mut c_void,
) -> OakUndoCommand {
guard_handle(|| unsafe {
if vtable.is_null() {
return Ok(CHandle::null());
}
let table = *vtable;
Ok(command_from_owned(UndoCommand::from_vtable(table, userdata)))
})
}
/// `oakundo_command_init_multi`: empty multi command, refcount 1.
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_init_multi() -> OakUndoCommand {
guard_handle(|| unsafe {
Ok(command_from_owned(UndoCommand::multi()))
})
}
/// `oakundo_command_multi_add_child` (stack takes one child ref).
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_multi_add_child(
multi: OakUndoCommand,
child: OakUndoCommand,
) -> c_int {
guard(|| unsafe {
let parent = command_to_mut(multi.ctx).ok_or(Error::Invalid)?;
if !parent.is_multi() {
return Err(Error::Invalid);
}
let child_cmd = command_take(child.ctx)?;
parent.multi_add_child(child_cmd);
Ok(())
})
}
/// `oakundo_command_multi_child_count`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_multi_child_count(
multi: OakUndoCommand,
out_count: *mut c_int,
) -> c_int {
guard(|| unsafe {
if out_count.is_null() {
return Err(Error::Invalid);
}
let parent = command_to_ref(multi.ctx).ok_or(Error::Invalid)?;
if !parent.is_multi() {
return Err(Error::Invalid);
}
*out_count = parent.multi_child_count() as c_int;
Ok(())
})
}
/// `oakundo_command_multi_child` (returned handle carries own ref).
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_multi_child(
multi: OakUndoCommand,
index: c_int,
out_child: *mut OakUndoCommand,
) -> c_int {
guard(|| unsafe {
if out_child.is_null() {
return Err(Error::Invalid);
}
let parent = command_to_ref(multi.ctx).ok_or(Error::Invalid)?;
if !parent.is_multi() {
return Err(Error::Invalid);
}
if index < 0 {
return Err(Error::NotFound);
}
let child = parent.multi_child(index as usize)?;
let ptr = child as *const UndoCommand as *mut UndoCommand;
*out_child = command_from_borrowed(ptr);
Ok(())
})
}
/// `oakundo_command_redo_now`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_redo_now(
command: OakUndoCommand,
) -> c_int {
guard(|| unsafe {
let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?;
c.redo_now();
Ok(())
})
}
/// `oakundo_command_undo_now`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_undo_now(
command: OakUndoCommand,
) -> c_int {
guard(|| unsafe {
let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?;
c.undo_now();
Ok(())
})
}
/// `oakundo_command_free`: NULL/empty no-op; clears `command->ctx`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_command_free(command: *mut OakUndoCommand) {
guard_void(|| unsafe {
if command.is_null() || (*command).ctx.is_null() {
return;
}
if let Some(release) = (*command).release {
release((*command).ctx);
}
(*command).ctx = std::ptr::null_mut();
})
}
}
/// `include/undo/undostack.h` exports (complete inventory):
/// undostack_init / undostack_free / undostack_push /
/// undostack_push_pre_executed / undostack_undo / undostack_redo /
/// undostack_jump / undostack_clear / undostack_can_undo /
/// undostack_can_redo / undostack_count / undostack_index /
/// undostack_command_text / undostack_command_is_done.
pub mod undostack {
use super::*;
/// `oakundo_undostack_init`: fresh stack, refcount 1.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_init() -> OakUndoStack {
guard_handle(|| {
Ok(make_owned(Mutex::new(UndoStack::new())))
})
}
/// `oakundo_undostack_free`: NULL/empty no-op; clears `stack->ctx`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_free(stack: *mut OakUndoStack) {
guard_void(|| unsafe {
if stack.is_null() || (*stack).ctx.is_null() {
return;
}
if let Some(release) = (*stack).release {
release((*stack).ctx);
}
(*stack).ctx = std::ptr::null_mut();
})
}
/// `oakundo_undostack_push`: redo then record; drops redoable tail.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_push(
stack: OakUndoStack,
command: OakUndoCommand,
name: *const c_char,
) -> c_int {
guard(|| {
with_stack(stack, |s| unsafe {
let cmd = command_take(command.ctx)?;
let name = read_name(name);
s.push(cmd, &name);
Ok(())
})
})
}
/// `oakundo_undostack_push_pre_executed`: record without redoing.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_push_pre_executed(
stack: OakUndoStack,
command: OakUndoCommand,
name: *const c_char,
) -> c_int {
guard(|| {
with_stack(stack, |s| unsafe {
let cmd = command_take(command.ctx)?;
let name = read_name(name);
s.push_pre_executed(cmd, &name);
Ok(())
})
})
}
/// `oakundo_undostack_undo`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_undo(stack: OakUndoStack) -> c_int {
guard(|| with_stack(stack, |s| s.undo()))
}
/// `oakundo_undostack_redo`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_redo(stack: OakUndoStack) -> c_int {
guard(|| with_stack(stack, |s| s.redo()))
}
/// `oakundo_undostack_jump` (clamped to 0; `index` is i64).
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_jump(
stack: OakUndoStack,
index: i64,
) -> c_int {
guard(|| with_stack(stack, |s| {
s.jump(index);
Ok(())
}))
}
/// `oakundo_undostack_clear`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_clear(stack: OakUndoStack) -> c_int {
guard(|| with_stack(stack, |s| {
s.clear();
Ok(())
}))
}
/// `oakundo_undostack_can_undo`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_can_undo(
stack: OakUndoStack,
out_value: *mut c_int,
) -> c_int {
guard(|| unsafe {
if out_value.is_null() {
return Err(Error::Invalid);
}
with_stack(stack, |s| {
*out_value = if s.can_undo() { 1 } else { 0 };
Ok(())
})
})
}
/// `oakundo_undostack_can_redo`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_can_redo(
stack: OakUndoStack,
out_value: *mut c_int,
) -> c_int {
guard(|| unsafe {
if out_value.is_null() {
return Err(Error::Invalid);
}
with_stack(stack, |s| {
*out_value = if s.can_redo() { 1 } else { 0 };
Ok(())
})
})
}
/// `oakundo_undostack_count`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_count(
stack: OakUndoStack,
out_count: *mut i64,
) -> c_int {
guard(|| unsafe {
if out_count.is_null() {
return Err(Error::Invalid);
}
with_stack(stack, |s| {
*out_count = s.command_count();
Ok(())
})
})
}
/// `oakundo_undostack_index`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_index(
stack: OakUndoStack,
out_index: *mut i64,
) -> c_int {
guard(|| unsafe {
if out_index.is_null() {
return Err(Error::Invalid);
}
with_stack(stack, |s| {
*out_index = s.done_count();
Ok(())
})
})
}
/// `oakundo_undostack_command_text` (two-stage string getter).
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_command_text(
stack: OakUndoStack,
row: i64,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
let result = catch_unwind(AssertUnwindSafe(|| -> Result<i32> {
with_stack(stack, |s| {
if row < 0 || row >= s.command_count() {
return Err(Error::NotFound);
}
let name = s.command_name(row)?;
let required = (name.len() + 1) as i32;
if !buf.is_null() && buf_size > 0 {
let copy_len = name.len().min((buf_size as usize).saturating_sub(1));
let bytes = name.as_bytes();
// SAFETY: `buf` points to `buf_size` writable bytes and
// we write at most `copy_len` (+ one NUL) of them.
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
*buf.add(copy_len) = 0;
}
}
Ok(required)
})
}));
match result {
Ok(Ok(required)) => required,
Ok(Err(e)) => e.code(),
Err(_) => crate::error::OAKUNDO_E_FAILED,
}
}
/// `oakundo_undostack_command_is_done`.
#[no_mangle]
pub unsafe extern "C" fn oakundo_undostack_command_is_done(
stack: OakUndoStack,
row: i64,
out_value: *mut c_int,
) -> c_int {
guard(|| unsafe {
if out_value.is_null() {
return Err(Error::Invalid);
}
with_stack(stack, |s| {
*out_value = if s.command_is_done(row)? { 1 } else { 0 };
Ok(())
})
})
}
}
+160
View File
@@ -0,0 +1,160 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Refcounted-handle scaffolding. Same pattern as the oaknode/oakcodec
//! crates (`src/node/rust/src/handle.rs`); intentionally duplicated rather
//! than shared — each module DLL must run its own addref/release code
//! (the function pointers in a handle always point into the DLL that
//! created the object).
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicU32, Ordering};
/// ABI version stamped into every handle.
pub const OAKUNDO_ABI_VERSION: u32 = 1;
/// Heap box behind a handle's `ctx`.
pub struct RefBox<T: ?Sized> {
/// Atomic reference count.
pub refs: AtomicU32,
/// Boxed value.
pub value: T,
}
/// The shared ABI value-handle type (single-lib unification, see
/// `docs/zh/plans/riir/single-lib.md`): one canonical
/// `{ctx, addref, release, abi_version}` type in `oakcore-rs`, re-exported
/// here so the crate's `ffi.rs` signatures and handle scaffolding stay
/// source-compatible.
pub use oakcore_rs::handle::CHandle;
/// Owned handle with count 1; empty on allocation failure.
pub fn make_owned<T: Send + 'static>(value: T) -> CHandle {
let boxed = RefBox {
refs: AtomicU32::new(1),
value,
};
let ctx = Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void;
CHandle {
ctx,
addref: Some(addref_owned::<T>),
release: Some(release_owned::<T>),
abi_version: OAKUNDO_ABI_VERSION,
}
}
/// Borrowed handle for an object owned elsewhere (release frees only
/// the box).
///
/// # Safety
/// Caller guarantees `ptr` outlives every derived handle.
pub unsafe fn make_borrowed<T: Send + 'static>(ptr: *mut T) -> CHandle {
let boxed = RefBox {
refs: AtomicU32::new(1),
value: ptr,
};
let ctx = Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void;
CHandle {
ctx,
addref: Some(addref_borrowed::<T>),
release: Some(release_borrowed::<T>),
abi_version: OAKUNDO_ABI_VERSION,
}
}
/// Typed view into a handle; `None` for empty handles.
///
/// # Safety
/// `T` must be the boxed type.
pub unsafe fn get<T: 'static>(h: &CHandle) -> Option<&T> {
unsafe {
if h.ctx.is_null() {
None
} else {
Some(&(*((h.ctx as *mut RefBox<T>))).value)
}
}
}
/// Panic-catching FFI wrapper for i32-returning exports.
pub fn guard<F: FnOnce() -> crate::error::Result<()>>(f: F) -> i32 {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(())) => crate::error::OAKUNDO_OK,
Ok(Err(e)) => e.code(),
Err(_) => crate::error::OAKUNDO_E_FAILED,
}
}
/// Panic-catching FFI wrapper for handle-returning exports.
pub fn guard_handle<F: FnOnce() -> crate::error::Result<CHandle>>(f: F) -> CHandle {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(h)) => h,
_ => CHandle::null(),
}
}
/// Panic-catching FFI wrapper for void exports.
pub fn guard_void<F: FnOnce()>(f: F) {
let _ = catch_unwind(AssertUnwindSafe(f));
}
/// Owned addref: bump the box's refcount.
unsafe extern "C" fn addref_owned<T: Send + 'static>(ctx: *mut std::ffi::c_void) {
unsafe {
if ctx.is_null() {
return;
}
let boxed = ctx as *mut RefBox<T>;
(&(*boxed).refs).fetch_add(1, Ordering::AcqRel);
}
}
/// Owned release: drop the box when the refcount hits zero.
unsafe extern "C" fn release_owned<T: Send + 'static>(ctx: *mut std::ffi::c_void) {
unsafe {
if ctx.is_null() {
return;
}
let boxed = ctx as *mut RefBox<T>;
if (&(*boxed).refs).fetch_sub(1, Ordering::AcqRel) == 1 {
drop(Box::from_raw(boxed));
}
}
}
/// Borrowed addref: bump the shell's refcount.
unsafe extern "C" fn addref_borrowed<T: Send + 'static>(ctx: *mut std::ffi::c_void) {
unsafe {
if ctx.is_null() {
return;
}
let boxed = ctx as *mut RefBox<*mut T>;
(&(*boxed).refs).fetch_add(1, Ordering::AcqRel);
}
}
/// Borrowed release: free only the shell, never the pointee.
unsafe extern "C" fn release_borrowed<T: Send + 'static>(ctx: *mut std::ffi::c_void) {
unsafe {
if ctx.is_null() {
return;
}
let boxed = ctx as *mut RefBox<*mut T>;
if (&(*boxed).refs).fetch_sub(1, Ordering::AcqRel) == 1 {
drop(Box::from_raw(boxed));
}
}
}
+37
View File
@@ -0,0 +1,37 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! # oakundo — the undo/redo history module (Rust)
//!
//! Reimplements the C++ oakundo module behind its frozen C ABI
//! (`include/undo/*.h`). See README.md for the architectural mapping —
//! most notably the **vtable-command pattern**: C++ subclassing of
//! `olive::UndoCommand` becomes a callback table + `userdata` pointer.
//!
//! ## FFI discipline
//!
//! Identical to the oaknode/oakcodec crates: every export goes through
//! [`handle::guard*`], handles are opaque refcounted boxes, shared
//! state behind `Mutex`.
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
pub mod error;
pub mod ffi;
pub mod handle;
pub mod undocommand;
pub mod undostack;
+472
View File
@@ -0,0 +1,472 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! `olive::UndoCommand` / `olive::MultiUndoCommand` — the undoable
//! operation and its composite. Mirrors `src/undo/src/undocommand.h`
//! and `include/undo/undocommand.h`.
//!
//! The C++ base is subclassed by other modules (`redo()`/`undo()`
//! overrides). Rust has no inheritance, so the C ABI models the same
//! polymorphism with a callback table + `userdata` pointer
//! ([`OakUndoCommandVtable`]); this module's [`UndoCommand`] holds a
//! [`CommandKind`] that is either that vtable-backed command or a
//! [`MultiUndoCommand`] composite. This vtable-command pattern is the
//! centerpiece of the oakundo architecture (see README.md).
//!
//! ## Command boxes
//!
//! Commands cross the FFI boundary through a dedicated [`CommandBox`]
//! (not the generic [`crate::handle::RefBox`]): it holds the raw
//! `*mut UndoCommand`, an `owns` flag (mirroring the C++
//! `OakUndoCommandBox`) and a refcount. An **owning** box owns and
//! destroys its command at refcount zero; a **borrowed** box (a
//! reference into a `MultiUndoCommand` or an `UndoStack`) owns only its
//! shell. `take_command` moves the command value out of an owning box,
//! turning it into a non-owning shell (the C++ `mark_container_owned`).
use std::ffi::c_void;
use std::sync::atomic::{AtomicU32, Ordering};
use crate::error::{Error, Result};
use crate::handle::{CHandle, OAKUNDO_ABI_VERSION};
/// `oakundo_command_vtable` — the caller-defined redo/undo/free
/// callback table. Any entry may be `None`; a `None` redo/undo makes
/// that direction a no-op, `None` free_fn skips userdata release.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct OakUndoCommandVtable {
/// Execute the operation.
pub redo: Option<unsafe extern "C" fn(*mut c_void)>,
/// Reverse the operation.
pub undo: Option<unsafe extern "C" fn(*mut c_void)>,
/// Release `userdata` when the command is destroyed.
pub free_fn: Option<unsafe extern "C" fn(*mut c_void)>,
}
/// What backs an [`UndoCommand`]: a caller-defined vtable command or a
/// composite of children. The direct analog of C++ virtual dispatch.
pub enum CommandKind {
/// Caller-defined command: callback table plus opaque userdata.
Vtable {
/// The copied callback table.
vtable: OakUndoCommandVtable,
/// Opaque caller state; owned by the command.
userdata: *mut c_void,
},
/// Composite command (`olive::MultiUndoCommand`).
Multi(MultiUndoCommand),
}
/// `olive::UndoCommand` — an undoable operation.
///
/// State mirrors the C++ base: `done_` (already executed) and
/// `modified_` (project-dirty flag snapshot). The C++ modified-state
/// *callbacks* are not part of the C ABI and are intentionally omitted
/// (see README.md decision 2); only the flag snapshot is retained.
///
/// `prepared` mirrors the C++ `prepared_` flag (the vtable has no
/// `prepare()` callback, so it starts `true` and never un-prepares);
/// `is_empty` marks the invariant bottom-of-stack command so the stack
/// can keep it out of `can_undo`.
pub struct UndoCommand {
/// The operation (vtable or composite).
kind: CommandKind,
/// Whether the command has been executed (`done_`).
done: bool,
/// Project-dirty flag snapshot recorded at the last redo.
///
/// Reserved for the future modified-state callbacks; with no C ABI
/// surface it is retained but never read.
#[allow(dead_code)]
modified: bool,
/// Whether `prepare()` has run (`prepared_`).
prepared: bool,
/// True for the stack's invariant bottom command.
is_empty: bool,
}
/// A command's userdata is caller-controlled; like the C++ object it
/// may be moved across threads when the owning stack is shared behind a
/// `Mutex`. Callbacks must be thread-safe with respect to the caller's
/// own locking, exactly as in the C++ implementation.
unsafe impl Send for UndoCommand {}
impl UndoCommand {
/// New vtable-backed command; takes ownership of `userdata`.
pub fn from_vtable(vtable: OakUndoCommandVtable, userdata: *mut c_void) -> Self {
UndoCommand {
kind: CommandKind::Vtable { vtable, userdata },
done: false,
modified: false,
prepared: true,
is_empty: false,
}
}
/// New empty composite command (`olive::MultiUndoCommand`).
pub fn multi() -> Self {
UndoCommand {
kind: CommandKind::Multi(MultiUndoCommand::new()),
done: false,
modified: false,
prepared: true,
is_empty: false,
}
}
/// The invariant bottom-of-stack command ("New/Open Project"); all
/// callbacks are no-ops and it is never undoable.
pub(crate) fn empty() -> Self {
UndoCommand {
kind: CommandKind::Vtable {
vtable: OakUndoCommandVtable {
redo: None,
undo: None,
free_fn: None,
},
userdata: std::ptr::null_mut(),
},
done: false,
modified: false,
prepared: true,
is_empty: true,
}
}
/// Add `child` to a composite command (takes one reference).
pub fn multi_add_child(&mut self, child: UndoCommand) {
match &mut self.kind {
CommandKind::Multi(m) => m.add_child(child),
CommandKind::Vtable { .. } => {
panic!("multi_add_child on a non-multi command")
}
}
}
/// Number of children of a composite command.
pub fn multi_child_count(&self) -> usize {
match &self.kind {
CommandKind::Multi(m) => m.child_count(),
CommandKind::Vtable { .. } => 0,
}
}
/// Reference to the child at `index` of a composite command.
pub fn multi_child(&self, index: usize) -> Result<&UndoCommand> {
match &self.kind {
CommandKind::Multi(m) => m.child(index),
CommandKind::Vtable { .. } => Err(Error::Invalid),
}
}
/// `redo_now` semantics: a no-op if already done.
pub fn redo_now(&mut self) {
if !self.done {
if !self.prepared {
self.set_prepared();
}
self.redo();
self.done = true;
}
}
/// `undo_now` semantics: a no-op if not done.
pub fn undo_now(&mut self) {
if self.done {
self.undo();
self.done = false;
}
}
/// `redo_and_set_modified`: redo, then snapshot-and-force the dirty
/// flag. Without callbacks this is just `redo_now`.
pub fn redo_and_set_modified(&mut self) {
self.redo_now();
}
/// `undo_and_set_modified`: undo, then restore the dirty flag.
pub fn undo_and_set_modified(&mut self) {
self.undo_now();
}
/// `has_prepared`: whether `prepare()` has run (vtable commands have
/// no prepare; always true).
pub fn has_prepared(&self) -> bool {
self.prepared
}
/// `set_prepared`.
pub fn set_prepared(&mut self) {
self.prepared = true;
}
/// `set_done`: mark as already executed without running redo.
pub fn set_done(&mut self, done: bool) {
self.done = done;
}
/// Whether the command is currently done (executed, not undone).
pub fn is_done(&self) -> bool {
self.done
}
/// Whether this is the invariant bottom-of-stack command.
pub(crate) fn is_empty(&self) -> bool {
self.is_empty
}
/// Whether this is a composite (`MultiUndoCommand`) command.
pub(crate) fn is_multi(&self) -> bool {
matches!(self.kind, CommandKind::Multi(_))
}
/// Run the operation (virtual dispatch).
fn redo(&mut self) {
match &mut self.kind {
CommandKind::Vtable { vtable, userdata } => {
if let Some(f) = vtable.redo {
// SAFETY: `userdata` is caller-supplied and outlives
// the command (owned by it). The callback is the one
// registered by the caller.
unsafe { f(*userdata) }
}
}
CommandKind::Multi(m) => m.redo(),
}
}
/// Reverse the operation (virtual dispatch).
fn undo(&mut self) {
match &mut self.kind {
CommandKind::Vtable { vtable, userdata } => {
if let Some(f) = vtable.undo {
// SAFETY: as in `redo`.
unsafe { f(*userdata) }
}
}
CommandKind::Multi(m) => m.undo(),
}
}
}
impl Drop for UndoCommand {
/// Invokes the vtable `free_fn` on `userdata` (composites free
/// children transitively).
fn drop(&mut self) {
if let CommandKind::Vtable { vtable, userdata } = &self.kind {
if let Some(f) = vtable.free_fn {
// SAFETY: `userdata` is owned by the command; this is the
// final release.
unsafe { f(*userdata) }
}
}
}
}
/// `olive::MultiUndoCommand` — a composite of child commands.
///
/// `redo` runs children in order; `undo` runs them in reverse. Owns one
/// reference to each child.
pub struct MultiUndoCommand {
/// Child commands in insertion order.
children: Vec<UndoCommand>,
}
/// See `UndoCommand::Send`.
unsafe impl Send for MultiUndoCommand {}
impl MultiUndoCommand {
/// New empty composite.
pub fn new() -> Self {
MultiUndoCommand {
children: Vec::new(),
}
}
/// Add a child (takes one reference).
pub fn add_child(&mut self, child: UndoCommand) {
self.children.push(child);
}
/// Child count.
pub fn child_count(&self) -> usize {
self.children.len()
}
/// Child at `index`.
pub fn child(&self, index: usize) -> Result<&UndoCommand> {
self.children.get(index).ok_or(Error::NotFound)
}
/// Redo all children in order.
pub fn redo(&mut self) {
for child in self.children.iter_mut() {
child.redo_and_set_modified();
}
}
/// Undo all children in reverse order.
pub fn undo(&mut self) {
for child in self.children.iter_mut().rev() {
child.undo_and_set_modified();
}
}
}
impl Default for MultiUndoCommand {
fn default() -> Self {
Self::new()
}
}
/// Heap box behind a command handle's `ctx`. Mirrors the C++
/// `OakUndoCommandBox` (`{command, owns, refs}`).
pub(crate) struct CommandBox {
/// Raw pointer to the command value.
command: *mut UndoCommand,
/// Whether this box owns (and must destroy) `command`.
owns: bool,
/// Atomic reference count.
refs: AtomicU32,
}
/// Owned handle for a fresh command value (refcount 1, owns `cmd`).
///
/// # Safety
/// The returned handle owns `cmd`; release it with `command_release`.
pub(crate) unsafe fn command_from_owned(cmd: UndoCommand) -> CHandle {
let boxed = Box::into_raw(Box::new(CommandBox {
command: Box::into_raw(Box::new(cmd)),
owns: true,
refs: AtomicU32::new(1),
})) as *mut c_void;
CHandle {
ctx: boxed,
addref: Some(command_addref),
release: Some(command_release),
abi_version: OAKUNDO_ABI_VERSION,
}
}
/// Borrowed handle for a command owned elsewhere (release frees only
/// the shell).
///
/// # Safety
/// `cmd` must outlive the returned handle.
pub(crate) unsafe fn command_from_borrowed(cmd: *mut UndoCommand) -> CHandle {
let boxed = Box::into_raw(Box::new(CommandBox {
command: cmd,
owns: false,
refs: AtomicU32::new(1),
})) as *mut c_void;
CHandle {
ctx: boxed,
addref: Some(command_addref),
release: Some(command_release),
abi_version: OAKUNDO_ABI_VERSION,
}
}
/// Read-only view of the command behind `ctx`; `None` for a null handle.
///
/// # Safety
/// `ctx` must come from a valid, live `CommandBox`.
pub(crate) unsafe fn command_to_ref(ctx: *mut c_void) -> Option<&'static UndoCommand> {
unsafe {
if ctx.is_null() {
return None;
}
let boxed = ctx as *mut CommandBox;
if (*boxed).command.is_null() {
return None;
}
Some(&*(*boxed).command)
}
}
/// Mutable view of the command behind `ctx`; `None` for a null handle.
///
/// # Safety
/// `ctx` must come from a valid, live `CommandBox`.
pub(crate) unsafe fn command_to_mut(ctx: *mut c_void) -> Option<&'static mut UndoCommand> {
unsafe {
if ctx.is_null() {
return None;
}
let boxed = ctx as *mut CommandBox;
if (*boxed).command.is_null() {
return None;
}
Some(&mut *(*boxed).command)
}
}
/// Move the command value out of an owning box, turning it into a
/// non-owning shell (the C++ `mark_container_owned` transfer).
///
/// # Safety
/// `ctx` must come from a valid, live `CommandBox`.
pub(crate) unsafe fn command_take(ctx: *mut c_void) -> Result<UndoCommand> {
unsafe {
if ctx.is_null() {
return Err(Error::Invalid);
}
let boxed = ctx as *mut CommandBox;
if !(*boxed).owns || (*boxed).command.is_null() {
return Err(Error::State);
}
let value = Box::from_raw((*boxed).command);
(*boxed).command = std::ptr::null_mut();
(*boxed).owns = false;
Ok(*value)
}
}
/// Increment a command box's refcount (the `addref` function pointer).
///
/// # Safety
/// `ctx` must come from a valid `CommandBox`.
pub(crate) unsafe extern "C" fn command_addref(ctx: *mut c_void) {
unsafe {
if ctx.is_null() {
return;
}
let boxed = ctx as *mut CommandBox;
(&(*boxed).refs).fetch_add(1, Ordering::AcqRel);
}
}
/// Decrement a command box's refcount; destroys at zero. Owned boxes
/// free their command first, then the shell; borrowed boxes free only
/// the shell.
///
/// # Safety
/// `ctx` must come from a valid `CommandBox`.
pub(crate) unsafe extern "C" fn command_release(ctx: *mut c_void) {
unsafe {
if ctx.is_null() {
return;
}
let boxed = ctx as *mut CommandBox;
if (&(*boxed).refs).fetch_sub(1, Ordering::AcqRel) == 1 {
if (*boxed).owns && !(*boxed).command.is_null() {
drop(Box::from_raw((*boxed).command));
}
drop(Box::from_raw(boxed));
}
}
}
+220
View File
@@ -0,0 +1,220 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! `olive::UndoStack` — the undo/redo history stack. Mirrors
//! `src/undo/src/undostack.h` and `include/undo/undostack.h`.
//!
//! Two deques: `commands` (done commands, oldest at the front) and
//! `undone` (undone commands, most-recently-undone at the front). The
//! fresh stack holds a single "New/Open Project" empty command so that
//! `can_undo` is false at the bottom (see `undostack.cpp::clear`).
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::undocommand::UndoCommand;
/// Maximum number of retained history rows (`k_max_undo_commands`).
pub const K_MAX_UNDO_COMMANDS: usize = 200;
/// The invariant bottom-of-stack command ("New/Open Project"); not
/// undoable, so `can_undo` returns false at index 0.
pub struct EmptyCommand;
impl EmptyCommand {
/// A no-op command (redo/undo do nothing).
pub fn new() -> UndoCommand {
UndoCommand::empty()
}
}
impl Default for EmptyCommand {
fn default() -> Self {
EmptyCommand
}
}
/// One history row: the command plus its user-visible label.
pub struct CommandEntry {
/// The command.
pub command: UndoCommand,
/// User-visible label.
pub name: String,
}
/// `olive::UndoStack`.
pub struct UndoStack {
/// Done commands, oldest at the front.
commands: VecDeque<CommandEntry>,
/// Undone commands, most-recently-undone at the front.
undone: VecDeque<CommandEntry>,
}
/// See `UndoCommand::Send`.
unsafe impl Send for UndoStack {}
impl UndoStack {
/// New stack; contains a single empty "New/Open Project" command.
pub fn new() -> Self {
let mut stack = UndoStack {
commands: VecDeque::new(),
undone: VecDeque::new(),
};
stack.clear();
stack
}
/// Push `command` and execute its redo; drops any redoable tail and
/// evicts the oldest row beyond the cap.
pub fn push(&mut self, command: UndoCommand, name: &str) {
if command.is_multi() && command.multi_child_count() == 0 {
return;
}
if self.can_redo() {
self.undone.clear();
}
let mut command = command;
command.redo_and_set_modified();
self.commands.push_back(CommandEntry {
command,
name: name.to_string(),
});
if self.commands.len() > K_MAX_UNDO_COMMANDS {
self.commands.pop_front();
}
}
/// Push an already-executed command (redo skipped); `set_done(true)`.
pub fn push_pre_executed(&mut self, command: UndoCommand, name: &str) {
if command.is_multi() && command.multi_child_count() == 0 {
return;
}
if self.can_redo() {
self.undone.clear();
}
let mut command = command;
command.set_done(true);
self.commands.push_back(CommandEntry {
command,
name: name.to_string(),
});
if self.commands.len() > K_MAX_UNDO_COMMANDS {
self.commands.pop_front();
}
}
/// Undo/redo until the done-command count equals `index`.
pub fn jump(&mut self, index: i64) {
let index = index.max(0);
while (self.commands.len() as i64) > index && self.can_undo() {
let _ = self.undo();
}
while (self.commands.len() as i64) < index && self.can_redo() {
let _ = self.redo();
}
}
/// Delete all commands and push a fresh empty command.
pub fn clear(&mut self) {
self.commands.clear();
self.undone.clear();
self.commands.push_back(CommandEntry {
command: EmptyCommand::new(),
name: "New/Open Project".to_string(),
});
}
/// Whether an undo is possible (more than the bottom empty command).
pub fn can_undo(&self) -> bool {
!self.commands.is_empty()
&& !self
.commands
.back()
.map_or(true, |entry| entry.command.is_empty())
}
/// Whether a redo is possible.
pub fn can_redo(&self) -> bool {
!self.undone.is_empty()
}
/// Undo the most recently done command, if any.
pub fn undo(&mut self) -> Result<()> {
if self.can_undo() {
if let Some(entry) = self.commands.pop_back() {
let mut command = entry.command;
command.undo_and_set_modified();
self.undone.push_front(CommandEntry {
command,
name: entry.name,
});
}
}
Ok(())
}
/// Redo the most recently undone command, if any.
pub fn redo(&mut self) -> Result<()> {
if self.can_redo() {
if let Some(entry) = self.undone.pop_front() {
let mut command = entry.command;
command.redo_and_set_modified();
self.commands.push_back(CommandEntry {
command,
name: entry.name,
});
}
}
Ok(())
}
/// Total number of history rows (done + undone commands).
pub fn command_count(&self) -> i64 {
(self.commands.len() + self.undone.len()) as i64
}
/// Number of done commands (the current position).
pub fn done_count(&self) -> i64 {
self.commands.len() as i64
}
/// Whether the row at `row` is currently done.
pub fn command_is_done(&self, row: i64) -> Result<bool> {
if row < 0 || row >= self.command_count() {
return Err(Error::NotFound);
}
Ok(row < self.commands.len() as i64)
}
/// Label of the history row at `row`.
pub fn command_name(&self, row: i64) -> Result<&str> {
if row < 0 || row >= self.command_count() {
return Err(Error::NotFound);
}
let row = row as usize;
if row < self.commands.len() {
Ok(self.commands[row].name.as_str())
} else {
Ok(self.undone[row - self.commands.len()].name.as_str())
}
}
}
impl Default for UndoStack {
fn default() -> Self {
Self::new()
}
}