use std::collections::VecDeque;
use std::fmt;
use std::time::Instant;
pub struct UndoStack<T: Clone + Send + 'static> {
current: T,
max_size: usize,
undo_stack: VecDeque<UndoEntry<T>>,
redo_stack: Vec<UndoEntry<T>>,
}
struct UndoEntry<T> {
apply_fn: Box<dyn Fn(&T) -> T + Send>,
undo_fn: Box<dyn Fn(&T) -> T + Send>,
label: Option<String>,
coalesce_key: Option<String>,
timestamp: Instant,
}
pub struct UndoCommand<T> {
apply_fn: Box<dyn Fn(&T) -> T + Send>,
undo_fn: Box<dyn Fn(&T) -> T + Send>,
label: Option<String>,
coalesce_key: Option<String>,
coalesce_window_ms: u64,
}
impl<T> UndoCommand<T> {
pub fn new(
apply: impl Fn(&T) -> T + Send + 'static,
undo: impl Fn(&T) -> T + Send + 'static,
) -> Self {
Self {
apply_fn: Box::new(apply),
undo_fn: Box::new(undo),
label: None,
coalesce_key: None,
coalesce_window_ms: 0,
}
}
pub fn label(mut self, label: &str) -> Self {
self.label = Some(label.to_string());
self
}
pub fn coalesce(mut self, key: &str, window_ms: u64) -> Self {
self.coalesce_key = Some(key.to_string());
self.coalesce_window_ms = window_ms;
self
}
}
impl<T: Clone + Send + 'static> UndoStack<T> {
pub fn new(initial: T) -> Self {
Self::with_max_size(initial, 100)
}
pub fn with_max_size(initial: T, max_size: usize) -> Self {
Self {
current: initial,
max_size,
undo_stack: VecDeque::new(),
redo_stack: Vec::new(),
}
}
pub fn push(&mut self, state: T) {
let old = self.current.clone();
let new = state;
let new_for_apply = new.clone();
let old_for_undo = old;
self.push_entry(
new,
Box::new(move |_| new_for_apply.clone()),
Box::new(move |_| old_for_undo.clone()),
None,
None,
);
}
pub fn push_labeled(&mut self, state: T, label: &str) {
let old = self.current.clone();
let new = state;
let new_for_apply = new.clone();
let old_for_undo = old;
self.push_entry(
new,
Box::new(move |_| new_for_apply.clone()),
Box::new(move |_| old_for_undo.clone()),
Some(label.to_string()),
None,
);
}
pub fn apply(&mut self, cmd: UndoCommand<T>) {
let new_state = (cmd.apply_fn)(&self.current);
if let Some(ref key) = cmd.coalesce_key
&& cmd.coalesce_window_ms > 0
&& let Some(top) = self.undo_stack.back()
&& top.coalesce_key.as_deref() == Some(key)
&& top.timestamp.elapsed().as_millis() < cmd.coalesce_window_ms as u128
{
let top = self.undo_stack.pop_back().unwrap();
let top_apply = top.apply_fn;
let cmd_apply = cmd.apply_fn;
let composed_apply: Box<dyn Fn(&T) -> T + Send> = Box::new(move |model| {
let intermediate = top_apply(model);
cmd_apply(&intermediate)
});
let top_undo = top.undo_fn;
let cmd_undo = cmd.undo_fn;
let composed_undo: Box<dyn Fn(&T) -> T + Send> = Box::new(move |model| {
let intermediate = cmd_undo(model);
top_undo(&intermediate)
});
self.undo_stack.push_back(UndoEntry {
apply_fn: composed_apply,
undo_fn: composed_undo,
label: top.label,
coalesce_key: top.coalesce_key,
timestamp: Instant::now(),
});
self.current = new_state;
self.redo_stack.clear();
return;
}
self.push_entry(
new_state,
cmd.apply_fn,
cmd.undo_fn,
cmd.label,
cmd.coalesce_key,
);
}
fn push_entry(
&mut self,
new_state: T,
apply_fn: Box<dyn Fn(&T) -> T + Send>,
undo_fn: Box<dyn Fn(&T) -> T + Send>,
label: Option<String>,
coalesce_key: Option<String>,
) {
self.undo_stack.push_back(UndoEntry {
apply_fn,
undo_fn,
label,
coalesce_key,
timestamp: Instant::now(),
});
self.current = new_state;
self.redo_stack.clear();
if self.undo_stack.len() > self.max_size {
self.undo_stack.pop_front();
}
}
pub fn undo(&mut self) -> bool {
match self.undo_stack.pop_back() {
Some(entry) => {
let old_state = (entry.undo_fn)(&self.current);
self.redo_stack.push(entry);
self.current = old_state;
true
}
None => false,
}
}
pub fn redo(&mut self) -> bool {
match self.redo_stack.pop() {
Some(entry) => {
let new_state = (entry.apply_fn)(&self.current);
self.undo_stack.push_back(entry);
self.current = new_state;
true
}
None => false,
}
}
pub fn current(&self) -> &T {
&self.current
}
pub fn current_mut(&mut self) -> &mut T {
&mut self.current
}
pub fn can_undo(&self) -> bool {
!self.undo_stack.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
pub fn undo_count(&self) -> usize {
self.undo_stack.len()
}
pub fn redo_count(&self) -> usize {
self.redo_stack.len()
}
pub fn history(&self) -> Vec<Option<&str>> {
self.undo_stack
.iter()
.rev()
.map(|e| e.label.as_deref())
.collect()
}
}
impl<T: Clone + Send + fmt::Debug + 'static> fmt::Debug for UndoStack<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UndoStack")
.field("current", &self.current)
.field("undo_count", &self.undo_stack.len())
.field("redo_count", &self.redo_stack.len())
.field("max_size", &self.max_size)
.finish()
}
}
impl<T> fmt::Debug for UndoCommand<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UndoCommand")
.field("label", &self.label)
.field("coalesce_key", &self.coalesce_key)
.field("coalesce_window_ms", &self.coalesce_window_ms)
.finish()
}
}