pub const MAX_HISTORY: usize = 10;
pub struct History<T> {
undo: Vec<T>,
redo: Vec<T>,
pending: Option<T>,
}
impl<T: Clone + PartialEq> History<T> {
pub fn new() -> Self {
Self {
undo: Vec::new(),
redo: Vec::new(),
pending: None,
}
}
pub fn begin(&mut self, current: &T) {
if self.pending.is_none() {
self.pending = Some(current.clone());
}
}
pub fn commit(&mut self, current: &T) {
if let Some(prev) = self.pending.take() {
if prev != *current {
push_capped(&mut self.undo, prev);
self.redo.clear();
}
}
}
pub fn begin_with(&mut self, current: impl FnOnce() -> T) {
if self.pending.is_none() {
self.pending = Some(current());
}
}
pub fn commit_with(&mut self, current: impl FnOnce() -> T) {
if let Some(prev) = self.pending.take() {
if prev != current() {
push_capped(&mut self.undo, prev);
self.redo.clear();
}
}
}
pub fn undo(&mut self, current: &mut T) -> bool {
self.pending = None;
match self.undo.pop() {
Some(prev) => {
push_capped(&mut self.redo, current.clone());
*current = prev;
true
}
None => false,
}
}
pub fn redo(&mut self, current: &mut T) -> bool {
self.pending = None;
match self.redo.pop() {
Some(next) => {
push_capped(&mut self.undo, current.clone());
*current = next;
true
}
None => false,
}
}
pub fn clear(&mut self) {
self.undo.clear();
self.redo.clear();
self.pending = None;
}
}
impl<T: Clone + PartialEq> Default for History<T> {
fn default() -> Self {
Self::new()
}
}
fn push_capped<T>(stack: &mut Vec<T>, item: T) {
stack.push(item);
if stack.len() > MAX_HISTORY {
stack.remove(0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn undo_then_redo_round_trips() {
let mut h = History::new();
let mut v = 0;
h.begin(&v);
v = 1;
h.commit(&v);
assert!(h.undo(&mut v));
assert_eq!(v, 0);
assert!(h.redo(&mut v));
assert_eq!(v, 1);
}
#[test]
fn no_op_gesture_records_nothing() {
let mut h = History::new();
let mut v = 5;
h.begin(&v);
h.commit(&v); assert!(!h.undo(&mut v));
assert_eq!(v, 5);
}
#[test]
fn begin_is_idempotent_within_a_gesture() {
let mut h = History::new();
let mut v = 0;
h.begin(&v); v = 1;
h.begin(&v); v = 2;
h.commit(&v);
h.undo(&mut v);
assert_eq!(v, 0, "the whole drag is one undo step");
}
#[test]
fn a_new_edit_clears_the_redo_stack() {
let mut h = History::new();
let mut v = 0;
h.begin(&v);
v = 1;
h.commit(&v);
h.undo(&mut v); h.begin(&v);
v = 2;
h.commit(&v);
assert!(!h.redo(&mut v), "redo dropped by the new edit");
assert_eq!(v, 2);
}
#[test]
fn keeps_at_most_max_history_steps() {
let mut h = History::new();
let mut v = 0;
for i in 1..=(MAX_HISTORY + 5) {
h.begin(&v);
v = i as i32;
h.commit(&v);
}
let mut count = 0;
while h.undo(&mut v) {
count += 1;
}
assert_eq!(count, MAX_HISTORY);
}
#[test]
fn begin_with_and_commit_with_round_trip() {
let mut h = History::new();
let mut v = 0;
h.begin_with(|| v);
v = 1;
h.commit_with(|| v);
assert!(h.undo(&mut v));
assert_eq!(v, 0);
assert!(h.redo(&mut v));
assert_eq!(v, 1);
}
#[test]
fn lazy_snapshot_is_built_only_when_needed() {
use std::cell::Cell;
let calls = Cell::new(0);
let mut h: History<i32> = History::new();
h.commit_with(|| {
calls.set(calls.get() + 1);
7
});
assert_eq!(
calls.get(),
0,
"commit_with skips the snapshot with nothing pending"
);
h.begin_with(|| {
calls.set(calls.get() + 1);
7
});
h.begin_with(|| {
calls.set(calls.get() + 1);
7
});
assert_eq!(calls.get(), 1, "begin_with is idempotent within a gesture");
}
}