use crate::*; impl<'a, T: TransportViewState> Content for TransportView<'a, T> { type Engine = Tui; fn content (&self) -> impl Widget { let state = self.0; let focused = state.transport_focused(); lay!( state.transport_selected().wrap(focused, TransportFocus::PlayPause, &Styled( None, match state.transport_state() { Some(TransportState::Rolling) => "▶ PLAYING", Some(TransportState::Starting) => "READY ...", Some(TransportState::Stopped) => "⏹ STOPPED", _ => unreachable!(), } ).min_xy(11, 2).push_x(1)).align_x().fill_x(), row!( state.transport_selected().wrap(focused, TransportFocus::Bpm, &Outset::X(1u16, { let bpm = state.bpm_value(); row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) } })), //let quant = state.focus().wrap(state.focused(), TransportFocus::Quant, &Outset::X(1u16, row! { //"QUANT ", ppq_to_name(state.0.quant as usize) //})), state.transport_selected().wrap(focused, TransportFocus::Sync, &Outset::X(1u16, row! { "SYNC ", pulses_to_name(state.sync_value() as usize) })) ).align_w().fill_x(), state.transport_selected().wrap(focused, TransportFocus::Clock, &{ let time1 = state.format_beat(); let time2 = state.format_msu(); row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1) }).align_e().fill_x(), ).fill_x().bg(Color::Rgb(40, 50, 30)) } } impl Content for SequencerTui { type Engine = Tui; fn content (&self) -> impl Widget { col!( widget(&TransportView(self)), Split::right(20, widget(&PhrasesView(self)), widget(&PhraseView(self)), ).min_y(20) ) } } /// Display mode of arranger #[derive(Clone, PartialEq)] pub enum ArrangerMode { /// Tracks are rows Horizontal, /// Tracks are columns Vertical(usize), } /// Arranger display mode can be cycled impl ArrangerMode { /// Cycle arranger display mode pub fn to_next (&mut self) { *self = match self { Self::Horizontal => Self::Vertical(1), Self::Vertical(1) => Self::Vertical(2), Self::Vertical(2) => Self::Vertical(2), Self::Vertical(0) => Self::Horizontal, Self::Vertical(_) => Self::Vertical(0), } } } /// Layout for standalone arranger app. impl Content for ArrangerTui { type Engine = Tui; fn content (&self) -> impl Widget { let arranger_focused = self.arranger_focused(); Split::up( 1, TransportView(self), Split::down( self.splits[0], lay!( Layers::new(move |add|{ match self.mode { ArrangerMode::Horizontal => add(&arranger_content_horizontal(self))?, ArrangerMode::Vertical(factor) => add(&arranger_content_vertical(self, factor))? }; add(&self.size) }) .grow_y(1) .border(Lozenge(Style::default() .bg(TuiTheme::border_bg()) .fg(TuiTheme::border_fg(arranger_focused)))), widget(&self.size), widget(&format!("[{}] Arranger", if self.entered { "■" } else { " " })) .fg(TuiTheme::title_fg(arranger_focused)) .push_x(1), ), Split::right( self.splits[1], PhrasesView(self), PhraseView(self), ) ) ) } } // TODO: Display phrases always in order of appearance impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> { type Engine = Tui; fn content (&self) -> impl Widget { let focused = self.0.phrases_focused(); let entered = self.0.entered(); let phrases = self.0.phrases(); let selected_phrase = self.0.phrase(); let mode = self.0.mode(); 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(PhrasesMode::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(PhrasesMode::Rename(phrase, _)) = mode { if focused && i == *phrase { row2 = format!("{row2}▄"); } }; let row2 = TuiStyle::bold(row2, true); add(&col!(row1, row2).fill_x().bg(color.base.rgb))?; if focused && i == selected_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 upper_left = format!("[{}] Phrases", if entered {"■"} else {" "}); let upper_right = format!("({})", self.0.phrases().len()); 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(), ) } } impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> { type Engine = Tui; fn content (&self) -> impl Widget { let phrase = self.0.phrase(); let size = self.0.size(); let focused = self.0.phrase_focused(); let entered = self.0.entered(); let keys = self.0.keys(); let buffer = self.0.buffer(); let note_len = self.0.note_len(); let note_axis = self.0.note_axis(); let time_axis = self.0.time_axis(); let FixedAxis { start: note_start, point: note_point, clamp: note_clamp } = *note_axis.read().unwrap(); let ScaledAxis { start: time_start, point: time_point, clamp: time_clamp, scale: time_scale } = *time_axis.read().unwrap(); //let color = Color::Rgb(0,255,0); //let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color); let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{ Ok(if to.area().h() >= 2 { to.buffer_update(to.area().set_w(5), &|cell, x, y|{ let y = y + (note_start / 2) as u16; if x < keys.area.width && y < keys.area.height { *cell = keys.get(x, y).clone() } }); }) }).fill_y(); let notes_bg_null = Color::Rgb(28, 35, 25); let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ let area = to.area(); let h = area.h() as usize; size.set_wh(area.w(), h); let mut axis = note_axis.write().unwrap(); if let Some(point) = axis.point { if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) { axis.start += 2; } } Ok(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 / 2; if src_x < buffer.width && src_y < buffer.height - 1 { buffer.get(src_x, buffer.height - src_y - 2).map(|src|{ cell.set_symbol(src.symbol()); cell.set_fg(src.fg); cell.set_bg(src.bg); }); } }); }) }).fill_x(); let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ Ok(if focused && 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 + (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::Rgb(0,255,0)))); } } }) }); 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(); let playhead = CustomWidget::new( |to:[u16;2]|Ok(Some(to.clip_h(1))), move|to: &mut TuiOutput|{ if let Some(_) = phrase { let now = self.0.now().get() as usize; // 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).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(); 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 = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border); let content = lay!(content, playhead); let mut upper_left = format!("[{}] Sequencer", if entered {"■"} else {" "}); if let Some(phrase) = phrase { upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name); } let mut lower_right = format!("┤{}├", size.format()); lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale)); //lower_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 { lower_right = format!("┤Note: {} {}├─{lower_right}", note_axis.read().unwrap().point.unwrap(), pulses_to_name(note_len)); //lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}", //pulses_to_name(*note_len), //note_start, //note_point.unwrap_or(0), //note_clamp.unwrap_or(0), //); } let upper_right = if let Some(phrase) = phrase { format!("┤Length: {}├", phrase.read().unwrap().length) } else { String::new() }; 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 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(), "]" )), } }) } }