1use std::collections::VecDeque;
2
3use super::state::{PlaylistItem, QueueItemId};
4
5const MAX_UNDO_DEPTH: usize = 100;
7
8const MAX_BATCH_SIZE: usize = 500;
10
11#[derive(Debug, Clone)]
16pub enum UndoEntry {
17 Added { ids: Vec<QueueItemId> },
19
20 Removed {
23 items: Vec<(Box<PlaylistItem>, Option<QueueItemId>)>,
24 },
25
26 Inserted { ids: Vec<QueueItemId> },
28
29 Moved {
31 id: QueueItemId,
32 was_after: Option<QueueItemId>,
33 },
34
35 MovedBatch {
37 entries: Vec<(QueueItemId, Option<QueueItemId>)>,
38 },
39
40 Replaced {
42 items: Vec<PlaylistItem>,
43 cursor: Option<QueueItemId>,
44 },
45
46 Batch(Vec<UndoEntry>),
48}
49
50#[derive(Debug, Default)]
58pub struct UndoStack {
59 undo: VecDeque<UndoEntry>,
60 redo: VecDeque<UndoEntry>,
61}
62
63impl UndoStack {
64 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn push(&mut self, entry: UndoEntry) {
71 self.redo.clear();
72 self.undo.push_back(Self::clamp_batch(entry));
73 if self.undo.len() > MAX_UNDO_DEPTH {
74 self.undo.pop_front();
75 }
76 }
77
78 pub fn pop_undo(&mut self) -> Option<UndoEntry> {
80 self.undo.pop_back()
81 }
82
83 pub fn pop_redo(&mut self) -> Option<UndoEntry> {
85 self.redo.pop_back()
86 }
87
88 pub fn push_redo(&mut self, entry: UndoEntry) {
90 self.redo.push_back(entry);
91 }
92
93 pub fn push_undo_keep_redo(&mut self, entry: UndoEntry) {
96 self.undo.push_back(Self::clamp_batch(entry));
97 if self.undo.len() > MAX_UNDO_DEPTH {
98 self.undo.pop_front();
99 }
100 }
101
102 fn clamp_batch(entry: UndoEntry) -> UndoEntry {
104 match entry {
105 UndoEntry::Batch(mut items) => {
106 items.truncate(MAX_BATCH_SIZE);
107 UndoEntry::Batch(items)
108 }
109 other => other,
110 }
111 }
112
113 pub fn can_undo(&self) -> bool {
114 !self.undo.is_empty()
115 }
116
117 pub fn can_redo(&self) -> bool {
118 !self.redo.is_empty()
119 }
120
121 pub fn undo_len(&self) -> usize {
122 self.undo.len()
123 }
124
125 pub fn redo_len(&self) -> usize {
126 self.redo.len()
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 fn dummy_id() -> QueueItemId {
135 QueueItemId::new()
136 }
137
138 #[test]
139 fn push_and_pop_undo() {
140 let mut stack = UndoStack::new();
141 let id = dummy_id();
142 stack.push(UndoEntry::Added { ids: vec![id] });
143 assert!(stack.can_undo());
144 assert!(!stack.can_redo());
145
146 let entry = stack.pop_undo().unwrap();
147 assert!(matches!(entry, UndoEntry::Added { .. }));
148 assert!(!stack.can_undo());
149 }
150
151 #[test]
152 fn new_action_clears_redo() {
153 let mut stack = UndoStack::new();
154 let id = dummy_id();
155 stack.push(UndoEntry::Added { ids: vec![id] });
156 let entry = stack.pop_undo().unwrap();
157 stack.push_redo(entry);
158 assert!(stack.can_redo());
159
160 stack.push(UndoEntry::Added { ids: vec![id] });
162 assert!(!stack.can_redo());
163 }
164
165 #[test]
166 fn max_depth_enforced() {
167 let mut stack = UndoStack::new();
168 for _ in 0..150 {
169 stack.push(UndoEntry::Added {
170 ids: vec![dummy_id()],
171 });
172 }
173 assert_eq!(stack.undo_len(), MAX_UNDO_DEPTH);
174 }
175
176 #[test]
177 fn push_undo_keep_redo_preserves_redo() {
178 let mut stack = UndoStack::new();
179 let id = dummy_id();
180 stack.push_redo(UndoEntry::Added { ids: vec![id] });
181 assert!(stack.can_redo());
182
183 stack.push_undo_keep_redo(UndoEntry::Added { ids: vec![id] });
184 assert!(stack.can_undo());
185 assert!(stack.can_redo()); }
187}