1use std::collections::VecDeque;
2
3use super::state::{PlaylistItem, QueueItemId};
4
5const MAX_UNDO_DEPTH: usize = 100;
7
8#[derive(Debug, Clone)]
13pub enum UndoEntry {
14 Added { ids: Vec<QueueItemId> },
16
17 Removed {
20 items: Vec<(Box<PlaylistItem>, Option<QueueItemId>)>,
21 },
22
23 Inserted { ids: Vec<QueueItemId> },
25
26 Moved {
28 id: QueueItemId,
29 was_after: Option<QueueItemId>,
30 },
31
32 MovedBatch {
34 entries: Vec<(QueueItemId, Option<QueueItemId>)>,
35 },
36
37 Replaced {
39 items: Vec<PlaylistItem>,
40 cursor: Option<QueueItemId>,
41 },
42
43 Batch(Vec<UndoEntry>),
45}
46
47#[derive(Debug, Default)]
55pub struct UndoStack {
56 undo: VecDeque<UndoEntry>,
57 redo: VecDeque<UndoEntry>,
58}
59
60impl UndoStack {
61 pub fn new() -> Self {
62 Self::default()
63 }
64
65 pub fn push(&mut self, entry: UndoEntry) {
67 self.redo.clear();
68 self.undo.push_back(entry);
69 if self.undo.len() > MAX_UNDO_DEPTH {
70 self.undo.pop_front();
71 }
72 }
73
74 pub fn pop_undo(&mut self) -> Option<UndoEntry> {
76 self.undo.pop_back()
77 }
78
79 pub fn pop_redo(&mut self) -> Option<UndoEntry> {
81 self.redo.pop_back()
82 }
83
84 pub fn push_redo(&mut self, entry: UndoEntry) {
86 self.redo.push_back(entry);
87 }
88
89 pub fn push_undo_keep_redo(&mut self, entry: UndoEntry) {
92 self.undo.push_back(entry);
93 if self.undo.len() > MAX_UNDO_DEPTH {
94 self.undo.pop_front();
95 }
96 }
97
98 pub fn can_undo(&self) -> bool {
99 !self.undo.is_empty()
100 }
101
102 pub fn can_redo(&self) -> bool {
103 !self.redo.is_empty()
104 }
105
106 pub fn undo_len(&self) -> usize {
107 self.undo.len()
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 fn dummy_id() -> QueueItemId {
116 QueueItemId::new()
117 }
118
119 #[test]
120 fn push_and_pop_undo() {
121 let mut stack = UndoStack::new();
122 let id = dummy_id();
123 stack.push(UndoEntry::Added { ids: vec![id] });
124 assert!(stack.can_undo());
125 assert!(!stack.can_redo());
126
127 let entry = stack.pop_undo().unwrap();
128 assert!(matches!(entry, UndoEntry::Added { .. }));
129 assert!(!stack.can_undo());
130 }
131
132 #[test]
133 fn new_action_clears_redo() {
134 let mut stack = UndoStack::new();
135 let id = dummy_id();
136 stack.push(UndoEntry::Added { ids: vec![id] });
137 let entry = stack.pop_undo().unwrap();
138 stack.push_redo(entry);
139 assert!(stack.can_redo());
140
141 stack.push(UndoEntry::Added { ids: vec![id] });
143 assert!(!stack.can_redo());
144 }
145
146 #[test]
147 fn max_depth_enforced() {
148 let mut stack = UndoStack::new();
149 for _ in 0..150 {
150 stack.push(UndoEntry::Added {
151 ids: vec![dummy_id()],
152 });
153 }
154 assert_eq!(stack.undo_len(), MAX_UNDO_DEPTH);
155 }
156
157 #[test]
158 fn push_undo_keep_redo_preserves_redo() {
159 let mut stack = UndoStack::new();
160 let id = dummy_id();
161 stack.push_redo(UndoEntry::Added { ids: vec![id] });
162 assert!(stack.can_redo());
163
164 stack.push_undo_keep_redo(UndoEntry::Added { ids: vec![id] });
165 assert!(stack.can_undo());
166 assert!(stack.can_redo()); }
168}