use std::collections::VecDeque;
#[derive(Clone, Debug, PartialEq)]
pub struct Settings {
pub max_undos: usize,
pub stable_time: f32,
pub auto_save_interval: f32,
}
impl Default for Settings {
fn default() -> Self {
Self {
max_undos: 100,
stable_time: 1.0,
auto_save_interval: 30.0,
}
}
}
#[derive(Clone)]
struct Flux<State> {
start_time: f64,
latest_change_time: f64,
latest_state: State,
}
#[derive(Clone)]
pub struct Undoer<State> {
settings: Settings,
undos: VecDeque<State>,
redos: Vec<State>,
flux: Option<Flux<State>>,
}
impl<State> Default for Undoer<State>
where
State: Clone + PartialEq,
{
fn default() -> Self {
Self {
settings: Settings::default(),
undos: VecDeque::new(),
redos: Vec::new(),
flux: None,
}
}
}
impl<State> Undoer<State>
where
State: Clone + PartialEq,
{
#[allow(dead_code)]
pub fn with_settings(settings: Settings) -> Self {
Self {
settings,
..Default::default()
}
}
pub fn has_undo(&self, current_state: &State) -> bool {
match self.undos.len() {
0 => false,
1 => self.undos.back() != Some(current_state),
_ => true,
}
}
pub fn has_redo(&self, current_state: &State) -> bool {
!self.redos.is_empty() && self.undos.back() == Some(current_state)
}
pub fn is_in_flux(&self) -> bool {
self.flux.is_some()
}
pub fn undo(&mut self, current_state: &State) -> Option<&State> {
if self.has_undo(current_state) {
self.flux = None;
if self.undos.back() == Some(current_state) {
if let Some(popped) = self.undos.pop_back() {
self.redos.push(popped);
}
} else {
self.redos.push(current_state.clone());
}
self.undos.back()
} else {
None
}
}
pub fn redo(&mut self, current_state: &State) -> Option<&State> {
if !self.undos.is_empty() && self.undos.back() != Some(current_state) {
self.redos.clear();
None
} else if let Some(state) = self.redos.pop() {
self.undos.push_back(state);
self.undos.back()
} else {
None
}
}
pub fn add_undo(&mut self, current_state: &State) {
if self.undos.back() != Some(current_state) {
self.undos.push_back(current_state.clone());
}
while self.undos.len() > self.settings.max_undos {
self.undos.pop_front();
}
self.flux = None;
}
pub fn feed_state(&mut self, current_time: f64, current_state: &State) {
match self.undos.back() {
None => {
self.add_undo(current_state);
}
Some(latest_undo) => {
if latest_undo == current_state {
self.flux = None;
} else {
self.redos.clear();
match self.flux.as_mut() {
None => {
self.flux = Some(Flux {
start_time: current_time,
latest_change_time: current_time,
latest_state: current_state.clone(),
});
}
Some(flux) => {
if &flux.latest_state == current_state {
let time_since_latest_change =
(current_time - flux.latest_change_time) as f32;
if time_since_latest_change >= self.settings.stable_time {
self.add_undo(current_state);
}
} else {
let time_since_flux_start =
(current_time - flux.start_time) as f32;
if time_since_flux_start >= self.settings.auto_save_interval {
self.add_undo(current_state);
} else {
flux.latest_change_time = current_time;
flux.latest_state = current_state.clone();
}
}
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, PartialEq, Debug)]
struct S(i32);
fn undoer() -> Undoer<S> {
Undoer::with_settings(Settings {
max_undos: 100,
stable_time: 1.0,
auto_save_interval: 30.0,
})
}
#[test]
fn first_feed_creates_baseline_and_no_undo() {
let mut u = undoer();
u.feed_state(0.0, &S(0));
assert!(!u.has_undo(&S(0)));
assert!(!u.has_redo(&S(0)));
}
#[test]
fn stable_time_coalesces_rapid_changes_into_one_point() {
let mut u = undoer();
u.feed_state(0.0, &S(0));
u.feed_state(0.1, &S(1));
assert!(u.is_in_flux());
u.feed_state(0.2, &S(2));
u.feed_state(0.3, &S(3));
u.feed_state(0.4, &S(3)); u.feed_state(1.5, &S(3)); assert!(u.has_undo(&S(3)));
assert!(!u.is_in_flux());
assert_eq!(u.undo(&S(3)).cloned(), Some(S(0)));
assert!(!u.has_undo(&S(0)));
}
#[test]
fn undo_then_redo_round_trips() {
let mut u = undoer();
u.feed_state(0.0, &S(0));
u.feed_state(0.1, &S(5));
u.feed_state(0.2, &S(5));
u.feed_state(2.0, &S(5)); assert!(u.has_undo(&S(5)));
let reverted = u.undo(&S(5)).cloned();
assert_eq!(reverted, Some(S(0)));
assert!(u.has_redo(&S(0)));
let advanced = u.redo(&S(0)).cloned();
assert_eq!(advanced, Some(S(5)));
assert!(!u.has_redo(&S(5)));
}
#[test]
fn auto_save_interval_forces_point_during_continuous_change() {
let mut u = undoer();
u.feed_state(0.0, &S(0));
for i in 1..=40 {
u.feed_state(i as f64, &S(i)); }
assert!(u.has_undo(&S(40)));
}
#[test]
fn changing_state_clears_redos() {
let mut u = undoer();
u.feed_state(0.0, &S(0));
u.feed_state(0.1, &S(1));
u.feed_state(2.0, &S(1)); u.undo(&S(1)); assert!(u.has_redo(&S(0)));
u.feed_state(2.1, &S(9));
assert!(!u.has_redo(&S(9)));
}
}