From 5550631254fd93cee1979c64caba14aea25b50ad Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 10 Dec 2024 19:46:09 +0100 Subject: [PATCH 001/971] removing direct uses of Color::Rgb --- crates/tek/src/tui/app_arranger.rs | 2 +- crates/tek/src/tui/app_sequencer.rs | 16 ++++++++-------- crates/tek/src/tui/engine_theme.rs | 29 +++++++++++++++++++++++++---- crates/tek/src/tui/phrase_list.rs | 6 +++--- crates/tek/src/tui/phrase_select.rs | 16 ++++++++-------- 5 files changed, 45 insertions(+), 24 deletions(-) diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index f5cc1afe..59c3f516 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -19,7 +19,7 @@ impl TryFrom<&Arc>> for ArrangerTui { selected: ArrangerSelection::Clip(0, 0), scenes: vec![], tracks: vec![], - color: Color::Rgb(28, 35, 25).into(), + color: TuiTheme::bg().into(), history: vec![], mode: ArrangerMode::Vertical(2), name: Arc::new(RwLock::new(String::new())), diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 75d0b9b5..effd20c5 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -345,12 +345,12 @@ impl From<&SequencerStatusBar> for SequencerMode { } } } -render!(|self:SequencerMode|{ - let orange = Color::Rgb(255,128,0); - let light = Color::Rgb(50,50,50); - let white = Color::Rgb(255,255,255); - let yellow = Color::Rgb(255,255,0); - let black = Color::Rgb(0,0,0); +render!(|self: SequencerMode|{ + let black = TuiTheme::g(0); + let light = TuiTheme::g(50); + let white = TuiTheme::g(255); + let orange = TuiTheme::orange(); + let yellow = TuiTheme::yellow(); row!([ Tui::bg(orange, Tui::fg(black, Tui::bold(true, self.mode))), Tui::bg(light, Tui::fg(white, row!((prefix, hotkey, suffix) in self.help.iter() => { @@ -374,8 +374,8 @@ impl<'a> From<&'a SequencerStatusBar> for SequencerStats<'a> { } } render!(|self:SequencerStats<'a>|{ - let orange = Color::Rgb(255,128,0); - let dark = Color::Rgb(25,25,25); + let orange = TuiTheme::orange(); + let dark = TuiTheme::g(25); let cpu = &self.cpu; let res = &self.res; let size = &self.size; diff --git a/crates/tek/src/tui/engine_theme.rs b/crates/tek/src/tui/engine_theme.rs index 738dd093..ccb66234 100644 --- a/crates/tek/src/tui/engine_theme.rs +++ b/crates/tek/src/tui/engine_theme.rs @@ -7,8 +7,8 @@ impl Theme for TuiTheme {} pub trait Theme { const HOTKEY_FG: Color = Color::Rgb(255, 255, 0); - fn black () -> Color { - Color::Rgb(0, 0, 0) + fn null () -> Color { + Color::Reset } fn bg () -> Color { Color::Rgb(28, 35, 25) @@ -17,10 +17,10 @@ pub trait Theme { Color::Rgb(40, 50, 30) } fn border_fg (focused: bool) -> Color { - if focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) } + if focused { Self::bo1() } else { Self::bo2() } } fn title_fg (focused: bool) -> Color { - if focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) } + if focused { Self::ti1() } else { Self::ti2() } } fn separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) @@ -34,4 +34,25 @@ pub trait Theme { fn status_bar_bg () -> Color { Color::Rgb(28, 35, 25) } + fn bo1 () -> Color { + Color::Rgb(100, 110, 40) + } + fn bo2 () -> Color { + Color::Rgb(70, 80, 50) + } + fn ti1 () -> Color { + Color::Rgb(150, 160, 90) + } + fn ti2 () -> Color { + Color::Rgb(120, 130, 100) + } + fn orange () -> Color { + Color::Rgb(255,128,0) + } + fn yellow () -> Color { + Color::Rgb(255,255,0) + } + fn g (g: u8) -> Color { + Color::Rgb(g, g, g) + } } diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 4d1cd9ce..1b32f448 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -122,9 +122,9 @@ 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 border_bg = if *focused {Color::Rgb(40, 50, 30)} else {Color::Reset}; - let border_color = if *entered {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)}; - let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)}; + 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 upper_left = format!("{title}"); let upper_right = format!("({})", phrases.len()); Tui::bg(border_bg, lay!(move|add|{ diff --git a/crates/tek/src/tui/phrase_select.rs b/crates/tek/src/tui/phrase_select.rs index b99a0861..aa5c80f2 100644 --- a/crates/tek/src/tui/phrase_select.rs +++ b/crates/tek/src/tui/phrase_select.rs @@ -10,15 +10,15 @@ pub struct PhraseSelector { // TODO: Display phrases always in order of appearance render!(|self: PhraseSelector|{ let Self { title, phrase, focused, entered } = self; - let border_bg = if *focused {Color::Rgb(40, 50, 30)} else { Color::Reset }; - let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)}; + let border_bg = if *focused {TuiTheme::border_bg()} else {TuiTheme::null()}; + let border_color = if *focused {TuiTheme::bo1()} else {TuiTheme::bo2()}; let border = Lozenge(Style::default().bg(border_bg).fg(border_color)); let title_color = if phrase.is_some() { - Color::Rgb(200,200,200) + TuiTheme::g(200) } else if *focused { - Color::Rgb(150, 160, 90) + TuiTheme::ti1() } else { - Color::Rgb(120, 130, 100) + TuiTheme::ti2() }; Tui::fixed_y(2, lay!(move|add|{ if phrase.is_none() { @@ -30,10 +30,10 @@ render!(|self: PhraseSelector|{ let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); add(&Tui::pull_y(0, Tui::inset_x(0, Tui::bg(color.dark.rgb, Tui::fill_x(col!([ Tui::fill_x(lay!([ - Tui::fill_x(Tui::at_w(Tui::fg(Color::Rgb(255,255,255), format!(" ")))), - Tui::fill_x(Tui::at_e(Tui::fg(Color::Rgb(255,255,255), PhraseLength::new(length, None)))) + 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(Color::Rgb(255,255,255), format!(" {name}"))) + Tui::bold(true, Tui::fg(TuiTheme::g(255), format!(" {name}"))) ]))))))?; } Ok(()) From 761ec78282d3063a911c72fd16588cbae21a1b0a Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 10 Dec 2024 20:16:06 +0100 Subject: [PATCH 002/971] flip it puside down --- crates/tek/src/api/clock.rs | 55 ++++++++++++----------------- crates/tek/src/api/phrase.rs | 2 +- crates/tek/src/tui/app_sequencer.rs | 34 +++++++++--------- crates/tek/src/tui/phrase_list.rs | 4 +-- 4 files changed, 42 insertions(+), 53 deletions(-) diff --git a/crates/tek/src/api/clock.rs b/crates/tek/src/api/clock.rs index 8e6e8574..6276b6b9 100644 --- a/crates/tek/src/api/clock.rs +++ b/crates/tek/src/api/clock.rs @@ -33,23 +33,6 @@ impl Command for ClockCommand { } } -#[derive(Clone)] -pub struct Timeline { - pub timebase: Arc, - pub started: Arc>>, - pub loopback: Arc>>, -} - -impl Default for Timeline { - fn default () -> Self { - Self { - timebase: Arc::new(Timebase::default()), - started: RwLock::new(None).into(), - loopback: RwLock::new(None).into(), - } - } -} - #[derive(Clone)] pub struct ClockModel { /// JACK transport handle. @@ -60,6 +43,8 @@ pub struct ClockModel { pub global: Arc, /// Global sample and usec at which playback started pub started: Arc>>, + /// Playback offset (when playing not from start) + pub offset: Arc, /// Current playhead position pub playhead: Arc, /// Note quantization factor @@ -83,6 +68,7 @@ impl From<&Arc>> for ClockModel { chunk: Arc::new((chunk as usize).into()), global: Arc::new(Moment::zero(&timebase)), playhead: Arc::new(Moment::zero(&timebase)), + offset: Arc::new(Moment::zero(&timebase)), started: RwLock::new(None).into(), timebase, } @@ -158,31 +144,34 @@ impl ClockModel { self.chunk.store(n_frames, Ordering::Relaxed); } pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { + // Store buffer length self.set_chunk(scope.n_frames() as usize); + + // Store reported global frame and usec let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; self.global.sample.set(current_frames as f64); self.global.usec.set(current_usecs as f64); + + // If transport has just started or just stopped, + // update starting point: let mut started = self.started.write().unwrap(); - match self.transport.query_state()? { - TransportState::Rolling => { - if started.is_none() { - let moment = Moment::zero(&self.timebase); - moment.sample.set(current_frames as f64); - moment.usec.set(current_usecs as f64); - *started = Some(moment); - } + match (self.transport.query_state()?, started.as_ref()) { + (TransportState::Rolling, None) => { + let moment = Moment::zero(&self.timebase); + moment.sample.set(current_frames as f64); + moment.usec.set(current_usecs as f64); + *started = Some(moment); }, - TransportState::Stopped => { - if started.is_some() { - *started = None; - } + (TransportState::Stopped, Some(_)) => { + *started = None; }, _ => {} }; - self.playhead.update_from_sample(match *started { - Some(ref instant) => current_frames as f64 - instant.sample.get(), - None => 0. - }); + + self.playhead.update_from_sample(started.as_ref() + .map(|started|current_frames as f64 - started.sample.get()) + .unwrap_or(0.)); + Ok(()) } } diff --git a/crates/tek/src/api/phrase.rs b/crates/tek/src/api/phrase.rs index 7125856a..05a88f71 100644 --- a/crates/tek/src/api/phrase.rs +++ b/crates/tek/src/api/phrase.rs @@ -159,7 +159,7 @@ impl Phrase { impl Default for Phrase { fn default () -> Self { - Self::new("(empty)", false, 0, None, Some(ItemColor::from(Color::Rgb(0, 0, 0)).into())) + Self::new("null", false, 0, None, Some(ItemColor::from(Color::Rgb(0, 0, 0)).into())) } } diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index effd20c5..3d4c4b48 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -107,38 +107,38 @@ impl Audio for SequencerTui { } } -render!(|self: SequencerTui|lay!([ - self.size, - Tui::shrink_y(1, col!([ +render!(|self: SequencerTui|lay!([self.size, Tui::split_up(1, + Tui::fill_xy(Tui::at_s(SequencerStatusBar::from(self))), + Tui::split_up(2, TransportView::from((self, if let SequencerFocus::Transport(_) = self.focus.inner() { true } else { false })), row!([ - Tui::fixed_x(20, Tui::split_up(2, PhraseSelector::edit_phrase( - &self.editor.phrase, - self.focused() == SequencerFocus::PhraseEditor, - self.entered() - ), col!([ - PhraseSelector::play_phrase( - &self.player, - self.focused() == SequencerFocus::PhrasePlay, - self.entered() - ), + Tui::fixed_x(20, Tui::split_up(4, col!([ PhraseSelector::next_phrase( &self.player, self.focused() == SequencerFocus::PhraseNext, self.entered() ), + PhraseSelector::play_phrase( + &self.player, + self.focused() == SequencerFocus::PhrasePlay, + self.entered() + ), + ]), col!([ + PhraseSelector::edit_phrase( + &self.editor.phrase, + self.focused() == SequencerFocus::PhraseEditor, + self.entered() + ), PhraseListView::from(self), - ]))), PhraseView::from(self) ]) - ])), - Tui::fill_xy(Tui::at_s(SequencerStatusBar::from(self))), -])); + ) +)])); impl HasClock for SequencerTui { fn clock (&self) -> &ClockModel { diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 1b32f448..2849e52d 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -270,13 +270,13 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option Cmd::Select( index.saturating_add(1) % state.phrases().len() ), - key!(Char(',')) => if index > 1 { + key!(Char('<')) => if index > 1 { state.set_phrase_index(state.phrase_index().saturating_sub(1)); Cmd::Phrase(Pool::Swap(index - 1, index)) } else { return None }, - key!(Char('.')) => if index < count.saturating_sub(1) { + key!(Char('>')) => if index < count.saturating_sub(1) { state.set_phrase_index(state.phrase_index() + 1); Cmd::Phrase(Pool::Swap(index + 1, index)) } else { From 1ceb2dd2daf50f2863305b5d93721f165ba7a53f Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 10 Dec 2024 21:06:21 +0100 Subject: [PATCH 003/971] colorize transport --- crates/tek/src/tui/app_arranger.rs | 11 ++--- crates/tek/src/tui/app_sequencer.rs | 14 ++++--- crates/tek/src/tui/app_transport.rs | 13 ++++-- crates/tek/src/tui/phrase_editor.rs | 63 ++++++++++++++--------------- crates/tek/src/tui/phrase_select.rs | 2 +- 5 files changed, 55 insertions(+), 48 deletions(-) diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 59c3f516..2f7de25d 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -118,12 +118,13 @@ impl Audio for ArrangerTui { render!(|self: ArrangerTui|{ let arranger_focused = self.arranger_focused(); let border = Lozenge(Style::default().bg(TuiTheme::border_bg()).fg(TuiTheme::border_fg(arranger_focused))); + let transport_focused = if let ArrangerFocus::Transport(_) = self.focus.inner() { + true + } else { + false + }; col!([ - TransportView::from((self, if let ArrangerFocus::Transport(_) = self.focus.inner() { - true - } else { - false - })), + TransportView::from((self, None, transport_focused)), col!([ Tui::fixed_y(self.splits[0], lay!([ border.wrap(Tui::grow_y(1, Layers::new(move |add|{ diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 3d4c4b48..8f536b0e 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -110,11 +110,15 @@ impl Audio for SequencerTui { render!(|self: SequencerTui|lay!([self.size, Tui::split_up(1, Tui::fill_xy(Tui::at_s(SequencerStatusBar::from(self))), Tui::split_up(2, - TransportView::from((self, if let SequencerFocus::Transport(_) = self.focus.inner() { - true - } else { - false - })), + TransportView::from(( + self, + self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(), + if let SequencerFocus::Transport(_) = self.focus.inner() { + true + } else { + false + } + )), row!([ Tui::fixed_x(20, Tui::split_up(4, col!([ PhraseSelector::next_phrase( diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index 1f05267a..61dd1d42 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -55,9 +55,10 @@ impl Audio for TransportTui { } } -render!(|self: TransportTui|TransportView::from((self, true))); +render!(|self: TransportTui|TransportView::from((self, None, true))); pub struct TransportView { + bg: Color, focused: bool, sr: String, @@ -74,17 +75,20 @@ pub struct TransportView { current_second: f64, } -impl From<(&T, bool)> for TransportView { - fn from ((state, focused): (&T, bool)) -> Self { +impl From<(&T, Option, bool)> for TransportView { + fn from ((state, color, focused): (&T, Option, bool)) -> Self { let clock = state.clock(); let sr = format!("{:.1}k", clock.timebase.sr.get() / 1000.0); let bpm = format!("{:.3}", clock.timebase.bpm.get()); let ppq = format!("{:.0}", clock.timebase.ppq.get()); + let color = color.unwrap_or(ItemColorTriplet::from(ItemColor::from(TuiTheme::g(100)))); + let bg = if focused { color.light.rgb } else { color.dark.rgb }; 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.; Self { + bg, focused, sr, bpm, @@ -100,6 +104,7 @@ impl From<(&T, bool)> for TransportView { } } else { Self { + bg, focused, sr, bpm, @@ -124,7 +129,7 @@ render!(|self: TransportField<'a>|{ }); render!(|self: TransportView|{ - let bg = if self.focused { TuiTheme::border_bg() } else { TuiTheme::bg() }; + let bg = self.bg; let border_style = Style::default() .bg(bg) .fg(TuiTheme::border_fg(self.focused)); diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 6a0090fe..e5c46422 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -210,39 +210,12 @@ render!(|self: PhraseView<'a>|{ //now: _, .. } = self; + let phrase_color = phrase.as_ref().map(|p|p.read().unwrap().color.clone()) + .unwrap_or(ItemColorTriplet::from(ItemColor::from(TuiTheme::g(64)))); + let title_color = if *focused{phrase_color.light.rgb}else{phrase_color.dark.rgb}; + let bg = if self.focused { TuiTheme::bg() } else { Color::Reset }; lay!([ - lay!(move|add|{ - //if *focused { - add(&Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(TuiTheme::border_fg(true))))?; - //} - let title_color = if *focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)}; - let upper_left = format!("{}", - phrase.as_ref().map(|p|p.read().unwrap().name.clone()).unwrap_or(String::new()) - ); - let lower_left = format!(""); - let mut lower_right = format!(" {} ", size.format()); - if *focused && *entered { - lower_right = format!("Note: {} ({}) {} {lower_right}", - note_point, to_note_name(*note_point), pulses_to_name(*note_len) - ); - } - let mut upper_right = format!("[{}]", if *entered {"■"} else {" "}); - if let Some(phrase) = phrase { - upper_right = format!("Time: {}/{} {} {upper_right}", - time_point, phrase.read().unwrap().length, pulses_to_name(view_mode.time_zoom()), - ) - }; - add(&Tui::push_x(1, Tui::at_nw(Tui::fg(title_color, upper_left))))?; - add(&Tui::at_sw(Tui::fg(title_color, lower_left)))?; - add(&Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right)))))?; - add(&Tui::fill_xy(Tui::at_se(Tui::pull_x(1, Tui::fg(title_color, lower_right)))))?; - Ok(()) - }), - Tui::bg(if self.focused { - TuiTheme::bg() - } else { - Color::Reset - }, Tui::inset_x(1, Tui::fill_x(row!([ + Tui::bg(bg, Tui::inset_x(1, Tui::fill_x(row!([ Tui::push_y(1, Tui::fill_y(Widget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{ Ok(if to.area().h() >= 2 { view_mode.render_keys(to, *note_hi, *note_lo) }) }))), @@ -262,7 +235,31 @@ render!(|self: PhraseView<'a>|{ }) })) ])), - ])))) + ])))), + lay!(move|add|{ + add(&Lozenge(Style::default().bg(phrase_color.base.rgb).fg(phrase_color.base.rgb)))?; + let upper_left = format!("{}", + phrase.as_ref().map(|p|p.read().unwrap().name.clone()).unwrap_or(String::new()) + ); + let lower_left = format!(""); + let mut lower_right = format!(" {} ", size.format()); + if *focused && *entered { + lower_right = format!("Note: {} ({}) {} {lower_right}", + note_point, to_note_name(*note_point), pulses_to_name(*note_len) + ); + } + let mut upper_right = format!("[{}]", if *entered {"■"} else {" "}); + if let Some(phrase) = phrase { + upper_right = format!("Time: {}/{} {} {upper_right}", + time_point, phrase.read().unwrap().length, pulses_to_name(view_mode.time_zoom()), + ) + }; + add(&Tui::push_x(1, Tui::at_nw(Tui::fg(TuiTheme::g(224), Tui::bg(title_color, upper_left)))))?; + add(&Tui::at_sw(Tui::bg(title_color, Tui::fg(TuiTheme::g(224), lower_left))))?; + add(&Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::bg(title_color, Tui::fg(TuiTheme::g(224), upper_right))))))?; + add(&Tui::fill_xy(Tui::at_se(Tui::pull_x(1, Tui::bg(title_color, Tui::fg(TuiTheme::g(224), lower_right))))))?; + Ok(()) + }), ]) }); diff --git a/crates/tek/src/tui/phrase_select.rs b/crates/tek/src/tui/phrase_select.rs index aa5c80f2..05300c8f 100644 --- a/crates/tek/src/tui/phrase_select.rs +++ b/crates/tek/src/tui/phrase_select.rs @@ -28,7 +28,7 @@ render!(|self: PhraseSelector|{ 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.dark.rgb, Tui::fill_x(col!([ + 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)))) From 2f623783ec277e5d390dd5aba34c8e55253ea38a Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 10 Dec 2024 21:26:33 +0100 Subject: [PATCH 004/971] simplify focus --- crates/tek/src/tui.rs | 1 + crates/tek/src/tui/app_sequencer.rs | 28 ++++++++++++++-------------- crates/tek/src/tui/app_transport.rs | 4 ++-- crates/tek/src/tui/phrase_editor.rs | 3 ++- crates/tek/src/tui/port_select.rs | 7 +++++++ 5 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 crates/tek/src/tui/port_select.rs diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 645073db..90c10630 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -22,6 +22,7 @@ mod phrase_rename; pub(crate) use phrase_rename::*; mod phrase_list; pub(crate) use phrase_list::*; mod phrase_player; pub(crate) use phrase_player::*; mod phrase_select; pub(crate) use phrase_select::*; +mod port_select; pub(crate) use port_select::*; //////////////////////////////////////////////////////// diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 8f536b0e..fe0d97c7 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -197,20 +197,20 @@ impl_focus!(SequencerTui SequencerFocus [ Transport(TransportFocus::Quant), Transport(TransportFocus::Clock), ], - &[ - PhrasePlay, - PhrasePlay, - PhraseEditor, - PhraseEditor, - PhraseEditor, - ], - &[ - PhraseNext, - PhraseNext, - PhraseEditor, - PhraseEditor, - PhraseEditor, - ], + //&[ + //PhrasePlay, + //PhrasePlay, + //PhraseEditor, + //PhraseEditor, + //PhraseEditor, + //], + //&[ + //PhraseNext, + //PhraseNext, + //PhraseEditor, + //PhraseEditor, + //PhraseEditor, + //], &[ PhraseList, PhraseList, diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index 61dd1d42..31b25284 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -123,8 +123,8 @@ impl From<(&T, Option, bool)> for TransportView { struct TransportField<'a>(&'a str, &'a str); render!(|self: TransportField<'a>|{ col!([ - Tui::fg(Color::Rgb(150, 150, 150), self.0), - Tui::bold(true, Tui::fg(Color::Rgb(200, 200, 200), self.1)), + Tui::fg(Color::Rgb(200, 200, 200), self.0), + Tui::bold(true, Tui::fg(Color::Rgb(220, 220, 220), self.1)), ]) }); diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index e5c46422..c6f9cf9b 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -436,7 +436,8 @@ impl PhraseViewMode { for (y, note) in (0..127).rev().enumerate() { for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) { let cell = target.get_mut(x, y).unwrap(); - cell.set_fg(Color::Rgb(48, 55, 45)); + //cell.set_fg(Color::Rgb(48, 55, 45)); + cell.set_fg(phrase.color.dark.rgb); cell.set_char(if time % 384 == 0 { '│' } else if time % 96 == 0 { diff --git a/crates/tek/src/tui/port_select.rs b/crates/tek/src/tui/port_select.rs new file mode 100644 index 00000000..3c71dfb0 --- /dev/null +++ b/crates/tek/src/tui/port_select.rs @@ -0,0 +1,7 @@ +use crate::*; + +pub struct PortSelector { + pub(crate) title: &'static str, +} + +render!(|self: PortSelector|{}); From 5cca7dc22b9d2a1c002f83e055359d108e5ab882 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 10 Dec 2024 21:43:34 +0100 Subject: [PATCH 005/971] rebind note length to ,. --- crates/tek/src/tui/phrase_editor.rs | 9 ++++++--- crates/tek/src/tui/phrase_list.rs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index c6f9cf9b..5fde1eec 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -528,9 +528,12 @@ impl InputToCommand for PhraseCommand { key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)), key!(Char('a')) => AppendNote, key!(Char('s')) => PutNote, - key!(Char('[')) => SetNoteLength(prev_note_length(state.note_len)), - key!(Char(']')) => SetNoteLength(next_note_length(state.note_len)), - key!(Char('n')) => { todo!("toggle keys vs notes") }, + // TODO: no triplet/dotted + key!(Char(',')) => SetNoteLength(prev_note_length(state.note_len)), + key!(Char('.')) => SetNoteLength(next_note_length(state.note_len)), + // TODO: with triplet/dotted + key!(Char('<')) => SetNoteLength(prev_note_length(state.note_len)), + key!(Char('>')) => SetNoteLength(next_note_length(state.note_len)), _ => match state.edit_mode { PhraseEditMode::Scroll => match from.event() { key!(Char('e')) => SetEditMode(PhraseEditMode::Note), diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 2849e52d..d11d202b 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -128,7 +128,7 @@ render!(|self: PhraseListView<'a>|{ let upper_left = format!("{title}"); let upper_right = format!("({})", phrases.len()); Tui::bg(border_bg, lay!(move|add|{ - add(&Lozenge(Style::default().bg(border_bg).fg(border_color)))?; + //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) From fa8316c6513f456caa2d3fccbe66dde2395b971c Mon Sep 17 00:00:00 2001 From: unspeaker Date: Tue, 10 Dec 2024 21:49:50 +0100 Subject: [PATCH 006/971] add global 'c' command --- crates/tek/src/tui/app_sequencer.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index fe0d97c7..4f67ef2e 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -443,6 +443,9 @@ impl InputToCommand for SequencerCommand { key!(Char('_')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(next_zoom)), key!(Char('=')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(prev_zoom)), key!(Char('+')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(prev_zoom)), + key!(Char('c')) => SequencerCommand::Phrases( + PhrasesCommand::input_to_command(&state.phrases, input)?, + ), _ => return None } })) From 042d480b67d13c490dac86dc5988ef00375fca83 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 11 Dec 2024 19:16:28 +0100 Subject: [PATCH 007/971] ItemPalette --- crates/tek/src/api/phrase.rs | 8 +- crates/tek/src/core/color.rs | 6 +- crates/tek/src/core/space.rs | 5 +- crates/tek/src/layout/measure.rs | 30 +- crates/tek/src/layout/split.rs | 61 ++- crates/tek/src/tui/app_arranger.rs | 15 +- crates/tek/src/tui/app_groovebox.rs | 0 crates/tek/src/tui/app_sequencer.rs | 83 ++-- crates/tek/src/tui/app_transport.rs | 64 +-- crates/tek/src/tui/engine_theme.rs | 3 + crates/tek/src/tui/phrase_editor.rs | 645 +++++++++++++++------------- crates/tek/src/tui/phrase_list.rs | 9 +- 12 files changed, 505 insertions(+), 424 deletions(-) create mode 100644 crates/tek/src/tui/app_groovebox.rs diff --git a/crates/tek/src/api/phrase.rs b/crates/tek/src/api/phrase.rs index 05a88f71..94aa24c2 100644 --- a/crates/tek/src/api/phrase.rs +++ b/crates/tek/src/api/phrase.rs @@ -75,7 +75,7 @@ impl Command for PhrasePoolCommand { Some(Self::SetLength(index, old_len)) }, SetColor(index, color) => { - let mut color = ItemColorTriplet::from(color); + let mut color = ItemPalette::from(color); std::mem::swap(&mut color, &mut model.phrases()[index].write().unwrap().color); Some(Self::SetColor(index, color.base)) }, @@ -104,7 +104,7 @@ pub struct Phrase { /// All notes are displayed with minimum length pub percussive: bool, /// Identifying color of phrase - pub color: ItemColorTriplet, + pub color: ItemPalette, } /// MIDI message structural @@ -116,7 +116,7 @@ impl Phrase { loop_on: bool, length: usize, notes: Option, - color: Option, + color: Option, ) -> Self { Self { uuid: uuid::Uuid::new_v4(), @@ -128,7 +128,7 @@ impl Phrase { loop_start: 0, loop_length: length, percussive: true, - color: color.unwrap_or_else(ItemColorTriplet::random) + color: color.unwrap_or_else(ItemPalette::random) } } pub fn set_length (&mut self, length: usize) { diff --git a/crates/tek/src/core/color.rs b/crates/tek/src/core/color.rs index 9464e1f2..8c245a85 100644 --- a/crates/tek/src/core/color.rs +++ b/crates/tek/src/core/color.rs @@ -10,7 +10,7 @@ pub struct ItemColor { } /// A color in OKHSL and RGB with lighter and darker variants. #[derive(Debug, Default, Copy, Clone, PartialEq)] -pub struct ItemColorTriplet { +pub struct ItemPalette { pub base: ItemColor, pub light: ItemColor, pub dark: ItemColor, @@ -44,7 +44,7 @@ impl ItemColor { self.okhsl.mix(other.okhsl, distance).into() } } -impl From for ItemColorTriplet { +impl From for ItemPalette { fn from (base: ItemColor) -> Self { let mut light = base.okhsl.clone(); light.lightness = (light.lightness * 1.15).min(Okhsl::::max_lightness()); @@ -54,7 +54,7 @@ impl From for ItemColorTriplet { Self { base, light: light.into(), dark: dark.into() } } } -impl ItemColorTriplet { +impl ItemPalette { pub fn random () -> Self { ItemColor::random().into() } diff --git a/crates/tek/src/core/space.rs b/crates/tek/src/core/space.rs index a83f78c7..6942f437 100644 --- a/crates/tek/src/core/space.rs +++ b/crates/tek/src/core/space.rs @@ -110,7 +110,10 @@ pub trait Area: Copy { [self.x(), self.y(), a, self.h()], [self.x() + a, self.y(), self.w().minus(a), self.h()], ), - _ => todo!(), + Direction::Left => ( + [self.x() + self.w() - a, self.y(), a, self.h()], + [self.x(), self.y(), self.w() - a, self.h()], + ), } } } diff --git a/crates/tek/src/layout/measure.rs b/crates/tek/src/layout/measure.rs index aaffbc18..cc528435 100644 --- a/crates/tek/src/layout/measure.rs +++ b/crates/tek/src/layout/measure.rs @@ -1,7 +1,8 @@ use crate::*; /// A widget that tracks its render width and height -pub struct Measure(PhantomData, AtomicUsize, AtomicUsize); +#[derive(Default)] +pub struct Measure(PhantomData, AtomicUsize, AtomicUsize, bool); impl Clone for Measure { fn clone (&self) -> Self { @@ -9,6 +10,7 @@ impl Clone for Measure { Default::default(), AtomicUsize::from(self.1.load(Ordering::Relaxed)), AtomicUsize::from(self.2.load(Ordering::Relaxed)), + self.3 ) } } @@ -16,8 +18,8 @@ impl Clone for Measure { impl std::fmt::Debug for Measure { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Measure") - .field("width", &self.0) - .field("height", &self.1) + .field("width", &self.1) + .field("height", &self.2) .finish() } } @@ -29,18 +31,24 @@ impl Measure { pub fn set_w (&self, w: impl Into) { self.1.store(w.into(), Ordering::Relaxed) } pub fn set_h (&self, h: impl Into) { self.2.store(h.into(), Ordering::Relaxed) } pub fn set_wh (&self, w: impl Into, h: impl Into) { self.set_w(w); self.set_h(h); } - pub fn new () -> Self { Self(PhantomData::default(), 0.into(), 0.into()) } + pub fn new () -> Self { Self(PhantomData::default(), 0.into(), 0.into(), false) } + pub fn debug () -> Self { Self(PhantomData::default(), 0.into(), 0.into(), true) } pub fn format (&self) -> String { format!("{}x{}", self.w(), self.h()) } } -impl Render for Measure { - fn min_size (&self, _: E::Size) -> Perhaps { +impl Render for Measure { + fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0u16.into(), 0u16.into()].into())) } - fn render (&self, to: &mut E::Output) -> Usually<()> { - self.set_w(to.area().w()); - self.set_h(to.area().h()); - Ok(()) + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + let w = to.area().w(); + self.set_w(w); + let h = to.area().h(); + self.set_h(h); + Ok(if self.3 { + to.blit(&format!(" {w} x {h} "), to.area.x(), to.area.y(), Some( + Style::default().bold().italic().bg(Color::Rgb(255, 0, 255)).fg(Color::Rgb(0,0,0)) + )) + }) } } - diff --git a/crates/tek/src/layout/split.rs b/crates/tek/src/layout/split.rs index 5b78d405..6da13980 100644 --- a/crates/tek/src/layout/split.rs +++ b/crates/tek/src/layout/split.rs @@ -4,41 +4,52 @@ impl LayoutSplit for E {} pub trait LayoutSplit { fn split , B: Render> ( - direction: Direction, amount: E::Unit, a: A, b: B + flip: bool, direction: Direction, amount: E::Unit, a: A, b: B ) -> Split { - Split::new(direction, amount, a, b) + Split::new(flip, direction, amount, a, b) } fn split_up , B: Render> ( - amount: E::Unit, a: A, b: B + flip: bool, amount: E::Unit, a: A, b: B ) -> Split { - Split::new(Direction::Up, amount, a, b) + Self::split(flip, Direction::Up, amount, a, b) + } + fn split_down , B: Render> ( + flip: bool, amount: E::Unit, a: A, b: B + ) -> Split { + Self::split(flip, Direction::Down, amount, a, b) + } + fn split_left , B: Render> ( + flip: bool, amount: E::Unit, a: A, b: B + ) -> Split { + Self::split(flip, Direction::Left, amount, a, b) + } + fn split_right , B: Render> ( + flip: bool, amount: E::Unit, a: A, b: B + ) -> Split { + Self::split(flip, Direction::Right, amount, a, b) } - - //fn split_flip > ( - //self, direction: Direction, amount: E::Unit, other: W - //) -> Split { Split::new(direction, amount, other, self) } } /// A binary split with fixed proportion pub struct Split, B: Render>( - pub Direction, pub E::Unit, A, B, PhantomData + pub bool, pub Direction, pub E::Unit, A, B, PhantomData ); impl, B: Render> Split { - pub fn new (direction: Direction, proportion: E::Unit, a: A, b: B) -> Self { - Self(direction, proportion, a, b, Default::default()) + pub fn new (flip: bool, direction: Direction, proportion: E::Unit, a: A, b: B) -> Self { + Self(flip, direction, proportion, a, b, Default::default()) } - pub fn up (proportion: E::Unit, a: A, b: B) -> Self { - Self(Direction::Up, proportion, a, b, Default::default()) + pub fn up (flip: bool, proportion: E::Unit, a: A, b: B) -> Self { + Self(flip, Direction::Up, proportion, a, b, Default::default()) } - pub fn down (proportion: E::Unit, a: A, b: B) -> Self { - Self(Direction::Down, proportion, a, b, Default::default()) + pub fn down (flip: bool, proportion: E::Unit, a: A, b: B) -> Self { + Self(flip, Direction::Down, proportion, a, b, Default::default()) } - pub fn left (proportion: E::Unit, a: A, b: B) -> Self { - Self(Direction::Left, proportion, a, b, Default::default()) + pub fn left (flip: bool, proportion: E::Unit, a: A, b: B) -> Self { + Self(flip, Direction::Left, proportion, a, b, Default::default()) } - pub fn right (proportion: E::Unit, a: A, b: B) -> Self { - Self(Direction::Right, proportion, a, b, Default::default()) + pub fn right (flip: bool, proportion: E::Unit, a: A, b: B) -> Self { + Self(flip, Direction::Right, proportion, a, b, Default::default()) } } @@ -47,9 +58,13 @@ impl, B: Render> Render for Split { Ok(Some(to)) } fn render (&self, to: &mut E::Output) -> Usually<()> { - let (a, b) = to.area().split_fixed(self.0, self.1); - to.render_in(a.into(), &self.2)?; - to.render_in(b.into(), &self.3)?; - Ok(()) + let (a, b) = to.area().split_fixed(self.1, self.2); + Ok(if self.0 { + to.render_in(a.into(), &self.4)?; + to.render_in(b.into(), &self.3)?; + } else { + to.render_in(a.into(), &self.3)?; + to.render_in(b.into(), &self.4)?; + }) } } diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 2f7de25d..f3e1026e 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -147,6 +147,7 @@ render!(|self: ArrangerTui|{ )) ])), Split::right( + false, self.splits[1], PhraseListView::from(self), PhraseView::from(self), @@ -174,6 +175,18 @@ impl HasPhrases for ArrangerTui { } } +impl HasEditor for ArrangerTui { + fn editor (&self) -> &PhraseEditorModel { + &self.editor + } + fn editor_focused (&self) -> bool { + self.focused() == ArrangerFocus::PhraseEditor + } + fn editor_entered (&self) -> bool { + self.entered() && self.editor_focused() + } +} + /// Sections in the arranger app that may be focused #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum ArrangerFocus { @@ -1173,7 +1186,7 @@ impl ArrangerControl for ArrangerTui { }, ArrangerSelection::Clip(t, s) => { if let Some(phrase) = &self.scenes_mut()[s].clips[t] { - phrase.write().unwrap().color = ItemColorTriplet::random(); + phrase.write().unwrap().color = ItemPalette::random(); } } } diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 4f67ef2e..bfd1b849 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -41,7 +41,7 @@ impl TryFrom<&Arc>> for SequencerTui { midi_buf: vec![vec![];65536], note_buf: vec![], perf: PerfModel::default(), - focus: FocusState::Focused(SequencerFocus::PhraseEditor) + focus: FocusState::Entered(SequencerFocus::PhraseEditor) }) } @@ -107,9 +107,9 @@ impl Audio for SequencerTui { } } -render!(|self: SequencerTui|lay!([self.size, Tui::split_up(1, - Tui::fill_xy(Tui::at_s(SequencerStatusBar::from(self))), - Tui::split_up(2, +render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1, + Tui::fill_xy(SequencerStatusBar::from(self)), + Tui::split_down(false, 2, TransportView::from(( self, self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(), @@ -119,28 +119,22 @@ render!(|self: SequencerTui|lay!([self.size, Tui::split_up(1, false } )), - row!([ - Tui::fixed_x(20, Tui::split_up(4, col!([ - PhraseSelector::next_phrase( - &self.player, - self.focused() == SequencerFocus::PhraseNext, - self.entered() - ), + Tui::split_left(false, 20, + Tui::fixed_x(20, Tui::split_down(false, 4, col!([ PhraseSelector::play_phrase( - &self.player, - self.focused() == SequencerFocus::PhrasePlay, - self.entered() + &self.player, self.focused() == SequencerFocus::PhrasePlay, self.entered() ), - ]), col!([ + PhraseSelector::next_phrase( + &self.player, self.focused() == SequencerFocus::PhraseNext, self.entered() + ), + ]), Tui::split_up(false, 2, PhraseSelector::edit_phrase( - &self.editor.phrase, - self.focused() == SequencerFocus::PhraseEditor, - self.entered() + &self.editor.phrase, self.focused() == SequencerFocus::PhraseEditor, self.entered() ), PhraseListView::from(self), - ]))), + ))), PhraseView::from(self) - ]) + ) ) )])); @@ -159,6 +153,18 @@ impl HasPhrases for SequencerTui { } } +impl HasEditor for SequencerTui { + fn editor (&self) -> &PhraseEditorModel { + &self.editor + } + fn editor_focused (&self) -> bool { + self.focused() == SequencerFocus::PhraseEditor + } + fn editor_entered (&self) -> bool { + self.entered() && self.editor_focused() + } +} + /// Sections in the sequencer app that may be focused #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum SequencerFocus { @@ -176,7 +182,7 @@ pub enum SequencerFocus { impl From<&SequencerTui> for Option { fn from (state: &SequencerTui) -> Self { match state.focus.inner() { - SequencerFocus::Transport(focus) => Some(focus), + Transport(focus) => Some(focus), _ => None } } @@ -219,7 +225,7 @@ impl_focus!(SequencerTui SequencerFocus [ PhraseEditor, ], ] => [self: { - if self.focus.is_entered() && self.focus.inner() == SequencerFocus::PhraseEditor { + if self.focus.is_entered() && self.focus.inner() == PhraseEditor { self.editor.edit_mode = PhraseEditMode::Note } else { self.editor.edit_mode = PhraseEditMode::Scroll @@ -403,9 +409,9 @@ pub enum SequencerCommand { Phrases(PhrasesCommand), Editor(PhraseCommand), Enqueue(Option>>), - Clear, - Undo, - Redo, + //Clear, + //Undo, + //Redo, } impl Command for SequencerCommand { @@ -419,9 +425,9 @@ impl Command for SequencerCommand { state.player.enqueue_next(phrase.as_ref()); None }, - Self::Undo => { todo!() }, - Self::Redo => { todo!() }, - Self::Clear => { todo!() }, + //Self::Undo => { todo!() }, + //Self::Redo => { todo!() }, + //Self::Clear => { todo!() }, }) } } @@ -453,37 +459,34 @@ impl InputToCommand for SequencerCommand { } pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option { + let stopped = state.clock().is_stopped(); Some(match input.event() { // Play/pause - key!(Char(' ')) => Clock( - if state.clock().is_stopped() { Play(None) } else { Pause(None) } - ), + key!(Char(' ')) => Clock(if stopped { Play(None) } else { Pause(None) }), // Play from start/rewind to start - key!(Shift-Char(' ')) => Clock( - if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } - ), + key!(Shift-Char(' ')) => Clock(if stopped { Play(Some(0)) } else { Pause(Some(0)) }), // Edit phrase key!(Char('e')) => match state.focused() { - SequencerFocus::PhrasePlay => Editor(Show( + PhrasePlay => Editor(Show( state.player.play_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) )), - SequencerFocus::PhraseNext => Editor(Show( + PhraseNext => Editor(Show( state.player.next_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) )), - SequencerFocus::PhraseList => Editor(Show( + PhraseList => Editor(Show( Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()) )), _ => return None, }, _ => match state.focused() { - SequencerFocus::Transport(_) => match TransportCommand::input_to_command(state, input)? { + Transport(_) => match TransportCommand::input_to_command(state, input)? { TransportCommand::Clock(command) => Clock(command), _ => return None, }, - SequencerFocus::PhraseEditor => Editor( + PhraseEditor => Editor( PhraseCommand::input_to_command(&state.editor, input)? ), - SequencerFocus::PhraseList => match input.event() { + PhraseList => match input.event() { key!(Enter) => Enqueue(Some( state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() )), diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index 31b25284..d4ca3d59 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -75,13 +75,13 @@ pub struct TransportView { current_second: f64, } -impl From<(&T, Option, bool)> for TransportView { - fn from ((state, color, focused): (&T, Option, bool)) -> Self { +impl From<(&T, Option, bool)> for TransportView { + fn from ((state, color, focused): (&T, Option, bool)) -> Self { let clock = state.clock(); let sr = format!("{:.1}k", clock.timebase.sr.get() / 1000.0); let bpm = format!("{:.3}", clock.timebase.bpm.get()); let ppq = format!("{:.0}", clock.timebase.ppq.get()); - let color = color.unwrap_or(ItemColorTriplet::from(ItemColor::from(TuiTheme::g(100)))); + let color = color.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(100)))); let bg = if focused { color.light.rgb } else { color.dark.rgb }; if let Some(started) = clock.started.read().unwrap().as_ref() { let current_sample = (clock.global.sample.get() - started.sample.get())/1000.; @@ -114,7 +114,7 @@ impl From<(&T, Option, bool)> for TransportView { global_second: format!("{:.1}s", clock.global.usec.get()/1000000.), current_sample: 0.0, current_second: 0.0, - beat: format!("0.0.00") + beat: format!("000.0.00") } } } @@ -133,31 +133,39 @@ render!(|self: TransportView|{ 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!(move|add|{ - add(&Lozenge(border_style))?; - add(&Tui::outset_x(1, row!([ - TransportField("Beat", self.beat.as_str()), " ", - TransportField("BPM ", self.bpm.as_str()), " ", - ]))) - }))))?; - add(&Tui::fill_x(Tui::center_x(Tui::pull_x(2, row!([ - 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")])) - } - }), - ])))))?; - add(&Tui::fill_x(Tui::at_e(lay!(move|add|{ - add(&Lozenge(border_style))?; - add(&Tui::outset_x(1, row!([ - TransportField("Second", format!("{:.1}s", self.current_second).as_str()), " ", - TransportField("Rate ", self.sr.as_str()), " ", - TransportField("Sample", format!("{:.0}k", self.current_sample).as_str()), - ]))) - })))) + 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(()) })) }); diff --git a/crates/tek/src/tui/engine_theme.rs b/crates/tek/src/tui/engine_theme.rs index ccb66234..a07dc7fa 100644 --- a/crates/tek/src/tui/engine_theme.rs +++ b/crates/tek/src/tui/engine_theme.rs @@ -10,6 +10,9 @@ pub trait Theme { fn null () -> Color { Color::Reset } + fn bg0 () -> Color { + Color::Rgb(20, 20, 20) + } fn bg () -> Color { Color::Rgb(28, 35, 25) } diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 5fde1eec..88ddd669 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -1,13 +1,17 @@ use crate::*; +pub trait HasEditor { + fn editor (&self) -> &PhraseEditorModel; + fn editor_focused (&self) -> bool; + fn editor_entered (&self) -> bool; +} + /// Contains state for viewing and editing a phrase pub struct PhraseEditorModel { /// Phrase being played pub(crate) phrase: Option>>, /// Length of note that will be inserted, in pulses pub(crate) note_len: usize, - /// The full piano roll is rendered to this buffer - pub(crate) buffer: BigBuffer, /// Notes currently held at input pub(crate) notes_in: Arc>, /// Notes currently held at output @@ -24,25 +28,7 @@ pub struct PhraseEditorModel { pub(crate) time_point: AtomicUsize, pub(crate) edit_mode: PhraseEditMode, - pub(crate) view_mode: PhraseViewMode, -} - -#[derive(Copy, Clone, Debug)] -pub enum PhraseEditMode { - Note, - Scroll, -} - -#[derive(Copy, Clone, Debug)] -pub enum PhraseViewMode { - PianoHorizontal { - time_zoom: usize, - note_zoom: PhraseViewNoteZoom, - }, - PianoVertical { - time_zoom: usize, - note_zoom: PhraseViewNoteZoom, - }, + pub(crate) view_mode: Box, } impl std::fmt::Debug for PhraseEditorModel { @@ -65,7 +51,6 @@ impl Default for PhraseEditorModel { Self { phrase: None, note_len: 24, - buffer: Default::default(), notes_in: RwLock::new([false;128]).into(), notes_out: RwLock::new([false;128]).into(), now: Pulse::default().into(), @@ -75,10 +60,11 @@ impl Default for PhraseEditorModel { note_point: 0.into(), time_start: 0.into(), time_point: 0.into(), - view_mode: PhraseViewMode::PianoHorizontal { + view_mode: Box::new(PianoHorizontal { + buffer: Default::default(), time_zoom: 24, note_zoom: PhraseViewNoteZoom::N(1) - }, + }), } } } @@ -96,7 +82,7 @@ impl PhraseEditorModel { let end = (start + self.note_len) % phrase.length; phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); - self.buffer = self.view_mode.draw(&phrase); + self.view_mode.show(Some(&phrase), self.note_len); } } /// Move time cursor forward by current note length @@ -109,61 +95,43 @@ impl PhraseEditorModel { /// Select which pattern to display. This pre-renders it to the buffer at full resolution. pub fn show_phrase (&mut self, phrase: Option>>) { if phrase.is_some() { - self.buffer = self.view_mode.draw(&*phrase.as_ref().unwrap().read().unwrap()); self.phrase = phrase; + let phrase = &*self.phrase.as_ref().unwrap().read().unwrap(); + self.view_mode.show(Some(&phrase), self.note_len); } else { - self.buffer = Default::default(); + self.view_mode.show(None, self.note_len); self.phrase = None; } } } -pub trait HasEditor { - fn editor (&self) -> &PhraseEditorModel; - fn editor_focused (&self) -> bool; - fn editor_entered (&self) -> bool; -} - -impl HasEditor for SequencerTui { - fn editor (&self) -> &PhraseEditorModel { - &self.editor - } - fn editor_focused (&self) -> bool { - self.focused() == SequencerFocus::PhraseEditor - } - fn editor_entered (&self) -> bool { - self.entered() && self.editor_focused() - } -} - -impl HasEditor for ArrangerTui { - fn editor (&self) -> &PhraseEditorModel { - &self.editor - } - fn editor_focused (&self) -> bool { - self.focused() == ArrangerFocus::PhraseEditor - } - fn editor_entered (&self) -> bool { - self.entered() && self.editor_focused() - } -} - pub struct PhraseView<'a> { - focused: bool, - entered: bool, - phrase: &'a Option>>, - buffer: &'a BigBuffer, - note_len: usize, - now: &'a Arc, - size: &'a Measure, - view_mode: &'a PhraseViewMode, note_point: usize, note_range: (usize, usize), - note_names: (&'a str, &'a str), time_start: usize, time_point: usize, + note_len: usize, + phrase: &'a Option>>, + view_mode: &'a Box, + now: &'a Arc, + size: &'a Measure, + focused: bool, + entered: bool, } - +render!(|self: PhraseView<'a>|{ + let bg = if self.focused { TuiTheme::g(32) } else { Color::Reset }; + let fg = self.phrase.as_ref() + .map(|p|p.read().unwrap().color.clone()) + .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); + Tui::bg(bg, lay!([ + PhraseTimeline(&self, fg), + PhraseViewNotes(&self, fg), + PhraseViewCursor(&self), + PhraseViewKeys(&self, fg), + PhraseViewStats(&self, fg), + //Measure::debug(), + ])) +}); impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { fn from (state: &'a T) -> Self { let editor = state.editor(); @@ -181,88 +149,118 @@ impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { editor.note_lo.store(note_lo, Ordering::Relaxed); } Self { - focused: state.editor_focused(), - entered: state.editor_entered(), - note_len: editor.note_len, - phrase: &editor.phrase, - buffer: &editor.buffer, - now: &editor.now, - size: &editor.size, - view_mode: &editor.view_mode, - note_point, note_range: (note_lo, note_hi), - note_names: (to_note_name(note_lo), to_note_name(note_hi)), - time_start: editor.time_start.load(Ordering::Relaxed), time_point: editor.time_point.load(Ordering::Relaxed), + note_len: editor.note_len, + phrase: &editor.phrase, + view_mode: &editor.view_mode, + size: &editor.size, + now: &editor.now, + focused: state.editor_focused(), + entered: state.editor_entered(), } } } -render!(|self: PhraseView<'a>|{ - let Self { - focused, entered, size, - phrase, view_mode, buffer, - note_point, note_len, - note_range: (note_lo, note_hi), - time_start, time_point, - //now: _, - .. - } = self; - let phrase_color = phrase.as_ref().map(|p|p.read().unwrap().color.clone()) - .unwrap_or(ItemColorTriplet::from(ItemColor::from(TuiTheme::g(64)))); - let title_color = if *focused{phrase_color.light.rgb}else{phrase_color.dark.rgb}; - let bg = if self.focused { TuiTheme::bg() } else { Color::Reset }; +pub struct PhraseTimeline<'a>(&'a PhraseView<'a>, ItemPalette); +render!(|self: PhraseTimeline<'a>|Tui::fg(TuiTheme::g(224), Tui::push_x(5, format!("|000.00.00")))); + +pub struct PhraseViewStats<'a>(&'a PhraseView<'a>, ItemPalette); +render!(|self: PhraseViewStats<'a>|{ + let title_color = if self.0.focused{self.1.light.rgb}else{self.1.dark.rgb}; lay!([ - Tui::bg(bg, Tui::inset_x(1, Tui::fill_x(row!([ - Tui::push_y(1, Tui::fill_y(Widget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{ - Ok(if to.area().h() >= 2 { view_mode.render_keys(to, *note_hi, *note_lo) }) - }))), - Tui::fill_x(lay!([ - Tui::push_y(1, Tui::fill_x(Widget::new(|to|Ok(Some(to)), |to: &mut TuiOutput|{ - size.set_wh(to.area.w(), to.area.h() as usize - 1); - let draw = to.area().h() >= 2; - Ok(if draw { view_mode.render_notes(to, buffer, *time_start, *note_hi) }) - }))), - Tui::push_y(1, Widget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ - Ok(if *focused && *entered { - view_mode.render_cursor( - to, - *time_point, *time_start, view_mode.time_zoom(), - *note_point, *note_len, *note_hi, *note_lo, - ) - }) - })) - ])), - ])))), - lay!(move|add|{ - add(&Lozenge(Style::default().bg(phrase_color.base.rgb).fg(phrase_color.base.rgb)))?; - let upper_left = format!("{}", - phrase.as_ref().map(|p|p.read().unwrap().name.clone()).unwrap_or(String::new()) - ); - let lower_left = format!(""); - let mut lower_right = format!(" {} ", size.format()); - if *focused && *entered { - lower_right = format!("Note: {} ({}) {} {lower_right}", - note_point, to_note_name(*note_point), pulses_to_name(*note_len) + Tui::at_sw({ + let mut lower_right = format!(" {} ", self.0.size.format()); + if self.0.focused && self.0.entered { + lower_right = format!( + "Note: {} ({}) {} {lower_right}", + self.0.note_point, + to_note_name(self.0.note_point), + pulses_to_name(self.0.note_len), ); } - let mut upper_right = format!("[{}]", if *entered {"■"} else {" "}); - if let Some(phrase) = phrase { - upper_right = format!("Time: {}/{} {} {upper_right}", - time_point, phrase.read().unwrap().length, pulses_to_name(view_mode.time_zoom()), + Tui::bg(title_color, Tui::fg(TuiTheme::g(224), lower_right)) + }), + Tui::fill_xy(Tui::at_se({ + let mut upper_right = format!("[{}]", if self.0.entered {"■"} else {" "}); + if let Some(phrase) = self.0.phrase { + upper_right = format!( + "Time: {}/{} {} {upper_right}", + self.0.time_point, + phrase.read().unwrap().length, + pulses_to_name(self.0.view_mode.time_zoom()), ) }; - add(&Tui::push_x(1, Tui::at_nw(Tui::fg(TuiTheme::g(224), Tui::bg(title_color, upper_left)))))?; - add(&Tui::at_sw(Tui::bg(title_color, Tui::fg(TuiTheme::g(224), lower_left))))?; - add(&Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::bg(title_color, Tui::fg(TuiTheme::g(224), upper_right))))))?; - add(&Tui::fill_xy(Tui::at_se(Tui::pull_x(1, Tui::bg(title_color, Tui::fg(TuiTheme::g(224), lower_right))))))?; - Ok(()) - }), + Tui::pull_x(1, Tui::bg(title_color, Tui::fg(TuiTheme::g(224), upper_right))) + })), ]) }); +struct PhraseViewKeys<'a>(&'a PhraseView<'a>, ItemPalette); +render!(|self: PhraseViewKeys<'a>|{ + let layout = |to:[u16;2]|Ok(Some(to.clip_w(5))); + Tui::fill_xy(Widget::new(layout, |to: &mut TuiOutput|Ok( + self.0.view_mode.render_keys(to, self.1.dark.rgb, Some(self.0.note_point), self.0.note_range) + ))) +}); + +struct PhraseViewNotes<'a>(&'a PhraseView<'a>, ItemPalette); +render!(|self: PhraseViewNotes<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|{ + self.0.size.set_wh(to.area.w(), to.area.h() as usize); + Ok(self.0.view_mode.render_notes(to, self.0.time_start, self.0.note_range.1)) +}))); + +struct PhraseViewCursor<'a>(&'a PhraseView<'a>); +render!(|self: PhraseViewCursor<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|Ok( + self.0.view_mode.render_cursor( + to, + self.0.time_point, + self.0.time_start, + self.0.view_mode.time_zoom(), + self.0.note_point, + self.0.note_len, + self.0.note_range.1, + self.0.note_range.0, + ) +)))); + +#[derive(Copy, Clone, Debug)] +pub enum PhraseEditMode { + Note, + Scroll, +} + +pub trait PhraseViewMode { + fn show (&mut self, phrase: Option<&Phrase>, note_len: usize); + fn time_zoom (&self) -> usize; + fn set_time_zoom (&mut self, time_zoom: usize); + fn buffer_width (&self, phrase: &Phrase) -> usize; + fn buffer_height (&self, phrase: &Phrase) -> usize; + fn render_keys (&self, + to: &mut TuiOutput, color: Color, point: Option, range: (usize, usize)); + fn render_notes (&self, + to: &mut TuiOutput, time_start: usize, note_hi: usize); + fn render_cursor ( + &self, + to: &mut TuiOutput, + time_point: usize, + time_start: usize, + time_zoom: usize, + note_point: usize, + note_len: usize, + note_hi: usize, + note_lo: usize, + ); +} + +pub struct PianoHorizontal { + time_zoom: usize, + note_zoom: PhraseViewNoteZoom, + buffer: BigBuffer, +} + #[derive(Copy, Clone, Debug)] pub enum PhraseViewNoteZoom { N(usize), @@ -270,97 +268,100 @@ pub enum PhraseViewNoteZoom { Octant, } -impl PhraseViewMode { - pub fn time_zoom (&self) -> usize { - match self { - Self::PianoHorizontal { time_zoom, .. } => *time_zoom, +impl PhraseViewMode for PianoHorizontal { + fn time_zoom (&self) -> usize { + self.time_zoom + } + fn set_time_zoom (&mut self, time_zoom: usize) { + self.time_zoom = time_zoom + } + fn show (&mut self, phrase: Option<&Phrase>, note_len: usize) { + if let Some(phrase) = phrase { + self.buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase)); + draw_piano_horizontal(&mut self.buffer, phrase, self.time_zoom, note_len); + } else { + self.buffer = Default::default(); + } + } + fn buffer_width (&self, phrase: &Phrase) -> usize { + phrase.length / self.time_zoom + } + /// Determine the required height to render the phrase. + fn buffer_height (&self, phrase: &Phrase) -> usize { + match self.note_zoom { + PhraseViewNoteZoom::Half => 64, + PhraseViewNoteZoom::N(n) => 128*n, _ => unimplemented!() } } - pub fn set_time_zoom (&mut self, time_zoom: usize) { - *self = match self { - Self::PianoHorizontal { note_zoom, .. } => Self::PianoHorizontal { - note_zoom: *note_zoom, - time_zoom, - }, - _ => unimplemented!() - } - } - /// Return a new [BigBuffer] containing a render of the phrase. - pub fn draw (&self, phrase: &Phrase) -> BigBuffer { - let mut buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase)); - match self { - Self::PianoHorizontal { time_zoom, note_zoom } => match note_zoom { - PhraseViewNoteZoom::N(_) => Self::draw_piano_horizontal( - &mut buffer, phrase, *time_zoom - ), - _ => unimplemented!(), - }, - _ => unimplemented!(), - } - buffer - } - /// Draw a subsection of the [BigBuffer] onto a regular ratatui [Buffer]. fn render_notes ( &self, - target: &mut TuiOutput, - source: &BigBuffer, + target: &mut TuiOutput, time_start: usize, note_hi: usize, ) { - let area = target.area(); + let [x0, y0, w, h] = target.area().xywh(); + let source = &self.buffer; let target = &mut target.buffer; - match self { - Self::PianoHorizontal { .. } => { - let [x0, y0, w, h] = area.xywh(); - for (x, target_x) in (x0..x0+w).enumerate() { - for (y, target_y) in (y0..y0+h).enumerate() { - if y > note_hi { - break - } - let source_x = time_start + x; - let source_y = note_hi - y; - // TODO: enable loop rollover: - //let source_x = (time_start + x) % source.width.max(1); - //let source_y = (note_hi - y) % source.height.max(1); - if source_x < source.width && source_y < source.height { - let target_cell = target.get_mut(target_x, target_y); - if let Some(source_cell) = source.get(source_x, source_y) { - *target_cell = source_cell.clone(); - } - } + for (x, target_x) in (x0..x0+w).enumerate() { + + for (y, target_y) in (y0..y0+h).enumerate() { + + if y > note_hi { + break + } + + let source_x = time_start + x; + let source_y = note_hi - y; + + // TODO: enable loop rollover: + //let source_x = (time_start + x) % source.width.max(1); + //let source_y = (note_hi - y) % source.height.max(1); + if source_x < source.width && source_y < source.height { + let target_cell = target.get_mut(target_x, target_y); + if let Some(source_cell) = source.get(source_x, source_y) { + *target_cell = source_cell.clone(); } } - }, - _ => unimplemented!() + + } + } } - fn render_keys (&self, to: &mut TuiOutput, note_hi: usize, note_lo: usize) { - let style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0))); - match self { - Self::PianoHorizontal { .. } => { - let [x0, y0, _, _] = to.area().xywh(); - for (y, note) in (note_lo..=note_hi).rev().enumerate() { - let key = match note % 12 { - 11 => "█████", - 10 => " ", - 9 => "█████", - 8 => " ", - 7 => "█████", - 6 => " ", - 5 => "█████", - 4 => "█████", - 3 => " ", - 2 => "█████", - 1 => " ", - 0 => "█████", - _ => unreachable!(), - }; - to.blit(&key, x0, y0 + y as u16, style); - to.blit(&format!("{}", to_note_name(note)), x0, y0 + y as u16, None); - } - }, - _ => unimplemented!() + fn render_keys ( + &self, + to: &mut TuiOutput, + color: Color, + point: Option, + (note_lo, note_hi): (usize, usize) + ) { + let [x, y0, _, _] = to.area().xywh(); + let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0))); + let note_off_style = Some(Style::default().fg(TuiTheme::g(160))); + let note_on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color).bold()); + for (y, note) in (note_lo..=note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) { + let key = match note % 12 { + 11 => "████▌", + 10 => " ", + 9 => "████▌", + 8 => " ", + 7 => "████▌", + 6 => " ", + 5 => "████▌", + 4 => "████▌", + 3 => " ", + 2 => "████▌", + 1 => " ", + 0 => "████▌", + _ => unreachable!(), + }; + to.blit(&key, x, y, key_style); + + if Some(note) == point { + to.blit(&format!("{:<5}", to_note_name(note)), x, y, note_on_style) + } else { + to.blit(&to_note_name(note), x, y, note_off_style) + }; } } fn render_cursor ( @@ -374,121 +375,147 @@ impl PhraseViewMode { note_hi: usize, note_lo: usize, ) { + let [x0, y0, w, _] = to.area().xywh(); let style = Some(Style::default().fg(Color::Rgb(0,255,0))); - match self { - Self::PianoHorizontal { .. } => { - let [x0, y0, w, _] = to.area().xywh(); - for (y, note) in (note_lo..=note_hi).rev().enumerate() { - if note == note_point { - for x in 0..w { - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_point && time_point < time_2 { - to.blit(&"█", x0 + x as u16, y0 + y as u16, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (x0 + x + 1)..(x0 + x + tail) { - to.blit(&"▄", x_tail, y0 + y as u16, style); - } - break - } + for (y, note) in (note_lo..=note_hi).rev().enumerate() { + if note == note_point { + for x in 0..w { + let time_1 = time_start + x as usize * time_zoom; + let time_2 = time_1 + time_zoom; + if time_1 <= time_point && time_point < time_2 { + to.blit(&"█", x0 + x as u16, y0 + y as u16, style); + let tail = note_len as u16 / time_zoom as u16; + for x_tail in (x0 + x + 1)..(x0 + x + tail) { + to.blit(&"▄", x_tail, y0 + y as u16, style); } break } } - }, - _ => unimplemented!() + break + } } } - /// Determine the required width to render the phrase. - fn buffer_width (&self, phrase: &Phrase) -> usize { - match self { - Self::PianoHorizontal { time_zoom, .. } => { - phrase.length / time_zoom - }, - Self::PianoVertical { note_zoom, .. } => match note_zoom { - PhraseViewNoteZoom::Half => 64, - PhraseViewNoteZoom::N(n) => 128*n, - _ => unimplemented!() - }, +} + +fn draw_piano_horizontal_bg ( + target: &mut BigBuffer, + phrase: &Phrase, + time_zoom: usize, + note_len: usize, +) { + for (y, note) in (0..127).rev().enumerate() { + for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) { + let cell = target.get_mut(x, y).unwrap(); + //cell.set_fg(Color::Rgb(48, 55, 45)); + cell.set_fg(phrase.color.dark.rgb); + cell.set_char(if time % 384 == 0 { + '│' + } else if time % 96 == 0 { + '╎' + } else if time % note_len == 0 { + '┊' + } else if (127 - note) % 12 == 1 { + '=' + } else { + '·' + }); } } - /// Determine the required height to render the phrase. - fn buffer_height (&self, phrase: &Phrase) -> usize { - match self { - Self::PianoHorizontal { note_zoom, .. } => match note_zoom { - PhraseViewNoteZoom::Half => 64, - PhraseViewNoteZoom::N(n) => 128*n, - _ => unimplemented!() - }, - Self::PianoVertical { time_zoom, .. } => { - phrase.length / time_zoom - }, - } - } - /// Draw the piano roll using full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_piano_horizontal ( - target: &mut BigBuffer, - phrase: &Phrase, - time_zoom: usize, - ) { - let color = phrase.color.light.rgb; - let style = Style::default().fg(color);//.bg(Color::Rgb(0, 0, 0)); +} + +fn draw_piano_horizontal_fg ( + target: &mut BigBuffer, + phrase: &Phrase, + time_zoom: usize, +) { + let style = Style::default().fg(phrase.color.light.rgb);//.bg(Color::Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() { + for (y, note) in (0..127).rev().enumerate() { - for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) { - let cell = target.get_mut(x, y).unwrap(); - //cell.set_fg(Color::Rgb(48, 55, 45)); - cell.set_fg(phrase.color.dark.rgb); - cell.set_char(if time % 384 == 0 { - '│' - } else if time % 96 == 0 { - '╎' - } else if note % 12 == 0 { - '=' - } else { - '·' - }); + let cell = target.get_mut(x, note).unwrap(); + if notes_on[note] { + cell.set_char('▄'); + cell.set_style(style); } } - let mut notes_on = [false;128]; - for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() { - let time_end = time_start + time_zoom; - for (y, note) in (0..127).rev().enumerate() { - let cell = target.get_mut(x, note).unwrap(); - if notes_on[note] { - cell.set_char('▄'); - cell.set_style(style); - } - } - for time in time_start..time_end { - for event in phrase.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - let cell = target.get_mut(x, note).unwrap(); - cell.set_char('█'); - cell.set_style(style); - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } + + let time_end = time_start + time_zoom; + for time in time_start..time_end { + for event in phrase.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + let cell = target.get_mut(x, note).unwrap(); + cell.set_char('█'); + cell.set_style(style); + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} } } } + } - /// TODO: Draw the piano roll using octant blocks (U+1CD00-U+1CDE5) - fn draw_piano_horizontal_octant ( - _: &mut BigBuffer, _: &Phrase, _: usize - ) { - unimplemented!() +} + +/// Draw the piano roll using full blocks on note on and half blocks on legato: █▄ █▄ █▄ +fn draw_piano_horizontal ( + target: &mut BigBuffer, + phrase: &Phrase, + time_zoom: usize, + note_len: usize, +) { + let color = phrase.color.light.rgb; + let style = Style::default().fg(color);//.bg(Color::Rgb(0, 0, 0)); + for (y, note) in (0..127).rev().enumerate() { + for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) { + let cell = target.get_mut(x, y).unwrap(); + //cell.set_fg(Color::Rgb(48, 55, 45)); + cell.set_fg(phrase.color.dark.rgb); + cell.set_char(if time % 384 == 0 { + '│' + } else if time % 96 == 0 { + '╎' + } else if time % note_len == 0 { + '┊' + } else if (127 - note) % 12 == 1 { + '=' + } else { + '·' + }); + } } - /// TODO: Draw the piano roll using half blocks: ▄▀▄ - fn draw_piano_horizontal_half ( - _: &mut BigBuffer, _: &Phrase, _: usize - ) { - unimplemented!() + let mut notes_on = [false;128]; + for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() { + let time_end = time_start + time_zoom; + for (y, note) in (0..127).rev().enumerate() { + let cell = target.get_mut(x, note).unwrap(); + if notes_on[note] { + cell.set_char('▄'); + cell.set_style(style); + } + } + for time in time_start..time_end { + for event in phrase.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + let cell = target.get_mut(x, note).unwrap(); + cell.set_char('█'); + cell.set_style(style); + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } } } diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index d11d202b..d0c2f8b7 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -122,12 +122,13 @@ 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 upper_left = format!("{title}"); let upper_right = format!("({})", phrases.len()); - Tui::bg(border_bg, lay!(move|add|{ + 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)) => { @@ -289,14 +290,14 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option Cmd::Phrase(Pool::Add(count, Phrase::new( - String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) + String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) ))), key!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new( - String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) + String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) ))), key!(Char('d')) => { let mut phrase = state.phrases()[index].read().unwrap().duplicate(); - phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); + phrase.color = ItemPalette::random_near(phrase.color, 0.25); Cmd::Phrase(Pool::Add(index + 1, phrase)) }, _ => return None From 32e547194a74d7ecf16c427ddadccff5f2da8597 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 11 Dec 2024 19:29:11 +0100 Subject: [PATCH 008/971] more color degrees --- crates/tek/src/core/color.rs | 43 +++++++++++++---- crates/tek/src/tui/phrase_editor.rs | 75 ++++++----------------------- 2 files changed, 49 insertions(+), 69 deletions(-) diff --git a/crates/tek/src/core/color.rs b/crates/tek/src/core/color.rs index 8c245a85..a86ac49f 100644 --- a/crates/tek/src/core/color.rs +++ b/crates/tek/src/core/color.rs @@ -11,9 +11,13 @@ pub struct ItemColor { /// A color in OKHSL and RGB with lighter and darker variants. #[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemPalette { - pub base: ItemColor, - pub light: ItemColor, - pub dark: ItemColor, + pub base: ItemColor, + pub light: ItemColor, + pub lighter: ItemColor, + pub lightest: ItemColor, + pub dark: ItemColor, + pub darker: ItemColor, + pub darkest: ItemColor, } /// Adds TUI RGB representation to an OKHSL value. impl From> for ItemColor { @@ -45,13 +49,34 @@ impl ItemColor { } } impl From for ItemPalette { + fn from (base: ItemColor) -> Self { - let mut light = base.okhsl.clone(); - light.lightness = (light.lightness * 1.15).min(Okhsl::::max_lightness()); - let mut dark = base.okhsl.clone(); - dark.lightness = (dark.lightness * 0.85).max(Okhsl::::min_lightness()); - dark.saturation = (dark.saturation * 0.85).max(Okhsl::::min_saturation()); - Self { base, light: light.into(), dark: dark.into() } + let mut light = base.okhsl.clone(); + light.lightness = (light.lightness * 1.2).min(Okhsl::::max_lightness()); + let mut lighter = light.clone(); + lighter.lightness = (lighter.lightness * 1.2).min(Okhsl::::max_lightness()); + let mut lightest = lighter.clone(); + lightest.lightness = (lightest.lightness * 1.2).min(Okhsl::::max_lightness()); + + let mut dark = base.okhsl.clone(); + dark.lightness = (dark.lightness * 0.75).max(Okhsl::::min_lightness()); + dark.saturation = (dark.saturation * 0.75).max(Okhsl::::min_saturation()); + let mut darker = dark.clone(); + darker.lightness = (darker.lightness * 0.66).max(Okhsl::::min_lightness()); + darker.saturation = (darker.saturation * 0.66).max(Okhsl::::min_saturation()); + let mut darkest = darker.clone(); + darkest.lightness = (darkest.lightness * 0.50).max(Okhsl::::min_lightness()); + darkest.saturation = (darkest.saturation * 0.50).max(Okhsl::::min_saturation()); + + Self { + base, + light: light.into(), + lighter: lighter.into(), + lightest: lightest.into(), + dark: dark.into(), + darker: darker.into(), + darkest: darkest.into(), + } } } impl ItemPalette { diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 88ddd669..669a43f1 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -202,7 +202,7 @@ struct PhraseViewKeys<'a>(&'a PhraseView<'a>, ItemPalette); render!(|self: PhraseViewKeys<'a>|{ let layout = |to:[u16;2]|Ok(Some(to.clip_w(5))); Tui::fill_xy(Widget::new(layout, |to: &mut TuiOutput|Ok( - self.0.view_mode.render_keys(to, self.1.dark.rgb, Some(self.0.note_point), self.0.note_range) + self.0.view_mode.render_keys(to, self.1.light.rgb, Some(self.0.note_point), self.0.note_range) ))) }); @@ -397,6 +397,17 @@ impl PhraseViewMode for PianoHorizontal { } } +/// Draw the piano roll using full blocks on note on and half blocks on legato: █▄ █▄ █▄ +fn draw_piano_horizontal ( + target: &mut BigBuffer, + phrase: &Phrase, + time_zoom: usize, + note_len: usize, +) { + draw_piano_horizontal_bg(target, phrase, time_zoom, note_len); + draw_piano_horizontal_fg(target, phrase, time_zoom); +} + fn draw_piano_horizontal_bg ( target: &mut BigBuffer, phrase: &Phrase, @@ -407,7 +418,8 @@ fn draw_piano_horizontal_bg ( for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) { let cell = target.get_mut(x, y).unwrap(); //cell.set_fg(Color::Rgb(48, 55, 45)); - cell.set_fg(phrase.color.dark.rgb); + cell.set_bg(phrase.color.darkest.rgb); + cell.set_fg(phrase.color.darker.rgb); cell.set_char(if time % 384 == 0 { '│' } else if time % 96 == 0 { @@ -428,7 +440,7 @@ fn draw_piano_horizontal_fg ( phrase: &Phrase, time_zoom: usize, ) { - let style = Style::default().fg(phrase.color.light.rgb);//.bg(Color::Rgb(0, 0, 0)); + let style = Style::default().fg(phrase.color.lightest.rgb);//.bg(Color::Rgb(0, 0, 0)); let mut notes_on = [false;128]; for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() { @@ -462,63 +474,6 @@ fn draw_piano_horizontal_fg ( } } -/// Draw the piano roll using full blocks on note on and half blocks on legato: █▄ █▄ █▄ -fn draw_piano_horizontal ( - target: &mut BigBuffer, - phrase: &Phrase, - time_zoom: usize, - note_len: usize, -) { - let color = phrase.color.light.rgb; - let style = Style::default().fg(color);//.bg(Color::Rgb(0, 0, 0)); - for (y, note) in (0..127).rev().enumerate() { - for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) { - let cell = target.get_mut(x, y).unwrap(); - //cell.set_fg(Color::Rgb(48, 55, 45)); - cell.set_fg(phrase.color.dark.rgb); - cell.set_char(if time % 384 == 0 { - '│' - } else if time % 96 == 0 { - '╎' - } else if time % note_len == 0 { - '┊' - } else if (127 - note) % 12 == 1 { - '=' - } else { - '·' - }); - } - } - let mut notes_on = [false;128]; - for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() { - let time_end = time_start + time_zoom; - for (y, note) in (0..127).rev().enumerate() { - let cell = target.get_mut(x, note).unwrap(); - if notes_on[note] { - cell.set_char('▄'); - cell.set_style(style); - } - } - for time in time_start..time_end { - for event in phrase.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - let cell = target.get_mut(x, note).unwrap(); - cell.set_char('█'); - cell.set_style(style); - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - } -} - #[derive(Clone, Debug)] pub enum PhraseCommand { // TODO: 1-9 seek markers that by default start every 8th of the phrase From be924d447e0a137777ad9d5a5662cf69f6accd00 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 11 Dec 2024 20:49:13 +0100 Subject: [PATCH 009/971] disable advanced focus in tek_sequencer --- crates/tek/src/core/focus.rs | 92 +++++---- crates/tek/src/core/test.rs | 49 +++++ crates/tek/src/tui.rs | 1 - crates/tek/src/tui/app_arranger.rs | 26 ++- crates/tek/src/tui/app_sequencer.rs | 283 ++++++++++++---------------- crates/tek/src/tui/app_transport.rs | 86 +++++---- crates/tek/src/tui/engine_focus.rs | 66 ------- crates/tek/src/tui/phrase_editor.rs | 47 +++-- crates/tek/src/tui/phrase_list.rs | 2 +- 9 files changed, 311 insertions(+), 341 deletions(-) create mode 100644 crates/tek/src/core/test.rs delete mode 100644 crates/tek/src/tui/engine_focus.rs diff --git a/crates/tek/src/core/focus.rs b/crates/tek/src/core/focus.rs index b153cfde..fe16eeae 100644 --- a/crates/tek/src/core/focus.rs +++ b/crates/tek/src/core/focus.rs @@ -34,7 +34,7 @@ impl FocusState { } #[derive(Copy, Clone, PartialEq, Debug)] -pub enum FocusCommand { +pub enum FocusCommand { Up, Down, Left, @@ -43,10 +43,11 @@ pub enum FocusCommand { Prev, Enter, Exit, + Set(T) } -impl Command for FocusCommand { - fn execute (self, state: &mut F) -> Perhaps { +impl Command for FocusCommand { + fn execute (self, state: &mut F) -> Perhaps> { use FocusCommand::*; match self { Next => { state.focus_next(); }, @@ -57,6 +58,7 @@ impl Command for FocusComman Right => { state.focus_right(); }, Enter => { state.focus_enter(); }, Exit => { state.focus_exit(); }, + Set(to) => { state.set_focused(to); }, } Ok(None) } @@ -64,7 +66,7 @@ impl Command for FocusComman /// Trait for things that have focusable subparts. pub trait HasFocus { - type Item: Copy + PartialEq + Debug; + type Item: Copy + PartialEq + Debug + Send + Sync; /// Get the currently focused item. fn focused (&self) -> Self::Item; /// Get the currently focused item. @@ -249,55 +251,67 @@ impl FocusOrder for T { } } -#[cfg(test)] -mod test { - use super::*; +pub trait FocusWrap { + fn wrap <'a, W: Render> (self, focus: T, content: &'a W) + -> impl Render + 'a; +} - #[test] - fn test_focus () { +pub fn to_focus_command (input: &TuiInput) -> Option> { + use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc}; + Some(match input.event() { + key!(Tab) => FocusCommand::Next, + key!(Shift-Tab) => FocusCommand::Prev, + key!(BackTab) => FocusCommand::Prev, + key!(Shift-BackTab) => FocusCommand::Prev, + key!(Up) => FocusCommand::Up, + key!(Down) => FocusCommand::Down, + key!(Left) => FocusCommand::Left, + key!(Right) => FocusCommand::Right, + key!(Enter) => FocusCommand::Enter, + key!(Esc) => FocusCommand::Exit, + _ => return None + }) +} - struct FocusTest { - focused: char, - cursor: (usize, usize) - } - - impl HasFocus for FocusTest { - type Item = char; +#[macro_export] macro_rules! impl_focus { + ($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => { + impl HasFocus for $Struct { + type Item = $Focus; + /// Get the currently focused item. fn focused (&self) -> Self::Item { - self.focused + self.focus.inner() } + /// Get the currently focused item. fn set_focused (&mut self, to: Self::Item) { - self.focused = to + self.focus.set_inner(to) + } + $(fn focus_updated (&mut $self) { $update_focus })? + } + impl HasEnter for $Struct { + /// Get the currently focused item. + fn entered (&self) -> bool { + self.focus.is_entered() + } + /// Get the currently focused item. + fn set_entered (&mut self, entered: bool) { + if entered { + self.focus.to_entered() + } else { + self.focus.to_focused() + } } } - - impl FocusGrid for FocusTest { + impl FocusGrid for $Struct { 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]] { - &[ - &['a', 'a', 'a', 'b', 'b', 'd'], - &['a', 'a', 'a', 'b', 'b', 'd'], - &['a', 'a', 'a', 'c', 'c', 'd'], - &['a', 'a', 'a', 'c', 'c', 'd'], - &['e', 'e', 'e', 'e', 'e', 'e'], - ] + fn focus_layout (&self) -> &[&[$Focus]] { + use $Focus::*; + &$Grid } } - - let mut tester = FocusTest { focused: 'a', cursor: (0, 0) }; - - tester.focus_right(); - assert_eq!(tester.cursor.0, 3); - assert_eq!(tester.focused, 'b'); - - tester.focus_down(); - assert_eq!(tester.cursor.1, 2); - assert_eq!(tester.focused, 'c'); - } } diff --git a/crates/tek/src/core/test.rs b/crates/tek/src/core/test.rs new file mode 100644 index 00000000..88699914 --- /dev/null +++ b/crates/tek/src/core/test.rs @@ -0,0 +1,49 @@ +#[cfg(test)] mod test_focus { + use super::focus::*; + #[test] fn test_focus () { + + struct FocusTest { + focused: char, + cursor: (usize, usize) + } + + impl HasFocus for FocusTest { + type Item = char; + fn focused (&self) -> Self::Item { + self.focused + } + fn set_focused (&mut self, to: Self::Item) { + self.focused = to + } + } + + impl FocusGrid for FocusTest { + 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]] { + &[ + &['a', 'a', 'a', 'b', 'b', 'd'], + &['a', 'a', 'a', 'b', 'b', 'd'], + &['a', 'a', 'a', 'c', 'c', 'd'], + &['a', 'a', 'a', 'c', 'c', 'd'], + &['e', 'e', 'e', 'e', 'e', 'e'], + ] + } + } + + let mut tester = FocusTest { focused: 'a', cursor: (0, 0) }; + + tester.focus_right(); + assert_eq!(tester.cursor.0, 3); + assert_eq!(tester.focused, 'b'); + + tester.focus_down(); + assert_eq!(tester.cursor.1, 2); + assert_eq!(tester.focused, 'c'); + + } +} diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 90c10630..6011ae72 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -1,6 +1,5 @@ use crate::*; -mod engine_focus; pub(crate) use engine_focus::*; mod engine_input; pub(crate) use engine_input::*; mod engine_style; pub(crate) use engine_style::*; mod engine_theme; pub(crate) use engine_theme::*; diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index f3e1026e..e94a5716 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -7,7 +7,6 @@ use crate::{ } }; - impl TryFrom<&Arc>> for ArrangerTui { type Error = Box; fn try_from (jack: &Arc>) -> Usually { @@ -200,6 +199,16 @@ pub enum ArrangerFocus { PhraseEditor, } +impl Into> for ArrangerFocus { + fn into (self) -> Option { + if let Self::Transport(transport) = self { + Some(transport) + } else { + None + } + } +} + impl From<&ArrangerTui> for Option { fn from (state: &ArrangerTui) -> Self { match state.focus.inner() { @@ -1075,7 +1084,7 @@ impl Handle for ArrangerTui { #[derive(Clone, Debug)] pub enum ArrangerCommand { - Focus(FocusCommand), + Focus(FocusCommand), Undo, Redo, Clear, @@ -1132,7 +1141,7 @@ impl Command for ArrangerClipCommand { } } -pub trait ArrangerControl: TransportControl { +pub trait ArrangerControl: TransportControl { fn selected (&self) -> ArrangerSelection; fn selected_mut (&mut self) -> &mut ArrangerSelection; fn activate (&mut self) -> Usually<()>; @@ -1212,7 +1221,7 @@ fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option match state.focused() { ArrangerFocus::Transport(_) => { - match TransportCommand::input_to_command(state, input)? { + match to_transport_command(state, input)? { TransportCommand::Clock(command) => Cmd::Clock(command), _ => return None, } @@ -1323,3 +1332,12 @@ fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option return None }) } + +impl TransportControl for ArrangerTui { + fn transport_focused (&self) -> Option { + match self.focus.inner() { + ArrangerFocus::Transport(focus) => Some(focus), + _ => None + } + } +} diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index bfd1b849..beece12f 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -1,9 +1,8 @@ use crate::{*, api::ClockCommand::{Play, Pause}}; -use super::phrase_editor::PhraseCommand::Show; -use super::app_transport::TransportCommand; -use KeyCode::{Char, Enter}; +use KeyCode::{Tab, BackTab, Char, Enter}; use SequencerCommand::*; use SequencerFocus::*; +use PhraseCommand::*; /// Create app state from JACK handle. impl TryFrom<&Arc>> for SequencerTui { @@ -41,7 +40,7 @@ impl TryFrom<&Arc>> for SequencerTui { midi_buf: vec![vec![];65536], note_buf: vec![], perf: PerfModel::default(), - focus: FocusState::Entered(SequencerFocus::PhraseEditor) + focus: SequencerFocus::PhraseEditor }) } @@ -60,7 +59,7 @@ pub struct SequencerTui { pub entered: bool, pub note_buf: Vec, pub midi_buf: Vec>>, - pub focus: FocusState, + pub focus: SequencerFocus, pub perf: PerfModel, } @@ -109,32 +108,26 @@ impl Audio for SequencerTui { render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1, Tui::fill_xy(SequencerStatusBar::from(self)), - Tui::split_down(false, 2, - TransportView::from(( - self, - self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(), - if let SequencerFocus::Transport(_) = self.focus.inner() { - true - } else { - false - } - )), - Tui::split_left(false, 20, - Tui::fixed_x(20, Tui::split_down(false, 4, col!([ - PhraseSelector::play_phrase( - &self.player, self.focused() == SequencerFocus::PhrasePlay, self.entered() - ), - PhraseSelector::next_phrase( - &self.player, self.focused() == SequencerFocus::PhraseNext, self.entered() - ), - ]), Tui::split_up(false, 2, - PhraseSelector::edit_phrase( - &self.editor.phrase, self.focused() == SequencerFocus::PhraseEditor, self.entered() - ), - PhraseListView::from(self), + Tui::split_left(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), + ]), Tui::split_up(false, 2, + PhraseSelector::edit_phrase(&self.editor.phrase, self.focused() == SequencerFocus::PhraseEditor, true), + PhraseListView::from(self), + ))), + col!([ + Tui::fixed_y(2, TransportView::from(( + self, + self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(), + if let SequencerFocus::Transport(_) = self.focus { + true + } else { + false + } ))), PhraseView::from(self) - ) + ]), ) )])); @@ -161,7 +154,19 @@ impl HasEditor for SequencerTui { self.focused() == SequencerFocus::PhraseEditor } fn editor_entered (&self) -> bool { - self.entered() && self.editor_focused() + true + } +} + +impl HasFocus for SequencerTui { + type Item = SequencerFocus; + /// Get the currently focused item. + fn focused (&self) -> Self::Item { + self.focus + } + /// Get the currently focused item. + fn set_focused (&mut self, to: Self::Item) { + self.focus = to } } @@ -174,64 +179,27 @@ pub enum SequencerFocus { PhraseList, /// The phrase editor (sequencer) is focused PhraseEditor, +} - PhrasePlay, - PhraseNext, +impl Into> for SequencerFocus { + fn into (self) -> Option { + if let Self::Transport(transport) = self { + Some(transport) + } else { + None + } + } } impl From<&SequencerTui> for Option { fn from (state: &SequencerTui) -> Self { - match state.focus.inner() { + match state.focus { Transport(focus) => Some(focus), _ => None } } } -impl_focus!(SequencerTui SequencerFocus [ - //&[ - //Menu, - //Menu, - //Menu, - //Menu, - //Menu, - //], - &[ - Transport(TransportFocus::PlayPause), - Transport(TransportFocus::Bpm), - Transport(TransportFocus::Sync), - Transport(TransportFocus::Quant), - Transport(TransportFocus::Clock), - ], - //&[ - //PhrasePlay, - //PhrasePlay, - //PhraseEditor, - //PhraseEditor, - //PhraseEditor, - //], - //&[ - //PhraseNext, - //PhraseNext, - //PhraseEditor, - //PhraseEditor, - //PhraseEditor, - //], - &[ - PhraseList, - PhraseList, - PhraseEditor, - PhraseEditor, - PhraseEditor, - ], -] => [self: { - if self.focus.is_entered() && self.focus.inner() == PhraseEditor { - self.editor.edit_mode = PhraseEditMode::Note - } else { - self.editor.edit_mode = PhraseEditMode::Scroll - } -}]); - /// Status bar for sequencer app #[derive(Clone)] pub struct SequencerStatusBar { @@ -272,8 +240,8 @@ impl From<&SequencerTui> for SequencerStatusBar { Transport(Sync) => " LAUNCH SYNC ", Transport(Quant) => " REC QUANT ", Transport(Clock) => " SEEK ", - PhrasePlay => " TO PLAY ", - PhraseNext => " UP NEXT ", + //PhrasePlay => " TO PLAY ", + //PhraseNext => " UP NEXT ", PhraseList => " PHRASES ", PhraseEditor => match state.editor.edit_mode { PhraseEditMode::Note => " EDIT MIDI ", @@ -299,16 +267,12 @@ impl From<&SequencerTui> for SequencerStatusBar { ("", ".,", " by beat"), ("", "<>", " by time"), ], - PhraseList => if state.entered() { - &[ - ("", "↕", " pick"), - ("", ".,", " move"), - ("", "⏎", " play"), - ("", "e", " edit"), - ] - } else { - default_help - }, + PhraseList => &[ + ("", "↕", " pick"), + ("", ".,", " move"), + ("", "⏎", " play"), + ("", "e", " edit"), + ], PhraseEditor => match state.editor.edit_mode { PhraseEditMode::Note => &[ ("", "✣", " cursor"), @@ -316,14 +280,8 @@ impl From<&SequencerTui> for SequencerStatusBar { PhraseEditMode::Scroll => &[ ("", "✣", " scroll"), ], - } - _ => if state.entered() { - &[ - ("", "Esc", " exit") - ] - } else { - default_help - } + }, + _ => default_help, } } } @@ -331,15 +289,15 @@ impl From<&SequencerTui> for SequencerStatusBar { render!(|self: SequencerStatusBar|{ lay!(|add|if self.width > 60 { - add(&row!(![ - SequencerMode::from(self), - SequencerStats::from(self), - ])) + add(&Tui::fill_x(Tui::fixed_y(1, lay!([ + Tui::fill_x(Tui::at_w(SequencerMode::from(self))), + Tui::fill_x(Tui::at_e(SequencerStats::from(self))), + ])))) } else { - add(&col!(![ - SequencerMode::from(self), - SequencerStats::from(self), - ])) + add(&Tui::fill_x(col!(![ + Tui::fill_x(Tui::center_x(SequencerMode::from(self))), + Tui::fill_x(Tui::center_x(SequencerStats::from(self))), + ]))) }) }); @@ -404,14 +362,11 @@ impl Handle for SequencerTui { #[derive(Clone, Debug)] pub enum SequencerCommand { - Focus(FocusCommand), + Focus(FocusCommand), Clock(ClockCommand), Phrases(PhrasesCommand), Editor(PhraseCommand), Enqueue(Option>>), - //Clear, - //Undo, - //Redo, } impl Command for SequencerCommand { @@ -425,76 +380,74 @@ impl Command for SequencerCommand { state.player.enqueue_next(phrase.as_ref()); None }, - //Self::Undo => { todo!() }, - //Self::Redo => { todo!() }, - //Self::Clear => { todo!() }, }) } } +impl Command for FocusCommand { + fn execute (self, state: &mut SequencerTui) -> Perhaps> { + if let FocusCommand::Set(to) = self { + state.set_focused(to); + } + Ok(None) + } +} + impl InputToCommand for SequencerCommand { fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { - if state.entered() { - to_sequencer_command(state, input) - .or_else(||to_focus_command(input).map(SequencerCommand::Focus)) - } else { - to_focus_command(input).map(SequencerCommand::Focus) - .or_else(||to_sequencer_command(state, input)) - }.or_else(||Some({ - let time_zoom = state.editor.view_mode.time_zoom(); - let next_zoom = next_note_length(time_zoom); - let prev_zoom = prev_note_length(time_zoom); - match input.event() { - key!(Char('-')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(next_zoom)), - key!(Char('_')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(next_zoom)), - key!(Char('=')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(prev_zoom)), - key!(Char('+')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(prev_zoom)), - key!(Char('c')) => SequencerCommand::Phrases( - PhrasesCommand::input_to_command(&state.phrases, input)?, - ), - _ => return None - } - })) + to_sequencer_command(state, input) + .or_else(||to_focus_command(input).map(Focus)) } } pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option { - let stopped = state.clock().is_stopped(); + use super::app_transport::TransportCommand; Some(match input.event() { - // Play/pause - key!(Char(' ')) => Clock(if stopped { Play(None) } else { Pause(None) }), - // Play from start/rewind to start - key!(Shift-Char(' ')) => Clock(if stopped { Play(Some(0)) } else { Pause(Some(0)) }), - // Edit phrase - key!(Char('e')) => match state.focused() { - PhrasePlay => Editor(Show( - state.player.play_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) - )), - PhraseNext => Editor(Show( - state.player.next_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) - )), - PhraseList => Editor(Show( - Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()) - )), - _ => return None, - }, - _ => match state.focused() { - Transport(_) => match TransportCommand::input_to_command(state, input)? { + + // Transport: Play/pause + key!(Char(' ')) => + Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), + + // Transport: Play from start or rewind to start + key!(Shift-Char(' ')) => + Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), + + // Editor: zoom + key!(Char('z')) | key!(Char('-')) | key!(Char('_'))| key!(Char('=')) | key!(Char('+')) => + Editor(PhraseCommand::input_to_command(&state.editor, input)?), + + // List: select phrase to edit, change color + key!(Char('[')) | key!(Char(']')) | key!(Char('c')) => + Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?), + + // Enqueue currently edited phrase + key!(Enter) => + Enqueue(Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())), + + // Switch between editor and list + key!(Tab) | key!(BackTab) | key!(Shift-Tab) | key!(Shift-BackTab) => match state.focus { + PhraseEditor => SequencerCommand::Focus(FocusCommand::Set(PhraseList)), + _ => SequencerCommand::Focus(FocusCommand::Set(PhraseList)), + } + + // Delegate to focused control: + _ => match state.focus { + PhraseEditor => Editor(PhraseCommand::input_to_command(&state.editor, input)?), + PhraseList => Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?), + Transport(_) => match to_transport_command(state, input)? { TransportCommand::Clock(command) => Clock(command), _ => return None, }, - PhraseEditor => Editor( - PhraseCommand::input_to_command(&state.editor, input)? - ), - PhraseList => match input.event() { - key!(Enter) => Enqueue(Some( - state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() - )), - _ => Phrases( - PhrasesCommand::input_to_command(&state.phrases, input)? - ), - } - _ => return None } + }) } + +impl TransportControl for SequencerTui { + fn transport_focused (&self) -> Option { + match self.focus { + SequencerFocus::Transport(focus) => Some(focus), + _ => None + } + } +} diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index d4ca3d59..a3c11beb 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -207,16 +207,28 @@ impl FocusWrap for Option { } } -impl_focus!(TransportTui TransportFocus [ - //&[Menu], - &[ - PlayPause, - Bpm, - Sync, - Quant, - Clock, - ], -]); +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; @@ -239,14 +251,24 @@ impl Handle for TransportTui { } } +pub trait TransportControl: HasClock + HasFocus { + fn transport_focused (&self) -> Option; +} + +impl TransportControl for TransportTui { + fn transport_focused (&self) -> Option { + Some(self.focus.inner()) + } +} + #[derive(Clone, Debug, PartialEq)] pub enum TransportCommand { - Focus(FocusCommand), + Focus(FocusCommand), Clock(ClockCommand), } -impl Command for TransportCommand { - fn execute (self, state: &mut T) -> Perhaps { +impl Command for TransportCommand { + fn execute (self, state: &mut TransportTui) -> Perhaps { Ok(match self { Self::Focus(cmd) => cmd.execute(state)?.map(Self::Focus), Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock), @@ -254,44 +276,26 @@ impl Command for TransportCommand { } } -pub trait TransportControl: HasClock + FocusGrid + HasEnter { - fn transport_focused (&self) -> Option; -} - -impl TransportControl for TransportTui { - fn transport_focused (&self) -> Option { - Some(self.focus.inner()) - } -} - -impl TransportControl for SequencerTui { - fn transport_focused (&self) -> Option { - match self.focus.inner() { - SequencerFocus::Transport(focus) => Some(focus), - _ => None +impl Command for FocusCommand { + fn execute (self, state: &mut TransportTui) -> Perhaps> { + if let FocusCommand::Set(to) = self { + state.set_focused(to); } + Ok(None) } } -impl TransportControl for ArrangerTui { - fn transport_focused (&self) -> Option { - match self.focus.inner() { - ArrangerFocus::Transport(focus) => Some(focus), - _ => None - } - } -} - -impl InputToCommand for TransportCommand { - fn input_to_command (state: &T, input: &TuiInput) -> Option { +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 +pub fn to_transport_command (state: &T, input: &TuiInput) -> Option where - T: TransportControl + T: TransportControl, + U: Into>, { Some(match input.event() { key!(Left) => Focus(Prev), diff --git a/crates/tek/src/tui/engine_focus.rs b/crates/tek/src/tui/engine_focus.rs deleted file mode 100644 index 4b0c417a..00000000 --- a/crates/tek/src/tui/engine_focus.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::*; - -pub trait FocusWrap { - fn wrap <'a, W: Render> (self, focus: T, content: &'a W) - -> impl Render + 'a; -} - -pub fn to_focus_command (input: &TuiInput) -> Option { - use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc}; - Some(match input.event() { - key!(Tab) => FocusCommand::Next, - key!(Shift-Tab) => FocusCommand::Prev, - key!(BackTab) => FocusCommand::Prev, - key!(Shift-BackTab) => FocusCommand::Prev, - key!(Up) => FocusCommand::Up, - key!(Down) => FocusCommand::Down, - key!(Left) => FocusCommand::Left, - key!(Right) => FocusCommand::Right, - key!(Enter) => FocusCommand::Enter, - key!(Esc) => FocusCommand::Exit, - _ => return None - }) -} - -#[macro_export] macro_rules! impl_focus { - ($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => { - impl HasFocus for $Struct { - type Item = $Focus; - /// 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) - } - $(fn focus_updated (&mut $self) { $update_focus })? - } - impl HasEnter for $Struct { - /// Get the currently focused item. - fn entered (&self) -> bool { - self.focus.is_entered() - } - /// Get the currently focused item. - fn set_entered (&mut self, entered: bool) { - if entered { - self.focus.to_entered() - } else { - self.focus.to_focused() - } - } - } - impl FocusGrid for $Struct { - fn focus_cursor (&self) -> (usize, usize) { - self.cursor - } - fn focus_cursor_mut (&mut self) -> &mut (usize, usize) { - &mut self.cursor - } - fn focus_layout (&self) -> &[&[$Focus]] { - use $Focus::*; - &$Grid - } - } - } -} diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 669a43f1..5ae39a71 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -62,7 +62,7 @@ impl Default for PhraseEditorModel { time_point: 0.into(), view_mode: Box::new(PianoHorizontal { buffer: Default::default(), - time_zoom: 24, + time_zoom: Some(24), note_zoom: PhraseViewNoteZoom::N(1) }), } @@ -190,7 +190,7 @@ render!(|self: PhraseViewStats<'a>|{ "Time: {}/{} {} {upper_right}", self.0.time_point, phrase.read().unwrap().length, - pulses_to_name(self.0.view_mode.time_zoom()), + pulses_to_name(self.0.view_mode.time_zoom().unwrap()), ) }; Tui::pull_x(1, Tui::bg(title_color, Tui::fg(TuiTheme::g(224), upper_right))) @@ -218,7 +218,6 @@ render!(|self: PhraseViewCursor<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|Ok( to, self.0.time_point, self.0.time_start, - self.0.view_mode.time_zoom(), self.0.note_point, self.0.note_len, self.0.note_range.1, @@ -234,8 +233,8 @@ pub enum PhraseEditMode { pub trait PhraseViewMode { fn show (&mut self, phrase: Option<&Phrase>, note_len: usize); - fn time_zoom (&self) -> usize; - fn set_time_zoom (&mut self, time_zoom: usize); + fn time_zoom (&self) -> Option; + fn set_time_zoom (&mut self, time_zoom: Option); fn buffer_width (&self, phrase: &Phrase) -> usize; fn buffer_height (&self, phrase: &Phrase) -> usize; fn render_keys (&self, @@ -247,7 +246,6 @@ pub trait PhraseViewMode { to: &mut TuiOutput, time_point: usize, time_start: usize, - time_zoom: usize, note_point: usize, note_len: usize, note_hi: usize, @@ -256,7 +254,7 @@ pub trait PhraseViewMode { } pub struct PianoHorizontal { - time_zoom: usize, + time_zoom: Option, note_zoom: PhraseViewNoteZoom, buffer: BigBuffer, } @@ -269,22 +267,23 @@ pub enum PhraseViewNoteZoom { } impl PhraseViewMode for PianoHorizontal { - fn time_zoom (&self) -> usize { + fn time_zoom (&self) -> Option { self.time_zoom } - fn set_time_zoom (&mut self, time_zoom: usize) { + fn set_time_zoom (&mut self, time_zoom: Option) { self.time_zoom = time_zoom } fn show (&mut self, phrase: Option<&Phrase>, note_len: usize) { if let Some(phrase) = phrase { self.buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase)); - draw_piano_horizontal(&mut self.buffer, phrase, self.time_zoom, note_len); + draw_piano_horizontal_bg(&mut self.buffer, phrase, self.time_zoom.unwrap(), note_len); + draw_piano_horizontal_fg(&mut self.buffer, phrase, self.time_zoom.unwrap()); } else { self.buffer = Default::default(); } } fn buffer_width (&self, phrase: &Phrase) -> usize { - phrase.length / self.time_zoom + phrase.length / self.time_zoom.unwrap() } /// Determine the required height to render the phrase. fn buffer_height (&self, phrase: &Phrase) -> usize { @@ -369,12 +368,12 @@ impl PhraseViewMode for PianoHorizontal { to: &mut TuiOutput, time_point: usize, time_start: usize, - time_zoom: usize, note_point: usize, note_len: usize, note_hi: usize, note_lo: usize, ) { + let time_zoom = self.time_zoom.unwrap(); let [x0, y0, w, _] = to.area().xywh(); let style = Some(Style::default().fg(Color::Rgb(0,255,0))); for (y, note) in (note_lo..=note_hi).rev().enumerate() { @@ -386,7 +385,7 @@ impl PhraseViewMode for PianoHorizontal { to.blit(&"█", x0 + x as u16, y0 + y as u16, style); let tail = note_len as u16 / time_zoom as u16; for x_tail in (x0 + x + 1)..(x0 + x + tail) { - to.blit(&"▄", x_tail, y0 + y as u16, style); + to.blit(&"▂", x_tail, y0 + y as u16, style); } break } @@ -417,7 +416,6 @@ fn draw_piano_horizontal_bg ( for (y, note) in (0..127).rev().enumerate() { for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) { let cell = target.get_mut(x, y).unwrap(); - //cell.set_fg(Color::Rgb(48, 55, 45)); cell.set_bg(phrase.color.darkest.rgb); cell.set_fg(phrase.color.darker.rgb); cell.set_char(if time % 384 == 0 { @@ -447,7 +445,7 @@ fn draw_piano_horizontal_fg ( for (y, note) in (0..127).rev().enumerate() { let cell = target.get_mut(x, note).unwrap(); if notes_on[note] { - cell.set_char('▄'); + cell.set_char('▂'); cell.set_style(style); } } @@ -484,7 +482,7 @@ pub enum PhraseCommand { SetNoteScroll(usize), SetTimeCursor(usize), SetTimeScroll(usize), - SetTimeZoom(usize), + SetTimeZoom(Option), Show(Option>>), SetEditMode(PhraseEditMode), ToggleDirection, @@ -504,10 +502,11 @@ impl InputToCommand for PhraseCommand { Some(match from.event() { key!(Char('`')) => ToggleDirection, key!(Esc) => SetEditMode(PhraseEditMode::Scroll), - key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)), - key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)), - key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)), - key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)), + key!(Char('z')) => SetTimeZoom(None), + key!(Char('-')) => SetTimeZoom(time_zoom.map(next_note_length)), + key!(Char('_')) => SetTimeZoom(time_zoom.map(next_note_length)), + key!(Char('=')) => SetTimeZoom(time_zoom.map(prev_note_length)), + key!(Char('+')) => SetTimeZoom(time_zoom.map(prev_note_length)), key!(Char('a')) => AppendNote, key!(Char('s')) => PutNote, // TODO: no triplet/dotted @@ -523,8 +522,8 @@ impl InputToCommand for PhraseCommand { key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)), key!(PageUp) => SetNoteScroll(note_lo + 3), key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)), - key!(Left) => SetTimeScroll(time_start.saturating_sub(time_zoom)), - key!(Right) => SetTimeScroll(time_start + time_zoom), + key!(Left) => SetTimeScroll(time_start.saturating_sub(time_zoom.unwrap())), + key!(Right) => SetTimeScroll(time_start + time_zoom.unwrap()), _ => return None }, PhraseEditMode::Note => match from.event() { @@ -535,8 +534,8 @@ impl InputToCommand for PhraseCommand { key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)), key!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)), key!(Right) => SetTimeCursor((time_point + note_len) % length), - key!(Shift-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)), - key!(Shift-Right) => SetTimeCursor((time_point + time_zoom) % length), + key!(Shift-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom.unwrap())), + key!(Shift-Right) => SetTimeCursor((time_point + time_zoom.unwrap()) % length), _ => return None }, } diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index d0c2f8b7..eed3fbe0 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -72,7 +72,7 @@ impl HasPhraseList for SequencerTui { self.focused() == SequencerFocus::PhraseList } fn phrases_entered (&self) -> bool { - self.entered() && self.phrases_focused() + true && self.phrases_focused() } fn phrases_mode (&self) -> &Option { &self.phrases.mode From d492dbb637d8c0b9c1514cd3ee117878cf19a578 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 11 Dec 2024 21:14:08 +0100 Subject: [PATCH 010/971] disable advanced sequencer focus, pt.2 --- crates/tek/src/tui/app_sequencer.rs | 195 +++++----------------------- crates/tek/src/tui/phrase_editor.rs | 61 +++------ crates/tek/src/tui/phrase_list.rs | 15 --- crates/tek/src/tui/status_bar.rs | 155 ++++++++++++++++++++++ 4 files changed, 209 insertions(+), 217 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index beece12f..63ab3c8b 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -1,5 +1,5 @@ use crate::{*, api::ClockCommand::{Play, Pause}}; -use KeyCode::{Tab, BackTab, Char, Enter}; +use KeyCode::{Tab, BackTab, Char, Enter, Esc}; use SequencerCommand::*; use SequencerFocus::*; use PhraseCommand::*; @@ -146,12 +146,27 @@ impl HasPhrases for SequencerTui { } } +impl HasPhraseList for SequencerTui { + fn phrases_focused (&self) -> bool { + true + } + fn phrases_entered (&self) -> bool { + true + } + fn phrases_mode (&self) -> &Option { + &self.phrases.mode + } + fn phrase_index (&self) -> usize { + self.phrases.phrase.load(Ordering::Relaxed) + } +} + impl HasEditor for SequencerTui { fn editor (&self) -> &PhraseEditorModel { &self.editor } fn editor_focused (&self) -> bool { - self.focused() == SequencerFocus::PhraseEditor + false } fn editor_entered (&self) -> bool { true @@ -200,160 +215,6 @@ impl From<&SequencerTui> for Option { } } -/// Status bar for sequencer app -#[derive(Clone)] -pub struct SequencerStatusBar { - pub(crate) width: usize, - pub(crate) cpu: Option, - pub(crate) size: String, - pub(crate) res: String, - pub(crate) mode: &'static str, - pub(crate) help: &'static [(&'static str, &'static str, &'static str)] -} - -impl StatusBar for SequencerStatusBar { - type State = SequencerTui; - fn hotkey_fg () -> Color { - TuiTheme::HOTKEY_FG - } - fn update (&mut self, _: &SequencerTui) { - todo!() - } -} - -impl From<&SequencerTui> for SequencerStatusBar { - fn from (state: &SequencerTui) -> Self { - use super::app_transport::TransportFocus::*; - let samples = state.clock.chunk.load(Ordering::Relaxed); - let rate = state.clock.timebase.sr.get() as f64; - let buffer = samples as f64 / rate; - let width = state.size.w(); - let default_help = &[("", "⏎", " enter"), ("", "✣", " navigate")]; - Self { - width, - cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), - size: format!("{}x{}│", width, state.size.h()), - res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), - mode: match state.focused() { - Transport(PlayPause) => " PLAY/PAUSE ", - Transport(Bpm) => " TEMPO ", - Transport(Sync) => " LAUNCH SYNC ", - Transport(Quant) => " REC QUANT ", - Transport(Clock) => " SEEK ", - //PhrasePlay => " TO PLAY ", - //PhraseNext => " UP NEXT ", - PhraseList => " PHRASES ", - PhraseEditor => match state.editor.edit_mode { - PhraseEditMode::Note => " EDIT MIDI ", - PhraseEditMode::Scroll => " VIEW MIDI ", - }, - }, - help: match state.focused() { - Transport(PlayPause) => &[ - ("", "⏎", " play/pause"), - ("", "✣", " navigate"), - ], - Transport(Bpm) => &[ - ("", ".,", " inc/dec"), - ("", "><", " fine"), - ], - Transport(Sync) => &[ - ("", ".,", " inc/dec"), - ], - Transport(Quant) => &[ - ("", ".,", " inc/dec"), - ], - Transport(Clock) => &[ - ("", ".,", " by beat"), - ("", "<>", " by time"), - ], - PhraseList => &[ - ("", "↕", " pick"), - ("", ".,", " move"), - ("", "⏎", " play"), - ("", "e", " edit"), - ], - PhraseEditor => match state.editor.edit_mode { - PhraseEditMode::Note => &[ - ("", "✣", " cursor"), - ], - PhraseEditMode::Scroll => &[ - ("", "✣", " scroll"), - ], - }, - _ => default_help, - } - } - } -} - -render!(|self: SequencerStatusBar|{ - lay!(|add|if self.width > 60 { - add(&Tui::fill_x(Tui::fixed_y(1, lay!([ - Tui::fill_x(Tui::at_w(SequencerMode::from(self))), - Tui::fill_x(Tui::at_e(SequencerStats::from(self))), - ])))) - } else { - add(&Tui::fill_x(col!(![ - Tui::fill_x(Tui::center_x(SequencerMode::from(self))), - Tui::fill_x(Tui::center_x(SequencerStats::from(self))), - ]))) - }) -}); - -struct SequencerMode { - mode: &'static str, - help: &'static [(&'static str, &'static str, &'static str)] -} -impl From<&SequencerStatusBar> for SequencerMode { - fn from (state: &SequencerStatusBar) -> Self { - Self { - mode: state.mode, - help: state.help, - } - } -} -render!(|self: SequencerMode|{ - let black = TuiTheme::g(0); - let light = TuiTheme::g(50); - let white = TuiTheme::g(255); - let orange = TuiTheme::orange(); - let yellow = TuiTheme::yellow(); - row!([ - Tui::bg(orange, Tui::fg(black, Tui::bold(true, self.mode))), - Tui::bg(light, Tui::fg(white, row!((prefix, hotkey, suffix) in self.help.iter() => { - row!([" ", prefix, Tui::fg(yellow, *hotkey), suffix]) - }))) - ]) -}); - -struct SequencerStats<'a> { - cpu: &'a Option, - size: &'a String, - res: &'a String, -} -impl<'a> From<&'a SequencerStatusBar> for SequencerStats<'a> { - fn from (state: &'a SequencerStatusBar) -> Self { - Self { - cpu: &state.cpu, - size: &state.size, - res: &state.res, - } - } -} -render!(|self:SequencerStats<'a>|{ - let orange = TuiTheme::orange(); - let dark = TuiTheme::g(25); - let cpu = &self.cpu; - let res = &self.res; - let size = &self.size; - Tui::bg(dark, row!([ - Tui::fg(orange, cpu), - Tui::fg(orange, res), - Tui::fg(orange, size), - ])) -}); - impl Handle for SequencerTui { fn handle (&mut self, i: &TuiInput) -> Perhaps { SequencerCommand::execute_with_state(self, i) @@ -404,6 +265,22 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option match state.focus { + PhraseEditor => SequencerCommand::Focus(FocusCommand::Set(PhraseList)), + _ => SequencerCommand::Focus(FocusCommand::Set(PhraseEditor)), + } + + // Esc: toggle between scrolling and editing + key!(Esc) => + Editor(SetEditMode(match state.editor.edit_mode { + PhraseEditMode::Scroll => PhraseEditMode::Note, + PhraseEditMode::Note => PhraseEditMode::Scroll, + })), + + // E: Toggle between editing currently playing or other phrase + //key!(Char('e')) => {} + // Transport: Play/pause key!(Char(' ')) => Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), @@ -424,12 +301,6 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option Enqueue(Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())), - // Switch between editor and list - key!(Tab) | key!(BackTab) | key!(Shift-Tab) | key!(Shift-BackTab) => match state.focus { - PhraseEditor => SequencerCommand::Focus(FocusCommand::Set(PhraseList)), - _ => SequencerCommand::Focus(FocusCommand::Set(PhraseList)), - } - // Delegate to focused control: _ => match state.focus { PhraseEditor => Editor(PhraseCommand::input_to_command(&state.editor, input)?), diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 5ae39a71..04d863fd 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -123,14 +123,10 @@ render!(|self: PhraseView<'a>|{ let fg = self.phrase.as_ref() .map(|p|p.read().unwrap().color.clone()) .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); - Tui::bg(bg, lay!([ - PhraseTimeline(&self, fg), - PhraseViewNotes(&self, fg), - PhraseViewCursor(&self), - PhraseViewKeys(&self, fg), - PhraseViewStats(&self, fg), - //Measure::debug(), - ])) + Tui::bg(bg, Tui::split_up(false, 2, + Tui::bg(fg.dark.rgb, lay!([PhraseTimeline(&self, fg), PhraseViewStats(&self, fg),])), + lay!([PhraseNotes(&self, fg), PhraseCursor(&self), PhraseKeys(&self, fg)]), + )) }); impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { fn from (state: &'a T) -> Self { @@ -138,10 +134,6 @@ impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { let height = editor.size.h(); let note_point = editor.note_point.load(Ordering::Relaxed); let mut note_lo = editor.note_lo.load(Ordering::Relaxed); - //if note_point < note_lo { - //note_lo = note_point; - //editor.note_lo.store(note_lo, Ordering::Relaxed); - //} let mut note_hi = 127.min((note_lo + height).saturating_sub(2)); if note_point > note_hi { note_lo += note_point - note_hi; @@ -169,51 +161,46 @@ render!(|self: PhraseTimeline<'a>|Tui::fg(TuiTheme::g(224), Tui::push_x(5, forma pub struct PhraseViewStats<'a>(&'a PhraseView<'a>, ItemPalette); render!(|self: PhraseViewStats<'a>|{ - let title_color = if self.0.focused{self.1.light.rgb}else{self.1.dark.rgb}; + let color = self.1.dark.rgb;//if self.0.focused{self.1.light.rgb}else{self.1.dark.rgb}; lay!([ - Tui::at_sw({ - let mut lower_right = format!(" {} ", self.0.size.format()); - if self.0.focused && self.0.entered { - lower_right = format!( - "Note: {} ({}) {} {lower_right}", - self.0.note_point, - to_note_name(self.0.note_point), - pulses_to_name(self.0.note_len), - ); - } - Tui::bg(title_color, Tui::fg(TuiTheme::g(224), lower_right)) - }), + Tui::at_sw(Tui::bg(color, Tui::fg(TuiTheme::g(224), format!( + " {} | Note: {} ({}) | {} ", + self.0.size.format(), + self.0.note_point, + to_note_name(self.0.note_point), + pulses_to_name(self.0.note_len), + )))), Tui::fill_xy(Tui::at_se({ let mut upper_right = format!("[{}]", if self.0.entered {"■"} else {" "}); if let Some(phrase) = self.0.phrase { upper_right = format!( - "Time: {}/{} {} {upper_right}", + " Time: {}/{} {} {upper_right} ", self.0.time_point, phrase.read().unwrap().length, pulses_to_name(self.0.view_mode.time_zoom().unwrap()), ) }; - Tui::pull_x(1, Tui::bg(title_color, Tui::fg(TuiTheme::g(224), upper_right))) - })), + Tui::bg(color, Tui::fg(TuiTheme::g(224), upper_right)) + })) ]) }); -struct PhraseViewKeys<'a>(&'a PhraseView<'a>, ItemPalette); -render!(|self: PhraseViewKeys<'a>|{ +struct PhraseKeys<'a>(&'a PhraseView<'a>, ItemPalette); +render!(|self: PhraseKeys<'a>|{ let layout = |to:[u16;2]|Ok(Some(to.clip_w(5))); Tui::fill_xy(Widget::new(layout, |to: &mut TuiOutput|Ok( self.0.view_mode.render_keys(to, self.1.light.rgb, Some(self.0.note_point), self.0.note_range) ))) }); -struct PhraseViewNotes<'a>(&'a PhraseView<'a>, ItemPalette); -render!(|self: PhraseViewNotes<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|{ +struct PhraseNotes<'a>(&'a PhraseView<'a>, ItemPalette); +render!(|self: PhraseNotes<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|{ self.0.size.set_wh(to.area.w(), to.area.h() as usize); Ok(self.0.view_mode.render_notes(to, self.0.time_start, self.0.note_range.1)) }))); -struct PhraseViewCursor<'a>(&'a PhraseView<'a>); -render!(|self: PhraseViewCursor<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|Ok( +struct PhraseCursor<'a>(&'a PhraseView<'a>); +render!(|self: PhraseCursor<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|Ok( self.0.view_mode.render_cursor( to, self.0.time_point, @@ -303,16 +290,12 @@ impl PhraseViewMode for PianoHorizontal { let source = &self.buffer; let target = &mut target.buffer; for (x, target_x) in (x0..x0+w).enumerate() { - for (y, target_y) in (y0..y0+h).enumerate() { - if y > note_hi { break } - let source_x = time_start + x; let source_y = note_hi - y; - // TODO: enable loop rollover: //let source_x = (time_start + x) % source.width.max(1); //let source_y = (note_hi - y) % source.height.max(1); @@ -322,9 +305,7 @@ impl PhraseViewMode for PianoHorizontal { *target_cell = source_cell.clone(); } } - } - } } fn render_keys ( diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index eed3fbe0..27dcafdb 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -67,21 +67,6 @@ pub trait HasPhraseList: HasPhrases { fn phrase_index (&self) -> usize; } -impl HasPhraseList for SequencerTui { - fn phrases_focused (&self) -> bool { - self.focused() == SequencerFocus::PhraseList - } - fn phrases_entered (&self) -> bool { - true && self.phrases_focused() - } - fn phrases_mode (&self) -> &Option { - &self.phrases.mode - } - fn phrase_index (&self) -> usize { - self.phrases.phrase.load(Ordering::Relaxed) - } -} - impl HasPhraseList for ArrangerTui { fn phrases_focused (&self) -> bool { self.focused() == ArrangerFocus::Phrases diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs index b3af3c5b..70cfa678 100644 --- a/crates/tek/src/tui/status_bar.rs +++ b/crates/tek/src/tui/status_bar.rs @@ -26,3 +26,158 @@ pub trait StatusBar: Render { Tui::to_north(state.into(), content) } } + +/// Status bar for sequencer app +#[derive(Clone)] +pub struct SequencerStatusBar { + pub(crate) width: usize, + pub(crate) cpu: Option, + pub(crate) size: String, + pub(crate) res: String, + pub(crate) mode: &'static str, + pub(crate) help: &'static [(&'static str, &'static str, &'static str)] +} + +impl StatusBar for SequencerStatusBar { + type State = SequencerTui; + fn hotkey_fg () -> Color { + TuiTheme::HOTKEY_FG + } + fn update (&mut self, _: &SequencerTui) { + todo!() + } +} + +impl From<&SequencerTui> for SequencerStatusBar { + fn from (state: &SequencerTui) -> Self { + use super::app_transport::TransportFocus::*; + use super::app_sequencer::SequencerFocus::*; + let samples = state.clock.chunk.load(Ordering::Relaxed); + let rate = state.clock.timebase.sr.get() as f64; + let buffer = samples as f64 / rate; + let width = state.size.w(); + let default_help = &[("", "⏎", " enter"), ("", "✣", " navigate")]; + Self { + width, + cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), + size: format!("{}x{}│", width, state.size.h()), + res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), + mode: match state.focused() { + Transport(PlayPause) => " PLAY/PAUSE ", + Transport(Bpm) => " TEMPO ", + Transport(Sync) => " LAUNCH SYNC ", + Transport(Quant) => " REC QUANT ", + Transport(Clock) => " SEEK ", + //PhrasePlay => " TO PLAY ", + //PhraseNext => " UP NEXT ", + PhraseList => " PHRASES ", + PhraseEditor => match state.editor.edit_mode { + PhraseEditMode::Note => " EDIT MIDI ", + PhraseEditMode::Scroll => " VIEW MIDI ", + }, + }, + help: match state.focused() { + Transport(PlayPause) => &[ + ("", "⏎", " play/pause"), + ("", "✣", " navigate"), + ], + Transport(Bpm) => &[ + ("", ".,", " inc/dec"), + ("", "><", " fine"), + ], + Transport(Sync) => &[ + ("", ".,", " inc/dec"), + ], + Transport(Quant) => &[ + ("", ".,", " inc/dec"), + ], + Transport(Clock) => &[ + ("", ".,", " by beat"), + ("", "<>", " by time"), + ], + PhraseList => &[ + ("", "↕", " pick"), + ("", ".,", " move"), + ("", "⏎", " play"), + ("", "e", " edit"), + ], + PhraseEditor => match state.editor.edit_mode { + PhraseEditMode::Note => &[ + ("", "✣", " cursor"), + ], + PhraseEditMode::Scroll => &[ + ("", "✣", " scroll"), + ], + }, + _ => default_help, + } + } + } +} + +render!(|self: SequencerStatusBar|{ + lay!(|add|if self.width > 60 { + add(&Tui::fill_x(Tui::fixed_y(1, lay!([ + Tui::fill_x(Tui::at_w(SequencerMode::from(self))), + Tui::fill_x(Tui::at_e(SequencerStats::from(self))), + ])))) + } else { + add(&Tui::fill_x(col!(![ + Tui::fill_x(Tui::center_x(SequencerMode::from(self))), + Tui::fill_x(Tui::center_x(SequencerStats::from(self))), + ]))) + }) +}); + +struct SequencerMode { + mode: &'static str, + help: &'static [(&'static str, &'static str, &'static str)] +} +impl From<&SequencerStatusBar> for SequencerMode { + fn from (state: &SequencerStatusBar) -> Self { + Self { + mode: state.mode, + help: state.help, + } + } +} +render!(|self: SequencerMode|{ + let black = TuiTheme::g(0); + let light = TuiTheme::g(50); + let white = TuiTheme::g(255); + let orange = TuiTheme::orange(); + let yellow = TuiTheme::yellow(); + row!([ + Tui::bg(orange, Tui::fg(black, Tui::bold(true, self.mode))), + Tui::bg(light, Tui::fg(white, row!((prefix, hotkey, suffix) in self.help.iter() => { + row!([" ", prefix, Tui::fg(yellow, *hotkey), suffix]) + }))) + ]) +}); + +struct SequencerStats<'a> { + cpu: &'a Option, + size: &'a String, + res: &'a String, +} +impl<'a> From<&'a SequencerStatusBar> for SequencerStats<'a> { + fn from (state: &'a SequencerStatusBar) -> Self { + Self { + cpu: &state.cpu, + size: &state.size, + res: &state.res, + } + } +} +render!(|self:SequencerStats<'a>|{ + let orange = TuiTheme::orange(); + let dark = TuiTheme::g(25); + let cpu = &self.cpu; + let res = &self.res; + let size = &self.size; + Tui::bg(dark, row!([ + Tui::fg(orange, cpu), + Tui::fg(orange, res), + Tui::fg(orange, size), + ])) +}); From 14fac03f5d46e28c504ac1cc735aef046ac2d02d Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 11 Dec 2024 21:29:44 +0100 Subject: [PATCH 011/971] fix keys overlap --- crates/tek/src/tui/app_sequencer.rs | 1 + crates/tek/src/tui/phrase_editor.rs | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 63ab3c8b..f788e013 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -23,6 +23,7 @@ impl TryFrom<&Arc>> for SequencerTui { let mut editor = PhraseEditorModel::default(); editor.show_phrase(Some(phrase.clone())); + editor.edit_mode = PhraseEditMode::Note; let mut player = PhrasePlayerModel::from(&clock); player.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase))); diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 04d863fd..2b033038 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -125,7 +125,7 @@ render!(|self: PhraseView<'a>|{ .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); Tui::bg(bg, Tui::split_up(false, 2, Tui::bg(fg.dark.rgb, lay!([PhraseTimeline(&self, fg), PhraseViewStats(&self, fg),])), - lay!([PhraseNotes(&self, fg), PhraseCursor(&self), PhraseKeys(&self, fg)]), + Split::right(false, 5, PhraseKeys(&self, fg), lay!([PhraseNotes(&self, fg), PhraseCursor(&self), ])), )) }); impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { @@ -162,15 +162,15 @@ render!(|self: PhraseTimeline<'a>|Tui::fg(TuiTheme::g(224), Tui::push_x(5, forma pub struct PhraseViewStats<'a>(&'a PhraseView<'a>, ItemPalette); render!(|self: PhraseViewStats<'a>|{ let color = self.1.dark.rgb;//if self.0.focused{self.1.light.rgb}else{self.1.dark.rgb}; - lay!([ - Tui::at_sw(Tui::bg(color, Tui::fg(TuiTheme::g(224), format!( + row!([ + Tui::bg(color, Tui::fg(TuiTheme::g(224), format!( " {} | Note: {} ({}) | {} ", self.0.size.format(), self.0.note_point, to_note_name(self.0.note_point), pulses_to_name(self.0.note_len), - )))), - Tui::fill_xy(Tui::at_se({ + ))), + { let mut upper_right = format!("[{}]", if self.0.entered {"■"} else {" "}); if let Some(phrase) = self.0.phrase { upper_right = format!( @@ -181,7 +181,7 @@ render!(|self: PhraseViewStats<'a>|{ ) }; Tui::bg(color, Tui::fg(TuiTheme::g(224), upper_right)) - })) + } ]) }); @@ -496,6 +496,7 @@ impl InputToCommand for PhraseCommand { // TODO: with triplet/dotted key!(Char('<')) => SetNoteLength(prev_note_length(state.note_len)), key!(Char('>')) => SetNoteLength(next_note_length(state.note_len)), + // TODO: '/' set triplet, '?' set dotted _ => match state.edit_mode { PhraseEditMode::Scroll => match from.event() { key!(Char('e')) => SetEditMode(PhraseEditMode::Note), From d0d187b5b68a8bf878b487537ea1e1c4e4f31f60 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 11 Dec 2024 21:58:26 +0100 Subject: [PATCH 012/971] edit toggle; reallow add/duplicate phrase --- crates/tek/src/tui/app_sequencer.rs | 14 ++++++++++++-- crates/tek/src/tui/phrase_list.rs | 11 +++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index f788e013..3cb8200f 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -280,7 +280,17 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option {} + key!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() { + let editing_phrase = state.editor.phrase.as_ref().map(|p|p.read().unwrap().clone()); + let selected_phrase = state.phrases.phrase().clone(); + if Some(selected_phrase.read().unwrap().clone()) != editing_phrase { + Editor(Show(Some(selected_phrase))) + } else { + Editor(Show(Some(playing_phrase.clone()))) + } + } else { + return None + }, // Transport: Play/pause key!(Char(' ')) => @@ -295,7 +305,7 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option + key!(Char('[')) | key!(Char(']')) | key!(Char('c')) | key!(Shift-Char('A')) | key!(Shift-Char('D')) => Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?), // Enqueue currently edited phrase diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 27dcafdb..7ffd6a20 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -33,6 +33,9 @@ impl Default for PhraseListModel { } impl PhraseListModel { + pub(crate) fn phrase (&self) -> &Arc> { + &self.phrases[self.phrase_index()] + } pub(crate) fn phrase_index (&self) -> usize { self.phrase.load(Ordering::Relaxed) } @@ -250,10 +253,10 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option Cmd::Import(Browse::Begin), key!(Char('x')) => Cmd::Export(Browse::Begin), key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())), - key!(Up) => Cmd::Select( + key!(Up) | key!(Char('[')) => Cmd::Select( index.overflowing_sub(1).0.min(state.phrases().len() - 1) ), - key!(Down) => Cmd::Select( + key!(Down) | key!(Char(']')) => Cmd::Select( index.saturating_add(1) % state.phrases().len() ), key!(Char('<')) => if index > 1 { @@ -274,13 +277,13 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option Cmd::Phrase(Pool::Add(count, Phrase::new( + key!(Char('a')) | key!(Shift-Char('A')) => Cmd::Phrase(Pool::Add(count, Phrase::new( String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) ))), key!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new( String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) ))), - key!(Char('d')) => { + key!(Char('d')) | key!(Shift-Char('D')) => { let mut phrase = state.phrases()[index].read().unwrap().duplicate(); phrase.color = ItemPalette::random_near(phrase.color, 0.25); Cmd::Phrase(Pool::Add(index + 1, phrase)) From a7ff74e27c8f16927d54167e2c26f013e00f1a57 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 12 Dec 2024 10:39:04 +0100 Subject: [PATCH 013/971] general unfuckeries --- crates/tek/src/api/jack.rs | 4 - crates/tek/src/api/player.rs | 12 +- crates/tek/src/core/audio.rs | 2 +- crates/tek/src/tui/app_arranger.rs | 6 - crates/tek/src/tui/app_groovebox.rs | 2 + crates/tek/src/tui/app_sequencer.rs | 14 +- crates/tek/src/tui/app_transport.rs | 117 +++++++---------- crates/tek/src/tui/engine_input.rs | 192 ++++++++++------------------ crates/tek/src/tui/phrase_length.rs | 1 + crates/tek/src/tui/phrase_list.rs | 72 +++++------ crates/tek/src/tui/phrase_select.rs | 105 +++++++-------- 11 files changed, 210 insertions(+), 317 deletions(-) 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)), } } } From 69faadac2bc5c65dd7c4b15161135062133309fa Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 12 Dec 2024 10:47:13 +0100 Subject: [PATCH 014/971] PhrasesMode -> PhraseListMode --- crates/tek/src/api/phrase.rs | 4 ++ crates/tek/src/tui/app_arranger.rs | 15 ++++++ crates/tek/src/tui/app_sequencer.rs | 2 +- crates/tek/src/tui/file_browser.rs | 12 ++--- crates/tek/src/tui/phrase_length.rs | 12 ++--- crates/tek/src/tui/phrase_list.rs | 78 ++++++++++++----------------- crates/tek/src/tui/phrase_rename.rs | 4 +- 7 files changed, 65 insertions(+), 62 deletions(-) diff --git a/crates/tek/src/api/phrase.rs b/crates/tek/src/api/phrase.rs index 94aa24c2..3f8ca6b8 100644 --- a/crates/tek/src/api/phrase.rs +++ b/crates/tek/src/api/phrase.rs @@ -5,6 +5,10 @@ pub trait HasPhrases { fn phrases_mut (&mut self) -> &mut Vec>>; } +pub trait HasPhrase { + fn phrase (&self) -> &Arc>; +} + #[derive(Clone, Debug, PartialEq)] pub enum PhrasePoolCommand { Add(usize, Phrase), diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 9d88445b..f8813c96 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -168,6 +168,21 @@ impl HasPhrases for ArrangerTui { } } +impl HasPhraseList for ArrangerTui { + fn phrases_focused (&self) -> bool { + self.focused() == ArrangerFocus::Phrases + } + fn phrases_entered (&self) -> bool { + self.entered() && self.phrases_focused() + } + fn phrases_mode (&self) -> &Option { + &self.phrases.mode + } + fn phrase_index (&self) -> usize { + self.phrases.phrase.load(Ordering::Relaxed) + } +} + impl HasEditor for ArrangerTui { fn editor (&self) -> &PhraseEditorModel { &self.editor diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 869b4ab1..db3e575e 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -148,7 +148,7 @@ impl HasPhraseList for SequencerTui { fn phrases_entered (&self) -> bool { true } - fn phrases_mode (&self) -> &Option { + fn phrases_mode (&self) -> &Option { &self.phrases.mode } fn phrase_index (&self) -> usize { diff --git a/crates/tek/src/tui/file_browser.rs b/crates/tek/src/tui/file_browser.rs index 436a3220..26a40c30 100644 --- a/crates/tek/src/tui/file_browser.rs +++ b/crates/tek/src/tui/file_browser.rs @@ -1,7 +1,7 @@ use crate::*; use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace}; use FileBrowserCommand::*; -use super::phrase_list::PhrasesMode::{Import, Export}; +use super::phrase_list::PhraseListMode::{Import, Export}; /// Browses for phrase to import/export #[derive(Debug, Clone)] @@ -121,12 +121,12 @@ impl Command for FileBrowserCommand { }, _ => todo!(), }, - Some(PhrasesMode::Export(index, ref mut browser)) => match self { + Some(PhraseListMode::Export(index, ref mut browser)) => match self { Cancel => { *mode = None; }, Chdir(cwd) => { - *mode = Some(PhrasesMode::Export(*index, FileBrowser::new(Some(cwd))?)); + *mode = Some(PhraseListMode::Export(*index, FileBrowser::new(Some(cwd))?)); }, Select(index) => { browser.index = index; @@ -141,7 +141,7 @@ impl Command for FileBrowserCommand { impl InputToCommand for FileBrowserCommand { fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option { - if let Some(PhrasesMode::Import(_index, browser)) = state.phrases_mode() { + if let Some(PhraseListMode::Import(_index, browser)) = state.phrases_mode() { Some(match from.event() { key!(Up) => Select( browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1)) @@ -157,7 +157,7 @@ impl InputToCommand for FileBrowserCommand { key!(Esc) => Self::Cancel, _ => return None }) - } else if let Some(PhrasesMode::Export(_index, browser)) = state.phrases_mode() { + } else if let Some(PhraseListMode::Export(_index, browser)) = state.phrases_mode() { Some(match from.event() { key!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())), key!(Down) => Select(browser.index.saturating_add(1) % browser.len()), @@ -177,7 +177,7 @@ impl InputToCommand for FileBrowserCommand { impl InputToCommand for PhraseLengthCommand { fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option { - if let Some(PhrasesMode::Length(_, length, _)) = state.phrases_mode() { + if let Some(PhraseListMode::Length(_, length, _)) = state.phrases_mode() { Some(match from.event() { key!(Up) => Self::Inc, key!(Down) => Self::Dec, diff --git a/crates/tek/src/tui/phrase_length.rs b/crates/tek/src/tui/phrase_length.rs index 675fa946..f6f49cde 100644 --- a/crates/tek/src/tui/phrase_length.rs +++ b/crates/tek/src/tui/phrase_length.rs @@ -1,5 +1,5 @@ use crate::*; -use super::phrase_list::{PhraseListModel, PhrasesMode}; +use super::phrase_list::{PhraseListModel, PhraseListMode}; use PhraseLengthFocus::*; use PhraseLengthCommand::*; @@ -75,13 +75,13 @@ render!(|self: PhraseLength|{ let ticks = ||self.ticks_string(); row!(move|add|match self.focus { None => - add(&row!([" ", bars(), "B", beats(), "b", ticks(), "T"])), + add(&row!([" ", bars(), ".", beats(), ".", ticks()])), Some(PhraseLengthFocus::Bar) => - add(&row!(["[", bars(), "]", beats(), "b", ticks(), "T"])), + add(&row!(["[", bars(), "]", beats(), ".", ticks()])), Some(PhraseLengthFocus::Beat) => - add(&row!([" ", bars(), "[", beats(), "]", ticks(), "T"])), + add(&row!([" ", bars(), "[", beats(), "]", ticks()])), Some(PhraseLengthFocus::Tick) => - add(&row!([" ", bars(), "B", beats(), "[", ticks(), "]"])), + add(&row!([" ", bars(), ".", beats(), "[", ticks()])), }) }); @@ -99,7 +99,7 @@ pub enum PhraseLengthCommand { impl Command for PhraseLengthCommand { fn execute (self, state: &mut PhraseListModel) -> Perhaps { match state.phrases_mode_mut().clone() { - Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) => match self { + Some(PhraseListMode::Length(phrase, ref mut length, ref mut focus)) => match self { Cancel => { *state.phrases_mode_mut() = None; }, Prev => { focus.prev() }, Next => { focus.next() }, diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 1e7a559e..7c301d55 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -1,6 +1,5 @@ use super::*; use crate::{ - *, api::PhrasePoolCommand as Pool, tui::{ phrase_rename::PhraseRenameCommand as Rename, @@ -16,9 +15,22 @@ pub struct PhraseListModel { /// Selected phrase pub(crate) phrase: AtomicUsize, /// Scroll offset - pub(crate) scroll: usize, + pub scroll: usize, /// Mode switch - pub(crate) mode: Option, + pub(crate) mode: Option, +} + +/// Modes for phrase pool +#[derive(Debug, Clone)] +pub enum PhraseListMode { + /// Renaming a pattern + Rename(usize, String), + /// Editing the length of a pattern + Length(usize, usize, PhraseLengthFocus), + /// Load phrase from disk + Import(usize, FileBrowser), + /// Save phrase to disk + Export(usize, FileBrowser), } impl Default for PhraseListModel { @@ -42,56 +54,28 @@ impl PhraseListModel { pub(crate) fn set_phrase_index (&self, value: usize) { self.phrase.store(value, Ordering::Relaxed); } - pub(crate) fn phrases_mode (&self) -> &Option { + pub(crate) fn phrases_mode (&self) -> &Option { &self.mode } - pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option { + pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option { &mut self.mode } } -/// Modes for phrase pool -#[derive(Debug, Clone)] -pub enum PhrasesMode { - /// Renaming a pattern - Rename(usize, String), - /// Editing the length of a pattern - Length(usize, usize, PhraseLengthFocus), - /// Load phrase from disk - Import(usize, FileBrowser), - /// Save phrase to disk - Export(usize, FileBrowser), -} - pub trait HasPhraseList: HasPhrases { fn phrases_focused (&self) -> bool; fn phrases_entered (&self) -> bool; - fn phrases_mode (&self) -> &Option; + fn phrases_mode (&self) -> &Option; fn phrase_index (&self) -> usize; } -impl HasPhraseList for ArrangerTui { - fn phrases_focused (&self) -> bool { - self.focused() == ArrangerFocus::Phrases - } - fn phrases_entered (&self) -> bool { - self.entered() && self.phrases_focused() - } - fn phrases_mode (&self) -> &Option { - &self.phrases.mode - } - fn phrase_index (&self) -> usize { - self.phrases.phrase.load(Ordering::Relaxed) - } -} - pub struct PhraseListView<'a> { pub(crate) title: &'static str, pub(crate) focused: bool, pub(crate) entered: bool, pub(crate) phrases: &'a Vec>>, pub(crate) index: usize, - pub(crate) mode: &'a Option + pub(crate) mode: &'a Option } impl<'a, T: HasPhraseList> From<&'a T> for PhraseListView<'a> { @@ -117,13 +101,13 @@ render!(|self: PhraseListView<'a>|{ 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 file_picker)) => add(file_picker), - Some(PhrasesMode::Export(_, ref file_picker)) => add(file_picker), + Some(PhraseListMode::Import(_, ref file_picker)) => add(file_picker), + Some(PhraseListMode::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 let Some(PhraseListMode::Length(phrase, new_length, focus)) = mode { if *focused && i == *phrase { length.pulses = *new_length; length.focus = Some(*focus); @@ -136,7 +120,7 @@ render!(|self: PhraseListView<'a>|{ })), Tui::bold(true, { let mut row2 = format!(" {name}"); - if let Some(PhrasesMode::Rename(phrase, _)) = mode { + if let Some(PhraseListMode::Rename(phrase, _)) = mode { if *focused && i == *phrase { row2 = format!("{row2}▄"); } @@ -175,7 +159,7 @@ impl Command for PhrasesCommand { PhraseRenameCommand::Begin => { let length = state.phrases()[state.phrase_index()].read().unwrap().length; *state.phrases_mode_mut() = Some( - PhrasesMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar) + PhraseListMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar) ); None }, @@ -185,7 +169,7 @@ impl Command for PhrasesCommand { PhraseLengthCommand::Begin => { let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone(); *state.phrases_mode_mut() = Some( - PhrasesMode::Rename(state.phrase_index(), name) + PhraseListMode::Rename(state.phrase_index(), name) ); None }, @@ -194,7 +178,7 @@ impl Command for PhrasesCommand { Import(command) => match command { FileBrowserCommand::Begin => { *state.phrases_mode_mut() = Some( - PhrasesMode::Import(state.phrase_index(), FileBrowser::new(None)?) + PhraseListMode::Import(state.phrase_index(), FileBrowser::new(None)?) ); None }, @@ -203,7 +187,7 @@ impl Command for PhrasesCommand { Export(command) => match command { FileBrowserCommand::Begin => { *state.phrases_mode_mut() = Some( - PhrasesMode::Export(state.phrase_index(), FileBrowser::new(None)?) + PhraseListMode::Export(state.phrase_index(), FileBrowser::new(None)?) ); None }, @@ -229,10 +213,10 @@ impl HasPhrases for PhraseListModel { impl InputToCommand for PhrasesCommand { fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option { Some(match state.phrases_mode() { - Some(PhrasesMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?), - Some(PhrasesMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?), - Some(PhrasesMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?), - Some(PhrasesMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?), + Some(PhraseListMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?), + Some(PhraseListMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?), + Some(PhraseListMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?), + Some(PhraseListMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?), _ => to_phrases_command(state, input)? }) } diff --git a/crates/tek/src/tui/phrase_rename.rs b/crates/tek/src/tui/phrase_rename.rs index bd90e91b..c2bab07a 100644 --- a/crates/tek/src/tui/phrase_rename.rs +++ b/crates/tek/src/tui/phrase_rename.rs @@ -12,7 +12,7 @@ impl Command for PhraseRenameCommand { fn execute (self, state: &mut PhraseListModel) -> Perhaps { use PhraseRenameCommand::*; match state.phrases_mode_mut().clone() { - Some(PhrasesMode::Rename(phrase, ref mut old_name)) => match self { + Some(PhraseListMode::Rename(phrase, ref mut old_name)) => match self { Set(s) => { state.phrases()[phrase].write().unwrap().name = s.into(); return Ok(Some(Self::Set(old_name.clone()))) @@ -36,7 +36,7 @@ impl Command for PhraseRenameCommand { impl InputToCommand for PhraseRenameCommand { fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option { use KeyCode::{Char, Backspace, Enter, Esc}; - if let Some(PhrasesMode::Rename(_, ref old_name)) = state.phrases_mode() { + if let Some(PhraseListMode::Rename(_, ref old_name)) = state.phrases_mode() { Some(match from.event() { key!(Char(c)) => { let mut new_name = old_name.clone(); From 09a7d17121f5867e87dee55bd4f911f2bb4b3c88 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 12 Dec 2024 17:56:03 +0100 Subject: [PATCH 015/971] extract piano_horizontal --- crates/tek/src/api.rs | 1 - crates/tek/src/api/color.rs | 6 - crates/tek/src/api/player.rs | 3 +- crates/tek/src/core/color.rs | 17 +- crates/tek/src/tui.rs | 20 +- crates/tek/src/tui/app_sequencer.rs | 10 +- crates/tek/src/tui/phrase_editor.rs | 559 ++++++++----------------- crates/tek/src/tui/phrase_list.rs | 45 +- crates/tek/src/tui/phrase_select.rs | 83 ++-- crates/tek/src/tui/piano_horizontal.rs | 207 +++++++++ crates/tek/src/tui/status_bar.rs | 2 +- 11 files changed, 470 insertions(+), 483 deletions(-) delete mode 100644 crates/tek/src/api/color.rs create mode 100644 crates/tek/src/tui/piano_horizontal.rs diff --git a/crates/tek/src/api.rs b/crates/tek/src/api.rs index a3c2e9ca..c4047c4c 100644 --- a/crates/tek/src/api.rs +++ b/crates/tek/src/api.rs @@ -3,7 +3,6 @@ use crate::*; mod phrase; pub(crate) use phrase::*; mod jack; pub(crate) use self::jack::*; mod clip; pub(crate) use clip::*; -mod color; pub(crate) use color::*; mod clock; pub(crate) use clock::*; mod player; pub(crate) use player::*; mod scene; pub(crate) use scene::*; diff --git a/crates/tek/src/api/color.rs b/crates/tek/src/api/color.rs deleted file mode 100644 index c23e6ba7..00000000 --- a/crates/tek/src/api/color.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::*; - -pub trait HasColor { - fn color (&self) -> ItemColor; - fn color_mut (&self) -> &mut ItemColor; -} diff --git a/crates/tek/src/api/player.rs b/crates/tek/src/api/player.rs index a536716b..f1a6809e 100644 --- a/crates/tek/src/api/player.rs +++ b/crates/tek/src/api/player.rs @@ -25,7 +25,8 @@ pub trait HasPlayPhrase: HasClock { 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; + let length = phrase.read().unwrap().length.max(1); // prevent div0 on empty phrase + let elapsed = (elapsed as usize % length) as f64; Some(elapsed) } else { None diff --git a/crates/tek/src/core/color.rs b/crates/tek/src/core/color.rs index a86ac49f..99b9a48c 100644 --- a/crates/tek/src/core/color.rs +++ b/crates/tek/src/core/color.rs @@ -2,6 +2,11 @@ use crate::*; use rand::{thread_rng, distributions::uniform::UniformSampler}; pub use ratatui::prelude::Color; +pub trait HasColor { + fn color (&self) -> ItemColor; + fn color_mut (&self) -> &mut ItemColor; +} + /// A color in OKHSL and RGB representations. #[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemColor { @@ -48,15 +53,19 @@ impl ItemColor { self.okhsl.mix(other.okhsl, distance).into() } } +impl From for ItemPalette { + fn from (base: Color) -> Self { + Self::from(ItemColor::from(base)) + } +} impl From for ItemPalette { - fn from (base: ItemColor) -> Self { let mut light = base.okhsl.clone(); - light.lightness = (light.lightness * 1.2).min(Okhsl::::max_lightness()); + light.lightness = (light.lightness * 1.33).min(Okhsl::::max_lightness()); let mut lighter = light.clone(); - lighter.lightness = (lighter.lightness * 1.2).min(Okhsl::::max_lightness()); + lighter.lightness = (lighter.lightness * 1.33).min(Okhsl::::max_lightness()); let mut lightest = lighter.clone(); - lightest.lightness = (lightest.lightness * 1.2).min(Okhsl::::max_lightness()); + lightest.lightness = (lightest.lightness * 1.33).min(Okhsl::::max_lightness()); let mut dark = base.okhsl.clone(); dark.lightness = (dark.lightness * 0.75).max(Okhsl::::min_lightness()); diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 6011ae72..f1072b7d 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -9,19 +9,21 @@ mod engine_output; pub(crate) use engine_output::*; mod app_transport; pub(crate) use app_transport::*; mod app_sequencer; pub(crate) use app_sequencer::*; +mod app_groovebox; pub(crate) use app_groovebox::*; mod app_arranger; pub(crate) use app_arranger::*; //////////////////////////////////////////////////////// -mod status_bar; pub(crate) use status_bar::*; -mod file_browser; pub(crate) use file_browser::*; -mod phrase_editor; pub(crate) use phrase_editor::*; -mod phrase_length; pub(crate) use phrase_length::*; -mod phrase_rename; pub(crate) use phrase_rename::*; -mod phrase_list; pub(crate) use phrase_list::*; -mod phrase_player; pub(crate) use phrase_player::*; -mod phrase_select; pub(crate) use phrase_select::*; -mod port_select; pub(crate) use port_select::*; +mod status_bar; pub(crate) use status_bar::*; +mod file_browser; pub(crate) use file_browser::*; +mod phrase_editor; pub(crate) use phrase_editor::*; +mod piano_horizontal; pub(crate) use piano_horizontal::*; +mod phrase_length; pub(crate) use phrase_length::*; +mod phrase_rename; pub(crate) use phrase_rename::*; +mod phrase_list; pub(crate) use phrase_list::*; +mod phrase_player; pub(crate) use phrase_player::*; +mod phrase_select; pub(crate) use phrase_select::*; +mod port_select; pub(crate) use port_select::*; //////////////////////////////////////////////////////// diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index db3e575e..0dcf3c32 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -103,7 +103,7 @@ impl Audio for SequencerTui { render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1, Tui::fill_xy(SequencerStatusBar::from(self)), - Tui::split_right(false, 20, + Tui::split_right(false, if self.size.w() > 60 { 20 } else if self.size.w() > 40 { 15 } else { 10 }, Tui::fixed_x(20, Tui::split_down(false, 4, col!([ PhraseSelector::play_phrase(&self.player), PhraseSelector::next_phrase(&self.player), @@ -273,6 +273,10 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option PhraseEditMode::Scroll, })), + // Enqueue currently edited phrase + key!(Char('q')) => + Enqueue(Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())), + // E: Toggle between editing currently playing or other phrase key!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() { let editing_phrase = state.editor.phrase.as_ref().map(|p|p.read().unwrap().clone()); @@ -302,10 +306,6 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?), - // Enqueue currently edited phrase - key!(Enter) => - Enqueue(Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())), - // Delegate to focused control: _ => match state.focus { PhraseEditor => Editor(PhraseCommand::input_to_command(&state.editor, input)?), diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 2b033038..9672b742 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -9,26 +9,55 @@ pub trait HasEditor { /// Contains state for viewing and editing a phrase pub struct PhraseEditorModel { /// Phrase being played - pub(crate) phrase: Option>>, - /// Length of note that will be inserted, in pulses - pub(crate) note_len: usize, - /// Notes currently held at input - pub(crate) notes_in: Arc>, - /// Notes currently held at output - pub(crate) notes_out: Arc>, - /// Current position of global playhead - pub(crate) now: Arc, - /// Width and height of notes area at last render - pub(crate) size: Measure, - + pub(crate) phrase: Option>>, + pub(crate) view_mode: Box, + pub(crate) edit_mode: PhraseEditMode, + // Lowest note displayed pub(crate) note_lo: AtomicUsize, + /// Note coordinate of cursor pub(crate) note_point: AtomicUsize, - + /// Length of note that will be inserted, in pulses + pub(crate) note_len: usize, + /// Notes currently held at input + pub(crate) notes_in: Arc>, + /// Notes currently held at output + pub(crate) notes_out: Arc>, + /// Earliest time displayed pub(crate) time_start: AtomicUsize, + /// Time coordinate of cursor pub(crate) time_point: AtomicUsize, + /// Current position of global playhead + pub(crate) now: Arc, + /// Width and height of notes area at last render + pub(crate) size: Measure, +} - pub(crate) edit_mode: PhraseEditMode, - pub(crate) view_mode: Box, +#[derive(Copy, Clone, Debug)] +pub enum PhraseEditMode { + Note, + Scroll, +} + +pub trait PhraseViewMode: Debug + Send + Sync { + fn show (&mut self, phrase: Option<&Phrase>, note_len: usize); + fn time_zoom (&self) -> Option; + fn set_time_zoom (&mut self, time_zoom: Option); + fn buffer_width (&self, phrase: &Phrase) -> usize; + fn buffer_height (&self, phrase: &Phrase) -> usize; + fn render_keys (&self, + to: &mut TuiOutput, color: Color, point: Option, range: (usize, usize)); + fn render_notes (&self, + to: &mut TuiOutput, time_start: usize, note_hi: usize); + fn render_cursor ( + &self, + to: &mut TuiOutput, + time_point: usize, + time_start: usize, + note_point: usize, + note_len: usize, + note_hi: usize, + note_lo: usize, + ); } impl std::fmt::Debug for PhraseEditorModel { @@ -105,6 +134,121 @@ impl PhraseEditorModel { } } +#[derive(Clone, Debug)] +pub enum PhraseCommand { + // TODO: 1-9 seek markers that by default start every 8th of the phrase + AppendNote, + PutNote, + SetNoteCursor(usize), + SetNoteLength(usize), + SetNoteScroll(usize), + SetTimeCursor(usize), + SetTimeScroll(usize), + SetTimeZoom(Option), + Show(Option>>), + SetEditMode(PhraseEditMode), + ToggleDirection, +} + +impl InputToCommand for PhraseCommand { + fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option { + use PhraseCommand::*; + use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right}; + let note_lo = state.note_lo.load(Ordering::Relaxed); + let note_point = state.note_point.load(Ordering::Relaxed); + let time_start = state.time_start.load(Ordering::Relaxed); + let time_point = state.time_point.load(Ordering::Relaxed); + let time_zoom = state.view_mode.time_zoom(); + let length = state.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + let note_len = state.note_len; + Some(match from.event() { + key!(Char('`')) => ToggleDirection, + key!(Esc) => SetEditMode(PhraseEditMode::Scroll), + key!(Char('z')) => SetTimeZoom(None), + key!(Char('-')) => SetTimeZoom(time_zoom.map(next_note_length)), + key!(Char('_')) => SetTimeZoom(time_zoom.map(next_note_length)), + key!(Char('=')) => SetTimeZoom(time_zoom.map(prev_note_length)), + key!(Char('+')) => SetTimeZoom(time_zoom.map(prev_note_length)), + key!(Char('a')) => AppendNote, + key!(Char('s')) => PutNote, + // TODO: no triplet/dotted + key!(Char(',')) => SetNoteLength(prev_note_length(state.note_len)), + key!(Char('.')) => SetNoteLength(next_note_length(state.note_len)), + // TODO: with triplet/dotted + key!(Char('<')) => SetNoteLength(prev_note_length(state.note_len)), + key!(Char('>')) => SetNoteLength(next_note_length(state.note_len)), + // TODO: '/' set triplet, '?' set dotted + _ => match state.edit_mode { + PhraseEditMode::Scroll => match from.event() { + key!(Char('e')) => SetEditMode(PhraseEditMode::Note), + + key!(Up) => SetNoteScroll(note_lo + 1), + key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)), + key!(PageUp) => SetNoteScroll(note_lo + 3), + key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)), + + key!(Left) => SetTimeScroll(time_start.saturating_sub(note_len)), + key!(Right) => SetTimeScroll(time_start + note_len), + key!(Shift-Left) => SetTimeScroll(time_point.saturating_sub(time_zoom.unwrap())), + key!(Shift-Right) => SetTimeScroll((time_point + time_zoom.unwrap()) % length), + _ => return None + }, + PhraseEditMode::Note => match from.event() { + key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll), + + key!(Up) => SetNoteCursor(note_point + 1), + key!(Down) => SetNoteCursor(note_point.saturating_sub(1)), + key!(PageUp) => SetNoteCursor(note_point + 3), + key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)), + + key!(Shift-Up) => SetNoteScroll(note_lo + 1), + key!(Shift-Down) => SetNoteScroll(note_lo.saturating_sub(1)), + key!(Shift-PageUp) => SetNoteScroll(note_point + 3), + key!(Shift-PageDown) => SetNoteScroll(note_point.saturating_sub(3)), + + key!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)), + key!(Right) => SetTimeCursor((time_point + note_len) % length), + + key!(Shift-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom.unwrap())), + key!(Shift-Right) => SetTimeCursor((time_point + time_zoom.unwrap()) % length), + + _ => return None + }, + } + }) + } +} + +impl Command for PhraseCommand { + fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { + use PhraseCommand::*; + match self { + Show(phrase) => { state.show_phrase(phrase); }, + SetEditMode(mode) => { state.edit_mode = mode; } + PutNote => { state.put_note(); }, + AppendNote => { state.put_note(); + state.time_cursor_advance(); }, + SetTimeZoom(zoom) => { state.view_mode.set_time_zoom(zoom); + state.show_phrase(state.phrase.clone()); }, + SetTimeScroll(time) => { state.time_start.store(time, Ordering::Relaxed); }, + SetTimeCursor(time) => { state.time_point.store(time, Ordering::Relaxed); }, + SetNoteLength(time) => { state.note_len = time; }, + SetNoteScroll(note) => { state.note_lo.store(note, Ordering::Relaxed); }, + SetNoteCursor(note) => { + let note = 127.min(note); + let start = state.note_lo.load(Ordering::Relaxed); + state.note_point.store(note, Ordering::Relaxed); + if note < start { + state.note_lo.store(note, Ordering::Relaxed); + } + }, + + _ => todo!("{:?}", self) + } + Ok(None) + } +} + pub struct PhraseView<'a> { note_point: usize, note_range: (usize, usize), @@ -112,22 +256,30 @@ pub struct PhraseView<'a> { time_point: usize, note_len: usize, phrase: &'a Option>>, - view_mode: &'a Box, + view_mode: &'a Box, now: &'a Arc, size: &'a Measure, focused: bool, entered: bool, } + render!(|self: PhraseView<'a>|{ let bg = if self.focused { TuiTheme::g(32) } else { Color::Reset }; let fg = self.phrase.as_ref() .map(|p|p.read().unwrap().color.clone()) .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); Tui::bg(bg, Tui::split_up(false, 2, - Tui::bg(fg.dark.rgb, lay!([PhraseTimeline(&self, fg), PhraseViewStats(&self, fg),])), - Split::right(false, 5, PhraseKeys(&self, fg), lay!([PhraseNotes(&self, fg), PhraseCursor(&self), ])), + Tui::bg(fg.dark.rgb, lay!([ + PhraseTimeline(&self, fg), + PhraseViewStats(&self, fg), + ])), + Split::right(false, 5, PhraseKeys(&self, fg), lay!([ + PhraseNotes(&self, fg), + PhraseCursor(&self), + ])), )) }); + impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { fn from (state: &'a T) -> Self { let editor = state.editor(); @@ -211,374 +363,3 @@ render!(|self: PhraseCursor<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|Ok( self.0.note_range.0, ) )))); - -#[derive(Copy, Clone, Debug)] -pub enum PhraseEditMode { - Note, - Scroll, -} - -pub trait PhraseViewMode { - fn show (&mut self, phrase: Option<&Phrase>, note_len: usize); - fn time_zoom (&self) -> Option; - fn set_time_zoom (&mut self, time_zoom: Option); - fn buffer_width (&self, phrase: &Phrase) -> usize; - fn buffer_height (&self, phrase: &Phrase) -> usize; - fn render_keys (&self, - to: &mut TuiOutput, color: Color, point: Option, range: (usize, usize)); - fn render_notes (&self, - to: &mut TuiOutput, time_start: usize, note_hi: usize); - fn render_cursor ( - &self, - to: &mut TuiOutput, - time_point: usize, - time_start: usize, - note_point: usize, - note_len: usize, - note_hi: usize, - note_lo: usize, - ); -} - -pub struct PianoHorizontal { - time_zoom: Option, - note_zoom: PhraseViewNoteZoom, - buffer: BigBuffer, -} - -#[derive(Copy, Clone, Debug)] -pub enum PhraseViewNoteZoom { - N(usize), - Half, - Octant, -} - -impl PhraseViewMode for PianoHorizontal { - fn time_zoom (&self) -> Option { - self.time_zoom - } - fn set_time_zoom (&mut self, time_zoom: Option) { - self.time_zoom = time_zoom - } - fn show (&mut self, phrase: Option<&Phrase>, note_len: usize) { - if let Some(phrase) = phrase { - self.buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase)); - draw_piano_horizontal_bg(&mut self.buffer, phrase, self.time_zoom.unwrap(), note_len); - draw_piano_horizontal_fg(&mut self.buffer, phrase, self.time_zoom.unwrap()); - } else { - self.buffer = Default::default(); - } - } - fn buffer_width (&self, phrase: &Phrase) -> usize { - phrase.length / self.time_zoom.unwrap() - } - /// Determine the required height to render the phrase. - fn buffer_height (&self, phrase: &Phrase) -> usize { - match self.note_zoom { - PhraseViewNoteZoom::Half => 64, - PhraseViewNoteZoom::N(n) => 128*n, - _ => unimplemented!() - } - } - fn render_notes ( - &self, - target: &mut TuiOutput, - time_start: usize, - note_hi: usize, - ) { - let [x0, y0, w, h] = target.area().xywh(); - let source = &self.buffer; - let target = &mut target.buffer; - for (x, target_x) in (x0..x0+w).enumerate() { - for (y, target_y) in (y0..y0+h).enumerate() { - if y > note_hi { - break - } - let source_x = time_start + x; - let source_y = note_hi - y; - // TODO: enable loop rollover: - //let source_x = (time_start + x) % source.width.max(1); - //let source_y = (note_hi - y) % source.height.max(1); - if source_x < source.width && source_y < source.height { - let target_cell = target.get_mut(target_x, target_y); - if let Some(source_cell) = source.get(source_x, source_y) { - *target_cell = source_cell.clone(); - } - } - } - } - } - fn render_keys ( - &self, - to: &mut TuiOutput, - color: Color, - point: Option, - (note_lo, note_hi): (usize, usize) - ) { - let [x, y0, _, _] = to.area().xywh(); - let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0))); - let note_off_style = Some(Style::default().fg(TuiTheme::g(160))); - let note_on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color).bold()); - for (y, note) in (note_lo..=note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) { - let key = match note % 12 { - 11 => "████▌", - 10 => " ", - 9 => "████▌", - 8 => " ", - 7 => "████▌", - 6 => " ", - 5 => "████▌", - 4 => "████▌", - 3 => " ", - 2 => "████▌", - 1 => " ", - 0 => "████▌", - _ => unreachable!(), - }; - to.blit(&key, x, y, key_style); - - if Some(note) == point { - to.blit(&format!("{:<5}", to_note_name(note)), x, y, note_on_style) - } else { - to.blit(&to_note_name(note), x, y, note_off_style) - }; - } - } - fn render_cursor ( - &self, - to: &mut TuiOutput, - time_point: usize, - time_start: usize, - note_point: usize, - note_len: usize, - note_hi: usize, - note_lo: usize, - ) { - let time_zoom = self.time_zoom.unwrap(); - let [x0, y0, w, _] = to.area().xywh(); - let style = Some(Style::default().fg(Color::Rgb(0,255,0))); - for (y, note) in (note_lo..=note_hi).rev().enumerate() { - if note == note_point { - for x in 0..w { - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_point && time_point < time_2 { - to.blit(&"█", x0 + x as u16, y0 + y as u16, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (x0 + x + 1)..(x0 + x + tail) { - to.blit(&"▂", x_tail, y0 + y as u16, style); - } - break - } - } - break - } - } - } -} - -/// Draw the piano roll using full blocks on note on and half blocks on legato: █▄ █▄ █▄ -fn draw_piano_horizontal ( - target: &mut BigBuffer, - phrase: &Phrase, - time_zoom: usize, - note_len: usize, -) { - draw_piano_horizontal_bg(target, phrase, time_zoom, note_len); - draw_piano_horizontal_fg(target, phrase, time_zoom); -} - -fn draw_piano_horizontal_bg ( - target: &mut BigBuffer, - phrase: &Phrase, - time_zoom: usize, - note_len: usize, -) { - for (y, note) in (0..127).rev().enumerate() { - for (x, time) in (0..target.width).map(|x|(x, x*time_zoom)) { - let cell = target.get_mut(x, y).unwrap(); - cell.set_bg(phrase.color.darkest.rgb); - cell.set_fg(phrase.color.darker.rgb); - cell.set_char(if time % 384 == 0 { - '│' - } else if time % 96 == 0 { - '╎' - } else if time % note_len == 0 { - '┊' - } else if (127 - note) % 12 == 1 { - '=' - } else { - '·' - }); - } - } -} - -fn draw_piano_horizontal_fg ( - target: &mut BigBuffer, - phrase: &Phrase, - time_zoom: usize, -) { - let style = Style::default().fg(phrase.color.lightest.rgb);//.bg(Color::Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() { - - for (y, note) in (0..127).rev().enumerate() { - let cell = target.get_mut(x, note).unwrap(); - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); - } - } - - let time_end = time_start + time_zoom; - for time in time_start..time_end { - for event in phrase.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - let cell = target.get_mut(x, note).unwrap(); - cell.set_char('█'); - cell.set_style(style); - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } -} - -#[derive(Clone, Debug)] -pub enum PhraseCommand { - // TODO: 1-9 seek markers that by default start every 8th of the phrase - AppendNote, - PutNote, - SetNoteCursor(usize), - SetNoteLength(usize), - SetNoteScroll(usize), - SetTimeCursor(usize), - SetTimeScroll(usize), - SetTimeZoom(Option), - Show(Option>>), - SetEditMode(PhraseEditMode), - ToggleDirection, -} - -impl InputToCommand for PhraseCommand { - fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option { - use PhraseCommand::*; - use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right}; - let note_lo = state.note_lo.load(Ordering::Relaxed); - let note_point = state.note_point.load(Ordering::Relaxed); - let time_start = state.time_start.load(Ordering::Relaxed); - let time_point = state.time_point.load(Ordering::Relaxed); - let time_zoom = state.view_mode.time_zoom(); - let length = state.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - let note_len = state.note_len; - Some(match from.event() { - key!(Char('`')) => ToggleDirection, - key!(Esc) => SetEditMode(PhraseEditMode::Scroll), - key!(Char('z')) => SetTimeZoom(None), - key!(Char('-')) => SetTimeZoom(time_zoom.map(next_note_length)), - key!(Char('_')) => SetTimeZoom(time_zoom.map(next_note_length)), - key!(Char('=')) => SetTimeZoom(time_zoom.map(prev_note_length)), - key!(Char('+')) => SetTimeZoom(time_zoom.map(prev_note_length)), - key!(Char('a')) => AppendNote, - key!(Char('s')) => PutNote, - // TODO: no triplet/dotted - key!(Char(',')) => SetNoteLength(prev_note_length(state.note_len)), - key!(Char('.')) => SetNoteLength(next_note_length(state.note_len)), - // TODO: with triplet/dotted - key!(Char('<')) => SetNoteLength(prev_note_length(state.note_len)), - key!(Char('>')) => SetNoteLength(next_note_length(state.note_len)), - // TODO: '/' set triplet, '?' set dotted - _ => match state.edit_mode { - PhraseEditMode::Scroll => match from.event() { - key!(Char('e')) => SetEditMode(PhraseEditMode::Note), - key!(Up) => SetNoteScroll(note_lo + 1), - key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)), - key!(PageUp) => SetNoteScroll(note_lo + 3), - key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)), - key!(Left) => SetTimeScroll(time_start.saturating_sub(time_zoom.unwrap())), - key!(Right) => SetTimeScroll(time_start + time_zoom.unwrap()), - _ => return None - }, - PhraseEditMode::Note => match from.event() { - key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll), - key!(Up) => SetNoteCursor(note_point + 1), - key!(Down) => SetNoteCursor(note_point.saturating_sub(1)), - key!(PageUp) => SetNoteCursor(note_point + 3), - key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)), - key!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)), - key!(Right) => SetTimeCursor((time_point + note_len) % length), - key!(Shift-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom.unwrap())), - key!(Shift-Right) => SetTimeCursor((time_point + time_zoom.unwrap()) % length), - _ => return None - }, - } - }) - } -} - -impl Command for PhraseCommand { - fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { - use PhraseCommand::*; - Ok(match self { - Show(phrase) => { - state.show_phrase(phrase); - None - }, - ToggleDirection => { - todo!() - }, - SetEditMode(mode) => { - state.edit_mode = mode; - None - } - AppendNote => { - state.put_note(); - state.time_cursor_advance(); - None - }, - PutNote => { - state.put_note(); - None - }, - SetTimeCursor(time) => { - state.time_point.store(time, Ordering::Relaxed); - None - }, - SetTimeScroll(time) => { - state.time_start.store(time, Ordering::Relaxed); - None - }, - SetTimeZoom(zoom) => { - state.view_mode.set_time_zoom(zoom); - state.show_phrase(state.phrase.clone()); - None - }, - SetNoteScroll(note) => { - state.note_lo.store(note, Ordering::Relaxed); - None - }, - SetNoteLength(time) => { - state.note_len = time; - None - }, - SetNoteCursor(note) => { - let note = 127.min(note); - let start = state.note_lo.load(Ordering::Relaxed); - state.note_point.store(note, Ordering::Relaxed); - if note < start { - state.note_lo.store(note, Ordering::Relaxed); - } - None - }, - }) - } -} diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 7c301d55..d014e134 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -1,11 +1,9 @@ use super::*; use crate::{ + tui::phrase_rename::PhraseRenameCommand as Rename, + tui::phrase_length::PhraseLengthCommand as Length, + tui::file_browser::FileBrowserCommand as Browse, api::PhrasePoolCommand as Pool, - tui::{ - phrase_rename::PhraseRenameCommand as Rename, - phrase_length::PhraseLengthCommand as Length, - file_browser::FileBrowserCommand as Browse, - } }; #[derive(Debug)] @@ -44,10 +42,22 @@ impl Default for PhraseListModel { } } -impl PhraseListModel { - pub(crate) fn phrase (&self) -> &Arc> { +impl HasPhrases for PhraseListModel { + fn phrases (&self) -> &Vec>> { + &self.phrases + } + fn phrases_mut (&mut self) -> &mut Vec>> { + &mut self.phrases + } +} + +impl HasPhrase for PhraseListModel { + fn phrase (&self) -> &Arc> { &self.phrases[self.phrase_index()] } +} + +impl PhraseListModel { pub(crate) fn phrase_index (&self) -> usize { self.phrase.load(Ordering::Relaxed) } @@ -142,11 +152,17 @@ render!(|self: PhraseListView<'a>|{ #[derive(Clone, PartialEq, Debug)] pub enum PhrasesCommand { - Select(usize), + /// Update the contents of the phrase pool Phrase(Pool), + /// Select a phrase from the phrase pool + Select(usize), + /// Rename a phrase Rename(Rename), + /// Change the length of a phrase Length(Length), + /// Import from file Import(Browse), + /// Export to file Export(Browse), } @@ -201,15 +217,6 @@ impl Command for PhrasesCommand { } } -impl HasPhrases for PhraseListModel { - fn phrases (&self) -> &Vec>> { - &self.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.phrases - } -} - impl InputToCommand for PhrasesCommand { fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option { Some(match state.phrases_mode() { @@ -233,10 +240,10 @@ fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option Cmd::Import(Browse::Begin), key!(Char('x')) => Cmd::Export(Browse::Begin), key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())), - key!(Up) | key!(Char('[')) => Cmd::Select( + key!(Char('[')) | key!(Up) => Cmd::Select( index.overflowing_sub(1).0.min(state.phrases().len() - 1) ), - key!(Down) | key!(Char(']')) => Cmd::Select( + key!(Char(']')) | key!(Down) => Cmd::Select( index.saturating_add(1) % state.phrases().len() ), key!(Char('<')) => if index > 1 { diff --git a/crates/tek/src/tui/phrase_select.rs b/crates/tek/src/tui/phrase_select.rs index f2fbe626..c23c234f 100644 --- a/crates/tek/src/tui/phrase_select.rs +++ b/crates/tek/src/tui/phrase_select.rs @@ -2,55 +2,38 @@ use crate::*; pub struct PhraseSelector { pub(crate) title: &'static str, - pub(crate) phrase: Option<(Moment, Option>>)>, + pub(crate) name: String, + pub(crate) color: ItemPalette, pub(crate) time: String, } // TODO: Display phrases always in order of appearance -render!(|self: PhraseSelector|{ - 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 = TuiTheme::g(200); - Tui::fixed_y(2, lay!(move|add|{ - //if phrase.is_none() { - //add(&Tui::fill_x(border))?; - //} - add(&Tui::push_x(1, Tui::fg(title_color, *title)))?; - 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(()) - })) -}); +render!(|self: PhraseSelector|Tui::fixed_y(1, lay!(move|add|{ + add(&Tui::push_x(1, Tui::fg(TuiTheme::g(240), self.title)))?; + add(&Tui::bg(self.color.base.rgb, Tui::fill_x(Tui::inset_x(1, Tui::fill_x(Tui::at_e(Tui::fg(self.color.lightest.rgb, &self.time)))))))?; + Ok(()) +}))); impl PhraseSelector { // beats elapsed pub fn play_phrase (state: &T) -> Self { - let phrase = state.play_phrase().clone(); - Self { - title: "Now:", - time: if let Some(elapsed) = state.pulses_since_start_looped() { - format!("+{:>}", state.clock().timebase.format_beats_0(elapsed)) - } else { - String::from("") - }, - phrase, - } + let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() { + let Phrase { ref name, color, .. } = *phrase.read().unwrap(); + (name.clone(), color) + } else { + ("".to_string(), ItemPalette::from(TuiTheme::g(64))) + }; + let time = if let Some(elapsed) = state.pulses_since_start_looped() { + format!("+{:>}", state.clock().timebase.format_beats_0(elapsed)) + } else { + String::from("") + }; + Self { title: "Now:", time, name, color, } } // beats until switchover pub fn next_phrase (state: &T) -> Self { - let phrase = state.next_phrase().clone(); - Self { - title: "Next:", - time: phrase.as_ref().map(|(t, _)|{ + let (time, name, color) = if let Some((t, Some(phrase))) = state.next_phrase() { + let Phrase { ref name, color, .. } = *phrase.read().unwrap(); + let time = { let target = t.pulse.get(); let current = state.clock().playhead.pulse.get(); if target > current { @@ -59,16 +42,20 @@ impl PhraseSelector { } else { String::new() } - }).unwrap_or(String::from("")), - phrase, - } + }; + (time, name.clone(), color) + } else { + ("".into(), "".into(), TuiTheme::g(64).into()) + }; + Self { title: "Next:", time, name, color, } } pub fn edit_phrase (phrase: &Option>>) -> Self { - let phrase = phrase.clone(); - Self { - title: "Edit:", - time: phrase.as_ref().map(|p|format!("{}", p.read().unwrap().length)).unwrap_or(String::new()), - phrase: Some((Moment::default(), phrase)), - } + let (time, name, color) = if let Some(phrase) = phrase { + let phrase = phrase.read().unwrap(); + (format!("{}", phrase.length), phrase.name.clone(), phrase.color) + } else { + ("".to_string(), "".to_string(), ItemPalette::from(TuiTheme::g(64))) + }; + Self { title: "Editing:", time, name, color } } } diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs new file mode 100644 index 00000000..82b5322b --- /dev/null +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -0,0 +1,207 @@ +use crate::*; +use super::*; + +pub struct PianoHorizontal { + pub(crate) time_zoom: Option, + pub(crate) note_zoom: PhraseViewNoteZoom, + pub(crate) buffer: BigBuffer, +} + +impl std::fmt::Debug for PianoHorizontal { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("PianoHorizontal") + .field("time_zoom", &self.time_zoom) + .field("note_zoom", &self.note_zoom) + .field("buffer", &format!("{}x{}", self.buffer.width, self.buffer.height)) + .finish() + } +} + +#[derive(Copy, Clone, Debug)] +pub enum PhraseViewNoteZoom { + N(usize), + Half, + Octant, +} + +impl PhraseViewMode for PianoHorizontal { + fn time_zoom (&self) -> Option { + self.time_zoom + } + fn set_time_zoom (&mut self, time_zoom: Option) { + self.time_zoom = time_zoom + } + fn show (&mut self, phrase: Option<&Phrase>, note_len: usize) { + if let Some(phrase) = phrase { + self.buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase)); + draw_piano_horizontal_bg(&mut self.buffer, phrase, self.time_zoom.unwrap(), note_len); + draw_piano_horizontal_fg(&mut self.buffer, phrase, self.time_zoom.unwrap()); + } else { + self.buffer = Default::default(); + } + } + fn buffer_width (&self, phrase: &Phrase) -> usize { + phrase.length / self.time_zoom.unwrap() + } + /// Determine the required height to render the phrase. + fn buffer_height (&self, phrase: &Phrase) -> usize { + match self.note_zoom { + PhraseViewNoteZoom::Half => 64, + PhraseViewNoteZoom::N(n) => 128*n, + _ => unimplemented!() + } + } + fn render_notes ( + &self, + target: &mut TuiOutput, + time_start: usize, + note_hi: usize, + ) { + let [x0, y0, w, h] = target.area().xywh(); + let source = &self.buffer; + let target = &mut target.buffer; + for (x, target_x) in (x0..x0+w).enumerate() { + for (y, target_y) in (y0..y0+h).enumerate() { + if y > note_hi { + break + } + let source_x = time_start + x; + let source_y = note_hi - y; + // TODO: enable loop rollover: + //let source_x = (time_start + x) % source.width.max(1); + //let source_y = (note_hi - y) % source.height.max(1); + if source_x < source.width && source_y < source.height { + let target_cell = target.get_mut(target_x, target_y); + if let Some(source_cell) = source.get(source_x, source_y) { + *target_cell = source_cell.clone(); + } + } + } + } + } + fn render_keys ( + &self, + to: &mut TuiOutput, + color: Color, + point: Option, + (note_lo, note_hi): (usize, usize) + ) { + let [x, y0, _, _] = to.area().xywh(); + let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0))); + let note_off_style = Some(Style::default().fg(TuiTheme::g(160))); + let note_on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color).bold()); + for (y, note) in (note_lo..=note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) { + let key = match note % 12 { + 11 => "████▌", + 10 => " ", + 9 => "████▌", + 8 => " ", + 7 => "████▌", + 6 => " ", + 5 => "████▌", + 4 => "████▌", + 3 => " ", + 2 => "████▌", + 1 => " ", + 0 => "████▌", + _ => unreachable!(), + }; + to.blit(&key, x, y, key_style); + + if Some(note) == point { + to.blit(&format!("{:<5}", to_note_name(note)), x, y, note_on_style) + } else { + to.blit(&to_note_name(note), x, y, note_off_style) + }; + } + } + fn render_cursor ( + &self, + to: &mut TuiOutput, + time_point: usize, + time_start: usize, + note_point: usize, + note_len: usize, + note_hi: usize, + note_lo: usize, + ) { + let time_zoom = self.time_zoom.unwrap(); + let [x0, y0, w, h] = to.area().xywh(); + let style = Some(Style::default().fg(Color::Rgb(0,255,0))); + for (y, note) in (note_lo..=note_hi).rev().enumerate() { + if note == note_point { + for x in 0..w { + let time_1 = time_start + x as usize * time_zoom; + let time_2 = time_1 + time_zoom; + if time_1 <= time_point && time_point < time_2 { + to.blit(&"█", x0 + x as u16, y0 + y as u16, style); + let tail = note_len as u16 / time_zoom as u16; + for x_tail in (x0 + x + 1)..(x0 + x + tail) { + to.blit(&"▂", x_tail, y0 + y as u16, style); + } + break + } + } + break + } + } + } +} + +/// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄ +fn draw_piano_horizontal_bg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize, note_len: usize) { + for (y, note) in (0..127).rev().enumerate() { + for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { + let cell = buf.get_mut(x, y).unwrap(); + cell.set_bg(phrase.color.darkest.rgb); + cell.set_fg(phrase.color.darker.rgb); + cell.set_char(if time % 384 == 0 { + '│' + } else if time % 96 == 0 { + '╎' + } else if time % note_len == 0 { + '┊' + } else if (127 - note) % 12 == 1 { + '=' + } else { + '·' + }); + } + } +} + +/// Draw the piano roll background using full blocks on note on and half blocks on legato: █▄ █▄ █▄ +fn draw_piano_horizontal_fg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize) { + let style = Style::default().fg(phrase.color.lightest.rgb);//.bg(Color::Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..phrase.length).step_by(zoom).enumerate() { + + for (y, note) in (0..127).rev().enumerate() { + let cell = buf.get_mut(x, note).unwrap(); + if notes_on[note] { + cell.set_char('▂'); + cell.set_style(style); + } + } + + let time_end = time_start + zoom; + for time in time_start..time_end { + for event in phrase.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + let cell = buf.get_mut(x, note).unwrap(); + cell.set_char('█'); + cell.set_style(style); + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } + + } +} diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs index 70cfa678..ff6660ed 100644 --- a/crates/tek/src/tui/status_bar.rs +++ b/crates/tek/src/tui/status_bar.rs @@ -116,7 +116,7 @@ impl From<&SequencerTui> for SequencerStatusBar { } render!(|self: SequencerStatusBar|{ - lay!(|add|if self.width > 60 { + lay!(|add|if self.width > 40 { add(&Tui::fill_x(Tui::fixed_y(1, lay!([ Tui::fill_x(Tui::at_w(SequencerMode::from(self))), Tui::fill_x(Tui::at_e(SequencerStats::from(self))), From 16618141645fc8994943cf01852af0e210fe240e Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 12 Dec 2024 18:21:23 +0100 Subject: [PATCH 016/971] wip: reenable sampler --- crates/tek/src/api.rs | 15 +- .../tek/src/api/_todo_api_sampler_sample.rs | 72 ----- crates/tek/src/api/_todo_api_sampler_voice.rs | 30 -- .../api/{_todo_api_sampler.rs => sampler.rs} | 120 ++++++- crates/tek/src/tui.rs | 1 + crates/tek/src/tui/_todo_tui_sampler_cmd.rs | 52 --- crates/tek/src/tui/app_groovebox.rs | 30 ++ .../{_todo_tui_sampler.rs => app_sampler.rs} | 296 ++++++++++-------- crates/tek/src/tui/app_sequencer.rs | 22 +- 9 files changed, 327 insertions(+), 311 deletions(-) delete mode 100644 crates/tek/src/api/_todo_api_sampler_sample.rs delete mode 100644 crates/tek/src/api/_todo_api_sampler_voice.rs rename crates/tek/src/api/{_todo_api_sampler.rs => sampler.rs} (57%) delete mode 100644 crates/tek/src/tui/_todo_tui_sampler_cmd.rs rename crates/tek/src/tui/{_todo_tui_sampler.rs => app_sampler.rs} (70%) diff --git a/crates/tek/src/api.rs b/crates/tek/src/api.rs index c4047c4c..88951c7a 100644 --- a/crates/tek/src/api.rs +++ b/crates/tek/src/api.rs @@ -1,9 +1,10 @@ use crate::*; -mod phrase; pub(crate) use phrase::*; -mod jack; pub(crate) use self::jack::*; -mod clip; pub(crate) use clip::*; -mod clock; pub(crate) use clock::*; -mod player; pub(crate) use player::*; -mod scene; pub(crate) use scene::*; -mod track; pub(crate) use track::*; +mod phrase; pub(crate) use phrase::*; +mod jack; pub(crate) use self::jack::*; +mod clip; pub(crate) use clip::*; +mod clock; pub(crate) use clock::*; +mod player; pub(crate) use player::*; +mod scene; pub(crate) use scene::*; +mod track; pub(crate) use track::*; +mod sampler; pub(crate) use sampler::*; diff --git a/crates/tek/src/api/_todo_api_sampler_sample.rs b/crates/tek/src/api/_todo_api_sampler_sample.rs deleted file mode 100644 index aa85676e..00000000 --- a/crates/tek/src/api/_todo_api_sampler_sample.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::*; - -/// A sound sample. -#[derive(Default, Debug)] -pub struct Sample { - pub name: String, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, -} - -impl Sample { - pub fn new (name: &str, start: usize, end: usize, channels: Vec>) -> Self { - Self { name: name.to_string(), start, end, channels, rate: None } - } - pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { - Voice { - sample: sample.clone(), - after, - position: sample.read().unwrap().start, - velocity: velocity.as_int() as f32 / 127.0, - } - } - pub fn from_edn <'e> (jack: &Arc>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option, Arc>)> { - let mut name = String::new(); - let mut file = String::new(); - let mut midi = None; - let mut start = 0usize; - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) { - file = String::from(*f); - } - if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { - start = *i as usize; - } - if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { - midi = Some(u7::from(*m as u8)); - } - }, - _ => panic!("unexpected in sample {name}"), - }); - let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; - Ok((midi, Arc::new(RwLock::new(Self { - name: name.into(), - start, - end, - channels: data, - rate: None - })))) - } - - /// Read WAV from file - pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path(src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - Ok((end, data)) - } -} diff --git a/crates/tek/src/api/_todo_api_sampler_voice.rs b/crates/tek/src/api/_todo_api_sampler_voice.rs deleted file mode 100644 index 1dd3ba4a..00000000 --- a/crates/tek/src/api/_todo_api_sampler_voice.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::*; - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] -pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after = self.after - 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position = self.position + 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity, - sample.channels[0][position] * self.velocity, - ]) - } - None - } -} diff --git a/crates/tek/src/api/_todo_api_sampler.rs b/crates/tek/src/api/sampler.rs similarity index 57% rename from crates/tek/src/api/_todo_api_sampler.rs rename to crates/tek/src/api/sampler.rs index 2976c08a..260fbf53 100644 --- a/crates/tek/src/api/_todo_api_sampler.rs +++ b/crates/tek/src/api/sampler.rs @@ -1,5 +1,18 @@ use crate::*; +pub struct SamplerAudio { + model: Arc> +} + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] +pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + /// The sampler plugin plays sounds. #[derive(Debug)] pub struct Sampler { @@ -14,6 +27,27 @@ pub struct Sampler { pub output_gain: f32 } +/// A sound sample. +#[derive(Default, Debug)] +pub struct Sample { + pub name: String, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, +} + +/// Load sample from WAV and assign to MIDI note. +#[macro_export] macro_rules! sample { + ($note:expr, $name:expr, $src:expr) => {{ + let (end, data) = read_sample_data($src)?; + ( + u7::from_int_lossy($note).into(), + Sample::new($name, 0, end, data).into() + ) + }}; +} + impl Sampler { pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { let mut name = String::new(); @@ -56,10 +90,6 @@ impl Sampler { } } -pub struct SamplerAudio { - model: Arc> -} - impl From<&Arc>> for SamplerAudio { fn from (model: &Arc>) -> Self { Self { model: model.clone() } @@ -133,3 +163,85 @@ impl SamplerAudio { } } + + +impl Sample { + pub fn new (name: &str, start: usize, end: usize, channels: Vec>) -> Self { + Self { name: name.to_string(), start, end, channels, rate: None } + } + pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { + Voice { + sample: sample.clone(), + after, + position: sample.read().unwrap().start, + velocity: velocity.as_int() as f32 / 127.0, + } + } + pub fn from_edn <'e> (jack: &Arc>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option, Arc>)> { + let mut name = String::new(); + let mut file = String::new(); + let mut midi = None; + let mut start = 0usize; + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) { + file = String::from(*f); + } + if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { + start = *i as usize; + } + if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { + midi = Some(u7::from(*m as u8)); + } + }, + _ => panic!("unexpected in sample {name}"), + }); + let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; + Ok((midi, Arc::new(RwLock::new(Self { + name: name.into(), + start, + end, + channels: data, + rate: None + })))) + } + + /// Read WAV from file + pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) + } +} + +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after = self.after - 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position = self.position + 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity, + sample.channels[0][position] * self.velocity, + ]) + } + None + } +} diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index f1072b7d..e037b5ad 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -9,6 +9,7 @@ mod engine_output; pub(crate) use engine_output::*; mod app_transport; pub(crate) use app_transport::*; mod app_sequencer; pub(crate) use app_sequencer::*; +mod app_sampler; pub(crate) use app_sampler::*; mod app_groovebox; pub(crate) use app_groovebox::*; mod app_arranger; pub(crate) use app_arranger::*; diff --git a/crates/tek/src/tui/_todo_tui_sampler_cmd.rs b/crates/tek/src/tui/_todo_tui_sampler_cmd.rs deleted file mode 100644 index a1b9e16a..00000000 --- a/crates/tek/src/tui/_todo_tui_sampler_cmd.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::*; -impl Handle for Sampler { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - key!(KeyCode::Up) => { - self.cursor.0 = if self.cursor.0 == 0 { - self.mapped.len() + self.unmapped.len() - 1 - } else { - self.cursor.0 - 1 - }; - Ok(Some(true)) - }, - key!(KeyCode::Down) => { - self.cursor.0 = (self.cursor.0 + 1) % (self.mapped.len() + self.unmapped.len()); - Ok(Some(true)) - }, - key!(KeyCode::Char('p')) => { - if let Some(sample) = self.sample() { - self.voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); - } - Ok(Some(true)) - }, - key!(KeyCode::Char('a')) => { - let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?)); - self.unmapped.push(sample); - Ok(Some(true)) - }, - key!(KeyCode::Char('r')) => { - if let Some(sample) = self.sample() { - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?)); - } - Ok(Some(true)) - }, - key!(KeyCode::Enter) => { - if let Some(sample) = self.sample() { - self.editing = Some(sample.clone()); - } - Ok(Some(true)) - } - _ => Ok(None) - } - } -} -impl Handle for AddSampleModal { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { - return Ok(Some(true)) - } - Ok(Some(true)) - } -} diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index 5f19bf4a..efd4f1fb 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -1,2 +1,32 @@ use crate::*; use super::*; + +impl TryFrom<&Arc>> for GrooveboxTui { + type Error = Box; + fn try_from (jack: &Arc>) -> Usually { + Ok(Self { + sequencer: SequencerTui::try_from(jack)?, + sampler: SamplerTui::try_from(jack)?, + focus: GrooveboxFocus::Sampler, + }) + } +} + +struct GrooveboxTui { + pub sequencer: SequencerTui, + pub sampler: SamplerTui, + pub focus: GrooveboxFocus, +} + +/// Sections that may be focused +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum GrooveboxFocus { + /// The transport (toolbar) is focused + Transport(TransportFocus), + /// The phrase list (pool) is focused + PhraseList, + /// The phrase editor (sequencer) is focused + PhraseEditor, + /// The sample player is focused + Sampler +} diff --git a/crates/tek/src/tui/_todo_tui_sampler.rs b/crates/tek/src/tui/app_sampler.rs similarity index 70% rename from crates/tek/src/tui/_todo_tui_sampler.rs rename to crates/tek/src/tui/app_sampler.rs index 791144dc..f733577b 100644 --- a/crates/tek/src/tui/_todo_tui_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -1,79 +1,5 @@ use crate::*; - -/// The sampler plugin plays sounds. -pub struct SamplerView { - _engine: PhantomData, - pub state: Sampler, - pub cursor: (usize, usize), - pub editing: Option>>, - pub buffer: Vec>, - pub modal: Arc>>>, -} - -impl SamplerView { - pub fn new ( - jack: &Arc>, - name: &str, - mapped: Option>>> - ) -> Usually> { - Jack::new(name)? - .midi_in("midi") - .audio_in("recL") - .audio_in("recR") - .audio_out("outL") - .audio_out("outR") - .run(|ports|Box::new(Self { - _engine: Default::default(), - jack: jack.clone(), - name: name.into(), - cursor: (0, 0), - editing: None, - mapped: mapped.unwrap_or_else(||BTreeMap::new()), - unmapped: vec![], - voices: Arc::new(RwLock::new(vec![])), - ports, - buffer: vec![vec![0.0;16384];2], - output_gain: 0.5, - modal: Default::default() - })) - } - /// Immutable reference to sample at cursor. - pub fn sample (&self) -> Option<&Arc>> { - for (i, sample) in self.mapped.values().enumerate() { - if i == self.cursor.0 { - return Some(sample) - } - } - for (i, sample) in self.unmapped.iter().enumerate() { - if i + self.mapped.len() == self.cursor.0 { - return Some(sample) - } - } - None - } -} - -/// A sound sample. -#[derive(Default, Debug)] -pub struct Sample { - pub name: String, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, -} - -/// Load sample from WAV and assign to MIDI note. -#[macro_export] macro_rules! sample { - ($note:expr, $name:expr, $src:expr) => {{ - let (end, data) = read_sample_data($src)?; - ( - u7::from_int_lossy($note).into(), - Sample::new($name, 0, end, data).into() - ) - }}; -} - +use super::*; use std::fs::File; use symphonia::core::codecs::CODEC_TYPE_NULL; use symphonia::core::errors::Error; @@ -82,6 +8,92 @@ use symphonia::core::probe::Hint; use symphonia::core::audio::SampleBuffer; use symphonia::default::get_codecs; +impl TryFrom<&Arc>> for SamplerTui { + type Error = Box; + fn try_from (jack: &Arc>) -> Usually { + let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?; + let audio_outs = vec![ + jack.read().unwrap().client().register_port("outL", AudioOut::default())?, + jack.read().unwrap().client().register_port("outR", AudioOut::default())?, + ]; + Ok(Self { + focus: SamplerFocus::_TODO, + cursor: (0, 0), + editing: None, + modal: Default::default(), + state: Sampler { + jack: jack.clone(), + name: "Sampler".into(), + mapped: BTreeMap::new(), + unmapped: vec![], + voices: Arc::new(RwLock::new(vec![])), + buffer: vec![vec![0.0;16384];2], + output_gain: 0.5, + midi_in, + audio_outs, + }, + }) + } +} + +pub struct SamplerTui { + pub focus: SamplerFocus, + pub state: Sampler, + pub cursor: (usize, usize), + pub editing: Option>>, + pub modal: Arc>>>, +} + +/// Sections that may be focused +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum SamplerFocus { + _TODO +} + +render!(|self: SamplerTui|render(|to|{ + let [x, y, _, height] = to.area(); + let style = Style::default().gray(); + let title = format!(" {} ({})", self.state.name, self.state.voices.read().unwrap().len()); + to.blit(&title, x+1, y, Some(style.white().bold().not_dim())); + let mut width = title.len() + 2; + let mut y1 = 1; + let mut j = 0; + for (note, sample) in self.state.mapped.iter() + .map(|(note, sample)|(Some(note), sample)) + .chain(self.state.unmapped.iter().map(|sample|(None, sample))) + { + if y1 >= height { + break + } + let active = j == self.cursor.0; + width = width.max( + draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)? + ); + y1 = y1 + 1; + j = j + 1; + } + let height = ((2 + y1) as u16).min(height); + //Ok(Some([x, y, (width as u16).min(to.area().w()), height])) + Ok(()) +})); + +impl SamplerTui { + /// Immutable reference to sample at cursor. + pub fn sample (&self) -> Option<&Arc>> { + for (i, sample) in self.state.mapped.values().enumerate() { + if i == self.cursor.0 { + return Some(sample) + } + } + for (i, sample) in self.state.unmapped.iter().enumerate() { + if i + self.state.mapped.len() == self.cursor.0 { + return Some(sample) + } + } + None + } +} + pub struct AddSampleModal { exited: bool, dir: PathBuf, @@ -204,33 +216,49 @@ impl AddSampleModal { } } -pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { - [Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ - modal.exit(); - Ok(true) - }], - [Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ - modal.prev(); - Ok(true) - }], - [Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ - modal.next(); - Ok(true) - }], - [Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ - if modal.pick()? { - modal.exit(); +fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { + todo!(); +} + +impl Handle for SamplerTui { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + let cursor = &mut self.cursor; + let unmapped = &mut self.state.unmapped; + let mapped = &self.state.mapped; + let voices = &self.state.voices; + match from.event() { + key!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 { + mapped.len() + unmapped.len() - 1 + } else { + cursor.0 - 1 + }, + key!(KeyCode::Down) => { + cursor.0 = (cursor.0 + 1) % (mapped.len() + unmapped.len()); + }, + key!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { + voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); + }, + key!(KeyCode::Char('a')) => { + let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + unmapped.push(sample); + }, + key!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { + *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); + }, + key!(KeyCode::Enter) => if let Some(sample) = self.sample() { + self.editing = Some(sample.clone()); + }, + _ => { + return Ok(None) + } } - Ok(true) - }], - [Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ - modal.try_preview()?; - Ok(true) - }] -}); + Ok(Some(true)) + } +} fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { - let (mut subdirs, mut files) = read_dir(dir)? + let (mut subdirs, mut files) = std::fs::read_dir(dir)? .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ let entry = entry.expect("failed to read drectory entry"); let meta = entry.metadata().expect("failed to read entry metadata"); @@ -316,42 +344,6 @@ impl Sample { } } -impl Render for SamplerView { - fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - todo!() - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - tui_render_sampler(self, to) - } -} - -pub fn tui_render_sampler (sampler: &SamplerView, to: &mut TuiOutput) -> Usually<()> { - let [x, y, _, height] = to.area(); - let style = Style::default().gray(); - let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len()); - to.blit(&title, x+1, y, Some(style.white().bold().not_dim())); - let mut width = title.len() + 2; - let mut y1 = 1; - let mut j = 0; - for (note, sample) in sampler.mapped.iter() - .map(|(note, sample)|(Some(note), sample)) - .chain(sampler.unmapped.iter().map(|sample|(None, sample))) - { - if y1 >= height { - break - } - let active = j == sampler.cursor.0; - width = width.max( - draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)? - ); - y1 = y1 + 1; - j = j + 1; - } - let height = ((2 + y1) as u16).min(height); - //Ok(Some([x, y, (width as u16).min(to.area().w()), height])) - Ok(()) -} - fn draw_sample ( to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool ) -> Usually { @@ -410,3 +402,37 @@ impl Render for AddSampleModal { //Lozenge(Style::default()).draw(to) } } + +//impl Handle for AddSampleModal { + //fn handle (&mut self, from: &TuiInput) -> Perhaps { + //if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { + //return Ok(Some(true)) + //} + //Ok(Some(true)) + //} +//} + +//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { + //[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ + //modal.exit(); + //Ok(true) + //}], + //[Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ + //modal.prev(); + //Ok(true) + //}], + //[Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ + //modal.next(); + //Ok(true) + //}], + //[Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ + //if modal.pick()? { + //modal.exit(); + //} + //Ok(true) + //}], + //[Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ + //modal.try_preview()?; + //Ok(true) + //}] +//}); diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 0dcf3c32..13ee7904 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -64,6 +64,17 @@ pub struct SequencerTui { pub perf: PerfModel, } +/// Sections in the sequencer app that may be focused +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum SequencerFocus { + /// The transport (toolbar) is focused + Transport(TransportFocus), + /// The phrase list (pool) is focused + PhraseList, + /// The phrase editor (sequencer) is focused + PhraseEditor, +} + impl Audio for SequencerTui { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { // Start profiling cycle @@ -180,17 +191,6 @@ impl HasFocus for SequencerTui { } } -/// Sections in the sequencer app that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum SequencerFocus { - /// The transport (toolbar) is focused - Transport(TransportFocus), - /// The phrase list (pool) is focused - PhraseList, - /// The phrase editor (sequencer) is focused - PhraseEditor, -} - impl Into> for SequencerFocus { fn into (self) -> Option { if let Self::Transport(transport) = self { From 1b44dc0ce86719842ae3eac3978637a29b839fd4 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 12 Dec 2024 22:32:40 +0100 Subject: [PATCH 017/971] wip: phrase view mode refactor --- crates/tek/src/tui.rs | 1 - crates/tek/src/tui/app_sequencer.rs | 151 +++++--- crates/tek/src/tui/engine_output.rs | 51 ++- crates/tek/src/tui/phrase_editor.rs | 305 ++++++++-------- crates/tek/src/tui/phrase_select.rs | 61 ---- crates/tek/src/tui/piano_horizontal.rs | 462 +++++++++++++++---------- crates/tek/src/tui/status_bar.rs | 17 +- 7 files changed, 552 insertions(+), 496 deletions(-) delete mode 100644 crates/tek/src/tui/phrase_select.rs diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index e037b5ad..1a962cc1 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -23,7 +23,6 @@ mod phrase_length; pub(crate) use phrase_length::*; mod phrase_rename; pub(crate) use phrase_rename::*; mod phrase_list; pub(crate) use phrase_list::*; mod phrase_player; pub(crate) use phrase_player::*; -mod phrase_select; pub(crate) use phrase_select::*; mod port_select; pub(crate) use port_select::*; //////////////////////////////////////////////////////// diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 13ee7904..b72fde0a 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -21,10 +21,6 @@ impl TryFrom<&Arc>> for SequencerTui { phrases.phrases.push(phrase.clone()); phrases.phrase.store(1, Ordering::Relaxed); - let mut editor = PhraseEditorModel::default(); - editor.show_phrase(Some(phrase.clone())); - editor.edit_mode = PhraseEditMode::Note; - let mut player = PhrasePlayerModel::from(&clock); player.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase))); @@ -32,7 +28,7 @@ impl TryFrom<&Arc>> for SequencerTui { clock, phrases, player, - editor, + editor: PhraseEditorModel::from(&phrase), jack: jack.clone(), size: Measure::new(), cursor: (0, 0), @@ -79,20 +75,20 @@ impl Audio for SequencerTui { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { // Start profiling cycle let t0 = self.perf.get_t0(); - // Update transport clock - if ClockAudio(self).process(client, scope) == Control::Quit { + if ClockAudio(self) + .process(client, scope) == Control::Quit + { return Control::Quit } - // Update MIDI sequencer - if PlayerAudio( - &mut self.player, - &mut self.note_buf, - &mut self.midi_buf, - ).process(client, scope) == Control::Quit { + if PlayerAudio(&mut self.player, &mut self.note_buf, &mut self.midi_buf) + .process(client, scope) == Control::Quit + { return Control::Quit } + // End profiling cycle + self.perf.update(t0, scope); // Update sequencer playhead indicator //self.now().set(0.); @@ -105,37 +101,105 @@ impl Audio for SequencerTui { //self.now().set(now); //} //} - // End profiling cycle - self.perf.update(t0, scope); - Control::Continue } } -render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1, - Tui::fill_xy(SequencerStatusBar::from(self)), - Tui::split_right(false, if self.size.w() > 60 { 20 } else if self.size.w() > 40 { 15 } else { 10 }, - Tui::fixed_x(20, Tui::split_down(false, 4, col!([ - PhraseSelector::play_phrase(&self.player), - PhraseSelector::next_phrase(&self.player), - ]), Tui::split_up(false, 2, - PhraseSelector::edit_phrase(&self.editor.phrase), - PhraseListView::from(self), - ))), - col!([ - Tui::fixed_y(2, TransportView::from(( - self, - self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(), - if let SequencerFocus::Transport(_) = self.focus { - true - } else { - false - } +render!(|self: SequencerTui|{ + + let content = lay!([self.size, Tui::split_up(false, 1, + Tui::fill_xy(SequencerStatusBar::from(self)), + Tui::split_right(false, if self.size.w() > 60 { 20 } else if self.size.w() > 40 { 15 } else { 10 }, + Tui::fixed_x(20, Tui::split_down(false, 4, col!([ + PhraseSelector::play_phrase(&self.player), + PhraseSelector::next_phrase(&self.player), + ]), Tui::split_up(false, 2, + PhraseSelector::edit_phrase(&self.editor.phrase.read().unwrap()), + PhraseListView::from(self), ))), - PhraseView::from(self) - ]), - ) -)])); + col!([ + Tui::fixed_y(2, TransportView::from(( + self, + self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(), + if let SequencerFocus::Transport(_) = self.focus { + true + } else { + false + } + ))), + PhraseView::from(self) + ]), + ) + )]); + + pub struct PhraseSelector { + pub(crate) title: &'static str, + pub(crate) name: String, + pub(crate) color: ItemPalette, + pub(crate) time: String, + } + + // TODO: Display phrases always in order of appearance + render!(|self: PhraseSelector|Tui::fixed_y(2, col!([ + lay!(move|add|{ + add(&Tui::push_x(1, Tui::fg(TuiTheme::g(240), self.title)))?; + add(&Tui::bg(self.color.base.rgb, Tui::fill_x(Tui::inset_x(1, Tui::fill_x(Tui::at_e( + Tui::fg(self.color.lightest.rgb, &self.time)))))))?; + Ok(()) + }), + Tui::bg(self.color.base.rgb, + Tui::fg(self.color.lightest.rgb, + Tui::bold(true, self.name.clone()))), + ]))); + + impl PhraseSelector { + // beats elapsed + pub fn play_phrase (state: &T) -> Self { + let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() { + let Phrase { ref name, color, .. } = *phrase.read().unwrap(); + (name.clone(), color) + } else { + ("".to_string(), ItemPalette::from(TuiTheme::g(64))) + }; + let time = if let Some(elapsed) = state.pulses_since_start_looped() { + format!("+{:>}", state.clock().timebase.format_beats_0(elapsed)) + } else { + String::from("") + }; + Self { title: "Now:", time, name, color, } + } + // beats until switchover + pub fn next_phrase (state: &T) -> Self { + let (time, name, color) = if let Some((t, Some(phrase))) = state.next_phrase() { + let Phrase { ref name, color, .. } = *phrase.read().unwrap(); + let time = { + 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() + } + }; + (time, name.clone(), color) + } else { + ("".into(), "".into(), TuiTheme::g(64).into()) + }; + Self { title: "Next:", time, name, color, } + } + pub fn edit_phrase (phrase: &Option>>) -> Self { + let (time, name, color) = if let Some(phrase) = phrase { + let phrase = phrase.read().unwrap(); + (format!("{}", phrase.length), phrase.name.clone(), phrase.color) + } else { + ("".to_string(), "".to_string(), ItemPalette::from(TuiTheme::g(64))) + }; + Self { title: "Editing:", time, name, color } + } + } + content +}); impl HasClock for SequencerTui { fn clock (&self) -> &ClockModel { @@ -266,20 +330,13 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option SequencerCommand::Focus(FocusCommand::Set(PhraseEditor)), } - // Esc: toggle between scrolling and editing - key!(Esc) => - Editor(SetEditMode(match state.editor.edit_mode { - PhraseEditMode::Scroll => PhraseEditMode::Note, - PhraseEditMode::Note => PhraseEditMode::Scroll, - })), - // Enqueue currently edited phrase key!(Char('q')) => Enqueue(Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())), // E: Toggle between editing currently playing or other phrase key!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() { - let editing_phrase = state.editor.phrase.as_ref().map(|p|p.read().unwrap().clone()); + let editing_phrase = state.editor.phrase.read().unwrap().map(|p|p.read().unwrap().clone()); let selected_phrase = state.phrases.phrase().clone(); if Some(selected_phrase.read().unwrap().clone()) != editing_phrase { Editor(Show(Some(selected_phrase))) diff --git a/crates/tek/src/tui/engine_output.rs b/crates/tek/src/tui/engine_output.rs index 5fac2e11..7ab69bb7 100644 --- a/crates/tek/src/tui/engine_output.rs +++ b/crates/tek/src/tui/engine_output.rs @@ -1,19 +1,6 @@ use crate::*; use ratatui::buffer::Cell; -/// Every struct that has [Content]<[Tui]> is a renderable [Render]<[Tui]>. -//impl> Render for C { - //fn min_size (&self, to: [u16;2]) -> Perhaps { - //self.content().min_size(to) - //} - //fn render (&self, to: &mut TuiOutput) -> Usually<()> { - //match self.min_size(to.area().wh().into())? { - //Some(wh) => to.render_in(to.area().clip(wh).into(), &self.content()), - //None => Ok(()) - //} - //} -//} - pub struct TuiOutput { pub buffer: Buffer, pub area: [u16;4] @@ -86,22 +73,6 @@ impl TuiOutput { } } -//impl Area for Rect { - //fn x (&self) -> u16 { self.x } - //fn y (&self) -> u16 { self.y } - //fn w (&self) -> u16 { self.width } - //fn h (&self) -> u16 { self.height } -//} - -pub fn half_block (lower: bool, upper: bool) -> Option { - match (lower, upper) { - (true, true) => Some('█'), - (true, false) => Some('▄'), - (false, true) => Some('▀'), - _ => None - } -} - #[derive(Default)] pub struct BigBuffer { pub width: usize, @@ -126,6 +97,12 @@ impl BigBuffer { } } +impl From<(usize, usize)> for BigBuffer { // cuteness overload + fn from ((width, height): (usize, usize)) -> Self { + Self::new(width, height) + } +} + pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) { for row in 0..area.h() { let y = area.y() + row; @@ -138,6 +115,22 @@ pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut C } } +//impl Area for Rect { + //fn x (&self) -> u16 { self.x } + //fn y (&self) -> u16 { self.y } + //fn w (&self) -> u16 { self.width } + //fn h (&self) -> u16 { self.height } +//} + +pub fn half_block (lower: bool, upper: bool) -> Option { + match (lower, upper) { + (true, true) => Some('█'), + (true, false) => Some('▄'), + (false, true) => Some('▀'), + _ => None + } +} + impl Render for () { fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(None) diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 9672b742..f2546128 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -9,15 +9,15 @@ pub trait HasEditor { /// Contains state for viewing and editing a phrase pub struct PhraseEditorModel { /// Phrase being played - pub(crate) phrase: Option>>, + pub(crate) phrase: Arc>>>>, + /// Renders the phrase pub(crate) view_mode: Box, - pub(crate) edit_mode: PhraseEditMode, // Lowest note displayed pub(crate) note_lo: AtomicUsize, /// Note coordinate of cursor pub(crate) note_point: AtomicUsize, /// Length of note that will be inserted, in pulses - pub(crate) note_len: usize, + pub(crate) note_len: Arc, /// Notes currently held at input pub(crate) notes_in: Arc>, /// Notes currently held at output @@ -32,32 +32,18 @@ pub struct PhraseEditorModel { pub(crate) size: Measure, } -#[derive(Copy, Clone, Debug)] -pub enum PhraseEditMode { - Note, - Scroll, +impl From<&Arc>> for PhraseEditorModel { + fn from (phrase: &Arc>) -> Self { + Self::from(Some(phrase.clone())) + } } -pub trait PhraseViewMode: Debug + Send + Sync { - fn show (&mut self, phrase: Option<&Phrase>, note_len: usize); - fn time_zoom (&self) -> Option; - fn set_time_zoom (&mut self, time_zoom: Option); - fn buffer_width (&self, phrase: &Phrase) -> usize; - fn buffer_height (&self, phrase: &Phrase) -> usize; - fn render_keys (&self, - to: &mut TuiOutput, color: Color, point: Option, range: (usize, usize)); - fn render_notes (&self, - to: &mut TuiOutput, time_start: usize, note_hi: usize); - fn render_cursor ( - &self, - to: &mut TuiOutput, - time_point: usize, - time_start: usize, - note_point: usize, - note_len: usize, - note_hi: usize, - note_lo: usize, - ); +impl From>>> for PhraseEditorModel { + fn from (phrase: Option>>) -> Self { + let model = Self::default(); + *model.phrase.write().unwrap() = phrase; + model + } } impl std::fmt::Debug for PhraseEditorModel { @@ -77,21 +63,23 @@ impl std::fmt::Debug for PhraseEditorModel { impl Default for PhraseEditorModel { fn default () -> Self { + let phrase = Arc::new(RwLock::new(None)); Self { - phrase: None, - note_len: 24, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - now: Pulse::default().into(), size: Measure::new(), - edit_mode: PhraseEditMode::Scroll, - note_lo: 0.into(), - note_point: 0.into(), + phrase: phrase.clone(), + now: Pulse::default().into(), time_start: 0.into(), time_point: 0.into(), + note_lo: 0.into(), + note_point: 0.into(), + note_len: Arc::from(AtomicUsize::from(24)), + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), view_mode: Box::new(PianoHorizontal { + phrase, buffer: Default::default(), - time_zoom: Some(24), + time_zoom: 24, + time_lock: true, note_zoom: PhraseViewNoteZoom::N(1) }), } @@ -101,17 +89,18 @@ impl Default for PhraseEditorModel { impl PhraseEditorModel { /// Put note at current position pub fn put_note (&mut self) { - if let Some(phrase) = &self.phrase { + if let Some(phrase) = *self.phrase.read().unwrap() { + let note_len = self.note_len.load(Ordering::Relaxed); let time = self.time_point.load(Ordering::Relaxed); let note = self.note_point.load(Ordering::Relaxed); let mut phrase = phrase.write().unwrap(); let key: u7 = u7::from(note as u8); let vel: u7 = 100.into(); let start = time; - let end = (start + self.note_len) % phrase.length; + let end = (start + note_len) % phrase.length; phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); - self.view_mode.show(Some(&phrase), self.note_len); + self.view_mode.show(Some(&phrase), note_len); } } /// Move time cursor forward by current note length @@ -134,119 +123,14 @@ impl PhraseEditorModel { } } -#[derive(Clone, Debug)] -pub enum PhraseCommand { - // TODO: 1-9 seek markers that by default start every 8th of the phrase - AppendNote, - PutNote, - SetNoteCursor(usize), - SetNoteLength(usize), - SetNoteScroll(usize), - SetTimeCursor(usize), - SetTimeScroll(usize), - SetTimeZoom(Option), - Show(Option>>), - SetEditMode(PhraseEditMode), - ToggleDirection, -} - -impl InputToCommand for PhraseCommand { - fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option { - use PhraseCommand::*; - use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right}; - let note_lo = state.note_lo.load(Ordering::Relaxed); - let note_point = state.note_point.load(Ordering::Relaxed); - let time_start = state.time_start.load(Ordering::Relaxed); - let time_point = state.time_point.load(Ordering::Relaxed); - let time_zoom = state.view_mode.time_zoom(); - let length = state.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - let note_len = state.note_len; - Some(match from.event() { - key!(Char('`')) => ToggleDirection, - key!(Esc) => SetEditMode(PhraseEditMode::Scroll), - key!(Char('z')) => SetTimeZoom(None), - key!(Char('-')) => SetTimeZoom(time_zoom.map(next_note_length)), - key!(Char('_')) => SetTimeZoom(time_zoom.map(next_note_length)), - key!(Char('=')) => SetTimeZoom(time_zoom.map(prev_note_length)), - key!(Char('+')) => SetTimeZoom(time_zoom.map(prev_note_length)), - key!(Char('a')) => AppendNote, - key!(Char('s')) => PutNote, - // TODO: no triplet/dotted - key!(Char(',')) => SetNoteLength(prev_note_length(state.note_len)), - key!(Char('.')) => SetNoteLength(next_note_length(state.note_len)), - // TODO: with triplet/dotted - key!(Char('<')) => SetNoteLength(prev_note_length(state.note_len)), - key!(Char('>')) => SetNoteLength(next_note_length(state.note_len)), - // TODO: '/' set triplet, '?' set dotted - _ => match state.edit_mode { - PhraseEditMode::Scroll => match from.event() { - key!(Char('e')) => SetEditMode(PhraseEditMode::Note), - - key!(Up) => SetNoteScroll(note_lo + 1), - key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)), - key!(PageUp) => SetNoteScroll(note_lo + 3), - key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)), - - key!(Left) => SetTimeScroll(time_start.saturating_sub(note_len)), - key!(Right) => SetTimeScroll(time_start + note_len), - key!(Shift-Left) => SetTimeScroll(time_point.saturating_sub(time_zoom.unwrap())), - key!(Shift-Right) => SetTimeScroll((time_point + time_zoom.unwrap()) % length), - _ => return None - }, - PhraseEditMode::Note => match from.event() { - key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll), - - key!(Up) => SetNoteCursor(note_point + 1), - key!(Down) => SetNoteCursor(note_point.saturating_sub(1)), - key!(PageUp) => SetNoteCursor(note_point + 3), - key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)), - - key!(Shift-Up) => SetNoteScroll(note_lo + 1), - key!(Shift-Down) => SetNoteScroll(note_lo.saturating_sub(1)), - key!(Shift-PageUp) => SetNoteScroll(note_point + 3), - key!(Shift-PageDown) => SetNoteScroll(note_point.saturating_sub(3)), - - key!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)), - key!(Right) => SetTimeCursor((time_point + note_len) % length), - - key!(Shift-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom.unwrap())), - key!(Shift-Right) => SetTimeCursor((time_point + time_zoom.unwrap()) % length), - - _ => return None - }, - } - }) - } -} - -impl Command for PhraseCommand { - fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { - use PhraseCommand::*; - match self { - Show(phrase) => { state.show_phrase(phrase); }, - SetEditMode(mode) => { state.edit_mode = mode; } - PutNote => { state.put_note(); }, - AppendNote => { state.put_note(); - state.time_cursor_advance(); }, - SetTimeZoom(zoom) => { state.view_mode.set_time_zoom(zoom); - state.show_phrase(state.phrase.clone()); }, - SetTimeScroll(time) => { state.time_start.store(time, Ordering::Relaxed); }, - SetTimeCursor(time) => { state.time_point.store(time, Ordering::Relaxed); }, - SetNoteLength(time) => { state.note_len = time; }, - SetNoteScroll(note) => { state.note_lo.store(note, Ordering::Relaxed); }, - SetNoteCursor(note) => { - let note = 127.min(note); - let start = state.note_lo.load(Ordering::Relaxed); - state.note_point.store(note, Ordering::Relaxed); - if note < start { - state.note_lo.store(note, Ordering::Relaxed); - } - }, - - _ => todo!("{:?}", self) - } - Ok(None) - } +pub trait PhraseViewMode: Debug + Send + Sync { + fn time_zoom (&self) -> usize; + fn set_time_zoom (&mut self, time_zoom: usize); + fn time_zoom_lock (&self) -> bool; + fn set_time_zoom_lock (&mut self, time_zoom: bool); + fn buffer_size (&self, phrase: &Phrase) -> (usize, usize); + fn redraw (&mut self); + fn phrase (&self) -> &Arc>>>>; } pub struct PhraseView<'a> { @@ -255,7 +139,7 @@ pub struct PhraseView<'a> { time_start: usize, time_point: usize, note_len: usize, - phrase: &'a Option>>, + phrase: Arc>>>>, view_mode: &'a Box, now: &'a Arc, size: &'a Measure, @@ -269,7 +153,7 @@ render!(|self: PhraseView<'a>|{ .map(|p|p.read().unwrap().color.clone()) .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); Tui::bg(bg, Tui::split_up(false, 2, - Tui::bg(fg.dark.rgb, lay!([ + Tui::bg(fg.dark.rgb, col!([ PhraseTimeline(&self, fg), PhraseViewStats(&self, fg), ])), @@ -297,8 +181,8 @@ impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { note_range: (note_lo, note_hi), time_start: editor.time_start.load(Ordering::Relaxed), time_point: editor.time_point.load(Ordering::Relaxed), - note_len: editor.note_len, - phrase: &editor.phrase, + note_len: editor.note_len.load(Ordering::Relaxed), + phrase: editor.phrase.clone(), view_mode: &editor.view_mode, size: &editor.size, now: &editor.now, @@ -363,3 +247,112 @@ render!(|self: PhraseCursor<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|Ok( self.0.note_range.0, ) )))); + +#[derive(Clone, Debug)] +pub enum PhraseCommand { + // TODO: 1-9 seek markers that by default start every 8th of the phrase + AppendNote, + PutNote, + SetNoteCursor(usize), + SetNoteLength(usize), + SetNoteScroll(usize), + SetTimeCursor(usize), + SetTimeScroll(usize), + SetTimeZoom(usize), + SetTimeZoomLock(bool), + Show(Option>>), + ToggleDirection, +} + +impl InputToCommand for PhraseCommand { + fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option { + use PhraseCommand::*; + use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right}; + let note_lo = state.note_lo.load(Ordering::Relaxed); + let note_point = state.note_point.load(Ordering::Relaxed); + let time_start = state.time_start.load(Ordering::Relaxed); + let time_point = state.time_point.load(Ordering::Relaxed); + let time_zoom = state.view_mode.time_zoom(); + let length = state.phrase.read().unwrap().map(|p|p.read().unwrap().length).unwrap_or(1); + let note_len = state.note_len.load(Ordering::Relaxed); + Some(match from.event() { + key!(Char('`')) => ToggleDirection, + key!(Char('z')) => SetTimeZoomLock(!state.view_mode.time_zoom_lock()), + key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)), + key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)), + key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)), + key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)), + key!(Char('a')) => AppendNote, + key!(Char('s')) => PutNote, + // TODO: no triplet/dotted + key!(Char(',')) => SetNoteLength(prev_note_length(note_len)), + key!(Char('.')) => SetNoteLength(next_note_length(note_len)), + // TODO: with triplet/dotted + key!(Char('<')) => SetNoteLength(prev_note_length(note_len)), + key!(Char('>')) => SetNoteLength(next_note_length(note_len)), + // TODO: '/' set triplet, '?' set dotted + _ => match from.event() { + key!(Up) => SetNoteCursor(note_point + 1), + key!(Down) => SetNoteCursor(note_point.saturating_sub(1)), + key!(PageUp) => SetNoteCursor(note_point + 3), + key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)), + key!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)), + key!(Right) => SetTimeCursor((time_point + note_len) % length), + key!(Shift-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)), + key!(Shift-Right) => SetTimeCursor((time_point + time_zoom) % length), + + key!(Ctrl-Up) => SetNoteScroll(note_lo + 1), + key!(Ctrl-Down) => SetNoteScroll(note_lo.saturating_sub(1)), + key!(Ctrl-PageUp) => SetNoteScroll(note_point + 3), + key!(Ctrl-PageDown) => SetNoteScroll(note_point.saturating_sub(3)), + key!(Ctrl-Left) => SetTimeScroll(time_start.saturating_sub(note_len)), + key!(Ctrl-Right) => SetTimeScroll(time_start + note_len), + + + + _ => return None + }, + }) + } +} + +impl Command for PhraseCommand { + fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { + use PhraseCommand::*; + match self { + Show(phrase) => { + state.show_phrase(phrase); + }, + PutNote => { + state.put_note(); + }, + AppendNote => { + state.put_note(); + state.time_cursor_advance(); + }, + SetTimeZoom(zoom) => { + state.view_mode.set_time_zoom(zoom); + state.show_phrase(state.phrase.clone()); + }, + SetTimeZoomLock(lock) => { + state.view_mode.set_zoom_lock(lock); + state.show_phrase(state.phrase.clone()); + }, + SetTimeScroll(time) => { state.time_start.store(time, Ordering::Relaxed); }, + SetTimeCursor(time) => { state.time_point.store(time, Ordering::Relaxed); }, + SetNoteLength(time) => { state.note_len.store(time, Ordering::Relaxed); }, + SetNoteScroll(note) => { state.note_lo.store(note, Ordering::Relaxed); }, + SetNoteCursor(note) => { + let note = 127.min(note); + let start = state.note_lo.load(Ordering::Relaxed); + state.note_point.store(note, Ordering::Relaxed); + if note < start { + state.note_lo.store(note, Ordering::Relaxed); + } + }, + + _ => todo!("{:?}", self) + } + Ok(None) + } +} diff --git a/crates/tek/src/tui/phrase_select.rs b/crates/tek/src/tui/phrase_select.rs deleted file mode 100644 index c23c234f..00000000 --- a/crates/tek/src/tui/phrase_select.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::*; - -pub struct PhraseSelector { - pub(crate) title: &'static str, - pub(crate) name: String, - pub(crate) color: ItemPalette, - pub(crate) time: String, -} - -// TODO: Display phrases always in order of appearance -render!(|self: PhraseSelector|Tui::fixed_y(1, lay!(move|add|{ - add(&Tui::push_x(1, Tui::fg(TuiTheme::g(240), self.title)))?; - add(&Tui::bg(self.color.base.rgb, Tui::fill_x(Tui::inset_x(1, Tui::fill_x(Tui::at_e(Tui::fg(self.color.lightest.rgb, &self.time)))))))?; - Ok(()) -}))); -impl PhraseSelector { - // beats elapsed - pub fn play_phrase (state: &T) -> Self { - let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() { - let Phrase { ref name, color, .. } = *phrase.read().unwrap(); - (name.clone(), color) - } else { - ("".to_string(), ItemPalette::from(TuiTheme::g(64))) - }; - let time = if let Some(elapsed) = state.pulses_since_start_looped() { - format!("+{:>}", state.clock().timebase.format_beats_0(elapsed)) - } else { - String::from("") - }; - Self { title: "Now:", time, name, color, } - } - // beats until switchover - pub fn next_phrase (state: &T) -> Self { - let (time, name, color) = if let Some((t, Some(phrase))) = state.next_phrase() { - let Phrase { ref name, color, .. } = *phrase.read().unwrap(); - let time = { - 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() - } - }; - (time, name.clone(), color) - } else { - ("".into(), "".into(), TuiTheme::g(64).into()) - }; - Self { title: "Next:", time, name, color, } - } - pub fn edit_phrase (phrase: &Option>>) -> Self { - let (time, name, color) = if let Some(phrase) = phrase { - let phrase = phrase.read().unwrap(); - (format!("{}", phrase.length), phrase.name.clone(), phrase.color) - } else { - ("".to_string(), "".to_string(), ItemPalette::from(TuiTheme::g(64))) - }; - Self { title: "Editing:", time, name, color } - } -} diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 82b5322b..0a73e619 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -1,12 +1,251 @@ use crate::*; use super::*; - pub struct PianoHorizontal { - pub(crate) time_zoom: Option, + pub(crate) phrase: Arc>>>>, + pub(crate) time_lock: bool, + pub(crate) time_zoom: usize, pub(crate) note_zoom: PhraseViewNoteZoom, + pub(crate) note_len: Arc, pub(crate) buffer: BigBuffer, + pub(crate) focused: bool, } +#[derive(Copy, Clone, Debug)] +pub enum PhraseViewNoteZoom { + N(usize), + Half, + Octant, +} +render!(|self: PianoHorizontal|{ + let bg = if self.focused { TuiTheme::g(32) } else { Color::Reset }; + let fg = self.phrase().read().unwrap() + .as_ref().map(|p|p.read().unwrap().color) + .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); + Tui::bg(bg, Tui::split_up(false, 2, + Tui::bg(fg.dark.rgb, PianoHorizontalTimeline { + start: "TIMELINE".into() + }), + Split::right(false, 5, PianoHorizontalKeys { + color: ItemPalette::random(), + note_lo: 0, + note_hi: 0, + note_point: None + }, lay!([ + PianoHorizontalNotes { + source: &self.buffer, + time_start: 0, + note_hi: 0, + }, + PianoHorizontalCursor { + time_zoom: 0, + time_point: 0, + time_start: 0, + note_point: 0, + note_len: 0, + note_hi: 0, + note_lo: 0, + }, + ])), + )) +}); +pub struct PianoHorizontalTimeline { + start: String +} +render!(|self: PianoHorizontalTimeline|{ + Tui::fg(TuiTheme::g(224), Tui::push_x(5, self.start.as_str())) +}); +pub struct PianoHorizontalKeys { + color: ItemPalette, + note_lo: usize, + note_hi: usize, + note_point: Option, +} +render!(|self: PianoHorizontalKeys|render(|to|Ok({ + let [x, y0, _, _] = to.area().xywh(); + let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0))); + let off_style = Some(Style::default().fg(TuiTheme::g(160))); + let on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(self.color.base.rgb).bold()); + for (y, note) in (self.note_lo..=self.note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) { + let key = match note % 12 { + 11 => "████▌", + 10 => " ", + 9 => "████▌", + 8 => " ", + 7 => "████▌", + 6 => " ", + 5 => "████▌", + 4 => "████▌", + 3 => " ", + 2 => "████▌", + 1 => " ", + 0 => "████▌", + _ => unreachable!(), + }; + to.blit(&key, x, y, key_style); + + if Some(note) == self.note_point { + to.blit(&format!("{:<5}", to_note_name(note)), x, y, on_style) + } else { + to.blit(&to_note_name(note), x, y, off_style) + }; + } +}))); +pub struct PianoHorizontalCursor { + time_zoom: usize, + time_point: usize, + time_start: usize, + note_point: usize, + note_len: usize, + note_hi: usize, + note_lo: usize, +} +render!(|self: PianoHorizontalCursor|render(|to|Ok({ + let [x0, y0, w, _] = to.area().xywh(); + let style = Some(Style::default().fg(Color::Rgb(0,255,0))); + for (y, note) in (self.note_lo..=self.note_hi).rev().enumerate() { + if note == self.note_point { + for x in 0..w { + let time_1 = self.time_start + x as usize * self.time_zoom; + let time_2 = time_1 + self.time_zoom; + if time_1 <= self.time_point && self.time_point < time_2 { + to.blit(&"█", x0 + x as u16, y0 + y as u16, style); + let tail = self.note_len as u16 / self.time_zoom as u16; + for x_tail in (x0 + x + 1)..(x0 + x + tail) { + to.blit(&"▂", x_tail, y0 + y as u16, style); + } + break + } + } + break + } + } +}))); +pub struct PianoHorizontalNotes<'a> { + source: &'a BigBuffer, + time_start: usize, + note_hi: usize, +} +render!(|self: PianoHorizontalNotes<'a>|render(|to|Ok({ + let [x0, y0, w, h] = to.area().xywh(); + let target = &mut to.buffer; + for (x, target_x) in (x0..x0+w).enumerate() { + for (y, target_y) in (y0..y0+h).enumerate() { + if y > self.note_hi { + break + } + let source_x = self.time_start + x; + let source_y = self.note_hi - y; + // TODO: enable loop rollover: + //let source_x = (time_start + x) % source.width.max(1); + //let source_y = (note_hi - y) % source.height.max(1); + if source_x < self.source.width && source_y < self.source.height { + let target_cell = target.get_mut(target_x, target_y); + if let Some(source_cell) = self.source.get(source_x, source_y) { + *target_cell = source_cell.clone(); + } + } + } + } +}))); +impl PianoHorizontal { + /// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_bg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize, note_len: usize) { + for (y, note) in (0..127).rev().enumerate() { + for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { + let cell = buf.get_mut(x, y).unwrap(); + cell.set_bg(phrase.color.darkest.rgb); + cell.set_fg(phrase.color.darker.rgb); + cell.set_char(if time % 384 == 0 { + '│' + } else if time % 96 == 0 { + '╎' + } else if time % note_len == 0 { + '┊' + } else if (127 - note) % 12 == 1 { + '=' + } else { + '·' + }); + } + } + } + /// Draw the piano roll background using full blocks on note on and half blocks on legato: █▄ █▄ █▄ + fn draw_fg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize) { + let style = Style::default().fg(phrase.color.lightest.rgb);//.bg(Color::Rgb(0, 0, 0)); + let mut notes_on = [false;128]; + for (x, time_start) in (0..phrase.length).step_by(zoom).enumerate() { + for (y, note) in (0..127).rev().enumerate() { + let cell = buf.get_mut(x, note).unwrap(); + if notes_on[note] { + cell.set_char('▂'); + cell.set_style(style); + } + } + + let time_end = time_start + zoom; + for time in time_start..time_end { + for event in phrase.notes[time].iter() { + match event { + MidiMessage::NoteOn { key, .. } => { + let note = key.as_int() as usize; + let cell = buf.get_mut(x, note).unwrap(); + cell.set_char('█'); + cell.set_style(style); + notes_on[note] = true + }, + MidiMessage::NoteOff { key, .. } => { + notes_on[key.as_int() as usize] = false + }, + _ => {} + } + } + } + + } + } +} +impl PhraseViewMode for PianoHorizontal { + fn phrase (&self) -> &Arc>>>> { + &self.phrase + } + fn time_zoom (&self) -> usize { + self.time_zoom + } + fn set_time_zoom (&mut self, time_zoom: usize) { + self.time_zoom = time_zoom; + self.redraw() + } + fn time_zoom_lock (&self) -> bool { + self.time_lock + } + fn set_time_zoom_lock (&mut self, time_lock: bool) { + self.time_lock = time_lock; + self.redraw() + } + /// Determine the required space to render the phrase. + fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { + let width = phrase.length / self.time_zoom(); + let height = match self.note_zoom { + PhraseViewNoteZoom::Half => 64, + PhraseViewNoteZoom::N(n) => 128*n, + _ => unimplemented!() + }; + (width, height) + } + fn redraw (&mut self) { + let buffer = if let Some(phrase) = &*self.phrase().read().unwrap() { + let phrase = phrase.read().unwrap(); + let mut buffer = BigBuffer::from(self.buffer_size(&phrase)); + let note_len = self.note_len.load(Ordering::Relaxed); + PianoHorizontal::draw_bg(&mut buffer, &phrase, self.time_zoom(), note_len); + PianoHorizontal::draw_fg(&mut buffer, &phrase, self.time_zoom()); + buffer + } else { + Default::default() + }; + self.buffer = buffer + } +} impl std::fmt::Debug for PianoHorizontal { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("PianoHorizontal") @@ -17,191 +256,34 @@ impl std::fmt::Debug for PianoHorizontal { } } -#[derive(Copy, Clone, Debug)] -pub enum PhraseViewNoteZoom { - N(usize), - Half, - Octant, -} - -impl PhraseViewMode for PianoHorizontal { - fn time_zoom (&self) -> Option { - self.time_zoom - } - fn set_time_zoom (&mut self, time_zoom: Option) { - self.time_zoom = time_zoom - } - fn show (&mut self, phrase: Option<&Phrase>, note_len: usize) { - if let Some(phrase) = phrase { - self.buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase)); - draw_piano_horizontal_bg(&mut self.buffer, phrase, self.time_zoom.unwrap(), note_len); - draw_piano_horizontal_fg(&mut self.buffer, phrase, self.time_zoom.unwrap()); - } else { - self.buffer = Default::default(); - } - } - fn buffer_width (&self, phrase: &Phrase) -> usize { - phrase.length / self.time_zoom.unwrap() - } - /// Determine the required height to render the phrase. - fn buffer_height (&self, phrase: &Phrase) -> usize { - match self.note_zoom { - PhraseViewNoteZoom::Half => 64, - PhraseViewNoteZoom::N(n) => 128*n, - _ => unimplemented!() - } - } - fn render_notes ( - &self, - target: &mut TuiOutput, - time_start: usize, - note_hi: usize, - ) { - let [x0, y0, w, h] = target.area().xywh(); - let source = &self.buffer; - let target = &mut target.buffer; - for (x, target_x) in (x0..x0+w).enumerate() { - for (y, target_y) in (y0..y0+h).enumerate() { - if y > note_hi { - break - } - let source_x = time_start + x; - let source_y = note_hi - y; - // TODO: enable loop rollover: - //let source_x = (time_start + x) % source.width.max(1); - //let source_y = (note_hi - y) % source.height.max(1); - if source_x < source.width && source_y < source.height { - let target_cell = target.get_mut(target_x, target_y); - if let Some(source_cell) = source.get(source_x, source_y) { - *target_cell = source_cell.clone(); - } - } - } - } - } - fn render_keys ( - &self, - to: &mut TuiOutput, - color: Color, - point: Option, - (note_lo, note_hi): (usize, usize) - ) { - let [x, y0, _, _] = to.area().xywh(); - let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0))); - let note_off_style = Some(Style::default().fg(TuiTheme::g(160))); - let note_on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color).bold()); - for (y, note) in (note_lo..=note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) { - let key = match note % 12 { - 11 => "████▌", - 10 => " ", - 9 => "████▌", - 8 => " ", - 7 => "████▌", - 6 => " ", - 5 => "████▌", - 4 => "████▌", - 3 => " ", - 2 => "████▌", - 1 => " ", - 0 => "████▌", - _ => unreachable!(), - }; - to.blit(&key, x, y, key_style); - - if Some(note) == point { - to.blit(&format!("{:<5}", to_note_name(note)), x, y, note_on_style) - } else { - to.blit(&to_note_name(note), x, y, note_off_style) - }; - } - } - fn render_cursor ( - &self, - to: &mut TuiOutput, - time_point: usize, - time_start: usize, - note_point: usize, - note_len: usize, - note_hi: usize, - note_lo: usize, - ) { - let time_zoom = self.time_zoom.unwrap(); - let [x0, y0, w, h] = to.area().xywh(); - let style = Some(Style::default().fg(Color::Rgb(0,255,0))); - for (y, note) in (note_lo..=note_hi).rev().enumerate() { - if note == note_point { - for x in 0..w { - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_point && time_point < time_2 { - to.blit(&"█", x0 + x as u16, y0 + y as u16, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (x0 + x + 1)..(x0 + x + tail) { - to.blit(&"▂", x_tail, y0 + y as u16, style); - } - break - } - } - break - } - } - } -} - -/// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄ -fn draw_piano_horizontal_bg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize, note_len: usize) { - for (y, note) in (0..127).rev().enumerate() { - for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { - let cell = buf.get_mut(x, y).unwrap(); - cell.set_bg(phrase.color.darkest.rgb); - cell.set_fg(phrase.color.darker.rgb); - cell.set_char(if time % 384 == 0 { - '│' - } else if time % 96 == 0 { - '╎' - } else if time % note_len == 0 { - '┊' - } else if (127 - note) % 12 == 1 { - '=' - } else { - '·' - }); - } - } -} - -/// Draw the piano roll background using full blocks on note on and half blocks on legato: █▄ █▄ █▄ -fn draw_piano_horizontal_fg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize) { - let style = Style::default().fg(phrase.color.lightest.rgb);//.bg(Color::Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..phrase.length).step_by(zoom).enumerate() { - - for (y, note) in (0..127).rev().enumerate() { - let cell = buf.get_mut(x, note).unwrap(); - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); - } - } - - let time_end = time_start + zoom; - for time in time_start..time_end { - for event in phrase.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - let cell = buf.get_mut(x, note).unwrap(); - cell.set_char('█'); - cell.set_style(style); - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } -} + //fn render_cursor ( + //&self, + //to: &mut TuiOutput, + //time_point: usize, + //time_start: usize, + //note_point: usize, + //note_len: usize, + //note_hi: usize, + //note_lo: usize, + //) { + //let time_zoom = self.time_zoom; + //let [x0, y0, w, h] = to.area().xywh(); + //let style = Some(Style::default().fg(Color::Rgb(0,255,0))); + //for (y, note) in (note_lo..=note_hi).rev().enumerate() { + //if note == note_point { + //for x in 0..w { + //let time_1 = time_start + x as usize * time_zoom; + //let time_2 = time_1 + time_zoom; + //if time_1 <= time_point && time_point < time_2 { + //to.blit(&"█", x0 + x as u16, y0 + y as u16, style); + //let tail = note_len as u16 / time_zoom as u16; + //for x_tail in (x0 + x + 1)..(x0 + x + tail) { + //to.blit(&"▂", x_tail, y0 + y as u16, style); + //} + //break + //} + //} + //break + //} + //} + //} diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs index ff6660ed..1939baa0 100644 --- a/crates/tek/src/tui/status_bar.rs +++ b/crates/tek/src/tui/status_bar.rs @@ -71,10 +71,7 @@ impl From<&SequencerTui> for SequencerStatusBar { //PhrasePlay => " TO PLAY ", //PhraseNext => " UP NEXT ", PhraseList => " PHRASES ", - PhraseEditor => match state.editor.edit_mode { - PhraseEditMode::Note => " EDIT MIDI ", - PhraseEditMode::Scroll => " VIEW MIDI ", - }, + PhraseEditor => " EDIT MIDI ", }, help: match state.focused() { Transport(PlayPause) => &[ @@ -101,14 +98,10 @@ impl From<&SequencerTui> for SequencerStatusBar { ("", "⏎", " play"), ("", "e", " edit"), ], - PhraseEditor => match state.editor.edit_mode { - PhraseEditMode::Note => &[ - ("", "✣", " cursor"), - ], - PhraseEditMode::Scroll => &[ - ("", "✣", " scroll"), - ], - }, + PhraseEditor => &[ + ("", "✣", " cursor"), + ("", "Ctrl-✣", " scroll"), + ], _ => default_help, } } From 2795c05275592475fee0373f786dbc7c541ab59c Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 12 Dec 2024 22:54:55 +0100 Subject: [PATCH 018/971] delegate more responsibilities to PhraseViewMode --- crates/tek/src/tui/app_arranger.rs | 2 +- crates/tek/src/tui/app_sequencer.rs | 10 +- crates/tek/src/tui/phrase_editor.rs | 190 ++++++++++------------------ 3 files changed, 71 insertions(+), 131 deletions(-) diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index f8813c96..778bd549 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -143,7 +143,7 @@ render!(|self: ArrangerTui|{ false, self.splits[1], PhraseListView::from(self), - PhraseView::from(self), + &self.editor, ) ]) ]) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index b72fde0a..0a12eebd 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -22,14 +22,14 @@ impl TryFrom<&Arc>> for SequencerTui { phrases.phrase.store(1, Ordering::Relaxed); let mut player = PhrasePlayerModel::from(&clock); - player.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase))); + player.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone()))); Ok(Self { + jack: jack.clone(), clock, phrases, player, editor: PhraseEditorModel::from(&phrase), - jack: jack.clone(), size: Measure::new(), cursor: (0, 0), entered: false, @@ -127,7 +127,7 @@ render!(|self: SequencerTui|{ false } ))), - PhraseView::from(self) + self.editor ]), ) )]); @@ -336,7 +336,9 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option if let Some((_, Some(playing_phrase))) = state.player.play_phrase() { - let editing_phrase = state.editor.phrase.read().unwrap().map(|p|p.read().unwrap().clone()); + let editing_phrase = state.editor.phrase() + .read().unwrap().as_ref() + .map(|p|p.read().unwrap().clone()); let selected_phrase = state.phrases.phrase().clone(); if Some(selected_phrase.read().unwrap().clone()) != editing_phrase { Editor(Show(Some(selected_phrase))) diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index f2546128..7c95c640 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -63,7 +63,8 @@ impl std::fmt::Debug for PhraseEditorModel { impl Default for PhraseEditorModel { fn default () -> Self { - let phrase = Arc::new(RwLock::new(None)); + let phrase = Arc::new(RwLock::new(None)); + let note_len = Arc::from(AtomicUsize::from(24)); Self { size: Measure::new(), phrase: phrase.clone(), @@ -72,24 +73,35 @@ impl Default for PhraseEditorModel { time_point: 0.into(), note_lo: 0.into(), note_point: 0.into(), - note_len: Arc::from(AtomicUsize::from(24)), + note_len: note_len.clone(), notes_in: RwLock::new([false;128]).into(), notes_out: RwLock::new([false;128]).into(), view_mode: Box::new(PianoHorizontal { - phrase, + phrase: Arc::new(RwLock::new(None)), buffer: Default::default(), time_zoom: 24, time_lock: true, - note_zoom: PhraseViewNoteZoom::N(1) + note_zoom: PhraseViewNoteZoom::N(1), + focused: true, + note_len }), } } } impl PhraseEditorModel { + /// Select which pattern to display. This pre-renders it to the buffer at full resolution. + pub fn show_phrase (&mut self, phrase: Option>>) { + *self.view_mode.phrase().write().unwrap() = if phrase.is_some() { + phrase.clone() + } else { + None + }; + self.view_mode.redraw(); + } /// Put note at current position - pub fn put_note (&mut self) { - if let Some(phrase) = *self.phrase.read().unwrap() { + pub fn put_note (&mut self, advance: bool) { + if let Some(phrase) = &*self.phrase.read().unwrap() { let note_len = self.note_len.load(Ordering::Relaxed); let time = self.time_point.load(Ordering::Relaxed); let note = self.note_point.load(Ordering::Relaxed); @@ -100,30 +112,20 @@ impl PhraseEditorModel { let end = (start + note_len) % phrase.length; phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); - self.view_mode.show(Some(&phrase), note_len); - } - } - /// Move time cursor forward by current note length - pub fn time_cursor_advance (&self) { - let point = self.time_point.load(Ordering::Relaxed); - let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - let forward = |time|(time + self.note_len) % length; - self.time_point.store(forward(point), Ordering::Relaxed); - } - /// Select which pattern to display. This pre-renders it to the buffer at full resolution. - pub fn show_phrase (&mut self, phrase: Option>>) { - if phrase.is_some() { - self.phrase = phrase; - let phrase = &*self.phrase.as_ref().unwrap().read().unwrap(); - self.view_mode.show(Some(&phrase), self.note_len); - } else { - self.view_mode.show(None, self.note_len); - self.phrase = None; + self.view_mode.redraw(); + if advance { + let point = self.time_point.load(Ordering::Relaxed); + let length = phrase.length; + let forward = |time|(time + note_len) % length; + self.time_point.store(forward(point), Ordering::Relaxed); + } } } } -pub trait PhraseViewMode: Debug + Send + Sync { +render!(|self: PhraseEditorModel|self.view_mode); + +pub trait PhraseViewMode: Render + Debug + Send + Sync { fn time_zoom (&self) -> usize; fn set_time_zoom (&mut self, time_zoom: usize); fn time_zoom_lock (&self) -> bool; @@ -133,6 +135,30 @@ pub trait PhraseViewMode: Debug + Send + Sync { fn phrase (&self) -> &Arc>>>>; } +impl PhraseViewMode for PhraseEditorModel { + fn time_zoom (&self) -> usize { + self.view_mode.time_zoom() + } + fn set_time_zoom (&mut self, time_zoom: usize) { + self.view_mode.set_time_zoom(time_zoom) + } + fn time_zoom_lock (&self) -> bool { + self.view_mode.time_zoom_lock() + } + fn set_time_zoom_lock (&mut self, time_lock: bool) { + self.view_mode.set_time_zoom_lock(time_lock); + } + fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { + self.view_mode.buffer_size(phrase) + } + fn redraw (&mut self) { + self.view_mode.redraw() + } + fn phrase (&self) -> &Arc>>>> { + self.view_mode.phrase() + } +} + pub struct PhraseView<'a> { note_point: usize, note_range: (usize, usize), @@ -147,23 +173,6 @@ pub struct PhraseView<'a> { entered: bool, } -render!(|self: PhraseView<'a>|{ - let bg = if self.focused { TuiTheme::g(32) } else { Color::Reset }; - let fg = self.phrase.as_ref() - .map(|p|p.read().unwrap().color.clone()) - .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); - Tui::bg(bg, Tui::split_up(false, 2, - Tui::bg(fg.dark.rgb, col!([ - PhraseTimeline(&self, fg), - PhraseViewStats(&self, fg), - ])), - Split::right(false, 5, PhraseKeys(&self, fg), lay!([ - PhraseNotes(&self, fg), - PhraseCursor(&self), - ])), - )) -}); - impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { fn from (state: &'a T) -> Self { let editor = state.editor(); @@ -192,62 +201,6 @@ impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { } } -pub struct PhraseTimeline<'a>(&'a PhraseView<'a>, ItemPalette); -render!(|self: PhraseTimeline<'a>|Tui::fg(TuiTheme::g(224), Tui::push_x(5, format!("|000.00.00")))); - -pub struct PhraseViewStats<'a>(&'a PhraseView<'a>, ItemPalette); -render!(|self: PhraseViewStats<'a>|{ - let color = self.1.dark.rgb;//if self.0.focused{self.1.light.rgb}else{self.1.dark.rgb}; - row!([ - Tui::bg(color, Tui::fg(TuiTheme::g(224), format!( - " {} | Note: {} ({}) | {} ", - self.0.size.format(), - self.0.note_point, - to_note_name(self.0.note_point), - pulses_to_name(self.0.note_len), - ))), - { - let mut upper_right = format!("[{}]", if self.0.entered {"■"} else {" "}); - if let Some(phrase) = self.0.phrase { - upper_right = format!( - " Time: {}/{} {} {upper_right} ", - self.0.time_point, - phrase.read().unwrap().length, - pulses_to_name(self.0.view_mode.time_zoom().unwrap()), - ) - }; - Tui::bg(color, Tui::fg(TuiTheme::g(224), upper_right)) - } - ]) -}); - -struct PhraseKeys<'a>(&'a PhraseView<'a>, ItemPalette); -render!(|self: PhraseKeys<'a>|{ - let layout = |to:[u16;2]|Ok(Some(to.clip_w(5))); - Tui::fill_xy(Widget::new(layout, |to: &mut TuiOutput|Ok( - self.0.view_mode.render_keys(to, self.1.light.rgb, Some(self.0.note_point), self.0.note_range) - ))) -}); - -struct PhraseNotes<'a>(&'a PhraseView<'a>, ItemPalette); -render!(|self: PhraseNotes<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|{ - self.0.size.set_wh(to.area.w(), to.area.h() as usize); - Ok(self.0.view_mode.render_notes(to, self.0.time_start, self.0.note_range.1)) -}))); - -struct PhraseCursor<'a>(&'a PhraseView<'a>); -render!(|self: PhraseCursor<'a>|Tui::fill_xy(render(|to: &mut TuiOutput|Ok( - self.0.view_mode.render_cursor( - to, - self.0.time_point, - self.0.time_start, - self.0.note_point, - self.0.note_len, - self.0.note_range.1, - self.0.note_range.0, - ) -)))); - #[derive(Clone, Debug)] pub enum PhraseCommand { // TODO: 1-9 seek markers that by default start every 8th of the phrase @@ -273,7 +226,8 @@ impl InputToCommand for PhraseCommand { let time_start = state.time_start.load(Ordering::Relaxed); let time_point = state.time_point.load(Ordering::Relaxed); let time_zoom = state.view_mode.time_zoom(); - let length = state.phrase.read().unwrap().map(|p|p.read().unwrap().length).unwrap_or(1); + let length = state.phrase().read().unwrap().as_ref() + .map(|p|p.read().unwrap().length).unwrap_or(1); let note_len = state.note_len.load(Ordering::Relaxed); Some(match from.event() { key!(Char('`')) => ToggleDirection, @@ -307,9 +261,6 @@ impl InputToCommand for PhraseCommand { key!(Ctrl-PageDown) => SetNoteScroll(note_point.saturating_sub(3)), key!(Ctrl-Left) => SetTimeScroll(time_start.saturating_sub(note_len)), key!(Ctrl-Right) => SetTimeScroll(time_start + note_len), - - - _ => return None }, }) @@ -320,29 +271,16 @@ impl Command for PhraseCommand { fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { use PhraseCommand::*; match self { - Show(phrase) => { - state.show_phrase(phrase); - }, - PutNote => { - state.put_note(); - }, - AppendNote => { - state.put_note(); - state.time_cursor_advance(); - }, - SetTimeZoom(zoom) => { - state.view_mode.set_time_zoom(zoom); - state.show_phrase(state.phrase.clone()); - }, - SetTimeZoomLock(lock) => { - state.view_mode.set_zoom_lock(lock); - state.show_phrase(state.phrase.clone()); - }, - SetTimeScroll(time) => { state.time_start.store(time, Ordering::Relaxed); }, - SetTimeCursor(time) => { state.time_point.store(time, Ordering::Relaxed); }, - SetNoteLength(time) => { state.note_len.store(time, Ordering::Relaxed); }, - SetNoteScroll(note) => { state.note_lo.store(note, Ordering::Relaxed); }, - SetNoteCursor(note) => { + Show(phrase) => { state.show_phrase(phrase); }, + PutNote => { state.put_note(false); }, + AppendNote => { state.put_note(true); }, + SetTimeZoom(zoom) => { state.view_mode.set_time_zoom(zoom); }, + SetTimeZoomLock(lock) => { state.view_mode.set_time_zoom_lock(lock); }, + SetTimeScroll(time) => { state.time_start.store(time, Ordering::Relaxed); }, + SetTimeCursor(time) => { state.time_point.store(time, Ordering::Relaxed); }, + SetNoteLength(time) => { state.note_len.store(time, Ordering::Relaxed); }, + SetNoteScroll(note) => { state.note_lo.store(note, Ordering::Relaxed); }, + SetNoteCursor(note) => { let note = 127.min(note); let start = state.note_lo.load(Ordering::Relaxed); state.note_point.store(note, Ordering::Relaxed); From 9619ef9739e99d630c8df7e334607d632542671f Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 12 Dec 2024 23:04:55 +0100 Subject: [PATCH 019/971] simplify sequencer init --- crates/tek/src/tui/app_sequencer.rs | 376 +++++++++++++--------------- crates/tek/src/tui/phrase_list.rs | 9 + crates/tek/src/tui/phrase_player.rs | 8 + 3 files changed, 198 insertions(+), 195 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 0a12eebd..468a0794 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -8,31 +8,22 @@ use PhraseCommand::*; impl TryFrom<&Arc>> for SequencerTui { type Error = Box; fn try_from (jack: &Arc>) -> Usually { - let clock = ClockModel::from(jack); - - let mut phrase = Phrase::default(); - phrase.name = "New".into(); - phrase.color = ItemColor::random().into(); - phrase.set_length(384); - - let mut phrases = PhraseListModel::default(); - let phrase = Arc::new(RwLock::new(phrase)); - phrases.phrases.push(phrase.clone()); - phrases.phrase.store(1, Ordering::Relaxed); - - let mut player = PhrasePlayerModel::from(&clock); - player.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone()))); - + let phrase = Arc::new(RwLock::new(Phrase::new( + "New", + true, + 4 * clock.timebase.ppq.get() as usize, + None, + Some(ItemColor::random().into()) + ))); Ok(Self { jack: jack.clone(), - clock, - phrases, - player, + phrases: PhraseListModel::from(&phrase), editor: PhraseEditorModel::from(&phrase), + player: PhrasePlayerModel::from((&clock, &phrase)), + clock, size: Measure::new(), cursor: (0, 0), - entered: false, split: 20, midi_buf: vec![vec![];65536], note_buf: vec![], @@ -53,7 +44,6 @@ pub struct SequencerTui { pub size: Measure, pub cursor: (usize, usize), pub split: u16, - pub entered: bool, pub note_buf: Vec, pub midi_buf: Vec>>, pub focus: SequencerFocus, @@ -105,181 +95,6 @@ impl Audio for SequencerTui { } } -render!(|self: SequencerTui|{ - - let content = lay!([self.size, Tui::split_up(false, 1, - Tui::fill_xy(SequencerStatusBar::from(self)), - Tui::split_right(false, if self.size.w() > 60 { 20 } else if self.size.w() > 40 { 15 } else { 10 }, - Tui::fixed_x(20, Tui::split_down(false, 4, col!([ - PhraseSelector::play_phrase(&self.player), - PhraseSelector::next_phrase(&self.player), - ]), Tui::split_up(false, 2, - PhraseSelector::edit_phrase(&self.editor.phrase.read().unwrap()), - PhraseListView::from(self), - ))), - col!([ - Tui::fixed_y(2, TransportView::from(( - self, - self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(), - if let SequencerFocus::Transport(_) = self.focus { - true - } else { - false - } - ))), - self.editor - ]), - ) - )]); - - pub struct PhraseSelector { - pub(crate) title: &'static str, - pub(crate) name: String, - pub(crate) color: ItemPalette, - pub(crate) time: String, - } - - // TODO: Display phrases always in order of appearance - render!(|self: PhraseSelector|Tui::fixed_y(2, col!([ - lay!(move|add|{ - add(&Tui::push_x(1, Tui::fg(TuiTheme::g(240), self.title)))?; - add(&Tui::bg(self.color.base.rgb, Tui::fill_x(Tui::inset_x(1, Tui::fill_x(Tui::at_e( - Tui::fg(self.color.lightest.rgb, &self.time)))))))?; - Ok(()) - }), - Tui::bg(self.color.base.rgb, - Tui::fg(self.color.lightest.rgb, - Tui::bold(true, self.name.clone()))), - ]))); - - impl PhraseSelector { - // beats elapsed - pub fn play_phrase (state: &T) -> Self { - let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() { - let Phrase { ref name, color, .. } = *phrase.read().unwrap(); - (name.clone(), color) - } else { - ("".to_string(), ItemPalette::from(TuiTheme::g(64))) - }; - let time = if let Some(elapsed) = state.pulses_since_start_looped() { - format!("+{:>}", state.clock().timebase.format_beats_0(elapsed)) - } else { - String::from("") - }; - Self { title: "Now:", time, name, color, } - } - // beats until switchover - pub fn next_phrase (state: &T) -> Self { - let (time, name, color) = if let Some((t, Some(phrase))) = state.next_phrase() { - let Phrase { ref name, color, .. } = *phrase.read().unwrap(); - let time = { - 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() - } - }; - (time, name.clone(), color) - } else { - ("".into(), "".into(), TuiTheme::g(64).into()) - }; - Self { title: "Next:", time, name, color, } - } - pub fn edit_phrase (phrase: &Option>>) -> Self { - let (time, name, color) = if let Some(phrase) = phrase { - let phrase = phrase.read().unwrap(); - (format!("{}", phrase.length), phrase.name.clone(), phrase.color) - } else { - ("".to_string(), "".to_string(), ItemPalette::from(TuiTheme::g(64))) - }; - Self { title: "Editing:", time, name, color } - } - } - content -}); - -impl HasClock for SequencerTui { - fn clock (&self) -> &ClockModel { - &self.clock - } -} - -impl HasPhrases for SequencerTui { - fn phrases (&self) -> &Vec>> { - &self.phrases.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.phrases.phrases - } -} - -impl HasPhraseList for SequencerTui { - fn phrases_focused (&self) -> bool { - true - } - fn phrases_entered (&self) -> bool { - true - } - fn phrases_mode (&self) -> &Option { - &self.phrases.mode - } - fn phrase_index (&self) -> usize { - self.phrases.phrase.load(Ordering::Relaxed) - } -} - -impl HasEditor for SequencerTui { - fn editor (&self) -> &PhraseEditorModel { - &self.editor - } - fn editor_focused (&self) -> bool { - false - } - fn editor_entered (&self) -> bool { - true - } -} - -impl HasFocus for SequencerTui { - type Item = SequencerFocus; - /// Get the currently focused item. - fn focused (&self) -> Self::Item { - self.focus - } - /// Get the currently focused item. - fn set_focused (&mut self, to: Self::Item) { - self.focus = to - } -} - -impl Into> for SequencerFocus { - fn into (self) -> Option { - if let Self::Transport(transport) = self { - Some(transport) - } else { - None - } - } -} - -impl From<&SequencerTui> for Option { - fn from (state: &SequencerTui) -> Self { - match state.focus { - Transport(focus) => Some(focus), - _ => None - } - } -} - -impl Handle for SequencerTui { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - SequencerCommand::execute_with_state(self, i) - } -} - #[derive(Clone, Debug)] pub enum SequencerCommand { Focus(FocusCommand), @@ -378,6 +193,98 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option 60 { 20 } else if self.size.w() > 40 { 15 } else { 10 }, + Tui::fixed_x(20, Tui::split_down(false, 4, col!([ + PhraseSelector::play_phrase(&self.player), + PhraseSelector::next_phrase(&self.player), + ]), Tui::split_up(false, 2, + PhraseSelector::edit_phrase(&self.editor.phrase.read().unwrap()), + PhraseListView::from(self), + ))), + col!([ + Tui::fixed_y(2, TransportView::from(( + self, + self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(), + if let SequencerFocus::Transport(_) = self.focus { + true + } else { + false + } + ))), + self.editor + ]), + ) +)])); + +pub struct PhraseSelector { + pub(crate) title: &'static str, + pub(crate) name: String, + pub(crate) color: ItemPalette, + pub(crate) time: String, +} + +// TODO: Display phrases always in order of appearance +render!(|self: PhraseSelector|Tui::fixed_y(2, col!([ + lay!(move|add|{ + add(&Tui::push_x(1, Tui::fg(TuiTheme::g(240), self.title)))?; + add(&Tui::bg(self.color.base.rgb, Tui::fill_x(Tui::inset_x(1, Tui::fill_x(Tui::at_e( + Tui::fg(self.color.lightest.rgb, &self.time)))))))?; + Ok(()) + }), + Tui::bg(self.color.base.rgb, + Tui::fg(self.color.lightest.rgb, + Tui::bold(true, self.name.clone()))), +]))); + +impl PhraseSelector { + // beats elapsed + pub fn play_phrase (state: &T) -> Self { + let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() { + let Phrase { ref name, color, .. } = *phrase.read().unwrap(); + (name.clone(), color) + } else { + ("".to_string(), ItemPalette::from(TuiTheme::g(64))) + }; + let time = if let Some(elapsed) = state.pulses_since_start_looped() { + format!("+{:>}", state.clock().timebase.format_beats_0(elapsed)) + } else { + String::from("") + }; + Self { title: "Now:", time, name, color, } + } + // beats until switchover + pub fn next_phrase (state: &T) -> Self { + let (time, name, color) = if let Some((t, Some(phrase))) = state.next_phrase() { + let Phrase { ref name, color, .. } = *phrase.read().unwrap(); + let time = { + 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() + } + }; + (time, name.clone(), color) + } else { + ("".into(), "".into(), TuiTheme::g(64).into()) + }; + Self { title: "Next:", time, name, color, } + } + pub fn edit_phrase (phrase: &Option>>) -> Self { + let (time, name, color) = if let Some(phrase) = phrase { + let phrase = phrase.read().unwrap(); + (format!("{}", phrase.length), phrase.name.clone(), phrase.color) + } else { + ("".to_string(), "".to_string(), ItemPalette::from(TuiTheme::g(64))) + }; + Self { title: "Editing:", time, name, color } + } +} + impl TransportControl for SequencerTui { fn transport_focused (&self) -> Option { match self.focus { @@ -386,3 +293,82 @@ impl TransportControl for SequencerTui { } } } + +impl HasClock for SequencerTui { + fn clock (&self) -> &ClockModel { + &self.clock + } +} + +impl HasPhrases for SequencerTui { + fn phrases (&self) -> &Vec>> { + &self.phrases.phrases + } + fn phrases_mut (&mut self) -> &mut Vec>> { + &mut self.phrases.phrases + } +} + +impl HasPhraseList for SequencerTui { + fn phrases_focused (&self) -> bool { + true + } + fn phrases_entered (&self) -> bool { + true + } + fn phrases_mode (&self) -> &Option { + &self.phrases.mode + } + fn phrase_index (&self) -> usize { + self.phrases.phrase.load(Ordering::Relaxed) + } +} + +impl HasEditor for SequencerTui { + fn editor (&self) -> &PhraseEditorModel { + &self.editor + } + fn editor_focused (&self) -> bool { + false + } + fn editor_entered (&self) -> bool { + true + } +} + +impl HasFocus for SequencerTui { + type Item = SequencerFocus; + /// Get the currently focused item. + fn focused (&self) -> Self::Item { + self.focus + } + /// Get the currently focused item. + fn set_focused (&mut self, to: Self::Item) { + self.focus = to + } +} + +impl Into> for SequencerFocus { + fn into (self) -> Option { + if let Self::Transport(transport) = self { + Some(transport) + } else { + None + } + } +} + +impl From<&SequencerTui> for Option { + fn from (state: &SequencerTui) -> Self { + match state.focus { + Transport(focus) => Some(focus), + _ => None + } + } +} + +impl Handle for SequencerTui { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + SequencerCommand::execute_with_state(self, i) + } +} diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index d014e134..5d862dcd 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -42,6 +42,15 @@ impl Default for PhraseListModel { } } +impl From<&Arc>> for PhraseListModel { + fn from (phrase: &Arc>) -> Self { + let mut model = Self::default(); + model.phrases.push(phrase.clone()); + model.phrase.store(1, Ordering::Relaxed); + model + } +} + impl HasPhrases for PhraseListModel { fn phrases (&self) -> &Vec>> { &self.phrases diff --git a/crates/tek/src/tui/phrase_player.rs b/crates/tek/src/tui/phrase_player.rs index e0928a40..0d70dd70 100644 --- a/crates/tek/src/tui/phrase_player.rs +++ b/crates/tek/src/tui/phrase_player.rs @@ -57,6 +57,14 @@ impl From<&ClockModel> for PhrasePlayerModel { } } +impl From<(&ClockModel, &Arc>)> for PhrasePlayerModel { + fn from ((clock, phrase): (&ClockModel, &Arc>)) -> Self { + let mut model = Self::from(clock); + model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone()))); + model + } +} + impl HasClock for PhrasePlayerModel { fn clock (&self) -> &ClockModel { &self.clock From 46467d3972c5573143a1aa3a0392b8d887700f02 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 12 Dec 2024 23:48:33 +0100 Subject: [PATCH 020/971] simplifying phrase editor --- crates/tek/src/tui/app_sequencer.rs | 22 +- crates/tek/src/tui/phrase_editor.rs | 419 ++++++++++++------------- crates/tek/src/tui/piano_horizontal.rs | 76 +++-- 3 files changed, 261 insertions(+), 256 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 468a0794..218d8571 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -66,31 +66,17 @@ impl Audio for SequencerTui { // Start profiling cycle let t0 = self.perf.get_t0(); // Update transport clock - if ClockAudio(self) - .process(client, scope) == Control::Quit - { + if Control::Quit == ClockAudio(self).process(client, scope) { return Control::Quit } // Update MIDI sequencer - if PlayerAudio(&mut self.player, &mut self.note_buf, &mut self.midi_buf) - .process(client, scope) == Control::Quit - { + if Control::Quit == PlayerAudio( + &mut self.player, &mut self.note_buf, &mut self.midi_buf + ).process(client, scope) { return Control::Quit } // End profiling cycle self.perf.update(t0, scope); - - // Update sequencer playhead indicator - //self.now().set(0.); - //if let Some((ref started_at, Some(ref playing))) = self.player.play_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.now().set(now); - //} - //} Control::Continue } } diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 7c95c640..96518b3f 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -6,201 +6,6 @@ pub trait HasEditor { fn editor_entered (&self) -> bool; } -/// Contains state for viewing and editing a phrase -pub struct PhraseEditorModel { - /// Phrase being played - pub(crate) phrase: Arc>>>>, - /// Renders the phrase - pub(crate) view_mode: Box, - // Lowest note displayed - pub(crate) note_lo: AtomicUsize, - /// Note coordinate of cursor - pub(crate) note_point: AtomicUsize, - /// Length of note that will be inserted, in pulses - pub(crate) note_len: Arc, - /// Notes currently held at input - pub(crate) notes_in: Arc>, - /// Notes currently held at output - pub(crate) notes_out: Arc>, - /// Earliest time displayed - pub(crate) time_start: AtomicUsize, - /// Time coordinate of cursor - pub(crate) time_point: AtomicUsize, - /// Current position of global playhead - pub(crate) now: Arc, - /// Width and height of notes area at last render - pub(crate) size: Measure, -} - -impl From<&Arc>> for PhraseEditorModel { - fn from (phrase: &Arc>) -> Self { - Self::from(Some(phrase.clone())) - } -} - -impl From>>> for PhraseEditorModel { - fn from (phrase: Option>>) -> Self { - let model = Self::default(); - *model.phrase.write().unwrap() = phrase; - model - } -} - -impl std::fmt::Debug for PhraseEditorModel { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("PhraseEditorModel") - .field("note_axis", &format!("{} {}", - self.note_lo.load(Ordering::Relaxed), - self.note_point.load(Ordering::Relaxed), - )) - .field("time_axis", &format!("{} {}", - self.time_start.load(Ordering::Relaxed), - self.time_point.load(Ordering::Relaxed), - )) - .finish() - } -} - -impl Default for PhraseEditorModel { - fn default () -> Self { - let phrase = Arc::new(RwLock::new(None)); - let note_len = Arc::from(AtomicUsize::from(24)); - Self { - size: Measure::new(), - phrase: phrase.clone(), - now: Pulse::default().into(), - time_start: 0.into(), - time_point: 0.into(), - note_lo: 0.into(), - note_point: 0.into(), - note_len: note_len.clone(), - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - view_mode: Box::new(PianoHorizontal { - phrase: Arc::new(RwLock::new(None)), - buffer: Default::default(), - time_zoom: 24, - time_lock: true, - note_zoom: PhraseViewNoteZoom::N(1), - focused: true, - note_len - }), - } - } -} - -impl PhraseEditorModel { - /// Select which pattern to display. This pre-renders it to the buffer at full resolution. - pub fn show_phrase (&mut self, phrase: Option>>) { - *self.view_mode.phrase().write().unwrap() = if phrase.is_some() { - phrase.clone() - } else { - None - }; - self.view_mode.redraw(); - } - /// Put note at current position - pub fn put_note (&mut self, advance: bool) { - if let Some(phrase) = &*self.phrase.read().unwrap() { - let note_len = self.note_len.load(Ordering::Relaxed); - let time = self.time_point.load(Ordering::Relaxed); - let note = self.note_point.load(Ordering::Relaxed); - let mut phrase = phrase.write().unwrap(); - let key: u7 = u7::from(note as u8); - let vel: u7 = 100.into(); - let start = time; - let end = (start + note_len) % phrase.length; - phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); - phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); - self.view_mode.redraw(); - if advance { - let point = self.time_point.load(Ordering::Relaxed); - let length = phrase.length; - let forward = |time|(time + note_len) % length; - self.time_point.store(forward(point), Ordering::Relaxed); - } - } - } -} - -render!(|self: PhraseEditorModel|self.view_mode); - -pub trait PhraseViewMode: Render + Debug + Send + Sync { - fn time_zoom (&self) -> usize; - fn set_time_zoom (&mut self, time_zoom: usize); - fn time_zoom_lock (&self) -> bool; - fn set_time_zoom_lock (&mut self, time_zoom: bool); - fn buffer_size (&self, phrase: &Phrase) -> (usize, usize); - fn redraw (&mut self); - fn phrase (&self) -> &Arc>>>>; -} - -impl PhraseViewMode for PhraseEditorModel { - fn time_zoom (&self) -> usize { - self.view_mode.time_zoom() - } - fn set_time_zoom (&mut self, time_zoom: usize) { - self.view_mode.set_time_zoom(time_zoom) - } - fn time_zoom_lock (&self) -> bool { - self.view_mode.time_zoom_lock() - } - fn set_time_zoom_lock (&mut self, time_lock: bool) { - self.view_mode.set_time_zoom_lock(time_lock); - } - fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { - self.view_mode.buffer_size(phrase) - } - fn redraw (&mut self) { - self.view_mode.redraw() - } - fn phrase (&self) -> &Arc>>>> { - self.view_mode.phrase() - } -} - -pub struct PhraseView<'a> { - note_point: usize, - note_range: (usize, usize), - time_start: usize, - time_point: usize, - note_len: usize, - phrase: Arc>>>>, - view_mode: &'a Box, - now: &'a Arc, - size: &'a Measure, - focused: bool, - entered: bool, -} - -impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { - fn from (state: &'a T) -> Self { - let editor = state.editor(); - let height = editor.size.h(); - let note_point = editor.note_point.load(Ordering::Relaxed); - let mut note_lo = editor.note_lo.load(Ordering::Relaxed); - let mut note_hi = 127.min((note_lo + height).saturating_sub(2)); - if note_point > note_hi { - note_lo += note_point - note_hi; - note_hi = note_point; - editor.note_lo.store(note_lo, Ordering::Relaxed); - } - Self { - note_point, - note_range: (note_lo, note_hi), - time_start: editor.time_start.load(Ordering::Relaxed), - time_point: editor.time_point.load(Ordering::Relaxed), - note_len: editor.note_len.load(Ordering::Relaxed), - phrase: editor.phrase.clone(), - view_mode: &editor.view_mode, - size: &editor.size, - now: &editor.now, - focused: state.editor_focused(), - entered: state.editor_entered(), - } - } -} - #[derive(Clone, Debug)] pub enum PhraseCommand { // TODO: 1-9 seek markers that by default start every 8th of the phrase @@ -220,18 +25,18 @@ pub enum PhraseCommand { impl InputToCommand for PhraseCommand { fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option { use PhraseCommand::*; - use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right}; - let note_lo = state.note_lo.load(Ordering::Relaxed); - let note_point = state.note_point.load(Ordering::Relaxed); - let time_start = state.time_start.load(Ordering::Relaxed); - let time_point = state.time_point.load(Ordering::Relaxed); - let time_zoom = state.view_mode.time_zoom(); + use KeyCode::{Char, Up, Down, PageUp, PageDown, Left, Right}; + let note_lo = state.range.note_lo.load(Ordering::Relaxed); + let note_point = state.point.note_point.load(Ordering::Relaxed); + let time_start = state.range.time_start.load(Ordering::Relaxed); + let time_point = state.point.time_point.load(Ordering::Relaxed); + let time_zoom = state.mode.time_zoom(); let length = state.phrase().read().unwrap().as_ref() .map(|p|p.read().unwrap().length).unwrap_or(1); - let note_len = state.note_len.load(Ordering::Relaxed); + let note_len = state.point.note_len.load(Ordering::Relaxed); Some(match from.event() { key!(Char('`')) => ToggleDirection, - key!(Char('z')) => SetTimeZoomLock(!state.view_mode.time_zoom_lock()), + key!(Char('z')) => SetTimeZoomLock(!state.mode.time_zoom_lock()), key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)), key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)), key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)), @@ -271,21 +76,21 @@ impl Command for PhraseCommand { fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { use PhraseCommand::*; match self { - Show(phrase) => { state.show_phrase(phrase); }, + Show(phrase) => { state.set_phrase(phrase); }, PutNote => { state.put_note(false); }, AppendNote => { state.put_note(true); }, - SetTimeZoom(zoom) => { state.view_mode.set_time_zoom(zoom); }, - SetTimeZoomLock(lock) => { state.view_mode.set_time_zoom_lock(lock); }, - SetTimeScroll(time) => { state.time_start.store(time, Ordering::Relaxed); }, - SetTimeCursor(time) => { state.time_point.store(time, Ordering::Relaxed); }, - SetNoteLength(time) => { state.note_len.store(time, Ordering::Relaxed); }, - SetNoteScroll(note) => { state.note_lo.store(note, Ordering::Relaxed); }, + SetTimeZoom(zoom) => { state.mode.set_time_zoom(zoom); }, + SetTimeZoomLock(lock) => { state.mode.set_time_zoom_lock(lock); }, + SetTimeScroll(time) => { state.range.time_start.store(time, Ordering::Relaxed); }, + SetTimeCursor(time) => { state.point.time_point.store(time, Ordering::Relaxed); }, + SetNoteLength(time) => { state.point.note_len.store(time, Ordering::Relaxed); }, + SetNoteScroll(note) => { state.range.note_lo.store(note, Ordering::Relaxed); }, SetNoteCursor(note) => { let note = 127.min(note); - let start = state.note_lo.load(Ordering::Relaxed); - state.note_point.store(note, Ordering::Relaxed); + let start = state.range.note_lo.load(Ordering::Relaxed); + state.point.note_point.store(note, Ordering::Relaxed); if note < start { - state.note_lo.store(note, Ordering::Relaxed); + state.range.note_lo.store(note, Ordering::Relaxed); } }, @@ -294,3 +99,191 @@ impl Command for PhraseCommand { Ok(None) } } + +/// Contains state for viewing and editing a phrase +pub struct PhraseEditorModel { + /// Phrase being played + pub phrase: Arc>>>>, + /// Renders the phrase + pub mode: Box, + /// The display window + pub range: PhraseEditorRange, + /// The note cursor + pub point: PhraseEditorPoint, +} + +impl Default for PhraseEditorModel { + fn default () -> Self { + let phrase = Arc::new(RwLock::new(None)); + let range = PhraseEditorRange::default(); + let point = PhraseEditorPoint::default(); + let mode = PianoHorizontal::new(&phrase, &range, &point); + Self { phrase, mode: Box::new(mode), range, point } + } +} + +render!(|self: PhraseEditorModel|self.mode); + +pub trait PhraseViewMode: Render + Debug + Send + Sync { + fn time_zoom (&self) -> usize; + fn set_time_zoom (&mut self, time_zoom: usize); + fn time_zoom_lock (&self) -> bool; + fn set_time_zoom_lock (&mut self, time_zoom: bool); + fn buffer_size (&self, phrase: &Phrase) -> (usize, usize); + fn redraw (&mut self); + fn phrase (&self) -> &Arc>>>>; + fn set_phrase (&mut self, phrase: Option>>) { + *self.phrase().write().unwrap() = phrase; + self.redraw(); + } +} + +impl PhraseViewMode for PhraseEditorModel { + fn time_zoom (&self) -> usize { + self.mode.time_zoom() + } + fn set_time_zoom (&mut self, time_zoom: usize) { + self.mode.set_time_zoom(time_zoom) + } + fn time_zoom_lock (&self) -> bool { + self.mode.time_zoom_lock() + } + fn set_time_zoom_lock (&mut self, time_lock: bool) { + self.mode.set_time_zoom_lock(time_lock); + } + fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { + self.mode.buffer_size(phrase) + } + fn redraw (&mut self) { + self.mode.redraw() + } + fn phrase (&self) -> &Arc>>>> { + self.mode.phrase() + } +} + +#[derive(Debug, Clone)] +pub struct PhraseEditorRange { + /// Earliest time displayed + pub time_start: Arc, + /// Time step + pub time_zoom: Arc, + // Lowest note displayed + pub note_lo: Arc, +} + +impl Default for PhraseEditorRange { + fn default () -> Self { + Self { + time_start: Arc::new(0.into()), + time_zoom: Arc::new(24.into()), + note_lo: Arc::new(0.into()), + } + } +} + +#[derive(Debug, Clone)] +pub struct PhraseEditorPoint { + /// Time coordinate of cursor + pub time_point: Arc, + /// Note coordinate of cursor + pub note_point: Arc, + /// Length of note that will be inserted, in pulses + pub note_len: Arc, +} + +impl Default for PhraseEditorPoint { + fn default () -> Self { + Self { + time_point: Arc::new(0.into()), + note_point: Arc::new(0.into()), + note_len: Arc::new(24.into()), + } + } +} + +impl PhraseEditorModel { + /// Put note at current position + pub fn put_note (&mut self, advance: bool) { + if let Some(phrase) = &*self.phrase.read().unwrap() { + let note_len = self.point.note_len.load(Ordering::Relaxed); + let time = self.point.time_point.load(Ordering::Relaxed); + let note = self.point.note_point.load(Ordering::Relaxed); + let mut phrase = phrase.write().unwrap(); + let key: u7 = u7::from(note as u8); + let vel: u7 = 100.into(); + let start = time; + let end = (start + note_len) % phrase.length; + phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); + phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); + self.mode.redraw(); + if advance { + let point = self.point.time_point.load(Ordering::Relaxed); + let length = phrase.length; + let forward = |time|(time + note_len) % length; + self.point.time_point.store(forward(point), Ordering::Relaxed); + } + } + } +} + +impl From<&Arc>> for PhraseEditorModel { + fn from (phrase: &Arc>) -> Self { + Self::from(Some(phrase.clone())) + } +} + +impl From>>> for PhraseEditorModel { + fn from (phrase: Option>>) -> Self { + let model = Self::default(); + *model.phrase.write().unwrap() = phrase; + model + } +} + +impl std::fmt::Debug for PhraseEditorModel { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("PhraseEditorModel") + .field("range", &self.range) + .field("point", &self.point) + .finish() + } +} + +fn autoscroll_notes ( + range: &PhraseEditorRange, point: &PhraseEditorPoint, height: usize +) -> (usize, (usize, usize)) { + let note_point = point.note_point.load(Ordering::Relaxed); + let mut note_lo = range.note_lo.load(Ordering::Relaxed); + let mut note_hi = 127.min((note_lo + height).saturating_sub(2)); + if note_point > note_hi { + note_lo += note_point - note_hi; + note_hi = note_point; + range.note_lo.store(note_lo, Ordering::Relaxed); + } + (note_point, (note_lo, note_hi)) +} + +//impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { + //fn from (state: &'a T) -> Self { + //let editor = state.editor(); + //let (note_point, note_range) = autoscroll_notes( + //&editor.range, + //&editor.point, + //editor.size.h() + //); + //Self { + //note_point, + //note_range, + //time_start: editor.range.time_start.load(Ordering::Relaxed), + //time_point: editor.point.time_point.load(Ordering::Relaxed), + //note_len: editor.point.note_len.load(Ordering::Relaxed), + //phrase: editor.phrase.clone(), + //mode: &editor.mode, + //size: &editor.size, + //now: &editor.now, + //focused: state.editor_focused(), + //entered: state.editor_entered(), + //} + //} +//} diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 0a73e619..cc2a510e 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -1,28 +1,42 @@ use crate::*; use super::*; + +/// A phrase, rendered as a horizontal piano roll. pub struct PianoHorizontal { - pub(crate) phrase: Arc>>>>, - pub(crate) time_lock: bool, - pub(crate) time_zoom: usize, - pub(crate) note_zoom: PhraseViewNoteZoom, - pub(crate) note_len: Arc, - pub(crate) buffer: BigBuffer, - pub(crate) focused: bool, + phrase: Arc>>>>, + time_lock: bool, + time_zoom: Arc, + note_len: Arc, + buffer: BigBuffer, + /// Width and height of notes area at last render + size: Measure, } -#[derive(Copy, Clone, Debug)] -pub enum PhraseViewNoteZoom { - N(usize), - Half, - Octant, + +impl PianoHorizontal { + pub fn new ( + phrase: &Arc>>>>, + range: &PhraseEditorRange, + point: &PhraseEditorPoint, + ) -> Self { + Self { + phrase: phrase.clone(), + buffer: Default::default(), + time_lock: true, + time_zoom: range.time_zoom.clone(), + note_len: point.note_len.clone(), + size: Measure::new() + } + } } + render!(|self: PianoHorizontal|{ - let bg = if self.focused { TuiTheme::g(32) } else { Color::Reset }; + let bg = TuiTheme::g(32); let fg = self.phrase().read().unwrap() .as_ref().map(|p|p.read().unwrap().color) .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); - Tui::bg(bg, Tui::split_up(false, 2, + Tui::bg(bg, Tui::split_down(false, 1, Tui::bg(fg.dark.rgb, PianoHorizontalTimeline { - start: "TIMELINE".into() + start: "|0".into() }), Split::right(false, 5, PianoHorizontalKeys { color: ItemPalette::random(), @@ -30,6 +44,7 @@ render!(|self: PianoHorizontal|{ note_hi: 0, note_point: None }, lay!([ + self.size, PianoHorizontalNotes { source: &self.buffer, time_start: 0, @@ -47,12 +62,14 @@ render!(|self: PianoHorizontal|{ ])), )) }); + pub struct PianoHorizontalTimeline { start: String } render!(|self: PianoHorizontalTimeline|{ Tui::fg(TuiTheme::g(224), Tui::push_x(5, self.start.as_str())) }); + pub struct PianoHorizontalKeys { color: ItemPalette, note_lo: usize, @@ -89,6 +106,7 @@ render!(|self: PianoHorizontalKeys|render(|to|Ok({ }; } }))); + pub struct PianoHorizontalCursor { time_zoom: usize, time_point: usize, @@ -119,6 +137,7 @@ render!(|self: PianoHorizontalCursor|render(|to|Ok({ } } }))); + pub struct PianoHorizontalNotes<'a> { source: &'a BigBuffer, time_start: usize, @@ -146,6 +165,7 @@ render!(|self: PianoHorizontalNotes<'a>|render(|to|Ok({ } } }))); + impl PianoHorizontal { /// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄ fn draw_bg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize, note_len: usize) { @@ -204,15 +224,16 @@ impl PianoHorizontal { } } } + impl PhraseViewMode for PianoHorizontal { fn phrase (&self) -> &Arc>>>> { &self.phrase } fn time_zoom (&self) -> usize { - self.time_zoom + self.time_zoom.load(Ordering::Relaxed) } fn set_time_zoom (&mut self, time_zoom: usize) { - self.time_zoom = time_zoom; + self.time_zoom.store(time_zoom, Ordering::Relaxed); self.redraw() } fn time_zoom_lock (&self) -> bool { @@ -224,13 +245,7 @@ impl PhraseViewMode for PianoHorizontal { } /// Determine the required space to render the phrase. fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { - let width = phrase.length / self.time_zoom(); - let height = match self.note_zoom { - PhraseViewNoteZoom::Half => 64, - PhraseViewNoteZoom::N(n) => 128*n, - _ => unimplemented!() - }; - (width, height) + (phrase.length / self.time_zoom(), 128) } fn redraw (&mut self) { let buffer = if let Some(phrase) = &*self.phrase().read().unwrap() { @@ -246,11 +261,11 @@ impl PhraseViewMode for PianoHorizontal { self.buffer = buffer } } + impl std::fmt::Debug for PianoHorizontal { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("PianoHorizontal") .field("time_zoom", &self.time_zoom) - .field("note_zoom", &self.note_zoom) .field("buffer", &format!("{}x{}", self.buffer.width, self.buffer.height)) .finish() } @@ -287,3 +302,14 @@ impl std::fmt::Debug for PianoHorizontal { //} //} //} + // Update sequencer playhead indicator + //self.now().set(0.); + //if let Some((ref started_at, Some(ref playing))) = self.player.play_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.now().set(now); + //} + //} From 391a3dba089c72e1c93a203ca502074a9294b2f9 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Thu, 12 Dec 2024 23:56:18 +0100 Subject: [PATCH 021/971] darker default color for transport --- crates/tek/src/tui/app_transport.rs | 2 +- crates/tek/src/tui/engine_output.rs | 10 ++-------- crates/tek/src/tui/piano_horizontal.rs | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index 13e60564..258757d1 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -75,7 +75,7 @@ impl From<(&T, Option, bool)> for TransportView { let sr = format!("{:.1}k", clock.timebase.sr.get() / 1000.0); let bpm = format!("{:.3}", clock.timebase.bpm.get()); let ppq = format!("{:.0}", clock.timebase.ppq.get()); - let color = color.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(100)))); + let color = color.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(32)))); let bg = if focused { color.light.rgb } else { color.dark.rgb }; if let Some(started) = clock.started.read().unwrap().as_ref() { let current_sample = (clock.global.sample.get() - started.sample.get())/1000.; diff --git a/crates/tek/src/tui/engine_output.rs b/crates/tek/src/tui/engine_output.rs index 7ab69bb7..6e4070c0 100644 --- a/crates/tek/src/tui/engine_output.rs +++ b/crates/tek/src/tui/engine_output.rs @@ -9,10 +9,7 @@ pub struct TuiOutput { impl Output for TuiOutput { #[inline] fn area (&self) -> [u16;4] { self.area } #[inline] fn area_mut (&mut self) -> &mut [u16;4] { &mut self.area } - #[inline] fn render_in (&mut self, - area: [u16;4], - widget: &dyn Render - ) -> Usually<()> { + #[inline] fn render_in (&mut self, area: [u16;4], widget: &dyn Render) -> Usually<()> { let last = self.area(); *self.area_mut() = area; widget.render(self)?; @@ -22,10 +19,7 @@ impl Output for TuiOutput { } impl TuiOutput { - pub fn buffer_update (&mut self, - area: [u16;4], - callback: &impl Fn(&mut Cell, u16, u16) - ) { + pub fn buffer_update (&mut self, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) { buffer_update(&mut self.buffer, area, callback); } pub fn fill_bold (&mut self, area: [u16;4], on: bool) { diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index cc2a510e..d005c424 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -44,7 +44,7 @@ render!(|self: PianoHorizontal|{ note_hi: 0, note_point: None }, lay!([ - self.size, + //self.size, PianoHorizontalNotes { source: &self.buffer, time_start: 0, From e34a8953575027399b02cdca005ef8ee1dab0777 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Fri, 13 Dec 2024 00:06:25 +0100 Subject: [PATCH 022/971] move PhrasePlayerModel to api/ --- crates/tek/src/api/player.rs | 209 +++++++++++++++++++++++----- crates/tek/src/core.rs | 2 - crates/tek/src/core/edn.rs | 3 +- crates/tek/src/core/pitch.rs | 1 - crates/tek/src/layout.rs | 2 - crates/tek/src/lib.rs | 2 +- crates/tek/src/tui.rs | 1 - crates/tek/src/tui/app_sequencer.rs | 2 +- crates/tek/src/tui/app_transport.rs | 10 +- crates/tek/src/tui/phrase_player.rs | 146 ------------------- 10 files changed, 185 insertions(+), 193 deletions(-) delete mode 100644 crates/tek/src/tui/phrase_player.rs diff --git a/crates/tek/src/api/player.rs b/crates/tek/src/api/player.rs index f1a6809e..4f338961 100644 --- a/crates/tek/src/api/player.rs +++ b/crates/tek/src/api/player.rs @@ -5,42 +5,102 @@ pub trait HasPlayer { fn player_mut (&mut self) -> &mut impl MidiPlayerApi; } -pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {} +/// Contains state for playing a phrase +pub struct PhrasePlayerModel { + /// State of clock and playhead + pub(crate) clock: ClockModel, + /// Start time and phrase being played + pub(crate) play_phrase: Option<(Moment, Option>>)>, + /// Start time and next phrase + pub(crate) next_phrase: Option<(Moment, Option>>)>, + /// Play input through output. + pub(crate) monitoring: bool, + /// Write input to sequence. + pub(crate) recording: bool, + /// Overdub input to sequence. + pub(crate) overdub: bool, + /// Send all notes off + pub(crate) reset: bool, // TODO?: after Some(nframes) + /// Record from MIDI ports to current sequence. + pub midi_ins: Vec>, + /// Play from current sequence to MIDI ports + pub midi_outs: Vec>, + /// Notes currently held at input + pub(crate) notes_in: Arc>, + /// Notes currently held at output + pub(crate) notes_out: Arc>, + /// MIDI output buffer + pub note_buf: Vec, +} -pub trait HasPlayPhrase: HasClock { - fn reset (&self) -> bool; - fn reset_mut (&mut self) -> &mut bool; - fn play_phrase (&self) -> &Option<(Moment, Option>>)>; - fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - fn next_phrase (&self) -> &Option<(Moment, Option>>)>; - 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() { - 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 length = phrase.read().unwrap().length.max(1); // prevent div0 on empty phrase - let elapsed = (elapsed as usize % length) as f64; - Some(elapsed) - } else { - None - } - } - fn enqueue_next (&mut self, phrase: Option<&Arc>>) { - let start = self.clock().next_launch_pulse() as f64; - let instant = Moment::from_pulse(&self.clock().timebase(), start); - let phrase = phrase.map(|p|p.clone()); - *self.next_phrase_mut() = Some((instant, phrase)); - *self.reset_mut() = true; +impl std::fmt::Debug for PhrasePlayerModel { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("PhrasePlayerModel") + .field("clock", &self.clock) + .field("play_phrase", &self.play_phrase) + .field("next_phrase", &self.next_phrase) + .finish() } } +impl From<&ClockModel> for PhrasePlayerModel { + fn from (clock: &ClockModel) -> Self { + Self { + clock: clock.clone(), + midi_ins: vec![], + midi_outs: vec![], + note_buf: vec![0;8], + reset: true, + recording: false, + monitoring: false, + overdub: false, + play_phrase: None, + next_phrase: None, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + } + } +} + +impl From<(&ClockModel, &Arc>)> for PhrasePlayerModel { + fn from ((clock, phrase): (&ClockModel, &Arc>)) -> Self { + let mut model = Self::from(clock); + model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone()))); + model + } +} + +impl HasClock for PhrasePlayerModel { + fn clock (&self) -> &ClockModel { + &self.clock + } +} + +impl HasMidiIns for PhrasePlayerModel { + fn midi_ins (&self) -> &Vec> { + &self.midi_ins + } + fn midi_ins_mut (&mut self) -> &mut Vec> { + &mut self.midi_ins + } +} + +impl HasMidiOuts for PhrasePlayerModel { + fn midi_outs (&self) -> &Vec> { + &self.midi_outs + } + fn midi_outs_mut (&mut self) -> &mut Vec> { + &mut self.midi_outs + } + fn midi_note (&mut self) -> &mut Vec { + &mut self.note_buf + } +} + +pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {} + +impl MidiPlayerApi for PhrasePlayerModel {} + pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns { fn notes_in (&self) -> &Arc>; @@ -115,6 +175,30 @@ pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns { } } +impl MidiRecordApi for PhrasePlayerModel { + fn recording (&self) -> bool { + self.recording + } + fn recording_mut (&mut self) -> &mut bool { + &mut self.recording + } + fn monitoring (&self) -> bool { + self.monitoring + } + fn monitoring_mut (&mut self) -> &mut bool { + &mut self.monitoring + } + fn overdub (&self) -> bool { + self.overdub + } + fn overdub_mut (&mut self) -> &mut bool { + &mut self.overdub + } + fn notes_in (&self) -> &Arc> { + &self.notes_in + } +} + pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts { fn notes_out (&self) -> &Arc>; @@ -248,6 +332,67 @@ pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts { } } +impl MidiPlaybackApi for PhrasePlayerModel { + fn notes_out (&self) -> &Arc> { + &self.notes_in + } +} + +pub trait HasPlayPhrase: HasClock { + fn reset (&self) -> bool; + fn reset_mut (&mut self) -> &mut bool; + fn play_phrase (&self) -> &Option<(Moment, Option>>)>; + fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + fn next_phrase (&self) -> &Option<(Moment, Option>>)>; + 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() { + 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 length = phrase.read().unwrap().length.max(1); // prevent div0 on empty phrase + let elapsed = (elapsed as usize % length) as f64; + Some(elapsed) + } else { + None + } + } + fn enqueue_next (&mut self, phrase: Option<&Arc>>) { + let start = self.clock().next_launch_pulse() as f64; + let instant = Moment::from_pulse(&self.clock().timebase(), start); + let phrase = phrase.map(|p|p.clone()); + *self.next_phrase_mut() = Some((instant, phrase)); + *self.reset_mut() = true; + } +} + +impl HasPlayPhrase for PhrasePlayerModel { + fn reset (&self) -> bool { + self.reset + } + fn reset_mut (&mut self) -> &mut bool { + &mut self.reset + } + fn play_phrase (&self) -> &Option<(Moment, Option>>)> { + &self.play_phrase + } + fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.play_phrase + } + fn next_phrase (&self) -> &Option<(Moment, Option>>)> { + &self.next_phrase + } + fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { + &mut self.next_phrase + } +} + /// Add "all notes off" to the start of a buffer. pub fn all_notes_off (output: &mut [Vec>]) { let mut buf = vec![]; diff --git a/crates/tek/src/core.rs b/crates/tek/src/core.rs index 8a3cd53f..c22c504e 100644 --- a/crates/tek/src/core.rs +++ b/crates/tek/src/core.rs @@ -1,5 +1,3 @@ -use crate::*; - mod audio; pub(crate) use audio::*; mod color; pub(crate) use color::*; mod command; pub(crate) use command::*; diff --git a/crates/tek/src/core/edn.rs b/crates/tek/src/core/edn.rs index 2709043e..b2c50a53 100644 --- a/crates/tek/src/core/edn.rs +++ b/crates/tek/src/core/edn.rs @@ -1,4 +1,5 @@ -pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError}; +pub use clojure_reader::edn::Edn; +//pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError}; /// EDN parsing helper. #[macro_export] macro_rules! edn { diff --git a/crates/tek/src/core/pitch.rs b/crates/tek/src/core/pitch.rs index 6a2ac714..64a2c6d8 100644 --- a/crates/tek/src/core/pitch.rs +++ b/crates/tek/src/core/pitch.rs @@ -1,5 +1,4 @@ use crate::*; -use midly::num::u7; pub fn to_note_name (n: usize) -> &'static str { if n > 127 { diff --git a/crates/tek/src/layout.rs b/crates/tek/src/layout.rs index 168e2022..6b4f538f 100644 --- a/crates/tek/src/layout.rs +++ b/crates/tek/src/layout.rs @@ -1,5 +1,3 @@ -use crate::*; - mod align; pub(crate) use align::*; mod bsp; pub(crate) use bsp::*; mod cond; pub(crate) use cond::*; diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs index 6faafea0..f13af271 100644 --- a/crates/tek/src/lib.rs +++ b/crates/tek/src/lib.rs @@ -19,7 +19,7 @@ pub(crate) use ratatui::{ pub(crate) use jack; pub(crate) use jack::{ Client, ProcessScope, Control, CycleTimes, - Port, PortSpec, MidiIn, MidiOut, AudioIn, AudioOut, Unowned, + Port, PortSpec, MidiIn, MidiOut, AudioOut, Unowned, Transport, TransportState, MidiIter, RawMidi, contrib::ClosureProcessHandler, }; diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 1a962cc1..eecc74af 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -22,7 +22,6 @@ mod piano_horizontal; pub(crate) use piano_horizontal::*; mod phrase_length; pub(crate) use phrase_length::*; mod phrase_rename; pub(crate) use phrase_rename::*; mod phrase_list; pub(crate) use phrase_list::*; -mod phrase_player; pub(crate) use phrase_player::*; mod port_select; pub(crate) use port_select::*; //////////////////////////////////////////////////////// diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 218d8571..f1a22941 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -1,5 +1,5 @@ use crate::{*, api::ClockCommand::{Play, Pause}}; -use KeyCode::{Tab, BackTab, Char, Enter, Esc}; +use KeyCode::{Tab, BackTab, Char}; use SequencerCommand::*; use SequencerFocus::*; use PhraseCommand::*; diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index 258757d1..f618eb48 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -127,12 +127,10 @@ render!(|self: TransportView|{ 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::outset_x(1, Tui::fixed_x(9, col!(|add|if self.0 { + add(&Tui::fg(Color::Rgb(0, 255, 0), col!(["▶ PLAYING", "▒ ▒ ▒ ▒ ▒"]))) + } else { + add(&Tui::fg(Color::Rgb(255, 128, 0), col!(["▒ ▒ ▒ ▒ ▒", "⏹ STOPPED"]))) }))) )); diff --git a/crates/tek/src/tui/phrase_player.rs b/crates/tek/src/tui/phrase_player.rs deleted file mode 100644 index 0d70dd70..00000000 --- a/crates/tek/src/tui/phrase_player.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::*; - -/// Contains state for playing a phrase -pub struct PhrasePlayerModel { - /// State of clock and playhead - pub(crate) clock: ClockModel, - /// Start time and phrase being played - pub(crate) play_phrase: Option<(Moment, Option>>)>, - /// Start time and next phrase - pub(crate) next_phrase: Option<(Moment, Option>>)>, - /// Play input through output. - pub(crate) monitoring: bool, - /// Write input to sequence. - pub(crate) recording: bool, - /// Overdub input to sequence. - pub(crate) overdub: bool, - /// Send all notes off - pub(crate) reset: bool, // TODO?: after Some(nframes) - /// Record from MIDI ports to current sequence. - pub midi_ins: Vec>, - /// Play from current sequence to MIDI ports - pub midi_outs: Vec>, - /// Notes currently held at input - pub(crate) notes_in: Arc>, - /// Notes currently held at output - pub(crate) notes_out: Arc>, - /// MIDI output buffer - pub note_buf: Vec, -} - -impl std::fmt::Debug for PhrasePlayerModel { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("PhrasePlayerModel") - .field("clock", &self.clock) - .field("play_phrase", &self.play_phrase) - .field("next_phrase", &self.next_phrase) - .finish() - } -} - -impl From<&ClockModel> for PhrasePlayerModel { - fn from (clock: &ClockModel) -> Self { - Self { - clock: clock.clone(), - midi_ins: vec![], - midi_outs: vec![], - note_buf: vec![0;8], - reset: true, - recording: false, - monitoring: false, - overdub: false, - play_phrase: None, - next_phrase: None, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - } - } -} - -impl From<(&ClockModel, &Arc>)> for PhrasePlayerModel { - fn from ((clock, phrase): (&ClockModel, &Arc>)) -> Self { - let mut model = Self::from(clock); - model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone()))); - model - } -} - -impl HasClock for PhrasePlayerModel { - fn clock (&self) -> &ClockModel { - &self.clock - } -} - -impl HasPlayPhrase for PhrasePlayerModel { - fn reset (&self) -> bool { - self.reset - } - fn reset_mut (&mut self) -> &mut bool { - &mut self.reset - } - fn play_phrase (&self) -> &Option<(Moment, Option>>)> { - &self.play_phrase - } - fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.play_phrase - } - fn next_phrase (&self) -> &Option<(Moment, Option>>)> { - &self.next_phrase - } - fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.next_phrase - } -} - -impl HasMidiIns for PhrasePlayerModel { - fn midi_ins (&self) -> &Vec> { - &self.midi_ins - } - fn midi_ins_mut (&mut self) -> &mut Vec> { - &mut self.midi_ins - } -} - -impl MidiRecordApi for PhrasePlayerModel { - fn recording (&self) -> bool { - self.recording - } - fn recording_mut (&mut self) -> &mut bool { - &mut self.recording - } - fn monitoring (&self) -> bool { - self.monitoring - } - fn monitoring_mut (&mut self) -> &mut bool { - &mut self.monitoring - } - fn overdub (&self) -> bool { - self.overdub - } - fn overdub_mut (&mut self) -> &mut bool { - &mut self.overdub - } - fn notes_in (&self) -> &Arc> { - &self.notes_in - } -} - -impl HasMidiOuts for PhrasePlayerModel { - fn midi_outs (&self) -> &Vec> { - &self.midi_outs - } - fn midi_outs_mut (&mut self) -> &mut Vec> { - &mut self.midi_outs - } - fn midi_note (&mut self) -> &mut Vec { - &mut self.note_buf - } -} - -impl MidiPlaybackApi for PhrasePlayerModel { - fn notes_out (&self) -> &Arc> { - &self.notes_in - } -} - -impl MidiPlayerApi for PhrasePlayerModel {} From 51351a16dc001bf9384f71cab592647991209b76 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Fri, 13 Dec 2024 00:12:36 +0100 Subject: [PATCH 023/971] prepare sampler entrypoint --- crates/tek/Cargo.toml | 8 ++++---- crates/tek/src/cli/cli_groovebox.rs | 0 crates/tek/src/cli/cli_sampler.rs | 22 ++++++++++++++++++++++ crates/tek/src/core/audio.rs | 14 +++++++------- crates/tek/src/tui/app_sampler.rs | 5 +++++ 5 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 crates/tek/src/cli/cli_groovebox.rs create mode 100644 crates/tek/src/cli/cli_sampler.rs diff --git a/crates/tek/Cargo.toml b/crates/tek/Cargo.toml index 2a39075e..77967f73 100644 --- a/crates/tek/Cargo.toml +++ b/crates/tek/Cargo.toml @@ -43,6 +43,10 @@ path = "src/cli/cli_sequencer.rs" name = "tek_transport" path = "src/cli/cli_transport.rs" +[[bin]] +name = "tek_sampler" +path = "src/cli/cli_sampler.rs" + #[[bin]] #name = "tek_mixer" #path = "src/cli_mixer.rs" @@ -51,10 +55,6 @@ path = "src/cli/cli_transport.rs" #name = "tek_track" #path = "src/cli_track.rs" -#[[bin]] -#name = "tek_sampler" -#path = "src/cli_sampler.rs" - #[[bin]] #name = "tek_plugin" #path = "src/cli_plugin.rs" diff --git a/crates/tek/src/cli/cli_groovebox.rs b/crates/tek/src/cli/cli_groovebox.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek/src/cli/cli_sampler.rs b/crates/tek/src/cli/cli_sampler.rs new file mode 100644 index 00000000..4b882fb9 --- /dev/null +++ b/crates/tek/src/cli/cli_sampler.rs @@ -0,0 +1,22 @@ +include!("../lib.rs"); + +pub fn main () -> Usually<()> { + SamplerCli::parse().run() +} + +#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct SamplerCli { + /// Name of JACK client + #[arg(short, long)] name: Option, + /// Path to plugin + #[arg(short, long)] path: Option, +} + +impl SamplerCli { + fn run (&self) -> Usually<()> { + Tui::run(JackClient::new("tek_sampler")?.activate_with(|x|{ + let sampler = SamplerTui::try_from(x)?; + Ok(sampler) + })?)?; + Ok(()) + } +} diff --git a/crates/tek/src/core/audio.rs b/crates/tek/src/core/audio.rs index 60939f6e..e9a50577 100644 --- a/crates/tek/src/core/audio.rs +++ b/crates/tek/src/core/audio.rs @@ -51,21 +51,21 @@ pub trait AudioEngine { fn thread_init (&self, _: &Client) {} - unsafe fn shutdown (&mut self, status: ClientStatus, reason: &str) {} + unsafe fn shutdown (&mut self, _status: ClientStatus, _reason: &str) {} - fn freewheel (&mut self, _: &Client, enabled: bool) {} + fn freewheel (&mut self, _: &Client, _enabled: bool) {} - fn client_registration (&mut self, _: &Client, name: &str, reg: bool) {} + fn client_registration (&mut self, _: &Client, _name: &str, _reg: bool) {} - fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) {} + fn port_registration (&mut self, _: &Client, _id: PortId, _reg: bool) {} - fn ports_connected (&mut self, _: &Client, a: PortId, b: PortId, are: bool) {} + fn ports_connected (&mut self, _: &Client, _a: PortId, _b: PortId, _are: bool) {} - fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control { + fn sample_rate (&mut self, _: &Client, _frames: Frames) -> Control { Control::Continue } - fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + fn port_rename (&mut self, _: &Client, _id: PortId, _old: &str, _new: &str) -> Control { Control::Continue } diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index f733577b..92b3843d 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -93,6 +93,11 @@ impl SamplerTui { None } } +impl Audio for SamplerTui { + #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + todo!() + } +} pub struct AddSampleModal { exited: bool, From 66c29525be03072d67db08020d9ab9a04c58c140 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Fri, 13 Dec 2024 01:14:14 +0100 Subject: [PATCH 024/971] wip: some trickery with piano roll size --- crates/tek/src/core/tui.rs | 0 crates/tek/src/layout/measure.rs | 66 +++++++---- crates/tek/src/tui/app_sampler.rs | 2 +- crates/tek/src/tui/phrase_editor.rs | 153 +++++++++++++++---------- crates/tek/src/tui/piano_horizontal.rs | 63 +++++----- 5 files changed, 165 insertions(+), 119 deletions(-) delete mode 100644 crates/tek/src/core/tui.rs diff --git a/crates/tek/src/core/tui.rs b/crates/tek/src/core/tui.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/tek/src/layout/measure.rs b/crates/tek/src/layout/measure.rs index cc528435..798366f8 100644 --- a/crates/tek/src/layout/measure.rs +++ b/crates/tek/src/layout/measure.rs @@ -2,38 +2,46 @@ use crate::*; /// A widget that tracks its render width and height #[derive(Default)] -pub struct Measure(PhantomData, AtomicUsize, AtomicUsize, bool); +pub struct Measure { + _engine: PhantomData, + pub x: Arc, + pub y: Arc, +} impl Clone for Measure { fn clone (&self) -> Self { - Self( - Default::default(), - AtomicUsize::from(self.1.load(Ordering::Relaxed)), - AtomicUsize::from(self.2.load(Ordering::Relaxed)), - self.3 - ) + Self { + _engine: Default::default(), + x: self.x.clone(), + y: self.y.clone(), + } } } impl std::fmt::Debug for Measure { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Measure") - .field("width", &self.1) - .field("height", &self.2) + .field("width", &self.x) + .field("height", &self.y) .finish() } } impl Measure { - pub fn w (&self) -> usize { self.1.load(Ordering::Relaxed) } - pub fn h (&self) -> usize { self.2.load(Ordering::Relaxed) } + pub fn w (&self) -> usize { self.x.load(Ordering::Relaxed) } + pub fn h (&self) -> usize { self.y.load(Ordering::Relaxed) } pub fn wh (&self) -> [usize;2] { [self.w(), self.h()] } - pub fn set_w (&self, w: impl Into) { self.1.store(w.into(), Ordering::Relaxed) } - pub fn set_h (&self, h: impl Into) { self.2.store(h.into(), Ordering::Relaxed) } + pub fn set_w (&self, w: impl Into) { self.x.store(w.into(), Ordering::Relaxed) } + pub fn set_h (&self, h: impl Into) { self.y.store(h.into(), Ordering::Relaxed) } pub fn set_wh (&self, w: impl Into, h: impl Into) { self.set_w(w); self.set_h(h); } - pub fn new () -> Self { Self(PhantomData::default(), 0.into(), 0.into(), false) } - pub fn debug () -> Self { Self(PhantomData::default(), 0.into(), 0.into(), true) } pub fn format (&self) -> String { format!("{}x{}", self.w(), self.h()) } + pub fn new () -> Self { + Self { + _engine: PhantomData::default(), + x: Arc::new(0.into()), + y: Arc::new(0.into()), + } + } } impl Render for Measure { @@ -41,14 +49,24 @@ impl Render for Measure { Ok(Some([0u16.into(), 0u16.into()].into())) } fn render (&self, to: &mut TuiOutput) -> Usually<()> { - let w = to.area().w(); - self.set_w(w); - let h = to.area().h(); - self.set_h(h); - Ok(if self.3 { - to.blit(&format!(" {w} x {h} "), to.area.x(), to.area.y(), Some( - Style::default().bold().italic().bg(Color::Rgb(255, 0, 255)).fg(Color::Rgb(0,0,0)) - )) - }) + self.set_w(to.area().w()); + self.set_h(to.area().h()); + Ok(()) } } + +impl Measure { + pub fn debug (&self) -> ShowMeasure { + let measure: Measure = (*self).clone(); + ShowMeasure(measure) + } +} + +pub struct ShowMeasure(Measure); +render!(|self: ShowMeasure|render(|to|Ok({ + let w = self.0.w(); + let h = self.0.h(); + to.blit(&format!(" {w} x {h} "), to.area.x(), to.area.y(), Some( + Style::default().bold().italic().bg(Color::Rgb(255, 0, 255)).fg(Color::Rgb(0,0,0)) + )) +}))); diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index 92b3843d..728d0b6c 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -95,7 +95,7 @@ impl SamplerTui { } impl Audio for SamplerTui { #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - todo!() + Control::Continue } } diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 96518b3f..9314d37b 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -1,4 +1,5 @@ use crate::*; +use Ordering::Relaxed; pub trait HasEditor { fn editor (&self) -> &PhraseEditorModel; @@ -26,17 +27,19 @@ impl InputToCommand for PhraseCommand { fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option { use PhraseCommand::*; use KeyCode::{Char, Up, Down, PageUp, PageDown, Left, Right}; - let note_lo = state.range.note_lo.load(Ordering::Relaxed); - let note_point = state.point.note_point.load(Ordering::Relaxed); - let time_start = state.range.time_start.load(Ordering::Relaxed); - let time_point = state.point.time_point.load(Ordering::Relaxed); - let time_zoom = state.mode.time_zoom(); + let point = state.point(); + let note_point = point.note_point.load(Relaxed); + let time_point = point.time_point.load(Relaxed); + let note_len = point.note_len.load(Relaxed); + let range = state.range(); + let note_lo = range.note_lo.load(Relaxed); + let time_start = range.time_start.load(Relaxed); + let time_zoom = range.time_zoom(); let length = state.phrase().read().unwrap().as_ref() .map(|p|p.read().unwrap().length).unwrap_or(1); - let note_len = state.point.note_len.load(Ordering::Relaxed); Some(match from.event() { key!(Char('`')) => ToggleDirection, - key!(Char('z')) => SetTimeZoomLock(!state.mode.time_zoom_lock()), + key!(Char('z')) => SetTimeZoomLock(!state.range().time_lock()), key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)), key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)), key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)), @@ -75,22 +78,24 @@ impl InputToCommand for PhraseCommand { impl Command for PhraseCommand { fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { use PhraseCommand::*; + let range = state.range(); + let point = state.point(); match self { - Show(phrase) => { state.set_phrase(phrase); }, - PutNote => { state.put_note(false); }, - AppendNote => { state.put_note(true); }, - SetTimeZoom(zoom) => { state.mode.set_time_zoom(zoom); }, - SetTimeZoomLock(lock) => { state.mode.set_time_zoom_lock(lock); }, - SetTimeScroll(time) => { state.range.time_start.store(time, Ordering::Relaxed); }, - SetTimeCursor(time) => { state.point.time_point.store(time, Ordering::Relaxed); }, - SetNoteLength(time) => { state.point.note_len.store(time, Ordering::Relaxed); }, - SetNoteScroll(note) => { state.range.note_lo.store(note, Ordering::Relaxed); }, - SetNoteCursor(note) => { + Show(phrase) => { state.set_phrase(phrase); }, + PutNote => { state.put_note(false); }, + AppendNote => { state.put_note(true); }, + SetTimeZoom(x) => { range.set_time_zoom(x); }, + SetTimeZoomLock(x) => { range.set_time_lock(x); }, + SetTimeScroll(x) => { range.set_time_start(x); }, + SetNoteScroll(x) => { range.set_note_lo(x); }, + SetNoteLength(x) => { point.set_note_len(x); }, + SetTimeCursor(x) => { point.set_time_point(x); }, + SetNoteCursor(note) => { let note = 127.min(note); - let start = state.range.note_lo.load(Ordering::Relaxed); - state.point.note_point.store(note, Ordering::Relaxed); + let start = range.note_lo.load(Relaxed); + point.note_point.store(note, Relaxed); if note < start { - state.range.note_lo.store(note, Ordering::Relaxed); + range.note_lo.store(note, Relaxed); } }, @@ -106,29 +111,21 @@ pub struct PhraseEditorModel { pub phrase: Arc>>>>, /// Renders the phrase pub mode: Box, - /// The display window - pub range: PhraseEditorRange, - /// The note cursor - pub point: PhraseEditorPoint, } impl Default for PhraseEditorModel { fn default () -> Self { let phrase = Arc::new(RwLock::new(None)); - let range = PhraseEditorRange::default(); - let point = PhraseEditorPoint::default(); - let mode = PianoHorizontal::new(&phrase, &range, &point); - Self { phrase, mode: Box::new(mode), range, point } + let mode = PianoHorizontal::new(&phrase); + Self { phrase, mode: Box::new(mode) } } } render!(|self: PhraseEditorModel|self.mode); pub trait PhraseViewMode: Render + Debug + Send + Sync { - fn time_zoom (&self) -> usize; - fn set_time_zoom (&mut self, time_zoom: usize); - fn time_zoom_lock (&self) -> bool; - fn set_time_zoom_lock (&mut self, time_zoom: bool); + fn range (&self) -> &PhraseEditorRange; + fn point (&self) -> &PhraseEditorPoint; fn buffer_size (&self, phrase: &Phrase) -> (usize, usize); fn redraw (&mut self); fn phrase (&self) -> &Arc>>>>; @@ -139,17 +136,11 @@ pub trait PhraseViewMode: Render + Debug + Send + Sync { } impl PhraseViewMode for PhraseEditorModel { - fn time_zoom (&self) -> usize { - self.mode.time_zoom() + fn range (&self) -> &PhraseEditorRange { + self.mode.range() } - fn set_time_zoom (&mut self, time_zoom: usize) { - self.mode.set_time_zoom(time_zoom) - } - fn time_zoom_lock (&self) -> bool { - self.mode.time_zoom_lock() - } - fn set_time_zoom_lock (&mut self, time_lock: bool) { - self.mode.set_time_zoom_lock(time_lock); + fn point (&self) -> &PhraseEditorPoint { + self.mode.point() } fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { self.mode.buffer_size(phrase) @@ -164,10 +155,16 @@ impl PhraseViewMode for PhraseEditorModel { #[derive(Debug, Clone)] pub struct PhraseEditorRange { + /// Length of visible time axis + pub time_axis: Arc, /// Earliest time displayed pub time_start: Arc, /// Time step pub time_zoom: Arc, + /// Auto rezoom to fit in time axis + pub time_lock: Arc, + /// Length of visible note axis + pub note_axis: Arc, // Lowest note displayed pub note_lo: Arc, } @@ -175,12 +172,35 @@ pub struct PhraseEditorRange { impl Default for PhraseEditorRange { fn default () -> Self { Self { + time_axis: Arc::new(0.into()), time_start: Arc::new(0.into()), time_zoom: Arc::new(24.into()), + time_lock: Arc::new(true.into()), + note_axis: Arc::new(0.into()), note_lo: Arc::new(0.into()), } } } +impl PhraseEditorRange { + pub fn time_zoom (&self) -> usize { + self.time_zoom.load(Relaxed) + } + pub fn set_time_zoom (&self, x: usize) { + self.time_zoom.store(x, Relaxed); + } + pub fn time_lock (&self) -> bool { + self.time_lock.load(Relaxed) + } + pub fn set_time_lock (&self, x: bool) { + self.time_lock.store(x, Relaxed); + } + pub fn set_time_start (&self, x: usize) { + self.time_start.store(x, Relaxed); + } + pub fn set_note_lo (&self, x: usize) { + self.note_lo.store(x, Relaxed); + } +} #[derive(Debug, Clone)] pub struct PhraseEditorPoint { @@ -201,27 +221,42 @@ impl Default for PhraseEditorPoint { } } } +impl PhraseEditorPoint { + pub fn note_len (&self) -> usize { + self.note_len.load(Relaxed) + } + pub fn set_note_len (&self, x: usize) { + self.note_len.store(x, Relaxed) + } + pub fn time_point (&self) -> usize { + self.time_point.load(Relaxed) + } + pub fn set_time_point (&self, x: usize) { + self.time_point.store(x, Relaxed) + } +} impl PhraseEditorModel { /// Put note at current position pub fn put_note (&mut self, advance: bool) { if let Some(phrase) = &*self.phrase.read().unwrap() { - let note_len = self.point.note_len.load(Ordering::Relaxed); - let time = self.point.time_point.load(Ordering::Relaxed); - let note = self.point.note_point.load(Ordering::Relaxed); + let point = self.point().clone(); + let note_len = point.note_len.load(Relaxed); + let time = point.time_point.load(Relaxed); + let note = point.note_point.load(Relaxed); let mut phrase = phrase.write().unwrap(); - let key: u7 = u7::from(note as u8); - let vel: u7 = 100.into(); - let start = time; - let end = (start + note_len) % phrase.length; + let key: u7 = u7::from(note as u8); + let vel: u7 = 100.into(); + let start = time; + let end = (start + note_len) % phrase.length; phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); self.mode.redraw(); if advance { - let point = self.point.time_point.load(Ordering::Relaxed); + let time = point.time_point.load(Relaxed); let length = phrase.length; let forward = |time|(time + note_len) % length; - self.point.time_point.store(forward(point), Ordering::Relaxed); + point.set_time_point(forward(time)); } } } @@ -244,8 +279,8 @@ impl From>>> for PhraseEditorModel { impl std::fmt::Debug for PhraseEditorModel { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("PhraseEditorModel") - .field("range", &self.range) - .field("point", &self.point) + .field("range", &self.range()) + .field("point", &self.point()) .finish() } } @@ -253,13 +288,13 @@ impl std::fmt::Debug for PhraseEditorModel { fn autoscroll_notes ( range: &PhraseEditorRange, point: &PhraseEditorPoint, height: usize ) -> (usize, (usize, usize)) { - let note_point = point.note_point.load(Ordering::Relaxed); - let mut note_lo = range.note_lo.load(Ordering::Relaxed); + let note_point = point.note_point.load(Relaxed); + let mut note_lo = range.note_lo.load(Relaxed); let mut note_hi = 127.min((note_lo + height).saturating_sub(2)); if note_point > note_hi { note_lo += note_point - note_hi; note_hi = note_point; - range.note_lo.store(note_lo, Ordering::Relaxed); + range.note_lo.store(note_lo, Relaxed); } (note_point, (note_lo, note_hi)) } @@ -275,9 +310,9 @@ fn autoscroll_notes ( //Self { //note_point, //note_range, - //time_start: editor.range.time_start.load(Ordering::Relaxed), - //time_point: editor.point.time_point.load(Ordering::Relaxed), - //note_len: editor.point.note_len.load(Ordering::Relaxed), + //time_start: editor.range.time_start.load(Relaxed), + //time_point: editor.point.time_point.load(Relaxed), + //note_len: editor.point.note_len.load(Relaxed), //phrase: editor.phrase.clone(), //mode: &editor.mode, //size: &editor.size, diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index d005c424..09ba70b2 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -3,28 +3,28 @@ use super::*; /// A phrase, rendered as a horizontal piano roll. pub struct PianoHorizontal { - phrase: Arc>>>>, - time_lock: bool, - time_zoom: Arc, - note_len: Arc, - buffer: BigBuffer, + phrase: Arc>>>>, + buffer: BigBuffer, /// Width and height of notes area at last render - size: Measure, + size: Measure, + /// The display window + range: PhraseEditorRange, + /// The note cursor + point: PhraseEditorPoint, } impl PianoHorizontal { - pub fn new ( - phrase: &Arc>>>>, - range: &PhraseEditorRange, - point: &PhraseEditorPoint, - ) -> Self { + pub fn new (phrase: &Arc>>>>) -> Self { + let size = Measure::new(); + let mut range = PhraseEditorRange::default(); + range.time_axis = size.x.clone(); + range.note_axis = size.y.clone(); Self { - phrase: phrase.clone(), - buffer: Default::default(), - time_lock: true, - time_zoom: range.time_zoom.clone(), - note_len: point.note_len.clone(), - size: Measure::new() + buffer: Default::default(), + phrase: phrase.clone(), + point: PhraseEditorPoint::default(), + range, + size, } } } @@ -229,31 +229,24 @@ impl PhraseViewMode for PianoHorizontal { fn phrase (&self) -> &Arc>>>> { &self.phrase } - fn time_zoom (&self) -> usize { - self.time_zoom.load(Ordering::Relaxed) + fn range (&self) -> &PhraseEditorRange { + &self.range } - fn set_time_zoom (&mut self, time_zoom: usize) { - self.time_zoom.store(time_zoom, Ordering::Relaxed); - self.redraw() - } - fn time_zoom_lock (&self) -> bool { - self.time_lock - } - fn set_time_zoom_lock (&mut self, time_lock: bool) { - self.time_lock = time_lock; - self.redraw() + fn point (&self) -> &PhraseEditorPoint { + &self.point } /// Determine the required space to render the phrase. fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { - (phrase.length / self.time_zoom(), 128) + (phrase.length / self.range.time_zoom(), 128) } fn redraw (&mut self) { let buffer = if let Some(phrase) = &*self.phrase().read().unwrap() { - let phrase = phrase.read().unwrap(); + let phrase = phrase.read().unwrap(); let mut buffer = BigBuffer::from(self.buffer_size(&phrase)); - let note_len = self.note_len.load(Ordering::Relaxed); - PianoHorizontal::draw_bg(&mut buffer, &phrase, self.time_zoom(), note_len); - PianoHorizontal::draw_fg(&mut buffer, &phrase, self.time_zoom()); + let note_len = self.point.note_len(); + let time_zoom = self.range.time_zoom(); + PianoHorizontal::draw_bg(&mut buffer, &phrase, time_zoom, note_len); + PianoHorizontal::draw_fg(&mut buffer, &phrase, time_zoom); buffer } else { Default::default() @@ -265,7 +258,7 @@ impl PhraseViewMode for PianoHorizontal { impl std::fmt::Debug for PianoHorizontal { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("PianoHorizontal") - .field("time_zoom", &self.time_zoom) + .field("time_zoom", &self.range.time_zoom) .field("buffer", &format!("{}x{}", self.buffer.width, self.buffer.height)) .finish() } From e92677d50c73cba8c9e69c090ac173c5d7586944 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Fri, 13 Dec 2024 16:12:32 +0100 Subject: [PATCH 025/971] queue 0 --- crates/tek/src/tui/app_sequencer.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index f1a22941..224c22cf 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -135,6 +135,10 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option Enqueue(Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())), + // 0: Enqueue phrase 0 (stop all) + key!(Char('0')) => + Enqueue(Some(state.phrases.phrases[0].clone())), + // E: Toggle between editing currently playing or other phrase key!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() { let editing_phrase = state.editor.phrase() @@ -199,7 +203,7 @@ render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1, false } ))), - self.editor + //self.editor ]), ) )])); From 3995ec0f03f24d61bf9d83d24ea0a34614e3b380 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 15:24:26 +0100 Subject: [PATCH 026/971] wip: fix phrase editor --- Justfile | 8 ++ crates/tek/src/layout.rs | 1 - crates/tek/src/layout/debug.rs | 11 --- crates/tek/src/layout/measure.rs | 17 +++- crates/tek/src/tui/app_arranger.rs | 1 + crates/tek/src/tui/app_sequencer.rs | 4 +- crates/tek/src/tui/engine_input.rs | 19 ++++ crates/tek/src/tui/phrase_editor.rs | 48 +++++++--- crates/tek/src/tui/piano_horizontal.rs | 117 ++++++++++++------------- 9 files changed, 131 insertions(+), 95 deletions(-) delete mode 100644 crates/tek/src/layout/debug.rs diff --git a/Justfile b/Justfile index 07b49870..19f8e663 100644 --- a/Justfile +++ b/Justfile @@ -17,18 +17,26 @@ ftpush: git push --tags -fu codeberg git push --tags -fu origin transport: + reset cargo run --bin tek_transport arranger: + reset cargo run --bin tek_arranger sequencer: + reset cargo run --bin tek_sequencer sequencer-release: + reset cargo run --release --bin tek_sequencer mixer: + reset cargo run --bin tek_mixer track: + reset cargo run --bin tek_track sampler: + reset cargo run --bin tek_sampler plugin: + reset cargo run --bin tek_plugin diff --git a/crates/tek/src/layout.rs b/crates/tek/src/layout.rs index 6b4f538f..c3007b8e 100644 --- a/crates/tek/src/layout.rs +++ b/crates/tek/src/layout.rs @@ -1,7 +1,6 @@ mod align; pub(crate) use align::*; mod bsp; pub(crate) use bsp::*; mod cond; pub(crate) use cond::*; -mod debug; pub(crate) use debug::*; mod fill; pub(crate) use fill::*; mod fixed; pub(crate) use fixed::*; mod inset_outset; pub(crate) use inset_outset::*; diff --git a/crates/tek/src/layout/debug.rs b/crates/tek/src/layout/debug.rs deleted file mode 100644 index 6de7cbce..00000000 --- a/crates/tek/src/layout/debug.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::*; - -impl> LayoutDebug for W {} - -pub trait LayoutDebug: Render + Sized { - fn debug (self) -> DebugOverlay { - DebugOverlay(Default::default(), self) - } -} - -pub struct DebugOverlay>(PhantomData, pub W); diff --git a/crates/tek/src/layout/measure.rs b/crates/tek/src/layout/measure.rs index 798366f8..12cd39a3 100644 --- a/crates/tek/src/layout/measure.rs +++ b/crates/tek/src/layout/measure.rs @@ -1,5 +1,15 @@ use crate::*; +impl LayoutDebug for E {} + +pub trait LayoutDebug { + fn debug > (other: W) -> DebugOverlay { + DebugOverlay(Default::default(), other) + } +} + +pub struct DebugOverlay>(PhantomData, pub W); + /// A widget that tracks its render width and height #[derive(Default)] pub struct Measure { @@ -57,13 +67,12 @@ impl Render for Measure { impl Measure { pub fn debug (&self) -> ShowMeasure { - let measure: Measure = (*self).clone(); - ShowMeasure(measure) + ShowMeasure(&self) } } -pub struct ShowMeasure(Measure); -render!(|self: ShowMeasure|render(|to|Ok({ +pub struct ShowMeasure<'a>(&'a Measure); +render!(|self: ShowMeasure<'a>|render(|to|Ok({ let w = self.0.w(); let h = self.0.h(); to.blit(&format!(" {w} x {h} "), to.area.x(), to.area.y(), Some( diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 778bd549..1860b848 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -1228,6 +1228,7 @@ fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option Cmd::Editor(PhraseCommand::Show(Some( state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() ))), + // WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor _ => match state.focused() { ArrangerFocus::Transport(_) => { match to_transport_command(state, input)? { diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 224c22cf..7508c363 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -196,14 +196,14 @@ render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1, col!([ Tui::fixed_y(2, TransportView::from(( self, - self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten(), + self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone(), if let SequencerFocus::Transport(_) = self.focus { true } else { false } ))), - //self.editor + Tui::fill_xy(&self.editor) ]), ) )])); diff --git a/crates/tek/src/tui/engine_input.rs b/crates/tek/src/tui/engine_input.rs index 3ac34da0..2d6f5686 100644 --- a/crates/tek/src/tui/engine_input.rs +++ b/crates/tek/src/tui/engine_input.rs @@ -28,20 +28,39 @@ impl Input for TuiInput { } } +//#[macro_export] macro_rules! key_pat { +//} +//#[macro_export] macro_rules! key_expr { +//} + /// Define key pattern in key match statement #[macro_export] macro_rules! key { + (Ctrl-Alt-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, + modifiers: KeyModifiers::CONTROL | KeyModifiers::ALT, + kind: KeyEventKind::Press, + state: KeyEventState::NONE + })) }; + (Ctrl-Alt-$code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, + modifiers: KeyModifiers::CONTROL | KeyModifiers::ALT, + kind: KeyEventKind::Press, + state: KeyEventState::NONE + })) }; + (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 })) }; diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 9314d37b..0af662bd 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -54,21 +54,23 @@ impl InputToCommand for PhraseCommand { key!(Char('>')) => SetNoteLength(next_note_length(note_len)), // TODO: '/' set triplet, '?' set dotted _ => match from.event() { - key!(Up) => SetNoteCursor(note_point + 1), - key!(Down) => SetNoteCursor(note_point.saturating_sub(1)), - key!(PageUp) => SetNoteCursor(note_point + 3), - key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)), - key!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)), - key!(Right) => SetTimeCursor((time_point + note_len) % length), - key!(Shift-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)), - key!(Shift-Right) => SetTimeCursor((time_point + time_zoom) % length), + key!(Up) => SetNoteCursor(note_point + 1), + key!(Down) => SetNoteCursor(note_point.saturating_sub(1)), + key!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)), + key!(Right) => SetTimeCursor((time_point + note_len) % length), + key!(Alt-Up) => SetNoteCursor(note_point + 3), + key!(Alt-Down) => SetNoteCursor(note_point.saturating_sub(3)), + key!(Alt-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)), + key!(Alt-Right) => SetTimeCursor((time_point + time_zoom) % length), - key!(Ctrl-Up) => SetNoteScroll(note_lo + 1), - key!(Ctrl-Down) => SetNoteScroll(note_lo.saturating_sub(1)), - key!(Ctrl-PageUp) => SetNoteScroll(note_point + 3), - key!(Ctrl-PageDown) => SetNoteScroll(note_point.saturating_sub(3)), - key!(Ctrl-Left) => SetTimeScroll(time_start.saturating_sub(note_len)), - key!(Ctrl-Right) => SetTimeScroll(time_start + note_len), + key!(Ctrl-Up) => SetNoteScroll(note_lo + 1), + key!(Ctrl-Down) => SetNoteScroll(note_lo.saturating_sub(1)), + key!(Ctrl-Left) => SetTimeScroll(time_start.saturating_sub(note_len)), + key!(Ctrl-Right) => SetTimeScroll(time_start + note_len), + key!(Ctrl-Alt-Up) => SetNoteScroll(note_point + 3), + key!(Ctrl-Alt-Down) => SetNoteScroll(note_point.saturating_sub(3)), + key!(Ctrl-Alt-Left) => SetTimeScroll(time_point.saturating_sub(time_zoom)), + key!(Ctrl-Alt-Right) => SetTimeScroll((time_point + time_zoom) % length), _ => return None }, }) @@ -151,6 +153,9 @@ impl PhraseViewMode for PhraseEditorModel { fn phrase (&self) -> &Arc>>>> { self.mode.phrase() } + fn set_phrase (&mut self, phrase: Option>>) { + self.mode.set_phrase(phrase) + } } #[derive(Debug, Clone)] @@ -194,12 +199,24 @@ impl PhraseEditorRange { pub fn set_time_lock (&self, x: bool) { self.time_lock.store(x, Relaxed); } + pub fn time_start (&self) -> usize { + self.time_start.load(Relaxed) + } pub fn set_time_start (&self, x: usize) { self.time_start.store(x, Relaxed); } pub fn set_note_lo (&self, x: usize) { self.note_lo.store(x, Relaxed); } + pub fn note_lo (&self) -> usize { + self.note_lo.load(Relaxed) + } + pub fn note_axis (&self) -> usize { + self.note_lo.load(Relaxed) + } + pub fn note_hi (&self) -> usize { + self.note_lo() + self.note_axis() + } } #[derive(Debug, Clone)] @@ -228,6 +245,9 @@ impl PhraseEditorPoint { pub fn set_note_len (&self, x: usize) { self.note_len.store(x, Relaxed) } + pub fn note_point (&self) -> usize { + self.note_point.load(Relaxed) + } pub fn time_point (&self) -> usize { self.time_point.load(Relaxed) } diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 09ba70b2..6b7a019a 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -4,6 +4,7 @@ use super::*; /// A phrase, rendered as a horizontal piano roll. pub struct PianoHorizontal { phrase: Arc>>>>, + /// Buffer where the whole phrase is rerendered on change buffer: BigBuffer, /// Width and height of notes area at last render size: Measure, @@ -11,6 +12,8 @@ pub struct PianoHorizontal { range: PhraseEditorRange, /// The note cursor point: PhraseEditorPoint, + /// The highlight color palette + color: ItemPalette, } impl PianoHorizontal { @@ -19,56 +22,69 @@ impl PianoHorizontal { let mut range = PhraseEditorRange::default(); range.time_axis = size.x.clone(); range.note_axis = size.y.clone(); + let phrase = phrase.clone(); + let color = phrase.read().unwrap().as_ref() + .map(|p|p.read().unwrap().color) + .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); Self { buffer: Default::default(), - phrase: phrase.clone(), point: PhraseEditorPoint::default(), - range, size, + range, + phrase, + color } } } render!(|self: PianoHorizontal|{ - let bg = TuiTheme::g(32); - let fg = self.phrase().read().unwrap() - .as_ref().map(|p|p.read().unwrap().color) - .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); + let bg = TuiTheme::g(32); + let fg = self.color; + let note_lo = self.range.note_lo(); + let note_hi = self.range.note_hi(); + let time_lock = self.range.time_lock(); + let time_start = self.range.time_start(); + let time_zoom = self.range.time_zoom(); + let time_point = self.point.time_point(); + let note_point = self.point.note_point(); + let note_len = self.point.note_len(); Tui::bg(bg, Tui::split_down(false, 1, - Tui::bg(fg.dark.rgb, PianoHorizontalTimeline { - start: "|0".into() - }), - Split::right(false, 5, PianoHorizontalKeys { - color: ItemPalette::random(), - note_lo: 0, - note_hi: 0, - note_point: None - }, lay!([ - //self.size, + Tui::debug(Tui::fill_x(Tui::push_x(5, Tui::bg(fg.darkest.rgb, Tui::fg(fg.lightest.rgb, + PianoHorizontalTimeline { + time_start, + time_zoom, + } + ))))), + Tui::fill_xy(Split::right(true, 5, Tui::debug(lay!([ + self.size, PianoHorizontalNotes { - source: &self.buffer, - time_start: 0, - note_hi: 0, + source: &self.buffer, + time_start, + note_hi, }, PianoHorizontalCursor { - time_zoom: 0, - time_point: 0, - time_start: 0, - note_point: 0, - note_len: 0, - note_hi: 0, - note_lo: 0, + time_zoom, + time_point, + time_start, + note_point: note_point, + note_len: note_len, + note_hi: note_hi, + note_lo: note_lo, }, - ])), + ])), PianoHorizontalKeys { + color: self.color, + note_lo, + note_hi, + note_point: Some(note_point), + })) )) }); pub struct PianoHorizontalTimeline { - start: String + time_start: usize, + time_zoom: usize, } -render!(|self: PianoHorizontalTimeline|{ - Tui::fg(TuiTheme::g(224), Tui::push_x(5, self.start.as_str())) -}); +render!(|self: PianoHorizontalTimeline|format!("{}*{}", self.time_start, self.time_zoom).as_str()); pub struct PianoHorizontalKeys { color: ItemPalette, @@ -253,6 +269,13 @@ impl PhraseViewMode for PianoHorizontal { }; self.buffer = buffer } + fn set_phrase (&mut self, phrase: Option>>) { + *self.phrase().write().unwrap() = phrase; + self.color = self.phrase.read().unwrap().as_ref() + .map(|p|p.read().unwrap().color) + .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); + self.redraw(); + } } impl std::fmt::Debug for PianoHorizontal { @@ -263,38 +286,6 @@ impl std::fmt::Debug for PianoHorizontal { .finish() } } - - //fn render_cursor ( - //&self, - //to: &mut TuiOutput, - //time_point: usize, - //time_start: usize, - //note_point: usize, - //note_len: usize, - //note_hi: usize, - //note_lo: usize, - //) { - //let time_zoom = self.time_zoom; - //let [x0, y0, w, h] = to.area().xywh(); - //let style = Some(Style::default().fg(Color::Rgb(0,255,0))); - //for (y, note) in (note_lo..=note_hi).rev().enumerate() { - //if note == note_point { - //for x in 0..w { - //let time_1 = time_start + x as usize * time_zoom; - //let time_2 = time_1 + time_zoom; - //if time_1 <= time_point && time_point < time_2 { - //to.blit(&"█", x0 + x as u16, y0 + y as u16, style); - //let tail = note_len as u16 / time_zoom as u16; - //for x_tail in (x0 + x + 1)..(x0 + x + tail) { - //to.blit(&"▂", x_tail, y0 + y as u16, style); - //} - //break - //} - //} - //break - //} - //} - //} // Update sequencer playhead indicator //self.now().set(0.); //if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase { From d003af85ca352350ee22a2e5c4ccad81e875d95c Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 16:23:16 +0100 Subject: [PATCH 027/971] rename engine_ -> tui_ --- crates/tek/src/tui.rs | 8 ++++---- crates/tek/src/tui/{engine_input.rs => tui_input.rs} | 0 crates/tek/src/tui/{engine_output.rs => tui_output.rs} | 0 crates/tek/src/tui/{engine_style.rs => tui_style.rs} | 0 crates/tek/src/tui/{engine_theme.rs => tui_theme.rs} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename crates/tek/src/tui/{engine_input.rs => tui_input.rs} (100%) rename crates/tek/src/tui/{engine_output.rs => tui_output.rs} (100%) rename crates/tek/src/tui/{engine_style.rs => tui_style.rs} (100%) rename crates/tek/src/tui/{engine_theme.rs => tui_theme.rs} (100%) diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index eecc74af..3a7325f0 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -1,9 +1,9 @@ use crate::*; -mod engine_input; pub(crate) use engine_input::*; -mod engine_style; pub(crate) use engine_style::*; -mod engine_theme; pub(crate) use engine_theme::*; -mod engine_output; pub(crate) use engine_output::*; +mod tui_input; pub(crate) use tui_input::*; +mod tui_style; pub(crate) use tui_style::*; +mod tui_theme; pub(crate) use tui_theme::*; +mod tui_output; pub(crate) use tui_output::*; //////////////////////////////////////////////////////// diff --git a/crates/tek/src/tui/engine_input.rs b/crates/tek/src/tui/tui_input.rs similarity index 100% rename from crates/tek/src/tui/engine_input.rs rename to crates/tek/src/tui/tui_input.rs diff --git a/crates/tek/src/tui/engine_output.rs b/crates/tek/src/tui/tui_output.rs similarity index 100% rename from crates/tek/src/tui/engine_output.rs rename to crates/tek/src/tui/tui_output.rs diff --git a/crates/tek/src/tui/engine_style.rs b/crates/tek/src/tui/tui_style.rs similarity index 100% rename from crates/tek/src/tui/engine_style.rs rename to crates/tek/src/tui/tui_style.rs diff --git a/crates/tek/src/tui/engine_theme.rs b/crates/tek/src/tui/tui_theme.rs similarity index 100% rename from crates/tek/src/tui/engine_theme.rs rename to crates/tek/src/tui/tui_theme.rs From 29abe29504163d96c0cca68bbb4be99a018b91eb Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 19:13:28 +0100 Subject: [PATCH 028/971] split key macro into key_pat and key_expr --- crates/tek/src/api.rs | 2 - crates/tek/src/core/focus.rs | 20 +- crates/tek/src/core/pitch.rs | 2 - crates/tek/src/tui.rs | 2 +- crates/tek/src/tui/app_arranger.rs | 546 ++++++++++++++-------------- crates/tek/src/tui/app_sampler.rs | 12 +- crates/tek/src/tui/app_sequencer.rs | 16 +- crates/tek/src/tui/app_transport.rs | 44 +-- crates/tek/src/tui/file_browser.rs | 44 +-- crates/tek/src/tui/phrase_editor.rs | 56 +-- crates/tek/src/tui/phrase_list.rs | 258 ++++++------- crates/tek/src/tui/phrase_rename.rs | 8 +- crates/tek/src/tui/tui_input.rs | 91 ++--- 13 files changed, 550 insertions(+), 551 deletions(-) diff --git a/crates/tek/src/api.rs b/crates/tek/src/api.rs index 88951c7a..013b4388 100644 --- a/crates/tek/src/api.rs +++ b/crates/tek/src/api.rs @@ -1,5 +1,3 @@ -use crate::*; - mod phrase; pub(crate) use phrase::*; mod jack; pub(crate) use self::jack::*; mod clip; pub(crate) use clip::*; diff --git a/crates/tek/src/core/focus.rs b/crates/tek/src/core/focus.rs index fe16eeae..0bae0cb6 100644 --- a/crates/tek/src/core/focus.rs +++ b/crates/tek/src/core/focus.rs @@ -259,16 +259,16 @@ pub trait FocusWrap { pub fn to_focus_command (input: &TuiInput) -> Option> { use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc}; Some(match input.event() { - key!(Tab) => FocusCommand::Next, - key!(Shift-Tab) => FocusCommand::Prev, - key!(BackTab) => FocusCommand::Prev, - key!(Shift-BackTab) => FocusCommand::Prev, - key!(Up) => FocusCommand::Up, - key!(Down) => FocusCommand::Down, - key!(Left) => FocusCommand::Left, - key!(Right) => FocusCommand::Right, - key!(Enter) => FocusCommand::Enter, - key!(Esc) => FocusCommand::Exit, + key_pat!(Tab) => FocusCommand::Next, + key_pat!(Shift-Tab) => FocusCommand::Prev, + key_pat!(BackTab) => FocusCommand::Prev, + key_pat!(Shift-BackTab) => FocusCommand::Prev, + key_pat!(Up) => FocusCommand::Up, + key_pat!(Down) => FocusCommand::Down, + key_pat!(Left) => FocusCommand::Left, + key_pat!(Right) => FocusCommand::Right, + key_pat!(Enter) => FocusCommand::Enter, + key_pat!(Esc) => FocusCommand::Exit, _ => return None }) } diff --git a/crates/tek/src/core/pitch.rs b/crates/tek/src/core/pitch.rs index 64a2c6d8..e442c60f 100644 --- a/crates/tek/src/core/pitch.rs +++ b/crates/tek/src/core/pitch.rs @@ -1,5 +1,3 @@ -use crate::*; - pub fn to_note_name (n: usize) -> &'static str { if n > 127 { panic!("to_note_name({n}): must be 0-127"); diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 3a7325f0..0fdb9d50 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -114,7 +114,7 @@ impl Tui { if ::crossterm::event::poll(poll).is_ok() { let event = TuiEvent::Input(::crossterm::event::read().unwrap()); match event { - key!(Ctrl-KeyCode::Char('c')) => { + key_pat!(Ctrl-KeyCode::Char('c')) => { exited.store(true, Ordering::Relaxed); }, _ => { diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 1860b848..0c1080fb 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -1,11 +1,7 @@ -use crate::{ - *, - api::{ - ArrangerTrackCommand, - ArrangerSceneCommand, - ArrangerClipCommand - } -}; +use crate::*; +use crate::api::ArrangerTrackCommand; +use crate::api::ArrangerSceneCommand; +use crate::api::ArrangerClipCommand; impl TryFrom<&Arc>> for ArrangerTui { type Error = Box; @@ -61,6 +57,273 @@ pub struct ArrangerTui { pub perf: PerfModel, } +impl Handle for ArrangerTui { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + ArrangerCommand::execute_with_state(self, i) + } +} + +#[derive(Clone, Debug)] +pub enum ArrangerCommand { + Focus(FocusCommand), + Undo, + Redo, + Clear, + Color(ItemColor), + Clock(ClockCommand), + Scene(ArrangerSceneCommand), + Track(ArrangerTrackCommand), + Clip(ArrangerClipCommand), + Select(ArrangerSelection), + Zoom(usize), + Phrases(PhrasesCommand), + Editor(PhraseCommand), +} + +impl Command for ArrangerCommand { + fn execute (self, state: &mut ArrangerTui) -> Perhaps { + use ArrangerCommand::*; + Ok(match self { + Focus(cmd) => cmd.execute(state)?.map(Focus), + Scene(cmd) => cmd.execute(state)?.map(Scene), + Track(cmd) => cmd.execute(state)?.map(Track), + Clip(cmd) => cmd.execute(state)?.map(Clip), + Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), + Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), + Clock(cmd) => cmd.execute(state)?.map(Clock), + Zoom(_) => { todo!(); }, + Select(selected) => { + *state.selected_mut() = selected; + None + }, + _ => { todo!() } + }) + } +} + +impl Command for ArrangerSceneCommand { + fn execute (self, _state: &mut ArrangerTui) -> Perhaps { + //todo!(); + Ok(None) + } +} + +impl Command for ArrangerTrackCommand { + fn execute (self, _state: &mut ArrangerTui) -> Perhaps { + //todo!(); + Ok(None) + } +} + +impl Command for ArrangerClipCommand { + fn execute (self, _state: &mut ArrangerTui) -> Perhaps { + //todo!(); + Ok(None) + } +} + +pub trait ArrangerControl: TransportControl { + fn selected (&self) -> ArrangerSelection; + fn selected_mut (&mut self) -> &mut ArrangerSelection; + fn activate (&mut self) -> Usually<()>; + fn selected_phrase (&self) -> Option>>; + fn toggle_loop (&mut self); + fn randomize_color (&mut self); +} + +impl ArrangerControl for ArrangerTui { + fn selected (&self) -> ArrangerSelection { + self.selected + } + fn selected_mut (&mut self) -> &mut ArrangerSelection { + &mut self.selected + } + fn activate (&mut self) -> Usually<()> { + if let ArrangerSelection::Scene(s) = self.selected { + for (t, track) in self.tracks.iter_mut().enumerate() { + let phrase = self.scenes[s].clips[t].clone(); + if track.player.play_phrase.is_some() || phrase.is_some() { + track.player.enqueue_next(phrase.as_ref()); + } + } + if self.clock().is_stopped() { + self.clock().play_from(Some(0))?; + } + } else if let ArrangerSelection::Clip(t, s) = self.selected { + let phrase = self.scenes()[s].clips[t].clone(); + self.tracks_mut()[t].player.enqueue_next(phrase.as_ref()); + }; + Ok(()) + } + fn selected_phrase (&self) -> Option>> { + self.selected_scene()?.clips.get(self.selected.track()?)?.clone() + } + fn toggle_loop (&mut self) { + if let Some(phrase) = self.selected_phrase() { + phrase.write().unwrap().toggle_loop() + } + } + fn randomize_color (&mut self) { + match self.selected { + ArrangerSelection::Mix => { + self.color = ItemColor::random_dark() + }, + ArrangerSelection::Track(t) => { + self.tracks_mut()[t].color = ItemColor::random() + }, + ArrangerSelection::Scene(s) => { + self.scenes_mut()[s].color = ItemColor::random() + }, + ArrangerSelection::Clip(t, s) => { + if let Some(phrase) = &self.scenes_mut()[s].clips[t] { + phrase.write().unwrap().color = ItemPalette::random(); + } + } + } + } +} +impl InputToCommand for ArrangerCommand { + fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option { + to_arranger_command(state, input) + .or_else(||to_focus_command(input).map(ArrangerCommand::Focus)) + } +} + + +fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { + use ArrangerCommand as Cmd; + use KeyCode::Char; + if !state.entered() { + return None + } + Some(match input.event() { + key_pat!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some( + state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() + ))), + // WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor + _ => match state.focused() { + ArrangerFocus::Transport(_) => { + match to_transport_command(state, input)? { + TransportCommand::Clock(command) => Cmd::Clock(command), + _ => return None, + } + }, + ArrangerFocus::PhraseEditor => { + Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?) + }, + ArrangerFocus::Phrases => { + Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?) + }, + ArrangerFocus::Arranger => { + use ArrangerSelection::*; + match input.event() { + key_pat!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), + key_pat!(Char('+')) => Cmd::Zoom(0), // TODO + key_pat!(Char('=')) => Cmd::Zoom(0), // TODO + key_pat!(Char('_')) => Cmd::Zoom(0), // TODO + key_pat!(Char('-')) => Cmd::Zoom(0), // TODO + key_pat!(Char('`')) => { todo!("toggle state mode") }, + key_pat!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), + key_pat!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), + _ => match state.selected() { + Mix => to_arranger_mix_command(input)?, + Track(t) => to_arranger_track_command(input, t)?, + Scene(s) => to_arranger_scene_command(input, s)?, + Clip(t, s) => to_arranger_clip_command(input, t, s)?, + } + } + } + } + }) +} + +fn to_arranger_mix_command (input: &TuiInput) -> Option { + use KeyCode::{Char, Down, Right, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + Some(match input.event() { + key_pat!(Down) => Cmd::Select(Select::Scene(0)), + key_pat!(Right) => Cmd::Select(Select::Track(0)), + key_pat!(Char(',')) => Cmd::Zoom(0), + key_pat!(Char('.')) => Cmd::Zoom(0), + key_pat!(Char('<')) => Cmd::Zoom(0), + key_pat!(Char('>')) => Cmd::Zoom(0), + key_pat!(Delete) => Cmd::Clear, + key_pat!(Char('c')) => Cmd::Color(ItemColor::random()), + _ => return None + }) +} + +fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { + use KeyCode::{Char, Down, Left, Right, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerTrackCommand as Track; + Some(match input.event() { + key_pat!(Down) => Cmd::Select(Select::Clip(t, 0)), + key_pat!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }), + key_pat!(Right) => Cmd::Select(Select::Track(t + 1)), + key_pat!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)), + key_pat!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)), + key_pat!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)), + key_pat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), + key_pat!(Delete) => Cmd::Track(Track::Delete(t)), + //key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())), + _ => return None + }) +} + +fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { + use KeyCode::{Char, Up, Down, Right, Enter, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerSceneCommand as Scene; + Some(match input.event() { + key_pat!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }), + key_pat!(Down) => Cmd::Select(Select::Scene(s + 1)), + key_pat!(Right) => Cmd::Select(Select::Clip(0, s)), + key_pat!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)), + key_pat!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)), + key_pat!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)), + key_pat!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)), + key_pat!(Enter) => Cmd::Scene(Scene::Play(s)), + key_pat!(Delete) => Cmd::Scene(Scene::Delete(s)), + //key_pat!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())), + _ => return None + }) +} + +fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option { + use KeyCode::{Char, Up, Down, Left, Right, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerClipCommand as Clip; + Some(match input.event() { + key_pat!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }), + key_pat!(Down) => Cmd::Select(Select::Clip(t, s + 1)), + key_pat!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }), + key_pat!(Right) => Cmd::Select(Select::Clip(t + 1, s)), + key_pat!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)), + key_pat!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)), + key_pat!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)), + key_pat!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)), + key_pat!(Delete) => Cmd::Clip(Clip::Set(t, s, None)), + //key_pat!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())), + //key_pat!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))), + //key_pat!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))), + _ => return None + }) +} + +impl TransportControl for ArrangerTui { + fn transport_focused (&self) -> Option { + match self.focus.inner() { + ArrangerFocus::Transport(focus) => Some(focus), + _ => None + } + } +} + impl Audio for ArrangerTui { #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { // Start profiling cycle @@ -1084,270 +1347,3 @@ impl ArrangerSelection { } } } - -impl Handle for ArrangerTui { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - ArrangerCommand::execute_with_state(self, i) - } -} - -#[derive(Clone, Debug)] -pub enum ArrangerCommand { - Focus(FocusCommand), - Undo, - Redo, - Clear, - Color(ItemColor), - Clock(ClockCommand), - Scene(ArrangerSceneCommand), - Track(ArrangerTrackCommand), - Clip(ArrangerClipCommand), - Select(ArrangerSelection), - Zoom(usize), - Phrases(PhrasesCommand), - Editor(PhraseCommand), -} - -impl Command for ArrangerCommand { - fn execute (self, state: &mut ArrangerTui) -> Perhaps { - use ArrangerCommand::*; - Ok(match self { - Focus(cmd) => cmd.execute(state)?.map(Focus), - Scene(cmd) => cmd.execute(state)?.map(Scene), - Track(cmd) => cmd.execute(state)?.map(Track), - Clip(cmd) => cmd.execute(state)?.map(Clip), - Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), - Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), - Clock(cmd) => cmd.execute(state)?.map(Clock), - Zoom(_) => { todo!(); }, - Select(selected) => { - *state.selected_mut() = selected; - None - }, - _ => { todo!() } - }) - } -} - -impl Command for ArrangerSceneCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} - -impl Command for ArrangerTrackCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} - -impl Command for ArrangerClipCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} - -pub trait ArrangerControl: TransportControl { - fn selected (&self) -> ArrangerSelection; - fn selected_mut (&mut self) -> &mut ArrangerSelection; - fn activate (&mut self) -> Usually<()>; - fn selected_phrase (&self) -> Option>>; - fn toggle_loop (&mut self); - fn randomize_color (&mut self); -} - -impl ArrangerControl for ArrangerTui { - fn selected (&self) -> ArrangerSelection { - self.selected - } - fn selected_mut (&mut self) -> &mut ArrangerSelection { - &mut self.selected - } - fn activate (&mut self) -> Usually<()> { - if let ArrangerSelection::Scene(s) = self.selected { - for (t, track) in self.tracks.iter_mut().enumerate() { - let phrase = self.scenes[s].clips[t].clone(); - if track.player.play_phrase.is_some() || phrase.is_some() { - track.player.enqueue_next(phrase.as_ref()); - } - } - if self.clock().is_stopped() { - self.clock().play_from(Some(0))?; - } - } else if let ArrangerSelection::Clip(t, s) = self.selected { - let phrase = self.scenes()[s].clips[t].clone(); - self.tracks_mut()[t].player.enqueue_next(phrase.as_ref()); - }; - Ok(()) - } - fn selected_phrase (&self) -> Option>> { - self.selected_scene()?.clips.get(self.selected.track()?)?.clone() - } - fn toggle_loop (&mut self) { - if let Some(phrase) = self.selected_phrase() { - phrase.write().unwrap().toggle_loop() - } - } - fn randomize_color (&mut self) { - match self.selected { - ArrangerSelection::Mix => { - self.color = ItemColor::random_dark() - }, - ArrangerSelection::Track(t) => { - self.tracks_mut()[t].color = ItemColor::random() - }, - ArrangerSelection::Scene(s) => { - self.scenes_mut()[s].color = ItemColor::random() - }, - ArrangerSelection::Clip(t, s) => { - if let Some(phrase) = &self.scenes_mut()[s].clips[t] { - phrase.write().unwrap().color = ItemPalette::random(); - } - } - } - } -} -impl InputToCommand for ArrangerCommand { - fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option { - to_arranger_command(state, input) - .or_else(||to_focus_command(input).map(ArrangerCommand::Focus)) - } -} - - -fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { - use ArrangerCommand as Cmd; - use KeyCode::Char; - if !state.entered() { - return None - } - Some(match input.event() { - key!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some( - state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() - ))), - // WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor - _ => match state.focused() { - ArrangerFocus::Transport(_) => { - match to_transport_command(state, input)? { - TransportCommand::Clock(command) => Cmd::Clock(command), - _ => return None, - } - }, - ArrangerFocus::PhraseEditor => { - Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?) - }, - ArrangerFocus::Phrases => { - Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?) - }, - ArrangerFocus::Arranger => { - use ArrangerSelection::*; - match input.event() { - key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), - key!(Char('+')) => Cmd::Zoom(0), // TODO - key!(Char('=')) => Cmd::Zoom(0), // TODO - key!(Char('_')) => Cmd::Zoom(0), // TODO - key!(Char('-')) => Cmd::Zoom(0), // TODO - key!(Char('`')) => { todo!("toggle state mode") }, - key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), - key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), - _ => match state.selected() { - Mix => to_arranger_mix_command(input)?, - Track(t) => to_arranger_track_command(input, t)?, - Scene(s) => to_arranger_scene_command(input, s)?, - Clip(t, s) => to_arranger_clip_command(input, t, s)?, - } - } - } - } - }) -} - -fn to_arranger_mix_command (input: &TuiInput) -> Option { - use KeyCode::{Char, Down, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - Some(match input.event() { - key!(Down) => Cmd::Select(Select::Scene(0)), - key!(Right) => Cmd::Select(Select::Track(0)), - key!(Char(',')) => Cmd::Zoom(0), - key!(Char('.')) => Cmd::Zoom(0), - key!(Char('<')) => Cmd::Zoom(0), - key!(Char('>')) => Cmd::Zoom(0), - key!(Delete) => Cmd::Clear, - key!(Char('c')) => Cmd::Color(ItemColor::random()), - _ => return None - }) -} - -fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { - use KeyCode::{Char, Down, Left, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerTrackCommand as Track; - Some(match input.event() { - key!(Down) => Cmd::Select(Select::Clip(t, 0)), - key!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }), - key!(Right) => Cmd::Select(Select::Track(t + 1)), - key!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)), - key!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)), - key!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)), - key!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), - key!(Delete) => Cmd::Track(Track::Delete(t)), - //key!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())), - _ => return None - }) -} - -fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { - use KeyCode::{Char, Up, Down, Right, Enter, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerSceneCommand as Scene; - Some(match input.event() { - key!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }), - key!(Down) => Cmd::Select(Select::Scene(s + 1)), - key!(Right) => Cmd::Select(Select::Clip(0, s)), - key!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)), - key!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)), - key!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)), - key!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)), - key!(Enter) => Cmd::Scene(Scene::Play(s)), - key!(Delete) => Cmd::Scene(Scene::Delete(s)), - //key!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())), - _ => return None - }) -} - -fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option { - use KeyCode::{Char, Up, Down, Left, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerClipCommand as Clip; - Some(match input.event() { - key!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }), - key!(Down) => Cmd::Select(Select::Clip(t, s + 1)), - key!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }), - key!(Right) => Cmd::Select(Select::Clip(t + 1, s)), - key!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)), - key!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)), - key!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)), - key!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)), - key!(Delete) => Cmd::Clip(Clip::Set(t, s, None)), - //key!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())), - //key!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))), - //key!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))), - _ => return None - }) -} - -impl TransportControl for ArrangerTui { - fn transport_focused (&self) -> Option { - match self.focus.inner() { - ArrangerFocus::Transport(focus) => Some(focus), - _ => None - } - } -} diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index 728d0b6c..2fa6b9a2 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -232,26 +232,26 @@ impl Handle for SamplerTui { let mapped = &self.state.mapped; let voices = &self.state.voices; match from.event() { - key!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 { + key_pat!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 { mapped.len() + unmapped.len() - 1 } else { cursor.0 - 1 }, - key!(KeyCode::Down) => { + key_pat!(KeyCode::Down) => { cursor.0 = (cursor.0 + 1) % (mapped.len() + unmapped.len()); }, - key!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { + key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); }, - key!(KeyCode::Char('a')) => { + key_pat!(KeyCode::Char('a')) => { let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); unmapped.push(sample); }, - key!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { + key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); }, - key!(KeyCode::Enter) => if let Some(sample) = self.sample() { + key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { self.editing = Some(sample.clone()); }, _ => { diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 7508c363..ee2a88e4 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -126,21 +126,21 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option match state.focus { + key_pat!(Tab) | key_pat!(BackTab) | key_pat!(Shift-Tab) | key_pat!(Shift-BackTab) => match state.focus { PhraseEditor => SequencerCommand::Focus(FocusCommand::Set(PhraseList)), _ => SequencerCommand::Focus(FocusCommand::Set(PhraseEditor)), } // Enqueue currently edited phrase - key!(Char('q')) => + key_pat!(Char('q')) => Enqueue(Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())), // 0: Enqueue phrase 0 (stop all) - key!(Char('0')) => + key_pat!(Char('0')) => Enqueue(Some(state.phrases.phrases[0].clone())), // E: Toggle between editing currently playing or other phrase - key!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() { + key_pat!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() { let editing_phrase = state.editor.phrase() .read().unwrap().as_ref() .map(|p|p.read().unwrap().clone()); @@ -155,19 +155,19 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option + key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), // Transport: Play from start or rewind to start - key!(Shift-Char(' ')) => + key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), // Editor: zoom - key!(Char('z')) | key!(Char('-')) | key!(Char('_'))| key!(Char('=')) | key!(Char('+')) => + key_pat!(Char('z')) | key_pat!(Char('-')) | key_pat!(Char('_'))| key_pat!(Char('=')) | key_pat!(Char('+')) => Editor(PhraseCommand::input_to_command(&state.editor, input)?), // List: select phrase to edit, change color - key!(Char('[')) | key!(Char(']')) | key!(Char('c')) | key!(Shift-Char('A')) | key!(Shift-Char('D')) => + key_pat!(Char('[')) | key_pat!(Char(']')) | key_pat!(Char('c')) | key_pat!(Shift-Char('A')) | key_pat!(Shift-Char('D')) => Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?), // Delegate to focused control: diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index f618eb48..b67d0e31 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -265,56 +265,56 @@ where U: Into>, { Some(match input.event() { - key!(Left) => Focus(Prev), - key!(Right) => Focus(Next), - key!(Char(' ')) => Clock(if state.clock().is_stopped() { + 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!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { + key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), _ => match state.transport_focused().unwrap() { TransportFocus::Bpm => match input.event() { - key!(Char(',')) => Clock(SetBpm(state.clock().bpm().get() - 1.0)), - key!(Char('.')) => Clock(SetBpm(state.clock().bpm().get() + 1.0)), - key!(Char('<')) => Clock(SetBpm(state.clock().bpm().get() - 0.001)), - key!(Char('>')) => Clock(SetBpm(state.clock().bpm().get() + 0.001)), + key_pat!(Char(',')) => Clock(SetBpm(state.clock().bpm().get() - 1.0)), + key_pat!(Char('.')) => Clock(SetBpm(state.clock().bpm().get() + 1.0)), + key_pat!(Char('<')) => Clock(SetBpm(state.clock().bpm().get() - 0.001)), + key_pat!(Char('>')) => Clock(SetBpm(state.clock().bpm().get() + 0.001)), _ => return None, }, TransportFocus::Quant => match input.event() { - key!(Char(',')) => Clock(SetQuant(state.clock().quant.prev())), - key!(Char('.')) => Clock(SetQuant(state.clock().quant.next())), - key!(Char('<')) => Clock(SetQuant(state.clock().quant.prev())), - key!(Char('>')) => Clock(SetQuant(state.clock().quant.next())), + key_pat!(Char(',')) => Clock(SetQuant(state.clock().quant.prev())), + key_pat!(Char('.')) => Clock(SetQuant(state.clock().quant.next())), + key_pat!(Char('<')) => Clock(SetQuant(state.clock().quant.prev())), + key_pat!(Char('>')) => Clock(SetQuant(state.clock().quant.next())), _ => return None, }, TransportFocus::Sync => match input.event() { - key!(Char(',')) => Clock(SetSync(state.clock().sync.prev())), - key!(Char('.')) => Clock(SetSync(state.clock().sync.next())), - key!(Char('<')) => Clock(SetSync(state.clock().sync.prev())), - key!(Char('>')) => Clock(SetSync(state.clock().sync.next())), + key_pat!(Char(',')) => Clock(SetSync(state.clock().sync.prev())), + key_pat!(Char('.')) => Clock(SetSync(state.clock().sync.next())), + key_pat!(Char('<')) => Clock(SetSync(state.clock().sync.prev())), + key_pat!(Char('>')) => Clock(SetSync(state.clock().sync.next())), _ => return None, }, TransportFocus::Clock => match input.event() { - key!(Char(',')) => todo!("transport seek bar"), - key!(Char('.')) => todo!("transport seek bar"), - key!(Char('<')) => todo!("transport seek beat"), - key!(Char('>')) => todo!("transport seek beat"), + 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, }, TransportFocus::PlayPause => match input.event() { - key!(Enter) => Clock( + key_pat!(Enter) => Clock( if state.clock().is_stopped() { Play(None) } else { Pause(None) } ), - key!(Shift-Enter) => Clock( + key_pat!(Shift-Enter) => Clock( if state.clock().is_stopped() { Play(Some(0)) } else { diff --git a/crates/tek/src/tui/file_browser.rs b/crates/tek/src/tui/file_browser.rs index 26a40c30..5baee32c 100644 --- a/crates/tek/src/tui/file_browser.rs +++ b/crates/tek/src/tui/file_browser.rs @@ -143,30 +143,30 @@ impl InputToCommand for FileBrowserCommand { fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option { if let Some(PhraseListMode::Import(_index, browser)) = state.phrases_mode() { Some(match from.event() { - key!(Up) => Select( + key_pat!(Up) => Select( browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1)) ), - key!(Down) => Select( + key_pat!(Down) => Select( browser.index.saturating_add(1) % browser.len() ), - key!(Right) => Chdir(browser.cwd.clone()), - key!(Left) => Chdir(browser.cwd.clone()), - key!(Enter) => Confirm, - key!(Char(_)) => { todo!() }, - key!(Backspace) => { todo!() }, - key!(Esc) => Self::Cancel, + key_pat!(Right) => Chdir(browser.cwd.clone()), + key_pat!(Left) => Chdir(browser.cwd.clone()), + key_pat!(Enter) => Confirm, + key_pat!(Char(_)) => { todo!() }, + key_pat!(Backspace) => { todo!() }, + key_pat!(Esc) => Self::Cancel, _ => return None }) } else if let Some(PhraseListMode::Export(_index, browser)) = state.phrases_mode() { Some(match from.event() { - key!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())), - key!(Down) => Select(browser.index.saturating_add(1) % browser.len()), - key!(Right) => Chdir(browser.cwd.clone()), - key!(Left) => Chdir(browser.cwd.clone()), - key!(Enter) => Confirm, - key!(Char(_)) => { todo!() }, - key!(Backspace) => { todo!() }, - key!(Esc) => Self::Cancel, + key_pat!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())), + key_pat!(Down) => Select(browser.index.saturating_add(1) % browser.len()), + key_pat!(Right) => Chdir(browser.cwd.clone()), + key_pat!(Left) => Chdir(browser.cwd.clone()), + key_pat!(Enter) => Confirm, + key_pat!(Char(_)) => { todo!() }, + key_pat!(Backspace) => { todo!() }, + key_pat!(Esc) => Self::Cancel, _ => return None }) } else { @@ -179,12 +179,12 @@ impl InputToCommand for PhraseLengthCommand { fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option { if let Some(PhraseListMode::Length(_, length, _)) = state.phrases_mode() { Some(match from.event() { - key!(Up) => Self::Inc, - key!(Down) => Self::Dec, - key!(Right) => Self::Next, - key!(Left) => Self::Prev, - key!(Enter) => Self::Set(*length), - key!(Esc) => Self::Cancel, + key_pat!(Up) => Self::Inc, + key_pat!(Down) => Self::Dec, + key_pat!(Right) => Self::Next, + key_pat!(Left) => Self::Prev, + key_pat!(Enter) => Self::Set(*length), + key_pat!(Esc) => Self::Cancel, _ => return None }) } else { diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 0af662bd..0f001e24 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -38,39 +38,39 @@ impl InputToCommand for PhraseCommand { let length = state.phrase().read().unwrap().as_ref() .map(|p|p.read().unwrap().length).unwrap_or(1); Some(match from.event() { - key!(Char('`')) => ToggleDirection, - key!(Char('z')) => SetTimeZoomLock(!state.range().time_lock()), - key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)), - key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)), - key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)), - key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)), - key!(Char('a')) => AppendNote, - key!(Char('s')) => PutNote, + key_pat!(Char('`')) => ToggleDirection, + key_pat!(Char('z')) => SetTimeZoomLock(!state.range().time_lock()), + key_pat!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)), + key_pat!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)), + key_pat!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)), + key_pat!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)), + key_pat!(Char('a')) => AppendNote, + key_pat!(Char('s')) => PutNote, // TODO: no triplet/dotted - key!(Char(',')) => SetNoteLength(prev_note_length(note_len)), - key!(Char('.')) => SetNoteLength(next_note_length(note_len)), + key_pat!(Char(',')) => SetNoteLength(prev_note_length(note_len)), + key_pat!(Char('.')) => SetNoteLength(next_note_length(note_len)), // TODO: with triplet/dotted - key!(Char('<')) => SetNoteLength(prev_note_length(note_len)), - key!(Char('>')) => SetNoteLength(next_note_length(note_len)), + key_pat!(Char('<')) => SetNoteLength(prev_note_length(note_len)), + key_pat!(Char('>')) => SetNoteLength(next_note_length(note_len)), // TODO: '/' set triplet, '?' set dotted _ => match from.event() { - key!(Up) => SetNoteCursor(note_point + 1), - key!(Down) => SetNoteCursor(note_point.saturating_sub(1)), - key!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)), - key!(Right) => SetTimeCursor((time_point + note_len) % length), - key!(Alt-Up) => SetNoteCursor(note_point + 3), - key!(Alt-Down) => SetNoteCursor(note_point.saturating_sub(3)), - key!(Alt-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)), - key!(Alt-Right) => SetTimeCursor((time_point + time_zoom) % length), + key_pat!(Up) => SetNoteCursor(note_point + 1), + key_pat!(Down) => SetNoteCursor(note_point.saturating_sub(1)), + key_pat!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)), + key_pat!(Right) => SetTimeCursor((time_point + note_len) % length), + key_pat!(Alt-Up) => SetNoteCursor(note_point + 3), + key_pat!(Alt-Down) => SetNoteCursor(note_point.saturating_sub(3)), + key_pat!(Alt-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)), + key_pat!(Alt-Right) => SetTimeCursor((time_point + time_zoom) % length), - key!(Ctrl-Up) => SetNoteScroll(note_lo + 1), - key!(Ctrl-Down) => SetNoteScroll(note_lo.saturating_sub(1)), - key!(Ctrl-Left) => SetTimeScroll(time_start.saturating_sub(note_len)), - key!(Ctrl-Right) => SetTimeScroll(time_start + note_len), - key!(Ctrl-Alt-Up) => SetNoteScroll(note_point + 3), - key!(Ctrl-Alt-Down) => SetNoteScroll(note_point.saturating_sub(3)), - key!(Ctrl-Alt-Left) => SetTimeScroll(time_point.saturating_sub(time_zoom)), - key!(Ctrl-Alt-Right) => SetTimeScroll((time_point + time_zoom) % length), + key_pat!(Ctrl-Up) => SetNoteScroll(note_lo + 1), + key_pat!(Ctrl-Down) => SetNoteScroll(note_lo.saturating_sub(1)), + key_pat!(Ctrl-Left) => SetTimeScroll(time_start.saturating_sub(note_len)), + key_pat!(Ctrl-Right) => SetTimeScroll(time_start + note_len), + key_pat!(Ctrl-Alt-Up) => SetNoteScroll(note_point + 3), + key_pat!(Ctrl-Alt-Down) => SetNoteScroll(note_point.saturating_sub(3)), + key_pat!(Ctrl-Alt-Left) => SetTimeScroll(time_point.saturating_sub(time_zoom)), + key_pat!(Ctrl-Alt-Right) => SetTimeScroll((time_point + time_zoom) % length), _ => return None }, }) diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 5d862dcd..2967a542 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -31,6 +31,135 @@ pub enum PhraseListMode { Export(usize, FileBrowser), } +#[derive(Clone, PartialEq, Debug)] +pub enum PhrasesCommand { + /// Update the contents of the phrase pool + Phrase(Pool), + /// Select a phrase from the phrase pool + Select(usize), + /// Rename a phrase + Rename(Rename), + /// Change the length of a phrase + Length(Length), + /// Import from file + Import(Browse), + /// Export to file + Export(Browse), +} + +impl Command for PhrasesCommand { + fn execute (self, state: &mut PhraseListModel) -> Perhaps { + use PhrasesCommand::*; + Ok(match self { + Phrase(command) => command.execute(state)?.map(Phrase), + Rename(command) => match command { + PhraseRenameCommand::Begin => { + let length = state.phrases()[state.phrase_index()].read().unwrap().length; + *state.phrases_mode_mut() = Some( + PhraseListMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar) + ); + None + }, + _ => command.execute(state)?.map(Rename) + }, + Length(command) => match command { + PhraseLengthCommand::Begin => { + let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone(); + *state.phrases_mode_mut() = Some( + PhraseListMode::Rename(state.phrase_index(), name) + ); + None + }, + _ => command.execute(state)?.map(Length) + }, + Import(command) => match command { + FileBrowserCommand::Begin => { + *state.phrases_mode_mut() = Some( + PhraseListMode::Import(state.phrase_index(), FileBrowser::new(None)?) + ); + None + }, + _ => command.execute(state)?.map(Import) + }, + Export(command) => match command { + FileBrowserCommand::Begin => { + *state.phrases_mode_mut() = Some( + PhraseListMode::Export(state.phrase_index(), FileBrowser::new(None)?) + ); + None + }, + _ => command.execute(state)?.map(Export) + }, + Select(phrase) => { + state.set_phrase_index(phrase); + None + }, + }) + } +} + +impl InputToCommand for PhrasesCommand { + fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option { + Some(match state.phrases_mode() { + Some(PhraseListMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?), + Some(PhraseListMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?), + Some(PhraseListMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?), + Some(PhraseListMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?), + _ => to_phrases_command(state, input)? + }) + } +} + +fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option { + use KeyCode::{Up, Down, Delete, Char}; + use PhrasesCommand as Cmd; + let index = state.phrase_index(); + let count = state.phrases().len(); + Some(match input.event() { + key_pat!(Char('n')) => Cmd::Rename(Rename::Begin), + key_pat!(Char('t')) => Cmd::Length(Length::Begin), + key_pat!(Char('m')) => Cmd::Import(Browse::Begin), + key_pat!(Char('x')) => Cmd::Export(Browse::Begin), + key_pat!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())), + key_pat!(Char('[')) | key_pat!(Up) => Cmd::Select( + index.overflowing_sub(1).0.min(state.phrases().len() - 1) + ), + key_pat!(Char(']')) | key_pat!(Down) => Cmd::Select( + index.saturating_add(1) % state.phrases().len() + ), + key_pat!(Char('<')) => if index > 1 { + state.set_phrase_index(state.phrase_index().saturating_sub(1)); + Cmd::Phrase(Pool::Swap(index - 1, index)) + } else { + return None + }, + key_pat!(Char('>')) => if index < count.saturating_sub(1) { + state.set_phrase_index(state.phrase_index() + 1); + Cmd::Phrase(Pool::Swap(index + 1, index)) + } else { + return None + }, + key_pat!(Delete) => if index > 0 { + state.set_phrase_index(index.min(count.saturating_sub(1))); + Cmd::Phrase(Pool::Delete(index)) + } else { + return None + }, + key_pat!(Char('a')) | key_pat!(Shift-Char('A')) => Cmd::Phrase(Pool::Add(count, Phrase::new( + String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) + ))), + key_pat!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new( + String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) + ))), + key_pat!(Char('d')) | key_pat!(Shift-Char('D')) => { + let mut phrase = state.phrases()[index].read().unwrap().duplicate(); + phrase.color = ItemPalette::random_near(phrase.color, 0.25); + Cmd::Phrase(Pool::Add(index + 1, phrase)) + }, + _ => return None + }) +} + impl Default for PhraseListModel { fn default () -> Self { Self { @@ -158,132 +287,3 @@ render!(|self: PhraseListView<'a>|{ add(&Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string()))))) })) }); - -#[derive(Clone, PartialEq, Debug)] -pub enum PhrasesCommand { - /// Update the contents of the phrase pool - Phrase(Pool), - /// Select a phrase from the phrase pool - Select(usize), - /// Rename a phrase - Rename(Rename), - /// Change the length of a phrase - Length(Length), - /// Import from file - Import(Browse), - /// Export to file - Export(Browse), -} - -impl Command for PhrasesCommand { - fn execute (self, state: &mut PhraseListModel) -> Perhaps { - use PhrasesCommand::*; - Ok(match self { - Phrase(command) => command.execute(state)?.map(Phrase), - Rename(command) => match command { - PhraseRenameCommand::Begin => { - let length = state.phrases()[state.phrase_index()].read().unwrap().length; - *state.phrases_mode_mut() = Some( - PhraseListMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar) - ); - None - }, - _ => command.execute(state)?.map(Rename) - }, - Length(command) => match command { - PhraseLengthCommand::Begin => { - let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone(); - *state.phrases_mode_mut() = Some( - PhraseListMode::Rename(state.phrase_index(), name) - ); - None - }, - _ => command.execute(state)?.map(Length) - }, - Import(command) => match command { - FileBrowserCommand::Begin => { - *state.phrases_mode_mut() = Some( - PhraseListMode::Import(state.phrase_index(), FileBrowser::new(None)?) - ); - None - }, - _ => command.execute(state)?.map(Import) - }, - Export(command) => match command { - FileBrowserCommand::Begin => { - *state.phrases_mode_mut() = Some( - PhraseListMode::Export(state.phrase_index(), FileBrowser::new(None)?) - ); - None - }, - _ => command.execute(state)?.map(Export) - }, - Select(phrase) => { - state.set_phrase_index(phrase); - None - }, - }) - } -} - -impl InputToCommand for PhrasesCommand { - fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option { - Some(match state.phrases_mode() { - Some(PhraseListMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?), - Some(PhraseListMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?), - Some(PhraseListMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?), - Some(PhraseListMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?), - _ => to_phrases_command(state, input)? - }) - } -} - -fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option { - use KeyCode::{Up, Down, Delete, Char}; - use PhrasesCommand as Cmd; - let index = state.phrase_index(); - let count = state.phrases().len(); - Some(match input.event() { - key!(Char('n')) => Cmd::Rename(Rename::Begin), - key!(Char('t')) => Cmd::Length(Length::Begin), - key!(Char('m')) => Cmd::Import(Browse::Begin), - key!(Char('x')) => Cmd::Export(Browse::Begin), - key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())), - key!(Char('[')) | key!(Up) => Cmd::Select( - index.overflowing_sub(1).0.min(state.phrases().len() - 1) - ), - key!(Char(']')) | key!(Down) => Cmd::Select( - index.saturating_add(1) % state.phrases().len() - ), - key!(Char('<')) => if index > 1 { - state.set_phrase_index(state.phrase_index().saturating_sub(1)); - Cmd::Phrase(Pool::Swap(index - 1, index)) - } else { - return None - }, - key!(Char('>')) => if index < count.saturating_sub(1) { - state.set_phrase_index(state.phrase_index() + 1); - Cmd::Phrase(Pool::Swap(index + 1, index)) - } else { - return None - }, - key!(Delete) => if index > 0 { - state.set_phrase_index(index.min(count.saturating_sub(1))); - Cmd::Phrase(Pool::Delete(index)) - } else { - return None - }, - key!(Char('a')) | key!(Shift-Char('A')) => Cmd::Phrase(Pool::Add(count, Phrase::new( - String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) - ))), - key!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new( - String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) - ))), - key!(Char('d')) | key!(Shift-Char('D')) => { - let mut phrase = state.phrases()[index].read().unwrap().duplicate(); - phrase.color = ItemPalette::random_near(phrase.color, 0.25); - Cmd::Phrase(Pool::Add(index + 1, phrase)) - }, - _ => return None - }) -} diff --git a/crates/tek/src/tui/phrase_rename.rs b/crates/tek/src/tui/phrase_rename.rs index c2bab07a..42233cfc 100644 --- a/crates/tek/src/tui/phrase_rename.rs +++ b/crates/tek/src/tui/phrase_rename.rs @@ -38,18 +38,18 @@ impl InputToCommand for PhraseRenameCommand { use KeyCode::{Char, Backspace, Enter, Esc}; if let Some(PhraseListMode::Rename(_, ref old_name)) = state.phrases_mode() { Some(match from.event() { - key!(Char(c)) => { + key_pat!(Char(c)) => { let mut new_name = old_name.clone(); new_name.push(*c); Self::Set(new_name) }, - key!(Backspace) => { + key_pat!(Backspace) => { let mut new_name = old_name.clone(); new_name.pop(); Self::Set(new_name) }, - key!(Enter) => Self::Confirm, - key!(Esc) => Self::Cancel, + key_pat!(Enter) => Self::Confirm, + key_pat!(Esc) => Self::Cancel, _ => return None }) } else { diff --git a/crates/tek/src/tui/tui_input.rs b/crates/tek/src/tui/tui_input.rs index 2d6f5686..96b7a88e 100644 --- a/crates/tek/src/tui/tui_input.rs +++ b/crates/tek/src/tui/tui_input.rs @@ -28,51 +28,58 @@ impl Input for TuiInput { } } -//#[macro_export] macro_rules! key_pat { -//} -//#[macro_export] macro_rules! key_expr { -//} +#[macro_export] macro_rules! key_event_pat { + ($code:pat, $modifiers: pat) => { + TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { + code: $code, + modifiers: $modifiers, + 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 + })) + }; +} -/// Define key pattern in key match statement -#[macro_export] macro_rules! key { - (Ctrl-Alt-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, - modifiers: KeyModifiers::CONTROL | KeyModifiers::ALT, - kind: KeyEventKind::Press, - state: KeyEventState::NONE - })) }; - (Ctrl-Alt-$code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, - modifiers: KeyModifiers::CONTROL | KeyModifiers::ALT, - kind: KeyEventKind::Press, - state: KeyEventState::NONE - })) }; +#[macro_export] macro_rules! key_event_expr { + ($code:expr, $modifiers: expr) => { + TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { + code: $code, + modifiers: $modifiers, + 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 + })) + }; +} - (Ctrl-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, - modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::NONE - })) }; +#[macro_export] macro_rules! key_pat { + (Ctrl-Alt-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) }; + (Ctrl-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL) }; + (Alt-$code:pat) => { key_event_pat!($code, KeyModifiers::ALT) }; + (Shift-$code:pat) => { key_event_pat!($code, KeyModifiers::SHIFT) }; + ($code:pat) => { key_event_pat!($code) }; +} - (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 - })) }; +#[macro_export] macro_rules! key_expr { + (Ctrl-Alt-$code:expr) => { key_event_expr!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) }; + (Ctrl-$code:expr) => { key_event_expr!($code, KeyModifiers::CONTROL) }; + (Alt-$code:expr) => { key_event_expr!($code, KeyModifiers::ALT) }; + (Shift-$code:expr) => { key_event_expr!($code, KeyModifiers::SHIFT) }; + ($code:expr) => { key_event_expr!($code) }; } /* From 70794e3cb9b5a945feca32a56733cf034eabe92b Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 19:37:00 +0100 Subject: [PATCH 029/971] simplify sequencer input delegation --- crates/tek/src/tui/app_sequencer.rs | 53 ++++++------- crates/tek/src/tui/phrase_editor.rs | 119 +++++++++++++++------------- crates/tek/src/tui/tui_input.rs | 36 ++++----- 3 files changed, 103 insertions(+), 105 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index ee2a88e4..14354b0d 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -125,19 +125,15 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option match state.focus { - PhraseEditor => SequencerCommand::Focus(FocusCommand::Set(PhraseList)), - _ => SequencerCommand::Focus(FocusCommand::Set(PhraseEditor)), - } - // Enqueue currently edited phrase - key_pat!(Char('q')) => - Enqueue(Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())), + key_pat!(Char('q')) => Enqueue(Some( + state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() + )), // 0: Enqueue phrase 0 (stop all) - key_pat!(Char('0')) => - Enqueue(Some(state.phrases.phrases[0].clone())), + key_pat!(Char('0')) => Enqueue(Some( + state.phrases.phrases[0].clone() + )), // E: Toggle between editing currently playing or other phrase key_pat!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() { @@ -155,29 +151,26 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option - Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), + key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { + Play(None) + } else { + Pause(None) + }), // Transport: Play from start or rewind to start - key_pat!(Shift-Char(' ')) => - Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), + key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { + Play(Some(0)) + } else { + Pause(Some(0)) + }), - // Editor: zoom - key_pat!(Char('z')) | key_pat!(Char('-')) | key_pat!(Char('_'))| key_pat!(Char('=')) | key_pat!(Char('+')) => - Editor(PhraseCommand::input_to_command(&state.editor, input)?), - - // List: select phrase to edit, change color - key_pat!(Char('[')) | key_pat!(Char(']')) | key_pat!(Char('c')) | key_pat!(Shift-Char('A')) | key_pat!(Shift-Char('D')) => - Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?), - - // Delegate to focused control: - _ => match state.focus { - PhraseEditor => Editor(PhraseCommand::input_to_command(&state.editor, input)?), - PhraseList => Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?), - Transport(_) => match to_transport_command(state, input)? { - TransportCommand::Clock(command) => Clock(command), - _ => return None, - }, + // Delegate to components: + _ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { + Editor(command) + } else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) { + Phrases(command) + } else { + return None } }) diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 0f001e24..8d2f3a60 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -1,5 +1,7 @@ use crate::*; use Ordering::Relaxed; +use KeyCode::{Char, Up, Down, Left, Right}; +use PhraseCommand::*; pub trait HasEditor { fn editor (&self) -> &PhraseEditorModel; @@ -18,61 +20,64 @@ pub enum PhraseCommand { SetTimeCursor(usize), SetTimeScroll(usize), SetTimeZoom(usize), - SetTimeZoomLock(bool), + SetTimeLock(bool), Show(Option>>), ToggleDirection, } impl InputToCommand for PhraseCommand { fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option { - use PhraseCommand::*; - use KeyCode::{Char, Up, Down, PageUp, PageDown, Left, Right}; - let point = state.point(); - let note_point = point.note_point.load(Relaxed); - let time_point = point.time_point.load(Relaxed); - let note_len = point.note_len.load(Relaxed); - let range = state.range(); - let note_lo = range.note_lo.load(Relaxed); - let time_start = range.time_start.load(Relaxed); - let time_zoom = range.time_zoom(); - let length = state.phrase().read().unwrap().as_ref() - .map(|p|p.read().unwrap().length).unwrap_or(1); - Some(match from.event() { - key_pat!(Char('`')) => ToggleDirection, - key_pat!(Char('z')) => SetTimeZoomLock(!state.range().time_lock()), - key_pat!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)), - key_pat!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)), - key_pat!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)), - key_pat!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)), - key_pat!(Char('a')) => AppendNote, - key_pat!(Char('s')) => PutNote, - // TODO: no triplet/dotted - key_pat!(Char(',')) => SetNoteLength(prev_note_length(note_len)), - key_pat!(Char('.')) => SetNoteLength(next_note_length(note_len)), - // TODO: with triplet/dotted - key_pat!(Char('<')) => SetNoteLength(prev_note_length(note_len)), - key_pat!(Char('>')) => SetNoteLength(next_note_length(note_len)), - // TODO: '/' set triplet, '?' set dotted - _ => match from.event() { - key_pat!(Up) => SetNoteCursor(note_point + 1), - key_pat!(Down) => SetNoteCursor(note_point.saturating_sub(1)), - key_pat!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)), - key_pat!(Right) => SetTimeCursor((time_point + note_len) % length), - key_pat!(Alt-Up) => SetNoteCursor(note_point + 3), - key_pat!(Alt-Down) => SetNoteCursor(note_point.saturating_sub(3)), - key_pat!(Alt-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)), - key_pat!(Alt-Right) => SetTimeCursor((time_point + time_zoom) % length), - key_pat!(Ctrl-Up) => SetNoteScroll(note_lo + 1), - key_pat!(Ctrl-Down) => SetNoteScroll(note_lo.saturating_sub(1)), - key_pat!(Ctrl-Left) => SetTimeScroll(time_start.saturating_sub(note_len)), - key_pat!(Ctrl-Right) => SetTimeScroll(time_start + note_len), - key_pat!(Ctrl-Alt-Up) => SetNoteScroll(note_point + 3), - key_pat!(Ctrl-Alt-Down) => SetNoteScroll(note_point.saturating_sub(3)), - key_pat!(Ctrl-Alt-Left) => SetTimeScroll(time_point.saturating_sub(time_zoom)), - key_pat!(Ctrl-Alt-Right) => SetTimeScroll((time_point + time_zoom) % length), - _ => return None - }, + let length = ||state + .phrase() + .read() + .unwrap() + .as_ref() + .map(|p|p.read().unwrap().length) + .unwrap_or(1); + + let range = state.range(); + let note_lo = ||range.note_lo.load(Relaxed); + let time_start = ||range.time_start.load(Relaxed); + let time_zoom = ||range.time_zoom(); + + let point = state.point(); + let note_point = ||point.note_point(); + let time_point = ||point.time_point(); + let note_len = ||point.note_len(); + + Some(match from.event() { + key_pat!(Ctrl-Alt-Up) => SetNoteScroll(note_point() + 3), + key_pat!(Ctrl-Alt-Down) => SetNoteScroll(note_point().saturating_sub(3)), + key_pat!(Ctrl-Alt-Left) => SetTimeScroll(time_point().saturating_sub(time_zoom())), + key_pat!(Ctrl-Alt-Right) => SetTimeScroll((time_point() + time_zoom()) % length()), + key_pat!(Ctrl-Up) => SetNoteScroll(note_lo() + 1), + key_pat!(Ctrl-Down) => SetNoteScroll(note_lo().saturating_sub(1)), + key_pat!(Ctrl-Left) => SetTimeScroll(time_start().saturating_sub(note_len())), + key_pat!(Ctrl-Right) => SetTimeScroll(time_start() + note_len()), + key_pat!(Alt-Up) => SetNoteCursor(note_point() + 3), + key_pat!(Alt-Down) => SetNoteCursor(note_point().saturating_sub(3)), + key_pat!(Alt-Left) => SetTimeCursor(time_point().saturating_sub(time_zoom())), + key_pat!(Alt-Right) => SetTimeCursor((time_point() + time_zoom()) % length()), + key_pat!(Up) => SetNoteCursor(note_point() + 1), + key_pat!(Down) => SetNoteCursor(note_point().saturating_sub(1)), + key_pat!(Left) => SetTimeCursor(time_point().saturating_sub(note_len())), + key_pat!(Right) => SetTimeCursor((time_point() + note_len()) % length()), + key_pat!(Char('`')) => ToggleDirection, + key_pat!(Char('z')) => SetTimeLock(!state.range().time_lock()), + key_pat!(Char('-')) => SetTimeZoom(next_note_length(time_zoom())), + key_pat!(Char('_')) => SetTimeZoom(next_note_length(time_zoom())), + key_pat!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom())), + key_pat!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom())), + key_pat!(Char('a')) => AppendNote, + key_pat!(Char('s')) => PutNote, + key_pat!(Char(',')) => SetNoteLength(prev_note_length(note_len())), // TODO: no 3plet + key_pat!(Char('.')) => SetNoteLength(next_note_length(note_len())), + key_pat!(Char('<')) => SetNoteLength(prev_note_length(note_len())), // TODO: 3plet + key_pat!(Char('>')) => SetNoteLength(next_note_length(note_len())), + // TODO: key_pat!(Char('/')) => // toggle 3plet + // TODO: key_pat!(Char('?')) => // toggle dotted + _ => return None }) } } @@ -83,15 +88,15 @@ impl Command for PhraseCommand { let range = state.range(); let point = state.point(); match self { - Show(phrase) => { state.set_phrase(phrase); }, - PutNote => { state.put_note(false); }, - AppendNote => { state.put_note(true); }, - SetTimeZoom(x) => { range.set_time_zoom(x); }, - SetTimeZoomLock(x) => { range.set_time_lock(x); }, - SetTimeScroll(x) => { range.set_time_start(x); }, - SetNoteScroll(x) => { range.set_note_lo(x); }, - SetNoteLength(x) => { point.set_note_len(x); }, - SetTimeCursor(x) => { point.set_time_point(x); }, + Show(phrase) => { state.set_phrase(phrase); }, + PutNote => { state.put_note(false); }, + AppendNote => { state.put_note(true); }, + SetTimeZoom(x) => { range.set_time_zoom(x); }, + SetTimeLock(x) => { range.set_time_lock(x); }, + SetTimeScroll(x) => { range.set_time_start(x); }, + SetNoteScroll(x) => { range.set_note_lo(x); }, + SetNoteLength(x) => { point.set_note_len(x); }, + SetTimeCursor(x) => { point.set_time_point(x); }, SetNoteCursor(note) => { let note = 127.min(note); let start = range.note_lo.load(Relaxed); diff --git a/crates/tek/src/tui/tui_input.rs b/crates/tek/src/tui/tui_input.rs index 96b7a88e..958a8b16 100644 --- a/crates/tek/src/tui/tui_input.rs +++ b/crates/tek/src/tui/tui_input.rs @@ -29,14 +29,6 @@ impl Input for TuiInput { } #[macro_export] macro_rules! key_event_pat { - ($code:pat, $modifiers: pat) => { - TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { - code: $code, - modifiers: $modifiers, - kind: KeyEventKind::Press, - state: KeyEventState::NONE - })) - }; ($code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, @@ -45,10 +37,7 @@ impl Input for TuiInput { state: KeyEventState::NONE })) }; -} - -#[macro_export] macro_rules! key_event_expr { - ($code:expr, $modifiers: expr) => { + ($code:pat, $modifiers: pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, modifiers: $modifiers, @@ -56,7 +45,18 @@ impl Input for TuiInput { state: KeyEventState::NONE })) }; - ($code:expr) => { +} + +#[macro_export] macro_rules! key_event_expr { + ($code:ident, $modifiers: expr) => { + TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { + code: $code, + modifiers: $modifiers, + kind: KeyEventKind::Press, + state: KeyEventState::NONE + })) + }; + ($code:ident) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code, modifiers: KeyModifiers::NONE, @@ -75,11 +75,11 @@ impl Input for TuiInput { } #[macro_export] macro_rules! key_expr { - (Ctrl-Alt-$code:expr) => { key_event_expr!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) }; - (Ctrl-$code:expr) => { key_event_expr!($code, KeyModifiers::CONTROL) }; - (Alt-$code:expr) => { key_event_expr!($code, KeyModifiers::ALT) }; - (Shift-$code:expr) => { key_event_expr!($code, KeyModifiers::SHIFT) }; - ($code:expr) => { key_event_expr!($code) }; + (Ctrl-Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) }; + (Ctrl-$code:ident) => { key_event_expr!($code, KeyModifiers::CONTROL) }; + (Alt-$code:ident) => { key_event_expr!($code, KeyModifiers::ALT) }; + (Shift-$code:ident) => { key_event_expr!($code, KeyModifiers::SHIFT) }; + ($code:ident) => { key_event_expr!($code) }; } /* From d06b95df2c2dcd06a89c629282f7cc48b2af9f33 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 20:48:55 +0100 Subject: [PATCH 030/971] double arc rwlock was silly --- crates/tek/src/tui/app_sequencer.rs | 6 +- crates/tek/src/tui/phrase_editor.rs | 89 +++++++++++++------------- crates/tek/src/tui/piano_horizontal.rs | 25 ++++---- 3 files changed, 59 insertions(+), 61 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 14354b0d..15908d15 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -137,9 +137,7 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option if let Some((_, Some(playing_phrase))) = state.player.play_phrase() { - let editing_phrase = state.editor.phrase() - .read().unwrap().as_ref() - .map(|p|p.read().unwrap().clone()); + let editing_phrase = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone()); let selected_phrase = state.phrases.phrase().clone(); if Some(selected_phrase.read().unwrap().clone()) != editing_phrase { Editor(Show(Some(selected_phrase))) @@ -183,7 +181,7 @@ render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1, PhraseSelector::play_phrase(&self.player), PhraseSelector::next_phrase(&self.player), ]), Tui::split_up(false, 2, - PhraseSelector::edit_phrase(&self.editor.phrase.read().unwrap()), + PhraseSelector::edit_phrase(self.editor.phrase()), PhraseListView::from(self), ))), col!([ diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 8d2f3a60..57263333 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -27,25 +27,15 @@ pub enum PhraseCommand { impl InputToCommand for PhraseCommand { fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option { - - let length = ||state - .phrase() - .read() - .unwrap() - .as_ref() - .map(|p|p.read().unwrap().length) - .unwrap_or(1); - + let length = ||state.phrase().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); let range = state.range(); let note_lo = ||range.note_lo.load(Relaxed); let time_start = ||range.time_start.load(Relaxed); let time_zoom = ||range.time_zoom(); - let point = state.point(); let note_point = ||point.note_point(); let time_point = ||point.time_point(); let note_len = ||point.note_len(); - Some(match from.event() { key_pat!(Ctrl-Alt-Up) => SetNoteScroll(note_point() + 3), key_pat!(Ctrl-Alt-Down) => SetNoteScroll(note_point().saturating_sub(3)), @@ -88,7 +78,7 @@ impl Command for PhraseCommand { let range = state.range(); let point = state.point(); match self { - Show(phrase) => { state.set_phrase(phrase); }, + Show(phrase) => { state.set_phrase(phrase.as_ref()); }, PutNote => { state.put_note(false); }, AppendNote => { state.put_note(true); }, SetTimeZoom(x) => { range.set_time_zoom(x); }, @@ -114,51 +104,51 @@ impl Command for PhraseCommand { /// Contains state for viewing and editing a phrase pub struct PhraseEditorModel { - /// Phrase being played - pub phrase: Arc>>>>, /// Renders the phrase pub mode: Box, } impl Default for PhraseEditorModel { fn default () -> Self { - let phrase = Arc::new(RwLock::new(None)); - let mode = PianoHorizontal::new(&phrase); - Self { phrase, mode: Box::new(mode) } + Self { mode: Box::new(PianoHorizontal::new(None)) } } } render!(|self: PhraseEditorModel|self.mode); pub trait PhraseViewMode: Render + Debug + Send + Sync { - fn range (&self) -> &PhraseEditorRange; - fn point (&self) -> &PhraseEditorPoint; + fn range (&self) -> &PhraseEditorRange; + fn point (&self) -> &PhraseEditorPoint; fn buffer_size (&self, phrase: &Phrase) -> (usize, usize); - fn redraw (&mut self); - fn phrase (&self) -> &Arc>>>>; - fn set_phrase (&mut self, phrase: Option>>) { - *self.phrase().write().unwrap() = phrase; + fn redraw (&mut self); + fn phrase (&self) -> &Option>>; + fn phrase_mut (&mut self) -> &mut Option>>; + fn set_phrase (&mut self, phrase: Option<&Arc>>) { + *self.phrase_mut() = phrase.map(|p|p.clone()); self.redraw(); } } impl PhraseViewMode for PhraseEditorModel { - fn range (&self) -> &PhraseEditorRange { + fn range (&self) -> &PhraseEditorRange { self.mode.range() } - fn point (&self) -> &PhraseEditorPoint { + fn point (&self) -> &PhraseEditorPoint { self.mode.point() } fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { self.mode.buffer_size(phrase) } - fn redraw (&mut self) { + fn redraw (&mut self) { self.mode.redraw() } - fn phrase (&self) -> &Arc>>>> { + fn phrase (&self) -> &Option>> { self.mode.phrase() } - fn set_phrase (&mut self, phrase: Option>>) { + fn phrase_mut (&mut self) -> &mut Option>> { + self.mode.phrase_mut() + } + fn set_phrase (&mut self, phrase: Option<&Arc>>) { self.mode.set_phrase(phrase) } } @@ -264,25 +254,32 @@ impl PhraseEditorPoint { impl PhraseEditorModel { /// Put note at current position pub fn put_note (&mut self, advance: bool) { - if let Some(phrase) = &*self.phrase.read().unwrap() { - let point = self.point().clone(); - let note_len = point.note_len.load(Relaxed); - let time = point.time_point.load(Relaxed); - let note = point.note_point.load(Relaxed); + let mut redraw = false; + if let Some(phrase) = self.phrase() { let mut phrase = phrase.write().unwrap(); - let key: u7 = u7::from(note as u8); + let note_start = self.point().time_point(); + let note_point = self.point().note_point(); + let note_len = self.point().note_len(); + let note_end = note_start + note_len; + let key: u7 = u7::from(note_point as u8); let vel: u7 = 100.into(); - let start = time; - let end = (start + note_len) % phrase.length; - phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); - phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); - self.mode.redraw(); - if advance { - let time = point.time_point.load(Relaxed); - let length = phrase.length; - let forward = |time|(time + note_len) % length; - point.set_time_point(forward(time)); + let length = phrase.length; + let note_end = note_end % length; + let note_on = MidiMessage::NoteOn { key, vel }; + if !phrase.notes[note_start].iter().any(|msg|*msg == note_on) { + phrase.notes[note_start].push(note_on); } + let note_off = MidiMessage::NoteOff { key, vel }; + if !phrase.notes[note_end].iter().any(|msg|*msg == note_off) { + phrase.notes[note_end].push(note_off); + } + if advance { + self.point().set_time_point(note_end); + } + redraw = true; + } + if redraw { + self.mode.redraw(); } } } @@ -295,8 +292,8 @@ impl From<&Arc>> for PhraseEditorModel { impl From>>> for PhraseEditorModel { fn from (phrase: Option>>) -> Self { - let model = Self::default(); - *model.phrase.write().unwrap() = phrase; + let mut model = Self::default(); + *model.phrase_mut() = phrase; model } } diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 6b7a019a..572f1f10 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -3,7 +3,7 @@ use super::*; /// A phrase, rendered as a horizontal piano roll. pub struct PianoHorizontal { - phrase: Arc>>>>, + phrase: Option>>, /// Buffer where the whole phrase is rerendered on change buffer: BigBuffer, /// Width and height of notes area at last render @@ -17,13 +17,13 @@ pub struct PianoHorizontal { } impl PianoHorizontal { - pub fn new (phrase: &Arc>>>>) -> Self { + pub fn new (phrase: Option<&Arc>>) -> Self { let size = Measure::new(); let mut range = PhraseEditorRange::default(); range.time_axis = size.x.clone(); range.note_axis = size.y.clone(); - let phrase = phrase.clone(); - let color = phrase.read().unwrap().as_ref() + let phrase = phrase.map(|p|p.clone()); + let color = phrase.as_ref() .map(|p|p.read().unwrap().color) .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); Self { @@ -242,9 +242,12 @@ impl PianoHorizontal { } impl PhraseViewMode for PianoHorizontal { - fn phrase (&self) -> &Arc>>>> { + fn phrase (&self) -> &Option>> { &self.phrase } + fn phrase_mut (&mut self) -> &mut Option>> { + &mut self.phrase + } fn range (&self) -> &PhraseEditorRange { &self.range } @@ -256,9 +259,10 @@ impl PhraseViewMode for PianoHorizontal { (phrase.length / self.range.time_zoom(), 128) } fn redraw (&mut self) { - let buffer = if let Some(phrase) = &*self.phrase().read().unwrap() { + let buffer = if let Some(phrase) = self.phrase.as_ref() { let phrase = phrase.read().unwrap(); - let mut buffer = BigBuffer::from(self.buffer_size(&phrase)); + let buf_size = self.buffer_size(&phrase); + let mut buffer = BigBuffer::from(buf_size); let note_len = self.point.note_len(); let time_zoom = self.range.time_zoom(); PianoHorizontal::draw_bg(&mut buffer, &phrase, time_zoom, note_len); @@ -269,10 +273,9 @@ impl PhraseViewMode for PianoHorizontal { }; self.buffer = buffer } - fn set_phrase (&mut self, phrase: Option>>) { - *self.phrase().write().unwrap() = phrase; - self.color = self.phrase.read().unwrap().as_ref() - .map(|p|p.read().unwrap().color) + fn set_phrase (&mut self, phrase: Option<&Arc>>) { + *self.phrase_mut() = phrase.map(|p|p.clone()); + self.color = phrase.map(|p|p.read().unwrap().color.clone()) .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); self.redraw(); } From 8f0decbe4d99ca79bbf8041f2a781f497f29e470 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 21:17:49 +0100 Subject: [PATCH 031/971] traits MIDIRange and MIDIPoint --- crates/tek/src/api.rs | 1 + crates/tek/src/api/note.rs | 94 ++++++++++ crates/tek/src/core.rs | 1 + crates/tek/src/core/perf.rs | 54 ++++++ crates/tek/src/core/time.rs | 53 ------ crates/tek/src/tui/phrase_editor.rs | 234 +++++++------------------ crates/tek/src/tui/piano_horizontal.rs | 34 ++-- 7 files changed, 236 insertions(+), 235 deletions(-) create mode 100644 crates/tek/src/api/note.rs create mode 100644 crates/tek/src/core/perf.rs diff --git a/crates/tek/src/api.rs b/crates/tek/src/api.rs index 013b4388..e7219c7d 100644 --- a/crates/tek/src/api.rs +++ b/crates/tek/src/api.rs @@ -2,6 +2,7 @@ mod phrase; pub(crate) use phrase::*; mod jack; pub(crate) use self::jack::*; mod clip; pub(crate) use clip::*; mod clock; pub(crate) use clock::*; +mod note; pub(crate) use note::*; mod player; pub(crate) use player::*; mod scene; pub(crate) use scene::*; mod track; pub(crate) use track::*; diff --git a/crates/tek/src/api/note.rs b/crates/tek/src/api/note.rs new file mode 100644 index 00000000..4cf55cbd --- /dev/null +++ b/crates/tek/src/api/note.rs @@ -0,0 +1,94 @@ +use crate::*; +use Ordering::Relaxed; + +#[derive(Debug, Clone)] +pub struct MIDIRangeModel { + /// Length of visible time axis + pub time_axis: Arc, + /// Earliest time displayed + pub time_start: Arc, + /// Time step + pub time_zoom: Arc, + /// Auto rezoom to fit in time axis + pub time_lock: Arc, + /// Length of visible note axis + pub note_axis: Arc, + // Lowest note displayed + pub note_lo: Arc, +} + +impl From<(usize, bool)> for MIDIRangeModel { + fn from ((time_zoom, time_lock): (usize, bool)) -> Self { + Self { + note_axis: Arc::new(0.into()), + note_lo: Arc::new(0.into()), + time_axis: Arc::new(0.into()), + time_start: Arc::new(0.into()), + time_zoom: Arc::new(time_zoom.into()), + time_lock: Arc::new(time_lock.into()), + } + } +} + +#[derive(Debug, Clone)] +pub struct MIDIPointModel { + /// Time coordinate of cursor + pub time_point: Arc, + /// Note coordinate of cursor + pub note_point: Arc, + /// Length of note that will be inserted, in pulses + pub note_len: Arc, +} + +impl Default for MIDIPointModel { + fn default () -> Self { + Self { + time_point: Arc::new(0.into()), + note_point: Arc::new(0.into()), + note_len: Arc::new(24.into()), + } + } +} + +pub trait MIDIRange { + fn time_zoom (&self) -> usize; + fn set_time_zoom (&self, x: usize); + fn time_lock (&self) -> bool; + fn set_time_lock (&self, x: bool); + fn time_start (&self) -> usize; + fn set_time_start (&self, x: usize); + fn note_lo (&self) -> usize; + fn set_note_lo (&self, x: usize); + fn note_axis (&self) -> usize; + fn note_hi (&self) -> usize; +} + +pub trait MIDIPoint { + fn note_len (&self) -> usize; + fn set_note_len (&self, x: usize); + fn note_point (&self) -> usize; + fn set_note_point (&self, x: usize); + fn time_point (&self) -> usize; + fn set_time_point (&self, x: usize); +} + +impl MIDIRange for MIDIRangeModel { + fn time_zoom (&self) -> usize { self.time_zoom.load(Relaxed) } + fn set_time_zoom (&self, x: usize) { self.time_zoom.store(x, Relaxed); } + fn time_lock (&self) -> bool { self.time_lock.load(Relaxed) } + fn set_time_lock (&self, x: bool) { self.time_lock.store(x, Relaxed); } + fn time_start (&self) -> usize { self.time_start.load(Relaxed) } + fn set_time_start (&self, x: usize) { self.time_start.store(x, Relaxed); } + fn set_note_lo (&self, x: usize) { self.note_lo.store(x, Relaxed); } + fn note_lo (&self) -> usize { self.note_lo.load(Relaxed) } + fn note_axis (&self) -> usize { self.note_lo.load(Relaxed) } + fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } +} +impl MIDIPoint for MIDIPointModel { + fn note_len (&self) -> usize { self.note_len.load(Relaxed)} + fn set_note_len (&self, x: usize) { self.note_len.store(x, Relaxed) } + fn note_point (&self) -> usize { self.note_point.load(Relaxed) } + fn set_note_point (&self, x: usize) { self.note_point.store(x, Relaxed) } + fn time_point (&self) -> usize { self.time_point.load(Relaxed) } + fn set_time_point (&self, x: usize) { self.time_point.store(x, Relaxed) } +} diff --git a/crates/tek/src/core.rs b/crates/tek/src/core.rs index c22c504e..029f46ac 100644 --- a/crates/tek/src/core.rs +++ b/crates/tek/src/core.rs @@ -6,6 +6,7 @@ mod engine; pub(crate) use engine::*; mod focus; pub(crate) use focus::*; mod input; pub(crate) use input::*; mod output; pub(crate) use output::*; +mod perf; pub(crate) use perf::*; mod pitch; pub(crate) use pitch::*; mod space; pub(crate) use space::*; mod time; pub(crate) use time::*; diff --git a/crates/tek/src/core/perf.rs b/crates/tek/src/core/perf.rs new file mode 100644 index 00000000..05403b11 --- /dev/null +++ b/crates/tek/src/core/perf.rs @@ -0,0 +1,54 @@ +use crate::*; + +/// Performance counter +pub struct PerfModel { + pub enabled: bool, + clock: quanta::Clock, + // In nanoseconds + used: AtomicF64, + // In microseconds + period: AtomicF64, +} + +impl Default for PerfModel { + fn default () -> Self { + Self { + enabled: true, + clock: quanta::Clock::new(), + used: Default::default(), + period: Default::default(), + } + } +} + +impl PerfModel { + pub fn get_t0 (&self) -> Option { + if self.enabled { + Some(self.clock.raw()) + } else { + None + } + } + pub fn update (&self, t0: Option, scope: &jack::ProcessScope) { + if let Some(t0) = t0 { + let t1 = self.clock.raw(); + self.used.store( + self.clock.delta_as_nanos(t0, t1) as f64, + Ordering::Relaxed, + ); + self.period.store( + scope.cycle_times().unwrap().period_usecs as f64, + Ordering::Relaxed, + ); + } + } + pub fn percentage (&self) -> Option { + let period = self.period.load(Ordering::Relaxed) * 1000.0; + if period > 0.0 { + let used = self.used.load(Ordering::Relaxed); + Some(100.0 * used / period) + } else { + None + } + } +} diff --git a/crates/tek/src/core/time.rs b/crates/tek/src/core/time.rs index 536e403b..6677431f 100644 --- a/crates/tek/src/core/time.rs +++ b/crates/tek/src/core/time.rs @@ -386,59 +386,6 @@ pub fn pulses_to_name (pulses: usize) -> &'static str { "" } -/// Performance counter -pub struct PerfModel { - pub enabled: bool, - clock: quanta::Clock, - // In nanoseconds - used: AtomicF64, - // In microseconds - period: AtomicF64, -} - -impl Default for PerfModel { - fn default () -> Self { - Self { - enabled: true, - clock: quanta::Clock::new(), - used: Default::default(), - period: Default::default(), - } - } -} - -impl PerfModel { - pub fn get_t0 (&self) -> Option { - if self.enabled { - Some(self.clock.raw()) - } else { - None - } - } - pub fn update (&self, t0: Option, scope: &jack::ProcessScope) { - if let Some(t0) = t0 { - let t1 = self.clock.raw(); - self.used.store( - self.clock.delta_as_nanos(t0, t1) as f64, - Ordering::Relaxed, - ); - self.period.store( - scope.cycle_times().unwrap().period_usecs as f64, - Ordering::Relaxed, - ); - } - } - pub fn percentage (&self) -> Option { - let period = self.period.load(Ordering::Relaxed) * 1000.0; - if period > 0.0 { - let used = self.used.load(Ordering::Relaxed); - Some(100.0 * used / period) - } else { - None - } - } -} - //#[cfg(test)] //mod test { //use super::*; diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 57263333..44c0e703 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -28,14 +28,13 @@ pub enum PhraseCommand { impl InputToCommand for PhraseCommand { fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option { let length = ||state.phrase().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - let range = state.range(); - let note_lo = ||range.note_lo.load(Relaxed); - let time_start = ||range.time_start.load(Relaxed); - let time_zoom = ||range.time_zoom(); - let point = state.point(); - let note_point = ||point.note_point(); - let time_point = ||point.time_point(); - let note_len = ||point.note_len(); + let note_lo = ||state.note_lo(); + let time_start = ||state.time_start(); + let time_zoom = ||state.time_zoom(); + let time_lock = ||state.time_lock(); + let note_point = ||state.note_point(); + let time_point = ||state.time_point(); + let note_len = ||state.note_len(); Some(match from.event() { key_pat!(Ctrl-Alt-Up) => SetNoteScroll(note_point() + 3), key_pat!(Ctrl-Alt-Down) => SetNoteScroll(note_point().saturating_sub(3)), @@ -54,7 +53,7 @@ impl InputToCommand for PhraseCommand { key_pat!(Left) => SetTimeCursor(time_point().saturating_sub(note_len())), key_pat!(Right) => SetTimeCursor((time_point() + note_len()) % length()), key_pat!(Char('`')) => ToggleDirection, - key_pat!(Char('z')) => SetTimeLock(!state.range().time_lock()), + key_pat!(Char('z')) => SetTimeLock(!time_lock()), key_pat!(Char('-')) => SetTimeZoom(next_note_length(time_zoom())), key_pat!(Char('_')) => SetTimeZoom(next_note_length(time_zoom())), key_pat!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom())), @@ -75,27 +74,24 @@ impl InputToCommand for PhraseCommand { impl Command for PhraseCommand { fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { use PhraseCommand::*; - let range = state.range(); - let point = state.point(); match self { Show(phrase) => { state.set_phrase(phrase.as_ref()); }, PutNote => { state.put_note(false); }, AppendNote => { state.put_note(true); }, - SetTimeZoom(x) => { range.set_time_zoom(x); }, - SetTimeLock(x) => { range.set_time_lock(x); }, - SetTimeScroll(x) => { range.set_time_start(x); }, - SetNoteScroll(x) => { range.set_note_lo(x); }, - SetNoteLength(x) => { point.set_note_len(x); }, - SetTimeCursor(x) => { point.set_time_point(x); }, + SetTimeZoom(x) => { state.set_time_zoom(x); }, + SetTimeLock(x) => { state.set_time_lock(x); }, + SetTimeScroll(x) => { state.set_time_start(x); }, + SetNoteScroll(x) => { state.set_note_lo(x); }, + SetNoteLength(x) => { state.set_note_len(x); }, + SetTimeCursor(x) => { state.set_time_point(x); }, SetNoteCursor(note) => { let note = 127.min(note); - let start = range.note_lo.load(Relaxed); - point.note_point.store(note, Relaxed); + let start = state.note_lo(); + state.set_note_point(note); if note < start { - range.note_lo.store(note, Relaxed); + state.set_note_lo(note) } }, - _ => todo!("{:?}", self) } Ok(None) @@ -116,9 +112,7 @@ impl Default for PhraseEditorModel { render!(|self: PhraseEditorModel|self.mode); -pub trait PhraseViewMode: Render + Debug + Send + Sync { - fn range (&self) -> &PhraseEditorRange; - fn point (&self) -> &PhraseEditorPoint; +pub trait PhraseViewMode: Render + MIDIRange + MIDIPoint + Debug + Send + Sync { fn buffer_size (&self, phrase: &Phrase) -> (usize, usize); fn redraw (&mut self); fn phrase (&self) -> &Option>>; @@ -129,13 +123,27 @@ pub trait PhraseViewMode: Render + Debug + Send + Sync { } } +impl MIDIRange for PhraseEditorModel { + fn time_zoom (&self) -> usize { self.mode.time_zoom() } + fn set_time_zoom (&self, x: usize) { self.mode.set_time_zoom(x); } + fn time_lock (&self) -> bool { self.mode.time_lock() } + fn set_time_lock (&self, x: bool) { self.mode.set_time_lock(x); } + fn time_start (&self) -> usize { self.mode.time_start() } + fn set_time_start (&self, x: usize) { self.mode.set_time_start(x); } + fn set_note_lo (&self, x: usize) { self.mode.set_note_lo(x); } + fn note_lo (&self) -> usize { self.mode.note_lo() } + fn note_axis (&self) -> usize { self.mode.note_lo() } + fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } +} +impl MIDIPoint for PhraseEditorModel { + fn note_len (&self) -> usize { self.mode.note_len()} + fn set_note_len (&self, x: usize) { self.mode.set_note_len(x) } + fn note_point (&self) -> usize { self.mode.note_point() } + fn set_note_point (&self, x: usize) { self.mode.set_note_point(x) } + fn time_point (&self) -> usize { self.mode.time_point() } + fn set_time_point (&self, x: usize) { self.mode.set_time_point(x) } +} impl PhraseViewMode for PhraseEditorModel { - fn range (&self) -> &PhraseEditorRange { - self.mode.range() - } - fn point (&self) -> &PhraseEditorPoint { - self.mode.point() - } fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { self.mode.buffer_size(phrase) } @@ -153,113 +161,15 @@ impl PhraseViewMode for PhraseEditorModel { } } -#[derive(Debug, Clone)] -pub struct PhraseEditorRange { - /// Length of visible time axis - pub time_axis: Arc, - /// Earliest time displayed - pub time_start: Arc, - /// Time step - pub time_zoom: Arc, - /// Auto rezoom to fit in time axis - pub time_lock: Arc, - /// Length of visible note axis - pub note_axis: Arc, - // Lowest note displayed - pub note_lo: Arc, -} - -impl Default for PhraseEditorRange { - fn default () -> Self { - Self { - time_axis: Arc::new(0.into()), - time_start: Arc::new(0.into()), - time_zoom: Arc::new(24.into()), - time_lock: Arc::new(true.into()), - note_axis: Arc::new(0.into()), - note_lo: Arc::new(0.into()), - } - } -} -impl PhraseEditorRange { - pub fn time_zoom (&self) -> usize { - self.time_zoom.load(Relaxed) - } - pub fn set_time_zoom (&self, x: usize) { - self.time_zoom.store(x, Relaxed); - } - pub fn time_lock (&self) -> bool { - self.time_lock.load(Relaxed) - } - pub fn set_time_lock (&self, x: bool) { - self.time_lock.store(x, Relaxed); - } - pub fn time_start (&self) -> usize { - self.time_start.load(Relaxed) - } - pub fn set_time_start (&self, x: usize) { - self.time_start.store(x, Relaxed); - } - pub fn set_note_lo (&self, x: usize) { - self.note_lo.store(x, Relaxed); - } - pub fn note_lo (&self) -> usize { - self.note_lo.load(Relaxed) - } - pub fn note_axis (&self) -> usize { - self.note_lo.load(Relaxed) - } - pub fn note_hi (&self) -> usize { - self.note_lo() + self.note_axis() - } -} - -#[derive(Debug, Clone)] -pub struct PhraseEditorPoint { - /// Time coordinate of cursor - pub time_point: Arc, - /// Note coordinate of cursor - pub note_point: Arc, - /// Length of note that will be inserted, in pulses - pub note_len: Arc, -} - -impl Default for PhraseEditorPoint { - fn default () -> Self { - Self { - time_point: Arc::new(0.into()), - note_point: Arc::new(0.into()), - note_len: Arc::new(24.into()), - } - } -} -impl PhraseEditorPoint { - pub fn note_len (&self) -> usize { - self.note_len.load(Relaxed) - } - pub fn set_note_len (&self, x: usize) { - self.note_len.store(x, Relaxed) - } - pub fn note_point (&self) -> usize { - self.note_point.load(Relaxed) - } - pub fn time_point (&self) -> usize { - self.time_point.load(Relaxed) - } - pub fn set_time_point (&self, x: usize) { - self.time_point.store(x, Relaxed) - } -} - impl PhraseEditorModel { /// Put note at current position pub fn put_note (&mut self, advance: bool) { let mut redraw = false; if let Some(phrase) = self.phrase() { let mut phrase = phrase.write().unwrap(); - let note_start = self.point().time_point(); - let note_point = self.point().note_point(); - let note_len = self.point().note_len(); + let note_start = self.time_point(); + let note_point = self.note_point(); + let note_len = self.note_len(); let note_end = note_start + note_len; let key: u7 = u7::from(note_point as u8); let vel: u7 = 100.into(); @@ -274,7 +184,7 @@ impl PhraseEditorModel { phrase.notes[note_end].push(note_off); } if advance { - self.point().set_time_point(note_end); + self.set_time_point(note_end); } redraw = true; } @@ -282,6 +192,25 @@ impl PhraseEditorModel { self.mode.redraw(); } } + /// Make sure cursor is within range + fn autoscroll ( + range: &impl MIDIRange, + point: &impl MIDIPoint, + height: usize + ) -> (usize, (usize, usize)) { + let note_point = point.note_point(); + let mut note_lo = range.note_lo(); + let mut note_hi = 127.min((note_lo + height).saturating_sub(2)); + if note_point > note_hi { + note_lo += note_point - note_hi; + note_hi = note_point; + range.set_note_lo(note_lo); + } + (note_point, (note_lo, note_hi)) + } + /// Make sure best usage of screen space is achieved by default + fn autozoom (&self) { + } } impl From<&Arc>> for PhraseEditorModel { @@ -301,46 +230,7 @@ impl From>>> for PhraseEditorModel { impl std::fmt::Debug for PhraseEditorModel { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("PhraseEditorModel") - .field("range", &self.range()) - .field("point", &self.point()) + .field("point", &self) .finish() } } - -fn autoscroll_notes ( - range: &PhraseEditorRange, point: &PhraseEditorPoint, height: usize -) -> (usize, (usize, usize)) { - let note_point = point.note_point.load(Relaxed); - let mut note_lo = range.note_lo.load(Relaxed); - let mut note_hi = 127.min((note_lo + height).saturating_sub(2)); - if note_point > note_hi { - note_lo += note_point - note_hi; - note_hi = note_point; - range.note_lo.store(note_lo, Relaxed); - } - (note_point, (note_lo, note_hi)) -} - -//impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { - //fn from (state: &'a T) -> Self { - //let editor = state.editor(); - //let (note_point, note_range) = autoscroll_notes( - //&editor.range, - //&editor.point, - //editor.size.h() - //); - //Self { - //note_point, - //note_range, - //time_start: editor.range.time_start.load(Relaxed), - //time_point: editor.point.time_point.load(Relaxed), - //note_len: editor.point.note_len.load(Relaxed), - //phrase: editor.phrase.clone(), - //mode: &editor.mode, - //size: &editor.size, - //now: &editor.now, - //focused: state.editor_focused(), - //entered: state.editor_entered(), - //} - //} -//} diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 572f1f10..ca559a2e 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -9,9 +9,9 @@ pub struct PianoHorizontal { /// Width and height of notes area at last render size: Measure, /// The display window - range: PhraseEditorRange, + range: MIDIRangeModel, /// The note cursor - point: PhraseEditorPoint, + point: MIDIPointModel, /// The highlight color palette color: ItemPalette, } @@ -19,7 +19,7 @@ pub struct PianoHorizontal { impl PianoHorizontal { pub fn new (phrase: Option<&Arc>>) -> Self { let size = Measure::new(); - let mut range = PhraseEditorRange::default(); + let mut range = MIDIRangeModel::from((24, true)); range.time_axis = size.x.clone(); range.note_axis = size.y.clone(); let phrase = phrase.map(|p|p.clone()); @@ -28,7 +28,7 @@ impl PianoHorizontal { .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); Self { buffer: Default::default(), - point: PhraseEditorPoint::default(), + point: MIDIPointModel::default(), size, range, phrase, @@ -241,6 +241,26 @@ impl PianoHorizontal { } } +impl MIDIRange for PianoHorizontal { + fn time_zoom (&self) -> usize { self.range.time_zoom() } + fn set_time_zoom (&self, x: usize) { self.range.set_time_zoom(x); } + fn time_lock (&self) -> bool { self.range.time_lock() } + fn set_time_lock (&self, x: bool) { self.range.set_time_lock(x); } + fn time_start (&self) -> usize { self.range.time_start() } + fn set_time_start (&self, x: usize) { self.range.set_time_start(x); } + fn set_note_lo (&self, x: usize) { self.range.set_note_lo(x); } + fn note_lo (&self) -> usize { self.range.note_lo() } + fn note_axis (&self) -> usize { self.range.note_lo() } + fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } +} +impl MIDIPoint for PianoHorizontal { + fn note_len (&self) -> usize { self.point.note_len()} + fn set_note_len (&self, x: usize) { self.point.set_note_len(x) } + fn note_point (&self) -> usize { self.point.note_point() } + fn set_note_point (&self, x: usize) { self.point.set_note_point(x) } + fn time_point (&self) -> usize { self.point.time_point() } + fn set_time_point (&self, x: usize) { self.point.set_time_point(x) } +} impl PhraseViewMode for PianoHorizontal { fn phrase (&self) -> &Option>> { &self.phrase @@ -248,12 +268,6 @@ impl PhraseViewMode for PianoHorizontal { fn phrase_mut (&mut self) -> &mut Option>> { &mut self.phrase } - fn range (&self) -> &PhraseEditorRange { - &self.range - } - fn point (&self) -> &PhraseEditorPoint { - &self.point - } /// Determine the required space to render the phrase. fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { (phrase.length / self.range.time_zoom(), 128) From aa8a1a3bd9faab08b956d1c6f97888420fd0a267 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 22:49:09 +0100 Subject: [PATCH 032/971] wip: fix autoscroll --- crates/tek/src/api/clock.rs | 8 +++++ crates/tek/src/api/note.rs | 43 ++++++++++++++++++++++--- crates/tek/src/api/player.rs | 6 +--- crates/tek/src/layout/measure.rs | 18 +++++++++++ crates/tek/src/tui/app_arranger.rs | 13 ++------ crates/tek/src/tui/app_sequencer.rs | 50 +++++++++++++++-------------- crates/tek/src/tui/app_transport.rs | 6 +--- crates/tek/src/tui/phrase_editor.rs | 36 ++++++--------------- 8 files changed, 106 insertions(+), 74 deletions(-) diff --git a/crates/tek/src/api/clock.rs b/crates/tek/src/api/clock.rs index 6276b6b9..8636852a 100644 --- a/crates/tek/src/api/clock.rs +++ b/crates/tek/src/api/clock.rs @@ -4,6 +4,14 @@ pub trait HasClock: Send + Sync { fn clock (&self) -> &ClockModel; } +#[macro_export] macro_rules! has_clock { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasClock for $Struct $(<$($L),*$($T),*>)? { + fn clock (&$self) -> &ClockModel { $cb } + } + } +} + #[derive(Clone, Debug, PartialEq)] pub enum ClockCommand { Play(Option), diff --git a/crates/tek/src/api/note.rs b/crates/tek/src/api/note.rs index 4cf55cbd..28be9ae7 100644 --- a/crates/tek/src/api/note.rs +++ b/crates/tek/src/api/note.rs @@ -53,36 +53,71 @@ impl Default for MIDIPointModel { pub trait MIDIRange { fn time_zoom (&self) -> usize; fn set_time_zoom (&self, x: usize); + fn time_lock (&self) -> bool; fn set_time_lock (&self, x: bool); + fn time_start (&self) -> usize; fn set_time_start (&self, x: usize); + fn note_lo (&self) -> usize; fn set_note_lo (&self, x: usize); + fn note_axis (&self) -> usize; - fn note_hi (&self) -> usize; + + fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } } pub trait MIDIPoint { fn note_len (&self) -> usize; fn set_note_len (&self, x: usize); + fn note_point (&self) -> usize; fn set_note_point (&self, x: usize); + fn time_point (&self) -> usize; fn set_time_point (&self, x: usize); + + fn note_end (&self) -> usize { self.note_point() + self.note_len() } +} + +pub trait MIDIViewport: MIDIRange + MIDIPoint + HasSize { + /// Make sure cursor is within range + fn autoscroll (&self) { + let note = self.note_point(); + let height = self.size().h(); + if note < self.note_lo() { + self.set_note_lo(note) + } + let note_point = self.note_point(); + let mut note_lo = self.note_lo(); + let mut note_hi = 127.min((note_lo + height).saturating_sub(2)); + if note_point > note_hi { + note_lo += note_point - note_hi; + note_hi = note_point; + self.set_note_lo(note_lo); + } + //(note_point, (note_lo, note_hi)) + } + /// Make sure best usage of screen space is achieved by default + fn autozoom (&self) { + } } impl MIDIRange for MIDIRangeModel { fn time_zoom (&self) -> usize { self.time_zoom.load(Relaxed) } fn set_time_zoom (&self, x: usize) { self.time_zoom.store(x, Relaxed); } + fn time_lock (&self) -> bool { self.time_lock.load(Relaxed) } fn set_time_lock (&self, x: bool) { self.time_lock.store(x, Relaxed); } + fn time_start (&self) -> usize { self.time_start.load(Relaxed) } fn set_time_start (&self, x: usize) { self.time_start.store(x, Relaxed); } - fn set_note_lo (&self, x: usize) { self.note_lo.store(x, Relaxed); } + fn note_lo (&self) -> usize { self.note_lo.load(Relaxed) } - fn note_axis (&self) -> usize { self.note_lo.load(Relaxed) } - fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } + fn set_note_lo (&self, x: usize) { self.note_lo.store(x, Relaxed); } + + fn note_axis (&self) -> usize { self.note_axis.load(Relaxed) } } impl MIDIPoint for MIDIPointModel { fn note_len (&self) -> usize { self.note_len.load(Relaxed)} diff --git a/crates/tek/src/api/player.rs b/crates/tek/src/api/player.rs index 4f338961..6e72dc98 100644 --- a/crates/tek/src/api/player.rs +++ b/crates/tek/src/api/player.rs @@ -70,11 +70,7 @@ impl From<(&ClockModel, &Arc>)> for PhrasePlayerModel { } } -impl HasClock for PhrasePlayerModel { - fn clock (&self) -> &ClockModel { - &self.clock - } -} +has_clock!(|self:PhrasePlayerModel|&self.clock); impl HasMidiIns for PhrasePlayerModel { fn midi_ins (&self) -> &Vec> { diff --git a/crates/tek/src/layout/measure.rs b/crates/tek/src/layout/measure.rs index 12cd39a3..af163125 100644 --- a/crates/tek/src/layout/measure.rs +++ b/crates/tek/src/layout/measure.rs @@ -1,5 +1,23 @@ use crate::*; +pub trait HasSize { + fn size (&self) -> &Measure; +} + +#[macro_export] macro_rules! implementor { + ($name:ident => $Trait:ident) => { + #[macro_export] macro_rules! $name { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? $Trait for $Struct $(<$($L),*$($T),*>)? { + fn clock (&$self) -> &ClockModel { $cb } + } + } + } + } +} + +implementor!(has_size => HasSize); + impl LayoutDebug for E {} pub trait LayoutDebug { diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 0c1080fb..13697b3f 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -412,16 +412,9 @@ render!(|self: ArrangerTui|{ ]) }); -impl HasClock for ArrangerTui { - fn clock (&self) -> &ClockModel { - &self.clock - } -} -impl HasClock for ArrangerTrack { - fn clock (&self) -> &ClockModel { - &self.player.clock() - } -} +has_clock!(|self:ArrangerTui|&self.clock); +has_clock!(|self:ArrangerTrack|self.player.clock()); + impl HasPhrases for ArrangerTui { fn phrases (&self) -> &Vec>> { &self.phrases.phrases diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 15908d15..b370767e 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -176,26 +176,32 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option 60 { 20 } else if self.size.w() > 40 { 15 } else { 10 }, - Tui::fixed_x(20, Tui::split_down(false, 4, col!([ - PhraseSelector::play_phrase(&self.player), - PhraseSelector::next_phrase(&self.player), - ]), Tui::split_up(false, 2, - PhraseSelector::edit_phrase(self.editor.phrase()), - PhraseListView::from(self), + Tui::split_right(false, if self.size.w() > 60 { + 20 + } else if self.size.w() > 40 { + 15 + } else { + 10 + }, + Tui::fixed_x(20, Tui::split_down(false, 4, col!([ + PhraseSelector::play_phrase(&self.player), + PhraseSelector::next_phrase(&self.player), + ]), Tui::split_up(false, 2, + PhraseSelector::edit_phrase(self.editor.phrase()), + PhraseListView::from(self), + ))), + col!([ + Tui::fixed_y(2, TransportView::from(( + self, + self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone(), + if let SequencerFocus::Transport(_) = self.focus { + true + } else { + false + } ))), - col!([ - Tui::fixed_y(2, TransportView::from(( - self, - self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone(), - if let SequencerFocus::Transport(_) = self.focus { - true - } else { - false - } - ))), - Tui::fill_xy(&self.editor) - ]), + Tui::fill_xy(&self.editor) + ]), ) )])); @@ -275,11 +281,7 @@ impl TransportControl for SequencerTui { } } -impl HasClock for SequencerTui { - fn clock (&self) -> &ClockModel { - &self.clock - } -} +has_clock!(|self:SequencerTui|&self.clock); impl HasPhrases for SequencerTui { fn phrases (&self) -> &Vec>> { diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index b67d0e31..587b1914 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -37,11 +37,7 @@ impl std::fmt::Debug for TransportTui { } } -impl HasClock for TransportTui { - fn clock (&self) -> &ClockModel { - &self.clock - } -} +has_clock!(|self:TransportTui|&self.clock); impl Audio for TransportTui { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 44c0e703..c2793400 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -83,14 +83,13 @@ impl Command for PhraseCommand { SetTimeScroll(x) => { state.set_time_start(x); }, SetNoteScroll(x) => { state.set_note_lo(x); }, SetNoteLength(x) => { state.set_note_len(x); }, - SetTimeCursor(x) => { state.set_time_point(x); }, + SetTimeCursor(x) => { + state.set_time_point(x); + state.autoscroll(); + }, SetNoteCursor(note) => { - let note = 127.min(note); - let start = state.note_lo(); - state.set_note_point(note); - if note < start { - state.set_note_lo(note) - } + state.set_note_point(note.min(127)); + state.autoscroll(); }, _ => todo!("{:?}", self) } @@ -123,6 +122,10 @@ pub trait PhraseViewMode: Render + MIDIRange + MIDIPoint + Debug + Send + S } } +impl MIDIViewport for PhraseEditorModel {} + +has_size!(|self:PhraseEditorModel|&self.size); + impl MIDIRange for PhraseEditorModel { fn time_zoom (&self) -> usize { self.mode.time_zoom() } fn set_time_zoom (&self, x: usize) { self.mode.set_time_zoom(x); } @@ -192,25 +195,6 @@ impl PhraseEditorModel { self.mode.redraw(); } } - /// Make sure cursor is within range - fn autoscroll ( - range: &impl MIDIRange, - point: &impl MIDIPoint, - height: usize - ) -> (usize, (usize, usize)) { - let note_point = point.note_point(); - let mut note_lo = range.note_lo(); - let mut note_hi = 127.min((note_lo + height).saturating_sub(2)); - if note_point > note_hi { - note_lo += note_point - note_hi; - note_hi = note_point; - range.set_note_lo(note_lo); - } - (note_point, (note_lo, note_hi)) - } - /// Make sure best usage of screen space is achieved by default - fn autozoom (&self) { - } } impl From<&Arc>> for PhraseEditorModel { From c27a4a52324df9a15479eae1d644bc86cee641f2 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 22:58:03 +0100 Subject: [PATCH 033/971] implement has_size --- crates/tek/src/layout/measure.rs | 14 ++++---------- crates/tek/src/tui/phrase_editor.rs | 22 +++++++++++----------- crates/tek/src/tui/piano_horizontal.rs | 2 ++ 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/crates/tek/src/layout/measure.rs b/crates/tek/src/layout/measure.rs index af163125..27429cb6 100644 --- a/crates/tek/src/layout/measure.rs +++ b/crates/tek/src/layout/measure.rs @@ -4,20 +4,14 @@ pub trait HasSize { fn size (&self) -> &Measure; } -#[macro_export] macro_rules! implementor { - ($name:ident => $Trait:ident) => { - #[macro_export] macro_rules! $name { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? $Trait for $Struct $(<$($L),*$($T),*>)? { - fn clock (&$self) -> &ClockModel { $cb } - } - } +#[macro_export] macro_rules! has_size { + (<$E:ty>|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasSize<$E> for $Struct $(<$($L),*$($T),*>)? { + fn size (&$self) -> &Measure<$E> { $cb } // TODO provide trait body } } } -implementor!(has_size => HasSize); - impl LayoutDebug for E {} pub trait LayoutDebug { diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index c2793400..fc579483 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -100,7 +100,7 @@ impl Command for PhraseCommand { /// Contains state for viewing and editing a phrase pub struct PhraseEditorModel { /// Renders the phrase - pub mode: Box, + pub mode: Box, } impl Default for PhraseEditorModel { @@ -111,12 +111,12 @@ impl Default for PhraseEditorModel { render!(|self: PhraseEditorModel|self.mode); -pub trait PhraseViewMode: Render + MIDIRange + MIDIPoint + Debug + Send + Sync { +pub trait PhraseViewMode: Render + HasSize + MIDIRange + MIDIPoint + Debug + Send + Sync { fn buffer_size (&self, phrase: &Phrase) -> (usize, usize); - fn redraw (&mut self); - fn phrase (&self) -> &Option>>; - fn phrase_mut (&mut self) -> &mut Option>>; - fn set_phrase (&mut self, phrase: Option<&Arc>>) { + fn redraw (&mut self); + fn phrase (&self) -> &Option>>; + fn phrase_mut (&mut self) -> &mut Option>>; + fn set_phrase (&mut self, phrase: Option<&Arc>>) { *self.phrase_mut() = phrase.map(|p|p.clone()); self.redraw(); } @@ -124,7 +124,7 @@ pub trait PhraseViewMode: Render + MIDIRange + MIDIPoint + Debug + Send + S impl MIDIViewport for PhraseEditorModel {} -has_size!(|self:PhraseEditorModel|&self.size); +has_size!(|self:PhraseEditorModel|self.mode.size()); impl MIDIRange for PhraseEditorModel { fn time_zoom (&self) -> usize { self.mode.time_zoom() } @@ -150,16 +150,16 @@ impl PhraseViewMode for PhraseEditorModel { fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { self.mode.buffer_size(phrase) } - fn redraw (&mut self) { + fn redraw (&mut self) { self.mode.redraw() } - fn phrase (&self) -> &Option>> { + fn phrase (&self) -> &Option>> { self.mode.phrase() } - fn phrase_mut (&mut self) -> &mut Option>> { + fn phrase_mut (&mut self) -> &mut Option>> { self.mode.phrase_mut() } - fn set_phrase (&mut self, phrase: Option<&Arc>>) { + fn set_phrase (&mut self, phrase: Option<&Arc>>) { self.mode.set_phrase(phrase) } } diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index ca559a2e..5cabba28 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -241,6 +241,8 @@ impl PianoHorizontal { } } +has_size!(|self:PianoHorizontal|&self.size); + impl MIDIRange for PianoHorizontal { fn time_zoom (&self) -> usize { self.range.time_zoom() } fn set_time_zoom (&self, x: usize) { self.range.set_time_zoom(x); } From 9497f530cd8f4022cbcbb5a8025d7c974b3ca5b8 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 23:01:40 +0100 Subject: [PATCH 034/971] impl has_phrases --- crates/tek/src/api/phrase.rs | 9 +++++++++ crates/tek/src/layout/measure.rs | 2 +- crates/tek/src/tui/app_arranger.rs | 10 +--------- crates/tek/src/tui/app_sequencer.rs | 10 +--------- crates/tek/src/tui/phrase_list.rs | 9 +-------- 5 files changed, 13 insertions(+), 27 deletions(-) diff --git a/crates/tek/src/api/phrase.rs b/crates/tek/src/api/phrase.rs index 3f8ca6b8..aa1f437a 100644 --- a/crates/tek/src/api/phrase.rs +++ b/crates/tek/src/api/phrase.rs @@ -5,6 +5,15 @@ pub trait HasPhrases { fn phrases_mut (&mut self) -> &mut Vec>>; } +#[macro_export] macro_rules! has_phrases { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasPhrases for $Struct $(<$($L),*$($T),*>)? { + fn phrases (&$self) -> &Vec>> { &$cb } + fn phrases_mut (&mut $self) -> &mut Vec>> { &mut$cb } + } + } +} + pub trait HasPhrase { fn phrase (&self) -> &Arc>; } diff --git a/crates/tek/src/layout/measure.rs b/crates/tek/src/layout/measure.rs index 27429cb6..987fe8a5 100644 --- a/crates/tek/src/layout/measure.rs +++ b/crates/tek/src/layout/measure.rs @@ -7,7 +7,7 @@ pub trait HasSize { #[macro_export] macro_rules! has_size { (<$E:ty>|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? HasSize<$E> for $Struct $(<$($L),*$($T),*>)? { - fn size (&$self) -> &Measure<$E> { $cb } // TODO provide trait body + fn size (&$self) -> &Measure<$E> { $cb } } } } diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 13697b3f..91681037 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -414,15 +414,7 @@ render!(|self: ArrangerTui|{ has_clock!(|self:ArrangerTui|&self.clock); has_clock!(|self:ArrangerTrack|self.player.clock()); - -impl HasPhrases for ArrangerTui { - fn phrases (&self) -> &Vec>> { - &self.phrases.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.phrases.phrases - } -} +has_phrases!(|self:ArrangerTui|self.phrases.phrases); impl HasPhraseList for ArrangerTui { fn phrases_focused (&self) -> bool { diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index b370767e..eb146915 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -282,15 +282,7 @@ impl TransportControl for SequencerTui { } has_clock!(|self:SequencerTui|&self.clock); - -impl HasPhrases for SequencerTui { - fn phrases (&self) -> &Vec>> { - &self.phrases.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.phrases.phrases - } -} +has_phrases!(|self:SequencerTui|self.phrases.phrases); impl HasPhraseList for SequencerTui { fn phrases_focused (&self) -> bool { diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 2967a542..972d62d5 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -180,14 +180,7 @@ impl From<&Arc>> for PhraseListModel { } } -impl HasPhrases for PhraseListModel { - fn phrases (&self) -> &Vec>> { - &self.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.phrases - } -} +has_phrases!(|self:PhraseListModel|self.phrases); impl HasPhrase for PhraseListModel { fn phrase (&self) -> &Arc> { From a5bcf3798e1962beafd096734ef4ebb0db9292a6 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 23:14:17 +0100 Subject: [PATCH 035/971] add has_player, has_editor --- crates/tek/src/api/player.rs | 9 +++++++++ crates/tek/src/tui/app_arranger.rs | 23 ++--------------------- crates/tek/src/tui/app_sequencer.rs | 13 +------------ crates/tek/src/tui/phrase_editor.rs | 10 ++++++++-- 4 files changed, 20 insertions(+), 35 deletions(-) diff --git a/crates/tek/src/api/player.rs b/crates/tek/src/api/player.rs index 6e72dc98..ff11e44f 100644 --- a/crates/tek/src/api/player.rs +++ b/crates/tek/src/api/player.rs @@ -5,6 +5,15 @@ pub trait HasPlayer { fn player_mut (&mut self) -> &mut impl MidiPlayerApi; } +#[macro_export] macro_rules! has_player { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? { + fn player (&$self) -> &impl MidiPlayerApi { &$cb } + fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb } + } + } +} + /// Contains state for playing a phrase pub struct PhrasePlayerModel { /// State of clock and playhead diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 91681037..35da9a0b 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -415,6 +415,8 @@ render!(|self: ArrangerTui|{ has_clock!(|self:ArrangerTui|&self.clock); has_clock!(|self:ArrangerTrack|self.player.clock()); has_phrases!(|self:ArrangerTui|self.phrases.phrases); +has_editor!(|self:ArrangerTui|self.editor); +has_player!(|self:ArrangerTrack|self.player); impl HasPhraseList for ArrangerTui { fn phrases_focused (&self) -> bool { @@ -431,18 +433,6 @@ impl HasPhraseList for ArrangerTui { } } -impl HasEditor for ArrangerTui { - fn editor (&self) -> &PhraseEditorModel { - &self.editor - } - fn editor_focused (&self) -> bool { - self.focused() == ArrangerFocus::PhraseEditor - } - fn editor_entered (&self) -> bool { - self.entered() && self.editor_focused() - } -} - /// Sections in the arranger app that may be focused #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum ArrangerFocus { @@ -1236,15 +1226,6 @@ pub struct ArrangerTrack { pub(crate) player: PhrasePlayerModel, } -impl HasPlayer for ArrangerTrack { - fn player (&self) -> &impl MidiPlayerApi { - &self.player - } - fn player_mut (&mut self) -> &mut impl MidiPlayerApi { - &mut self.player - } -} - impl ArrangerTrackApi for ArrangerTrack { /// Name of track fn name (&self) -> &Arc> { diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index eb146915..44cbe082 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -283,6 +283,7 @@ impl TransportControl for SequencerTui { has_clock!(|self:SequencerTui|&self.clock); has_phrases!(|self:SequencerTui|self.phrases.phrases); +has_editor!(|self:SequencerTui|self.editor); impl HasPhraseList for SequencerTui { fn phrases_focused (&self) -> bool { @@ -299,18 +300,6 @@ impl HasPhraseList for SequencerTui { } } -impl HasEditor for SequencerTui { - fn editor (&self) -> &PhraseEditorModel { - &self.editor - } - fn editor_focused (&self) -> bool { - false - } - fn editor_entered (&self) -> bool { - true - } -} - impl HasFocus for SequencerTui { type Item = SequencerFocus; /// Get the currently focused item. diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index fc579483..92efbcc5 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -5,8 +5,14 @@ use PhraseCommand::*; pub trait HasEditor { fn editor (&self) -> &PhraseEditorModel; - fn editor_focused (&self) -> bool; - fn editor_entered (&self) -> bool; +} + +#[macro_export] macro_rules! has_editor { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? { + fn editor (&$self) -> &PhraseEditorModel { &$cb } + } + } } #[derive(Clone, Debug)] From f783984a74596c22a94fb23944ffb162b8251375 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 23:20:57 +0100 Subject: [PATCH 036/971] remove HasFocus from SequencerTui; update status bar --- crates/tek/src/tui/app_sequencer.rs | 19 ++------- crates/tek/src/tui/app_transport.rs | 2 +- crates/tek/src/tui/status_bar.rs | 60 ++++++++--------------------- 3 files changed, 20 insertions(+), 61 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 44cbe082..3c2022dd 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -107,9 +107,10 @@ impl Command for SequencerCommand { impl Command for FocusCommand { fn execute (self, state: &mut SequencerTui) -> Perhaps> { - if let FocusCommand::Set(to) = self { - state.set_focused(to); - } + // Focus commands can be received but are ignored. + //if let FocusCommand::Set(to) = self { + //state.set_focused(to); + //} Ok(None) } } @@ -300,18 +301,6 @@ impl HasPhraseList for SequencerTui { } } -impl HasFocus for SequencerTui { - type Item = SequencerFocus; - /// Get the currently focused item. - fn focused (&self) -> Self::Item { - self.focus - } - /// Get the currently focused item. - fn set_focused (&mut self, to: Self::Item) { - self.focus = to - } -} - impl Into> for SequencerFocus { fn into (self) -> Option { if let Self::Transport(transport) = self { diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index 587b1914..af48ff98 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -214,7 +214,7 @@ impl Handle for TransportTui { } } -pub trait TransportControl: HasClock + HasFocus { +pub trait TransportControl: HasClock + { fn transport_focused (&self) -> Option; } diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs index 1939baa0..cc8846ad 100644 --- a/crates/tek/src/tui/status_bar.rs +++ b/crates/tek/src/tui/status_bar.rs @@ -50,60 +50,30 @@ impl StatusBar for SequencerStatusBar { impl From<&SequencerTui> for SequencerStatusBar { fn from (state: &SequencerTui) -> Self { - use super::app_transport::TransportFocus::*; - use super::app_sequencer::SequencerFocus::*; let samples = state.clock.chunk.load(Ordering::Relaxed); let rate = state.clock.timebase.sr.get() as f64; let buffer = samples as f64 / rate; let width = state.size.w(); - let default_help = &[("", "⏎", " enter"), ("", "✣", " navigate")]; Self { width, cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), size: format!("{}x{}│", width, state.size.h()), res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), - mode: match state.focused() { - Transport(PlayPause) => " PLAY/PAUSE ", - Transport(Bpm) => " TEMPO ", - Transport(Sync) => " LAUNCH SYNC ", - Transport(Quant) => " REC QUANT ", - Transport(Clock) => " SEEK ", - //PhrasePlay => " TO PLAY ", - //PhraseNext => " UP NEXT ", - PhraseList => " PHRASES ", - PhraseEditor => " EDIT MIDI ", - }, - help: match state.focused() { - Transport(PlayPause) => &[ - ("", "⏎", " play/pause"), - ("", "✣", " navigate"), - ], - Transport(Bpm) => &[ - ("", ".,", " inc/dec"), - ("", "><", " fine"), - ], - Transport(Sync) => &[ - ("", ".,", " inc/dec"), - ], - Transport(Quant) => &[ - ("", ".,", " inc/dec"), - ], - Transport(Clock) => &[ - ("", ".,", " by beat"), - ("", "<>", " by time"), - ], - PhraseList => &[ - ("", "↕", " pick"), - ("", ".,", " move"), - ("", "⏎", " play"), - ("", "e", " edit"), - ], - PhraseEditor => &[ - ("", "✣", " cursor"), - ("", "Ctrl-✣", " scroll"), - ], - _ => default_help, - } + mode: " SEQUENCER ", + help: &[ + ("", "SPACE", " play/pause"), + ("", "✣", " cursor"), + ("", "Ctrl-✣", " scroll"), + ("", ".,", " length"), + ("", "><", " triplet"), + ("", "[]", " phrase"), + ("", "{}", " order"), + ("en", "q", "ueue"), + ("", "e", "dit"), + ("", "a", "dd note"), + ("", "A", "dd phrase"), + ("", "D", "uplicate phrase"), + ] } } } From 32eb1bf0852861408729ba1be0f2ea35b303f503 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 23:24:07 +0100 Subject: [PATCH 037/971] add has_phrase --- crates/tek/src/api/phrase.rs | 8 ++++++++ crates/tek/src/tui/phrase_list.rs | 7 +------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/tek/src/api/phrase.rs b/crates/tek/src/api/phrase.rs index aa1f437a..1fed237d 100644 --- a/crates/tek/src/api/phrase.rs +++ b/crates/tek/src/api/phrase.rs @@ -18,6 +18,14 @@ pub trait HasPhrase { fn phrase (&self) -> &Arc>; } +#[macro_export] macro_rules! has_phrase { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasPhrase for $Struct $(<$($L),*$($T),*>)? { + fn phrase (&$self) -> &Arc> { &$cb } + } + } +} + #[derive(Clone, Debug, PartialEq)] pub enum PhrasePoolCommand { Add(usize, Phrase), diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 972d62d5..8bb886ee 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -181,12 +181,7 @@ impl From<&Arc>> for PhraseListModel { } has_phrases!(|self:PhraseListModel|self.phrases); - -impl HasPhrase for PhraseListModel { - fn phrase (&self) -> &Arc> { - &self.phrases[self.phrase_index()] - } -} +has_phrase!(|self:PhraseListModel|self.phrases[self.phrase_index()]); impl PhraseListModel { pub(crate) fn phrase_index (&self) -> usize { From 9f97c44c8444d73392040aa329688ad3e13f0c72 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 23:32:07 +0100 Subject: [PATCH 038/971] add audio! macro --- crates/tek/src/api/_todo_api_mixer.rs | 6 +- crates/tek/src/api/_todo_api_plugin.rs | 92 ++++++++++++------------- crates/tek/src/api/jack.rs | 40 ++++++----- crates/tek/src/api/sampler.rs | 16 ++--- crates/tek/src/tui/app_arranger.rs | 95 ++++++++++++-------------- crates/tek/src/tui/app_sampler.rs | 6 +- crates/tek/src/tui/app_sequencer.rs | 38 +++++------ crates/tek/src/tui/app_transport.rs | 8 +-- 8 files changed, 139 insertions(+), 162 deletions(-) diff --git a/crates/tek/src/api/_todo_api_mixer.rs b/crates/tek/src/api/_todo_api_mixer.rs index cd8df774..8e5f0135 100644 --- a/crates/tek/src/api/_todo_api_mixer.rs +++ b/crates/tek/src/api/_todo_api_mixer.rs @@ -20,8 +20,4 @@ impl From<&Arc>> for MixerAudio { } } -impl Audio for MixerAudio { - fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { - Control::Continue - } -} +audio!(|self: MixerAudio, _, _|Control::Continue); diff --git a/crates/tek/src/api/_todo_api_plugin.rs b/crates/tek/src/api/_todo_api_plugin.rs index 3edbac42..161c59d7 100644 --- a/crates/tek/src/api/_todo_api_plugin.rs +++ b/crates/tek/src/api/_todo_api_plugin.rs @@ -62,53 +62,51 @@ impl From<&Arc>> for PluginAudio { } } -impl Audio for PluginAudio { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - let state = &mut*self.0.write().unwrap(); - match state.plugin.as_mut() { - Some(PluginKind::LV2(LV2Plugin { - features, - ref mut instance, - ref mut input_buffer, - .. - })) => { - let urid = features.midi_urid(); - input_buffer.clear(); - for port in state.midi_ins.iter() { - let mut atom = ::livi::event::LV2AtomSequence::new( - &features, - scope.n_frames() as usize - ); - for event in port.iter(scope) { - match event.bytes.len() { - 3 => atom.push_midi_event::<3>( - event.time as i64, - urid, - &event.bytes[0..3] - ).unwrap(), - _ => {} - } +audio!(|self: PluginAudio, _, _|{ + let state = &mut*self.0.write().unwrap(); + match state.plugin.as_mut() { + Some(PluginKind::LV2(LV2Plugin { + features, + ref mut instance, + ref mut input_buffer, + .. + })) => { + let urid = features.midi_urid(); + input_buffer.clear(); + for port in state.midi_ins.iter() { + let mut atom = ::livi::event::LV2AtomSequence::new( + &features, + scope.n_frames() as usize + ); + for event in port.iter(scope) { + match event.bytes.len() { + 3 => atom.push_midi_event::<3>( + event.time as i64, + urid, + &event.bytes[0..3] + ).unwrap(), + _ => {} } - input_buffer.push(atom); } - let mut outputs = vec![]; - for _ in state.midi_outs.iter() { - outputs.push(::livi::event::LV2AtomSequence::new( - &features, - scope.n_frames() as usize - )); - } - let ports = ::livi::EmptyPortConnections::new() - .with_atom_sequence_inputs(input_buffer.iter()) - .with_atom_sequence_outputs(outputs.iter_mut()) - .with_audio_inputs(state.audio_ins.iter().map(|o|o.as_slice(scope))) - .with_audio_outputs(state.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); - unsafe { - instance.run(scope.n_frames() as usize, ports).unwrap() - }; - }, - _ => {} - } - Control::Continue + input_buffer.push(atom); + } + let mut outputs = vec![]; + for _ in state.midi_outs.iter() { + outputs.push(::livi::event::LV2AtomSequence::new( + &features, + scope.n_frames() as usize + )); + } + let ports = ::livi::EmptyPortConnections::new() + .with_atom_sequence_inputs(input_buffer.iter()) + .with_atom_sequence_outputs(outputs.iter_mut()) + .with_audio_inputs(state.audio_ins.iter().map(|o|o.as_slice(scope))) + .with_audio_outputs(state.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); + unsafe { + instance.run(scope.n_frames() as usize, ports).unwrap() + }; + }, + _ => {} } -} + Control::Continue +}); diff --git a/crates/tek/src/api/jack.rs b/crates/tek/src/api/jack.rs index d93c8e66..99b0edcf 100644 --- a/crates/tek/src/api/jack.rs +++ b/crates/tek/src/api/jack.rs @@ -1,5 +1,29 @@ use crate::*; +/// Trait for things that have a JACK process callback. +pub trait Audio: Send + Sync { + fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { + Control::Continue + } + fn callback ( + state: &Arc>, client: &Client, scope: &ProcessScope + ) -> Control where Self: Sized { + if let Ok(mut state) = state.write() { + state.process(client, scope) + } else { + Control::Quit + } + } +} + +#[macro_export] macro_rules! audio { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { + #[inline] fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { $cb } + } + } +} + pub trait HasMidiIns { fn midi_ins (&self) -> &Vec>; fn midi_ins_mut (&mut self) -> &mut Vec>; @@ -54,22 +78,6 @@ impl JackActivate for JackClient { } } -/// Trait for things that have a JACK process callback. -pub trait Audio: Send + Sync { - fn process(&mut self, _: &Client, _: &ProcessScope) -> Control { - Control::Continue - } - fn callback( - state: &Arc>, client: &Client, scope: &ProcessScope - ) -> Control where Self: Sized { - if let Ok(mut state) = state.write() { - state.process(client, scope) - } else { - Control::Quit - } - } -} - /// A UI component that may be associated with a JACK client by the `Jack` factory. pub trait AudioComponent: Component + Audio { /// Perform type erasure for collecting heterogeneous devices. diff --git a/crates/tek/src/api/sampler.rs b/crates/tek/src/api/sampler.rs index 260fbf53..07855dbd 100644 --- a/crates/tek/src/api/sampler.rs +++ b/crates/tek/src/api/sampler.rs @@ -96,15 +96,13 @@ impl From<&Arc>> for SamplerAudio { } } -impl Audio for SamplerAudio { - #[inline] fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - self.process_midi_in(scope); - self.clear_output_buffer(); - self.process_audio_out(scope); - self.write_output_buffer(scope); - Control::Continue - } -} +audio!(|self: SamplerAudio, _client, scope|{ + self.process_midi_in(scope); + self.clear_output_buffer(); + self.process_audio_out(scope); + self.write_output_buffer(scope); + Control::Continue +}); impl SamplerAudio { diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 35da9a0b..884eb487 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -323,53 +323,11 @@ impl TransportControl for ArrangerTui { } } } - -impl Audio for ArrangerTui { - #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - // Start profiling cycle - let t0 = self.perf.get_t0(); - - // Update transport clock - if ClockAudio(self).process(client, scope) == Control::Quit { - return Control::Quit - } - - // Update MIDI sequencers - if TracksAudio( - &mut self.tracks, - &mut self.note_buf, - &mut self.midi_buf, - Default::default(), - ).process(client, scope) == Control::Quit { - return Control::Quit - } - - // FIXME: one of these per playing track - //self.now.set(0.); - //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.play_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.now.set(now); - //} - //} - //} - //} - //} - - // End profiling cycle - self.perf.update(t0, scope); - - return Control::Continue - } -} - +has_clock!(|self:ArrangerTui|&self.clock); +has_clock!(|self:ArrangerTrack|self.player.clock()); +has_phrases!(|self:ArrangerTui|self.phrases.phrases); +has_editor!(|self:ArrangerTui|self.editor); +has_player!(|self:ArrangerTrack|self.player); // Layout for standalone arranger app. render!(|self: ArrangerTui|{ let arranger_focused = self.arranger_focused(); @@ -411,12 +369,43 @@ render!(|self: ArrangerTui|{ ]) ]) }); - -has_clock!(|self:ArrangerTui|&self.clock); -has_clock!(|self:ArrangerTrack|self.player.clock()); -has_phrases!(|self:ArrangerTui|self.phrases.phrases); -has_editor!(|self:ArrangerTui|self.editor); -has_player!(|self:ArrangerTrack|self.player); +audio!(|self: ArrangerTui, client, scope|{ + // Start profiling cycle + let t0 = self.perf.get_t0(); + // Update transport clock + if ClockAudio(self).process(client, scope) == Control::Quit { + return Control::Quit + } + // Update MIDI sequencers + let tracks = &mut self.tracks; + let note_buf = &mut self.note_buf; + let midi_buf = &mut self.midi_buf; + if TracksAudio(tracks, note_buf, midi_buf, Default::default()) + .process(client, scope) == Control::Quit { + return Control::Quit + } + // FIXME: one of these per playing track + //self.now.set(0.); + //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.play_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.now.set(now); + //} + //} + //} + //} + //} + // End profiling cycle + self.perf.update(t0, scope); + return Control::Continue +}); impl HasPhraseList for ArrangerTui { fn phrases_focused (&self) -> bool { diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index 2fa6b9a2..795b5f58 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -50,6 +50,7 @@ pub enum SamplerFocus { _TODO } +audio!(|self: SamplerTui, _client, _scope|Control::Continue); render!(|self: SamplerTui|render(|to|{ let [x, y, _, height] = to.area(); let style = Style::default().gray(); @@ -93,11 +94,6 @@ impl SamplerTui { None } } -impl Audio for SamplerTui { - #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - Control::Continue - } -} pub struct AddSampleModal { exited: bool, diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 3c2022dd..c8a70f20 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -61,26 +61,6 @@ pub enum SequencerFocus { PhraseEditor, } -impl Audio for SequencerTui { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - // Start profiling cycle - let t0 = self.perf.get_t0(); - // Update transport clock - if Control::Quit == ClockAudio(self).process(client, scope) { - return Control::Quit - } - // Update MIDI sequencer - if Control::Quit == PlayerAudio( - &mut self.player, &mut self.note_buf, &mut self.midi_buf - ).process(client, scope) { - return Control::Quit - } - // End profiling cycle - self.perf.update(t0, scope); - Control::Continue - } -} - #[derive(Clone, Debug)] pub enum SequencerCommand { Focus(FocusCommand), @@ -175,6 +155,24 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option 60 { diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index af48ff98..b4c3cd6b 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -38,13 +38,7 @@ impl std::fmt::Debug for TransportTui { } has_clock!(|self:TransportTui|&self.clock); - -impl Audio for TransportTui { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - ClockAudio(self).process(client, scope) - } -} - +audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope)); render!(|self: TransportTui|TransportView::from((self, None, true))); pub struct TransportView { From 81cb532af37c474f20883995b1a0ba18075617e1 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 23:41:07 +0100 Subject: [PATCH 039/971] rewrite and put in action MIDIViewport::autoscroll (does nothing) --- crates/tek/src/api.rs | 3 +- crates/tek/src/api/clip.rs | 20 ------ crates/tek/src/api/name.rs | 0 crates/tek/src/api/note.rs | 104 ++++++++++++----------------- crates/tek/src/tui/app_arranger.rs | 20 +++++- 5 files changed, 61 insertions(+), 86 deletions(-) delete mode 100644 crates/tek/src/api/clip.rs delete mode 100644 crates/tek/src/api/name.rs diff --git a/crates/tek/src/api.rs b/crates/tek/src/api.rs index e7219c7d..96cc6722 100644 --- a/crates/tek/src/api.rs +++ b/crates/tek/src/api.rs @@ -1,6 +1,5 @@ -mod phrase; pub(crate) use phrase::*; mod jack; pub(crate) use self::jack::*; -mod clip; pub(crate) use clip::*; +mod phrase; pub(crate) use phrase::*; mod clock; pub(crate) use clock::*; mod note; pub(crate) use note::*; mod player; pub(crate) use player::*; diff --git a/crates/tek/src/api/clip.rs b/crates/tek/src/api/clip.rs deleted file mode 100644 index fe8781ce..00000000 --- a/crates/tek/src/api/clip.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::*; - -#[derive(Clone, Debug)] -pub enum ArrangerClipCommand { - Play, - Get(usize, usize), - Set(usize, usize, Option>>), - Edit(Option>>), - SetLoop(bool), - RandomColor, -} - -//impl Command for ArrangerClipCommand { - //fn execute (self, state: &mut T) -> Perhaps { - //match self { - //_ => todo!() - //} - //Ok(None) - //} -//} diff --git a/crates/tek/src/api/name.rs b/crates/tek/src/api/name.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/tek/src/api/note.rs b/crates/tek/src/api/note.rs index 28be9ae7..8aa38330 100644 --- a/crates/tek/src/api/note.rs +++ b/crates/tek/src/api/note.rs @@ -1,6 +1,24 @@ use crate::*; use Ordering::Relaxed; +pub trait MIDIViewport: MIDIRange + MIDIPoint + HasSize { + /// Make sure cursor is within range + fn autoscroll (&self) { + let note_lo = self.note_lo(); + let note_axis = self.note_axis(); + let note_hi = (note_lo + note_axis).min(127); + let note_point = self.note_point().min(127); + if note_point < note_lo { + self.set_note_lo(note_point); + } else if note_point > note_hi { + self.set_note_lo(note_lo + note_point - note_hi); + } + } + /// Make sure best usage of screen space is achieved by default + fn autozoom (&self) { + } +} + #[derive(Debug, Clone)] pub struct MIDIRangeModel { /// Length of visible time axis @@ -16,7 +34,6 @@ pub struct MIDIRangeModel { // Lowest note displayed pub note_lo: Arc, } - impl From<(usize, bool)> for MIDIRangeModel { fn from ((time_zoom, time_lock): (usize, bool)) -> Self { Self { @@ -29,6 +46,29 @@ impl From<(usize, bool)> for MIDIRangeModel { } } } +pub trait MIDIRange { + fn time_zoom (&self) -> usize; + fn set_time_zoom (&self, x: usize); + fn time_lock (&self) -> bool; + fn set_time_lock (&self, x: bool); + fn time_start (&self) -> usize; + fn set_time_start (&self, x: usize); + fn note_lo (&self) -> usize; + fn set_note_lo (&self, x: usize); + fn note_axis (&self) -> usize; + fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } +} +impl MIDIRange for MIDIRangeModel { + fn time_zoom (&self) -> usize { self.time_zoom.load(Relaxed) } + fn set_time_zoom (&self, x: usize) { self.time_zoom.store(x, Relaxed); } + fn time_lock (&self) -> bool { self.time_lock.load(Relaxed) } + fn set_time_lock (&self, x: bool) { self.time_lock.store(x, Relaxed); } + fn time_start (&self) -> usize { self.time_start.load(Relaxed) } + fn set_time_start (&self, x: usize) { self.time_start.store(x, Relaxed); } + fn note_lo (&self) -> usize { self.note_lo.load(Relaxed) } + fn set_note_lo (&self, x: usize) { self.note_lo.store(x, Relaxed); } + fn note_axis (&self) -> usize { self.note_axis.load(Relaxed) } +} #[derive(Debug, Clone)] pub struct MIDIPointModel { @@ -39,7 +79,6 @@ pub struct MIDIPointModel { /// Length of note that will be inserted, in pulses pub note_len: Arc, } - impl Default for MIDIPointModel { fn default () -> Self { Self { @@ -49,76 +88,15 @@ impl Default for MIDIPointModel { } } } - -pub trait MIDIRange { - fn time_zoom (&self) -> usize; - fn set_time_zoom (&self, x: usize); - - fn time_lock (&self) -> bool; - fn set_time_lock (&self, x: bool); - - fn time_start (&self) -> usize; - fn set_time_start (&self, x: usize); - - fn note_lo (&self) -> usize; - fn set_note_lo (&self, x: usize); - - fn note_axis (&self) -> usize; - - fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } -} - pub trait MIDIPoint { fn note_len (&self) -> usize; fn set_note_len (&self, x: usize); - fn note_point (&self) -> usize; fn set_note_point (&self, x: usize); - fn time_point (&self) -> usize; fn set_time_point (&self, x: usize); - fn note_end (&self) -> usize { self.note_point() + self.note_len() } } - -pub trait MIDIViewport: MIDIRange + MIDIPoint + HasSize { - /// Make sure cursor is within range - fn autoscroll (&self) { - let note = self.note_point(); - let height = self.size().h(); - if note < self.note_lo() { - self.set_note_lo(note) - } - let note_point = self.note_point(); - let mut note_lo = self.note_lo(); - let mut note_hi = 127.min((note_lo + height).saturating_sub(2)); - if note_point > note_hi { - note_lo += note_point - note_hi; - note_hi = note_point; - self.set_note_lo(note_lo); - } - //(note_point, (note_lo, note_hi)) - } - /// Make sure best usage of screen space is achieved by default - fn autozoom (&self) { - } -} - -impl MIDIRange for MIDIRangeModel { - fn time_zoom (&self) -> usize { self.time_zoom.load(Relaxed) } - fn set_time_zoom (&self, x: usize) { self.time_zoom.store(x, Relaxed); } - - fn time_lock (&self) -> bool { self.time_lock.load(Relaxed) } - fn set_time_lock (&self, x: bool) { self.time_lock.store(x, Relaxed); } - - fn time_start (&self) -> usize { self.time_start.load(Relaxed) } - fn set_time_start (&self, x: usize) { self.time_start.store(x, Relaxed); } - - fn note_lo (&self) -> usize { self.note_lo.load(Relaxed) } - fn set_note_lo (&self, x: usize) { self.note_lo.store(x, Relaxed); } - - fn note_axis (&self) -> usize { self.note_axis.load(Relaxed) } -} impl MIDIPoint for MIDIPointModel { fn note_len (&self) -> usize { self.note_len.load(Relaxed)} fn set_note_len (&self, x: usize) { self.note_len.store(x, Relaxed) } diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 884eb487..e0feca43 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -1,7 +1,6 @@ use crate::*; use crate::api::ArrangerTrackCommand; use crate::api::ArrangerSceneCommand; -use crate::api::ArrangerClipCommand; impl TryFrom<&Arc>> for ArrangerTui { type Error = Box; @@ -1302,3 +1301,22 @@ impl ArrangerSelection { } } } + +#[derive(Clone, Debug)] +pub enum ArrangerClipCommand { + Play, + Get(usize, usize), + Set(usize, usize, Option>>), + Edit(Option>>), + SetLoop(bool), + RandomColor, +} + +//impl Command for ArrangerClipCommand { + //fn execute (self, state: &mut T) -> Perhaps { + //match self { + //_ => todo!() + //} + //Ok(None) + //} +//} From 6ee3abed8c3198b056e4a07ad24d63700b63e866 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 23:41:57 +0100 Subject: [PATCH 040/971] MIDI -> Midi --- crates/tek/src/api/note.rs | 18 +++++++++--------- crates/tek/src/tui/phrase_editor.rs | 8 ++++---- crates/tek/src/tui/piano_horizontal.rs | 12 ++++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/tek/src/api/note.rs b/crates/tek/src/api/note.rs index 8aa38330..e39d0024 100644 --- a/crates/tek/src/api/note.rs +++ b/crates/tek/src/api/note.rs @@ -1,7 +1,7 @@ use crate::*; use Ordering::Relaxed; -pub trait MIDIViewport: MIDIRange + MIDIPoint + HasSize { +pub trait MidiViewport: MidiRange + MidiPoint + HasSize { /// Make sure cursor is within range fn autoscroll (&self) { let note_lo = self.note_lo(); @@ -20,7 +20,7 @@ pub trait MIDIViewport: MIDIRange + MIDIPoint + HasSize { } #[derive(Debug, Clone)] -pub struct MIDIRangeModel { +pub struct MidiRangeModel { /// Length of visible time axis pub time_axis: Arc, /// Earliest time displayed @@ -34,7 +34,7 @@ pub struct MIDIRangeModel { // Lowest note displayed pub note_lo: Arc, } -impl From<(usize, bool)> for MIDIRangeModel { +impl From<(usize, bool)> for MidiRangeModel { fn from ((time_zoom, time_lock): (usize, bool)) -> Self { Self { note_axis: Arc::new(0.into()), @@ -46,7 +46,7 @@ impl From<(usize, bool)> for MIDIRangeModel { } } } -pub trait MIDIRange { +pub trait MidiRange { fn time_zoom (&self) -> usize; fn set_time_zoom (&self, x: usize); fn time_lock (&self) -> bool; @@ -58,7 +58,7 @@ pub trait MIDIRange { fn note_axis (&self) -> usize; fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } } -impl MIDIRange for MIDIRangeModel { +impl MidiRange for MidiRangeModel { fn time_zoom (&self) -> usize { self.time_zoom.load(Relaxed) } fn set_time_zoom (&self, x: usize) { self.time_zoom.store(x, Relaxed); } fn time_lock (&self) -> bool { self.time_lock.load(Relaxed) } @@ -71,7 +71,7 @@ impl MIDIRange for MIDIRangeModel { } #[derive(Debug, Clone)] -pub struct MIDIPointModel { +pub struct MidiPointModel { /// Time coordinate of cursor pub time_point: Arc, /// Note coordinate of cursor @@ -79,7 +79,7 @@ pub struct MIDIPointModel { /// Length of note that will be inserted, in pulses pub note_len: Arc, } -impl Default for MIDIPointModel { +impl Default for MidiPointModel { fn default () -> Self { Self { time_point: Arc::new(0.into()), @@ -88,7 +88,7 @@ impl Default for MIDIPointModel { } } } -pub trait MIDIPoint { +pub trait MidiPoint { fn note_len (&self) -> usize; fn set_note_len (&self, x: usize); fn note_point (&self) -> usize; @@ -97,7 +97,7 @@ pub trait MIDIPoint { fn set_time_point (&self, x: usize); fn note_end (&self) -> usize { self.note_point() + self.note_len() } } -impl MIDIPoint for MIDIPointModel { +impl MidiPoint for MidiPointModel { fn note_len (&self) -> usize { self.note_len.load(Relaxed)} fn set_note_len (&self, x: usize) { self.note_len.store(x, Relaxed) } fn note_point (&self) -> usize { self.note_point.load(Relaxed) } diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 92efbcc5..5caf119e 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -117,7 +117,7 @@ impl Default for PhraseEditorModel { render!(|self: PhraseEditorModel|self.mode); -pub trait PhraseViewMode: Render + HasSize + MIDIRange + MIDIPoint + Debug + Send + Sync { +pub trait PhraseViewMode: Render + HasSize + MidiRange + MidiPoint + Debug + Send + Sync { fn buffer_size (&self, phrase: &Phrase) -> (usize, usize); fn redraw (&mut self); fn phrase (&self) -> &Option>>; @@ -128,11 +128,11 @@ pub trait PhraseViewMode: Render + HasSize + MIDIRange + MIDIPoint + D } } -impl MIDIViewport for PhraseEditorModel {} +impl MidiViewport for PhraseEditorModel {} has_size!(|self:PhraseEditorModel|self.mode.size()); -impl MIDIRange for PhraseEditorModel { +impl MidiRange for PhraseEditorModel { fn time_zoom (&self) -> usize { self.mode.time_zoom() } fn set_time_zoom (&self, x: usize) { self.mode.set_time_zoom(x); } fn time_lock (&self) -> bool { self.mode.time_lock() } @@ -144,7 +144,7 @@ impl MIDIRange for PhraseEditorModel { fn note_axis (&self) -> usize { self.mode.note_lo() } fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } } -impl MIDIPoint for PhraseEditorModel { +impl MidiPoint for PhraseEditorModel { fn note_len (&self) -> usize { self.mode.note_len()} fn set_note_len (&self, x: usize) { self.mode.set_note_len(x) } fn note_point (&self) -> usize { self.mode.note_point() } diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 5cabba28..38516249 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -9,9 +9,9 @@ pub struct PianoHorizontal { /// Width and height of notes area at last render size: Measure, /// The display window - range: MIDIRangeModel, + range: MidiRangeModel, /// The note cursor - point: MIDIPointModel, + point: MidiPointModel, /// The highlight color palette color: ItemPalette, } @@ -19,7 +19,7 @@ pub struct PianoHorizontal { impl PianoHorizontal { pub fn new (phrase: Option<&Arc>>) -> Self { let size = Measure::new(); - let mut range = MIDIRangeModel::from((24, true)); + let mut range = MidiRangeModel::from((24, true)); range.time_axis = size.x.clone(); range.note_axis = size.y.clone(); let phrase = phrase.map(|p|p.clone()); @@ -28,7 +28,7 @@ impl PianoHorizontal { .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); Self { buffer: Default::default(), - point: MIDIPointModel::default(), + point: MidiPointModel::default(), size, range, phrase, @@ -243,7 +243,7 @@ impl PianoHorizontal { has_size!(|self:PianoHorizontal|&self.size); -impl MIDIRange for PianoHorizontal { +impl MidiRange for PianoHorizontal { fn time_zoom (&self) -> usize { self.range.time_zoom() } fn set_time_zoom (&self, x: usize) { self.range.set_time_zoom(x); } fn time_lock (&self) -> bool { self.range.time_lock() } @@ -255,7 +255,7 @@ impl MIDIRange for PianoHorizontal { fn note_axis (&self) -> usize { self.range.note_lo() } fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } } -impl MIDIPoint for PianoHorizontal { +impl MidiPoint for PianoHorizontal { fn note_len (&self) -> usize { self.point.note_len()} fn set_note_len (&self, x: usize) { self.point.set_note_len(x) } fn note_point (&self) -> usize { self.point.note_point() } From 03e3a82238c157317ccd4e417c83fd2257b48364 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 15 Dec 2024 00:16:12 +0100 Subject: [PATCH 041/971] show all editor coordinates --- crates/tek/src/tui/app_sequencer.rs | 15 +++++++-------- crates/tek/src/tui/phrase_editor.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index c8a70f20..2197f104 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -173,23 +173,22 @@ audio!(|self:SequencerTui,client,scope|{ Control::Continue }); -render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 1, - Tui::fill_xy(SequencerStatusBar::from(self)), +render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 3, + Tui::fill_xy(col!([ + PhraseEditStatus(&self.editor), + SequencerStatusBar::from(self) + ])), Tui::split_right(false, if self.size.w() > 60 { 20 } else if self.size.w() > 40 { 15 } else { 10 - }, - Tui::fixed_x(20, Tui::split_down(false, 4, col!([ + }, Tui::fixed_x(20, col!([ PhraseSelector::play_phrase(&self.player), PhraseSelector::next_phrase(&self.player), - ]), Tui::split_up(false, 2, - PhraseSelector::edit_phrase(self.editor.phrase()), PhraseListView::from(self), - ))), - col!([ + ])), col!([ Tui::fixed_y(2, TransportView::from(( self, self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone(), diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 5caf119e..40bd74ab 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -224,3 +224,32 @@ impl std::fmt::Debug for PhraseEditorModel { .finish() } } + +pub struct PhraseEditStatus<'a>(pub &'a PhraseEditorModel); +render!(|self:PhraseEditStatus<'a>|row!(|add|{ + let (color, name, length) = if let Some(phrase) = self.0.phrase().as_ref().map(|p|p.read().unwrap()) { + (phrase.color, phrase.name.clone(), phrase.length) + } else { + (ItemPalette::from(TuiTheme::g(64)), String::new(), 0) + }; + let bg = color.base.rgb; + let fg = color.lightest.rgb; + let mut field = |name:&str, value:String|add(&Tui::fixed_xy(8, 2, + Tui::bg(bg, Tui::fg(fg, col!([ + name, Tui::bold(true, &value) + ]))))); + + field("Edit", format!("{name}"))?; + field("Length", format!("{length}"))?; + field("TimePt", format!("{}", self.0.time_point()))?; + field("TimeZoom", format!("{}", self.0.time_zoom()))?; + field("TimeLock", format!("{}", self.0.time_lock()))?; + field("TimeStrt", format!("{}", self.0.time_start()))?; + field("NoteLen", format!("{}", self.0.note_len()))?; + field("NoteLo", format!("{}", self.0.note_lo()))?; + field("NoteAxis", format!("{}", self.0.note_axis()))?; + field("NoteHi", format!("{}", self.0.note_hi()))?; + field("NotePt", format!("{}", self.0.note_point()))?; + + Ok(()) +})); From 999dc5906e424ed6694e629ffd8fe0e395b76ed4 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 15 Dec 2024 00:39:23 +0100 Subject: [PATCH 042/971] remove modality; rename splits --- crates/tek/src/layout/split.rs | 8 +- crates/tek/src/tui/app_sequencer.rs | 122 ++++++++++++++++++++++++- crates/tek/src/tui/piano_horizontal.rs | 2 +- 3 files changed, 125 insertions(+), 7 deletions(-) diff --git a/crates/tek/src/layout/split.rs b/crates/tek/src/layout/split.rs index 6da13980..e4c98785 100644 --- a/crates/tek/src/layout/split.rs +++ b/crates/tek/src/layout/split.rs @@ -8,22 +8,22 @@ pub trait LayoutSplit { ) -> Split { Split::new(flip, direction, amount, a, b) } - fn split_up , B: Render> ( + fn split_n , B: Render> ( flip: bool, amount: E::Unit, a: A, b: B ) -> Split { Self::split(flip, Direction::Up, amount, a, b) } - fn split_down , B: Render> ( + fn split_s , B: Render> ( flip: bool, amount: E::Unit, a: A, b: B ) -> Split { Self::split(flip, Direction::Down, amount, a, b) } - fn split_left , B: Render> ( + fn split_w , B: Render> ( flip: bool, amount: E::Unit, a: A, b: B ) -> Split { Self::split(flip, Direction::Left, amount, a, b) } - fn split_right , B: Render> ( + fn split_e , B: Render> ( flip: bool, amount: E::Unit, a: A, b: B ) -> Split { Self::split(flip, Direction::Right, amount, a, b) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 2197f104..c4a881b1 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -173,12 +173,12 @@ audio!(|self:SequencerTui,client,scope|{ Control::Continue }); -render!(|self: SequencerTui|lay!([self.size, Tui::split_up(false, 3, +render!(|self: SequencerTui|lay!([self.size, Tui::split_n(false, 3, Tui::fill_xy(col!([ PhraseEditStatus(&self.editor), SequencerStatusBar::from(self) ])), - Tui::split_right(false, if self.size.w() > 60 { + Tui::split_e(false, if self.size.w() > 60 { 20 } else if self.size.w() > 40 { 15 @@ -322,3 +322,121 @@ impl Handle for SequencerTui { SequencerCommand::execute_with_state(self, i) } } + +/// Status bar for sequencer app +#[derive(Clone)] +pub struct SequencerStatusBar { + pub(crate) width: usize, + pub(crate) cpu: Option, + pub(crate) size: String, + pub(crate) res: String, + pub(crate) mode: &'static str, + pub(crate) help: &'static [(&'static str, &'static str, &'static str)] +} + +impl StatusBar for SequencerStatusBar { + type State = SequencerTui; + fn hotkey_fg () -> Color { + TuiTheme::HOTKEY_FG + } + fn update (&mut self, _: &SequencerTui) { + todo!() + } +} + +impl From<&SequencerTui> for SequencerStatusBar { + fn from (state: &SequencerTui) -> Self { + let samples = state.clock.chunk.load(Ordering::Relaxed); + let rate = state.clock.timebase.sr.get() as f64; + let buffer = samples as f64 / rate; + let width = state.size.w(); + Self { + width, + cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), + size: format!("{}x{}│", width, state.size.h()), + res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), + mode: " SEQUENCER ", + help: &[ + ("", "SPACE", " play/pause"), + ("", "✣", " cursor"), + ("", "Ctrl-✣", " scroll"), + ("", ".,", " length"), + ("", "><", " triplet"), + ("", "[]", " phrase"), + ("", "{}", " order"), + ("en", "q", "ueue"), + ("", "e", "dit"), + ("", "a", "dd note"), + ("", "A", "dd phrase"), + ("", "D", "uplicate phrase"), + ] + } + } +} + +render!(|self: SequencerStatusBar|{ + lay!(|add|if self.width > 40 { + add(&Tui::fill_x(Tui::fixed_y(1, lay!([ + Tui::fill_x(Tui::at_w(SequencerModeline::from(self))), + Tui::fill_x(Tui::at_e(SequencerStats::from(self))), + ])))) + } else { + add(&Tui::fill_x(Tui::fixed_y(2, col!(![ + Tui::fill_x(Tui::center_x(SequencerModeline::from(self))), + Tui::fill_x(Tui::center_x(SequencerStats::from(self))), + ])))) + }) +}); + +struct SequencerModeline { + mode: &'static str, + help: &'static [(&'static str, &'static str, &'static str)] +} +impl From<&SequencerStatusBar> for SequencerModeline { + fn from (state: &SequencerStatusBar) -> Self { + Self { + mode: state.mode, + help: state.help, + } + } +} +render!(|self: SequencerModeline|{ + let black = TuiTheme::g(0); + let light = TuiTheme::g(50); + let white = TuiTheme::g(255); + let orange = TuiTheme::orange(); + let yellow = TuiTheme::yellow(); + row!([ + //Tui::bg(orange, Tui::fg(black, Tui::bold(true, self.mode))), + Tui::bg(light, Tui::fg(white, row!((prefix, hotkey, suffix) in self.help.iter() => { + row!([" ", prefix, Tui::fg(yellow, *hotkey), suffix]) + }))) + ]) +}); + +struct SequencerStats<'a> { + cpu: &'a Option, + size: &'a String, + res: &'a String, +} +impl<'a> From<&'a SequencerStatusBar> for SequencerStats<'a> { + fn from (state: &'a SequencerStatusBar) -> Self { + Self { + cpu: &state.cpu, + size: &state.size, + res: &state.res, + } + } +} +render!(|self:SequencerStats<'a>|{ + let orange = TuiTheme::orange(); + let dark = TuiTheme::g(25); + let cpu = &self.cpu; + let res = &self.res; + let size = &self.size; + Tui::bg(dark, row!([ + Tui::fg(orange, cpu), + Tui::fg(orange, res), + Tui::fg(orange, size), + ])) +}); diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 38516249..891d5c7e 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -48,7 +48,7 @@ render!(|self: PianoHorizontal|{ let time_point = self.point.time_point(); let note_point = self.point.note_point(); let note_len = self.point.note_len(); - Tui::bg(bg, Tui::split_down(false, 1, + Tui::bg(bg, Tui::split_s(false, 1, Tui::debug(Tui::fill_x(Tui::push_x(5, Tui::bg(fg.darkest.rgb, Tui::fg(fg.lightest.rgb, PianoHorizontalTimeline { time_start, From a25272ad1b026a24f25dedc13c38d3b75534db7b Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 15 Dec 2024 01:09:10 +0100 Subject: [PATCH 043/971] rework status bar --- crates/tek/src/tui/app_sequencer.rs | 131 +++++++++------------------- crates/tek/src/tui/app_transport.rs | 22 ++--- 2 files changed, 54 insertions(+), 99 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index c4a881b1..2be7c65a 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -173,10 +173,10 @@ audio!(|self:SequencerTui,client,scope|{ Control::Continue }); -render!(|self: SequencerTui|lay!([self.size, Tui::split_n(false, 3, +render!(|self: SequencerTui|lay!([self.size, Tui::split_n(false, 4, Tui::fill_xy(col!([ PhraseEditStatus(&self.editor), - SequencerStatusBar::from(self) + SequencerStatusBar::from(self), ])), Tui::split_e(false, if self.size.w() > 60 { 20 @@ -326,12 +326,11 @@ impl Handle for SequencerTui { /// Status bar for sequencer app #[derive(Clone)] pub struct SequencerStatusBar { - pub(crate) width: usize, - pub(crate) cpu: Option, - pub(crate) size: String, - pub(crate) res: String, - pub(crate) mode: &'static str, - pub(crate) help: &'static [(&'static str, &'static str, &'static str)] + pub(crate) width: usize, + pub(crate) cpu: Option, + pub(crate) size: String, + pub(crate) res: String, + pub(crate) playing: bool, } impl StatusBar for SequencerStatusBar { @@ -352,91 +351,47 @@ impl From<&SequencerTui> for SequencerStatusBar { let width = state.size.w(); Self { width, + playing: state.clock.is_rolling(), cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), size: format!("{}x{}│", width, state.size.h()), res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), - mode: " SEQUENCER ", - help: &[ - ("", "SPACE", " play/pause"), - ("", "✣", " cursor"), - ("", "Ctrl-✣", " scroll"), - ("", ".,", " length"), - ("", "><", " triplet"), - ("", "[]", " phrase"), - ("", "{}", " order"), - ("en", "q", "ueue"), - ("", "e", "dit"), - ("", "a", "dd note"), - ("", "A", "dd phrase"), - ("", "D", "uplicate phrase"), - ] } } } -render!(|self: SequencerStatusBar|{ - lay!(|add|if self.width > 40 { - add(&Tui::fill_x(Tui::fixed_y(1, lay!([ - Tui::fill_x(Tui::at_w(SequencerModeline::from(self))), - Tui::fill_x(Tui::at_e(SequencerStats::from(self))), - ])))) - } else { - add(&Tui::fill_x(Tui::fixed_y(2, col!(![ - Tui::fill_x(Tui::center_x(SequencerModeline::from(self))), - Tui::fill_x(Tui::center_x(SequencerStats::from(self))), - ])))) - }) -}); - -struct SequencerModeline { - mode: &'static str, - help: &'static [(&'static str, &'static str, &'static str)] -} -impl From<&SequencerStatusBar> for SequencerModeline { - fn from (state: &SequencerStatusBar) -> Self { - Self { - mode: state.mode, - help: state.help, - } - } -} -render!(|self: SequencerModeline|{ - let black = TuiTheme::g(0); - let light = TuiTheme::g(50); - let white = TuiTheme::g(255); - let orange = TuiTheme::orange(); - let yellow = TuiTheme::yellow(); - row!([ - //Tui::bg(orange, Tui::fg(black, Tui::bold(true, self.mode))), - Tui::bg(light, Tui::fg(white, row!((prefix, hotkey, suffix) in self.help.iter() => { - row!([" ", prefix, Tui::fg(yellow, *hotkey), suffix]) - }))) +render!(|self: SequencerStatusBar|Tui::fixed_y(2, row!([ + Tui::fixed_xy(11, 2, PlayPause(self.playing)), + lay!([ + { + let bg = TuiTheme::g(50); + let fg = TuiTheme::g(255); + let single = |binding, command|row!([" ", col!([ + Tui::fg(TuiTheme::yellow(), binding), + command + ])]); + let double = |(b1, c1), (b2, c2)|col!([ + row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1, " "]), + row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2, " "]), + ]); + Tui::bg(bg, Tui::fg(fg, row!([ + single("SPACE", "play/pause"), + double((" ✣", "cursor"), ("C-✣", "scroll"), ), + double((",.", "note"), ("<>", "triplet"),), + double(("[]", "phrase"), ("{}", "order"), ), + double(("q", "enqueue"), ("e", "edit"), ), + ]))) + }, + Tui::fill_xy(Tui::at_se({ + let orange = TuiTheme::orange(); + let dark = TuiTheme::g(25); + //let cpu = &self.cpu; + //let res = &self.res; + let size = &self.size; + Tui::bg(dark, row!([ + //Tui::fg(orange, cpu), + //Tui::fg(orange, res), + Tui::fg(orange, size), + ])) + })), ]) -}); - -struct SequencerStats<'a> { - cpu: &'a Option, - size: &'a String, - res: &'a String, -} -impl<'a> From<&'a SequencerStatusBar> for SequencerStats<'a> { - fn from (state: &'a SequencerStatusBar) -> Self { - Self { - cpu: &state.cpu, - size: &state.size, - res: &state.res, - } - } -} -render!(|self:SequencerStats<'a>|{ - let orange = TuiTheme::orange(); - let dark = TuiTheme::g(25); - let cpu = &self.cpu; - let res = &self.res; - let size = &self.size; - Tui::bg(dark, row!([ - Tui::fg(orange, cpu), - Tui::fg(orange, res), - Tui::fg(orange, size), - ])) -}); +]))); diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index b4c3cd6b..bb238e35 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -114,18 +114,8 @@ render!(|self: TransportView|{ 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(&Tui::fg(Color::Rgb(0, 255, 0), col!(["▶ PLAYING", "▒ ▒ ▒ ▒ ▒"]))) - } else { - add(&Tui::fg(Color::Rgb(255, 128, 0), col!(["▒ ▒ ▒ ▒ ▒", "⏹ STOPPED"]))) - }))) - )); - Tui::bg(self.bg, Tui::fill_x(row!([ - PlayPause(self.started), " ", + //PlayPause(self.started), " ", col!([ Field("Beat", self.beat.as_str()), Field("BPM ", self.bpm.as_str()), @@ -139,6 +129,16 @@ render!(|self: TransportView|{ }); +pub struct PlayPause(pub 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(&Tui::fg(Color::Rgb(0, 255, 0), col!(["▶ PLAYING", "▒ ▒ ▒ ▒ ▒"]))) + } else { + add(&Tui::fg(Color::Rgb(255, 128, 0), col!(["▒ ▒ ▒ ▒ ▒", "⏹ STOPPED"]))) + }))) +)); + impl HasFocus for TransportTui { type Item = TransportFocus; fn focused (&self) -> Self::Item { From ddba9e038264510ad554825d4498afc67a7f9e89 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 15 Dec 2024 01:46:20 +0100 Subject: [PATCH 044/971] showing keys again --- crates/tek/src/api/note.rs | 3 + crates/tek/src/tui/app_sequencer.rs | 2 +- crates/tek/src/tui/app_transport.rs | 4 +- crates/tek/src/tui/phrase_editor.rs | 46 +++++----- crates/tek/src/tui/piano_horizontal.rs | 63 ++++++------- crates/tek/src/tui/status_bar.rs | 118 ------------------------- 6 files changed, 65 insertions(+), 171 deletions(-) diff --git a/crates/tek/src/api/note.rs b/crates/tek/src/api/note.rs index e39d0024..3429cb77 100644 --- a/crates/tek/src/api/note.rs +++ b/crates/tek/src/api/note.rs @@ -56,7 +56,9 @@ pub trait MidiRange { fn note_lo (&self) -> usize; fn set_note_lo (&self, x: usize); fn note_axis (&self) -> usize; + fn time_axis (&self) -> usize; fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } + fn time_end (&self) -> usize { self.time_start() + self.time_axis() * self.time_zoom() } } impl MidiRange for MidiRangeModel { fn time_zoom (&self) -> usize { self.time_zoom.load(Relaxed) } @@ -68,6 +70,7 @@ impl MidiRange for MidiRangeModel { fn note_lo (&self) -> usize { self.note_lo.load(Relaxed) } fn set_note_lo (&self, x: usize) { self.note_lo.store(x, Relaxed); } fn note_axis (&self) -> usize { self.note_axis.load(Relaxed) } + fn time_axis (&self) -> usize { self.time_axis.load(Relaxed) } } #[derive(Debug, Clone)] diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 2be7c65a..16b75cd8 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -173,7 +173,7 @@ audio!(|self:SequencerTui,client,scope|{ Control::Continue }); -render!(|self: SequencerTui|lay!([self.size, Tui::split_n(false, 4, +render!(|self: SequencerTui|lay!([self.size, Tui::split_n(false, 5, Tui::fill_xy(col!([ PhraseEditStatus(&self.editor), SequencerStatusBar::from(self), diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index bb238e35..7c4b70a5 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -117,8 +117,8 @@ render!(|self: TransportView|{ Tui::bg(self.bg, Tui::fill_x(row!([ //PlayPause(self.started), " ", col!([ - Field("Beat", self.beat.as_str()), - Field("BPM ", self.bpm.as_str()), + Field(" Beat", self.beat.as_str()), + Field(" BPM ", self.bpm.as_str()), ]), " ", col!([ diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 40bd74ab..2263c001 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -141,7 +141,8 @@ impl MidiRange for PhraseEditorModel { fn set_time_start (&self, x: usize) { self.mode.set_time_start(x); } fn set_note_lo (&self, x: usize) { self.mode.set_note_lo(x); } fn note_lo (&self) -> usize { self.mode.note_lo() } - fn note_axis (&self) -> usize { self.mode.note_lo() } + fn note_axis (&self) -> usize { self.mode.note_axis() } + fn time_axis (&self) -> usize { self.mode.time_axis() } fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } } impl MidiPoint for PhraseEditorModel { @@ -232,24 +233,29 @@ render!(|self:PhraseEditStatus<'a>|row!(|add|{ } else { (ItemPalette::from(TuiTheme::g(64)), String::new(), 0) }; - let bg = color.base.rgb; + let bg = color.darker.rgb; let fg = color.lightest.rgb; - let mut field = |name:&str, value:String|add(&Tui::fixed_xy(8, 2, - Tui::bg(bg, Tui::fg(fg, col!([ - name, Tui::bold(true, &value) - ]))))); - - field("Edit", format!("{name}"))?; - field("Length", format!("{length}"))?; - field("TimePt", format!("{}", self.0.time_point()))?; - field("TimeZoom", format!("{}", self.0.time_zoom()))?; - field("TimeLock", format!("{}", self.0.time_lock()))?; - field("TimeStrt", format!("{}", self.0.time_start()))?; - field("NoteLen", format!("{}", self.0.note_len()))?; - field("NoteLo", format!("{}", self.0.note_lo()))?; - field("NoteAxis", format!("{}", self.0.note_axis()))?; - field("NoteHi", format!("{}", self.0.note_hi()))?; - field("NotePt", format!("{}", self.0.note_point()))?; - - Ok(()) + add(&Tui::fill_x(Tui::bg(bg, row!(|add|{ + add(&Tui::fixed_xy(16, 3, col!(![ + row!(![" Edit ", Tui::bold(true, format!("{name}"))]), + row!(![" Length ", Tui::bold(true, format!("{length}"))]), + row!(![" Loop ", Tui::bold(true, format!("on"))]), + ])))?; + add(&Tui::fixed_xy(12, 3, col!(![ + row!(!["Time ", Tui::bold(true, format!("{}", self.0.time_point()))]), + row!(!["Note ", Tui::bold(true, format!("{}", self.0.note_point()))]), + row!(!["Len ", Tui::bold(true, format!("{}", self.0.note_len()))]), + ])))?; + add(&Tui::fixed_xy(20, 3, col!(![ + row!(!["TimeRange ", Tui::bold(true, format!("{}-{}", self.0.time_start(), self.0.time_end()))]), + row!(!["TimeAxis ", Tui::bold(true, format!("{}", self.0.time_axis()))]), + row!(!["TimeZoom ", Tui::bold(true, format!("{} {}", self.0.time_zoom(), self.0.time_lock()))]), + ])))?; + add(&Tui::fixed_xy(20, 3, col!(![ + row!(!["NoteRange", Tui::bold(true, format!("{}-{}", self.0.note_lo(), self.0.note_hi()))]), + row!(!["NoteAxis ", Tui::bold(true, format!("{}", self.0.note_axis()))]), + "" + ])))?; + Ok(()) + })))) })); diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 891d5c7e..ff048a0b 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -48,36 +48,38 @@ render!(|self: PianoHorizontal|{ let time_point = self.point.time_point(); let note_point = self.point.note_point(); let note_len = self.point.note_len(); - Tui::bg(bg, Tui::split_s(false, 1, - Tui::debug(Tui::fill_x(Tui::push_x(5, Tui::bg(fg.darkest.rgb, Tui::fg(fg.lightest.rgb, - PianoHorizontalTimeline { - time_start, - time_zoom, - } - ))))), - Tui::fill_xy(Split::right(true, 5, Tui::debug(lay!([ - self.size, - PianoHorizontalNotes { - source: &self.buffer, - time_start, + lay!([ + &self.size, + Tui::fill_xy(Tui::bg(bg, Tui::split_s(false, 1, + Tui::fill_x(Tui::push_x(5, Tui::bg(fg.darkest.rgb, Tui::fg(fg.lightest.rgb, + PianoHorizontalTimeline { + time_start, + time_zoom, + } + )))), + Tui::split_e(true, 5, Tui::debug(lay!([ + PianoHorizontalNotes { + source: &self.buffer, + time_start, + note_hi, + }, + PianoHorizontalCursor { + time_zoom, + time_point, + time_start, + note_point: note_point, + note_len: note_len, + note_hi: note_hi, + note_lo: note_lo, + }, + ])), PianoHorizontalKeys { + color: self.color, + note_lo, note_hi, - }, - PianoHorizontalCursor { - time_zoom, - time_point, - time_start, - note_point: note_point, - note_len: note_len, - note_hi: note_hi, - note_lo: note_lo, - }, - ])), PianoHorizontalKeys { - color: self.color, - note_lo, - note_hi, - note_point: Some(note_point), - })) - )) + note_point: Some(note_point), + }), + ))) + ]) }); pub struct PianoHorizontalTimeline { @@ -252,7 +254,8 @@ impl MidiRange for PianoHorizontal { fn set_time_start (&self, x: usize) { self.range.set_time_start(x); } fn set_note_lo (&self, x: usize) { self.range.set_note_lo(x); } fn note_lo (&self) -> usize { self.range.note_lo() } - fn note_axis (&self) -> usize { self.range.note_lo() } + fn note_axis (&self) -> usize { self.range.note_axis() } + fn time_axis (&self) -> usize { self.range.time_axis() } fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } } impl MidiPoint for PianoHorizontal { diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs index cc8846ad..b3af3c5b 100644 --- a/crates/tek/src/tui/status_bar.rs +++ b/crates/tek/src/tui/status_bar.rs @@ -26,121 +26,3 @@ pub trait StatusBar: Render { Tui::to_north(state.into(), content) } } - -/// Status bar for sequencer app -#[derive(Clone)] -pub struct SequencerStatusBar { - pub(crate) width: usize, - pub(crate) cpu: Option, - pub(crate) size: String, - pub(crate) res: String, - pub(crate) mode: &'static str, - pub(crate) help: &'static [(&'static str, &'static str, &'static str)] -} - -impl StatusBar for SequencerStatusBar { - type State = SequencerTui; - fn hotkey_fg () -> Color { - TuiTheme::HOTKEY_FG - } - fn update (&mut self, _: &SequencerTui) { - todo!() - } -} - -impl From<&SequencerTui> for SequencerStatusBar { - fn from (state: &SequencerTui) -> Self { - let samples = state.clock.chunk.load(Ordering::Relaxed); - let rate = state.clock.timebase.sr.get() as f64; - let buffer = samples as f64 / rate; - let width = state.size.w(); - Self { - width, - cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), - size: format!("{}x{}│", width, state.size.h()), - res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), - mode: " SEQUENCER ", - help: &[ - ("", "SPACE", " play/pause"), - ("", "✣", " cursor"), - ("", "Ctrl-✣", " scroll"), - ("", ".,", " length"), - ("", "><", " triplet"), - ("", "[]", " phrase"), - ("", "{}", " order"), - ("en", "q", "ueue"), - ("", "e", "dit"), - ("", "a", "dd note"), - ("", "A", "dd phrase"), - ("", "D", "uplicate phrase"), - ] - } - } -} - -render!(|self: SequencerStatusBar|{ - lay!(|add|if self.width > 40 { - add(&Tui::fill_x(Tui::fixed_y(1, lay!([ - Tui::fill_x(Tui::at_w(SequencerMode::from(self))), - Tui::fill_x(Tui::at_e(SequencerStats::from(self))), - ])))) - } else { - add(&Tui::fill_x(col!(![ - Tui::fill_x(Tui::center_x(SequencerMode::from(self))), - Tui::fill_x(Tui::center_x(SequencerStats::from(self))), - ]))) - }) -}); - -struct SequencerMode { - mode: &'static str, - help: &'static [(&'static str, &'static str, &'static str)] -} -impl From<&SequencerStatusBar> for SequencerMode { - fn from (state: &SequencerStatusBar) -> Self { - Self { - mode: state.mode, - help: state.help, - } - } -} -render!(|self: SequencerMode|{ - let black = TuiTheme::g(0); - let light = TuiTheme::g(50); - let white = TuiTheme::g(255); - let orange = TuiTheme::orange(); - let yellow = TuiTheme::yellow(); - row!([ - Tui::bg(orange, Tui::fg(black, Tui::bold(true, self.mode))), - Tui::bg(light, Tui::fg(white, row!((prefix, hotkey, suffix) in self.help.iter() => { - row!([" ", prefix, Tui::fg(yellow, *hotkey), suffix]) - }))) - ]) -}); - -struct SequencerStats<'a> { - cpu: &'a Option, - size: &'a String, - res: &'a String, -} -impl<'a> From<&'a SequencerStatusBar> for SequencerStats<'a> { - fn from (state: &'a SequencerStatusBar) -> Self { - Self { - cpu: &state.cpu, - size: &state.size, - res: &state.res, - } - } -} -render!(|self:SequencerStats<'a>|{ - let orange = TuiTheme::orange(); - let dark = TuiTheme::g(25); - let cpu = &self.cpu; - let res = &self.res; - let size = &self.size; - Tui::bg(dark, row!([ - Tui::fg(orange, cpu), - Tui::fg(orange, res), - Tui::fg(orange, size), - ])) -}); From b799f6dbd02818059714c2e6fab82310da7fbe48 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 15 Dec 2024 02:09:49 +0100 Subject: [PATCH 045/971] autoselect --- crates/tek/src/tui/app_sequencer.rs | 16 +++++++++--- crates/tek/src/tui/phrase_editor.rs | 38 +++++++++++++++-------------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 16b75cd8..e4862f3a 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -74,7 +74,17 @@ impl Command for SequencerCommand { fn execute (self, state: &mut SequencerTui) -> Perhaps { Ok(match self { Self::Focus(cmd) => cmd.execute(state)?.map(Focus), - Self::Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), + Self::Phrases(cmd) => { + match cmd { + // autoselect: automatically load selected phrase in editor + PhrasesCommand::Select(_) => { + let undo = cmd.execute(&mut state.phrases)?.map(Phrases); + Editor(Show(Some(state.phrases.phrase().clone()))).execute(state)?; + undo + }, + _ => cmd.execute(&mut state.phrases)?.map(Phrases) + } + }, Self::Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), Self::Clock(cmd) => cmd.execute(state)?.map(Clock), Self::Enqueue(phrase) => { @@ -370,8 +380,8 @@ render!(|self: SequencerStatusBar|Tui::fixed_y(2, row!([ command ])]); let double = |(b1, c1), (b2, c2)|col!([ - row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1, " "]), - row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2, " "]), + row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,]), + row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,]), ]); Tui::bg(bg, Tui::fg(fg, row!([ single("SPACE", "play/pause"), diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 2263c001..bdc6c307 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -236,26 +236,28 @@ render!(|self:PhraseEditStatus<'a>|row!(|add|{ let bg = color.darker.rgb; let fg = color.lightest.rgb; add(&Tui::fill_x(Tui::bg(bg, row!(|add|{ - add(&Tui::fixed_xy(16, 3, col!(![ + add(&Tui::fixed_xy(26, 3, col!(![ row!(![" Edit ", Tui::bold(true, format!("{name}"))]), row!(![" Length ", Tui::bold(true, format!("{length}"))]), - row!(![" Loop ", Tui::bold(true, format!("on"))]), - ])))?; - add(&Tui::fixed_xy(12, 3, col!(![ - row!(!["Time ", Tui::bold(true, format!("{}", self.0.time_point()))]), - row!(!["Note ", Tui::bold(true, format!("{}", self.0.note_point()))]), - row!(!["Len ", Tui::bold(true, format!("{}", self.0.note_len()))]), - ])))?; - add(&Tui::fixed_xy(20, 3, col!(![ - row!(!["TimeRange ", Tui::bold(true, format!("{}-{}", self.0.time_start(), self.0.time_end()))]), - row!(!["TimeAxis ", Tui::bold(true, format!("{}", self.0.time_axis()))]), - row!(!["TimeZoom ", Tui::bold(true, format!("{} {}", self.0.time_zoom(), self.0.time_lock()))]), - ])))?; - add(&Tui::fixed_xy(20, 3, col!(![ - row!(!["NoteRange", Tui::bold(true, format!("{}-{}", self.0.note_lo(), self.0.note_hi()))]), - row!(!["NoteAxis ", Tui::bold(true, format!("{}", self.0.note_axis()))]), - "" - ])))?; + row!(![" Loop ", Tui::bold(true, format!("on"))])])))?; + add(&Tui::fixed_xy(25, 3, col!(![ + row!(!["Time ", Tui::bold(true, format!("{}", self.0.time_point()))]), + row!(!["View ", Tui::bold(true, format!("{}-{} ({}*{})", + self.0.time_start(), + self.0.time_end(), + self.0.time_axis(), + self.0.time_zoom()))])])))?; + add(&Tui::fixed_xy(25, 3, col!(![ + row!(!["Note ", Tui::bold(true, format!("{:4} ({:3}) {:4}", + to_note_name(self.0.note_point()), + self.0.note_point(), + self.0.note_len()))]), + row!(!["View ", Tui::bold(true, format!("{}-{} ({})", + to_note_name(self.0.note_lo()), + to_note_name(self.0.note_hi()), + self.0.note_axis()))])])))?; + add(&Tui::fixed_xy(16, 3, col!(![ + row!(!["TimeLock ", Tui::bold(true, format!("{}", self.0.time_lock()))])])))?; Ok(()) })))) })); From 2198d14a406a6ee6a478594c6e5d1460ad8a8bef Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 15 Dec 2024 16:00:46 +0100 Subject: [PATCH 046/971] fix autoscroll keys range --- crates/tek/src/api/note.rs | 6 +++--- crates/tek/src/tui/app_sequencer.rs | 2 +- crates/tek/src/tui/phrase_editor.rs | 5 +++-- crates/tek/src/tui/piano_horizontal.rs | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/tek/src/api/note.rs b/crates/tek/src/api/note.rs index 3429cb77..3d4e1e49 100644 --- a/crates/tek/src/api/note.rs +++ b/crates/tek/src/api/note.rs @@ -6,12 +6,12 @@ pub trait MidiViewport: MidiRange + MidiPoint + HasSize { fn autoscroll (&self) { let note_lo = self.note_lo(); let note_axis = self.note_axis(); - let note_hi = (note_lo + note_axis).min(127); + let note_hi = self.note_hi().saturating_sub(1); let note_point = self.note_point().min(127); if note_point < note_lo { self.set_note_lo(note_point); } else if note_point > note_hi { - self.set_note_lo(note_lo + note_point - note_hi); + self.set_note_lo((note_lo + note_point).saturating_sub(note_hi)); } } /// Make sure best usage of screen space is achieved by default @@ -57,7 +57,7 @@ pub trait MidiRange { fn set_note_lo (&self, x: usize); fn note_axis (&self) -> usize; fn time_axis (&self) -> usize; - fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } + fn note_hi (&self) -> usize { self.note_lo() + self.note_axis().saturating_sub(1) } fn time_end (&self) -> usize { self.time_start() + self.time_axis() * self.time_zoom() } } impl MidiRange for MidiRangeModel { diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index e4862f3a..cc13e803 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -188,7 +188,7 @@ render!(|self: SequencerTui|lay!([self.size, Tui::split_n(false, 5, PhraseEditStatus(&self.editor), SequencerStatusBar::from(self), ])), - Tui::split_e(false, if self.size.w() > 60 { + Tui::split_w(false, if self.size.w() > 60 { 20 } else if self.size.w() > 40 { 15 diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index bdc6c307..6d45b62f 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -143,8 +143,8 @@ impl MidiRange for PhraseEditorModel { fn note_lo (&self) -> usize { self.mode.note_lo() } fn note_axis (&self) -> usize { self.mode.note_axis() } fn time_axis (&self) -> usize { self.mode.time_axis() } - fn note_hi (&self) -> usize { self.note_lo() + self.note_axis() } } + impl MidiPoint for PhraseEditorModel { fn note_len (&self) -> usize { self.mode.note_len()} fn set_note_len (&self, x: usize) { self.mode.set_note_len(x) } @@ -153,6 +153,7 @@ impl MidiPoint for PhraseEditorModel { fn time_point (&self) -> usize { self.mode.time_point() } fn set_time_point (&self, x: usize) { self.mode.set_time_point(x) } } + impl PhraseViewMode for PhraseEditorModel { fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { self.mode.buffer_size(phrase) @@ -221,7 +222,7 @@ impl From>>> for PhraseEditorModel { impl std::fmt::Debug for PhraseEditorModel { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("PhraseEditorModel") - .field("point", &self) + .field("mode", &self.mode) .finish() } } diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index ff048a0b..748b7799 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -99,7 +99,7 @@ render!(|self: PianoHorizontalKeys|render(|to|Ok({ let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0))); let off_style = Some(Style::default().fg(TuiTheme::g(160))); let on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(self.color.base.rgb).bold()); - for (y, note) in (self.note_lo..=self.note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) { + for (y, note) in (self.note_lo..self.note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) { let key = match note % 12 { 11 => "████▌", 10 => " ", From 33259d1526141b3f02165989a4f270ff5d28a1c1 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 15 Dec 2024 16:34:25 +0100 Subject: [PATCH 047/971] remove SequencerFocus --- crates/tek/src/tui/app_sequencer.rs | 62 +---------------------------- 1 file changed, 1 insertion(+), 61 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index cc13e803..7316f348 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -1,7 +1,6 @@ use crate::{*, api::ClockCommand::{Play, Pause}}; use KeyCode::{Tab, BackTab, Char}; use SequencerCommand::*; -use SequencerFocus::*; use PhraseCommand::*; /// Create app state from JACK handle. @@ -28,7 +27,6 @@ impl TryFrom<&Arc>> for SequencerTui { midi_buf: vec![vec![];65536], note_buf: vec![], perf: PerfModel::default(), - focus: SequencerFocus::PhraseEditor }) } @@ -46,24 +44,11 @@ pub struct SequencerTui { pub split: u16, pub note_buf: Vec, pub midi_buf: Vec>>, - pub focus: SequencerFocus, pub perf: PerfModel, } -/// Sections in the sequencer app that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum SequencerFocus { - /// The transport (toolbar) is focused - Transport(TransportFocus), - /// The phrase list (pool) is focused - PhraseList, - /// The phrase editor (sequencer) is focused - PhraseEditor, -} - #[derive(Clone, Debug)] pub enum SequencerCommand { - Focus(FocusCommand), Clock(ClockCommand), Phrases(PhrasesCommand), Editor(PhraseCommand), @@ -73,7 +58,6 @@ pub enum SequencerCommand { impl Command for SequencerCommand { fn execute (self, state: &mut SequencerTui) -> Perhaps { Ok(match self { - Self::Focus(cmd) => cmd.execute(state)?.map(Focus), Self::Phrases(cmd) => { match cmd { // autoselect: automatically load selected phrase in editor @@ -95,25 +79,13 @@ impl Command for SequencerCommand { } } -impl Command for FocusCommand { - fn execute (self, state: &mut SequencerTui) -> Perhaps> { - // Focus commands can be received but are ignored. - //if let FocusCommand::Set(to) = self { - //state.set_focused(to); - //} - Ok(None) - } -} - impl InputToCommand for SequencerCommand { fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { to_sequencer_command(state, input) - .or_else(||to_focus_command(input).map(Focus)) } } pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option { - use super::app_transport::TransportCommand; Some(match input.event() { // Enqueue currently edited phrase @@ -202,11 +174,7 @@ render!(|self: SequencerTui|lay!([self.size, Tui::split_n(false, 5, Tui::fixed_y(2, TransportView::from(( self, self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone(), - if let SequencerFocus::Transport(_) = self.focus { - true - } else { - false - } + true ))), Tui::fill_xy(&self.editor) ]), @@ -280,15 +248,6 @@ impl PhraseSelector { } } -impl TransportControl for SequencerTui { - fn transport_focused (&self) -> Option { - match self.focus { - SequencerFocus::Transport(focus) => Some(focus), - _ => None - } - } -} - has_clock!(|self:SequencerTui|&self.clock); has_phrases!(|self:SequencerTui|self.phrases.phrases); has_editor!(|self:SequencerTui|self.editor); @@ -308,25 +267,6 @@ impl HasPhraseList for SequencerTui { } } -impl Into> for SequencerFocus { - fn into (self) -> Option { - if let Self::Transport(transport) = self { - Some(transport) - } else { - None - } - } -} - -impl From<&SequencerTui> for Option { - fn from (state: &SequencerTui) -> Self { - match state.focus { - Transport(focus) => Some(focus), - _ => None - } - } -} - impl Handle for SequencerTui { fn handle (&mut self, i: &TuiInput) -> Perhaps { SequencerCommand::execute_with_state(self, i) From f71ee5c5213cca4bd8fc3e73cac1cc2815a113ce Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 15 Dec 2024 16:39:49 +0100 Subject: [PATCH 048/971] tab toggles pool visibility in sequencer --- crates/tek/src/cli/cli_sequencer.rs | 6 ++-- crates/tek/src/tui/app_sequencer.rs | 54 +++++++++++++++++------------ 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/crates/tek/src/cli/cli_sequencer.rs b/crates/tek/src/cli/cli_sequencer.rs index ec18266e..ff86569d 100644 --- a/crates/tek/src/cli/cli_sequencer.rs +++ b/crates/tek/src/cli/cli_sequencer.rs @@ -20,12 +20,12 @@ pub struct SequencerCli { impl SequencerCli { fn run (&self) -> Usually<()> { Tui::run(JackClient::new("tek_sequencer")?.activate_with(|jack|{ - let mut app = SequencerTui::try_from(jack)?; + let midi_in = jack.read().unwrap().register_port("in", MidiIn::default())?; + let midi_out = jack.read().unwrap().register_port("out", MidiOut::default())?; + let mut app = SequencerTui::try_from(jack)?; //app.editor.view_mode.set_time_zoom(1); // TODO: create from arguments - let midi_in = app.jack.read().unwrap().register_port("in", MidiIn::default())?; app.player.midi_ins.push(midi_in); - let midi_out = app.jack.read().unwrap().register_port("out", MidiOut::default())?; app.player.midi_outs.push(midi_out); if let Some(_) = self.name.as_ref() { // TODO: sequencer.name = Arc::new(RwLock::new(name.clone())); diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 7316f348..e9db5496 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -16,17 +16,16 @@ impl TryFrom<&Arc>> for SequencerTui { Some(ItemColor::random().into()) ))); Ok(Self { - jack: jack.clone(), - phrases: PhraseListModel::from(&phrase), - editor: PhraseEditorModel::from(&phrase), - player: PhrasePlayerModel::from((&clock, &phrase)), + _jack: jack.clone(), + phrases: PhraseListModel::from(&phrase), + editor: PhraseEditorModel::from(&phrase), + player: PhrasePlayerModel::from((&clock, &phrase)), clock, - size: Measure::new(), - cursor: (0, 0), - split: 20, - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), + size: Measure::new(), + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + show_pool: true, }) } @@ -34,17 +33,16 @@ impl TryFrom<&Arc>> for SequencerTui { /// Root view for standalone `tek_sequencer`. pub struct SequencerTui { - pub jack: Arc>, - pub clock: ClockModel, - pub phrases: PhraseListModel, - pub player: PhrasePlayerModel, - pub editor: PhraseEditorModel, - pub size: Measure, - pub cursor: (usize, usize), - pub split: u16, - pub note_buf: Vec, - pub midi_buf: Vec>>, - pub perf: PerfModel, + _jack: Arc>, + pub(crate) clock: ClockModel, + pub(crate) phrases: PhraseListModel, + pub(crate) player: PhrasePlayerModel, + pub(crate) editor: PhraseEditorModel, + pub(crate) size: Measure, + pub(crate) show_pool: bool, + pub(crate) note_buf: Vec, + pub(crate) midi_buf: Vec>>, + pub(crate) perf: PerfModel, } #[derive(Clone, Debug)] @@ -53,6 +51,7 @@ pub enum SequencerCommand { Phrases(PhrasesCommand), Editor(PhraseCommand), Enqueue(Option>>), + ShowPool(bool), } impl Command for SequencerCommand { @@ -75,6 +74,10 @@ impl Command for SequencerCommand { state.player.enqueue_next(phrase.as_ref()); None }, + Self::ShowPool(value) => { + state.show_pool = value; + None + } }) } } @@ -88,6 +91,9 @@ impl InputToCommand for SequencerCommand { pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option { Some(match input.event() { + // Toggle visibility of phrase pool column + key_pat!(Tab) => ShowPool(!state.show_pool), + // Enqueue currently edited phrase key_pat!(Char('q')) => Enqueue(Some( state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() @@ -137,7 +143,7 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option 60 { + Tui::split_w(false, if !self.show_pool{ + 0 + } else if self.size.w() > 60 { 20 } else if self.size.w() > 40 { 15 From f5dcd3cba11881a27a6e832f2308b12d5e909f6d Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 15 Dec 2024 16:43:18 +0100 Subject: [PATCH 049/971] remove to_sequencer_command --- crates/tek/src/tui/app_sequencer.rs | 115 ++++++++++++---------------- 1 file changed, 48 insertions(+), 67 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index e9db5496..e37ab0b2 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -84,63 +84,51 @@ impl Command for SequencerCommand { impl InputToCommand for SequencerCommand { fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { - to_sequencer_command(state, input) - } -} - -pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option { - Some(match input.event() { - - // Toggle visibility of phrase pool column - key_pat!(Tab) => ShowPool(!state.show_pool), - - // Enqueue currently edited phrase - key_pat!(Char('q')) => Enqueue(Some( - state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() - )), - - // 0: Enqueue phrase 0 (stop all) - key_pat!(Char('0')) => Enqueue(Some( - state.phrases.phrases[0].clone() - )), - - // E: Toggle between editing currently playing or other phrase - key_pat!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() { - let editing_phrase = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone()); - let selected_phrase = state.phrases.phrase().clone(); - if Some(selected_phrase.read().unwrap().clone()) != editing_phrase { - Editor(Show(Some(selected_phrase))) + Some(match input.event() { + // Toggle visibility of phrase pool column + key_pat!(Tab) => ShowPool(!state.show_pool), + // Enqueue currently edited phrase + key_pat!(Char('q')) => Enqueue(Some( + state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() + )), + // 0: Enqueue phrase 0 (stop all) + key_pat!(Char('0')) => Enqueue(Some( + state.phrases.phrases[0].clone() + )), + // E: Toggle between editing currently playing or other phrase + key_pat!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() { + let editing_phrase = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone()); + let selected_phrase = state.phrases.phrase().clone(); + if Some(selected_phrase.read().unwrap().clone()) != editing_phrase { + Editor(Show(Some(selected_phrase))) + } else { + Editor(Show(Some(playing_phrase.clone()))) + } } else { - Editor(Show(Some(playing_phrase.clone()))) + return None + }, + // Transport: Play/pause + key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { + Play(None) + } else { + Pause(None) + }), + // Transport: Play from start or rewind to start + key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { + Play(Some(0)) + } else { + Pause(Some(0)) + }), + // Delegate to components: + _ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { + Editor(command) + } else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) { + Phrases(command) + } else { + return None } - } else { - return None - }, - - // Transport: Play/pause - key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { - Play(None) - } else { - Pause(None) - }), - - // Transport: Play from start or rewind to start - key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { - Play(Some(0)) - } else { - Pause(Some(0)) - }), - - // Delegate to components: - _ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { - Editor(command) - } else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) { - Phrases(command) - } else { - return None - } - - }) + }) + } } audio!(|self:SequencerTui, client, scope|{ @@ -321,8 +309,6 @@ render!(|self: SequencerStatusBar|Tui::fixed_y(2, row!([ Tui::fixed_xy(11, 2, PlayPause(self.playing)), lay!([ { - let bg = TuiTheme::g(50); - let fg = TuiTheme::g(255); let single = |binding, command|row!([" ", col!([ Tui::fg(TuiTheme::yellow(), binding), command @@ -331,7 +317,7 @@ render!(|self: SequencerStatusBar|Tui::fixed_y(2, row!([ row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,]), row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,]), ]); - Tui::bg(bg, Tui::fg(fg, row!([ + Tui::bg(TuiTheme::g(50), Tui::fg(TuiTheme::g(255), row!([ single("SPACE", "play/pause"), double((" ✣", "cursor"), ("C-✣", "scroll"), ), double((",.", "note"), ("<>", "triplet"),), @@ -340,15 +326,10 @@ render!(|self: SequencerStatusBar|Tui::fixed_y(2, row!([ ]))) }, Tui::fill_xy(Tui::at_se({ - let orange = TuiTheme::orange(); - let dark = TuiTheme::g(25); - //let cpu = &self.cpu; - //let res = &self.res; - let size = &self.size; - Tui::bg(dark, row!([ - //Tui::fg(orange, cpu), - //Tui::fg(orange, res), - Tui::fg(orange, size), + Tui::bg(TuiTheme::g(25), row!([ + Tui::fg(TuiTheme::orange(), &self.cpu), + Tui::fg(TuiTheme::orange(), &self.res), + Tui::fg(TuiTheme::orange(), &self.size), ])) })), ]) From 9dd1d62de3ff602c148b90f3c8be2df514a905fd Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 15 Dec 2024 19:08:17 +0100 Subject: [PATCH 050/971] refactor app_sequencer --- crates/tek/src/tui/app_sequencer.rs | 98 +++++++++++--------------- crates/tek/src/tui/app_transport.rs | 22 ++---- crates/tek/src/tui/phrase_editor.rs | 2 +- crates/tek/src/tui/phrase_list.rs | 4 +- crates/tek/src/tui/piano_horizontal.rs | 2 +- 5 files changed, 54 insertions(+), 74 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index e37ab0b2..49530e8c 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -149,33 +149,24 @@ audio!(|self:SequencerTui, client, scope|{ Control::Continue }); -render!(|self: SequencerTui|lay!([self.size, Tui::split_n(false, 5, - Tui::fill_xy(col!([ - PhraseEditStatus(&self.editor), - SequencerStatusBar::from(self), - ])), - Tui::split_w(false, if !self.show_pool{ - 0 - } else if self.size.w() > 60 { - 20 - } else if self.size.w() > 40 { - 15 - } else { - 10 - }, Tui::fixed_x(20, col!([ - PhraseSelector::play_phrase(&self.player), - PhraseSelector::next_phrase(&self.player), - PhraseListView::from(self), - ])), col!([ - Tui::fixed_y(2, TransportView::from(( - self, - self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone(), - true - ))), - Tui::fill_xy(&self.editor) - ]), - ) -)])); +render!(|self: SequencerTui|{ + let w = self.size.w(); + let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + let pool_w = if self.show_pool { phrase_w } else { 0 }; + let pool = Tui::fill_y(Tui::at_e(PhraseListView::from(self))); + let with_pool = move|x|Tui::split_w(false, pool_w, pool, x); + let with_status = |x|Tui::split_n(false, 2, SequencerStatusBar::from(self), x); + let with_bar = |x|Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x); + let with_size = |x|lay!([self.size, x]); + let editor = with_bar(with_pool(Tui::fill_xy(&self.editor))); + let color = self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone(); + let play = Tui::fixed_xy(11, 2, PlayPause(self.clock.is_rolling())); + let playing = Tui::fixed_xy(14, 2, PhraseSelector::play_phrase(&self.player)); + let next = Tui::fixed_xy(14, 2, PhraseSelector::next_phrase(&self.player)); + let transport = Tui::fixed_y(2, TransportView::from((self, color, true))); + let toolbar = row!([play, playing, next, transport]); + with_size(with_status(col!([ toolbar, editor, ]))) +}); pub struct PhraseSelector { pub(crate) title: &'static str, @@ -305,32 +296,29 @@ impl From<&SequencerTui> for SequencerStatusBar { } } -render!(|self: SequencerStatusBar|Tui::fixed_y(2, row!([ - Tui::fixed_xy(11, 2, PlayPause(self.playing)), - lay!([ - { - let single = |binding, command|row!([" ", col!([ - Tui::fg(TuiTheme::yellow(), binding), - command - ])]); - let double = |(b1, c1), (b2, c2)|col!([ - row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,]), - row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,]), - ]); - Tui::bg(TuiTheme::g(50), Tui::fg(TuiTheme::g(255), row!([ - single("SPACE", "play/pause"), - double((" ✣", "cursor"), ("C-✣", "scroll"), ), - double((",.", "note"), ("<>", "triplet"),), - double(("[]", "phrase"), ("{}", "order"), ), - double(("q", "enqueue"), ("e", "edit"), ), - ]))) - }, - Tui::fill_xy(Tui::at_se({ - Tui::bg(TuiTheme::g(25), row!([ - Tui::fg(TuiTheme::orange(), &self.cpu), - Tui::fg(TuiTheme::orange(), &self.res), - Tui::fg(TuiTheme::orange(), &self.size), - ])) - })), - ]) +render!(|self: SequencerStatusBar|Tui::fixed_y(2, lay!([ + { + let single = |binding, command|row!([" ", col!([ + Tui::fg(TuiTheme::yellow(), binding), + command + ])]); + let double = |(b1, c1), (b2, c2)|col!([ + row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,]), + row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,]), + ]); + Tui::bg(TuiTheme::g(50), Tui::fg(TuiTheme::g(255), row!([ + single("SPACE", "play/pause"), + double((" ✣", "cursor"), ("C-✣", "scroll"), ), + double((",.", "note"), ("<>", "triplet"),), + double(("[]", "phrase"), ("{}", "order"), ), + double(("q", "enqueue"), ("e", "edit"), ), + ]))) + }, + Tui::fill_xy(Tui::at_se({ + Tui::bg(TuiTheme::g(25), row!([ + Tui::fg(TuiTheme::orange(), &self.cpu), + Tui::fg(TuiTheme::orange(), &self.res), + Tui::fg(TuiTheme::orange(), &self.size), + ])) + })), ]))); diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index 7c4b70a5..d6da53e9 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -62,21 +62,17 @@ pub struct TransportView { impl From<(&T, Option, bool)> for TransportView { fn from ((state, color, focused): (&T, Option, bool)) -> Self { let clock = state.clock(); - let sr = format!("{:.1}k", clock.timebase.sr.get() / 1000.0); - let bpm = format!("{:.3}", clock.timebase.bpm.get()); - let ppq = format!("{:.0}", clock.timebase.ppq.get()); - let color = color.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(32)))); - let bg = if focused { color.light.rgb } else { color.dark.rgb }; + let sr = format!("{:.1}k", clock.timebase.sr.get() / 1000.0); + let bpm = format!("{:.3}", clock.timebase.bpm.get()); + let ppq = format!("{:.0}", clock.timebase.ppq.get()); + let color = color.unwrap_or(ItemPalette::from(TuiTheme::g(32))); + let bg = color.dark.rgb; 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.; Self { - bg, - focused, - sr, - bpm, - ppq, + bg, focused, sr, bpm, ppq, started: true, global_sample: format!("{:.0}k", started.sample.get()/1000.), global_second: format!("{:.1}s", started.usec.get()/1000.), @@ -88,11 +84,7 @@ impl From<(&T, Option, bool)> for TransportView { } } else { Self { - bg, - focused, - sr, - bpm, - ppq, + bg, focused, sr, bpm, ppq, started: false, global_sample: format!("{:.0}k", clock.global.sample.get()/1000.), global_second: format!("{:.1}s", clock.global.usec.get()/1000000.), diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 6d45b62f..f254541a 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -234,7 +234,7 @@ render!(|self:PhraseEditStatus<'a>|row!(|add|{ } else { (ItemPalette::from(TuiTheme::g(64)), String::new(), 0) }; - let bg = color.darker.rgb; + let bg = color.base.rgb; let fg = color.lightest.rgb; add(&Tui::fill_x(Tui::bg(bg, row!(|add|{ add(&Tui::fixed_xy(26, 3, col!(![ diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 8bb886ee..f6e7fe16 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -271,7 +271,7 @@ render!(|self: PhraseListView<'a>|{ }))?; }) }))))?; - 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()))))) + add(&Tui::fill_x(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))))?; + add(&Tui::fill_x(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string()))))) })) }); diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 748b7799..49d66a2d 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -98,7 +98,7 @@ render!(|self: PianoHorizontalKeys|render(|to|Ok({ let [x, y0, _, _] = to.area().xywh(); let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0))); let off_style = Some(Style::default().fg(TuiTheme::g(160))); - let on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(self.color.base.rgb).bold()); + let on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(self.color.light.rgb).bold()); for (y, note) in (self.note_lo..self.note_hi).rev().enumerate().map(|(y, n)|(y0 + y as u16, n)) { let key = match note % 12 { 11 => "████▌", From dcd6bc24a7918a18857031e03add494e496273f5 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 15 Dec 2024 20:07:52 +0100 Subject: [PATCH 051/971] simplify PhraseListView and arranger layout --- crates/tek/src/tui/app_arranger.rs | 63 +++++++++++++---------------- crates/tek/src/tui/app_sequencer.rs | 18 ++++----- crates/tek/src/tui/phrase_list.rs | 59 ++++++++++----------------- 3 files changed, 56 insertions(+), 84 deletions(-) diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index e0feca43..ff689c59 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -196,9 +196,7 @@ fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option Cmd::Editor(PhraseCommand::Show(Some( - state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() - ))), + key_pat!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))), // WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor _ => match state.focused() { ArrangerFocus::Transport(_) => { @@ -327,46 +325,39 @@ has_clock!(|self:ArrangerTrack|self.player.clock()); has_phrases!(|self:ArrangerTui|self.phrases.phrases); has_editor!(|self:ArrangerTui|self.editor); has_player!(|self:ArrangerTrack|self.player); -// Layout for standalone arranger app. render!(|self: ArrangerTui|{ let arranger_focused = self.arranger_focused(); - let border = Lozenge(Style::default().bg(TuiTheme::border_bg()).fg(TuiTheme::border_fg(arranger_focused))); let transport_focused = if let ArrangerFocus::Transport(_) = self.focus.inner() { true } else { false }; - col!([ - TransportView::from((self, None, transport_focused)), - col!([ - Tui::fixed_y(self.splits[0], lay!([ - border.wrap(Tui::grow_y(1, Layers::new(move |add|{ - match self.mode { - ArrangerMode::Horizontal => - add(&arranger_content_horizontal(self))?, - ArrangerMode::Vertical(factor) => - add(&arranger_content_vertical(self, factor))? - }; - add(&self.size) - }))), - self.size, - Tui::push_x(1, Tui::fg( - TuiTheme::title_fg(arranger_focused), - format!("[{}] Arranger", if self.entered { - "■" - } else { - " " - }) - )) - ])), - Split::right( - false, - self.splits[1], - PhraseListView::from(self), - &self.editor, - ) - ]) - ]) + let transport = TransportView::from((self, None, transport_focused)); + let with_transport = move|x|col!([transport, x]); + let border = Lozenge(Style::default() + .bg(TuiTheme::border_bg()) + .fg(TuiTheme::border_fg(arranger_focused))); + let arranger = move||border.wrap(Tui::grow_y(1, lay!(|add|{ + match self.mode { + ArrangerMode::Horizontal => add(&arranger_content_horizontal(self))?, + ArrangerMode::Vertical(factor) => add(&arranger_content_vertical(self, factor))? + }; + add(&self.size) + }))); + with_transport(col!([ + Tui::fixed_y(self.splits[0], lay!([ + arranger(), + Tui::push_x(1, Tui::fg( + TuiTheme::title_fg(arranger_focused), + format!("[{}] Arranger", if self.entered { + "■" + } else { + " " + }) + )) + ])), + Split::right(false, self.splits[1], PhraseListView(&self.phrases), &self.editor), + ])) }); audio!(|self: ArrangerTui, client, scope|{ // Start profiling cycle diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 49530e8c..f0625a0a 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -88,13 +88,9 @@ impl InputToCommand for SequencerCommand { // Toggle visibility of phrase pool column key_pat!(Tab) => ShowPool(!state.show_pool), // Enqueue currently edited phrase - key_pat!(Char('q')) => Enqueue(Some( - state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() - )), + key_pat!(Char('q')) => Enqueue(Some(state.phrases.phrase().clone())), // 0: Enqueue phrase 0 (stop all) - key_pat!(Char('0')) => Enqueue(Some( - state.phrases.phrases[0].clone() - )), + key_pat!(Char('0')) => Enqueue(Some(state.phrases.phrases()[0].clone())), // E: Toggle between editing currently playing or other phrase key_pat!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() { let editing_phrase = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone()); @@ -153,7 +149,7 @@ render!(|self: SequencerTui|{ let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; let pool_w = if self.show_pool { phrase_w } else { 0 }; - let pool = Tui::fill_y(Tui::at_e(PhraseListView::from(self))); + let pool = Tui::fill_y(Tui::at_e(PhraseListView(&self.phrases))); let with_pool = move|x|Tui::split_w(false, pool_w, pool, x); let with_status = |x|Tui::split_n(false, 2, SequencerStatusBar::from(self), x); let with_bar = |x|Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x); @@ -168,6 +164,10 @@ render!(|self: SequencerTui|{ with_size(with_status(col!([ toolbar, editor, ]))) }); +has_clock!(|self:SequencerTui|&self.clock); +has_phrases!(|self:SequencerTui|self.phrases.phrases); +has_editor!(|self:SequencerTui|self.editor); + pub struct PhraseSelector { pub(crate) title: &'static str, pub(crate) name: String, @@ -235,10 +235,6 @@ impl PhraseSelector { } } -has_clock!(|self:SequencerTui|&self.clock); -has_phrases!(|self:SequencerTui|self.phrases.phrases); -has_editor!(|self:SequencerTui|self.editor); - impl HasPhraseList for SequencerTui { fn phrases_focused (&self) -> bool { true diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index f6e7fe16..2ee0a514 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -5,17 +5,20 @@ use crate::{ tui::file_browser::FileBrowserCommand as Browse, api::PhrasePoolCommand as Pool, }; +use Ordering::Relaxed; #[derive(Debug)] pub struct PhraseListModel { /// Collection of phrases pub(crate) phrases: Vec>>, /// Selected phrase - pub(crate) phrase: AtomicUsize, - /// Scroll offset - pub scroll: usize, + pub(crate) phrase: AtomicUsize, /// Mode switch - pub(crate) mode: Option, + pub(crate) mode: Option, + /// Rendered size + size: Measure, + /// Scroll offset + scroll: usize, } /// Modes for phrase pool @@ -167,6 +170,7 @@ impl Default for PhraseListModel { phrase: 0.into(), scroll: 0, mode: None, + size: Measure::new(), } } } @@ -175,7 +179,7 @@ impl From<&Arc>> for PhraseListModel { fn from (phrase: &Arc>) -> Self { let mut model = Self::default(); model.phrases.push(phrase.clone()); - model.phrase.store(1, Ordering::Relaxed); + model.phrase.store(1, Relaxed); model } } @@ -185,10 +189,10 @@ has_phrase!(|self:PhraseListModel|self.phrases[self.phrase_index()]); impl PhraseListModel { pub(crate) fn phrase_index (&self) -> usize { - self.phrase.load(Ordering::Relaxed) + self.phrase.load(Relaxed) } pub(crate) fn set_phrase_index (&self, value: usize) { - self.phrase.store(value, Ordering::Relaxed); + self.phrase.store(value, Relaxed); } pub(crate) fn phrases_mode (&self) -> &Option { &self.mode @@ -205,35 +209,15 @@ pub trait HasPhraseList: HasPhrases { fn phrase_index (&self) -> usize; } -pub struct PhraseListView<'a> { - pub(crate) title: &'static str, - pub(crate) focused: bool, - pub(crate) entered: bool, - pub(crate) phrases: &'a Vec>>, - pub(crate) index: usize, - pub(crate) mode: &'a Option -} - -impl<'a, T: HasPhraseList> From<&'a T> for PhraseListView<'a> { - fn from (state: &'a T) -> Self { - Self { - title: "Pool:", - focused: state.phrases_focused(), - entered: state.phrases_entered(), - phrases: state.phrases(), - index: state.phrase_index(), - mode: state.phrases_mode(), - } - } -} +pub struct PhraseListView<'a>(pub(crate) &'a PhraseListModel); // TODO: Display phrases always in order of appearance render!(|self: PhraseListView<'a>|{ - let Self { title, focused, entered, phrases, index, mode } = self; - let bg = TuiTheme::g(32); - let title_color = TuiTheme::ti1(); - let upper_left = format!("{title}"); - let upper_right = format!("({})", phrases.len()); + let PhraseListModel { phrases, mode, .. } = self.0; + let bg = TuiTheme::g(32); + let title_color = TuiTheme::ti1(); + let upper_left = "Pool:"; + 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 { @@ -244,7 +228,7 @@ render!(|self: PhraseListView<'a>|{ let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); let mut length = PhraseLength::new(length, None); if let Some(PhraseListMode::Length(phrase, new_length, focus)) = mode { - if *focused && i == *phrase { + if i == *phrase { length.pulses = *new_length; length.focus = Some(*focus); } @@ -257,14 +241,14 @@ render!(|self: PhraseListView<'a>|{ Tui::bold(true, { let mut row2 = format!(" {name}"); if let Some(PhraseListMode::Rename(phrase, _)) = mode { - if *focused && i == *phrase { + if i == *phrase { row2 = format!("{row2}▄"); } }; row2 }), ]))))?; - if *entered && i == *index { + if i == self.0.phrase_index() { add(&CORNERS)?; } Ok(()) @@ -272,6 +256,7 @@ render!(|self: PhraseListView<'a>|{ }) }))))?; add(&Tui::fill_x(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))))?; - add(&Tui::fill_x(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string()))))) + add(&Tui::fill_x(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))))?; + add(&self.0.size) })) }); From d401870b2d918d2edf9ce26a2960b030a7025ede Mon Sep 17 00:00:00 2001 From: unspeaker Date: Mon, 16 Dec 2024 04:18:40 +0100 Subject: [PATCH 052/971] refactor bsp, rebalance color, BIG PLAY BUTTON --- crates/tek/src/api/note.rs | 2 +- crates/tek/src/core/color.rs | 6 +- crates/tek/src/layout/bsp.rs | 219 +++++++++++++++++----------- crates/tek/src/tui.rs | 16 +- crates/tek/src/tui/app_sequencer.rs | 47 +++--- crates/tek/src/tui/app_transport.rs | 57 ++++---- crates/tek/src/tui/phrase_editor.rs | 10 +- crates/tek/src/tui/status_bar.rs | 7 +- crates/tek/src/tui/tui_style.rs | 3 + 9 files changed, 215 insertions(+), 152 deletions(-) diff --git a/crates/tek/src/api/note.rs b/crates/tek/src/api/note.rs index 3d4e1e49..6267a8d2 100644 --- a/crates/tek/src/api/note.rs +++ b/crates/tek/src/api/note.rs @@ -86,7 +86,7 @@ impl Default for MidiPointModel { fn default () -> Self { Self { time_point: Arc::new(0.into()), - note_point: Arc::new(0.into()), + note_point: Arc::new(36.into()), note_len: Arc::new(24.into()), } } diff --git a/crates/tek/src/core/color.rs b/crates/tek/src/core/color.rs index 99b9a48c..48d10fec 100644 --- a/crates/tek/src/core/color.rs +++ b/crates/tek/src/core/color.rs @@ -61,11 +61,11 @@ impl From for ItemPalette { impl From for ItemPalette { fn from (base: ItemColor) -> Self { let mut light = base.okhsl.clone(); - light.lightness = (light.lightness * 1.33).min(Okhsl::::max_lightness()); + light.lightness = (light.lightness * 4. / 3.).min(Okhsl::::max_lightness()); let mut lighter = light.clone(); - lighter.lightness = (lighter.lightness * 1.33).min(Okhsl::::max_lightness()); + lighter.lightness = (lighter.lightness * 5. / 3.).min(Okhsl::::max_lightness()); let mut lightest = lighter.clone(); - lightest.lightness = (lightest.lightness * 1.33).min(Okhsl::::max_lightness()); + lightest.lightness = (lightest.lightness * 4. / 3.).min(Okhsl::::max_lightness()); let mut dark = base.okhsl.clone(); dark.lightness = (dark.lightness * 0.75).max(Okhsl::::min_lightness()); diff --git a/crates/tek/src/layout/bsp.rs b/crates/tek/src/layout/bsp.rs index a0c3c07c..1cdead54 100644 --- a/crates/tek/src/layout/bsp.rs +++ b/crates/tek/src/layout/bsp.rs @@ -1,105 +1,150 @@ use crate::*; -impl LayoutBspStatic for E {} +pub enum Bsp, Y: Render> { + /// X is north of Y + N(Option, Option), + /// X is south of Y + S(Option, Option), + /// X is east of Y + E(Option, Option), + /// X is west of Y + W(Option, Option), + /// X is above Y + A(Option, Option), + /// X is below Y + B(Option, Option), + /// Should be avoided. + Null(PhantomData), +} -pub trait LayoutBspStatic: { - fn over , B: Render> (a: A, b: B) -> Over { - Over(Default::default(), a, b) - } - fn under , B: Render> (a: A, b: B) -> Under { - Under(Default::default(), a, b) - } - fn to_north , B: Render> (a: A, b: B) -> ToNorth { - ToNorth(None, a, b) - } - fn to_south , B: Render> (a: A, b: B) -> ToSouth { - ToSouth(None, a, b) - } - fn to_east , B: Render> (a: A, b: B) -> ToEast { - ToEast(None, a, b) - } - fn to_west , B: Render> (a: A, b: B) -> ToWest { - ToWest(None, a, b) +render!(|self:Bsp, Y: Render>|match self { + Bsp::Null(_) => { () }, + Bsp::N(a, b) => { todo!("") }, + Bsp::S(a, b) => { todo!("") }, + Bsp::E(a, b) => { todo!("") }, + Bsp::W(a, b) => { todo!("") }, + Bsp::A(a, b) => { todo!("") }, + Bsp::B(a, b) => { todo!("") }, +}); + +impl, Y: Render> Bsp { + pub fn new (x: X) -> Self { Self::A(Some(x), None) } + pub fn n (x: X, y: Y) -> Self { Self::N(Some(x), Some(y)) } + pub fn s (x: X, y: Y) -> Self { Self::S(Some(x), Some(y)) } + pub fn e (x: X, y: Y) -> Self { Self::E(Some(x), Some(y)) } + pub fn w (x: X, y: Y) -> Self { Self::W(Some(x), Some(y)) } + pub fn a (x: X, y: Y) -> Self { Self::A(Some(x), Some(y)) } + pub fn b (x: X, y: Y) -> Self { Self::B(Some(x), Some(y)) } +} + +impl, Y: Render> Default for Bsp { + fn default () -> Self { + Self::Null(Default::default()) } } -pub trait LayoutBspFixedStatic: { - fn to_north , B: Render> (n: E::Unit, a: A, b: B) -> ToNorth { - ToNorth(Some(n), a, b) - } - fn to_south , B: Render> (n: E::Unit, a: A, b: B) -> ToSouth { - ToSouth(Some(n), a, b) - } - fn to_east , B: Render> (n: E::Unit, a: A, b: B) -> ToEast { - ToEast(Some(n), a, b) - } - fn to_west , B: Render> (n: E::Unit, a: A, b: B) -> ToWest { - ToWest(Some(n), a, b) - } -} +/////////////////////////////////////////////////////////////////////////////////////////////////// -pub struct Over, B: Render>(PhantomData, A, B); +//impl LayoutBspStatic for E {} -pub struct Under, B: Render>(PhantomData, A, B); +//pub trait LayoutBspStatic: { + //fn n , B: Render> (a: A, b: B) -> ToNorth { + //ToNorth(None, a, b) + //} + //fn s , B: Render> (a: A, b: B) -> ToSouth { + //ToSouth(None, a, b) + //} + //fn e , B: Render> (a: A, b: B) -> ToEast { + //ToEast(None, a, b) + //} + //fn w , B: Render> (a: A, b: B) -> ToWest { + //ToWest(None, a, b) + //} + //fn i , B: Render> (a: A, b: B) -> Over { + //Over(Default::default(), a, b) + //} + //fn o , B: Render> (a: A, b: B) -> Under { + //Under(Default::default(), a, b) + //} +//} -pub struct ToNorth, B: Render>(Option, A, B); +//pub trait LayoutBspFixedStatic: { + //fn to_north , B: Render> (n: E::Unit, a: A, b: B) -> ToNorth { + //ToNorth(Some(n), a, b) + //} + //fn to_south , B: Render> (n: E::Unit, a: A, b: B) -> ToSouth { + //ToSouth(Some(n), a, b) + //} + //fn to_east , B: Render> (n: E::Unit, a: A, b: B) -> ToEast { + //ToEast(Some(n), a, b) + //} + //fn to_west , B: Render> (n: E::Unit, a: A, b: B) -> ToWest { + //ToWest(Some(n), a, b) + //} +//} -pub struct ToSouth, B: Render>(Option, A, B); +//pub struct Over, B: Render>(PhantomData, A, B); -pub struct ToEast(Option, A, B); +//pub struct Under, B: Render>(PhantomData, A, B); -pub struct ToWest, B: Render>(Option, A, B); +//pub struct ToNorth, B: Render>(Option, A, B); -impl, B: Render> Render for Over { - fn min_size (&self, _: E::Size) -> Perhaps { - todo!(); - } - fn render (&self, _: &mut E::Output) -> Usually<()> { - Ok(()) - } -} +//pub struct ToSouth, B: Render>(Option, A, B); -impl, B: Render> Render for Under { - fn min_size (&self, _: E::Size) -> Perhaps { - todo!(); - } - fn render (&self, _: &mut E::Output) -> Usually<()> { - Ok(()) - } -} +//pub struct ToEast(Option, A, B); -impl, B: Render> Render for ToNorth { - fn min_size (&self, _: E::Size) -> Perhaps { - todo!(); - } - fn render (&self, _: &mut E::Output) -> Usually<()> { - Ok(()) - } -} +//pub struct ToWest, B: Render>(Option, A, B); -impl, B: Render> Render for ToSouth { - fn min_size (&self, _: E::Size) -> Perhaps { - todo!(); - } - fn render (&self, _: &mut E::Output) -> Usually<()> { - Ok(()) - } -} +//impl, B: Render> Render for Over { + //fn min_size (&self, _: E::Size) -> Perhaps { + //todo!(); + //} + //fn render (&self, _: &mut E::Output) -> Usually<()> { + //Ok(()) + //} +//} -impl, B: Render> Render for ToWest { - fn min_size (&self, _: E::Size) -> Perhaps { - todo!(); - } - fn render (&self, _: &mut E::Output) -> Usually<()> { - Ok(()) - } -} +//impl, B: Render> Render for Under { + //fn min_size (&self, _: E::Size) -> Perhaps { + //todo!(); + //} + //fn render (&self, _: &mut E::Output) -> Usually<()> { + //Ok(()) + //} +//} -impl, B: Render> Render for ToEast { - fn min_size (&self, _: E::Size) -> Perhaps { - todo!(); - } - fn render (&self, _: &mut E::Output) -> Usually<()> { - Ok(()) - } -} +//impl, B: Render> Render for ToNorth { + //fn min_size (&self, _: E::Size) -> Perhaps { + //todo!(); + //} + //fn render (&self, _: &mut E::Output) -> Usually<()> { + //Ok(()) + //} +//} + +//impl, B: Render> Render for ToSouth { + //fn min_size (&self, _: E::Size) -> Perhaps { + //todo!(); + //} + //fn render (&self, _: &mut E::Output) -> Usually<()> { + //Ok(()) + //} +//} + +//impl, B: Render> Render for ToWest { + //fn min_size (&self, _: E::Size) -> Perhaps { + //todo!(); + //} + //fn render (&self, _: &mut E::Output) -> Usually<()> { + //Ok(()) + //} +//} + +//impl, B: Render> Render for ToEast { + //fn min_size (&self, _: E::Size) -> Perhaps { + //todo!(); + //} + //fn render (&self, _: &mut E::Output) -> Usually<()> { + //Ok(()) + //} +//} diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 0fdb9d50..e5628203 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -173,14 +173,16 @@ impl Tui { } } -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()), - )) -}); +//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, diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index f0625a0a..7703d467 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -85,6 +85,10 @@ impl Command for SequencerCommand { impl InputToCommand for SequencerCommand { fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { Some(match input.event() { + key_pat!(Char('u')) => { todo!("undo") }, + key_pat!(Char('U')) => { todo!("redo") }, + key_pat!(Ctrl-Char('k')) => { todo!("keyboard") }, + // Toggle visibility of phrase pool column key_pat!(Tab) => ShowPool(!state.show_pool), // Enqueue currently edited phrase @@ -156,14 +160,16 @@ render!(|self: SequencerTui|{ let with_size = |x|lay!([self.size, x]); let editor = with_bar(with_pool(Tui::fill_xy(&self.editor))); let color = self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone(); - let play = Tui::fixed_xy(11, 2, PlayPause(self.clock.is_rolling())); - let playing = Tui::fixed_xy(14, 2, PhraseSelector::play_phrase(&self.player)); - let next = Tui::fixed_xy(14, 2, PhraseSelector::next_phrase(&self.player)); + let play = Tui::fixed_xy(5, 2, PlayPause(self.clock.is_rolling())); let transport = Tui::fixed_y(2, TransportView::from((self, color, true))); - let toolbar = row!([play, playing, next, transport]); + let toolbar = row!([play, col!([ + PhraseSelector::play_phrase(&self.player), + PhraseSelector::next_phrase(&self.player), + ]), transport]); with_size(with_status(col!([ toolbar, editor, ]))) }); +has_size!(|self:SequencerTui|&self.size); has_clock!(|self:SequencerTui|&self.clock); has_phrases!(|self:SequencerTui|self.phrases.phrases); has_editor!(|self:SequencerTui|self.editor); @@ -176,16 +182,12 @@ pub struct PhraseSelector { } // TODO: Display phrases always in order of appearance -render!(|self: PhraseSelector|Tui::fixed_y(2, col!([ - lay!(move|add|{ - add(&Tui::push_x(1, Tui::fg(TuiTheme::g(240), self.title)))?; - add(&Tui::bg(self.color.base.rgb, Tui::fill_x(Tui::inset_x(1, Tui::fill_x(Tui::at_e( - Tui::fg(self.color.lightest.rgb, &self.time)))))))?; - Ok(()) - }), - Tui::bg(self.color.base.rgb, - Tui::fg(self.color.lightest.rgb, - Tui::bold(true, self.name.clone()))), +render!(|self: PhraseSelector|Tui::fixed_xy(24, 1, row!([ + Tui::fg(self.color.lighter.rgb, Tui::bold(true, &self.title)), + Tui::bg(self.color.base.rgb, Tui::fg(self.color.lighter.rgb, row!([ + format!("{:8}", &self.name[0..8.min(self.name.len())]), + Tui::bg(self.color.dark.rgb, &self.time), + ]))), ]))); impl PhraseSelector { @@ -200,9 +202,9 @@ impl PhraseSelector { let time = if let Some(elapsed) = state.pulses_since_start_looped() { format!("+{:>}", state.clock().timebase.format_beats_0(elapsed)) } else { - String::from("") + String::from(" ") }; - Self { title: "Now:", time, name, color, } + Self { title: " Now|", time, name, color, } } // beats until switchover pub fn next_phrase (state: &T) -> Self { @@ -219,17 +221,24 @@ impl PhraseSelector { } }; (time, name.clone(), color) + } else if let Some((_, Some(phrase))) = state.play_phrase() { + let phrase = phrase.read().unwrap(); + if phrase.loop_on { + (" ".into(), phrase.name.clone(), phrase.color.clone()) + } else { + (" ".into(), " ".into(), TuiTheme::g(64).into()) + } } else { - ("".into(), "".into(), TuiTheme::g(64).into()) + (" ".into(), " ".into(), TuiTheme::g(64).into()) }; - Self { title: "Next:", time, name, color, } + Self { title: " Next|", time, name, color, } } pub fn edit_phrase (phrase: &Option>>) -> Self { let (time, name, color) = if let Some(phrase) = phrase { let phrase = phrase.read().unwrap(); (format!("{}", phrase.length), phrase.name.clone(), phrase.color) } else { - ("".to_string(), "".to_string(), ItemPalette::from(TuiTheme::g(64))) + ("".to_string(), " ".to_string(), ItemPalette::from(TuiTheme::g(64))) }; Self { title: "Editing:", time, name, color } } diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index d6da53e9..af4660d7 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -42,18 +42,18 @@ audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope)); render!(|self: TransportTui|TransportView::from((self, None, true))); pub struct TransportView { - bg: Color, - focused: bool, + color: ItemPalette, + focused: bool, - sr: String, - bpm: String, - ppq: String, - beat: String, + sr: String, + bpm: String, + ppq: String, + beat: String, - global_sample: String, - global_second: String, + global_sample: String, + global_second: String, - started: bool, + started: bool, current_sample: f64, current_second: f64, @@ -66,13 +66,12 @@ impl From<(&T, Option, bool)> for TransportView { let bpm = format!("{:.3}", clock.timebase.bpm.get()); let ppq = format!("{:.0}", clock.timebase.ppq.get()); let color = color.unwrap_or(ItemPalette::from(TuiTheme::g(32))); - let bg = color.dark.rgb; 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.; Self { - bg, focused, sr, bpm, ppq, + color, focused, sr, bpm, ppq, started: true, global_sample: format!("{:.0}k", started.sample.get()/1000.), global_second: format!("{:.1}s", started.usec.get()/1000.), @@ -84,7 +83,7 @@ impl From<(&T, Option, bool)> for TransportView { } } else { Self { - bg, focused, sr, bpm, ppq, + color, focused, sr, bpm, ppq, started: false, global_sample: format!("{:.0}k", clock.global.sample.get()/1000.), global_second: format!("{:.1}s", clock.global.usec.get()/1000000.), @@ -99,23 +98,25 @@ impl From<(&T, Option, bool)> for TransportView { render!(|self: TransportView|{ - struct Field<'a>(&'a str, &'a str); + let color = self.color; + + struct Field<'a>(&'a str, &'a str, &'a ItemPalette); 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)), + Tui::bg(Color::Reset, Tui::bold(true, + Tui::fg(self.2.lighter.rgb, self.0))), + Tui::fg(self.2.lighter.rgb, format!("{:>10}", self.1)), ])); - Tui::bg(self.bg, Tui::fill_x(row!([ + Tui::bg(color.base.rgb, Tui::fill_x(row!([ //PlayPause(self.started), " ", col!([ - Field(" Beat", self.beat.as_str()), - Field(" BPM ", self.bpm.as_str()), + Field(" Beat|", self.beat.as_str(), &color), + Field(" BPM|", self.bpm.as_str(), &color), ]), " ", col!([ - Field("Time ", format!("{:.1}s", self.current_second).as_str()), - Field("Sample", format!("{:.0}k", self.current_sample).as_str()), + Field(" Time|", format!("{:.1}s", self.current_second).as_str(), &color), + Field(" Smpl|", format!("{:.1}k", self.current_sample).as_str(), &color), ]), ]))) @@ -124,11 +125,17 @@ render!(|self: TransportView|{ pub struct PlayPause(pub 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(&Tui::fg(Color::Rgb(0, 255, 0), col!(["▶ PLAYING", "▒ ▒ ▒ ▒ ▒"]))) + Tui::fixed_x(5, col!(|add|if self.0 { + add(&Tui::fg(Color::Rgb(0, 255, 0), col!([ + " 🭍🭑🬽 ", + " 🭞🭜🭘 " + ]))) } else { - add(&Tui::fg(Color::Rgb(255, 128, 0), col!(["▒ ▒ ▒ ▒ ▒", "⏹ STOPPED"]))) - }))) + add(&Tui::fg(Color::Rgb(255, 128, 0), col!([ + " ▗▄▖ ", + " ▝▀▘ " + ]))) + })) )); impl HasFocus for TransportTui { diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index f254541a..17f146b4 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -107,15 +107,19 @@ impl Command for PhraseCommand { pub struct PhraseEditorModel { /// Renders the phrase pub mode: Box, + + pub size: Measure } impl Default for PhraseEditorModel { fn default () -> Self { - Self { mode: Box::new(PianoHorizontal::new(None)) } + Self { mode: Box::new(PianoHorizontal::new(None)), size: Measure::new() } } } -render!(|self: PhraseEditorModel|self.mode); +has_size!(|self:PhraseEditorModel|&self.size); +render!(|self: PhraseEditorModel|&self.mode); +//render!(|self: PhraseEditorModel|lay!(|add|{add(&self.size)?;add(self.mode)}));//bollocks pub trait PhraseViewMode: Render + HasSize + MidiRange + MidiPoint + Debug + Send + Sync { fn buffer_size (&self, phrase: &Phrase) -> (usize, usize); @@ -130,8 +134,6 @@ pub trait PhraseViewMode: Render + HasSize + MidiRange + MidiPoint + D impl MidiViewport for PhraseEditorModel {} -has_size!(|self:PhraseEditorModel|self.mode.size()); - impl MidiRange for PhraseEditorModel { fn time_zoom (&self) -> usize { self.mode.time_zoom() } fn set_time_zoom (&self, x: usize) { self.mode.set_time_zoom(x); } diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs index b3af3c5b..926abf72 100644 --- a/crates/tek/src/tui/status_bar.rs +++ b/crates/tek/src/tui/status_bar.rs @@ -13,16 +13,11 @@ pub trait StatusBar: Render { row!([a, b, c] in commands.iter() => { row!([a, Tui::fg(hotkey_fg, Tui::bold(true, b)), c]) }) - //Tui::reduce(commands.iter(), |prev, [a, b, c]| - //Tui::to_east(prev, - //Tui::to_east(a, - //Tui::to_east(Tui::fg(hotkey_fg, Tui::bold(true, b)), - //c)))) } fn with <'a> (state: &'a Self::State, content: impl Render) -> impl Render where Self: Sized, &'a Self::State: Into { - Tui::to_north(state.into(), content) + Bsp::n(state.into(), content) } } diff --git a/crates/tek/src/tui/tui_style.rs b/crates/tek/src/tui/tui_style.rs index 8c7eb0ca..28c6075a 100644 --- a/crates/tek/src/tui/tui_style.rs +++ b/crates/tek/src/tui/tui_style.rs @@ -7,6 +7,9 @@ impl Tui { pub(crate) fn bg > (color: Color, w: W) -> Background { Background(color, w) } + pub(crate) fn fg_bg > (fg: Color, bg: Color, w: W) -> Background> { + Background(bg, Foreground(fg, w)) + } pub(crate) fn bold > (on: bool, w: W) -> Bold { Bold(on, w) } From e57415aac97a004c3ecfd625466cf38d0bbb87f7 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Mon, 16 Dec 2024 18:10:26 +0100 Subject: [PATCH 053/971] wip: structure PianoHorizontal render sanely --- crates/tek/src/tui/app_sequencer.rs | 16 +++--- crates/tek/src/tui/app_transport.rs | 26 +++++----- crates/tek/src/tui/phrase_editor.rs | 37 ++++++++----- crates/tek/src/tui/piano_horizontal.rs | 72 +++++++++++++------------- 4 files changed, 81 insertions(+), 70 deletions(-) diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 7703d467..7d29202e 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -184,10 +184,10 @@ pub struct PhraseSelector { // TODO: Display phrases always in order of appearance render!(|self: PhraseSelector|Tui::fixed_xy(24, 1, row!([ Tui::fg(self.color.lighter.rgb, Tui::bold(true, &self.title)), - Tui::bg(self.color.base.rgb, Tui::fg(self.color.lighter.rgb, row!([ + Tui::fg_bg(self.color.lighter.rgb, self.color.base.rgb, row!([ format!("{:8}", &self.name[0..8.min(self.name.len())]), Tui::bg(self.color.dark.rgb, &self.time), - ]))), + ])), ]))); impl PhraseSelector { @@ -311,19 +311,19 @@ render!(|self: SequencerStatusBar|Tui::fixed_y(2, lay!([ row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,]), row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,]), ]); - Tui::bg(TuiTheme::g(50), Tui::fg(TuiTheme::g(255), row!([ + Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!([ single("SPACE", "play/pause"), double((" ✣", "cursor"), ("C-✣", "scroll"), ), double((",.", "note"), ("<>", "triplet"),), double(("[]", "phrase"), ("{}", "order"), ), double(("q", "enqueue"), ("e", "edit"), ), - ]))) + ])) }, Tui::fill_xy(Tui::at_se({ - Tui::bg(TuiTheme::g(25), row!([ - Tui::fg(TuiTheme::orange(), &self.cpu), - Tui::fg(TuiTheme::orange(), &self.res), - Tui::fg(TuiTheme::orange(), &self.size), + Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), row!([ + &self.cpu, + &self.res, + &self.size, ])) })), ]))); diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index af4660d7..fe8a4316 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -102,21 +102,21 @@ render!(|self: TransportView|{ struct Field<'a>(&'a str, &'a str, &'a ItemPalette); render!(|self: Field<'a>|row!([ - Tui::bg(Color::Reset, Tui::bold(true, - Tui::fg(self.2.lighter.rgb, self.0))), - Tui::fg(self.2.lighter.rgb, format!("{:>10}", self.1)), + Tui::fg_bg(self.2.lightest.rgb, self.2.darkest.rgb, Tui::bold(true, self.0)), + Tui::fg_bg(self.2.lighter.rgb, self.2.darkest.rgb, "│"), + Tui::fg_bg(self.2.lighter.rgb, self.2.base.rgb, format!("{:>10}", self.1)), ])); Tui::bg(color.base.rgb, Tui::fill_x(row!([ //PlayPause(self.started), " ", col!([ - Field(" Beat|", self.beat.as_str(), &color), - Field(" BPM|", self.bpm.as_str(), &color), + Field(" Beat", self.beat.as_str(), &color), + Field(" BPM", self.bpm.as_str(), &color), ]), " ", col!([ - Field(" Time|", format!("{:.1}s", self.current_second).as_str(), &color), - Field(" Smpl|", format!("{:.1}k", self.current_sample).as_str(), &color), + Field(" Time", format!("{:.1}s", self.current_second).as_str(), &color), + Field(" Smpl", format!("{:.1}k", self.current_sample).as_str(), &color), ]), ]))) @@ -128,12 +128,12 @@ render!(|self: PlayPause|Tui::bg( Tui::fixed_x(5, col!(|add|if self.0 { add(&Tui::fg(Color::Rgb(0, 255, 0), col!([ " 🭍🭑🬽 ", - " 🭞🭜🭘 " + " 🭞🭜🭘 ", ]))) } else { add(&Tui::fg(Color::Rgb(255, 128, 0), col!([ " ▗▄▖ ", - " ▝▀▘ " + " ▝▀▘ ", ]))) })) )); @@ -168,8 +168,8 @@ impl FocusWrap for TransportFocus { fn wrap <'a, W: Render> (self, focus: TransportFocus, content: &'a W) -> impl Render + 'a { - let focused = focus == self; - let corners = focused.then_some(CORNERS); + 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]) } @@ -179,8 +179,8 @@ impl FocusWrap for Option { fn wrap <'a, W: Render> (self, focus: TransportFocus, content: &'a W) -> impl Render + 'a { - let focused = Some(focus) == self; - let corners = focused.then_some(CORNERS); + 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]) } diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 17f146b4..cbd11c60 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -231,34 +231,43 @@ impl std::fmt::Debug for PhraseEditorModel { pub struct PhraseEditStatus<'a>(pub &'a PhraseEditorModel); render!(|self:PhraseEditStatus<'a>|row!(|add|{ - let (color, name, length) = if let Some(phrase) = self.0.phrase().as_ref().map(|p|p.read().unwrap()) { - (phrase.color, phrase.name.clone(), phrase.length) + let (color, name, length, looped) = if let Some(phrase) = self.0.phrase().as_ref().map(|p|p.read().unwrap()) { + (phrase.color, phrase.name.clone(), phrase.length, phrase.loop_on) } else { - (ItemPalette::from(TuiTheme::g(64)), String::new(), 0) + (ItemPalette::from(TuiTheme::g(64)), String::new(), 0, false) }; let bg = color.base.rgb; let fg = color.lightest.rgb; - add(&Tui::fill_x(Tui::bg(bg, row!(|add|{ + let field = move|x, y|row!([ + Tui::fg_bg(color.lightest.rgb, Color::Reset, Tui::bold(true, x)), + Tui::fg_bg(color.lighter.rgb, Color::Reset, Tui::bold(true, "│")), + &y + ]); + add(&Tui::fill_x(Tui::fg_bg(fg, bg, row!(|add|{ add(&Tui::fixed_xy(26, 3, col!(![ - row!(![" Edit ", Tui::bold(true, format!("{name}"))]), - row!(![" Length ", Tui::bold(true, format!("{length}"))]), - row!(![" Loop ", Tui::bold(true, format!("on"))])])))?; + field(" Edit", format!("{name}")), + field(" Length", format!("{length}")), + field(" Loop", format!("{looped}")), + ])))?; add(&Tui::fixed_xy(25, 3, col!(![ - row!(!["Time ", Tui::bold(true, format!("{}", self.0.time_point()))]), - row!(!["View ", Tui::bold(true, format!("{}-{} ({}*{})", + field(" Time", format!("{}", + self.0.time_point())), + field(" View", format!("{}-{} ({}*{})", self.0.time_start(), self.0.time_end(), self.0.time_axis(), - self.0.time_zoom()))])])))?; + self.0.time_zoom())) + ])))?; add(&Tui::fixed_xy(25, 3, col!(![ - row!(!["Note ", Tui::bold(true, format!("{:4} ({:3}) {:4}", + field(" Note", format!("{:4} ({:3}) {:4}", to_note_name(self.0.note_point()), self.0.note_point(), - self.0.note_len()))]), - row!(!["View ", Tui::bold(true, format!("{}-{} ({})", + self.0.note_len())), + field(" View", format!("{}-{} ({})", to_note_name(self.0.note_lo()), to_note_name(self.0.note_hi()), - self.0.note_axis()))])])))?; + self.0.note_axis())) + ])))?; add(&Tui::fixed_xy(16, 3, col!(![ row!(!["TimeLock ", Tui::bold(true, format!("{}", self.0.time_lock()))])])))?; Ok(()) diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs index 49d66a2d..78fdb539 100644 --- a/crates/tek/src/tui/piano_horizontal.rs +++ b/crates/tek/src/tui/piano_horizontal.rs @@ -6,7 +6,7 @@ pub struct PianoHorizontal { phrase: Option>>, /// Buffer where the whole phrase is rerendered on change buffer: BigBuffer, - /// Width and height of notes area at last render + /// Size of actual notes area size: Measure, /// The display window range: MidiRangeModel, @@ -39,7 +39,7 @@ impl PianoHorizontal { render!(|self: PianoHorizontal|{ let bg = TuiTheme::g(32); - let fg = self.color; + let color = self.color; let note_lo = self.range.note_lo(); let note_hi = self.range.note_hi(); let time_lock = self.range.time_lock(); @@ -48,45 +48,47 @@ render!(|self: PianoHorizontal|{ let time_point = self.point.time_point(); let note_point = self.point.note_point(); let note_len = self.point.note_len(); - lay!([ - &self.size, - Tui::fill_xy(Tui::bg(bg, Tui::split_s(false, 1, - Tui::fill_x(Tui::push_x(5, Tui::bg(fg.darkest.rgb, Tui::fg(fg.lightest.rgb, - PianoHorizontalTimeline { - time_start, - time_zoom, - } - )))), - Tui::split_e(true, 5, Tui::debug(lay!([ - PianoHorizontalNotes { - source: &self.buffer, - time_start, - note_hi, - }, - PianoHorizontalCursor { - time_zoom, - time_point, - time_start, - note_point: note_point, - note_len: note_len, - note_hi: note_hi, - note_lo: note_lo, - }, - ])), PianoHorizontalKeys { - color: self.color, - note_lo, - note_hi, - note_point: Some(note_point), - }), - ))) - ]) + let timeline = move||PianoHorizontalTimeline { + color, + time_start, + time_zoom, + }; + let notes = move||PianoHorizontalNotes { + source: &self.buffer, + time_start, + note_hi, + }; + let cursor = move||PianoHorizontalCursor { + time_zoom, + time_point, + time_start, + note_point: note_point, + note_len: note_len, + note_hi: note_hi, + note_lo: note_lo, + }; + let keys = move||PianoHorizontalKeys { + color: self.color, + note_lo, + note_hi, + note_point: Some(note_point), + }; + Tui::fill_xy(Tui::bg(bg, Tui::split_s(false, 1, + Tui::fill_x(Tui::push_x(5, timeline())), + Tui::split_e(true, 5, Tui::debug(lay!([&self.size, notes(), cursor()])), keys()), + ))) }); pub struct PianoHorizontalTimeline { + color: ItemPalette, time_start: usize, time_zoom: usize, } -render!(|self: PianoHorizontalTimeline|format!("{}*{}", self.time_start, self.time_zoom).as_str()); +render!(|self: PianoHorizontalTimeline|Tui::fg_bg( + self.color.lightest.rgb, + self.color.darkest.rgb, + format!("{}*{}", self.time_start, self.time_zoom).as_str() +)); pub struct PianoHorizontalKeys { color: ItemPalette, From 6cc81acd708d7b57b89f9332b63479381b47b984 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Mon, 16 Dec 2024 19:12:50 +0100 Subject: [PATCH 054/971] extract tui_border.rs --- crates/tek/src/tui.rs | 1 + crates/tek/src/tui/tui_border.rs | 211 +++++++++++++++++++++++++++++++ crates/tek/src/tui/tui_style.rs | 205 ------------------------------ 3 files changed, 212 insertions(+), 205 deletions(-) create mode 100644 crates/tek/src/tui/tui_border.rs diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index e5628203..2f9b5f1c 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -4,6 +4,7 @@ mod tui_input; pub(crate) use tui_input::*; mod tui_style; pub(crate) use tui_style::*; mod tui_theme; pub(crate) use tui_theme::*; mod tui_output; pub(crate) use tui_output::*; +mod tui_border; pub(crate) use tui_border::*; //////////////////////////////////////////////////////// diff --git a/crates/tek/src/tui/tui_border.rs b/crates/tek/src/tui/tui_border.rs new file mode 100644 index 00000000..76e615d9 --- /dev/null +++ b/crates/tek/src/tui/tui_border.rs @@ -0,0 +1,211 @@ +use crate::*; + +pub struct Bordered>(pub S, pub W); + +render!(|self: Bordered>|{ + Tui::fill_xy(lay!([Border(self.0), Tui::inset_xy(1, 1, widget(&self.1))])) +}); + +pub struct Border(pub S); + +impl Render for Border { + fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { + Ok(Some([0, 0])) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + let area = to.area(); + if area.w() > 0 && area.y() > 0 { + to.blit(&self.0.nw(), area.x(), area.y(), self.0.style()); + to.blit(&self.0.ne(), area.x() + area.w() - 1, area.y(), self.0.style()); + to.blit(&self.0.sw(), area.x(), area.y() + area.h() - 1, self.0.style()); + to.blit(&self.0.se(), area.x() + area.w() - 1, area.y() + area.h() - 1, self.0.style()); + for x in area.x()+1..area.x()+area.w()-1 { + to.blit(&self.0.n(), x, area.y(), self.0.style()); + to.blit(&self.0.s(), x, area.y() + area.h() - 1, self.0.style()); + } + for y in area.y()+1..area.y()+area.h()-1 { + to.blit(&self.0.w(), area.x(), y, self.0.style()); + to.blit(&self.0.e(), area.x() + area.w() - 1, y, self.0.style()); + } + } + Ok(()) + } +} + +pub trait BorderStyle: Send + Sync + Copy { + fn wrap > (self, w: W) -> Bordered { + Bordered(self, w) + } + const NW: &'static str = ""; + const N: &'static str = ""; + const NE: &'static str = ""; + const E: &'static str = ""; + const SE: &'static str = ""; + const S: &'static str = ""; + const SW: &'static str = ""; + const W: &'static str = ""; + fn n (&self) -> &str { Self::N } + fn s (&self) -> &str { Self::S } + fn e (&self) -> &str { Self::E } + fn w (&self) -> &str { Self::W } + fn nw (&self) -> &str { Self::NW } + fn ne (&self) -> &str { Self::NE } + fn sw (&self) -> &str { Self::SW } + fn se (&self) -> &str { Self::SE } + #[inline] fn draw <'a> ( + &self, to: &mut TuiOutput + ) -> Usually<()> { + self.draw_horizontal(to, None)?; + self.draw_vertical(to, None)?; + self.draw_corners(to, None)?; + Ok(()) + } + #[inline] fn draw_horizontal ( + &self, to: &mut TuiOutput, style: Option