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 pub fn redo_len(&self) -> usize {
111 self.redo.len()
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn dummy_id() -> QueueItemId {
120 QueueItemId::new()
121 }
122
123 #[test]
124 fn push_and_pop_undo() {
125 let mut stack = UndoStack::new();
126 let id = dummy_id();
127 stack.push(UndoEntry::Added { ids: vec![id] });
128 assert!(stack.can_undo());
129 assert!(!stack.can_redo());
130
131 let entry = stack.pop_undo().unwrap();
132 assert!(matches!(entry, UndoEntry::Added { .. }));
133 assert!(!stack.can_undo());
134 }
135
136 #[test]
137 fn new_action_clears_redo() {
138 let mut stack = UndoStack::new();
139 let id = dummy_id();
140 stack.push(UndoEntry::Added { ids: vec![id] });
141 let entry = stack.pop_undo().unwrap();
142 stack.push_redo(entry);
143 assert!(stack.can_redo());
144
145 stack.push(UndoEntry::Added { ids: vec![id] });
147 assert!(!stack.can_redo());
148 }
149
150 #[test]
151 fn max_depth_enforced() {
152 let mut stack = UndoStack::new();
153 for _ in 0..150 {
154 stack.push(UndoEntry::Added {
155 ids: vec![dummy_id()],
156 });
157 }
158 assert_eq!(stack.undo_len(), MAX_UNDO_DEPTH);
159 }
160
161 #[test]
162 fn push_undo_keep_redo_preserves_redo() {
163 let mut stack = UndoStack::new();
164 let id = dummy_id();
165 stack.push_redo(UndoEntry::Added { ids: vec![id] });
166 assert!(stack.can_redo());
167
168 stack.push_undo_keep_redo(UndoEntry::Added { ids: vec![id] });
169 assert!(stack.can_undo());
170 assert!(stack.can_redo()); }
172}