diff --git a/crates/tek_core/src/command.rs b/crates/tek_core/src/command.rs index ee2ffc9d..22100bde 100644 --- a/crates/tek_core/src/command.rs +++ b/crates/tek_core/src/command.rs @@ -6,28 +6,65 @@ pub trait Command: Sized { pub trait MatchInput: Sized { fn match_input (state: &S, input: &E::Input) -> Option; } -pub struct Menu, C: Command> { +pub struct MenuBar> { + pub menus: Vec>, + pub index: usize, +} +impl> MenuBar { + pub fn new () -> Self { Self { menus: vec![], index: 0 } } + pub fn add (mut self, menu: Menu) -> Self { + self.menus.push(menu); + self + } +} +pub struct Menu> { + pub title: String, pub items: Vec>, pub index: usize, } -impl, C: Command> Menu { - pub const fn item (command: C, name: &'static str, key: &'static str) -> MenuItem { - MenuItem::Command(command, name, key) +impl> Menu { + pub fn new (title: impl AsRef) -> Self { + Self { + title: title.as_ref().to_string(), + items: vec![], + index: 0 + } + } + pub fn add (mut self, item: MenuItem) -> Self { + self.items.push(item); + self + } + pub fn sep (mut self) -> Self { + self.items.push(MenuItem::sep()); + self + } + pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self { + self.items.push(MenuItem::cmd(hotkey, text, command)); + self + } + pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self { + self.items.push(MenuItem::off(hotkey, text)); + self } } -pub enum MenuItem, C: Command> { +pub enum MenuItem> { /// Unused. __(PhantomData, PhantomData), /// A separator. Skip it. Separator, /// A menu item with command, description and hotkey. - Command(C, &'static str, &'static str) + Command(&'static str, &'static str, C), + /// A menu item that can't be activated but has description and hotkey + Disabled(&'static str, &'static str) } -impl, C: Command> MenuItem { +impl> MenuItem { pub fn sep () -> Self { Self::Separator } - pub fn cmd (command: C, text: &'static str, hotkey: &'static str) -> Self { - Self::Command(command, text, hotkey) + pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self { + Self::Command(hotkey, text, command) + } + pub fn off (hotkey: &'static str, text: &'static str) -> Self { + Self::Disabled(hotkey, text) } } diff --git a/crates/tek_sequencer/src/arranger.rs b/crates/tek_sequencer/src/arranger.rs index 1ac1041a..38ca36a1 100644 --- a/crates/tek_sequencer/src/arranger.rs +++ b/crates/tek_sequencer/src/arranger.rs @@ -24,6 +24,8 @@ pub struct Arranger { pub phrases_split: u16, /// Width and height of app at last render pub size: Measure, + /// Menu bar + pub menu: MenuBar, } /// Sections in the arranger app that may be focused #[derive(Copy, Clone, PartialEq, Eq)] @@ -141,6 +143,59 @@ impl Arranger { } else { Arc::new(TransportTime::default()) }, + menu: { + use ArrangerCommand::*; + MenuBar::new() + .add({ + use ArrangementCommand::*; + Menu::new("File") + .cmd("n", "New project", Arrangement(New)) + .cmd("l", "Load project", Arrangement(Load)) + .cmd("s", "Save project", Arrangement(Save)) + }) + .add({ + use TransportCommand::*; + Menu::new("Transport") + .cmd("p", "Play", Transport(Play)) + .cmd("s", "Play from start", Transport(PlayFromStart)) + .cmd("a", "Pause", Transport(Pause)) + }) + .add({ + use ArrangementCommand::*; + Menu::new("Track") + .cmd("a", "Append new", Arrangement(AddTrack)) + .cmd("i", "Insert new", Arrangement(AddTrack)) + .cmd("n", "Rename", Arrangement(AddTrack)) + .cmd("d", "Delete", Arrangement(AddTrack)) + .cmd(">", "Move up", Arrangement(AddTrack)) + .cmd("<", "Move down", Arrangement(AddTrack)) + }) + .add({ + use ArrangementCommand::*; + Menu::new("Scene") + .cmd("a", "Append new", Arrangement(AddScene)) + .cmd("i", "Insert new", Arrangement(AddTrack)) + .cmd("n", "Rename", Arrangement(AddTrack)) + .cmd("d", "Delete", Arrangement(AddTrack)) + .cmd(">", "Move up", Arrangement(AddTrack)) + .cmd("<", "Move down", Arrangement(AddTrack)) + }) + .add({ + use PhrasePoolCommand::*; + use PhraseRenameCommand as Rename; + use PhraseLengthCommand as Length; + Menu::new("Phrase") + .cmd("a", "Append new", Phrases(Append)) + .cmd("i", "Insert new", Phrases(Insert)) + .cmd("n", "Rename", Phrases(Rename(Rename::Begin))) + .cmd("t", "Set length", Phrases(Length(Length::Begin))) + .cmd("d", "Delete", Phrases(Delete)) + .cmd("l", "Load from MIDI...", Phrases(Import)) + .cmd("s", "Save to MIDI...", Phrases(Export)) + .cmd(">", "Move up", Phrases(MoveUp)) + .cmd("<", "Move down", Phrases(MoveDown)) + }) + } }; app.update_focus(); app diff --git a/crates/tek_sequencer/src/arranger_cmd.rs b/crates/tek_sequencer/src/arranger_cmd.rs index bd28d4f5..ee3d9f03 100644 --- a/crates/tek_sequencer/src/arranger_cmd.rs +++ b/crates/tek_sequencer/src/arranger_cmd.rs @@ -15,6 +15,9 @@ pub enum ArrangerCommand { } #[derive(Clone, PartialEq)] pub enum ArrangementCommand { + New, + Load, + Save, ToggleViewMode, Delete, Activate, @@ -67,6 +70,9 @@ impl Command> for ArrangementCommand { fn run (&self, state: &mut Arrangement) -> Perhaps { use ArrangementCommand::*; match self { + New => todo!(), + Load => todo!(), + Save => todo!(), ToggleViewMode => { state.mode.to_next(); }, Delete => { state.delete(); }, Activate => { state.activate(); }, diff --git a/crates/tek_sequencer/src/arranger_tui.rs b/crates/tek_sequencer/src/arranger_tui.rs index ffa6377a..934cf7fb 100644 --- a/crates/tek_sequencer/src/arranger_tui.rs +++ b/crates/tek_sequencer/src/arranger_tui.rs @@ -533,7 +533,7 @@ impl MatchInput> for ArrangerCommand { key!(KeyCode::Down) => Some(Self::FocusDown), key!(KeyCode::Left) => Some(Self::FocusLeft), key!(KeyCode::Right) => Some(Self::FocusRight), - key!(KeyCode::Char(' ')) => Some(Self::Transport(TransportCommand::TogglePlay)), + key!(KeyCode::Char(' ')) => Some(Self::Transport(TransportCommand::PlayToggle)), _ => match state.focused() { ArrangerFocus::Transport => state.transport.as_ref() .map(|t|TransportCommand::match_input(&*t.read().unwrap(), input) diff --git a/crates/tek_sequencer/src/sequencer_cmd.rs b/crates/tek_sequencer/src/sequencer_cmd.rs index a6b97b90..f0296454 100644 --- a/crates/tek_sequencer/src/sequencer_cmd.rs +++ b/crates/tek_sequencer/src/sequencer_cmd.rs @@ -24,6 +24,8 @@ pub enum PhrasePoolCommand { Duplicate, RandomColor, Edit, + Import, + Export, Rename(PhraseRenameCommand), Length(PhraseLengthCommand), } @@ -91,89 +93,65 @@ impl Command> for SequencerCommand { } impl Command> for PhrasePoolCommand { fn run (&self, state: &mut PhrasePool) -> Perhaps { + use PhrasePoolCommand::*; + use PhraseRenameCommand as Rename; + use PhraseLengthCommand as Length; match self { - Self::Prev => { - state.select_prev() - }, - Self::Next => { - state.select_next() - }, - Self::Delete => { - state.delete_selected() - }, - Self::Append => { - state.append_new(None, None) - }, - Self::Insert => { - state.insert_new(None, None) - }, - Self::Duplicate => { - state.insert_dup() - }, - Self::Edit => { - todo!(); - } - Self::RandomColor => { - state.randomize_color() - }, - Self::MoveUp => { - state.move_up() - }, - Self::MoveDown => { - state.move_down() - }, - Self::Rename(PhraseRenameCommand::Begin) => { - state.begin_rename() - }, - Self::Rename(_) => { - unreachable!() - }, - Self::Length(PhraseLengthCommand::Begin) => { - state.begin_length() - }, - Self::Length(_) => { - unreachable!() - }, + Prev => { state.select_prev() }, + Next => { state.select_next() }, + Delete => { state.delete_selected() }, + Append => { state.append_new(None, None) }, + Insert => { state.insert_new(None, None) }, + Duplicate => { state.insert_dup() }, + Edit => { todo!(); } + RandomColor => { state.randomize_color() }, + MoveUp => { state.move_up() }, + MoveDown => { state.move_down() }, + Rename(Rename::Begin) => { state.begin_rename() }, + Rename(_) => { unreachable!() }, + Length(Length::Begin) => { state.begin_length() }, + Length(_) => { unreachable!() }, + Import => todo!(), + Export => todo!(), } Ok(None) } } impl Command> for PhraseRenameCommand { fn run (&self, state: &mut PhrasePool) -> Perhaps { + use PhraseRenameCommand::*; if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = state.mode { match self { - Self::Begin => { - unreachable!(); - }, - Self::Backspace => { + Begin => { unreachable!(); }, + Backspace => { let mut phrase = state.phrases[phrase].write().unwrap(); let old_name = phrase.name.clone(); phrase.name.pop(); return Ok(Some(Self::Set(old_name))) }, - Self::Append(c) => { + Append(c) => { let mut phrase = state.phrases[phrase].write().unwrap(); let old_name = phrase.name.clone(); phrase.name.push(*c); return Ok(Some(Self::Set(old_name))) }, - Self::Set(s) => { + Set(s) => { let mut phrase = state.phrases[phrase].write().unwrap(); phrase.name = s.into(); return Ok(Some(Self::Set(old_name.clone()))) }, - Self::Confirm => { + Confirm => { let old_name = old_name.clone(); state.mode = None; return Ok(Some(Self::Set(old_name))) }, - Self::Cancel => { + Cancel => { let mut phrase = state.phrases[phrase].write().unwrap(); phrase.name = old_name.clone(); } }; Ok(None) - } else if *self == Self::Begin { + } else if *self == Begin { todo!() } else { unreachable!() @@ -182,46 +160,31 @@ impl Command> for PhraseRenameCommand { } impl Command> for PhraseLengthCommand { fn run (&self, state: &mut PhrasePool) -> Perhaps { + use PhraseLengthCommand::*; if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = state.mode { match self { - Self::Begin => { - unreachable!(); + Begin => { unreachable!(); }, + Cancel => { state.mode = None; }, + Confirm => { return Self::Set(*length).run(state) }, + Prev => { focus.prev() }, + Next => { focus.next() }, + Inc => { + use PhraseLengthFocus::*; + match focus { + Bar => { *length += 4 * PPQ }, + Beat => { *length += PPQ }, + Tick => { *length += 1 }, + } }, - Self::Prev => { - focus.prev() + Dec => { + use PhraseLengthFocus::*; + match focus { + Bar => { *length = length.saturating_sub(4 * PPQ) }, + Beat => { *length = length.saturating_sub(PPQ) }, + Tick => { *length = length.saturating_sub(1) }, + } }, - Self::Next => { - focus.next() - }, - Self::Inc => match focus { - PhraseLengthFocus::Bar => { - *length += 4 * PPQ - }, - PhraseLengthFocus::Beat => { - *length += PPQ - }, - PhraseLengthFocus::Tick => { - *length += 1 - }, - }, - Self::Dec => match focus { - PhraseLengthFocus::Bar => { - *length = length.saturating_sub(4 * PPQ) - }, - PhraseLengthFocus::Beat => { - *length = length.saturating_sub(PPQ) - }, - PhraseLengthFocus::Tick => { - *length = length.saturating_sub(1) - }, - }, - Self::Cancel => { - state.mode = None; - }, - Self::Confirm => { - return Self::Set(*length).run(state) - }, - Self::Set(length) => { + Set(length) => { let mut phrase = state.phrases[phrase].write().unwrap(); let old_length = phrase.length; phrase.length = *length; @@ -230,7 +193,7 @@ impl Command> for PhraseLengthCommand { }, } Ok(None) - } else if *self == Self::Begin { + } else if *self == Begin { todo!() } else { unreachable!() @@ -239,54 +202,35 @@ impl Command> for PhraseLengthCommand { } impl Command> for PhraseEditorCommand { fn run (&self, state: &mut PhraseEditor) -> Perhaps { + use PhraseEditorCommand::*; match self { - Self::ToggleDirection => { - state.mode = !state.mode; - }, - Self::EnterEditMode => { - state.entered = true; - }, - Self::ExitEditMode => { - state.entered = false; - }, - Self::TimeZoomOut => { - state.time_zoom_out() - }, - Self::TimeZoomIn => { - state.time_zoom_in() - }, - Self::NoteLengthDec => { - state.note_length_dec() - }, - Self::NoteLengthInc => { - state.note_length_inc() - }, - Self::NotePageUp => { - state.note_page_up() - }, - Self::NotePageDown => { - state.note_page_down() - }, - Self::NoteAppend => if state.entered { + ToggleDirection => { state.mode = !state.mode; }, + EnterEditMode => { state.entered = true; }, + ExitEditMode => { state.entered = false; }, + TimeZoomOut => { state.time_zoom_out() }, + TimeZoomIn => { state.time_zoom_in() }, + NoteLengthDec => { state.note_length_dec() }, + NoteLengthInc => { state.note_length_inc() }, + NotePageUp => { state.note_page_up() }, + NotePageDown => { state.note_page_down() }, + NoteAppend => if state.entered { state.put(); state.time_cursor_advance(); }, - Self::NoteSet => if state.entered { - state.put(); - }, - Self::GoUp => match state.entered { + NoteSet => if state.entered { state.put(); }, + GoUp => match state.entered { true => state.note_cursor_inc(), false => state.note_scroll_inc(), }, - Self::GoDown => match state.entered { + GoDown => match state.entered { true => state.note_cursor_dec(), false => state.note_scroll_dec(), }, - Self::GoLeft => match state.entered { + GoLeft => match state.entered { true => state.time_cursor_dec(), false => state.time_scroll_dec(), }, - Self::GoRight => match state.entered { + GoRight => match state.entered { true => state.time_cursor_inc(), false => state.time_scroll_inc(), }, diff --git a/crates/tek_sequencer/src/sequencer_tui.rs b/crates/tek_sequencer/src/sequencer_tui.rs index aeb8a917..bb7aa9d5 100644 --- a/crates/tek_sequencer/src/sequencer_tui.rs +++ b/crates/tek_sequencer/src/sequencer_tui.rs @@ -30,7 +30,7 @@ impl MatchInput> for SequencerCommand { key!(KeyCode::Down) => Some(Self::FocusDown), key!(KeyCode::Left) => Some(Self::FocusLeft), key!(KeyCode::Right) => Some(Self::FocusRight), - key!(KeyCode::Char(' ')) => Some(Self::Transport(TransportCommand::TogglePlay)), + key!(KeyCode::Char(' ')) => Some(Self::Transport(TransportCommand::PlayToggle)), _ => match state.focused() { SequencerFocus::Transport => state.transport.as_ref() .map(|t|TransportCommand::match_input(&*t.read().unwrap(), input) diff --git a/crates/tek_sequencer/src/transport_cmd.rs b/crates/tek_sequencer/src/transport_cmd.rs index b1169151..d77c8092 100644 --- a/crates/tek_sequencer/src/transport_cmd.rs +++ b/crates/tek_sequencer/src/transport_cmd.rs @@ -3,8 +3,10 @@ use crate::*; pub enum TransportCommand { FocusNext, FocusPrev, + Play, + Pause, + PlayToggle, PlayFromStart, - TogglePlay, Increment, Decrement, FineIncrement, @@ -25,7 +27,7 @@ impl Command> for TransportCommand { Self::FocusPrev => { state.focus.prev(); }, - Self::TogglePlay => { + Self::PlayToggle => { state.toggle_play()?; }, Self::Increment => {