Skip to main content

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}