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
+63 -200
View File
@@ -15,31 +15,27 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! The process-wide undo stack, undo groups and the command-success
//! observer hook (M14 R1: sunk from the engine facade's `undo.rs`).
//! observer hook.
//!
//! The facade used to own the process-wide stack (the module-00 analogue
//! of `EngineCore::undo_stack()`), the open undo group and the
//! write-through notification as facade state; all of it is process state,
//! so this module holds it and the facade forwards. The observer registry
//! The retired engine facade used to own the process-wide stack (the
//! module-00 analogue of `EngineCore::undo_stack()`), the open undo group
//! and the write-through notification as facade state; all of it is
//! process state, so this module holds it directly. The observer registry
//! lets downstream modules (the oakstorage write-through session manager)
//! subscribe to "a command was recorded" notifications without a facade
//! round-trip.
//! subscribe to "a command was recorded" notifications.
//!
//! M14 R5: everything in this module is Rust-typed — the process-wide
//! stack is a plain `static Mutex<UndoStack>` and the open group holds an
//! [`UndoCommand`] value. The only remaining `CHandle` is the
//! [`push_or_run`] facade entry, which converts the incoming module
//! command handle into an [`UndoCommand`] value at the boundary.
//! Everything here is Rust-typed: the process-wide stack is a plain
//! `static Mutex<UndoStack>` and the open group holds an
//! [`UndoCommand`] value. The former `CHandle`-marshalling entry point
//! (`push_or_run`) and the raw-pointer out-parameter queries
//! (`can_undo(out)` / `command_text(buf, size)` / `command_is_done(out)`)
//! were deleted with the C ABI — the same operations are exposed as
//! value-typed functions below.
use std::ffi::{c_char, c_int, c_void};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::{Mutex, OnceLock};
use crate::error::{Error, OAKUNDO_E_FAILED, Result};
use crate::handle::CHandle;
use crate::undocommand::{
command_free, command_from_owned, command_redo_now, command_take, UndoCommand,
};
use crate::error::Result;
use crate::undocommand::UndoCommand;
use crate::undostack::UndoStack;
/// The process-wide undo stack, created lazily on first use and kept for
@@ -49,13 +45,6 @@ fn global_stack() -> &'static Mutex<UndoStack> {
STACK.get_or_init(|| Mutex::new(UndoStack::new()))
}
/// Stable opaque token for the engine's `oakengine_undo_handle` export:
/// the stack's address (never dereferenced by callers; lives for the
/// process).
pub fn stack_token() -> *mut c_void {
global_stack() as *const Mutex<UndoStack> as *mut c_void
}
/// Run `f` on the process-wide stack; a poisoned mutex is recovered (its
/// inner value is still valid).
fn with_stack<R>(f: impl FnOnce(&mut UndoStack) -> Result<R>) -> Result<R> {
@@ -121,7 +110,7 @@ fn group_lock() -> std::sync::MutexGuard<'static, Option<OpenGroup>> {
pub fn group_begin(name: &str) -> Result<()> {
let mut g = group_lock();
if g.is_some() {
return Err(Error::State);
return Err(crate::error::Error::State);
}
*g = Some(OpenGroup {
multi: UndoCommand::multi(),
@@ -135,7 +124,7 @@ pub fn group_begin(name: &str) -> Result<()> {
/// when no group is open.
pub fn group_end() -> Result<()> {
let mut g = group_lock();
let open = g.take().ok_or(Error::State)?;
let open = g.take().ok_or(crate::error::Error::State)?;
let multi = open.multi;
let name = open.name;
drop(g);
@@ -155,7 +144,7 @@ pub fn group_end() -> Result<()> {
/// no group is open. No observers fire (nothing was recorded).
pub fn group_abort() -> Result<()> {
let mut g = group_lock();
let open = g.take().ok_or(Error::State)?;
let open = g.take().ok_or(crate::error::Error::State)?;
let mut multi = open.multi;
drop(g);
// The multi command itself is never marked done (each child was
@@ -170,79 +159,29 @@ pub fn group_abort() -> Result<()> {
Ok(())
}
/// Push `command` onto the stack and execute its redo (or add it to the
/// open group). `command` is consumed: the stack/multi takes the command
/// value, leaving the caller's handle as a non-owning shell that still
/// needs its own release. Returns 0 on success, a module error code
/// otherwise. Command observers fire only when the command was recorded on
/// the STACK — a group child joins the group, and the group itself
/// notifies at [`group_end`].
pub fn push_or_run(command: CHandle, name: &str) -> c_int {
/// Push `command` onto the process-wide stack (redo then record), or into
/// the open group as an already-done child. On a stack record the command
/// observers fire (the oakstorage write-through persists the edit).
pub fn push(command: UndoCommand, name: &str) -> Result<()> {
let mut g = group_lock();
if let Some(group) = g.as_mut() {
// The group takes the command value (command_take), so the eager
// redo must happen on the still-owned handle FIRST — the group
// receives an already-done command (C++ semantics: add_child +
// redo_now, net effect identical for the group's reverse-order
// undo).
let rc = command_redo_now(command);
if rc != 0 {
return rc;
}
// SAFETY: `command` is a live module command handle (facade
// contract); `command_take` moves the value out of its box and
// marks the shell non-owning.
return match unsafe { command_take(command.ctx) } {
Ok(cmd) => {
group.multi.multi_add_child(cmd);
0
}
Err(e) => e.code(),
};
// The group takes the command as an already-done child: the eager
// redo must run BEFORE the child joins the group (C++ semantics:
// add_child + redo_now, net effect identical for the group's
// reverse-order undo).
let mut command = command;
command.redo_now();
group.multi.multi_add_child(command);
return Ok(());
}
// No group open: take the command value and push it onto the
// process-wide stack (redo then record).
// SAFETY: as above, `command` is a live module command handle.
let cmd = match unsafe { command_take(command.ctx) } {
Ok(cmd) => cmd,
Err(e) => return e.code(),
};
drop(g);
let mut guard = global_stack().lock().unwrap_or_else(|e| e.into_inner());
guard.push(cmd, name);
guard.push(command, name);
drop(guard);
// The stack took the command; its redo already ran (plan M13 D2):
// persist the write-through subscribers.
// The stack took the command; its redo already ran: persist the
// write-through subscribers.
notify_observers();
0
}
// ---------------------------------------------------------------------------
// Safe value-typed surface (M14 R3)
//
// The direct-rlib frontends (the app) hold module [`UndoCommand`] values,
// not CHandles. The functions below are the safe twins of the handle-based
// API above; the handle marshalling they perform stays inside this crate.
// ---------------------------------------------------------------------------
/// Push `command` onto the process-wide stack (redo then record), or into
/// the open group. On a stack record the command observers fire (the
/// oakstorage write-through persists the edit).
pub fn push(command: UndoCommand, name: &str) -> Result<()> {
// SAFETY: `command_from_owned` boxes the value; `push_or_run` takes the
// value out of the handle (stack push or group child), and
// `command_free` releases the remaining non-owning shell (dropping the
// command itself when nobody took it).
let mut handle = unsafe { command_from_owned(command) };
if handle.is_null() {
return Err(Error::NoMem);
}
let rc = push_or_run(handle, name);
unsafe{command_free(&mut handle)};
if rc == 0 {
Ok(())
} else {
Err(Error::from_code(rc))
}
Ok(())
}
/// Step the process-wide stack back one entry (no-op at the bottom). On
@@ -261,14 +200,12 @@ pub fn redo() -> Result<()> {
/// Whether the process-wide stack has an entry to undo.
pub fn undoable() -> bool {
let mut v: c_int = 0;
unsafe{can_undo(&mut v).is_ok() && v != 0}
can_undo()
}
/// Whether the process-wide stack has an entry to redo.
pub fn redoable() -> bool {
let mut v: c_int = 0;
unsafe{can_redo(&mut v).is_ok() && v != 0}
can_redo()
}
// ---------------------------------------------------------------------------
@@ -285,30 +222,14 @@ pub fn index() -> Result<i64> {
with_stack(|s| Ok(s.done_count()))
}
/// Whether an undo is possible (1/0 via `out_value`; a module error code
/// otherwise).
pub unsafe fn can_undo(out_value: *mut c_int) -> Result<()> {
if out_value.is_null() {
return Err(Error::Invalid);
}
let value = with_stack(|s| Ok(if s.can_undo() { 1 } else { 0 }))?;
// SAFETY: `out_value` points to at least one writable `c_int`, per the
// facade contract.
unsafe { *out_value = value };
Ok(())
/// Whether an undo is possible.
pub fn can_undo() -> bool {
with_stack(|s| Ok(s.can_undo())).unwrap_or(false)
}
/// Whether a redo is possible (1/0 via `out_value`; a module error code
/// otherwise).
pub unsafe fn can_redo(out_value: *mut c_int) -> Result<()> {
if out_value.is_null() {
return Err(Error::Invalid);
}
let value = with_stack(|s| Ok(if s.can_redo() { 1 } else { 0 }))?;
// SAFETY: `out_value` points to at least one writable `c_int`, per the
// facade contract.
unsafe { *out_value = value };
Ok(())
/// Whether a redo is possible.
pub fn can_redo() -> bool {
with_stack(|s| Ok(s.can_redo())).unwrap_or(false)
}
/// Undo/redo until the done-command count equals `index`. On success the
@@ -331,58 +252,15 @@ pub fn clear() -> Result<()> {
})
}
/// Two-stage label getter for the row at `row` (see
/// [`crate::undostack::undostack_command_text`]): returns the required
/// size including the NUL, or a module error code.
pub unsafe fn command_text(row: i64, buf: *mut c_char, buf_size: c_int) -> c_int {
let result = catch_unwind(AssertUnwindSafe(|| -> Result<i32> {
with_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(_) => OAKUNDO_E_FAILED,
}
}
/// Whether the row at `row` is done (1/0 via `out_value`; a module error
/// code otherwise — `-20004` for an out-of-range row).
pub unsafe fn command_is_done(row: i64, out_value: *mut c_int) -> Result<()> {
if out_value.is_null() {
return Err(Error::Invalid);
}
let done = with_stack(|s| s.command_is_done(row))?;
// SAFETY: `out_value` points to at least one writable `c_int`, per the
// facade contract.
unsafe { *out_value = if done { 1 } else { 0 } };
Ok(())
}
/// The user-visible label of the row at `row` (the safe twin of the
/// two-stage [`command_text`]; the history panel's row query).
/// The user-visible label of the row at `row` (the safe replacement for
/// the C-ABI two-stage `command_text(buf, size)`; the history panel's row
/// query). `NotFound` for an out-of-range row.
pub fn command_name(row: i64) -> Result<String> {
with_stack(|s| s.command_name(row).map(|n| n.to_string()))
}
/// Whether the row at `row` is done (the safe twin of
/// [`command_is_done`]; the history panel's gray-row query).
/// Whether the row at `row` is done (the safe replacement for the C-ABI
/// `command_is_done(out)`; the history panel's gray-row query).
pub fn command_done(row: i64) -> Result<bool> {
with_stack(|s| s.command_is_done(row))
}
@@ -405,21 +283,12 @@ mod tests {
}
/// The stack/group/observer state is process-wide: every test here runs
/// serially under this lock, mirroring the facade's GLOBAL_STACK_LOCK
/// pattern.
/// serially under this lock.
static LOCK: Mutex<()> = Mutex::new(());
fn vtable_command() -> CHandle {
use crate::undocommand::{command_init, OakUndoCommandVtable};
unsafe{
command_init(
&OakUndoCommandVtable {
redo: None,
undo: None,
free_fn: None,
},
std::ptr::null_mut(),
)}
/// A no-op command value.
fn noop_command() -> UndoCommand {
UndoCommand::from_closures(|| {}, || {})
}
#[test]
@@ -431,23 +300,18 @@ mod tests {
assert!(clear().is_ok());
assert_eq!(count().unwrap(), 1);
assert_eq!(index().unwrap(), 1);
let mut v: c_int = 1;
assert!(unsafe{can_undo(&mut v).is_ok()});
assert_eq!(v, 0);
assert!(!can_undo());
// Push fires the observer once.
let cmd = vtable_command();
assert_eq!(push_or_run(cmd, "alpha"), 0);
push(noop_command(), "alpha").unwrap();
assert_eq!(count().unwrap(), 2);
assert_eq!(COUNT.load(std::sync::atomic::Ordering::SeqCst), 1);
// Group begin/end fires the observer once at end.
assert!(group_begin("grouped").is_ok());
assert!(group_begin("again").is_err()); // State
let c1 = vtable_command();
let c2 = vtable_command();
assert_eq!(push_or_run(c1, "c1"), 0);
assert_eq!(push_or_run(c2, "c2"), 0);
push(noop_command(), "c1").unwrap();
push(noop_command(), "c2").unwrap();
// Children joined the group: no observer fire yet.
assert_eq!(COUNT.load(std::sync::atomic::Ordering::SeqCst), 1);
assert_eq!(count().unwrap(), 2);
@@ -457,8 +321,7 @@ mod tests {
// Abort fires nothing.
assert!(group_begin("abort").is_ok());
let c3 = vtable_command();
assert_eq!(push_or_run(c3, "c3"), 0);
push(noop_command(), "c3").unwrap();
assert!(group_abort().is_ok());
assert_eq!(count().unwrap(), 3);
assert_eq!(COUNT.load(std::sync::atomic::Ordering::SeqCst), 2);
@@ -470,9 +333,9 @@ mod tests {
assert!(clear().is_ok());
}
/// The safe value-typed surface (M14 R3): `push` redoes and records a
/// closure command, `undo`/`redo` step the stack, and the observers
/// fire on every recorded mutation.
/// The value-typed surface: `push` redoes and records a closure
/// command, `undo`/`redo` step the stack, and the observers fire on
/// every recorded mutation.
#[test]
fn value_push_and_undo_redo() {
let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
@@ -511,9 +374,9 @@ mod tests {
assert!(clear().is_ok());
}
/// The safe row getters answer like the C-ABI twins: labels survive an
/// undo (undone rows stay labeled) and `command_done` flips with the
/// stack pointer.
/// The row getters answer like the C-ABI twins: labels survive an undo
/// (undone rows stay labeled) and `command_done` flips with the stack
/// pointer.
#[test]
fn value_command_name_and_done() {
let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner());
-160
View File
@@ -1,160 +0,0 @@
// 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));
}
}
}
+16 -10
View File
@@ -16,22 +16,28 @@
//! # 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.
//! Reimplements the C++ oakundo module. See README.md for the
//! architectural mapping — most notably the **trait-object command
//! pattern**: C++ subclassing of `olive::UndoCommand` becomes a boxed
//! [`undocommand::Command`] (closure-backed or a
//! [`undocommand::MultiUndoCommand`] composite) behind a plain owned
//! [`undocommand::UndoCommand`] value.
//!
//! ## FFI discipline
//! ## Consumers
//!
//! Identical to the oaknode/oakcodec crates: every export goes through
//! [`handle::guard*`], handles are opaque refcounted boxes, shared
//! state behind `Mutex`.
//! Every consumer is in-process Rust: the app's edit layer
//! (`oakui::graphops` / `oakui::real`) drives the process-wide stack
//! through [`global`], `oaktimeline`/`oaktask`/`oaknode`/`oakplugin`
//! build commands as [`undocommand::UndoCommand`] values, and
//! `oakstorage` subscribes to the command-success observers in
//! [`global`]. The former frozen C ABI (`include/undo/*.h`), the
//! refcounted [`CHandle`] layer and the callback-table vtable commands
//! were deleted with the engine facade — the crate is pure owned-value
//! Rust with no `unsafe`.
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(missing_docs)]
pub mod error;
pub mod global;
pub mod handle;
pub mod undocommand;
pub mod undostack;
+88 -442
View File
@@ -15,66 +15,42 @@
// 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`.
//! operation and its composite. Mirrors `src/undo/src/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 crate boundaries 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`).
//!
//! The handle-level functions below (`command_init` ... `command_free`)
//! were sunk from the former C ABI export layer: they keep the
//! `CHandle`-based signatures so the facade bridges and integration
//! tests can drive the command machinery directly. The `CHandle` layer
//! itself is removed in a later milestone.
use std::ffi::{c_int, c_void};
use std::sync::atomic::{AtomicU32, Ordering};
//! overrides); the Rust equivalent of that virtual dispatch is a
//! trait-object command. [`UndoCommand`] boxes a [`Command`] (implemented
//! by closure-backed commands and by the [`MultiUndoCommand`] composite)
//! plus the command's state flags. This replaces the former
//! `OakUndoCommandVtable` + `userdata` callback-table model, which only
//! existed to cross the frozen C ABI (see README.md). With every consumer
//! in-process there is no callback table, no `extern "C"` trampoline and
//! no refcounted `CHandle` — commands are owned values.
use crate::error::{Error, Result};
use crate::handle::{guard, guard_handle, guard_void, 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)>,
}
/// A single undoable operation (the C++ virtual `redo()`/`undo()` pair).
///
/// Implementations must be `Send`: the owning [`UndoCommand`] lives in
/// stacks shared behind a `Mutex`, so it may move across threads (exactly
/// as the C++ commands did when the owning stack was shared).
pub trait Command: Send {
/// Apply the change.
fn redo(&mut self);
/// Revert the change.
fn undo(&mut self);
/// 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),
/// `Some` for composite commands; used by [`UndoCommand::multi_*`].
/// Defaults to `None` (a plain command is never a composite).
fn as_multi(&self) -> Option<&MultiUndoCommand> {
None
}
/// `Some` for composite commands; used by [`UndoCommand::multi_*`].
/// Defaults to `None` (a plain command is never a composite).
fn as_multi_mut(&mut self) -> Option<&mut MultiUndoCommand> {
None
}
}
/// `olive::UndoCommand` — an undoable operation.
@@ -84,13 +60,13 @@ pub enum CommandKind {
/// *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
/// `prepared` mirrors the C++ `prepared_` flag (the command trait 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,
/// The operation (closure-backed or composite).
inner: Box<dyn Command>,
/// Whether the command has been executed (`done_`).
done: bool,
/// Project-dirty flag snapshot recorded at the last redo.
@@ -105,17 +81,13 @@ pub struct UndoCommand {
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 {
/// Wrap any [`Command`] as an undoable command value. This is the
/// construction path for whole-struct commands (the app's timeline
/// and task commands); one-off edits use [`UndoCommand::from_closures`].
pub fn new(command: impl Command + 'static) -> Self {
UndoCommand {
kind: CommandKind::Vtable { vtable, userdata },
inner: Box::new(command),
done: false,
modified: false,
prepared: true,
@@ -123,89 +95,57 @@ impl UndoCommand {
}
}
/// 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,
}
}
/// New closure-backed command (M14 R3): `redo`/`undo` are plain Rust
/// closures, boxed behind the vtable machinery. This is the safe way
/// for direct-rlib consumers (the app) to build composite edit
/// commands without writing their own `extern "C"` trampolines.
/// New closure-backed command: `redo` runs on redo, `undo` on undo.
/// The safe way for direct-rlib consumers (the app) to build edit
/// commands without writing any callback machinery.
pub fn from_closures(
redo: impl FnMut() + Send + 'static,
undo: impl FnMut() + Send + 'static,
) -> Self {
let state = Box::into_raw(Box::new(ClosureCommand {
UndoCommand::new(ClosureCommand {
redo: Box::new(redo),
undo: Box::new(undo),
}));
UndoCommand::from_vtable(
OakUndoCommandVtable {
redo: Some(closure_redo),
undo: Some(closure_undo),
free_fn: Some(closure_free),
},
state as *mut c_void,
)
})
}
/// The invariant bottom-of-stack command ("New/Open Project"); all
/// callbacks are no-ops and it is never undoable.
/// New empty composite command (`olive::MultiUndoCommand`).
pub fn multi() -> Self {
UndoCommand::new(MultiUndoCommand::new())
}
/// The invariant bottom-of-stack command ("New/Open Project"); a
/// no-op in both directions and 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,
}
let mut command = UndoCommand::from_closures(|| {}, || {});
command.is_empty = true;
command
}
/// 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")
}
match self.inner.as_multi_mut() {
Some(m) => m.add_child(child),
None => panic!("multi_add_child on a non-multi command"),
}
}
/// Number of children of a composite command.
/// Number of children of a composite command (0 for plain commands).
pub fn multi_child_count(&self) -> usize {
match &self.kind {
CommandKind::Multi(m) => m.child_count(),
CommandKind::Vtable { .. } => 0,
}
self.inner.as_multi().map_or(0, |m| m.child_count())
}
/// 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),
}
self.inner
.as_multi()
.map_or(Err(Error::Invalid), |m| m.child(index))
}
/// Mutable reference to the child at `index` of a composite command.
pub fn multi_child_mut(&mut self, index: usize) -> Result<&mut UndoCommand> {
match &mut self.kind {
CommandKind::Multi(m) => m.child_mut(index),
CommandKind::Vtable { .. } => Err(Error::Invalid),
match self.inner.as_multi_mut() {
Some(m) => m.child_mut(index),
None => Err(Error::Invalid),
}
}
@@ -239,8 +179,8 @@ impl UndoCommand {
self.undo_now();
}
/// `has_prepared`: whether `prepare()` has run (vtable commands have
/// no prepare; always true).
/// `has_prepared`: whether `prepare()` has run (commands have no
/// prepare; always true).
pub fn has_prepared(&self) -> bool {
self.prepared
}
@@ -267,49 +207,17 @@ impl UndoCommand {
/// Whether this is a composite (`MultiUndoCommand`) command.
pub(crate) fn is_multi(&self) -> bool {
matches!(self.kind, CommandKind::Multi(_))
self.inner.as_multi().is_some()
}
/// 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(),
}
self.inner.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) }
}
}
self.inner.undo();
}
}
@@ -321,43 +229,14 @@ struct ClosureCommand {
undo: Box<dyn FnMut() + Send>,
}
/// `redo` trampoline for closure commands.
///
/// # Safety
/// `userdata` must be the `Box<ClosureCommand>` produced by
/// [`UndoCommand::from_closures`]; the owning command keeps it alive.
unsafe extern "C" fn closure_redo(userdata: *mut c_void) {
if userdata.is_null() {
return;
impl Command for ClosureCommand {
fn redo(&mut self) {
(self.redo)();
}
// SAFETY: per the from_closures contract; the command owns the box.
let state = unsafe { &mut *(userdata as *mut ClosureCommand) };
(state.redo)();
}
/// `undo` trampoline for closure commands.
///
/// # Safety
/// As [`closure_redo`].
unsafe extern "C" fn closure_undo(userdata: *mut c_void) {
if userdata.is_null() {
return;
fn undo(&mut self) {
(self.undo)();
}
// SAFETY: per the from_closures contract; the command owns the box.
let state = unsafe { &mut *(userdata as *mut ClosureCommand) };
(state.undo)();
}
/// `free` trampoline for closure commands: drops the boxed closures.
///
/// # Safety
/// As [`closure_redo`]; called at most once (the command's final release).
unsafe extern "C" fn closure_free(userdata: *mut c_void) {
if userdata.is_null() {
return;
}
// SAFETY: per the from_closures contract; this is the final release.
unsafe { drop(Box::from_raw(userdata as *mut ClosureCommand)) };
}
/// `olive::MultiUndoCommand` — a composite of child commands.
@@ -369,9 +248,6 @@ pub struct MultiUndoCommand {
children: Vec<UndoCommand>,
}
/// See `UndoCommand::Send`.
unsafe impl Send for MultiUndoCommand {}
impl MultiUndoCommand {
/// New empty composite.
pub fn new() -> Self {
@@ -421,250 +297,20 @@ impl Default for MultiUndoCommand {
}
}
/// 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,
}
impl Command for MultiUndoCommand {
fn redo(&mut self) {
MultiUndoCommand::redo(self);
}
/// Owned handle for a fresh command value (refcount 1, owns `cmd`).
///
/// # Safety
/// The returned handle owns `cmd`; release it with `command_release`.
pub 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,
fn undo(&mut self) {
MultiUndoCommand::undo(self);
}
fn as_multi(&self) -> Option<&MultiUndoCommand> {
Some(self)
}
fn as_multi_mut(&mut self) -> Option<&mut MultiUndoCommand> {
Some(self)
}
}
/// Borrowed handle for a command owned elsewhere (release frees only
/// the shell).
///
/// # Safety
/// `cmd` must outlive the returned handle.
pub 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 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 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 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));
}
}
}
// ---------------------------------------------------------------------------
// Handle-level command API (sunk from the former C ABI export layer)
// ---------------------------------------------------------------------------
/// Create a vtable-backed command handle (refcount 1); a `NULL` vtable
/// yields an empty handle (`oakundo_command_init`).
pub unsafe fn command_init(vtable: *const OakUndoCommandVtable, userdata: *mut c_void) -> CHandle {
guard_handle(|| unsafe {
if vtable.is_null() {
return Ok(CHandle::null());
}
let table = *vtable;
Ok(command_from_owned(UndoCommand::from_vtable(table, userdata)))
})
}
/// Create an empty multi command handle (refcount 1)
/// (`oakundo_command_init_multi`).
pub fn command_init_multi() -> CHandle {
guard_handle(|| unsafe { Ok(command_from_owned(UndoCommand::multi())) })
}
/// Add `child` to a multi command handle (the stack/multi takes one
/// child reference); `E_INVALID` for an empty handle or a non-multi
/// parent (`oakundo_command_multi_add_child`).
pub fn command_multi_add_child(multi: CHandle, child: CHandle) -> 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(())
})
}
/// Number of children of a multi command handle; `E_INVALID` for an
/// empty handle or a non-multi parent (`oakundo_command_multi_child_count`).
pub unsafe fn command_multi_child_count(multi: CHandle, 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(())
})
}
/// Write a borrowed handle to the child at `index` of a multi command
/// (the returned handle carries its own shell ref); `E_NOT_FOUND` for
/// an out-of-range index (`oakundo_command_multi_child`).
pub unsafe fn command_multi_child(multi: CHandle, index: c_int, out_child: *mut CHandle) -> 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(())
})
}
/// Execute a command handle's redo (no-op when already done)
/// (`oakundo_command_redo_now`).
pub fn command_redo_now(command: CHandle) -> c_int {
guard(|| unsafe {
let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?;
c.redo_now();
Ok(())
})
}
/// Execute a command handle's undo (no-op when not done)
/// (`oakundo_command_undo_now`).
pub fn command_undo_now(command: CHandle) -> c_int {
guard(|| unsafe {
let c = command_to_mut(command.ctx).ok_or(Error::Invalid)?;
c.undo_now();
Ok(())
})
}
/// Release a command handle in place: run the release callback, then
/// clear `ctx`. `NULL` / empty handles are no-ops
/// (`oakundo_command_free`).
pub unsafe fn command_free(command: *mut CHandle) {
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();
})
}
+9 -213
View File
@@ -15,21 +15,21 @@
// 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`.
//! `src/undo/src/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`).
//!
//! The former handle-level exports (`undostack_init` … `undostack_free`)
//! are gone with the C ABI: the stack is a plain owned value, shared
//! process-wide through the [`crate::global`] module.
use std::collections::VecDeque;
use std::ffi::{c_char, c_int, CStr};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Mutex;
use crate::error::{Error, OAKUNDO_E_FAILED, Result};
use crate::handle::{get, guard, guard_handle, guard_void, make_owned, CHandle};
use crate::undocommand::{command_take, UndoCommand};
use crate::error::Result;
use crate::undocommand::UndoCommand;
/// Maximum number of retained history rows (`k_max_undo_commands`).
pub const K_MAX_UNDO_COMMANDS: usize = 200;
@@ -67,9 +67,6 @@ pub struct UndoStack {
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 {
@@ -198,7 +195,7 @@ impl UndoStack {
/// 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);
return Err(crate::error::Error::NotFound);
}
Ok(row < self.commands.len() as i64)
}
@@ -206,7 +203,7 @@ impl UndoStack {
/// 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);
return Err(crate::error::Error::NotFound);
}
let row = row as usize;
if row < self.commands.len() {
@@ -222,204 +219,3 @@ impl Default for UndoStack {
Self::new()
}
}
// ---------------------------------------------------------------------------
// Handle-level stack API (sunk from the former C ABI export layer)
// ---------------------------------------------------------------------------
/// 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()
}
}
/// 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).
pub fn with_stack<R>(stack: &CHandle, f: impl FnOnce(&mut UndoStack) -> Result<R>) -> Result<R> {
// SAFETY: the stack handle always boxes a `Mutex<UndoStack>` (created
// by `undostack_init`).
let m = unsafe { get::<Mutex<UndoStack>>(stack) }.ok_or(Error::Invalid)?;
let mut guard = m.lock().unwrap_or_else(|e| e.into_inner());
f(&mut guard)
}
/// Create a fresh stack handle (refcount 1) (`oakundo_undostack_init`).
pub fn undostack_init() -> CHandle {
guard_handle(|| Ok(make_owned(Mutex::new(UndoStack::new()))))
}
/// Release a stack handle in place; `NULL` / empty handles are no-ops
/// (`oakundo_undostack_free`).
pub fn undostack_free(stack: *mut CHandle) {
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();
})
}
/// Push a command handle onto the stack (redo then record; the stack
/// takes ownership of the command value, leaving a non-owning shell)
/// (`oakundo_undostack_push`).
pub fn undostack_push(stack: CHandle, command: CHandle, 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(())
})
})
}
/// Push an already-executed command handle (redo skipped)
/// (`oakundo_undostack_push_pre_executed`).
pub fn undostack_push_pre_executed(stack: CHandle, command: CHandle, 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(())
})
})
}
/// Undo one command on the stack (`oakundo_undostack_undo`).
pub fn undostack_undo(stack: CHandle) -> c_int {
guard(|| with_stack(&stack, |s| s.undo()))
}
/// Redo one command on the stack (`oakundo_undostack_redo`).
pub fn undostack_redo(stack: CHandle) -> c_int {
guard(|| with_stack(&stack, |s| s.redo()))
}
/// Jump to a done-command index (clamped to 0; `index` is i64)
/// (`oakundo_undostack_jump`).
pub fn undostack_jump(stack: CHandle, index: i64) -> c_int {
guard(|| {
with_stack(&stack, |s| {
s.jump(index);
Ok(())
})
})
}
/// Clear the stack back to the empty bottom command
/// (`oakundo_undostack_clear`).
pub fn undostack_clear(stack: CHandle) -> c_int {
guard(|| {
with_stack(&stack, |s| {
s.clear();
Ok(())
})
})
}
/// Write whether an undo is possible (`oakundo_undostack_can_undo`).
pub fn undostack_can_undo(stack: CHandle, 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(())
})
})
}
/// Write whether a redo is possible (`oakundo_undostack_can_redo`).
pub fn undostack_can_redo(stack: CHandle, 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(())
})
})
}
/// Write the total history row count (`oakundo_undostack_count`).
pub fn undostack_count(stack: CHandle, 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(())
})
})
}
/// Write the done-command count (`oakundo_undostack_index`).
pub fn undostack_index(stack: CHandle, 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(())
})
})
}
/// Two-stage label getter for the row at `row`: returns the required
/// size (including NUL), or an error code; copies (truncating) when a
/// buffer is supplied (`oakundo_undostack_command_text`).
pub fn undostack_command_text(stack: CHandle, 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(_) => OAKUNDO_E_FAILED,
}
}
/// Write whether the row at `row` is done (`oakundo_undostack_command_is_done`).
pub fn undostack_command_is_done(stack: CHandle, 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(())
})
})
}