1#![forbid(unsafe_code)]
2
3use std::any::Any;
43use std::fmt;
44use std::sync::atomic::{AtomicU64, Ordering};
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct UndoWidgetId(u64);
51
52impl UndoWidgetId {
53 pub fn new() -> Self {
55 static COUNTER: AtomicU64 = AtomicU64::new(1);
56 Self(COUNTER.fetch_add(1, Ordering::Relaxed))
57 }
58
59 #[must_use]
63 pub const fn from_raw(id: u64) -> Self {
64 Self(id)
65 }
66
67 #[must_use]
69 pub const fn raw(self) -> u64 {
70 self.0
71 }
72}
73
74impl Default for UndoWidgetId {
75 fn default() -> Self {
76 Self::new()
77 }
78}
79
80impl fmt::Display for UndoWidgetId {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 write!(f, "Widget({})", self.0)
83 }
84}
85
86#[derive(Debug, Clone)]
90pub enum TextEditOperation {
91 Insert {
93 position: usize,
95 text: String,
97 },
98 Delete {
100 position: usize,
102 deleted_text: String,
104 },
105 Replace {
107 position: usize,
109 old_text: String,
111 new_text: String,
113 },
114 SetValue {
116 old_value: String,
118 new_value: String,
120 },
121}
122
123impl TextEditOperation {
124 #[must_use]
126 pub fn description(&self) -> &'static str {
127 match self {
128 Self::Insert { .. } => "Insert text",
129 Self::Delete { .. } => "Delete text",
130 Self::Replace { .. } => "Replace text",
131 Self::SetValue { .. } => "Set value",
132 }
133 }
134
135 #[must_use]
137 pub fn size_bytes(&self) -> usize {
138 std::mem::size_of::<Self>()
139 + match self {
140 Self::Insert { text, .. } => text.len(),
141 Self::Delete { deleted_text, .. } => deleted_text.len(),
142 Self::Replace {
143 old_text, new_text, ..
144 } => old_text.len() + new_text.len(),
145 Self::SetValue {
146 old_value,
147 new_value,
148 } => old_value.len() + new_value.len(),
149 }
150 }
151}
152
153#[derive(Debug, Clone)]
155pub enum SelectionOperation {
156 Changed {
158 old_anchor: Option<usize>,
160 old_cursor: usize,
162 new_anchor: Option<usize>,
164 new_cursor: usize,
166 },
167}
168
169#[derive(Debug, Clone)]
171pub enum TreeOperation {
172 Expand {
174 path: Vec<usize>,
176 },
177 Collapse {
179 path: Vec<usize>,
181 },
182 ToggleBatch {
184 expanded: Vec<Vec<usize>>,
186 collapsed: Vec<Vec<usize>>,
188 },
189}
190
191impl TreeOperation {
192 #[must_use]
194 pub fn description(&self) -> &'static str {
195 match self {
196 Self::Expand { .. } => "Expand node",
197 Self::Collapse { .. } => "Collapse node",
198 Self::ToggleBatch { .. } => "Toggle nodes",
199 }
200 }
201}
202
203#[derive(Debug, Clone)]
205pub enum ListOperation {
206 Select {
208 old_selection: Option<usize>,
210 new_selection: Option<usize>,
212 },
213 MultiSelect {
215 old_selections: Vec<usize>,
217 new_selections: Vec<usize>,
219 },
220}
221
222impl ListOperation {
223 #[must_use]
225 pub fn description(&self) -> &'static str {
226 match self {
227 Self::Select { .. } => "Change selection",
228 Self::MultiSelect { .. } => "Change selections",
229 }
230 }
231}
232
233#[derive(Debug, Clone)]
235pub enum TableOperation {
236 Sort {
238 old_column: Option<usize>,
240 old_ascending: bool,
242 new_column: Option<usize>,
244 new_ascending: bool,
246 },
247 Filter {
249 old_filter: String,
251 new_filter: String,
253 },
254 SelectRow {
256 old_row: Option<usize>,
258 new_row: Option<usize>,
260 },
261}
262
263impl TableOperation {
264 #[must_use]
266 pub fn description(&self) -> &'static str {
267 match self {
268 Self::Sort { .. } => "Change sort",
269 Self::Filter { .. } => "Apply filter",
270 Self::SelectRow { .. } => "Select row",
271 }
272 }
273}
274
275pub type TextEditApplyFn =
277 Box<dyn Fn(UndoWidgetId, &TextEditOperation) -> Result<(), String> + Send + Sync>;
278
279pub type TextEditUndoFn =
281 Box<dyn Fn(UndoWidgetId, &TextEditOperation) -> Result<(), String> + Send + Sync>;
282
283pub struct WidgetTextEditCmd {
285 widget_id: UndoWidgetId,
287 operation: TextEditOperation,
289 apply_fn: Option<TextEditApplyFn>,
291 undo_fn: Option<TextEditUndoFn>,
293 executed: bool,
295}
296
297impl WidgetTextEditCmd {
298 #[must_use]
300 pub fn new(widget_id: UndoWidgetId, operation: TextEditOperation) -> Self {
301 Self {
302 widget_id,
303 operation,
304 apply_fn: None,
305 undo_fn: None,
306 executed: false,
307 }
308 }
309
310 #[must_use]
312 pub fn with_apply<F>(mut self, f: F) -> Self
313 where
314 F: Fn(UndoWidgetId, &TextEditOperation) -> Result<(), String> + Send + Sync + 'static,
315 {
316 self.apply_fn = Some(Box::new(f));
317 self
318 }
319
320 #[must_use]
322 pub fn with_undo<F>(mut self, f: F) -> Self
323 where
324 F: Fn(UndoWidgetId, &TextEditOperation) -> Result<(), String> + Send + Sync + 'static,
325 {
326 self.undo_fn = Some(Box::new(f));
327 self
328 }
329
330 #[must_use]
332 pub fn widget_id(&self) -> UndoWidgetId {
333 self.widget_id
334 }
335
336 #[must_use]
338 pub fn operation(&self) -> &TextEditOperation {
339 &self.operation
340 }
341}
342
343impl fmt::Debug for WidgetTextEditCmd {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 f.debug_struct("WidgetTextEditCmd")
346 .field("widget_id", &self.widget_id)
347 .field("operation", &self.operation)
348 .field("executed", &self.executed)
349 .finish()
350 }
351}
352
353impl WidgetTextEditCmd {
359 #[must_use = "handle the result; errors indicate the edit was not applied"]
361 pub fn execute(&mut self) -> Result<(), String> {
362 if let Some(ref apply_fn) = self.apply_fn {
363 apply_fn(self.widget_id, &self.operation)?;
364 }
365 self.executed = true;
366 Ok(())
367 }
368
369 #[must_use = "handle the result; errors indicate the undo was not applied"]
371 pub fn undo(&mut self) -> Result<(), String> {
372 if let Some(ref undo_fn) = self.undo_fn {
373 undo_fn(self.widget_id, &self.operation)?;
374 }
375 self.executed = false;
376 Ok(())
377 }
378
379 #[must_use = "handle the result; errors indicate the redo was not applied"]
381 pub fn redo(&mut self) -> Result<(), String> {
382 self.execute()
383 }
384
385 #[must_use]
387 pub fn description(&self) -> &'static str {
388 self.operation.description()
389 }
390
391 #[must_use]
393 pub fn size_bytes(&self) -> usize {
394 std::mem::size_of::<Self>() + self.operation.size_bytes()
395 }
396}
397
398pub trait UndoSupport {
406 fn undo_widget_id(&self) -> UndoWidgetId;
408
409 fn create_snapshot(&self) -> Box<dyn Any + Send>;
413
414 fn restore_snapshot(&mut self, snapshot: &dyn Any) -> bool;
418}
419
420pub trait TextInputUndoExt: UndoSupport {
422 fn text_value(&self) -> &str;
424
425 fn set_text_value(&mut self, value: &str);
427
428 fn cursor_position(&self) -> usize;
430
431 fn set_cursor_position(&mut self, pos: usize);
433
434 fn insert_text_at(&mut self, position: usize, text: &str);
436
437 fn delete_text_range(&mut self, start: usize, end: usize);
439}
440
441pub trait TreeUndoExt: UndoSupport {
443 fn is_node_expanded(&self, path: &[usize]) -> bool;
445
446 fn expand_node(&mut self, path: &[usize]);
448
449 fn collapse_node(&mut self, path: &[usize]);
451}
452
453pub trait ListUndoExt: UndoSupport {
455 fn selected_index(&self) -> Option<usize>;
457
458 fn set_selected_index(&mut self, index: Option<usize>);
460}
461
462pub trait TableUndoExt: UndoSupport {
464 fn sort_state(&self) -> (Option<usize>, bool);
466
467 fn set_sort_state(&mut self, column: Option<usize>, ascending: bool);
469
470 fn filter_text(&self) -> &str;
472
473 fn set_filter_text(&mut self, filter: &str);
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn test_undo_widget_id_uniqueness() {
483 let id1 = UndoWidgetId::new();
484 let id2 = UndoWidgetId::new();
485 assert_ne!(id1, id2);
486 }
487
488 #[test]
489 fn test_undo_widget_id_from_raw() {
490 let id = UndoWidgetId::from_raw(42);
491 assert_eq!(id.raw(), 42);
492 }
493
494 #[test]
495 fn test_text_edit_operation_description() {
496 assert_eq!(
497 TextEditOperation::Insert {
498 position: 0,
499 text: "x".to_string()
500 }
501 .description(),
502 "Insert text"
503 );
504 assert_eq!(
505 TextEditOperation::Delete {
506 position: 0,
507 deleted_text: "x".to_string()
508 }
509 .description(),
510 "Delete text"
511 );
512 }
513
514 #[test]
515 fn test_text_edit_operation_size_bytes() {
516 let op = TextEditOperation::Insert {
517 position: 0,
518 text: "hello".to_string(),
519 };
520 assert!(op.size_bytes() > 5);
521 }
522
523 #[test]
524 fn test_widget_text_edit_cmd_creation() {
525 let widget_id = UndoWidgetId::new();
526 let cmd = WidgetTextEditCmd::new(
527 widget_id,
528 TextEditOperation::Insert {
529 position: 0,
530 text: "test".to_string(),
531 },
532 );
533 assert_eq!(cmd.widget_id(), widget_id);
534 assert_eq!(cmd.description(), "Insert text");
535 }
536
537 #[test]
538 fn test_widget_text_edit_cmd_with_callbacks() {
539 use std::sync::Arc;
540 use std::sync::atomic::{AtomicBool, Ordering};
541
542 let applied = Arc::new(AtomicBool::new(false));
543 let undone = Arc::new(AtomicBool::new(false));
544 let applied_clone = applied.clone();
545 let undone_clone = undone.clone();
546
547 let widget_id = UndoWidgetId::new();
548 let mut cmd = WidgetTextEditCmd::new(
549 widget_id,
550 TextEditOperation::Insert {
551 position: 0,
552 text: "test".to_string(),
553 },
554 )
555 .with_apply(move |_, _| {
556 applied_clone.store(true, Ordering::SeqCst);
557 Ok(())
558 })
559 .with_undo(move |_, _| {
560 undone_clone.store(true, Ordering::SeqCst);
561 Ok(())
562 });
563
564 cmd.execute().unwrap();
565 assert!(applied.load(Ordering::SeqCst));
566
567 cmd.undo().unwrap();
568 assert!(undone.load(Ordering::SeqCst));
569 }
570
571 #[test]
572 fn test_tree_operation_description() {
573 assert_eq!(
574 TreeOperation::Expand { path: vec![0] }.description(),
575 "Expand node"
576 );
577 assert_eq!(
578 TreeOperation::Collapse { path: vec![0] }.description(),
579 "Collapse node"
580 );
581 }
582
583 #[test]
584 fn test_list_operation_description() {
585 assert_eq!(
586 ListOperation::Select {
587 old_selection: None,
588 new_selection: Some(0)
589 }
590 .description(),
591 "Change selection"
592 );
593 }
594
595 #[test]
596 fn test_table_operation_description() {
597 assert_eq!(
598 TableOperation::Sort {
599 old_column: None,
600 old_ascending: true,
601 new_column: Some(0),
602 new_ascending: true
603 }
604 .description(),
605 "Change sort"
606 );
607 }
608
609 #[test]
612 fn widget_id_display() {
613 let id = UndoWidgetId::from_raw(7);
614 assert_eq!(format!("{id}"), "Widget(7)");
615 }
616
617 #[test]
618 fn widget_id_default_is_unique() {
619 let a = UndoWidgetId::default();
620 let b = UndoWidgetId::default();
621 assert_ne!(a, b);
622 }
623
624 #[test]
625 fn widget_id_hash_eq() {
626 let id = UndoWidgetId::from_raw(99);
627 let id2 = UndoWidgetId::from_raw(99);
628 assert_eq!(id, id2);
629
630 use std::collections::HashSet;
631 let mut set = HashSet::new();
632 set.insert(id);
633 assert!(set.contains(&id2));
634 }
635
636 #[test]
639 fn text_edit_replace_description() {
640 let op = TextEditOperation::Replace {
641 position: 0,
642 old_text: "old".to_string(),
643 new_text: "new".to_string(),
644 };
645 assert_eq!(op.description(), "Replace text");
646 }
647
648 #[test]
649 fn text_edit_set_value_description() {
650 let op = TextEditOperation::SetValue {
651 old_value: "".to_string(),
652 new_value: "hello".to_string(),
653 };
654 assert_eq!(op.description(), "Set value");
655 }
656
657 #[test]
658 fn text_edit_delete_size_bytes() {
659 let op = TextEditOperation::Delete {
660 position: 5,
661 deleted_text: "abc".to_string(),
662 };
663 assert!(op.size_bytes() >= 3);
664 }
665
666 #[test]
667 fn text_edit_replace_size_bytes() {
668 let op = TextEditOperation::Replace {
669 position: 0,
670 old_text: "aaa".to_string(),
671 new_text: "bbbbb".to_string(),
672 };
673 assert!(op.size_bytes() >= 8); }
676
677 #[test]
678 fn text_edit_set_value_size_bytes() {
679 let op = TextEditOperation::SetValue {
680 old_value: "x".to_string(),
681 new_value: "yyyy".to_string(),
682 };
683 assert!(op.size_bytes() >= 5); }
685
686 #[test]
689 fn cmd_execute_without_callbacks_succeeds() {
690 let mut cmd = WidgetTextEditCmd::new(
691 UndoWidgetId::from_raw(1),
692 TextEditOperation::Insert {
693 position: 0,
694 text: "hi".to_string(),
695 },
696 );
697 assert!(cmd.execute().is_ok());
698 }
699
700 #[test]
701 fn cmd_undo_without_callbacks_succeeds() {
702 let mut cmd = WidgetTextEditCmd::new(
703 UndoWidgetId::from_raw(1),
704 TextEditOperation::Delete {
705 position: 0,
706 deleted_text: "x".to_string(),
707 },
708 );
709 assert!(cmd.undo().is_ok());
710 }
711
712 #[test]
713 fn cmd_redo_calls_execute() {
714 use std::sync::Arc;
715 use std::sync::atomic::{AtomicUsize, Ordering};
716
717 let count = Arc::new(AtomicUsize::new(0));
718 let count_clone = count.clone();
719
720 let mut cmd = WidgetTextEditCmd::new(
721 UndoWidgetId::from_raw(1),
722 TextEditOperation::Insert {
723 position: 0,
724 text: "t".to_string(),
725 },
726 )
727 .with_apply(move |_, _| {
728 count_clone.fetch_add(1, Ordering::SeqCst);
729 Ok(())
730 });
731
732 cmd.execute().unwrap();
733 assert_eq!(count.load(Ordering::SeqCst), 1);
734
735 cmd.redo().unwrap();
736 assert_eq!(count.load(Ordering::SeqCst), 2);
737 }
738
739 #[test]
740 fn cmd_debug_format() {
741 let cmd = WidgetTextEditCmd::new(
742 UndoWidgetId::from_raw(5),
743 TextEditOperation::Insert {
744 position: 0,
745 text: "abc".to_string(),
746 },
747 );
748 let dbg = format!("{cmd:?}");
749 assert!(dbg.contains("WidgetTextEditCmd"));
750 assert!(dbg.contains("Insert"));
751 }
752
753 #[test]
754 fn cmd_size_bytes_nonzero() {
755 let cmd = WidgetTextEditCmd::new(
756 UndoWidgetId::from_raw(1),
757 TextEditOperation::Insert {
758 position: 0,
759 text: "hello world".to_string(),
760 },
761 );
762 assert!(cmd.size_bytes() > 11);
763 }
764
765 #[test]
768 fn tree_toggle_batch_description() {
769 let op = TreeOperation::ToggleBatch {
770 expanded: vec![vec![0, 1]],
771 collapsed: vec![vec![2]],
772 };
773 assert_eq!(op.description(), "Toggle nodes");
774 }
775
776 #[test]
779 fn list_multi_select_description() {
780 let op = ListOperation::MultiSelect {
781 old_selections: vec![0, 1],
782 new_selections: vec![2, 3],
783 };
784 assert_eq!(op.description(), "Change selections");
785 }
786
787 #[test]
790 fn table_filter_description() {
791 let op = TableOperation::Filter {
792 old_filter: "".to_string(),
793 new_filter: "test".to_string(),
794 };
795 assert_eq!(op.description(), "Apply filter");
796 }
797
798 #[test]
799 fn table_select_row_description() {
800 let op = TableOperation::SelectRow {
801 old_row: Some(0),
802 new_row: Some(5),
803 };
804 assert_eq!(op.description(), "Select row");
805 }
806
807 #[test]
810 fn selection_operation_fields() {
811 let op = SelectionOperation::Changed {
812 old_anchor: Some(0),
813 old_cursor: 5,
814 new_anchor: None,
815 new_cursor: 10,
816 };
817 let dbg = format!("{op:?}");
818 assert!(dbg.contains("Changed"));
819 }
820}