egui/util/undoer.rs
1use std::collections::VecDeque;
2
3#[derive(Clone, Debug, PartialEq)]
4#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
5pub struct Settings {
6 /// Maximum number of undos.
7 /// If your state is resource intensive, you should keep this low.
8 ///
9 /// Default: `100`
10 pub max_undos: usize,
11
12 /// When that state hasn't changed for this many seconds,
13 /// create a new undo point (if one is needed).
14 ///
15 /// Default value: `1.0` seconds.
16 pub stable_time: f32,
17
18 /// If the state is changing so often that we never get to `stable_time`,
19 /// then still create a save point every `auto_save_interval` seconds,
20 /// so we have something to undo to.
21 ///
22 /// Default value: `30` seconds.
23 pub auto_save_interval: f32,
24}
25
26impl Default for Settings {
27 fn default() -> Self {
28 Self {
29 max_undos: 100,
30 stable_time: 1.0,
31 auto_save_interval: 30.0,
32 }
33 }
34}
35
36/// Automatic undo system.
37///
38/// Every frame you feed it the most recent state.
39/// The [`Undoer`] compares it with the latest undo point
40/// and if there is a change it may create a new undo point.
41///
42/// [`Undoer`] follows two simple rules:
43///
44/// 1) If the state has changed since the latest undo point, but has
45/// remained stable for `stable_time` seconds, an new undo point is created.
46/// 2) If the state does not stabilize within `auto_save_interval` seconds, an undo point is created.
47///
48/// Rule 1) will make sure an undo point is not created until you _stop_ dragging that slider.
49/// Rule 2) will make sure that you will get some undo points even if you are constantly changing the state.
50#[derive(Clone)]
51#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
52pub struct Undoer<State> {
53 settings: Settings,
54
55 /// New undoes are added to the back.
56 /// Two adjacent undo points are never equal.
57 /// The latest undo point may (often) be the current state.
58 undos: VecDeque<State>,
59
60 /// Stores redos immediately after a sequence of undos.
61 /// Gets cleared every time the state changes.
62 /// Does not need to be a deque, because there can only be up to `undos.len()` redos,
63 /// which is already limited to `settings.max_undos`.
64 redos: Vec<State>,
65
66 #[cfg_attr(feature = "serde", serde(skip))]
67 flux: Option<Flux<State>>,
68}
69
70impl<State> std::fmt::Debug for Undoer<State> {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 let Self { undos, redos, .. } = self;
73 f.debug_struct("Undoer")
74 .field("undo count", &undos.len())
75 .field("redo count", &redos.len())
76 .finish()
77 }
78}
79
80impl<State> Default for Undoer<State>
81where
82 State: Clone + PartialEq,
83{
84 #[inline]
85 fn default() -> Self {
86 Self {
87 settings: Settings::default(),
88 undos: VecDeque::new(),
89 redos: Vec::new(),
90 flux: None,
91 }
92 }
93}
94
95/// Represents how the current state is changing
96#[derive(Clone)]
97struct Flux<State> {
98 start_time: f64,
99 latest_change_time: f64,
100 latest_state: State,
101}
102
103impl<State> Undoer<State>
104where
105 State: Clone + PartialEq,
106{
107 /// Create a new [`Undoer`] with the given [`Settings`].
108 pub fn with_settings(settings: Settings) -> Self {
109 Self {
110 settings,
111 ..Default::default()
112 }
113 }
114
115 /// Do we have an undo point different from the given state?
116 pub fn has_undo(&self, current_state: &State) -> bool {
117 match self.undos.len() {
118 0 => false,
119 1 => self.undos.back() != Some(current_state),
120 _ => true,
121 }
122 }
123
124 pub fn has_redo(&self, current_state: &State) -> bool {
125 !self.redos.is_empty() && self.undos.back() == Some(current_state)
126 }
127
128 /// Return true if the state is currently changing
129 pub fn is_in_flux(&self) -> bool {
130 self.flux.is_some()
131 }
132
133 pub fn undo(&mut self, current_state: &State) -> Option<&State> {
134 if self.has_undo(current_state) {
135 self.flux = None;
136
137 if self.undos.back() == Some(current_state) {
138 #[expect(clippy::unwrap_used)] // we just checked that undos is not empty
139 self.redos.push(self.undos.pop_back().unwrap());
140 } else {
141 self.redos.push(current_state.clone());
142 }
143
144 // Note: we keep the undo point intact.
145 self.undos.back()
146 } else {
147 None
148 }
149 }
150
151 pub fn redo(&mut self, current_state: &State) -> Option<&State> {
152 if !self.undos.is_empty() && self.undos.back() != Some(current_state) {
153 // state changed since the last undo, redos should be cleared.
154 self.redos.clear();
155 None
156 } else if let Some(state) = self.redos.pop() {
157 self.undos.push_back(state);
158 self.undos.back()
159 } else {
160 None
161 }
162 }
163
164 /// Add an undo point if, and only if, there has been a change since the latest undo point.
165 pub fn add_undo(&mut self, current_state: &State) {
166 if self.undos.back() != Some(current_state) {
167 self.undos.push_back(current_state.clone());
168 }
169 while self.undos.len() > self.settings.max_undos {
170 self.undos.pop_front();
171 }
172 self.flux = None;
173 }
174
175 /// Call this as often as you want (e.g. every frame)
176 /// and [`Undoer`] will determine if a new undo point should be created.
177 ///
178 /// * `current_time`: current time in seconds.
179 pub fn feed_state(&mut self, current_time: f64, current_state: &State) {
180 match self.undos.back() {
181 None => {
182 // First time feed_state is called.
183 // always create an undo point:
184 self.add_undo(current_state);
185 }
186 Some(latest_undo) => {
187 if latest_undo == current_state {
188 self.flux = None;
189 } else {
190 self.redos.clear();
191
192 match self.flux.as_mut() {
193 None => {
194 self.flux = Some(Flux {
195 start_time: current_time,
196 latest_change_time: current_time,
197 latest_state: current_state.clone(),
198 });
199 }
200 Some(flux) => {
201 if &flux.latest_state == current_state {
202 let time_since_latest_change =
203 (current_time - flux.latest_change_time) as f32;
204 if time_since_latest_change >= self.settings.stable_time {
205 self.add_undo(current_state);
206 }
207 } else {
208 let time_since_flux_start = (current_time - flux.start_time) as f32;
209 if time_since_flux_start >= self.settings.auto_save_interval {
210 self.add_undo(current_state);
211 } else {
212 flux.latest_change_time = current_time;
213 flux.latest_state = current_state.clone();
214 }
215 }
216 }
217 }
218 }
219 }
220 }
221 }
222}