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