From 6c266fcfcace951c76648495db8fcd0433319ea2 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 2 Jan 2025 21:03:20 +0100 Subject: [PATCH] new key binding macro --- engine/src/tui/tui_input.rs | 110 +++++++++++++----------------- src/arranger.rs | 2 +- src/arranger/arranger_command.rs | 10 +-- src/clock/clock_tui.rs | 8 +-- src/command.rs | 74 +++++++++++++++------ src/event.rs | 1 - src/file.rs | 7 +- src/groovebox.rs | 81 ++++++++++------------ src/lib.rs | 4 +- src/midi/midi_editor.rs | 73 ++++++++++---------- src/pool.rs | 6 +- src/pool/clip_length.rs | 4 +- src/pool/clip_rename.rs | 6 +- src/sampler/sample_import.rs | 2 +- src/sampler/sampler_tui.rs | 6 +- src/sequencer.rs | 111 ++++++++++++++++--------------- 16 files changed, 254 insertions(+), 251 deletions(-) delete mode 100644 src/event.rs diff --git a/engine/src/tui/tui_input.rs b/engine/src/tui/tui_input.rs index e1e06b88..4b029293 100644 --- a/engine/src/tui/tui_input.rs +++ b/engine/src/tui/tui_input.rs @@ -1,14 +1,15 @@ use crate::{*, tui::*}; +pub use crossterm::event::Event; #[derive(Debug, Clone)] pub struct TuiIn { pub(crate) exited: Arc, - pub(crate) event: crossterm::event::Event, + pub(crate) event: Event, } impl Input for TuiIn { - type Event = crossterm::event::Event; - fn event (&self) -> &crossterm::event::Event { + type Event = Event; + fn event (&self) -> &Event { &self.event } fn is_done (&self) -> bool { @@ -19,6 +20,47 @@ impl Input for TuiIn { } } +/// Define a key +pub const fn key (code: KeyCode) -> Event { + let modifiers = KeyModifiers::NONE; + let kind = KeyEventKind::Press; + let state = KeyEventState::NONE; + Event::Key(KeyEvent { code, modifiers, kind, state }) +} + +/// Add Ctrl modifier to key +pub const fn ctrl (event: Event) -> Event { + match event { + Event::Key(mut event) => { + event.modifiers = event.modifiers.union(KeyModifiers::CONTROL) + }, + _ => {} + } + event +} + +/// Add Alt modifier to key +pub const fn alt (event: Event) -> Event { + match event { + Event::Key(mut event) => { + event.modifiers = event.modifiers.union(KeyModifiers::ALT) + }, + _ => {} + } + event +} + +/// Add Shift modifier to key +pub const fn shift (event: Event) -> Event { + match event { + Event::Key(mut event) => { + event.modifiers = event.modifiers.union(KeyModifiers::SHIFT) + }, + _ => {} + } + event +} + #[macro_export] macro_rules! key_pat { (Ctrl-Alt-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) }; (Ctrl-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL) }; @@ -86,65 +128,3 @@ impl Input for TuiIn { }) }; } - -/// Define a key -pub const fn key (code: KeyCode) -> KeyEvent { - let modifiers = KeyModifiers::NONE; - let kind = KeyEventKind::Press; - let state = KeyEventState::NONE; - KeyEvent { code, modifiers, kind, state } -} - -/// Add Ctrl modifier to key -pub const fn ctrl (key: KeyEvent) -> KeyEvent { - KeyEvent { modifiers: key.modifiers.union(KeyModifiers::CONTROL), ..key } -} - -/// Add Alt modifier to key -pub const fn alt (key: KeyEvent) -> KeyEvent { - KeyEvent { modifiers: key.modifiers.union(KeyModifiers::ALT), ..key } -} - -/// Add Shift modifier to key -pub const fn shift (key: KeyEvent) -> KeyEvent { - KeyEvent { modifiers: key.modifiers.union(KeyModifiers::SHIFT), ..key } -} - -/* -/// Define a keymap -#[macro_export] macro_rules! keymap { - ($T:ty { $([$k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr]),* $(,)? }) => { - &[ - $((KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as KeyHandler<$T>)),* - ] as &'static [KeyBinding<$T>] - } -} - -*/ - -/* - -impl TuiIn { - // TODO remove - pub fn handle_keymap (&self, state: &mut T, keymap: &KeyMap) -> Usually { - match self.event() { - TuiEvent::Input(Key(event)) => { - for (code, modifiers, _, _, command) in keymap.iter() { - if *code == event.code && modifiers.bits() == event.modifiers.bits() { - return command(state) - } - } - }, - _ => {} - }; - Ok(false) - } -} - -pub type KeyHandler = &'static dyn Fn(&mut T)->Usually; - -pub type KeyBinding = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler); - -pub type KeyMap = [KeyBinding]; - -*/ diff --git a/src/arranger.rs b/src/arranger.rs index 0debca70..481e9d83 100644 --- a/src/arranger.rs +++ b/src/arranger.rs @@ -164,4 +164,4 @@ audio!(|self: ArrangerTui, client, scope|{ has_clock!(|self: ArrangerTui|&self.clock); has_phrases!(|self: ArrangerTui|self.pool.phrases); has_editor!(|self: ArrangerTui|self.editor); -handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); +handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input.event())); diff --git a/src/arranger/arranger_command.rs b/src/arranger/arranger_command.rs index 6d76447a..4cb2a2fa 100644 --- a/src/arranger/arranger_command.rs +++ b/src/arranger/arranger_command.rs @@ -45,7 +45,7 @@ pub enum ArrangerClipCommand { SetColor(usize, usize, ItemPalette), } -input_to_command!(ArrangerCommand: |state: ArrangerTui, input|match input.event() { +input_to_command!(ArrangerCommand: |state: ArrangerTui, input: Event|match input { key_pat!(Char('u')) => Self::History(-1), key_pat!(Char('U')) => Self::History(1), // TODO: k: toggle on-screen keyboard @@ -74,7 +74,7 @@ input_to_command!(ArrangerCommand: |state: ArrangerTui, input|match input.e let t_len = state.tracks.len(); let s_len = state.scenes.len(); match state.selected() { - Selected::Clip(t, s) => match input.event() { + Selected::Clip(t, s) => match input { key_pat!(Char('g')) => Some(Cmd::Phrases(PoolCommand::Select(0))), key_pat!(Char('q')) => Some(Cmd::Clip(Clip::Enqueue(t, s))), key_pat!(Char(',')) => Some(Cmd::Clip(Clip::Put(t, s, None))), @@ -96,7 +96,7 @@ input_to_command!(ArrangerCommand: |state: ArrangerTui, input|match input.e _ => None }, - Selected::Scene(s) => match input.event() { + Selected::Scene(s) => match input { key_pat!(Char(',')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))), key_pat!(Char('.')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))), key_pat!(Char('<')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))), @@ -116,7 +116,7 @@ input_to_command!(ArrangerCommand: |state: ArrangerTui, input|match input.e _ => None }, - Selected::Track(t) => match input.event() { + Selected::Track(t) => match input { key_pat!(Char(',')) => Some(Cmd::Track(Track::Swap(t, t - 1))), key_pat!(Char('.')) => Some(Cmd::Track(Track::Swap(t, t + 1))), key_pat!(Char('<')) => Some(Cmd::Track(Track::Swap(t, t - 1))), @@ -135,7 +135,7 @@ input_to_command!(ArrangerCommand: |state: ArrangerTui, input|match input.e _ => None }, - Selected::Mix => match input.event() { + Selected::Mix => match input { key_pat!(Delete) => Some(Cmd::Clear), key_pat!(Char('0')) => Some(Cmd::StopAll), key_pat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())), diff --git a/src/clock/clock_tui.rs b/src/clock/clock_tui.rs index 120e2d43..3bde3068 100644 --- a/src/clock/clock_tui.rs +++ b/src/clock/clock_tui.rs @@ -10,7 +10,7 @@ pub struct TransportTui { } has_clock!(|self: TransportTui|&self.clock); audio!(|self: TransportTui, client, scope|ClockAudio(self).process(client, scope)); -handle!(|self: TransportTui, from|TransportCommand::execute_with_state(self, from)); +handle!(|self: TransportTui, input|TransportCommand::execute_with_state(self, input.event())); render!(Tui: (self: TransportTui) => TransportView { compact: false, clock: &self.clock @@ -115,10 +115,10 @@ command!(|self:TransportCommand,state:TransportTui|match self { Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock), _ => unreachable!(), }); -impl InputToCommand for TransportCommand { - fn input_to_command (state: &TransportTui, input: &TuiIn) -> Option { +impl InputToCommand for TransportCommand { + fn input_to_command (state: &TransportTui, input: &Event) -> Option { use TransportCommand::*; - Some(match input.event() { + Some(match input { key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { Play(None) } else { diff --git a/src/command.rs b/src/command.rs index fc4dcc32..742ec027 100644 --- a/src/command.rs +++ b/src/command.rs @@ -18,18 +18,18 @@ pub trait Command: Send + Sync + Sized { } #[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 { + ($Command:ty: |$state:ident:$State:ty, $input:ident:$Input:ty| $handler:expr) => { + impl InputToCommand<$Input, $State> for $Command { + fn input_to_command ($state: &$State, $input: &$Input) -> Option { Some($handler) } } } } -pub trait InputToCommand: Command + Sized { - fn input_to_command (state: &S, input: &E::Input) -> Option; - fn execute_with_state (state: &mut S, input: &E::Input) -> Perhaps { +pub trait InputToCommand: Command + Sized { + fn input_to_command (state: &S, input: &I) -> Option; + fn execute_with_state (state: &mut S, input: &I) -> Perhaps { Ok(if let Some(command) = Self::input_to_command(state, input) { let _undo = command.execute(state)?; Some(true) @@ -41,35 +41,67 @@ pub trait InputToCommand: Command + Sized { pub type KeyMapping = [(E, &'static dyn Fn(&T)->U);N]; -pub struct EventMap<'a, const N: usize, E, T, U>( - pub [(E, &'a dyn Fn(T) -> U); N], - pub Option<&'a dyn Fn(T) -> U>, -); +pub struct EventMap<'a, S, I: PartialEq, C> { + pub bindings: &'a [(I, &'a dyn Fn(&S) -> Option)], + pub fallback: Option<&'a dyn Fn(&S, &I) -> Option> +} -impl<'a, const N: usize, E: PartialEq, T, U> EventMap<'a, N, E, T, U> { - pub fn handle (&self, context: T, event: &E) -> Option { - for (binding, handler) in self.0.iter() { - if event == binding { - return Some(handler(context)) +impl<'a, S, I: PartialEq, C> EventMap<'a, S, I, C> { + pub fn handle (&self, state: &S, input: &I) -> Option { + for (binding, handler) in self.bindings.iter() { + if input == binding { + return handler(state) } } - return None + if let Some(fallback) = self.fallback { + fallback(state, input) + } else { + None + } } } #[macro_export] macro_rules! event_map { ($events:expr) => { - EventMap($events, None) + EventMap { bindings: $events, fallback: None } }; ($events:expr, $default: expr) => { - EventMap($events, $default) + EventMap { bindings: $events, fallback: Some($default) } }; } #[macro_export] macro_rules! event_map_input_to_command { - ($Engine:ty: $Model:ty: $Command:ty: $EventMap:expr) => { - input_to_command!($Command: <$Engine>|state: $Model, input|{ - event_map!($EventMap).handle(state, input.event())? + ($Input:ty: $Model:ty: $Command:ty: $EventMap:expr) => { + input_to_command!($Command: |state: $Model, input: $Input|{ + event_map!($EventMap).handle(state, input)? }); } } + +#[macro_export] macro_rules! keymap { + ( + $KEYS:ident: |$state:ident: $State:ty, $input:ident: $Input:ty| $Command:ty + { $($key:expr => $handler:expr),* $(,)? } $(,)? + ) => { + pub const $KEYS: EventMap<'static, $State, $Input, $Command> = EventMap { + fallback: None, + bindings: &[ $(($key, &|$state|Some($handler)),)* ] + }; + input_to_command!($Command: |state: $State, input: $Input|{ + $KEYS.handle(state, input)? + }); + }; + ( + $KEYS:ident: |$state:ident: $State:ty, $input:ident: $Input:ty| $Command:ty + { $($key:expr => $handler:expr),* $(,)? }, + $default:expr + ) => { + pub const $KEYS: EventMap<'static, $State, $Input, $Command> = EventMap { + fallback: Some(&|$state, $input|$default), + bindings: &[ $(($key, &|$state|Some($handler)),)* ] + }; + input_to_command!($Command: |state: $State, input: $Input|{ + $KEYS.handle(state, input)? + }); + }; +} diff --git a/src/event.rs b/src/event.rs deleted file mode 100644 index c7b7e813..00000000 --- a/src/event.rs +++ /dev/null @@ -1 +0,0 @@ -use crate::*; diff --git a/src/file.rs b/src/file.rs index d4bd8b14..8d826eeb 100644 --- a/src/file.rs +++ b/src/file.rs @@ -114,12 +114,11 @@ command!(|self: FileBrowserCommand, state: PoolModel|{ }; None }); -input_to_command!(FileBrowserCommand:|state: PoolModel,from|{ - +input_to_command!(FileBrowserCommand: |state: PoolModel, input: Event|{ use FileBrowserCommand::*; use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char}; if let Some(PoolMode::Import(_index, browser)) = &state.mode { - match from.event() { + match input { key_pat!(Up) => Select(browser.index.overflowing_sub(1).0 .min(browser.len().saturating_sub(1))), key_pat!(Down) => Select(browser.index.saturating_add(1) @@ -133,7 +132,7 @@ input_to_command!(FileBrowserCommand:|state: PoolModel,from|{ _ => return None } } else if let Some(PoolMode::Export(_index, browser)) = &state.mode { - match from.event() { + match input { key_pat!(Up) => Select(browser.index.overflowing_sub(1).0 .min(browser.len())), key_pat!(Down) => Select(browser.index.saturating_add(1) diff --git a/src/groovebox.rs b/src/groovebox.rs index f66baa21..5d1ee3b0 100644 --- a/src/groovebox.rs +++ b/src/groovebox.rs @@ -144,35 +144,28 @@ pub enum GrooveboxCommand { Sampler(SamplerCommand), } -handle!(|self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input)); - -input_to_command!(GrooveboxCommand: |state: Groovebox, input|match input.event() { +handle!(|self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input.event())); +keymap!(KEYS_GROOVEBOX: |state: Groovebox, input: Event| GrooveboxCommand { + // Tab: Toggle compact mode + key(Tab) => Cmd::Compact(!state.compact), + // q: Enqueue currently edited phrase + key(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())), + // 0: Enqueue phrase 0 (stop all) + key(Char('0')) => Cmd::Enqueue(Some(state.pool.phrases()[0].clone())), // TODO: k: toggle on-screen keyboard - key_pat!(Ctrl-Char('k')) => { - todo!("keyboard") - }, - + ctrl(key(Char('k'))) => todo!("keyboard"), // Transport: Play from start or rewind to start - key_pat!(Char(' ')) => Cmd::Clock( + ctrl(key(Char(' '))) => Cmd::Clock( if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } ), - - // Tab: Toggle visibility of sidebars - key_pat!(Tab) => Cmd::Compact(!state.compact), - - // q: Enqueue currently edited phrase - key_pat!(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())), - // 0: Enqueue phrase 0 (stop all) - key_pat!(Char('0')) => Cmd::Enqueue(Some(state.pool.phrases()[0].clone())), - - key_pat!(Shift-Char('R')) => Cmd::Sampler(if state.sampler.recording.is_some() { + // Shift-R: toggle recording + shift(key(Char('R'))) => Cmd::Sampler(if state.sampler.recording.is_some() { SamplerCommand::RecordFinish } else { SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8)) }), - // e: Toggle between editing currently playing or other phrase - key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() { + shift(key(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.pool.phrase().clone(); Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { @@ -183,39 +176,33 @@ input_to_command!(GrooveboxCommand: |state: Groovebox, input|match input.ev } else { return None }, - - // For the rest, use the default keybindings of the components. - // The ones defined above supersede them. - _ => if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { - Cmd::Editor(command) - } else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { - Cmd::Pool(command) - } else { - return None - } -}); +}, Some(if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { + Cmd::Editor(command) +} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { + Cmd::Pool(command) +} else { + return None +})); command!(|self: GrooveboxCommand, state: Groovebox|match self { Self::Enqueue(phrase) => { state.player.enqueue_next(phrase.as_ref()); None }, - Self::Pool(cmd) => { - match cmd { - // autoselect: automatically load selected phrase in editor - PoolCommand::Select(_) => { - let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - state.editor.set_phrase(Some(state.pool.phrase())); - undo - }, - // update color in all places simultaneously - PoolCommand::Phrase(SetColor(index, _)) => { - let undo = cmd.delegate(&mut state.pool, Self::Pool)?; - state.editor.set_phrase(Some(state.pool.phrase())); - undo - }, - _ => cmd.delegate(&mut state.pool, Self::Pool)? - } + Self::Pool(cmd) => match cmd { + // autoselect: automatically load selected phrase in editor + PoolCommand::Select(_) => { + let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + state.editor.set_phrase(Some(state.pool.phrase())); + undo + }, + // update color in all places simultaneously + PoolCommand::Phrase(SetColor(index, _)) => { + let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + state.editor.set_phrase(Some(state.pool.phrase())); + undo + }, + _ => cmd.delegate(&mut state.pool, Self::Pool)? }, Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?, Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, diff --git a/src/lib.rs b/src/lib.rs index 536a212e..a94809fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,9 @@ pub(crate) use ::tek_layout::{ Input, Handle, handle, kexp, key_pat, key_event_pat, key_event_expr, tui::{ - Tui, TuiIn, TuiOut, + Tui, + TuiIn, key, ctrl, shift, alt, + TuiOut, crossterm::{ self, event::{ diff --git a/src/midi/midi_editor.rs b/src/midi/midi_editor.rs index 84bc0662..0ee905f7 100644 --- a/src/midi/midi_editor.rs +++ b/src/midi/midi_editor.rs @@ -147,44 +147,45 @@ pub enum MidiEditCommand { Show(Option>>), } -event_map_input_to_command!(Tui: MidiEditor: MidiEditCommand: MidiEditor::KEYS); +handle!(|self: MidiEditor, input|MidiEditCommand::execute_with_state(self, input.event())); + +keymap!(KEYS_MIDI_EDITOR: |s: MidiEditor, _input: Event| MidiEditCommand { + key(Up) => SetNoteCursor(s.note_point() + 1), + key(Char('w')) => SetNoteCursor(s.note_point() + 1), + key(Down) => SetNoteCursor(s.note_point().saturating_sub(1)), + key(Char('s')) => SetNoteCursor(s.note_point().saturating_sub(1)), + key(Left) => SetTimeCursor(s.time_point().saturating_sub(s.note_len())), + key(Char('a')) => SetTimeCursor(s.time_point().saturating_sub(s.note_len())), + key(Right) => SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length()), + ctrl(alt(key(Up))) => SetNoteScroll(s.note_point() + 3), + ctrl(alt(key(Down))) => SetNoteScroll(s.note_point().saturating_sub(3)), + ctrl(alt(key(Left))) => SetTimeScroll(s.time_point().saturating_sub(s.time_zoom().get())), + ctrl(alt(key(Right))) => SetTimeScroll((s.time_point() + s.time_zoom().get()) % s.phrase_length()), + ctrl(key(Up)) => SetNoteScroll(s.note_lo().get() + 1), + ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(1)), + ctrl(key(Left)) => SetTimeScroll(s.time_start().get().saturating_sub(s.note_len())), + ctrl(key(Right)) => SetTimeScroll(s.time_start().get() + s.note_len()), + alt(key(Up)) => SetNoteCursor(s.note_point() + 3), + alt(key(Down)) => SetNoteCursor(s.note_point().saturating_sub(3)), + alt(key(Left)) => SetTimeCursor(s.time_point().saturating_sub(s.time_zoom().get())), + alt(key(Right)) => SetTimeCursor((s.time_point() + s.time_zoom().get()) % s.phrase_length()), + key(Char('d')) => SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length()), + key(Char('z')) => SetTimeLock(!s.time_lock().get()), + key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::next(s.time_zoom().get()) }), + key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::next(s.time_zoom().get()) }), + key(Char('=')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::prev(s.time_zoom().get()) }), + key(Char('+')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::prev(s.time_zoom().get()) }), + key(Enter) => PutNote, + ctrl(key(Enter)) => AppendNote, + key(Char(',')) => SetNoteLength(Note::prev(s.note_len())), + key(Char('.')) => SetNoteLength(Note::next(s.note_len())), + key(Char('<')) => SetNoteLength(Note::prev(s.note_len())), + key(Char('>')) => SetNoteLength(Note::next(s.note_len())), + //// TODO: key_pat!(Char('/')) => // toggle 3plet + //// TODO: key_pat!(Char('?')) => // toggle dotted +}); impl MidiEditor { - const KEYS: KeyMapping<31, Event, Self, MidiEditCommand> = [ - (kexp!(Ctrl-Alt-Up), &|s: &Self|SetNoteScroll(s.note_point() + 3)), - (kexp!(Ctrl-Alt-Down), &|s: &Self|SetNoteScroll(s.note_point().saturating_sub(3))), - (kexp!(Ctrl-Alt-Left), &|s: &Self|SetTimeScroll(s.time_point().saturating_sub(s.time_zoom().get()))), - (kexp!(Ctrl-Alt-Right), &|s: &Self|SetTimeScroll((s.time_point() + s.time_zoom().get()) % s.phrase_length())), - (kexp!(Ctrl-Up), &|s: &Self|SetNoteScroll(s.note_lo().get() + 1)), - (kexp!(Ctrl-Down), &|s: &Self|SetNoteScroll(s.note_lo().get().saturating_sub(1))), - (kexp!(Ctrl-Left), &|s: &Self|SetTimeScroll(s.time_start().get().saturating_sub(s.note_len()))), - (kexp!(Ctrl-Right), &|s: &Self|SetTimeScroll(s.time_start().get() + s.note_len())), - (kexp!(Alt-Up), &|s: &Self|SetNoteCursor(s.note_point() + 3)), - (kexp!(Alt-Down), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(3))), - (kexp!(Alt-Left), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.time_zoom().get()))), - (kexp!(Alt-Right), &|s: &Self|SetTimeCursor((s.time_point() + s.time_zoom().get()) % s.phrase_length())), - (kexp!(Up), &|s: &Self|SetNoteCursor(s.note_point() + 1)), - (kexp!(Char('w')), &|s: &Self|SetNoteCursor(s.note_point() + 1)), - (kexp!(Down), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(1))), - (kexp!(Char('s')), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(1))), - (kexp!(Left), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.note_len()))), - (kexp!(Char('a')), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.note_len()))), - (kexp!(Right), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())), - (kexp!(Char('d')), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())), - (kexp!(Char('z')), &|s: &Self|SetTimeLock(!s.time_lock().get())), - (kexp!(Char('-')), &|s: &Self|SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::next(s.time_zoom().get()) })), - (kexp!(Char('_')), &|s: &Self|SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::next(s.time_zoom().get()) })), - (kexp!(Char('=')), &|s: &Self|SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::prev(s.time_zoom().get()) })), - (kexp!(Char('+')), &|s: &Self|SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { Note::prev(s.time_zoom().get()) })), - (kexp!(Enter), &|s: &Self|PutNote), - (kexp!(Ctrl-Enter), &|s: &Self|AppendNote), - (kexp!(Char(',')), &|s: &Self|SetNoteLength(Note::prev(s.note_len()))), // TODO: no 3plet - (kexp!(Char('.')), &|s: &Self|SetNoteLength(Note::next(s.note_len()))), - (kexp!(Char('<')), &|s: &Self|SetNoteLength(Note::prev(s.note_len()))), // TODO: 3plet - (kexp!(Char('>')), &|s: &Self|SetNoteLength(Note::next(s.note_len()))), - //// TODO: key_pat!(Char('/')) => // toggle 3plet - //// TODO: key_pat!(Char('?')) => // toggle dotted - ]; fn phrase_length (&self) -> usize { self.phrase().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) } diff --git a/src/pool.rs b/src/pool.rs index 4c862e23..08f1f528 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -103,7 +103,7 @@ command!(|self:PoolCommand, state: PoolModel|{ } }); -input_to_command!(PoolCommand:|state: PoolModel,input|match state.phrases_mode() { +input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.phrases_mode() { Some(PoolMode::Rename(..)) => Self::Rename(PhraseRenameCommand::input_to_command(state, input)?), Some(PoolMode::Length(..)) => Self::Length(PhraseLengthCommand::input_to_command(state, input)?), Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), @@ -111,12 +111,12 @@ input_to_command!(PoolCommand:|state: PoolModel,input|match state.phrases_m _ => to_phrases_command(state, input)? }); -fn to_phrases_command (state: &PoolModel, input: &TuiIn) -> Option { +fn to_phrases_command (state: &PoolModel, input: &Event) -> Option { use KeyCode::{Up, Down, Delete, Char}; use PoolCommand as Cmd; let index = state.phrase_index(); let count = state.phrases().len(); - Some(match input.event() { + Some(match input { key_pat!(Char('n')) => Cmd::Rename(PhraseRenameCommand::Begin), key_pat!(Char('t')) => Cmd::Length(PhraseLengthCommand::Begin), key_pat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin), diff --git a/src/pool/clip_length.rs b/src/pool/clip_length.rs index 076c9d62..b278d6a0 100644 --- a/src/pool/clip_length.rs +++ b/src/pool/clip_length.rs @@ -127,9 +127,9 @@ command!(|self:PhraseLengthCommand,state:PoolModel|{ None }); -input_to_command!(PhraseLengthCommand:|state:PoolModel,from|{ +input_to_command!(PhraseLengthCommand: |state: PoolModel, input: Event|{ if let Some(PoolMode::Length(_, length, _)) = state.phrases_mode() { - match from.event() { + match input { key_pat!(Up) => Self::Inc, key_pat!(Down) => Self::Dec, key_pat!(Right) => Self::Next, diff --git a/src/pool/clip_rename.rs b/src/pool/clip_rename.rs index 313b4ce0..d3cb8138 100644 --- a/src/pool/clip_rename.rs +++ b/src/pool/clip_rename.rs @@ -34,11 +34,11 @@ impl Command for PhraseRenameCommand { } } -impl InputToCommand for PhraseRenameCommand { - fn input_to_command (state: &PoolModel, from: &TuiIn) -> Option { +impl InputToCommand for PhraseRenameCommand { + fn input_to_command (state: &PoolModel, input: &Event) -> Option { use KeyCode::{Char, Backspace, Enter, Esc}; if let Some(PoolMode::Rename(_, ref old_name)) = state.phrases_mode() { - Some(match from.event() { + Some(match input { key_pat!(Char(c)) => { let mut new_name = old_name.clone(); new_name.push(*c); diff --git a/src/sampler/sample_import.rs b/src/sampler/sample_import.rs index 045825a5..adef3b31 100644 --- a/src/sampler/sample_import.rs +++ b/src/sampler/sample_import.rs @@ -1,7 +1,7 @@ use crate::*; use super::*; -input_to_command!(FileBrowserCommand:|state:SamplerTui,input|match input { +input_to_command!(FileBrowserCommand: |state:SamplerTui, input: Event|match input { _ => return None }); diff --git a/src/sampler/sampler_tui.rs b/src/sampler/sampler_tui.rs index 07e49f28..dc12b399 100644 --- a/src/sampler/sampler_tui.rs +++ b/src/sampler/sampler_tui.rs @@ -103,7 +103,7 @@ pub enum SamplerMode { Import(usize, FileBrowser), } -handle!(|self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input)); +handle!(|self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input.event())); pub enum SamplerTuiCommand { Import(FileBrowserCommand), @@ -112,11 +112,11 @@ pub enum SamplerTuiCommand { Sample(SamplerCommand), } -input_to_command!(SamplerTuiCommand: |state: SamplerTui, input|match state.mode { +input_to_command!(SamplerTuiCommand: |state: SamplerTui, input: Event| match state.mode { Some(SamplerMode::Import(..)) => Self::Import( FileBrowserCommand::input_to_command(state, input)? ), - _ => match input.event() { + _ => match input { // load sample key_pat!(Shift-Char('L')) => { Self::Import(FileBrowserCommand::Begin) diff --git a/src/sequencer.rs b/src/sequencer.rs index e5aca001..498e0185 100644 --- a/src/sequencer.rs +++ b/src/sequencer.rs @@ -14,6 +14,8 @@ pub struct SequencerTui { pub transport: bool, pub selectors: bool, + pub compact: bool, + pub clock: Clock, pub size: Measure, pub status: bool, @@ -34,6 +36,7 @@ from_jack!(|jack|SequencerTui { editor: MidiEditor::from(&phrase), player: MidiPlayer::from((&clock, &phrase)), + compact: true, transport: true, selectors: true, size: Measure::new(), @@ -45,33 +48,36 @@ from_jack!(|jack|SequencerTui { } }); render!(Tui: (self: SequencerTui) => { - let w = self.size.w(); - let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let pool_w = if self.pool.visible { phrase_w } else { 0 }; - let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool)))); - let with_pool = move|x|Bsp::w(Fixed::x(pool_w, pool), x); - let status = SequencerStatus::from(self); - let with_status = |x|Bsp::n(Fixed::x(if self.status { 2 } else { 0 }, status), x); - let with_editbar = |x|Bsp::n(Fixed::x(1, MidiEditStatus(&self.editor)), x); - let with_size = |x|lay!(self.size.clone(), x); - let editor = with_editbar(with_pool(Fill::xy(&self.editor))); - + let w = + self.size.w(); + let phrase_w = + if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; let color = self.player.play_phrase().as_ref().map(|(_,p)| p.as_ref().map(|p|p.read().unwrap().color) ).flatten().clone(); - - let toolbar = Tui::when(self.transport, TransportView::new(true, &self.clock)); - - let play_queue = Tui::when(self.selectors, row!( - ClipSelected::play_phrase(&self.player), - ClipSelected::next_phrase(&self.player), - )); - - Min::y(15, with_size(with_status(col!( + let toolbar = Tui::when(self.transport, + TransportView::new(true, &self.clock)); + let selectors = Tui::when(self.selectors, + Bsp::e(ClipSelected::play_phrase(&self.player), ClipSelected::next_phrase(&self.player))); + let pool_w = + if self.pool.visible { phrase_w } else { 0 }; + let pool = + Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool)))); + let edit_clip = + MidiEditClip(&self.editor); + self.size.of(Bsp::s( toolbar, - play_queue, - editor, - )))) + Bsp::s( + lay!(Align::w(edit_clip), Align::e(selectors)), + Bsp::n( + Align::x(Fixed::y(1, MidiEditStatus(&self.editor))), + Bsp::w( + Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool)))), + Fill::xy(&self.editor), + ), + ) + ) + )) }); audio!(|self:SequencerTui, client, scope|{ // Start profiling cycle @@ -94,15 +100,16 @@ has_size!(|self:SequencerTui|&self.size); has_clock!(|self:SequencerTui|&self.clock); has_phrases!(|self:SequencerTui|self.pool.phrases); has_editor!(|self:SequencerTui|self.editor); -handle!(|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input)); +handle!(|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input.event())); #[derive(Clone, Debug)] pub enum SequencerCommand { + Compact(bool), History(isize), Clock(ClockCommand), Pool(PoolCommand), Editor(MidiEditCommand), Enqueue(Option>>), } -input_to_command!(SequencerCommand: |state: SequencerTui, input|match input.event() { +input_to_command!(SequencerCommand: |state: SequencerTui, input: Event|match input { // TODO: k: toggle on-screen keyboard key_pat!(Ctrl-Char('k')) => { todo!("keyboard") }, // Transport: Play/pause @@ -117,8 +124,8 @@ input_to_command!(SequencerCommand: |state: SequencerTui, input|match input key_pat!(Char('u')) => Cmd::History(-1), // Shift-U: redo key_pat!(Char('U')) => Cmd::History( 1), - // Tab: Toggle visibility of phrase pool column - key_pat!(Tab) => Cmd::Pool(PoolCommand::Show(!state.pool.visible)), + // Tab: Toggle compact mode + key_pat!(Tab) => Cmd::Compact(!state.compact), // q: Enqueue currently edited phrase key_pat!(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())), // 0: Enqueue phrase 0 (stop all) @@ -146,38 +153,34 @@ input_to_command!(SequencerCommand: |state: SequencerTui, input|match input } }); command!(|self: SequencerCommand, state: SequencerTui|match self { - Self::Pool(cmd) => { - let mut default = |cmd: PoolCommand|cmd - .execute(&mut state.pool) - .map(|x|x.map(Cmd::Pool)); - match cmd { - // autoselect: automatically load selected phrase in editor - PoolCommand::Select(_) => { - let undo = default(cmd)?; - state.editor.set_phrase(Some(state.pool.phrase())); - undo - }, - // update color in all places simultaneously - PoolCommand::Phrase(SetColor(index, _)) => { - let undo = default(cmd)?; - state.editor.set_phrase(Some(state.pool.phrase())); - undo - }, - _ => default(cmd)? - } - }, - Self::Editor(cmd) => { - let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Cmd::Editor)); - match cmd { - _ => default()? - } - }, - Self::Clock(cmd) => cmd.execute(state)?.map(Cmd::Clock), Self::Enqueue(phrase) => { state.player.enqueue_next(phrase.as_ref()); None }, + Self::Pool(cmd) => match cmd { + // autoselect: automatically load selected phrase in editor + PoolCommand::Select(_) => { + let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + state.editor.set_phrase(Some(state.pool.phrase())); + undo + }, + // update color in all places simultaneously + PoolCommand::Phrase(SetColor(index, _)) => { + let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + state.editor.set_phrase(Some(state.pool.phrase())); + undo + }, + _ => cmd.delegate(&mut state.pool, Self::Pool)? + }, + Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, + Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, Self::History(delta) => { todo!("undo/redo") }, + Self::Compact(compact) => if state.compact != compact { + state.compact = compact; + Some(Self::Compact(!compact)) + } else { + None + }, });