add command! and input_to_command! macros

This commit is contained in:
🪞👃🪞 2024-12-17 18:21:30 +01:00
parent 93413ae303
commit fdafd15a01
4 changed files with 146 additions and 162 deletions

View file

@ -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<Self> {
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<Self> {
$handler
}
}
}
}
#[derive(Clone)]
pub enum NextPrev {
Next,
@ -65,7 +85,7 @@ impl<E: Engine, S, C: Command<S>> Menu<E, S, C> {
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 {

View file

@ -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!(<Tui>|self:GrooveboxTui,input|Ok(None));
handle!(<Tui>|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input));
command!(|self:GrooveboxCommand,state:GrooveboxTui|todo!());
input_to_command!(GrooveboxCommand: <Tui>|state:GrooveboxTui,input|todo!());

View file

@ -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<RwLock<JackClient>>> for SamplerTui {
type Error = Box<dyn std::error::Error>;
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
@ -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!(<Tui>|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<RwLock<Sample>>> {
@ -256,41 +260,6 @@ fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
todo!();
}
handle!(<Tui>|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<OsString>, Vec<OsString>)> {
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{

View file

@ -54,100 +54,88 @@ pub enum SequencerCommand {
ShowPool(bool),
}
impl Command<SequencerTui> for SequencerCommand {
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
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!(<Tui>|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input));
input_to_command!(SequencerCommand: <Tui>|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<Tui, SequencerTui> for SequencerCommand {
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
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!(<Tui>|self:SequencerTui|&self.size);
has_clock!(|self:SequencerTui|&self.clock);
has_phrases!(|self:SequencerTui|self.phrases.phrases);
has_editor!(|self:SequencerTui|self.editor);
handle!(<Tui>|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 };