Skip to main content

egui_macview/state/
undo.rs

1//! Authentic AppKit `NSUndoManager` transaction state restoration.
2//!
3//! Normative Reference: SPEC-008 §2.
4//! Grounded in bounded ringbuffer mechanics with strict capacity ceilings
5//! and property-verified cyclic state preservation.
6
7use std::collections::VecDeque;
8
9/// Default capacity for undo and redo transaction snapshots.
10pub const DEFAULT_UNDO_CAPACITY: usize = 50;
11
12/// Bounded ringbuffer undo/redo state manager (`NSUndoManager` equivalent).
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct NSUndoManager<T> {
15    undo_stack: VecDeque<T>,
16    redo_stack: VecDeque<T>,
17    capacity: usize,
18    active_group_initial: Option<T>,
19}
20
21impl<T: Clone + PartialEq> Default for NSUndoManager<T> {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl<T: Clone + PartialEq> NSUndoManager<T> {
28    /// Creates a new `NSUndoManager` with default capacity (`DEFAULT_UNDO_CAPACITY = 50`).
29    pub fn new() -> Self {
30        Self::with_capacity(DEFAULT_UNDO_CAPACITY)
31    }
32
33    /// Creates a new `NSUndoManager` with a custom maximum capacity.
34    pub fn with_capacity(capacity: usize) -> Self {
35        let cap = capacity.max(1);
36        Self {
37            undo_stack: VecDeque::with_capacity(cap),
38            redo_stack: VecDeque::with_capacity(cap),
39            capacity: cap,
40            active_group_initial: None,
41        }
42    }
43
44    /// Maximum snapshot capacity.
45    #[inline]
46    pub fn capacity(&self) -> usize {
47        self.capacity
48    }
49
50    /// Number of recorded undo steps available.
51    #[inline]
52    pub fn undo_count(&self) -> usize {
53        self.undo_stack.len()
54    }
55
56    /// Number of recorded redo steps available.
57    #[inline]
58    pub fn redo_count(&self) -> usize {
59        self.redo_stack.len()
60    }
61
62    /// Returns `true` if an undo operation can be performed.
63    #[inline]
64    pub fn can_undo(&self) -> bool {
65        !self.undo_stack.is_empty()
66    }
67
68    /// Returns `true` if a redo operation can be performed.
69    #[inline]
70    pub fn can_redo(&self) -> bool {
71        !self.redo_stack.is_empty()
72    }
73
74    /// Returns `true` if a coalesced undo group is currently open.
75    #[inline]
76    pub fn is_grouping(&self) -> bool {
77        self.active_group_initial.is_some()
78    }
79
80    /// Non-destructively inspects the most recent undo snapshot.
81    #[inline]
82    pub fn peek_undo(&self) -> Option<&T> {
83        self.undo_stack.back()
84    }
85
86    /// Non-destructively inspects the most recent redo snapshot.
87    #[inline]
88    pub fn peek_redo(&self) -> Option<&T> {
89        self.redo_stack.back()
90    }
91
92    /// Begins a coalesced undo group (such as a continuous slider or canvas drag).
93    ///
94    /// While a group is active, intermediate [`Self::push_snapshot`] calls are ignored
95    /// until [`Self::end_grouping`] commits the net state transition as a single undo step.
96    pub fn begin_grouping(&mut self, initial_state: T) {
97        if self.active_group_initial.is_none() {
98            self.active_group_initial = Some(initial_state);
99        }
100    }
101
102    /// Ends the active coalesced undo group and records a single undo snapshot
103    /// if `final_state` differs from the group's `initial_state`.
104    pub fn end_grouping(&mut self, final_state: &T) -> bool {
105        if let Some(initial) = self.active_group_initial.take() {
106            if &initial != final_state {
107                self.redo_stack.clear();
108                if self.undo_stack.back() != Some(&initial) {
109                    if self.undo_stack.len() >= self.capacity {
110                        self.undo_stack.pop_front();
111                    }
112                    self.undo_stack.push_back(initial);
113                }
114                return true;
115            }
116        }
117        false
118    }
119
120    /// Records a new state snapshot on the undo stack.
121    ///
122    /// In accordance with SPEC-008 §2.2:
123    /// 1. Ignores intermediate snapshots while a coalesced group (`begin_grouping`) is active.
124    /// 2. Deduplicates consecutive identical snapshots (`no-op` protection).
125    /// 3. Drops the oldest entry if the undo stack exceeds capacity.
126    /// 4. Unconditionally clears the redo stack.
127    pub fn push_snapshot(&mut self, state: T) {
128        if self.active_group_initial.is_some() {
129            return;
130        }
131        self.redo_stack.clear();
132        if self.undo_stack.back() == Some(&state) {
133            return;
134        }
135        if self.undo_stack.len() >= self.capacity {
136            self.undo_stack.pop_front();
137        }
138        self.undo_stack.push_back(state);
139    }
140
141    /// Performs an undo operation:
142    /// - Pushes `*current_state` onto the redo stack.
143    /// - Restores `*current_state` from the most recent undo snapshot.
144    /// - Returns `true` if state was restored, or `false` if `undo_stack` was empty.
145    pub fn undo(&mut self, current_state: &mut T) -> bool {
146        if let Some(initial) = self.active_group_initial.take() {
147            if *current_state != initial {
148                if self.redo_stack.len() >= self.capacity {
149                    self.redo_stack.pop_front();
150                }
151                self.redo_stack.push_back(current_state.clone());
152                *current_state = initial;
153                return true;
154            }
155        }
156        if let Some(prev) = self.undo_stack.pop_back() {
157            if self.redo_stack.len() >= self.capacity {
158                self.redo_stack.pop_front();
159            }
160            self.redo_stack.push_back(current_state.clone());
161            *current_state = prev;
162            true
163        } else {
164            false
165        }
166    }
167
168    /// Performs a redo operation:
169    /// - Pushes `*current_state` onto the undo stack.
170    /// - Restores `*current_state` from the most recent redo snapshot.
171    /// - Returns `true` if state was restored, or `false` if `redo_stack` was empty.
172    pub fn redo(&mut self, current_state: &mut T) -> bool {
173        self.active_group_initial = None;
174        if let Some(next) = self.redo_stack.pop_back() {
175            if self.undo_stack.len() >= self.capacity {
176                self.undo_stack.pop_front();
177            }
178            self.undo_stack.push_back(current_state.clone());
179            *current_state = next;
180            true
181        } else {
182            false
183        }
184    }
185
186    /// Clears all undo and redo history.
187    pub fn clear(&mut self) {
188        self.undo_stack.clear();
189        self.redo_stack.clear();
190        self.active_group_initial = None;
191    }
192
193    /// Validates all `NSUndoManager` structural invariants (`GL-1015 §3.5`):
194    /// 1. `capacity >= 1`
195    /// 2. `undo_stack.len() <= capacity` and `redo_stack.len() <= capacity`
196    /// 3. Zero consecutive identical snapshots in `undo_stack`
197    pub fn validate_invariants(&self) -> Result<(), String> {
198        if self.capacity == 0 {
199            return Err("NSUndoManager capacity must be >= 1".to_string());
200        }
201        if self.undo_stack.len() > self.capacity {
202            return Err(format!(
203                "undo_stack len ({}) exceeds capacity ({})",
204                self.undo_stack.len(),
205                self.capacity
206            ));
207        }
208        if self.redo_stack.len() > self.capacity {
209            return Err(format!(
210                "redo_stack len ({}) exceeds capacity ({})",
211                self.redo_stack.len(),
212                self.capacity
213            ));
214        }
215        for i in 1..self.undo_stack.len() {
216            if self.undo_stack[i - 1] == self.undo_stack[i] {
217                return Err(format!(
218                    "duplicate consecutive undo snapshot at index {}",
219                    i
220                ));
221            }
222        }
223        Ok(())
224    }
225}