1use crate::drawings::{ChartPoint, Drawing};
42use std::collections::HashSet;
43
44#[derive(Debug, Clone)]
46pub(crate) enum DrawingCommand {
47 Snapshot {
49 drawings: Vec<(usize, Drawing)>,
51 ids: Vec<usize>,
53 },
54 Add(Drawing),
56 Delete {
58 drawing: Drawing,
60 index: usize,
62 },
63 Reorder {
65 id: usize,
67 layers: Vec<(usize, i32)>,
69 },
70 Modify {
72 id: usize,
74 old_state: Drawing,
76 },
77}
78
79impl DrawingCommand {
80 pub(crate) fn drawing_id(&self) -> usize {
82 match self {
83 DrawingCommand::Snapshot { ids, .. } => ids.first().copied().unwrap_or(0),
84 DrawingCommand::Add(d) | DrawingCommand::Delete { drawing: d, .. } => d.id,
85 DrawingCommand::Modify { id, .. } | DrawingCommand::Reorder { id, .. } => *id,
86 }
87 }
88
89 pub(crate) fn shift_bar_indices(&mut self, from: f32, delta: f32) {
91 match self {
92 DrawingCommand::Snapshot { drawings, .. } => {
93 for (_, drawing) in drawings {
94 drawing.shift_bar_indices(from, delta);
95 }
96 }
97 DrawingCommand::Reorder { .. } => {}
98 DrawingCommand::Add(d) | DrawingCommand::Delete { drawing: d, .. } => {
99 d.shift_bar_indices(from, delta);
100 }
101 DrawingCommand::Modify { old_state, .. } => {
102 old_state.shift_bar_indices(from, delta);
103 }
104 }
105 }
106}
107
108#[derive(Debug, Clone, Default)]
110pub struct HistoryService {
111 undo_stack: Vec<DrawingCommand>,
113 redo_stack: Vec<DrawingCommand>,
115 max_stack_size: usize,
117 affected_ids: Vec<usize>,
118}
119
120impl HistoryService {
121 pub(crate) fn visit_chart_points(&mut self, visit: &mut impl FnMut(&mut ChartPoint)) {
122 for command in self.undo_stack.iter_mut().chain(&mut self.redo_stack) {
123 match command {
124 DrawingCommand::Snapshot { drawings, .. } => {
125 for (_, drawing) in drawings {
126 drawing.chart_points.iter_mut().for_each(&mut *visit);
127 }
128 }
129 DrawingCommand::Add(drawing)
130 | DrawingCommand::Delete { drawing, .. }
131 | DrawingCommand::Modify {
132 old_state: drawing, ..
133 } => {
134 drawing.chart_points.iter_mut().for_each(&mut *visit);
135 }
136 DrawingCommand::Reorder { .. } => {}
137 }
138 }
139 }
140
141 pub fn new() -> Self {
143 Self {
144 undo_stack: Vec::new(),
145 redo_stack: Vec::new(),
146 affected_ids: Vec::new(),
147 max_stack_size: 100, }
149 }
150
151 pub fn with_max_size(max_size: usize) -> Self {
154 Self {
155 undo_stack: Vec::with_capacity(max_size.min(100)),
156 redo_stack: Vec::with_capacity(max_size.min(100)),
157 affected_ids: Vec::new(),
158 max_stack_size: max_size,
159 }
160 }
161
162 pub fn push_snapshot(&mut self, drawings: Vec<(usize, Drawing)>) {
165 if !drawings.is_empty() {
166 let ids = drawings.iter().map(|(_, drawing)| drawing.id).collect();
167 self.push_command(DrawingCommand::Snapshot { drawings, ids });
168 }
169 }
170
171 pub(crate) fn affected_ids(&self) -> &[usize] {
173 &self.affected_ids
174 }
175
176 fn track_affected(&mut self, command: &DrawingCommand) {
177 self.affected_ids = match command {
178 DrawingCommand::Snapshot { ids, .. } => ids.clone(),
179 DrawingCommand::Reorder { layers, .. } => layers.iter().map(|(id, _)| *id).collect(),
180 _ => vec![command.drawing_id()],
181 };
182 self.affected_ids.sort_unstable();
183 self.affected_ids.dedup();
184 }
185
186 pub fn push_add(&mut self, drawing: Drawing) {
188 self.push_command(DrawingCommand::Add(drawing));
189 }
190
191 pub(crate) fn pop_add(&mut self, id: usize) -> bool {
193 let last = matches!(self.undo_stack.last(), Some(DrawingCommand::Add(d)) if d.id == id);
194 if last {
195 self.undo_stack.pop();
196 }
197 last
198 }
199
200 pub(crate) fn merge_text_placement(
203 &mut self,
204 id: usize,
205 edit: &mut Self,
206 drawing: Option<&Drawing>,
207 ) -> bool {
208 let added = matches!(self.undo_stack.as_slice(), [DrawingCommand::Add(d)] if d.id == id);
209 let edited = matches!(edit.undo_stack.as_slice(), [DrawingCommand::Modify { id: changed, .. }] if *changed == id)
210 || matches!(edit.undo_stack.as_slice(), [DrawingCommand::Delete { drawing, .. }] if drawing.id == id);
211 if !added || !edited {
212 return false;
213 }
214 self.clear();
215 if let Some(drawing) = drawing {
216 self.push_add(drawing.clone());
217 }
218 edit.clear();
219 true
220 }
221
222 pub fn push_delete(&mut self, drawing: Drawing, index: usize) {
224 self.push_command(DrawingCommand::Delete { drawing, index });
225 }
226
227 pub fn push_modify(&mut self, id: usize, old_state: Drawing) {
229 self.push_command(DrawingCommand::Modify { id, old_state });
230 }
231
232 pub(crate) fn push_reorder(&mut self, id: usize, layers: Vec<(usize, i32)>) {
234 self.push_command(DrawingCommand::Reorder { id, layers });
235 }
236
237 pub(crate) fn push_command(&mut self, command: DrawingCommand) {
239 self.redo_stack.clear();
241
242 if self.max_stack_size > 0 && self.undo_stack.len() >= self.max_stack_size {
244 self.undo_stack.remove(0);
245 }
246
247 self.undo_stack.push(command);
248 }
249
250 pub fn undo(&mut self, drawings: &mut Vec<Drawing>) -> Option<usize> {
254 let command = self.undo_stack.pop()?;
255 let affected_id = command.drawing_id();
256 self.track_affected(&command);
257
258 match command {
259 DrawingCommand::Snapshot {
260 drawings: before,
261 ids,
262 } => {
263 let current = restore_snapshot(drawings, before, &ids);
264 self.redo_stack.push(DrawingCommand::Snapshot {
265 drawings: current,
266 ids,
267 });
268 }
269 DrawingCommand::Reorder { id, mut layers } => {
270 for (id, layer) in &mut layers {
271 if let Some(drawing) = drawings.iter_mut().find(|d| d.id == *id) {
272 std::mem::swap(&mut drawing.z_order, layer);
273 }
274 }
275 self.redo_stack.push(DrawingCommand::Reorder { id, layers });
276 }
277 DrawingCommand::Add(drawing) => {
278 drawings.retain(|d| d.id != drawing.id);
280 self.redo_stack.push(DrawingCommand::Add(drawing));
281 }
282 DrawingCommand::Delete { drawing, index } => {
283 drawings.insert(index.min(drawings.len()), drawing.clone());
285 self.redo_stack
286 .push(DrawingCommand::Delete { drawing, index });
287 }
288 DrawingCommand::Modify { id, old_state } => {
289 if let Some(current) = drawings.iter_mut().find(|d| d.id == id) {
291 let current_state = current.clone();
292 *current = old_state;
293 self.redo_stack.push(DrawingCommand::Modify {
294 id,
295 old_state: current_state,
296 });
297 }
298 }
299 }
300
301 Some(affected_id)
302 }
303
304 pub fn redo(&mut self, drawings: &mut Vec<Drawing>) -> Option<usize> {
308 let command = self.redo_stack.pop()?;
309 let affected_id = command.drawing_id();
310 self.track_affected(&command);
311
312 match command {
313 DrawingCommand::Snapshot {
314 drawings: before,
315 ids,
316 } => {
317 let current = restore_snapshot(drawings, before, &ids);
318 self.undo_stack.push(DrawingCommand::Snapshot {
319 drawings: current,
320 ids,
321 });
322 }
323 DrawingCommand::Reorder { id, mut layers } => {
324 for (id, layer) in &mut layers {
325 if let Some(drawing) = drawings.iter_mut().find(|d| d.id == *id) {
326 std::mem::swap(&mut drawing.z_order, layer);
327 }
328 }
329 self.undo_stack.push(DrawingCommand::Reorder { id, layers });
330 }
331 DrawingCommand::Add(drawing) => {
332 drawings.push(drawing.clone());
334 self.undo_stack.push(DrawingCommand::Add(drawing));
335 }
336 DrawingCommand::Delete { drawing, mut index } => {
337 if let Some(at) = drawings.iter().position(|d| d.id == drawing.id) {
339 drawings.remove(at);
340 index = at;
341 }
342 self.undo_stack
343 .push(DrawingCommand::Delete { drawing, index });
344 }
345 DrawingCommand::Modify { id, old_state } => {
346 if let Some(current) = drawings.iter_mut().find(|d| d.id == id) {
348 let current_state = current.clone();
349 *current = old_state;
350 self.undo_stack.push(DrawingCommand::Modify {
351 id,
352 old_state: current_state,
353 });
354 }
355 }
356 }
357
358 Some(affected_id)
359 }
360
361 #[inline]
363 pub fn can_undo(&self) -> bool {
364 !self.undo_stack.is_empty()
365 }
366
367 #[inline]
369 pub fn can_redo(&self) -> bool {
370 !self.redo_stack.is_empty()
371 }
372
373 #[inline]
375 pub fn undo_count(&self) -> usize {
376 self.undo_stack.len()
377 }
378
379 #[inline]
381 pub fn redo_count(&self) -> usize {
382 self.redo_stack.len()
383 }
384
385 pub fn clear(&mut self) {
387 self.undo_stack.clear();
388 self.redo_stack.clear();
389 self.affected_ids.clear();
390 }
391
392 pub fn shift_bar_indices(&mut self, from: f32, delta: f32) {
394 for cmd in &mut self.undo_stack {
395 cmd.shift_bar_indices(from, delta);
396 }
397 for cmd in &mut self.redo_stack {
398 cmd.shift_bar_indices(from, delta);
399 }
400 }
401
402 pub fn prune(&mut self, existing_ids: &HashSet<usize>) {
413 self.undo_stack.retain(|cmd| {
414 matches!(cmd, DrawingCommand::Snapshot { .. })
415 || existing_ids.contains(&cmd.drawing_id())
416 });
417 }
418
419 pub fn last_undo_description(&self) -> Option<&'static str> {
421 self.undo_stack.last().map(|cmd| match cmd {
422 DrawingCommand::Snapshot { .. } => "Edit Drawings",
423 DrawingCommand::Add(_) => "Add Drawing",
424 DrawingCommand::Delete { .. } => "Delete Drawing",
425 DrawingCommand::Modify { .. } => "Modify Drawing",
426 DrawingCommand::Reorder { .. } => "Reorder Drawings",
427 })
428 }
429
430 pub fn last_redo_description(&self) -> Option<&'static str> {
432 self.redo_stack.last().map(|cmd| match cmd {
433 DrawingCommand::Snapshot { .. } => "Edit Drawings",
434 DrawingCommand::Add(_) => "Add Drawing",
435 DrawingCommand::Delete { .. } => "Delete Drawing",
436 DrawingCommand::Modify { .. } => "Modify Drawing",
437 DrawingCommand::Reorder { .. } => "Reorder Drawings",
438 })
439 }
440}
441
442fn restore_snapshot(
446 live: &mut Vec<Drawing>,
447 mut before: Vec<(usize, Drawing)>,
448 ids: &[usize],
449) -> Vec<(usize, Drawing)> {
450 let mut current = Vec::new();
451 let mut restored = Vec::new();
452 for (index, drawing) in live.drain(..).enumerate() {
453 if !ids.contains(&drawing.id) {
454 restored.push(drawing);
455 continue;
456 }
457 if let Some(at) = before.iter().position(|(_, old)| old.id == drawing.id) {
458 restored.push(before.remove(at).1);
459 }
460 current.push((index, drawing));
461 }
462 for (index, drawing) in before {
463 restored.insert(index.min(restored.len()), drawing);
464 }
465 *live = restored;
466 current
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use crate::drawings::{ChartPoint, DrawingToolType};
473
474 fn make_drawing(id: usize) -> Drawing {
475 Drawing::new(id, DrawingToolType::TrendLine)
476 }
477
478 fn drawing(id: usize, stroke_width: f32) -> Drawing {
479 let mut d = make_drawing(id);
480 d.stroke_width = stroke_width;
481 d
482 }
483
484 fn state(drawings: &[Drawing]) -> Vec<(usize, f32, i32)> {
487 drawings
488 .iter()
489 .map(|d| (d.id, d.stroke_width, d.z_order))
490 .collect()
491 }
492
493 #[test]
497 fn undo_and_redo_restore_each_command() {
498 type Record = fn(&mut HistoryService);
499 let cases: [(&str, Vec<Drawing>, Vec<Drawing>, Record); 7] = [
500 (
501 "partial bulk edit",
502 vec![drawing(2, 9.0), drawing(1, 2.0)],
503 vec![drawing(2, 9.0), drawing(1, 8.0)],
504 |h| h.push_snapshot(vec![(1, drawing(1, 2.0))]),
505 ),
506 (
507 "bulk edit and order",
508 vec![drawing(1, 2.0), drawing(2, 3.0)],
509 vec![drawing(2, 8.0)],
510 |h| h.push_snapshot(vec![(0, drawing(1, 2.0)), (1, drawing(2, 3.0))]),
511 ),
512 (
513 "paste",
514 vec![drawing(2, 1.0), drawing(3, 1.0)],
515 vec![drawing(2, 1.0), drawing(3, 1.0), drawing(1, 1.0)],
516 |h| {
517 h.push_command(DrawingCommand::Snapshot {
518 drawings: Vec::new(),
519 ids: vec![1],
520 })
521 },
522 ),
523 (
524 "reorder",
525 vec![drawing(1, 1.0), drawing(2, 1.0)],
526 vec![
527 Drawing {
528 z_order: 1,
529 ..drawing(1, 1.0)
530 },
531 Drawing {
532 z_order: 2,
533 ..drawing(2, 1.0)
534 },
535 ],
536 |h| h.push_reorder(1, vec![(1, 0), (2, 0)]),
537 ),
538 (
539 "add",
540 vec![drawing(2, 1.0)],
541 vec![drawing(2, 1.0), drawing(1, 1.0)],
542 |h| h.push_add(drawing(1, 1.0)),
543 ),
544 (
545 "delete",
546 vec![drawing(2, 1.0), drawing(1, 1.0), drawing(3, 1.0)],
547 vec![drawing(2, 1.0), drawing(3, 1.0)],
548 |h| h.push_delete(drawing(1, 1.0), 1),
549 ),
550 (
551 "modify",
552 vec![drawing(1, 2.0)],
553 vec![drawing(1, 5.0)],
554 |h| h.push_modify(1, drawing(1, 2.0)),
555 ),
556 ];
557
558 for (name, mut before, mut after, record) in cases {
559 let mut history = HistoryService::new();
560 let mut drawings = after.clone();
561 record(&mut history);
562 if name == "bulk edit and order" {
563 let independent = drawing(3, 7.0);
565 before.push(independent.clone());
566 after.push(independent.clone());
567 drawings.push(independent);
568 }
569
570 assert_eq!(history.undo(&mut drawings), Some(1), "{name}: undo id");
571 assert_eq!(state(&drawings), state(&before), "{name}: after undo");
572 assert_eq!(history.redo(&mut drawings), Some(1), "{name}: redo id");
573 assert_eq!(state(&drawings), state(&after), "{name}: after redo");
574 assert_eq!(history.undo(&mut drawings), Some(1), "{name}: undo again");
575 assert_eq!(state(&drawings), state(&before), "{name}: after undo again");
576 }
577 }
578
579 #[test]
580 fn test_new_action_clears_redo() {
581 let mut history = HistoryService::new();
582 let mut drawings = Vec::new();
583
584 let d1 = make_drawing(1);
586 drawings.push(d1.clone());
587 history.push_add(d1);
588
589 history.undo(&mut drawings);
590 assert!(history.can_redo());
591
592 let d2 = make_drawing(2);
593 drawings.push(d2.clone());
594 history.push_add(d2);
595
596 assert!(!history.can_redo()); }
598
599 #[test]
601 fn test_max_stack_size() {
602 let mut history = HistoryService::with_max_size(3);
603 let mut drawings = Vec::new();
604
605 for i in 1..=5 {
606 let d = make_drawing(i);
607 drawings.push(d.clone());
608 history.push_add(d);
609 }
610
611 for expected in [5, 4, 3] {
612 assert_eq!(history.undo(&mut drawings), Some(expected));
613 }
614 assert_eq!(history.undo(&mut drawings), None);
615 }
616
617 #[test]
620 fn test_shift_bar_indices() {
621 let anchored = |id, bar_idx| {
622 let mut d = make_drawing(id);
623 d.chart_points.push(ChartPoint::new(bar_idx, 50.0));
624 d
625 };
626 let mut history = HistoryService::new();
627 let mut drawings = vec![anchored(1, 120.0), anchored(2, 200.0)];
628
629 history.push_modify(1, anchored(1, 100.0));
630 history.push_add(anchored(2, 200.0));
631 history.undo(&mut drawings); history.shift_bar_indices(f32::NEG_INFINITY, 50.0);
634
635 assert_eq!(history.undo(&mut drawings), Some(1));
637 assert_eq!(drawings[0].chart_points[0].bar_idx, 150.0);
638 history.redo(&mut drawings); assert_eq!(history.redo(&mut drawings), Some(2));
641 assert_eq!(drawings[1].chart_points[0].bar_idx, 250.0);
642 }
643
644 #[test]
649 fn test_undo_add_then_prune_preserves_redo() {
650 let mut history = HistoryService::new();
651 let mut drawings = Vec::new();
652
653 let drawing = make_drawing(1);
654 drawings.push(drawing.clone());
655 history.push_add(drawing);
656
657 history.undo(&mut drawings);
659 assert!(drawings.is_empty());
660 assert!(history.can_redo());
661
662 history.prune(&HashSet::new());
665 assert!(
666 history.can_redo(),
667 "redo entry for an undone Add must survive prune"
668 );
669
670 let affected = history.redo(&mut drawings);
672 assert_eq!(affected, Some(1));
673 assert_eq!(drawings.len(), 1);
674 assert_eq!(drawings[0].id, 1);
675 }
676}