Skip to main content

phosphor_app/state/
navigation.rs

1//! NavState methods: navigation.
2
3use super::*;
4
5impl NavState {
6
7    // ── Space menu ──
8
9    /// Toggle the space menu open/closed.
10    pub fn toggle_space_menu(&mut self) {
11        self.space_menu.toggle();
12    }
13
14    /// Handle a key press while the space menu is open.
15    /// Returns a SpaceAction if an action should be performed.
16
17    /// Handle a key press while the space menu is open.
18    /// Returns a SpaceAction if an action should be performed.
19    pub fn space_menu_handle(&mut self, ch: char) -> Option<SpaceAction> {
20        self.space_menu.open = false;
21        match ch {
22            '1' => { self.focus_pane(Pane::Transport); None }
23            '2' => { self.focus_pane(Pane::Tracks); None }
24            '3' => { self.focus_pane(Pane::ClipView); None }
25            'p' => Some(SpaceAction::PlayPause),
26            '0' => Some(SpaceAction::Stop),
27            'r' => Some(SpaceAction::ToggleRecord),
28            'l' => Some(SpaceAction::ToggleLoop),
29            'm' => Some(SpaceAction::ToggleMetronome),
30            '!' => Some(SpaceAction::Panic),
31            'a' => Some(SpaceAction::AddInstrument),
32            's' => Some(SpaceAction::Save),
33            'o' => Some(SpaceAction::Open),
34            'd' => Some(SpaceAction::Delete),
35            'e' => Some(SpaceAction::EditMode),
36            'q' => Some(SpaceAction::Quantize),
37            'w' => Some(SpaceAction::Presets),
38            'v' => Some(SpaceAction::CycleTheme),
39            'n' => Some(SpaceAction::NewTrack),
40            'h' => {
41                self.space_menu.open = true;
42                self.space_menu.section = SpaceMenuSection::Help;
43                self.space_menu.cursor = 0;
44                None
45            }
46            _ => None,
47        }
48    }
49
50    // ── Pane focus ──
51
52
53    // ── Pane focus ──
54
55    pub fn focus_pane(&mut self, pane: Pane) {
56        if self.focused_pane == Pane::Tracks { self.track_selected = false; self.element_locked = false; }
57        self.focused_pane = pane;
58        tracing::debug!("focused pane: {:?}", pane);
59    }
60
61
62    pub fn focus_next_pane(&mut self) {
63        self.focus_pane(self.focused_pane.next());
64    }
65
66    // ── Navigation ──
67
68
69    // ── Navigation ──
70
71    pub fn move_up(&mut self) {
72        if self.instrument_modal.open { self.instrument_modal.move_up(); return; }
73        if self.space_menu.open { self.space_menu.move_up(); return; }
74        if self.fx_menu.open { self.fx_menu.move_up(); return; }
75        match self.focused_pane {
76            Pane::Transport => {} // no vertical nav in transport
77            Pane::Tracks if !self.track_selected => {
78                if self.track_cursor > 0 {
79                    self.track_cursor -= 1;
80                    if self.track_cursor < self.track_scroll {
81                        self.track_scroll = self.track_cursor;
82                    }
83                }
84            }
85            Pane::Tracks => {
86                // Track is selected — j/k locked, does nothing here
87                // (future: could navigate within track elements)
88            }
89            Pane::ClipView => {
90                match self.clip_view.focus {
91                    ClipViewFocus::PianoRoll if self.clip_view.clip_tab == ClipTab::InstConfig => {
92                        if self.clip_view.inst_config_cursor > 0 {
93                            self.clip_view.inst_config_cursor -= 1;
94                        }
95                    }
96                    ClipViewFocus::PianoRoll if self.clip_view.clip_tab == ClipTab::Settings => {
97                        if self.clip_view.piano_roll.settings_cursor > 0 {
98                            self.clip_view.piano_roll.settings_cursor -= 1;
99                        }
100                    }
101                    ClipViewFocus::PianoRoll => self.clip_view.piano_roll.move_up(),
102                    ClipViewFocus::FxPanel => {
103                        if self.clip_view.fx_panel_tab == FxPanelTab::Synth {
104                            if self.clip_view.synth_param_cursor > 0 {
105                                self.clip_view.synth_param_cursor -= 1;
106                            }
107                        } else if self.clip_view.fx_cursor > 0 {
108                            self.clip_view.fx_cursor -= 1;
109                        }
110                    }
111                }
112            }
113        }
114    }
115
116
117    pub fn move_down(&mut self) {
118        if self.instrument_modal.open { self.instrument_modal.move_down(); return; }
119        if self.space_menu.open { self.space_menu.move_down(); return; }
120        if self.fx_menu.open { self.fx_menu.move_down(); return; }
121        match self.focused_pane {
122            Pane::Transport => {}
123            Pane::Tracks if !self.track_selected => {
124                if self.track_cursor + 1 < self.tracks.len() {
125                    self.track_cursor += 1;
126                    if self.track_cursor >= self.track_scroll + MAX_VISIBLE_TRACKS {
127                        self.track_scroll = self.track_cursor + 1 - MAX_VISIBLE_TRACKS;
128                    }
129                }
130            }
131            Pane::Tracks => {
132                // Track is selected — j/k locked
133            }
134            Pane::ClipView => {
135                match self.clip_view.focus {
136                    ClipViewFocus::PianoRoll if self.clip_view.clip_tab == ClipTab::InstConfig => {
137                        if self.clip_view.inst_config_cursor + 1 < INST_CONFIG_PARAM_COUNT {
138                            self.clip_view.inst_config_cursor += 1;
139                        }
140                    }
141                    ClipViewFocus::PianoRoll if self.clip_view.clip_tab == ClipTab::Settings => {
142                        const SETTINGS_COUNT: usize = 3; // grid, snap, velocity
143                        if self.clip_view.piano_roll.settings_cursor + 1 < SETTINGS_COUNT {
144                            self.clip_view.piano_roll.settings_cursor += 1;
145                        }
146                    }
147                    ClipViewFocus::PianoRoll => self.clip_view.piano_roll.move_down(),
148                    ClipViewFocus::FxPanel => {
149                        if self.clip_view.fx_panel_tab == FxPanelTab::Synth {
150                            let max = self.current_track().map(|t| t.synth_params.len()).unwrap_or(0);
151                            if self.clip_view.synth_param_cursor + 1 < max {
152                                self.clip_view.synth_param_cursor += 1;
153                            }
154                        } else {
155                            let max = self.active_fx_chain_len();
156                            if self.clip_view.fx_cursor + 1 < max {
157                                self.clip_view.fx_cursor += 1;
158                            }
159                        }
160                    }
161                }
162            }
163        }
164    }
165
166
167    pub fn move_left(&mut self) {
168        if self.focused_pane == Pane::Tracks && self.track_selected {
169            self.track_element = self.track_element.move_left();
170            // Keep clip view in sync when navigating between clips
171            if let TrackElement::Clip(idx) = self.track_element {
172                self.open_clip_view(self.track_cursor, idx);
173            }
174        } else if self.focused_pane == Pane::ClipView {
175            match self.clip_view.focus {
176                ClipViewFocus::PianoRoll if self.clip_view.clip_tab == ClipTab::InstConfig => {
177                    // h = placeholder for future inst config param adjustment
178                }
179                ClipViewFocus::PianoRoll if self.clip_view.clip_tab == ClipTab::Settings => {
180                    // h = adjust setting left (prev grid, toggle snap, decrease velocity)
181                    self.adjust_setting(-1);
182                }
183                ClipViewFocus::PianoRoll => {
184                    self.clip_view.focus = ClipViewFocus::FxPanel;
185                }
186                ClipViewFocus::FxPanel if self.clip_view.fx_panel_tab == FxPanelTab::Synth => {
187                    // h = decrease parameter value
188                    self.adjust_synth_param(-0.05);
189                }
190                _ => {}
191            }
192        }
193    }
194
195
196    pub fn move_right(&mut self) {
197        if self.focused_pane == Pane::Tracks && self.track_selected {
198            let num_clips = self.current_track().map(|t| t.clips.len()).unwrap_or(0);
199            self.track_element = self.track_element.move_right(num_clips);
200            // Keep clip view in sync when navigating between clips
201            if let TrackElement::Clip(idx) = self.track_element {
202                self.open_clip_view(self.track_cursor, idx);
203            }
204        } else if self.focused_pane == Pane::ClipView {
205            match self.clip_view.focus {
206                ClipViewFocus::PianoRoll if self.clip_view.clip_tab == ClipTab::InstConfig => {
207                    // l = placeholder for future inst config param adjustment
208                }
209                ClipViewFocus::PianoRoll if self.clip_view.clip_tab == ClipTab::Settings => {
210                    // l = adjust setting right (next grid, toggle snap, increase velocity)
211                    self.adjust_setting(1);
212                }
213                ClipViewFocus::FxPanel if self.clip_view.fx_panel_tab == FxPanelTab::Synth => {
214                    // l = increase parameter value
215                    self.adjust_synth_param(0.05);
216                }
217                ClipViewFocus::FxPanel => {
218                    self.clip_view.focus = ClipViewFocus::PianoRoll;
219                }
220                _ => {}
221            }
222        }
223    }
224
225    /// Adjust the currently selected setting in the Settings tab.
226    pub fn adjust_setting(&mut self, direction: i32) {
227        match self.clip_view.piano_roll.settings_cursor {
228            0 => {
229                // Grid resolution
230                if direction > 0 {
231                    self.clip_view.piano_roll.grid = self.clip_view.piano_roll.grid.next();
232                } else {
233                    self.clip_view.piano_roll.grid = self.clip_view.piano_roll.grid.prev();
234                }
235                self.clip_view.piano_roll.update_column_count();
236            }
237            1 => {
238                // Snap on/off
239                self.clip_view.piano_roll.snap_enabled = !self.clip_view.piano_roll.snap_enabled;
240            }
241            2 => {
242                // Default velocity
243                let v = self.clip_view.piano_roll.default_velocity as i32 + direction * 5;
244                self.clip_view.piano_roll.default_velocity = v.clamp(1, 127) as u8;
245            }
246            _ => {}
247        }
248    }
249
250    /// Adjust the currently selected synth parameter by delta.
251    /// Returns the (mixer_id, param_index, new_value) if changed, for sending to audio.
252
253    pub fn enter(&mut self) -> Option<SpaceAction> {
254        // Space menu open → select item via space_menu_handle using the key from cursor position
255        if self.space_menu.open {
256            match self.space_menu.section {
257                SpaceMenuSection::Actions => {
258                    if let Some((key, _, _)) = SPACE_ACTIONS.get(self.space_menu.cursor) {
259                        // Extract the char after "spc+"
260                        if let Some(ch) = key.strip_prefix("spc+").and_then(|s| s.chars().next()) {
261                            return self.space_menu_handle(ch);
262                        }
263                    }
264                    self.space_menu.open = false;
265                    return None;
266                }
267                SpaceMenuSection::Help => {
268                    // Help topics just show info — no action
269                    return None;
270                }
271            }
272        }
273        // FX menu open → select item
274        if self.fx_menu.open {
275            self.fx_menu_select();
276            return None;
277        }
278
279        match self.focused_pane {
280            Pane::Transport => {} // transport elements handled by app
281            Pane::Tracks => {
282                if !self.track_selected {
283                    self.track_selected = true;
284                    self.track_element = TrackElement::Label;
285                    self.show_current_track_controls();
286                } else {
287                    self.activate_element();
288                }
289            }
290            Pane::ClipView => {}
291        }
292        None
293    }
294
295
296    pub fn escape(&mut self) {
297        if self.instrument_modal.open {
298            self.instrument_modal.open = false;
299            return;
300        }
301        if self.space_menu.open {
302            self.space_menu.open = false;
303            return;
304        }
305        if self.fx_menu.open {
306            self.fx_menu.open = false;
307            return;
308        }
309        match self.focused_pane {
310            Pane::Transport => {} // no escape action in transport
311            Pane::Tracks => {
312                if self.element_locked {
313                    // Release the locked element — back to element navigation
314                    self.element_locked = false;
315                } else if self.track_selected {
316                    self.track_selected = false;
317                    self.element_locked = false;
318                    self.track_element = TrackElement::Label;
319                    self.clip_view_visible = false;
320                    self.clip_view_target = None;
321                }
322            }
323            Pane::ClipView => self.focus_pane(Pane::Tracks),
324        }
325    }
326
327    /// Cycle tabs in the clip view (FX panel or piano roll side).
328    /// Cycle through ALL tabs in buffer 3: trk fx → synth → inst config → piano → auto → trk fx...
329
330    /// Cycle tabs in the clip view (FX panel or piano roll side).
331    /// Cycle through ALL tabs in buffer 3: trk fx → synth → inst config → piano → auto → trk fx...
332    pub fn cycle_tab(&mut self) {
333        if self.focused_pane != Pane::ClipView { return; }
334
335        // The step grid is only a tab on a track that has one. Everywhere else
336        // the cycle steps straight over it, so no key can land on a view with
337        // nothing behind it.
338        let has_sequencer = self.current_track().is_some_and(|t| t.sequencer.is_some());
339
340        match (self.clip_view.focus, self.clip_view.fx_panel_tab, self.clip_view.clip_tab) {
341            // FX panel: trk fx → synth
342            (ClipViewFocus::FxPanel, FxPanelTab::TrackFx, _) => {
343                self.clip_view.fx_panel_tab = FxPanelTab::Synth;
344            }
345            // FX panel: synth → the step grid, or past it to inst config.
346            //
347            // The grid sits directly after the child's own panel because that
348            // is the pair a sequencer track is worked on: the sound and the
349            // pattern driving it, one Tab apart.
350            (ClipViewFocus::FxPanel, FxPanelTab::Synth, _) => {
351                self.clip_view.focus = ClipViewFocus::PianoRoll;
352                if has_sequencer {
353                    self.clip_view.clip_tab = ClipTab::Sequencer;
354                    self.clip_view.sequencer.focus_band(SeqBand::Grid);
355                } else {
356                    self.clip_view.clip_tab = ClipTab::InstConfig;
357                    self.clip_view.inst_config_cursor = 0;
358                }
359            }
360            // Step grid → inst config
361            (ClipViewFocus::PianoRoll, _, ClipTab::Sequencer) => {
362                self.clip_view.clip_tab = ClipTab::InstConfig;
363                self.clip_view.inst_config_cursor = 0;
364            }
365            // Inst config → piano roll
366            (ClipViewFocus::PianoRoll, _, ClipTab::InstConfig) => {
367                self.clip_view.clip_tab = ClipTab::PianoRoll;
368                self.clip_view.piano_roll.focus = PianoRollFocus::Navigation;
369                self.clip_view.piano_roll.column = 0;
370            }
371            // Piano roll: piano → auto
372            (ClipViewFocus::PianoRoll, _, ClipTab::PianoRoll) => {
373                self.clip_view.clip_tab = ClipTab::Settings;
374            }
375            // Piano roll: auto → back to trk fx
376            (ClipViewFocus::PianoRoll, _, ClipTab::Settings) => {
377                self.clip_view.focus = ClipViewFocus::FxPanel;
378                self.clip_view.fx_panel_tab = FxPanelTab::TrackFx;
379                self.clip_view.fx_cursor = 0;
380            }
381        }
382    }
383
384}