diff --git a/crates/tek/src/cli/cli_arranger.rs b/crates/tek/src/cli/cli_arranger.rs index 10f8223c..dde92c12 100644 --- a/crates/tek/src/cli/cli_arranger.rs +++ b/crates/tek/src/cli/cli_arranger.rs @@ -23,6 +23,7 @@ impl ArrangerCli { fn run (&self) -> Usually<()> { Tui::run(JackClient::new("tek_arranger")?.activate_with(|jack|{ let mut app = ArrangerTui::try_from(jack)?; + app.color = ItemPalette::random(); if let Some(name) = self.name.as_ref() { *app.name.write().unwrap() = name.clone(); } diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 4534c36a..fcf1ba66 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -25,6 +25,15 @@ mod app_sampler; pub(crate) use app_sampler::*; mod app_groovebox; pub(crate) use app_groovebox::*; mod app_arranger; pub(crate) use app_arranger::*; +/////////////////////////////////////////////////////// + +mod arranger_clip; pub(crate) use arranger_clip::*; +mod arranger_scene; pub(crate) use arranger_scene::*; +mod arranger_select; pub(crate) use arranger_select::*; +mod arranger_track; pub(crate) use arranger_track::*; +mod arranger_mode_h; pub(crate) use arranger_mode_h::*; +mod arranger_mode_v; pub(crate) use arranger_mode_v::*; + //////////////////////////////////////////////////////// mod status_bar; pub(crate) use status_bar::*; diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index c326cd01..20c27cd4 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -11,7 +11,6 @@ pub struct ArrangerTui { pub selected: ArrangerSelection, pub mode: ArrangerMode, pub color: ItemPalette, - pub entered: bool, pub size: Measure, pub cursor: (usize, usize), pub menu_bar: Option>, @@ -20,7 +19,6 @@ pub struct ArrangerTui { pub note_buf: Vec, pub midi_buf: Vec>>, pub editor: PhraseEditorModel, - pub focus: FocusState, pub perf: PerfModel, } from_jack!(|jack| ArrangerTui Self { @@ -38,13 +36,11 @@ from_jack!(|jack| ArrangerTui Self { size: Measure::new(), cursor: (0, 0), splits: [16, 20], - entered: false, menu_bar: None, status_bar: None, midi_buf: vec![vec![];65536], note_buf: vec![], perf: PerfModel::default(), - focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)), }); has_clock!(|self: ArrangerTui|&self.clock); has_phrases!(|self: ArrangerTui|self.phrases.phrases); @@ -52,9 +48,8 @@ has_editor!(|self: ArrangerTui|self.editor); handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); render!(|self: ArrangerTui|{ let arranger = ||lay!(|add|{ - add(&Fill::wh(Lozenge(Style::default() - .bg(TuiTheme::border_bg()) - .fg(TuiTheme::border_fg(true)))))?; + let color = self.color; + add(&Fill::wh(Lozenge(Style::default().bg(color.light.rgb).fg(color.darker.rgb))))?; add(&self.size)?; match self.mode { ArrangerMode::H => todo!("horizontal arranger"), @@ -62,7 +57,7 @@ render!(|self: ArrangerTui|{ Align::se(Fill::wh(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(true), format!("{}x{}", self.size.w(), self.size.h())) ))), - Tui::bg(self.color.dark.rgb, lay!(![ + Tui::bg(color.darkest.rgb, lay!(![ ArrangerVColumnSeparator::from(self), ArrangerVRowSeparator::from((self, factor)), col!(![ @@ -118,7 +113,6 @@ audio!(|self: ArrangerTui, client, scope|{ return Control::Continue }); #[derive(Clone, Debug)] pub enum ArrangerCommand { - Focus(FocusCommand), Undo, Redo, Clear, @@ -132,50 +126,27 @@ audio!(|self: ArrangerTui, client, scope|{ Phrases(PhrasesCommand), Editor(PhraseCommand), } -input_to_command!(ArrangerCommand: |state:ArrangerTui,input| - to_arranger_command(state, input) - .or_else(||to_focus_command(input).map(ArrangerCommand::Focus))?); +input_to_command!(ArrangerCommand: |state:ArrangerTui,input|to_arranger_command(state, input)?); fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { + use ArrangerSelection::*; use ArrangerCommand as Cmd; use KeyCode::Char; - if !state.entered() { - return None - } + // WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor Some(match input.event() { key_pat!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))), - // WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor - _ => match state.focused() { - ArrangerFocus::Transport(_) => { - match to_transport_command(state, input)? { - TransportCommand::Clock(command) => Cmd::Clock(command), - _ => return None, - } - }, - ArrangerFocus::PhraseEditor => { - Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?) - }, - ArrangerFocus::Phrases => { - Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?) - }, - ArrangerFocus::Arranger => { - use ArrangerSelection::*; - match input.event() { - key_pat!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), - key_pat!(Char('+')) => Cmd::Zoom(0), // TODO - key_pat!(Char('=')) => Cmd::Zoom(0), // TODO - key_pat!(Char('_')) => Cmd::Zoom(0), // TODO - key_pat!(Char('-')) => Cmd::Zoom(0), // TODO - key_pat!(Char('`')) => { todo!("toggle state mode") }, - key_pat!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), - key_pat!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), - _ => match state.selected() { - Mix => to_arranger_mix_command(input)?, - Track(t) => to_arranger_track_command(input, t)?, - Scene(s) => to_arranger_scene_command(input, s)?, - Clip(t, s) => to_arranger_clip_command(input, t, s)?, - } - } - } + key_pat!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), + key_pat!(Char('+')) => Cmd::Zoom(0), // TODO + key_pat!(Char('=')) => Cmd::Zoom(0), // TODO + key_pat!(Char('_')) => Cmd::Zoom(0), // TODO + key_pat!(Char('-')) => Cmd::Zoom(0), // TODO + key_pat!(Char('`')) => { todo!("toggle state mode") }, + key_pat!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), + key_pat!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), + _ => match state.selected() { + Mix => to_arranger_mix_command(input)?, + Track(t) => to_arranger_track_command(input, t)?, + Scene(s) => to_arranger_scene_command(input, s)?, + Clip(t, s) => to_arranger_clip_command(input, t, s)?, } }) } @@ -184,8 +155,8 @@ fn to_arranger_mix_command (input: &TuiInput) -> Option { use ArrangerCommand as Cmd; use ArrangerSelection as Select; Some(match input.event() { - key_pat!(Down) => Cmd::Select(Select::Scene(0)), - key_pat!(Right) => Cmd::Select(Select::Track(0)), + key_pat!(Char('s')) => Cmd::Select(Select::Scene(0)), + key_pat!(Char('d')) => Cmd::Select(Select::Track(0)), key_pat!(Char(',')) => Cmd::Zoom(0), key_pat!(Char('.')) => Cmd::Zoom(0), key_pat!(Char('<')) => Cmd::Zoom(0), @@ -195,68 +166,9 @@ fn to_arranger_mix_command (input: &TuiInput) -> Option { _ => return None }) } -fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { - use KeyCode::{Char, Down, Left, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerTrackCommand as Track; - Some(match input.event() { - key_pat!(Down) => Cmd::Select(Select::Clip(t, 0)), - key_pat!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }), - key_pat!(Right) => Cmd::Select(Select::Track(t + 1)), - key_pat!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)), - key_pat!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)), - key_pat!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)), - key_pat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), - key_pat!(Delete) => Cmd::Track(Track::Delete(t)), - //key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemPalette::random())), - _ => return None - }) -} -fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { - use KeyCode::{Char, Up, Down, Right, Enter, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerSceneCommand as Scene; - Some(match input.event() { - key_pat!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }), - key_pat!(Down) => Cmd::Select(Select::Scene(s + 1)), - key_pat!(Right) => Cmd::Select(Select::Clip(0, s)), - key_pat!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)), - key_pat!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)), - key_pat!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)), - key_pat!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)), - key_pat!(Enter) => Cmd::Scene(Scene::Play(s)), - key_pat!(Delete) => Cmd::Scene(Scene::Delete(s)), - //key_pat!(Char('c')) => Cmd::Track(Scene::Color(s, ItemPalette::random())), - _ => return None - }) -} -fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option { - use KeyCode::{Char, Up, Down, Left, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerClipCommand as Clip; - Some(match input.event() { - key_pat!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }), - key_pat!(Down) => Cmd::Select(Select::Clip(t, s + 1)), - key_pat!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }), - key_pat!(Right) => Cmd::Select(Select::Clip(t + 1, s)), - key_pat!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)), - key_pat!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)), - key_pat!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)), - key_pat!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)), - key_pat!(Delete) => Cmd::Clip(Clip::Set(t, s, None)), - //key_pat!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemPalette::random())), - //key_pat!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))), - //key_pat!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))), - _ => return None - }) -} command!(|self:ArrangerCommand,state:ArrangerTui|{ use ArrangerCommand::*; match self { - Focus(cmd) => cmd.execute(state)?.map(Focus), Scene(cmd) => cmd.execute(state)?.map(Scene), Track(cmd) => cmd.execute(state)?.map(Track), Clip(cmd) => cmd.execute(state)?.map(Clip), @@ -279,15 +191,43 @@ command!(|self:ArrangerCommand,state:ArrangerTui|{ command!(|self:ArrangerSceneCommand,_state:ArrangerTui|None); command!(|self:ArrangerTrackCommand,_state:ArrangerTui|None); command!(|self:ArrangerClipCommand, _state:ArrangerTui|None); -pub trait ArrangerControl: TransportControl { - fn selected (&self) -> ArrangerSelection; - fn selected_mut (&mut self) -> &mut ArrangerSelection; - fn selected_phrase (&self) -> Option>>; - fn activate (&mut self) -> Usually<()>; - fn toggle_loop (&mut self); - fn randomize_color (&mut self); +#[derive(Clone, Debug)] +pub enum ArrangerClipCommand { + Play, + Get(usize, usize), + Set(usize, usize, Option>>), + Edit(Option>>), + SetLoop(bool), + RandomColor, } -impl ArrangerControl for ArrangerTui { + +/// Display mode of arranger +#[derive(Clone, PartialEq)] +pub enum ArrangerMode { + /// Tracks are columns + V(usize), + /// Tracks are rows + H, +} + +/// Arranger display mode can be cycled +impl ArrangerMode { + /// Cycle arranger display mode + pub fn to_next (&mut self) { + *self = match self { + Self::H => Self::V(1), + Self::V(1) => Self::V(2), + Self::V(2) => Self::V(2), + Self::V(0) => Self::H, + Self::V(_) => Self::V(0), + } + } +} + +fn any_size (_: E::Size) -> Perhaps{ + Ok(Some([0.into(),0.into()].into())) +} +impl ArrangerTui { fn selected (&self) -> ArrangerSelection { self.selected } @@ -338,549 +278,6 @@ impl ArrangerControl for ArrangerTui { } } } -impl TransportControl for ArrangerTui { - fn transport_focused (&self) -> Option { - match self.focus.inner() { - ArrangerFocus::Transport(focus) => Some(focus), - _ => None - } - } -} - -/// Sections in the arranger app that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum ArrangerFocus { - /// The transport (toolbar) is focused - Transport(TransportFocus), - /// The arrangement (grid) is focused - Arranger, - /// The phrase list (pool) is focused - Phrases, - /// The phrase editor (sequencer) is focused - PhraseEditor, -} - -impl Into> for ArrangerFocus { - fn into (self) -> Option { - if let Self::Transport(transport) = self { - Some(transport) - } else { - None - } - } -} - -impl From<&ArrangerTui> for Option { - fn from (state: &ArrangerTui) -> Self { - match state.focus.inner() { - ArrangerFocus::Transport(focus) => Some(focus), - _ => None - } - } -} - -impl_focus!(ArrangerTui ArrangerFocus [ - &[ - Transport(TransportFocus::PlayPause), - Transport(TransportFocus::Bpm), - Transport(TransportFocus::Sync), - Transport(TransportFocus::Quant), - Transport(TransportFocus::Clock), - ], - &[Arranger;5], - &[Phrases, Phrases, PhraseEditor, PhraseEditor, PhraseEditor], -]); - -/// Status bar for arranger app -#[derive(Copy, Clone, Debug)] -pub enum ArrangerStatus { - Transport, - ArrangerMix, - ArrangerTrack, - ArrangerScene, - ArrangerClip, - PhrasePool, - PhraseView, - PhraseEdit, -} - -/// Display mode of arranger -#[derive(Clone, PartialEq)] -pub enum ArrangerMode { - /// Tracks are columns - V(usize), - /// Tracks are rows - H, -} - -/// Arranger display mode can be cycled -impl ArrangerMode { - /// Cycle arranger display mode - pub fn to_next (&mut self) { - *self = match self { - Self::H => Self::V(1), - Self::V(1) => Self::V(2), - Self::V(2) => Self::V(2), - Self::V(0) => Self::H, - Self::V(_) => Self::V(0), - } - } -} - -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 any_size (_: E::Size) -> Perhaps{ - Ok(Some([0.into(),0.into()].into())) -} - -struct ArrangerVColumnSeparator { - cols: Vec<(usize, usize)>, - scenes_w: u16, - sep_fg: Color, -} -impl From<&ArrangerTui> for ArrangerVColumnSeparator { - fn from (state: &ArrangerTui) -> Self { - Self { - cols: track_widths(state.tracks()), - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - sep_fg: TuiTheme::separator_fg(false), - } - } -} -render!(|self: ArrangerVColumnSeparator|render(move|to: &mut TuiOutput|{ - let style = Some(Style::default().fg(self.sep_fg)); - Ok(for x in self.cols.iter().map(|col|col.1) { - let x = self.scenes_w + to.area().x() + x as u16; - for y in to.area().y()..to.area().y2() { - to.blit(&"▎", x, y, style); - } - }) -})); - -struct ArrangerVRowSeparator { - rows: Vec<(usize, usize)>, - sep_fg: Color, -} -impl From<(&ArrangerTui, usize)> for ArrangerVRowSeparator { - fn from ((state, factor): (&ArrangerTui, usize)) -> Self { - Self { - rows: ArrangerScene::ppqs(state.scenes(), factor), - sep_fg: TuiTheme::separator_fg(false), - } - } -} - -render!(|self: ArrangerVRowSeparator|render(move|to: &mut TuiOutput|{ - Ok(for y in self.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 = self.sep_fg; - } - } - }) -})); - -struct ArrangerVCursor { - cols: Vec<(usize, usize)>, - rows: Vec<(usize, usize)>, - focused: bool, - selected: ArrangerSelection, - scenes_w: u16, - header_h: u16, -} -impl From<(&ArrangerTui, usize)> for ArrangerVCursor { - fn from ((state, factor): (&ArrangerTui, usize)) -> Self { - Self { - cols: track_widths(state.tracks()), - rows: ArrangerScene::ppqs(state.scenes(), factor), - focused: true, - selected: state.selected, - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - header_h: 3, - } - } -} -render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ - let area = to.area(); - let focused = self.focused; - 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(), self.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, - self.header_h + area.y() + (self.rows[s].1/PPQ) as u16, - self.cols[t].0 as u16, - (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 = 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(self.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(self.header_h) }, &CORNERS)? - }) -})); - -struct ArrangerVHeader<'a> { - tracks: &'a Vec, - cols: Vec<(usize, usize)>, - focused: bool, - selected: ArrangerSelection, - scenes_w: u16, - header_h: u16, - timebase: &'a Arc, - current: &'a Arc, -} -impl<'a> From<&'a ArrangerTui> for ArrangerVHeader<'a> { - fn from (state: &'a ArrangerTui) -> Self { - Self { - tracks: &state.tracks, - cols: track_widths(state.tracks()), - focused: true, - selected: state.selected, - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - header_h: 3, - timebase: state.clock().timebase(), - current: &state.clock().playhead, - } - } -} -render!(|self: ArrangerVHeader<'a>|row!( - (track, w) in self.tracks.iter().zip(self.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 = Tui::bold(true, Tui::fg(track.color.lightest.rgb, name)); - // beats elapsed - let elapsed = 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(); - let elapsed = self.timebase.format_beats_1_short( - (elapsed as usize % length) as f64 - ); - format!("▎+{elapsed:>}") - } else { - String::from("▎") - }; - // beats until switchover - let until_next = track.player.next_phrase().as_ref().map(|(t, _)|{ - let target = t.pulse.get(); - let current = self.current.pulse.get(); - if target > current { - let remaining = target - current; - format!("▎-{:>}", self.timebase.format_beats_0_short(remaining)) - } else { - String::new() - } - }).unwrap_or(String::from("▎")); - let timer = col!([until_next, elapsed]); - // name of active MIDI input - let _input = format!("▎>{}", track.player.midi_ins().get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into())); - // name of active MIDI output - let _output = format!("▎<{}", track.player.midi_outs().get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into())); - Tui::push_x(self.scenes_w, - Tui::bg(track.color().base.rgb, - Tui::min_xy(w as u16, self.header_h, - col!([name, timer])))) - } -)); - -struct ArrangerVContent<'a> { - size: &'a Measure, - scenes: &'a Vec, - tracks: &'a Vec, - rows: Vec<(usize, usize)>, - cols: Vec<(usize, usize)>, - header_h: u16, -} -impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVContent<'a> { - fn from ((state, factor): (&'a ArrangerTui, usize)) -> Self { - Self { - size: &state.size, - scenes: &state.scenes, - tracks: &state.tracks, - rows: ArrangerScene::ppqs(state.scenes(), factor), - cols: track_widths(state.tracks()), - header_h: 3, - } - } -} -render!(|self: ArrangerVContent<'a>|Fixed::h( - (self.size.h() as u16).saturating_sub(self.header_h), - col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => { - let height = 1.max((pulses / PPQ) as u16); - let playing = scene.is_playing(self.tracks); - Fixed::h(height, row!([ - if playing { "▶ " } else { " " }, - Tui::bold(true, scene.name.read().unwrap().as_str()), - row!((track, w) in self.cols.iter().map(|col|col.0).enumerate() => { - Fixed::wh(w as u16, height, Layers::new(move |add|{ - let mut bg = TuiTheme::border_bg(); - match (self.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; - bg = color.dark.rgb; - if let Some((_, Some(ref playing))) = track.player.play_phrase() { - if *playing.read().unwrap() == *phrase.read().unwrap() { - bg = color.light.rgb - } - }; - add(&Fixed::w(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?; - }, - _ => {} - }; - //add(&Background(bg)) - Ok(()) - })) - })]) - ) - }) -)); - -impl HasScenes for ArrangerTui { - fn scenes (&self) -> &Vec { - &self.scenes - } - fn scenes_mut (&mut self) -> &mut Vec { - &mut self.scenes - } - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerScene> - { - let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); - let scene = ArrangerScene { - name: Arc::new(name.into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(||ItemPalette::random()), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok(&mut self.scenes_mut()[index]) - } - fn selected_scene (&self) -> Option<&ArrangerScene> { - self.selected.scene().map(|s|self.scenes().get(s)).flatten() - } - fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { - self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() - } -} - -#[derive(Default, Debug, Clone)] pub struct ArrangerScene { - /// Name of scene - pub(crate) name: Arc>, - /// Clips in scene, one per track - pub(crate) clips: Vec>>>, - /// Identifying color of scene - pub(crate) color: ItemPalette, -} -impl ArrangerSceneApi for ArrangerScene { - fn name (&self) -> &Arc> { - &self.name - } - fn clips (&self) -> &Vec>>> { - &self.clips - } - fn color (&self) -> ItemPalette { - self.color - } -} -impl HasTracks for ArrangerTui { - fn tracks (&self) -> &Vec { - &self.tracks - } - fn tracks_mut (&mut self) -> &mut Vec { - &mut self.tracks - } -} -impl ArrangerTracksApi for ArrangerTui { - fn track_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerTrack> - { - let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); - let track = ArrangerTrack { - width: name.len() + 2, - name: Arc::new(name.into()), - color: color.unwrap_or_else(||ItemPalette::random()), - player: PhrasePlayerModel::from(&self.clock), - }; - self.tracks_mut().push(track); - let index = self.tracks().len() - 1; - Ok(&mut self.tracks_mut()[index]) - } - fn track_del (&mut self, index: usize) { - self.tracks_mut().remove(index); - for scene in self.scenes_mut().iter_mut() { - scene.clips.remove(index); - } - } -} - -#[derive(Debug)] pub struct ArrangerTrack { - /// Name of track - pub(crate) name: Arc>, - /// Preferred width of track column - pub(crate) width: usize, - /// Identifying color of track - pub(crate) color: ItemPalette, - /// MIDI player state - pub(crate) player: PhrasePlayerModel, -} -has_clock!(|self:ArrangerTrack|self.player.clock()); -has_player!(|self:ArrangerTrack|self.player); -impl ArrangerTrackApi for ArrangerTrack { - /// Name of track - fn name (&self) -> &Arc> { - &self.name - } - /// Preferred width of track column - fn width (&self) -> usize { - self.width - } - /// Preferred width of track column - fn width_mut (&mut self) -> &mut usize { - &mut self.width - } - /// Identifying color of track - fn color (&self) -> ItemPalette { - self.color - } -} -#[derive(PartialEq, Clone, Copy, Debug)] -/// Represents the current user selection in the arranger -pub enum ArrangerSelection { - /// The whole mix is selected - Mix, - /// A track is selected. - Track(usize), - /// A scene is selected. - Scene(usize), - /// A clip (track × scene) is selected. - Clip(usize, usize), -} - -/// Focus identification methods -impl ArrangerSelection { - pub fn description ( - &self, - tracks: &Vec, - scenes: &Vec, - ) -> String { - format!("Selected: {}", match self { - Self::Mix => format!("Everything"), - Self::Track(t) => match tracks.get(*t) { - Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), - None => format!("T??"), - }, - Self::Scene(s) => match scenes.get(*s) { - Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), - None => format!("S??"), - }, - Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { - (Some(_), Some(scene)) => match scene.clip(*t) { - Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), - None => format!("T{t} S{s}: Empty") - }, - _ => format!("T{t} S{s}: Empty"), - } - }) - } - pub fn is_mix (&self) -> bool { - match self { Self::Mix => true, _ => false } - } - pub fn is_track (&self) -> bool { - match self { Self::Track(_) => true, _ => false } - } - pub fn is_scene (&self) -> bool { - match self { Self::Scene(_) => true, _ => false } - } - pub fn is_clip (&self) -> bool { - match self { Self::Clip(_, _) => true, _ => false } - } - pub fn track (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(t, _) => Some(*t), - Track(t) => Some(*t), - _ => None - } - } - pub fn scene (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(_, s) => Some(*s), - Scene(s) => Some(*s), - _ => None - } - } -} - -#[derive(Clone, Debug)] -pub enum ArrangerClipCommand { - Play, - Get(usize, usize), - Set(usize, usize, Option>>), - Edit(Option>>), - SetLoop(bool), - RandomColor, -} //impl Command for ArrangerClipCommand { //fn execute (self, state: &mut T) -> Perhaps { @@ -890,12 +287,6 @@ pub enum ArrangerClipCommand { //Ok(None) //} //} - -pub fn arranger_content_horizontal ( - view: &ArrangerTui, -) -> impl Render + use<'_> { - todo!() -} //let focused = true; //let _tracks = view.tracks(); //lay!( diff --git a/crates/tek/src/tui/arranger_clip.rs b/crates/tek/src/tui/arranger_clip.rs new file mode 100644 index 00000000..040db3aa --- /dev/null +++ b/crates/tek/src/tui/arranger_clip.rs @@ -0,0 +1,23 @@ +use crate::*; + +pub fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option { + use KeyCode::{Char, Up, Down, Left, Right, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerClipCommand as Clip; + Some(match input.event() { + key_pat!(Char('w')) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }), + key_pat!(Char('s')) => Cmd::Select(Select::Clip(t, s + 1)), + key_pat!(Char('a')) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }), + key_pat!(Char('d')) => Cmd::Select(Select::Clip(t + 1, s)), + key_pat!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)), + key_pat!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)), + key_pat!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)), + key_pat!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)), + key_pat!(Delete) => Cmd::Clip(Clip::Set(t, s, None)), + //key_pat!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemPalette::random())), + //key_pat!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))), + //key_pat!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))), + _ => return None + }) +} diff --git a/crates/tek/src/tui/arranger_mode_h.rs b/crates/tek/src/tui/arranger_mode_h.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs new file mode 100644 index 00000000..227d982f --- /dev/null +++ b/crates/tek/src/tui/arranger_mode_v.rs @@ -0,0 +1,274 @@ +use crate::*; + +pub struct ArrangerVColumnSeparator { + cols: Vec<(usize, usize)>, + scenes_w: u16, + sep_fg: Color, +} + +impl From<&ArrangerTui> for ArrangerVColumnSeparator { + fn from (state: &ArrangerTui) -> Self { + Self { + cols: track_widths(state.tracks()), + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + sep_fg: TuiTheme::separator_fg(false), + } + } +} + +render!(|self: ArrangerVColumnSeparator|render(move|to: &mut TuiOutput|{ + let style = Some(Style::default().fg(self.sep_fg)); + Ok(for x in self.cols.iter().map(|col|col.1) { + let x = self.scenes_w + to.area().x() + x as u16; + for y in to.area().y()..to.area().y2() { + to.blit(&"▎", x, y, style); + } + }) +})); + +pub struct ArrangerVRowSeparator { + rows: Vec<(usize, usize)>, + sep_fg: Color, +} + +impl From<(&ArrangerTui, usize)> for ArrangerVRowSeparator { + fn from ((state, factor): (&ArrangerTui, usize)) -> Self { + Self { + rows: ArrangerScene::ppqs(state.scenes(), factor), + sep_fg: TuiTheme::separator_fg(false), + } + } +} + +render!(|self: ArrangerVRowSeparator|render(move|to: &mut TuiOutput|{ + Ok(for y in self.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 = self.sep_fg; + } + } + }) +})); + +pub struct ArrangerVCursor { + cols: Vec<(usize, usize)>, + rows: Vec<(usize, usize)>, + focused: bool, + selected: ArrangerSelection, + scenes_w: u16, + header_h: u16, +} + +impl From<(&ArrangerTui, usize)> for ArrangerVCursor { + fn from ((state, factor): (&ArrangerTui, usize)) -> Self { + Self { + cols: track_widths(state.tracks()), + rows: ArrangerScene::ppqs(state.scenes(), factor), + focused: true, + selected: state.selected, + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + header_h: 3, + } + } +} + +render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ + let area = to.area(); + let focused = self.focused; + 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(), self.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, + self.header_h + area.y() + (self.rows[s].1/PPQ) as u16, + self.cols[t].0 as u16, + (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 = 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(self.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(self.header_h) }, &CORNERS)? + }) +})); + +pub struct ArrangerVHeader<'a> { + tracks: &'a Vec, + cols: Vec<(usize, usize)>, + focused: bool, + selected: ArrangerSelection, + scenes_w: u16, + header_h: u16, + timebase: &'a Arc, + current: &'a Arc, +} + +impl<'a> From<&'a ArrangerTui> for ArrangerVHeader<'a> { + fn from (state: &'a ArrangerTui) -> Self { + Self { + tracks: &state.tracks, + cols: track_widths(state.tracks()), + focused: true, + selected: state.selected, + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + header_h: 3, + timebase: state.clock().timebase(), + current: &state.clock().playhead, + } + } +} + +render!(|self: ArrangerVHeader<'a>|row!( + (track, w) in self.tracks.iter().zip(self.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 = Tui::bold(true, Tui::fg(track.color.lightest.rgb, name)); + // beats elapsed + let elapsed = 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(); + let elapsed = self.timebase.format_beats_1_short( + (elapsed as usize % length) as f64 + ); + format!("▎+{elapsed:>}") + } else { + String::from("▎") + }; + // beats until switchover + let until_next = track.player.next_phrase().as_ref().map(|(t, _)|{ + let target = t.pulse.get(); + let current = self.current.pulse.get(); + if target > current { + let remaining = target - current; + format!("▎-{:>}", self.timebase.format_beats_0_short(remaining)) + } else { + String::new() + } + }).unwrap_or(String::from("▎")); + let timer = col!([until_next, elapsed]); + // name of active MIDI input + let _input = format!("▎>{}", track.player.midi_ins().get(0) + .map(|port|port.short_name()) + .transpose()? + .unwrap_or("(none)".into())); + // name of active MIDI output + let _output = format!("▎<{}", track.player.midi_outs().get(0) + .map(|port|port.short_name()) + .transpose()? + .unwrap_or("(none)".into())); + Tui::push_x(self.scenes_w, + Tui::bg(track.color().base.rgb, + Tui::min_xy(w as u16, self.header_h, + col!([name, timer])))) + } +)); + +pub struct ArrangerVContent<'a> { + size: &'a Measure, + scenes: &'a Vec, + tracks: &'a Vec, + rows: Vec<(usize, usize)>, + cols: Vec<(usize, usize)>, + header_h: u16, +} + +impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVContent<'a> { + fn from ((state, factor): (&'a ArrangerTui, usize)) -> Self { + Self { + size: &state.size, + scenes: &state.scenes, + tracks: &state.tracks, + rows: ArrangerScene::ppqs(state.scenes(), factor), + cols: track_widths(state.tracks()), + header_h: 3, + } + } +} + +render!(|self: ArrangerVContent<'a>|Fixed::h( + (self.size.h() as u16).saturating_sub(self.header_h), + col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => { + let height = 1.max((pulses / PPQ) as u16); + let playing = scene.is_playing(self.tracks); + Fixed::h(height, row!([ + if playing { "▶ " } else { " " }, + Tui::bold(true, scene.name.read().unwrap().as_str()), + row!((track, w) in self.cols.iter().map(|col|col.0).enumerate() => { + Fixed::wh(w as u16, height, Layers::new(move |add|{ + let mut bg = TuiTheme::border_bg(); + match (self.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; + bg = color.dark.rgb; + if let Some((_, Some(ref playing))) = track.player.play_phrase() { + if *playing.read().unwrap() == *phrase.read().unwrap() { + bg = color.light.rgb + } + }; + add(&Fixed::w(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?; + }, + _ => {} + }; + //add(&Background(bg)) + Ok(()) + })) + })]) + ) + }) +)); + +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 +} diff --git a/crates/tek/src/tui/arranger_scene.rs b/crates/tek/src/tui/arranger_scene.rs new file mode 100644 index 00000000..4f0139be --- /dev/null +++ b/crates/tek/src/tui/arranger_scene.rs @@ -0,0 +1,66 @@ +use crate::*; +pub fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { + use KeyCode::{Char, Up, Down, Right, Enter, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerSceneCommand as Scene; + Some(match input.event() { + key_pat!(Char('w')) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }), + key_pat!(Char('s')) => Cmd::Select(Select::Scene(s + 1)), + key_pat!(Char('d')) => Cmd::Select(Select::Clip(0, s)), + key_pat!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)), + key_pat!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)), + key_pat!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)), + key_pat!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)), + key_pat!(Char('q')) => Cmd::Scene(Scene::Play(s)), + key_pat!(Delete) => Cmd::Scene(Scene::Delete(s)), + //key_pat!(Char('c')) => Cmd::Track(Scene::Color(s, ItemPalette::random())), + _ => return None + }) +} +impl HasScenes for ArrangerTui { + fn scenes (&self) -> &Vec { + &self.scenes + } + fn scenes_mut (&mut self) -> &mut Vec { + &mut self.scenes + } + fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerScene> + { + let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); + let scene = ArrangerScene { + name: Arc::new(name.into()), + clips: vec![None;self.tracks().len()], + color: color.unwrap_or_else(||ItemPalette::random()), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok(&mut self.scenes_mut()[index]) + } + fn selected_scene (&self) -> Option<&ArrangerScene> { + self.selected.scene().map(|s|self.scenes().get(s)).flatten() + } + fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { + self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() + } +} +#[derive(Default, Debug, Clone)] pub struct ArrangerScene { + /// Name of scene + pub(crate) name: Arc>, + /// Clips in scene, one per track + pub(crate) clips: Vec>>>, + /// Identifying color of scene + pub(crate) color: ItemPalette, +} +impl ArrangerSceneApi for ArrangerScene { + fn name (&self) -> &Arc> { + &self.name + } + fn clips (&self) -> &Vec>>> { + &self.clips + } + fn color (&self) -> ItemPalette { + self.color + } +} diff --git a/crates/tek/src/tui/arranger_select.rs b/crates/tek/src/tui/arranger_select.rs new file mode 100644 index 00000000..0718e7e9 --- /dev/null +++ b/crates/tek/src/tui/arranger_select.rs @@ -0,0 +1,70 @@ +use crate::*; + +#[derive(PartialEq, Clone, Copy, Debug)] +/// Represents the current user selection in the arranger +pub enum ArrangerSelection { + /// The whole mix is selected + Mix, + /// A track is selected. + Track(usize), + /// A scene is selected. + Scene(usize), + /// A clip (track × scene) is selected. + Clip(usize, usize), +} + +/// Focus identification methods +impl ArrangerSelection { + pub fn description ( + &self, + tracks: &Vec, + scenes: &Vec, + ) -> String { + format!("Selected: {}", match self { + Self::Mix => format!("Everything"), + Self::Track(t) => match tracks.get(*t) { + Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), + None => format!("T??"), + }, + Self::Scene(s) => match scenes.get(*s) { + Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), + None => format!("S??"), + }, + Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { + (Some(_), Some(scene)) => match scene.clip(*t) { + Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), + None => format!("T{t} S{s}: Empty") + }, + _ => format!("T{t} S{s}: Empty"), + } + }) + } + pub fn is_mix (&self) -> bool { + match self { Self::Mix => true, _ => false } + } + pub fn is_track (&self) -> bool { + match self { Self::Track(_) => true, _ => false } + } + pub fn is_scene (&self) -> bool { + match self { Self::Scene(_) => true, _ => false } + } + pub fn is_clip (&self) -> bool { + match self { Self::Clip(_, _) => true, _ => false } + } + pub fn track (&self) -> Option { + use ArrangerSelection::*; + match self { + Clip(t, _) => Some(*t), + Track(t) => Some(*t), + _ => None + } + } + pub fn scene (&self) -> Option { + use ArrangerSelection::*; + match self { + Clip(_, s) => Some(*s), + Scene(s) => Some(*s), + _ => None + } + } +} diff --git a/crates/tek/src/tui/arranger_track.rs b/crates/tek/src/tui/arranger_track.rs new file mode 100644 index 00000000..74077109 --- /dev/null +++ b/crates/tek/src/tui/arranger_track.rs @@ -0,0 +1,80 @@ +use crate::*; +pub fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { + use KeyCode::{Char, Down, Left, Right, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerTrackCommand as Track; + Some(match input.event() { + key_pat!(Char('s')) => Cmd::Select(Select::Clip(t, 0)), + key_pat!(Char('a')) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }), + key_pat!(Char('d')) => Cmd::Select(Select::Track(t + 1)), + key_pat!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)), + key_pat!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)), + key_pat!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)), + key_pat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), + key_pat!(Delete) => Cmd::Track(Track::Delete(t)), + //key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemPalette::random())), + _ => return None + }) +} +impl HasTracks for ArrangerTui { + fn tracks (&self) -> &Vec { + &self.tracks + } + fn tracks_mut (&mut self) -> &mut Vec { + &mut self.tracks + } +} +impl ArrangerTracksApi for ArrangerTui { + fn track_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerTrack> + { + let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); + let track = ArrangerTrack { + width: name.len() + 2, + name: Arc::new(name.into()), + color: color.unwrap_or_else(||ItemPalette::random()), + player: PhrasePlayerModel::from(&self.clock), + }; + self.tracks_mut().push(track); + let index = self.tracks().len() - 1; + Ok(&mut self.tracks_mut()[index]) + } + fn track_del (&mut self, index: usize) { + self.tracks_mut().remove(index); + for scene in self.scenes_mut().iter_mut() { + scene.clips.remove(index); + } + } +} + +#[derive(Debug)] pub struct ArrangerTrack { + /// Name of track + pub(crate) name: Arc>, + /// Preferred width of track column + pub(crate) width: usize, + /// Identifying color of track + pub(crate) color: ItemPalette, + /// MIDI player state + pub(crate) player: PhrasePlayerModel, +} +has_clock!(|self:ArrangerTrack|self.player.clock()); +has_player!(|self:ArrangerTrack|self.player); +impl ArrangerTrackApi for ArrangerTrack { + /// Name of track + fn name (&self) -> &Arc> { + &self.name + } + /// Preferred width of track column + fn width (&self) -> usize { + self.width + } + /// Preferred width of track column + fn width_mut (&mut self) -> &mut usize { + &mut self.width + } + /// Identifying color of track + fn color (&self) -> ItemPalette { + self.color + } +} diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs index 1835f563..05f47c3e 100644 --- a/crates/tek/src/tui/status_bar.rs +++ b/crates/tek/src/tui/status_bar.rs @@ -86,112 +86,125 @@ render!(|self: SequencerStatus|Fixed::h(2, lay!([ })), ]))); -impl Bar for ArrangerStatus { - type State = (ArrangerFocus, ArrangerSelection, bool); - fn hotkey_fg () -> Color where Self: Sized { - TuiTheme::HOTKEY_FG - } - fn update (&mut self, (focused, selected, entered): &Self::State) { - *self = match focused { - //ArrangerFocus::Menu => { todo!() }, - ArrangerFocus::Transport(_) => ArrangerStatus::Transport, - ArrangerFocus::Arranger => match selected { - ArrangerSelection::Mix => ArrangerStatus::ArrangerMix, - ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack, - ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene, - ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip, - }, - ArrangerFocus::Phrases => ArrangerStatus::PhrasePool, - ArrangerFocus::PhraseEditor => match entered { - true => ArrangerStatus::PhraseEdit, - false => ArrangerStatus::PhraseView, - }, - } - } +//impl Bar for ArrangerStatus { + //type State = (ArrangerFocus, ArrangerSelection, bool); + //fn hotkey_fg () -> Color where Self: Sized { + //TuiTheme::HOTKEY_FG + //} + //fn update (&mut self, (focused, selected, entered): &Self::State) { + //*self = match focused { + ////ArrangerFocus::Menu => { todo!() }, + //ArrangerFocus::Transport(_) => ArrangerStatus::Transport, + //ArrangerFocus::Arranger => match selected { + //ArrangerSelection::Mix => ArrangerStatus::ArrangerMix, + //ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack, + //ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene, + //ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip, + //}, + //ArrangerFocus::Phrases => ArrangerStatus::PhrasePool, + //ArrangerFocus::PhraseEditor => match entered { + //true => ArrangerStatus::PhraseEdit, + //false => ArrangerStatus::PhraseView, + //}, + //} + //} +//} + +//render!(|self: ArrangerStatus|{ + + //let label = match self { + //Self::Transport => "TRANSPORT", + //Self::ArrangerMix => "PROJECT", + //Self::ArrangerTrack => "TRACK", + //Self::ArrangerScene => "SCENE", + //Self::ArrangerClip => "CLIP", + //Self::PhrasePool => "SEQ LIST", + //Self::PhraseView => "VIEW SEQ", + //Self::PhraseEdit => "EDIT SEQ", + //}; + + //let status_bar_bg = TuiTheme::status_bar_bg(); + + //let mode_bg = TuiTheme::mode_bg(); + //let mode_fg = TuiTheme::mode_fg(); + //let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} ")))); + + //let commands = match self { + //Self::ArrangerMix => Self::command(&[ + //["", "c", "olor"], + //["", "<>", "resize"], + //["", "+-", "zoom"], + //["", "n", "ame/number"], + //["", "Enter", " stop all"], + //]), + //Self::ArrangerClip => Self::command(&[ + //["", "g", "et"], + //["", "s", "et"], + //["", "a", "dd"], + //["", "i", "ns"], + //["", "d", "up"], + //["", "e", "dit"], + //["", "c", "olor"], + //["re", "n", "ame"], + //["", ",.", "select"], + //["", "Enter", " launch"], + //]), + //Self::ArrangerTrack => Self::command(&[ + //["re", "n", "ame"], + //["", ",.", "resize"], + //["", "<>", "move"], + //["", "i", "nput"], + //["", "o", "utput"], + //["", "m", "ute"], + //["", "s", "olo"], + //["", "Del", "ete"], + //["", "Enter", " stop"], + //]), + //Self::ArrangerScene => Self::command(&[ + //["re", "n", "ame"], + //["", "Del", "ete"], + //["", "Enter", " launch"], + //]), + //Self::PhrasePool => Self::command(&[ + //["", "a", "ppend"], + //["", "i", "nsert"], + //["", "d", "uplicate"], + //["", "Del", "ete"], + //["", "c", "olor"], + //["re", "n", "ame"], + //["leng", "t", "h"], + //["", ",.", "move"], + //["", "+-", "resize view"], + //]), + //Self::PhraseView => Self::command(&[ + //["", "enter", " edit"], + //["", "arrows/pgup/pgdn", " scroll"], + //["", "+=", "zoom"], + //]), + //Self::PhraseEdit => Self::command(&[ + //["", "esc", " exit"], + //["", "a", "ppend"], + //["", "s", "et"], + //["", "][", "length"], + //["", "+-", "zoom"], + //]), + //_ => Self::command(&[]) + //}; + + ////let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); + //Tui::bg(status_bar_bg, Fill::w(row!([mode, commands]))) + +//}); + +/// Status bar for arranger app +#[derive(Copy, Clone, Debug)] +pub enum ArrangerStatus { + Transport, + ArrangerMix, + ArrangerTrack, + ArrangerScene, + ArrangerClip, + PhrasePool, + PhraseView, + PhraseEdit, } - -render!(|self: ArrangerStatus|{ - - let label = match self { - Self::Transport => "TRANSPORT", - Self::ArrangerMix => "PROJECT", - Self::ArrangerTrack => "TRACK", - Self::ArrangerScene => "SCENE", - Self::ArrangerClip => "CLIP", - Self::PhrasePool => "SEQ LIST", - Self::PhraseView => "VIEW SEQ", - Self::PhraseEdit => "EDIT SEQ", - }; - - let status_bar_bg = TuiTheme::status_bar_bg(); - - let mode_bg = TuiTheme::mode_bg(); - let mode_fg = TuiTheme::mode_fg(); - let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} ")))); - - let commands = match self { - Self::ArrangerMix => Self::command(&[ - ["", "c", "olor"], - ["", "<>", "resize"], - ["", "+-", "zoom"], - ["", "n", "ame/number"], - ["", "Enter", " stop all"], - ]), - Self::ArrangerClip => Self::command(&[ - ["", "g", "et"], - ["", "s", "et"], - ["", "a", "dd"], - ["", "i", "ns"], - ["", "d", "up"], - ["", "e", "dit"], - ["", "c", "olor"], - ["re", "n", "ame"], - ["", ",.", "select"], - ["", "Enter", " launch"], - ]), - Self::ArrangerTrack => Self::command(&[ - ["re", "n", "ame"], - ["", ",.", "resize"], - ["", "<>", "move"], - ["", "i", "nput"], - ["", "o", "utput"], - ["", "m", "ute"], - ["", "s", "olo"], - ["", "Del", "ete"], - ["", "Enter", " stop"], - ]), - Self::ArrangerScene => Self::command(&[ - ["re", "n", "ame"], - ["", "Del", "ete"], - ["", "Enter", " launch"], - ]), - Self::PhrasePool => Self::command(&[ - ["", "a", "ppend"], - ["", "i", "nsert"], - ["", "d", "uplicate"], - ["", "Del", "ete"], - ["", "c", "olor"], - ["re", "n", "ame"], - ["leng", "t", "h"], - ["", ",.", "move"], - ["", "+-", "resize view"], - ]), - Self::PhraseView => Self::command(&[ - ["", "enter", " edit"], - ["", "arrows/pgup/pgdn", " scroll"], - ["", "+=", "zoom"], - ]), - Self::PhraseEdit => Self::command(&[ - ["", "esc", " exit"], - ["", "a", "ppend"], - ["", "s", "et"], - ["", "][", "length"], - ["", "+-", "zoom"], - ]), - _ => Self::command(&[]) - }; - - //let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); - Tui::bg(status_bar_bg, Fill::w(row!([mode, commands]))) - -});