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