From 416acd9f7b6826db0462ac02028d6fde3ab7f881 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Mon, 25 Nov 2024 17:57:20 +0100 Subject: [PATCH] separate tui model and view layers --- crates/tek_tui/src/lib.rs | 20 +- crates/tek_tui/src/tui_content.rs | 370 ----------------- crates/tek_tui/src/tui_impls.rs | 87 ---- crates/tek_tui/src/tui_model.rs | 378 ------------------ crates/tek_tui/src/tui_model_arranger.rs | 114 ++++++ crates/tek_tui/src/tui_model_clock.rs | 33 ++ crates/tek_tui/src/tui_model_file_browser.rs | 62 +++ crates/tek_tui/src/tui_model_phrase_editor.rs | 54 +++ crates/tek_tui/src/tui_model_phrase_length.rs | 37 ++ crates/tek_tui/src/tui_model_phrase_list.rs | 37 ++ crates/tek_tui/src/tui_model_phrase_player.rs | 48 +++ .../src/{tui_view.rs => tui_view_arranger.rs} | 286 +++---------- crates/tek_tui/src/tui_view_file_browser.rs | 24 ++ crates/tek_tui/src/tui_view_phrase_editor.rs | 365 +++++++++++++++++ crates/tek_tui/src/tui_view_phrase_length.rs | 35 ++ crates/tek_tui/src/tui_view_phrase_list.rs | 116 ++++++ crates/tek_tui/src/tui_view_sequencer.rs | 20 + crates/tek_tui/src/tui_view_transport.rs | 102 +++++ crates/tek_tui/src/tui_widget.rs | 31 -- 19 files changed, 1124 insertions(+), 1095 deletions(-) delete mode 100644 crates/tek_tui/src/tui_content.rs create mode 100644 crates/tek_tui/src/tui_model_arranger.rs create mode 100644 crates/tek_tui/src/tui_model_clock.rs create mode 100644 crates/tek_tui/src/tui_model_file_browser.rs create mode 100644 crates/tek_tui/src/tui_model_phrase_editor.rs create mode 100644 crates/tek_tui/src/tui_model_phrase_length.rs create mode 100644 crates/tek_tui/src/tui_model_phrase_list.rs create mode 100644 crates/tek_tui/src/tui_model_phrase_player.rs rename crates/tek_tui/src/{tui_view.rs => tui_view_arranger.rs} (69%) create mode 100644 crates/tek_tui/src/tui_view_file_browser.rs create mode 100644 crates/tek_tui/src/tui_view_phrase_editor.rs create mode 100644 crates/tek_tui/src/tui_view_phrase_length.rs create mode 100644 crates/tek_tui/src/tui_view_phrase_list.rs create mode 100644 crates/tek_tui/src/tui_view_sequencer.rs create mode 100644 crates/tek_tui/src/tui_view_transport.rs delete mode 100644 crates/tek_tui/src/tui_widget.rs diff --git a/crates/tek_tui/src/lib.rs b/crates/tek_tui/src/lib.rs index 230b907a..72937bf6 100644 --- a/crates/tek_tui/src/lib.rs +++ b/crates/tek_tui/src/lib.rs @@ -14,7 +14,6 @@ use std::fmt::Debug; submod! { tui_apps tui_command - tui_content tui_control tui_debug tui_focus @@ -24,10 +23,23 @@ submod! { tui_impls tui_jack tui_menu - tui_model tui_select tui_status tui_theme - tui_view - tui_widget + + tui_model_arranger + tui_model_clock + tui_model_file_browser + tui_model_phrase_editor + tui_model_phrase_length + tui_model_phrase_list + tui_model_phrase_player + + tui_view_arranger + tui_view_file_browser + tui_view_phrase_editor + tui_view_phrase_length + tui_view_phrase_list + tui_view_sequencer + tui_view_transport } diff --git a/crates/tek_tui/src/tui_content.rs b/crates/tek_tui/src/tui_content.rs deleted file mode 100644 index efacae38..00000000 --- a/crates/tek_tui/src/tui_content.rs +++ /dev/null @@ -1,370 +0,0 @@ -use crate::*; - -impl Content for TransportView { - type Engine = Tui; - fn content (&self) -> impl Widget { - let Self { state, selected, focused, bpm, sync, quant, beat, msu, } = self; - row!( - selected.wrap(TransportFocus::PlayPause, &Styled( - None, - match *state { - Some(TransportState::Rolling) => "▶ PLAYING", - Some(TransportState::Starting) => "READY ...", - Some(TransportState::Stopped) => "⏹ STOPPED", - _ => "???", - } - ).min_xy(11, 2).push_x(1)), - selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, { - row! { - "BPM ", - format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0) - } - })), - selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! { - "QUANT ", pulses_to_name(*quant as usize) - })), - selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! { - "SYNC ", pulses_to_name(*sync as usize) - })), - selected.wrap(TransportFocus::Clock, &{ - row!("B" , beat.as_str(), " T", msu.as_str()).outset_x(1) - }).align_e().fill_x(), - ).fill_x().bg(Color::Rgb(40, 50, 30)) - } -} - -impl Content for SequencerTui { - type Engine = Tui; - fn content (&self) -> impl Widget { - lay!( - col!( - TransportView::from(self), - Split::right(20, - widget(&PhrasesView(self)), - widget(&PhraseView(self)), - ).min_y(20) - ), - self.perf.percentage() - .map(|cpu|format!("{cpu:.03}%")) - .fg(Color::Rgb(255,128,0)) - .align_sw(), - ) - } -} - -/// Display mode of arranger -#[derive(Clone, PartialEq)] -pub enum ArrangerMode { - /// Tracks are rows - Horizontal, - /// Tracks are columns - Vertical(usize), -} - -/// Arranger display mode can be cycled -impl ArrangerMode { - /// Cycle arranger display mode - pub fn to_next (&mut self) { - *self = match self { - Self::Horizontal => Self::Vertical(1), - Self::Vertical(1) => Self::Vertical(2), - Self::Vertical(2) => Self::Vertical(2), - Self::Vertical(0) => Self::Horizontal, - Self::Vertical(_) => Self::Vertical(0), - } - } -} - -/// Layout for standalone arranger app. -impl Content for ArrangerTui { - type Engine = Tui; - fn content (&self) -> impl Widget { - let arranger_focused = self.arranger_focused(); - Split::down( - 1, - TransportView::from(self), - Split::down( - self.splits[0], - lay!( - 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) - }) - .grow_y(1) - .border(Lozenge(Style::default() - .bg(TuiTheme::border_bg()) - .fg(TuiTheme::border_fg(arranger_focused)))), - widget(&self.size), - widget(&format!("[{}] Arranger", if self.entered { - "■" - } else { - " " - })) - .fg(TuiTheme::title_fg(arranger_focused)) - .push_x(1), - ), - Split::right( - self.splits[1], - PhrasesView(self), - PhraseView(self), - ) - ) - ) - } -} - -// TODO: Display phrases always in order of appearance -impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> { - type Engine = Tui; - fn content (&self) -> impl Widget { - let focused = self.0.phrases_focused(); - let entered = self.0.phrases_entered(); - let mode = self.0.phrases_mode(); - let content = Stack::down(move|add|match mode { - Some(PhrasesMode::Import(_, ref browser)) => { - add(browser) - }, - Some(PhrasesMode::Export(_, ref browser)) => { - add(browser) - }, - _ => { - let phrases = self.0.phrases(); - let selected = self.0.phrase_index(); - for (i, phrase) in phrases.iter().enumerate() { - add(&Layers::new(|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 = length.align_e().fill_x(); - let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x(); - let mut row2 = format!(" {name}"); - if let Some(PhrasesMode::Rename(phrase, _)) = mode { - if focused && i == *phrase { - row2 = format!("{row2}▄"); - } - }; - let row2 = TuiStyle::bold(row2, true); - add(&col!(row1, row2).fill_x().bg(color.base.rgb))?; - if focused && i == selected { - add(&CORNERS)?; - } - Ok(()) - }))?; - } - Ok(()) - } - }); - let border_color = if focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)}; - let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); - let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border); - let title_color = if focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)}; - let upper_left = format!("[{}] Phrases", if entered {"■"} else {" "}); - let upper_right = format!("({})", self.0.phrases().len()); - lay!( - content, - TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), - TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), - ) - } -} - -impl Content for FileBrowser { - type Engine = Tui; - fn content (&self) -> impl Widget { - Stack::down(|add|{ - let mut i = 0; - for (_, name) in self.dirs.iter() { - if i >= self.scroll { - add(&TuiStyle::bold(name.as_str(), i == self.index))?; - } - i += 1; - } - for (_, name) in self.files.iter() { - if i >= self.scroll { - add(&TuiStyle::bold(name.as_str(), i == self.index))?; - } - i += 1; - } - add(&format!("{}/{i}", self.index))?; - Ok(()) - }) - } -} - -impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> { - type Engine = Tui; - fn content (&self) -> impl Widget { - let phrase = self.0.phrase_editing(); - let size = self.0.size(); - let focused = self.0.phrase_editor_focused(); - let entered = self.0.phrase_editor_entered(); - let keys = self.0.keys(); - let buffer = self.0.buffer(); - let note_len = self.0.note_len(); - let note_axis = self.0.note_axis(); - let time_axis = self.0.time_axis(); - let FixedAxis { start: note_start, point: note_point, clamp: note_clamp } - = *note_axis.read().unwrap(); - let ScaledAxis { start: time_start, point: time_point, clamp: time_clamp, scale: time_scale } - = *time_axis.read().unwrap(); - //let color = Color::Rgb(0,255,0); - //let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color); - let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{ - Ok(if to.area().h() >= 2 { - to.buffer_update(to.area().set_w(5), &|cell, x, y|{ - let y = y + (note_start / 2) as u16; - if x < keys.area.width && y < keys.area.height { - *cell = keys.get(x, y).clone() - } - }); - }) - }).fill_y(); - let notes_bg_null = Color::Rgb(28, 35, 25); - let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ - let area = to.area(); - let h = area.h() as usize; - size.set_wh(area.w(), h); - let mut axis = note_axis.write().unwrap(); - if let Some(point) = axis.point { - if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) { - axis.start += 2; - } - } - Ok(if to.area().h() >= 2 { - let area = to.area(); - to.buffer_update(area, &move |cell, x, y|{ - cell.set_bg(notes_bg_null); - let src_x = (x as usize + time_start) * time_scale; - let src_y = y as usize + note_start / 2; - if src_x < buffer.width && src_y < buffer.height - 1 { - buffer.get(src_x, buffer.height - src_y - 2).map(|src|{ - cell.set_symbol(src.symbol()); - cell.set_fg(src.fg); - cell.set_bg(src.bg); - }); - } - }); - }) - }).fill_x(); - let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ - Ok(if focused && entered { - let area = to.area(); - if let (Some(time), Some(note)) = (time_point, note_point) { - let x1 = area.x() + (time / time_scale) as u16; - let x2 = x1 + (note_len / time_scale) as u16; - let y = area.y() + note.saturating_sub(note_start) as u16 / 2; - let c = if note % 2 == 0 { "▀" } else { "▄" }; - for x in x1..x2 { - to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0)))); - } - } - }) - }); - let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30)); - let playhead_active = playhead_inactive.clone().yellow().bold().not_dim(); - let playhead = CustomWidget::new( - |to:[u16;2]|Ok(Some(to.clip_h(1))), - move|to: &mut TuiOutput|{ - if let Some(_) = phrase { - let now = self.0.now().get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length; - let time_clamp = time_clamp - .expect("time_axis of sequencer expected to be clamped"); - for x in 0..(time_clamp/time_scale).saturating_sub(time_start) { - let this_step = time_start + (x + 0) * time_scale; - let next_step = time_start + (x + 1) * time_scale; - let x = to.area().x() + x as u16; - let active = this_step <= now && now < next_step; - let character = if active { "|" } else { "·" }; - let style = if active { playhead_active } else { playhead_inactive }; - to.blit(&character, x, to.area.y(), Some(style)); - } - } - Ok(()) - } - ).push_x(6).align_sw(); - let border_color = if focused{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 = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); - let note_area = lay!(notes, cursor).fill_x(); - let piano_roll = row!(keys, note_area).fill_x(); - let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border); - let content = lay!(content, playhead); - let mut upper_left = format!("[{}] Sequencer", if entered {"■"} else {" "}); - if let Some(phrase) = phrase { - upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name); - } - let mut lower_right = format!("┤{}├", size.format()); - lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale)); - //lower_right = format!("Zoom: {} (+{}:{}*{}|{})", - //pulses_to_name(time_scale), - //time_start, time_point.unwrap_or(0), - //time_scale, time_clamp.unwrap_or(0), - //); - if focused && entered { - lower_right = format!("┤Note: {} {}├─{lower_right}", - note_axis.read().unwrap().point.unwrap(), - pulses_to_name(note_len)); - //lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}", - //pulses_to_name(*note_len), - //note_start, - //note_point.unwrap_or(0), - //note_clamp.unwrap_or(0), - //); - } - let upper_right = if let Some(phrase) = phrase { - format!("┤Length: {}├", phrase.read().unwrap().length) - } else { - String::new() - }; - lay!( - content, - TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), - TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), - TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(), - ) - } -} - -impl Content for PhraseLength { - type Engine = Tui; - fn content (&self) -> impl Widget { - Layers::new(move|add|{ - match self.focus { - None => add(&row!( - " ", self.bars_string(), - ".", self.beats_string(), - ".", self.ticks_string(), - " " - )), - Some(PhraseLengthFocus::Bar) => add(&row!( - "[", self.bars_string(), - "]", self.beats_string(), - ".", self.ticks_string(), - " " - )), - Some(PhraseLengthFocus::Beat) => add(&row!( - " ", self.bars_string(), - "[", self.beats_string(), - "]", self.ticks_string(), - " " - )), - Some(PhraseLengthFocus::Tick) => add(&row!( - " ", self.bars_string(), - ".", self.beats_string(), - "[", self.ticks_string(), - "]" - )), - } - }) - } -} diff --git a/crates/tek_tui/src/tui_impls.rs b/crates/tek_tui/src/tui_impls.rs index e9841eca..b4175dad 100644 --- a/crates/tek_tui/src/tui_impls.rs +++ b/crates/tek_tui/src/tui_impls.rs @@ -174,68 +174,6 @@ macro_rules! impl_phrase_editor_control { } } } -macro_rules! impl_phrases_view_state { - ($Struct:ident $(:: $field:ident)* [$self1:ident: $focus:expr] [$self2:ident: $enter:expr]) => { - impl PhrasesViewState for $Struct { - fn phrases_focused (&$self1) -> bool { - $focus - } - fn phrases_entered (&$self2) -> bool { - $enter - } - fn phrases (&self) -> &Vec>> { - &self$(.$field)*.phrases - } - fn phrase_index (&self) -> usize { - self$(.$field)*.phrase.load(Ordering::Relaxed) - } - fn phrases_mode (&self) -> &Option { - &self$(.$field)*.mode - } - } - } -} -macro_rules! impl_phrase_view_state { - ($Struct:ident $(:: $field:ident)* [$self1:ident : $focused:expr] [$self2:ident : $entered:expr]) => { - impl PhraseViewState for $Struct { - fn phrase_editing (&self) -> &Option>> { - &self$(.$field)*.phrase - } - fn phrase_editor_focused (&$self1) -> bool { - $focused - //self$(.$field)*.focus.is_focused() - } - fn phrase_editor_entered (&$self2) -> bool { - $entered - //self$(.$field)*.focus.is_entered() - } - fn phrase_editor_size (&self) -> &Measure { - todo!() - } - fn keys (&self) -> &Buffer { - &self$(.$field)*.keys - } - fn buffer (&self) -> &BigBuffer { - &self$(.$field)*.buffer - } - fn note_len (&self) -> usize { - self$(.$field)*.note_len - } - fn note_axis (&self) -> &RwLock> { - &self$(.$field)*.note_axis - } - fn time_axis (&self) -> &RwLock> { - &self$(.$field)*.time_axis - } - fn now (&self) -> &Arc { - &self$(.$field)*.now - } - fn size (&self) -> &Measure { - &self$(.$field)*.size - } - } - } -} impl_jack_api!(TransportTui::jack); impl_jack_api!(SequencerTui::jack); @@ -268,28 +206,3 @@ impl_phrase_editor_control!(ArrangerTui [self: todo!()] [self, phrase: todo!()] ); - -impl_phrases_view_state!(PhrasesModel - [self: false] - [self: false] -); -impl_phrases_view_state!(SequencerTui::phrases - [self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)] - [self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)] -); -impl_phrases_view_state!(ArrangerTui::phrases - [self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)] - [self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)] -); -impl_phrase_view_state!(PhraseEditorModel - [self: true] - [self: true] -); -impl_phrase_view_state!(SequencerTui::editor - [self: self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)] - [self: self.entered() && self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)] -); -impl_phrase_view_state!(ArrangerTui::editor - [self: self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)] - [self: self.entered() && self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)]) -; diff --git a/crates/tek_tui/src/tui_model.rs b/crates/tek_tui/src/tui_model.rs index 5e42f242..c7b7e813 100644 --- a/crates/tek_tui/src/tui_model.rs +++ b/crates/tek_tui/src/tui_model.rs @@ -1,379 +1 @@ use crate::*; - -#[derive(Clone)] -pub struct ClockModel { - /// JACK transport handle. - pub(crate) transport: Arc, - /// Playback state - pub(crate) playing: Arc>>, - /// Global sample and usec at which playback started - pub(crate) started: Arc>>, - /// Current moment in time - pub(crate) current: Arc, - /// Note quantization factor - pub(crate) quant: Arc, - /// Launch quantization factor - pub(crate) sync: Arc, - /// TODO: Enable metronome? - pub(crate) metronome: bool, -} - -impl From<&Arc> for ClockModel { - fn from (transport: &Arc) -> Self { - Self { - current: Instant::default().into(), - playing: RwLock::new(None).into(), - quant: Quantize::default().into(), - started: RwLock::new(None).into(), - sync: LaunchSync::default().into(), - transport: transport.clone(), - metronome: false, - } - } -} - -/// 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<(Instant, Option>>)>, - /// Start time and next phrase - pub(crate) next_phrase: Option<(Instant, 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 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(), - } - } -} - -/// 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 keys are rendered to this buffer - pub(crate) keys: Buffer, - /// The full piano roll is rendered to this buffer - pub(crate) buffer: BigBuffer, - /// Cursor/scroll/zoom in pitch axis - pub(crate) note_axis: RwLock>, - /// Cursor/scroll/zoom in time axis - pub(crate) time_axis: RwLock>, - /// Display mode - pub(crate) mode: bool, - /// 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 -} - -impl Default for PhraseEditorModel { - fn default () -> Self { - Self { - phrase: None, - note_len: 24, - keys: keys_vert(), - buffer: Default::default(), - mode: false, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - now: Pulse::default().into(), - size: Measure::new(), - note_axis: RwLock::new(FixedAxis { - start: 12, - point: Some(36), - clamp: Some(127) - }), - time_axis: RwLock::new(ScaledAxis { - start: 00, - point: Some(00), - clamp: Some(000), - scale: 24 - }), - } - } -} - -#[derive(Debug)] -pub struct PhrasesModel { - /// Collection of phrases - pub(crate) phrases: Vec>>, - /// Selected phrase - pub(crate) phrase: AtomicUsize, - /// Scroll offset - pub(crate) scroll: usize, - /// Mode switch - pub(crate) mode: Option, -} - -impl Default for PhrasesModel { - fn default () -> Self { - Self { - phrases: vec![RwLock::new(Phrase::default()).into()], - phrase: 0.into(), - scroll: 0, - mode: None, - } - } -} - -/// 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), -} - -/// Browses for phrase to import/export -#[derive(Debug, Clone)] -pub struct FileBrowser { - pub cwd: PathBuf, - pub dirs: Vec<(OsString, String)>, - pub files: Vec<(OsString, String)>, - pub filter: String, - pub index: usize, - pub scroll: usize, - pub size: Measure -} - -impl FileBrowser { - pub fn new (cwd: Option) -> Usually { - let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; - let mut dirs = vec![]; - let mut files = vec![]; - for entry in std::fs::read_dir(&cwd)? { - let entry = entry?; - let name = entry.file_name(); - let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); - let meta = entry.metadata()?; - if meta.is_dir() { - dirs.push((name, format!("📁 {decoded}"))); - } else if meta.is_file() { - files.push((name, format!("📄 {decoded}"))); - } - } - Ok(Self { - cwd, - dirs, - files, - filter: "".to_string(), - index: 0, - scroll: 0, - size: Measure::new(), - }) - } - pub fn len (&self) -> usize { - self.dirs.len() + self.files.len() - } - pub fn is_dir (&self) -> bool { - self.index < self.dirs.len() - } - pub fn is_file (&self) -> bool { - self.index >= self.dirs.len() - } - pub fn path (&self) -> PathBuf { - self.cwd.join(if self.is_dir() { - &self.dirs[self.index].0 - } else if self.is_file() { - &self.files[self.index - self.dirs.len()].0 - } else { - unreachable!() - }) - } - pub fn chdir (&self) -> Usually { - Self::new(Some(self.path())) - } -} - -/// Displays and edits phrase length. -pub struct PhraseLength { - /// Pulses per beat (quaver) - pub ppq: usize, - /// Beats per bar - pub bpb: usize, - /// Length of phrase in pulses - pub pulses: usize, - /// Selected subdivision - pub focus: Option, -} - -impl PhraseLength { - pub fn new (pulses: usize, focus: Option) -> Self { - Self { ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { - self.pulses / (self.bpb * self.ppq) - } - pub fn beats (&self) -> usize { - (self.pulses % (self.bpb * self.ppq)) / self.ppq - } - pub fn ticks (&self) -> usize { - self.pulses % self.ppq - } - pub fn bars_string (&self) -> String { - format!("{}", self.bars()) - } - pub fn beats_string (&self) -> String { - format!("{}", self.beats()) - } - pub fn ticks_string (&self) -> String { - format!("{:>02}", self.ticks()) - } -} - -impl HasScenes for ArrangerTui { - fn scenes (&self) -> &Vec { - &self.scenes - } - fn scenes_mut (&mut self) -> &mut Vec { - &mut self.scenes - } - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerScene> - { - let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); - let scene = ArrangerScene { - name: Arc::new(name.into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(||ItemColor::random()), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok(&mut self.scenes_mut()[index]) - } - fn selected_scene (&self) -> Option<&ArrangerScene> { - self.selected.scene().map(|s|self.scenes().get(s)).flatten() - } - fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { - self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() - } -} - -#[derive(Default, Debug, Clone)] -pub struct ArrangerScene { - /// Name of scene - pub(crate) name: Arc>, - /// Clips in scene, one per track - pub(crate) clips: Vec>>>, - /// Identifying color of scene - pub(crate) color: ItemColor, -} - -impl ArrangerSceneApi for ArrangerScene { - fn name (&self) -> &Arc> { - &self.name - } - fn clips (&self) -> &Vec>>> { - &self.clips - } - fn color (&self) -> ItemColor { - self.color - } -} - -impl HasTracks for ArrangerTui { - fn tracks (&self) -> &Vec { - &self.tracks - } - fn tracks_mut (&mut self) -> &mut Vec { - &mut self.tracks - } -} - -impl ArrangerTracksApi for ArrangerTui { - fn track_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerTrack> - { - let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); - let track = ArrangerTrack { - width: name.len() + 2, - name: Arc::new(name.into()), - color: color.unwrap_or_else(||ItemColor::random()), - player: PhrasePlayerModel::from(&self.clock), - }; - self.tracks_mut().push(track); - let index = self.tracks().len() - 1; - Ok(&mut self.tracks_mut()[index]) - } - fn track_del (&mut self, index: usize) { - self.tracks_mut().remove(index); - for scene in self.scenes_mut().iter_mut() { - scene.clips.remove(index); - } - } -} - -#[derive(Debug)] -pub struct ArrangerTrack { - /// Name of track - pub(crate) name: Arc>, - /// Preferred width of track column - pub(crate) width: usize, - /// Identifying color of track - pub(crate) color: ItemColor, - /// MIDI player state - pub(crate) player: PhrasePlayerModel, -} - -impl ArrangerTrackApi for ArrangerTrack { - /// Name of track - fn name (&self) -> &Arc> { - &self.name - } - /// Preferred width of track column - fn width (&self) -> usize { - self.width - } - /// Preferred width of track column - fn width_mut (&mut self) -> &mut usize { - &mut self.width - } - /// Identifying color of track - fn color (&self) -> ItemColor { - self.color - } -} diff --git a/crates/tek_tui/src/tui_model_arranger.rs b/crates/tek_tui/src/tui_model_arranger.rs new file mode 100644 index 00000000..493d1dc3 --- /dev/null +++ b/crates/tek_tui/src/tui_model_arranger.rs @@ -0,0 +1,114 @@ +use crate::*; + +impl HasScenes for ArrangerTui { + fn scenes (&self) -> &Vec { + &self.scenes + } + fn scenes_mut (&mut self) -> &mut Vec { + &mut self.scenes + } + fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerScene> + { + let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); + let scene = ArrangerScene { + name: Arc::new(name.into()), + clips: vec![None;self.tracks().len()], + color: color.unwrap_or_else(||ItemColor::random()), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok(&mut self.scenes_mut()[index]) + } + fn selected_scene (&self) -> Option<&ArrangerScene> { + self.selected.scene().map(|s|self.scenes().get(s)).flatten() + } + fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { + self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() + } +} + +#[derive(Default, Debug, Clone)] +pub struct ArrangerScene { + /// Name of scene + pub(crate) name: Arc>, + /// Clips in scene, one per track + pub(crate) clips: Vec>>>, + /// Identifying color of scene + pub(crate) color: ItemColor, +} + +impl ArrangerSceneApi for ArrangerScene { + fn name (&self) -> &Arc> { + &self.name + } + fn clips (&self) -> &Vec>>> { + &self.clips + } + fn color (&self) -> ItemColor { + self.color + } +} + +impl HasTracks for ArrangerTui { + fn tracks (&self) -> &Vec { + &self.tracks + } + fn tracks_mut (&mut self) -> &mut Vec { + &mut self.tracks + } +} + +impl ArrangerTracksApi for ArrangerTui { + fn track_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerTrack> + { + let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); + let track = ArrangerTrack { + width: name.len() + 2, + name: Arc::new(name.into()), + color: color.unwrap_or_else(||ItemColor::random()), + player: PhrasePlayerModel::from(&self.clock), + }; + self.tracks_mut().push(track); + let index = self.tracks().len() - 1; + Ok(&mut self.tracks_mut()[index]) + } + fn track_del (&mut self, index: usize) { + self.tracks_mut().remove(index); + for scene in self.scenes_mut().iter_mut() { + scene.clips.remove(index); + } + } +} + +#[derive(Debug)] +pub struct ArrangerTrack { + /// Name of track + pub(crate) name: Arc>, + /// Preferred width of track column + pub(crate) width: usize, + /// Identifying color of track + pub(crate) color: ItemColor, + /// MIDI player state + pub(crate) player: PhrasePlayerModel, +} + +impl ArrangerTrackApi for ArrangerTrack { + /// Name of track + fn name (&self) -> &Arc> { + &self.name + } + /// Preferred width of track column + fn width (&self) -> usize { + self.width + } + /// Preferred width of track column + fn width_mut (&mut self) -> &mut usize { + &mut self.width + } + /// Identifying color of track + fn color (&self) -> ItemColor { + self.color + } +} diff --git a/crates/tek_tui/src/tui_model_clock.rs b/crates/tek_tui/src/tui_model_clock.rs new file mode 100644 index 00000000..ccab54e3 --- /dev/null +++ b/crates/tek_tui/src/tui_model_clock.rs @@ -0,0 +1,33 @@ +use crate::*; + +#[derive(Clone)] +pub struct ClockModel { + /// JACK transport handle. + pub(crate) transport: Arc, + /// Playback state + pub(crate) playing: Arc>>, + /// Global sample and usec at which playback started + pub(crate) started: Arc>>, + /// Current moment in time + pub(crate) current: Arc, + /// Note quantization factor + pub(crate) quant: Arc, + /// Launch quantization factor + pub(crate) sync: Arc, + /// TODO: Enable metronome? + pub(crate) metronome: bool, +} + +impl From<&Arc> for ClockModel { + fn from (transport: &Arc) -> Self { + Self { + current: Instant::default().into(), + playing: RwLock::new(None).into(), + quant: Quantize::default().into(), + started: RwLock::new(None).into(), + sync: LaunchSync::default().into(), + transport: transport.clone(), + metronome: false, + } + } +} diff --git a/crates/tek_tui/src/tui_model_file_browser.rs b/crates/tek_tui/src/tui_model_file_browser.rs new file mode 100644 index 00000000..e539d01c --- /dev/null +++ b/crates/tek_tui/src/tui_model_file_browser.rs @@ -0,0 +1,62 @@ +use crate::*; + +/// Browses for phrase to import/export +#[derive(Debug, Clone)] +pub struct FileBrowser { + pub cwd: PathBuf, + pub dirs: Vec<(OsString, String)>, + pub files: Vec<(OsString, String)>, + pub filter: String, + pub index: usize, + pub scroll: usize, + pub size: Measure +} + +impl FileBrowser { + pub fn new (cwd: Option) -> Usually { + let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; + let mut dirs = vec![]; + let mut files = vec![]; + for entry in std::fs::read_dir(&cwd)? { + let entry = entry?; + let name = entry.file_name(); + let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); + let meta = entry.metadata()?; + if meta.is_dir() { + dirs.push((name, format!("📁 {decoded}"))); + } else if meta.is_file() { + files.push((name, format!("📄 {decoded}"))); + } + } + Ok(Self { + cwd, + dirs, + files, + filter: "".to_string(), + index: 0, + scroll: 0, + size: Measure::new(), + }) + } + pub fn len (&self) -> usize { + self.dirs.len() + self.files.len() + } + pub fn is_dir (&self) -> bool { + self.index < self.dirs.len() + } + pub fn is_file (&self) -> bool { + self.index >= self.dirs.len() + } + pub fn path (&self) -> PathBuf { + self.cwd.join(if self.is_dir() { + &self.dirs[self.index].0 + } else if self.is_file() { + &self.files[self.index - self.dirs.len()].0 + } else { + unreachable!() + }) + } + pub fn chdir (&self) -> Usually { + Self::new(Some(self.path())) + } +} diff --git a/crates/tek_tui/src/tui_model_phrase_editor.rs b/crates/tek_tui/src/tui_model_phrase_editor.rs new file mode 100644 index 00000000..5998e297 --- /dev/null +++ b/crates/tek_tui/src/tui_model_phrase_editor.rs @@ -0,0 +1,54 @@ +use crate::*; + +/// 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 keys are rendered to this buffer + pub(crate) keys: Buffer, + /// The full piano roll is rendered to this buffer + pub(crate) buffer: BigBuffer, + /// Cursor/scroll/zoom in pitch axis + pub(crate) note_axis: RwLock>, + /// Cursor/scroll/zoom in time axis + pub(crate) time_axis: RwLock>, + /// Display mode + pub(crate) mode: bool, + /// 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 +} + +impl Default for PhraseEditorModel { + fn default () -> Self { + Self { + phrase: None, + note_len: 24, + keys: keys_vert(), + buffer: Default::default(), + mode: false, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + now: Pulse::default().into(), + size: Measure::new(), + note_axis: RwLock::new(FixedAxis { + start: 12, + point: Some(36), + clamp: Some(127) + }), + time_axis: RwLock::new(ScaledAxis { + start: 00, + point: Some(00), + clamp: Some(000), + scale: 24 + }), + } + } +} diff --git a/crates/tek_tui/src/tui_model_phrase_length.rs b/crates/tek_tui/src/tui_model_phrase_length.rs new file mode 100644 index 00000000..a9979787 --- /dev/null +++ b/crates/tek_tui/src/tui_model_phrase_length.rs @@ -0,0 +1,37 @@ +use crate::*; + +/// Displays and edits phrase length. +pub struct PhraseLength { + /// Pulses per beat (quaver) + pub ppq: usize, + /// Beats per bar + pub bpb: usize, + /// Length of phrase in pulses + pub pulses: usize, + /// Selected subdivision + pub focus: Option, +} + +impl PhraseLength { + pub fn new (pulses: usize, focus: Option) -> Self { + Self { ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { + self.pulses / (self.bpb * self.ppq) + } + pub fn beats (&self) -> usize { + (self.pulses % (self.bpb * self.ppq)) / self.ppq + } + pub fn ticks (&self) -> usize { + self.pulses % self.ppq + } + pub fn bars_string (&self) -> String { + format!("{}", self.bars()) + } + pub fn beats_string (&self) -> String { + format!("{}", self.beats()) + } + pub fn ticks_string (&self) -> String { + format!("{:>02}", self.ticks()) + } +} diff --git a/crates/tek_tui/src/tui_model_phrase_list.rs b/crates/tek_tui/src/tui_model_phrase_list.rs new file mode 100644 index 00000000..46b31cc8 --- /dev/null +++ b/crates/tek_tui/src/tui_model_phrase_list.rs @@ -0,0 +1,37 @@ +use crate::*; + +#[derive(Debug)] +pub struct PhrasesModel { + /// Collection of phrases + pub(crate) phrases: Vec>>, + /// Selected phrase + pub(crate) phrase: AtomicUsize, + /// Scroll offset + pub(crate) scroll: usize, + /// Mode switch + pub(crate) mode: Option, +} + +impl Default for PhrasesModel { + fn default () -> Self { + Self { + phrases: vec![RwLock::new(Phrase::default()).into()], + phrase: 0.into(), + scroll: 0, + mode: None, + } + } +} + +/// 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), +} diff --git a/crates/tek_tui/src/tui_model_phrase_player.rs b/crates/tek_tui/src/tui_model_phrase_player.rs new file mode 100644 index 00000000..2e18fb33 --- /dev/null +++ b/crates/tek_tui/src/tui_model_phrase_player.rs @@ -0,0 +1,48 @@ +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<(Instant, Option>>)>, + /// Start time and next phrase + pub(crate) next_phrase: Option<(Instant, 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 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(), + } + } +} diff --git a/crates/tek_tui/src/tui_view.rs b/crates/tek_tui/src/tui_view_arranger.rs similarity index 69% rename from crates/tek_tui/src/tui_view.rs rename to crates/tek_tui/src/tui_view_arranger.rs index ac29aad0..c8b4364b 100644 --- a/crates/tek_tui/src/tui_view.rs +++ b/crates/tek_tui/src/tui_view_arranger.rs @@ -1,62 +1,70 @@ use crate::*; -pub struct TransportView { - pub(crate) state: Option, - pub(crate) selected: Option, - pub(crate) focused: bool, - pub(crate) bpm: f64, - pub(crate) sync: f64, - pub(crate) quant: f64, - pub(crate) beat: String, - pub(crate) msu: String, -} -impl<'a, T> From<&'a T> for TransportView -where - T: ClockApi, - Option: From<&'a T> -{ - fn from (state: &'a T) -> Self { - let selected = state.into(); - Self { - selected, - focused: selected.is_some(), - state: state.transport_state().read().unwrap().clone(), - bpm: state.bpm().get(), - sync: state.sync().get(), - quant: state.quant().get(), - beat: state.current().format_beat(), - msu: state.current().usec.format_msu(), - } - } -} -impl From<&TransportTui> for Option { - fn from (state: &TransportTui) -> Self { - match state.focus.inner() { - AppFocus::Content(focus) => Some(focus), - _ => None - } - } -} -impl From<&SequencerTui> for Option { - fn from (state: &SequencerTui) -> Self { - match state.focus.inner() { - AppFocus::Content(SequencerFocus::Transport(focus)) => Some(focus), - _ => None - } - } -} -impl From<&ArrangerTui> for Option { - fn from (state: &ArrangerTui) -> Self { - match state.focus.inner() { - AppFocus::Content(ArrangerFocus::Transport(focus)) => Some(focus), - _ => None - } +/// Layout for standalone arranger app. +impl Content for ArrangerTui { + type Engine = Tui; + fn content (&self) -> impl Widget { + let arranger_focused = self.arranger_focused(); + Split::down( + 1, + TransportView::from(self), + Split::down( + self.splits[0], + lay!( + 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) + }) + .grow_y(1) + .border(Lozenge(Style::default() + .bg(TuiTheme::border_bg()) + .fg(TuiTheme::border_fg(arranger_focused)))), + widget(&self.size), + widget(&format!("[{}] Arranger", if self.entered { + "■" + } else { + " " + })) + .fg(TuiTheme::title_fg(arranger_focused)) + .push_x(1), + ), + Split::right( + self.splits[1], + PhrasesView(self), + PhraseView(self), + ) + ) + ) } } -pub struct PhrasesView<'a, T: PhrasesViewState>(pub &'a T); +/// Display mode of arranger +#[derive(Clone, PartialEq)] +pub enum ArrangerMode { + /// Tracks are rows + Horizontal, + /// Tracks are columns + Vertical(usize), +} -pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T); +/// Arranger display mode can be cycled +impl ArrangerMode { + /// Cycle arranger display mode + pub fn to_next (&mut self) { + *self = match self { + Self::Horizontal => Self::Vertical(1), + Self::Vertical(1) => Self::Vertical(2), + Self::Vertical(2) => Self::Vertical(2), + Self::Vertical(0) => Self::Horizontal, + Self::Vertical(_) => Self::Vertical(0), + } + } +} pub trait ArrangerViewState { fn arranger_focused (&self) -> bool; @@ -67,28 +75,6 @@ impl ArrangerViewState for ArrangerTui { } } -pub trait PhrasesViewState: Send + Sync { - fn phrases_focused (&self) -> bool; - fn phrases_entered (&self) -> bool; - fn phrases (&self) -> &Vec>>; - fn phrase_index (&self) -> usize; - fn phrases_mode (&self) -> &Option; -} - -pub trait PhraseViewState: Send + Sync { - fn phrase_editing (&self) -> &Option>>; - fn phrase_editor_focused (&self) -> bool; - fn phrase_editor_size (&self) -> &Measure; - fn phrase_editor_entered (&self) -> bool; - fn keys (&self) -> &Buffer; - fn buffer (&self) -> &BigBuffer; - fn note_len (&self) -> usize; - fn note_axis (&self) -> &RwLock>; - fn time_axis (&self) -> &RwLock>; - fn now (&self) -> &Arc; - fn size (&self) -> &Measure; -} - fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { let mut widths = vec![]; let mut total = 0; @@ -480,153 +466,3 @@ pub fn arranger_content_horizontal ( ) ) } - -/// Colors of piano keys -const KEY_COLORS: [(Color, Color);6] = [ - (Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)), - (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), - (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), - (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), - (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), - (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), -]; - -pub(crate) fn keys_vert () -> Buffer { - let area = [0, 0, 5, 64]; - let mut buffer = Buffer::empty(Rect { - x: area.x(), y: area.y(), width: area.w(), height: area.h() - }); - buffer_update(&mut buffer, area, &|cell, x, y| { - let y = 63 - y; - match x { - 0 => { - cell.set_char('▀'); - let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize]; - cell.set_fg(fg); - cell.set_bg(bg); - }, - 1 => { - cell.set_char('▀'); - cell.set_fg(Color::White); - cell.set_bg(Color::White); - }, - 2 => if y % 6 == 0 { - cell.set_char('C'); - }, - 3 => if y % 6 == 0 { - cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); - }, - _ => {} - } - }); - buffer -} - -const NTH_OCTAVE: [&'static str; 11] = [ - "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", -]; - -impl PhraseEditorModel { - pub fn put (&mut self) { - if let (Some(phrase), Some(time), Some(note)) = ( - &self.phrase, - self.time_axis.read().unwrap().point, - self.note_axis.read().unwrap().point, - ) { - let mut phrase = phrase.write().unwrap(); - let key: u7 = u7::from((127 - note) as u8); - let vel: u7 = 100.into(); - let start = time; - 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::redraw(&phrase); - } - } - /// Select which pattern to display. This pre-renders it to the buffer at full resolution. - pub fn show (&mut self, phrase: Option>>) { - if let Some(phrase) = phrase { - self.phrase = Some(phrase.clone()); - self.time_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length); - self.buffer = Self::redraw(&*phrase.read().unwrap()); - } else { - self.phrase = None; - self.time_axis.write().unwrap().clamp = Some(0); - self.buffer = Default::default(); - } - } - fn redraw (phrase: &Phrase) -> BigBuffer { - let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65); - Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq); - Self::fill_seq_fg(&mut buf, &phrase); - buf - } - fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) { - for x in 0..buf.width { - // Only fill as far as phrase length - if x as usize >= length { break } - // Fill each row with background characters - for y in 0 .. buf.height { - buf.get_mut(x, y).map(|cell|{ - cell.set_char(if ppq == 0 { - '·' - } else if x % (4 * ppq) == 0 { - '│' - } else if x % ppq == 0 { - '╎' - } else { - '·' - }); - cell.set_fg(Color::Rgb(48, 64, 56)); - cell.modifier = Modifier::DIM; - }); - } - } - } - fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) { - let mut notes_on = [false;128]; - for x in 0..buf.width { - if x as usize >= phrase.length { - break - } - if let Some(notes) = phrase.notes.get(x as usize) { - if phrase.percussive { - for note in notes { - match note { - MidiMessage::NoteOn { key, .. } => - notes_on[key.as_int() as usize] = true, - _ => {} - } - } - } else { - for note in notes { - match note { - MidiMessage::NoteOn { key, .. } => - notes_on[key.as_int() as usize] = true, - MidiMessage::NoteOff { key, .. } => - notes_on[key.as_int() as usize] = false, - _ => {} - } - } - } - for y in 0..buf.height { - if y >= 64 { - break - } - if let Some(block) = half_block( - notes_on[y as usize * 2], - notes_on[y as usize * 2 + 1], - ) { - buf.get_mut(x, y).map(|cell|{ - cell.set_char(block); - cell.set_fg(Color::White); - }); - } - } - if phrase.percussive { - notes_on.fill(false); - } - } - } - } -} diff --git a/crates/tek_tui/src/tui_view_file_browser.rs b/crates/tek_tui/src/tui_view_file_browser.rs new file mode 100644 index 00000000..30fcbff2 --- /dev/null +++ b/crates/tek_tui/src/tui_view_file_browser.rs @@ -0,0 +1,24 @@ +use crate::*; + +impl Content for FileBrowser { + type Engine = Tui; + fn content (&self) -> impl Widget { + Stack::down(|add|{ + let mut i = 0; + for (_, name) in self.dirs.iter() { + if i >= self.scroll { + add(&TuiStyle::bold(name.as_str(), i == self.index))?; + } + i += 1; + } + for (_, name) in self.files.iter() { + if i >= self.scroll { + add(&TuiStyle::bold(name.as_str(), i == self.index))?; + } + i += 1; + } + add(&format!("{}/{i}", self.index))?; + Ok(()) + }) + } +} diff --git a/crates/tek_tui/src/tui_view_phrase_editor.rs b/crates/tek_tui/src/tui_view_phrase_editor.rs new file mode 100644 index 00000000..7d8cc860 --- /dev/null +++ b/crates/tek_tui/src/tui_view_phrase_editor.rs @@ -0,0 +1,365 @@ +use crate::*; + +impl Widget for PhraseEditorModel { + type Engine = Tui; + fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + PhraseView(self).layout(to) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + PhraseView(self).render(to) + } +} + +pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T); + +impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> { + type Engine = Tui; + fn content (&self) -> impl Widget { + let phrase = self.0.phrase_editing(); + let size = self.0.size(); + let focused = self.0.phrase_editor_focused(); + let entered = self.0.phrase_editor_entered(); + let keys = self.0.keys(); + let buffer = self.0.buffer(); + let note_len = self.0.note_len(); + let note_axis = self.0.note_axis(); + let time_axis = self.0.time_axis(); + let FixedAxis { start: note_start, point: note_point, clamp: note_clamp } + = *note_axis.read().unwrap(); + let ScaledAxis { start: time_start, point: time_point, clamp: time_clamp, scale: time_scale } + = *time_axis.read().unwrap(); + //let color = Color::Rgb(0,255,0); + //let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color); + let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{ + Ok(if to.area().h() >= 2 { + to.buffer_update(to.area().set_w(5), &|cell, x, y|{ + let y = y + (note_start / 2) as u16; + if x < keys.area.width && y < keys.area.height { + *cell = keys.get(x, y).clone() + } + }); + }) + }).fill_y(); + let notes_bg_null = Color::Rgb(28, 35, 25); + let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ + let area = to.area(); + let h = area.h() as usize; + size.set_wh(area.w(), h); + let mut axis = note_axis.write().unwrap(); + if let Some(point) = axis.point { + if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) { + axis.start += 2; + } + } + Ok(if to.area().h() >= 2 { + let area = to.area(); + to.buffer_update(area, &move |cell, x, y|{ + cell.set_bg(notes_bg_null); + let src_x = (x as usize + time_start) * time_scale; + let src_y = y as usize + note_start / 2; + if src_x < buffer.width && src_y < buffer.height - 1 { + buffer.get(src_x, buffer.height - src_y - 2).map(|src|{ + cell.set_symbol(src.symbol()); + cell.set_fg(src.fg); + cell.set_bg(src.bg); + }); + } + }); + }) + }).fill_x(); + let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ + Ok(if focused && entered { + let area = to.area(); + if let (Some(time), Some(note)) = (time_point, note_point) { + let x1 = area.x() + (time / time_scale) as u16; + let x2 = x1 + (note_len / time_scale) as u16; + let y = area.y() + note.saturating_sub(note_start) as u16 / 2; + let c = if note % 2 == 0 { "▀" } else { "▄" }; + for x in x1..x2 { + to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0)))); + } + } + }) + }); + let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30)); + let playhead_active = playhead_inactive.clone().yellow().bold().not_dim(); + let playhead = CustomWidget::new( + |to:[u16;2]|Ok(Some(to.clip_h(1))), + move|to: &mut TuiOutput|{ + if let Some(_) = phrase { + let now = self.0.now().get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length; + let time_clamp = time_clamp + .expect("time_axis of sequencer expected to be clamped"); + for x in 0..(time_clamp/time_scale).saturating_sub(time_start) { + let this_step = time_start + (x + 0) * time_scale; + let next_step = time_start + (x + 1) * time_scale; + let x = to.area().x() + x as u16; + let active = this_step <= now && now < next_step; + let character = if active { "|" } else { "·" }; + let style = if active { playhead_active } else { playhead_inactive }; + to.blit(&character, x, to.area.y(), Some(style)); + } + } + Ok(()) + } + ).push_x(6).align_sw(); + let border_color = if focused{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 = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); + let note_area = lay!(notes, cursor).fill_x(); + let piano_roll = row!(keys, note_area).fill_x(); + let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border); + let content = lay!(content, playhead); + let mut upper_left = format!("[{}] Sequencer", if entered {"■"} else {" "}); + if let Some(phrase) = phrase { + upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name); + } + let mut lower_right = format!("┤{}├", size.format()); + lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale)); + //lower_right = format!("Zoom: {} (+{}:{}*{}|{})", + //pulses_to_name(time_scale), + //time_start, time_point.unwrap_or(0), + //time_scale, time_clamp.unwrap_or(0), + //); + if focused && entered { + lower_right = format!("┤Note: {} {}├─{lower_right}", + note_axis.read().unwrap().point.unwrap(), + pulses_to_name(note_len)); + //lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}", + //pulses_to_name(*note_len), + //note_start, + //note_point.unwrap_or(0), + //note_clamp.unwrap_or(0), + //); + } + let upper_right = if let Some(phrase) = phrase { + format!("┤Length: {}├", phrase.read().unwrap().length) + } else { + String::new() + }; + lay!( + content, + TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), + TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), + TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(), + ) + } +} + +pub trait PhraseViewState: Send + Sync { + fn phrase_editing (&self) -> &Option>>; + fn phrase_editor_focused (&self) -> bool; + fn phrase_editor_size (&self) -> &Measure; + fn phrase_editor_entered (&self) -> bool; + fn keys (&self) -> &Buffer; + fn buffer (&self) -> &BigBuffer; + fn note_len (&self) -> usize; + fn note_axis (&self) -> &RwLock>; + fn time_axis (&self) -> &RwLock>; + fn now (&self) -> &Arc; + fn size (&self) -> &Measure; +} + +macro_rules! impl_phrase_view_state { + ($Struct:ident $(:: $field:ident)* [$self1:ident : $focused:expr] [$self2:ident : $entered:expr]) => { + impl PhraseViewState for $Struct { + fn phrase_editing (&self) -> &Option>> { + &self$(.$field)*.phrase + } + fn phrase_editor_focused (&$self1) -> bool { + $focused + //self$(.$field)*.focus.is_focused() + } + fn phrase_editor_entered (&$self2) -> bool { + $entered + //self$(.$field)*.focus.is_entered() + } + fn phrase_editor_size (&self) -> &Measure { + todo!() + } + fn keys (&self) -> &Buffer { + &self$(.$field)*.keys + } + fn buffer (&self) -> &BigBuffer { + &self$(.$field)*.buffer + } + fn note_len (&self) -> usize { + self$(.$field)*.note_len + } + fn note_axis (&self) -> &RwLock> { + &self$(.$field)*.note_axis + } + fn time_axis (&self) -> &RwLock> { + &self$(.$field)*.time_axis + } + fn now (&self) -> &Arc { + &self$(.$field)*.now + } + fn size (&self) -> &Measure { + &self$(.$field)*.size + } + } + } +} +impl_phrase_view_state!(PhraseEditorModel + [self: true] + [self: true] +); +impl_phrase_view_state!(SequencerTui::editor + [self: self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)] + [self: self.entered() && self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)] +); +impl_phrase_view_state!(ArrangerTui::editor + [self: self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)] + [self: self.entered() && self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)]) +; + +/// Colors of piano keys +const KEY_COLORS: [(Color, Color);6] = [ + (Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)), + (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), + (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), + (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), + (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), + (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), +]; + +pub(crate) fn keys_vert () -> Buffer { + let area = [0, 0, 5, 64]; + let mut buffer = Buffer::empty(Rect { + x: area.x(), y: area.y(), width: area.w(), height: area.h() + }); + buffer_update(&mut buffer, area, &|cell, x, y| { + let y = 63 - y; + match x { + 0 => { + cell.set_char('▀'); + let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize]; + cell.set_fg(fg); + cell.set_bg(bg); + }, + 1 => { + cell.set_char('▀'); + cell.set_fg(Color::White); + cell.set_bg(Color::White); + }, + 2 => if y % 6 == 0 { + cell.set_char('C'); + }, + 3 => if y % 6 == 0 { + cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); + }, + _ => {} + } + }); + buffer +} + +const NTH_OCTAVE: [&'static str; 11] = [ + "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", +]; + +impl PhraseEditorModel { + pub fn put (&mut self) { + if let (Some(phrase), Some(time), Some(note)) = ( + &self.phrase, + self.time_axis.read().unwrap().point, + self.note_axis.read().unwrap().point, + ) { + let mut phrase = phrase.write().unwrap(); + let key: u7 = u7::from((127 - note) as u8); + let vel: u7 = 100.into(); + let start = time; + 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::redraw(&phrase); + } + } + /// Select which pattern to display. This pre-renders it to the buffer at full resolution. + pub fn show (&mut self, phrase: Option>>) { + if let Some(phrase) = phrase { + self.phrase = Some(phrase.clone()); + self.time_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length); + self.buffer = Self::redraw(&*phrase.read().unwrap()); + } else { + self.phrase = None; + self.time_axis.write().unwrap().clamp = Some(0); + self.buffer = Default::default(); + } + } + fn redraw (phrase: &Phrase) -> BigBuffer { + let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65); + Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq); + Self::fill_seq_fg(&mut buf, &phrase); + buf + } + fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) { + for x in 0..buf.width { + // Only fill as far as phrase length + if x as usize >= length { break } + // Fill each row with background characters + for y in 0 .. buf.height { + buf.get_mut(x, y).map(|cell|{ + cell.set_char(if ppq == 0 { + '·' + } else if x % (4 * ppq) == 0 { + '│' + } else if x % ppq == 0 { + '╎' + } else { + '·' + }); + cell.set_fg(Color::Rgb(48, 64, 56)); + cell.modifier = Modifier::DIM; + }); + } + } + } + fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) { + let mut notes_on = [false;128]; + for x in 0..buf.width { + if x as usize >= phrase.length { + break + } + if let Some(notes) = phrase.notes.get(x as usize) { + if phrase.percussive { + for note in notes { + match note { + MidiMessage::NoteOn { key, .. } => + notes_on[key.as_int() as usize] = true, + _ => {} + } + } + } else { + for note in notes { + match note { + MidiMessage::NoteOn { key, .. } => + notes_on[key.as_int() as usize] = true, + MidiMessage::NoteOff { key, .. } => + notes_on[key.as_int() as usize] = false, + _ => {} + } + } + } + for y in 0..buf.height { + if y >= 64 { + break + } + if let Some(block) = half_block( + notes_on[y as usize * 2], + notes_on[y as usize * 2 + 1], + ) { + buf.get_mut(x, y).map(|cell|{ + cell.set_char(block); + cell.set_fg(Color::White); + }); + } + } + if phrase.percussive { + notes_on.fill(false); + } + } + } + } +} diff --git a/crates/tek_tui/src/tui_view_phrase_length.rs b/crates/tek_tui/src/tui_view_phrase_length.rs new file mode 100644 index 00000000..80bbe72f --- /dev/null +++ b/crates/tek_tui/src/tui_view_phrase_length.rs @@ -0,0 +1,35 @@ +use crate::*; + +impl Content for PhraseLength { + type Engine = Tui; + fn content (&self) -> impl Widget { + Layers::new(move|add|{ + match self.focus { + None => add(&row!( + " ", self.bars_string(), + ".", self.beats_string(), + ".", self.ticks_string(), + " " + )), + Some(PhraseLengthFocus::Bar) => add(&row!( + "[", self.bars_string(), + "]", self.beats_string(), + ".", self.ticks_string(), + " " + )), + Some(PhraseLengthFocus::Beat) => add(&row!( + " ", self.bars_string(), + "[", self.beats_string(), + "]", self.ticks_string(), + " " + )), + Some(PhraseLengthFocus::Tick) => add(&row!( + " ", self.bars_string(), + ".", self.beats_string(), + "[", self.ticks_string(), + "]" + )), + } + }) + } +} diff --git a/crates/tek_tui/src/tui_view_phrase_list.rs b/crates/tek_tui/src/tui_view_phrase_list.rs new file mode 100644 index 00000000..91ee0055 --- /dev/null +++ b/crates/tek_tui/src/tui_view_phrase_list.rs @@ -0,0 +1,116 @@ +use crate::*; + +impl Widget for PhrasesModel { + type Engine = Tui; + fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + PhrasesView(self).layout(to) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + PhrasesView(self).render(to) + } +} + +pub struct PhrasesView<'a, T: PhrasesViewState>(pub &'a T); + +// TODO: Display phrases always in order of appearance +impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> { + type Engine = Tui; + fn content (&self) -> impl Widget { + let focused = self.0.phrases_focused(); + let entered = self.0.phrases_entered(); + let mode = self.0.phrases_mode(); + let content = Stack::down(move|add|match mode { + Some(PhrasesMode::Import(_, ref browser)) => { + add(browser) + }, + Some(PhrasesMode::Export(_, ref browser)) => { + add(browser) + }, + _ => { + let phrases = self.0.phrases(); + let selected = self.0.phrase_index(); + for (i, phrase) in phrases.iter().enumerate() { + add(&Layers::new(|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 = length.align_e().fill_x(); + let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x(); + let mut row2 = format!(" {name}"); + if let Some(PhrasesMode::Rename(phrase, _)) = mode { + if focused && i == *phrase { + row2 = format!("{row2}▄"); + } + }; + let row2 = TuiStyle::bold(row2, true); + add(&col!(row1, row2).fill_x().bg(color.base.rgb))?; + if focused && i == selected { + add(&CORNERS)?; + } + Ok(()) + }))?; + } + Ok(()) + } + }); + let border_color = if focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)}; + let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); + let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border); + let title_color = if focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)}; + let upper_left = format!("[{}] Phrases", if entered {"■"} else {" "}); + let upper_right = format!("({})", self.0.phrases().len()); + lay!( + content, + TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), + TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), + ) + } +} + +pub trait PhrasesViewState: Send + Sync { + fn phrases_focused (&self) -> bool; + fn phrases_entered (&self) -> bool; + fn phrases (&self) -> &Vec>>; + fn phrase_index (&self) -> usize; + fn phrases_mode (&self) -> &Option; +} + +macro_rules! impl_phrases_view_state { + ($Struct:ident $(:: $field:ident)* [$self1:ident: $focus:expr] [$self2:ident: $enter:expr]) => { + impl PhrasesViewState for $Struct { + fn phrases_focused (&$self1) -> bool { + $focus + } + fn phrases_entered (&$self2) -> bool { + $enter + } + fn phrases (&self) -> &Vec>> { + &self$(.$field)*.phrases + } + fn phrase_index (&self) -> usize { + self$(.$field)*.phrase.load(Ordering::Relaxed) + } + fn phrases_mode (&self) -> &Option { + &self$(.$field)*.mode + } + } + } +} + +impl_phrases_view_state!(PhrasesModel + [self: false] + [self: false] +); +impl_phrases_view_state!(SequencerTui::phrases + [self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)] + [self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)] +); +impl_phrases_view_state!(ArrangerTui::phrases + [self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)] + [self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)] +); diff --git a/crates/tek_tui/src/tui_view_sequencer.rs b/crates/tek_tui/src/tui_view_sequencer.rs new file mode 100644 index 00000000..a27a7e65 --- /dev/null +++ b/crates/tek_tui/src/tui_view_sequencer.rs @@ -0,0 +1,20 @@ +use crate::*; + +impl Content for SequencerTui { + type Engine = Tui; + fn content (&self) -> impl Widget { + lay!( + col!( + TransportView::from(self), + Split::right(20, + widget(&PhrasesView(self)), + widget(&PhraseView(self)), + ).min_y(20) + ), + self.perf.percentage() + .map(|cpu|format!("{cpu:.03}%")) + .fg(Color::Rgb(255,128,0)) + .align_sw(), + ) + } +} diff --git a/crates/tek_tui/src/tui_view_transport.rs b/crates/tek_tui/src/tui_view_transport.rs new file mode 100644 index 00000000..efdef0be --- /dev/null +++ b/crates/tek_tui/src/tui_view_transport.rs @@ -0,0 +1,102 @@ +use crate::*; + +impl Widget for TransportTui { + type Engine = Tui; + fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + TransportView::from(self).layout(to) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + TransportView::from(self).render(to) + } +} + +pub struct TransportView { + pub(crate) state: Option, + pub(crate) selected: Option, + pub(crate) focused: bool, + pub(crate) bpm: f64, + pub(crate) sync: f64, + pub(crate) quant: f64, + pub(crate) beat: String, + pub(crate) msu: String, +} + +impl Content for TransportView { + type Engine = Tui; + fn content (&self) -> impl Widget { + let Self { state, selected, focused, bpm, sync, quant, beat, msu, } = self; + row!( + selected.wrap(TransportFocus::PlayPause, &Styled( + None, + match *state { + Some(TransportState::Rolling) => "▶ PLAYING", + Some(TransportState::Starting) => "READY ...", + Some(TransportState::Stopped) => "⏹ STOPPED", + _ => "???", + } + ).min_xy(11, 2).push_x(1)), + selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, { + row! { + "BPM ", + format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0) + } + })), + selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! { + "QUANT ", pulses_to_name(*quant as usize) + })), + selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! { + "SYNC ", pulses_to_name(*sync as usize) + })), + selected.wrap(TransportFocus::Clock, &{ + row!("B" , beat.as_str(), " T", msu.as_str()).outset_x(1) + }).align_e().fill_x(), + ).fill_x().bg(Color::Rgb(40, 50, 30)) + } +} + +impl<'a, T> From<&'a T> for TransportView +where + T: ClockApi, + Option: From<&'a T> +{ + fn from (state: &'a T) -> Self { + let selected = state.into(); + Self { + selected, + focused: selected.is_some(), + state: state.transport_state().read().unwrap().clone(), + bpm: state.bpm().get(), + sync: state.sync().get(), + quant: state.quant().get(), + beat: state.current().format_beat(), + msu: state.current().usec.format_msu(), + } + } +} + +impl From<&TransportTui> for Option { + fn from (state: &TransportTui) -> Self { + match state.focus.inner() { + AppFocus::Content(focus) => Some(focus), + _ => None + } + } +} + +impl From<&SequencerTui> for Option { + fn from (state: &SequencerTui) -> Self { + match state.focus.inner() { + AppFocus::Content(SequencerFocus::Transport(focus)) => Some(focus), + _ => None + } + } +} + +impl From<&ArrangerTui> for Option { + fn from (state: &ArrangerTui) -> Self { + match state.focus.inner() { + AppFocus::Content(ArrangerFocus::Transport(focus)) => Some(focus), + _ => None + } + } +} diff --git a/crates/tek_tui/src/tui_widget.rs b/crates/tek_tui/src/tui_widget.rs deleted file mode 100644 index d15106e6..00000000 --- a/crates/tek_tui/src/tui_widget.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::*; - -impl Widget for TransportTui { - type Engine = Tui; - fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - TransportView::from(self).layout(to) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - TransportView::from(self).render(to) - } -} - -impl Widget for PhrasesModel { - type Engine = Tui; - fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - PhrasesView(self).layout(to) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - PhrasesView(self).render(to) - } -} - -impl Widget for PhraseEditorModel { - type Engine = Tui; - fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - PhraseView(self).layout(to) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - PhraseView(self).render(to) - } -}