diff --git a/crates/tek_sequencer/src/arranger_tui.rs b/crates/tek_sequencer/src/arranger_tui.rs index 6279f802..77f9e73c 100644 --- a/crates/tek_sequencer/src/arranger_tui.rs +++ b/crates/tek_sequencer/src/arranger_tui.rs @@ -39,100 +39,7 @@ impl Content for Arranger { ) } } -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 status_bar_bg = Arranger::::status_bar_bg(); - let mode_bg = Arranger::::mode_bg(); - let mode_fg = Arranger::::mode_fg(); - let mode = TuiStyle::bold(format!(" {label} "), true).bg(mode_bg).fg(mode_fg); - 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(status_bar_bg) - } -} -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(Arranger::::hotkey_fg()), - widget(c), - ))?; - }) - }) -} + impl Content for Arrangement { type Engine = Tui; fn content (&self) -> impl Widget { @@ -145,571 +52,3 @@ impl Content for Arrangement { }) } } -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 = Arranger::::border_bg(); - let sep_fg = Arranger::::separator_fg(false); - 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(sep_fg)); - 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 = sep_fg; - } - } - }) - }))?; - // 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 - }, - }; - let bg = Arranger::::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(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 = Arranger::::title_fg(self.0.focused); - 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(Arranger::::border_bg())), - 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, i: &TuiInput) -> Perhaps { - if let Some(entered) = self.entered() { - use ArrangerFocus::*; - if let Some(true) = match entered { - Transport => self.transport.as_mut().map(|t|t.handle(i)).transpose()?.flatten(), - Arrangement => self.arrangement.handle(i)?, - PhrasePool => self.phrases.write().unwrap().handle(i)?, - PhraseEditor => self.editor.handle(i)?, - } { - return Ok(Some(true)) - } - } - Ok(if let Some(command) = ArrangerCommand::input_to_command(self, i) { - 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::input_to_command(self, from) { - let _undo = command.execute(self)?; - Some(true) - } else { - None - }) - } -} -impl InputToCommand> for ArrangerCommand { - fn input_to_command (state: &Arranger, input: &TuiInput) -> Option { - use FocusCommand::*; - use ArrangerCommand::*; - match input.event() { - key!(KeyCode::Tab) => Some(Focus(Next)), - key!(Shift-KeyCode::Tab) => Some(Focus(Prev)), - key!(KeyCode::BackTab) => Some(Focus(Prev)), - key!(Shift-KeyCode::BackTab) => Some(Focus(Prev)), - key!(KeyCode::Up) => Some(Focus(Up)), - key!(KeyCode::Down) => Some(Focus(Down)), - key!(KeyCode::Left) => Some(Focus(Left)), - key!(KeyCode::Right) => Some(Focus(Right)), - key!(KeyCode::Enter) => Some(Focus(Enter)), - key!(KeyCode::Esc) => Some(Focus(Exit)), - key!(KeyCode::Char(' ')) => Some(Transport(TransportCommand::PlayToggle)), - _ => match state.focused() { - ArrangerFocus::Transport => state.transport.as_ref() - .map(|t|TransportCommand::input_to_command(&*t.read().unwrap(), input) - .map(Transport)) - .flatten(), - ArrangerFocus::PhrasePool => - PhrasePoolCommand::input_to_command(&*state.phrases.read().unwrap(), input) - .map(Phrases), - ArrangerFocus::PhraseEditor => - PhraseEditorCommand::input_to_command(&state.editor, input) - .map(Editor), - ArrangerFocus::Arrangement => match input.event() { - key!(KeyCode::Char('e')) => Some(EditPhrase), - _ => ArrangementCommand::input_to_command(&state.arrangement, &input) - .map(Arrangement) - } - } - } - } -} -impl InputToCommand> for ArrangementCommand { - fn input_to_command (_: &Arrangement, input: &TuiInput) -> Option { - use ArrangementCommand::*; - match input.event() { - key!(KeyCode::Char('`')) => Some(ToggleViewMode), - key!(KeyCode::Delete) => Some(Delete), - key!(KeyCode::Enter) => Some(Activate), - key!(KeyCode::Char('.')) => Some(Increment), - key!(KeyCode::Char(',')) => Some(Decrement), - key!(KeyCode::Char('+')) => Some(ZoomIn), - key!(KeyCode::Char('=')) => Some(ZoomOut), - key!(KeyCode::Char('_')) => Some(ZoomOut), - key!(KeyCode::Char('-')) => Some(ZoomOut), - key!(KeyCode::Char('<')) => Some(MoveBack), - key!(KeyCode::Char('>')) => Some(MoveForward), - key!(KeyCode::Char('c')) => Some(RandomColor), - key!(KeyCode::Char('s')) => Some(Put), - key!(KeyCode::Char('g')) => Some(Get), - key!(KeyCode::Char('e')) => Some(Edit), - key!(Ctrl-KeyCode::Char('a')) => Some(AddScene), - key!(Ctrl-KeyCode::Char('t')) => Some(AddTrack), - key!(KeyCode::Char('l')) => Some(ToggleLoop), - key!(KeyCode::Up) => Some(GoUp), - key!(KeyCode::Down) => Some(GoDown), - key!(KeyCode::Left) => Some(GoLeft), - key!(KeyCode::Right) => Some(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)) - //} -//} - -trait ArrangerTheme { - fn border_bg () -> Color; - fn border_fg (focused: bool) -> Color; - fn title_fg (focused: bool) -> Color; - fn separator_fg (focused: bool) -> Color; - fn hotkey_fg () -> Color; - fn mode_bg () -> Color; - fn mode_fg () -> Color; - fn status_bar_bg () -> Color; -} - -impl ArrangerTheme for Arranger { - fn border_bg () -> Color { - Color::Rgb(40, 50, 30) - } - fn border_fg (focused: bool) -> Color { - if focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) } - } - fn title_fg (focused: bool) -> Color { - if focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) } - } - fn separator_fg (_: bool) -> Color { - Color::Rgb(0, 0, 0) - } - fn hotkey_fg () -> Color { - Color::Rgb(255, 255, 0) - } - fn mode_bg () -> Color { - Color::Rgb(150, 160, 90) - } - fn mode_fg () -> Color { - Color::Rgb(255, 255, 255) - } - fn status_bar_bg () -> Color { - Color::Rgb(28, 35, 25) - } -} diff --git a/crates/tek_sequencer/src/arranger_tui_bar.rs b/crates/tek_sequencer/src/arranger_tui_bar.rs new file mode 100644 index 00000000..722154df --- /dev/null +++ b/crates/tek_sequencer/src/arranger_tui_bar.rs @@ -0,0 +1,97 @@ +use crate::*; + +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 status_bar_bg = Arranger::::status_bar_bg(); + let mode_bg = Arranger::::mode_bg(); + let mode_fg = Arranger::::mode_fg(); + let mode = TuiStyle::bold(format!(" {label} "), true).bg(mode_bg).fg(mode_fg); + 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(status_bar_bg) + } +} + +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(Arranger::::hotkey_fg()), + widget(c), + ))?; + }) + }) +} diff --git a/crates/tek_sequencer/src/arranger_tui_cmd.rs b/crates/tek_sequencer/src/arranger_tui_cmd.rs new file mode 100644 index 00000000..4ec5e1bf --- /dev/null +++ b/crates/tek_sequencer/src/arranger_tui_cmd.rs @@ -0,0 +1,154 @@ +use crate::*; + +/// Handle top-level events in standalone arranger. +impl Handle for Arranger { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + if let Some(entered) = self.entered() { + use ArrangerFocus::*; + if let Some(true) = match entered { + Transport => self.transport.as_mut().map(|t|t.handle(i)).transpose()?.flatten(), + Arrangement => self.arrangement.handle(i)?, + PhrasePool => self.phrases.write().unwrap().handle(i)?, + PhraseEditor => self.editor.handle(i)?, + } { + return Ok(Some(true)) + } + } + Ok(if let Some(command) = ArrangerCommand::input_to_command(self, i) { + 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::input_to_command(self, from) { + let _undo = command.execute(self)?; + Some(true) + } else { + None + }) + } +} + +impl InputToCommand> for ArrangerCommand { + fn input_to_command (state: &Arranger, input: &TuiInput) -> Option { + use FocusCommand::*; + use ArrangerCommand::*; + match input.event() { + key!(KeyCode::Tab) => Some(Focus(Next)), + key!(Shift-KeyCode::Tab) => Some(Focus(Prev)), + key!(KeyCode::BackTab) => Some(Focus(Prev)), + key!(Shift-KeyCode::BackTab) => Some(Focus(Prev)), + key!(KeyCode::Up) => Some(Focus(Up)), + key!(KeyCode::Down) => Some(Focus(Down)), + key!(KeyCode::Left) => Some(Focus(Left)), + key!(KeyCode::Right) => Some(Focus(Right)), + key!(KeyCode::Enter) => Some(Focus(Enter)), + key!(KeyCode::Esc) => Some(Focus(Exit)), + key!(KeyCode::Char(' ')) => Some(Transport(TransportCommand::PlayToggle)), + _ => match state.focused() { + ArrangerFocus::Transport => state.transport.as_ref() + .map(|t|TransportCommand::input_to_command(&*t.read().unwrap(), input) + .map(Transport)) + .flatten(), + ArrangerFocus::PhrasePool => + PhrasePoolCommand::input_to_command(&*state.phrases.read().unwrap(), input) + .map(Phrases), + ArrangerFocus::PhraseEditor => + PhraseEditorCommand::input_to_command(&state.editor, input) + .map(Editor), + ArrangerFocus::Arrangement => match input.event() { + key!(KeyCode::Char('e')) => Some(EditPhrase), + _ => ArrangementCommand::input_to_command(&state.arrangement, &input) + .map(Arrangement) + } + } + } + } +} +impl InputToCommand> for ArrangementCommand { + fn input_to_command (_: &Arrangement, input: &TuiInput) -> Option { + use ArrangementCommand::*; + match input.event() { + key!(KeyCode::Char('`')) => Some(ToggleViewMode), + key!(KeyCode::Delete) => Some(Delete), + key!(KeyCode::Enter) => Some(Activate), + key!(KeyCode::Char('.')) => Some(Increment), + key!(KeyCode::Char(',')) => Some(Decrement), + key!(KeyCode::Char('+')) => Some(ZoomIn), + key!(KeyCode::Char('=')) => Some(ZoomOut), + key!(KeyCode::Char('_')) => Some(ZoomOut), + key!(KeyCode::Char('-')) => Some(ZoomOut), + key!(KeyCode::Char('<')) => Some(MoveBack), + key!(KeyCode::Char('>')) => Some(MoveForward), + key!(KeyCode::Char('c')) => Some(RandomColor), + key!(KeyCode::Char('s')) => Some(Put), + key!(KeyCode::Char('g')) => Some(Get), + key!(KeyCode::Char('e')) => Some(Edit), + key!(Ctrl-KeyCode::Char('a')) => Some(AddScene), + key!(Ctrl-KeyCode::Char('t')) => Some(AddTrack), + key!(KeyCode::Char('l')) => Some(ToggleLoop), + key!(KeyCode::Up) => Some(GoUp), + key!(KeyCode::Down) => Some(GoDown), + key!(KeyCode::Left) => Some(GoLeft), + key!(KeyCode::Right) => Some(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)) + //} +//} diff --git a/crates/tek_sequencer/src/arranger_tui_col.rs b/crates/tek_sequencer/src/arranger_tui_col.rs new file mode 100644 index 00000000..d44cec54 --- /dev/null +++ b/crates/tek_sequencer/src/arranger_tui_col.rs @@ -0,0 +1,39 @@ +use crate::*; + +pub trait ArrangerTheme { + fn border_bg () -> Color; + fn border_fg (focused: bool) -> Color; + fn title_fg (focused: bool) -> Color; + fn separator_fg (focused: bool) -> Color; + fn hotkey_fg () -> Color; + fn mode_bg () -> Color; + fn mode_fg () -> Color; + fn status_bar_bg () -> Color; +} + +impl ArrangerTheme for Arranger { + fn border_bg () -> Color { + Color::Rgb(40, 50, 30) + } + fn border_fg (focused: bool) -> Color { + if focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) } + } + fn title_fg (focused: bool) -> Color { + if focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) } + } + fn separator_fg (_: bool) -> Color { + Color::Rgb(0, 0, 0) + } + fn hotkey_fg () -> Color { + Color::Rgb(255, 255, 0) + } + fn mode_bg () -> Color { + Color::Rgb(150, 160, 90) + } + fn mode_fg () -> Color { + Color::Rgb(255, 255, 255) + } + fn status_bar_bg () -> Color { + Color::Rgb(28, 35, 25) + } +} diff --git a/crates/tek_sequencer/src/arranger_tui_hor.rs b/crates/tek_sequencer/src/arranger_tui_hor.rs new file mode 100644 index 00000000..2f54a0df --- /dev/null +++ b/crates/tek_sequencer/src/arranger_tui_hor.rs @@ -0,0 +1,197 @@ +use crate::*; + +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(Arranger::::border_bg())), + 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; + }) + }), + ) + ) + } +} diff --git a/crates/tek_sequencer/src/arranger_tui_ver.rs b/crates/tek_sequencer/src/arranger_tui_ver.rs new file mode 100644 index 00000000..5fa68ad3 --- /dev/null +++ b/crates/tek_sequencer/src/arranger_tui_ver.rs @@ -0,0 +1,188 @@ +use crate::*; + +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 = Arranger::::border_bg(); + let sep_fg = Arranger::::separator_fg(false); + 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(sep_fg)); + 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 = sep_fg; + } + } + }) + }))?; + // 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 + }, + }; + let bg = Arranger::::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(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 = Arranger::::title_fg(self.0.focused); + 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) + } +} + diff --git a/crates/tek_sequencer/src/lib.rs b/crates/tek_sequencer/src/lib.rs index 22a1cb97..2ed8a4f6 100644 --- a/crates/tek_sequencer/src/lib.rs +++ b/crates/tek_sequencer/src/lib.rs @@ -7,9 +7,23 @@ pub(crate) use tek_core::jack::*; pub(crate) use std::sync::{Arc, RwLock}; submod! { - arranger arranger_cmd arranger_tui arranger_snd - sequencer sequencer_cmd sequencer_tui sequencer_snd - transport transport_cmd transport_tui transport_snd + arranger + arranger_cmd + arranger_snd + arranger_tui + arranger_tui_bar + arranger_tui_cmd + arranger_tui_col + arranger_tui_hor + arranger_tui_ver + sequencer + sequencer_cmd + sequencer_snd + sequencer_tui + transport + transport_cmd + transport_snd + transport_tui } /// Octave number (from -1 to 9)