mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
new key binding macro
This commit is contained in:
parent
5bc19a45d2
commit
6c266fcfca
16 changed files with 254 additions and 251 deletions
|
|
@ -1,14 +1,15 @@
|
||||||
use crate::{*, tui::*};
|
use crate::{*, tui::*};
|
||||||
|
pub use crossterm::event::Event;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TuiIn {
|
pub struct TuiIn {
|
||||||
pub(crate) exited: Arc<AtomicBool>,
|
pub(crate) exited: Arc<AtomicBool>,
|
||||||
pub(crate) event: crossterm::event::Event,
|
pub(crate) event: Event,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input<Tui> for TuiIn {
|
impl Input<Tui> for TuiIn {
|
||||||
type Event = crossterm::event::Event;
|
type Event = Event;
|
||||||
fn event (&self) -> &crossterm::event::Event {
|
fn event (&self) -> &Event {
|
||||||
&self.event
|
&self.event
|
||||||
}
|
}
|
||||||
fn is_done (&self) -> bool {
|
fn is_done (&self) -> bool {
|
||||||
|
|
@ -19,6 +20,47 @@ impl Input<Tui> 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 {
|
#[macro_export] macro_rules! key_pat {
|
||||||
(Ctrl-Alt-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) };
|
(Ctrl-Alt-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) };
|
||||||
(Ctrl-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL) };
|
(Ctrl-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL) };
|
||||||
|
|
@ -86,65 +128,3 @@ impl Input<Tui> 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 <T> (&self, state: &mut T, keymap: &KeyMap<T>) -> Usually<bool> {
|
|
||||||
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<T> = &'static dyn Fn(&mut T)->Usually<bool>;
|
|
||||||
|
|
||||||
pub type KeyBinding<T> = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler<T>);
|
|
||||||
|
|
||||||
pub type KeyMap<T> = [KeyBinding<T>];
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
|
||||||
|
|
@ -164,4 +164,4 @@ audio!(|self: ArrangerTui, client, scope|{
|
||||||
has_clock!(|self: ArrangerTui|&self.clock);
|
has_clock!(|self: ArrangerTui|&self.clock);
|
||||||
has_phrases!(|self: ArrangerTui|self.pool.phrases);
|
has_phrases!(|self: ArrangerTui|self.pool.phrases);
|
||||||
has_editor!(|self: ArrangerTui|self.editor);
|
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()));
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ pub enum ArrangerClipCommand {
|
||||||
SetColor(usize, usize, ItemPalette),
|
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),
|
||||||
key_pat!(Char('U')) => Self::History(1),
|
key_pat!(Char('U')) => Self::History(1),
|
||||||
// TODO: k: toggle on-screen keyboard
|
// 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 t_len = state.tracks.len();
|
||||||
let s_len = state.scenes.len();
|
let s_len = state.scenes.len();
|
||||||
match state.selected() {
|
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('g')) => Some(Cmd::Phrases(PoolCommand::Select(0))),
|
||||||
key_pat!(Char('q')) => Some(Cmd::Clip(Clip::Enqueue(t, s))),
|
key_pat!(Char('q')) => Some(Cmd::Clip(Clip::Enqueue(t, s))),
|
||||||
key_pat!(Char(',')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
|
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
|
_ => 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))),
|
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
|
_ => 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))),
|
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
|
_ => None
|
||||||
},
|
},
|
||||||
Selected::Mix => match input.event() {
|
Selected::Mix => match input {
|
||||||
key_pat!(Delete) => Some(Cmd::Clear),
|
key_pat!(Delete) => Some(Cmd::Clear),
|
||||||
key_pat!(Char('0')) => Some(Cmd::StopAll),
|
key_pat!(Char('0')) => Some(Cmd::StopAll),
|
||||||
key_pat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())),
|
key_pat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())),
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ pub struct TransportTui {
|
||||||
}
|
}
|
||||||
has_clock!(|self: TransportTui|&self.clock);
|
has_clock!(|self: TransportTui|&self.clock);
|
||||||
audio!(|self: TransportTui, client, scope|ClockAudio(self).process(client, scope));
|
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 {
|
render!(Tui: (self: TransportTui) => TransportView {
|
||||||
compact: false,
|
compact: false,
|
||||||
clock: &self.clock
|
clock: &self.clock
|
||||||
|
|
@ -115,10 +115,10 @@ command!(|self:TransportCommand,state:TransportTui|match self {
|
||||||
Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock),
|
Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
});
|
});
|
||||||
impl InputToCommand<Tui, TransportTui> for TransportCommand {
|
impl InputToCommand<Event, TransportTui> for TransportCommand {
|
||||||
fn input_to_command (state: &TransportTui, input: &TuiIn) -> Option<Self> {
|
fn input_to_command (state: &TransportTui, input: &Event) -> Option<Self> {
|
||||||
use TransportCommand::*;
|
use TransportCommand::*;
|
||||||
Some(match input.event() {
|
Some(match input {
|
||||||
key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() {
|
key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() {
|
||||||
Play(None)
|
Play(None)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,18 @@ pub trait Command<S>: Send + Sync + Sized {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export] macro_rules! input_to_command {
|
#[macro_export] macro_rules! input_to_command {
|
||||||
($Command:ty: <$Engine:ty>|$state:ident:$State:ty,$input:ident|$handler:expr) => {
|
($Command:ty: |$state:ident:$State:ty, $input:ident:$Input:ty| $handler:expr) => {
|
||||||
impl InputToCommand<$Engine, $State> for $Command {
|
impl InputToCommand<$Input, $State> for $Command {
|
||||||
fn input_to_command ($state: &$State, $input: &<$Engine as Engine>::Input) -> Option<Self> {
|
fn input_to_command ($state: &$State, $input: &$Input) -> Option<Self> {
|
||||||
Some($handler)
|
Some($handler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait InputToCommand<E: Engine, S>: Command<S> + Sized {
|
pub trait InputToCommand<I, S>: Command<S> + Sized {
|
||||||
fn input_to_command (state: &S, input: &E::Input) -> Option<Self>;
|
fn input_to_command (state: &S, input: &I) -> Option<Self>;
|
||||||
fn execute_with_state (state: &mut S, input: &E::Input) -> Perhaps<bool> {
|
fn execute_with_state (state: &mut S, input: &I) -> Perhaps<bool> {
|
||||||
Ok(if let Some(command) = Self::input_to_command(state, input) {
|
Ok(if let Some(command) = Self::input_to_command(state, input) {
|
||||||
let _undo = command.execute(state)?;
|
let _undo = command.execute(state)?;
|
||||||
Some(true)
|
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 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 struct EventMap<'a, S, I: PartialEq, C> {
|
||||||
pub [(E, &'a dyn Fn(T) -> U); N],
|
pub bindings: &'a [(I, &'a dyn Fn(&S) -> Option<C>)],
|
||||||
pub Option<&'a dyn Fn(T) -> U>,
|
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> {
|
impl<'a, S, I: PartialEq, C> EventMap<'a, S, I, C> {
|
||||||
pub fn handle (&self, context: T, event: &E) -> Option<U> {
|
pub fn handle (&self, state: &S, input: &I) -> Option<C> {
|
||||||
for (binding, handler) in self.0.iter() {
|
for (binding, handler) in self.bindings.iter() {
|
||||||
if event == binding {
|
if input == binding {
|
||||||
return Some(handler(context))
|
return handler(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return None
|
if let Some(fallback) = self.fallback {
|
||||||
|
fallback(state, input)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export] macro_rules! event_map {
|
#[macro_export] macro_rules! event_map {
|
||||||
($events:expr) => {
|
($events:expr) => {
|
||||||
EventMap($events, None)
|
EventMap { bindings: $events, fallback: None }
|
||||||
};
|
};
|
||||||
($events:expr, $default: expr) => {
|
($events:expr, $default: expr) => {
|
||||||
EventMap($events, $default)
|
EventMap { bindings: $events, fallback: Some($default) }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export] macro_rules! event_map_input_to_command {
|
#[macro_export] macro_rules! event_map_input_to_command {
|
||||||
($Engine:ty: $Model:ty: $Command:ty: $EventMap:expr) => {
|
($Input:ty: $Model:ty: $Command:ty: $EventMap:expr) => {
|
||||||
input_to_command!($Command: <$Engine>|state: $Model, input|{
|
input_to_command!($Command: |state: $Model, input: $Input|{
|
||||||
event_map!($EventMap).handle(state, input.event())?
|
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)?
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
@ -114,12 +114,11 @@ command!(|self: FileBrowserCommand, state: PoolModel|{
|
||||||
};
|
};
|
||||||
None
|
None
|
||||||
});
|
});
|
||||||
input_to_command!(FileBrowserCommand:<Tui>|state: PoolModel,from|{
|
input_to_command!(FileBrowserCommand: |state: PoolModel, input: Event|{
|
||||||
|
|
||||||
use FileBrowserCommand::*;
|
use FileBrowserCommand::*;
|
||||||
use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char};
|
use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char};
|
||||||
if let Some(PoolMode::Import(_index, browser)) = &state.mode {
|
if let Some(PoolMode::Import(_index, browser)) = &state.mode {
|
||||||
match from.event() {
|
match input {
|
||||||
key_pat!(Up) => Select(browser.index.overflowing_sub(1).0
|
key_pat!(Up) => Select(browser.index.overflowing_sub(1).0
|
||||||
.min(browser.len().saturating_sub(1))),
|
.min(browser.len().saturating_sub(1))),
|
||||||
key_pat!(Down) => Select(browser.index.saturating_add(1)
|
key_pat!(Down) => Select(browser.index.saturating_add(1)
|
||||||
|
|
@ -133,7 +132,7 @@ input_to_command!(FileBrowserCommand:<Tui>|state: PoolModel,from|{
|
||||||
_ => return None
|
_ => return None
|
||||||
}
|
}
|
||||||
} else if let Some(PoolMode::Export(_index, browser)) = &state.mode {
|
} 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
|
key_pat!(Up) => Select(browser.index.overflowing_sub(1).0
|
||||||
.min(browser.len())),
|
.min(browser.len())),
|
||||||
key_pat!(Down) => Select(browser.index.saturating_add(1)
|
key_pat!(Down) => Select(browser.index.saturating_add(1)
|
||||||
|
|
|
||||||
|
|
@ -144,35 +144,28 @@ pub enum GrooveboxCommand {
|
||||||
Sampler(SamplerCommand),
|
Sampler(SamplerCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
handle!(<Tui>|self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input));
|
handle!(<Tui>|self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input.event()));
|
||||||
|
keymap!(KEYS_GROOVEBOX: |state: Groovebox, input: Event| GrooveboxCommand {
|
||||||
input_to_command!(GrooveboxCommand: <Tui>|state: Groovebox, input|match input.event() {
|
// 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
|
// TODO: k: toggle on-screen keyboard
|
||||||
key_pat!(Ctrl-Char('k')) => {
|
ctrl(key(Char('k'))) => todo!("keyboard"),
|
||||||
todo!("keyboard")
|
|
||||||
},
|
|
||||||
|
|
||||||
// Transport: Play from start or rewind to start
|
// 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)) }
|
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
|
||||||
),
|
),
|
||||||
|
// Shift-R: toggle recording
|
||||||
// Tab: Toggle visibility of sidebars
|
shift(key(Char('R'))) => Cmd::Sampler(if state.sampler.recording.is_some() {
|
||||||
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() {
|
|
||||||
SamplerCommand::RecordFinish
|
SamplerCommand::RecordFinish
|
||||||
} else {
|
} else {
|
||||||
SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8))
|
SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8))
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// e: Toggle between editing currently playing or other phrase
|
// 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 editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
|
||||||
let selected = state.pool.phrase().clone();
|
let selected = state.pool.phrase().clone();
|
||||||
Cmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
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 {
|
} else {
|
||||||
return None
|
return None
|
||||||
},
|
},
|
||||||
|
}, Some(if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
|
||||||
// For the rest, use the default keybindings of the components.
|
Cmd::Editor(command)
|
||||||
// The ones defined above supersede them.
|
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
||||||
_ => if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) {
|
Cmd::Pool(command)
|
||||||
Cmd::Editor(command)
|
} else {
|
||||||
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
|
return None
|
||||||
Cmd::Pool(command)
|
}));
|
||||||
} else {
|
|
||||||
return None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
command!(|self: GrooveboxCommand, state: Groovebox|match self {
|
||||||
Self::Enqueue(phrase) => {
|
Self::Enqueue(phrase) => {
|
||||||
state.player.enqueue_next(phrase.as_ref());
|
state.player.enqueue_next(phrase.as_ref());
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
Self::Pool(cmd) => {
|
Self::Pool(cmd) => match cmd {
|
||||||
match cmd {
|
// autoselect: automatically load selected phrase in editor
|
||||||
// autoselect: automatically load selected phrase in editor
|
PoolCommand::Select(_) => {
|
||||||
PoolCommand::Select(_) => {
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
state.editor.set_phrase(Some(state.pool.phrase()));
|
||||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
undo
|
||||||
undo
|
},
|
||||||
},
|
// update color in all places simultaneously
|
||||||
// update color in all places simultaneously
|
PoolCommand::Phrase(SetColor(index, _)) => {
|
||||||
PoolCommand::Phrase(SetColor(index, _)) => {
|
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
||||||
let undo = cmd.delegate(&mut state.pool, Self::Pool)?;
|
state.editor.set_phrase(Some(state.pool.phrase()));
|
||||||
state.editor.set_phrase(Some(state.pool.phrase()));
|
undo
|
||||||
undo
|
},
|
||||||
},
|
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
||||||
_ => cmd.delegate(&mut state.pool, Self::Pool)?
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?,
|
Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?,
|
||||||
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ pub(crate) use ::tek_layout::{
|
||||||
Input, Handle, handle,
|
Input, Handle, handle,
|
||||||
kexp, key_pat, key_event_pat, key_event_expr,
|
kexp, key_pat, key_event_pat, key_event_expr,
|
||||||
tui::{
|
tui::{
|
||||||
Tui, TuiIn, TuiOut,
|
Tui,
|
||||||
|
TuiIn, key, ctrl, shift, alt,
|
||||||
|
TuiOut,
|
||||||
crossterm::{
|
crossterm::{
|
||||||
self,
|
self,
|
||||||
event::{
|
event::{
|
||||||
|
|
|
||||||
|
|
@ -147,44 +147,45 @@ pub enum MidiEditCommand {
|
||||||
Show(Option<Arc<RwLock<MidiClip>>>),
|
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 {
|
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 {
|
fn phrase_length (&self) -> usize {
|
||||||
self.phrase().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
|
self.phrase().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::Rename(..)) => Self::Rename(PhraseRenameCommand::input_to_command(state, input)?),
|
||||||
Some(PoolMode::Length(..)) => Self::Length(PhraseLengthCommand::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)?),
|
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)?
|
_ => 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 KeyCode::{Up, Down, Delete, Char};
|
||||||
use PoolCommand as Cmd;
|
use PoolCommand as Cmd;
|
||||||
let index = state.phrase_index();
|
let index = state.phrase_index();
|
||||||
let count = state.phrases().len();
|
let count = state.phrases().len();
|
||||||
Some(match input.event() {
|
Some(match input {
|
||||||
key_pat!(Char('n')) => Cmd::Rename(PhraseRenameCommand::Begin),
|
key_pat!(Char('n')) => Cmd::Rename(PhraseRenameCommand::Begin),
|
||||||
key_pat!(Char('t')) => Cmd::Length(PhraseLengthCommand::Begin),
|
key_pat!(Char('t')) => Cmd::Length(PhraseLengthCommand::Begin),
|
||||||
key_pat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
|
key_pat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
|
||||||
|
|
|
||||||
|
|
@ -127,9 +127,9 @@ command!(|self:PhraseLengthCommand,state:PoolModel|{
|
||||||
None
|
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() {
|
if let Some(PoolMode::Length(_, length, _)) = state.phrases_mode() {
|
||||||
match from.event() {
|
match input {
|
||||||
key_pat!(Up) => Self::Inc,
|
key_pat!(Up) => Self::Inc,
|
||||||
key_pat!(Down) => Self::Dec,
|
key_pat!(Down) => Self::Dec,
|
||||||
key_pat!(Right) => Self::Next,
|
key_pat!(Right) => Self::Next,
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,11 @@ impl Command<PoolModel> for PhraseRenameCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputToCommand<Tui, PoolModel> for PhraseRenameCommand {
|
impl InputToCommand<Event, PoolModel> for PhraseRenameCommand {
|
||||||
fn input_to_command (state: &PoolModel, from: &TuiIn) -> Option<Self> {
|
fn input_to_command (state: &PoolModel, input: &Event) -> Option<Self> {
|
||||||
use KeyCode::{Char, Backspace, Enter, Esc};
|
use KeyCode::{Char, Backspace, Enter, Esc};
|
||||||
if let Some(PoolMode::Rename(_, ref old_name)) = state.phrases_mode() {
|
if let Some(PoolMode::Rename(_, ref old_name)) = state.phrases_mode() {
|
||||||
Some(match from.event() {
|
Some(match input {
|
||||||
key_pat!(Char(c)) => {
|
key_pat!(Char(c)) => {
|
||||||
let mut new_name = old_name.clone();
|
let mut new_name = old_name.clone();
|
||||||
new_name.push(*c);
|
new_name.push(*c);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
input_to_command!(FileBrowserCommand:<Tui>|state:SamplerTui,input|match input {
|
input_to_command!(FileBrowserCommand: |state:SamplerTui, input: Event|match input {
|
||||||
_ => return None
|
_ => return None
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ pub enum SamplerMode {
|
||||||
Import(usize, FileBrowser),
|
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 {
|
pub enum SamplerTuiCommand {
|
||||||
Import(FileBrowserCommand),
|
Import(FileBrowserCommand),
|
||||||
|
|
@ -112,11 +112,11 @@ pub enum SamplerTuiCommand {
|
||||||
Sample(SamplerCommand),
|
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(
|
Some(SamplerMode::Import(..)) => Self::Import(
|
||||||
FileBrowserCommand::input_to_command(state, input)?
|
FileBrowserCommand::input_to_command(state, input)?
|
||||||
),
|
),
|
||||||
_ => match input.event() {
|
_ => match input {
|
||||||
// load sample
|
// load sample
|
||||||
key_pat!(Shift-Char('L')) => {
|
key_pat!(Shift-Char('L')) => {
|
||||||
Self::Import(FileBrowserCommand::Begin)
|
Self::Import(FileBrowserCommand::Begin)
|
||||||
|
|
|
||||||
111
src/sequencer.rs
111
src/sequencer.rs
|
|
@ -14,6 +14,8 @@ pub struct SequencerTui {
|
||||||
|
|
||||||
pub transport: bool,
|
pub transport: bool,
|
||||||
pub selectors: bool,
|
pub selectors: bool,
|
||||||
|
pub compact: bool,
|
||||||
|
|
||||||
pub clock: Clock,
|
pub clock: Clock,
|
||||||
pub size: Measure<Tui>,
|
pub size: Measure<Tui>,
|
||||||
pub status: bool,
|
pub status: bool,
|
||||||
|
|
@ -34,6 +36,7 @@ from_jack!(|jack|SequencerTui {
|
||||||
editor: MidiEditor::from(&phrase),
|
editor: MidiEditor::from(&phrase),
|
||||||
player: MidiPlayer::from((&clock, &phrase)),
|
player: MidiPlayer::from((&clock, &phrase)),
|
||||||
|
|
||||||
|
compact: true,
|
||||||
transport: true,
|
transport: true,
|
||||||
selectors: true,
|
selectors: true,
|
||||||
size: Measure::new(),
|
size: Measure::new(),
|
||||||
|
|
@ -45,33 +48,36 @@ from_jack!(|jack|SequencerTui {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
render!(Tui: (self: SequencerTui) => {
|
render!(Tui: (self: SequencerTui) => {
|
||||||
let w = self.size.w();
|
let w =
|
||||||
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
self.size.w();
|
||||||
let pool_w = if self.pool.visible { phrase_w } else { 0 };
|
let phrase_w =
|
||||||
let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool))));
|
if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||||
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 color = self.player.play_phrase().as_ref().map(|(_,p)|
|
let color = self.player.play_phrase().as_ref().map(|(_,p)|
|
||||||
p.as_ref().map(|p|p.read().unwrap().color)
|
p.as_ref().map(|p|p.read().unwrap().color)
|
||||||
).flatten().clone();
|
).flatten().clone();
|
||||||
|
let toolbar = Tui::when(self.transport,
|
||||||
let toolbar = Tui::when(self.transport, TransportView::new(true, &self.clock));
|
TransportView::new(true, &self.clock));
|
||||||
|
let selectors = Tui::when(self.selectors,
|
||||||
let play_queue = Tui::when(self.selectors, row!(
|
Bsp::e(ClipSelected::play_phrase(&self.player), ClipSelected::next_phrase(&self.player)));
|
||||||
ClipSelected::play_phrase(&self.player),
|
let pool_w =
|
||||||
ClipSelected::next_phrase(&self.player),
|
if self.pool.visible { phrase_w } else { 0 };
|
||||||
));
|
let pool =
|
||||||
|
Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool))));
|
||||||
Min::y(15, with_size(with_status(col!(
|
let edit_clip =
|
||||||
|
MidiEditClip(&self.editor);
|
||||||
|
self.size.of(Bsp::s(
|
||||||
toolbar,
|
toolbar,
|
||||||
play_queue,
|
Bsp::s(
|
||||||
editor,
|
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|{
|
audio!(|self:SequencerTui, client, scope|{
|
||||||
// Start profiling cycle
|
// Start profiling cycle
|
||||||
|
|
@ -94,15 +100,16 @@ has_size!(<Tui>|self:SequencerTui|&self.size);
|
||||||
has_clock!(|self:SequencerTui|&self.clock);
|
has_clock!(|self:SequencerTui|&self.clock);
|
||||||
has_phrases!(|self:SequencerTui|self.pool.phrases);
|
has_phrases!(|self:SequencerTui|self.pool.phrases);
|
||||||
has_editor!(|self:SequencerTui|self.editor);
|
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 {
|
#[derive(Clone, Debug)] pub enum SequencerCommand {
|
||||||
|
Compact(bool),
|
||||||
History(isize),
|
History(isize),
|
||||||
Clock(ClockCommand),
|
Clock(ClockCommand),
|
||||||
Pool(PoolCommand),
|
Pool(PoolCommand),
|
||||||
Editor(MidiEditCommand),
|
Editor(MidiEditCommand),
|
||||||
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
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
|
// TODO: k: toggle on-screen keyboard
|
||||||
key_pat!(Ctrl-Char('k')) => { todo!("keyboard") },
|
key_pat!(Ctrl-Char('k')) => { todo!("keyboard") },
|
||||||
// Transport: Play/pause
|
// Transport: Play/pause
|
||||||
|
|
@ -117,8 +124,8 @@ input_to_command!(SequencerCommand: <Tui>|state: SequencerTui, input|match input
|
||||||
key_pat!(Char('u')) => Cmd::History(-1),
|
key_pat!(Char('u')) => Cmd::History(-1),
|
||||||
// Shift-U: redo
|
// Shift-U: redo
|
||||||
key_pat!(Char('U')) => Cmd::History( 1),
|
key_pat!(Char('U')) => Cmd::History( 1),
|
||||||
// Tab: Toggle visibility of phrase pool column
|
// Tab: Toggle compact mode
|
||||||
key_pat!(Tab) => Cmd::Pool(PoolCommand::Show(!state.pool.visible)),
|
key_pat!(Tab) => Cmd::Compact(!state.compact),
|
||||||
// q: Enqueue currently edited phrase
|
// q: Enqueue currently edited phrase
|
||||||
key_pat!(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())),
|
key_pat!(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())),
|
||||||
// 0: Enqueue phrase 0 (stop all)
|
// 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 {
|
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) => {
|
Self::Enqueue(phrase) => {
|
||||||
state.player.enqueue_next(phrase.as_ref());
|
state.player.enqueue_next(phrase.as_ref());
|
||||||
None
|
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) => {
|
Self::History(delta) => {
|
||||||
todo!("undo/redo")
|
todo!("undo/redo")
|
||||||
},
|
},
|
||||||
|
Self::Compact(compact) => if state.compact != compact {
|
||||||
|
state.compact = compact;
|
||||||
|
Some(Self::Compact(!compact))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue