diff --git a/crates/tek/src/api/jack.rs b/crates/tek/src/api/jack.rs index 97ba47d8..d93c8e66 100644 --- a/crates/tek/src/api/jack.rs +++ b/crates/tek/src/api/jack.rs @@ -1,9 +1,5 @@ use crate::*; -pub trait JackApi { - fn jack (&self) -> &Arc>; -} - pub trait HasMidiIns { fn midi_ins (&self) -> &Vec>; fn midi_ins_mut (&mut self) -> &mut Vec>; diff --git a/crates/tek/src/api/player.rs b/crates/tek/src/api/player.rs index f728e3b8..a536716b 100644 --- a/crates/tek/src/api/player.rs +++ b/crates/tek/src/api/player.rs @@ -16,7 +16,17 @@ pub trait HasPlayPhrase: HasClock { fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)>; fn pulses_since_start (&self) -> Option { if let Some((started, Some(_))) = self.play_phrase().as_ref() { - Some(self.clock().playhead.pulse.get() - started.pulse.get()) + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + Some(elapsed) + } else { + None + } + } + fn pulses_since_start_looped (&self) -> Option { + if let Some((started, Some(phrase))) = self.play_phrase().as_ref() { + let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); + let elapsed = (elapsed as usize % phrase.read().unwrap().length) as f64; + Some(elapsed) } else { None } diff --git a/crates/tek/src/core/audio.rs b/crates/tek/src/core/audio.rs index 176b62a6..60939f6e 100644 --- a/crates/tek/src/core/audio.rs +++ b/crates/tek/src/core/audio.rs @@ -1,7 +1,7 @@ use crate::*; use jack::*; -#[derive(Debug)] +#[derive(Debug, Clone)] /// Event enum for JACK events. pub enum JackEvent { ThreadInit, diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index e94a5716..9d88445b 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -61,12 +61,6 @@ pub struct ArrangerTui { pub perf: PerfModel, } -impl JackApi for ArrangerTui { - fn jack (&self) -> &Arc> { - &self.jack - } -} - impl Audio for ArrangerTui { #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { // Start profiling cycle diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index e69de29b..5f19bf4a 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -0,0 +1,2 @@ +use crate::*; +use super::*; diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 3cb8200f..869b4ab1 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -64,12 +64,6 @@ pub struct SequencerTui { pub perf: PerfModel, } -impl JackApi for SequencerTui { - fn jack (&self) -> &Arc> { - &self.jack - } -} - impl Audio for SequencerTui { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { // Start profiling cycle @@ -109,12 +103,12 @@ impl Audio for SequencerTui { render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1, Tui::fill_xy(SequencerStatusBar::from(self)), - Tui::split_left(false, 20, + Tui::split_right(false, 20, Tui::fixed_x(20, Tui::split_down(false, 4, col!([ - PhraseSelector::play_phrase(&self.player, false, true), - PhraseSelector::next_phrase(&self.player, false, true), + PhraseSelector::play_phrase(&self.player), + PhraseSelector::next_phrase(&self.player), ]), Tui::split_up(false, 2, - PhraseSelector::edit_phrase(&self.editor.phrase, self.focused() == SequencerFocus::PhraseEditor, true), + PhraseSelector::edit_phrase(&self.editor.phrase), PhraseListView::from(self), ))), col!([ diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index a3c11beb..13e60564 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -43,12 +43,6 @@ impl HasClock for TransportTui { } } -impl JackApi for TransportTui { - fn jack (&self) -> &Arc> { - &self.jack - } -} - impl Audio for TransportTui { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { ClockAudio(self).process(client, scope) @@ -120,55 +114,53 @@ impl From<(&T, Option, bool)> for TransportView { } } -struct TransportField<'a>(&'a str, &'a str); -render!(|self: TransportField<'a>|{ - col!([ - Tui::fg(Color::Rgb(200, 200, 200), self.0), - Tui::bold(true, Tui::fg(Color::Rgb(220, 220, 220), self.1)), - ]) -}); render!(|self: TransportView|{ - let bg = self.bg; - let border_style = Style::default() - .bg(bg) - .fg(TuiTheme::border_fg(self.focused)); - let play_bg = if self.started{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}; - Tui::bg(bg, lay!(move|add|{ - add(&Tui::fill_x(Tui::at_w(lay!([ - //Lozenge(border_style), - Tui::outset_x(0, row!([ - Tui::bg(play_bg, Tui::outset_x(1, Tui::fixed_x(9, col!(|add|{ - if self.started { - add(&col!([Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING"), ""])) - } else { - add(&col!(["", Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED")])) - } - })))), " ", - Tui::fixed_x(10, TransportField("Beat", - self.beat.as_str())), " ", - Tui::fixed_x(8, TransportField("BPM ", - self.bpm.as_str())), " ", - Tui::fixed_x(8, TransportField("Second", - format!("{:.1}s", self.current_second).as_str())), " ", - Tui::fixed_x(8, TransportField("Rate ", - self.sr.as_str())), " ", - Tui::fixed_x(8, TransportField("Sample", - format!("{:.0}k", self.current_sample).as_str())), - ])) - ]))))?; - //add(&Tui::fill_x(Tui::center_x(Tui::pull_x(2, row!([ - //
- //])))))?; - //add(&Tui::fill_x(Tui::at_e(lay!(move|add|{ - ////add(&Lozenge(border_style))?; - //add(&Tui::outset_x(1, row!([ - //]))) - //})))) - Ok(()) - })) + + struct Field<'a>(&'a str, &'a str); + render!(|self: Field<'a>|row!([ + Tui::fg(Color::Rgb(200, 200, 200), self.0), + " ", + Tui::bold(true, Tui::fg(Color::Rgb(220, 220, 220), self.1)), + ])); + + struct PlayPause(bool); + render!(|self: PlayPause|Tui::bg( + if self.0{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}, + Tui::outset_x(1, Tui::fixed_x(9, col!(|add|{ + if self.0 { + add(&col!([Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING"), ""])) + } else { + add(&col!(["", Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED")])) + } + }))) + )); + + Tui::bg(self.bg, Tui::fill_x(row!([ + PlayPause(self.started), " ", + col!([ + Field("Beat", self.beat.as_str()), + Field("BPM ", self.bpm.as_str()), + ]), + " ", + col!([ + Field("Time ", format!("{:.1}s", self.current_second).as_str()), + Field("Sample", format!("{:.0}k", self.current_sample).as_str()), + ]), + ]))) + }); +impl HasFocus for TransportTui { + type Item = TransportFocus; + fn focused (&self) -> Self::Item { + self.focus.inner() + } + fn set_focused (&mut self, to: Self::Item) { + self.focus.set_inner(to) + } +} + /// Which item of the transport toolbar is focused #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum TransportFocus { @@ -207,29 +199,6 @@ impl FocusWrap for Option { } } -impl HasFocus for TransportTui { - type Item = TransportFocus; - /// Get the currently focused item. - fn focused (&self) -> Self::Item { - self.focus.inner() - } - /// Get the currently focused item. - fn set_focused (&mut self, to: Self::Item) { - self.focus.set_inner(to) - } -} - -//impl_focus!(TransportTui TransportFocus [ - ////&[Menu], - //&[ - //PlayPause, - //Bpm, - //Sync, - //Quant, - //Clock, - //], -//]); - #[derive(Copy, Clone)] pub struct TransportStatusBar; diff --git a/crates/tek/src/tui/engine_input.rs b/crates/tek/src/tui/engine_input.rs index 7508bf22..3ac34da0 100644 --- a/crates/tek/src/tui/engine_input.rs +++ b/crates/tek/src/tui/engine_input.rs @@ -1,56 +1,62 @@ use crate::*; +use Ordering::Relaxed; +use ::crossterm::event::*; pub struct TuiInput { pub(crate) exited: Arc, - pub(crate) event: TuiEvent, + pub(crate) event: TuiEvent, } #[derive(Debug, Clone)] pub enum TuiEvent { /// Terminal input - Input(::crossterm::event::Event), - /// Update values but not the whole form. - Update, - /// Update the whole form. - Redraw, - /// Device gains focus - Focus, - /// Device loses focus - Blur, - // /// JACK notification - // Jack(JackEvent) + Input(Event), + /// JACK notification + _Jack(JackEvent) } impl Input for TuiInput { type Event = TuiEvent; - fn event (&self) -> &TuiEvent { &self.event } - fn is_done (&self) -> bool { self.exited.fetch_and(true, Ordering::Relaxed) } - fn done (&self) { self.exited.store(true, Ordering::Relaxed); } -} - -impl TuiInput { - // TODO remove - pub fn handle_keymap (&self, state: &mut T, keymap: &KeyMap) -> Usually { - match self.event() { - TuiEvent::Input(crossterm::event::Event::Key(event)) => { - for (code, modifiers, _, _, command) in keymap.iter() { - if *code == event.code && modifiers.bits() == event.modifiers.bits() { - return command(state) - } - } - }, - _ => {} - }; - Ok(false) + fn event (&self) -> &TuiEvent { + &self.event + } + fn is_done (&self) -> bool { + self.exited.fetch_and(true, Relaxed) + } + fn done (&self) { + self.exited.store(true, Relaxed); } } -pub type KeyHandler = &'static dyn Fn(&mut T)->Usually; - -pub type KeyBinding = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler); - -pub type KeyMap = [KeyBinding]; +/// Define key pattern in key match statement +#[macro_export] macro_rules! key { + (Ctrl-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, + modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::NONE + })) }; + (Ctrl-$code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, + modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::NONE + })) }; + (Alt-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, + modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, state: KeyEventState::NONE + })) }; + (Alt-$code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, + modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, state: KeyEventState::NONE + })) }; + (Shift-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, + modifiers: KeyModifiers::SHIFT, kind: KeyEventKind::Press, state: KeyEventState::NONE + })) }; + (Shift-$code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, + modifiers: KeyModifiers::SHIFT, kind: KeyEventKind::Press, state: KeyEventState::NONE + })) }; + ($code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, + modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: KeyEventState::NONE + })) }; + ($code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, + modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: KeyEventState::NONE + })) }; +} +/* /// Define a key pub const fn key (code: KeyCode) -> KeyEvent { let modifiers = KeyModifiers::NONE; @@ -83,99 +89,31 @@ pub const fn shift (key: KeyEvent) -> KeyEvent { } } -/// Define a key in a keymap -#[macro_export] macro_rules! map_key { - ($k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr) => { - (KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as &dyn Fn()->Usually) +*/ + +/* + +impl TuiInput { + // TODO remove + pub fn handle_keymap (&self, state: &mut T, keymap: &KeyMap) -> Usually { + match self.event() { + TuiEvent::Input(Key(event)) => { + for (code, modifiers, _, _, command) in keymap.iter() { + if *code == event.code && modifiers.bits() == event.modifiers.bits() { + return command(state) + } + } + }, + _ => {} + }; + Ok(false) } } -/// Shorthand for key match statement -#[macro_export] macro_rules! match_key { - ($event:expr, { - $($key:pat=>$block:expr),* $(,)? - }) => { - match $event { - $(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $key, - modifiers: crossterm::event::KeyModifiers::NONE, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - }) => { - $block - })* - _ => Ok(None) - } - } -} +pub type KeyHandler = &'static dyn Fn(&mut T)->Usually; -/// Define key pattern in key match statement -#[macro_export] macro_rules! key { - ($code:pat) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::NONE, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - }; - (Ctrl-$code:pat) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::CONTROL, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - }; - (Alt-$code:pat) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::ALT, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - }; - (Shift-$code:pat) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::SHIFT, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - } -} +pub type KeyBinding = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler); -#[macro_export] macro_rules! key_lit { - ($code:expr) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::NONE, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - }; - (Ctrl-$code:expr) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::CONTROL, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - }; - (Alt-$code:expr) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::ALT, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - }; - (Shift-$code:expr) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::SHIFT, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - } -} +pub type KeyMap = [KeyBinding]; + +*/ diff --git a/crates/tek/src/tui/phrase_length.rs b/crates/tek/src/tui/phrase_length.rs index f6527cf4..675fa946 100644 --- a/crates/tek/src/tui/phrase_length.rs +++ b/crates/tek/src/tui/phrase_length.rs @@ -4,6 +4,7 @@ use PhraseLengthFocus::*; use PhraseLengthCommand::*; /// Displays and edits phrase length. +#[derive(Clone)] pub struct PhraseLength { /// Pulses per beat (quaver) pub ppq: usize, diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 7ffd6a20..1e7a559e 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -110,50 +110,46 @@ impl<'a, T: HasPhraseList> From<&'a T> for PhraseListView<'a> { // TODO: Display phrases always in order of appearance render!(|self: PhraseListView<'a>|{ let Self { title, focused, entered, phrases, index, mode } = self; - let bg = if *focused {TuiTheme::g(32)} else {TuiTheme::null()}; - let border_bg = if *focused {TuiTheme::bg()} else {TuiTheme::null()}; - let border_color = if *entered {TuiTheme::bo1()} else {TuiTheme::bo2()}; - let title_color = if *focused {TuiTheme::ti1()} else {TuiTheme::ti2()}; + let bg = TuiTheme::g(32); + let title_color = TuiTheme::ti1(); let upper_left = format!("{title}"); let upper_right = format!("({})", phrases.len()); Tui::bg(bg, lay!(move|add|{ //add(&Lozenge(Style::default().bg(border_bg).fg(border_color)))?; add(&Tui::inset_xy(0, 1, Tui::fill_xy(col!(move|add|match mode { - Some(PhrasesMode::Import(_, ref browser)) => { - add(browser) - }, - Some(PhrasesMode::Export(_, ref browser)) => { - add(browser) - }, - _ => { - for (i, phrase) in phrases.iter().enumerate() { - add(&lay!(|add|{ - let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); - let mut length = PhraseLength::new(length, None); - if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode { - if *focused && i == *phrase { - length.pulses = *new_length; - length.focus = Some(*focus); - } + Some(PhrasesMode::Import(_, ref file_picker)) => add(file_picker), + Some(PhrasesMode::Export(_, ref file_picker)) => add(file_picker), + _ => Ok(for (i, phrase) in phrases.iter().enumerate() { + add(&lay!(|add|{ + let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); + let mut length = PhraseLength::new(length, None); + if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode { + if *focused && i == *phrase { + length.pulses = *new_length; + length.focus = Some(*focus); } - let length = Tui::fill_x(Tui::at_e(length)); - let row1 = Tui::fill_x(lay!([Tui::fill_x(Tui::at_w(format!(" {i}"))), length])); - let mut row2 = format!(" {name}"); - if let Some(PhrasesMode::Rename(phrase, _)) = mode { - if *focused && i == *phrase { - row2 = format!("{row2}▄"); - } - }; - let row2 = Tui::bold(true, row2); - add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([row1, row2]))))?; - if *entered && i == *index { - add(&CORNERS)?; - } - Ok(()) - }))?; - } - Ok(()) - } + } + add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([ + Tui::fill_x(lay!(|add|{ + add(&Tui::fill_x(Tui::at_w(format!(" {i}"))))?; + add(&Tui::fill_x(Tui::at_e(Tui::pull_x(1, length.clone())))) + })), + Tui::bold(true, { + let mut row2 = format!(" {name}"); + if let Some(PhrasesMode::Rename(phrase, _)) = mode { + if *focused && i == *phrase { + row2 = format!("{row2}▄"); + } + }; + row2 + }), + ]))))?; + if *entered && i == *index { + add(&CORNERS)?; + } + Ok(()) + }))?; + }) }))))?; add(&Tui::fill_xy(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))))?; add(&Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string()))))) diff --git a/crates/tek/src/tui/phrase_select.rs b/crates/tek/src/tui/phrase_select.rs index 05300c8f..f2fbe626 100644 --- a/crates/tek/src/tui/phrase_select.rs +++ b/crates/tek/src/tui/phrase_select.rs @@ -3,79 +3,72 @@ use crate::*; pub struct PhraseSelector { pub(crate) title: &'static str, pub(crate) phrase: Option<(Moment, Option>>)>, - pub(crate) focused: bool, - pub(crate) entered: bool, + pub(crate) time: String, } // TODO: Display phrases always in order of appearance render!(|self: PhraseSelector|{ - let Self { title, phrase, focused, entered } = self; - let border_bg = if *focused {TuiTheme::border_bg()} else {TuiTheme::null()}; - let border_color = if *focused {TuiTheme::bo1()} else {TuiTheme::bo2()}; + let Self { title, phrase, time } = self; + let border_bg = TuiTheme::border_bg(); + let border_color = TuiTheme::bo1(); let border = Lozenge(Style::default().bg(border_bg).fg(border_color)); - let title_color = if phrase.is_some() { - TuiTheme::g(200) - } else if *focused { - TuiTheme::ti1() - } else { - TuiTheme::ti2() - }; + let title_color = TuiTheme::g(200); Tui::fixed_y(2, lay!(move|add|{ - if phrase.is_none() { - add(&Tui::fill_x(border))?; - } + //if phrase.is_none() { + //add(&Tui::fill_x(border))?; + //} add(&Tui::push_x(1, Tui::fg(title_color, *title)))?; - add(&Tui::push_y(0, Tui::fill_xy(Layers::new(move|add|{ - if let Some((instant, Some(phrase))) = phrase { - let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); - add(&Tui::pull_y(0, Tui::inset_x(0, Tui::bg(color.base.rgb, Tui::fill_x(col!([ - Tui::fill_x(lay!([ - Tui::fill_x(Tui::at_w(Tui::fg(TuiTheme::g(255), format!(" ")))), - Tui::fill_x(Tui::at_e(Tui::fg(TuiTheme::g(255), PhraseLength::new(length, None)))) - ])), - Tui::bold(true, Tui::fg(TuiTheme::g(255), format!(" {name}"))) - ]))))))?; - } - Ok(()) - })))) + if let Some((_started, Some(phrase))) = phrase { + let Phrase { ref name, color, .. } = *phrase.read().unwrap(); + add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([ + Tui::fill_x(lay!([ + Tui::fill_x(Tui::at_w(Tui::fg(TuiTheme::g(255), format!(" ")))), + Tui::inset_x(1, Tui::fill_x(Tui::at_e(Tui::fg(TuiTheme::g(255), time)))) + ])), + Tui::bold(true, Tui::fg(TuiTheme::g(255), format!(" {name}"))) + ]))))?; + } + Ok(()) })) }); - impl PhraseSelector { - pub fn play_phrase ( - state: &T, - focused: bool, - entered: bool, - ) -> Self { + // beats elapsed + pub fn play_phrase (state: &T) -> Self { + let phrase = state.play_phrase().clone(); Self { - focused, - entered: focused && entered, - phrase: state.play_phrase().clone(), - title: "Now:", + title: "Now:", + time: if let Some(elapsed) = state.pulses_since_start_looped() { + format!("+{:>}", state.clock().timebase.format_beats_0(elapsed)) + } else { + String::from("") + }, + phrase, } } - pub fn next_phrase ( - state: &T, - focused: bool, - entered: bool, - ) -> Self { + // beats until switchover + pub fn next_phrase (state: &T) -> Self { + let phrase = state.next_phrase().clone(); Self { - focused, - entered: focused && entered, - phrase: state.next_phrase().clone(), - title: "Next:", + title: "Next:", + time: phrase.as_ref().map(|(t, _)|{ + let target = t.pulse.get(); + let current = state.clock().playhead.pulse.get(); + if target > current { + let remaining = target - current; + format!("-{:>}", state.clock().timebase.format_beats_0(remaining)) + } else { + String::new() + } + }).unwrap_or(String::from("")), + phrase, } } - pub fn edit_phrase ( - phrase: &Option>>, - focused: bool, - entered: bool, - ) -> Self { + pub fn edit_phrase (phrase: &Option>>) -> Self { + let phrase = phrase.clone(); Self { - focused, - entered: focused && entered, - phrase: Some((Moment::default(), phrase.clone())), - title: "Edit:", + title: "Edit:", + time: phrase.as_ref().map(|p|format!("{}", p.read().unwrap().length)).unwrap_or(String::new()), + phrase: Some((Moment::default(), phrase)), } } }