mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 12:16:42 +01:00
455 lines
20 KiB
Rust
455 lines
20 KiB
Rust
use crate::*;
|
|
|
|
impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
|
|
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
|
|
to_transport_command(state, input)
|
|
.or_else(||to_focus_command(input).map(TransportCommand::Focus))
|
|
}
|
|
}
|
|
|
|
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
|
|
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
|
|
to_sequencer_command(state, input)
|
|
.or_else(||to_focus_command(input).map(SequencerCommand::Focus))
|
|
}
|
|
}
|
|
|
|
impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
|
|
fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option<Self> {
|
|
to_arranger_command(state, input)
|
|
.or_else(||to_focus_command(input).map(ArrangerCommand::Focus))
|
|
}
|
|
}
|
|
|
|
fn to_focus_command (input: &TuiInput) -> Option<FocusCommand> {
|
|
use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc};
|
|
Some(match input.event() {
|
|
key!(Tab) => FocusCommand::Next,
|
|
key!(Shift-Tab) => FocusCommand::Prev,
|
|
key!(BackTab) => FocusCommand::Prev,
|
|
key!(Shift-BackTab) => FocusCommand::Prev,
|
|
key!(Up) => FocusCommand::Up,
|
|
key!(Down) => FocusCommand::Down,
|
|
key!(Left) => FocusCommand::Left,
|
|
key!(Right) => FocusCommand::Right,
|
|
key!(Enter) => FocusCommand::Enter,
|
|
key!(Esc) => FocusCommand::Exit,
|
|
_ => return None
|
|
})
|
|
}
|
|
|
|
fn to_transport_command <T> (state: &T, input: &TuiInput) -> Option<TransportCommand>
|
|
where
|
|
T: TransportControl
|
|
{
|
|
use ClockCommand::{SetBpm, SetQuant, SetSync};
|
|
use TransportCommand::{Focus, Clock};
|
|
use KeyCode::{Enter, Left, Right, Char};
|
|
Some(match input.event() {
|
|
key!(Left) => Focus(FocusCommand::Prev),
|
|
key!(Right) => Focus(FocusCommand::Next),
|
|
key!(Char(' ')) => todo!("toolbar space"),
|
|
key!(Shift-Char(' ')) => todo!("toolbar shift-space"),
|
|
_ => match state.transport_focused().unwrap() {
|
|
TransportFocus::Bpm => match input.event() {
|
|
key!(Char(',')) => Clock(SetBpm(state.bpm().get() - 1.0)),
|
|
key!(Char('.')) => Clock(SetBpm(state.bpm().get() + 1.0)),
|
|
key!(Char('<')) => Clock(SetBpm(state.bpm().get() - 0.001)),
|
|
key!(Char('>')) => Clock(SetBpm(state.bpm().get() + 0.001)),
|
|
_ => return None,
|
|
},
|
|
TransportFocus::Quant => match input.event() {
|
|
key!(Char(',')) => Clock(SetQuant(state.quant().prev())),
|
|
key!(Char('.')) => Clock(SetQuant(state.quant().next())),
|
|
key!(Char('<')) => Clock(SetQuant(state.quant().prev())),
|
|
key!(Char('>')) => Clock(SetQuant(state.quant().next())),
|
|
_ => return None,
|
|
},
|
|
TransportFocus::Sync => match input.event() {
|
|
key!(Char(',')) => Clock(SetSync(state.sync().prev())),
|
|
key!(Char('.')) => Clock(SetSync(state.sync().next())),
|
|
key!(Char('<')) => Clock(SetSync(state.sync().prev())),
|
|
key!(Char('>')) => Clock(SetSync(state.sync().next())),
|
|
_ => return None,
|
|
},
|
|
TransportFocus::Clock => match input.event() {
|
|
key!(Char(',')) => todo!("transport seek bar"),
|
|
key!(Char('.')) => todo!("transport seek bar"),
|
|
key!(Char('<')) => todo!("transport seek beat"),
|
|
key!(Char('>')) => todo!("transport seek beat"),
|
|
_ => return None,
|
|
},
|
|
TransportFocus::PlayPause => match input.event() {
|
|
key!(Enter) => todo!("transport play toggle"),
|
|
key!(Shift-Enter) => todo!("transport shift-play toggle"),
|
|
_ => return None,
|
|
},
|
|
}
|
|
})
|
|
}
|
|
|
|
fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<SequencerCommand> {
|
|
use SequencerCommand::*;
|
|
use KeyCode::Char;
|
|
if !state.entered() {
|
|
return None
|
|
}
|
|
Some(match input.event() {
|
|
key!(Char('e')) => Editor(
|
|
PhraseCommand::Show(state.phrase_to_edit().clone())
|
|
),
|
|
key!(Char(' ')) => Clock(
|
|
if let Some(TransportState::Stopped) = *state.clock.playing.read().unwrap() {
|
|
ClockCommand::Play(None)
|
|
} else {
|
|
ClockCommand::Pause(None)
|
|
}
|
|
),
|
|
key!(Shift-Char(' ')) => Clock(
|
|
if let Some(TransportState::Stopped) = *state.clock.playing.read().unwrap() {
|
|
ClockCommand::Play(Some(0))
|
|
} else {
|
|
ClockCommand::Pause(Some(0))
|
|
}
|
|
),
|
|
_ => match state.focused() {
|
|
AppFocus::Menu => { todo!() },
|
|
AppFocus::Content(focused) => match focused {
|
|
SequencerFocus::Transport(_) => {
|
|
match TransportCommand::input_to_command(state, input)? {
|
|
TransportCommand::Clock(_) => { todo!() },
|
|
TransportCommand::Focus(command) => Focus(command),
|
|
}
|
|
},
|
|
SequencerFocus::Phrases => Phrases(
|
|
PhrasesCommand::input_to_command(state, input)?
|
|
),
|
|
SequencerFocus::PhraseEditor => Editor(
|
|
PhraseCommand::input_to_command(state, input)?
|
|
),
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
|
|
use ArrangerCommand as Cmd;
|
|
use KeyCode::Char;
|
|
if !state.entered() {
|
|
return None
|
|
}
|
|
Some(match input.event() {
|
|
key!(Char('e')) => Cmd::Editor(PhraseCommand::Show(state.phrase_to_edit().clone())),
|
|
_ => match state.focused() {
|
|
AppFocus::Menu => { todo!() },
|
|
AppFocus::Content(focused) => match focused {
|
|
ArrangerFocus::Transport(_) => {
|
|
use TransportCommand::{Clock, Focus};
|
|
match TransportCommand::input_to_command(state, input)? {
|
|
Clock(_) => { todo!() },
|
|
Focus(command) => Cmd::Focus(command)
|
|
}
|
|
},
|
|
ArrangerFocus::PhraseEditor => {
|
|
Cmd::Editor(PhraseCommand::input_to_command(state, input)?)
|
|
},
|
|
ArrangerFocus::Phrases => {
|
|
Cmd::Phrases(PhrasesCommand::input_to_command(state, input)?)
|
|
},
|
|
ArrangerFocus::Arranger => {
|
|
use ArrangerSelection::*;
|
|
match input.event() {
|
|
key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)),
|
|
key!(Char('+')) => Cmd::Zoom(0), // TODO
|
|
key!(Char('=')) => Cmd::Zoom(0), // TODO
|
|
key!(Char('_')) => Cmd::Zoom(0), // TODO
|
|
key!(Char('-')) => Cmd::Zoom(0), // TODO
|
|
key!(Char('`')) => { todo!("toggle state mode") },
|
|
key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add),
|
|
key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add),
|
|
_ => match state.selected() {
|
|
Mix => to_arranger_mix_command(input)?,
|
|
Track(t) => to_arranger_track_command(input, t)?,
|
|
Scene(s) => to_arranger_scene_command(input, s)?,
|
|
Clip(t, s) => to_arranger_clip_command(input, t, s)?,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
|
|
use KeyCode::{Char, Down, Right, Delete};
|
|
use ArrangerCommand as Cmd;
|
|
use ArrangerSelection as Select;
|
|
Some(match input.event() {
|
|
key!(Down) => Cmd::Select(Select::Scene(0)),
|
|
key!(Right) => Cmd::Select(Select::Track(0)),
|
|
key!(Char(',')) => Cmd::Zoom(0),
|
|
key!(Char('.')) => Cmd::Zoom(0),
|
|
key!(Char('<')) => Cmd::Zoom(0),
|
|
key!(Char('>')) => Cmd::Zoom(0),
|
|
key!(Delete) => Cmd::Clear,
|
|
key!(Char('c')) => Cmd::Color(ItemColor::random()),
|
|
_ => return None
|
|
})
|
|
}
|
|
|
|
fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option<ArrangerCommand> {
|
|
use KeyCode::{Char, Down, Left, Right, Delete};
|
|
use ArrangerCommand as Cmd;
|
|
use ArrangerSelection as Select;
|
|
use ArrangerTrackCommand as Track;
|
|
Some(match input.event() {
|
|
key!(Down) => Cmd::Select(Select::Clip(t, 0)),
|
|
key!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }),
|
|
key!(Right) => Cmd::Select(Select::Track(t + 1)),
|
|
key!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)),
|
|
key!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)),
|
|
key!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)),
|
|
key!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)),
|
|
key!(Delete) => Cmd::Track(Track::Delete(t)),
|
|
//key!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())),
|
|
_ => return None
|
|
})
|
|
}
|
|
|
|
fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<ArrangerCommand> {
|
|
use KeyCode::{Char, Up, Down, Right, Enter, Delete};
|
|
use ArrangerCommand as Cmd;
|
|
use ArrangerSelection as Select;
|
|
use ArrangerSceneCommand as Scene;
|
|
Some(match input.event() {
|
|
key!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }),
|
|
key!(Down) => Cmd::Select(Select::Scene(s + 1)),
|
|
key!(Right) => Cmd::Select(Select::Clip(0, s)),
|
|
key!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
|
key!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
|
key!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)),
|
|
key!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)),
|
|
key!(Enter) => Cmd::Scene(Scene::Play(s)),
|
|
key!(Delete) => Cmd::Scene(Scene::Delete(s)),
|
|
//key!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())),
|
|
_ => return None
|
|
})
|
|
}
|
|
|
|
fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<ArrangerCommand> {
|
|
use KeyCode::{Char, Up, Down, Left, Right, Delete};
|
|
use ArrangerCommand as Cmd;
|
|
use ArrangerSelection as Select;
|
|
use ArrangerClipCommand as Clip;
|
|
Some(match input.event() {
|
|
key!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }),
|
|
key!(Down) => Cmd::Select(Select::Clip(t, s + 1)),
|
|
key!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }),
|
|
key!(Right) => Cmd::Select(Select::Clip(t + 1, s)),
|
|
key!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)),
|
|
key!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)),
|
|
key!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)),
|
|
key!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)),
|
|
key!(Delete) => Cmd::Clip(Clip::Set(t, s, None)),
|
|
//key!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())),
|
|
//key!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))),
|
|
//key!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))),
|
|
_ => return None
|
|
})
|
|
}
|
|
|
|
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhrasesCommand {
|
|
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
|
|
use PhraseRenameCommand as Rename;
|
|
use PhraseLengthCommand as Length;
|
|
use FileBrowserCommand as Browse;
|
|
Some(match state.phrases_mode() {
|
|
Some(PhrasesMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?),
|
|
Some(PhrasesMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?),
|
|
Some(PhrasesMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?),
|
|
Some(PhrasesMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?),
|
|
_ => to_phrases_command(state, input)?
|
|
})
|
|
}
|
|
}
|
|
|
|
fn to_phrases_command <T: PhrasesControl> (state: &T, input: &TuiInput) -> Option<PhrasesCommand> {
|
|
use KeyCode::{Up, Down, Delete, Char};
|
|
use PhrasesCommand as Cmd;
|
|
use PhrasePoolCommand as Pool;
|
|
use PhraseRenameCommand as Rename;
|
|
use PhraseLengthCommand as Length;
|
|
use FileBrowserCommand as Browse;
|
|
let index = state.phrase_index();
|
|
let count = state.phrases().len();
|
|
Some(match input.event() {
|
|
key!(Char('n')) => Cmd::Rename(Rename::Begin),
|
|
key!(Char('t')) => Cmd::Length(Length::Begin),
|
|
key!(Char('m')) => Cmd::Import(Browse::Begin),
|
|
key!(Char('x')) => Cmd::Export(Browse::Begin),
|
|
key!(Up) => Cmd::Select(index.overflowing_sub(1).0.min(state.phrases().len() - 1)),
|
|
key!(Down) => Cmd::Select(index.saturating_add(1) % state.phrases().len()),
|
|
key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
|
|
key!(Char(',')) => if index > 1 {
|
|
state.set_phrase_index(state.phrase_index().saturating_sub(1));
|
|
Cmd::Phrase(Pool::Swap(index - 1, index))
|
|
} else {
|
|
return None
|
|
},
|
|
key!(Char('.')) => if index < count.saturating_sub(1) {
|
|
state.set_phrase_index(state.phrase_index() + 1);
|
|
Cmd::Phrase(Pool::Swap(index + 1, index))
|
|
} else {
|
|
return None
|
|
},
|
|
key!(Delete) => if index > 0 {
|
|
state.set_phrase_index(index.min(count.saturating_sub(1)));
|
|
Cmd::Phrase(Pool::Delete(index))
|
|
} else {
|
|
return None
|
|
},
|
|
key!(Char('a')) => Cmd::Phrase(Pool::Add(
|
|
count, Phrase::new(
|
|
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
|
)
|
|
)),
|
|
key!(Char('i')) => Cmd::Phrase(Pool::Add(
|
|
index + 1, Phrase::new(
|
|
String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random())
|
|
)
|
|
)),
|
|
key!(Char('d')) => {
|
|
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
|
|
phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25);
|
|
Cmd::Phrase(Pool::Add(index + 1, phrase))
|
|
},
|
|
_ => return None
|
|
})
|
|
}
|
|
|
|
impl<T: PhrasesControl> InputToCommand<Tui, T> for FileBrowserCommand {
|
|
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
|
|
use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace};
|
|
use FileBrowserCommand::*;
|
|
if let Some(PhrasesMode::Import(index, browser)) = state.phrases_mode() {
|
|
Some(match from.event() {
|
|
key!(Up) => Select(
|
|
browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1))
|
|
),
|
|
key!(Down) => Select(
|
|
browser.index.saturating_add(1) % browser.len()
|
|
),
|
|
key!(Right) => Chdir(browser.cwd.clone()),
|
|
key!(Left) => Chdir(browser.cwd.clone()),
|
|
key!(Enter) => Confirm,
|
|
key!(Char(c)) => { todo!() },
|
|
key!(Backspace) => { todo!() },
|
|
key!(Esc) => Self::Cancel,
|
|
_ => return None
|
|
})
|
|
} else if let Some(PhrasesMode::Export(index, browser)) = state.phrases_mode() {
|
|
Some(match from.event() {
|
|
key!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())),
|
|
key!(Down) => Select(browser.index.saturating_add(1) % browser.len()),
|
|
key!(Right) => Chdir(browser.cwd.clone()),
|
|
key!(Left) => Chdir(browser.cwd.clone()),
|
|
key!(Enter) => Confirm,
|
|
key!(Char(c)) => { todo!() },
|
|
key!(Backspace) => { todo!() },
|
|
key!(Esc) => Self::Cancel,
|
|
_ => return None
|
|
})
|
|
} else {
|
|
unreachable!()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhraseLengthCommand {
|
|
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
|
|
use KeyCode::{Up, Down, Right, Left, Enter, Esc};
|
|
if let Some(PhrasesMode::Length(_, length, _)) = state.phrases_mode() {
|
|
Some(match from.event() {
|
|
key!(Up) => Self::Inc,
|
|
key!(Down) => Self::Dec,
|
|
key!(Right) => Self::Next,
|
|
key!(Left) => Self::Prev,
|
|
key!(Enter) => Self::Set(*length),
|
|
key!(Esc) => Self::Cancel,
|
|
_ => return None
|
|
})
|
|
} else {
|
|
unreachable!()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: PhrasesControl> InputToCommand<Tui, T> for PhraseRenameCommand {
|
|
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
|
|
use KeyCode::{Char, Backspace, Enter, Esc};
|
|
if let Some(PhrasesMode::Rename(_, ref old_name)) = state.phrases_mode() {
|
|
Some(match from.event() {
|
|
key!(Char(c)) => {
|
|
let mut new_name = old_name.clone();
|
|
new_name.push(*c);
|
|
Self::Set(new_name)
|
|
},
|
|
key!(Backspace) => {
|
|
let mut new_name = old_name.clone();
|
|
new_name.pop();
|
|
Self::Set(new_name)
|
|
},
|
|
key!(Enter) => Self::Confirm,
|
|
key!(Esc) => Self::Cancel,
|
|
_ => return None
|
|
})
|
|
} else {
|
|
unreachable!()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: PhraseEditorControl + HasEnter> InputToCommand<Tui, T> for PhraseCommand {
|
|
fn input_to_command (state: &T, from: &TuiInput) -> Option<Self> {
|
|
use PhraseCommand::*;
|
|
use KeyCode::{Char, Enter, Esc, Up, Down, PageUp, PageDown, Left, Right};
|
|
Some(match from.event() {
|
|
key!(Char('`')) => ToggleDirection,
|
|
key!(Enter) => EnterEditMode,
|
|
key!(Esc) => ExitEditMode,
|
|
key!(Char('a')) => NoteAppend,
|
|
key!(Char('s')) => NoteSet,
|
|
key!(Char('[')) => NoteLengthSet(prev_note_length(state.note_len())),
|
|
key!(Char(']')) => NoteLengthSet(next_note_length(state.note_len())),
|
|
key!(Char('-')) => TimeZoomSet(next_note_length(state.time_axis().read().unwrap().scale)),
|
|
key!(Char('_')) => TimeZoomSet(next_note_length(state.time_axis().read().unwrap().scale)),
|
|
key!(Char('=')) => TimeZoomSet(prev_note_length(state.time_axis().read().unwrap().scale)),
|
|
key!(Char('+')) => TimeZoomSet(prev_note_length(state.time_axis().read().unwrap().scale)),
|
|
key!(Up) => match state.phrase_editor_entered() {
|
|
true => NoteCursorSet(state.note_axis().write().unwrap().point_plus(1)),
|
|
false => NoteScrollSet(state.note_axis().write().unwrap().start_plus(1)),
|
|
},
|
|
key!(Down) => match state.phrase_editor_entered() {
|
|
true => NoteCursorSet(state.note_axis().write().unwrap().point_minus(1)),
|
|
false => NoteScrollSet(state.note_axis().write().unwrap().start_minus(1)),
|
|
},
|
|
key!(PageUp) => match state.phrase_editor_entered() {
|
|
true => NoteCursorSet(state.note_axis().write().unwrap().point_plus(3)),
|
|
false => NoteScrollSet(state.note_axis().write().unwrap().start_plus(3)),
|
|
},
|
|
key!(PageDown) => match state.phrase_editor_entered() {
|
|
true => NoteCursorSet(state.note_axis().write().unwrap().point_minus(3)),
|
|
false => NoteScrollSet(state.note_axis().write().unwrap().start_minus(3)),
|
|
},
|
|
key!(Left) => match state.phrase_editor_entered() {
|
|
true => TimeCursorSet(state.note_axis().write().unwrap().point_minus(1)),
|
|
false => TimeScrollSet(state.note_axis().write().unwrap().start_minus(1)),
|
|
},
|
|
key!(Right) => match state.phrase_editor_entered() {
|
|
true => TimeCursorSet(state.note_axis().write().unwrap().point_plus(1)),
|
|
false => TimeScrollSet(state.note_axis().write().unwrap().start_plus(1)),
|
|
},
|
|
_ => return None
|
|
})
|
|
}
|
|
}
|