diff --git a/crates/tek_api/src/arrange.rs b/crates/tek_api/src/arrange.rs index 1bed1b40..dd6c2757 100644 --- a/crates/tek_api/src/arrange.rs +++ b/crates/tek_api/src/arrange.rs @@ -1,5 +1,6 @@ use crate::*; +#[derive(Debug)] pub struct Arrangement { /// JACK client handle (needs to not be dropped for standalone mode to work). pub jack: Arc>, @@ -12,9 +13,10 @@ pub struct Arrangement { /// Collection of tracks. pub tracks: Vec, /// Collection of scenes. - pub scenes: Vec, + pub scenes: Vec, } +#[derive(Debug)] pub struct ArrangementTrack { /// Name of track pub name: Arc>, @@ -26,57 +28,93 @@ pub struct ArrangementTrack { pub player: MIDIPlayer } +#[derive(Default, Debug, Clone)] +pub struct ArrangementScene { + /// Name of scene + pub name: Arc>, + /// Clips in scene, one per track + pub clips: Vec>>>, + /// Identifying color of scene + pub color: ItemColor, +} + impl Audio for Arrangement { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { for track in self.tracks.iter_mut() { - track.player.process(client, scope); + if track.process(client, scope) == Control::Quit { + return Control::Quit + } } Control::Continue } } +impl Audio for ArrangementTrack { + #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + self.player.process(client, scope) + } +} + impl Arrangement { - fn is_stopped (&self) -> bool { + pub fn is_stopped (&self) -> bool { *self.clock.playing.read().unwrap() == Some(TransportState::Stopped) } } -#[derive(Clone)] -pub enum ArrangementCommand { - New, - Load, - Save, - ToggleViewMode, - Delete, - Activate, - Increment, - Decrement, - ZoomIn, - ZoomOut, - Go(Direction), - Edit(Option>>), - Scene(SceneCommand), - Track(ArrangementTrackCommand), - Clip(ArrangementClipCommand), -} +impl ArrangementScene { + /// Returns the pulse length of the longest phrase in the scene + pub fn pulses (&self) -> usize { + self.clips.iter().fold(0, |a, p|{ + a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) + }) + } -#[derive(Clone)] -pub enum ArrangementTrackCommand { - Next, - Prev, - Add, - Delete, - MoveForward, - MoveBack, - RandomColor, - SetSize(usize), - SetZoom(usize), -} + /// Returns true if all phrases in the scene are + /// currently playing on the given collection of tracks. + pub fn is_playing (&self, tracks: &[MIDIPlayer]) -> bool { + self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(clip) => tracks + .get(track_index) + .map(|track|if let Some((_, Some(phrase))) = &track.phrase { + *phrase.read().unwrap() == *clip.read().unwrap() + } else { + false + }) + .unwrap_or(false), + None => true + }) + } -#[derive(Clone)] -pub enum ArrangementClipCommand { - Get(usize, usize), - Put(usize, usize, Option>>), - Edit(Option>>), - SetLoop(bool), + pub fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } + } + + //TODO + //pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually { + //let mut name = None; + //let mut clips = vec![]; + //edn!(edn in args { + //Edn::Map(map) => { + //let key = map.get(&Edn::Key(":name")); + //if let Some(Edn::Str(n)) = key { + //name = Some(*n); + //} else { + //panic!("unexpected key in scene '{name:?}': {key:?}") + //} + //}, + //Edn::Symbol("_") => { + //clips.push(None); + //}, + //Edn::Int(i) => { + //clips.push(Some(*i as usize)); + //}, + //_ => panic!("unexpected in scene '{name:?}': {edn:?}") + //}); + //Ok(ArrangementScene { + //name: Arc::new(name.unwrap_or("").to_string().into()), + //color: ItemColor::random(), + //clips, + //}) + //} } diff --git a/crates/tek_api/src/arrange_cmd.rs b/crates/tek_api/src/arrange_cmd.rs new file mode 100644 index 00000000..189d18e9 --- /dev/null +++ b/crates/tek_api/src/arrange_cmd.rs @@ -0,0 +1,94 @@ +use crate::*; + +#[derive(Clone)] +pub enum ArrangementCommand { + Clear, + Export, + Import, + StopAll, + Scene(ArrangementSceneCommand), + Track(ArrangementTrackCommand), + Clip(ArrangementClipCommand), +} + +#[derive(Clone)] +pub enum ArrangementSceneCommand { + Add, + Delete, + RandomColor, + Play, + Swap(usize), + SetSize(usize), + SetZoom(usize), +} + +#[derive(Clone)] +pub enum ArrangementTrackCommand { + Add, + Delete, + RandomColor, + Stop, + Swap(usize), + SetSize(usize), + SetZoom(usize), +} + +#[derive(Clone)] +pub enum ArrangementClipCommand { + Play, + Get(usize, usize), + Set(usize, usize, Option>>), + Edit(Option>>), + SetLoop(bool), + RandomColor, +} + +impl Command for ArrangementCommand { + fn execute (self, state: &mut Arrangement) -> Perhaps { + match self { + Self::Clear => todo!(), + Self::Export => todo!(), + Self::Import => todo!(), + Self::StopAll => todo!(), + Self::Scene(command) => { return Ok(command.execute(state)?.map(Self::Scene)) }, + Self::Track(command) => { return Ok(command.execute(state)?.map(Self::Track)) }, + Self::Clip(command) => { return Ok(command.execute(state)?.map(Self::Clip)) }, + } + return Ok(None) + } +} + +impl Command for ArrangementSceneCommand { + fn execute (self, state: &mut Arrangement) -> Perhaps { + todo!() + } +} + +impl Command for ArrangementTrackCommand { + fn execute (self, state: &mut Arrangement) -> Perhaps { + todo!() + } +} + +impl Command for ArrangementClipCommand { + fn execute (self, state: &mut Arrangement) -> Perhaps { + todo!() + } +} + +//impl Command for ArrangementSceneCommand { +//} + //Edit(phrase) => { state.state.phrase = phrase.clone() }, + //ToggleViewMode => { state.state.mode.to_next(); }, + //Delete => { state.state.delete(); }, + //Activate => { state.state.activate(); }, + //ZoomIn => { state.state.zoom_in(); }, + //ZoomOut => { state.state.zoom_out(); }, + //MoveBack => { state.state.move_back(); }, + //MoveForward => { state.state.move_forward(); }, + //RandomColor => { state.state.randomize_color(); }, + //Put => { state.state.phrase_put(); }, + //Get => { state.state.phrase_get(); }, + //AddScene => { state.state.scene_add(None, None)?; }, + //AddTrack => { state.state.track_add(None, None)?; }, + //ToggleLoop => { state.state.toggle_loop() }, diff --git a/crates/tek_api/src/lib.rs b/crates/tek_api/src/lib.rs index 464637bc..14ab00db 100644 --- a/crates/tek_api/src/lib.rs +++ b/crates/tek_api/src/lib.rs @@ -10,21 +10,31 @@ pub(crate) use tek_core::jack::{ submod! { api_jack + arrange + arrange_cmd + clock + mixer + mixer_track + phrase + plugin plugin_kind plugin_lv2 + pool - sample + sampler - scene - scene_cmd + sampler_sample + sampler_voice + sequencer - track + + status + transport transport_cmd - voice } diff --git a/crates/tek_api/src/track.rs b/crates/tek_api/src/mixer_track.rs similarity index 100% rename from crates/tek_api/src/track.rs rename to crates/tek_api/src/mixer_track.rs index 6c527ca8..84c27945 100644 --- a/crates/tek_api/src/track.rs +++ b/crates/tek_api/src/mixer_track.rs @@ -14,16 +14,6 @@ pub struct MixerTrack { pub devices: Vec>, } -pub trait MixerTrackDevice: Audio + Debug { - fn boxed (self) -> Box where Self: Sized + 'static { - Box::new(self) - } -} - -impl MixerTrackDevice for Sampler {} - -impl MixerTrackDevice for Plugin {} - //impl MixerTrackDevice for LV2Plugin {} impl MixerTrack { @@ -81,3 +71,13 @@ impl MixerTrack { Ok(track) } } + +pub trait MixerTrackDevice: Audio + Debug { + fn boxed (self) -> Box where Self: Sized + 'static { + Box::new(self) + } +} + +impl MixerTrackDevice for Sampler {} + +impl MixerTrackDevice for Plugin {} diff --git a/crates/tek_api/src/pool.rs b/crates/tek_api/src/pool.rs index 2f2ed845..ab2bc95a 100644 --- a/crates/tek_api/src/pool.rs +++ b/crates/tek_api/src/pool.rs @@ -4,46 +4,18 @@ use crate::*; pub struct PhrasePool { /// Phrases in the pool pub phrases: Vec>>, - /// Highlighted phrase - pub phrase: usize, } #[derive(Clone, PartialEq)] pub enum PhrasePoolCommand { - Prev, - Next, - MoveUp, - MoveDown, - Delete, - Append, - Insert, - Duplicate, - RandomColor, - Edit, - Import, - Export, - Rename(PhraseRenameCommand), - Length(PhraseLengthCommand), -} - -#[derive(Clone, PartialEq)] -pub enum PhraseRenameCommand { - Begin, - Backspace, - Append(char), - Set(String), - Confirm, - Cancel, -} - -#[derive(Clone, PartialEq)] -pub enum PhraseLengthCommand { - Begin, - Next, - Prev, - Inc, - Dec, - Set(usize), - Confirm, - Cancel, + Select(usize), + Add(usize), + Delete(usize), + Duplicate(usize), + Swap(usize), + RandomColor(usize), + Import(String), + Export(String), + SetName(String), + SetLength(String), } diff --git a/crates/tek_api/src/sample.rs b/crates/tek_api/src/sampler_sample.rs similarity index 100% rename from crates/tek_api/src/sample.rs rename to crates/tek_api/src/sampler_sample.rs diff --git a/crates/tek_api/src/voice.rs b/crates/tek_api/src/sampler_voice.rs similarity index 100% rename from crates/tek_api/src/voice.rs rename to crates/tek_api/src/sampler_voice.rs diff --git a/crates/tek_api/src/scene.rs b/crates/tek_api/src/scene.rs deleted file mode 100644 index dd330507..00000000 --- a/crates/tek_api/src/scene.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::*; - -#[derive(Default, Debug, Clone)] -pub struct Scene { - /// Name of scene - pub name: Arc>, - /// Clips in scene, one per track - pub clips: Vec>>>, - /// Identifying color of scene - pub color: ItemColor, -} - -impl Scene { - //TODO - //pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually { - //let mut name = None; - //let mut clips = vec![]; - //edn!(edn in args { - //Edn::Map(map) => { - //let key = map.get(&Edn::Key(":name")); - //if let Some(Edn::Str(n)) = key { - //name = Some(*n); - //} else { - //panic!("unexpected key in scene '{name:?}': {key:?}") - //} - //}, - //Edn::Symbol("_") => { - //clips.push(None); - //}, - //Edn::Int(i) => { - //clips.push(Some(*i as usize)); - //}, - //_ => panic!("unexpected in scene '{name:?}': {edn:?}") - //}); - //Ok(Scene { - //name: Arc::new(name.unwrap_or("").to_string().into()), - //color: ItemColor::random(), - //clips, - //}) - //} - - /// Returns the pulse length of the longest phrase in the scene - pub fn pulses (&self) -> usize { - self.clips.iter().fold(0, |a, p|{ - a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) - }) - } - - /// Returns true if all phrases in the scene are - /// currently playing on the given collection of tracks. - pub fn is_playing (&self, tracks: &[MIDIPlayer]) -> bool { - self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(clip) => tracks - .get(track_index) - .map(|track|if let Some((_, Some(phrase))) = &track.phrase { - *phrase.read().unwrap() == *clip.read().unwrap() - } else { - false - }) - .unwrap_or(false), - None => true - }) - } - - pub fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } - } - -} diff --git a/crates/tek_api/src/scene_cmd.rs b/crates/tek_api/src/scene_cmd.rs deleted file mode 100644 index 622864c4..00000000 --- a/crates/tek_api/src/scene_cmd.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::*; - -#[derive(Clone)] -pub enum SceneCommand { - Next, - Prev, - Add, - Delete, - MoveForward, - MoveBack, - RandomColor, - SetSize(usize), - SetZoom(usize), -} diff --git a/crates/tek_api/src/status.rs b/crates/tek_api/src/status.rs new file mode 100644 index 00000000..ea4cbfff --- /dev/null +++ b/crates/tek_api/src/status.rs @@ -0,0 +1,19 @@ +use crate::*; + +pub trait StatusBar: Widget { + fn hotkey_fg () -> Color; + + fn command (commands: &[[impl Widget;3]]) -> impl Widget + '_ { + let hotkey_fg = Self::hotkey_fg(); + Stack::right(move |add|{ + Ok(for [a, b, c] in commands.iter() { + add(&row!( + " ", + widget(a), + widget(b).bold(true).fg(hotkey_fg), + widget(c), + ))?; + }) + }) + } +} diff --git a/crates/tek_core/src/command.rs b/crates/tek_core/src/command.rs index 67aa325e..233f17ea 100644 --- a/crates/tek_core/src/command.rs +++ b/crates/tek_core/src/command.rs @@ -1,5 +1,11 @@ use crate::*; +#[derive(Clone)] +pub enum NextPrev { + Next, + Prev, +} + pub trait Command: Sized { fn translate (self, _: &S) -> Self { self } fn execute (self, state: &mut S) -> Perhaps; diff --git a/crates/tek_tui/src/lib.rs b/crates/tek_tui/src/lib.rs index c281d39f..cef37461 100644 --- a/crates/tek_tui/src/lib.rs +++ b/crates/tek_tui/src/lib.rs @@ -11,32 +11,46 @@ pub(crate) use std::fs::read_dir; submod! { tui_app + tui_arrangement + tui_arrangement_cmd + tui_arrangement_foc + tui_arranger tui_arranger_bar tui_arranger_cmd - tui_arranger_col tui_arranger_foc tui_arranger_hor tui_arranger_ver + tui_mixer tui_mixer_cmd + tui_phrase tui_phrase_cmd + tui_plugin tui_plugin_cmd tui_plugin_lv2 tui_plugin_lv2_gui tui_plugin_vst2 tui_plugin_vst3 + tui_pool tui_pool_cmd + tui_pool_length + tui_pool_rename + tui_sampler tui_sampler_cmd + tui_sequencer tui_sequencer_bar tui_sequencer_cmd tui_sequencer_foc + + tui_theme + tui_transport tui_transport_bar tui_transport_cmd diff --git a/crates/tek_tui/src/tui_app.rs b/crates/tek_tui/src/tui_app.rs index 24e54882..ffe51b6f 100644 --- a/crates/tek_tui/src/tui_app.rs +++ b/crates/tek_tui/src/tui_app.rs @@ -1,39 +1,44 @@ use crate::*; -pub struct App +pub struct App where E: Engine, - T: Widget + Handle + Audio, C: Command, - S: Widget + U: From>> + Widget + Handle, + A: From>> + Audio, + S: From>> + StatusBar { - state: T, cursor: (usize, usize), entered: bool, menu_bar: Option>, - status_bar: Option + status_bar: Option, history: Vec, size: Measure, + ui: U, + audio: A, + model: Arc>, } -impl App, TransportViewCommand, TransportStatusBar> { - fn new () -> Self { +impl From for App +where + E: Engine, + C: Command, + U: From>> + Widget + Handle, + A: From>> + Audio, + S: From>> + Widget +{ + fn from (model: T) -> Self { + let model = Arc::new(RwLock::new(model)); Self { - state: TransportView { - _engine: Default::default(), - state: Transport { - jack: Default::default(), - transport: Default::default(), - clock: Default::default(), - metronome: false - } - }, - cursor: (0, 0), - entered: false, - menu_bar: None, - status_bar: None, - history: vec![], - size: Default::default(), + cursor: (0, 0), + entered: false, + menu_bar: None, + status_bar: None, + history: vec![], + size: (0, 0).into(), + ui: U::from(model.clone()), + audio: A::from(model.clone()), + model, } } } diff --git a/crates/tek_tui/src/tui_arrangement.rs b/crates/tek_tui/src/tui_arrangement.rs index 80f792e2..4d8dc56c 100644 --- a/crates/tek_tui/src/tui_arrangement.rs +++ b/crates/tek_tui/src/tui_arrangement.rs @@ -1,149 +1,61 @@ use crate::*; pub struct ArrangementEditor { - /// Global JACK client - pub jack: Arc>, - /// Global timebase - pub clock: Arc, - /// Name of arranger - pub name: Arc>, - /// Collection of phrases. - pub phrases: Arc>>, - /// Collection of tracks. - pub tracks: Vec, - /// Collection of scenes. - pub scenes: Vec, + pub state: Arrangement, /// Currently selected element. pub selected: ArrangementEditorFocus, /// Display mode of arranger - pub mode: ArrangementViewMode, - /// Whether the arranger is currently focused - pub focused: bool, + pub mode: ArrangementEditorMode, /// Background color of arrangement pub color: ItemColor, /// Width and height of arrangement area at last render pub size: Measure, + /// Whether the arranger is currently focused + pub focused: bool, /// Whether this is currently in edit mode pub entered: bool, } /// Display mode of arranger -#[derive(PartialEq)] -pub enum ArrangementViewMode { +#[derive(Clone, PartialEq)] +pub enum ArrangementEditorMode { /// Tracks are rows Horizontal, /// Tracks are columns Vertical(usize), } +impl Audio for ArrangementEditor { + #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + self.state.process(client, scope) + } +} + impl Content for ArrangementEditor { type Engine = Tui; fn content (&self) -> impl Widget { Layers::new(move |add|{ match self.mode { - ArrangementViewMode::Horizontal => { add(&HorizontalArranger(&self)) }, - ArrangementViewMode::Vertical(factor) => { add(&VerticalArranger(&self, factor)) }, - }?; + ArrangementEditorMode::Horizontal => + add(&HorizontalArranger(&self))?, + ArrangementEditorMode::Vertical(factor) => + add(&VerticalArranger(&self, factor))? + }; add(&self.size) }) } } -#[derive(PartialEq, Clone, Copy)] -/// Represents the current user selection in the arranger -pub enum ArrangementEditorFocus { - /// The whole mix is selected - Mix, - /// A track is selected. - Track(usize), - /// A scene is selected. - Scene(usize), - /// A clip (track × scene) is selected. - Clip(usize, usize), -} - -/// Focus identification methods -impl ArrangementEditorFocus { - pub fn description ( - &self, - tracks: &Vec, - scenes: &Vec, - ) -> String { - format!("Selected: {}", match self { - Self::Mix => format!("Everything"), - Self::Track(t) => match tracks.get(*t) { - Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), - None => format!("T??"), - }, - Self::Scene(s) => match scenes.get(*s) { - Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), - None => format!("S??"), - }, - Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { - (Some(_), Some(scene)) => match scene.clip(*t) { - Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), - None => format!("T{t} S{s}: Empty") - }, - _ => format!("T{t} S{s}: Empty"), - } - }) - } - pub fn is_mix (&self) -> bool { match self { Self::Mix => true, _ => false } } - pub fn is_track (&self) -> bool { match self { Self::Track(_) => true, _ => false } } - pub fn is_scene (&self) -> bool { match self { Self::Scene(_) => true, _ => false } } - pub fn is_clip (&self) -> bool { match self { Self::Clip(_, _) => true, _ => false } } - pub fn track (&self) -> Option { - match self { Self::Clip(t, _) => Some(*t), Self::Track(t) => Some(*t), _ => None } - } - pub fn track_next (&mut self, last_track: usize) { - *self = match self { - Self::Mix => - Self::Track(0), - Self::Track(t) => - Self::Track(last_track.min(*t + 1)), - Self::Scene(s) => - Self::Clip(0, *s), - Self::Clip(t, s) => - Self::Clip(last_track.min(*t + 1), *s), - } - } - pub fn track_prev (&mut self) { - *self = match self { - Self::Mix => - Self::Mix, - Self::Scene(s) => - Self::Scene(*s), - Self::Track(t) => - if *t == 0 { Self::Mix } else { Self::Track(*t - 1) }, - Self::Clip(t, s) => - if *t == 0 { Self::Scene(*s) } else { Self::Clip(t.saturating_sub(1), *s) } - } - } - pub fn scene (&self) -> Option { - match self { Self::Clip(_, s) => Some(*s), Self::Scene(s) => Some(*s), _ => None } - } - pub fn scene_next (&mut self, last_scene: usize) { - *self = match self { - Self::Mix => - Self::Scene(0), - Self::Track(t) => - Self::Clip(*t, 0), - Self::Scene(s) => - Self::Scene(last_scene.min(*s + 1)), - Self::Clip(t, s) => - Self::Clip(*t, last_scene.min(*s + 1)), - } - } - pub fn scene_prev (&mut self) { - *self = match self { - Self::Mix => - Self::Mix, - Self::Track(t) => - Self::Track(*t), - Self::Scene(s) => - if *s == 0 { Self::Mix } else { Self::Scene(*s - 1) }, - Self::Clip(t, s) => - if *s == 0 { Self::Track(*t) } else { Self::Clip(*t, s.saturating_sub(1)) } +impl ArrangementEditor { + pub fn new (state: Arrangement) -> Self { + Self { + state, + selected: ArrangementEditorFocus::Clip(0, 0), + mode: ArrangementEditorMode::Vertical(2), + color: Color::Rgb(28, 35, 25).into(), + size: Measure::new(), + focused: false, + entered: false, } } } diff --git a/crates/tek_tui/src/tui_arrangement_cmd.rs b/crates/tek_tui/src/tui_arrangement_cmd.rs new file mode 100644 index 00000000..e7bb7412 --- /dev/null +++ b/crates/tek_tui/src/tui_arrangement_cmd.rs @@ -0,0 +1,139 @@ +use crate::*; + +#[derive(Clone)] +pub enum ArrangementEditorCommand { + Edit(ArrangementCommand), + Select(ArrangementEditorFocus), + Zoom(usize), +} + +/// Handle events for arrangement. +impl Handle for ArrangementEditor { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + ArrangementEditorCommand::execute_with_state(self, from) + } +} + +impl InputToCommand> for ArrangementEditorCommand { + fn input_to_command (state: &ArrangementEditor, input: &TuiInput) -> Option { + use ArrangementEditorCommand as Cmd; + use ArrangementCommand as Edit; + use ArrangementEditorFocus as Focus; + use ArrangementTrackCommand as Track; + use ArrangementClipCommand as Clip; + use ArrangementSceneCommand as Scene; + Some(match input.event() { + // FIXME: boundary conditions + + key!(KeyCode::Up) => match state.selected { + ArrangementEditorFocus::Mix => return None, + ArrangementEditorFocus::Track(t) => return None, + ArrangementEditorFocus::Scene(s) => Cmd::Select(Focus::Scene(s - 1)), + ArrangementEditorFocus::Clip(t, s) => Cmd::Select(Focus::Clip(t, s - 1)), + }, + + key!(KeyCode::Down) => match state.selected { + ArrangementEditorFocus::Mix => Cmd::Select(Focus::Scene(0)), + ArrangementEditorFocus::Track(t) => Cmd::Select(Focus::Clip(t, 0)), + ArrangementEditorFocus::Scene(s) => Cmd::Select(Focus::Scene(s + 1)), + ArrangementEditorFocus::Clip(t, s) => Cmd::Select(Focus::Clip(t, s + 1)), + }, + + key!(KeyCode::Left) => match state.selected { + ArrangementEditorFocus::Mix => return None, + ArrangementEditorFocus::Track(t) => Cmd::Select(Focus::Track(t - 1)), + ArrangementEditorFocus::Scene(s) => return None, + ArrangementEditorFocus::Clip(t, s) => Cmd::Select(Focus::Clip(t - 1, s)), + }, + + key!(KeyCode::Right) => match state.selected { + ArrangementEditorFocus::Mix => return None, + ArrangementEditorFocus::Track(t) => Cmd::Select(Focus::Track(t + 1)), + ArrangementEditorFocus::Scene(s) => Cmd::Select(Focus::Clip(0, s)), + ArrangementEditorFocus::Clip(t, s) => Cmd::Select(Focus::Clip(t, s - 1)), + }, + + key!(KeyCode::Char('+')) => Cmd::Zoom(0), + + key!(KeyCode::Char('=')) => Cmd::Zoom(0), + + key!(KeyCode::Char('_')) => Cmd::Zoom(0), + + key!(KeyCode::Char('-')) => Cmd::Zoom(0), + + key!(KeyCode::Char('`')) => { todo!("toggle view mode") }, + + key!(KeyCode::Char(',')) => match state.selected { + ArrangementEditorFocus::Mix => Cmd::Zoom(0), + ArrangementEditorFocus::Track(t) => Cmd::Edit(Edit::Track(Track::Swap(0))), + ArrangementEditorFocus::Scene(s) => Cmd::Edit(Edit::Scene(Scene::Swap(0))), + ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Set(t, s, None))), + }, + + key!(KeyCode::Char('.')) => match state.selected { + ArrangementEditorFocus::Mix => Cmd::Zoom(0), + ArrangementEditorFocus::Track(t) => Cmd::Edit(Edit::Track(Track::Swap(0))), + ArrangementEditorFocus::Scene(s) => Cmd::Edit(Edit::Scene(Scene::Swap(0))), + ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Set(t, s, None))), + }, + + key!(KeyCode::Char('<')) => match state.selected { + ArrangementEditorFocus::Mix => Cmd::Zoom(0), + ArrangementEditorFocus::Track(t) => Cmd::Edit(Edit::Track(Track::Swap(0))), + ArrangementEditorFocus::Scene(s) => Cmd::Edit(Edit::Scene(Scene::Swap(0))), + ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Set(t, s, None))), + }, + + key!(KeyCode::Char('>')) => match state.selected { + ArrangementEditorFocus::Mix => Cmd::Zoom(0), + ArrangementEditorFocus::Track(t) => Cmd::Edit(Edit::Track(Track::Swap(0))), + ArrangementEditorFocus::Scene(s) => Cmd::Edit(Edit::Scene(Scene::Swap(0))), + ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Set(t, s, None))), + }, + + key!(KeyCode::Enter) => match state.selected { + ArrangementEditorFocus::Mix => return None, + ArrangementEditorFocus::Track(t) => return None, + ArrangementEditorFocus::Scene(s) => return None, + ArrangementEditorFocus::Clip(t, s) => return None, + }, + + key!(KeyCode::Delete) => match state.selected { + ArrangementEditorFocus::Mix => Cmd::Edit(Edit::Clear), + ArrangementEditorFocus::Track(t) => Cmd::Edit(Edit::Track(Track::Delete(t))), + ArrangementEditorFocus::Scene(s) => Cmd::Edit(Edit::Scene(Scene::Delete(s))), + ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Set(t, s, None))), + }, + + key!(KeyCode::Char('c')) => Cmd::Edit(Edit::Clip(Clip::RandomColor)), + + key!(KeyCode::Char('s')) => match state.selected { + ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Set(t, s, None))), + _ => return None, + }, + + key!(KeyCode::Char('g')) => match state.selected { + ArrangementEditorFocus::Clip(t, s) => Cmd::Edit(Edit::Clip(Clip::Get(t, s))), + _ => return None, + }, + + key!(Ctrl-KeyCode::Char('a')) => Cmd::Edit(Edit::Scene(Scene::Add)), + + key!(Ctrl-KeyCode::Char('t')) => Cmd::Edit(Edit::Track(Track::Add)), + + key!(KeyCode::Char('l')) => Cmd::Edit(Edit::Clip(Clip::SetLoop(false))), + + _ => return None + }) + } +} + +impl Command> for ArrangementEditorCommand { + fn execute (self, state: &mut ArrangementEditor) -> Perhaps { + match self { + Self::Zoom(zoom) => { todo!(); }, + Self::Select(selected) => { state.selected = selected; }, + Self::Edit(command) => return command.execute(state.state).map(Self::Edit), + } + } +} diff --git a/crates/tek_tui/src/tui_arrangement_foc.rs b/crates/tek_tui/src/tui_arrangement_foc.rs new file mode 100644 index 00000000..b7b85124 --- /dev/null +++ b/crates/tek_tui/src/tui_arrangement_foc.rs @@ -0,0 +1,100 @@ +use crate::*; + +#[derive(PartialEq, Clone, Copy)] +/// Represents the current user selection in the arranger +pub enum ArrangementEditorFocus { + /// The whole mix is selected + Mix, + /// A track is selected. + Track(usize), + /// A scene is selected. + Scene(usize), + /// A clip (track × scene) is selected. + Clip(usize, usize), +} + +/// Focus identification methods +impl ArrangementEditorFocus { + pub fn description ( + &self, + tracks: &Vec, + scenes: &Vec, + ) -> String { + format!("Selected: {}", match self { + Self::Mix => format!("Everything"), + Self::Track(t) => match tracks.get(*t) { + Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), + None => format!("T??"), + }, + Self::Scene(s) => match scenes.get(*s) { + Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), + None => format!("S??"), + }, + Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { + (Some(_), Some(scene)) => match scene.clip(*t) { + Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), + None => format!("T{t} S{s}: Empty") + }, + _ => format!("T{t} S{s}: Empty"), + } + }) + } + pub fn is_mix (&self) -> bool { match self { Self::Mix => true, _ => false } } + pub fn is_track (&self) -> bool { match self { Self::Track(_) => true, _ => false } } + pub fn is_scene (&self) -> bool { match self { Self::Scene(_) => true, _ => false } } + pub fn is_clip (&self) -> bool { match self { Self::Clip(_, _) => true, _ => false } } + pub fn track (&self) -> Option { + match self { Self::Clip(t, _) => Some(*t), Self::Track(t) => Some(*t), _ => None } + } + pub fn track_next (&mut self, last_track: usize) { + *self = match self { + Self::Mix => + Self::Track(0), + Self::Track(t) => + Self::Track(last_track.min(*t + 1)), + Self::Scene(s) => + Self::Clip(0, *s), + Self::Clip(t, s) => + Self::Clip(last_track.min(*t + 1), *s), + } + } + pub fn track_prev (&mut self) { + *self = match self { + Self::Mix => + Self::Mix, + Self::Scene(s) => + Self::Scene(*s), + Self::Track(t) => + if *t == 0 { Self::Mix } else { Self::Track(*t - 1) }, + Self::Clip(t, s) => + if *t == 0 { Self::Scene(*s) } else { Self::Clip(t.saturating_sub(1), *s) } + } + } + pub fn scene (&self) -> Option { + match self { Self::Clip(_, s) => Some(*s), Self::Scene(s) => Some(*s), _ => None } + } + pub fn scene_next (&mut self, last_scene: usize) { + *self = match self { + Self::Mix => + Self::Scene(0), + Self::Track(t) => + Self::Clip(*t, 0), + Self::Scene(s) => + Self::Scene(last_scene.min(*s + 1)), + Self::Clip(t, s) => + Self::Clip(*t, last_scene.min(*s + 1)), + } + } + pub fn scene_prev (&mut self) { + *self = match self { + Self::Mix => + Self::Mix, + Self::Track(t) => + Self::Track(*t), + Self::Scene(s) => + if *s == 0 { Self::Mix } else { Self::Scene(*s - 1) }, + Self::Clip(t, s) => + if *s == 0 { Self::Track(*t) } else { Self::Clip(*t, s.saturating_sub(1)) } + } + } +} diff --git a/crates/tek_tui/src/tui_arranger.rs b/crates/tek_tui/src/tui_arranger.rs index 0f1ce93d..1e3d7a6d 100644 --- a/crates/tek_tui/src/tui_arranger.rs +++ b/crates/tek_tui/src/tui_arranger.rs @@ -6,49 +6,39 @@ pub struct ArrangerView { pub sequencer: SequencerView, /// Contains all the sequencers. pub arrangement: ArrangementEditor, - /// Status bar - pub status: ArrangerStatusBar, /// Height of arrangement pub split: u16, /// Width and height of app at last render pub size: Measure, - /// Menu bar - pub menu: MenuBar, - /// Command history - pub history: Vec, - /// Which view is focused - pub cursor: (usize, usize), - /// Whether the focused view is entered - pub entered: bool, } impl Audio for ArrangerView { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - if let Some(ref transport) = self.transport { - transport.write().unwrap().process(client, scope); + #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + if self.sequencer.transport.process(client, scope) == Control::Quit { + return Control::Quit } - let Arrangement { scenes, ref mut tracks, selected, .. } = &mut self.arrangement; - for track in tracks.iter_mut() { - track.player.process(client, scope); + if self.arrangement.process(client, scope) == Control::Quit { + return Control::Quit } - if let ArrangementEditorFocus::Clip(t, s) = selected { - if let Some(Some(Some(phrase))) = scenes.get(*s).map(|scene|scene.clips.get(*t)) { - if let Some(track) = tracks.get(*t) { + if let ArrangementEditorFocus::Clip(t, s) = self.arrangement.selected { + let phrase = self.arrangement.state.scenes.get(s).map(|scene|scene.clips.get(t)); + if let Some(Some(Some(phrase))) = phrase { + if let Some(track) = self.arrangement.state.tracks.get(t) { if let Some((ref started_at, Some(ref playing))) = track.player.phrase { let phrase = phrase.read().unwrap(); if *playing.read().unwrap() == *phrase { let pulse = self.clock.current.pulse.get(); let start = started_at.pulse.get(); let now = (pulse - start) % phrase.length as f64; - self.editor.now.set(now); + self.sequencer.editor.now.set(now); return Control::Continue } } } } } - self.editor.now.set(0.); - Control::Continue + self.sequencer.editor.now.set(0.); + self.state.process(client, scope) } } @@ -57,9 +47,9 @@ impl Content for ArrangerView { type Engine = Tui; fn content (&self) -> impl Widget { let focused = self.arrangement.focused; - let border_bg = Arranger::::border_bg(); - let border_fg = Arranger::::border_fg(focused); - let title_fg = Arranger::::title_fg(focused); + let border_bg = TuiTheme::border_bg(); + let border_fg = TuiTheme::border_fg(focused); + let title_fg = TuiTheme::title_fg(focused); let border = Lozenge(Style::default().bg(border_bg).fg(border_fg)); let entered = if self.arrangement.entered { "■" } else { " " }; Split::down( @@ -95,109 +85,14 @@ impl Content for ArrangerView { /// General methods for arranger impl ArrangerView { pub fn new ( - jack: &Arc>, - transport: Option>>>, - arrangement: Arrangement, - phrases: Arc>>, + sequencer: SequencerView, + arrangement: ArrangementEditor, ) -> Self { - let mut app = Self { - jack: jack.clone(), - focus_cursor: (0, 1), - entered: false, - phrases_split: 20, - arrangement_split: 15, - editor: PhraseEditor::new(), - status: ArrangerStatusBar::ArrangementClip, - transport: transport.clone(), - arrangement, - phrases, - history: vec![], - size: Measure::new(), - clock: if let Some(ref transport) = transport { - transport.read().unwrap().clock.clone() - } else { - Arc::new(Clock::default()) - }, - menu: { - use ArrangerViewCommand::*; - MenuBar::new() - .add({ - use ArrangementCommand::*; - Menu::new("File") - .cmd("n", "New project", Arrangement(New)) - .cmd("l", "Load project", Arrangement(Load)) - .cmd("s", "Save project", Arrangement(Save)) - }) - .add({ - use TransportViewCommand::*; - Menu::new("Transport") - .cmd("p", "Play", Transport(Play)) - .cmd("s", "Play from start", Transport(PlayFromStart)) - .cmd("a", "Pause", Transport(Pause)) - }) - .add({ - use ArrangementCommand::*; - Menu::new("Track") - .cmd("a", "Append new", Arrangement(AddTrack)) - .cmd("i", "Insert new", Arrangement(AddTrack)) - .cmd("n", "Rename", Arrangement(AddTrack)) - .cmd("d", "Delete", Arrangement(AddTrack)) - .cmd(">", "Move up", Arrangement(AddTrack)) - .cmd("<", "Move down", Arrangement(AddTrack)) - }) - .add({ - use ArrangementCommand::*; - Menu::new("Scene") - .cmd("a", "Append new", Arrangement(AddScene)) - .cmd("i", "Insert new", Arrangement(AddTrack)) - .cmd("n", "Rename", Arrangement(AddTrack)) - .cmd("d", "Delete", Arrangement(AddTrack)) - .cmd(">", "Move up", Arrangement(AddTrack)) - .cmd("<", "Move down", Arrangement(AddTrack)) - }) - .add({ - use PhrasePoolCommand::*; - use PhraseRenameCommand as Rename; - use PhraseLengthCommand as Length; - Menu::new("Phrase") - .cmd("a", "Append new", Phrases(Append)) - .cmd("i", "Insert new", Phrases(Insert)) - .cmd("n", "Rename", Phrases(Rename(Rename::Begin))) - .cmd("t", "Set length", Phrases(Length(Length::Begin))) - .cmd("d", "Delete", Phrases(Delete)) - .cmd("l", "Load from MIDI...", Phrases(Import)) - .cmd("s", "Save to MIDI...", Phrases(Export)) - .cmd(">", "Move up", Phrases(MoveUp)) - .cmd("<", "Move down", Phrases(MoveDown)) - }) - } - }; + let mut app = Self { sequencer, arrangement, split: 15, size: Default::default() }; app.update_focus(); app } - //pub fn new ( - //jack: &Arc>, - //clock: &Arc, - //name: &str, - //phrases: &Arc>> - //) -> Self { - //Self { - //jack: jack.clone(), - //clock: clock.clone(), - //name: Arc::new(RwLock::new(name.into())), - //mode: ArrangementViewMode::Vertical(2), - //selected: ArrangementEditorFocus::Clip(0, 0), - //phrases: phrases.clone(), - //scenes: vec![], - //tracks: vec![], - //focused: false, - //color: Color::Rgb(28, 35, 25).into(), - //size: Measure::new(), - //entered: false, - //} - //} - /// Toggle global play/pause pub fn toggle_play (&mut self) -> Perhaps { match self.transport { @@ -216,10 +111,12 @@ impl ArrangerView { } } /// Focus the editor with the current phrase - pub fn show_phrase (&mut self) { self.editor.show(self.arrangement.phrase().as_ref()); } + pub fn show_phrase (&mut self) { + self.editor.show(self.arrangement.state.phrase().as_ref()); + } /// Focus the editor with the current phrase pub fn edit_phrase (&mut self) { - if self.arrangement.selected.is_clip() && self.arrangement.phrase().is_none() { + if self.arrangement.selected.is_clip() && self.arrangement.state.phrase().is_none() { self.phrases.write().unwrap().append_new(None, Some(self.next_color().into())); self.arrangement.phrase_put(); } @@ -284,7 +181,7 @@ impl ArrangerView { } } pub fn delete (&mut self) { - match self.selected { + match self.arrangement.selected { ArrangementEditorFocus::Track(_) => self.track_del(), ArrangementEditorFocus::Scene(_) => self.scene_del(), ArrangementEditorFocus::Clip(_, _) => self.phrase_del(), @@ -292,7 +189,7 @@ impl ArrangerView { } } pub fn increment (&mut self) { - match self.selected { + match self.arrangement.selected { ArrangementEditorFocus::Track(_) => self.track_width_inc(), ArrangementEditorFocus::Scene(_) => self.scene_next(), ArrangementEditorFocus::Clip(_, _) => self.phrase_next(), @@ -300,7 +197,7 @@ impl ArrangerView { } } pub fn decrement (&mut self) { - match self.selected { + match self.arrangement.selected { ArrangementEditorFocus::Track(_) => self.track_width_dec(), ArrangementEditorFocus::Scene(_) => self.scene_prev(), ArrangementEditorFocus::Clip(_, _) => self.phrase_prev(), @@ -308,13 +205,13 @@ impl ArrangerView { } } pub fn zoom_in (&mut self) { - if let ArrangementViewMode::Vertical(factor) = self.mode { - self.mode = ArrangementViewMode::Vertical(factor + 1) + if let ArrangementEditorMode::Vertical(factor) = self.mode { + self.mode = ArrangementEditorMode::Vertical(factor + 1) } } pub fn zoom_out (&mut self) { - if let ArrangementViewMode::Vertical(factor) = self.mode { - self.mode = ArrangementViewMode::Vertical(factor.saturating_sub(1)) + if let ArrangementEditorMode::Vertical(factor) = self.mode { + self.mode = ArrangementEditorMode::Vertical(factor.saturating_sub(1)) } } pub fn is_first_row (&self) -> bool { @@ -336,25 +233,25 @@ impl ArrangerView { } pub fn go_up (&mut self) { match self.mode { - ArrangementViewMode::Horizontal => self.track_prev(), + ArrangementEditorMode::Horizontal => self.track_prev(), _ => self.scene_prev(), }; } pub fn go_down (&mut self) { match self.mode { - ArrangementViewMode::Horizontal => self.track_next(), + ArrangementEditorMode::Horizontal => self.track_next(), _ => self.scene_next(), }; } pub fn go_left (&mut self) { match self.mode { - ArrangementViewMode::Horizontal => self.scene_prev(), + ArrangementEditorMode::Horizontal => self.scene_prev(), _ => self.track_prev(), }; } pub fn go_right (&mut self) { match self.mode { - ArrangementViewMode::Horizontal => self.scene_next(), + ArrangementEditorMode::Horizontal => self.scene_next(), _ => self.track_next(), }; } @@ -457,10 +354,10 @@ impl Arrangement { } /// Methods for scenes in arrangement impl Arrangement { - pub fn scene (&self) -> Option<&Scene> { + pub fn scene (&self) -> Option<&ArrangementScene> { self.selected.scene().map(|s|self.scenes.get(s)).flatten() } - pub fn scene_mut (&mut self) -> Option<&mut Scene> { + pub fn scene_mut (&mut self) -> Option<&mut ArrangementScene> { self.selected.scene().map(|s|self.scenes.get_mut(s)).flatten() } pub fn scene_next (&mut self) { @@ -573,7 +470,7 @@ impl ArrangementTrack { } /// Arranger display mode can be cycled -impl ArrangementViewMode { +impl ArrangementEditorMode { /// Cycle arranger display mode pub fn to_next (&mut self) { *self = match self { diff --git a/crates/tek_tui/src/tui_arranger_bar.rs b/crates/tek_tui/src/tui_arranger_bar.rs index 233cdca8..2118af03 100644 --- a/crates/tek_tui/src/tui_arranger_bar.rs +++ b/crates/tek_tui/src/tui_arranger_bar.rs @@ -11,6 +11,13 @@ pub enum ArrangerStatusBar { PhraseView, PhraseEdit, } + +impl StatusBar for ArrangerStatusBar { + fn hotkey_fg () -> Color { + TuiTheme::hotkey_fg() + } +} + impl Content for ArrangerStatusBar { type Engine = Tui; fn content (&self) -> impl Widget { @@ -24,19 +31,19 @@ impl Content for ArrangerStatusBar { Self::PhraseView => "VIEW SEQ", Self::PhraseEdit => "EDIT SEQ", }; - let status_bar_bg = Arranger::::status_bar_bg(); - let mode_bg = Arranger::::mode_bg(); - let mode_fg = Arranger::::mode_fg(); + let status_bar_bg = TuiTheme::status_bar_bg(); + let mode_bg = TuiTheme::mode_bg(); + let mode_fg = TuiTheme::mode_fg(); let mode = TuiStyle::bold(format!(" {label} "), true).bg(mode_bg).fg(mode_fg); let commands = match self { - Self::ArrangementMix => command(&[ + Self::ArrangementMix => Self::command(&[ ["", "c", "olor"], ["", "<>", "resize"], ["", "+-", "zoom"], ["", "n", "ame/number"], ["", "Enter", " stop all"], ]), - Self::ArrangementClip => command(&[ + Self::ArrangementClip => Self::command(&[ ["", "g", "et"], ["", "s", "et"], ["", "a", "dd"], @@ -48,7 +55,7 @@ impl Content for ArrangerStatusBar { ["", ",.", "select"], ["", "Enter", " launch"], ]), - Self::ArrangementTrack => command(&[ + Self::ArrangementTrack => Self::command(&[ ["re", "n", "ame"], ["", ",.", "resize"], ["", "<>", "move"], @@ -59,12 +66,12 @@ impl Content for ArrangerStatusBar { ["", "Del", "ete"], ["", "Enter", " stop"], ]), - Self::ArrangementScene => command(&[ + Self::ArrangementScene => Self::command(&[ ["re", "n", "ame"], ["", "Del", "ete"], ["", "Enter", " launch"], ]), - Self::PhrasePool => command(&[ + Self::PhrasePool => Self::command(&[ ["", "a", "ppend"], ["", "i", "nsert"], ["", "d", "uplicate"], @@ -75,34 +82,81 @@ impl Content for ArrangerStatusBar { ["", ",.", "move"], ["", "+-", "resize view"], ]), - Self::PhraseView => command(&[ + Self::PhraseView => Self::command(&[ ["", "enter", " edit"], ["", "arrows/pgup/pgdn", " scroll"], ["", "+=", "zoom"], ]), - Self::PhraseEdit => command(&[ + Self::PhraseEdit => Self::command(&[ ["", "esc", " exit"], ["", "a", "ppend"], ["", "s", "et"], ["", "][", "length"], ["", "+-", "zoom"], ]), - _ => command(&[]) + _ => Self::command(&[]) }; //let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); row!(mode, commands).fill_x().bg(status_bar_bg) } } -fn command (commands: &[[impl Widget;3]]) -> impl Widget + '_ { - Stack::right(|add|{ - Ok(for [a, b, c] in commands.iter() { - add(&row!( - " ", - widget(a), - widget(b).bold(true).fg(Arranger::::hotkey_fg()), - widget(c), - ))?; - }) - }) -} +//pub fn arranger_menu_bar () -> MenuBar { + //use ArrangementEditorCommand as Cmd; + //use ArrangementCommand as Edit; + //use ArrangementEditorFocus as Focus; + //use ArrangementTrackCommand as Track; + //use ArrangementClipCommand as Clip; + //use ArrangementSceneCommand as Scene; + //use TransportCommand as Transport; + //MenuBar::new() + //.add({ + //use ArrangementCommand::*; + //Menu::new("File") + //.cmd("n", "New project", ArrangerViewCommand::Arrangement(New)) + //.cmd("l", "Load project", ArrangerViewCommand::Arrangement(Load)) + //.cmd("s", "Save project", ArrangerViewCommand::Arrangement(Save)) + //}) + //.add({ + //use TransportViewCommand::*; + //Menu::new("Transport") + //.cmd("p", "Play", TransportCommand::Transport(Play(None))) + //.cmd("P", "Play from start", TransportCommand::Transport(Play(Some(0)))) + //.cmd("s", "Pause", TransportCommand::Transport(Stop(None))) + //.cmd("S", "Stop and rewind", TransportCommand::Transport(Stop(Some(0)))) + //}) + //.add({ + //use ArrangementCommand::*; + //Menu::new("Track") + //.cmd("a", "Append new", ArrangerViewCommand::Arrangement(AddTrack)) + //.cmd("i", "Insert new", ArrangerViewCommand::Arrangement(AddTrack)) + //.cmd("n", "Rename", ArrangerViewCommand::Arrangement(AddTrack)) + //.cmd("d", "Delete", ArrangerViewCommand::Arrangement(AddTrack)) + //.cmd(">", "Move up", ArrangerViewCommand::Arrangement(AddTrack)) + //.cmd("<", "Move down", ArrangerViewCommand::Arrangement(AddTrack)) + //}) + //.add({ + //use ArrangementCommand::*; + //Menu::new("Scene") + //.cmd("a", "Append new", ArrangerViewCommand::Arrangement(AddScene)) + //.cmd("i", "Insert new", ArrangerViewCommand::Arrangement(AddTrack)) + //.cmd("n", "Rename", ArrangerViewCommand::Arrangement(AddTrack)) + //.cmd("d", "Delete", ArrangerViewCommand::Arrangement(AddTrack)) + //.cmd(">", "Move up", ArrangerViewCommand::Arrangement(AddTrack)) + //.cmd("<", "Move down", ArrangerViewCommand::Arrangement(AddTrack)) + //}) + //.add({ + //use PhraseRenameCommand as Rename; + //use PhraseLengthCommand as Length; + //Menu::new("Phrase") + //.cmd("a", "Append new", PhrasePoolCommand::Phrases(Append)) + //.cmd("i", "Insert new", PhrasePoolCommand::Phrases(Insert)) + //.cmd("n", "Rename", PhrasePoolCommand::Phrases(Rename(Rename::Begin))) + //.cmd("t", "Set length", PhrasePoolCommand::Phrases(Length(Length::Begin))) + //.cmd("d", "Delete", PhrasePoolCommand::Phrases(Delete)) + //.cmd("l", "Load from MIDI...", PhrasePoolCommand::Phrases(Import)) + //.cmd("s", "Save to MIDI...", PhrasePoolCommand::Phrases(Export)) + //.cmd(">", "Move up", PhrasePoolCommand::Phrases(MoveUp)) + //.cmd("<", "Move down", PhrasePoolCommand::Phrases(MoveDown)) + //}) +//} diff --git a/crates/tek_tui/src/tui_arranger_cmd.rs b/crates/tek_tui/src/tui_arranger_cmd.rs index ab90ee65..2edc2047 100644 --- a/crates/tek_tui/src/tui_arranger_cmd.rs +++ b/crates/tek_tui/src/tui_arranger_cmd.rs @@ -4,10 +4,10 @@ use crate::*; pub enum ArrangerViewCommand { Focus(FocusCommand), Transport(TransportCommand), + Arrangement(ArrangementEditorCommand), + EditPhrase(Option>>), Phrases(PhrasePoolCommand), Editor(PhraseEditorCommand), - Arrangement(ArrangementCommand), - EditPhrase(Option>>), } /// Handle top-level events in standalone arranger. @@ -16,10 +16,10 @@ impl Handle for ArrangerView { if let Some(entered) = self.entered() { use ArrangerViewFocus::*; if let Some(true) = match entered { - Transport => self.transport.as_mut().map(|t|t.handle(i)).transpose()?.flatten(), + Transport => self.sequencer.transport.map(|t|t.handle(i)).transpose()?.flatten(), Arrangement => self.arrangement.handle(i)?, - PhrasePool => self.phrases.write().unwrap().handle(i)?, - PhraseEditor => self.editor.handle(i)?, + PhrasePool => self.sequencer.phrases.handle(i)?, + PhraseEditor => self.sequencer.editor.handle(i)?, } { return Ok(Some(true)) } @@ -70,44 +70,6 @@ impl InputToCommand> for ArrangerViewCommand { } } -/// Handle events for arrangement. -impl Handle for Arrangement { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - ArrangementCommand::execute_with_state(self, from) - } -} - -impl InputToCommand> for ArrangementCommand { - fn input_to_command (state: &Arrangement, input: &TuiInput) -> Option { - use ArrangementCommand::*; - match input.event() { - key!(KeyCode::Char('`')) => Some(ToggleViewMode), - key!(KeyCode::Delete) => Some(Delete), - key!(KeyCode::Enter) => Some(Activate), - key!(KeyCode::Char('.')) => Some(Increment), - key!(KeyCode::Char(',')) => Some(Decrement), - key!(KeyCode::Char('+')) => Some(ZoomIn), - key!(KeyCode::Char('=')) => Some(ZoomOut), - key!(KeyCode::Char('_')) => Some(ZoomOut), - key!(KeyCode::Char('-')) => Some(ZoomOut), - key!(KeyCode::Char('<')) => Some(MoveBack), - key!(KeyCode::Char('>')) => Some(MoveForward), - key!(KeyCode::Char('c')) => Some(RandomColor), - key!(KeyCode::Char('s')) => Some(Put), - key!(KeyCode::Char('g')) => Some(Get), - key!(KeyCode::Char('e')) => Some(Edit(state.phrase())), - key!(Ctrl-KeyCode::Char('a')) => Some(AddScene), - key!(Ctrl-KeyCode::Char('t')) => Some(AddTrack), - key!(KeyCode::Char('l')) => Some(ToggleLoop), - key!(KeyCode::Up) => Some(GoUp), - key!(KeyCode::Down) => Some(GoDown), - key!(KeyCode::Left) => Some(GoLeft), - key!(KeyCode::Right) => Some(GoRight), - _ => None - } - } -} - //impl ArrangerView { ///// Helper for event passthru to focused component //fn handle_focused (&mut self, from: &TuiInput) -> Perhaps { @@ -160,33 +122,6 @@ impl InputToCommand> for ArrangementCommand { //} //} -#[derive(Clone)] -pub enum ArrangementCommand { - New, - Load, - Save, - ToggleViewMode, - Delete, - Activate, - Increment, - Decrement, - ZoomIn, - ZoomOut, - MoveBack, - MoveForward, - RandomColor, - Put, - Get, - AddScene, - AddTrack, - ToggleLoop, - GoUp, - GoDown, - GoLeft, - GoRight, - Edit(Option>>), -} - impl Command> for ArrangerViewCommand { fn execute (self, state: &mut ArrangerView) -> Perhaps { let undo = match self { @@ -194,59 +129,28 @@ impl Command> for ArrangerViewCommand { delegate(cmd, Self::Focus, state) }, Self::Phrases(cmd) => { - delegate(cmd, Self::Phrases, &mut*state.phrases.write().unwrap()) + delegate(cmd, Self::Phrases, &mut*state.sequencer.phrases.write().unwrap()) }, Self::Editor(cmd) => { - delegate(cmd, Self::Editor, &mut state.editor) + delegate(cmd, Self::Editor, &mut state.sequencer.editor) }, Self::Arrangement(cmd) => { delegate(cmd, Self::Arrangement, &mut state.arrangement) }, - Self::Transport(cmd) => if let Some(ref transport) = state.transport { + Self::Transport(cmd) => if let Some(ref transport) = state.sequencer.transport { delegate(cmd, Self::Transport, &mut*transport.write().unwrap()) } else { Ok(None) }, Self::EditPhrase(phrase) => { - state.editor.phrase = phrase.clone(); + state.sequencer.editor.phrase = phrase.clone(); state.focus(ArrangerViewFocus::PhraseEditor); state.focus_enter(); Ok(None) } }?; - state.show_phrase(); - state.update_status(); + state.sequencer.show_phrase(); + state.sequencer.update_status(); return Ok(undo); } } -impl Command> for ArrangementCommand { - fn execute (self, state: &mut Arrangement) -> Perhaps { - use ArrangementCommand::*; - match self { - New => todo!(), - Load => todo!(), - Save => todo!(), - Edit(phrase) => { state.phrase = phrase.clone() }, - ToggleViewMode => { state.mode.to_next(); }, - Delete => { state.delete(); }, - Activate => { state.activate(); }, - Increment => { state.increment(); }, - Decrement => { state.decrement(); }, - ZoomIn => { state.zoom_in(); }, - ZoomOut => { state.zoom_out(); }, - MoveBack => { state.move_back(); }, - MoveForward => { state.move_forward(); }, - RandomColor => { state.randomize_color(); }, - Put => { state.phrase_put(); }, - Get => { state.phrase_get(); }, - AddScene => { state.scene_add(None, None)?; }, - AddTrack => { state.track_add(None, None)?; }, - ToggleLoop => { state.toggle_loop() }, - GoUp => { state.go_up() }, - GoDown => { state.go_down() }, - GoLeft => { state.go_left() }, - GoRight => { state.go_right() }, - }; - Ok(None) - } -} diff --git a/crates/tek_tui/src/tui_arranger_col.rs b/crates/tek_tui/src/tui_arranger_col.rs deleted file mode 100644 index d44cec54..00000000 --- a/crates/tek_tui/src/tui_arranger_col.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::*; - -pub trait ArrangerTheme { - fn border_bg () -> Color; - fn border_fg (focused: bool) -> Color; - fn title_fg (focused: bool) -> Color; - fn separator_fg (focused: bool) -> Color; - fn hotkey_fg () -> Color; - fn mode_bg () -> Color; - fn mode_fg () -> Color; - fn status_bar_bg () -> Color; -} - -impl ArrangerTheme for Arranger { - fn border_bg () -> Color { - Color::Rgb(40, 50, 30) - } - fn border_fg (focused: bool) -> Color { - if focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) } - } - fn title_fg (focused: bool) -> Color { - if focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) } - } - fn separator_fg (_: bool) -> Color { - Color::Rgb(0, 0, 0) - } - fn hotkey_fg () -> Color { - Color::Rgb(255, 255, 0) - } - fn mode_bg () -> Color { - Color::Rgb(150, 160, 90) - } - fn mode_fg () -> Color { - Color::Rgb(255, 255, 255) - } - fn status_bar_bg () -> Color { - Color::Rgb(28, 35, 25) - } -} diff --git a/crates/tek_tui/src/tui_pool.rs b/crates/tek_tui/src/tui_pool.rs index 9a59b0d2..0cedab7c 100644 --- a/crates/tek_tui/src/tui_pool.rs +++ b/crates/tek_tui/src/tui_pool.rs @@ -3,6 +3,8 @@ use crate::*; pub struct PhrasePoolView { _engine: PhantomData, state: PhrasePool, + /// Selected phrase + pub phrase: usize, /// Scroll offset pub scroll: usize, /// Mode switch @@ -21,68 +23,16 @@ pub enum PhrasePoolMode { Length(usize, usize, PhraseLengthFocus), } -/// Displays and edits phrase length. -pub struct PhraseLength { - _engine: PhantomData, - /// Pulses per beat (quaver) - pub ppq: usize, - /// Beats per bar - pub bpb: usize, - /// Length of phrase in pulses - pub pulses: usize, - /// Selected subdivision - pub focus: Option, -} - -impl PhraseLength { - pub fn new (pulses: usize, focus: Option) -> Self { - Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { self.pulses / (self.bpb * self.ppq) } - pub fn beats (&self) -> usize { (self.pulses % (self.bpb * self.ppq)) / self.ppq } - pub fn ticks (&self) -> usize { self.pulses % self.ppq } - pub fn bars_string (&self) -> String { format!("{}", self.bars()) } - pub fn beats_string (&self) -> String { format!("{}", self.beats()) } - pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) } -} - -/// Focused field of `PhraseLength` -#[derive(Copy, Clone)] pub enum PhraseLengthFocus { - /// Editing the number of bars - Bar, - /// Editing the number of beats - Beat, - /// Editing the number of ticks - Tick, -} - -impl PhraseLengthFocus { - pub fn next (&mut self) { - *self = match self { - Self::Bar => Self::Beat, - Self::Beat => Self::Tick, - Self::Tick => Self::Bar, - } - } - pub fn prev (&mut self) { - *self = match self { - Self::Bar => Self::Tick, - Self::Beat => Self::Bar, - Self::Tick => Self::Beat, - } - } -} - impl PhrasePool { - pub fn new () -> Self { + pub fn new (phrases: PhrasePool) -> Self { Self { _engine: Default::default(), scroll: 0, phrase: 0, - phrases: vec![Arc::new(RwLock::new(Phrase::default()))], mode: None, focused: false, entered: false, + phrases, } } pub fn len (&self) -> usize { self.phrases.len() } @@ -196,37 +146,3 @@ impl Content for PhrasePool { ) } } - -impl Content for PhraseLength { - type Engine = Tui; - fn content (&self) -> impl Widget { - Layers::new(move|add|{ - match self.focus { - None => add(&row!( - " ", self.bars_string(), - ".", self.beats_string(), - ".", self.ticks_string(), - " " - )), - Some(PhraseLengthFocus::Bar) => add(&row!( - "[", self.bars_string(), - "]", self.beats_string(), - ".", self.ticks_string(), - " " - )), - Some(PhraseLengthFocus::Beat) => add(&row!( - " ", self.bars_string(), - "[", self.beats_string(), - "]", self.ticks_string(), - " " - )), - Some(PhraseLengthFocus::Tick) => add(&row!( - " ", self.bars_string(), - ".", self.beats_string(), - "[", self.ticks_string(), - "]" - )), - } - }) - } -} diff --git a/crates/tek_tui/src/tui_pool_cmd.rs b/crates/tek_tui/src/tui_pool_cmd.rs index a85ca00f..8c54dba8 100644 --- a/crates/tek_tui/src/tui_pool_cmd.rs +++ b/crates/tek_tui/src/tui_pool_cmd.rs @@ -1,109 +1,52 @@ use crate::*; #[derive(Clone, PartialEq)] -pub enum PhrasePoolCommand { - Prev, - Next, - MoveUp, - MoveDown, - Delete, - Append, - Insert, - Duplicate, - RandomColor, - Edit, - Import, - Export, +pub enum PhrasePoolViewCommand { + Edit(PhrasePoolCommand), Rename(PhraseRenameCommand), Length(PhraseLengthCommand), } -#[derive(Clone, PartialEq)] -pub enum PhraseRenameCommand { - Begin, - Backspace, - Append(char), - Set(String), - Confirm, - Cancel, -} - -#[derive(Clone, PartialEq)] -pub enum PhraseLengthCommand { - Begin, - Next, - Prev, - Inc, - Dec, - Set(usize), - Confirm, - Cancel, -} - -impl Handle for PhrasePool { +impl Handle for PhrasePoolView { fn handle (&mut self, from: &TuiInput) -> Perhaps { - if let Some(command) = PhrasePoolCommand::input_to_command(self, from) { - let _undo = command.execute(self)?; - return Ok(Some(true)) - } - Ok(None) + PhrasePoolViewCommand::execute_with_state(self, from) } } -impl InputToCommand> for PhrasePoolCommand { - fn input_to_command (state: &PhrasePool, input: &TuiInput) -> Option { +impl InputToCommand> for PhrasePoolViewCommand { + fn input_to_command (state: &PhrasePoolView, input: &TuiInput) -> Option { + use PhrasePoolViewCommand as Cmd; + use PhrasePoolCommand as Edit; + use PhraseRenameCommand as Rename; + use PhraseLengthCommand as Length; match input.event() { - key!(KeyCode::Up) => Some(Self::Prev), - key!(KeyCode::Down) => Some(Self::Next), - key!(KeyCode::Char(',')) => Some(Self::MoveUp), - key!(KeyCode::Char('.')) => Some(Self::MoveDown), - key!(KeyCode::Delete) => Some(Self::Delete), - key!(KeyCode::Char('a')) => Some(Self::Append), - key!(KeyCode::Char('i')) => Some(Self::Insert), - key!(KeyCode::Char('d')) => Some(Self::Duplicate), - key!(KeyCode::Char('c')) => Some(Self::RandomColor), - key!(KeyCode::Char('n')) => Some(Self::Rename(PhraseRenameCommand::Begin)), - key!(KeyCode::Char('t')) => Some(Self::Length(PhraseLengthCommand::Begin)), + key!(KeyCode::Up) => Some(Cmd::Edit(Edit::Select(0))), + key!(KeyCode::Down) => Some(Cmd::Edit(Edit::Select(0))), + key!(KeyCode::Char(',')) => Some(Cmd::Edit(Edit::Swap(0))), + key!(KeyCode::Char('.')) => Some(Cmd::Edit(Edit::Swap(0))), + key!(KeyCode::Delete) => Some(Cmd::Edit(Edit::Delete(0))), + key!(KeyCode::Char('a')) => Some(Cmd::Edit(Edit::Add(0))), + key!(KeyCode::Char('i')) => Some(Cmd::Edit(Edit::Add(0))), + key!(KeyCode::Char('d')) => Some(Cmd::Edit(Edit::Duplicate(0))), + key!(KeyCode::Char('c')) => Some(Cmd::Edit(Edit::RandomColor(0))), + key!(KeyCode::Char('n')) => Some(Cmd::Rename(Rename::Begin)), + key!(KeyCode::Char('t')) => Some(Cmd::Length(Length::Begin)), _ => match state.mode { - Some(PhrasePoolMode::Rename(..)) => PhraseRenameCommand::input_to_command(state, input) - .map(Self::Rename), - Some(PhrasePoolMode::Length(..)) => PhraseLengthCommand::input_to_command(state, input) - .map(Self::Length), + Some(PhrasePoolMode::Rename(..)) => { + Rename::input_to_command(state, input).map(Cmd::Rename) + }, + Some(PhrasePoolMode::Length(..)) => { + Length::input_to_command(state, input).map(Cmd::Length) + }, _ => None } } } } -impl InputToCommand> for PhraseRenameCommand { - fn input_to_command (_: &PhrasePool, from: &TuiInput) -> Option { - match from.event() { - key!(KeyCode::Backspace) => Some(Self::Backspace), - key!(KeyCode::Enter) => Some(Self::Confirm), - key!(KeyCode::Esc) => Some(Self::Cancel), - key!(KeyCode::Char(c)) => Some(Self::Append(*c)), - _ => None - } - } -} - -impl InputToCommand> for PhraseLengthCommand { - fn input_to_command (_: &PhrasePool, from: &TuiInput) -> Option { - match from.event() { - key!(KeyCode::Up) => Some(Self::Inc), - key!(KeyCode::Down) => Some(Self::Dec), - key!(KeyCode::Right) => Some(Self::Next), - key!(KeyCode::Left) => Some(Self::Prev), - key!(KeyCode::Enter) => Some(Self::Confirm), - key!(KeyCode::Esc) => Some(Self::Cancel), - _ => None - } - } -} - -impl Command> for PhrasePoolCommand { - fn execute (self, state: &mut PhrasePool) -> Perhaps { - use PhrasePoolCommand::*; +impl Command> for PhrasePoolViewCommand { + fn execute (self, state: &mut PhrasePoolView) -> Perhaps { + use PhrasePoolViewCommand::*; use PhraseRenameCommand as Rename; use PhraseLengthCommand as Length; match self { @@ -123,103 +66,3 @@ impl Command> for PhrasePoolCommand { Ok(None) } } - -impl Command> for PhraseRenameCommand { - fn translate (self, state: &PhrasePool) -> Self { - use PhraseRenameCommand::*; - if let Some(PhrasePoolMode::Rename(_, ref old_name)) = state.mode { - match self { - Backspace => { - let mut new_name = old_name.clone(); - new_name.pop(); - return Self::Set(new_name) - }, - Append(c) => { - let mut new_name = old_name.clone(); - new_name.push(c); - return Self::Set(new_name) - }, - _ => {} - } - } else if self != Begin { - unreachable!() - } - self - } - fn execute (self, state: &mut PhrasePool) -> Perhaps { - use PhraseRenameCommand::*; - if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = state.mode { - match self { - Set(s) => { - state.phrases[phrase].write().unwrap().name = s.into(); - return Ok(Some(Self::Set(old_name.clone()))) - }, - Confirm => { - let old_name = old_name.clone(); - state.mode = None; - return Ok(Some(Self::Set(old_name))) - }, - Cancel => { - let mut phrase = state.phrases[phrase].write().unwrap(); - phrase.name = old_name.clone(); - }, - _ => unreachable!() - }; - Ok(None) - } else if self == Begin { - todo!() - } else { - unreachable!() - } - } -} - -impl Command> for PhraseLengthCommand { - fn translate (self, state: &PhrasePool) -> Self { - use PhraseLengthCommand::*; - if let Some(PhrasePoolMode::Length(_, length, _)) = state.mode { - match self { - Confirm => { return Self::Set(length) }, - _ => self - } - } else if self == Begin { - todo!() - } else { - unreachable!() - } - } - fn execute (self, state: &mut PhrasePool) -> Perhaps { - use PhraseLengthFocus::*; - use PhraseLengthCommand::*; - if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = state.mode { - match self { - Cancel => { state.mode = None; }, - Prev => { focus.prev() }, - Next => { focus.next() }, - Inc => match focus { - Bar => { *length += 4 * PPQ }, - Beat => { *length += PPQ }, - Tick => { *length += 1 }, - }, - Dec => match focus { - Bar => { *length = length.saturating_sub(4 * PPQ) }, - Beat => { *length = length.saturating_sub(PPQ) }, - Tick => { *length = length.saturating_sub(1) }, - }, - Set(length) => { - let mut phrase = state.phrases[phrase].write().unwrap(); - let old_length = phrase.length; - phrase.length = length; - state.mode = None; - return Ok(Some(Self::Set(old_length))) - }, - _ => unreachable!() - } - Ok(None) - } else if self == Begin { - todo!() - } else { - unreachable!() - } - } -} diff --git a/crates/tek_tui/src/tui_pool_length.rs b/crates/tek_tui/src/tui_pool_length.rs new file mode 100644 index 00000000..516eeab1 --- /dev/null +++ b/crates/tek_tui/src/tui_pool_length.rs @@ -0,0 +1,176 @@ +use crate::*; + +/// Displays and edits phrase length. +pub struct PhraseLength { + _engine: PhantomData, + /// Pulses per beat (quaver) + pub ppq: usize, + /// Beats per bar + pub bpb: usize, + /// Length of phrase in pulses + pub pulses: usize, + /// Selected subdivision + pub focus: Option, +} + +impl PhraseLength { + pub fn new (pulses: usize, focus: Option) -> Self { + Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { + self.pulses / (self.bpb * self.ppq) + } + pub fn beats (&self) -> usize { + (self.pulses % (self.bpb * self.ppq)) / self.ppq + } + pub fn ticks (&self) -> usize { + self.pulses % self.ppq + } + pub fn bars_string (&self) -> String { + format!("{}", self.bars()) + } + pub fn beats_string (&self) -> String { + format!("{}", self.beats()) + } + pub fn ticks_string (&self) -> String { + format!("{:>02}", self.ticks()) + } +} + +impl Content for PhraseLength { + type Engine = Tui; + fn content (&self) -> impl Widget { + Layers::new(move|add|{ + match self.focus { + None => add(&row!( + " ", self.bars_string(), + ".", self.beats_string(), + ".", self.ticks_string(), + " " + )), + Some(PhraseLengthFocus::Bar) => add(&row!( + "[", self.bars_string(), + "]", self.beats_string(), + ".", self.ticks_string(), + " " + )), + Some(PhraseLengthFocus::Beat) => add(&row!( + " ", self.bars_string(), + "[", self.beats_string(), + "]", self.ticks_string(), + " " + )), + Some(PhraseLengthFocus::Tick) => add(&row!( + " ", self.bars_string(), + ".", self.beats_string(), + "[", self.ticks_string(), + "]" + )), + } + }) + } +} + +/// Focused field of `PhraseLength` +#[derive(Copy, Clone)] +pub enum PhraseLengthFocus { + /// Editing the number of bars + Bar, + /// Editing the number of beats + Beat, + /// Editing the number of ticks + Tick, +} + +impl PhraseLengthFocus { + pub fn next (&mut self) { + *self = match self { + Self::Bar => Self::Beat, + Self::Beat => Self::Tick, + Self::Tick => Self::Bar, + } + } + pub fn prev (&mut self) { + *self = match self { + Self::Bar => Self::Tick, + Self::Beat => Self::Bar, + Self::Tick => Self::Beat, + } + } +} + +#[derive(Clone, PartialEq)] +pub enum PhraseLengthCommand { + Begin, + Next, + Prev, + Inc, + Dec, + Set(usize), + Confirm, + Cancel, +} + +impl InputToCommand> for PhraseLengthCommand { + fn input_to_command (_: &PhrasePoolView, from: &TuiInput) -> Option { + match from.event() { + key!(KeyCode::Up) => Some(Self::Inc), + key!(KeyCode::Down) => Some(Self::Dec), + key!(KeyCode::Right) => Some(Self::Next), + key!(KeyCode::Left) => Some(Self::Prev), + key!(KeyCode::Enter) => Some(Self::Confirm), + key!(KeyCode::Esc) => Some(Self::Cancel), + _ => None + } + } +} + +impl Command> for PhraseLengthCommand { + fn translate (self, state: &PhrasePoolView) -> Self { + use PhraseLengthCommand::*; + if let Some(PhrasePoolMode::Length(_, length, _)) = state.mode { + match self { + Confirm => { return Self::Set(length) }, + _ => self + } + } else if self == Begin { + todo!() + } else { + unreachable!() + } + } + fn execute (self, state: &mut PhrasePoolView) -> Perhaps { + use PhraseLengthFocus::*; + use PhraseLengthCommand::*; + if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = state.mode { + match self { + Cancel => { state.mode = None; }, + Prev => { focus.prev() }, + Next => { focus.next() }, + Inc => match focus { + Bar => { *length += 4 * PPQ }, + Beat => { *length += PPQ }, + Tick => { *length += 1 }, + }, + Dec => match focus { + Bar => { *length = length.saturating_sub(4 * PPQ) }, + Beat => { *length = length.saturating_sub(PPQ) }, + Tick => { *length = length.saturating_sub(1) }, + }, + Set(length) => { + let mut phrase = state.phrases[phrase].write().unwrap(); + let old_length = phrase.length; + phrase.length = length; + state.mode = None; + return Ok(Some(Self::Set(old_length))) + }, + _ => unreachable!() + } + Ok(None) + } else if self == Begin { + todo!() + } else { + unreachable!() + } + } +} diff --git a/crates/tek_tui/src/tui_pool_rename.rs b/crates/tek_tui/src/tui_pool_rename.rs new file mode 100644 index 00000000..4b263db9 --- /dev/null +++ b/crates/tek_tui/src/tui_pool_rename.rs @@ -0,0 +1,73 @@ +use crate::*; + +#[derive(Clone, PartialEq)] +pub enum PhraseRenameCommand { + Begin, + Backspace, + Append(char), + Set(String), + Confirm, + Cancel, +} + +impl InputToCommand> for PhraseRenameCommand { + fn input_to_command (_: &PhrasePoolView, from: &TuiInput) -> Option { + match from.event() { + key!(KeyCode::Backspace) => Some(Self::Backspace), + key!(KeyCode::Enter) => Some(Self::Confirm), + key!(KeyCode::Esc) => Some(Self::Cancel), + key!(KeyCode::Char(c)) => Some(Self::Append(*c)), + _ => None + } + } +} + +impl Command> for PhraseRenameCommand { + fn translate (self, state: &PhrasePoolView) -> Self { + use PhraseRenameCommand::*; + if let Some(PhrasePoolMode::Rename(_, ref old_name)) = state.mode { + match self { + Backspace => { + let mut new_name = old_name.clone(); + new_name.pop(); + return Self::Set(new_name) + }, + Append(c) => { + let mut new_name = old_name.clone(); + new_name.push(c); + return Self::Set(new_name) + }, + _ => {} + } + } else if self != Begin { + unreachable!() + } + self + } + fn execute (self, state: &mut PhrasePoolView) -> Perhaps { + use PhraseRenameCommand::*; + if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = state.mode { + match self { + Set(s) => { + state.phrases[phrase].write().unwrap().name = s.into(); + return Ok(Some(Self::Set(old_name.clone()))) + }, + Confirm => { + let old_name = old_name.clone(); + state.mode = None; + return Ok(Some(Self::Set(old_name))) + }, + Cancel => { + let mut phrase = state.phrases[phrase].write().unwrap(); + phrase.name = old_name.clone(); + }, + _ => unreachable!() + }; + Ok(None) + } else if self == Begin { + todo!() + } else { + unreachable!() + } + } +} diff --git a/crates/tek_tui/src/tui_sequencer.rs b/crates/tek_tui/src/tui_sequencer.rs index 164db388..f34fd87d 100644 --- a/crates/tek_tui/src/tui_sequencer.rs +++ b/crates/tek_tui/src/tui_sequencer.rs @@ -13,10 +13,6 @@ pub struct SequencerView { pub editor: PhraseEditor, /// Phrase player pub player: MIDIPlayer, - /// Which view is focused - pub cursor: (usize, usize), - /// Whether the currently focused item is entered - pub entered: bool, } /// JACK process callback for sequencer app diff --git a/crates/tek_tui/src/tui_sequencer_bar.rs b/crates/tek_tui/src/tui_sequencer_bar.rs index 9efe09e6..59c1c55b 100644 --- a/crates/tek_tui/src/tui_sequencer_bar.rs +++ b/crates/tek_tui/src/tui_sequencer_bar.rs @@ -6,3 +6,17 @@ pub enum SequencerStatusBar { PhrasePool, PhraseEditor, } + +impl StatusBar for SequencerStatusBar { + fn hotkey_fg () -> Color { + TuiTheme::hotkey_fg() + } +} + +impl Content for SequencerStatusBar { + type Engine = Tui; + fn content (&self) -> impl Widget { + todo!(); + "" + } +} diff --git a/crates/tek_tui/src/tui_theme.rs b/crates/tek_tui/src/tui_theme.rs new file mode 100644 index 00000000..a4be1110 --- /dev/null +++ b/crates/tek_tui/src/tui_theme.rs @@ -0,0 +1,30 @@ +use crate::*; + +pub struct TuiTheme; + +impl TuiTheme { + pub fn border_bg () -> Color { + Color::Rgb(40, 50, 30) + } + pub fn border_fg (focused: bool) -> Color { + if focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) } + } + pub fn title_fg (focused: bool) -> Color { + if focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) } + } + pub fn separator_fg (_: bool) -> Color { + Color::Rgb(0, 0, 0) + } + pub fn hotkey_fg () -> Color { + Color::Rgb(255, 255, 0) + } + pub fn mode_bg () -> Color { + Color::Rgb(150, 160, 90) + } + pub fn mode_fg () -> Color { + Color::Rgb(255, 255, 255) + } + pub fn status_bar_bg () -> Color { + Color::Rgb(28, 35, 25) + } +} diff --git a/crates/tek_tui/src/tui_transport.rs b/crates/tek_tui/src/tui_transport.rs index 7e40de48..02487ef3 100644 --- a/crates/tek_tui/src/tui_transport.rs +++ b/crates/tek_tui/src/tui_transport.rs @@ -8,6 +8,7 @@ pub struct TransportView { state: Transport, focus: TransportViewFocus, focused: bool, + size: Measure, } /// JACK process callback for transport app diff --git a/crates/tek_tui/src/tui_transport_bar.rs b/crates/tek_tui/src/tui_transport_bar.rs index c7b7e813..bc8d33f2 100644 --- a/crates/tek_tui/src/tui_transport_bar.rs +++ b/crates/tek_tui/src/tui_transport_bar.rs @@ -1 +1,17 @@ use crate::*; + +pub struct TransportStatusBar; + +impl StatusBar for TransportStatusBar { + fn hotkey_fg () -> Color { + TuiTheme::hotkey_fg() + } +} + +impl Content for TransportStatusBar { + type Engine = Tui; + fn content (&self) -> impl Widget { + todo!(); + "" + } +}