1mod clip_view;
12mod input;
13mod loop_editor;
14mod menu;
15mod track;
16mod transport_ui;
17pub mod undo;
18
19pub use clip_view::*;
20pub use input::*;
21pub use loop_editor::*;
22pub use menu::*;
23pub use track::*;
24pub use transport_ui::*;
25mod navigation;
26mod params;
27mod track_ops;
28pub use track_ops::initial_tracks;
29
30use phosphor_core::project::TrackKind;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Pane {
36 Transport,
37 Tracks,
38 ClipView,
39}
40
41impl Pane {
42 pub fn number(self) -> u8 {
43 match self {
44 Self::Transport => 1,
45 Self::Tracks => 2,
46 Self::ClipView => 3,
47 }
48 }
49
50 pub fn from_number(n: u8) -> Option<Self> {
51 match n {
52 1 => Some(Self::Transport),
53 2 => Some(Self::Tracks),
54 3 => Some(Self::ClipView),
55 _ => None,
56 }
57 }
58
59 pub fn next(self) -> Self {
60 match self {
61 Self::Transport => Self::Tracks,
62 Self::Tracks => Self::ClipView,
63 Self::ClipView => Self::Transport,
64 }
65 }
66
67 pub fn prev(self) -> Self {
68 match self {
69 Self::Transport => Self::ClipView,
70 Self::Tracks => Self::Transport,
71 Self::ClipView => Self::Tracks,
72 }
73 }
74
75 pub fn label(self) -> &'static str {
76 match self {
77 Self::Transport => "transport",
78 Self::Tracks => "tracks",
79 Self::ClipView => "clip",
80 }
81 }
82}
83
84pub const MAX_VISIBLE_TRACKS: usize = 5;
87pub const INST_CONFIG_PARAM_COUNT: usize = 15;
89
90#[derive(Debug)]
91pub struct NavState {
92 pub focused_pane: Pane,
93 pub track_cursor: usize,
94 pub track_scroll: usize,
95 pub track_selected: bool,
96 pub track_element: TrackElement,
97 pub number_buf: NumberBuffer,
98 pub space_menu: SpaceMenu,
99 pub clip_view: ClipViewState,
100 pub clip_view_visible: bool,
101 pub clip_view_target: Option<(usize, usize)>,
103 pub fx_menu: FxMenu,
105 pub instrument_modal: InstrumentModal,
106 pub loop_editor: LoopEditor,
107 pub transport_ui: TransportUiState,
108 pub tracks: Vec<TrackState>,
109 pub input_modal: InputModal,
111 pub confirm_modal: ConfirmModal,
113 pub undo_stack: undo::UndoStack,
115 pub clip_locked: bool,
118 pub recording_grace: usize,
122}
123
124impl NavState {
125 pub fn new(tracks: Vec<TrackState>) -> Self {
126 Self {
127 focused_pane: Pane::Tracks,
128 track_cursor: 0,
129 track_scroll: 0,
130 track_selected: false,
131 track_element: TrackElement::Label,
132 number_buf: NumberBuffer::new(),
133 space_menu: SpaceMenu::new(),
134 clip_view: ClipViewState::new(),
135 clip_view_visible: false,
136 clip_view_target: None,
137 fx_menu: FxMenu::new(),
138 instrument_modal: InstrumentModal::new(),
139 loop_editor: LoopEditor::new(),
140 transport_ui: TransportUiState::new(),
141 tracks,
142 input_modal: InputModal::new(),
143 confirm_modal: ConfirmModal::new(),
144 undo_stack: undo::UndoStack::new(),
145 clip_locked: false,
146 recording_grace: 0,
147 }
148 }
149 pub fn visible_tracks(&self) -> &[TrackState] {
150 let end = (self.track_scroll + MAX_VISIBLE_TRACKS).min(self.tracks.len());
151 &self.tracks[self.track_scroll..end]
152 }
153
154 pub fn can_scroll_up(&self) -> bool { self.track_scroll > 0 }
155
156 pub fn can_scroll_down(&self) -> bool {
157 self.track_scroll + MAX_VISIBLE_TRACKS < self.tracks.len()
158 }
159
160 pub fn current_track(&self) -> Option<&TrackState> { self.tracks.get(self.track_cursor) }
161
162 pub fn current_track_mut(&mut self) -> Option<&mut TrackState> {
163 self.tracks.get_mut(self.track_cursor)
164 }
165
166 pub fn active_clip(&self) -> Option<&Clip> {
167 let (ti, ci) = self.clip_view_target?;
168 self.tracks.get(ti)?.clips.get(ci)
169 }
170
171 pub fn active_clip_mut(&mut self) -> Option<&mut Clip> {
172 let (ti, ci) = self.clip_view_target?;
173 self.tracks.get_mut(ti)?.clips.get_mut(ci)
174 }
175
176 pub fn active_clip_track(&self) -> Option<&TrackState> {
177 let (ti, _) = self.clip_view_target?;
178 self.tracks.get(ti)
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn pane_numbers() {
188 assert_eq!(Pane::Transport.number(), 1);
189 assert_eq!(Pane::Tracks.number(), 2);
190 assert_eq!(Pane::ClipView.number(), 3);
191 assert_eq!(Pane::from_number(1), Some(Pane::Transport));
192 assert_eq!(Pane::from_number(2), Some(Pane::Tracks));
193 assert_eq!(Pane::from_number(3), Some(Pane::ClipView));
194 assert_eq!(Pane::from_number(9), None);
195 }
196
197 #[test]
198 fn track_element_navigation_full() {
199 let e = TrackElement::Label;
200 assert_eq!(e.move_right(3), TrackElement::Fx);
201 assert_eq!(TrackElement::Fx.move_right(3), TrackElement::Volume);
202 assert_eq!(TrackElement::Volume.move_right(3), TrackElement::Mute);
203 assert_eq!(TrackElement::Mute.move_right(3), TrackElement::Solo);
204 assert_eq!(TrackElement::Solo.move_right(3), TrackElement::RecordArm);
205 assert_eq!(TrackElement::RecordArm.move_right(3), TrackElement::Clip(0));
206 assert_eq!(TrackElement::Clip(2).move_right(3), TrackElement::Clip(2));
207 }
208
209 #[test]
210 fn track_element_left_full() {
211 assert_eq!(TrackElement::Clip(0).move_left(), TrackElement::RecordArm);
212 assert_eq!(TrackElement::RecordArm.move_left(), TrackElement::Solo);
213 assert_eq!(TrackElement::Solo.move_left(), TrackElement::Mute);
214 assert_eq!(TrackElement::Mute.move_left(), TrackElement::Volume);
215 assert_eq!(TrackElement::Volume.move_left(), TrackElement::Fx);
216 assert_eq!(TrackElement::Fx.move_left(), TrackElement::Label);
217 assert_eq!(TrackElement::Label.move_left(), TrackElement::Label);
218 }
219
220 #[test]
221 fn initial_tracks_has_sends_and_master() {
222 let tracks = initial_tracks();
223 assert_eq!(tracks.len(), 3); assert_eq!(tracks[0].kind, TrackKind::SendA);
225 assert_eq!(tracks[1].kind, TrackKind::SendB);
226 assert_eq!(tracks[2].kind, TrackKind::Master);
227 }
228
229 #[test]
230 fn sends_are_at_end() {
231 let mut nav = NavState::new(initial_tracks());
232 nav.move_down();
233 nav.move_down();
234 assert_eq!(nav.track_cursor, 2);
235 assert_eq!(nav.tracks[nav.track_cursor].kind, TrackKind::Master);
236 }
237
238 #[test]
239 fn fx_menu_opens_and_closes() {
240 let mut nav = NavState::new(initial_tracks());
241 nav.enter(); nav.move_right(); assert_eq!(nav.track_element, TrackElement::Fx);
245 nav.enter(); assert!(nav.fx_menu.open);
247
248 nav.escape(); assert!(!nav.fx_menu.open);
250 }
251
252 #[test]
253 fn fx_menu_add_effect() {
254 let mut nav = NavState::new(initial_tracks());
255 let initial_count = nav.tracks[0].fx_chain.len();
256 nav.enter();
257 nav.move_right(); nav.enter(); nav.enter(); assert!(!nav.fx_menu.open);
261 assert_eq!(nav.tracks[0].fx_chain.len(), initial_count + 1);
262 assert_eq!(nav.tracks[0].fx_chain.last().unwrap().fx_type, FxType::Reverb);
263 }
264
265 #[test]
266 fn clip_view_focus_toggle() {
267 let mut nav = NavState::new(initial_tracks());
268 nav.clip_view_visible = true;
270 nav.clip_view_target = Some((0, 0));
271
272 nav.focus_pane(Pane::ClipView);
273 assert_eq!(nav.clip_view.focus, ClipViewFocus::PianoRoll);
274
275 nav.move_left(); assert_eq!(nav.clip_view.focus, ClipViewFocus::FxPanel);
277 }
278
279 #[test]
280 fn clip_view_tabs_cycle() {
281 let mut nav = NavState::new(initial_tracks());
282 nav.focused_pane = Pane::ClipView;
283 nav.clip_view.focus = ClipViewFocus::FxPanel;
284
285 assert_eq!(nav.clip_view.fx_panel_tab, FxPanelTab::TrackFx);
287 nav.cycle_tab();
288 assert_eq!(nav.clip_view.fx_panel_tab, FxPanelTab::Synth);
289 nav.cycle_tab();
290 assert_eq!(nav.clip_view.focus, ClipViewFocus::PianoRoll);
292 assert_eq!(nav.clip_view.clip_tab, ClipTab::InstConfig);
293 nav.cycle_tab();
294 assert_eq!(nav.clip_view.clip_tab, ClipTab::PianoRoll);
296 nav.cycle_tab();
297 assert_eq!(nav.clip_view.clip_tab, ClipTab::Settings);
298 nav.cycle_tab();
299 assert_eq!(nav.clip_view.focus, ClipViewFocus::FxPanel);
301 assert_eq!(nav.clip_view.fx_panel_tab, FxPanelTab::TrackFx);
302 }
303
304 #[test]
305 fn arm_toggle() {
306 let mut nav = NavState::new(initial_tracks());
307 assert!(!nav.tracks[0].armed); nav.toggle_arm();
309 assert!(nav.tracks[0].armed);
310 nav.toggle_arm();
311 assert!(!nav.tracks[0].armed);
312 }
313
314 #[test]
315 fn space_menu_toggle() {
316 let mut nav = NavState::new(initial_tracks());
317 assert!(!nav.space_menu.open);
318 nav.toggle_space_menu();
319 assert!(nav.space_menu.open);
320 nav.toggle_space_menu();
321 assert!(!nav.space_menu.open);
322 }
323
324 #[test]
325 fn space_menu_handle_pane_jump() {
326 let mut nav = NavState::new(initial_tracks());
327 nav.toggle_space_menu();
328 let action = nav.space_menu_handle('2');
329 assert_eq!(nav.focused_pane, Pane::Tracks);
330 assert!(action.is_none());
331 assert!(!nav.space_menu.open);
332
333 nav.toggle_space_menu();
334 let action = nav.space_menu_handle('1');
335 assert_eq!(nav.focused_pane, Pane::Transport);
336 assert!(action.is_none());
337 }
338
339 #[test]
340 fn space_menu_handle_play_pause() {
341 let mut nav = NavState::new(initial_tracks());
342 nav.toggle_space_menu();
343 let action = nav.space_menu_handle('p');
344 assert_eq!(action, Some(SpaceAction::PlayPause));
345 assert!(!nav.space_menu.open);
346 }
347
348 #[test]
349 fn space_menu_enter_select() {
350 let mut nav = NavState::new(initial_tracks());
351 nav.toggle_space_menu();
352 let action = nav.enter();
354 assert!(action.is_none()); assert!(!nav.space_menu.open);
356 }
357
358 #[test]
359 fn space_menu_nav_and_help() {
360 let mut nav = NavState::new(initial_tracks());
361 nav.toggle_space_menu();
362 assert_eq!(nav.space_menu.section, SpaceMenuSection::Actions);
363 nav.space_menu.switch_section();
364 assert_eq!(nav.space_menu.section, SpaceMenuSection::Help);
365 assert_eq!(nav.space_menu.cursor, 0);
366 }
367
368 #[test]
369 fn number_buffer_commit() {
370 let mut buf = NumberBuffer::new();
371 buf.push_digit('1');
372 assert_eq!(buf.commit(), Some(1));
373 buf.push_digit('1');
374 buf.push_digit('2');
375 assert_eq!(buf.commit(), Some(12));
376 }
377
378 #[test]
379 fn number_buffer_empty_commit() {
380 assert_eq!(NumberBuffer::new().commit(), None);
381 }
382
383 #[test]
384 fn nav_cursor_bounds() {
385 let mut nav = NavState::new(initial_tracks());
386 for _ in 0..20 { nav.move_down(); }
387 assert_eq!(nav.track_cursor, 2); }
389
390 #[test]
391 fn enter_escape_track() {
392 let mut nav = NavState::new(initial_tracks());
393 nav.enter();
394 assert!(nav.track_selected);
395 nav.escape();
396 assert!(!nav.track_selected);
397 }
398
399 #[test]
400 fn mute_solo_toggle() {
401 let mut nav = NavState::new(initial_tracks());
402 nav.toggle_mute();
403 assert!(nav.tracks[0].muted);
404 nav.toggle_solo();
405 assert!(nav.tracks[0].soloed);
406 }
407
408 #[test]
409 fn volume_element_in_chain() {
410 let e = TrackElement::Fx;
412 assert_eq!(e.move_right(1), TrackElement::Volume);
413 assert_eq!(TrackElement::Volume.move_left(), TrackElement::Fx);
414 }
415}