diff --git a/crates/tek_api/src/status.rs b/crates/tek_api/src/status.rs index b7df3005..c7b7e813 100644 --- a/crates/tek_api/src/status.rs +++ b/crates/tek_api/src/status.rs @@ -1,25 +1 @@ use crate::*; - -pub trait StatusBar: Widget { - fn hotkey_fg () -> Color where Self: Sized; - - fn update (&mut self, state: &S); - - fn command (commands: &[[impl Widget;3]]) - -> impl Widget + '_ - where - Self: Sized - { - 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/engine.rs b/crates/tek_core/src/engine.rs index 6fdb692b..aa0c5bee 100644 --- a/crates/tek_core/src/engine.rs +++ b/crates/tek_core/src/engine.rs @@ -122,6 +122,21 @@ impl> Widget for Option { self.as_ref().map(|widget|widget.render(to)).unwrap_or(Ok(())) } } +/// Render either of two widgets depending on predicate +pub struct Either, B: Widget>( + pub bool, + pub A, + pub B, +); +impl, B: Widget> Widget for Either { + type Engine = E; + fn layout (&self, to: E::Size) -> Perhaps { + if self.0 { self.1.layout(to) } else { self.2.layout(to) } + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + if self.0 { self.1.render(to) } else { self.2.render(to) } + } +} /// A custom [Widget] defined by passing layout and render closures in place. pub struct CustomWidget< E: Engine, diff --git a/crates/tek_tui/src/lib.rs b/crates/tek_tui/src/lib.rs index 49df6db4..664f5641 100644 --- a/crates/tek_tui/src/lib.rs +++ b/crates/tek_tui/src/lib.rs @@ -10,73 +10,69 @@ pub(crate) use std::path::PathBuf; pub(crate) use std::ffi::OsString; pub(crate) use std::fs::read_dir; +use std::fmt::Debug; + submod! { tui_arranger - tui_arranger_bar - tui_arranger_cmd - tui_arranger_foc - tui_arranger_hor - tui_arranger_ver - //tui_mixer // TODO - //tui_mixer_cmd - tui_phrase - tui_phrase_cmd - //tui_plugin // TODO - //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 // TODO //tui_sampler_cmd - tui_sequencer - tui_sequencer_bar - tui_sequencer_cmd - tui_sequencer_foc - + tui_status tui_theme - tui_transport - tui_transport_bar - tui_transport_cmd - tui_transport_foc } -pub struct AppView +pub struct AppView where E: Engine, A: Widget + Handle + Audio, C: Command, + S: StatusBar, { pub app: A, pub cursor: (usize, usize), pub entered: bool, pub menu_bar: Option>, - pub status_bar: Option>>, + pub status_bar: Option, pub history: Vec, pub size: Measure, } -impl AppView +#[derive(Debug, Copy, Clone)] +pub enum AppViewCommand { + Focus(FocusCommand), + Undo, + Redo, + App(T) +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum AppViewFocus { + Menu, + Content(F), +} + +impl AppView where E: Engine, A: Widget + Handle + Audio, - C: Command + C: Command, + S: StatusBar { pub fn new ( app: A, menu_bar: Option>, - status_bar: Option>>, + status_bar: Option, ) -> Self { Self { app, @@ -90,10 +86,11 @@ where } } -impl Content for AppView +impl Content for AppView where A: Widget + Handle + Audio, - C: Command + C: Command, + S: StatusBar, { type Engine = Tui; fn content (&self) -> impl Widget { @@ -106,25 +103,15 @@ where row!(menu in menus.iter() => { row!(" ", menu.title.as_str(), " ") }), - Split::up( - if self.status_bar.is_some() { 1 } else { 0 }, - widget(&self.status_bar), + Either( + self.status_bar.is_some(), + Split::up( + 1, + widget(self.status_bar.as_ref().unwrap()), + widget(&self.app) + ), widget(&self.app) ) ) } } - -#[derive(Debug, Copy, Clone)] -pub enum AppViewCommand { - Focus(FocusCommand), - Undo, - Redo, - App(T) -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum AppViewFocus { - Menu, - Content(F), -} diff --git a/crates/tek_tui/src/tui_arranger.rs b/crates/tek_tui/src/tui_arranger.rs index 8fded36f..2440ae84 100644 --- a/crates/tek_tui/src/tui_arranger.rs +++ b/crates/tek_tui/src/tui_arranger.rs @@ -1,6 +1,92 @@ use crate::*; -pub type ArrangerApp = AppView, ArrangerViewCommand>; +pub type ArrangerApp = AppView< + E, + ArrangerView, + ArrangerViewCommand, + ArrangerStatusBar +>; + +/// Root level object for standalone `tek_arranger` +pub struct ArrangerView { + pub model: ArrangerModel, + /// Sequencer component + pub sequencer: SequencerView, + /// Height of arrangement + pub split: u16, + /// Currently selected element. + pub selected: ArrangerSelection, + /// Display mode of arranger + pub mode: ArrangerMode, + /// Background color of arrangement + pub color: ItemColor, + /// Whether the arranger is currently focused + pub focused: bool, + /// Whether this is currently in edit mode + pub entered: bool, + /// Width and height of arrangement area at last render + pub size: Measure, +} + +/// Display mode of arranger +#[derive(Clone, PartialEq)] +pub enum ArrangerMode { + /// Tracks are rows + Horizontal, + /// Tracks are columns + Vertical(usize), +} + +#[derive(Clone)] +pub enum ArrangerViewCommand { + Focus(FocusCommand), + Edit(ArrangerCommand), + Select(ArrangerSelection), + Zoom(usize), + Transport(TransportViewCommand), + Phrases(PhrasePoolViewCommand), + Editor(PhraseEditorCommand), + EditPhrase(Option>>), +} + +/// Sections in the arranger app that may be focused +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum ArrangerViewFocus { + /// The transport (toolbar) is focused + Transport, + /// The arrangement (grid) is focused + Arranger, + /// The phrase list (pool) is focused + PhrasePool, + /// The phrase editor (sequencer) is focused + PhraseEditor, +} + +#[derive(PartialEq, Clone, Copy)] +/// Represents the current user selection in the arranger +pub enum ArrangerSelection { + /// 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), +} + +/// Status bar for arranger app +#[derive(Copy, Clone)] +pub enum ArrangerStatusBar { + Transport, + ArrangerMix, + ArrangerTrack, + ArrangerScene, + ArrangerClip, + PhrasePool, + PhraseView, + PhraseEdit, +} impl TryFrom<&Arc>> for ArrangerApp { type Error = Box; @@ -26,48 +112,18 @@ impl From for ArrangerView { model, sequencer: SequencerView::from(&model.sequencer), split: 20, - selected: ArrangerFocus::Clip(0, 0), + selected: ArrangerSelection::Clip(0, 0), mode: ArrangerMode::Vertical(2), color: Color::Rgb(28, 35, 25).into(), size: Measure::new(), focused: false, entered: false, }; - view.update_focus(); + //view.update_focus(); view } } -/// Root level object for standalone `tek_arranger` -pub struct ArrangerView { - pub model: ArrangerModel, - /// Sequencer component - pub sequencer: SequencerView, - /// Height of arrangement - pub split: u16, - /// Currently selected element. - pub selected: ArrangerFocus, - /// Display mode of arranger - pub mode: ArrangerMode, - /// Background color of arrangement - pub color: ItemColor, - /// Whether the arranger is currently focused - pub focused: bool, - /// Whether this is currently in edit mode - pub entered: bool, - /// Width and height of arrangement area at last render - pub size: Measure, -} - -/// Display mode of arranger -#[derive(Clone, PartialEq)] -pub enum ArrangerMode { - /// Tracks are rows - Horizontal, - /// Tracks are columns - Vertical(usize), -} - /// Arranger display mode can be cycled impl ArrangerMode { /// Cycle arranger display mode @@ -136,7 +192,7 @@ impl ArrangerView { let scenes = self.model.scenes(); let tracks = self.model.tracks_mut(); match self.selected { - ArrangerFocus::Scene(s) => { + ArrangerSelection::Scene(s) => { for (t, track) in tracks.iter_mut().enumerate() { let player = &mut track.player; let clip = scenes[s].clips[t].as_ref(); @@ -150,7 +206,7 @@ impl ArrangerView { //self.transport.toggle_play() //} }, - ArrangerFocus::Clip(t, s) => { + ArrangerSelection::Clip(t, s) => { tracks[t].player.enqueue_next(scenes[s].clips[t]); }, _ => {} @@ -165,8 +221,8 @@ impl ArrangerView { pub fn is_last_row (&self) -> bool { let selected = self.selected; (self.model.scenes().len() == 0 && (selected.is_mix() || selected.is_track())) || match selected { - ArrangerFocus::Scene(s) => s == self.model.scenes().len() - 1, - ArrangerFocus::Clip(_, s) => s == self.model.scenes().len() - 1, + ArrangerSelection::Scene(s) => s == self.model.scenes().len() - 1, + ArrangerSelection::Clip(_, s) => s == self.model.scenes().len() - 1, _ => false } } @@ -179,16 +235,16 @@ impl ArrangerView { pub fn randomize_color (&mut self) { match self.selected { - ArrangerFocus::Mix => { + ArrangerSelection::Mix => { self.color = ItemColor::random_dark() }, - ArrangerFocus::Track(t) => { + ArrangerSelection::Track(t) => { self.model.tracks_mut()[t].color = ItemColor::random() }, - ArrangerFocus::Scene(s) => { + ArrangerSelection::Scene(s) => { self.model.scenes_mut()[s].color = ItemColor::random() }, - ArrangerFocus::Clip(t, s) => { + ArrangerSelection::Clip(t, s) => { if let Some(phrase) = &self.model.scenes_mut()[s].clips[t] { phrase.write().unwrap().color = ItemColorTriplet::random(); } @@ -212,7 +268,7 @@ impl Audio for ArrangerView { return Control::Quit } // FIXME: one of these per playing track - if let ArrangerFocus::Clip(t, s) = self.selected { + if let ArrangerSelection::Clip(t, s) = self.selected { let phrase = self.model.scenes().get(s).map(|scene|scene.clips.get(t)); if let Some(Some(Some(phrase))) = phrase { if let Some(track) = self.model.tracks().get(t) { @@ -233,3 +289,994 @@ impl Audio for ArrangerView { return Control::Continue } } + +/// Handle top-level events in standalone arranger. +impl Handle for ArrangerView { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + ArrangerViewCommand::execute_with_state(self, i) + } +} + +impl InputToCommand> for ArrangerViewCommand { + fn input_to_command (view: &ArrangerView, input: &TuiInput) -> Option { + use FocusCommand::*; + use ArrangerViewCommand::*; + Some(match input.event() { + key!(KeyCode::Tab) => Focus(Next), + key!(Shift-KeyCode::Tab) => Focus(Prev), + key!(KeyCode::BackTab) => Focus(Prev), + key!(Shift-KeyCode::BackTab) => Focus(Prev), + key!(KeyCode::Up) => Focus(Up), + key!(KeyCode::Down) => Focus(Down), + key!(KeyCode::Left) => Focus(Left), + key!(KeyCode::Right) => Focus(Right), + key!(KeyCode::Enter) => Focus(Enter), + key!(KeyCode::Esc) => Focus(Exit), + key!(KeyCode::Char(' ')) => { + Transport(TransportViewCommand::Transport(TransportCommand::Play(None))) + }, + _ => match view.focused() { + ArrangerViewFocus::Transport => Transport( + TransportViewCommand::input_to_command(&view.sequencer.transport, input)? + ), + ArrangerViewFocus::PhraseEditor => Editor( + PhraseEditorCommand::input_to_command(&view.sequencer.editor, input)? + ), + ArrangerViewFocus::PhrasePool => match input.event() { + key!(KeyCode::Char('e')) => EditPhrase( + Some(view.sequencer.phrases.phrase().clone()) + ), + _ => Phrases( + PhrasePoolViewCommand::input_to_command(&view.sequencer.phrases, input)? + ) + }, + ArrangerViewFocus::Arranger => { + use ArrangerSelection as Focus; + use ArrangerCommand as Model; + use ArrangerTrackCommand as Track; + use ArrangerClipCommand as Clip; + use ArrangerSceneCommand as Scene; + match input.event() { + key!(KeyCode::Char('e')) => EditPhrase( + view.selected_phrase() + ), + _ => match input.event() { + // FIXME: boundary conditions + + key!(KeyCode::Up) => match view.selected { + ArrangerSelection::Mix => return None, + ArrangerSelection::Track(t) => return None, + ArrangerSelection::Scene(s) => Select(Focus::Scene(s - 1)), + ArrangerSelection::Clip(t, s) => Select(Focus::Clip(t, s - 1)), + }, + + key!(KeyCode::Down) => match view.selected { + ArrangerSelection::Mix => Select(Focus::Scene(0)), + ArrangerSelection::Track(t) => Select(Focus::Clip(t, 0)), + ArrangerSelection::Scene(s) => Select(Focus::Scene(s + 1)), + ArrangerSelection::Clip(t, s) => Select(Focus::Clip(t, s + 1)), + }, + + key!(KeyCode::Left) => match view.selected { + ArrangerSelection::Mix => return None, + ArrangerSelection::Track(t) => Select(Focus::Track(t - 1)), + ArrangerSelection::Scene(s) => return None, + ArrangerSelection::Clip(t, s) => Select(Focus::Clip(t - 1, s)), + }, + + key!(KeyCode::Right) => match view.selected { + ArrangerSelection::Mix => return None, + ArrangerSelection::Track(t) => Select(Focus::Track(t + 1)), + ArrangerSelection::Scene(s) => Select(Focus::Clip(0, s)), + ArrangerSelection::Clip(t, s) => Select(Focus::Clip(t, s - 1)), + }, + + key!(KeyCode::Char('+')) => Zoom(0), + + key!(KeyCode::Char('=')) => Zoom(0), + + key!(KeyCode::Char('_')) => Zoom(0), + + key!(KeyCode::Char('-')) => Zoom(0), + + key!(KeyCode::Char('`')) => { todo!("toggle view mode") }, + + key!(KeyCode::Char(',')) => match view.selected { + ArrangerSelection::Mix => Zoom(0), + ArrangerSelection::Track(t) => Edit(Model::Track(Track::Swap(t, t - 1))), + ArrangerSelection::Scene(s) => Edit(Model::Scene(Scene::Swap(s, s - 1))), + ArrangerSelection::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))), + }, + + key!(KeyCode::Char('.')) => match view.selected { + ArrangerSelection::Mix => Zoom(0), + ArrangerSelection::Track(t) => Edit(Model::Track(Track::Swap(t, t + 1))), + ArrangerSelection::Scene(s) => Edit(Model::Scene(Scene::Swap(s, s + 1))), + ArrangerSelection::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))), + }, + + key!(KeyCode::Char('<')) => match view.selected { + ArrangerSelection::Mix => Zoom(0), + ArrangerSelection::Track(t) => Edit(Model::Track(Track::Swap(t, t - 1))), + ArrangerSelection::Scene(s) => Edit(Model::Scene(Scene::Swap(s, s - 1))), + ArrangerSelection::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))), + }, + + key!(KeyCode::Char('>')) => match view.selected { + ArrangerSelection::Mix => Zoom(0), + ArrangerSelection::Track(t) => Edit(Model::Track(Track::Swap(t, t + 1))), + ArrangerSelection::Scene(s) => Edit(Model::Scene(Scene::Swap(s, s + 1))), + ArrangerSelection::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))), + }, + + key!(KeyCode::Enter) => match view.selected { + ArrangerSelection::Mix => return None, + ArrangerSelection::Track(t) => return None, + ArrangerSelection::Scene(s) => Edit(Model::Scene(Scene::Play(s))), + ArrangerSelection::Clip(t, s) => return None, + }, + + key!(KeyCode::Delete) => match view.selected { + ArrangerSelection::Mix => Edit(Model::Clear), + ArrangerSelection::Track(t) => Edit(Model::Track(Track::Delete(t))), + ArrangerSelection::Scene(s) => Edit(Model::Scene(Scene::Delete(s))), + ArrangerSelection::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))), + }, + + key!(KeyCode::Char('c')) => Edit(Model::Clip(Clip::RandomColor)), + + key!(KeyCode::Char('s')) => match view.selected { + ArrangerSelection::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))), + _ => return None, + }, + + key!(KeyCode::Char('g')) => match view.selected { + ArrangerSelection::Clip(t, s) => Edit(Model::Clip(Clip::Get(t, s))), + _ => return None, + }, + + key!(Ctrl-KeyCode::Char('a')) => Edit(Model::Scene(Scene::Add)), + + key!(Ctrl-KeyCode::Char('t')) => Edit(Model::Track(Track::Add)), + + key!(KeyCode::Char('l')) => Edit(Model::Clip(Clip::SetLoop(false))), + + _ => return None + } + } + } + } + }) + } +} + +impl Command> for ArrangerViewCommand { + fn execute (self, view: &mut ArrangerView) -> Perhaps { + let undo = match self { + Self::Focus(cmd) => + delegate(cmd, Self::Focus, view), + Self::Phrases(cmd) => + delegate(cmd, Self::Phrases, &mut view.sequencer.phrases), + Self::Editor(cmd) => + delegate(cmd, Self::Editor, &mut view.sequencer.editor), + Self::Transport(cmd) => + delegate(cmd, Self::Transport, &mut view.sequencer.transport), + Self::Zoom(zoom) => { + todo!(); + }, + Self::Select(selected) => { + view.selected = selected; + Ok(None) + }, + Self::Edit(command) => { + return Ok(command.execute(&mut view.model)?.map(Self::Edit)) + }, + Self::EditPhrase(phrase) => { + view.sequencer.editor.phrase = phrase.clone(); + view.focus(ArrangerViewFocus::PhraseEditor); + view.focus_enter(); + Ok(None) + } + }?; + view.show_phrase(); + view.update_status(); + return Ok(undo); + } +} + + //pub fn phrase_next (&mut self) { + //if let ArrangerSelection::Clip(track, scene) = self.selected { + //if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] { + //let phrases = self.model.phrases.read().unwrap(); + //let index = phrases.index_of(&*phrase.read().unwrap()); + //if let Some(index) = index { + //if index < phrases.len().saturating_sub(1) { + //*phrase = phrases[index + 1].clone(); + //} + //} + //} + //} + //} + //pub fn phrase_prev (&mut self) { + //if let ArrangerSelection::Clip(track, scene) = self.selected { + //if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] { + //let phrases = self.model.phrases.read().unwrap(); + //let index = phrases.index_of(&*phrase.read().unwrap()); + //if let Some(index) = index { + //if index > 0 { + //*phrase = phrases[index - 1].clone(); + //} + //} + //} + //} + //} + + //pub fn phrase_get (&mut self) { + //if let ArrangerSelection::Clip(track, scene) = self.selected { + //if let Some(phrase) = &self.model.scenes[scene].clips[track] { + //let mut phrases = self.model.phrases.write().unwrap(); + //if let Some(index) = &*phrases.index_of(&*phrase.read().unwrap()) { + //self.model.phrase = index; + //} + //} + //} + //} + + ///// Focus the editor with the current phrase + //pub fn edit_phrase (&mut self) { + //if self.arrangement.selected.is_clip() && self.arrangement.phrase().is_none() { + //self.sequencer.phrases.append_new(None, Some(self.next_color().into())); + //self.arrangement.phrase_put(); + //} + //self.show_phrase(); + //self.focus(ArrangerViewFocus::PhraseEditor); + //self.sequencer.editor.entered = true; + //} + + //pub fn next_color (&self) -> ItemColor { + //if let ArrangerSelection::Clip(track, scene) = self.arrangement.selected { + //let track_color = self.arrangement.model.tracks[track].color; + //let scene_color = self.arrangement.model.scenes[scene].color; + //track_color.mix(scene_color, 0.5).mix(ItemColor::random(), 0.25) + //} else { + //panic!("could not compute next color") + //} + //} + //pub fn phrase_del (&mut self) { + //let track_index = self.selected.track(); + //let scene_index = self.selected.scene(); + //track_index + //.and_then(|index|self.model.tracks.get_mut(index).map(|track|(index, track))) + //.map(|(track_index, _)|scene_index + //.and_then(|index|self.model.scenes.get_mut(index)) + //.map(|scene|scene.clips[track_index] = None)); + //} + //pub fn phrase_put (&mut self) { + //if let ArrangerSelection::Clip(track, scene) = self.selected { + //self.model.scenes[scene].clips[track] = self.selected_phrase().clone(); + //} + //} + //pub fn selected_scene (&self) -> Option<&ArrangerScene> { + //self.selected.scene().map(|s|self.model.scenes.get(s)).flatten() + //} + //pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { + //self.selected.scene().map(|s|self.model.scenes.get_mut(s)).flatten() + //} + //pub fn selected_phrase (&self) -> Option>> { + //self.selected_scene()?.clips.get(self.selected.track()?)?.clone() + //} + +/// Focus layout of arranger app +impl FocusGrid for ArrangerApp { + type Item = AppViewFocus; + fn cursor (&self) -> (usize, usize) { + self.cursor + } + fn cursor_mut (&mut self) -> &mut (usize, usize) { + &mut self.cursor + } + fn focus_enter (&mut self) { + use AppViewFocus::*; + use ArrangerViewFocus::*; + let focused = self.focused(); + if !self.entered { + self.entered = focused == Content(Arranger); + self.app.sequencer.editor.entered = focused == Content(PhraseEditor); + self.app.sequencer.phrases.entered = focused == Content(PhrasePool); + } + } + fn focus_exit (&mut self) { + if self.entered { + self.entered = false; + self.app.sequencer.editor.entered = false; + self.app.sequencer.phrases.entered = false; + } + } + fn entered (&self) -> Option { + if self.entered { + Some(self.focused()) + } else { + None + } + } + fn layout (&self) -> &[&[Self::Item]] { + use AppViewFocus::*; + use ArrangerViewFocus::*; + &[ + &[Menu, Menu ], + &[Content(Transport), Content(Transport) ], + &[Content(Arranger), Content(Arranger) ], + &[Content(PhrasePool), Content(PhraseEditor)], + ] + } + fn update_focus (&mut self) { + use AppViewFocus::*; + use ArrangerViewFocus::*; + let focused = self.focused(); + self.app.focused = focused == Content(Arranger); + self.app.sequencer.transport.focused = focused == Content(Transport); + self.app.sequencer.phrases.focused = focused == Content(PhrasePool); + self.app.sequencer.editor.focused = focused == Content(PhraseEditor); + if let Some(status_bar) = self.status_bar { + status_bar.update(&( + self.focused(), + self.app.selected, + self.app.sequencer.editor.entered + )) + } + } +} + +/// Focus identification methods +impl ArrangerSelection { + 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 { + use ArrangerSelection::*; + match self { + Clip(t, _) => Some(*t), + Track(t) => Some(*t), + _ => None + } + } + pub fn scene (&self) -> Option { + use ArrangerSelection::*; + match self { + Clip(_, s) => Some(*s), + Scene(s) => Some(*s), + _ => None + } + } +} + //pub fn track_next (&mut self, last_track: usize) { + //use ArrangerSelection::*; + //*self = match self { + //Mix => Track(0), + //Track(t) => Track(last_track.min(*t + 1)), + //Scene(s) => Clip(0, *s), + //Clip(t, s) => Clip(last_track.min(*t + 1), *s), + //} + //} + //pub fn track_prev (&mut self) { + //use ArrangerSelection::*; + //*self = match self { + //Mix => Mix, + //Scene(s) => Scene(*s), + //Track(t) => if *t == 0 { Mix } else { Track(*t - 1) }, + //Clip(t, s) => if *t == 0 { Scene(*s) } else { Clip(t.saturating_sub(1), *s) } + //} + //} + //pub fn scene_next (&mut self, last_scene: usize) { + //use ArrangerSelection::*; + //*self = match self { + //Mix => Scene(0), + //Track(t) => Clip(*t, 0), + //Scene(s) => Scene(last_scene.min(*s + 1)), + //Clip(t, s) => Clip(*t, last_scene.min(*s + 1)), + //} + //} + //pub fn scene_prev (&mut self) { + //use ArrangerSelection::*; + //*self = match self { + //Mix => Mix, + //Track(t) => Track(*t), + //Scene(s) => if *s == 0 { Mix } else { Scene(*s - 1) }, + //Clip(t, s) => if *s == 0 { Track(*t) } else { Clip(*t, s.saturating_sub(1)) } + //} + //} + +impl StatusBar for ArrangerStatusBar { + + type State = (AppViewFocus, ArrangerSelection, bool); + + fn hotkey_fg () -> Color where Self: Sized { + TuiTheme::hotkey_fg() + } + fn update (&mut self, (focused, selected, entered): &Self::State) { + use AppViewFocus::*; + if let Content(focused) = focused { + *self = match focused { + ArrangerViewFocus::Transport => ArrangerStatusBar::Transport, + ArrangerViewFocus::Arranger => match selected { + ArrangerSelection::Mix => ArrangerStatusBar::ArrangerMix, + ArrangerSelection::Track(_) => ArrangerStatusBar::ArrangerTrack, + ArrangerSelection::Scene(_) => ArrangerStatusBar::ArrangerScene, + ArrangerSelection::Clip(_, _) => ArrangerStatusBar::ArrangerClip, + }, + ArrangerViewFocus::PhrasePool => ArrangerStatusBar::PhrasePool, + ArrangerViewFocus::PhraseEditor => match entered { + true => ArrangerStatusBar::PhraseEdit, + false => ArrangerStatusBar::PhraseView, + }, + } + } + } +} + +impl Content for ArrangerStatusBar { + type Engine = Tui; + fn content (&self) -> impl Widget { + let label = match self { + Self::Transport => "TRANSPORT", + Self::ArrangerMix => "PROJECT", + Self::ArrangerTrack => "TRACK", + Self::ArrangerScene => "SCENE", + Self::ArrangerClip => "CLIP", + Self::PhrasePool => "SEQ LIST", + Self::PhraseView => "VIEW SEQ", + Self::PhraseEdit => "EDIT SEQ", + }; + 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::ArrangerMix => Self::command(&[ + ["", "c", "olor"], + ["", "<>", "resize"], + ["", "+-", "zoom"], + ["", "n", "ame/number"], + ["", "Enter", " stop all"], + ]), + Self::ArrangerClip => Self::command(&[ + ["", "g", "et"], + ["", "s", "et"], + ["", "a", "dd"], + ["", "i", "ns"], + ["", "d", "up"], + ["", "e", "dit"], + ["", "c", "olor"], + ["re", "n", "ame"], + ["", ",.", "select"], + ["", "Enter", " launch"], + ]), + Self::ArrangerTrack => Self::command(&[ + ["re", "n", "ame"], + ["", ",.", "resize"], + ["", "<>", "move"], + ["", "i", "nput"], + ["", "o", "utput"], + ["", "m", "ute"], + ["", "s", "olo"], + ["", "Del", "ete"], + ["", "Enter", " stop"], + ]), + Self::ArrangerScene => Self::command(&[ + ["re", "n", "ame"], + ["", "Del", "ete"], + ["", "Enter", " launch"], + ]), + Self::PhrasePool => Self::command(&[ + ["", "a", "ppend"], + ["", "i", "nsert"], + ["", "d", "uplicate"], + ["", "Del", "ete"], + ["", "c", "olor"], + ["re", "n", "ame"], + ["leng", "t", "h"], + ["", ",.", "move"], + ["", "+-", "resize view"], + ]), + Self::PhraseView => Self::command(&[ + ["", "enter", " edit"], + ["", "arrows/pgup/pgdn", " scroll"], + ["", "+=", "zoom"], + ]), + Self::PhraseEdit => Self::command(&[ + ["", "esc", " exit"], + ["", "a", "ppend"], + ["", "s", "et"], + ["", "][", "length"], + ["", "+-", "zoom"], + ]), + _ => 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) + } +} + +//pub fn arranger_menu_bar () -> MenuBar { + //use ArrangerCommand as Cmd; + //use ArrangerCommand as Edit; + //use ArrangerSelection as Focus; + //use ArrangerTrackCommand as Track; + //use ArrangerClipCommand as Clip; + //use ArrangerSceneCommand as Scene; + //use TransportCommand as Transport; + //MenuBar::new() + //.add({ + //use ArrangerCommand::*; + //Menu::new("File") + //.cmd("n", "New project", ArrangerViewCommand::Arranger(New)) + //.cmd("l", "Load project", ArrangerViewCommand::Arranger(Load)) + //.cmd("s", "Save project", ArrangerViewCommand::Arranger(Save)) + //}) + //.add({ + //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 ArrangerCommand::*; + //Menu::new("Track") + //.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("<", "Move down", ArrangerViewCommand::Arranger(AddTrack)) + //}) + //.add({ + //use ArrangerCommand::*; + //Menu::new("Scene") + //.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddScene)) + //.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("<", "Move down", ArrangerViewCommand::Arranger(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)) + //}) +//} + +fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { + let mut widths = vec![]; + let mut total = 0; + for track in tracks.iter() { + let width = track.width; + widths.push((width, total)); + total += width; + } + widths.push((0, total)); + widths +} + +pub fn arranger_content_vertical ( + view: &ArrangerView, + factor: usize +) -> impl Widget + use<'_> { + let clock = view.model.clock(); + let tracks = view.model.tracks(); + let scenes = view.model.scenes(); + let cols = track_widths(tracks); + let rows = ArrangerScene::ppqs(scenes, factor); + let bg = view.color; + let clip_bg = TuiTheme::border_bg(); + let sep_fg = TuiTheme::separator_fg(false); + let header_h = 3u16;//5u16; + let scenes_w = 3 + ArrangerScene::longest_name(scenes) as u16; // x of 1st track + let arrangement = Layers::new(move |add|{ + let rows: &[(usize, usize)] = rows.as_ref(); + let cols: &[(usize, usize)] = cols.as_ref(); + let any_size = |_|Ok(Some([0,0])); + // column separators + add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{ + let style = Some(Style::default().fg(sep_fg)); + Ok(for x in cols.iter().map(|col|col.1) { + let x = scenes_w + to.area().x() + x as u16; + for y in to.area().y()..to.area().y2() { to.blit(&"▎", x, y, style); } + }) + }))?; + // row separators + add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{ + Ok(for y in rows.iter().map(|row|row.1) { + let y = to.area().y() + (y / PPQ) as u16 + 1; + if y >= to.buffer.area.height { break } + for x in to.area().x()..to.area().x2().saturating_sub(2) { + if x < to.buffer.area.x && y < to.buffer.area.y { + let cell = to.buffer.get_mut(x, y); + cell.modifier = Modifier::UNDERLINED; + cell.underline_color = sep_fg; + } + } + }) + }))?; + // track titles + let header = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{ + // name and width of track + let name = track.name.read().unwrap(); + let player = &track.player; + let max_w = w.saturating_sub(1).min(name.len()).max(2); + let name = format!("▎{}", &name[0..max_w]); + let name = TuiStyle::bold(name, true); + // beats elapsed + let elapsed = if let Some((_, Some(phrase))) = player.phrase.as_ref() { + let length = phrase.read().unwrap().length; + let elapsed = player.pulses_since_start().unwrap(); + let elapsed = clock.timebase().format_beats_1_short( + (elapsed as usize % length) as f64 + ); + format!("▎+{elapsed:>}") + } else { + String::from("▎") + }; + // beats until switchover + let until_next = player.next_phrase.as_ref().map(|(t, _)|{ + let target = t.pulse.get(); + let current = clock.current.pulse.get(); + if target > current { + let remaining = target - current; + format!("▎-{:>}", clock.timebase().format_beats_0_short(remaining)) + } else { + String::new() + } + }).unwrap_or(String::from("▎")); + // name of active MIDI input + let input = format!("▎>{}", track.player.midi_inputs.get(0) + .map(|port|port.short_name()) + .transpose()? + .unwrap_or("(none)".into())); + // name of active MIDI output + let output = format!("▎<{}", track.player.midi_outputs.get(0) + .map(|port|port.short_name()) + .transpose()? + .unwrap_or("(none)".into())); + col!(name, /*input, output,*/ until_next, elapsed) + .min_xy(w as u16, header_h) + .bg(track.color.rgb) + .push_x(scenes_w) + }); + // tracks and scenes + let content = col!( + // scenes: + (scene, pulses) in scenes.iter().zip(rows.iter().map(|row|row.0)) => { + let height = 1.max((pulses / PPQ) as u16); + let playing = scene.is_playing(tracks); + Stack::right(move |add| { + // scene title: + add(&row!( + if playing { "▶ " } else { " " }, + TuiStyle::bold(scene.name.read().unwrap().as_str(), true), + ).fixed_xy(scenes_w, height).bg(scene.color.rgb))?; + // clip per track: + Ok(for (track, w) in cols.iter().map(|col|col.0).enumerate() { + add(&Layers::new(move |add|{ + let mut bg = clip_bg; + match (tracks.get(track), scene.clips.get(track)) { + (Some(track), Some(Some(phrase))) => { + let name = &(phrase as &Arc>).read().unwrap().name; + let name = format!("{}", name); + let max_w = name.len().min((w as usize).saturating_sub(2)); + let color = phrase.read().unwrap().color; + add(&name.as_str()[0..max_w].push_x(1).fixed_x(w as u16))?; + bg = color.dark.rgb; + if let Some((_, Some(ref playing))) = track.player.phrase { + if *playing.read().unwrap() == *phrase.read().unwrap() { + bg = color.light.rgb + } + }; + }, + _ => {} + }; + add(&Background(bg)) + }).fixed_xy(w as u16, height))?; + }) + }).fixed_y(height) + } + ).fixed_y((view.size.h() as u16).saturating_sub(header_h)); + // full grid with header and footer + add(&col!(header, content))?; + // cursor + add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{ + let area = to.area(); + let focused = view.focused; + let selected = view.selected; + let get_track_area = |t: usize| [ + scenes_w + area.x() + cols[t].1 as u16, area.y(), + cols[t].0 as u16, area.h(), + ]; + let get_scene_area = |s: usize| [ + area.x(), header_h + area.y() + (rows[s].1 / PPQ) as u16, + area.w(), (rows[s].0 / PPQ) as u16 + ]; + let get_clip_area = |t: usize, s: usize| [ + scenes_w + area.x() + cols[t].1 as u16, + header_h + area.y() + (rows[s].1/PPQ) as u16, + cols[t].0 as u16, + (rows[s].0 / PPQ) as u16 + ]; + let mut track_area: Option<[u16;4]> = None; + let mut scene_area: Option<[u16;4]> = None; + let mut clip_area: Option<[u16;4]> = None; + let area = match selected { + ArrangerSelection::Mix => area, + ArrangerSelection::Track(t) => { + track_area = Some(get_track_area(t)); + area + }, + ArrangerSelection::Scene(s) => { + scene_area = Some(get_scene_area(s)); + area + }, + ArrangerSelection::Clip(t, s) => { + track_area = Some(get_track_area(t)); + scene_area = Some(get_scene_area(s)); + clip_area = Some(get_clip_area(t, s)); + area + }, + }; + let bg = TuiTheme::border_bg(); + if let Some([x, y, width, height]) = track_area { + to.fill_fg([x, y, 1, height], bg); + to.fill_fg([x + width, y, 1, height], bg); + } + if let Some([_, y, _, height]) = scene_area { + to.fill_ul([area.x(), y - 1, area.w(), 1], bg); + to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg); + } + Ok(if focused { + to.render_in(if let Some(clip_area) = clip_area { clip_area } + else if let Some(track_area) = track_area { track_area.clip_h(header_h) } + else if let Some(scene_area) = scene_area { scene_area.clip_w(scenes_w) } + else { area.clip_w(scenes_w).clip_h(header_h) }, &CORNERS)? + }) + })) + }).bg(bg.rgb); + let color = TuiTheme::title_fg(view.focused); + let size = format!("{}x{}", view.size.w(), view.size.h()); + let lower_right = TuiStyle::fg(size, color).pull_x(1).align_se().fill_xy(); + lay!(arrangement, lower_right) +} + +pub fn arranger_content_horizontal ( + view: &ArrangerView, +) -> impl Widget + use<'_> { + let focused = view.focused; + let _tracks = view.model.tracks(); + lay!( + focused.then_some(Background(TuiTheme::border_bg())), + row!( + // name + CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + todo!() + //let Self(tracks, selected) = self; + //let yellow = Some(Style::default().yellow().bold().not_dim()); + //let white = Some(Style::default().white().bold().not_dim()); + //let area = to.area(); + //let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()]; + //let offset = 0; // track scroll offset + //for y in 0..area.h() { + //if y == 0 { + //to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?; + //} else if y % 2 == 0 { + //let index = (y as usize - 2) / 2 + offset; + //if let Some(track) = tracks.get(index) { + //let selected = selected.track() == Some(index); + //let style = if selected { yellow } else { white }; + //to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?; + //to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?; + //} + //} + //} + //Ok(Some(area)) + }), + // monitor + CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + todo!() + //let Self(tracks) = self; + //let mut area = to.area(); + //let on = Some(Style::default().not_dim().green().bold()); + //let off = Some(DIM); + //area.x += 1; + //for y in 0..area.h() { + //if y == 0 { + ////" MON ".blit(to.buffer, area.x, area.y + y, style2)?; + //} else if y % 2 == 0 { + //let index = (y as usize - 2) / 2; + //if let Some(track) = tracks.get(index) { + //let style = if track.monitoring { on } else { off }; + //to.blit(&" MON ", area.x(), area.y() + y, style)?; + //} else { + //area.height = y; + //break + //} + //} + //} + //area.width = 4; + //Ok(Some(area)) + }), + // record + CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + todo!() + //let Self(tracks) = self; + //let mut area = to.area(); + //let on = Some(Style::default().not_dim().red().bold()); + //let off = Some(Style::default().dim()); + //area.x += 1; + //for y in 0..area.h() { + //if y == 0 { + ////" REC ".blit(to.buffer, area.x, area.y + y, style2)?; + //} else if y % 2 == 0 { + //let index = (y as usize - 2) / 2; + //if let Some(track) = tracks.get(index) { + //let style = if track.recording { on } else { off }; + //to.blit(&" REC ", area.x(), area.y() + y, style)?; + //} else { + //area.height = y; + //break + //} + //} + //} + //area.width = 4; + //Ok(Some(area)) + }), + // overdub + CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + todo!() + //let Self(tracks) = self; + //let mut area = to.area(); + //let on = Some(Style::default().not_dim().yellow().bold()); + //let off = Some(Style::default().dim()); + //area.x = area.x + 1; + //for y in 0..area.h() { + //if y == 0 { + ////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?; + //} else if y % 2 == 0 { + //let index = (y as usize - 2) / 2; + //if let Some(track) = tracks.get(index) { + //to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub { + //on + //} else { + //off + //})?; + //} else { + //area.height = y; + //break + //} + //} + //} + //area.width = 4; + //Ok(Some(area)) + }), + // erase + CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + todo!() + //let Self(tracks) = self; + //let mut area = to.area(); + //let off = Some(Style::default().dim()); + //area.x = area.x + 1; + //for y in 0..area.h() { + //if y == 0 { + ////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?; + //} else if y % 2 == 0 { + //let index = (y as usize - 2) / 2; + //if let Some(_) = tracks.get(index) { + //to.blit(&" DEL ", area.x(), area.y() + y, off)?; + //} else { + //area.height = y; + //break + //} + //} + //} + //area.width = 4; + //Ok(Some(area)) + }), + // gain + CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + todo!() + //let Self(tracks) = self; + //let mut area = to.area(); + //let off = Some(Style::default().dim()); + //area.x = area.x() + 1; + //for y in 0..area.h() { + //if y == 0 { + ////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?; + //} else if y % 2 == 0 { + //let index = (y as usize - 2) / 2; + //if let Some(_) = tracks.get(index) { + //to.blit(&" +0.0 ", area.x(), area.y() + y, off)?; + //} else { + //area.height = y; + //break + //} + //} + //} + //area.width = 7; + //Ok(Some(area)) + }), + // scenes + CustomWidget::new(|_|{todo!()}, |to: &mut TuiOutput|{ + let [x, y, _, height] = to.area(); + let mut x2 = 0; + Ok(for (scene_index, scene) in view.model.scenes().iter().enumerate() { + let active_scene = view.selected.scene() == Some(scene_index); + let sep = Some(if active_scene { + Style::default().yellow().not_dim() + } else { + Style::default().dim() + }); + for y in y+1..y+height { + to.blit(&"│", x + x2, y, sep); + } + let name = scene.name.read().unwrap(); + let mut x3 = name.len() as u16; + to.blit(&*name, x + x2, y, sep); + for (i, clip) in scene.clips.iter().enumerate() { + let active_track = view.selected.track() == Some(i); + if let Some(clip) = clip { + let y2 = y + 2 + i as u16 * 2; + let label = format!("{}", clip.read().unwrap().name); + to.blit(&label, x + x2, y2, Some(if active_track && active_scene { + Style::default().not_dim().yellow().bold() + } else { + Style::default().not_dim() + })); + x3 = x3.max(label.len() as u16) + } + } + x2 = x2 + x3 + 1; + }) + }), + ) + ) +} diff --git a/crates/tek_tui/src/tui_arranger_bar.rs b/crates/tek_tui/src/tui_arranger_bar.rs deleted file mode 100644 index 15642be7..00000000 --- a/crates/tek_tui/src/tui_arranger_bar.rs +++ /dev/null @@ -1,180 +0,0 @@ -use crate::*; - -/// Status bar for arranger ap -pub enum ArrangerStatusBar { - Transport, - ArrangerMix, - ArrangerTrack, - ArrangerScene, - ArrangerClip, - PhrasePool, - PhraseView, - PhraseEdit, -} - -impl StatusBar> for ArrangerStatusBar { - fn hotkey_fg () -> Color where Self: Sized { - TuiTheme::hotkey_fg() - } - fn update (&mut self, state: &ArrangerApp) { - use AppViewFocus::*; - if let Content(focused) = state.focused() { - *self = match focused { - ArrangerViewFocus::Transport => ArrangerStatusBar::Transport, - ArrangerViewFocus::Arranger => match state.app.selected { - ArrangerFocus::Mix => ArrangerStatusBar::ArrangerMix, - ArrangerFocus::Track(_) => ArrangerStatusBar::ArrangerTrack, - ArrangerFocus::Scene(_) => ArrangerStatusBar::ArrangerScene, - ArrangerFocus::Clip(_, _) => ArrangerStatusBar::ArrangerClip, - }, - ArrangerViewFocus::PhrasePool => ArrangerStatusBar::PhrasePool, - ArrangerViewFocus::PhraseEditor => match state.app.sequencer.editor.entered { - true => ArrangerStatusBar::PhraseEdit, - false => ArrangerStatusBar::PhraseView, - }, - } - } - } -} - -impl Content for ArrangerStatusBar { - type Engine = Tui; - fn content (&self) -> impl Widget { - let label = match self { - Self::Transport => "TRANSPORT", - Self::ArrangerMix => "PROJECT", - Self::ArrangerTrack => "TRACK", - Self::ArrangerScene => "SCENE", - Self::ArrangerClip => "CLIP", - Self::PhrasePool => "SEQ LIST", - Self::PhraseView => "VIEW SEQ", - Self::PhraseEdit => "EDIT SEQ", - }; - 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::ArrangerMix => Self::command(&[ - ["", "c", "olor"], - ["", "<>", "resize"], - ["", "+-", "zoom"], - ["", "n", "ame/number"], - ["", "Enter", " stop all"], - ]), - Self::ArrangerClip => Self::command(&[ - ["", "g", "et"], - ["", "s", "et"], - ["", "a", "dd"], - ["", "i", "ns"], - ["", "d", "up"], - ["", "e", "dit"], - ["", "c", "olor"], - ["re", "n", "ame"], - ["", ",.", "select"], - ["", "Enter", " launch"], - ]), - Self::ArrangerTrack => Self::command(&[ - ["re", "n", "ame"], - ["", ",.", "resize"], - ["", "<>", "move"], - ["", "i", "nput"], - ["", "o", "utput"], - ["", "m", "ute"], - ["", "s", "olo"], - ["", "Del", "ete"], - ["", "Enter", " stop"], - ]), - Self::ArrangerScene => Self::command(&[ - ["re", "n", "ame"], - ["", "Del", "ete"], - ["", "Enter", " launch"], - ]), - Self::PhrasePool => Self::command(&[ - ["", "a", "ppend"], - ["", "i", "nsert"], - ["", "d", "uplicate"], - ["", "Del", "ete"], - ["", "c", "olor"], - ["re", "n", "ame"], - ["leng", "t", "h"], - ["", ",.", "move"], - ["", "+-", "resize view"], - ]), - Self::PhraseView => Self::command(&[ - ["", "enter", " edit"], - ["", "arrows/pgup/pgdn", " scroll"], - ["", "+=", "zoom"], - ]), - Self::PhraseEdit => Self::command(&[ - ["", "esc", " exit"], - ["", "a", "ppend"], - ["", "s", "et"], - ["", "][", "length"], - ["", "+-", "zoom"], - ]), - _ => 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) - } -} - -//pub fn arranger_menu_bar () -> MenuBar { - //use ArrangerCommand as Cmd; - //use ArrangerCommand as Edit; - //use ArrangerFocus as Focus; - //use ArrangerTrackCommand as Track; - //use ArrangerClipCommand as Clip; - //use ArrangerSceneCommand as Scene; - //use TransportCommand as Transport; - //MenuBar::new() - //.add({ - //use ArrangerCommand::*; - //Menu::new("File") - //.cmd("n", "New project", ArrangerViewCommand::Arranger(New)) - //.cmd("l", "Load project", ArrangerViewCommand::Arranger(Load)) - //.cmd("s", "Save project", ArrangerViewCommand::Arranger(Save)) - //}) - //.add({ - //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 ArrangerCommand::*; - //Menu::new("Track") - //.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("<", "Move down", ArrangerViewCommand::Arranger(AddTrack)) - //}) - //.add({ - //use ArrangerCommand::*; - //Menu::new("Scene") - //.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddScene)) - //.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("<", "Move down", ArrangerViewCommand::Arranger(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 deleted file mode 100644 index 39562edf..00000000 --- a/crates/tek_tui/src/tui_arranger_cmd.rs +++ /dev/null @@ -1,289 +0,0 @@ -use crate::*; - -#[derive(Clone)] -pub enum ArrangerViewCommand { - Focus(FocusCommand), - Edit(ArrangerCommand), - Select(ArrangerFocus), - Zoom(usize), - Transport(TransportViewCommand), - Phrases(PhrasePoolViewCommand), - Editor(PhraseEditorCommand), - EditPhrase(Option>>), -} - -/// Handle top-level events in standalone arranger. -impl Handle for ArrangerView { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - ArrangerViewCommand::execute_with_state(self, i) - } -} - -impl InputToCommand> for ArrangerViewCommand { - fn input_to_command (view: &ArrangerView, input: &TuiInput) -> Option { - use FocusCommand::*; - use ArrangerViewCommand::*; - Some(match input.event() { - key!(KeyCode::Tab) => Focus(Next), - key!(Shift-KeyCode::Tab) => Focus(Prev), - key!(KeyCode::BackTab) => Focus(Prev), - key!(Shift-KeyCode::BackTab) => Focus(Prev), - key!(KeyCode::Up) => Focus(Up), - key!(KeyCode::Down) => Focus(Down), - key!(KeyCode::Left) => Focus(Left), - key!(KeyCode::Right) => Focus(Right), - key!(KeyCode::Enter) => Focus(Enter), - key!(KeyCode::Esc) => Focus(Exit), - key!(KeyCode::Char(' ')) => { - Transport(TransportViewCommand::Transport(TransportCommand::Play(None))) - }, - _ => match view.focused() { - ArrangerViewFocus::Transport => Transport( - TransportViewCommand::input_to_command(&view.sequencer.transport, input)? - ), - ArrangerViewFocus::PhraseEditor => Editor( - PhraseEditorCommand::input_to_command(&view.sequencer.editor, input)? - ), - ArrangerViewFocus::PhrasePool => match input.event() { - key!(KeyCode::Char('e')) => EditPhrase( - Some(view.sequencer.phrases.phrase().clone()) - ), - _ => Phrases( - PhrasePoolViewCommand::input_to_command(&view.sequencer.phrases, input)? - ) - }, - ArrangerViewFocus::Arranger => { - use ArrangerFocus as Focus; - use ArrangerCommand as Model; - use ArrangerTrackCommand as Track; - use ArrangerClipCommand as Clip; - use ArrangerSceneCommand as Scene; - match input.event() { - key!(KeyCode::Char('e')) => EditPhrase( - view.selected_phrase() - ), - _ => match input.event() { - // FIXME: boundary conditions - - key!(KeyCode::Up) => match view.selected { - ArrangerFocus::Mix => return None, - ArrangerFocus::Track(t) => return None, - ArrangerFocus::Scene(s) => Select(Focus::Scene(s - 1)), - ArrangerFocus::Clip(t, s) => Select(Focus::Clip(t, s - 1)), - }, - - key!(KeyCode::Down) => match view.selected { - ArrangerFocus::Mix => Select(Focus::Scene(0)), - ArrangerFocus::Track(t) => Select(Focus::Clip(t, 0)), - ArrangerFocus::Scene(s) => Select(Focus::Scene(s + 1)), - ArrangerFocus::Clip(t, s) => Select(Focus::Clip(t, s + 1)), - }, - - key!(KeyCode::Left) => match view.selected { - ArrangerFocus::Mix => return None, - ArrangerFocus::Track(t) => Select(Focus::Track(t - 1)), - ArrangerFocus::Scene(s) => return None, - ArrangerFocus::Clip(t, s) => Select(Focus::Clip(t - 1, s)), - }, - - key!(KeyCode::Right) => match view.selected { - ArrangerFocus::Mix => return None, - ArrangerFocus::Track(t) => Select(Focus::Track(t + 1)), - ArrangerFocus::Scene(s) => Select(Focus::Clip(0, s)), - ArrangerFocus::Clip(t, s) => Select(Focus::Clip(t, s - 1)), - }, - - key!(KeyCode::Char('+')) => Zoom(0), - - key!(KeyCode::Char('=')) => Zoom(0), - - key!(KeyCode::Char('_')) => Zoom(0), - - key!(KeyCode::Char('-')) => Zoom(0), - - key!(KeyCode::Char('`')) => { todo!("toggle view mode") }, - - key!(KeyCode::Char(',')) => match view.selected { - ArrangerFocus::Mix => Zoom(0), - ArrangerFocus::Track(t) => Edit(Model::Track(Track::Swap(t, t - 1))), - ArrangerFocus::Scene(s) => Edit(Model::Scene(Scene::Swap(s, s - 1))), - ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))), - }, - - key!(KeyCode::Char('.')) => match view.selected { - ArrangerFocus::Mix => Zoom(0), - ArrangerFocus::Track(t) => Edit(Model::Track(Track::Swap(t, t + 1))), - ArrangerFocus::Scene(s) => Edit(Model::Scene(Scene::Swap(s, s + 1))), - ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))), - }, - - key!(KeyCode::Char('<')) => match view.selected { - ArrangerFocus::Mix => Zoom(0), - ArrangerFocus::Track(t) => Edit(Model::Track(Track::Swap(t, t - 1))), - ArrangerFocus::Scene(s) => Edit(Model::Scene(Scene::Swap(s, s - 1))), - ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))), - }, - - key!(KeyCode::Char('>')) => match view.selected { - ArrangerFocus::Mix => Zoom(0), - ArrangerFocus::Track(t) => Edit(Model::Track(Track::Swap(t, t + 1))), - ArrangerFocus::Scene(s) => Edit(Model::Scene(Scene::Swap(s, s + 1))), - ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))), - }, - - key!(KeyCode::Enter) => match view.selected { - ArrangerFocus::Mix => return None, - ArrangerFocus::Track(t) => return None, - ArrangerFocus::Scene(s) => Edit(Model::Scene(Scene::Play(s))), - ArrangerFocus::Clip(t, s) => return None, - }, - - key!(KeyCode::Delete) => match view.selected { - ArrangerFocus::Mix => Edit(Model::Clear), - ArrangerFocus::Track(t) => Edit(Model::Track(Track::Delete(t))), - ArrangerFocus::Scene(s) => Edit(Model::Scene(Scene::Delete(s))), - ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))), - }, - - key!(KeyCode::Char('c')) => Edit(Model::Clip(Clip::RandomColor)), - - key!(KeyCode::Char('s')) => match view.selected { - ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Set(t, s, None))), - _ => return None, - }, - - key!(KeyCode::Char('g')) => match view.selected { - ArrangerFocus::Clip(t, s) => Edit(Model::Clip(Clip::Get(t, s))), - _ => return None, - }, - - key!(Ctrl-KeyCode::Char('a')) => Edit(Model::Scene(Scene::Add)), - - key!(Ctrl-KeyCode::Char('t')) => Edit(Model::Track(Track::Add)), - - key!(KeyCode::Char('l')) => Edit(Model::Clip(Clip::SetLoop(false))), - - _ => return None - } - } - } - } - }) - } -} - -impl Command> for ArrangerViewCommand { - fn execute (self, view: &mut ArrangerView) -> Perhaps { - let undo = match self { - Self::Focus(cmd) => - delegate(cmd, Self::Focus, view), - Self::Phrases(cmd) => - delegate(cmd, Self::Phrases, &mut view.sequencer.phrases), - Self::Editor(cmd) => - delegate(cmd, Self::Editor, &mut view.sequencer.editor), - Self::Transport(cmd) => - delegate(cmd, Self::Transport, &mut view.sequencer.transport), - Self::Zoom(zoom) => { - todo!(); - }, - Self::Select(selected) => { - view.selected = selected; - Ok(None) - }, - Self::Edit(command) => { - return Ok(command.execute(&mut view.model)?.map(Self::Edit)) - }, - Self::EditPhrase(phrase) => { - view.sequencer.editor.phrase = phrase.clone(); - view.focus(ArrangerViewFocus::PhraseEditor); - view.focus_enter(); - Ok(None) - } - }?; - view.show_phrase(); - view.update_status(); - return Ok(undo); - } -} - - //pub fn phrase_next (&mut self) { - //if let ArrangerFocus::Clip(track, scene) = self.selected { - //if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] { - //let phrases = self.model.phrases.read().unwrap(); - //let index = phrases.index_of(&*phrase.read().unwrap()); - //if let Some(index) = index { - //if index < phrases.len().saturating_sub(1) { - //*phrase = phrases[index + 1].clone(); - //} - //} - //} - //} - //} - //pub fn phrase_prev (&mut self) { - //if let ArrangerFocus::Clip(track, scene) = self.selected { - //if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] { - //let phrases = self.model.phrases.read().unwrap(); - //let index = phrases.index_of(&*phrase.read().unwrap()); - //if let Some(index) = index { - //if index > 0 { - //*phrase = phrases[index - 1].clone(); - //} - //} - //} - //} - //} - - //pub fn phrase_get (&mut self) { - //if let ArrangerFocus::Clip(track, scene) = self.selected { - //if let Some(phrase) = &self.model.scenes[scene].clips[track] { - //let mut phrases = self.model.phrases.write().unwrap(); - //if let Some(index) = &*phrases.index_of(&*phrase.read().unwrap()) { - //self.model.phrase = index; - //} - //} - //} - //} - - ///// Focus the editor with the current phrase - //pub fn edit_phrase (&mut self) { - //if self.arrangement.selected.is_clip() && self.arrangement.phrase().is_none() { - //self.sequencer.phrases.append_new(None, Some(self.next_color().into())); - //self.arrangement.phrase_put(); - //} - //self.show_phrase(); - //self.focus(ArrangerViewFocus::PhraseEditor); - //self.sequencer.editor.entered = true; - //} - - //pub fn next_color (&self) -> ItemColor { - //if let ArrangerFocus::Clip(track, scene) = self.arrangement.selected { - //let track_color = self.arrangement.model.tracks[track].color; - //let scene_color = self.arrangement.model.scenes[scene].color; - //track_color.mix(scene_color, 0.5).mix(ItemColor::random(), 0.25) - //} else { - //panic!("could not compute next color") - //} - //} - //pub fn phrase_del (&mut self) { - //let track_index = self.selected.track(); - //let scene_index = self.selected.scene(); - //track_index - //.and_then(|index|self.model.tracks.get_mut(index).map(|track|(index, track))) - //.map(|(track_index, _)|scene_index - //.and_then(|index|self.model.scenes.get_mut(index)) - //.map(|scene|scene.clips[track_index] = None)); - //} - //pub fn phrase_put (&mut self) { - //if let ArrangerFocus::Clip(track, scene) = self.selected { - //self.model.scenes[scene].clips[track] = self.selected_phrase().clone(); - //} - //} - //pub fn selected_scene (&self) -> Option<&ArrangerScene> { - //self.selected.scene().map(|s|self.model.scenes.get(s)).flatten() - //} - //pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { - //self.selected.scene().map(|s|self.model.scenes.get_mut(s)).flatten() - //} - //pub fn selected_phrase (&self) -> Option>> { - //self.selected_scene()?.clips.get(self.selected.track()?)?.clone() - //} diff --git a/crates/tek_tui/src/tui_arranger_foc.rs b/crates/tek_tui/src/tui_arranger_foc.rs deleted file mode 100644 index 9a9762a2..00000000 --- a/crates/tek_tui/src/tui_arranger_foc.rs +++ /dev/null @@ -1,176 +0,0 @@ -use crate::*; - -/// Sections in the arranger app that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum ArrangerViewFocus { - /// The transport (toolbar) is focused - Transport, - /// The arrangement (grid) is focused - Arranger, - /// The phrase list (pool) is focused - PhrasePool, - /// The phrase editor (sequencer) is focused - PhraseEditor, -} - -/// Focus layout of arranger app -impl FocusGrid for AppView, ArrangerViewCommand> { - type Item = AppViewFocus; - fn cursor (&self) -> (usize, usize) { - self.cursor - } - fn cursor_mut (&mut self) -> &mut (usize, usize) { - &mut self.cursor - } - fn focus_enter (&mut self) { - use AppViewFocus::*; - use ArrangerViewFocus::*; - let focused = self.focused(); - if !self.entered { - self.entered = focused == Content(Arranger); - self.app.sequencer.editor.entered = focused == Content(PhraseEditor); - self.app.sequencer.phrases.entered = focused == Content(PhrasePool); - } - } - fn focus_exit (&mut self) { - if self.entered { - self.entered = false; - self.app.sequencer.editor.entered = false; - self.app.sequencer.phrases.entered = false; - } - } - fn entered (&self) -> Option { - if self.entered { - Some(self.focused()) - } else { - None - } - } - fn layout (&self) -> &[&[Self::Item]] { - use AppViewFocus::*; - use ArrangerViewFocus::*; - &[ - &[Menu, Menu ], - &[Content(Transport), Content(Transport) ], - &[Content(Arranger), Content(Arranger) ], - &[Content(PhrasePool), Content(PhraseEditor)], - ] - } - fn update_focus (&mut self) { - use AppViewFocus::*; - use ArrangerViewFocus::*; - let focused = self.focused(); - self.app.focused = focused == Content(Arranger); - self.app.sequencer.transport.focused = focused == Content(Transport); - self.app.sequencer.phrases.focused = focused == Content(PhrasePool); - self.app.sequencer.editor.focused = focused == Content(PhraseEditor); - if let Some(status_bar) = self.status_bar { - status_bar.update(&self.app) - } - } -} - -#[derive(PartialEq, Clone, Copy)] -/// Represents the current user selection in the arranger -pub enum ArrangerFocus { - /// 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 ArrangerFocus { - 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 { - use ArrangerFocus::*; - match self { - Clip(t, _) => Some(*t), - Track(t) => Some(*t), - _ => None - } - } - pub fn scene (&self) -> Option { - use ArrangerFocus::*; - match self { - Clip(_, s) => Some(*s), - Scene(s) => Some(*s), - _ => None - } - } -} - //pub fn track_next (&mut self, last_track: usize) { - //use ArrangerFocus::*; - //*self = match self { - //Mix => Track(0), - //Track(t) => Track(last_track.min(*t + 1)), - //Scene(s) => Clip(0, *s), - //Clip(t, s) => Clip(last_track.min(*t + 1), *s), - //} - //} - //pub fn track_prev (&mut self) { - //use ArrangerFocus::*; - //*self = match self { - //Mix => Mix, - //Scene(s) => Scene(*s), - //Track(t) => if *t == 0 { Mix } else { Track(*t - 1) }, - //Clip(t, s) => if *t == 0 { Scene(*s) } else { Clip(t.saturating_sub(1), *s) } - //} - //} - //pub fn scene_next (&mut self, last_scene: usize) { - //use ArrangerFocus::*; - //*self = match self { - //Mix => Scene(0), - //Track(t) => Clip(*t, 0), - //Scene(s) => Scene(last_scene.min(*s + 1)), - //Clip(t, s) => Clip(*t, last_scene.min(*s + 1)), - //} - //} - //pub fn scene_prev (&mut self) { - //use ArrangerFocus::*; - //*self = match self { - //Mix => Mix, - //Track(t) => Track(*t), - //Scene(s) => if *s == 0 { Mix } else { Scene(*s - 1) }, - //Clip(t, s) => if *s == 0 { Track(*t) } else { Clip(*t, s.saturating_sub(1)) } - //} - //} diff --git a/crates/tek_tui/src/tui_arranger_hor.rs b/crates/tek_tui/src/tui_arranger_hor.rs deleted file mode 100644 index 0c48c650..00000000 --- a/crates/tek_tui/src/tui_arranger_hor.rs +++ /dev/null @@ -1,194 +0,0 @@ -use crate::*; - -pub fn arranger_content_horizontal ( - view: &ArrangerView, -) -> impl Widget + use<'_> { - let focused = view.focused; - let _tracks = view.model.tracks(); - lay!( - focused.then_some(Background(TuiTheme::border_bg())), - row!( - // name - CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - todo!() - //let Self(tracks, selected) = self; - //let yellow = Some(Style::default().yellow().bold().not_dim()); - //let white = Some(Style::default().white().bold().not_dim()); - //let area = to.area(); - //let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()]; - //let offset = 0; // track scroll offset - //for y in 0..area.h() { - //if y == 0 { - //to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?; - //} else if y % 2 == 0 { - //let index = (y as usize - 2) / 2 + offset; - //if let Some(track) = tracks.get(index) { - //let selected = selected.track() == Some(index); - //let style = if selected { yellow } else { white }; - //to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?; - //to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?; - //} - //} - //} - //Ok(Some(area)) - }), - // monitor - CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - todo!() - //let Self(tracks) = self; - //let mut area = to.area(); - //let on = Some(Style::default().not_dim().green().bold()); - //let off = Some(DIM); - //area.x += 1; - //for y in 0..area.h() { - //if y == 0 { - ////" MON ".blit(to.buffer, area.x, area.y + y, style2)?; - //} else if y % 2 == 0 { - //let index = (y as usize - 2) / 2; - //if let Some(track) = tracks.get(index) { - //let style = if track.monitoring { on } else { off }; - //to.blit(&" MON ", area.x(), area.y() + y, style)?; - //} else { - //area.height = y; - //break - //} - //} - //} - //area.width = 4; - //Ok(Some(area)) - }), - // record - CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - todo!() - //let Self(tracks) = self; - //let mut area = to.area(); - //let on = Some(Style::default().not_dim().red().bold()); - //let off = Some(Style::default().dim()); - //area.x += 1; - //for y in 0..area.h() { - //if y == 0 { - ////" REC ".blit(to.buffer, area.x, area.y + y, style2)?; - //} else if y % 2 == 0 { - //let index = (y as usize - 2) / 2; - //if let Some(track) = tracks.get(index) { - //let style = if track.recording { on } else { off }; - //to.blit(&" REC ", area.x(), area.y() + y, style)?; - //} else { - //area.height = y; - //break - //} - //} - //} - //area.width = 4; - //Ok(Some(area)) - }), - // overdub - CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - todo!() - //let Self(tracks) = self; - //let mut area = to.area(); - //let on = Some(Style::default().not_dim().yellow().bold()); - //let off = Some(Style::default().dim()); - //area.x = area.x + 1; - //for y in 0..area.h() { - //if y == 0 { - ////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?; - //} else if y % 2 == 0 { - //let index = (y as usize - 2) / 2; - //if let Some(track) = tracks.get(index) { - //to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub { - //on - //} else { - //off - //})?; - //} else { - //area.height = y; - //break - //} - //} - //} - //area.width = 4; - //Ok(Some(area)) - }), - // erase - CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - todo!() - //let Self(tracks) = self; - //let mut area = to.area(); - //let off = Some(Style::default().dim()); - //area.x = area.x + 1; - //for y in 0..area.h() { - //if y == 0 { - ////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?; - //} else if y % 2 == 0 { - //let index = (y as usize - 2) / 2; - //if let Some(_) = tracks.get(index) { - //to.blit(&" DEL ", area.x(), area.y() + y, off)?; - //} else { - //area.height = y; - //break - //} - //} - //} - //area.width = 4; - //Ok(Some(area)) - }), - // gain - CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - todo!() - //let Self(tracks) = self; - //let mut area = to.area(); - //let off = Some(Style::default().dim()); - //area.x = area.x() + 1; - //for y in 0..area.h() { - //if y == 0 { - ////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?; - //} else if y % 2 == 0 { - //let index = (y as usize - 2) / 2; - //if let Some(_) = tracks.get(index) { - //to.blit(&" +0.0 ", area.x(), area.y() + y, off)?; - //} else { - //area.height = y; - //break - //} - //} - //} - //area.width = 7; - //Ok(Some(area)) - }), - // scenes - CustomWidget::new(|_|{todo!()}, |to: &mut TuiOutput|{ - let [x, y, _, height] = to.area(); - let mut x2 = 0; - Ok(for (scene_index, scene) in view.model.scenes().iter().enumerate() { - let active_scene = view.selected.scene() == Some(scene_index); - let sep = Some(if active_scene { - Style::default().yellow().not_dim() - } else { - Style::default().dim() - }); - for y in y+1..y+height { - to.blit(&"│", x + x2, y, sep); - } - let name = scene.name.read().unwrap(); - let mut x3 = name.len() as u16; - to.blit(&*name, x + x2, y, sep); - for (i, clip) in scene.clips.iter().enumerate() { - let active_track = view.selected.track() == Some(i); - if let Some(clip) = clip { - let y2 = y + 2 + i as u16 * 2; - let label = format!("{}", clip.read().unwrap().name); - to.blit(&label, x + x2, y2, Some(if active_track && active_scene { - Style::default().not_dim().yellow().bold() - } else { - Style::default().not_dim() - })); - x3 = x3.max(label.len() as u16) - } - } - x2 = x2 + x3 + 1; - }) - }), - ) - ) -} diff --git a/crates/tek_tui/src/tui_arranger_ver.rs b/crates/tek_tui/src/tui_arranger_ver.rs deleted file mode 100644 index de51c565..00000000 --- a/crates/tek_tui/src/tui_arranger_ver.rs +++ /dev/null @@ -1,200 +0,0 @@ -use crate::*; - -fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { - let mut widths = vec![]; - let mut total = 0; - for track in tracks.iter() { - let width = track.width; - widths.push((width, total)); - total += width; - } - widths.push((0, total)); - widths -} - -pub fn arranger_content_vertical ( - view: &ArrangerView, - factor: usize -) -> impl Widget + use<'_> { - let clock = view.model.clock(); - let tracks = view.model.tracks(); - let scenes = view.model.scenes(); - let cols = track_widths(tracks); - let rows = ArrangerScene::ppqs(scenes, factor); - let bg = view.color; - let clip_bg = TuiTheme::border_bg(); - let sep_fg = TuiTheme::separator_fg(false); - let header_h = 3u16;//5u16; - let scenes_w = 3 + ArrangerScene::longest_name(scenes) as u16; // x of 1st track - let arrangement = Layers::new(move |add|{ - let rows: &[(usize, usize)] = rows.as_ref(); - let cols: &[(usize, usize)] = cols.as_ref(); - let any_size = |_|Ok(Some([0,0])); - // column separators - add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{ - let style = Some(Style::default().fg(sep_fg)); - Ok(for x in cols.iter().map(|col|col.1) { - let x = scenes_w + to.area().x() + x as u16; - for y in to.area().y()..to.area().y2() { to.blit(&"▎", x, y, style); } - }) - }))?; - // row separators - add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{ - Ok(for y in rows.iter().map(|row|row.1) { - let y = to.area().y() + (y / PPQ) as u16 + 1; - if y >= to.buffer.area.height { break } - for x in to.area().x()..to.area().x2().saturating_sub(2) { - if x < to.buffer.area.x && y < to.buffer.area.y { - let cell = to.buffer.get_mut(x, y); - cell.modifier = Modifier::UNDERLINED; - cell.underline_color = sep_fg; - } - } - }) - }))?; - // track titles - let header = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{ - // name and width of track - let name = track.name.read().unwrap(); - let player = &track.player; - let max_w = w.saturating_sub(1).min(name.len()).max(2); - let name = format!("▎{}", &name[0..max_w]); - let name = TuiStyle::bold(name, true); - // beats elapsed - let elapsed = if let Some((_, Some(phrase))) = player.phrase.as_ref() { - let length = phrase.read().unwrap().length; - let elapsed = player.pulses_since_start().unwrap(); - let elapsed = clock.timebase().format_beats_1_short( - (elapsed as usize % length) as f64 - ); - format!("▎+{elapsed:>}") - } else { - String::from("▎") - }; - // beats until switchover - let until_next = player.next_phrase.as_ref().map(|(t, _)|{ - let target = t.pulse.get(); - let current = clock.current.pulse.get(); - if target > current { - let remaining = target - current; - format!("▎-{:>}", clock.timebase().format_beats_0_short(remaining)) - } else { - String::new() - } - }).unwrap_or(String::from("▎")); - // name of active MIDI input - let input = format!("▎>{}", track.player.midi_inputs.get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into())); - // name of active MIDI output - let output = format!("▎<{}", track.player.midi_outputs.get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into())); - col!(name, /*input, output,*/ until_next, elapsed) - .min_xy(w as u16, header_h) - .bg(track.color.rgb) - .push_x(scenes_w) - }); - // tracks and scenes - let content = col!( - // scenes: - (scene, pulses) in scenes.iter().zip(rows.iter().map(|row|row.0)) => { - let height = 1.max((pulses / PPQ) as u16); - let playing = scene.is_playing(tracks); - Stack::right(move |add| { - // scene title: - add(&row!( - if playing { "▶ " } else { " " }, - TuiStyle::bold(scene.name.read().unwrap().as_str(), true), - ).fixed_xy(scenes_w, height).bg(scene.color.rgb))?; - // clip per track: - Ok(for (track, w) in cols.iter().map(|col|col.0).enumerate() { - add(&Layers::new(move |add|{ - let mut bg = clip_bg; - match (tracks.get(track), scene.clips.get(track)) { - (Some(track), Some(Some(phrase))) => { - let name = &(phrase as &Arc>).read().unwrap().name; - let name = format!("{}", name); - let max_w = name.len().min((w as usize).saturating_sub(2)); - let color = phrase.read().unwrap().color; - add(&name.as_str()[0..max_w].push_x(1).fixed_x(w as u16))?; - bg = color.dark.rgb; - if let Some((_, Some(ref playing))) = track.player.phrase { - if *playing.read().unwrap() == *phrase.read().unwrap() { - bg = color.light.rgb - } - }; - }, - _ => {} - }; - add(&Background(bg)) - }).fixed_xy(w as u16, height))?; - }) - }).fixed_y(height) - } - ).fixed_y((view.size.h() as u16).saturating_sub(header_h)); - // full grid with header and footer - add(&col!(header, content))?; - // cursor - add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{ - let area = to.area(); - let focused = view.focused; - let selected = view.selected; - let get_track_area = |t: usize| [ - scenes_w + area.x() + cols[t].1 as u16, area.y(), - cols[t].0 as u16, area.h(), - ]; - let get_scene_area = |s: usize| [ - area.x(), header_h + area.y() + (rows[s].1 / PPQ) as u16, - area.w(), (rows[s].0 / PPQ) as u16 - ]; - let get_clip_area = |t: usize, s: usize| [ - scenes_w + area.x() + cols[t].1 as u16, - header_h + area.y() + (rows[s].1/PPQ) as u16, - cols[t].0 as u16, - (rows[s].0 / PPQ) as u16 - ]; - let mut track_area: Option<[u16;4]> = None; - let mut scene_area: Option<[u16;4]> = None; - let mut clip_area: Option<[u16;4]> = None; - let area = match selected { - ArrangerFocus::Mix => area, - ArrangerFocus::Track(t) => { - track_area = Some(get_track_area(t)); - area - }, - ArrangerFocus::Scene(s) => { - scene_area = Some(get_scene_area(s)); - area - }, - ArrangerFocus::Clip(t, s) => { - track_area = Some(get_track_area(t)); - scene_area = Some(get_scene_area(s)); - clip_area = Some(get_clip_area(t, s)); - area - }, - }; - let bg = TuiTheme::border_bg(); - if let Some([x, y, width, height]) = track_area { - to.fill_fg([x, y, 1, height], bg); - to.fill_fg([x + width, y, 1, height], bg); - } - if let Some([_, y, _, height]) = scene_area { - to.fill_ul([area.x(), y - 1, area.w(), 1], bg); - to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg); - } - Ok(if focused { - to.render_in(if let Some(clip_area) = clip_area { clip_area } - else if let Some(track_area) = track_area { track_area.clip_h(header_h) } - else if let Some(scene_area) = scene_area { scene_area.clip_w(scenes_w) } - else { area.clip_w(scenes_w).clip_h(header_h) }, &CORNERS)? - }) - })) - }).bg(bg.rgb); - let color = TuiTheme::title_fg(view.focused); - let size = format!("{}x{}", view.size.w(), view.size.h()); - let lower_right = TuiStyle::fg(size, color).pull_x(1).align_se().fill_xy(); - lay!(arrangement, lower_right) -} diff --git a/crates/tek_tui/src/tui_mixer.rs b/crates/tek_tui/src/tui_mixer.rs index c2b49b5b..8c8ccab4 100644 --- a/crates/tek_tui/src/tui_mixer.rs +++ b/crates/tek_tui/src/tui_mixer.rs @@ -166,3 +166,89 @@ impl Content for Track { } } } + +impl Handle for Mixer { + fn handle (&mut self, engine: &TuiInput) -> Perhaps { + if let TuiEvent::Input(crossterm::event::Event::Key(event)) = engine.event() { + + match event.code { + //KeyCode::Char('c') => { + //if event.modifiers == KeyModifiers::CONTROL { + //self.exit(); + //} + //}, + KeyCode::Down => { + self.selected_track = (self.selected_track + 1) % self.tracks.len(); + println!("{}", self.selected_track); + return Ok(Some(true)) + }, + KeyCode::Up => { + if self.selected_track == 0 { + self.selected_track = self.tracks.len() - 1; + } else { + self.selected_track -= 1; + } + println!("{}", self.selected_track); + return Ok(Some(true)) + }, + KeyCode::Left => { + if self.selected_column == 0 { + self.selected_column = 6 + } else { + self.selected_column -= 1; + } + return Ok(Some(true)) + }, + KeyCode::Right => { + if self.selected_column == 6 { + self.selected_column = 0 + } else { + self.selected_column += 1; + } + return Ok(Some(true)) + }, + _ => { + println!("\n{event:?}"); + } + } + + } + Ok(None) + } +} +impl Handle for Track { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + match from.event() { + //, NONE, "chain_cursor_up", "move cursor up", || { + key!(KeyCode::Up) => { + Ok(Some(true)) + }, + // , NONE, "chain_cursor_down", "move cursor down", || { + key!(KeyCode::Down) => { + Ok(Some(true)) + }, + // Left, NONE, "chain_cursor_left", "move cursor left", || { + key!(KeyCode::Left) => { + //if let Some(track) = app.arranger.track_mut() { + //track.device = track.device.saturating_sub(1); + //return Ok(true) + //} + Ok(Some(true)) + }, + // , NONE, "chain_cursor_right", "move cursor right", || { + key!(KeyCode::Right) => { + //if let Some(track) = app.arranger.track_mut() { + //track.device = (track.device + 1).min(track.devices.len().saturating_sub(1)); + //return Ok(true) + //} + Ok(Some(true)) + }, + // , NONE, "chain_mode_switch", "switch the display mode", || { + key!(KeyCode::Char('`')) => { + //app.chain_mode = !app.chain_mode; + Ok(Some(true)) + }, + _ => Ok(None) + } + } +} diff --git a/crates/tek_tui/src/tui_mixer_cmd.rs b/crates/tek_tui/src/tui_mixer_cmd.rs deleted file mode 100644 index 559f2896..00000000 --- a/crates/tek_tui/src/tui_mixer_cmd.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::*; - -impl Handle for Mixer { - fn handle (&mut self, engine: &TuiInput) -> Perhaps { - if let TuiEvent::Input(crossterm::event::Event::Key(event)) = engine.event() { - - match event.code { - //KeyCode::Char('c') => { - //if event.modifiers == KeyModifiers::CONTROL { - //self.exit(); - //} - //}, - KeyCode::Down => { - self.selected_track = (self.selected_track + 1) % self.tracks.len(); - println!("{}", self.selected_track); - return Ok(Some(true)) - }, - KeyCode::Up => { - if self.selected_track == 0 { - self.selected_track = self.tracks.len() - 1; - } else { - self.selected_track -= 1; - } - println!("{}", self.selected_track); - return Ok(Some(true)) - }, - KeyCode::Left => { - if self.selected_column == 0 { - self.selected_column = 6 - } else { - self.selected_column -= 1; - } - return Ok(Some(true)) - }, - KeyCode::Right => { - if self.selected_column == 6 { - self.selected_column = 0 - } else { - self.selected_column += 1; - } - return Ok(Some(true)) - }, - _ => { - println!("\n{event:?}"); - } - } - - } - Ok(None) - } -} -impl Handle for Track { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - //, NONE, "chain_cursor_up", "move cursor up", || { - key!(KeyCode::Up) => { - Ok(Some(true)) - }, - // , NONE, "chain_cursor_down", "move cursor down", || { - key!(KeyCode::Down) => { - Ok(Some(true)) - }, - // Left, NONE, "chain_cursor_left", "move cursor left", || { - key!(KeyCode::Left) => { - //if let Some(track) = app.arranger.track_mut() { - //track.device = track.device.saturating_sub(1); - //return Ok(true) - //} - Ok(Some(true)) - }, - // , NONE, "chain_cursor_right", "move cursor right", || { - key!(KeyCode::Right) => { - //if let Some(track) = app.arranger.track_mut() { - //track.device = (track.device + 1).min(track.devices.len().saturating_sub(1)); - //return Ok(true) - //} - Ok(Some(true)) - }, - // , NONE, "chain_mode_switch", "switch the display mode", || { - key!(KeyCode::Char('`')) => { - //app.chain_mode = !app.chain_mode; - Ok(Some(true)) - }, - _ => Ok(None) - } - } -} diff --git a/crates/tek_tui/src/tui_phrase.rs b/crates/tek_tui/src/tui_phrase.rs index abf2b8e8..efa57cea 100644 --- a/crates/tek_tui/src/tui_phrase.rs +++ b/crates/tek_tui/src/tui_phrase.rs @@ -351,3 +351,157 @@ pub(crate) fn keys_vert () -> Buffer { const NTH_OCTAVE: [&'static str; 11] = [ "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", ]; + +#[derive(Clone, PartialEq)] +pub enum PhraseEditorCommand { + // TODO: 1-9 seek markers that by default start every 8th of the phrase + ToggleDirection, + EnterEditMode, + ExitEditMode, + NoteAppend, + NoteSet, + NoteCursorSet(usize), + NoteLengthSet(usize), + NoteScrollSet(usize), + TimeCursorSet(usize), + TimeScrollSet(usize), + TimeZoomSet(usize), +} + +impl Handle for PhraseEditor { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + PhraseEditorCommand::execute_with_state(self, from) + } +} + +impl InputToCommand> for PhraseEditorCommand { + fn input_to_command (state: &PhraseEditor, from: &TuiInput) -> Option { + use PhraseEditorCommand::*; + Some(match from.event() { + key!(KeyCode::Char('`')) => ToggleDirection, + key!(KeyCode::Enter) => EnterEditMode, + key!(KeyCode::Esc) => ExitEditMode, + key!(KeyCode::Char('[')) => NoteLengthSet(0), + key!(KeyCode::Char(']')) => NoteLengthSet(0), + key!(KeyCode::Char('a')) => NoteAppend, + key!(KeyCode::Char('s')) => NoteSet, + key!(KeyCode::Char('-')) => TimeZoomSet(0), + key!(KeyCode::Char('_')) => TimeZoomSet(0), + key!(KeyCode::Char('=')) => TimeZoomSet(0), + key!(KeyCode::Char('+')) => TimeZoomSet(0), + key!(KeyCode::PageUp) => NoteScrollSet(0), + key!(KeyCode::PageDown) => NoteScrollSet(0), + key!(KeyCode::Up) => match state.entered { + true => NoteCursorSet(0), + false => NoteScrollSet(0), + }, + key!(KeyCode::Down) => match state.entered { + true => NoteCursorSet(0), + false => NoteScrollSet(0), + }, + key!(KeyCode::Left) => match state.entered { + true => TimeCursorSet(0), + false => TimeScrollSet(0), + }, + key!(KeyCode::Right) => match state.entered { + true => TimeCursorSet(0), + false => TimeScrollSet(0), + }, + _ => return None + }) + } +} + +impl Command> for PhraseEditorCommand { + //fn translate (self, state: &PhraseEditor) -> Self { + //use PhraseEditorCommand::*; + //match self { + //GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, }, + //GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, }, + //GoLeft => match state.entered { true => TimeCursorDec, false => TimeScrollDec, }, + //GoRight => match state.entered { true => TimeCursorInc, false => TimeScrollInc, }, + //_ => self + //} + //} + fn execute (self, state: &mut PhraseEditor) -> Perhaps { + use PhraseEditorCommand::*; + match self.translate(state) { + ToggleDirection => { + state.mode = !state.mode; + }, + EnterEditMode => { + state.entered = true; + }, + ExitEditMode => { + state.entered = false; + }, + TimeZoomOut => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().scale = next_note_length(scale) + }, + TimeZoomIn => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().scale = prev_note_length(scale) + }, + TimeCursorDec => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().point_dec(scale); + }, + TimeCursorInc => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().point_inc(scale); + }, + TimeScrollDec => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().start_dec(scale); + }, + TimeScrollInc => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().start_inc(scale); + }, + NoteCursorDec => { + let mut axis = state.note_axis.write().unwrap(); + axis.point_inc(1); + if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } } + }, + NoteCursorInc => { + let mut axis = state.note_axis.write().unwrap(); + axis.point_dec(1); + if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } } + }, + NoteScrollDec => { + state.note_axis.write().unwrap().start_inc(1); + }, + NoteScrollInc => { + state.note_axis.write().unwrap().start_dec(1); + }, + NoteLengthDec => { + state.note_len = prev_note_length(state.note_len) + }, + NoteLengthInc => { + state.note_len = next_note_length(state.note_len) + }, + NotePageUp => { + let mut axis = state.note_axis.write().unwrap(); + axis.start_dec(3); + axis.point_dec(3); + }, + NotePageDown => { + let mut axis = state.note_axis.write().unwrap(); + axis.start_inc(3); + axis.point_inc(3); + }, + NoteAppend => { + if state.entered { + state.put(); + state.time_cursor_advance(); + } + }, + NoteSet => { + if state.entered { state.put(); } + }, + _ => unreachable!() + } + Ok(None) + } +} diff --git a/crates/tek_tui/src/tui_phrase_cmd.rs b/crates/tek_tui/src/tui_phrase_cmd.rs deleted file mode 100644 index 5715b39e..00000000 --- a/crates/tek_tui/src/tui_phrase_cmd.rs +++ /dev/null @@ -1,155 +0,0 @@ -use crate::*; - -#[derive(Clone, PartialEq)] -pub enum PhraseEditorCommand { - // TODO: 1-9 seek markers that by default start every 8th of the phrase - ToggleDirection, - EnterEditMode, - ExitEditMode, - NoteAppend, - NoteSet, - NoteCursorSet(usize), - NoteLengthSet(usize), - NoteScrollSet(usize), - TimeCursorSet(usize), - TimeScrollSet(usize), - TimeZoomSet(usize), -} - -impl Handle for PhraseEditor { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - PhraseEditorCommand::execute_with_state(self, from) - } -} - -impl InputToCommand> for PhraseEditorCommand { - fn input_to_command (state: &PhraseEditor, from: &TuiInput) -> Option { - use PhraseEditorCommand::*; - Some(match from.event() { - key!(KeyCode::Char('`')) => ToggleDirection, - key!(KeyCode::Enter) => EnterEditMode, - key!(KeyCode::Esc) => ExitEditMode, - key!(KeyCode::Char('[')) => NoteLengthSet(0), - key!(KeyCode::Char(']')) => NoteLengthSet(0), - key!(KeyCode::Char('a')) => NoteAppend, - key!(KeyCode::Char('s')) => NoteSet, - key!(KeyCode::Char('-')) => TimeZoomSet(0), - key!(KeyCode::Char('_')) => TimeZoomSet(0), - key!(KeyCode::Char('=')) => TimeZoomSet(0), - key!(KeyCode::Char('+')) => TimeZoomSet(0), - key!(KeyCode::PageUp) => NoteScrollSet(0), - key!(KeyCode::PageDown) => NoteScrollSet(0), - key!(KeyCode::Up) => match state.entered { - true => NoteCursorSet(0), - false => NoteScrollSet(0), - }, - key!(KeyCode::Down) => match state.entered { - true => NoteCursorSet(0), - false => NoteScrollSet(0), - }, - key!(KeyCode::Left) => match state.entered { - true => TimeCursorSet(0), - false => TimeScrollSet(0), - }, - key!(KeyCode::Right) => match state.entered { - true => TimeCursorSet(0), - false => TimeScrollSet(0), - }, - _ => return None - }) - } -} - -impl Command> for PhraseEditorCommand { - //fn translate (self, state: &PhraseEditor) -> Self { - //use PhraseEditorCommand::*; - //match self { - //GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, }, - //GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, }, - //GoLeft => match state.entered { true => TimeCursorDec, false => TimeScrollDec, }, - //GoRight => match state.entered { true => TimeCursorInc, false => TimeScrollInc, }, - //_ => self - //} - //} - fn execute (self, state: &mut PhraseEditor) -> Perhaps { - use PhraseEditorCommand::*; - match self.translate(state) { - ToggleDirection => { - state.mode = !state.mode; - }, - EnterEditMode => { - state.entered = true; - }, - ExitEditMode => { - state.entered = false; - }, - TimeZoomOut => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().scale = next_note_length(scale) - }, - TimeZoomIn => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().scale = prev_note_length(scale) - }, - TimeCursorDec => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().point_dec(scale); - }, - TimeCursorInc => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().point_inc(scale); - }, - TimeScrollDec => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().start_dec(scale); - }, - TimeScrollInc => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().start_inc(scale); - }, - NoteCursorDec => { - let mut axis = state.note_axis.write().unwrap(); - axis.point_inc(1); - if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } } - }, - NoteCursorInc => { - let mut axis = state.note_axis.write().unwrap(); - axis.point_dec(1); - if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } } - }, - NoteScrollDec => { - state.note_axis.write().unwrap().start_inc(1); - }, - NoteScrollInc => { - state.note_axis.write().unwrap().start_dec(1); - }, - NoteLengthDec => { - state.note_len = prev_note_length(state.note_len) - }, - NoteLengthInc => { - state.note_len = next_note_length(state.note_len) - }, - NotePageUp => { - let mut axis = state.note_axis.write().unwrap(); - axis.start_dec(3); - axis.point_dec(3); - }, - NotePageDown => { - let mut axis = state.note_axis.write().unwrap(); - axis.start_inc(3); - axis.point_inc(3); - }, - NoteAppend => { - if state.entered { - state.put(); - state.time_cursor_advance(); - } - }, - NoteSet => { - if state.entered { state.put(); } - }, - _ => unreachable!() - } - Ok(None) - } -} diff --git a/crates/tek_tui/src/tui_plugin.rs b/crates/tek_tui/src/tui_plugin.rs index c42c1c85..ff6a0971 100644 --- a/crates/tek_tui/src/tui_plugin.rs +++ b/crates/tek_tui/src/tui_plugin.rs @@ -83,3 +83,67 @@ fn draw_header (state: &Plugin, to: &mut TuiOutput, x: u16, y: u16, w: u1 } Ok(Rect { x, y, width: w, height: 1 }) } + +impl Handle for Plugin { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + match from.event() { + key!(KeyCode::Up) => { + self.selected = self.selected.saturating_sub(1); + Ok(Some(true)) + }, + key!(KeyCode::Down) => { + self.selected = (self.selected + 1).min(match &self.plugin { + Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, + _ => unimplemented!() + }); + Ok(Some(true)) + }, + key!(KeyCode::PageUp) => { + self.selected = self.selected.saturating_sub(8); + Ok(Some(true)) + }, + key!(KeyCode::PageDown) => { + self.selected = (self.selected + 10).min(match &self.plugin { + Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, + _ => unimplemented!() + }); + Ok(Some(true)) + }, + key!(KeyCode::Char(',')) => { + match self.plugin.as_mut() { + Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { + let index = port_list[self.selected].index; + if let Some(value) = instance.control_input(index) { + instance.set_control_input(index, value - 0.01); + } + }, + _ => {} + } + Ok(Some(true)) + }, + key!(KeyCode::Char('.')) => { + match self.plugin.as_mut() { + Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { + let index = port_list[self.selected].index; + if let Some(value) = instance.control_input(index) { + instance.set_control_input(index, value + 0.01); + } + }, + _ => {} + } + Ok(Some(true)) + }, + key!(KeyCode::Char('g')) => { + match self.plugin { + Some(PluginKind::LV2(ref mut plugin)) => { + plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?); + }, + Some(_) => unreachable!(), + None => {} + } + Ok(Some(true)) + }, + _ => Ok(None) + } + } +} diff --git a/crates/tek_tui/src/tui_plugin_cmd.rs b/crates/tek_tui/src/tui_plugin_cmd.rs deleted file mode 100644 index 2ecbebd7..00000000 --- a/crates/tek_tui/src/tui_plugin_cmd.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::*; -impl Handle for Plugin { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - key!(KeyCode::Up) => { - self.selected = self.selected.saturating_sub(1); - Ok(Some(true)) - }, - key!(KeyCode::Down) => { - self.selected = (self.selected + 1).min(match &self.plugin { - Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, - _ => unimplemented!() - }); - Ok(Some(true)) - }, - key!(KeyCode::PageUp) => { - self.selected = self.selected.saturating_sub(8); - Ok(Some(true)) - }, - key!(KeyCode::PageDown) => { - self.selected = (self.selected + 10).min(match &self.plugin { - Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, - _ => unimplemented!() - }); - Ok(Some(true)) - }, - key!(KeyCode::Char(',')) => { - match self.plugin.as_mut() { - Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { - let index = port_list[self.selected].index; - if let Some(value) = instance.control_input(index) { - instance.set_control_input(index, value - 0.01); - } - }, - _ => {} - } - Ok(Some(true)) - }, - key!(KeyCode::Char('.')) => { - match self.plugin.as_mut() { - Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { - let index = port_list[self.selected].index; - if let Some(value) = instance.control_input(index) { - instance.set_control_input(index, value + 0.01); - } - }, - _ => {} - } - Ok(Some(true)) - }, - key!(KeyCode::Char('g')) => { - match self.plugin { - Some(PluginKind::LV2(ref mut plugin)) => { - plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?); - }, - Some(_) => unreachable!(), - None => {} - } - Ok(Some(true)) - }, - _ => Ok(None) - } - } -} diff --git a/crates/tek_tui/src/tui_pool.rs b/crates/tek_tui/src/tui_pool.rs index 3b3295d0..602dbc11 100644 --- a/crates/tek_tui/src/tui_pool.rs +++ b/crates/tek_tui/src/tui_pool.rs @@ -132,3 +132,87 @@ impl Content for PhrasePoolView { ) } } + +#[derive(Clone, PartialEq)] +pub enum PhrasePoolViewCommand { + Select(usize), + Edit(PhrasePoolCommand), + Rename(PhraseRenameCommand), + Length(PhraseLengthCommand), +} + +impl Handle for PhrasePoolView { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + PhrasePoolViewCommand::execute_with_state(self, from) + } +} + +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(Cmd::Select(0)), + key!(KeyCode::Down) => Some(Cmd::Select(0)), + key!(KeyCode::Char(',')) => Some(Cmd::Edit(Edit::Swap(0, 0))), + key!(KeyCode::Char('.')) => Some(Cmd::Edit(Edit::Swap(0, 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(..)) => { + Rename::input_to_command(state, input).map(Cmd::Rename) + }, + Some(PhrasePoolMode::Length(..)) => { + Length::input_to_command(state, input).map(Cmd::Length) + }, + _ => None + } + } + } +} + +impl Command> for PhrasePoolViewCommand { + fn execute (self, view: &mut PhrasePoolView) -> Perhaps { + use PhraseRenameCommand as Rename; + use PhraseLengthCommand as Length; + match self { + Self::Select(phrase) => { + view.phrase = phrase + }, + Self::Edit(command) => { + return Ok(command.execute(&mut view.model)?.map(Self::Edit)) + } + Self::Rename(command) => match command { + Rename::Begin => { + view.mode = Some(PhrasePoolMode::Rename( + view.phrase, + view.model.phrases[view.phrase].read().unwrap().name.clone() + )) + }, + _ => { + return Ok(command.execute(view)?.map(Self::Rename)) + } + }, + Self::Length(command) => match command { + Length::Begin => { + view.mode = Some(PhrasePoolMode::Length( + view.phrase, + view.model.phrases[view.phrase].read().unwrap().length, + PhraseLengthFocus::Bar + )) + }, + _ => { + return Ok(command.execute(view)?.map(Self::Length)) + } + }, + } + Ok(None) + } +} diff --git a/crates/tek_tui/src/tui_pool_cmd.rs b/crates/tek_tui/src/tui_pool_cmd.rs deleted file mode 100644 index 014779a7..00000000 --- a/crates/tek_tui/src/tui_pool_cmd.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::*; - -#[derive(Clone, PartialEq)] -pub enum PhrasePoolViewCommand { - Select(usize), - Edit(PhrasePoolCommand), - Rename(PhraseRenameCommand), - Length(PhraseLengthCommand), -} - -impl Handle for PhrasePoolView { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - PhrasePoolViewCommand::execute_with_state(self, from) - } -} - -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(Cmd::Select(0)), - key!(KeyCode::Down) => Some(Cmd::Select(0)), - key!(KeyCode::Char(',')) => Some(Cmd::Edit(Edit::Swap(0, 0))), - key!(KeyCode::Char('.')) => Some(Cmd::Edit(Edit::Swap(0, 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(..)) => { - Rename::input_to_command(state, input).map(Cmd::Rename) - }, - Some(PhrasePoolMode::Length(..)) => { - Length::input_to_command(state, input).map(Cmd::Length) - }, - _ => None - } - } - } -} - -impl Command> for PhrasePoolViewCommand { - fn execute (self, view: &mut PhrasePoolView) -> Perhaps { - use PhraseRenameCommand as Rename; - use PhraseLengthCommand as Length; - match self { - Self::Select(phrase) => { - view.phrase = phrase - }, - Self::Edit(command) => { - return Ok(command.execute(&mut view.model)?.map(Self::Edit)) - } - Self::Rename(command) => match command { - Rename::Begin => { - view.mode = Some(PhrasePoolMode::Rename( - view.phrase, - view.model.phrases[view.phrase].read().unwrap().name.clone() - )) - }, - _ => { - return Ok(command.execute(view)?.map(Self::Rename)) - } - }, - Self::Length(command) => match command { - Length::Begin => { - view.mode = Some(PhrasePoolMode::Length( - view.phrase, - view.model.phrases[view.phrase].read().unwrap().length, - PhraseLengthFocus::Bar - )) - }, - _ => { - return Ok(command.execute(view)?.map(Self::Length)) - } - }, - } - Ok(None) - } -} diff --git a/crates/tek_tui/src/tui_sequencer.rs b/crates/tek_tui/src/tui_sequencer.rs index 7a5a8f74..79984359 100644 --- a/crates/tek_tui/src/tui_sequencer.rs +++ b/crates/tek_tui/src/tui_sequencer.rs @@ -1,6 +1,11 @@ use crate::*; -pub type SequencerApp = AppView, SequencerViewCommand>; +pub type SequencerApp = AppView< + E, + SequencerView, + SequencerViewCommand, + SequencerStatusBar, +>; impl TryFrom<&Arc>> for SequencerApp { type Error = Box; @@ -44,6 +49,33 @@ pub struct SequencerView { pub split: u16, } +/// Status bar for sequencer app +#[derive(Copy, Clone)] +pub enum SequencerStatusBar { + Transport, + PhrasePool, + PhraseEditor, +} + +#[derive(Clone, PartialEq)] +pub enum SequencerViewCommand { + Focus(FocusCommand), + Transport(TransportViewCommand), + Phrases(PhrasePoolViewCommand), + Editor(PhraseEditorCommand), +} + +/// Sections in the sequencer app that may be focused +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum SequencerFocus { + /// The transport (toolbar) is focused + Transport, + /// The phrase list (pool) is focused + PhrasePool, + /// The phrase editor (sequencer) is focused + PhraseEditor, +} + impl Content for SequencerView { type Engine = Tui; fn content (&self) -> impl Widget { @@ -62,3 +94,108 @@ impl Audio for SequencerView { self.model.process(client, scope) } } + +impl StatusBar for SequencerStatusBar { + type State = (); + fn hotkey_fg () -> Color { + TuiTheme::hotkey_fg() + } + fn update (&mut self, state: &()) { + todo!() + } +} + +impl Content for SequencerStatusBar { + type Engine = Tui; + fn content (&self) -> impl Widget { + todo!(); + "" + } +} + +impl Handle for SequencerView { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + SequencerViewCommand::execute_with_state(self, i) + } +} + +impl Command> for SequencerViewCommand { + fn execute (self, state: &mut SequencerView) -> Perhaps { + match self { + Self::Focus(cmd) => delegate(cmd, Self::Focus, state), + Self::Phrases(cmd) => delegate(cmd, Self::Phrases, &mut state.phrases), + Self::Editor(cmd) => delegate(cmd, Self::Editor, &mut state.editor), + Self::Transport(cmd) => delegate(cmd, Self::Transport, &mut state.transport) + } + } +} + +impl InputToCommand> for SequencerViewCommand { + fn input_to_command (state: &SequencerView, input: &TuiInput) -> Option { + use FocusCommand::*; + match input.event() { + key!(KeyCode::Tab) => Some(Self::Focus(Next)), + key!(Shift-KeyCode::Tab) => Some(Self::Focus(Prev)), + key!(KeyCode::BackTab) => Some(Self::Focus(Prev)), + key!(Shift-KeyCode::BackTab) => Some(Self::Focus(Prev)), + key!(KeyCode::Up) => Some(Self::Focus(Up)), + key!(KeyCode::Down) => Some(Self::Focus(Down)), + key!(KeyCode::Left) => Some(Self::Focus(Left)), + key!(KeyCode::Right) => Some(Self::Focus(Right)), + _ => match state.focused() { + SequencerFocus::Transport => TransportViewCommand::input_to_command( + &state.transport, input + ).map(Self::Transport), + SequencerFocus::PhrasePool => PhrasePoolViewCommand::input_to_command( + &state.phrases, input + ).map(Self::Phrases), + SequencerFocus::PhraseEditor => PhraseEditorCommand::input_to_command( + &state.editor, input + ).map(Self::Editor), + + } + } + } +} + +impl FocusGrid for SequencerApp { + type Item = AppViewFocus; + fn cursor (&self) -> (usize, usize) { + self.cursor + } + fn cursor_mut (&mut self) -> &mut (usize, usize) { + &mut self.cursor + } + fn focus_enter (&mut self) { + let focused = self.focused(); + if !self.entered { + self.entered = true; + // TODO + } + } + fn focus_exit (&mut self) { + if self.entered { + self.entered = false; + // TODO + } + } + fn entered (&self) -> Option { + if self.entered { + Some(self.focused()) + } else { + None + } + } + fn layout (&self) -> &[&[Self::Item]] { + use AppViewFocus::*; + use SequencerFocus::*; + &[ + &[Menu, Menu ], + &[Content(Transport), Content(Transport) ], + &[Content(PhrasePool), Content(PhraseEditor)], + ] + } + fn update_focus (&mut self) { + // TODO + } +} diff --git a/crates/tek_tui/src/tui_sequencer_bar.rs b/crates/tek_tui/src/tui_sequencer_bar.rs deleted file mode 100644 index 59c1c55b..00000000 --- a/crates/tek_tui/src/tui_sequencer_bar.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::*; - -/// Status bar for sequencer app -pub enum SequencerStatusBar { - Transport, - 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_sequencer_cmd.rs b/crates/tek_tui/src/tui_sequencer_cmd.rs deleted file mode 100644 index ae05dab4..00000000 --- a/crates/tek_tui/src/tui_sequencer_cmd.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::*; - -#[derive(Clone, PartialEq)] -pub enum SequencerViewCommand { - Focus(FocusCommand), - Transport(TransportViewCommand), - Phrases(PhrasePoolViewCommand), - Editor(PhraseEditorCommand), -} - -impl Handle for SequencerView { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - SequencerViewCommand::execute_with_state(self, i) - } -} - -impl Command> for SequencerViewCommand { - fn execute (self, state: &mut SequencerView) -> Perhaps { - match self { - Self::Focus(cmd) => delegate(cmd, Self::Focus, state), - Self::Phrases(cmd) => delegate(cmd, Self::Phrases, &mut state.phrases), - Self::Editor(cmd) => delegate(cmd, Self::Editor, &mut state.editor), - Self::Transport(cmd) => delegate(cmd, Self::Transport, &mut state.transport) - } - } -} - -impl InputToCommand> for SequencerViewCommand { - fn input_to_command (state: &SequencerView, input: &TuiInput) -> Option { - use FocusCommand::*; - match input.event() { - key!(KeyCode::Tab) => Some(Self::Focus(Next)), - key!(Shift-KeyCode::Tab) => Some(Self::Focus(Prev)), - key!(KeyCode::BackTab) => Some(Self::Focus(Prev)), - key!(Shift-KeyCode::BackTab) => Some(Self::Focus(Prev)), - key!(KeyCode::Up) => Some(Self::Focus(Up)), - key!(KeyCode::Down) => Some(Self::Focus(Down)), - key!(KeyCode::Left) => Some(Self::Focus(Left)), - key!(KeyCode::Right) => Some(Self::Focus(Right)), - _ => match state.focused() { - SequencerFocus::Transport => TransportViewCommand::input_to_command( - &state.transport, input - ).map(Self::Transport), - SequencerFocus::PhrasePool => PhrasePoolViewCommand::input_to_command( - &state.phrases, input - ).map(Self::Phrases), - SequencerFocus::PhraseEditor => PhraseEditorCommand::input_to_command( - &state.editor, input - ).map(Self::Editor), - - } - } - } -} diff --git a/crates/tek_tui/src/tui_sequencer_foc.rs b/crates/tek_tui/src/tui_sequencer_foc.rs deleted file mode 100644 index 9611358e..00000000 --- a/crates/tek_tui/src/tui_sequencer_foc.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::*; - -/// Sections in the sequencer app that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum SequencerFocus { - /// The transport (toolbar) is focused - Transport, - /// The phrase list (pool) is focused - PhrasePool, - /// The phrase editor (sequencer) is focused - PhraseEditor, -} - -impl FocusGrid for AppView, SequencerViewCommand> { - type Item = AppViewFocus; - fn cursor (&self) -> (usize, usize) { - self.cursor - } - fn cursor_mut (&mut self) -> &mut (usize, usize) { - &mut self.cursor - } - fn focus_enter (&mut self) { - let focused = self.focused(); - if !self.entered { - self.entered = true; - // TODO - } - } - fn focus_exit (&mut self) { - if self.entered { - self.entered = false; - // TODO - } - } - fn entered (&self) -> Option { - if self.entered { - Some(self.focused()) - } else { - None - } - } - fn layout (&self) -> &[&[Self::Item]] { - use AppViewFocus::*; - use SequencerFocus::*; - &[ - &[Menu, Menu ], - &[Content(Transport), Content(Transport) ], - &[Content(PhrasePool), Content(PhraseEditor)], - ] - } - fn update_focus (&mut self) { - // TODO - } -} diff --git a/crates/tek_tui/src/tui_status.rs b/crates/tek_tui/src/tui_status.rs new file mode 100644 index 00000000..faa2910c --- /dev/null +++ b/crates/tek_tui/src/tui_status.rs @@ -0,0 +1,28 @@ +use crate::*; + +pub trait StatusBar: Copy + Widget { + + type State; + + fn hotkey_fg () -> Color where Self: Sized; + + fn update (&mut self, state: &Self::State) where Self: Sized; + + fn command (commands: &[[impl Widget;3]]) + -> impl Widget + '_ + where + Self: Sized + { + 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_tui/src/tui_theme.rs b/crates/tek_tui/src/tui_theme.rs index a4be1110..da2f5ab5 100644 --- a/crates/tek_tui/src/tui_theme.rs +++ b/crates/tek_tui/src/tui_theme.rs @@ -15,7 +15,7 @@ impl TuiTheme { pub fn separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) } - pub fn hotkey_fg () -> Color { + pub const fn hotkey_fg () -> Color { Color::Rgb(255, 255, 0) } pub fn mode_bg () -> Color { diff --git a/crates/tek_tui/src/tui_transport.rs b/crates/tek_tui/src/tui_transport.rs index 97ccde76..f1d50727 100644 --- a/crates/tek_tui/src/tui_transport.rs +++ b/crates/tek_tui/src/tui_transport.rs @@ -1,6 +1,11 @@ use crate::*; -pub type TransportApp = AppView, TransportViewCommand>; +pub type TransportApp = AppView< + E, + TransportView, + TransportViewCommand, + TransportStatusBar +>; impl TryFrom<&Arc>> for TransportApp { type Error = Box; @@ -78,3 +83,194 @@ impl Audio for TransportView { self.model.process(client, scope) } } + +#[derive(Copy, Clone)] +pub struct TransportStatusBar; + +impl StatusBar for TransportStatusBar { + type State = (); + fn hotkey_fg () -> Color { + TuiTheme::hotkey_fg() + } + fn update (&mut self, state: &()) { + todo!() + } +} + +impl Content for TransportStatusBar { + type Engine = Tui; + fn content (&self) -> impl Widget { + todo!(); + "" + } +} + +#[derive(Copy, Clone, PartialEq)] +pub enum TransportViewCommand { + Focus(FocusCommand), + Transport(TransportCommand), +} + +impl Handle for TransportView { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + TransportViewCommand::execute_with_state(self, from) + } +} + +impl InputToCommand> for TransportViewCommand { + fn input_to_command (view: &TransportView, input: &TuiInput) -> Option { + use TransportViewFocus as Focus; + use FocusCommand as FocusCmd; + use TransportCommand as Cmd; + let clock = view.model.clock(); + Some(match input.event() { + + key!(KeyCode::Left) => Self::Focus(FocusCmd::Prev), + key!(KeyCode::Right) => Self::Focus(FocusCmd::Next), + + key!(KeyCode::Char('.')) => Self::Transport(match view.focus { + Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() + 1.0), + Focus::Quant => Cmd::SetQuant(next_note_length(clock.quant.get()as usize)as f64), + Focus::Sync => Cmd::SetSync(next_note_length(clock.sync.get()as usize)as f64+1.), + Focus::PlayPause => {todo!()}, + Focus::Clock => {todo!()} + }), + key!(KeyCode::Char(',')) => Self::Transport(match view.focus { + Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() - 1.0), + Focus::Quant => Cmd::SetQuant(prev_note_length(clock.quant.get()as usize)as f64), + Focus::Sync => Cmd::SetSync(prev_note_length(clock.sync.get()as usize)as f64+1.), + Focus::PlayPause => {todo!()}, + Focus::Clock => {todo!()} + }), + key!(KeyCode::Char('>')) => Self::Transport(match view.focus { + Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() + 0.001), + Focus::Quant => Cmd::SetQuant(next_note_length(clock.quant.get()as usize)as f64), + Focus::Sync => Cmd::SetSync(next_note_length(clock.sync.get()as usize)as f64+1.), + Focus::PlayPause => {todo!()}, + Focus::Clock => {todo!()} + }), + key!(KeyCode::Char('<')) => Self::Transport(match view.focus { + Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() - 0.001), + Focus::Quant => Cmd::SetQuant(prev_note_length(clock.quant.get()as usize)as f64), + Focus::Sync => Cmd::SetSync(prev_note_length(clock.sync.get()as usize)as f64+1.), + Focus::PlayPause => {todo!()}, + Focus::Clock => {todo!()} + }), + + _ => return None + }) + } +} + +impl Command> for TransportViewCommand { + fn execute (self, view: &mut TransportView) -> Perhaps { + let clock = view.model.clock(); + Ok(Some(match self { + Self::Focus(command) => Self::Focus({ + use FocusCommand::*; + match command { + Next => { todo!() }, + Prev => { todo!() }, + _ => { todo!() } + } + }), + Self::Transport(command) => Self::Transport({ + use TransportCommand::*; + match command { + SetBpm(bpm) => SetBpm(clock.timebase().bpm.set(bpm)), + SetQuant(quant) => SetQuant(clock.quant.set(quant)), + SetSync(sync) => SetSync(clock.sync.set(sync)), + _ => { + todo!() + } + } + }), + })) + } +} + +impl TransportViewFocus { + pub fn next (&mut self) { + *self = match self { + Self::PlayPause => Self::Bpm, + Self::Bpm => Self::Quant, + Self::Quant => Self::Sync, + Self::Sync => Self::Clock, + Self::Clock => Self::PlayPause, + } + } + pub fn prev (&mut self) { + *self = match self { + Self::PlayPause => Self::Clock, + Self::Bpm => Self::PlayPause, + Self::Quant => Self::Bpm, + Self::Sync => Self::Quant, + Self::Clock => Self::Sync, + } + } + pub fn wrap <'a, W: Widget> ( + self, parent_focus: bool, focus: Self, widget: &'a W + ) -> impl Widget + 'a { + let focused = parent_focus && focus == self; + let corners = focused.then_some(CORNERS); + let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50))); + lay!(corners, highlight, *widget) + } +} + +/// Which item of the transport toolbar is focused +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum TransportViewFocus { + Bpm, + Sync, + PlayPause, + Clock, + Quant, +} + +impl FocusGrid for TransportApp { + type Item = AppViewFocus; + fn cursor (&self) -> (usize, usize) { + self.cursor + } + fn cursor_mut (&mut self) -> &mut (usize, usize) { + &mut self.cursor + } + fn focus_enter (&mut self) { + let focused = self.focused(); + if !self.entered { + self.entered = true; + // TODO + } + } + fn focus_exit (&mut self) { + if self.entered { + self.entered = false; + // TODO + } + } + fn entered (&self) -> Option { + if self.entered { + Some(self.focused()) + } else { + None + } + } + fn layout (&self) -> &[&[Self::Item]] { + use AppViewFocus::*; + use TransportViewFocus::*; + &[ + &[Menu], + &[ + Content(Bpm), + Content(Sync), + Content(PlayPause), + Content(Clock), + Content(Quant), + ], + ] + } + fn update_focus (&mut self) { + // TODO + } +} diff --git a/crates/tek_tui/src/tui_transport_bar.rs b/crates/tek_tui/src/tui_transport_bar.rs deleted file mode 100644 index bc8d33f2..00000000 --- a/crates/tek_tui/src/tui_transport_bar.rs +++ /dev/null @@ -1,17 +0,0 @@ -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!(); - "" - } -} diff --git a/crates/tek_tui/src/tui_transport_cmd.rs b/crates/tek_tui/src/tui_transport_cmd.rs deleted file mode 100644 index 78c08d04..00000000 --- a/crates/tek_tui/src/tui_transport_cmd.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::*; - -#[derive(Copy, Clone, PartialEq)] -pub enum TransportViewCommand { - Focus(FocusCommand), - Transport(TransportCommand), -} - -impl Handle for TransportView { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - TransportViewCommand::execute_with_state(self, from) - } -} - -impl InputToCommand> for TransportViewCommand { - fn input_to_command (view: &TransportView, input: &TuiInput) -> Option { - use TransportViewFocus as Focus; - use FocusCommand as FocusCmd; - use TransportCommand as Cmd; - let clock = view.model.clock(); - Some(match input.event() { - - key!(KeyCode::Left) => Self::Focus(FocusCmd::Prev), - key!(KeyCode::Right) => Self::Focus(FocusCmd::Next), - - key!(KeyCode::Char('.')) => Self::Transport(match view.focus { - Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() + 1.0), - Focus::Quant => Cmd::SetQuant(next_note_length(clock.quant.get()as usize)as f64), - Focus::Sync => Cmd::SetSync(next_note_length(clock.sync.get()as usize)as f64+1.), - Focus::PlayPause => {todo!()}, - Focus::Clock => {todo!()} - }), - key!(KeyCode::Char(',')) => Self::Transport(match view.focus { - Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() - 1.0), - Focus::Quant => Cmd::SetQuant(prev_note_length(clock.quant.get()as usize)as f64), - Focus::Sync => Cmd::SetSync(prev_note_length(clock.sync.get()as usize)as f64+1.), - Focus::PlayPause => {todo!()}, - Focus::Clock => {todo!()} - }), - key!(KeyCode::Char('>')) => Self::Transport(match view.focus { - Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() + 0.001), - Focus::Quant => Cmd::SetQuant(next_note_length(clock.quant.get()as usize)as f64), - Focus::Sync => Cmd::SetSync(next_note_length(clock.sync.get()as usize)as f64+1.), - Focus::PlayPause => {todo!()}, - Focus::Clock => {todo!()} - }), - key!(KeyCode::Char('<')) => Self::Transport(match view.focus { - Focus::Bpm => Cmd::SetBpm(clock.timebase().bpm.get() - 0.001), - Focus::Quant => Cmd::SetQuant(prev_note_length(clock.quant.get()as usize)as f64), - Focus::Sync => Cmd::SetSync(prev_note_length(clock.sync.get()as usize)as f64+1.), - Focus::PlayPause => {todo!()}, - Focus::Clock => {todo!()} - }), - - _ => return None - }) - } -} - -impl Command> for TransportViewCommand { - fn execute (self, view: &mut TransportView) -> Perhaps { - let clock = view.model.clock(); - Ok(Some(match self { - Self::Focus(command) => Self::Focus({ - use FocusCommand::*; - match command { - Next => { todo!() }, - Prev => { todo!() }, - _ => { todo!() } - } - }), - Self::Transport(command) => Self::Transport({ - use TransportCommand::*; - match command { - SetBpm(bpm) => SetBpm(clock.timebase().bpm.set(bpm)), - SetQuant(quant) => SetQuant(clock.quant.set(quant)), - SetSync(sync) => SetSync(clock.sync.set(sync)), - _ => { - todo!() - } - } - }), - })) - } -} diff --git a/crates/tek_tui/src/tui_transport_foc.rs b/crates/tek_tui/src/tui_transport_foc.rs deleted file mode 100644 index fd5d6b04..00000000 --- a/crates/tek_tui/src/tui_transport_foc.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::*; - -impl TransportViewFocus { - pub fn next (&mut self) { - *self = match self { - Self::PlayPause => Self::Bpm, - Self::Bpm => Self::Quant, - Self::Quant => Self::Sync, - Self::Sync => Self::Clock, - Self::Clock => Self::PlayPause, - } - } - pub fn prev (&mut self) { - *self = match self { - Self::PlayPause => Self::Clock, - Self::Bpm => Self::PlayPause, - Self::Quant => Self::Bpm, - Self::Sync => Self::Quant, - Self::Clock => Self::Sync, - } - } - pub fn wrap <'a, W: Widget> ( - self, parent_focus: bool, focus: Self, widget: &'a W - ) -> impl Widget + 'a { - let focused = parent_focus && focus == self; - let corners = focused.then_some(CORNERS); - let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50))); - lay!(corners, highlight, *widget) - } -} - -/// Which item of the transport toolbar is focused -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum TransportViewFocus { - Bpm, - Sync, - PlayPause, - Clock, - Quant, -} - -impl FocusGrid for AppView, TransportViewCommand> { - type Item = AppViewFocus; - fn cursor (&self) -> (usize, usize) { - self.cursor - } - fn cursor_mut (&mut self) -> &mut (usize, usize) { - &mut self.cursor - } - fn focus_enter (&mut self) { - let focused = self.focused(); - if !self.entered { - self.entered = true; - // TODO - } - } - fn focus_exit (&mut self) { - if self.entered { - self.entered = false; - // TODO - } - } - fn entered (&self) -> Option { - if self.entered { - Some(self.focused()) - } else { - None - } - } - fn layout (&self) -> &[&[Self::Item]] { - use AppViewFocus::*; - use TransportViewFocus::*; - &[ - &[Menu], - &[ - Content(Bpm), - Content(Sync), - Content(PlayPause), - Content(Clock), - Content(Quant), - ], - ] - } - fn update_focus (&mut self) { - // TODO - } -}