common/undo_redo.rs
1use crate::event::{Event, EventHub, Origin, UndoRedoEvent};
2use crate::types::EntityId;
3use anyhow::{Result, anyhow};
4use std::any::Any;
5use std::collections::HashMap;
6use std::sync::Arc;
7
8/// Trait for commands that can be undone and redone.
9///
10/// Implementors can optionally support command merging by overriding the
11/// `can_merge` and `merge` methods. This allows the UndoRedoManager to combine
12/// multiple commands of the same type into a single command, which is useful for
13/// operations like continuous typing or dragging.
14pub trait UndoRedoCommand: Send {
15 /// Undoes the command, reverting its effects
16 fn undo(&mut self) -> Result<()>;
17
18 /// Redoes the command, reapplying its effects
19 fn redo(&mut self) -> Result<()>;
20
21 /// Returns true if this command can be merged with the other command.
22 ///
23 /// By default, commands cannot be merged. Override this method to enable
24 /// merging for specific command types.
25 ///
26 /// # Example
27 /// ```test
28 /// fn can_merge(&self, other: &dyn UndoRedoCommand) -> bool {
29 /// // Check if the other command is of the same type
30 /// if let Some(_) = other.as_any().downcast_ref::<Self>() {
31 /// return true;
32 /// }
33 /// false
34 /// }
35 /// ```
36 fn can_merge(&self, _other: &dyn UndoRedoCommand) -> bool {
37 false
38 }
39
40 /// Merges this command with the other command.
41 /// Returns true if the merge was successful.
42 ///
43 /// This method is called only if `can_merge` returns true.
44 ///
45 /// # Example
46 /// ```test
47 /// use common::undo_redo::UndoRedoCommand;
48 ///
49 /// fn merge(&mut self, other: &dyn UndoRedoCommand) -> bool {
50 /// if let Some(other_cmd) = other.as_any().downcast_ref::<Self>() {
51 /// // Merge the commands
52 /// self.value += other_cmd.value;
53 /// return true;
54 /// }
55 /// false
56 /// }
57 /// ```
58 fn merge(&mut self, _other: &dyn UndoRedoCommand) -> bool {
59 false
60 }
61
62 /// Returns the type ID of this command for type checking.
63 ///
64 /// This is used for downcasting in the `can_merge` and `merge` methods.
65 ///
66 /// # Example
67 /// ```test
68 /// fn as_any(&self) -> &dyn Any {
69 /// self
70 /// }
71 /// ```
72 fn as_any(&self) -> &dyn Any;
73}
74
75/// A composite command that groups multiple commands as one.
76///
77/// This allows treating a sequence of commands as a single unit for undo/redo operations.
78/// When a composite command is undone or redone, all its contained commands are undone
79/// or redone in the appropriate order.
80///
81/// # Example
82/// ```test
83/// use common::undo_redo::CompositeCommand;
84/// let mut composite = CompositeCommand::new();
85/// composite.add_command(Box::new(Command1::new()));
86/// composite.add_command(Box::new(Command2::new()));
87/// // Now composite can be treated as a single command
88/// ```
89pub struct CompositeCommand {
90 commands: Vec<Box<dyn UndoRedoCommand>>,
91 pub stack_id: u64,
92}
93
94impl CompositeCommand {
95 /// Creates a new empty composite command.
96 pub fn new(stack_id: Option<u64>) -> Self {
97 CompositeCommand {
98 commands: Vec::new(),
99 stack_id: stack_id.unwrap_or(0),
100 }
101 }
102
103 /// Adds a command to this composite.
104 ///
105 /// Commands are executed, undone, and redone in the order they are added.
106 pub fn add_command(&mut self, command: Box<dyn UndoRedoCommand>) {
107 self.commands.push(command);
108 }
109
110 /// Returns true if this composite contains no commands.
111 pub fn is_empty(&self) -> bool {
112 self.commands.is_empty()
113 }
114}
115
116impl UndoRedoCommand for CompositeCommand {
117 fn undo(&mut self) -> Result<()> {
118 // Undo commands in reverse order
119 for command in self.commands.iter_mut().rev() {
120 command.undo()?;
121 }
122 Ok(())
123 }
124
125 fn redo(&mut self) -> Result<()> {
126 // Redo commands in original order
127 for command in self.commands.iter_mut() {
128 command.redo()?;
129 }
130 Ok(())
131 }
132
133 fn as_any(&self) -> &dyn Any {
134 self
135 }
136}
137/// Trait for commands that can be executed asynchronously with progress tracking and cancellation.
138///
139/// This trait extends the basic UndoRedoCommand trait with asynchronous capabilities.
140/// Implementors must also implement the UndoRedoCommand trait to ensure compatibility
141/// with the existing undo/redo system.
142pub trait AsyncUndoRedoCommand: UndoRedoCommand {
143 /// Starts the undo operation asynchronously and returns immediately.
144 /// Returns Ok(()) if the operation was successfully started.
145 fn start_undo(&mut self) -> Result<()>;
146
147 /// Starts the redo operation asynchronously and returns immediately.
148 /// Returns Ok(()) if the operation was successfully started.
149 fn start_redo(&mut self) -> Result<()>;
150
151 /// Checks the progress of the current operation.
152 /// Returns a value between 0.0 (not started) and 1.0 (completed).
153 fn check_progress(&self) -> f32;
154
155 /// Attempts to cancel the in-progress operation.
156 /// Returns Ok(()) if cancellation was successful or if no operation is in progress.
157 fn cancel(&mut self) -> Result<()>;
158
159 /// Checks if the current operation is complete.
160 /// Returns true if the operation has finished successfully.
161 fn is_complete(&self) -> bool;
162}
163
164#[derive(Default)]
165struct StackData {
166 undo_stack: Vec<Box<dyn UndoRedoCommand>>,
167 redo_stack: Vec<Box<dyn UndoRedoCommand>>,
168}
169
170/// Manager for undo and redo operations.
171///
172/// The UndoRedoManager maintains multiple stacks of commands:
173/// - Each stack has an undo stack for commands that can be undone
174/// - Each stack has a redo stack for commands that have been undone and can be redone
175///
176/// It also supports:
177/// - Grouping multiple commands as a single unit using begin_composite/end_composite
178/// - Merging commands of the same type when appropriate
179/// - Switching between different stacks
180pub struct UndoRedoManager {
181 stacks: HashMap<u64, StackData>,
182 next_stack_id: u64,
183 in_progress_composite: Option<CompositeCommand>,
184 composite_nesting_level: usize,
185 composite_stack_id: Option<u64>,
186 event_hub: Option<Arc<EventHub>>,
187}
188
189impl Default for UndoRedoManager {
190 fn default() -> Self {
191 Self::new()
192 }
193}
194
195impl UndoRedoManager {
196 /// Creates a new empty UndoRedoManager with one default stack (ID 0).
197 pub fn new() -> Self {
198 let mut stacks = HashMap::new();
199 stacks.insert(0, StackData::default());
200 UndoRedoManager {
201 stacks,
202 next_stack_id: 1,
203 in_progress_composite: None,
204 composite_nesting_level: 0,
205 composite_stack_id: None,
206 event_hub: None,
207 }
208 }
209
210 /// Inject the event hub to allow sending undo/redo related events
211 pub fn set_event_hub(&mut self, event_hub: &Arc<EventHub>) {
212 self.event_hub = Some(Arc::clone(event_hub));
213 }
214
215 /// Undoes the most recent command on the specified stack.
216 /// If `stack_id` is None, the global stack (ID 0) is used.
217 ///
218 /// The undone command is moved to the redo stack.
219 /// Returns Ok(()) if successful or if there are no commands to undo.
220 pub fn undo(&mut self, stack_id: Option<u64>) -> Result<()> {
221 let target_stack_id = stack_id.unwrap_or(0);
222 let stack = self
223 .stacks
224 .get_mut(&target_stack_id)
225 .ok_or_else(|| anyhow!("Stack with ID {} not found", target_stack_id))?;
226
227 if let Some(mut command) = stack.undo_stack.pop() {
228 if let Err(e) = command.undo() {
229 log::error!("Undo failed, re-pushing command to undo stack: {e}");
230 stack.undo_stack.push(command);
231 return Err(e);
232 }
233 stack.redo_stack.push(command);
234 if let Some(event_hub) = &self.event_hub {
235 event_hub.send_event(Event {
236 origin: Origin::UndoRedo(UndoRedoEvent::Undone),
237 ids: Vec::<EntityId>::new(),
238 data: None,
239 });
240 }
241 }
242 Ok(())
243 }
244
245 /// Redoes the most recently undone command on the specified stack.
246 /// If `stack_id` is None, the global stack (ID 0) is used.
247 ///
248 /// The redone command is moved back to the undo stack.
249 /// Returns Ok(()) if successful or if there are no commands to redo.
250 pub fn redo(&mut self, stack_id: Option<u64>) -> Result<()> {
251 let target_stack_id = stack_id.unwrap_or(0);
252 let stack = self
253 .stacks
254 .get_mut(&target_stack_id)
255 .ok_or_else(|| anyhow!("Stack with ID {} not found", target_stack_id))?;
256
257 if let Some(mut command) = stack.redo_stack.pop() {
258 if let Err(e) = command.redo() {
259 log::error!("Redo failed, re-pushing command to redo stack: {e}");
260 stack.redo_stack.push(command);
261 return Err(e);
262 }
263 stack.undo_stack.push(command);
264 if let Some(event_hub) = &self.event_hub {
265 event_hub.send_event(Event {
266 origin: Origin::UndoRedo(UndoRedoEvent::Redone),
267 ids: Vec::<EntityId>::new(),
268 data: None,
269 });
270 }
271 }
272 Ok(())
273 }
274
275 /// Begins a composite command group.
276 ///
277 /// All commands added between begin_composite and end_composite will be treated as a single command.
278 /// This is useful for operations that logically represent a single action but require multiple
279 /// commands to implement.
280 ///
281 /// # Example
282 /// ```test
283 /// let mut manager = UndoRedoManager::new();
284 /// manager.begin_composite();
285 /// manager.add_command(Box::new(Command1::new()));
286 /// manager.add_command(Box::new(Command2::new()));
287 /// manager.end_composite();
288 /// // Now undo() will undo both commands as a single unit
289 /// ```
290 pub fn begin_composite(&mut self, stack_id: Option<u64>) -> Result<()> {
291 if self.composite_stack_id.is_some() && self.composite_stack_id != stack_id {
292 return Err(anyhow!(
293 "Cannot begin a composite on a different stack while another composite is in progress"
294 ));
295 }
296
297 // Set the target stack ID for this composite
298 self.composite_stack_id = stack_id;
299
300 // Increment the nesting level
301 self.composite_nesting_level += 1;
302
303 // If there's no composite in progress, create one
304 if self.in_progress_composite.is_none() {
305 self.in_progress_composite = Some(CompositeCommand::new(stack_id));
306 }
307
308 // not sure if we want to send events for composites
309 if let Some(event_hub) = &self.event_hub {
310 event_hub.send_event(Event {
311 origin: Origin::UndoRedo(UndoRedoEvent::BeginComposite),
312 ids: Vec::<EntityId>::new(),
313 data: None,
314 });
315 }
316 Ok(())
317 }
318
319 /// Ends the current composite command group and adds it to the specified undo stack.
320 ///
321 /// If no commands were added to the composite, nothing is added to the undo stack.
322 /// If this is a nested composite, only the outermost composite is added to the undo stack.
323 pub fn end_composite(&mut self) {
324 // Decrement the nesting level
325 if self.composite_nesting_level > 0 {
326 self.composite_nesting_level -= 1;
327 }
328
329 // Only end the composite if we're at the outermost level
330 if self.composite_nesting_level == 0 {
331 if let Some(composite) = self.in_progress_composite.take()
332 && !composite.is_empty()
333 {
334 let target_stack_id = self.composite_stack_id.unwrap_or(0);
335 let stack = self
336 .stacks
337 .get_mut(&target_stack_id)
338 .expect("Stack must exist");
339 stack.undo_stack.push(Box::new(composite));
340 stack.redo_stack.clear();
341 }
342 // not sure if we want to send events for composites
343 if let Some(event_hub) = &self.event_hub {
344 event_hub.send_event(Event {
345 origin: Origin::UndoRedo(UndoRedoEvent::EndComposite),
346 ids: Vec::<EntityId>::new(),
347 data: None,
348 });
349 }
350 }
351 }
352
353 pub fn cancel_composite(&mut self) {
354 // Decrement the nesting level
355 if self.composite_nesting_level > 0 {
356 self.composite_nesting_level -= 1;
357 }
358
359 // Undo any sub-commands that were already executed in this composite
360 if let Some(ref mut composite) = self.in_progress_composite {
361 let _ = composite.undo();
362 }
363
364 self.in_progress_composite = None;
365 self.composite_stack_id = None;
366
367 // not sure if we want to send events for composites
368 if let Some(event_hub) = &self.event_hub {
369 event_hub.send_event(Event {
370 origin: Origin::UndoRedo(UndoRedoEvent::CancelComposite),
371 ids: Vec::<EntityId>::new(),
372 data: None,
373 });
374 }
375 }
376
377 /// Adds a command to the global undo stack (ID 0).
378 pub fn add_command(&mut self, command: Box<dyn UndoRedoCommand>) {
379 let _ = self.add_command_to_stack(command, None);
380 }
381
382 /// Adds a command to the specified undo stack.
383 /// If `stack_id` is None, the global stack (ID 0) is used.
384 ///
385 /// This method handles several cases:
386 /// 1. If a composite command is in progress, the command is added to the composite
387 /// 2. If the command can be merged with the last command on the specified undo stack, they are merged
388 /// 3. Otherwise, the command is added to the specified undo stack as a new entry
389 ///
390 /// In all cases, the redo stack of the stack is cleared when a new command is added.
391 pub fn add_command_to_stack(
392 &mut self,
393 command: Box<dyn UndoRedoCommand>,
394 stack_id: Option<u64>,
395 ) -> Result<()> {
396 // If we have a composite in progress, add the command to it
397 if let Some(composite) = &mut self.in_progress_composite {
398 // ensure that the stack_id is the same as the composite's stack
399 if composite.stack_id != stack_id.unwrap_or(0) {
400 return Err(anyhow!(
401 "Cannot add command to composite with different stack ID"
402 ));
403 }
404 composite.add_command(command);
405 return Ok(());
406 }
407
408 let target_stack_id = stack_id.unwrap_or(0);
409 let stack = self
410 .stacks
411 .get_mut(&target_stack_id)
412 .ok_or_else(|| anyhow!("Stack with ID {} does not exist", target_stack_id))?;
413
414 // Try to merge with the last command if possible
415 if let Some(last_command) = stack.undo_stack.last_mut()
416 && last_command.can_merge(&*command)
417 && last_command.merge(&*command)
418 {
419 // Successfully merged, no need to add the new command
420 stack.redo_stack.clear();
421 return Ok(());
422 }
423
424 // If we couldn't merge, just add the command normally
425 stack.undo_stack.push(command);
426 stack.redo_stack.clear();
427 Ok(())
428 }
429
430 /// Returns true if there are commands that can be undone on the specified stack.
431 /// If `stack_id` is None, the global stack (ID 0) is used.
432 pub fn can_undo(&self, stack_id: Option<u64>) -> bool {
433 let target_stack_id = stack_id.unwrap_or(0);
434 self.stacks
435 .get(&target_stack_id)
436 .map(|s| !s.undo_stack.is_empty())
437 .unwrap_or(false)
438 }
439
440 /// Returns true if there are commands that can be redone on the specified stack.
441 /// If `stack_id` is None, the global stack (ID 0) is used.
442 pub fn can_redo(&self, stack_id: Option<u64>) -> bool {
443 let target_stack_id = stack_id.unwrap_or(0);
444 self.stacks
445 .get(&target_stack_id)
446 .map(|s| !s.redo_stack.is_empty())
447 .unwrap_or(false)
448 }
449
450 /// Clears the undo and redo history for a specific stack.
451 ///
452 /// This method removes all commands from both the undo and redo stacks of the specified stack.
453 pub fn clear_stack(&mut self, stack_id: u64) {
454 if let Some(stack) = self.stacks.get_mut(&stack_id) {
455 stack.undo_stack.clear();
456 stack.redo_stack.clear();
457 }
458 }
459
460 /// Clears all undo and redo history from all stacks.
461 pub fn clear_all_stacks(&mut self) {
462 for stack in self.stacks.values_mut() {
463 stack.undo_stack.clear();
464 stack.redo_stack.clear();
465 }
466 self.in_progress_composite = None;
467 self.composite_nesting_level = 0;
468 }
469
470 /// Creates a new undo/redo stack and returns its ID.
471 pub fn create_new_stack(&mut self) -> u64 {
472 let id = self.next_stack_id;
473 self.stacks.insert(id, StackData::default());
474 self.next_stack_id += 1;
475 id
476 }
477
478 /// Deletes an undo/redo stack by its ID.
479 ///
480 /// The default stack (ID 0) cannot be deleted.
481 pub fn delete_stack(&mut self, stack_id: u64) -> Result<()> {
482 if stack_id == 0 {
483 return Err(anyhow!("Cannot delete the default stack"));
484 }
485 if self.stacks.remove(&stack_id).is_some() {
486 Ok(())
487 } else {
488 Err(anyhow!("Stack with ID {} does not exist", stack_id))
489 }
490 }
491
492 /// Gets the size of the undo stack for a specific stack.
493 pub fn get_stack_size(&self, stack_id: u64) -> usize {
494 self.stacks
495 .get(&stack_id)
496 .map(|s| s.undo_stack.len())
497 .unwrap_or(0)
498 }
499}