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                self.redos.push(self.undos.pop_back().unwrap());
139            } else {
140                self.redos.push(current_state.clone());
141            }
142
143            // Note: we keep the undo point intact.
144            self.undos.back()
145        } else {
146            None
147        }
148    }
149
150    pub fn redo(&mut self, current_state: &State) -> Option<&State> {
151        if !self.undos.is_empty() && self.undos.back() != Some(current_state) {
152            // state changed since the last undo, redos should be cleared.
153            self.redos.clear();
154            None
155        } else if let Some(state) = self.redos.pop() {
156            self.undos.push_back(state);
157            self.undos.back()
158        } else {
159            None
160        }
161    }
162
163    /// Add an undo point if, and only if, there has been a change since the latest undo point.
164    pub fn add_undo(&mut self, current_state: &State) {
165        if self.undos.back() != Some(current_state) {
166            self.undos.push_back(current_state.clone());
167        }
168        while self.undos.len() > self.settings.max_undos {
169            self.undos.pop_front();
170        }
171        self.flux = None;
172    }
173
174    /// Call this as often as you want (e.g. every frame)
175    /// and [`Undoer`] will determine if a new undo point should be created.
176    ///
177    /// * `current_time`: current time in seconds.
178    pub fn feed_state(&mut self, current_time: f64, current_state: &State) {
179        match self.undos.back() {
180            None => {
181                // First time feed_state is called.
182                // always create an undo point:
183                self.add_undo(current_state);
184            }
185            Some(latest_undo) => {
186                if latest_undo == current_state {
187                    self.flux = None;
188                } else {
189                    self.redos.clear();
190
191                    match self.flux.as_mut() {
192                        None => {
193                            self.flux = Some(Flux {
194                                start_time: current_time,
195                                latest_change_time: current_time,
196                                latest_state: current_state.clone(),
197                            });
198                        }
199                        Some(flux) => {
200                            if &flux.latest_state == current_state {
201                                let time_since_latest_change =
202                                    (current_time - flux.latest_change_time) as f32;
203                                if time_since_latest_change >= self.settings.stable_time {
204                                    self.add_undo(current_state);
205                                }
206                            } else {
207                                let time_since_flux_start = (current_time - flux.start_time) as f32;
208                                if time_since_flux_start >= self.settings.auto_save_interval {
209                                    self.add_undo(current_state);
210                                } else {
211                                    flux.latest_change_time = current_time;
212                                    flux.latest_state = current_state.clone();
213                                }
214                            }
215                        }
216                    }
217                }
218            }
219        }
220    }
221}