Skip to main content

photon_ui/
undo_stack.rs

1/// A generic undo/redo stack.
2///
3/// Stores snapshots of state. Calling [`undo`](UndoStack::undo) moves backward
4/// through history; [`redo`](UndoStack::redo) moves forward. Pushing a new
5/// item after undoing discards the redo branch.
6///
7/// # Type parameters
8///
9/// `T` must implement `Clone` because the stack stores owned snapshots.
10#[derive(Debug, Clone)]
11pub struct UndoStack<T: Clone> {
12    history: Vec<T>,
13    index: usize,
14}
15
16impl<T: Clone> UndoStack<T> {
17    /// Create an empty undo stack.
18    pub fn new() -> Self {
19        Self {
20            history: Vec::new(),
21            index: 0,
22        }
23    }
24
25    /// Push a new state snapshot onto the stack.
26    ///
27    /// Any redo history after the current position is discarded.
28    pub fn push(&mut self, item: T) {
29        self.history.truncate(self.index + 1);
30        self.history.push(item);
31        self.index = self.history.len().saturating_sub(1);
32    }
33
34    /// Move one step backward in history and return the state at the new
35    /// position.
36    ///
37    /// If already at the earliest state, returns that state again.
38    pub fn undo(&mut self) -> Option<&T> {
39        if self.index == 0 {
40            self.history.first()
41        } else {
42            self.index -= 1;
43            self.history.get(self.index)
44        }
45    }
46
47    /// Move one step forward in history and return the state at the new
48    /// position.
49    ///
50    /// Returns `None` if already at the most recent state.
51    pub fn redo(&mut self) -> Option<&T> {
52        if self.index + 1 < self.history.len() {
53            self.index += 1;
54            self.history.get(self.index)
55        } else {
56            None
57        }
58    }
59
60    /// Return the state at the current position without changing it.
61    pub fn current(&self) -> Option<&T> {
62        self.history.get(self.index)
63    }
64}
65
66impl<T: Clone> Default for UndoStack<T> {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn current_returns_none_when_empty() {
78        let stack: UndoStack<i32> = UndoStack::new();
79        assert!(stack.current().is_none());
80    }
81
82    #[test]
83    fn redo_at_end_returns_none() {
84        let mut stack = UndoStack::new();
85        stack.push(1);
86        assert!(stack.redo().is_none());
87    }
88
89    #[test]
90    fn undo_redo_sequence() {
91        let mut stack = UndoStack::new();
92        stack.push(1);
93        stack.push(2);
94        stack.push(3);
95        assert_eq!(stack.current(), Some(&3));
96        assert_eq!(stack.undo(), Some(&2));
97        assert_eq!(stack.undo(), Some(&1));
98        assert_eq!(stack.undo(), Some(&1));
99        assert_eq!(stack.redo(), Some(&2));
100        assert_eq!(stack.redo(), Some(&3));
101        assert_eq!(stack.redo(), None);
102    }
103
104    #[test]
105    fn push_after_undo_overwrites() {
106        let mut stack = UndoStack::new();
107        stack.push(1);
108        stack.push(2);
109        stack.undo();
110        stack.push(3);
111        assert_eq!(stack.current(), Some(&3));
112        assert_eq!(stack.redo(), None);
113    }
114}