diff --git a/crates/tek/src/tui/arranger_command.rs b/crates/tek/src/tui/arranger_command.rs index 71e23a31..d4ba4e8d 100644 --- a/crates/tek/src/tui/arranger_command.rs +++ b/crates/tek/src/tui/arranger_command.rs @@ -1,11 +1,9 @@ use crate::*; use ClockCommand::{Play, Pause}; -use KeyCode::{Char, Delete, Tab}; +use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right}; #[derive(Clone, Debug)] pub enum ArrangerCommand { - Undo, - Redo, - Clear, + History(isize), Color(ItemPalette), Clock(ClockCommand), Scene(ArrangerSceneCommand), @@ -15,12 +13,8 @@ use KeyCode::{Char, Delete, Tab}; Zoom(usize), Phrases(PhrasesCommand), Editor(PhraseCommand), - ShowPool(bool), - GetClip(usize, usize), - PutClip(usize, usize, Option>>), - EnqueueClip(usize, usize), - EnqueueScene(usize), StopAll, + Clear, } #[derive(Clone, Debug)] pub enum ArrangerTrackCommand { @@ -34,6 +28,7 @@ pub enum ArrangerTrackCommand { } #[derive(Clone, Debug)] pub enum ArrangerSceneCommand { + Enqueue(usize), Add, Delete(usize), Swap(usize, usize), @@ -43,119 +38,127 @@ pub enum ArrangerSceneCommand { } #[derive(Clone, Debug)] pub enum ArrangerClipCommand { - Play, Get(usize, usize), - Set(usize, usize, Option>>), + Put(usize, usize, Option>>), + Enqueue(usize, usize), Edit(Option>>), - SetLoop(bool), - SetColor(ItemPalette), + SetLoop(usize, usize, bool), + SetColor(usize, usize, ItemPalette), } -input_to_command!(ArrangerCommand: |state: ArrangerTui, input|{ - use ArrangerSelection as Selected; - use ArrangerSceneCommand as Scene; - use ArrangerTrackCommand as Track; - use ArrangerClipCommand as Clip; - // WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor - match input.event() { - // TODO: u: undo - key_pat!(Char('u')) => { todo!("undo") }, - // TODO: Shift-U: redo - key_pat!(Char('U')) => { todo!("redo") }, - // TODO: k: toggle on-screen keyboard - key_pat!(Ctrl-Char('k')) => { todo!("keyboard") }, - // Transport: Play/pause - key_pat!(Char(' ')) => - Self::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), - // Transport: Play from start or rewind to start - key_pat!(Shift-Char(' ')) => - Self::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), - key_pat!(Char('e')) => - Self::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))), - key_pat!(Char('l')) => - Self::Clip(ArrangerClipCommand::SetLoop(false)), - key_pat!(Ctrl-Char('a')) => - Self::Scene(ArrangerSceneCommand::Add), - key_pat!(Ctrl-Char('t')) => - Self::Track(ArrangerTrackCommand::Add), - key_pat!(Char('0')) => match state.selected() { - Selected::Mix => Self::StopAll, - Selected::Track(_t) => return None, - Selected::Scene(_s) => return None, - Selected::Clip(_t, _s) => return None, - }, - // Tab: Toggle visibility of phrase pool column - key_pat!(Tab) => - Self::ShowPool(!state.show_pool), - _ => { - let t_len = state.tracks.len(); - let s_len = state.scenes.len(); - match state.selected() { - Selected::Clip(t, s) => match input.event() { - key_pat!(Char('w')) => Some(Self::Select(if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) })), - key_pat!(Char('s')) => Some(Self::Select(Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1))))), - key_pat!(Char('a')) => Some(Self::Select(if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) })), - key_pat!(Char('d')) => Some(Self::Select(Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s))), - key_pat!(Char(',')) => Some(Self::Clip(Clip::Set(t, s, None))), - key_pat!(Char('.')) => Some(Self::Clip(Clip::Set(t, s, None))), - key_pat!(Char('<')) => Some(Self::Clip(Clip::Set(t, s, None))), - key_pat!(Char('>')) => Some(Self::Clip(Clip::Set(t, s, None))), - key_pat!(Char('g')) => Some(Self::Phrases(PhrasesCommand::Select(0))), - key_pat!(Char('p')) => Some(Self::PutClip(t, s, Some(state.phrases.phrase().clone()))), - key_pat!(Char('q')) => Some(Self::EnqueueClip(t, s)), - key_pat!(Delete) => Some(Self::Clip(Clip::Set(t, s, None))), - //key_pat!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemPalette::random())), - //key_pat!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))), - //key_pat!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))), - _ => None - }, - Selected::Scene(s) => match input.event() { - key_pat!(Char('w')) => Some(Self::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix })), - key_pat!(Char('s')) => Some(Self::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1))))), - key_pat!(Char('d')) => Some(Self::Select(Selected::Clip(0, s))), - key_pat!(Char(',')) => Some(Self::Scene(Scene::Swap(s, s - 1))), - key_pat!(Char('.')) => Some(Self::Scene(Scene::Swap(s, s + 1))), - key_pat!(Char('<')) => Some(Self::Scene(Scene::Swap(s, s - 1))), - key_pat!(Char('>')) => Some(Self::Scene(Scene::Swap(s, s + 1))), - key_pat!(Char('c')) => Some(Self::Scene(Scene::SetColor(s, ItemPalette::random()))), - key_pat!(Char('q')) => Some(Self::EnqueueScene(s)), - key_pat!(Delete) => Some(Self::Scene(Scene::Delete(s))), - //key_pat!(Char('c')) => Cmd::Track(Scene::Color(s, ItemPalette::random())), - _ => None - }, - Selected::Track(t) => match input.event() { - key_pat!(Char('s')) => Some(Self::Select(Selected::Clip(t, 0))), - key_pat!(Char('a')) => Some(Self::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix })), - key_pat!(Char('d')) => Some(Self::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1))))), - key_pat!(Char('c')) => Some(Self::Track(Track::SetColor(t, ItemPalette::random()))), - key_pat!(Char(',')) => Some(Self::Track(Track::Swap(t, t - 1))), - key_pat!(Char('.')) => Some(Self::Track(Track::Swap(t, t + 1))), - key_pat!(Char('<')) => Some(Self::Track(Track::Swap(t, t - 1))), - key_pat!(Char('>')) => Some(Self::Track(Track::Swap(t, t + 1))), - key_pat!(Delete) => Some(Self::Track(Track::Delete(t))), - //key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemPalette::random())), - _ => return None - }, - Selected::Mix => match input.event() { - // 0: Enqueue phrase 0 (stop all) - key_pat!(Char('0')) => Some(Self::StopAll), - key_pat!(Char('s')) => Some(Self::Select(Selected::Scene(0))), - key_pat!(Char('d')) => Some(Self::Select(Selected::Track(0))), - key_pat!(Delete) => Some(Self::Clear), - key_pat!(Char('c')) => Some(Self::Color(ItemPalette::random())), - _ => None - }, - } - }.or_else(||if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { +input_to_command!(ArrangerCommand: |state: ArrangerTui, input|match input.event() { + // TODO: u: undo + key_pat!(Char('u')) => { todo!("undo") }, + // TODO: Shift-U: redo + key_pat!(Char('U')) => { todo!("redo") }, + // TODO: k: toggle on-screen keyboard + key_pat!(Ctrl-Char('k')) => { todo!("keyboard") }, + // Transport: Play/pause + key_pat!(Char(' ')) => + Self::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), + // Transport: Play from start or rewind to start + key_pat!(Shift-Char(' ')) => + Self::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), + key_pat!(Char('e')) => + Self::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))), + key_pat!(Ctrl-Left) => + Self::Scene(ArrangerSceneCommand::Add), + key_pat!(Ctrl-Char('t')) => + Self::Track(ArrangerTrackCommand::Add), + // Tab: Toggle visibility of phrase pool column + key_pat!(Tab) => + Self::Phrases(PhrasesCommand::Show(!state.show_pool)), + _ => to_arrangement_command(state, input).or_else(||{ + if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { Some(Self::Editor(command)) } else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) { Some(Self::Phrases(command)) } else { None - })? - } + } + })? }); -command!(|self:ArrangerCommand,state:ArrangerTui|match self { +fn to_arrangement_command (state: &ArrangerTui, input: &TuiInput) -> Option { + use ArrangerCommand as Cmd; + use ArrangerSelection as Selected; + use ArrangerSceneCommand as Scene; + use ArrangerTrackCommand as Track; + use ArrangerClipCommand as Clip; + let t_len = state.tracks.len(); + let s_len = state.scenes.len(); + match state.selected() { + Selected::Clip(t, s) => match input.event() { + key_pat!(Char('g')) => Some(Cmd::Phrases(PhrasesCommand::Select(0))), + 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))), + key_pat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))), + key_pat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))), + key_pat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.phrases.phrase().clone())))), + key_pat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))), + key_pat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))), + + key_pat!(Up) => Some(Cmd::Select( + if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) })), + key_pat!(Down) => Some(Cmd::Select( + Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1))))), + key_pat!(Left) => Some(Cmd::Select( + if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) })), + key_pat!(Right) => Some(Cmd::Select( + Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s))), + + _ => None + }, + Selected::Scene(s) => match input.event() { + 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('q')) => Some(Cmd::Scene(Scene::Enqueue(s))), + key_pat!(Delete) => Some(Cmd::Scene(Scene::Delete(s))), + key_pat!(Char('c')) => Some(Cmd::Scene(Scene::SetColor(s, ItemPalette::random()))), + + key_pat!(Up) => Some( + Cmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix })), + key_pat!(Down) => Some( + Cmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1))))), + key_pat!(Right) => Some( + Cmd::Select(Selected::Clip(0, s))), + + _ => None + }, + Selected::Track(t) => match input.event() { + 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!(Delete) => Some(Cmd::Track(Track::Delete(t))), + key_pat!(Char('c')) => Some(Cmd::Track(Track::SetColor(t, ItemPalette::random()))), + + key_pat!(Down) => Some( + Cmd::Select(Selected::Clip(t, 0))), + key_pat!(Left) => Some( + Cmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix })), + key_pat!(Right) => Some( + Cmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1))))), + + _ => None + }, + Selected::Mix => match input.event() { + key_pat!(Delete) => Some(Cmd::Clear), + key_pat!(Char('0')) => Some(Cmd::StopAll), + key_pat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())), + + key_pat!(Down) => Some( + Cmd::Select(Selected::Scene(0))), + key_pat!(Right) => Some( + Cmd::Select(Selected::Track(0))), + + _ => None + }, + } +} +command!(|self: ArrangerCommand, state: ArrangerTui|match self { Self::Scene(cmd) => cmd.execute(state)?.map(Self::Scene), Self::Track(cmd) => cmd.execute(state)?.map(Self::Track), Self::Clip(cmd) => cmd.execute(state)?.map(Self::Clip), @@ -171,10 +174,6 @@ command!(|self:ArrangerCommand,state:ArrangerTui|match self { state.color = palette; Some(Self::Color(old)) }, - Self::ShowPool(show) => { - state.show_pool = show; - None - }, Self::Phrases(cmd) => { let mut default = |cmd: PhrasesCommand|{ cmd.execute(&mut state.phrases).map(|x|x.map(Self::Phrases)) @@ -186,7 +185,7 @@ command!(|self:ArrangerCommand,state:ArrangerTui|match self { state.editor.set_phrase(Some(state.phrases.phrase())); undo }, - // update color in all places simultaneously + // reload phrase in editor to update color PhrasesCommand::Phrase(PhrasePoolCommand::SetColor(index, _)) => { let undo = default(cmd)?; state.editor.set_phrase(Some(state.phrases.phrase())); @@ -195,31 +194,14 @@ command!(|self:ArrangerCommand,state:ArrangerTui|match self { _ => default(cmd)? } }, - Self::Undo => { todo!() }, - Self::Redo => { todo!() }, - Self::Clear => { todo!() }, - Self::GetClip(track, scene) => { todo!() }, - Self::PutClip(track, scene, phrase) => { - let old = state.scenes[scene].clips[track].clone(); - state.scenes[scene].clips[track] = phrase; - Some(Self::PutClip(track, scene, old)) - }, - Self::EnqueueClip(track, scene) => { - state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); - None - }, - Self::EnqueueScene(scene) => { - for track in 0..state.tracks.len() { - state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); - } - None - }, + Self::History(_) => { todo!() }, Self::StopAll => { for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); } None }, + Self::Clear => { todo!() }, }); command!(|self: ArrangerTrackCommand, state: ArrangerTui|match self { Self::SetColor(index, color) => { @@ -233,7 +215,7 @@ command!(|self: ArrangerTrackCommand, state: ArrangerTui|match self { }, _ => None }); -command!(|self:ArrangerSceneCommand,state:ArrangerTui|match self { +command!(|self: ArrangerSceneCommand, state: ArrangerTui|match self { Self::Delete(index) => { state.scene_del(index); None @@ -243,8 +225,24 @@ command!(|self:ArrangerSceneCommand,state:ArrangerTui|match self { state.scenes[index].color = color; Some(Self::SetColor(index, old)) }, + Self::Enqueue(scene) => { + for track in 0..state.tracks.len() { + state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); + } + None + }, _ => None }); -command!(|self:ArrangerClipCommand, _state:ArrangerTui|match self { +command!(|self: ArrangerClipCommand, state: ArrangerTui|match self { + Self::Get(track, scene) => { todo!() }, + Self::Put(track, scene, phrase) => { + let old = state.scenes[scene].clips[track].clone(); + state.scenes[scene].clips[track] = phrase; + Some(Self::Put(track, scene, old)) + }, + Self::Enqueue(track, scene) => { + state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); + None + }, _ => None }); diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 16ab2c5b..85c69ea3 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -8,6 +8,7 @@ use crate::{ #[derive(Debug)] pub struct PhraseListModel { + pub(crate) visible: bool, /// Collection of phrases pub(crate) phrases: Vec>>, /// Selected phrase @@ -35,6 +36,7 @@ pub enum PhraseListMode { #[derive(Clone, PartialEq, Debug)] pub enum PhrasesCommand { + Show(bool), /// Update the contents of the phrase pool Phrase(Pool), /// Select a phrase from the phrase pool @@ -52,7 +54,10 @@ pub enum PhrasesCommand { command!(|self:PhrasesCommand, state:PhraseListModel|{ use PhrasesCommand::*; match self { - Phrase(command) => command.execute(state)?.map(Phrase), + Show(visible) => { + state.visible = visible; + Some(Self::Show(!visible)) + } Rename(command) => match command { PhraseRenameCommand::Begin => { let length = state.phrases()[state.phrase_index()].read().unwrap().length; @@ -95,6 +100,7 @@ command!(|self:PhrasesCommand, state:PhraseListModel|{ state.set_phrase_index(phrase); None }, + Phrase(command) => command.execute(state)?.map(Phrase), } }); @@ -158,6 +164,7 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option Self { Self { + visible: true, phrases: vec![RwLock::new(Phrase::default()).into()], phrase: 0.into(), scroll: 0, diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs index e388010c..a0cc3bc1 100644 --- a/crates/tek/src/tui/status_bar.rs +++ b/crates/tek/src/tui/status_bar.rs @@ -119,10 +119,10 @@ impl ArrangerStatus { Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!([ single("SPACE", "play/pause"), single(" Ctrl", " scroll"), - single(" wsad", " cell"), + single(" ▲▼▶◀", " cell"), double(("p", "put"), ("g", "get")), double(("q", "enqueue"), ("e", "edit")), - single(" ▲▼▶◀", " note"), + single(" wsad", " note"), double(("a", "append"), ("s", "set"),), double((",.", "length"), ("<>", "triplet"),), double(("[]", "phrase"), ("{}", "order"),),