use crate::*; impl Content for Sequencer { type Engine = Tui; fn content (&self) -> impl Widget { Stack::down(move|add|{ add(&self.transport)?; add(&self.phrases.clone() .split(Direction::Right, 20, &self.editor as &dyn Widget) .min_y(20)) }) } } // TODO: Display phrases always in order of appearance impl Content for PhrasePool { type Engine = Tui; fn content (&self) -> impl Widget { let content = col!( (i, phrase) in self.phrases.iter().enumerate() => Layers::new(|add|{ let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); let row1 = lay!(format!(" {i}").align_w().fill_x(), if let Some(PhrasePoolMode::Length(phrase, new_length, focus)) = self.mode { if self.focused && i == phrase { PhraseLength::new(new_length, Some(focus)) } else { PhraseLength::new(length, None) } } else { PhraseLength::new(length, None) }.align_e().fill_x() ).fill_x(); let row2 = if let Some(PhrasePoolMode::Rename(phrase, _)) = self.mode { if self.focused && i == phrase { format!(" {}▄", name) } else { format!(" {}", name) } } else { format!(" {}", name) }; add(&col!(row1, row2).fill_x().bg(if i == self.phrase { color //Color::Rgb(40, 50, 30) } else { color //Color::Rgb(28, 35, 25) }))?; if self.focused && i == self.phrase { add(&CORNERS)?; } Ok(()) }) ) .fill_xy() .bg(Color::Rgb(28, 35, 25)) .border(Lozenge(Style::default() .bg(Color::Rgb(40, 50, 30)) .fg(if self.focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) }))); lay!(content, TuiStyle::fg("Phrases", if self.focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) }).push_x(1)) } } impl Content for PhraseEditor { type Engine = Tui; fn content (&self) -> impl Widget { //let field_bg = Color::Rgb(28, 35, 25); //let toolbar = Stack::down(move|add|{ ////let name = format!("{:>9}", self.name.read().unwrap().as_str()); ////add(&col!("Track:", TuiStyle::bg(name.as_str(), field_bg)))?; //if let Some(phrase) = &self.phrase { //let phrase = phrase.read().unwrap(); //let length = format!("{}q{}p", phrase.length / PPQ, phrase.length % PPQ); //let length = format!("{:>9}", &length); //let loop_on = format!("{:>9}", if phrase.loop_on { "on" } else { "off" }); //let loop_start = format!("{:>9}", phrase.loop_start); //let loop_end = format!("{:>9}", phrase.loop_length); //add(&"")?; //add(&col!("Length:", TuiStyle::bg(length.as_str(), field_bg)))?; //add(&col!("Loop:", TuiStyle::bg(loop_on.as_str(), field_bg)))?; //add(&col!("L. start:", TuiStyle::bg(loop_start.as_str(), field_bg)))?; //add(&col!("L. length:", TuiStyle::bg(loop_end.as_str(), field_bg)))?; //} //Ok(()) //}).min_x(10); let piano_roll = lay!( // keys CustomWidget::new(|_|Ok(Some([32u16,4u16])), |to: &mut TuiOutput|{ if to.area().h() < 2 { return Ok(()) } Ok(to.buffer_update(to.area().set_w(5).shrink_y(2), &|cell, x, y|{ let y = y + self.note_axis.start as u16; if x < self.keys.area.width && y < self.keys.area.height { *cell = self.keys.get(x, y).clone() } })) }).fill_y(), // playhead CustomWidget::new(|_|Ok(Some([32u16,2u16])), |to: &mut TuiOutput|{ if let Some(phrase) = &self.phrase { let time_0 = self.time_axis.start; let time_z = self.time_axis.scale; let now = 0; // TODO FIXME: self.now % phrase.read().unwrap().length; let [x, y, width, _] = to.area(); let x2 = x as usize + Self::H_KEYS_OFFSET; let x3 = x as usize + width as usize; for x in x2..x3 { let step = (time_0 + x2) * time_z; let next_step = (time_0 + x2 + 1) * time_z; to.blit(&"-", x as u16, y, Some(PhraseEditor::::style_timer_step( now, step as usize, next_step as usize ))); } } Ok(()) }).fill_x(), // notes CustomWidget::new(|_|Ok(Some([32u16,4u16])), |to: &mut TuiOutput|{ let offset = Self::H_KEYS_OFFSET as u16; if to.area().h() < 2 || to.area().w() < offset { return Ok(()) } let area = to.area().push_x(offset).shrink_x(offset).shrink_y(2); Ok(to.buffer_update(area, &move |cell, x, y|{ cell.set_bg(Color::Rgb(20, 20, 20)); let src_x = ((x as usize + self.time_axis.start) * self.time_axis.scale) as usize; let src_y = (y as usize + self.note_axis.start) as usize; if src_x < self.buffer.width && src_y < self.buffer.height - 1 { let src = self.buffer.get(src_x, self.buffer.height - src_y); src.map(|src|{ cell.set_symbol(src.symbol()); cell.set_fg(src.fg); }); } })) }).fill_x(), // note cursor CustomWidget::new(|_|Ok(Some([1u16,1u16])), |to: &mut TuiOutput|{ if self.entered { let area = to.area(); if let (Some(time), Some(note)) = (self.time_axis.point, self.note_axis.point) { let x = area.x() + Self::H_KEYS_OFFSET as u16 + time as u16; let y = area.y() + 1 + note as u16 / 2; let c = if note % 2 == 0 { "▀" } else { "▄" }; to.blit(&c, x, y, self.style_focus()); } } Ok(()) }), // zoom CustomWidget::new(|_|Ok(Some([10u16,1u16])), |to: &mut TuiOutput|{ let [x, y, w, h] = to.area.xywh(); let quant = ppq_to_name(self.time_axis.scale); Ok(to.blit( &quant, x + w - 1 - quant.len() as u16, y + h - 2, self.style_focus() )) }), ); let content = row!(piano_roll) .fill_x() .bg(Color::Rgb(40, 50, 30)) .border(Lozenge(Style::default() .bg(Color::Rgb(40, 50, 30)) .fg(if self.focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) }))); lay!(content, TuiStyle::fg("Sequencer", if self.focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) }).push_x(1)) } } impl PhraseEditor { const H_KEYS_OFFSET: usize = 5; /// Select which pattern to display. This pre-renders it to the buffer at full resolution. pub fn show (&mut self, phrase: Option<&Arc>>) { if let Some(phrase) = phrase { self.phrase = Some(phrase.clone()); let width = usize::MAX.min(phrase.read().unwrap().length); let mut buffer = BigBuffer::new(width, 64); let phrase = phrase.read().unwrap(); Self::fill_seq_bg(&mut buffer, phrase.length, phrase.ppq); Self::fill_seq_fg(&mut buffer, &phrase); self.buffer = buffer; } else { self.phrase = None; self.buffer = Default::default(); } } fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) { for x in 0..buf.width { if x as usize >= length { break } let style = Style::default(); buf.get_mut(x, 0).map(|cell|{ cell.set_char('-'); cell.set_style(style); }); 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::Gray); 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/2 { 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); } } } } pub(crate) fn style_focus (&self) -> Option