use crate::*; pub(crate) const HEADER_H: u16 = 0; // 5 pub(crate) const SCENES_W_OFFSET: u16 = 0; render!(TuiOut: (self: Arranger) => { let toolbar = |x|Bsp::s(self.toolbar_view(), x); let pool = |x|Bsp::w(self.pool_view(), x); let editing = |x|Bsp::n(Bsp::e(self.editor.clip_status(), self.editor.edit_status()), x); let enclosed = |x|Outer(Style::default().fg(Color::Rgb(72,72,72))).enclose(x); let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); let arrrrrr = Fixed::y(27, Map::new( move||[ (0, 2, self.output_row_header(), self.output_row_cells()), (2, 3, self.elapsed_row_header(), self.elapsed_row_cells()), (4, 3, self.next_row_header(), self.next_row_cells()), (6, 3, self.track_row_header(), self.track_row_cells()), (8, 20, self.scene_row_headers(), self.scene_row_cells()), (25, 2, self.input_row_header(), self.input_row_cells()), ].into_iter(), move|(y, h, header, cells), index|map_south(y, h, Fill::x(Align::w(Bsp::e( Fixed::xy(scenes_w, h, header), Fixed::xy(self.tracks.len() as u16*6, h, cells) )))))); self.size.of(toolbar(pool(editing(Bsp::s(arrrrrr, enclosed(&self.editor)))))) }); impl Arranger { pub const LEFT_SEP: char = '▎'; pub const TRACK_MIN_WIDTH: usize = 4; /// A 1-row cell. fn cell > (color: ItemPalette, field: T) -> impl Content { Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field)) } /// A phat line fn phat_lo (fg: Color, bg: Color) -> impl Content { Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"▄"))) } fn phat_hi (fg: Color, bg: Color) -> impl Content { Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"▀"))) } /// A cell that is 3-row on its own, but stacks, giving (N+1)*2 rows per N cells. fn phat_cell > (color: ItemPalette, last: ItemPalette, field: T) -> impl Content { Bsp::s( Self::phat_lo(color.base.rgb, last.base.rgb), Bsp::n( Self::phat_hi(color.base.rgb, last.base.rgb), Fixed::y(1, Fill::x(Tui::fg_bg(color.lightest.rgb, color.base.rgb, field))), ) ) } fn phat_cell_3 > (top: Color, middle: Color, bottom: Color, field: T) -> impl Content { Bsp::s( Self::phat_lo(middle, top), Bsp::n( Self::phat_hi(bottom, middle), Fixed::y(1, Fill::x(Tui::bg(middle, field))), ) ) } fn output_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (||Tui::bold(true, Tui::fg_bg(TuiTheme::g(0), TuiTheme::g(200), "[ ] Out 1: NI")).boxed()).into() } fn output_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { //let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); (move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| { let w = (x2 - x1) as u16; let color: ItemPalette = track.color().dark.into(); let cell = Bsp::s(format!(" M S "), Self::phat_hi(color.dark.rgb, color.darker.rgb)); map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, cell))) })).boxed()).into() } fn elapsed_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Playing")).boxed()).into() } fn elapsed_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| { //let color = track.color(); let color: ItemPalette = track.color().dark.into(); let timebase = self.clock().timebase(); let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb, if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() { let length = phrase.read().unwrap().length; let elapsed = track.player.pulses_since_start().unwrap() as usize; format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64)) } else { String::new() }); let cell = Bsp::s(value, Self::phat_hi(color.dark.rgb, color.darker.rgb)); Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell)) })).boxed()).into() } fn next_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Next")).boxed()).into() } fn next_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| { let color: ItemPalette = track.color(); let color: ItemPalette = track.color().dark.into(); let until_next = Self::cell(color, Tui::bold(true, Self::cell_until_next(track, &self.clock().playhead))); let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb, until_next); let cell = Bsp::s(value, Self::phat_hi(color.dark.rgb, color.darker.rgb)); Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell)) })).boxed()).into() } fn track_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Track")).boxed()).into() } fn track_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { let iter = ||self.tracks_with_widths(); (move||Fixed::y(2, Map::new(iter, move|(_, track, x1, x2), i| { let color = track.color(); let name = format!(" {}", &track.name); Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, Tui::fg_bg(color.lightest.rgb, color.base.rgb, Self::phat_cell(color, color.darkest.rgb.into(), Tui::bold(true, name))))) })).boxed()).into() } fn input_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (||Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(0), TuiTheme::g(200), "[ ] In 1: Korg"))).boxed()).into() } fn input_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| { let w = (x2 - x1) as u16; let cell = Bsp::s("[Rec]", "[Mon]"); let color: ItemPalette = track.color().dark.into(); map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, cell))) })).boxed()).into() } fn scene_row_headers <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (||{ let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); Tui::bg(Color::Rgb(0,0,0), Fill::y(Map::new( ||self.scenes_with_heights(2), move|(_, scene, y1, y2), i| { let h = (y2 - y1) as u16; let color = scene.color(); let name = format!("🭬{}", &scene.name); let cell = Self::phat_cell(color, *last_color.read().unwrap(), name); *last_color.write().unwrap() = color; map_south(y1 as u16, 2, Fill::x(cell)) } ))).boxed() }).into() } fn scene_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { (move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| { let w = (x2 - x1) as u16; let cell = Bsp::s("[Rec]", "[Mon]"); let color: ItemPalette = track.color().dark.into(); let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); map_east(x1 as u16, w, Fixed::x(w, Tui::bg(Color::Rgb(0,0,0), Fill::y(Map::new( ||self.scenes_with_heights(2), move|(_, scene, y1, y2), i| { let h = (y2 - y1) as u16; let color = scene.color(); let name = format!("🭬{}", &scene.name); //*last_color.write().unwrap() = color map_south(y1 as u16, 2, Fill::x(Self::phat_cell_3( TuiTheme::g(32).into(), TuiTheme::g(32).into(), TuiTheme::g(32).into(), //Tui::fg(TuiTheme::g(64), " ⏺ ") Tui::fg(TuiTheme::g(64), " ⏹ ") ))) } ))).boxed() )) })).boxed()).into() } pub fn tracks_with_widths (&self) -> impl Iterator { Self::tracks_with_widths_static(self.tracks.as_slice()) } fn tracks_with_widths_static (tracks: &[ArrangerTrack]) -> impl Iterator { let mut x = 0; tracks.iter().enumerate().map(move |(index, track)|{ let data = (index, track, x, x + track.width); x += track.width; data }) } fn track_column_separators <'a> (&'a self) -> impl Content + 'a { let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); let fg = Color::Rgb(64,64,64); Map::new(move||self.tracks_with_widths(), move|(_n, _track, x1, x2), _i|{ Push::x(scenes_w, map_east(x1 as u16, (x2 - x1) as u16, Fixed::x((x2 - x1) as u16, Tui::fg(fg, RepeatV(&"·"))))) }) } /// name and width of track fn cell_name (track: &ArrangerTrack, _w: usize) -> impl Content { Tui::bold(true, Tui::fg(track.color.lightest.rgb, track.name().clone())) } /// beats until switchover fn cell_until_next (track: &ArrangerTrack, current: &Arc) -> Option> { let timebase = ¤t.timebase; let mut result = String::new(); if let Some((t, _)) = track.player.next_phrase().as_ref() { let target = t.pulse.get(); let current = current.pulse.get(); if target > current { result = format!("-{:>}", timebase.format_beats_0_short(target - current)) } } Some(result) } pub fn scenes_with_heights (&self, h: usize) -> impl Iterator { let mut y = 0; self.scenes.iter().enumerate().map(move|(index, scene)|{ let data = (index, scene, y, y + h); y += h; data }) } fn scene_rows (&self) -> impl Content + use<'_> { let scenes_w = 16.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); Map::new(||self.scenes_with_heights(1), move|(_, scene, y1, y2), i| { let h = (y2 - y1) as u16; let color = scene.color(); let cell = Fixed::y(h, Fixed::x(scenes_w, Self::cell(color, scene.name.clone()))); map_south(y1 as u16, 1, cell) }) } pub fn scene_heights (scenes: &[ArrangerScene], factor: usize) -> Vec<(usize, usize)> { let mut total = 0; if factor == 0 { scenes.iter().map(|scene|{ let pulses = scene.pulses().max(PPQ); total += pulses; (pulses, total - pulses) }).collect() } else { (0..=scenes.len()).map(|i|{ (factor*PPQ, factor*PPQ*i) }).collect() } } fn cell_scene <'a> ( tracks: &'a [ArrangerTrack], scene: &'a ArrangerScene, pulses: usize ) -> impl Content + use<'a> { let height = 1.max((pulses / PPQ) as u16); let playing = scene.is_playing(tracks); let icon = Tui::bg( scene.color.base.rgb, if playing { "▶ " } else { " " } ); let name = Tui::fg_bg(scene.color.lightest.rgb, scene.color.base.rgb, Expand::x(1, Tui::bold(true, scene.name.clone())) ); let clips = Map::new(||Arranger::tracks_with_widths_static(tracks), move|(index, track, x1, x2), _| Push::x((x2 - x1) as u16, Self::cell_clip(scene, index, track, (x2 - x1) as u16, height)) ); Fixed::y(height, Bsp::e(icon, Bsp::e(name, clips))) } fn cell_clip <'a> ( scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16 ) -> impl Content + use<'a> { scene.clips.get(index).map(|clip|clip.as_ref().map(|phrase|{ let phrase = phrase.read().unwrap(); let mut bg = TuiTheme::border_bg(); let name = phrase.name.to_string(); let max_w = name.len().min((w as usize).saturating_sub(2)); let color = phrase.color; bg = color.dark.rgb; if let Some((_, Some(ref playing))) = track.player.play_phrase() { if *playing.read().unwrap() == *phrase { bg = color.light.rgb } }; Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w])))); })) } fn toolbar_view (&self) -> impl Content + use<'_> { Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock)))) } fn pool_view (&self) -> impl Content + use<'_> { let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; let pool_w = if self.pool.visible { phrase_w } else { 0 }; let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool)))); Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool)))) } pub 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 } fn scene_row_sep <'a> (&'a self) -> impl Content + 'a { let fg = Color::Rgb(255,255,255); Map::new(move||self.scenes_with_heights(1), |_, _|"") //Map(||rows.iter(), |(_n, _scene, y1, _y2), _i| { //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 { //if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((x, y))) { //cell.modifier = Modifier::UNDERLINED; //cell.underline_color = fg; //} ////} //} //}) } fn cursor (&self) -> impl Content + '_ { let color = self.color; let bg = color.lighter.rgb;//Color::Rgb(0, 255, 0); let selected = self.selected(); let cols = Arranger::track_widths(&self.tracks); let rows = Arranger::scene_heights(&self.scenes, 1); let scenes_w = 16.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); let focused = true; let reticle = Reticle(Style { fg: Some(self.color.lighter.rgb), bg: None, underline_color: None, add_modifier: Modifier::empty(), sub_modifier: Modifier::DIM }); RenderThunk::new(move|to: &mut TuiOut|{ let area = to.area(); let [x, y, w, h] = area.xywh(); let mut track_area: Option<[u16;4]> = match selected { ArrangerSelection::Track(t) | ArrangerSelection::Clip(t, _) => Some([ x + scenes_w + cols[t].1 as u16, y, cols[t].0 as u16, h, ]), _ => None }; let mut scene_area: Option<[u16;4]> = match selected { ArrangerSelection::Scene(s) | ArrangerSelection::Clip(_, s) => Some([ x, y + HEADER_H + (rows[s].1 / PPQ) as u16, w, (rows[s].0 / PPQ) as u16 ]), _ => None }; let mut clip_area: Option<[u16;4]> = match selected { ArrangerSelection::Clip(t, s) => Some([ (scenes_w + x + cols[t].1 as u16).saturating_sub(1), HEADER_H + y + (rows[s].1/PPQ) as u16, cols[t].0 as u16 + 2, (rows[s].0 / PPQ) as u16 ]), _ => None }; 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([x, y - 1, w, 1], bg); to.fill_ul([x, y + height - 1, w, 1], bg); } if focused { to.place(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) }, &reticle) }; }) } } //pub struct ArrangerVCursor { //cols: Vec<(usize, usize)>, //rows: Vec<(usize, usize)>, //color: ItemPalette, //reticle: Reticle, //selected: ArrangerSelection, //scenes_w: u16, //} //from!(|args:(&Arranger, usize)|ArrangerVCursor = Self { //cols: Arranger::track_widths(&args.0.tracks), //rows: Arranger::scene_heights(&args.0.scenes, args.1), //selected: args.0.selected(), //scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&args.0.scenes) as u16, //color: args.0.color, //reticle: Reticle(Style { //fg: Some(args.0.color.lighter.rgb), //bg: None, //underline_color: None, //add_modifier: Modifier::empty(), //sub_modifier: Modifier::DIM //}), //}); //impl Content for ArrangerVCursor { //fn render (&self, to: &mut TuiOut) { //let area = to.area(); //let focused = true; //let selected = self.selected; //let get_track_area = |t: usize| [ //self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(), //self.cols[t].0 as u16, area.h(), //]; //let get_scene_area = |s: usize| [ //area.x(), HEADER_H + area.y() + (self.rows[s].1 / PPQ) as u16, //area.w(), (self.rows[s].0 / PPQ) as u16 //]; //let get_clip_area = |t: usize, s: usize| [ //(self.scenes_w + area.x() + self.cols[t].1 as u16).saturating_sub(1), //HEADER_H + area.y() + (self.rows[s].1/PPQ) as u16, //self.cols[t].0 as u16 + 2, //(self.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 = self.color.lighter.rgb;//Color::Rgb(0, 255, 0); //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); //} //if focused { //to.place(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(self.scenes_w) //} else { //area.clip_w(self.scenes_w).clip_h(HEADER_H) //}, &self.reticle) //}; //} //} //impl Arranger { //fn render_mode (state: &Self) -> impl Content + use<'_> { //match state.mode { //ArrangerMode::H => todo!("horizontal arranger"), //ArrangerMode::V(factor) => Self::render_mode_v(state, factor), //} //} //} //render!(TuiOut: (self: Arranger) => { //let pool_w = if self.pool.visible { self.splits[1] } else { 0 }; //let color = self.color; //let layout = Bsp::a(Fill::xy(ArrangerStatus::from(self)), //Bsp::n(Fixed::x(pool_w, PoolView(self.pool.visible, &self.pool)), //Bsp::n(TransportView::new(true, &self.clock), //Bsp::s(Fixed::y(1, MidiEditStatus(&self.editor)), //Bsp::n(Fill::x(Fixed::y(20, //Bsp::a(Fill::xy(Tui::bg(color.darkest.rgb, "background")), //Bsp::a( //Fill::xy(Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb))), //Self::render_mode(self))))), Fill::y(&"fixme: self.editor")))))); //self.size.of(layout) //}); //Align::n(Fill::xy(lay!( //Align::n(Fill::xy(Tui::bg(self.color.darkest.rgb, " "))), //Align::n(Fill::xy(ArrangerVRowSep::from((self, 1)))), //Align::n(Fill::xy(ArrangerVColSep::from(self))), //Align::n(Fill::xy(ArrangerVClips::new(self, 1))), //Align::n(Fill::xy(ArrangerVCursor::from((self, 1)))))))))))))))); //Align::n(Fill::xy(":"))))))))))))); //"todo:")))))))); //Bsp::s( //Align::n(Fixed::y(1, Fill::x(ArrangerVIns::from(self)))), //Bsp::s( //Fixed::y(20, Align::n(ArrangerVClips::new(self, 1))), //Fill::x(Fixed::y(1, ArrangerVOuts::from(self))))))))))))); //Bsp::s( //Bsp::s( //Bsp::s( //Fill::xy(ArrangerVClips::new(self, 1)), //Fill::x(ArrangerVOuts::from(self)))))