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 Shuffled {
46 shuffle: bool,
47 order: Vec<(QueueItemId, Option<u32>)>,
48 },
49
50 Batch(Vec<UndoEntry>),
52}
53
54#[derive(Debug, Default)]
62pub struct UndoStack {
63 undo: VecDeque<UndoEntry>,
64 redo: VecDeque<UndoEntry>,
65}
66
67impl UndoStack {
68 pub fn new() -> Self {
69 Self::default()
70 }
71
72 pub fn push(&mut self, entry: UndoEntry) {
74 self.redo.clear();
75 self.undo.push_back(entry);
76 if self.undo.len() > MAX_UNDO_DEPTH {
77 self.undo.pop_front();
78 }
79 }
80
81 pub fn pop_undo(&mut self) -> Option<UndoEntry> {
83 self.undo.pop_back()
84 }
85
86 pub fn pop_redo(&mut self) -> Option<UndoEntry> {
88 self.redo.pop_back()
89 }
90
91 pub fn push_redo(&mut self, entry: UndoEntry) {
93 self.redo.push_back(entry);
94 }
95
96 pub fn push_undo_keep_redo(&mut self, entry: UndoEntry) {
99 self.undo.push_back(entry);
100 if self.undo.len() > MAX_UNDO_DEPTH {
101 self.undo.pop_front();
102 }
103 }
104
105 pub fn can_undo(&self) -> bool {
106 !self.undo.is_empty()
107 }
108
109 pub fn can_redo(&self) -> bool {
110 !self.redo.is_empty()
111 }
112
113 pub fn undo_len(&self) -> usize {
114 self.undo.len()
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 fn dummy_id() -> QueueItemId {
123 QueueItemId::new()
124 }
125
126 #[test]
127 fn push_and_pop_undo() {
128 let mut stack = UndoStack::new();
129 let id = dummy_id();
130 stack.push(UndoEntry::Added { ids: vec![id] });
131 assert!(stack.can_undo());
132 assert!(!stack.can_redo());
133
134 let entry = stack.pop_undo().unwrap();
135 assert!(matches!(entry, UndoEntry::Added { .. }));
136 assert!(!stack.can_undo());
137 }
138
139 #[test]
140 fn new_action_clears_redo() {
141 let mut stack = UndoStack::new();
142 let id = dummy_id();
143 stack.push(UndoEntry::Added { ids: vec![id] });
144 let entry = stack.pop_undo().unwrap();
145 stack.push_redo(entry);
146 assert!(stack.can_redo());
147
148 stack.push(UndoEntry::Added { ids: vec![id] });
150 assert!(!stack.can_redo());
151 }
152
153 #[test]
154 fn max_depth_enforced() {
155 let mut stack = UndoStack::new();
156 for _ in 0..150 {
157 stack.push(UndoEntry::Added {
158 ids: vec![dummy_id()],
159 });
160 }
161 assert_eq!(stack.undo_len(), MAX_UNDO_DEPTH);
162 }
163
164 #[test]
165 fn push_undo_keep_redo_preserves_redo() {
166 let mut stack = UndoStack::new();
167 let id = dummy_id();
168 stack.push_redo(UndoEntry::Added { ids: vec![id] });
169 assert!(stack.can_redo());
170
171 stack.push_undo_keep_redo(UndoEntry::Added { ids: vec![id] });
172 assert!(stack.can_undo());
173 assert!(stack.can_redo()); }
175}