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