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 {
61 undo_stack: Vec::new(),
62 redo_stack: Vec::new(),
63 max_undos: 200,
64 }
65 }
66
67 /// Set the maximum number of undo steps retained.
68 pub fn with_max_undos(mut self, n: usize) -> Self {
69 self.max_undos = n;
70 self
71 }
72
73 /// Push `cmd` without executing it.
74 ///
75 /// Use this when the action has **already** been applied to the state;
76 /// the command only needs to know how to undo (and redo) it.
77 /// Clears the redo stack.
78 pub fn add(&mut self, cmd: Box<dyn UndoRedoCommand>) {
79 self.redo_stack.clear();
80 self.undo_stack.push(cmd);
81 if self.undo_stack.len() > self.max_undos {
82 self.undo_stack.remove(0);
83 }
84 }
85
86 /// Execute `cmd.do_it()` and push it onto the undo stack.
87 ///
88 /// Use this when the action has **not** yet been applied.
89 pub fn add_and_do(&mut self, mut cmd: Box<dyn UndoRedoCommand>) {
90 cmd.do_it();
91 self.add(cmd);
92 }
93
94 /// Undo the most recent operation. No-op if the stack is empty.
95 pub fn undo(&mut self) {
96 if let Some(mut cmd) = self.undo_stack.pop() {
97 cmd.undo_it();
98 self.redo_stack.push(cmd);
99 }
100 }
101
102 /// Redo the most recently undone operation. No-op if the redo stack is empty.
103 pub fn redo(&mut self) {
104 if let Some(mut cmd) = self.redo_stack.pop() {
105 cmd.do_it();
106 self.undo_stack.push(cmd);
107 }
108 }
109
110 /// Returns `true` if there is at least one operation that can be undone.
111 pub fn can_undo(&self) -> bool {
112 !self.undo_stack.is_empty()
113 }
114
115 /// Returns `true` if there is at least one operation that can be redone.
116 pub fn can_redo(&self) -> bool {
117 !self.redo_stack.is_empty()
118 }
119
120 /// Name of the operation that `undo()` would reverse, if any.
121 pub fn undo_name(&self) -> Option<&str> {
122 self.undo_stack.last().map(|c| c.name())
123 }
124
125 /// Name of the operation that `redo()` would re-apply, if any.
126 pub fn redo_name(&self) -> Option<&str> {
127 self.redo_stack.last().map(|c| c.name())
128 }
129
130 /// Discard all undo and redo history.
131 pub fn clear_history(&mut self) {
132 self.undo_stack.clear();
133 self.redo_stack.clear();
134 }
135}
136
137impl Default for UndoBuffer {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143// ---------------------------------------------------------------------------
144// DoUndoActions — closure-based command
145// ---------------------------------------------------------------------------
146
147/// A command backed by two closures: one for `do_it` and one for `undo_it`.
148///
149/// This is the Rust equivalent of the C# `DoUndoActions` class. Use it for
150/// simple operations where capturing state in closures is natural.
151///
152/// For operations that share state with an owning object (e.g. text editing),
153/// consider using `std::rc::Rc<std::cell::RefCell<T>>` to share mutable state
154/// between the owning widget and the undo command closures.
155pub struct DoUndoActions {
156 name: String,
157 do_fn: Box<dyn FnMut()>,
158 undo_fn: Box<dyn FnMut()>,
159}
160
161impl DoUndoActions {
162 /// Create a command with the given `name`, `do_action`, and `undo_action`.
163 pub fn new(
164 name: impl Into<String>,
165 do_action: impl FnMut() + 'static,
166 undo_action: impl FnMut() + 'static,
167 ) -> Self {
168 Self {
169 name: name.into(),
170 do_fn: Box::new(do_action),
171 undo_fn: Box::new(undo_action),
172 }
173 }
174}
175
176impl UndoRedoCommand for DoUndoActions {
177 fn name(&self) -> &str {
178 &self.name
179 }
180 fn do_it(&mut self) {
181 (self.do_fn)()
182 }
183 fn undo_it(&mut self) {
184 (self.undo_fn)()
185 }
186}