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:
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user