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 Self { focused, phrases, mode, .. } = self; 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(PhrasePoolMode::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(PhrasePoolMode::Rename(phrase, _)) = mode { if *focused && i == *phrase { row2 = format!("{row2}▄"); } }; let row2 = TuiStyle::bold(row2, true); let bg = if i == self.phrase { color } else { color }; add(&col!(row1, row2).fill_x().bg(bg))?; if *focused && i == self.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 title = format!("Phrases ({})", phrases.len()); let title = TuiStyle::fg(title, title_color).push_x(1); Layers::new(move|add|{ add(&content)?; add(&title)?; Ok(()) }) } } impl Content for PhraseEditor { type Engine = Tui; fn content (&self) -> impl Widget { let Self { focused, entered, keys, phrase, buffer, note_len, .. } = self; let FixedAxis { start: note_start, point: note_point, clamp: note_clamp } = *self.note_axis.read().unwrap(); let ScaledAxis { start: time_start, point: time_point, clamp: time_clamp, scale: time_scale } = *self.time_axis.read().unwrap(); let color = Color::Rgb(0,255,0); let color = phrase.as_ref().map(|p|p.read().unwrap().color).unwrap_or(color); let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{ if to.area().h() >= 2 { to.buffer_update(to.area().set_w(5), &|cell, x, y|{ let y = y + note_start as u16; if x < keys.area.width && y < keys.area.height { *cell = keys.get(x, y).clone() } }); } Ok(()) }).fill_y(); let notes_bg_null = Color::Rgb(28, 35, 25); let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ let w = to.area.w() as usize; let h = to.area.h() as usize; self.width.store(w, Ordering::Relaxed); self.height.store(h, Ordering::Relaxed); let mut axis = self.note_axis.write().unwrap(); if let Some(point) = axis.point { if point.saturating_sub(axis.start) >= (h * 2).saturating_sub(1) { axis.start += 1; } } 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; if src_x < buffer.width && src_y < buffer.height - 1 { buffer.get(src_x, buffer.height - src_y - 1).map(|src|{ cell.set_symbol(src.symbol()); cell.set_fg(src.fg); cell.set_bg(src.bg); }); } }); } Ok(()) }).fill_x(); let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ if *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 + (self.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))); } } } Ok(()) }); let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(0,0,0)); 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.now.load(Ordering::Relaxed); // 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 { let this_step = (x * time_scale + time_start + 0) * time_scale; let next_step = (x * time_scale + time_start + 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_bg = Color::Rgb(40, 50, 30); let content = piano_roll.bg(content_bg).border(border); let content = lay!(content, playhead); let mut upper_left = String::from("Sequencer"); if let Some(phrase) = phrase { upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name); } let mut upper_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 { upper_right = format!("Note: {} (+{}:{}|{}) {upper_right}", pulses_to_name(*note_len), note_start, note_point.unwrap_or(0), note_clamp.unwrap_or(0), ); } let lower_right = format!( "{}x{}", self.width.load(Ordering::Relaxed), self.height.load(Ordering::Relaxed), ); 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 PhraseEditor { pub fn put (&mut self) { if let (Some(phrase), Some(time), Some(note)) = ( &self.phrase, self.time_axis.read().unwrap().point, self.note_axis.read().unwrap().point, ) { let mut phrase = phrase.write().unwrap(); let key: u7 = u7::from((127 - note) as u8); let vel: u7 = 100.into(); let start = time; let end = (start + self.note_len) % phrase.length; phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); self.buffer = Self::redraw(&phrase); } } /// 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()); self.time_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length); self.buffer = Self::redraw(&*phrase.read().unwrap()); } else { self.phrase = None; self.time_axis.write().unwrap().clamp = Some(0); self.buffer = Default::default(); } } fn redraw (phrase: &Phrase) -> BigBuffer { let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65); Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq); Self::fill_seq_fg(&mut buf, &phrase); buf } 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::Rgb(48, 64, 56)); 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 { 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); } } } } } /// Colors of piano keys const KEY_COLORS: [(Color, Color);6] = [ (Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)), (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), ]; pub(crate) fn keys_vert () -> Buffer { let area = [0, 0, 5, 64]; let mut buffer = Buffer::empty(Rect { x: area.x(), y: area.y(), width: area.w(), height: area.h() }); buffer_update(&mut buffer, area, &|cell, x, y| { let y = 63 - y; match x { 0 => { cell.set_char('▀'); let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize]; cell.set_fg(fg); cell.set_bg(bg); }, 1 => { cell.set_char('▀'); cell.set_fg(Color::White); cell.set_bg(Color::White); }, 2 => if y % 6 == 0 { cell.set_char('C'); }, 3 => if y % 6 == 0 { cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); }, _ => {} } }); buffer } 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(), "]" )), } }) } }