use crate::*; pub struct TransportView<'a, T: TransportViewState>(pub &'a T); pub struct PhrasesView<'a, T: PhrasesViewState>(pub &'a T); pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T); pub trait TransportViewState: ClockApi + Send + Sync { fn transport_selected (&self) -> Option; fn transport_focused (&self) -> bool { self.transport_selected().is_some() } fn transport_bpm_value (&self) -> f64 { self.bpm().get() } fn transport_sync_value (&self) -> f64 { self.sync().get() } fn transport_format_beat (&self) -> String { self.current().format_beat() } fn transport_format_msu (&self) -> String { self.current().usec.format_msu() } } impl TransportViewState for TransportTui { fn transport_selected (&self) -> Option { if let AppFocus::Content(focus) = self.focus.inner() { Some(focus) } else { None } } } impl TransportViewState for SequencerTui { fn transport_selected (&self) -> Option { if let AppFocus::Content(SequencerFocus::Transport(focus)) = self.focus.inner() { Some(focus) } else { None } } } impl TransportViewState for ArrangerTui { fn transport_selected (&self) -> Option { if let AppFocus::Content(ArrangerFocus::Transport(focus)) = self.focus.inner() { Some(focus) } else { None } } } pub trait ArrangerViewState { fn arranger_focused (&self) -> bool; } impl ArrangerViewState for ArrangerTui { fn arranger_focused (&self) -> bool { self.focused() == AppFocus::Content(ArrangerFocus::Arranger) } } pub trait PhrasesViewState: Send + Sync { fn phrases_focused (&self) -> bool; fn phrases_entered (&self) -> bool; fn phrases (&self) -> Vec>>; fn phrase_index (&self) -> usize; fn phrase_mode (&self) -> &Option; } macro_rules! impl_phrases_view_state { ($Struct:ident $(:: $field:ident)* [$self1:ident: $focus:expr] [$self2:ident: $enter:expr]) => { impl PhrasesViewState for $Struct { fn phrases_focused (&$self1) -> bool { $focus } fn phrases_entered (&$self2) -> bool { $enter } fn phrases (&self) -> Vec>> { todo!() } fn phrase_index (&self) -> usize { todo!() } fn phrase_mode (&self) -> &Option { &self$(.$field)*.mode } } } } impl_phrases_view_state!(PhrasesModel [self: false] [self: false]); impl_phrases_view_state!(SequencerTui::phrases [self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)] [self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)]); impl_phrases_view_state!(ArrangerTui::phrases [self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)] [self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)]); pub trait PhraseViewState: Send + Sync { fn phrase_editing (&self) -> &Option>>; fn phrase_editor_focused (&self) -> bool; fn phrase_editor_size (&self) -> &Measure; fn phrase_editor_entered (&self) -> bool; fn keys (&self) -> &Buffer; fn buffer (&self) -> &BigBuffer; fn note_len (&self) -> usize; fn note_axis (&self) -> &RwLock>; fn time_axis (&self) -> &RwLock>; fn now (&self) -> &Arc; fn size (&self) -> &Measure; } macro_rules! impl_phrase_view_state { ($Struct:ident $(:: $field:ident)* [$self1:ident : $focused:expr] [$self2:ident : $entered:expr]) => { impl PhraseViewState for $Struct { fn phrase_editing (&self) -> &Option>> { &self$(.$field)*.phrase } fn phrase_editor_focused (&$self1) -> bool { $focused //self$(.$field)*.focus.is_focused() } fn phrase_editor_entered (&$self2) -> bool { $entered //self$(.$field)*.focus.is_entered() } fn phrase_editor_size (&self) -> &Measure { todo!() } fn keys (&self) -> &Buffer { &self$(.$field)*.keys } fn buffer (&self) -> &BigBuffer { &self$(.$field)*.buffer } fn note_len (&self) -> usize { self$(.$field)*.note_len } fn note_axis (&self) -> &RwLock> { &self$(.$field)*.note_axis } fn time_axis (&self) -> &RwLock> { &self$(.$field)*.time_axis } fn now (&self) -> &Arc { &self$(.$field)*.now } fn size (&self) -> &Measure { &self$(.$field)*.size } } } } impl_phrase_view_state!(PhraseEditorModel [self: true] [self: true]); impl_phrase_view_state!(SequencerTui::editor [self: self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)] [self: self.entered() && self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)]); impl_phrase_view_state!(ArrangerTui::editor [self: self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)] [self: self.entered() && self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)]); fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { let mut widths = vec![]; let mut total = 0; for track in tracks.iter() { let width = track.width; widths.push((width, total)); total += width; } widths.push((0, total)); widths } pub fn arranger_content_vertical ( view: &ArrangerTui, factor: usize ) -> impl Widget + use<'_> { let timebase = view.timebase(); let current = view.current(); let tracks = view.tracks(); let scenes = view.scenes(); let cols = track_widths(tracks); let rows = ArrangerScene::ppqs(scenes, factor); let bg = view.color; let clip_bg = TuiTheme::border_bg(); let sep_fg = TuiTheme::separator_fg(false); let header_h = 3u16;//5u16; let scenes_w = 3 + ArrangerScene::longest_name(scenes) as u16; // x of 1st track let arrangement = Layers::new(move |add|{ let rows: &[(usize, usize)] = rows.as_ref(); let cols: &[(usize, usize)] = cols.as_ref(); let any_size = |_|Ok(Some([0,0])); // column separators add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{ let style = Some(Style::default().fg(sep_fg)); Ok(for x in cols.iter().map(|col|col.1) { let x = scenes_w + to.area().x() + x as u16; for y in to.area().y()..to.area().y2() { to.blit(&"▎", x, y, style); } }) }))?; // row separators add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{ Ok(for y in rows.iter().map(|row|row.1) { let y = to.area().y() + (y / PPQ) as u16 + 1; if y >= to.buffer.area.height { break } for x in to.area().x()..to.area().x2().saturating_sub(2) { if x < to.buffer.area.x && y < to.buffer.area.y { let cell = to.buffer.get_mut(x, y); cell.modifier = Modifier::UNDERLINED; cell.underline_color = sep_fg; } } }) }))?; // track titles let header = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{ // name and width of track let name = track.name().read().unwrap(); let max_w = w.saturating_sub(1).min(name.len()).max(2); let name = format!("▎{}", &name[0..max_w]); let name = TuiStyle::bold(name, true); // beats elapsed let elapsed = if let Some((_, Some(phrase))) = track.phrase().as_ref() { let length = phrase.read().unwrap().length; let elapsed = track.pulses_since_start().unwrap(); let elapsed = timebase.format_beats_1_short( (elapsed as usize % length) as f64 ); format!("▎+{elapsed:>}") } else { String::from("▎") }; // beats until switchover let until_next = track.next_phrase().as_ref().map(|(t, _)|{ let target = t.pulse.get(); let current = current.pulse.get(); if target > current { let remaining = target - current; format!("▎-{:>}", timebase.format_beats_0_short(remaining)) } else { String::new() } }).unwrap_or(String::from("▎")); // name of active MIDI input let input = format!("▎>{}", track.midi_ins().get(0) .map(|port|port.short_name()) .transpose()? .unwrap_or("(none)".into())); // name of active MIDI output let output = format!("▎<{}", track.midi_outs().get(0) .map(|port|port.short_name()) .transpose()? .unwrap_or("(none)".into())); col!(name, /*input, output,*/ until_next, elapsed) .min_xy(w as u16, header_h) .bg(track.color().rgb) .push_x(scenes_w) }); // tracks and scenes let content = col!( // scenes: (scene, pulses) in scenes.iter().zip(rows.iter().map(|row|row.0)) => { let height = 1.max((pulses / PPQ) as u16); let playing = scene.is_playing(tracks); Stack::right(move |add| { // scene title: add(&row!( if playing { "▶ " } else { " " }, TuiStyle::bold(scene.name.read().unwrap().as_str(), true), ).fixed_xy(scenes_w, height).bg(scene.color.rgb))?; // clip per track: Ok(for (track, w) in cols.iter().map(|col|col.0).enumerate() { add(&Layers::new(move |add|{ let mut bg = clip_bg; match (tracks.get(track), scene.clips.get(track)) { (Some(track), Some(Some(phrase))) => { let name = &(phrase as &Arc>).read().unwrap().name; let name = format!("{}", name); let max_w = name.len().min((w as usize).saturating_sub(2)); let color = phrase.read().unwrap().color; add(&name.as_str()[0..max_w].push_x(1).fixed_x(w as u16))?; bg = color.dark.rgb; if let Some((_, Some(ref playing))) = track.phrase() { if *playing.read().unwrap() == *phrase.read().unwrap() { bg = color.light.rgb } }; }, _ => {} }; add(&Background(bg)) }).fixed_xy(w as u16, height))?; }) }).fixed_y(height) } ).fixed_y((view.size.h() as u16).saturating_sub(header_h)); // full grid with header and footer add(&col!(header, content))?; // cursor add(&CustomWidget::new(any_size, move|to: &mut TuiOutput|{ let area = to.area(); let focused = view.arranger_focused(); let selected = view.selected; let get_track_area = |t: usize| [ scenes_w + area.x() + cols[t].1 as u16, area.y(), cols[t].0 as u16, area.h(), ]; let get_scene_area = |s: usize| [ area.x(), header_h + area.y() + (rows[s].1 / PPQ) as u16, area.w(), (rows[s].0 / PPQ) as u16 ]; let get_clip_area = |t: usize, s: usize| [ scenes_w + area.x() + cols[t].1 as u16, header_h + area.y() + (rows[s].1/PPQ) as u16, cols[t].0 as u16, (rows[s].0 / PPQ) as u16 ]; let mut track_area: Option<[u16;4]> = None; let mut scene_area: Option<[u16;4]> = None; let mut clip_area: Option<[u16;4]> = None; let area = match selected { ArrangerSelection::Mix => area, ArrangerSelection::Track(t) => { track_area = Some(get_track_area(t)); area }, ArrangerSelection::Scene(s) => { scene_area = Some(get_scene_area(s)); area }, ArrangerSelection::Clip(t, s) => { track_area = Some(get_track_area(t)); scene_area = Some(get_scene_area(s)); clip_area = Some(get_clip_area(t, s)); area }, }; let bg = TuiTheme::border_bg(); if let Some([x, y, width, height]) = track_area { to.fill_fg([x, y, 1, height], bg); to.fill_fg([x + width, y, 1, height], bg); } if let Some([_, y, _, height]) = scene_area { to.fill_ul([area.x(), y - 1, area.w(), 1], bg); to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg); } Ok(if focused { to.render_in(if let Some(clip_area) = clip_area { clip_area } else if let Some(track_area) = track_area { track_area.clip_h(header_h) } else if let Some(scene_area) = scene_area { scene_area.clip_w(scenes_w) } else { area.clip_w(scenes_w).clip_h(header_h) }, &CORNERS)? }) })) }).bg(bg.rgb); let color = TuiTheme::title_fg(view.arranger_focused()); let size = format!("{}x{}", view.size.w(), view.size.h()); let lower_right = TuiStyle::fg(size, color).pull_x(1).align_se().fill_xy(); lay!(arrangement, lower_right) } pub fn arranger_content_horizontal ( view: &ArrangerTui, ) -> impl Widget + use<'_> { let focused = view.arranger_focused(); let _tracks = view.tracks(); lay!( focused.then_some(Background(TuiTheme::border_bg())), row!( // name CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ todo!() //let Self(tracks, selected) = self; //let yellow = Some(Style::default().yellow().bold().not_dim()); //let white = Some(Style::default().white().bold().not_dim()); //let area = to.area(); //let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()]; //let offset = 0; // track scroll offset //for y in 0..area.h() { //if y == 0 { //to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?; //} else if y % 2 == 0 { //let index = (y as usize - 2) / 2 + offset; //if let Some(track) = tracks.get(index) { //let selected = selected.track() == Some(index); //let style = if selected { yellow } else { white }; //to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?; //to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?; //} //} //} //Ok(Some(area)) }), // monitor CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ todo!() //let Self(tracks) = self; //let mut area = to.area(); //let on = Some(Style::default().not_dim().green().bold()); //let off = Some(DIM); //area.x += 1; //for y in 0..area.h() { //if y == 0 { ////" MON ".blit(to.buffer, area.x, area.y + y, style2)?; //} else if y % 2 == 0 { //let index = (y as usize - 2) / 2; //if let Some(track) = tracks.get(index) { //let style = if track.monitoring { on } else { off }; //to.blit(&" MON ", area.x(), area.y() + y, style)?; //} else { //area.height = y; //break //} //} //} //area.width = 4; //Ok(Some(area)) }), // record CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ todo!() //let Self(tracks) = self; //let mut area = to.area(); //let on = Some(Style::default().not_dim().red().bold()); //let off = Some(Style::default().dim()); //area.x += 1; //for y in 0..area.h() { //if y == 0 { ////" REC ".blit(to.buffer, area.x, area.y + y, style2)?; //} else if y % 2 == 0 { //let index = (y as usize - 2) / 2; //if let Some(track) = tracks.get(index) { //let style = if track.recording { on } else { off }; //to.blit(&" REC ", area.x(), area.y() + y, style)?; //} else { //area.height = y; //break //} //} //} //area.width = 4; //Ok(Some(area)) }), // overdub CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ todo!() //let Self(tracks) = self; //let mut area = to.area(); //let on = Some(Style::default().not_dim().yellow().bold()); //let off = Some(Style::default().dim()); //area.x = area.x + 1; //for y in 0..area.h() { //if y == 0 { ////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?; //} else if y % 2 == 0 { //let index = (y as usize - 2) / 2; //if let Some(track) = tracks.get(index) { //to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub { //on //} else { //off //})?; //} else { //area.height = y; //break //} //} //} //area.width = 4; //Ok(Some(area)) }), // erase CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ todo!() //let Self(tracks) = self; //let mut area = to.area(); //let off = Some(Style::default().dim()); //area.x = area.x + 1; //for y in 0..area.h() { //if y == 0 { ////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?; //} else if y % 2 == 0 { //let index = (y as usize - 2) / 2; //if let Some(_) = tracks.get(index) { //to.blit(&" DEL ", area.x(), area.y() + y, off)?; //} else { //area.height = y; //break //} //} //} //area.width = 4; //Ok(Some(area)) }), // gain CustomWidget::new(|_|{todo!()}, |_: &mut TuiOutput|{ todo!() //let Self(tracks) = self; //let mut area = to.area(); //let off = Some(Style::default().dim()); //area.x = area.x() + 1; //for y in 0..area.h() { //if y == 0 { ////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?; //} else if y % 2 == 0 { //let index = (y as usize - 2) / 2; //if let Some(_) = tracks.get(index) { //to.blit(&" +0.0 ", area.x(), area.y() + y, off)?; //} else { //area.height = y; //break //} //} //} //area.width = 7; //Ok(Some(area)) }), // scenes CustomWidget::new(|_|{todo!()}, |to: &mut TuiOutput|{ let [x, y, _, height] = to.area(); let mut x2 = 0; Ok(for (scene_index, scene) in view.scenes().iter().enumerate() { let active_scene = view.selected.scene() == Some(scene_index); let sep = Some(if active_scene { Style::default().yellow().not_dim() } else { Style::default().dim() }); for y in y+1..y+height { to.blit(&"│", x + x2, y, sep); } let name = scene.name.read().unwrap(); let mut x3 = name.len() as u16; to.blit(&*name, x + x2, y, sep); for (i, clip) in scene.clips.iter().enumerate() { let active_track = view.selected.track() == Some(i); if let Some(clip) = clip { let y2 = y + 2 + i as u16 * 2; let label = format!("{}", clip.read().unwrap().name); to.blit(&label, x + x2, y2, Some(if active_track && active_scene { Style::default().not_dim().yellow().bold() } else { Style::default().not_dim() })); x3 = x3.max(label.len() as u16) } } x2 = x2 + x3 + 1; }) }), ) ) } /// 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 } const NTH_OCTAVE: [&'static str; 11] = [ "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", ]; impl PhraseEditorModel { 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 { // 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 { 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); } } } } }