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