GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/history.rs (8.6K)
1 //! Undo/redo history: a snapshot stack any editing state can own.
2 //!
3 //! The toolkit deliberately does not define what an undoable step *is* —
4 //! that differs per app (a node graph, a palette, a KDL tree, a text
5 //! buffer). It defines the stack: `History<T>` holds snapshots of the
6 //! caller's own state type, one per recorded step, with the three rules
7 //! every undo system needs and every hand-rolled one gets subtly wrong:
8 //!
9 //! - **fork on new edit** — recording after an undo drops the redo branch;
10 //! - **one entry per gesture** — a drag or a typed run is one step, via
11 //! [`History::begin_gesture`] (records lazily, so a gesture that never
12 //! changes anything leaves nothing) and [`History::record_grouped`]
13 //! (consecutive records in the same group keep only the first snapshot);
14 //! - **a cap** — the oldest entries fall off.
15 //!
16 //! Routing is the other half of the system and lives in the runner: the
17 //! `undo` / `redo` chords from `input.kdl` (cce-ui domain defaults
18 //! `ctrl+z` / `ctrl+shift+z`) go first to the focused widget
19 //! ([`ContextAction::Undo`](crate::widget::ContextAction) — a text box
20 //! undoes its own typing), then to the app's
21 //! [`Application::undo`](crate::engine::Application::undo) /
22 //! [`redo`](crate::engine::Application::redo). An app that owns a
23 //! project-wide history answers there; an app with several editing states
24 //! consults them in order and answers with the first that has something.
25 //!
26 //! Snapshots, not commands: the state types in this DE are small and
27 //! cloneable, and a snapshot restore is correct no matter what happened to
28 //! the state in between (an MCP edit, a reload) — a command's inverse is
29 //! not. Apps whose state is large can hold a diff type in `T` instead; the
30 //! stack does not care.
31
32 /// Default cap on undo depth.
33 pub const DEFAULT_LIMIT: usize = 256;
34
35 #[derive(Debug, Clone)]
36 pub struct History<T> {
37 undo: Vec<T>,
38 redo: Vec<T>,
39 limit: usize,
40 /// A gesture's pre-state, held until the first change commits it.
41 pending: Option<T>,
42 /// The group of the last record, for coalescing typed runs.
43 last_group: Option<u32>,
44 }
45
46 impl<T> Default for History<T> {
47 fn default() -> Self {
48 Self::new()
49 }
50 }
51
52 impl<T> History<T> {
53 pub fn new() -> Self {
54 Self::with_limit(DEFAULT_LIMIT)
55 }
56
57 pub fn with_limit(limit: usize) -> Self {
58 History { undo: Vec::new(), redo: Vec::new(), limit: limit.max(1), pending: None, last_group: None }
59 }
60
61 /// Record `before` as the state the next undo returns to. Forks: the
62 /// redo branch is dropped. Ends any coalescing group.
63 pub fn record(&mut self, before: T) {
64 self.last_group = None;
65 self.push(before);
66 }
67
68 /// Record `before` unless the previous record was in the same `group`,
69 /// in which case the earlier snapshot already covers this change and
70 /// nothing is pushed. Typing "hello" with group per keystroke is one
71 /// step; call [`break_group`](Self::break_group) when something else
72 /// happens between two keystrokes (a cursor move) so the next one starts
73 /// a fresh step.
74 pub fn record_grouped(&mut self, before: T, group: u32) {
75 if self.last_group == Some(group) && !self.undo.is_empty() {
76 // Still one step. A redo branch cannot exist here: an undo
77 // breaks the group, so a fresh record after it forks as usual.
78 return;
79 }
80 self.push(before);
81 self.last_group = Some(group);
82 }
83
84 /// End the current coalescing group: the next grouped record starts a
85 /// new step even if it is in the same group.
86 pub fn break_group(&mut self) {
87 self.last_group = None;
88 }
89
90 /// Start a gesture (a drag): hold `before` without recording it. The
91 /// first [`commit_gesture`](Self::commit_gesture) records it; a gesture
92 /// that ends without one leaves no history entry.
93 pub fn begin_gesture(&mut self, before: T) {
94 self.pending = Some(before);
95 }
96
97 /// The gesture changed something: record its pre-state, once. Returns
98 /// whether this call was the one that recorded it.
99 pub fn commit_gesture(&mut self) -> bool {
100 match self.pending.take() {
101 Some(before) => {
102 self.record(before);
103 true
104 }
105 None => false,
106 }
107 }
108
109 /// Drop a gesture that changed nothing (or was abandoned).
110 pub fn cancel_gesture(&mut self) {
111 self.pending = None;
112 }
113
114 pub fn in_gesture(&self) -> bool {
115 self.pending.is_some()
116 }
117
118 /// Step back: returns the snapshot to restore, having filed `current`
119 /// on the redo stack. `None` when there is nothing to undo — `current`
120 /// is dropped in that case, so callers can pass a fresh clone.
121 pub fn undo(&mut self, current: T) -> Option<T> {
122 let target = self.undo.pop()?;
123 self.redo.push(current);
124 self.pending = None;
125 self.last_group = None;
126 Some(target)
127 }
128
129 /// Step forward: the counterpart of [`undo`](Self::undo).
130 pub fn redo(&mut self, current: T) -> Option<T> {
131 let target = self.redo.pop()?;
132 self.undo.push(current);
133 self.pending = None;
134 self.last_group = None;
135 Some(target)
136 }
137
138 pub fn can_undo(&self) -> bool {
139 !self.undo.is_empty()
140 }
141
142 pub fn can_redo(&self) -> bool {
143 !self.redo.is_empty()
144 }
145
146 pub fn undo_len(&self) -> usize {
147 self.undo.len()
148 }
149
150 pub fn redo_len(&self) -> usize {
151 self.redo.len()
152 }
153
154 /// Forget everything — a new document, a new editing session.
155 pub fn clear(&mut self) {
156 self.undo.clear();
157 self.redo.clear();
158 self.pending = None;
159 self.last_group = None;
160 }
161
162 fn push(&mut self, before: T) {
163 self.undo.push(before);
164 self.redo.clear();
165 if self.undo.len() > self.limit {
166 let excess = self.undo.len() - self.limit;
167 self.undo.drain(..excess);
168 }
169 }
170 }
171
172 #[cfg(test)]
173 mod tests {
174 use super::*;
175
176 #[test]
177 fn undo_redo_walk_and_fork() {
178 let mut h = History::new();
179 let mut v = 0;
180 for next in 1..=3 {
181 h.record(v);
182 v = next;
183 }
184 assert_eq!((h.undo_len(), h.redo_len()), (3, 0));
185 v = h.undo(v).unwrap();
186 assert_eq!(v, 2);
187 v = h.undo(v).unwrap();
188 assert_eq!(v, 1);
189 assert_eq!((h.undo_len(), h.redo_len()), (1, 2));
190 v = h.redo(v).unwrap();
191 assert_eq!(v, 2);
192 // A new edit after an undo drops the remaining redo branch.
193 h.record(v);
194 v = 10;
195 assert_eq!((h.undo_len(), h.redo_len()), (3, 0));
196 assert!(h.redo(v).is_none());
197 v = h.undo(v).unwrap();
198 assert_eq!(v, 2);
199 assert_eq!(h.redo(v), Some(10));
200 }
201
202 #[test]
203 fn gesture_records_once_and_only_if_committed() {
204 let mut h: History<i32> = History::new();
205 h.begin_gesture(0);
206 assert!(h.in_gesture());
207 h.cancel_gesture();
208 assert!(!h.can_undo(), "an abandoned gesture leaves nothing");
209
210 h.begin_gesture(0);
211 assert!(h.commit_gesture());
212 assert!(!h.commit_gesture(), "second motion is the same step");
213 assert!(!h.commit_gesture());
214 assert_eq!(h.undo_len(), 1);
215 assert_eq!(h.undo(5), Some(0));
216 }
217
218 #[test]
219 fn grouped_records_coalesce_until_broken() {
220 let mut h: History<&str> = History::new();
221 h.record_grouped("", 1);
222 h.record_grouped("h", 1);
223 h.record_grouped("he", 1);
224 assert_eq!(h.undo_len(), 1, "a typed run is one step");
225 h.record_grouped("hel", 2);
226 assert_eq!(h.undo_len(), 2, "a different group starts a step");
227 h.break_group();
228 h.record_grouped("hel ", 2);
229 assert_eq!(h.undo_len(), 3, "break_group splits the same group");
230 assert_eq!(h.undo("hel w"), Some("hel "));
231 // An undo ends the group too: the next grouped record is a fresh
232 // step (and forks the redo branch).
233 h.record_grouped("hel ", 2);
234 assert_eq!((h.undo_len(), h.redo_len()), (3, 0));
235 }
236
237 #[test]
238 fn limit_drops_the_oldest() {
239 let mut h = History::with_limit(2);
240 h.record(1);
241 h.record(2);
242 h.record(3);
243 assert_eq!(h.undo_len(), 2);
244 assert_eq!(h.undo(4), Some(3));
245 assert_eq!(h.undo(3), Some(2));
246 assert_eq!(h.undo(2), None);
247 }
248
249 #[test]
250 fn clear_forgets_everything() {
251 let mut h = History::new();
252 h.record(1);
253 h.begin_gesture(2);
254 h.clear();
255 assert!(!h.can_undo() && !h.can_redo() && !h.in_gesture());
256 }
257 }