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.
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}