use crate::drawings::Drawing;
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub enum DrawingCommand {
Add(Drawing),
Delete(Drawing),
Modify {
id: usize,
old_state: Drawing,
},
}
impl DrawingCommand {
pub fn drawing_id(&self) -> usize {
match self {
DrawingCommand::Add(d) => d.id,
DrawingCommand::Delete(d) => d.id,
DrawingCommand::Modify { id, .. } => *id,
}
}
pub fn shift_bar_indices(&mut self, delta: f32) {
match self {
DrawingCommand::Add(d) | DrawingCommand::Delete(d) => {
d.shift_bar_indices(delta);
}
DrawingCommand::Modify { old_state, .. } => {
old_state.shift_bar_indices(delta);
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct HistoryService {
undo_stack: Vec<DrawingCommand>,
redo_stack: Vec<DrawingCommand>,
max_stack_size: usize,
}
impl HistoryService {
pub fn new() -> Self {
Self {
undo_stack: Vec::new(),
redo_stack: Vec::new(),
max_stack_size: 100, }
}
pub fn with_max_size(max_size: usize) -> Self {
Self {
undo_stack: Vec::with_capacity(max_size.min(100)),
redo_stack: Vec::with_capacity(max_size.min(100)),
max_stack_size: max_size,
}
}
pub fn push_add(&mut self, drawing: Drawing) {
self.push_command(DrawingCommand::Add(drawing));
}
pub fn push_delete(&mut self, drawing: Drawing) {
self.push_command(DrawingCommand::Delete(drawing));
}
pub fn push_modify(&mut self, id: usize, old_state: Drawing) {
self.push_command(DrawingCommand::Modify { id, old_state });
}
fn push_command(&mut self, command: DrawingCommand) {
self.redo_stack.clear();
if self.max_stack_size > 0 && self.undo_stack.len() >= self.max_stack_size {
self.undo_stack.remove(0);
}
self.undo_stack.push(command);
}
pub fn undo(&mut self, drawings: &mut Vec<Drawing>) -> Option<usize> {
let command = self.undo_stack.pop()?;
let affected_id = command.drawing_id();
match command {
DrawingCommand::Add(drawing) => {
drawings.retain(|d| d.id != drawing.id);
self.redo_stack.push(DrawingCommand::Add(drawing));
}
DrawingCommand::Delete(drawing) => {
drawings.push(drawing.clone());
self.redo_stack.push(DrawingCommand::Delete(drawing));
}
DrawingCommand::Modify { id, old_state } => {
if let Some(current) = drawings.iter_mut().find(|d| d.id == id) {
let current_state = current.clone();
*current = old_state;
self.redo_stack.push(DrawingCommand::Modify {
id,
old_state: current_state,
});
}
}
}
Some(affected_id)
}
pub fn redo(&mut self, drawings: &mut Vec<Drawing>) -> Option<usize> {
let command = self.redo_stack.pop()?;
let affected_id = command.drawing_id();
match command {
DrawingCommand::Add(drawing) => {
drawings.push(drawing.clone());
self.undo_stack.push(DrawingCommand::Add(drawing));
}
DrawingCommand::Delete(drawing) => {
drawings.retain(|d| d.id != drawing.id);
self.undo_stack.push(DrawingCommand::Delete(drawing));
}
DrawingCommand::Modify { id, old_state } => {
if let Some(current) = drawings.iter_mut().find(|d| d.id == id) {
let current_state = current.clone();
*current = old_state;
self.undo_stack.push(DrawingCommand::Modify {
id,
old_state: current_state,
});
}
}
}
Some(affected_id)
}
#[inline]
pub fn can_undo(&self) -> bool {
!self.undo_stack.is_empty()
}
#[inline]
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
#[inline]
pub fn undo_count(&self) -> usize {
self.undo_stack.len()
}
#[inline]
pub fn redo_count(&self) -> usize {
self.redo_stack.len()
}
pub fn clear(&mut self) {
self.undo_stack.clear();
self.redo_stack.clear();
}
pub fn shift_bar_indices(&mut self, delta: f32) {
for cmd in &mut self.undo_stack {
cmd.shift_bar_indices(delta);
}
for cmd in &mut self.redo_stack {
cmd.shift_bar_indices(delta);
}
}
pub fn prune(&mut self, existing_ids: &HashSet<usize>) {
self.undo_stack
.retain(|cmd| existing_ids.contains(&cmd.drawing_id()));
}
pub fn last_undo_description(&self) -> Option<&'static str> {
self.undo_stack.last().map(|cmd| match cmd {
DrawingCommand::Add(_) => "Add Drawing",
DrawingCommand::Delete(_) => "Delete Drawing",
DrawingCommand::Modify { .. } => "Modify Drawing",
})
}
pub fn last_redo_description(&self) -> Option<&'static str> {
self.redo_stack.last().map(|cmd| match cmd {
DrawingCommand::Add(_) => "Add Drawing",
DrawingCommand::Delete(_) => "Delete Drawing",
DrawingCommand::Modify { .. } => "Modify Drawing",
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::drawings::DrawingToolType;
fn make_drawing(id: usize) -> Drawing {
Drawing::new(id, DrawingToolType::TrendLine)
}
#[test]
fn test_undo_add() {
let mut history = HistoryService::new();
let mut drawings = Vec::new();
let drawing = make_drawing(1);
drawings.push(drawing.clone());
history.push_add(drawing);
assert_eq!(drawings.len(), 1);
assert!(history.can_undo());
let affected = history.undo(&mut drawings);
assert_eq!(affected, Some(1));
assert_eq!(drawings.len(), 0);
assert!(history.can_redo());
}
#[test]
fn test_undo_delete() {
let mut history = HistoryService::new();
let drawing = make_drawing(1);
let mut drawings = vec![drawing.clone()];
drawings.clear();
history.push_delete(drawing);
history.undo(&mut drawings);
assert_eq!(drawings.len(), 1);
assert_eq!(drawings[0].id, 1);
}
#[test]
fn test_undo_modify() {
let mut history = HistoryService::new();
let mut drawing = make_drawing(1);
drawing.stroke_width = 2.0;
let mut drawings = vec![drawing.clone()];
let old_state = drawing.clone();
drawings[0].stroke_width = 5.0;
history.push_modify(1, old_state);
history.undo(&mut drawings);
assert_eq!(drawings[0].stroke_width, 2.0);
}
#[test]
fn test_redo() {
let mut history = HistoryService::new();
let mut drawings = Vec::new();
let drawing = make_drawing(1);
drawings.push(drawing.clone());
history.push_add(drawing);
history.undo(&mut drawings);
assert_eq!(drawings.len(), 0);
history.redo(&mut drawings);
assert_eq!(drawings.len(), 1);
}
#[test]
fn test_new_action_clears_redo() {
let mut history = HistoryService::new();
let mut drawings = Vec::new();
let d1 = make_drawing(1);
drawings.push(d1.clone());
history.push_add(d1);
history.undo(&mut drawings);
assert!(history.can_redo());
let d2 = make_drawing(2);
drawings.push(d2.clone());
history.push_add(d2);
assert!(!history.can_redo()); }
#[test]
fn test_max_stack_size() {
let mut history = HistoryService::with_max_size(3);
let mut drawings = Vec::new();
for i in 1..=5 {
let d = make_drawing(i);
drawings.push(d.clone());
history.push_add(d);
}
assert_eq!(history.undo_count(), 3);
}
#[test]
fn test_shift_bar_indices() {
let mut history = HistoryService::new();
let mut drawing = make_drawing(1);
drawing
.chart_points
.push(crate::drawings::ChartPoint::new(100.0, 50.0));
history.push_add(drawing);
history.shift_bar_indices(50.0);
}
#[test]
fn test_undo_add_then_prune_preserves_redo() {
let mut history = HistoryService::new();
let mut drawings = Vec::new();
let drawing = make_drawing(1);
drawings.push(drawing.clone());
history.push_add(drawing);
history.undo(&mut drawings);
assert!(drawings.is_empty());
assert!(history.can_redo());
history.prune(&HashSet::new());
assert!(
history.can_redo(),
"redo entry for an undone Add must survive prune"
);
let affected = history.redo(&mut drawings);
assert_eq!(affected, Some(1));
assert_eq!(drawings.len(), 1);
assert_eq!(drawings[0].id, 1);
}
#[test]
fn test_descriptions() {
let mut history = HistoryService::new();
assert_eq!(history.last_undo_description(), None);
history.push_add(make_drawing(1));
assert_eq!(history.last_undo_description(), Some("Add Drawing"));
history.push_delete(make_drawing(2));
assert_eq!(history.last_undo_description(), Some("Delete Drawing"));
}
}