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 pub fn execute(&mut self) -> Result<(), String> {
361 if let Some(ref apply_fn) = self.apply_fn {
362 apply_fn(self.widget_id, &self.operation)?;
363 }
364 self.executed = true;
365 Ok(())
366 }
367
368 pub fn undo(&mut self) -> Result<(), String> {
370 if let Some(ref undo_fn) = self.undo_fn {
371 undo_fn(self.widget_id, &self.operation)?;
372 }
373 self.executed = false;
374 Ok(())
375 }
376
377 pub fn redo(&mut self) -> Result<(), String> {
379 self.execute()
380 }
381
382 #[must_use]
384 pub fn description(&self) -> &'static str {
385 self.operation.description()
386 }
387
388 #[must_use]
390 pub fn size_bytes(&self) -> usize {
391 std::mem::size_of::<Self>() + self.operation.size_bytes()
392 }
393}
394
395pub trait UndoSupport {
403 fn undo_widget_id(&self) -> UndoWidgetId;
405
406 fn create_snapshot(&self) -> Box<dyn Any + Send>;
410
411 fn restore_snapshot(&mut self, snapshot: &dyn Any) -> bool;
415}
416
417pub trait TextInputUndoExt: UndoSupport {
419 fn text_value(&self) -> &str;
421
422 fn set_text_value(&mut self, value: &str);
424
425 fn cursor_position(&self) -> usize;
427
428 fn set_cursor_position(&mut self, pos: usize);
430
431 fn insert_text_at(&mut self, position: usize, text: &str);
433
434 fn delete_text_range(&mut self, start: usize, end: usize);
436}
437
438pub trait TreeUndoExt: UndoSupport {
440 fn is_node_expanded(&self, path: &[usize]) -> bool;
442
443 fn expand_node(&mut self, path: &[usize]);
445
446 fn collapse_node(&mut self, path: &[usize]);
448}
449
450pub trait ListUndoExt: UndoSupport {
452 fn selected_index(&self) -> Option<usize>;
454
455 fn set_selected_index(&mut self, index: Option<usize>);
457}
458
459pub trait TableUndoExt: UndoSupport {
461 fn sort_state(&self) -> (Option<usize>, bool);
463
464 fn set_sort_state(&mut self, column: Option<usize>, ascending: bool);
466
467 fn filter_text(&self) -> &str;
469
470 fn set_filter_text(&mut self, filter: &str);
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 fn test_undo_widget_id_uniqueness() {
480 let id1 = UndoWidgetId::new();
481 let id2 = UndoWidgetId::new();
482 assert_ne!(id1, id2);
483 }
484
485 #[test]
486 fn test_undo_widget_id_from_raw() {
487 let id = UndoWidgetId::from_raw(42);
488 assert_eq!(id.raw(), 42);
489 }
490
491 #[test]
492 fn test_text_edit_operation_description() {
493 assert_eq!(
494 TextEditOperation::Insert {
495 position: 0,
496 text: "x".to_string()
497 }
498 .description(),
499 "Insert text"
500 );
501 assert_eq!(
502 TextEditOperation::Delete {
503 position: 0,
504 deleted_text: "x".to_string()
505 }
506 .description(),
507 "Delete text"
508 );
509 }
510
511 #[test]
512 fn test_text_edit_operation_size_bytes() {
513 let op = TextEditOperation::Insert {
514 position: 0,
515 text: "hello".to_string(),
516 };
517 assert!(op.size_bytes() > 5);
518 }
519
520 #[test]
521 fn test_widget_text_edit_cmd_creation() {
522 let widget_id = UndoWidgetId::new();
523 let cmd = WidgetTextEditCmd::new(
524 widget_id,
525 TextEditOperation::Insert {
526 position: 0,
527 text: "test".to_string(),
528 },
529 );
530 assert_eq!(cmd.widget_id(), widget_id);
531 assert_eq!(cmd.description(), "Insert text");
532 }
533
534 #[test]
535 fn test_widget_text_edit_cmd_with_callbacks() {
536 use std::sync::Arc;
537 use std::sync::atomic::{AtomicBool, Ordering};
538
539 let applied = Arc::new(AtomicBool::new(false));
540 let undone = Arc::new(AtomicBool::new(false));
541 let applied_clone = applied.clone();
542 let undone_clone = undone.clone();
543
544 let widget_id = UndoWidgetId::new();
545 let mut cmd = WidgetTextEditCmd::new(
546 widget_id,
547 TextEditOperation::Insert {
548 position: 0,
549 text: "test".to_string(),
550 },
551 )
552 .with_apply(move |_, _| {
553 applied_clone.store(true, Ordering::SeqCst);
554 Ok(())
555 })
556 .with_undo(move |_, _| {
557 undone_clone.store(true, Ordering::SeqCst);
558 Ok(())
559 });
560
561 cmd.execute().unwrap();
562 assert!(applied.load(Ordering::SeqCst));
563
564 cmd.undo().unwrap();
565 assert!(undone.load(Ordering::SeqCst));
566 }
567
568 #[test]
569 fn test_tree_operation_description() {
570 assert_eq!(
571 TreeOperation::Expand { path: vec![0] }.description(),
572 "Expand node"
573 );
574 assert_eq!(
575 TreeOperation::Collapse { path: vec![0] }.description(),
576 "Collapse node"
577 );
578 }
579
580 #[test]
581 fn test_list_operation_description() {
582 assert_eq!(
583 ListOperation::Select {
584 old_selection: None,
585 new_selection: Some(0)
586 }
587 .description(),
588 "Change selection"
589 );
590 }
591
592 #[test]
593 fn test_table_operation_description() {
594 assert_eq!(
595 TableOperation::Sort {
596 old_column: None,
597 old_ascending: true,
598 new_column: Some(0),
599 new_ascending: true
600 }
601 .description(),
602 "Change sort"
603 );
604 }
605}