Skip to main content

blockworx_tools/history/
undoer.rs

1//! An automatic undo stack fed the current state every frame.
2//!
3//! egui's `egui::util::undoer::Undoer` (© Rerun / emilk, dual-licensed MIT OR
4//! Apache-2.0), reimplemented over [`Duration`] so nothing above the backend
5//! needs a toolkit to remember where it has been. The rules are its two:
6//!
7//! 1. a state that has differed from the newest point and then held still for
8//!    `stable_time` becomes a point of its own;
9//! 2. a state that never holds still becomes one every `auto_save_interval`
10//!    anyway, so a run of changes that never settles still leaves something to
11//!    take back.
12
13use core::time::Duration;
14use std::collections::VecDeque;
15
16#[derive(Clone)]
17pub struct Settings {
18    /// How many points are kept. Past it the oldest is dropped.
19    pub max_undos: usize,
20    /// How long a changed state must hold still before it becomes a point.
21    pub stable_time: Duration,
22    /// How long a state that never holds still may go before it becomes one
23    /// regardless.
24    pub auto_save_interval: Duration,
25}
26
27impl Default for Settings {
28    fn default() -> Self {
29        Self {
30            max_undos: 100,
31            stable_time: Duration::from_secs(1),
32            auto_save_interval: Duration::from_secs(30),
33        }
34    }
35}
36
37/// How the state is changing right now: when the run of changes began, when
38/// the latest of them landed, and what it landed on.
39#[derive(Clone)]
40struct Flux<State> {
41    started: Duration,
42    changed: Duration,
43    latest: State,
44}
45
46#[derive(Clone)]
47pub struct Undoer<State> {
48    settings: Settings,
49    /// Newest at the back. Two adjacent points are never equal, and the
50    /// newest is often the current state.
51    undos: VecDeque<State>,
52    /// Cleared whenever the state changes, so a fresh edit drops the future.
53    redos: Vec<State>,
54    flux: Option<Flux<State>>,
55}
56
57impl<State: Clone + PartialEq> Undoer<State> {
58    pub fn with_settings(settings: Settings) -> Self {
59        Self {
60            settings,
61            undos: VecDeque::new(),
62            redos: Vec::new(),
63            flux: None,
64        }
65    }
66
67    /// Is there a point to go back to that is not where we already stand?
68    pub fn has_undo(&self, current: &State) -> bool {
69        match self.undos.len() {
70            0 => false,
71            1 => self.undos.back() != Some(current),
72            _ => true,
73        }
74    }
75
76    pub fn undo(&mut self, current: &State) -> Option<&State> {
77        if !self.has_undo(current) {
78            return None;
79        }
80        self.flux = None;
81        if let Some(state) = self.undos.pop_back_if(|state| state == current) {
82            self.redos.push(state);
83        } else {
84            self.redos.push(current.clone());
85        }
86        self.undos.back()
87    }
88
89    pub fn redo(&mut self, current: &State) -> Option<&State> {
90        if !self.undos.is_empty() && self.undos.back() != Some(current) {
91            // The state moved on since the undo, so the future it belonged to
92            // is gone.
93            self.redos.clear();
94            return None;
95        }
96        let state = self.redos.pop()?;
97        self.undos.push_back(state);
98        self.undos.back()
99    }
100
101    /// Make `current` a point, unless it already is the newest one.
102    pub fn add_undo(&mut self, current: &State) {
103        if self.undos.back() != Some(current) {
104            self.undos.push_back(current.clone());
105        }
106        while self.undos.len() > self.settings.max_undos {
107            self.undos.pop_front();
108        }
109        self.flux = None;
110    }
111
112    /// Offer this frame's state, and let the two rules decide whether it
113    /// becomes a point.
114    pub fn feed_state(&mut self, now: Duration, current: &State) {
115        let Some(latest) = self.undos.back() else {
116            self.add_undo(current);
117            return;
118        };
119        if latest == current {
120            self.flux = None;
121            return;
122        }
123        self.redos.clear();
124        let Some(flux) = self.flux.as_mut() else {
125            self.flux = Some(Flux {
126                started: now,
127                changed: now,
128                latest: current.clone(),
129            });
130            return;
131        };
132        if &flux.latest == current {
133            if now.saturating_sub(flux.changed) >= self.settings.stable_time {
134                self.add_undo(current);
135            }
136        } else if now.saturating_sub(flux.started) >= self.settings.auto_save_interval {
137            self.add_undo(current);
138        } else {
139            flux.changed = now;
140            flux.latest = current.clone();
141        }
142    }
143}