use crate::*; pub struct PhraseView<'a> { focused: bool, entered: bool, phrase: &'a Option>>, buffer: &'a BigBuffer, note_len: usize, now: &'a Arc, size: &'a Measure, view_mode: &'a PhraseViewMode, note_point: usize, note_range: (usize, usize), note_names: (&'a str, &'a str), time_start: usize, time_point: usize, } impl<'a, T: HasEditor> From<&'a T> for PhraseView<'a> { fn from (state: &'a T) -> Self { let editor = state.editor(); let height = editor.size.h(); let note_point = editor.note_point.load(Ordering::Relaxed); let mut note_lo = editor.note_lo.load(Ordering::Relaxed); if note_point < note_lo { note_lo = note_point; editor.note_lo.store(note_lo, Ordering::Relaxed); } let mut note_hi = 127.min(note_lo + height + 1); if note_point > note_hi { note_lo += note_point - note_hi; note_hi = note_point; editor.note_lo.store(note_lo, Ordering::Relaxed); } Self { focused: state.editor_focused(), entered: state.editor_entered(), note_len: editor.note_len, phrase: &editor.phrase, buffer: &editor.buffer, now: &editor.now, size: &editor.size, view_mode: &editor.view_mode, note_point, note_range: (note_lo, note_hi), note_names: (to_note_name(note_lo), to_note_name(note_hi)), time_start: editor.time_start.load(Ordering::Relaxed), time_point: editor.time_point.load(Ordering::Relaxed), } } } impl<'a> Content for PhraseView<'a> { type Engine = Tui; fn content (&self) -> impl Widget { let Self { focused, entered, phrase, size, buffer, view_mode, note_len, note_range: (note_lo, note_hi), note_names: (note_lo_name, note_hi_name), note_point, time_start, time_point, now, .. } = self; let upper_left = format!( "{note_hi} {note_hi_name} {}", phrase.as_ref().map(|p|p.read().unwrap().name.clone()).unwrap_or(String::new()) ); let lower_left = format!( "{note_lo} {note_lo_name}" ); let mut lower_right = format!( "┤{}├", size.format() ); if *focused && *entered { lower_right = format!("┤Note: {} ({}) {}├─{lower_right}", note_point, to_note_name(*note_point), pulses_to_name(*note_len) ); } let mut upper_right = format!( "[{}]", if *entered {"■"} else {" "} ); if let Some(phrase) = phrase { upper_right = format!("┤Time: {}/{} {}├{upper_right}", time_point, phrase.read().unwrap().length, pulses_to_name(view_mode.time_zoom()), ) }; let title_color = if *focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)}; lay!( row!( CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(2))), move|to: &mut TuiOutput|{ Ok(if to.area().h() >= 2 { view_mode.blit_keys(to, *note_hi, *note_lo) }) }).fill_y(), lay!( CustomWidget::new(|to|Ok(Some(to)), |to: &mut TuiOutput|{ size.set_wh(to.area.w(), to.area.h() as usize - 1); let draw = to.area().h() >= 2; Ok(if draw { view_mode.blit_notes( to, buffer, *time_start, *note_hi ) }) }).fill_x(), CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ Ok(if *focused && *entered { view_mode.blit_cursor( to, *time_point, *time_start, view_mode.time_zoom(), *note_point, *note_len, *note_hi, *note_lo, ) }) }) ).fill_x() ).fill_x().bg(Color::Rgb(40, 50, 30)).border(Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(if *focused{ Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) }))), CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_h(1))), move|to: &mut TuiOutput|{ //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(); //if let Some(_) = phrase { //let now = now.get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length; //let time_clamp = time_clamp; //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(), TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw(), TuiStyle::fg(lower_left.to_string(), title_color).push_x(1).align_sw(), 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(), ) } } #[derive(Copy, Clone, Debug)] pub enum PhraseViewMode { PianoHorizontal { time_zoom: usize, note_zoom: PhraseViewNoteZoom, }, PianoVertical { time_zoom: usize, note_zoom: PhraseViewNoteZoom, }, } #[derive(Copy, Clone, Debug)] pub enum PhraseViewNoteZoom { N(usize), Half, Octant, } impl PhraseViewMode { pub fn time_zoom (&self) -> usize { match self { Self::PianoHorizontal { time_zoom, .. } => *time_zoom, _ => unimplemented!() } } pub fn set_time_zoom (&mut self, time_zoom: usize) { *self = match self { Self::PianoHorizontal { note_zoom, .. } => Self::PianoHorizontal { note_zoom: *note_zoom, time_zoom, }, _ => unimplemented!() } } /// Return a new [BigBuffer] containing a render of the phrase. pub fn draw (&self, phrase: &Phrase) -> BigBuffer { let mut buffer = BigBuffer::new(self.buffer_width(phrase), self.buffer_height(phrase)); match self { Self::PianoHorizontal { time_zoom, note_zoom } => match note_zoom { PhraseViewNoteZoom::N(_) => Self::draw_piano_horizontal( &mut buffer, phrase, *time_zoom, 1 ), _ => unimplemented!(), }, _ => unimplemented!(), } buffer } /// Draw a subsection of the [BigBuffer] onto a regular ratatui [Buffer]. fn blit_notes ( &self, target: &mut TuiOutput, source: &BigBuffer, time_start: usize, note_hi: usize, ) { let area = target.area(); let target = &mut target.buffer; match self { Self::PianoHorizontal { .. } => { let [x0, y0, w, h] = area.xywh(); for (x, target_x) in (x0..x0+w).enumerate() { for (y, target_y) in (y0..y0+h).enumerate() { let source_x = time_start + x; let source_y = note_hi - y; // TODO: enable loop rollover: //let source_x = (time_start + x) % source.width.max(1); //let source_y = (note_hi - y) % source.height.max(1); if source_x < source.width && source_y < source.height { let target_cell = target.get_mut(target_x, target_y); target_cell.set_char('x'); if let Some(source_cell) = source.get(source_x, source_y) { *target_cell = source_cell.clone(); } } } } }, _ => unimplemented!() } } fn blit_keys (&self, to: &mut TuiOutput, note_hi: usize, note_lo: usize) { let style = Some(Style::default().fg(Color::White).bg(Color::Rgb(0, 0, 0))); match self { Self::PianoHorizontal { .. } => { let [x0, y0, _, h] = to.area().xywh(); for (y, note) in (note_lo..note_hi).rev().enumerate() { to.blit(&match note % 12 { 11 => "██", 10 => " ", 9 => "██", 8 => " ", 7 => "██", 6 => " ", 5 => "██", 4 => "██", 3 => " ", 2 => "██", 1 => " ", 0 => "██", _ => unreachable!(), }, x0, y0 + y as u16, style) } }, _ => unimplemented!() } } fn blit_cursor ( &self, to: &mut TuiOutput, time_point: usize, time_start: usize, time_scale: usize, note_point: usize, note_len: usize, note_hi: usize, note_lo: usize, ) { match self { Self::PianoHorizontal { .. } => { let [x0, y0, w, h] = to.area().xywh(); for (y, note) in (note_lo..note_hi).rev().enumerate() { if note == note_point { for x in 0..w { let time_1 = time_start + x as usize * time_scale; let time_2 = time_1 + time_scale; if time_1 <= time_point && time_point < time_2 { to.blit(&"█", x0 + x as u16, y0 + y as u16, None); break } } break } } }, _ => unimplemented!() } } /// Determine the required width to render the phrase. fn buffer_width (&self, phrase: &Phrase) -> usize { match self { Self::PianoHorizontal { time_zoom, .. } => { phrase.length / time_zoom }, Self::PianoVertical { note_zoom, .. } => match note_zoom { PhraseViewNoteZoom::Half => 64, PhraseViewNoteZoom::N(n) => 128*n, _ => unimplemented!() }, } } /// Determine the required height to render the phrase. fn buffer_height (&self, phrase: &Phrase) -> usize { match self { Self::PianoHorizontal { note_zoom, .. } => match note_zoom { PhraseViewNoteZoom::Half => 64, PhraseViewNoteZoom::N(n) => 128*n, _ => unimplemented!() }, Self::PianoVertical { time_zoom, .. } => { phrase.length / time_zoom }, } } /// Draw the piano roll using full blocks on note on and half blocks on legato: █▄ █▄ █▄ fn draw_piano_horizontal ( target: &mut BigBuffer, phrase: &Phrase, time_zoom: usize, _: usize ) { //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)); let mut notes_on = [false;128]; for (y, _) in (127..0).enumerate() { for (x, _) in (0..phrase.length).step_by(time_zoom).enumerate() { let cell = target.get_mut(x, y).unwrap(); cell.set_fg(Color::Rgb(28, 35, 25)); cell.set_char('·'); } } for (x, time_start) in (0..phrase.length).step_by(time_zoom).enumerate() { let time_end = time_start + time_zoom; for time in time_start..time_end { for (y, note) in (127..0).enumerate() { let cell = target.get_mut(x, y).unwrap(); if notes_on[note] { cell.set_fg(Color::Rgb(255, 255, 255)); cell.set_bg(Color::Rgb(0, 0, 0)); cell.set_char('▄'); } else { cell.set_char('x'); } } for event in phrase.notes[time].iter() { match event { MidiMessage::NoteOn { key, .. } => { let note = key.as_int() as usize; target.get_mut(x, 127 - note).unwrap().set_char('█'); notes_on[note] = true }, MidiMessage::NoteOff { key, .. } => { notes_on[key.as_int() as usize] = false }, _ => {} } } } } } /// TODO: Draw the piano roll using octant blocks (U+1CD00-U+1CDE5) fn draw_piano_horizontal_octant ( _: &mut BigBuffer, _: &Phrase, _: usize ) { unimplemented!() } /// TODO: Draw the piano roll using half blocks: ▄▀▄ fn draw_piano_horizontal_half ( _: &mut BigBuffer, _: &Phrase, _: usize ) { unimplemented!() } } //impl PhraseEditorModel { //pub(crate) fn redraw (phrase: &Phrase, mode: PhraseViewMode) -> BigBuffer { //let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65); //Self::fill_seq_bg(mode, &mut buf, phrase.length, phrase.ppq); //Self::fill_seq_fg(mode, &mut buf, &phrase); //buf //} //fn fill_seq_bg (_mode: PhraseViewMode, 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 { //if y >= 64 { //break //} //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 (mode: PhraseViewMode, buf: &mut BigBuffer, phrase: &Phrase) { //match mode { //PhraseViewMode::Horizontal => //Self::fill_seq_fg_horizontal(buf, phrase), //PhraseViewMode::HorizontalHalf => //Self::fill_seq_fg_horizontal_half(buf, phrase), //PhraseViewMode::Vertical => //Self::fill_seq_fg_vertical(buf, phrase), //} //} //fn fill_seq_fg_horizontal (buf: &mut BigBuffer, phrase: &Phrase) { //let mut notes_on = [false;128]; //for x in 0..buf.width { //} //} //fn fill_seq_fg_horizontal_half (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 > 63 { //break //} //let y = 63 - y; //if let Some(block) = half_block( //notes_on[y as usize * 2 + 1], //notes_on[y as usize * 2], //) { //buf.get_mut(x, y).map(|cell|{ //cell.set_char(block); //cell.set_fg(Color::White); //}); //} //} //if phrase.percussive { //notes_on.fill(false); //} //} //} //} //} ////const NTH_OCTAVE: [&'static str; 11] = [ ////"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "X", //////"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", ////];