diff --git a/crates/tek_tui/src/lib.rs b/crates/tek_tui/src/lib.rs index 574f6d61..1299b8ea 100644 --- a/crates/tek_tui/src/lib.rs +++ b/crates/tek_tui/src/lib.rs @@ -24,12 +24,15 @@ submod! { //tui_mixer // TODO tui_phrase + tui_phrase_cmd + tui_phrase_view //tui_plugin // TODO //tui_plugin_lv2 //tui_plugin_lv2_gui //tui_plugin_vst2 //tui_plugin_vst3 tui_pool + tui_pool_cmd tui_pool_view //tui_sampler // TODO //tui_sampler_cmd @@ -44,57 +47,7 @@ submod! { tui_transport_view } -//pub struct AppView -//where - //E: Engine, - //A: Widget + Audio, - //C: Command, - //S: StatusBar, -//{ - //pub app: A, - //pub cursor: (usize, usize), - //pub entered: bool, - //pub menu_bar: Option>, - //pub status_bar: Option, - //pub history: Vec, - //pub size: Measure, -//} - -//#[derive(Debug, Clone)] -//pub enum AppViewCommand { - //App(T) -//} - -//#[derive(Debug, Copy, Clone, PartialEq)] -//pub enum AppViewFocus { - //Menu, - //Content(F), -//} - -//impl AppView -//where - //E: Engine, - //A: Widget + Audio, - //C: Command, - //S: StatusBar -//{ - //pub fn new ( - //app: A, - //menu_bar: Option>, - //status_bar: Option, - //) -> Self { - //Self { - //app, - //cursor: (0, 0), - //entered: false, - //history: vec![], - //size: Measure::new(), - //menu_bar, - //status_bar, - //} - //} -//} - +// TODO impl Content for AppView where A: Widget + Audio, diff --git a/crates/tek_tui/src/tui_arranger_cmd.rs b/crates/tek_tui/src/tui_arranger_cmd.rs index a64b4226..74a3a9c3 100644 --- a/crates/tek_tui/src/tui_arranger_cmd.rs +++ b/crates/tek_tui/src/tui_arranger_cmd.rs @@ -21,7 +21,7 @@ pub enum ArrangerCommand { Select(ArrangerSelection), Zoom(usize), Phrases(PhrasePoolCommand), - Editor(PhraseEditorCommand), + Editor(PhraseCommand), EditPhrase(Option>>), } diff --git a/crates/tek_tui/src/tui_arranger_jack.rs b/crates/tek_tui/src/tui_arranger_jack.rs index 1e2eeb77..1bcee333 100644 --- a/crates/tek_tui/src/tui_arranger_jack.rs +++ b/crates/tek_tui/src/tui_arranger_jack.rs @@ -7,19 +7,13 @@ impl JackApi for ArrangerTui { } impl Audio for ArrangerTui { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - TracksAudio( + #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + if TracksAudio( &mut self.app.tracks, &mut self.app.note_buf, &mut self.app.midi_buf, Default::default(), - ).process(client, scope) - } -} - -impl Audio for ArrangerTui { - #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - if self.process(client, scope) == Control::Quit { + ).process(client, scope) == Control::Quit { return Control::Quit } // FIXME: one of these per playing track diff --git a/crates/tek_tui/src/tui_arranger_view.rs b/crates/tek_tui/src/tui_arranger_view.rs index 13a6b2a1..2f59d0d2 100644 --- a/crates/tek_tui/src/tui_arranger_view.rs +++ b/crates/tek_tui/src/tui_arranger_view.rs @@ -57,7 +57,7 @@ impl Content for ArrangerTui { ), Split::right( self.splits[1], - widget(&self.phrases), + widget(&PhrasesView(self)), widget(&PhraseView(self)), ) ) diff --git a/crates/tek_tui/src/tui_phrase.rs b/crates/tek_tui/src/tui_phrase.rs index 41d72c8f..4318bf03 100644 --- a/crates/tek_tui/src/tui_phrase.rs +++ b/crates/tek_tui/src/tui_phrase.rs @@ -1,8 +1,7 @@ use crate::*; /// Contains state for viewing and editing a phrase -pub struct PhraseTui { - _engine: PhantomData, +pub struct PhraseTui { /// Phrase being played pub phrase: Option>>, /// Length of note that will be inserted, in pulses @@ -28,10 +27,10 @@ pub struct PhraseTui { /// Current position of global playhead pub now: Arc, /// Width and height of notes area at last render - pub size: Measure + pub size: Measure } -impl Widget for PhraseTui { +impl Widget for PhraseTui { type Engine = Tui; fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { PhraseView(&self, Default::default()).layout(to) @@ -41,192 +40,9 @@ impl Widget for PhraseTui { } } -pub struct PhraseView<'a, T: PhraseTuiViewState>(pub &'a T); - -pub trait PhraseTuiViewState: 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 PhraseTuiViewState 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: PhraseTuiViewState> 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 PhraseTui { +impl PhraseTui { pub fn new () -> Self { Self { - _engine: Default::default(), phrase: None, note_len: 24, notes_in: Arc::new(RwLock::new([false;128])), @@ -408,157 +224,3 @@ pub(crate) fn keys_vert () -> Buffer { const NTH_OCTAVE: [&'static str; 11] = [ "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", ]; - -#[derive(Clone, PartialEq, Debug)] -pub enum PhraseCommand { - // TODO: 1-9 seek markers that by default start every 8th of the phrase - ToggleDirection, - EnterEditMode, - ExitEditMode, - NoteAppend, - NoteSet, - NoteCursorSet(usize), - NoteLengthSet(usize), - NoteScrollSet(usize), - TimeCursorSet(usize), - TimeScrollSet(usize), - TimeZoomSet(usize), -} - -impl Handle for PhraseTui { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - PhraseCommand::execute_with_state(self, from) - } -} - -impl InputToCommand> for PhraseCommand { - fn input_to_command (state: &PhraseTui, from: &TuiInput) -> Option { - use PhraseCommand::*; - Some(match from.event() { - key!(KeyCode::Char('`')) => ToggleDirection, - key!(KeyCode::Enter) => EnterEditMode, - key!(KeyCode::Esc) => ExitEditMode, - key!(KeyCode::Char('[')) => NoteLengthSet(0), - key!(KeyCode::Char(']')) => NoteLengthSet(0), - key!(KeyCode::Char('a')) => NoteAppend, - key!(KeyCode::Char('s')) => NoteSet, - key!(KeyCode::Char('-')) => TimeZoomSet(0), - key!(KeyCode::Char('_')) => TimeZoomSet(0), - key!(KeyCode::Char('=')) => TimeZoomSet(0), - key!(KeyCode::Char('+')) => TimeZoomSet(0), - key!(KeyCode::PageUp) => NoteScrollSet(0), - key!(KeyCode::PageDown) => NoteScrollSet(0), - key!(KeyCode::Up) => match state.entered { - true => NoteCursorSet(0), - false => NoteScrollSet(0), - }, - key!(KeyCode::Down) => match state.entered { - true => NoteCursorSet(0), - false => NoteScrollSet(0), - }, - key!(KeyCode::Left) => match state.entered { - true => TimeCursorSet(0), - false => TimeScrollSet(0), - }, - key!(KeyCode::Right) => match state.entered { - true => TimeCursorSet(0), - false => TimeScrollSet(0), - }, - _ => return None - }) - } -} - -impl Command> for PhraseCommand { - //fn translate (self, state: &PhraseTui) -> Self { - //use PhraseCommand::*; - //match self { - //GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, }, - //GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, }, - //GoLeft => match state.entered { true => TimeCursorDec, false => TimeScrollDec, }, - //GoRight => match state.entered { true => TimeCursorInc, false => TimeScrollInc, }, - //_ => self - //} - //} - fn execute (self, state: &mut PhraseTui) -> Perhaps { - use PhraseCommand::*; - match self.translate(state) { - ToggleDirection => { - state.mode = !state.mode; - }, - EnterEditMode => { - state.entered = true; - }, - ExitEditMode => { - state.entered = false; - }, - TimeZoomOut => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().scale = next_note_length(scale) - }, - TimeZoomIn => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().scale = prev_note_length(scale) - }, - TimeCursorDec => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().point_dec(scale); - }, - TimeCursorInc => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().point_inc(scale); - }, - TimeScrollDec => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().start_dec(scale); - }, - TimeScrollInc => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().start_inc(scale); - }, - NoteCursorDec => { - let mut axis = state.note_axis.write().unwrap(); - axis.point_inc(1); - if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } } - }, - NoteCursorInc => { - let mut axis = state.note_axis.write().unwrap(); - axis.point_dec(1); - if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } } - }, - NoteScrollDec => { - state.note_axis.write().unwrap().start_inc(1); - }, - NoteScrollInc => { - state.note_axis.write().unwrap().start_dec(1); - }, - NoteLengthDec => { - state.note_len = prev_note_length(state.note_len) - }, - NoteLengthInc => { - state.note_len = next_note_length(state.note_len) - }, - NotePageUp => { - let mut axis = state.note_axis.write().unwrap(); - axis.start_dec(3); - axis.point_dec(3); - }, - NotePageDown => { - let mut axis = state.note_axis.write().unwrap(); - axis.start_inc(3); - axis.point_inc(3); - }, - NoteAppend => { - if state.entered { - state.put(); - state.time_cursor_advance(); - } - }, - NoteSet => { - if state.entered { state.put(); } - }, - _ => unreachable!() - } - Ok(None) - } -} diff --git a/crates/tek_tui/src/tui_phrase_cmd.rs b/crates/tek_tui/src/tui_phrase_cmd.rs new file mode 100644 index 00000000..30a657c1 --- /dev/null +++ b/crates/tek_tui/src/tui_phrase_cmd.rs @@ -0,0 +1,155 @@ +use crate::*; + +impl Handle for PhraseTui { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + PhraseCommand::execute_with_state(self, from) + } +} + +#[derive(Clone, PartialEq, Debug)] +pub enum PhraseCommand { + // TODO: 1-9 seek markers that by default start every 8th of the phrase + ToggleDirection, + EnterEditMode, + ExitEditMode, + NoteAppend, + NoteSet, + NoteCursorSet(usize), + NoteLengthSet(usize), + NoteScrollSet(usize), + TimeCursorSet(usize), + TimeScrollSet(usize), + TimeZoomSet(usize), +} + +impl InputToCommand for PhraseCommand { + fn input_to_command (state: &PhraseTui, from: &TuiInput) -> Option { + use PhraseCommand::*; + Some(match from.event() { + key!(KeyCode::Char('`')) => ToggleDirection, + key!(KeyCode::Enter) => EnterEditMode, + key!(KeyCode::Esc) => ExitEditMode, + key!(KeyCode::Char('[')) => NoteLengthSet(0), + key!(KeyCode::Char(']')) => NoteLengthSet(0), + key!(KeyCode::Char('a')) => NoteAppend, + key!(KeyCode::Char('s')) => NoteSet, + key!(KeyCode::Char('-')) => TimeZoomSet(0), + key!(KeyCode::Char('_')) => TimeZoomSet(0), + key!(KeyCode::Char('=')) => TimeZoomSet(0), + key!(KeyCode::Char('+')) => TimeZoomSet(0), + key!(KeyCode::PageUp) => NoteScrollSet(0), + key!(KeyCode::PageDown) => NoteScrollSet(0), + key!(KeyCode::Up) => match state.entered { + true => NoteCursorSet(0), + false => NoteScrollSet(0), + }, + key!(KeyCode::Down) => match state.entered { + true => NoteCursorSet(0), + false => NoteScrollSet(0), + }, + key!(KeyCode::Left) => match state.entered { + true => TimeCursorSet(0), + false => TimeScrollSet(0), + }, + key!(KeyCode::Right) => match state.entered { + true => TimeCursorSet(0), + false => TimeScrollSet(0), + }, + _ => return None + }) + } +} + +impl Command for PhraseCommand { + //fn translate (self, state: &PhraseTui) -> Self { + //use PhraseCommand::*; + //match self { + //GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, }, + //GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, }, + //GoLeft => match state.entered { true => TimeCursorDec, false => TimeScrollDec, }, + //GoRight => match state.entered { true => TimeCursorInc, false => TimeScrollInc, }, + //_ => self + //} + //} + fn execute (self, state: &mut PhraseTui) -> Perhaps { + use PhraseCommand::*; + match self.translate(state) { + ToggleDirection => { + state.mode = !state.mode; + }, + EnterEditMode => { + state.entered = true; + }, + ExitEditMode => { + state.entered = false; + }, + TimeZoomOut => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().scale = next_note_length(scale) + }, + TimeZoomIn => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().scale = prev_note_length(scale) + }, + TimeCursorDec => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().point_dec(scale); + }, + TimeCursorInc => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().point_inc(scale); + }, + TimeScrollDec => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().start_dec(scale); + }, + TimeScrollInc => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().start_inc(scale); + }, + NoteCursorDec => { + let mut axis = state.note_axis.write().unwrap(); + axis.point_inc(1); + if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } } + }, + NoteCursorInc => { + let mut axis = state.note_axis.write().unwrap(); + axis.point_dec(1); + if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } } + }, + NoteScrollDec => { + state.note_axis.write().unwrap().start_inc(1); + }, + NoteScrollInc => { + state.note_axis.write().unwrap().start_dec(1); + }, + NoteLengthDec => { + state.note_len = prev_note_length(state.note_len) + }, + NoteLengthInc => { + state.note_len = next_note_length(state.note_len) + }, + NotePageUp => { + let mut axis = state.note_axis.write().unwrap(); + axis.start_dec(3); + axis.point_dec(3); + }, + NotePageDown => { + let mut axis = state.note_axis.write().unwrap(); + axis.start_inc(3); + axis.point_inc(3); + }, + NoteAppend => { + if state.entered { + state.put(); + state.time_cursor_advance(); + } + }, + NoteSet => { + if state.entered { state.put(); } + }, + _ => unreachable!() + } + Ok(None) + } +} diff --git a/crates/tek_tui/src/tui_phrase_view.rs b/crates/tek_tui/src/tui_phrase_view.rs new file mode 100644 index 00000000..fbaf9def --- /dev/null +++ b/crates/tek_tui/src/tui_phrase_view.rs @@ -0,0 +1,183 @@ +use crate::*; + +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(), + ) + } +} diff --git a/crates/tek_tui/src/tui_pool_cmd.rs b/crates/tek_tui/src/tui_pool_cmd.rs index c2d6a822..e01ed388 100644 --- a/crates/tek_tui/src/tui_pool_cmd.rs +++ b/crates/tek_tui/src/tui_pool_cmd.rs @@ -1,6 +1,6 @@ use crate::*; -impl Handle for PhrasePoolView { +impl Handle for PhrasesTui { fn handle (&mut self, from: &TuiInput) -> Perhaps { PhrasesCommand::execute_with_state(self, from) } @@ -33,8 +33,8 @@ pub enum PhraseLengthCommand { Cancel, } -impl InputToCommand> for PhrasesCommand { - fn input_to_command (state: &PhrasePoolView, input: &TuiInput) -> Option { +impl InputToCommand for PhrasesCommand { + fn input_to_command (state: &PhrasesTui, input: &TuiInput) -> Option { use PhrasesCommand as Cmd; use PhrasePoolCommand as Edit; use PhraseRenameCommand as Rename; @@ -52,10 +52,10 @@ impl InputToCommand> for PhrasesCommand { key!(KeyCode::Char('n')) => Some(Cmd::Rename(Rename::Begin)), key!(KeyCode::Char('t')) => Some(Cmd::Length(Length::Begin)), _ => match state.mode { - Some(PhrasePoolMode::Rename(..)) => { + Some(PhrasesMode::Rename(..)) => { Rename::input_to_command(state, input).map(Cmd::Rename) }, - Some(PhrasePoolMode::Length(..)) => { + Some(PhrasesMode::Length(..)) => { Length::input_to_command(state, input).map(Cmd::Length) }, _ => None @@ -64,8 +64,8 @@ impl InputToCommand> for PhrasesCommand { } } -impl Command> for PhrasesCommand { - fn execute (self, view: &mut PhrasePoolView) -> Perhaps { +impl Command for PhrasesCommand { + fn execute (self, view: &mut PhrasesTui) -> Perhaps { use PhraseRenameCommand as Rename; use PhraseLengthCommand as Length; match self { @@ -77,7 +77,7 @@ impl Command> for PhrasesCommand { } Self::Rename(command) => match command { Rename::Begin => { - view.mode = Some(PhrasePoolMode::Rename( + view.mode = Some(PhrasesMode::Rename( view.phrase, view.phrases[view.phrase].read().unwrap().name.clone() )) @@ -88,7 +88,7 @@ impl Command> for PhrasesCommand { }, Self::Length(command) => match command { Length::Begin => { - view.mode = Some(PhrasePoolMode::Length( + view.mode = Some(PhrasesMode::Length( view.phrase, view.phrases[view.phrase].read().unwrap().length, PhraseLengthFocus::Bar @@ -103,9 +103,9 @@ impl Command> for PhrasesCommand { } } -impl InputToCommand> for PhraseLengthCommand { - fn input_to_command (view: &PhrasePoolView, from: &TuiInput) -> Option { - if let Some(PhrasePoolMode::Length(_, length, _)) = view.mode { +impl InputToCommand for PhraseLengthCommand { + fn input_to_command (view: &PhrasesTui, from: &TuiInput) -> Option { + if let Some(PhrasesMode::Length(_, length, _)) = view.mode { Some(match from.event() { key!(KeyCode::Up) => Self::Inc, key!(KeyCode::Down) => Self::Dec, @@ -121,11 +121,11 @@ impl InputToCommand> for PhraseLengthCommand { } } -impl Command> for PhraseLengthCommand { - fn execute (self, view: &mut PhrasePoolView) -> Perhaps { +impl Command for PhraseLengthCommand { + fn execute (self, view: &mut PhrasesTui) -> Perhaps { use PhraseLengthFocus::*; use PhraseLengthCommand::*; - if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = view.mode { + if let Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) = view.mode { match self { Self::Cancel => { view.mode = None; @@ -157,7 +157,7 @@ impl Command> for PhraseLengthCommand { } Ok(None) } else if self == Begin { - view.mode = Some(PhrasePoolMode::Length( + view.mode = Some(PhrasesMode::Length( view.phrase, view.phrases[view.phrase].read().unwrap().length, PhraseLengthFocus::Bar @@ -169,9 +169,9 @@ impl Command> for PhraseLengthCommand { } } -impl InputToCommand> for PhraseRenameCommand { - fn input_to_command (view: &PhrasePoolView, from: &TuiInput) -> Option { - if let Some(PhrasePoolMode::Rename(_, ref old_name)) = view.mode { +impl InputToCommand for PhraseRenameCommand { + fn input_to_command (view: &PhrasesTui, from: &TuiInput) -> Option { + if let Some(PhrasesMode::Rename(_, ref old_name)) = view.mode { Some(match from.event() { key!(KeyCode::Char(c)) => { let mut new_name = old_name.clone(); @@ -193,10 +193,10 @@ impl InputToCommand> for PhraseRenameCommand { } } -impl Command> for PhraseRenameCommand { - fn execute (self, view: &mut PhrasePoolView) -> Perhaps { +impl Command for PhraseRenameCommand { + fn execute (self, view: &mut PhrasesTui) -> Perhaps { use PhraseRenameCommand::*; - if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = view.mode { + if let Some(PhrasesMode::Rename(phrase, ref mut old_name)) = view.mode { match self { Set(s) => { view.phrases[phrase].write().unwrap().name = s.into(); @@ -215,7 +215,7 @@ impl Command> for PhraseRenameCommand { }; Ok(None) } else if self == Begin { - view.mode = Some(PhrasePoolMode::Rename( + view.mode = Some(PhrasesMode::Rename( view.phrase, view.phrases[view.phrase].read().unwrap().name.clone() )); diff --git a/crates/tek_tui/src/tui_pool_view.rs b/crates/tek_tui/src/tui_pool_view.rs index 56144e46..76250360 100644 --- a/crates/tek_tui/src/tui_pool_view.rs +++ b/crates/tek_tui/src/tui_pool_view.rs @@ -42,17 +42,17 @@ pub struct PhrasesView<'a, T: PhrasesViewState>(&'a T); impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> { type Engine = Tui; fn content (&self) -> impl Widget { - let focused = self.focused(); - let entered = self.entered(); - let phrases = self.phrases(); - let phrase = self.phrase(); - let mode = self.mode(); + let focused = self.0.focused(); + let entered = self.0.entered(); + let phrases = self.0.phrases(); + let 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 { + if focused && i == phrase { length.pulses = new_length; length.focus = Some(focus); } @@ -61,17 +61,17 @@ impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> { 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}▄"); } + if focused && i == phrase { row2 = format!("{row2}▄"); } }; let row2 = TuiStyle::bold(row2, true); add(&col!(row1, row2).fill_x().bg(color.base.rgb))?; - Ok(if *focused && i == phrase { add(&CORNERS)?; }) + Ok(if focused && i == phrase { add(&CORNERS)?; }) }) ); - let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)}; + 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 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!("({})", phrases.len()); lay!( diff --git a/crates/tek_tui/src/tui_sequencer.rs b/crates/tek_tui/src/tui_sequencer.rs index e840a1fb..fe53fa33 100644 --- a/crates/tek_tui/src/tui_sequencer.rs +++ b/crates/tek_tui/src/tui_sequencer.rs @@ -83,8 +83,8 @@ impl Content for SequencerTui { col!( widget(&TransportRefView(self)), Split::right(20, - widget(&self.phrases), - widget(&self.editor) + widget(&PhrasesView(self)), + widget(&PhraseView(self)), ).min_y(20) ) } diff --git a/crates/tek_tui/src/tui_sequencer_cmd.rs b/crates/tek_tui/src/tui_sequencer_cmd.rs index 88ddcbc9..f3dcbe77 100644 --- a/crates/tek_tui/src/tui_sequencer_cmd.rs +++ b/crates/tek_tui/src/tui_sequencer_cmd.rs @@ -49,9 +49,10 @@ impl Command for SequencerCommand { use SequencerCommand::*; match self { Focus(cmd) => delegate(cmd, Focus, state), - Phrases(cmd) => delegate(cmd, Phrases, &mut state.phrases), - Editor(cmd) => delegate(cmd, Editor, &mut state.editor), - Transport(cmd) => delegate(cmd, Transport, &mut state.transport) + Phrases(cmd) => delegate(cmd, Phrases, &mut state.phrases), + Editor(cmd) => delegate(cmd, Editor, &mut state.editor), + Clock(cmd) => delegate(cmd, Clock, &mut state.transport), + Playhead(cmd) => delegate(cmd, Playhead, &mut state.transport) } } } diff --git a/crates/tek_tui/src/tui_transport.rs b/crates/tek_tui/src/tui_transport.rs index 1bd4f4c1..64cef671 100644 --- a/crates/tek_tui/src/tui_transport.rs +++ b/crates/tek_tui/src/tui_transport.rs @@ -19,7 +19,7 @@ pub struct TransportTui { metronome: bool, focus: TransportFocus, focused: bool, - size: Measure, + size: Measure, } /// Create app state from JACK handle. diff --git a/crates/tek_tui/src/tui_transport_focus.rs b/crates/tek_tui/src/tui_transport_focus.rs index a55d5ad9..4a6d75cb 100644 --- a/crates/tek_tui/src/tui_transport_focus.rs +++ b/crates/tek_tui/src/tui_transport_focus.rs @@ -45,6 +45,7 @@ impl HasFocus for TransportTui { } impl FocusEnter for TransportTui { + type Item = TransportFocus; fn focus_enter (&mut self) { self.entered = true; }