Skip to main content

agg_gui/
undo.rs

1//! Shared undo / redo infrastructure.
2//!
3//! Mirrors the C# agg-sharp `IUndoRedoCommand` / `UndoBuffer` pattern so that
4//! any subsystem — text editing, layout, graph editing — can participate in a
5//! common, extensible undo stack.
6//!
7//! # Usage
8//!
9//! ```rust,ignore
10//! use agg_gui::undo::{DoUndoActions, UndoBuffer};
11//!
12//! let mut buf = UndoBuffer::new();
13//!
14//! // Execute an action and make it undoable:
15//! let v = std::rc::Rc::new(std::cell::Cell::new(0i32));
16//! let v2 = v.clone();
17//! buf.add_and_do(Box::new(DoUndoActions::new(
18//!     "set value",
19//!     move || v.set(42),
20//!     move || v2.set(0),
21//! )));
22//! ```
23
24// ---------------------------------------------------------------------------
25// IUndoRedoCommand — the core trait
26// ---------------------------------------------------------------------------
27
28/// A named, reversible operation.
29///
30/// Implement this trait to participate in the shared undo/redo stack.
31/// The `do_it` / `undo_it` methods are called by [`UndoBuffer`] on redo and
32/// undo respectively.
33pub trait UndoRedoCommand {
34    /// Short human-readable description, e.g. `"insert text"`.
35    fn name(&self) -> &str;
36    /// Re-apply the operation (called on Redo).
37    fn do_it(&mut self);
38    /// Reverse the operation (called on Undo).
39    fn undo_it(&mut self);
40}
41
42// ---------------------------------------------------------------------------
43// UndoBuffer
44// ---------------------------------------------------------------------------
45
46/// Two-stack undo/redo history buffer.
47///
48/// Mirrors the C# `UndoBuffer` class: when a new action is added the redo
49/// stack is cleared (so a new branch cannot be redone).  The undo stack is
50/// size-limited; the oldest entries are dropped when the limit is exceeded.
51pub struct UndoBuffer {
52    undo_stack: Vec<Box<dyn UndoRedoCommand>>,
53    redo_stack: Vec<Box<dyn UndoRedoCommand>>,
54    max_undos:  usize,
55}
56
57impl UndoBuffer {
58    /// Create a new buffer with a default history limit of 200 entries.
59    pub fn new() -> Self {
60        Self { undo_stack: Vec::new(), redo_stack: Vec::new(), max_undos: 200 }
61    }
62
63    /// Set the maximum number of undo steps retained.
64    pub fn with_max_undos(mut self, n: usize) -> Self { self.max_undos = n; self }
65
66    /// Push `cmd` without executing it.
67    ///
68    /// Use this when the action has **already** been applied to the state;
69    /// the command only needs to know how to undo (and redo) it.
70    /// Clears the redo stack.
71    pub fn add(&mut self, cmd: Box<dyn UndoRedoCommand>) {
72        self.redo_stack.clear();
73        self.undo_stack.push(cmd);
74        if self.undo_stack.len() > self.max_undos {
75            self.undo_stack.remove(0);
76        }
77    }
78
79    /// Execute `cmd.do_it()` and push it onto the undo stack.
80    ///
81    /// Use this when the action has **not** yet been applied.
82    pub fn add_and_do(&mut self, mut cmd: Box<dyn UndoRedoCommand>) {
83        cmd.do_it();
84        self.add(cmd);
85    }
86
87    /// Undo the most recent operation.  No-op if the stack is empty.
88    pub fn undo(&mut self) {
89        if let Some(mut cmd) = self.undo_stack.pop() {
90            cmd.undo_it();
91            self.redo_stack.push(cmd);
92        }
93    }
94
95    /// Redo the most recently undone operation.  No-op if the redo stack is empty.
96    pub fn redo(&mut self) {
97        if let Some(mut cmd) = self.redo_stack.pop() {
98            cmd.do_it();
99            self.undo_stack.push(cmd);
100        }
101    }
102
103    /// Returns `true` if there is at least one operation that can be undone.
104    pub fn can_undo(&self) -> bool { !self.undo_stack.is_empty() }
105
106    /// Returns `true` if there is at least one operation that can be redone.
107    pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() }
108
109    /// Name of the operation that `undo()` would reverse, if any.
110    pub fn undo_name(&self) -> Option<&str> { self.undo_stack.last().map(|c| c.name()) }
111
112    /// Name of the operation that `redo()` would re-apply, if any.
113    pub fn redo_name(&self) -> Option<&str> { self.redo_stack.last().map(|c| c.name()) }
114
115    /// Discard all undo and redo history.
116    pub fn clear_history(&mut self) {
117        self.undo_stack.clear();
118        self.redo_stack.clear();
119    }
120}
121
122impl Default for UndoBuffer { fn default() -> Self { Self::new() } }
123
124// ---------------------------------------------------------------------------
125// DoUndoActions — closure-based command
126// ---------------------------------------------------------------------------
127
128/// A command backed by two closures: one for `do_it` and one for `undo_it`.
129///
130/// This is the Rust equivalent of the C# `DoUndoActions` class.  Use it for
131/// simple operations where capturing state in closures is natural.
132///
133/// For operations that share state with an owning object (e.g. text editing),
134/// consider using `std::rc::Rc<std::cell::RefCell<T>>` to share mutable state
135/// between the owning widget and the undo command closures.
136pub struct DoUndoActions {
137    name:    String,
138    do_fn:   Box<dyn FnMut()>,
139    undo_fn: Box<dyn FnMut()>,
140}
141
142impl DoUndoActions {
143    /// Create a command with the given `name`, `do_action`, and `undo_action`.
144    pub fn new(
145        name: impl Into<String>,
146        do_action:   impl FnMut() + 'static,
147        undo_action: impl FnMut() + 'static,
148    ) -> Self {
149        Self {
150            name:    name.into(),
151            do_fn:   Box::new(do_action),
152            undo_fn: Box::new(undo_action),
153        }
154    }
155}
156
157impl UndoRedoCommand for DoUndoActions {
158    fn name(&self)     -> &str { &self.name }
159    fn do_it(&mut self)   { (self.do_fn)() }
160    fn undo_it(&mut self) { (self.undo_fn)() }
161}