git.lucas.co / cce-ui
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 }