diff --git a/crates/tek_api/src/api_jack.rs b/crates/tek_api/src/api_jack.rs index ce25e46a..9f5ff5ef 100644 --- a/crates/tek_api/src/api_jack.rs +++ b/crates/tek_api/src/api_jack.rs @@ -1,10 +1,5 @@ use crate::*; -pub trait HasJack { - fn jack (&self) -> &impl JackApi; -} - pub trait JackApi { fn jack (&self) -> &Arc>; - fn transport (&self) -> &RwLock>; } diff --git a/crates/tek_api/src/api_player.rs b/crates/tek_api/src/api_player.rs index 7d51defc..3d847705 100644 --- a/crates/tek_api/src/api_player.rs +++ b/crates/tek_api/src/api_player.rs @@ -1,6 +1,6 @@ use crate::*; -pub trait HasPlayer: HasJack { +pub trait HasPlayer: JackApi { fn player (&self) -> &impl PlayerApi; fn player_mut (&mut self) -> &mut impl PlayerApi; } diff --git a/crates/tek_tui/src/lib.rs b/crates/tek_tui/src/lib.rs index 6e4cd298..574f6d61 100644 --- a/crates/tek_tui/src/lib.rs +++ b/crates/tek_tui/src/lib.rs @@ -15,6 +15,7 @@ submod! { tui_arranger tui_arranger_cmd tui_arranger_focus + tui_arranger_jack tui_arranger_scene tui_arranger_select tui_arranger_status @@ -29,6 +30,7 @@ submod! { //tui_plugin_vst2 //tui_plugin_vst3 tui_pool + tui_pool_view //tui_sampler // TODO //tui_sampler_cmd tui_sequencer @@ -37,61 +39,61 @@ submod! { tui_theme tui_transport tui_transport_cmd + tui_transport_focus + tui_transport_jack + tui_transport_view } -pub struct AppView -where - E: Engine, - A: Widget + Audio, - C: Command, - S: StatusBar, -{ - pub app: A, - pub cursor: (usize, usize), - pub entered: bool, - pub menu_bar: Option>, - pub status_bar: Option, - pub history: Vec, - pub size: Measure, -} +//pub struct AppView +//where + //E: Engine, + //A: Widget + Audio, + //C: Command, + //S: StatusBar, +//{ + //pub app: A, + //pub cursor: (usize, usize), + //pub entered: bool, + //pub menu_bar: Option>, + //pub status_bar: Option, + //pub history: Vec, + //pub size: Measure, +//} -#[derive(Debug, Clone)] -pub enum AppViewCommand { - Focus(FocusCommand), - Undo, - Redo, - App(T) -} +//#[derive(Debug, Clone)] +//pub enum AppViewCommand { + //App(T) +//} -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum AppViewFocus { - Menu, - Content(F), -} +//#[derive(Debug, Copy, Clone, PartialEq)] +//pub enum AppViewFocus { + //Menu, + //Content(F), +//} -impl AppView -where - E: Engine, - A: Widget + Audio, - C: Command, - S: StatusBar -{ - pub fn new ( - app: A, - menu_bar: Option>, - status_bar: Option, - ) -> Self { - Self { - app, - cursor: (0, 0), - entered: false, - history: vec![], - size: Measure::new(), - menu_bar, - status_bar, - } - } -} +//impl AppView +//where + //E: Engine, + //A: Widget + Audio, + //C: Command, + //S: StatusBar +//{ + //pub fn new ( + //app: A, + //menu_bar: Option>, + //status_bar: Option, + //) -> Self { + //Self { + //app, + //cursor: (0, 0), + //entered: false, + //history: vec![], + //size: Measure::new(), + //menu_bar, + //status_bar, + //} + //} +//} impl Content for AppView where diff --git a/crates/tek_tui/src/tui_arranger.rs b/crates/tek_tui/src/tui_arranger.rs index 07e2a9c6..1d33b221 100644 --- a/crates/tek_tui/src/tui_arranger.rs +++ b/crates/tek_tui/src/tui_arranger.rs @@ -1,111 +1,70 @@ use crate::*; -impl TryFrom<&Arc>> for ArrangerApp { +/// Root view for standalone `tek_arranger` +pub struct ArrangerTui { + pub jack: Arc>, + pub transport: jack::Transport, + pub playing: RwLock>, + pub started: RwLock>, + pub current: Instant, + pub quant: Quantize, + pub sync: LaunchSync, + pub metronome: bool, + pub phrases: Vec>>, + pub phrase: usize, + pub tracks: Vec, + pub scenes: Vec, + pub name: Arc>, + pub splits: [u16;2], + pub selected: ArrangerSelection, + pub mode: ArrangerMode, + pub color: ItemColor, + pub entered: bool, + pub size: Measure, + pub note_buf: Vec, + pub midi_buf: Vec>>, + pub cursor: (usize, usize), + pub menu_bar: Option>, + pub status_bar: Option, + pub history: Vec, +} + +impl TryFrom<&Arc>> for ArrangerTui { type Error = Box; fn try_from (jack: &Arc>) -> Usually { - Ok(Self::new(ArrangerView { - name: Arc::new(RwLock::new(String::new())), - phrases: vec![], - phrase: 0, - scenes: vec![], - tracks: vec![], - metronome: false, - playing: None.into(), - started: None.into(), - transport: jack.read().unwrap().transport(), - current: Instant::default(), - jack: jack.clone(), - selected: ArrangerSelection::Clip(0, 0), - mode: ArrangerMode::Vertical(2), - color: Color::Rgb(28, 35, 25).into(), - size: Measure::new(), - entered: false, - quant: Default::default(), - sync: Default::default(), - splits: [20, 20], - note_buf: vec![], - midi_buf: vec![], - }.into(), None, None)) + Ok(Self { + name: Arc::new(RwLock::new(String::new())), + phrases: vec![], + phrase: 0, + scenes: vec![], + tracks: vec![], + metronome: false, + playing: None.into(), + started: None.into(), + transport: jack.read().unwrap().transport(), + current: Instant::default(), + jack: jack.clone(), + selected: ArrangerSelection::Clip(0, 0), + mode: ArrangerMode::Vertical(2), + color: Color::Rgb(28, 35, 25).into(), + size: Measure::new(), + entered: false, + quant: Default::default(), + sync: Default::default(), + splits: [20, 20], + note_buf: vec![], + midi_buf: vec![], + cursor: (0, 0), + entered: false, + history: vec![], + size: Measure::new(), + menu_bar: None, + status_bar: None, + }) } } -pub type ArrangerApp = AppView< - E, - ArrangerView, - ArrangerAppCommand, - ArrangerStatusBar ->; - -/// Root view for standalone `tek_arranger` -pub struct ArrangerView { - pub(crate) jack: Arc>, - pub(crate) playing: RwLock>, - pub(crate) started: RwLock>, - pub(crate) current: Instant, - pub(crate) quant: Quantize, - pub(crate) sync: LaunchSync, - pub(crate) transport: jack::Transport, - pub(crate) metronome: bool, - pub(crate) phrases: Vec>>, - pub(crate) phrase: usize, - pub(crate) tracks: Vec, - pub(crate) scenes: Vec, - pub(crate) name: Arc>, - pub(crate) splits: [u16;2], - pub(crate) selected: ArrangerSelection, - pub(crate) mode: ArrangerMode, - pub(crate) color: ItemColor, - pub(crate) entered: bool, - pub(crate) size: Measure, - pub(crate) note_buf: Vec, - pub(crate) midi_buf: Vec>>, -} - -impl HasJack for ArrangerView { - fn jack (&self) -> &Arc> { - &self.transport.jack() - } -} - -impl Audio for ArrangerApp { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - TracksAudio( - &mut self.app.tracks, - &mut self.app.note_buf, - &mut self.app.midi_buf, - Default::default(), - ).process(client, scope) - } -} - -impl ClockApi for ArrangerView { - fn timebase (&self) -> &Arc { - &self.current.timebase - } - fn quant (&self) -> &Quantize { - &self.quant - } - fn sync (&self) -> &LaunchSync { - &self.sync - } -} - -impl PlayheadApi for ArrangerView { - fn current (&self) -> &Instant { - &self.current - } - fn transport (&self) -> &jack::Transport { - &self.transport - } - fn playing (&self) -> &RwLock> { - &self.playing - } - fn started (&self) -> &RwLock> { - &self.started - } -} - -impl HasPhrases for ArrangerView { +impl HasPhrases for ArrangerTui { fn phrases (&self) -> &Vec>> { &self.phrases } @@ -115,7 +74,7 @@ impl HasPhrases for ArrangerView { } /// General methods for arranger -impl ArrangerView { +impl ArrangerTui { pub fn selected_scene (&self) -> Option<&ArrangerScene> { self.selected.scene().map(|s|self.scenes().get(s)).flatten() } @@ -196,33 +155,14 @@ impl ArrangerView { } } -impl Audio for ArrangerView { - #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - if self.process(client, scope) == Control::Quit { - return Control::Quit - } - // FIXME: one of these per playing track - if let ArrangerSelection::Clip(t, s) = self.selected { - let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t)); - if let Some(Some(Some(phrase))) = phrase { - if let Some(track) = self.tracks().get(t) { - if let Some((ref started_at, Some(ref playing))) = track.player.phrase { - let phrase = phrase.read().unwrap(); - if *playing.read().unwrap() == *phrase { - let pulse = self.current().pulse.get(); - let start = started_at.pulse.get(); - let now = (pulse - start) % phrase.length as f64; - self.editor.now.set(now); - return Control::Continue - } - } - } - } - } - self.editor.now.set(0.); - return Control::Continue - } -} + + + + + + + + //pub fn track_next (&mut self, last_track: usize) { //use ArrangerSelection::*; //*self = match self { diff --git a/crates/tek_tui/src/tui_arranger_cmd.rs b/crates/tek_tui/src/tui_arranger_cmd.rs index b2f18835..510fd5f9 100644 --- a/crates/tek_tui/src/tui_arranger_cmd.rs +++ b/crates/tek_tui/src/tui_arranger_cmd.rs @@ -1,34 +1,34 @@ use crate::*; /// Handle top-level events in standalone arranger. -impl Handle for ArrangerApp { +impl Handle for ArrangerTui { fn handle (&mut self, i: &TuiInput) -> Perhaps { - ArrangerAppCommand::execute_with_state(self, i) + ArrangerCommand::execute_with_state(self, i) } } -pub type ArrangerAppCommand = AppViewCommand; - #[derive(Clone, Debug)] -pub enum ArrangerViewCommand { +pub enum ArrangerCommand { + Focus(FocusCommand), + Undo, + Redo, Clear, + Clock(ClockCommand), + Playhead(PlayheadCommand), Scene(ArrangerSceneCommand), Track(ArrangerTrackCommand), Clip(ArrangerClipCommand), Select(ArrangerSelection), Zoom(usize), - Clock(ClockCommand), - Playhead(PlayheadCommand), Phrases(PhrasePoolViewCommand), Editor(PhraseEditorCommand), EditPhrase(Option>>), } -impl InputToCommand> for ArrangerAppCommand { - fn input_to_command (view: &ArrangerApp, input: &TuiInput) -> Option { - use AppViewFocus::*; +impl InputToCommand for ArrangerCommand { + fn input_to_command (view: &ArrangerTui, input: &TuiInput) -> Option { use FocusCommand::*; - use ArrangerViewCommand::*; + use ArrangerCommand::*; Some(match input.event() { key!(KeyCode::Tab) => Self::Focus(Next), key!(Shift-KeyCode::Tab) => Self::Focus(Prev), @@ -56,11 +56,11 @@ impl InputToCommand> for ArrangerAppCommand { } }, Content(ArrangerFocus::PhraseEditor) => Editor( - PhraseEditorCommand::input_to_command(&view.app.editor, input)? + PhraseEditorCommand::input_to_command(&view.editor, input)? ), Content(ArrangerFocus::PhrasePool) => match input.event() { key!(KeyCode::Char('e')) => EditPhrase( - Some(view.app.phrase().clone()) + Some(view.phrase().clone()) ), _ => Phrases( PhrasePoolViewCommand::input_to_command(view, input)? @@ -76,28 +76,28 @@ impl InputToCommand> for ArrangerAppCommand { _ => match input.event() { // FIXME: boundary conditions - key!(KeyCode::Up) => match view.app.selected { + key!(KeyCode::Up) => match view.selected { Select::Mix => return None, Select::Track(t) => return None, Select::Scene(s) => Select(Select::Scene(s - 1)), Select::Clip(t, s) => Select(Select::Clip(t, s - 1)), }, - key!(KeyCode::Down) => match view.app.selected { + key!(KeyCode::Down) => match view.selected { Select::Mix => Select(Select::Scene(0)), Select::Track(t) => Select(Select::Clip(t, 0)), Select::Scene(s) => Select(Select::Scene(s + 1)), Select::Clip(t, s) => Select(Select::Clip(t, s + 1)), }, - key!(KeyCode::Left) => match view.app.selected { + key!(KeyCode::Left) => match view.selected { Select::Mix => return None, Select::Track(t) => Select(Select::Track(t - 1)), Select::Scene(s) => return None, Select::Clip(t, s) => Select(Select::Clip(t - 1, s)), }, - key!(KeyCode::Right) => match view.app.selected { + key!(KeyCode::Right) => match view.selected { Select::Mix => return None, Select::Track(t) => Select(Select::Track(t + 1)), Select::Scene(s) => Select(Select::Clip(0, s)), @@ -114,42 +114,42 @@ impl InputToCommand> for ArrangerAppCommand { key!(KeyCode::Char('`')) => { todo!("toggle view mode") }, - key!(KeyCode::Char(',')) => match view.app.selected { + key!(KeyCode::Char(',')) => match view.selected { Select::Mix => Zoom(0), Select::Track(t) => Track(Track::Swap(t, t - 1)), Select::Scene(s) => Scene(Scene::Swap(s, s - 1)), Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), }, - key!(KeyCode::Char('.')) => match view.app.selected { + key!(KeyCode::Char('.')) => match view.selected { Select::Mix => Zoom(0), Select::Track(t) => Track(Track::Swap(t, t + 1)), Select::Scene(s) => Scene(Scene::Swap(s, s + 1)), Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), }, - key!(KeyCode::Char('<')) => match view.app.selected { + key!(KeyCode::Char('<')) => match view.selected { Select::Mix => Zoom(0), Select::Track(t) => Track(Track::Swap(t, t - 1)), Select::Scene(s) => Scene(Scene::Swap(s, s - 1)), Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), }, - key!(KeyCode::Char('>')) => match view.app.selected { + key!(KeyCode::Char('>')) => match view.selected { Select::Mix => Zoom(0), Select::Track(t) => Track(Track::Swap(t, t + 1)), Select::Scene(s) => Scene(Scene::Swap(s, s + 1)), Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), }, - key!(KeyCode::Enter) => match view.app.selected { + key!(KeyCode::Enter) => match view.selected { Select::Mix => return None, Select::Track(t) => return None, Select::Scene(s) => Scene(Scene::Play(s)), Select::Clip(t, s) => return None, }, - key!(KeyCode::Delete) => match view.app.selected { + key!(KeyCode::Delete) => match view.selected { Select::Mix => Clear, Select::Track(t) => Track(Track::Delete(t)), Select::Scene(s) => Scene(Scene::Delete(s)), @@ -158,12 +158,12 @@ impl InputToCommand> for ArrangerAppCommand { key!(KeyCode::Char('c')) => Clip(Clip::RandomColor), - key!(KeyCode::Char('s')) => match view.app.selected { + key!(KeyCode::Char('s')) => match view.selected { Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), _ => return None, }, - key!(KeyCode::Char('g')) => match view.app.selected { + key!(KeyCode::Char('g')) => match view.selected { Select::Clip(t, s) => Clip(Clip::Get(t, s)), _ => return None, }, @@ -183,9 +183,8 @@ impl InputToCommand> for ArrangerAppCommand { } } -impl Command> for ArrangerAppCommand { - fn execute (self, state: &mut ArrangerApp) -> Perhaps { - use AppViewCommand::*; +impl Command for ArrangerCommand { + fn execute (self, state: &mut ArrangerTui) -> Perhaps { let undo = match self { Focus(cmd) => { delegate(cmd, Focus, state) }, App(cmd) => { delegate(cmd, App, state) } @@ -197,17 +196,17 @@ impl Command> for ArrangerAppCommand { } } -impl Command> for ArrangerViewCommand { - fn execute (self, state: &mut ArrangerApp) -> Perhaps { - use ArrangerViewCommand::*; +impl Command for ArrangerCommand { + fn execute (self, state: &mut ArrangerTui) -> Perhaps { + use ArrangerCommand::*; match self { - Scene(cmd) => { delegate(cmd, Scene, &mut state.app) }, - Track(cmd) => { delegate(cmd, Track, &mut state.app) }, - Clip(cmd) => { delegate(cmd, Clip, &mut state.app) }, - Phrases(cmd) => { delegate(cmd, Phrases, &mut state.app) }, - Editor(cmd) => { delegate(cmd, Editor, &mut state.app) }, - Clock(cmd) => { delegate(cmd, Clock, &mut state.app) }, - Playhead(cmd) => { delegate(cmd, Playhead, &mut state.app) }, + Scene(cmd) => { delegate(cmd, Scene, &mut state) }, + Track(cmd) => { delegate(cmd, Track, &mut state) }, + Clip(cmd) => { delegate(cmd, Clip, &mut state) }, + Phrases(cmd) => { delegate(cmd, Phrases, &mut state) }, + Editor(cmd) => { delegate(cmd, Editor, &mut state) }, + Clock(cmd) => { delegate(cmd, Clock, &mut state) }, + Playhead(cmd) => { delegate(cmd, Playhead, &mut state) }, Zoom(zoom) => { todo!(); }, Select(selected) => { state.selected = selected; Ok(None) }, EditPhrase(phrase) => { diff --git a/crates/tek_tui/src/tui_arranger_focus.rs b/crates/tek_tui/src/tui_arranger_focus.rs index c17ed942..df9e75ea 100644 --- a/crates/tek_tui/src/tui_arranger_focus.rs +++ b/crates/tek_tui/src/tui_arranger_focus.rs @@ -14,6 +14,7 @@ pub enum ArrangerFocus { } impl FocusEnter for ArrangerApp { + type Item = AppViewFocus; fn focus_enter (&mut self) { use AppViewFocus::*; use ArrangerFocus::*; diff --git a/crates/tek_tui/src/tui_arranger_jack.rs b/crates/tek_tui/src/tui_arranger_jack.rs new file mode 100644 index 00000000..1e2eeb77 --- /dev/null +++ b/crates/tek_tui/src/tui_arranger_jack.rs @@ -0,0 +1,73 @@ +use crate::*; + +impl JackApi for ArrangerTui { + fn jack (&self) -> &Arc> { + &self.jack + } +} + +impl Audio for ArrangerTui { + fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + TracksAudio( + &mut self.app.tracks, + &mut self.app.note_buf, + &mut self.app.midi_buf, + Default::default(), + ).process(client, scope) + } +} + +impl Audio for ArrangerTui { + #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + if self.process(client, scope) == Control::Quit { + return Control::Quit + } + // FIXME: one of these per playing track + if let ArrangerSelection::Clip(t, s) = self.selected { + let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t)); + if let Some(Some(Some(phrase))) = phrase { + if let Some(track) = self.tracks().get(t) { + if let Some((ref started_at, Some(ref playing))) = track.player.phrase { + let phrase = phrase.read().unwrap(); + if *playing.read().unwrap() == *phrase { + let pulse = self.current().pulse.get(); + let start = started_at.pulse.get(); + let now = (pulse - start) % phrase.length as f64; + self.editor.now.set(now); + return Control::Continue + } + } + } + } + } + self.editor.now.set(0.); + return Control::Continue + } +} + +impl ClockApi for ArrangerTui { + fn timebase (&self) -> &Arc { + &self.current.timebase + } + fn quant (&self) -> &Quantize { + &self.quant + } + fn sync (&self) -> &LaunchSync { + &self.sync + } +} + +impl PlayheadApi for ArrangerTui { + fn current (&self) -> &Instant { + &self.current + } + fn transport (&self) -> &jack::Transport { + &self.transport + } + fn playing (&self) -> &RwLock> { + &self.playing + } + fn started (&self) -> &RwLock> { + &self.started + } +} diff --git a/crates/tek_tui/src/tui_arranger_view.rs b/crates/tek_tui/src/tui_arranger_view.rs index 4406ea0d..82ba5c38 100644 --- a/crates/tek_tui/src/tui_arranger_view.rs +++ b/crates/tek_tui/src/tui_arranger_view.rs @@ -29,7 +29,7 @@ impl Content for ArrangerView { fn content (&self) -> impl Widget { Split::up( 1, - widget(&TransportRef(self)), + widget(&TransportView(self)), Split::down( self.splits[0], lay!( @@ -45,20 +45,20 @@ impl Content for ArrangerView { .grow_y(1) .border(Lozenge(Style::default() .bg(TuiTheme::border_bg()) - .fg(TuiTheme::border_fg(self.focused)))), + .fg(TuiTheme::border_fg(self.focused() == ArrangerFocus::Arranger)))), widget(&self.size), widget(&format!("[{}] Arranger", if self.entered { "■" } else { " " })) - .fg(TuiTheme::title_fg(self.focused)) + .fg(TuiTheme::title_fg(self.focused() == ArrangerFocus::Arranger)) .push_x(1), ), Split::right( self.splits[1], widget(&self.phrases), - widget(&PhraseEditorRef(self)), + widget(&PhraseView(self)), ) ) ) diff --git a/crates/tek_tui/src/tui_phrase.rs b/crates/tek_tui/src/tui_phrase.rs index 30b3a029..82152201 100644 --- a/crates/tek_tui/src/tui_phrase.rs +++ b/crates/tek_tui/src/tui_phrase.rs @@ -34,14 +34,14 @@ pub struct PhraseEditor { impl Widget for PhraseEditor { type Engine = Tui; fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - PhraseEditorRef(&self, Default::default()).layout(to) + PhraseView(&self, Default::default()).layout(to) } fn render (&self, to: &mut TuiOutput) -> Usually<()> { - PhraseEditorRef(&self, Default::default()).render(to) + PhraseView(&self, Default::default()).render(to) } } -pub struct PhraseEditorRef<'a, T: PhraseEditorViewState>(pub &'a T); +pub struct PhraseView<'a, T: PhraseEditorViewState>(pub &'a T); pub trait PhraseEditorViewState: Send + Sync { fn focused (&self) -> bool; @@ -89,7 +89,7 @@ impl PhraseEditorViewState for PhraseEditor { } } -impl<'a, T: PhraseEditorViewState> Content for PhraseEditorRef<'a, T> { +impl<'a, T: PhraseEditorViewState> Content for PhraseView<'a, T> { type Engine = Tui; fn content (&self) -> impl Widget { let phrase = self.0.phrase(); diff --git a/crates/tek_tui/src/tui_pool.rs b/crates/tek_tui/src/tui_pool.rs index 3e6120a0..249546b2 100644 --- a/crates/tek_tui/src/tui_pool.rs +++ b/crates/tek_tui/src/tui_pool.rs @@ -1,7 +1,6 @@ use crate::*; -pub struct PhrasePoolView { - _engine: PhantomData, +pub struct PhrasesTui { /// Collection of phrases pub phrases: Vec>>, /// Selected phrase @@ -24,10 +23,9 @@ pub enum PhrasePoolMode { Length(usize, usize, PhraseLengthFocus), } -impl PhrasePoolView { +impl PhrasesTui { pub fn new (phrases: Vec>>) -> Self { Self { - _engine: Default::default(), scroll: 0, phrase: 0, mode: None, @@ -93,210 +91,8 @@ impl PhrasePoolView { } } -// TODO: Display phrases always in order of appearance -impl Content for PhrasePoolView { - type Engine = Tui; - fn content (&self) -> impl Widget { - let Self { focused, phrases, mode, .. } = self; - let content = col!( - (i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{ - let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); - let mut length = PhraseLength::new(length, None); - if let Some(PhrasePoolMode::Length(phrase, new_length, focus)) = mode { - if *focused && i == *phrase { - length.pulses = *new_length; - length.focus = Some(*focus); - } - } - let length = length.align_e().fill_x(); - let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x(); - let mut row2 = format!(" {name}"); - if let Some(PhrasePoolMode::Rename(phrase, _)) = mode { - if *focused && i == *phrase { row2 = format!("{row2}▄"); } - }; - let row2 = TuiStyle::bold(row2, true); - add(&col!(row1, row2).fill_x().bg(color.base.rgb))?; - Ok(if *focused && i == self.phrase { add(&CORNERS)?; }) - }) - ); - let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)}; - let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); - let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border); - let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)}; - let upper_left = format!("[{}] Phrases", if self.entered {"■"} else {" "}); - let upper_right = format!("({})", phrases.len()); - lay!( - content, - TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), - TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), - ) - } -} - -#[derive(Clone, PartialEq, Debug)] -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)?.map(Self::Edit)) - } - Self::Rename(command) => match command { - Rename::Begin => { - view.mode = Some(PhrasePoolMode::Rename( - view.phrase, - view.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.phrases[view.phrase].read().unwrap().length, - PhraseLengthFocus::Bar - )) - }, - _ => { - return Ok(command.execute(view)?.map(Self::Length)) - } - }, - } - Ok(None) - } -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum PhraseLengthCommand { - Begin, - Next, - Prev, - Inc, - Dec, - Set(usize), - Cancel, -} - -impl InputToCommand> for PhraseLengthCommand { - fn input_to_command (view: &PhrasePoolView, from: &TuiInput) -> Option { - if let Some(PhrasePoolMode::Length(_, length, _)) = view.mode { - Some(match from.event() { - key!(KeyCode::Up) => Self::Inc, - key!(KeyCode::Down) => Self::Dec, - key!(KeyCode::Right) => Self::Next, - key!(KeyCode::Left) => Self::Prev, - key!(KeyCode::Enter) => Self::Set(length), - key!(KeyCode::Esc) => Self::Cancel, - _ => return None - }) - } else { - unreachable!() - } - } -} - -impl Command> for PhraseLengthCommand { - fn execute (self, view: &mut PhrasePoolView) -> Perhaps { - use PhraseLengthFocus::*; - use PhraseLengthCommand::*; - if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = view.mode { - match self { - Self::Cancel => { - view.mode = None; - }, - Self::Prev => { - focus.prev() - }, - Self::Next => { - focus.next() - }, - Self::Inc => match focus { - Bar => { *length += 4 * PPQ }, - Beat => { *length += PPQ }, - Tick => { *length += 1 }, - }, - Self::Dec => match focus { - Bar => { *length = length.saturating_sub(4 * PPQ) }, - Beat => { *length = length.saturating_sub(PPQ) }, - Tick => { *length = length.saturating_sub(1) }, - }, - Self::Set(length) => { - let mut phrase = view.phrases[phrase].write().unwrap(); - let old_length = phrase.length; - phrase.length = length; - view.mode = None; - return Ok(Some(Self::Set(old_length))) - }, - _ => unreachable!() - } - Ok(None) - } else if self == Begin { - view.mode = Some(PhrasePoolMode::Length( - view.phrase, - view.phrases[view.phrase].read().unwrap().length, - PhraseLengthFocus::Bar - )); - Ok(None) - } else { - unreachable!() - } - } -} - /// Displays and edits phrase length. -pub struct PhraseLength { - _engine: PhantomData, +pub struct PhraseLength { /// Pulses per beat (quaver) pub ppq: usize, /// Beats per bar @@ -331,7 +127,7 @@ impl PhraseLength { } } -impl Content for PhraseLength { +impl Content for PhraseLength { type Engine = Tui; fn content (&self) -> impl Widget { Layers::new(move|add|{ @@ -392,68 +188,3 @@ impl PhraseLengthFocus { } } } - -#[derive(Clone, Debug, PartialEq)] -pub enum PhraseRenameCommand { - Begin, - Set(String), - Confirm, - Cancel, -} - -impl InputToCommand> for PhraseRenameCommand { - fn input_to_command (view: &PhrasePoolView, from: &TuiInput) -> Option { - if let Some(PhrasePoolMode::Rename(_, ref old_name)) = view.mode { - Some(match from.event() { - key!(KeyCode::Char(c)) => { - let mut new_name = old_name.clone(); - new_name.push(*c); - Self::Set(new_name) - }, - key!(KeyCode::Backspace) => { - let mut new_name = old_name.clone(); - new_name.pop(); - Self::Set(new_name) - }, - key!(KeyCode::Enter) => Self::Confirm, - key!(KeyCode::Esc) => Self::Cancel, - _ => return None - }) - } else { - unreachable!() - } - } -} - -impl Command> for PhraseRenameCommand { - fn execute (self, view: &mut PhrasePoolView) -> Perhaps { - use PhraseRenameCommand::*; - if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = view.mode { - match self { - Set(s) => { - view.phrases[phrase].write().unwrap().name = s.into(); - return Ok(Some(Self::Set(old_name.clone()))) - }, - Confirm => { - let old_name = old_name.clone(); - view.mode = None; - return Ok(Some(Self::Set(old_name))) - }, - Cancel => { - let mut phrase = view.phrases[phrase].write().unwrap(); - phrase.name = old_name.clone(); - }, - _ => unreachable!() - }; - Ok(None) - } else if self == Begin { - view.mode = Some(PhrasePoolMode::Rename( - view.phrase, - view.phrases[view.phrase].read().unwrap().name.clone() - )); - Ok(None) - } else { - unreachable!() - } - } -} diff --git a/crates/tek_tui/src/tui_pool_cmd.rs b/crates/tek_tui/src/tui_pool_cmd.rs new file mode 100644 index 00000000..8f3fc6a1 --- /dev/null +++ b/crates/tek_tui/src/tui_pool_cmd.rs @@ -0,0 +1,227 @@ +use crate::*; + +impl Handle for PhrasePoolView { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + PhrasePoolViewCommand::execute_with_state(self, from) + } +} + +#[derive(Clone, PartialEq, Debug)] +pub enum PhrasePoolViewCommand { + Select(usize), + Edit(PhrasePoolCommand), + Rename(PhraseRenameCommand), + Length(PhraseLengthCommand), +} + +#[derive(Clone, Debug, PartialEq)] +pub enum PhraseRenameCommand { + Begin, + Set(String), + Confirm, + Cancel, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum PhraseLengthCommand { + Begin, + Next, + Prev, + Inc, + Dec, + Set(usize), + Cancel, +} + +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)?.map(Self::Edit)) + } + Self::Rename(command) => match command { + Rename::Begin => { + view.mode = Some(PhrasePoolMode::Rename( + view.phrase, + view.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.phrases[view.phrase].read().unwrap().length, + PhraseLengthFocus::Bar + )) + }, + _ => { + return Ok(command.execute(view)?.map(Self::Length)) + } + }, + } + Ok(None) + } +} + +impl InputToCommand> for PhraseLengthCommand { + fn input_to_command (view: &PhrasePoolView, from: &TuiInput) -> Option { + if let Some(PhrasePoolMode::Length(_, length, _)) = view.mode { + Some(match from.event() { + key!(KeyCode::Up) => Self::Inc, + key!(KeyCode::Down) => Self::Dec, + key!(KeyCode::Right) => Self::Next, + key!(KeyCode::Left) => Self::Prev, + key!(KeyCode::Enter) => Self::Set(length), + key!(KeyCode::Esc) => Self::Cancel, + _ => return None + }) + } else { + unreachable!() + } + } +} + +impl Command> for PhraseLengthCommand { + fn execute (self, view: &mut PhrasePoolView) -> Perhaps { + use PhraseLengthFocus::*; + use PhraseLengthCommand::*; + if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = view.mode { + match self { + Self::Cancel => { + view.mode = None; + }, + Self::Prev => { + focus.prev() + }, + Self::Next => { + focus.next() + }, + Self::Inc => match focus { + Bar => { *length += 4 * PPQ }, + Beat => { *length += PPQ }, + Tick => { *length += 1 }, + }, + Self::Dec => match focus { + Bar => { *length = length.saturating_sub(4 * PPQ) }, + Beat => { *length = length.saturating_sub(PPQ) }, + Tick => { *length = length.saturating_sub(1) }, + }, + Self::Set(length) => { + let mut phrase = view.phrases[phrase].write().unwrap(); + let old_length = phrase.length; + phrase.length = length; + view.mode = None; + return Ok(Some(Self::Set(old_length))) + }, + _ => unreachable!() + } + Ok(None) + } else if self == Begin { + view.mode = Some(PhrasePoolMode::Length( + view.phrase, + view.phrases[view.phrase].read().unwrap().length, + PhraseLengthFocus::Bar + )); + Ok(None) + } else { + unreachable!() + } + } +} + +impl InputToCommand> for PhraseRenameCommand { + fn input_to_command (view: &PhrasePoolView, from: &TuiInput) -> Option { + if let Some(PhrasePoolMode::Rename(_, ref old_name)) = view.mode { + Some(match from.event() { + key!(KeyCode::Char(c)) => { + let mut new_name = old_name.clone(); + new_name.push(*c); + Self::Set(new_name) + }, + key!(KeyCode::Backspace) => { + let mut new_name = old_name.clone(); + new_name.pop(); + Self::Set(new_name) + }, + key!(KeyCode::Enter) => Self::Confirm, + key!(KeyCode::Esc) => Self::Cancel, + _ => return None + }) + } else { + unreachable!() + } + } +} + +impl Command> for PhraseRenameCommand { + fn execute (self, view: &mut PhrasePoolView) -> Perhaps { + use PhraseRenameCommand::*; + if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = view.mode { + match self { + Set(s) => { + view.phrases[phrase].write().unwrap().name = s.into(); + return Ok(Some(Self::Set(old_name.clone()))) + }, + Confirm => { + let old_name = old_name.clone(); + view.mode = None; + return Ok(Some(Self::Set(old_name))) + }, + Cancel => { + let mut phrase = view.phrases[phrase].write().unwrap(); + phrase.name = old_name.clone(); + }, + _ => unreachable!() + }; + Ok(None) + } else if self == Begin { + view.mode = Some(PhrasePoolMode::Rename( + view.phrase, + view.phrases[view.phrase].read().unwrap().name.clone() + )); + Ok(None) + } else { + unreachable!() + } + } +} diff --git a/crates/tek_tui/src/tui_pool_view.rs b/crates/tek_tui/src/tui_pool_view.rs new file mode 100644 index 00000000..5288f12c --- /dev/null +++ b/crates/tek_tui/src/tui_pool_view.rs @@ -0,0 +1,51 @@ +use crate::*; + +impl Widget for TransportTui { + type Engine = Tui; + fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + TransportView(&self, Default::default()).layout(to) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + TransportView(&self, Default::default()).render(to) + } +} + +// TODO: Display phrases always in order of appearance +impl Content for PhrasePoolView { + type Engine = Tui; + fn content (&self) -> impl Widget { + let Self { focused, phrases, mode, .. } = self; + let content = col!( + (i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{ + let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); + let mut length = PhraseLength::new(length, None); + if let Some(PhrasePoolMode::Length(phrase, new_length, focus)) = mode { + if *focused && i == *phrase { + length.pulses = *new_length; + length.focus = Some(*focus); + } + } + let length = length.align_e().fill_x(); + let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x(); + let mut row2 = format!(" {name}"); + if let Some(PhrasePoolMode::Rename(phrase, _)) = mode { + if *focused && i == *phrase { row2 = format!("{row2}▄"); } + }; + let row2 = TuiStyle::bold(row2, true); + add(&col!(row1, row2).fill_x().bg(color.base.rgb))?; + Ok(if *focused && i == self.phrase { add(&CORNERS)?; }) + }) + ); + let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)}; + let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); + let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border); + let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)}; + let upper_left = format!("[{}] Phrases", if self.entered {"■"} else {" "}); + let upper_right = format!("({})", phrases.len()); + lay!( + content, + TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), + TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), + ) + } +} diff --git a/crates/tek_tui/src/tui_sequencer.rs b/crates/tek_tui/src/tui_sequencer.rs index 167acc77..727f09b7 100644 --- a/crates/tek_tui/src/tui_sequencer.rs +++ b/crates/tek_tui/src/tui_sequencer.rs @@ -1,13 +1,6 @@ use crate::*; -pub type SequencerApp = AppView< - E, - SequencerView, - SequencerViewCommand, - SequencerStatusBar, ->; - -impl TryFrom<&Arc>> for SequencerApp { +impl TryFrom<&Arc>> for SequencerTui { type Error = Box; fn try_from (jack: &Arc>) -> Usually { let clock = Arc::new(Clock::from(Instant::default())); @@ -25,14 +18,8 @@ impl TryFrom<&Arc>> for SequencerApp { } } -impl Handle for SequencerApp { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - SequencerCommand::execute_with_state(self, i) - } -} - /// Root view for standalone `tek_sequencer`. -pub struct SequencerView { +pub struct SequencerTui { jack: Arc>, playing: RwLock>, started: RwLock>, @@ -43,7 +30,6 @@ pub struct SequencerView { metronome: bool, phrases: Vec>>, view_phrase: usize, - editor: PhraseEditor, split: u16, /// Start time and phrase being played play_phrase: Option<(Instant, Option>>)>, @@ -90,7 +76,7 @@ pub enum SequencerStatusBar { PhraseEditor, } -impl Content for SequencerView { +impl Content for SequencerTui { type Engine = Tui; fn content (&self) -> impl Widget { col!( @@ -103,7 +89,7 @@ impl Content for SequencerView { } } -impl Audio for SequencerView { +impl Audio for SequencerTui { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { self.model.process(client, scope) } @@ -127,11 +113,11 @@ impl Content for SequencerStatusBar { } } -impl HasFocus for SequencerApp { +impl HasFocus for SequencerTui { type Item = AppViewFocus; } -impl FocusEnter for SequencerApp { +impl FocusEnter for SequencerTui { fn focus_enter (&mut self) { let focused = self.focused(); if !self.entered { @@ -154,7 +140,7 @@ impl FocusEnter for SequencerApp { } } -impl FocusGrid for SequencerApp { +impl FocusGrid for SequencerTui { type Item = AppViewFocus; fn focus_cursor (&self) -> (usize, usize) { self.cursor @@ -176,13 +162,13 @@ impl FocusGrid for SequencerApp { } } -impl HasJack for SequencerView { +impl JackApi for SequencerTui { fn jack (&self) -> &Arc> { &self.jack } } -impl HasPhrases for SequencerView { +impl HasPhrases for SequencerTui { fn phrases (&self) -> &Vec>> { &self.phrases } @@ -191,7 +177,7 @@ impl HasPhrases for SequencerView { } } -impl HasPhrase for SequencerView { +impl HasPhrase for SequencerTui { fn reset (&self) -> bool { self.reset } @@ -212,7 +198,7 @@ impl HasPhrase for SequencerView { } } -impl MidiInputApi for SequencerView { +impl MidiInputApi for SequencerTui { fn midi_ins(&self) -> &Vec> { todo!() } @@ -242,7 +228,7 @@ impl MidiInputApi for SequencerView { } } -impl MidiOutputApi for SequencerView { +impl MidiOutputApi for SequencerTui { fn midi_outs (&self) -> &Vec> { todo!() } @@ -257,7 +243,7 @@ impl MidiOutputApi for SequencerView { } } -impl ClockApi for SequencerView { +impl ClockApi for SequencerTui { fn timebase (&self) -> &Arc { todo!() } @@ -269,7 +255,7 @@ impl ClockApi for SequencerView { } } -impl PlayheadApi for SequencerView { +impl PlayheadApi for SequencerTui { fn current(&self) -> &Instant { todo!() } @@ -284,9 +270,9 @@ impl PlayheadApi for SequencerView { } } -impl PlayerApi for SequencerView {} +impl PlayerApi for SequencerTui {} -impl TransportViewState for SequencerView { +impl TransportViewState for SequencerTui { fn focus (&self) -> TransportViewFocus { self.focus } diff --git a/crates/tek_tui/src/tui_sequencer_cmd.rs b/crates/tek_tui/src/tui_sequencer_cmd.rs index e8d80e22..72e27783 100644 --- a/crates/tek_tui/src/tui_sequencer_cmd.rs +++ b/crates/tek_tui/src/tui_sequencer_cmd.rs @@ -1,19 +1,28 @@ use crate::*; -pub type SequencerCommand = AppViewCommand; +impl Handle for SequencerTui { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + SequencerCommand::execute_with_state(self, i) + } +} #[derive(Clone, Debug, PartialEq)] -pub enum SequencerViewCommand { - Transport(TransportCommand), +pub enum SequencerCommand { + Focus(FocusCommand), + Undo, + Redo, + Clear, + Clock(ClockCommand), + Playhead(PlayheadCommand), Phrases(PhrasePoolViewCommand), Editor(PhraseEditorCommand), } -impl InputToCommand> for SequencerCommand { - fn input_to_command (state: &SequencerApp, input: &TuiInput) -> Option { +impl InputToCommand for SequencerCommand { + fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { use AppViewFocus::*; use FocusCommand::*; - use SequencerViewCommand::*; + use SequencerCommand::*; match input.event() { key!(KeyCode::Tab) => Some(Self::Focus(Next)), key!(Shift-KeyCode::Tab) => Some(Self::Focus(Prev)), @@ -39,8 +48,8 @@ impl InputToCommand> for SequencerCommand { } } -impl Command> for SequencerCommand { - fn execute (self, state: &mut SequencerApp) -> Perhaps { +impl Command for SequencerCommand { + fn execute (self, state: &mut SequencerTui) -> Perhaps { use AppViewCommand::*; match self { Focus(cmd) => delegate(cmd, Focus, state), @@ -49,9 +58,9 @@ impl Command> for SequencerCommand { } } -impl Command> for SequencerViewCommand { - fn execute (self, state: &mut SequencerApp) -> Perhaps { - use SequencerViewCommand::*; +impl Command for SequencerCommand { + fn execute (self, state: &mut SequencerTui) -> Perhaps { + use SequencerCommand::*; match self { Phrases(cmd) => delegate(cmd, Phrases, &mut state.phrases), Editor(cmd) => delegate(cmd, Editor, &mut state.editor), @@ -60,7 +69,7 @@ impl Command> for SequencerViewCommand { } } -impl TransportControl for SequencerApp { +impl TransportControl for SequencerTui { fn bpm (&self) -> &BeatsPerMinute { self.app.bpm() } diff --git a/crates/tek_tui/src/tui_transport.rs b/crates/tek_tui/src/tui_transport.rs index 3142c4d7..6f4f40c9 100644 --- a/crates/tek_tui/src/tui_transport.rs +++ b/crates/tek_tui/src/tui_transport.rs @@ -1,31 +1,7 @@ use crate::*; -/// Root type of application. -pub type TransportApp = AppView< - E, - TransportView, - AppViewCommand, - TransportStatusBar ->; - -/// Create app state from JACK handle. -impl TryFrom<&Arc>> for TransportApp { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - Ok(Self::new(TransportView { - metronome: false, - transport: jack.read().unwrap().transport(), - jack: jack.clone(), - focused: false, - focus: TransportViewFocus::PlayPause, - size: Measure::new(), - }.into(), None, None)) - } -} - /// Stores and displays time-related info. -pub struct TransportView { - _engine: PhantomData, +pub struct TransportTui { jack: Arc>, /// Playback state playing: RwLock>, @@ -46,7 +22,22 @@ pub struct TransportView { size: Measure, } -impl std::fmt::Debug for TransportView { +/// Create app state from JACK handle. +impl TryFrom<&Arc>> for TransportTui { + type Error = Box; + fn try_from (jack: &Arc>) -> Usually { + Ok(Self { + metronome: false, + transport: jack.read().unwrap().transport(), + jack: jack.clone(), + focused: false, + focus: TransportViewFocus::PlayPause, + size: Measure::new(), + }) + } +} + +impl std::fmt::Debug for TransportTui { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("transport") .field("jack", &self.jack) @@ -54,233 +45,3 @@ impl std::fmt::Debug for TransportView { .finish() } } - -/// Which item of the transport toolbar is focused -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum TransportViewFocus { - Bpm, - Sync, - PlayPause, - Clock, - Quant, -} - -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) - } -} - -#[derive(Copy, Clone)] -pub struct TransportStatusBar; - -impl Widget for TransportView { - type Engine = Tui; - fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - TransportRef(&self, Default::default()).layout(to) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - TransportRef(&self, Default::default()).render(to) - } -} - -pub struct TransportRef<'a, T: TransportViewState>(pub &'a T); - -pub trait TransportViewState: Send + Sync { - fn focus (&self) -> TransportViewFocus; - fn focused (&self) -> bool; - fn transport_state (&self) -> Option; - fn bpm_value (&self) -> f64; - fn sync_value (&self) -> f64; - fn format_beat (&self) -> String; - fn format_msu (&self) -> String; -} - -impl TransportViewState for TransportView { - fn focus (&self) -> TransportViewFocus { - self.focus - } - fn focused (&self) -> bool { - self.focused - } - fn transport_state (&self) -> Option { - *self.playing().read().unwrap() - } - fn bpm_value (&self) -> f64 { - self.bpm().get() - } - fn sync_value (&self) -> f64 { - self.sync().get() - } - fn format_beat (&self) -> String { - self.current().format_beat() - } - fn format_msu (&self) -> String { - self.current().usec.format_msu() - } -} - -impl<'a, T: TransportViewState> Content for TransportRef<'a, T> { - type Engine = Tui; - fn content (&self) -> impl Widget { - let state = self.0; - lay!( - state.focus().wrap(state.focused(), TransportViewFocus::PlayPause, &Styled( - None, - match state.transport_state() { - Some(TransportState::Rolling) => "▶ PLAYING", - Some(TransportState::Starting) => "READY ...", - Some(TransportState::Stopped) => "⏹ STOPPED", - _ => unreachable!(), - } - ).min_xy(11, 2).push_x(1)).align_x().fill_x(), - - row!( - state.focus().wrap(state.focused(), TransportViewFocus::Bpm, &Outset::X(1u16, { - let bpm = state.bpm_value(); - row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) } - })), - //let quant = state.focus().wrap(state.focused(), TransportViewFocus::Quant, &Outset::X(1u16, row! { - //"QUANT ", ppq_to_name(state.0.quant as usize) - //})), - state.focus().wrap(state.focused(), TransportViewFocus::Sync, &Outset::X(1u16, row! { - "SYNC ", pulses_to_name(state.sync_value() as usize) - })) - ).align_w().fill_x(), - - state.focus().wrap(state.focused(), TransportViewFocus::Clock, &{ - let time1 = state.format_beat(); - let time2 = state.format_msu(); - row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1) - }).align_e().fill_x(), - - ).fill_x().bg(Color::Rgb(40, 50, 30)) - } -} - -impl Audio for TransportView { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - PlayheadAudio(self).process(client, scope) - } -} - -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!(); - "" - } -} - -impl HasFocus for TransportApp { - type Item = AppViewFocus; -} - -impl FocusEnter for TransportApp { - fn focus_enter (&mut self) { - self.entered = true; - } - fn focus_exit (&mut self) { - self.entered = false; - } - fn focus_entered (&self) -> Option { - if self.entered { - Some(self.focused()) - } else { - None - } - } -} - -impl FocusGrid for TransportApp { - type Item = AppViewFocus; - fn focus_cursor (&self) -> (usize, usize) { - self.cursor - } - fn focus_cursor_mut (&mut self) -> &mut (usize, usize) { - &mut self.cursor - } - fn focus_layout (&self) -> &[&[Self::Item]] { - use AppViewFocus::*; - use TransportViewFocus::*; - &[ - &[Menu], - &[ - Content(Bpm), - Content(Sync), - Content(PlayPause), - Content(Clock), - Content(Quant), - ], - ] - } - fn focus_update (&mut self) { - // TODO - } -} - -impl HasJack for TransportView { - fn jack (&self) -> &Arc> { - &self.jack - } -} - -impl ClockApi for TransportView { - fn timebase (&self) -> &Arc { - &self.current.timebase - } - fn quant (&self) -> &Quantize { - &self.quant - } - fn sync (&self) -> &LaunchSync { - &self.sync - } -} - -impl PlayheadApi for TransportView { - fn current (&self) -> &Instant { - &self.current - } - fn transport (&self) -> &jack::Transport { - &self.transport - } - fn playing (&self) -> &RwLock> { - &self.playing - } - fn started (&self) -> &RwLock> { - &self.started - } -} diff --git a/crates/tek_tui/src/tui_transport_cmd.rs b/crates/tek_tui/src/tui_transport_cmd.rs index 642eae46..a90a8d3e 100644 --- a/crates/tek_tui/src/tui_transport_cmd.rs +++ b/crates/tek_tui/src/tui_transport_cmd.rs @@ -1,7 +1,7 @@ use crate::*; /// Handle input. -impl Handle for TransportApp { +impl Handle for TransportTui { fn handle (&mut self, from: &TuiInput) -> Perhaps { AppViewCommand::::execute_with_state(self, from) } @@ -9,15 +9,16 @@ impl Handle for TransportApp { #[derive(Clone, Debug, PartialEq)] pub enum TransportCommand { + Focus(FocusCommand), Clock(ClockCommand), Playhead(PlayheadCommand), } -impl InputToCommand> for AppViewCommand { - fn input_to_command (app: &TransportApp, input: &TuiInput) -> Option { +impl InputToCommand for AppViewCommand { + fn input_to_command (app: &TransportTui, input: &TuiInput) -> Option { use KeyCode::{Left, Right}; use FocusCommand::{Prev, Next}; - use AppViewCommand::{Focus, App}; + use TransportCommand::{Focus, Clock, Playhead}; Some(match input.event() { key!(Left) => Focus(Prev), key!(Right) => Focus(Next), @@ -26,36 +27,6 @@ impl InputToCommand> for AppViewCommand } } -pub trait TransportControl: FocusGrid> { - fn quant (&self) -> &Quantize; - fn bpm (&self) -> &BeatsPerMinute; - fn next_quant (&self) -> f64 { - next_note_length(self.quant().get() as usize) as f64 - } - fn prev_quant (&self) -> f64 { - prev_note_length(self.quant().get() as usize) as f64 - } - fn sync (&self) -> &LaunchSync; - fn next_sync (&self) -> f64 { - next_note_length(self.sync().get() as usize) as f64 - } - fn prev_sync (&self) -> f64 { - prev_note_length(self.sync().get() as usize) as f64 - } -} - -impl TransportControl for TransportApp { - fn bpm (&self) -> &BeatsPerMinute { - self.app.bpm() - } - fn quant (&self) -> &Quantize { - self.app.quant() - } - fn sync (&self) -> &LaunchSync { - self.app.sync() - } -} - impl InputToCommand for TransportCommand { fn input_to_command (state: &T, input: &TuiInput) -> Option { use KeyCode::Char; @@ -102,8 +73,38 @@ impl InputToCommand for TransportCommand { } } -impl Command> for AppViewCommand { - fn execute (self, state: &mut TransportApp) -> Perhaps { +pub trait TransportControl: FocusGrid> { + fn quant (&self) -> &Quantize; + fn bpm (&self) -> &BeatsPerMinute; + fn next_quant (&self) -> f64 { + next_note_length(self.quant().get() as usize) as f64 + } + fn prev_quant (&self) -> f64 { + prev_note_length(self.quant().get() as usize) as f64 + } + fn sync (&self) -> &LaunchSync; + fn next_sync (&self) -> f64 { + next_note_length(self.sync().get() as usize) as f64 + } + fn prev_sync (&self) -> f64 { + prev_note_length(self.sync().get() as usize) as f64 + } +} + +impl TransportControl for TransportTui { + fn bpm (&self) -> &BeatsPerMinute { + self.bpm() + } + fn quant (&self) -> &Quantize { + self.quant() + } + fn sync (&self) -> &LaunchSync { + self.sync() + } +} + +impl Command for AppViewCommand { + fn execute (self, state: &mut TransportTui) -> Perhaps { use AppViewCommand::{Focus, App}; use FocusCommand::{Next, Prev}; Ok(Some(match self { diff --git a/crates/tek_tui/src/tui_transport_focus.rs b/crates/tek_tui/src/tui_transport_focus.rs new file mode 100644 index 00000000..ebb71bdd --- /dev/null +++ b/crates/tek_tui/src/tui_transport_focus.rs @@ -0,0 +1,87 @@ +use crate::*; + +/// Which item of the transport toolbar is focused +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum TransportViewFocus { + Bpm, + Sync, + PlayPause, + Clock, + Quant, +} + +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) + } +} + +impl HasFocus for TransportTui { + type Item = AppViewFocus; +} + +impl FocusEnter for TransportTui { + fn focus_enter (&mut self) { + self.entered = true; + } + fn focus_exit (&mut self) { + self.entered = false; + } + fn focus_entered (&self) -> Option { + if self.entered { + Some(self.focused()) + } else { + None + } + } +} + +impl FocusGrid for TransportTui { + type Item = AppViewFocus; + fn focus_cursor (&self) -> (usize, usize) { + self.cursor + } + fn focus_cursor_mut (&mut self) -> &mut (usize, usize) { + &mut self.cursor + } + fn focus_layout (&self) -> &[&[Self::Item]] { + use AppViewFocus::*; + use TransportViewFocus::*; + &[ + &[Menu], + &[ + Content(Bpm), + Content(Sync), + Content(PlayPause), + Content(Clock), + Content(Quant), + ], + ] + } + fn focus_update (&mut self) { + // TODO + } +} diff --git a/crates/tek_tui/src/tui_transport_jack.rs b/crates/tek_tui/src/tui_transport_jack.rs new file mode 100644 index 00000000..71d0d277 --- /dev/null +++ b/crates/tek_tui/src/tui_transport_jack.rs @@ -0,0 +1,40 @@ +use crate::*; + +impl Audio for TransportTui { + fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + PlayheadAudio(self).process(client, scope) + } +} + +impl JackApi for TransportTui { + fn jack (&self) -> &Arc> { + &self.jack + } +} + +impl ClockApi for TransportTui { + fn timebase (&self) -> &Arc { + &self.current.timebase + } + fn quant (&self) -> &Quantize { + &self.quant + } + fn sync (&self) -> &LaunchSync { + &self.sync + } +} + +impl PlayheadApi for TransportTui { + fn current (&self) -> &Instant { + &self.current + } + fn transport (&self) -> &jack::Transport { + &self.transport + } + fn playing (&self) -> &RwLock> { + &self.playing + } + fn started (&self) -> &RwLock> { + &self.started + } +} diff --git a/crates/tek_tui/src/tui_transport_status.rs b/crates/tek_tui/src/tui_transport_status.rs new file mode 100644 index 00000000..c3936650 --- /dev/null +++ b/crates/tek_tui/src/tui_transport_status.rs @@ -0,0 +1,22 @@ +use crate::*; + +#[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!(); + "" + } +} diff --git a/crates/tek_tui/src/tui_transport_view.rs b/crates/tek_tui/src/tui_transport_view.rs new file mode 100644 index 00000000..cca53508 --- /dev/null +++ b/crates/tek_tui/src/tui_transport_view.rs @@ -0,0 +1,85 @@ +use crate::*; + +impl Widget for TransportTui { + type Engine = Tui; + fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + TransportView(&self, Default::default()).layout(to) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + TransportView(&self, Default::default()).render(to) + } +} + +pub struct TransportView<'a, T: TransportViewState>(pub &'a T); + +pub trait TransportViewState: Send + Sync { + fn focus (&self) -> TransportViewFocus; + fn focused (&self) -> bool; + fn transport_state (&self) -> Option; + fn bpm_value (&self) -> f64; + fn sync_value (&self) -> f64; + fn format_beat (&self) -> String; + fn format_msu (&self) -> String; +} + +impl TransportViewState for TransportTui { + fn focus (&self) -> TransportViewFocus { + self.focus + } + fn focused (&self) -> bool { + self.focused + } + fn transport_state (&self) -> Option { + *self.playing().read().unwrap() + } + fn bpm_value (&self) -> f64 { + self.bpm().get() + } + fn sync_value (&self) -> f64 { + self.sync().get() + } + fn format_beat (&self) -> String { + self.current().format_beat() + } + fn format_msu (&self) -> String { + self.current().usec.format_msu() + } +} + +impl<'a, T: TransportViewState> Content for TransportView<'a, T> { + type Engine = Tui; + fn content (&self) -> impl Widget { + let state = self.0; + lay!( + state.focus().wrap(state.focused(), TransportViewFocus::PlayPause, &Styled( + None, + match state.transport_state() { + Some(TransportState::Rolling) => "▶ PLAYING", + Some(TransportState::Starting) => "READY ...", + Some(TransportState::Stopped) => "⏹ STOPPED", + _ => unreachable!(), + } + ).min_xy(11, 2).push_x(1)).align_x().fill_x(), + + row!( + state.focus().wrap(state.focused(), TransportViewFocus::Bpm, &Outset::X(1u16, { + let bpm = state.bpm_value(); + row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) } + })), + //let quant = state.focus().wrap(state.focused(), TransportViewFocus::Quant, &Outset::X(1u16, row! { + //"QUANT ", ppq_to_name(state.0.quant as usize) + //})), + state.focus().wrap(state.focused(), TransportViewFocus::Sync, &Outset::X(1u16, row! { + "SYNC ", pulses_to_name(state.sync_value() as usize) + })) + ).align_w().fill_x(), + + state.focus().wrap(state.focused(), TransportViewFocus::Clock, &{ + let time1 = state.format_beat(); + let time2 = state.format_msu(); + row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1) + }).align_e().fill_x(), + + ).fill_x().bg(Color::Rgb(40, 50, 30)) + } +}