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)) }) } } /// Handle top-level events in standalone arranger. impl Handle for Sequencer { fn handle (&mut self, from: &TuiInput) -> Perhaps { if !match self.focused() { SequencerFocus::Transport => self.transport.handle(from)?, SequencerFocus::PhrasePool => self.phrases.handle(from)?, SequencerFocus::PhraseEditor => self.editor.handle(from)? }.unwrap_or(false) { match from.event() { // Tab navigation key!(KeyCode::Tab) => { self.focus_next(); }, key!(Shift-KeyCode::Tab) => { self.focus_prev(); }, key!(KeyCode::BackTab) => { self.focus_prev(); }, key!(Shift-KeyCode::BackTab) => { self.focus_prev(); }, // Directional navigation key!(KeyCode::Up) => { self.focus_up(); }, key!(KeyCode::Down) => { self.focus_down(); }, key!(KeyCode::Left) => { self.focus_left(); }, key!(KeyCode::Right) => { self.focus_right(); }, // Global play/pause binding key!(KeyCode::Char(' ')) => match self.transport { Some(ref mut transport) => { transport.write().unwrap().toggle_play()?; }, None => { return Ok(None) } }, _ => {} } }; Ok(Some(true)) } } // 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 color = phrase.read().unwrap().color; add(&format!(" {i}").fixed_y(2).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 Handle for PhrasePool { fn handle (&mut self, from: &TuiInput) -> Perhaps { match from.event() { key!(KeyCode::Up) => self.phrase = if self.phrase > 0 { self.phrase - 1 } else { self.phrases.len() - 1 }, key!(KeyCode::Down) => { self.phrase = (self.phrase + 1) % self.phrases.len() }, key!(KeyCode::Char('i')) => { self.phrases.insert(self.phrase, Arc::new(RwLock::new(Phrase::default()))); self.phrase += 1; }, key!(KeyCode::Char('a')) => { self.phrases.push(Arc::new(RwLock::new(Phrase::default()))); self.phrase = self.phrases.len() - 1; }, _ => return Ok(None), } return Ok(Some(true)) } } 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|{ 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!(toolbar, 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 Handle for PhraseEditor { fn handle (&mut self, from: &TuiInput) -> Perhaps { match from.event() { key!(KeyCode::Char('`')) => { self.mode = !self.mode; }, key!(KeyCode::Up) => match self.entered { true => { self.note_axis.point_dec(); }, false => { self.note_axis.start_dec(); }, }, key!(KeyCode::Down) => match self.entered { true => { self.note_axis.point_inc(); }, false => { self.note_axis.start_inc(); }, }, key!(KeyCode::Left) => match self.entered { true => { self.time_axis.point_dec(); }, false => { self.time_axis.start_dec(); }, }, key!(KeyCode::Right) => match self.entered { true => { self.time_axis.point_inc(); }, false => { self.time_axis.start_inc(); }, }, _ => { return Ok(None) } } return Ok(Some(true)) } } 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