Skip to main content

phosphor_app/state/
menu.rs

1//! Menu state — SpaceMenu, FxMenu, InstrumentModal, FX types.
2
3// ── FX System ──
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum FxType {
7    Reverb,
8    Delay,
9    Gate,
10    Eq,
11    Limiter,
12    Compressor,
13}
14
15impl FxType {
16    pub fn label(self) -> &'static str {
17        match self {
18            Self::Reverb => "reverb",
19            Self::Delay => "delay",
20            Self::Gate => "gate",
21            Self::Eq => "eq",
22            Self::Limiter => "limiter",
23            Self::Compressor => "comp",
24        }
25    }
26
27    pub const ALL: &[FxType] = &[
28        Self::Reverb, Self::Delay, Self::Gate, Self::Eq, Self::Limiter, Self::Compressor,
29    ];
30}
31
32/// An FX instance on a track.
33#[derive(Debug, Clone)]
34pub struct FxInstance {
35    pub fx_type: FxType,
36    pub enabled: bool,
37    /// Placeholder parameter values (0.0..1.0).
38    pub params: Vec<(String, f32)>,
39}
40
41impl FxInstance {
42    pub fn new(fx_type: FxType) -> Self {
43        let params = match fx_type {
44            FxType::Reverb => vec![
45                ("mix".into(), 0.3), ("decay".into(), 0.5), ("size".into(), 0.6),
46            ],
47            FxType::Delay => vec![
48                ("time".into(), 0.4), ("feedback".into(), 0.3), ("mix".into(), 0.25),
49            ],
50            FxType::Gate => vec![
51                ("thresh".into(), 0.5), ("attack".into(), 0.1), ("release".into(), 0.3),
52            ],
53            FxType::Eq => vec![
54                ("low".into(), 0.5), ("mid".into(), 0.5), ("high".into(), 0.5),
55            ],
56            FxType::Limiter => vec![
57                ("thresh".into(), 0.8), ("release".into(), 0.2),
58            ],
59            FxType::Compressor => vec![
60                ("thresh".into(), 0.6), ("ratio".into(), 0.4), ("attack".into(), 0.1),
61                ("release".into(), 0.3),
62            ],
63        };
64        Self { fx_type, enabled: true, params }
65    }
66}
67
68/// FX menu state (opened when pressing Enter on fx button).
69#[derive(Debug)]
70pub struct FxMenu {
71    pub open: bool,
72    pub cursor: usize,
73}
74
75impl Default for FxMenu {
76    fn default() -> Self { Self::new() }
77}
78
79impl FxMenu {
80    pub fn new() -> Self {
81        Self { open: false, cursor: 0 }
82    }
83
84    pub fn item_count(&self) -> usize {
85        FxType::ALL.len()
86    }
87
88    pub fn move_up(&mut self) {
89        if self.cursor > 0 { self.cursor -= 1; }
90    }
91
92    pub fn move_down(&mut self) {
93        if self.cursor + 1 < self.item_count() { self.cursor += 1; }
94    }
95}
96
97// ── Instrument Selection Modal ──
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum InstrumentType {
101    Synth,
102    DrumRack,
103    DX7,
104    Jupiter8,
105    Odyssey,
106    Juno60,
107    Sampler,
108}
109
110impl InstrumentType {
111    pub fn label(self) -> &'static str {
112        match self {
113            Self::Synth => "Phosphor Synth",
114            Self::DrumRack => "Drum Rack",
115            Self::DX7 => "DX7",
116            Self::Jupiter8 => "Jupiter-8",
117            Self::Odyssey => "Odyssey",
118            Self::Juno60 => "Juno-60",
119            Self::Sampler => "Sampler",
120        }
121    }
122
123    pub fn description(self) -> &'static str {
124        match self {
125            Self::Synth => "polyphonic subtractive synthesizer",
126            Self::DrumRack => "drum machine with sample pads",
127            Self::DX7 => "6-operator FM synthesizer",
128            Self::Jupiter8 => "dual-VCO analog poly synthesizer",
129            Self::Odyssey => "duophonic synth with 3 filter types",
130            Self::Juno60 => "single-DCO poly with BBD chorus",
131            Self::Sampler => "sample-based instrument",
132        }
133    }
134
135    pub const ALL: &[InstrumentType] = &[Self::Synth, Self::DrumRack, Self::DX7, Self::Jupiter8, Self::Odyssey, Self::Juno60, Self::Sampler];
136}
137
138#[derive(Debug)]
139pub struct InstrumentModal {
140    pub open: bool,
141    pub cursor: usize,
142}
143
144impl Default for InstrumentModal {
145    fn default() -> Self { Self::new() }
146}
147
148impl InstrumentModal {
149    pub fn new() -> Self {
150        Self { open: false, cursor: 0 }
151    }
152
153    pub fn move_up(&mut self) {
154        if self.cursor > 0 { self.cursor -= 1; }
155    }
156
157    pub fn move_down(&mut self) {
158        if self.cursor + 1 < InstrumentType::ALL.len() { self.cursor += 1; }
159    }
160
161    pub fn selected(&self) -> InstrumentType {
162        InstrumentType::ALL[self.cursor]
163    }
164}
165
166// ── Space Menu ──
167
168/// Actions that can be triggered from the space menu.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum SpaceAction {
171    PlayPause,
172    ToggleRecord,
173    ToggleLoop,
174    ToggleMetronome,
175    Panic,
176    Save,
177    Open,
178    AddInstrument,
179    Delete,
180    CycleTheme,
181    NewTrack,
182    EditMode,
183    Quantize,
184}
185
186// ── Confirmation Modal ──
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum ConfirmKind {
190    DeleteTrack,
191    DeleteClip,
192}
193
194#[derive(Debug)]
195pub struct ConfirmModal {
196    pub open: bool,
197    pub kind: ConfirmKind,
198    pub message: String,
199}
200
201impl Default for ConfirmModal {
202    fn default() -> Self { Self::new() }
203}
204
205impl ConfirmModal {
206    pub fn new() -> Self {
207        Self { open: false, kind: ConfirmKind::DeleteTrack, message: String::new() }
208    }
209
210    pub fn show(&mut self, kind: ConfirmKind, message: &str) {
211        self.open = true;
212        self.kind = kind;
213        self.message = message.to_string();
214    }
215
216    pub fn close(&mut self) {
217        self.open = false;
218        self.message.clear();
219    }
220}
221
222// ── Input Modal (for file path entry) ──
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub enum InputModalKind {
226    SaveAs,
227    Open,
228}
229
230#[derive(Debug)]
231pub struct InputModal {
232    pub open: bool,
233    pub kind: InputModalKind,
234    pub buffer: String,
235    pub cursor: usize,
236}
237
238impl Default for InputModal {
239    fn default() -> Self { Self::new() }
240}
241
242impl InputModal {
243    pub fn new() -> Self {
244        Self { open: false, kind: InputModalKind::SaveAs, buffer: String::new(), cursor: 0 }
245    }
246
247    pub fn open_save(&mut self, default_name: &str) {
248        self.open = true;
249        self.kind = InputModalKind::SaveAs;
250        self.buffer = format!("sessions/{default_name}");
251        self.cursor = self.buffer.len();
252    }
253
254    pub fn open_load(&mut self) {
255        self.open = true;
256        self.kind = InputModalKind::Open;
257        self.buffer = "sessions/".to_string();
258        self.cursor = self.buffer.len();
259    }
260
261    pub fn type_char(&mut self, ch: char) {
262        self.buffer.insert(self.cursor, ch);
263        self.cursor += 1;
264    }
265
266    pub fn backspace(&mut self) {
267        if self.cursor > 0 {
268            self.cursor -= 1;
269            self.buffer.remove(self.cursor);
270        }
271    }
272
273    pub fn delete(&mut self) {
274        if self.cursor < self.buffer.len() {
275            self.buffer.remove(self.cursor);
276        }
277    }
278
279    pub fn move_left(&mut self) {
280        if self.cursor > 0 { self.cursor -= 1; }
281    }
282
283    pub fn move_right(&mut self) {
284        if self.cursor < self.buffer.len() { self.cursor += 1; }
285    }
286
287    pub fn move_home(&mut self) {
288        self.cursor = 0;
289    }
290
291    pub fn move_end(&mut self) {
292        self.cursor = self.buffer.len();
293    }
294
295    pub fn close(&mut self) {
296        self.open = false;
297        self.buffer.clear();
298        self.cursor = 0;
299    }
300
301    pub fn value(&self) -> &str {
302        &self.buffer
303    }
304}
305
306/// The space menu: press Space to open, Space again to close.
307/// Shows all Space+key shortcuts, actions, and help topics.
308#[derive(Debug)]
309pub struct SpaceMenu {
310    pub open: bool,
311    pub cursor: usize,
312    /// Which section is active.
313    pub section: SpaceMenuSection,
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum SpaceMenuSection {
318    /// Main shortcuts list.
319    Actions,
320    /// Help topics.
321    Help,
322}
323
324impl Default for SpaceMenu {
325    fn default() -> Self { Self::new() }
326}
327
328impl SpaceMenu {
329    pub fn new() -> Self {
330        Self { open: false, cursor: 0, section: SpaceMenuSection::Actions }
331    }
332
333    pub fn toggle(&mut self) {
334        self.open = !self.open;
335        if self.open { self.cursor = 0; self.section = SpaceMenuSection::Actions; }
336    }
337
338    pub fn move_up(&mut self) {
339        if self.cursor > 0 { self.cursor -= 1; }
340    }
341
342    pub fn move_down(&mut self) {
343        let max = self.item_count();
344        if self.cursor + 1 < max { self.cursor += 1; }
345    }
346
347    pub fn switch_section(&mut self) {
348        self.section = match self.section {
349            SpaceMenuSection::Actions => SpaceMenuSection::Help,
350            SpaceMenuSection::Help => SpaceMenuSection::Actions,
351        };
352        self.cursor = 0;
353    }
354
355    fn item_count(&self) -> usize {
356        match self.section {
357            SpaceMenuSection::Actions => SPACE_ACTIONS.len(),
358            SpaceMenuSection::Help => HELP_TOPICS.len(),
359        }
360    }
361}
362
363/// Space menu action entries: (key, label, description).
364pub const SPACE_ACTIONS: &[(&str, &str, &str)] = &[
365    ("spc+1", "transport", "focus transport controls"),
366    ("spc+2", "tracks",    "focus the tracks panel"),
367    ("spc+3", "clip view", "focus clip / piano roll panel"),
368    ("spc+p", "play/pause","toggle transport playback"),
369    ("spc+r", "record",    "toggle global recording"),
370    ("spc+l", "loop",      "edit loop region"),
371    ("spc+m", "metronome", "toggle click track"),
372    ("spc+!", "panic",     "kill all sound immediately"),
373    ("spc+a", "add instr", "add instrument track"),
374    ("spc+s", "save",      "save project"),
375    ("spc+o", "open",      "open project"),
376    ("spc+d", "delete",    "delete selected track/clip"),
377    ("spc+e", "edit mode", "note-level piano roll editing"),
378    ("spc+q", "quantize",  "snap notes to grid"),
379    ("spc+v", "vibe",      "cycle color theme"),
380    ("spc+h", "help",      "open help topics"),
381];
382
383// ── Quantize Modal ──
384
385use super::clip_view::GridResolution;
386
387#[derive(Debug)]
388pub struct QuantizeModal {
389    pub open: bool,
390    pub grid: GridResolution,
391    pub strength: u8,
392    pub cursor: usize,
393}
394
395impl Default for QuantizeModal {
396    fn default() -> Self { Self::new() }
397}
398
399impl QuantizeModal {
400    pub fn new() -> Self {
401        Self { open: false, grid: GridResolution::Eighth, strength: 100, cursor: 0 }
402    }
403    pub fn open_with(&mut self, grid: GridResolution) {
404        self.open = true;
405        self.grid = grid;
406        self.strength = 100;
407        self.cursor = 0;
408    }
409    pub fn close(&mut self) { self.open = false; }
410    pub fn move_up(&mut self) { if self.cursor > 0 { self.cursor -= 1; } }
411    pub fn move_down(&mut self) { if self.cursor < 2 { self.cursor += 1; } }
412    pub fn adjust(&mut self, direction: i32) {
413        match self.cursor {
414            0 => { if direction > 0 { self.grid = self.grid.next(); } else { self.grid = self.grid.prev(); } }
415            1 => { self.strength = (self.strength as i32 + direction * 25).clamp(25, 100) as u8; }
416            _ => {}
417        }
418    }
419}
420
421/// Help topic entries: (title, short description).
422pub const HELP_TOPICS: &[(&str, &str)] = &[
423    ("navigation",  "moving between tracks, clips, and panes"),
424    ("transport",   "play, pause, stop, record, loop, BPM"),
425    ("tracks",      "mute, solo, arm, fx, volume, routing"),
426    ("clips",       "selecting, jumping, clip-level fx"),
427    ("piano roll",  "editing MIDI notes, velocity, quantize"),
428    ("fx & mixing", "adding effects, sends, master bus"),
429    ("shortcuts",   "full keyboard shortcut reference"),
430    ("plugins",     "loading and managing plugins"),
431];