use crate::*; use ClockCommand::{Play, Pause, SetBpm, SetQuant, SetSync}; use TransportCommand::{Focus, Clock}; use FocusCommand::{Next, Prev}; use KeyCode::{Enter, Left, Right, Char}; /// Transport clock app. pub struct TransportTui { pub jack: Arc>, pub clock: ClockModel, pub size: Measure, pub cursor: (usize, usize), pub focus: TransportFocus, pub color: ItemPalette, } from_jack!(|jack|TransportTui Self { jack: jack.clone(), clock: ClockModel::from(jack), size: Measure::new(), cursor: (0, 0), focus: TransportFocus::PlayPause, color: ItemPalette::random(), }); has_clock!(|self: TransportTui|&self.clock); audio!(|self: TransportTui, client, scope|ClockAudio(self).process(client, scope)); handle!(|self: TransportTui, from|TransportCommand::execute_with_state(self, from)); render!(Tui: (self: TransportTui) => Align::x(Fixed::y(3, row!( Fixed::x(5, Fixed::y(3, PlayPause(false))), TransportView::new(self, Some(self.color), true), )))); impl std::fmt::Debug for TransportTui { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("TransportTui") .field("jack", &self.jack) .field("size", &self.size) .field("cursor", &self.cursor) .finish() } } pub struct TransportView { color: ItemPalette, focused: bool, sr: String, chunk: String, latency: String, bpm: String, ppq: String, beat: String, global_sample: String, global_second: String, started: bool, current_sample: f64, current_second: f64, } impl TransportView { pub fn new (state: &impl HasClock, color: Option, focused: bool) -> Self { let clock = state.clock(); let rate = clock.timebase.sr.get(); let chunk = clock.chunk.load(Relaxed); let latency = chunk as f64 / rate * 1000.; let sr = format!("{:.1}k", rate / 1000.0); let bpm = format!("{:.3}", clock.timebase.bpm.get()); let ppq = format!("{:.0}", clock.timebase.ppq.get()); let chunk = format!("{chunk}"); let latency = format!("{latency}"); let color = color.unwrap_or(ItemPalette::from(TuiTheme::g(32))); let ( global_sample, global_second, current_sample, current_second, beat ) = if let Some(started) = clock.started.read().unwrap().as_ref() { let current_sample = (clock.global.sample.get() - started.sample.get())/1000.; let current_usec = clock.global.usec.get() - started.usec.get(); let current_second = current_usec/1000000.; let global_sample = format!("{:.0}k", started.sample.get()/1000.); let global_second = format!("{:.1}s", started.usec.get()/1000.); let beat = clock.timebase.format_beats_1(clock.timebase.usecs_to_pulse(current_usec)); (global_sample, global_second, current_sample, current_second, beat) } else { let global_sample = format!("{:.0}k", clock.global.sample.get()/1000.); let global_second = format!("{:.1}s", clock.global.usec.get()/1000000.); let current_sample = 0.0; let current_second = 0.0; let beat = format!("000.0.00"); (global_sample, global_second, current_sample, current_second, beat) }; Self { color, focused, sr, bpm, ppq, chunk, latency, started: false, global_sample, global_second, current_sample, current_second, beat } } } render!(Tui: (self: TransportView) => { let color = self.color; //let transport_field = move|label, value|row!( //Tui::fg_bg(color.lightest.rgb, color.base.rgb, Tui::bold(true, label)), //Tui::fg_bg(color.base.rgb, color.darkest.rgb, "▌"), //Tui::fg_bg(color.lightest.rgb, color.darkest.rgb, format!("{:>10}", value)), //Tui::fg_bg(color.darkest.rgb, color.base.rgb, "▌"), //); Min::x(35, Fixed::y(3, Tui::bg(color.base.rgb, "kyp")))/*Bsp::e( Fixed::x(17, col!( transport_field(" Beat", self.beat.clone()), transport_field(" Time", format!("{:.1}s", self.current_second)), transport_field(" BPM", self.bpm.clone()), )), Fixed::x(17, col!( transport_field(" Rate", format!("{}", self.sr)), transport_field(" Chunk", format!("{}", self.chunk)), transport_field(" Lag", format!("{:.3}ms", self.latency)), )), )))*/ }); pub struct PlayPause(pub bool); render!(Tui: (self: PlayPause) => Tui::bg( if self.0{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}, Fixed::x(5, Tui::either(self.0, Tui::fg(Color::Rgb(0, 255, 0), col!( " 🭍🭑🬽 ", " 🭞🭜🭘 ", )), Tui::fg(Color::Rgb(255, 128, 0), col!( " ▗▄▖ ", " ▝▀▘ ", )))) )); impl HasFocus for TransportTui { type Item = TransportFocus; fn focused (&self) -> Self::Item { self.focus } fn set_focused (&mut self, to: Self::Item) { self.focus = to } } /// Which item of the transport toolbar is focused #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum TransportFocus { Bpm, Sync, PlayPause, Clock, Quant, } impl FocusWrap for TransportFocus { fn wrap <'a, W: Content> (self, focus: TransportFocus, content: &'a W) -> impl Content + 'a { let focused = focus == self; let corners = focused.then_some(CORNERS); //let highlight = focused.then_some(Tui::bg(Color::Rgb(60, 70, 50))); lay!(corners, /*highlight,*/ content) } } impl FocusWrap for Option { fn wrap <'a, W: Content> (self, focus: TransportFocus, content: &'a W) -> impl Content + 'a { let focused = Some(focus) == self; let corners = focused.then_some(CORNERS); //let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50))); lay!(corners, /*highlight,*/ content) } } pub trait TransportControl: HasClock + { fn transport_focused (&self) -> Option; } impl TransportControl for TransportTui { fn transport_focused (&self) -> Option { Some(self.focus) } } #[derive(Clone, Debug, PartialEq)] pub enum TransportCommand { Focus(FocusCommand), Clock(ClockCommand), } command!(|self:TransportCommand,state:TransportTui|match self { //Self::Focus(cmd) => cmd.execute(state)?.map(Self::Focus), Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock), _ => unreachable!(), }); //command!(|self:TransportFocus,state:TransportTui|{ //if let FocusCommand::Set(to) = self { state.set_focused(to); } //Ok(None) //}); impl InputToCommand for TransportCommand { fn input_to_command (state: &TransportTui, input: &TuiInput) -> Option { to_transport_command(state, input) .or_else(||to_focus_command(input).map(TransportCommand::Focus)) } } pub fn to_transport_command (state: &T, input: &TuiInput) -> Option where T: TransportControl, U: Into>, { Some(match input.event() { key_pat!(Left) => Focus(Prev), key_pat!(Right) => Focus(Next), key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), _ => match state.transport_focused().unwrap() { TransportFocus::Bpm => to_bpm_command(input, state.clock().bpm().get())?, TransportFocus::Quant => to_quant_command(input, &state.clock().quant)?, TransportFocus::Sync => to_sync_command(input, &state.clock().sync)?, TransportFocus::Clock => to_seek_command(input)?, TransportFocus::PlayPause => match input.event() { key_pat!(Enter) => Clock( if state.clock().is_stopped() { Play(None) } else { Pause(None) } ), key_pat!(Shift-Enter) => Clock( if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } ), _ => return None, }, } }) } fn to_bpm_command (input: &TuiInput, bpm: f64) -> Option { Some(match input.event() { key_pat!(Char(',')) => Clock(SetBpm(bpm - 1.0)), key_pat!(Char('.')) => Clock(SetBpm(bpm + 1.0)), key_pat!(Char('<')) => Clock(SetBpm(bpm - 0.001)), key_pat!(Char('>')) => Clock(SetBpm(bpm + 0.001)), _ => return None, }) } fn to_quant_command (input: &TuiInput, quant: &Quantize) -> Option { Some(match input.event() { key_pat!(Char(',')) => Clock(SetQuant(quant.prev())), key_pat!(Char('.')) => Clock(SetQuant(quant.next())), key_pat!(Char('<')) => Clock(SetQuant(quant.prev())), key_pat!(Char('>')) => Clock(SetQuant(quant.next())), _ => return None, }) } fn to_sync_command (input: &TuiInput, sync: &LaunchSync) -> Option { Some(match input.event() { key_pat!(Char(',')) => Clock(SetSync(sync.prev())), key_pat!(Char('.')) => Clock(SetSync(sync.next())), key_pat!(Char('<')) => Clock(SetSync(sync.prev())), key_pat!(Char('>')) => Clock(SetSync(sync.next())), _ => return None, }) } fn to_seek_command (input: &TuiInput) -> Option { Some(match input.event() { key_pat!(Char(',')) => todo!("transport seek bar"), key_pat!(Char('.')) => todo!("transport seek bar"), key_pat!(Char('<')) => todo!("transport seek beat"), key_pat!(Char('>')) => todo!("transport seek beat"), _ => return None, }) } /////////////////////////////////////////////////////////////////////////////////////////////////// //struct Field(&'static str, String); //render!(|self: Field|{ //Tui::to_east("│", Tui::to_east( //Tui::bold(true, self.0), //Tui::bg(Color::Rgb(0, 0, 0), self.1.as_str()), //)) //}); //pub struct TransportView { //pub(crate) state: Option, //pub(crate) selected: Option, //pub(crate) focused: bool, //pub(crate) bpm: f64, //pub(crate) sync: f64, //pub(crate) quant: f64, //pub(crate) beat: String, //pub(crate) msu: String, //} ////)?; ////match *state { ////Some(TransportState::Rolling) => { ////add(&row!( ////"│", ////TuiStyle::fg("▶ PLAYING", Color::Rgb(0, 255, 0)), ////format!("│0 (0)"), ////format!("│00m00s000u"), ////format!("│00B 0b 00/00") ////))?; ////add(&row!("│Now ", row!( ////format!("│0 (0)"), //sample(chunk) ////format!("│00m00s000u"), //msu ////format!("│00B 0b 00/00"), //bbt ////)))?; ////}, ////_ => { ////add(&row!("│", TuiStyle::fg("⏹ STOPPED", Color::Rgb(255, 128, 0))))?; ////add(&"")?; ////} ////} ////Ok(()) ////}).fill_x().bg(Color::Rgb(40, 50, 30)) ////}); //impl<'a, T: HasClock> From<&'a T> for TransportView where Option: From<&'a T> { //fn from (state: &'a T) -> Self { //let selected = state.into(); //Self { //selected, //focused: selected.is_some(), //state: Some(state.clock().transport.query_state().unwrap()), //bpm: state.clock().bpm().get(), //sync: state.clock().sync.get(), //quant: state.clock().quant.get(), //beat: state.clock().playhead.format_beat(), //msu: state.clock().playhead.usec.format_msu(), //} //} //} //row!( ////selected.wrap(TransportFocus::PlayPause, &play_pause.fixed_xy(10, 3)), //row!( //col!( //Field("SR ", format!("192000")), //Field("BUF ", format!("1024")), //Field("LEN ", format!("21300")), //Field("CPU ", format!("00.0%")) //), //col!( //Field("PUL ", format!("000000000")), //Field("PPQ ", format!("96")), //Field("BBT ", format!("00B0b00p")) //), //col!( //Field("SEC ", format!("000000.000")), //Field("BPM ", format!("000.000")), //Field("MSU ", format!("00m00s00u")) //), //), //selected.wrap(TransportFocus::Bpm, &Margin::X(1u16, { //row! { //"BPM ", //format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0) //} //})), //selected.wrap(TransportFocus::Sync, &Margin::X(1u16, row! { //"SYNC ", pulses_to_name(*sync as usize) //})), //selected.wrap(TransportFocus::Quant, &Margin::X(1u16, row! { //"QUANT ", pulses_to_name(*quant as usize) //})), //selected.wrap(TransportFocus::Clock, &{ //row!("B" , beat.as_str(), " T", msu.as_str()).margin_x(1) //}).align_e().fill_x(), //).fill_x().bg(Color::Rgb(40, 50, 30))