Files
oak-editor/crates/oak-undo/src/undocommand.rs
T
Mike-Solar 244d5e860f
CI / Build & test (Windows) (push) Failing after 7s
workspace: kebab-case crates, app under crates/oak-app, shared versions
All crates take the oak-* kebab-case naming (oak-audio, oak-codec,
oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin,
oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the
lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: ->
oak_core::, ...) across all 226 referencing files.

The GUI application moves from the workspace root into
crates/oak-app/: src/, build.rs (paths fixed for the new location) and
tests/ travel with it, the root Cargo.toml becomes workspace-only
([workspace] + workspace.package + profiles), and the app package
inherits the workspace version. The screenshots example becomes a
standalone crate examples/simple_player/ with its own Cargo.toml.

Every crate now inherits the single workspace version
(version.workspace = true), and the workflows' crate paths and the
build docs follow the renames.

Validated with a clean cargo check --workspace.
2026-08-22 16:58:37 +08:00

317 lines
8.9 KiB
Rust

// 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`.
//!
//! The C++ base is subclassed by other modules (`redo()`/`undo()`
//! 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};
/// 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);
/// `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.
///
/// 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 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 (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.
///
/// 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,
}
impl UndoCommand {
/// 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 {
inner: Box::new(command),
done: false,
modified: false,
prepared: true,
is_empty: false,
}
}
/// 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 {
UndoCommand::new(ClosureCommand {
redo: Box::new(redo),
undo: Box::new(undo),
})
}
/// 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 {
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 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 (0 for plain commands).
pub fn multi_child_count(&self) -> usize {
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> {
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 self.inner.as_multi_mut() {
Some(m) => m.child_mut(index),
None => 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 (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 {
self.inner.as_multi().is_some()
}
/// Run the operation (virtual dispatch).
fn redo(&mut self) {
self.inner.redo();
}
/// Reverse the operation (virtual dispatch).
fn undo(&mut self) {
self.inner.undo();
}
}
/// Closure-backed command state (see [`UndoCommand::from_closures`]).
struct ClosureCommand {
/// The redo closure.
redo: Box<dyn FnMut() + Send>,
/// The undo closure.
undo: Box<dyn FnMut() + Send>,
}
impl Command for ClosureCommand {
fn redo(&mut self) {
(self.redo)();
}
fn undo(&mut self) {
(self.undo)();
}
}
/// `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>,
}
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)
}
/// Mutable child at `index`.
pub fn child_mut(&mut self, index: usize) -> Result<&mut UndoCommand> {
self.children.get_mut(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()
}
}
impl Command for MultiUndoCommand {
fn redo(&mut self) {
MultiUndoCommand::redo(self);
}
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)
}
}