Skip to main content

phosphor_app/state/
undo.rs

1//! Undo/redo system — `u` to undo, `r` to redo (vim-style).
2//!
3//! Each destructive action pushes an UndoAction that captures exactly
4//! what was changed so it can be reversed.
5
6use super::TrackState;
7use phosphor_core::clip::NoteSnapshot;
8
9/// A single undoable action.
10#[derive(Debug, Clone)]
11pub enum UndoAction {
12    /// Notes were deleted from a clip (undo = add them back).
13    DeleteNotes {
14        track_idx: usize,
15        clip_idx: usize,
16        notes: Vec<NoteSnapshot>,
17    },
18    /// Notes were added to a clip via paste (undo = remove them).
19    PasteNotes {
20        track_idx: usize,
21        clip_idx: usize,
22        notes: Vec<NoteSnapshot>,
23    },
24    /// A note was drawn (added).
25    DrawNote {
26        track_idx: usize,
27        clip_idx: usize,
28        note: NoteSnapshot,
29    },
30    /// A note was toggled off (removed by pressing n on it).
31    RemoveNote {
32        track_idx: usize,
33        clip_idx: usize,
34        note: NoteSnapshot,
35    },
36    /// A clip was deleted.
37    DeleteClip {
38        track_idx: usize,
39        clip_idx: usize,
40        clip: super::Clip,
41    },
42    /// Notes were moved in the piano roll. Stores snapshots of the notes
43    /// BEFORE the move so undo can restore their original positions.
44    MoveNotes {
45        track_idx: usize,
46        clip_idx: usize,
47        /// (note_index, original_snapshot) for each moved note.
48        before: Vec<(usize, NoteSnapshot)>,
49    },
50    /// A clip's position or size was changed (move/stretch/trim).
51    /// Stores the clip's previous start_tick, length_ticks, notes, and hidden_notes.
52    ModifyClip {
53        track_idx: usize,
54        clip_idx: usize,
55        prev_start: i64,
56        prev_length: i64,
57        prev_notes: Vec<NoteSnapshot>,
58        prev_hidden: Vec<(i64, i64, u8, u8)>,
59    },
60    /// A clip was added (paste/duplicate). Undo = remove it.
61    AddClip {
62        track_idx: usize,
63        clip_idx: usize,
64    },
65    /// A track was deleted. Stores full track state for restoration.
66    DeleteTrack {
67        track_idx: usize,
68        track: TrackState,
69        mixer_id: usize,
70    },
71}
72
73/// Undo/redo stack.
74#[derive(Debug)]
75pub struct UndoStack {
76    undo: Vec<UndoAction>,
77    redo: Vec<UndoAction>,
78    max_size: usize,
79}
80
81impl Default for UndoStack {
82    fn default() -> Self { Self::new() }
83}
84
85impl UndoStack {
86    pub fn new() -> Self {
87        Self { undo: Vec::new(), redo: Vec::new(), max_size: 100 }
88    }
89
90    /// Push a new action. Clears the redo stack (new timeline branch).
91    pub fn push(&mut self, action: UndoAction) {
92        self.undo.push(action);
93        self.redo.clear();
94        if self.undo.len() > self.max_size {
95            self.undo.remove(0);
96        }
97    }
98
99    /// Push to undo stack WITHOUT clearing redo (used during redo operations).
100    pub fn push_undo_only(&mut self, action: UndoAction) {
101        self.undo.push(action);
102        if self.undo.len() > self.max_size {
103            self.undo.remove(0);
104        }
105    }
106
107    /// Pop the last undo action (for undoing). Returns it so caller can reverse it.
108    pub fn pop_undo(&mut self) -> Option<UndoAction> {
109        self.undo.pop()
110    }
111
112    /// Push an action to redo stack (after undoing).
113    pub fn push_redo(&mut self, action: UndoAction) {
114        self.redo.push(action);
115    }
116
117    /// Pop from redo stack (for redoing).
118    pub fn pop_redo(&mut self) -> Option<UndoAction> {
119        self.redo.pop()
120    }
121
122    pub fn can_undo(&self) -> bool { !self.undo.is_empty() }
123    pub fn can_redo(&self) -> bool { !self.redo.is_empty() }
124}