Skip to main content

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}