use crate::*; /// Layout for standalone arranger app. impl Content for Arranger { type Engine = Tui; fn content (&self) -> impl Widget { Split::down( 2, widget(&self.transport), Split::up( 1, widget(&self.status), Split::down( self.arrangement_split, widget(&self.arrangement), Split::right( self.phrases_split, self.phrases.clone(), widget(&self.editor), ) ) ) ) } } impl Content for ArrangerStatusBar { type Engine = Tui; fn content (&self) -> impl Widget { let label = match self { Self::Transport => "TRANSPORT", Self::ArrangementMix => "COMPOSITION", Self::ArrangementTrack => "TRACK", Self::ArrangementScene => "SCENE", Self::ArrangementClip => "CLIP", Self::PhrasePool => "PHRASES", Self::PhraseEditor => "SEQUENCER", }; let label = TuiStyle::bg(format!(" {label} "), Color::Rgb(150, 160, 90)) .fg(Color::Rgb(0, 0, 0)) .bold(true); row!(label, " ", match self { Self::ArrangementClip => "[G]et [S]et [A]dd [I]nsert [D]uplicate [E]dit [C]olor [,.]Choose", Self::PhrasePool => "[A]ppend [I]nsert [D]uplicate [C]olor re[N]ame leng[T]h [,.]Move [<>]Resize", _ => "" }) } } impl Content for Arrangement { type Engine = Tui; fn content (&self) -> impl Widget { Layers::new(move |add|{ match self.mode { ArrangementViewMode::Horizontal => add(&HorizontalArranger(&self)), ArrangementViewMode::Vertical(factor) => add(&VerticalArranger(&self, factor)) }?; let color = if self.focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)}; if self.focused { let commands = "[G]et [S]et [A]dd [I]nsert [D]uplicate [E]dit [C]olor"; let lower_left = Align::SW(TuiStyle::fg(commands, color).push_x(1)); add(&lower_left)?; } let description = self.selected.description(&self.tracks, &self.scenes); let lower_right = Align::SE(TuiStyle::fg(description.as_str(), color).pull_x(1)); add(&lower_right)?; let upper_left = TuiStyle::fg("Session", color).push_x(1); add(&upper_left) }) } } impl<'a> Content for VerticalArranger<'a, Tui> { type Engine = Tui; fn content (&self) -> impl Widget { let Self(state, factor) = self; let (cols, rows) = if *factor == 0 {( state.track_widths(), Scene::ppqs(state.scenes.as_slice()), )} else {( state.track_widths(), (0..=state.scenes.len()).map(|i|(factor*PPQ, factor*PPQ*i)).collect::>(), )}; let tracks: &[ArrangementTrack] = state.tracks.as_ref(); let scenes: &[Scene] = state.scenes.as_ref(); let offset = 3 + Scene::longest_name(scenes) as u16; // x of 1st track let bg = state.color; let clip_bg = Color::Rgb(40, 50, 30); let border_bg = Color::Rgb(40, 50, 30); let border_hi = Color::Rgb(100, 110, 40); let border_lo = Color::Rgb(70, 80, 50); let border_fg = if self.0.focused { border_hi } else { border_lo }; let border = Lozenge(Style::default().bg(border_bg).fg(border_fg)); Layers::new(move |add|{ let rows: &[(usize, usize)] = rows.as_ref(); let cols: &[(usize, usize)] = cols.as_ref(); // column separators add(&CustomWidget::new(|_|Ok(Some([0,0])), move|to: &mut TuiOutput|{ let area = to.area(); let style = Some(Style::default().fg(COLOR_SEPARATOR)); for x in cols.iter().map(|col|col.1) { let x = offset + area.x() + x as u16 - 1; for y in area.y()..area.y2() { to.blit(&"▎", x, y, style); } } Ok(()) }))?; // row separators add(&CustomWidget::new(|_|Ok(Some([0,0])), move|to: &mut TuiOutput|{ let area = to.area(); for y in rows.iter().map(|row|row.1) { let y = area.y() + (y / PPQ) as u16 + 1; if y >= to.buffer.area.height { break } for x in area.x()..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 = COLOR_SEPARATOR; } } } Ok(()) }))?; // track titles let track_titles = row!((track, w) in tracks.iter().zip(cols.iter().map(|col|col.0))=>{ let name = track.name.read().unwrap(); let max_w = w.saturating_sub(1).min(name.len()); (&format!("▎{}", &name[0..max_w]).as_str()) .min_xy(w as u16, 2) .bg(track.color) .push_x(offset - 1) }); // scene titles let scene_name = |scene, playing: bool, height|row!( if playing { "▶ " } else { " " }, (scene as &Scene).name.read().unwrap().as_str(), ).fixed_xy(offset.saturating_sub(1), height); // scene clips let scene_clip = |scene, track: usize, w: u16, h: u16|Layers::new(move |add|{ let mut color = clip_bg; match (tracks.get(track), (scene as &Scene).clips.get(track)) { (Some(_), 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)); add(&name.as_str()[0..max_w].push_x(1).fixed_x(w))?; color = (phrase as &Arc>).read().unwrap().color; }, _ => {} }; add(&Background(color)) }).fixed_xy(w, h); add(&col!(track_titles, 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(&scene_name(scene, playing, height).bg(scene.color))?; // clip per track: for (track, w) in cols.iter().map(|col|col.0).enumerate() { add(&scene_clip(scene, track, w as u16, height))?; } Ok(()) }).fixed_y(height) } )))?; // cursor add(&CustomWidget::new(|_|Ok(Some([0,0])), move|to: &mut TuiOutput|{ let area = to.area(); let focused = state.focused; let selected = state.selected; let get_track_area = |t: usize| [ offset + area.x() + cols[t].1 as u16 - 1, area.y(), cols[t].0 as u16, area.h(), ]; let get_scene_area = |s: usize| [ area.x(), 2 + area.y() + (rows[s].1 / PPQ) as u16, area.w(), (rows[s].0 / PPQ) as u16 ]; let get_clip_area = |t: usize, s: usize| [ offset+area.x()+cols[t].1 as u16 - 1, 2+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 { ArrangementFocus::Mix => { if focused { to.fill_bg(area, Color::Rgb(40, 50, 30)); } area }, ArrangementFocus::Track(t) => { track_area = Some(get_track_area(t)); area }, ArrangementFocus::Scene(s) => { scene_area = Some(get_scene_area(s)); area }, ArrangementFocus::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 }, }; if let Some([x, y, width, height]) = track_area { to.fill_fg([x, y, 1, height], Color::Rgb(70, 80, 50)); to.fill_fg([x + width, y, 1, height], Color::Rgb(70, 80, 50)); } if let Some([_, y, _, height]) = scene_area { to.fill_ul([area.x(), y - 1, area.w(), 1], Color::Rgb(70, 80, 50)); to.fill_ul([area.x(), y + height - 1, area.w(), 1], Color::Rgb(70, 80, 50)); } if focused { if let Some(clip_area) = clip_area { to.render_in(clip_area, &CORNERS)?; //to.fill_bg(clip_area, Color::Rgb(40, 50, 30)); } else if let Some(track_area) = track_area { to.render_in(track_area.clip_h(2), &CORNERS)?; //to.fill_bg(track_area, Color::Rgb(40, 50, 30)); } else if let Some(scene_area) = scene_area { to.render_in(scene_area.clip_w(offset-1), &CORNERS)?; //to.fill_bg(scene_area, Color::Rgb(40, 50, 30)); } } Ok(()) }))?; Ok(()) }).bg(bg).border(border) } } impl<'a> Content for HorizontalArranger<'a, Tui> { type Engine = Tui; fn content (&self) -> impl Widget { let Arrangement { tracks, focused, .. } = self.0; let _tracks = tracks.as_slice(); lay!( focused.then_some(Background(Color::Rgb(40, 50, 30))), 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 Arrangement { scenes, selected, .. } = self.0; let area = to.area(); let mut x2 = 0; let [x, y, _, height] = area; for (scene_index, scene) in scenes.iter().enumerate() { let active_scene = 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 = 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; } //Ok(Some([x, y, x2, height])) Ok(()) }), ) ) } }