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