new key binding macro

This commit is contained in:
🪞👃🪞 2025-01-02 21:03:20 +01:00
parent 5bc19a45d2
commit 6c266fcfca
16 changed files with 254 additions and 251 deletions

View file

@ -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!(<Tui>|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input));
handle!(<Tui>|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input.event()));

View file

@ -45,7 +45,7 @@ pub enum ArrangerClipCommand {
SetColor(usize, usize, ItemPalette),
}
input_to_command!(ArrangerCommand: <Tui>|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: <Tui>|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: <Tui>|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: <Tui>|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: <Tui>|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())),

View file

@ -10,7 +10,7 @@ pub struct TransportTui {
}
has_clock!(|self: TransportTui|&self.clock);
audio!(|self: TransportTui, client, scope|ClockAudio(self).process(client, scope));
handle!(<Tui>|self: TransportTui, from|TransportCommand::execute_with_state(self, from));
handle!(<Tui>|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<Tui, TransportTui> for TransportCommand {
fn input_to_command (state: &TransportTui, input: &TuiIn) -> Option<Self> {
impl InputToCommand<Event, TransportTui> for TransportCommand {
fn input_to_command (state: &TransportTui, input: &Event) -> Option<Self> {
use TransportCommand::*;
Some(match input.event() {
Some(match input {
key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() {
Play(None)
} else {

View file

@ -18,18 +18,18 @@ pub trait Command<S>: 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<Self> {
($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<Self> {
Some($handler)
}
}
}
}
pub trait InputToCommand<E: Engine, S>: Command<S> + Sized {
fn input_to_command (state: &S, input: &E::Input) -> Option<Self>;
fn execute_with_state (state: &mut S, input: &E::Input) -> Perhaps<bool> {
pub trait InputToCommand<I, S>: Command<S> + Sized {
fn input_to_command (state: &S, input: &I) -> Option<Self>;
fn execute_with_state (state: &mut S, input: &I) -> Perhaps<bool> {
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<E: Engine, S>: Command<S> + Sized {
pub type KeyMapping<const N: usize, E, T, U> = [(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<C>)],
pub fallback: Option<&'a dyn Fn(&S, &I) -> Option<C>>
}
impl<'a, const N: usize, E: PartialEq, T, U> EventMap<'a, N, E, T, U> {
pub fn handle (&self, context: T, event: &E) -> Option<U> {
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<C> {
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)?
});
};
}

View file

@ -1 +0,0 @@
use crate::*;

View file

@ -114,12 +114,11 @@ command!(|self: FileBrowserCommand, state: PoolModel|{
};
None
});
input_to_command!(FileBrowserCommand:<Tui>|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:<Tui>|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)

View file

@ -144,35 +144,28 @@ pub enum GrooveboxCommand {
Sampler(SamplerCommand),
}
handle!(<Tui>|self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input));
input_to_command!(GrooveboxCommand: <Tui>|state: Groovebox, input|match input.event() {
handle!(<Tui>|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: <Tui>|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)?,

View file

@ -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::{

View file

@ -147,44 +147,45 @@ pub enum MidiEditCommand {
Show(Option<Arc<RwLock<MidiClip>>>),
}
event_map_input_to_command!(Tui: MidiEditor: MidiEditCommand: MidiEditor::KEYS);
handle!(<Tui>|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)
}

View file

@ -103,7 +103,7 @@ command!(|self:PoolCommand, state: PoolModel|{
}
});
input_to_command!(PoolCommand:<Tui>|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:<Tui>|state: PoolModel,input|match state.phrases_m
_ => to_phrases_command(state, input)?
});
fn to_phrases_command (state: &PoolModel, input: &TuiIn) -> Option<PoolCommand> {
fn to_phrases_command (state: &PoolModel, input: &Event) -> Option<PoolCommand> {
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),

View file

@ -127,9 +127,9 @@ command!(|self:PhraseLengthCommand,state:PoolModel|{
None
});
input_to_command!(PhraseLengthCommand:<Tui>|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,

View file

@ -34,11 +34,11 @@ impl Command<PoolModel> for PhraseRenameCommand {
}
}
impl InputToCommand<Tui, PoolModel> for PhraseRenameCommand {
fn input_to_command (state: &PoolModel, from: &TuiIn) -> Option<Self> {
impl InputToCommand<Event, PoolModel> for PhraseRenameCommand {
fn input_to_command (state: &PoolModel, input: &Event) -> Option<Self> {
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);

View file

@ -1,7 +1,7 @@
use crate::*;
use super::*;
input_to_command!(FileBrowserCommand:<Tui>|state:SamplerTui,input|match input {
input_to_command!(FileBrowserCommand: |state:SamplerTui, input: Event|match input {
_ => return None
});

View file

@ -103,7 +103,7 @@ pub enum SamplerMode {
Import(usize, FileBrowser),
}
handle!(<Tui>|self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input));
handle!(<Tui>|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: <Tui>|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)

View file

@ -14,6 +14,8 @@ pub struct SequencerTui {
pub transport: bool,
pub selectors: bool,
pub compact: bool,
pub clock: Clock,
pub size: Measure<Tui>,
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!(<Tui>|self:SequencerTui|&self.size);
has_clock!(|self:SequencerTui|&self.clock);
has_phrases!(|self:SequencerTui|self.pool.phrases);
has_editor!(|self:SequencerTui|self.editor);
handle!(<Tui>|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input));
handle!(<Tui>|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<Arc<RwLock<MidiClip>>>),
}
input_to_command!(SequencerCommand: <Tui>|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: <Tui>|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: <Tui>|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
},
});