use crate::*; /// Layout for standalone arranger app. impl Content for Arranger { type Engine = Tui; fn content (&self) -> impl Widget { let focused = self.arrangement.focused; 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)); Split::down( 1, row!(menu in self.menu.menus.iter() => { row!(" ", menu.title.as_str(), " ") }), Split::up( 1, widget(&self.status), Split::up( 1, widget(&self.transport), Split::down( self.arrangement_split, lay!( widget(&self.arrangement).grow_y(1).border(border), widget(&self.arrangement.size), widget(&"[ ] Arrangement").fg(title_color).push_x(1), ), 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 => "PROJECT", Self::ArrangementTrack => "TRACK", Self::ArrangementScene => "SCENE", Self::ArrangementClip => "CLIP", Self::PhrasePool => "SEQ LIST", Self::PhraseView => "VIEW SEQ", Self::PhraseEdit => "EDIT SEQ", }; let mode = TuiStyle::bg(format!(" {label} "), Color::Rgb(150, 160, 90)) .fg(Color::Rgb(0, 0, 0)) .bold(true); let commands = match self { Self::ArrangementMix => command(&[ ["", "c", "olor"], ["", "<>", "resize"], ["", "+-", "zoom"], ["", "n", "ame/number"], ["", "Enter", " stop all"], ]), Self::ArrangementClip => command(&[ ["", "g", "et"], ["", "s", "et"], ["", "a", "dd"], ["", "i", "ns"], ["", "d", "up"], ["", "e", "dit"], ["", "c", "olor"], ["re", "n", "ame"], ["", ",.", "select"], ["", "Enter", " launch"], ]), Self::ArrangementTrack => command(&[ ["re", "n", "ame"], ["", ",.", "resize"], ["", "<>", "move"], ["", "i", "nput"], ["", "o", "utput"], ["", "m", "ute"], ["", "s", "olo"], ["", "Del", "ete"], ["", "Enter", " stop"], ]), Self::ArrangementScene => command(&[ ["re", "n", "ame"], ["", "Del", "ete"], ["", "Enter", " launch"], ]), Self::PhrasePool => command(&[ ["", "a", "ppend"], ["", "i", "nsert"], ["", "d", "uplicate"], ["", "Del", "ete"], ["", "c", "olor"], ["re", "n", "ame"], ["leng", "t", "h"], ["", ",.", "move"], ["", "+-", "resize view"], ]), Self::PhraseView => command(&[ ["", "enter", " edit"], ["", "arrows/pgup/pgdn", " scroll"], ["", "+=", "zoom"], ]), Self::PhraseEdit => command(&[ ["", "esc", " exit"], ["", "a", "ppend"], ["", "s", "et"], ["", "][", "length"], ["", "+-", "zoom"], ]), _ => command(&[]) }; //let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); row!(mode, commands).fill_x().bg(Color::Rgb(28, 35, 25)) } } fn command (commands: &[[impl Widget;3]]) -> impl Widget + '_ { Stack::right(|add|{ Ok(for [a, b, c] in commands.iter() { add(&row!(" ", widget(a), widget(b).bold(true).fg(Color::Rgb(255,255,0)), widget(c)))?; }) }) } 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)}; //add(&TuiStyle::fg("Project", color).push_x(2))?; add(&self.size) }) } } impl<'a> Content for VerticalArranger<'a, Tui> { type Engine = Tui; fn content (&self) -> impl Widget { let Self(state, factor) = self; let tracks = state.tracks.as_ref() as &[ArrangementTrack]; let scenes = state.scenes.as_ref(); let cols = state.track_widths(); let rows = Scene::ppqs(scenes, *factor); let bg = state.color; let clip_bg = Color::Rgb(40, 50, 30); let header_h = 3u16;//5u16; let scenes_w = 3 + Scene::longest_name(scenes) as u16; // x of 1st track let clock = &self.0.clock; 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(Color::Rgb(0, 0, 0))); 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 = Color::Rgb(0, 0, 0); } } }) }))?; // 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 player = &track.player; 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))) = player.phrase.as_ref() { let length = phrase.read().unwrap().length; let elapsed = player.pulses_since_start().unwrap(); let elapsed = clock.timebase().format_beats_1_short( (elapsed as usize % length) as f64 ); format!("▎+{elapsed:>}") } else { String::from("▎") }; // beats until switchover let until_next = player.next_phrase.as_ref().map(|(t, _)|{ let target = t.pulse.get(); let current = clock.current.pulse.get(); if target > current { let remaining = target - current; format!("▎-{:>}", clock.timebase().format_beats_0_short(remaining)) } else { String::new() } }).unwrap_or(String::from("▎")); // name of active MIDI input let input = format!("▎>{}", track.player.midi_inputs.get(0) .map(|port|port.short_name()) .transpose()? .unwrap_or("(none)".into())); // name of active MIDI output let output = format!("▎<{}", track.player.midi_outputs.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) }); // scene titles let scene_name = |scene, playing: bool, height|row!( if playing { "▶ " } else { " " }, TuiStyle::bold((scene as &Scene).name.read().unwrap().as_str(), true), ).fixed_xy(scenes_w, height); // scene clips let scene_clip = |scene, track: usize, w: u16, h: u16|Layers::new(move |add|{ let mut bg = clip_bg; match (tracks.get(track), (scene as &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))?; bg = color.dark.rgb; if let Some((_, Some(ref playing))) = track.player.phrase { if *playing.read().unwrap() == *phrase.read().unwrap() { bg = color.light.rgb } }; }, _ => {} }; add(&Background(bg)) }).fixed_xy(w, h); // 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(&scene_name(scene, playing, height).bg(scene.color.rgb))?; // clip per track: Ok(for (track, w) in cols.iter().map(|col|col.0).enumerate() { add(&scene_clip(scene, track, w as u16, height))?; }) }).fixed_y(height) } ).fixed_y((self.0.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 = state.focused; let selected = state.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 { ArrangementFocus::Mix => 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)); } 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 = if self.0.focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)}; let size = format!("{}x{}", self.0.size.w(), self.0.size.h()); let lower_right = TuiStyle::fg(size, color).pull_x(1).align_se().fill_xy(); lay!(arrangement, lower_right) } } 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; Ok(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; }) }), ) ) } } /// Handle top-level events in standalone arranger. impl Handle for Arranger { fn handle (&mut self, from: &TuiInput) -> Perhaps { Ok(if let Some(command) = ArrangerCommand::match_input(self, from) { let _undo = command.execute(self)?; Some(true) } else { None }) } } /// Handle events for arrangement. impl Handle for Arrangement { fn handle (&mut self, from: &TuiInput) -> Perhaps { Ok(if let Some(command) = ArrangementCommand::match_input(self, from) { let _undo = command.execute(self)?; Some(true) } else { None }) } } impl MatchInput> for ArrangerCommand { fn match_input (state: &Arranger, input: &TuiInput) -> Option { match input.event() { key!(KeyCode::Tab) => Some(Self::FocusNext), key!(Shift-KeyCode::Tab) => Some(Self::FocusPrev), key!(KeyCode::BackTab) => Some(Self::FocusPrev), key!(Shift-KeyCode::BackTab) => Some(Self::FocusPrev), key!(KeyCode::Up) => Some(Self::FocusUp), key!(KeyCode::Down) => Some(Self::FocusDown), key!(KeyCode::Left) => Some(Self::FocusLeft), key!(KeyCode::Right) => Some(Self::FocusRight), key!(KeyCode::Char(' ')) => Some(Self::Transport(TransportCommand::PlayToggle)), _ => match state.focused() { ArrangerFocus::Transport => state.transport.as_ref() .map(|t|TransportCommand::match_input(&*t.read().unwrap(), input) .map(Self::Transport)) .flatten(), ArrangerFocus::PhrasePool => PhrasePoolCommand::match_input(&*state.phrases.read().unwrap(), input) .map(Self::Phrases), ArrangerFocus::PhraseEditor => PhraseEditorCommand::match_input(&state.editor, input) .map(Self::Editor), ArrangerFocus::Arrangement => ArrangementCommand::match_input(&state.arrangement, &input) .map(Self::Arrangement) } } } } impl MatchInput> for ArrangementCommand { fn match_input (_: &Arrangement, input: &TuiInput) -> Option { match input.event() { key!(KeyCode::Char('`')) => Some(Self::ToggleViewMode), key!(KeyCode::Delete) => Some(Self::Delete), key!(KeyCode::Enter) => Some(Self::Activate), key!(KeyCode::Char('.')) => Some(Self::Increment), key!(KeyCode::Char(',')) => Some(Self::Decrement), key!(KeyCode::Char('+')) => Some(Self::ZoomIn), key!(KeyCode::Char('=')) => Some(Self::ZoomOut), key!(KeyCode::Char('_')) => Some(Self::ZoomOut), key!(KeyCode::Char('-')) => Some(Self::ZoomOut), key!(KeyCode::Char('<')) => Some(Self::MoveBack), key!(KeyCode::Char('>')) => Some(Self::MoveForward), key!(KeyCode::Char('c')) => Some(Self::RandomColor), key!(KeyCode::Char('s')) => Some(Self::Put), key!(KeyCode::Char('g')) => Some(Self::Get), key!(Ctrl-KeyCode::Char('a')) => Some(Self::AddScene), key!(Ctrl-KeyCode::Char('t')) => Some(Self::AddTrack), key!(KeyCode::Char('l')) => Some(Self::ToggleLoop), key!(KeyCode::Up) => Some(Self::GoUp), key!(KeyCode::Down) => Some(Self::GoDown), key!(KeyCode::Left) => Some(Self::GoLeft), key!(KeyCode::Right) => Some(Self::GoRight), _ => None } } } //impl Arranger { ///// Helper for event passthru to focused component //fn handle_focused (&mut self, from: &TuiInput) -> Perhaps { //match self.focused() { //ArrangerFocus::Transport => self.transport.handle(from), //ArrangerFocus::PhrasePool => self.handle_pool(from), //ArrangerFocus::PhraseEditor => self.editor.handle(from), //ArrangerFocus::Arrangement => self.handle_arrangement(from) //.and_then(|result|{self.show_phrase();Ok(result)}), //} //} ///// Helper for phrase event passthru when phrase pool is focused //fn handle_pool (&mut self, from: &TuiInput) -> Perhaps { //match from.event() { //key!(KeyCode::Char('<')) => { //self.phrases_split = self.phrases_split.saturating_sub(1).max(12); //}, //key!(KeyCode::Char('>')) => { //self.phrases_split = self.phrases_split + 1; //}, //_ => return self.phrases.handle(from) //} //Ok(Some(true)) //} ///// Helper for phrase event passthru when arrangement is focused //fn handle_arrangement (&mut self, from: &TuiInput) -> Perhaps { //let mut handle_phrase = ||{ //let result = self.phrases.handle(from); //self.arrangement.phrase_put(); //result //}; //match from.event() { //key!(KeyCode::Char('a')) => return handle_phrase(), //key!(KeyCode::Char('i')) => return handle_phrase(), //key!(KeyCode::Char('d')) => return handle_phrase(), //key!(KeyCode::Char('<')) => if self.arrangement.selected == ArrangementFocus::Mix { //self.arrangement_split = self.arrangement_split.saturating_sub(1).max(12); //} else { //return self.arrangement.handle(from) //}, //key!(KeyCode::Char('>')) => if self.arrangement.selected == ArrangementFocus::Mix { //self.arrangement_split = self.arrangement_split + 1; //} else { //return self.arrangement.handle(from) //}, //_ => return self.arrangement.handle(from) //} //self.show_phrase(); //Ok(Some(true)) //} //}