diff --git a/crates/tek_tui/src/lib.rs b/crates/tek_tui/src/lib.rs index 2fa90752..4f94f952 100644 --- a/crates/tek_tui/src/lib.rs +++ b/crates/tek_tui/src/lib.rs @@ -14,6 +14,7 @@ use std::fmt::Debug; submod! { tui_apis tui_command + tui_content tui_control tui_focus tui_handle @@ -26,35 +27,40 @@ submod! { tui_status tui_theme tui_view + tui_widget } -fn content_with_menu_and_status <'a, S: Send + Sync + 'a, C: Command> ( - content: &'a impl Widget, - menu_bar: Option<&'a MenuBar>, - status_bar: Option> -) -> impl Widget + 'a { +fn content_with_menu_and_status <'a, A, S, C> ( + content: &'a A, + menu_bar: &'a Option>, + status_bar: &'a Option +) -> impl Widget + 'a +where + A: Widget, + S: Send + Sync + 'a, + C: Command +{ let menus = menu_bar.as_ref().map_or_else( ||&[] as &[Menu<_, _, _>], |m|m.menus.as_slice() ); - let content = Either( - status_bar.is_none(), - widget(&content), - Split::up( - 1, - widget(status_bar.as_ref().unwrap()), - widget(&content) - ), - ); Either( menu_bar.is_none(), - widget(&content), + Either( + status_bar.is_none(), + widget(content), + Split::up( + 1, + widget(status_bar.as_ref().unwrap()), + widget(content) + ), + ), Split::down( 1, row!(menu in menus.iter() => { row!(" ", menu.title.as_str(), " ") }), - widget(&content) + widget(content) ) ) } diff --git a/crates/tek_tui/src/tui_command.rs b/crates/tek_tui/src/tui_command.rs index 1fb89811..14684328 100644 --- a/crates/tek_tui/src/tui_command.rs +++ b/crates/tek_tui/src/tui_command.rs @@ -83,6 +83,7 @@ pub enum PhraseCommand { impl Command for TransportCommand { fn execute (self, state: &mut T) -> Perhaps { use TransportCommand::{Focus, Clock, Playhead}; + use FocusCommand::{Next, Prev}; use ClockCommand::{SetBpm, SetQuant, SetSync}; Ok(Some(match self { Focus(Next) => { todo!() } @@ -133,6 +134,24 @@ impl Command for ArrangerCommand { } } +impl Command for ArrangerSceneCommand { + fn execute (self, state: &mut T) -> Perhaps { + todo!() + } +} + +impl Command for ArrangerTrackCommand { + fn execute (self, state: &mut T) -> Perhaps { + todo!() + } +} + +impl Command for ArrangerClipCommand { + fn execute (self, state: &mut T) -> Perhaps { + todo!() + } +} + impl Command for PhrasesCommand { fn execute (self, view: &mut PhrasesTui) -> Perhaps { use PhraseRenameCommand as Rename; diff --git a/crates/tek_tui/src/tui_content.rs b/crates/tek_tui/src/tui_content.rs new file mode 100644 index 00000000..34aeef4a --- /dev/null +++ b/crates/tek_tui/src/tui_content.rs @@ -0,0 +1,311 @@ +use crate::*; + +impl<'a, T: TransportViewState> Content for TransportView<'a, T> { + type Engine = Tui; + fn content (&self) -> impl Widget { + let state = self.0; + lay!( + state.focus().wrap(state.is_focused(), TransportFocus::PlayPause, &Styled( + None, + match state.transport_state() { + Some(TransportState::Rolling) => "▶ PLAYING", + Some(TransportState::Starting) => "READY ...", + Some(TransportState::Stopped) => "⏹ STOPPED", + _ => unreachable!(), + } + ).min_xy(11, 2).push_x(1)).align_x().fill_x(), + + row!( + state.focus().wrap(state.is_focused(), TransportFocus::Bpm, &Outset::X(1u16, { + let bpm = state.bpm_value(); + row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) } + })), + //let quant = state.focus().wrap(state.focused(), TransportFocus::Quant, &Outset::X(1u16, row! { + //"QUANT ", ppq_to_name(state.0.quant as usize) + //})), + state.focus().wrap(state.is_focused(), TransportFocus::Sync, &Outset::X(1u16, row! { + "SYNC ", pulses_to_name(state.sync_value() as usize) + })) + ).align_w().fill_x(), + + state.focus().wrap(state.is_focused(), TransportFocus::Clock, &{ + let time1 = state.format_beat(); + let time2 = state.format_msu(); + row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1) + }).align_e().fill_x(), + + ).fill_x().bg(Color::Rgb(40, 50, 30)) + } +} + +impl Content for SequencerTui { + type Engine = Tui; + fn content (&self) -> impl Widget { + col!( + widget(&TransportView(self)), + Split::right(20, + widget(&PhrasesView(self)), + widget(&PhraseView(self)), + ).min_y(20) + ) + } +} + +/// Layout for standalone arranger app. +impl Content for ArrangerTui { + type Engine = Tui; + fn content (&self) -> impl Widget { + Split::up( + 1, + widget(&TransportView(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(ArrangerViewState::focused(self))))), + widget(&self.size), + widget(&format!("[{}] Arranger", if self.entered { + "■" + } else { + " " + })) + .fg(TuiTheme::title_fg(ArrangerViewState::focused(self))) + .push_x(1), + ), + Split::right( + self.splits[1], + widget(&PhrasesView(self)), + widget(&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.focused(); + let entered = self.0.entered(); + let phrases = self.0.phrases(); + let selected_phrase = self.0.phrase(); + let mode = self.0.mode(); + let content = col!( + (i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{ + let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); + let mut length = PhraseLength::new(length, None); + if let Some(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_phrase { + add(&CORNERS)?; + } + 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<'a, T: PhraseViewState> Content for PhraseView<'a, T> { + type Engine = Tui; + fn content (&self) -> impl Widget { + let phrase = self.0.phrase(); + let size = self.0.size(); + let focused = self.0.focused(); + let entered = self.0.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_control.rs b/crates/tek_tui/src/tui_control.rs index 8ea8b31b..e483942f 100644 --- a/crates/tek_tui/src/tui_control.rs +++ b/crates/tek_tui/src/tui_control.rs @@ -20,25 +20,37 @@ pub trait TransportControl { impl TransportControl for TransportTui { fn bpm (&self) -> &BeatsPerMinute { - self.bpm() + &self.current.timebase.bpm } fn quant (&self) -> &Quantize { - self.quant() + &self.quant } fn sync (&self) -> &LaunchSync { - self.sync() + &self.sync } } impl TransportControl for SequencerTui { fn bpm (&self) -> &BeatsPerMinute { - self.bpm() + &self.current.timebase.bpm } fn quant (&self) -> &Quantize { - self.quant() + &self.quant } fn sync (&self) -> &LaunchSync { - self.sync() + &self.sync + } +} + +impl TransportControl for ArrangerTui { + fn bpm (&self) -> &BeatsPerMinute { + &self.current.timebase.bpm + } + fn quant (&self) -> &Quantize { + &self.quant + } + fn sync (&self) -> &LaunchSync { + &self.sync } } diff --git a/crates/tek_tui/src/tui_focus.rs b/crates/tek_tui/src/tui_focus.rs index 27883611..9db3f17d 100644 --- a/crates/tek_tui/src/tui_focus.rs +++ b/crates/tek_tui/src/tui_focus.rs @@ -72,22 +72,22 @@ impl TransportFocus { //type Item = TransportFocus; //} -impl FocusEnter for TransportTui { - type Item = TransportFocus; - fn focus_enter (&mut self) { - self.entered = true; - } - fn focus_exit (&mut self) { - self.entered = false; - } - fn focus_entered (&self) -> Option { - if self.entered { - Some(self.focused()) - } else { - None - } - } -} +//impl FocusEnter for TransportTui { + //type Item = TransportFocus; + //fn focus_enter (&mut self) { + //self.entered = true; + //} + //fn focus_exit (&mut self) { + //self.entered = false; + //} + //fn focus_entered (&self) -> Option { + //if self.entered { + //Some(self.focused()) + //} else { + //None + //} + //} +//} impl FocusGrid for TransportTui { type Item = TransportFocus; diff --git a/crates/tek_tui/src/tui_input.rs b/crates/tek_tui/src/tui_input.rs index 736bee90..22761b4d 100644 --- a/crates/tek_tui/src/tui_input.rs +++ b/crates/tek_tui/src/tui_input.rs @@ -61,8 +61,17 @@ impl InputToCommand for SequencerCommand { key!(KeyCode::Left) => Some(Self::Focus(Left)), key!(KeyCode::Right) => Some(Self::Focus(Right)), _ => Some(Self::App(match state.focused() { - SequencerFocus::Transport => - TransportCommand::input_to_command(&state, input).map(Transport), + SequencerFocus::Transport => { + use TransportCommand::{Clock, Playhead}; + match TransportCommand::input_to_command(view, input)? { + Clock(command) => { + todo!() + }, + Playhead(command) => { + todo!() + }, + } + }, SequencerFocus::Phrases => PhrasesCommand::input_to_command(&state.phrases, input).map(Phrases), SequencerFocus::PhraseEditor => diff --git a/crates/tek_tui/src/tui_jack.rs b/crates/tek_tui/src/tui_jack.rs index db3c6e9f..91b6cc71 100644 --- a/crates/tek_tui/src/tui_jack.rs +++ b/crates/tek_tui/src/tui_jack.rs @@ -36,7 +36,7 @@ impl Audio for ArrangerTui { let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t)); if let Some(Some(Some(phrase))) = phrase { if let Some(track) = self.tracks().get(t) { - if let Some((ref started_at, Some(ref playing))) = track.player.phrase { + if let Some((ref started_at, Some(ref playing))) = track.play_phrase { let phrase = phrase.read().unwrap(); if *playing.read().unwrap() == *phrase { let pulse = self.current().pulse.get(); diff --git a/crates/tek_tui/src/tui_view.rs b/crates/tek_tui/src/tui_view.rs index c42fd135..aaaafab1 100644 --- a/crates/tek_tui/src/tui_view.rs +++ b/crates/tek_tui/src/tui_view.rs @@ -1,15 +1,5 @@ use crate::*; -impl Widget for TransportTui { - type Engine = Tui; - fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - TransportView(&self, Default::default()).layout(to) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - TransportView(&self, Default::default()).render(to) - } -} - pub struct TransportView<'a, T: TransportViewState>(pub &'a T); pub trait TransportViewState: Send + Sync { @@ -27,7 +17,7 @@ impl TransportViewState for TransportTui { self.focus } fn is_focused (&self) -> bool { - self.focused + true } fn transport_state (&self) -> Option { *self.playing().read().unwrap() @@ -46,63 +36,12 @@ impl TransportViewState for TransportTui { } } -impl<'a, T: TransportViewState> Content for TransportView<'a, T> { - type Engine = Tui; - fn content (&self) -> impl Widget { - let state = self.0; - lay!( - state.focus().wrap(state.is_focused(), TransportFocus::PlayPause, &Styled( - None, - match state.transport_state() { - Some(TransportState::Rolling) => "▶ PLAYING", - Some(TransportState::Starting) => "READY ...", - Some(TransportState::Stopped) => "⏹ STOPPED", - _ => unreachable!(), - } - ).min_xy(11, 2).push_x(1)).align_x().fill_x(), - - row!( - state.focus().wrap(state.is_focused(), TransportFocus::Bpm, &Outset::X(1u16, { - let bpm = state.bpm_value(); - row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) } - })), - //let quant = state.focus().wrap(state.focused(), TransportFocus::Quant, &Outset::X(1u16, row! { - //"QUANT ", ppq_to_name(state.0.quant as usize) - //})), - state.focus().wrap(state.is_focused(), TransportFocus::Sync, &Outset::X(1u16, row! { - "SYNC ", pulses_to_name(state.sync_value() as usize) - })) - ).align_w().fill_x(), - - state.focus().wrap(state.is_focused(), TransportFocus::Clock, &{ - let time1 = state.format_beat(); - let time2 = state.format_msu(); - row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1) - }).align_e().fill_x(), - - ).fill_x().bg(Color::Rgb(40, 50, 30)) - } -} - -impl Content for SequencerTui { - type Engine = Tui; - fn content (&self) -> impl Widget { - col!( - widget(&TransportView(self)), - Split::right(20, - widget(&PhrasesView(self)), - widget(&PhraseView(self)), - ).min_y(20) - ) - } -} - impl TransportViewState for SequencerTui { fn focus (&self) -> TransportFocus { self.focus } fn is_focused (&self) -> bool { - self.focused + self.focused() == SequencerFocus::Transport } fn transport_state (&self) -> Option { *self.playing().read().unwrap() @@ -121,6 +60,68 @@ impl TransportViewState for SequencerTui { } } +impl TransportViewState for ArrangerTui { + fn focus (&self) -> TransportFocus { + self.focus + } + fn is_focused (&self) -> bool { + self.focused() == ArrangerFocus::Transport + } + fn transport_state (&self) -> Option { + *self.playing().read().unwrap() + } + fn bpm_value (&self) -> f64 { + self.bpm().get() + } + fn sync_value (&self) -> f64 { + self.sync().get() + } + fn format_beat (&self) -> String { + self.current().format_beat() + } + fn format_msu (&self) -> String { + self.current().usec.format_msu() + } +} + +pub trait ArrangerViewState { + fn is_focused (&self) -> bool; +} + +impl ArrangerViewState for ArrangerTui { + fn is_focused (&self) -> bool { + self.focused() == ArrangerFocus::Arranger + } +} + +pub struct PhrasesView<'a, T: PhrasesViewState>(pub &'a T); + +pub trait PhrasesViewState: Send + Sync { + fn focused (&self) -> bool; + fn entered (&self) -> bool; + fn phrases (&self) -> Vec>>; + fn phrase (&self) -> usize; + fn mode (&self) -> &Option; +} + +impl PhrasesViewState for PhrasesTui { + fn focused (&self) -> bool { + todo!() + } + fn entered (&self) -> bool { + todo!() + } + fn phrases (&self) -> Vec>> { + todo!() + } + fn phrase (&self) -> usize { + todo!() + } + fn mode (&self) -> &Option { + &self.mode + } +} + impl PhrasesViewState for SequencerTui { fn focused (&self) -> bool { todo!() @@ -134,11 +135,77 @@ impl PhrasesViewState for SequencerTui { fn phrase (&self) -> usize { todo!() } - fn mode (&self) -> Option<&PhrasesMode> { + fn mode (&self) -> &Option { &self.mode } } +impl PhrasesViewState for ArrangerTui { + fn focused (&self) -> bool { + todo!() + } + fn entered (&self) -> bool { + todo!() + } + fn phrases (&self) -> Vec>> { + todo!() + } + fn phrase (&self) -> usize { + todo!() + } + fn mode (&self) -> &Option { + &self.mode + } +} + +pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T); + +pub trait PhraseViewState: Send + Sync { + fn focused (&self) -> bool; + fn entered (&self) -> bool; + fn keys (&self) -> &Buffer; + fn phrase (&self) -> &Option>>; + fn buffer (&self) -> &BigBuffer; + fn note_len (&self) -> usize; + fn note_axis (&self) -> &RwLock>; + fn time_axis (&self) -> &RwLock>; + fn size (&self) -> &Measure; + fn now (&self) -> &Arc; +} + +impl PhraseViewState for PhraseTui { + fn focused (&self) -> bool { + self.focused + } + fn entered (&self) -> bool { + self.entered + } + fn keys (&self) -> &Buffer { + &self.keys + } + fn phrase (&self) -> &Option>> { + &self.phrase + } + fn buffer (&self) -> &BigBuffer { + &self.buffer + } + fn note_len (&self) -> usize { + self.note_len + } + fn note_axis (&self) -> &RwLock> { + &self.note_axis + } + fn time_axis (&self) -> &RwLock> { + &self.time_axis + } + fn size (&self) -> &Measure { + &self.size + } + fn now (&self) -> &Arc { + &self.now + } +} + impl PhraseViewState for SequencerTui { fn focused (&self) -> bool { todo!() @@ -172,6 +239,75 @@ impl PhraseViewState for SequencerTui { } } +impl PhraseViewState for ArrangerTui { + fn focused (&self) -> bool { + todo!() + } + fn entered (&self) -> bool { + todo!() + } + fn keys (&self) -> &Buffer { + todo!() + } + fn phrase (&self) -> &Option>> { + todo!() + } + fn buffer (&self) -> &BigBuffer { + todo!() + } + fn note_len (&self) -> usize { + todo!() + } + fn note_axis (&self) -> &RwLock> { + todo!() + } + fn time_axis (&self) -> &RwLock> { + todo!() + } + fn size (&self) -> &Measure { + todo!() + } + fn now (&self) -> &Arc { + todo!() + } +} + +/// 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()) + } +} + /// Display mode of arranger #[derive(Clone, PartialEq)] pub enum ArrangerMode { @@ -195,48 +331,6 @@ impl ArrangerMode { } } -/// Layout for standalone arranger app. -impl Content for ArrangerTui { - type Engine = Tui; - fn content (&self) -> impl Widget { - Split::up( - 1, - widget(&TransportView(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(self.focused() == ArrangerFocus::Arranger)))), - widget(&self.size), - widget(&format!("[{}] Arranger", if self.entered { - "■" - } else { - " " - })) - .fg(TuiTheme::title_fg(self.focused() == ArrangerFocus::Arranger)) - .push_x(1), - ), - Split::right( - self.splits[1], - widget(&PhrasesView(self)), - widget(&PhraseView(self)), - ) - ) - ) - } -} - fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { let mut widths = vec![]; let mut total = 0; @@ -629,350 +723,6 @@ pub fn arranger_content_horizontal ( ) } -impl TransportViewState for ArrangerTui { - fn focus (&self) -> TransportFocus { - self.focus - } - fn is_focused (&self) -> bool { - self.focused - } - fn transport_state (&self) -> Option { - *self.playing().read().unwrap() - } - fn bpm_value (&self) -> f64 { - self.bpm().get() - } - fn sync_value (&self) -> f64 { - self.sync().get() - } - fn format_beat (&self) -> String { - self.current().format_beat() - } - fn format_msu (&self) -> String { - self.current().usec.format_msu() - } -} - -impl PhrasesViewState for ArrangerTui { - fn focused (&self) -> bool { - todo!() - } - fn entered (&self) -> bool { - todo!() - } - fn phrases (&self) -> Vec>> { - todo!() - } - fn phrase (&self) -> usize { - todo!() - } - fn mode (&self) -> Option<&PhrasesMode> { - &self.mode - } -} - -impl PhraseViewState for ArrangerTui { - fn focused (&self) -> bool { - todo!() - } - fn entered (&self) -> bool { - todo!() - } - fn keys (&self) -> &Buffer { - todo!() - } - fn phrase (&self) -> &Option>> { - todo!() - } - fn buffer (&self) -> &BigBuffer { - todo!() - } - fn note_len (&self) -> usize { - todo!() - } - fn note_axis (&self) -> &RwLock> { - todo!() - } - fn time_axis (&self) -> &RwLock> { - todo!() - } - fn size (&self) -> &Measure { - todo!() - } - fn now (&self) -> &Arc { - todo!() - } -} - -impl Widget for PhrasesTui { - 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 trait PhrasesViewState { - fn focused (&self) -> bool; - fn entered (&self) -> bool; - fn phrases (&self) -> Vec>>; - fn phrase (&self) -> usize; - fn mode (&self) -> Option<&PhrasesMode>; -} - -impl PhrasesViewState for PhrasesTui { - fn focused (&self) -> bool { - todo!() - } - fn entered (&self) -> bool { - todo!() - } - fn phrases (&self) -> Vec>> { - todo!() - } - fn phrase (&self) -> usize { - todo!() - } - fn mode (&self) -> Option<&PhrasesMode> { - &self.mode - } -} - -pub struct PhrasesView<'a, T: PhrasesViewState>(&'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.focused(); - let entered = self.0.entered(); - let phrases = self.0.phrases(); - let selected_phrase = self.0.phrase(); - let mode = self.0.mode(); - let content = col!( - (i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{ - let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); - let mut length = PhraseLength::new(length, None); - if let Some(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_phrase { - add(&CORNERS)?; - } - 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 struct PhraseView<'a, T: PhraseViewState>(pub &'a T); - -pub trait PhraseViewState: Send + Sync { - fn focused (&self) -> bool; - fn entered (&self) -> bool; - fn keys (&self) -> &Buffer; - fn phrase (&self) -> &Option>>; - fn buffer (&self) -> &BigBuffer; - fn note_len (&self) -> usize; - fn note_axis (&self) -> &RwLock>; - fn time_axis (&self) -> &RwLock>; - fn size (&self) -> &Measure; - fn now (&self) -> &Arc; -} - -impl PhraseViewState for PhraseTui { - fn focused (&self) -> bool { - self.focused - } - fn entered (&self) -> bool { - self.entered - } - fn keys (&self) -> &Buffer { - &self.keys - } - fn phrase (&self) -> &Option>> { - &self.phrase - } - fn buffer (&self) -> &BigBuffer { - &self.buffer - } - fn note_len (&self) -> usize { - self.note_len - } - fn note_axis (&self) -> &RwLock> { - &self.note_axis - } - fn time_axis (&self) -> &RwLock> { - &self.time_axis - } - fn size (&self) -> &Measure { - &self.size - } - fn now (&self) -> &Arc { - &self.now - } -} - -impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> { - type Engine = Tui; - fn content (&self) -> impl Widget { - let phrase = self.0.phrase(); - let size = self.0.size(); - let focused = self.0.focused(); - let entered = self.0.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(), - ) - } -} - /// Colors of piano keys const KEY_COLORS: [(Color, Color);6] = [ (Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)), @@ -1017,83 +767,3 @@ pub(crate) fn keys_vert () -> Buffer { const NTH_OCTAVE: [&'static str; 11] = [ "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", ]; - -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(), - "]" - )), - } - }) - } -} - -/// 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 Widget for PhraseTui { - type Engine = Tui; - fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - PhraseView(&self, Default::default()).layout(to) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - PhraseView(&self, Default::default()).render(to) - } -} diff --git a/crates/tek_tui/src/tui_widget.rs b/crates/tek_tui/src/tui_widget.rs new file mode 100644 index 00000000..1d62f554 --- /dev/null +++ b/crates/tek_tui/src/tui_widget.rs @@ -0,0 +1,31 @@ +use crate::*; + +impl Widget for TransportTui { + type Engine = Tui; + fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + TransportView(self).layout(to) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + TransportView(self).render(to) + } +} + +impl Widget for PhrasesTui { + 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 PhraseTui { + 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) + } +}