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