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.
33///
34/// `as_any_mut` is the escape hatch for in-stroke coalescing — see
35/// [`UndoBuffer::try_coalesce_last`]. Implementations downcast the
36/// top-of-stack command back to their concrete type and merge a fresh
37/// same-stroke action into the existing one (replacing its `after`
38/// snapshot) instead of pushing a new command. Required so multi-event
39/// strokes — slider drag, node drag, typing into a number field — land
40/// as a single undo step.
41///
42/// Every implementor must provide `as_any_mut` — typically the one-liner
43/// `{ self }`. Commands that don't want coalescing leave the method
44/// alone; their `try_coalesce_last` predicate just returns `false` and
45/// `add_and_do` runs the usual path.
46pub trait UndoRedoCommand: 'static {
47 /// Short human-readable description, e.g. `"insert text"`.
48 fn name(&self) -> &str;
49 /// Re-apply the operation (called on Redo).
50 fn do_it(&mut self);
51 /// Reverse the operation (called on Undo).
52 fn undo_it(&mut self);
53 /// Downcast hook for in-stroke coalescing. Implementors forward
54 /// `self`; the predicate passed to [`UndoBuffer::try_coalesce_last`]
55 /// runs `cmd.as_any_mut().downcast_mut::<ConcreteType>()` to inspect
56 /// the top of the stack.
57 fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
58}
59
60// ---------------------------------------------------------------------------
61// UndoBuffer
62// ---------------------------------------------------------------------------
63
64/// Two-stack undo/redo history buffer.
65///
66/// Mirrors the C# `UndoBuffer` class: when a new action is added the redo
67/// stack is cleared (so a new branch cannot be redone). The undo stack is
68/// size-limited; the oldest entries are dropped when the limit is exceeded.
69pub struct UndoBuffer {
70 undo_stack: Vec<Box<dyn UndoRedoCommand>>,
71 redo_stack: Vec<Box<dyn UndoRedoCommand>>,
72 max_undos: usize,
73}
74
75impl UndoBuffer {
76 /// Create a new buffer with a default history limit of 200 entries.
77 pub fn new() -> Self {
78 Self {
79 undo_stack: Vec::new(),
80 redo_stack: Vec::new(),
81 max_undos: 200,
82 }
83 }
84
85 /// Set the maximum number of undo steps retained.
86 pub fn with_max_undos(mut self, n: usize) -> Self {
87 self.max_undos = n;
88 self
89 }
90
91 /// Push `cmd` without executing it.
92 ///
93 /// Use this when the action has **already** been applied to the state;
94 /// the command only needs to know how to undo (and redo) it.
95 /// Clears the redo stack.
96 pub fn add(&mut self, cmd: Box<dyn UndoRedoCommand>) {
97 self.redo_stack.clear();
98 self.undo_stack.push(cmd);
99 if self.undo_stack.len() > self.max_undos {
100 self.undo_stack.remove(0);
101 }
102 }
103
104 /// Execute `cmd.do_it()` and push it onto the undo stack.
105 ///
106 /// Use this when the action has **not** yet been applied.
107 pub fn add_and_do(&mut self, mut cmd: Box<dyn UndoRedoCommand>) {
108 cmd.do_it();
109 self.add(cmd);
110 }
111
112 /// Undo the most recent operation. No-op if the stack is empty.
113 pub fn undo(&mut self) {
114 if let Some(mut cmd) = self.undo_stack.pop() {
115 cmd.undo_it();
116 self.redo_stack.push(cmd);
117 }
118 }
119
120 /// Redo the most recently undone operation. No-op if the redo stack is empty.
121 pub fn redo(&mut self) {
122 if let Some(mut cmd) = self.redo_stack.pop() {
123 cmd.do_it();
124 self.undo_stack.push(cmd);
125 }
126 }
127
128 /// Returns `true` if there is at least one operation that can be undone.
129 pub fn can_undo(&self) -> bool {
130 !self.undo_stack.is_empty()
131 }
132
133 /// Returns `true` if there is at least one operation that can be redone.
134 pub fn can_redo(&self) -> bool {
135 !self.redo_stack.is_empty()
136 }
137
138 /// Name of the operation that `undo()` would reverse, if any.
139 pub fn undo_name(&self) -> Option<&str> {
140 self.undo_stack.last().map(|c| c.name())
141 }
142
143 /// Name of the operation that `redo()` would re-apply, if any.
144 pub fn redo_name(&self) -> Option<&str> {
145 self.redo_stack.last().map(|c| c.name())
146 }
147
148 /// Discard all undo and redo history.
149 pub fn clear_history(&mut self) {
150 self.undo_stack.clear();
151 self.redo_stack.clear();
152 }
153
154 /// In-stroke coalescing. Pass a closure that inspects the top of
155 /// the undo stack and decides whether the action that just
156 /// occurred is part of the same logical stroke:
157 ///
158 /// * `f` downcasts the top command via `cmd.as_any_mut()` to its
159 /// concrete type and, if the keys match (same node + same
160 /// property, same node drag, etc.), updates the command's
161 /// `after` snapshot to reflect the latest value and applies the
162 /// change to the document — returning `true`.
163 /// * If the top command is a different kind or targets different
164 /// state, the closure returns `false` and the caller falls back
165 /// to `add_and_do` to push a fresh command.
166 ///
167 /// Implementations of `do_it` are NOT re-run when coalescing
168 /// succeeds — the closure is responsible for any document-side
169 /// mutation. The redo stack is cleared on a successful merge,
170 /// matching the semantics of `add` / `add_and_do`.
171 ///
172 /// Returns `true` when coalescing succeeded.
173 pub fn try_coalesce_last<F>(&mut self, mut f: F) -> bool
174 where
175 F: FnMut(&mut dyn UndoRedoCommand) -> bool,
176 {
177 if let Some(top) = self.undo_stack.last_mut() {
178 if f(top.as_mut()) {
179 self.redo_stack.clear();
180 return true;
181 }
182 }
183 false
184 }
185}
186
187impl Default for UndoBuffer {
188 fn default() -> Self {
189 Self::new()
190 }
191}
192
193// ---------------------------------------------------------------------------
194// DoUndoActions — closure-based command
195// ---------------------------------------------------------------------------
196
197/// A command backed by two closures: one for `do_it` and one for `undo_it`.
198///
199/// This is the Rust equivalent of the C# `DoUndoActions` class. Use it for
200/// simple operations where capturing state in closures is natural.
201///
202/// For operations that share state with an owning object (e.g. text editing),
203/// consider using `std::rc::Rc<std::cell::RefCell<T>>` to share mutable state
204/// between the owning widget and the undo command closures.
205pub struct DoUndoActions {
206 name: String,
207 do_fn: Box<dyn FnMut()>,
208 undo_fn: Box<dyn FnMut()>,
209}
210
211impl DoUndoActions {
212 /// Create a command with the given `name`, `do_action`, and `undo_action`.
213 pub fn new(
214 name: impl Into<String>,
215 do_action: impl FnMut() + 'static,
216 undo_action: impl FnMut() + 'static,
217 ) -> Self {
218 Self {
219 name: name.into(),
220 do_fn: Box::new(do_action),
221 undo_fn: Box::new(undo_action),
222 }
223 }
224}
225
226impl UndoRedoCommand for DoUndoActions {
227 fn name(&self) -> &str {
228 &self.name
229 }
230 fn do_it(&mut self) {
231 (self.do_fn)()
232 }
233 fn undo_it(&mut self) {
234 (self.undo_fn)()
235 }
236 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
237 self
238 }
239}