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}
184
185// ── Confirmation Modal ──
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum ConfirmKind {
189    DeleteTrack,
190    DeleteClip,
191}
192
193#[derive(Debug)]
194pub struct ConfirmModal {
195    pub open: bool,
196    pub kind: ConfirmKind,
197    pub message: String,
198}
199
200impl Default for ConfirmModal {
201    fn default() -> Self { Self::new() }
202}
203
204impl ConfirmModal {
205    pub fn new() -> Self {
206        Self { open: false, kind: ConfirmKind::DeleteTrack, message: String::new() }
207    }
208
209    pub fn show(&mut self, kind: ConfirmKind, message: &str) {
210        self.open = true;
211        self.kind = kind;
212        self.message = message.to_string();
213    }
214
215    pub fn close(&mut self) {
216        self.open = false;
217        self.message.clear();
218    }
219}
220
221// ── Input Modal (for file path entry) ──
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum InputModalKind {
225    SaveAs,
226    Open,
227}
228
229#[derive(Debug)]
230pub struct InputModal {
231    pub open: bool,
232    pub kind: InputModalKind,
233    pub buffer: String,
234    pub cursor: usize,
235}
236
237impl Default for InputModal {
238    fn default() -> Self { Self::new() }
239}
240
241impl InputModal {
242    pub fn new() -> Self {
243        Self { open: false, kind: InputModalKind::SaveAs, buffer: String::new(), cursor: 0 }
244    }
245
246    pub fn open_save(&mut self, default_name: &str) {
247        self.open = true;
248        self.kind = InputModalKind::SaveAs;
249        self.buffer = format!("sessions/{default_name}");
250        self.cursor = self.buffer.len();
251    }
252
253    pub fn open_load(&mut self) {
254        self.open = true;
255        self.kind = InputModalKind::Open;
256        self.buffer = "sessions/".to_string();
257        self.cursor = self.buffer.len();
258    }
259
260    pub fn type_char(&mut self, ch: char) {
261        self.buffer.insert(self.cursor, ch);
262        self.cursor += 1;
263    }
264
265    pub fn backspace(&mut self) {
266        if self.cursor > 0 {
267            self.cursor -= 1;
268            self.buffer.remove(self.cursor);
269        }
270    }
271
272    pub fn delete(&mut self) {
273        if self.cursor < self.buffer.len() {
274            self.buffer.remove(self.cursor);
275        }
276    }
277
278    pub fn move_left(&mut self) {
279        if self.cursor > 0 { self.cursor -= 1; }
280    }
281
282    pub fn move_right(&mut self) {
283        if self.cursor < self.buffer.len() { self.cursor += 1; }
284    }
285
286    pub fn move_home(&mut self) {
287        self.cursor = 0;
288    }
289
290    pub fn move_end(&mut self) {
291        self.cursor = self.buffer.len();
292    }
293
294    pub fn close(&mut self) {
295        self.open = false;
296        self.buffer.clear();
297        self.cursor = 0;
298    }
299
300    pub fn value(&self) -> &str {
301        &self.buffer
302    }
303}
304
305/// The space menu: press Space to open, Space again to close.
306/// Shows all Space+key shortcuts, actions, and help topics.
307#[derive(Debug)]
308pub struct SpaceMenu {
309    pub open: bool,
310    pub cursor: usize,
311    /// Which section is active.
312    pub section: SpaceMenuSection,
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum SpaceMenuSection {
317    /// Main shortcuts list.
318    Actions,
319    /// Help topics.
320    Help,
321}
322
323impl Default for SpaceMenu {
324    fn default() -> Self { Self::new() }
325}
326
327impl SpaceMenu {
328    pub fn new() -> Self {
329        Self { open: false, cursor: 0, section: SpaceMenuSection::Actions }
330    }
331
332    pub fn toggle(&mut self) {
333        self.open = !self.open;
334        if self.open { self.cursor = 0; self.section = SpaceMenuSection::Actions; }
335    }
336
337    pub fn move_up(&mut self) {
338        if self.cursor > 0 { self.cursor -= 1; }
339    }
340
341    pub fn move_down(&mut self) {
342        let max = self.item_count();
343        if self.cursor + 1 < max { self.cursor += 1; }
344    }
345
346    pub fn switch_section(&mut self) {
347        self.section = match self.section {
348            SpaceMenuSection::Actions => SpaceMenuSection::Help,
349            SpaceMenuSection::Help => SpaceMenuSection::Actions,
350        };
351        self.cursor = 0;
352    }
353
354    fn item_count(&self) -> usize {
355        match self.section {
356            SpaceMenuSection::Actions => SPACE_ACTIONS.len(),
357            SpaceMenuSection::Help => HELP_TOPICS.len(),
358        }
359    }
360}
361
362/// Space menu action entries: (key, label, description).
363pub const SPACE_ACTIONS: &[(&str, &str, &str)] = &[
364    ("spc+1", "transport", "focus transport controls"),
365    ("spc+2", "tracks",    "focus the tracks panel"),
366    ("spc+3", "clip view", "focus clip / piano roll panel"),
367    ("spc+p", "play/pause","toggle transport playback"),
368    ("spc+r", "record",    "toggle global recording"),
369    ("spc+l", "loop",      "edit loop region"),
370    ("spc+m", "metronome", "toggle click track"),
371    ("spc+!", "panic",     "kill all sound immediately"),
372    ("spc+a", "add instr", "add instrument track"),
373    ("spc+s", "save",      "save project"),
374    ("spc+o", "open",      "open project"),
375    ("spc+d", "delete",    "delete selected track/clip"),
376    ("spc+e", "edit mode", "note-level piano roll editing"),
377    ("spc+v", "vibe",      "cycle color theme"),
378    ("spc+h", "help",      "open help topics"),
379];
380
381/// Help topic entries: (title, short description).
382pub const HELP_TOPICS: &[(&str, &str)] = &[
383    ("navigation",  "moving between tracks, clips, and panes"),
384    ("transport",   "play, pause, stop, record, loop, BPM"),
385    ("tracks",      "mute, solo, arm, fx, volume, routing"),
386    ("clips",       "selecting, jumping, clip-level fx"),
387    ("piano roll",  "editing MIDI notes, velocity, quantize"),
388    ("fx & mixing", "adding effects, sends, master bus"),
389    ("shortcuts",   "full keyboard shortcut reference"),
390    ("plugins",     "loading and managing plugins"),
391];