diff --git a/crates/tek/src/core/command.rs b/crates/tek/src/core/command.rs index 480a42e8..92afc4d1 100644 --- a/crates/tek/src/core/command.rs +++ b/crates/tek/src/core/command.rs @@ -1,5 +1,25 @@ use crate::*; +#[macro_export] macro_rules! command { + (|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => { + impl Command<$State> for $Command { + fn execute ($self, $state: &mut $State) -> Perhaps { + Ok($handler) + } + } + } +} + +#[macro_export] macro_rules! input_to_command { + ($Command:ty: <$Engine:ty>|$state:ident:$State:ty,$input:ident|$handler:expr) => { + impl InputToCommand<$Engine, $State> for $Command { + fn input_to_command ($state: &$State, $input: &<$Engine as Engine>::Input) -> Option { + $handler + } + } + } +} + #[derive(Clone)] pub enum NextPrev { Next, @@ -65,7 +85,7 @@ impl> Menu { self } pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self { - self.items.push(MenuItem::cmd(hotkey, text, command)); + self.items.push(MenuItem::cmd(hotkey, text, command)); self } pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self { diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index 7b44bfc5..21dd5376 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -20,6 +20,13 @@ pub struct GrooveboxTui { pub split: u16, } +pub enum GrooveboxCommand { + Sequencer(SequencerCommand), + Sampler(SamplerCommand), +} + render!(|self:GrooveboxTui|Bsp::s(Tui::fixed_y(self.split, &self.sequencer), &self.sampler)); audio!(|self:GrooveboxTui,_client,_process|Control::Continue); -handle!(|self:GrooveboxTui,input|Ok(None)); +handle!(|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input)); +command!(|self:GrooveboxCommand,state:GrooveboxTui|todo!()); +input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|todo!()); diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index cfc9e445..d4ace6a1 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -8,6 +8,9 @@ use symphonia::core::probe::Hint; use symphonia::core::audio::SampleBuffer; use symphonia::default::get_codecs; +pub enum SamplerCommand { +} + impl TryFrom<&Arc>> for SamplerTui { type Error = Box; fn try_from (jack: &Arc>) -> Usually { @@ -65,54 +68,55 @@ render!(|self: SamplerTui|{ })), col!([ - "", - row!([ - " ", Tui::bold(true, "Sampler."), " Voices: ", + Tui::push_x(2, row!([ + Tui::bold(true, "Sampler"), "|Voices: ", &format!("{}", self.state.voices.read().unwrap().len()), - ]), - " ", + ])), Tui::either(self.state.unmapped.len() > 0, col!((index, sample) in self.state.unmapped.iter().enumerate() => - { &format!("| Unmapped #{index}") }), " · No unmapped samples."), - " ", + { &format!("| Unmapped #{index}") }), "· No unmapped samples."), Tui::either(self.state.mapped.len() > 0, col!((index, sample) in self.state.unmapped.iter().enumerate() => - { &format!("| Mapped #{index}") }), " · No mapped samples."), + { &format!("| Mapped #{index}") }), "· No mapped samples."), ]) - - //render(|to|{ - //let [x, y, w, h] = to.area(); - //let style = Style::default().gray(); - //let voice = self.state.voices.read().unwrap().len(); - //let title = format!(" {} ({voice} voice(s) playing now)", self.state.name); - //to.blit(&title, x+1, y, Some(style.white().bold().not_dim())); - - //let mut width = title.len() + 2; - //let mut y1 = 1; - //let mut j = 0; - //for (note, sample) in self.state.mapped.iter() - //.map(|(note, sample)|(Some(note), sample)) - //.chain(self.state.unmapped.iter().map(|sample|(None, sample))) - //{ - //if y1 >= h { - //break - //} - //let active = j == self.cursor.0; - //width = width.max( - //draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)? - //); - //y1 = y1 + 1; - //j = j + 1; - //} - //let h = ((2 + y1) as u16).min(h); - ////Ok(Some([x, y, (width as u16).min(to.area().w()), h])) - //Ok(()) - //}), - //]) - ])) }); +handle!(|self:SamplerTui,from|{ + let cursor = &mut self.cursor; + let unmapped = &mut self.state.unmapped; + let mapped = &self.state.mapped; + let voices = &self.state.voices; + match from.event() { + key_pat!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 { + mapped.len() + unmapped.len() - 1 + } else { + cursor.0 - 1 + }, + key_pat!(KeyCode::Down) => { + cursor.0 = (cursor.0 + 1) % (mapped.len() + unmapped.len()); + }, + key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { + voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); + }, + key_pat!(KeyCode::Char('a')) => { + let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + unmapped.push(sample); + }, + key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { + *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + }, + key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { + self.editing = Some(sample.clone()); + }, + _ => { + return Ok(None) + } + } + Ok(Some(true)) +}); + impl SamplerTui { /// Immutable reference to sample at cursor. pub fn sample (&self) -> Option<&Arc>> { @@ -256,41 +260,6 @@ fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { todo!(); } -handle!(|self:SamplerTui,from|{ - let cursor = &mut self.cursor; - let unmapped = &mut self.state.unmapped; - let mapped = &self.state.mapped; - let voices = &self.state.voices; - match from.event() { - key_pat!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 { - mapped.len() + unmapped.len() - 1 - } else { - cursor.0 - 1 - }, - key_pat!(KeyCode::Down) => { - cursor.0 = (cursor.0 + 1) % (mapped.len() + unmapped.len()); - }, - key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { - voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); - }, - key_pat!(KeyCode::Char('a')) => { - let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); - unmapped.push(sample); - }, - key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); - }, - key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { - self.editing = Some(sample.clone()); - }, - _ => { - return Ok(None) - } - } - Ok(Some(true)) -}); - fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { let (mut subdirs, mut files) = std::fs::read_dir(dir)? .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 029d79fa..631f56c5 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -54,100 +54,88 @@ pub enum SequencerCommand { ShowPool(bool), } -impl Command for SequencerCommand { - fn execute (self, state: &mut SequencerTui) -> Perhaps { - Ok(match self { - Self::Phrases(cmd) => { - let mut default = |cmd: PhrasesCommand|cmd.execute(&mut state.phrases).map(|x|x.map(Phrases)); - match cmd { - // autoselect: automatically load selected phrase in editor - PhrasesCommand::Select(_) => { - let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); - undo - }, - // update color in all places simultaneously - PhrasesCommand::Phrase(SetColor(index, _)) => { - let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); - undo - }, - _ => default(cmd)? - } - }, - Self::Editor(cmd) => { - let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Editor)); - match cmd { - _ => default()? - } - }, - Self::Clock(cmd) => cmd.execute(state)?.map(Clock), - Self::Enqueue(phrase) => { - state.player.enqueue_next(phrase.as_ref()); - None - }, - Self::ShowPool(value) => { - state.show_pool = value; - None - } - }) +handle!(|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input)); +input_to_command!(SequencerCommand: |state:SequencerTui,input|Some(match input.event() { + // Transport: Play/pause + key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { + Play(None) } else { Pause(None) }), + // Transport: Play from start or rewind to start + key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { + Play(Some(0)) } else { Pause(Some(0)) }), + // TODO: u: undo + key_pat!(Char('u')) => { todo!("undo") }, + // TODO: Shift-U: redo + key_pat!(Char('U')) => { todo!("redo") }, + // TODO: k: toggle on-screen keyboard + key_pat!(Ctrl-Char('k')) => { todo!("keyboard") }, + // Tab: Toggle visibility of phrase pool column + key_pat!(Tab) => ShowPool(!state.show_pool), + // q: Enqueue currently edited phrase + key_pat!(Char('q')) => Enqueue(Some(state.phrases.phrase().clone())), + // 0: Enqueue phrase 0 (stop all) + key_pat!(Char('0')) => Enqueue(Some(state.phrases.phrases()[0].clone())), + // e: Toggle between editing currently playing or other phrase + key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() { + let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone()); + let selected = state.phrases.phrase().clone(); + Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { + selected + } else { + playing.clone() + }))) + } else { + return None + }, + // For the rest, use the default keybindings of the components. + // The ones defined above supersede them. + _ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { + Editor(command) + } else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) { + Phrases(command) + } else { + return None } -} - -impl InputToCommand for SequencerCommand { - fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { - Some(match input.event() { - // TODO: u: undo - key_pat!(Char('u')) => { todo!("undo") }, - // TODO: Shift-U: redo - key_pat!(Char('U')) => { todo!("redo") }, - // TODO: k: toggle on-screen keyboard - key_pat!(Ctrl-Char('k')) => { todo!("keyboard") }, - // Tab: Toggle visibility of phrase pool column - key_pat!(Tab) => ShowPool(!state.show_pool), - // q: Enqueue currently edited phrase - key_pat!(Char('q')) => Enqueue(Some(state.phrases.phrase().clone())), - // 0: Enqueue phrase 0 (stop all) - key_pat!(Char('0')) => Enqueue(Some(state.phrases.phrases()[0].clone())), - // e: Toggle between editing currently playing or other phrase - key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() { - let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone()); - let selected = state.phrases.phrase().clone(); - Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { - selected - } else { - playing.clone() - }))) - } else { - return None +})); +command!(|self: SequencerCommand, state: SequencerTui|match self { + Self::Phrases(cmd) => { + let mut default = |cmd: PhrasesCommand|cmd.execute(&mut state.phrases).map(|x|x.map(Phrases)); + match cmd { + // autoselect: automatically load selected phrase in editor + PhrasesCommand::Select(_) => { + let undo = default(cmd)?; + state.editor.set_phrase(Some(state.phrases.phrase())); + undo }, - - // Transport: Play/pause - key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { - Play(None) } else { Pause(None) }), - - // Transport: Play from start or rewind to start - key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { - Play(Some(0)) } else { Pause(Some(0)) }), - - // For the rest, use the default keybindings of the components. - // The ones defined above supersede them. - _ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { - Editor(command) - } else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) { - Phrases(command) - } else { - return None - } - }) + // update color in all places simultaneously + PhrasesCommand::Phrase(SetColor(index, _)) => { + let undo = default(cmd)?; + state.editor.set_phrase(Some(state.phrases.phrase())); + undo + }, + _ => default(cmd)? + } + }, + Self::Editor(cmd) => { + let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Editor)); + match cmd { + _ => default()? + } + }, + Self::Clock(cmd) => cmd.execute(state)?.map(Clock), + Self::Enqueue(phrase) => { + state.player.enqueue_next(phrase.as_ref()); + None + }, + Self::ShowPool(value) => { + state.show_pool = value; + None } -} +}); has_size!(|self:SequencerTui|&self.size); has_clock!(|self:SequencerTui|&self.clock); has_phrases!(|self:SequencerTui|self.phrases.phrases); has_editor!(|self:SequencerTui|self.editor); -handle!(|self:SequencerTui,i|SequencerCommand::execute_with_state(self, i)); render!(|self: SequencerTui|{ let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };