diff --git a/crates/tek_tui/src/lib.rs b/crates/tek_tui/src/lib.rs index 9473d1d3..6e4cd298 100644 --- a/crates/tek_tui/src/lib.rs +++ b/crates/tek_tui/src/lib.rs @@ -13,8 +13,14 @@ use std::fmt::Debug; submod! { tui_arranger - tui_arranger_track + tui_arranger_cmd + tui_arranger_focus tui_arranger_scene + tui_arranger_select + tui_arranger_status + tui_arranger_track + tui_arranger_view + //tui_mixer // TODO tui_phrase //tui_plugin // TODO diff --git a/crates/tek_tui/src/tui_arranger.rs b/crates/tek_tui/src/tui_arranger.rs index c7dde4c1..07e2a9c6 100644 --- a/crates/tek_tui/src/tui_arranger.rs +++ b/crates/tek_tui/src/tui_arranger.rs @@ -36,6 +36,37 @@ pub type ArrangerApp = AppView< ArrangerStatusBar >; +/// Root view for standalone `tek_arranger` +pub struct ArrangerView { + pub(crate) jack: Arc>, + pub(crate) playing: RwLock>, + pub(crate) started: RwLock>, + pub(crate) current: Instant, + pub(crate) quant: Quantize, + pub(crate) sync: LaunchSync, + pub(crate) transport: jack::Transport, + pub(crate) metronome: bool, + pub(crate) phrases: Vec>>, + pub(crate) phrase: usize, + pub(crate) tracks: Vec, + pub(crate) scenes: Vec, + pub(crate) name: Arc>, + pub(crate) splits: [u16;2], + pub(crate) selected: ArrangerSelection, + pub(crate) mode: ArrangerMode, + pub(crate) color: ItemColor, + pub(crate) entered: bool, + pub(crate) size: Measure, + pub(crate) note_buf: Vec, + pub(crate) midi_buf: Vec>>, +} + +impl HasJack for ArrangerView { + fn jack (&self) -> &Arc> { + &self.transport.jack() + } +} + impl Audio for ArrangerApp { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { TracksAudio( @@ -47,257 +78,6 @@ impl Audio for ArrangerApp { } } -/// Handle top-level events in standalone arranger. -impl Handle for ArrangerApp { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - ArrangerAppCommand::execute_with_state(self, i) - } -} - -pub type ArrangerAppCommand = AppViewCommand; - -#[derive(Clone, Debug)] -pub enum ArrangerViewCommand { - Clear, - Scene(ArrangerSceneCommand), - Track(ArrangerTrackCommand), - Clip(ArrangerClipCommand), - Select(ArrangerSelection), - Zoom(usize), - Clock(ClockCommand), - Playhead(PlayheadCommand), - Phrases(PhrasePoolViewCommand), - Editor(PhraseEditorCommand), - EditPhrase(Option>>), -} - -impl InputToCommand> for ArrangerAppCommand { - fn input_to_command (view: &ArrangerApp, input: &TuiInput) -> Option { - use AppViewFocus::*; - use FocusCommand::*; - use ArrangerViewCommand::*; - Some(match input.event() { - key!(KeyCode::Tab) => Self::Focus(Next), - key!(Shift-KeyCode::Tab) => Self::Focus(Prev), - key!(KeyCode::BackTab) => Self::Focus(Prev), - key!(Shift-KeyCode::BackTab) => Self::Focus(Prev), - key!(KeyCode::Up) => Self::Focus(Up), - key!(KeyCode::Down) => Self::Focus(Down), - key!(KeyCode::Left) => Self::Focus(Left), - key!(KeyCode::Right) => Self::Focus(Right), - key!(KeyCode::Enter) => Self::Focus(Enter), - key!(KeyCode::Esc) => Self::Focus(Exit), - key!(KeyCode::Char(' ')) => { - Self::App(Playhead(PlayheadCommand::Play(None))) - }, - _ => Self::App(match view.focused() { - Content(ArrangerFocus::Transport) => { - use TransportCommand::{Clock, Playhead}; - match TransportCommand::input_to_command(view, input)? { - Clock(command) => { - todo!() - }, - Playhead(command) => { - todo!() - }, - } - }, - Content(ArrangerFocus::PhraseEditor) => Editor( - PhraseEditorCommand::input_to_command(&view.app.editor, input)? - ), - Content(ArrangerFocus::PhrasePool) => match input.event() { - key!(KeyCode::Char('e')) => EditPhrase( - Some(view.app.phrase().clone()) - ), - _ => Phrases( - PhrasePoolViewCommand::input_to_command(view, input)? - ) - }, - Content(ArrangerFocus::Arranger) => { - use ArrangerSelection as Select; - use ArrangerTrackCommand as Track; - use ArrangerClipCommand as Clip; - use ArrangerSceneCommand as Scene; - match input.event() { - key!(KeyCode::Char('e')) => EditPhrase(view.phrase()), - _ => match input.event() { - // FIXME: boundary conditions - - key!(KeyCode::Up) => match view.app.selected { - Select::Mix => return None, - Select::Track(t) => return None, - Select::Scene(s) => Select(Select::Scene(s - 1)), - Select::Clip(t, s) => Select(Select::Clip(t, s - 1)), - }, - - key!(KeyCode::Down) => match view.app.selected { - Select::Mix => Select(Select::Scene(0)), - Select::Track(t) => Select(Select::Clip(t, 0)), - Select::Scene(s) => Select(Select::Scene(s + 1)), - Select::Clip(t, s) => Select(Select::Clip(t, s + 1)), - }, - - key!(KeyCode::Left) => match view.app.selected { - Select::Mix => return None, - Select::Track(t) => Select(Select::Track(t - 1)), - Select::Scene(s) => return None, - Select::Clip(t, s) => Select(Select::Clip(t - 1, s)), - }, - - key!(KeyCode::Right) => match view.app.selected { - Select::Mix => return None, - Select::Track(t) => Select(Select::Track(t + 1)), - Select::Scene(s) => Select(Select::Clip(0, s)), - Select::Clip(t, s) => Select(Select::Clip(t, s - 1)), - }, - - key!(KeyCode::Char('+')) => Zoom(0), - - key!(KeyCode::Char('=')) => Zoom(0), - - key!(KeyCode::Char('_')) => Zoom(0), - - key!(KeyCode::Char('-')) => Zoom(0), - - key!(KeyCode::Char('`')) => { todo!("toggle view mode") }, - - key!(KeyCode::Char(',')) => match view.app.selected { - Select::Mix => Zoom(0), - Select::Track(t) => Track(Track::Swap(t, t - 1)), - Select::Scene(s) => Scene(Scene::Swap(s, s - 1)), - Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), - }, - - key!(KeyCode::Char('.')) => match view.app.selected { - Select::Mix => Zoom(0), - Select::Track(t) => Track(Track::Swap(t, t + 1)), - Select::Scene(s) => Scene(Scene::Swap(s, s + 1)), - Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), - }, - - key!(KeyCode::Char('<')) => match view.app.selected { - Select::Mix => Zoom(0), - Select::Track(t) => Track(Track::Swap(t, t - 1)), - Select::Scene(s) => Scene(Scene::Swap(s, s - 1)), - Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), - }, - - key!(KeyCode::Char('>')) => match view.app.selected { - Select::Mix => Zoom(0), - Select::Track(t) => Track(Track::Swap(t, t + 1)), - Select::Scene(s) => Scene(Scene::Swap(s, s + 1)), - Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), - }, - - key!(KeyCode::Enter) => match view.app.selected { - Select::Mix => return None, - Select::Track(t) => return None, - Select::Scene(s) => Scene(Scene::Play(s)), - Select::Clip(t, s) => return None, - }, - - key!(KeyCode::Delete) => match view.app.selected { - Select::Mix => Clear, - Select::Track(t) => Track(Track::Delete(t)), - Select::Scene(s) => Scene(Scene::Delete(s)), - Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), - }, - - key!(KeyCode::Char('c')) => Clip(Clip::RandomColor), - - key!(KeyCode::Char('s')) => match view.app.selected { - Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), - _ => return None, - }, - - key!(KeyCode::Char('g')) => match view.app.selected { - Select::Clip(t, s) => Clip(Clip::Get(t, s)), - _ => return None, - }, - - key!(Ctrl-KeyCode::Char('a')) => Scene(Scene::Add), - - key!(Ctrl-KeyCode::Char('t')) => Track(Track::Add), - - key!(KeyCode::Char('l')) => Clip(Clip::SetLoop(false)), - - _ => return None - } - } - } - }) - }) - } -} - -impl Command> for ArrangerAppCommand { - fn execute (self, state: &mut ArrangerApp) -> Perhaps { - use AppViewCommand::*; - let undo = match self { - Focus(cmd) => { delegate(cmd, Focus, state) }, - App(cmd) => { delegate(cmd, App, state) } - _ => {todo!()} - }?; - state.show_phrase(); - state.update_status(); - return Ok(undo); - } -} - -impl Command> for ArrangerViewCommand { - fn execute (self, state: &mut ArrangerApp) -> Perhaps { - use ArrangerViewCommand::*; - match self { - Scene(cmd) => { delegate(cmd, Scene, &mut state.app) }, - Track(cmd) => { delegate(cmd, Track, &mut state.app) }, - Clip(cmd) => { delegate(cmd, Clip, &mut state.app) }, - Phrases(cmd) => { delegate(cmd, Phrases, &mut state.app) }, - Editor(cmd) => { delegate(cmd, Editor, &mut state.app) }, - Clock(cmd) => { delegate(cmd, Clock, &mut state.app) }, - Playhead(cmd) => { delegate(cmd, Playhead, &mut state.app) }, - Zoom(zoom) => { todo!(); }, - Select(selected) => { state.selected = selected; Ok(None) }, - EditPhrase(phrase) => { - state.editor.phrase = phrase.clone(); - state.focus(ArrangerFocus::PhraseEditor); - state.focus_enter(); - Ok(None) - } - } - } -} - -/// Root view for standalone `tek_arranger` -pub struct ArrangerView { - jack: Arc>, - playing: RwLock>, - started: RwLock>, - current: Instant, - quant: Quantize, - sync: LaunchSync, - transport: jack::Transport, - metronome: bool, - phrases: Vec>>, - phrase: usize, - tracks: Vec, - scenes: Vec, - name: Arc>, - splits: [u16;2], - selected: ArrangerSelection, - mode: ArrangerMode, - color: ItemColor, - entered: bool, - size: Measure, - note_buf: Vec, - midi_buf: Vec>>, -} - -impl HasJack for ArrangerView { - fn jack (&self) -> &Arc> { - &self.transport.jack() - } -} - impl ClockApi for ArrangerView { fn timebase (&self) -> &Arc { &self.current.timebase @@ -334,175 +114,17 @@ impl HasPhrases for ArrangerView { } } -impl HasTracks for ArrangerView { - fn tracks (&self) -> &Vec { - &self.tracks - } - fn tracks_mut (&mut self) -> &mut Vec { - &mut self.tracks - } -} - -impl ArrangerTracksApi for ArrangerView { - 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(||ItemColor::random()), - midi_ins: vec![], - midi_outs: vec![], - reset: true, - recording: false, - monitoring: false, - overdub: false, - play_phrase: None, - next_phrase: None, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - }; - 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); - } - } -} - -impl HasScenes for ArrangerView { - 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(||ItemColor::random()), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok(&mut self.scenes_mut()[index]) - } -} - -/// Display mode of arranger -#[derive(Clone, PartialEq)] -pub enum ArrangerMode { - /// Tracks are rows - Horizontal, - /// Tracks are columns - Vertical(usize), -} - -/// Sections in the arranger app that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum ArrangerFocus { - /// The transport (toolbar) is focused - Transport, - /// The arrangement (grid) is focused - Arranger, - /// The phrase list (pool) is focused - PhrasePool, - /// The phrase editor (sequencer) is focused - PhraseEditor, -} - -#[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), -} - -/// Status bar for arranger app -#[derive(Copy, Clone, Debug)] -pub enum ArrangerStatusBar { - Transport, - ArrangerMix, - ArrangerTrack, - ArrangerScene, - ArrangerClip, - PhrasePool, - PhraseView, - PhraseEdit, -} - -/// Arranger display mode can be cycled -impl ArrangerMode { - /// Cycle arranger display mode - pub fn to_next (&mut self) { - *self = match self { - Self::Horizontal => Self::Vertical(1), - Self::Vertical(1) => Self::Vertical(2), - Self::Vertical(2) => Self::Vertical(2), - Self::Vertical(0) => Self::Horizontal, - Self::Vertical(_) => Self::Vertical(0), - } - } -} - -/// Layout for standalone arranger app. -impl Content for ArrangerView { - type Engine = Tui; - fn content (&self) -> impl Widget { - Split::up( - 1, - widget(&TransportRef(self)), - Split::down( - self.splits[0], - lay!( - Layers::new(move |add|{ - match self.mode { - ArrangerMode::Horizontal => - add(&arranger_content_horizontal(self))?, - ArrangerMode::Vertical(factor) => - add(&arranger_content_vertical(self, factor))? - }; - add(&self.size) - }) - .grow_y(1) - .border(Lozenge(Style::default() - .bg(TuiTheme::border_bg()) - .fg(TuiTheme::border_fg(self.focused)))), - widget(&self.size), - widget(&format!("[{}] Arranger", if self.entered { - "■" - } else { - " " - })) - .fg(TuiTheme::title_fg(self.focused)) - .push_x(1), - ), - Split::right( - self.splits[1], - widget(&self.phrases), - widget(&PhraseEditorRef(self)), - ) - ) - ) - } -} - /// General methods for arranger impl ArrangerView { + pub fn selected_scene (&self) -> Option<&ArrangerScene> { + self.selected.scene().map(|s|self.scenes().get(s)).flatten() + } + pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { + self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() + } + pub fn selected_phrase (&self) -> Option>> { + self.selected_scene()?.clips.get(self.selected.track()?)?.clone() + } /// Focus the editor with the current phrase pub fn show_phrase (&mut self) { @@ -572,39 +194,6 @@ impl ArrangerView { } } } - pub fn selected_scene (&self) -> Option<&ArrangerScene> { - self.selected.scene().map(|s|self.scenes().get(s)).flatten() - } - pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { - self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() - } - pub fn selected_phrase (&self) -> Option>> { - self.selected_scene()?.clips.get(self.selected.track()?)?.clone() - } -} - -impl TransportViewState for ArrangerView { - fn focus (&self) -> TransportViewFocus { - self.focus - } - fn focused (&self) -> bool { - self.focused - } - fn transport_state (&self) -> Option { - *self.playing().read().unwrap() - } - fn bpm_value (&self) -> f64 { - self.bpm().get() - } - fn sync_value (&self) -> f64 { - self.sync().get() - } - fn format_beat (&self) -> String { - self.current().format_beat() - } - fn format_msu (&self) -> String { - self.current().usec.format_msu() - } } impl Audio for ArrangerView { @@ -634,6 +223,101 @@ impl Audio for ArrangerView { return Control::Continue } } + //pub fn track_next (&mut self, last_track: usize) { + //use ArrangerSelection::*; + //*self = match self { + //Mix => Track(0), + //Track(t) => Track(last_track.min(*t + 1)), + //Scene(s) => Clip(0, *s), + //Clip(t, s) => Clip(last_track.min(*t + 1), *s), + //} + //} + //pub fn track_prev (&mut self) { + //use ArrangerSelection::*; + //*self = match self { + //Mix => Mix, + //Scene(s) => Scene(*s), + //Track(t) => if *t == 0 { Mix } else { Track(*t - 1) }, + //Clip(t, s) => if *t == 0 { Scene(*s) } else { Clip(t.saturating_sub(1), *s) } + //} + //} + //pub fn scene_next (&mut self, last_scene: usize) { + //use ArrangerSelection::*; + //*self = match self { + //Mix => Scene(0), + //Track(t) => Clip(*t, 0), + //Scene(s) => Scene(last_scene.min(*s + 1)), + //Clip(t, s) => Clip(*t, last_scene.min(*s + 1)), + //} + //} + //pub fn scene_prev (&mut self) { + //use ArrangerSelection::*; + //*self = match self { + //Mix => Mix, + //Track(t) => Track(*t), + //Scene(s) => if *s == 0 { Mix } else { Scene(*s - 1) }, + //Clip(t, s) => if *s == 0 { Track(*t) } else { Clip(*t, s.saturating_sub(1)) } + //} + //} + +//pub fn arranger_menu_bar () -> MenuBar { + //use ArrangerCommand as Cmd; + //use ArrangerCommand as Edit; + //use ArrangerSelection as Focus; + //use ArrangerTrackCommand as Track; + //use ArrangerClipCommand as Clip; + //use ArrangerSceneCommand as Scene; + //use TransportCommand as Transport; + //MenuBar::new() + //.add({ + //use ArrangerCommand::*; + //Menu::new("File") + //.cmd("n", "New project", ArrangerViewCommand::Arranger(New)) + //.cmd("l", "Load project", ArrangerViewCommand::Arranger(Load)) + //.cmd("s", "Save project", ArrangerViewCommand::Arranger(Save)) + //}) + //.add({ + //Menu::new("Transport") + //.cmd("p", "Play", TransportCommand::Transport(Play(None))) + //.cmd("P", "Play from start", TransportCommand::Transport(Play(Some(0)))) + //.cmd("s", "Pause", TransportCommand::Transport(Stop(None))) + //.cmd("S", "Stop and rewind", TransportCommand::Transport(Stop(Some(0)))) + //}) + //.add({ + //use ArrangerCommand::*; + //Menu::new("Track") + //.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("<", "Move down", ArrangerViewCommand::Arranger(AddTrack)) + //}) + //.add({ + //use ArrangerCommand::*; + //Menu::new("Scene") + //.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddScene)) + //.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack)) + //.cmd("<", "Move down", ArrangerViewCommand::Arranger(AddTrack)) + //}) + //.add({ + //use PhraseRenameCommand as Rename; + //use PhraseLengthCommand as Length; + //Menu::new("Phrase") + //.cmd("a", "Append new", PhrasePoolCommand::Phrases(Append)) + //.cmd("i", "Insert new", PhrasePoolCommand::Phrases(Insert)) + //.cmd("n", "Rename", PhrasePoolCommand::Phrases(Rename(Rename::Begin))) + //.cmd("t", "Set length", PhrasePoolCommand::Phrases(Length(Length::Begin))) + //.cmd("d", "Delete", PhrasePoolCommand::Phrases(Delete)) + //.cmd("l", "Load from MIDI...", PhrasePoolCommand::Phrases(Import)) + //.cmd("s", "Save to MIDI...", PhrasePoolCommand::Phrases(Export)) + //.cmd(">", "Move up", PhrasePoolCommand::Phrases(MoveUp)) + //.cmd("<", "Move down", PhrasePoolCommand::Phrases(MoveDown)) + //}) +//} //pub fn phrase_next (&mut self) { //if let ArrangerSelection::Clip(track, scene) = self.selected { @@ -716,717 +400,3 @@ impl Audio for ArrangerView { //pub fn selected_phrase (&self) -> Option>> { //self.selected_scene()?.clips.get(self.selected.track()?)?.clone() //} - -impl FocusEnter for ArrangerApp { - fn focus_enter (&mut self) { - use AppViewFocus::*; - use ArrangerFocus::*; - let focused = self.focused(); - if !self.entered { - self.entered = focused == Content(Arranger); - self.app.editor.entered = focused == Content(PhraseEditor); - self.app.phrases.entered = focused == Content(PhrasePool); - } - } - fn focus_exit (&mut self) { - if self.entered { - self.entered = false; - self.app.editor.entered = false; - self.app.phrases.entered = false; - } - } - fn focus_entered (&self) -> Option { - if self.entered { - Some(self.focused()) - } else { - None - } - } -} - -/// Focus layout of arranger app -impl FocusGrid for ArrangerApp { - type Item = AppViewFocus; - fn focus_cursor (&self) -> (usize, usize) { - self.cursor - } - fn focus_cursor_mut (&mut self) -> &mut (usize, usize) { - &mut self.cursor - } - fn focus_layout (&self) -> &[&[Self::Item]] { - use AppViewFocus::*; - use ArrangerFocus::*; - &[ - &[Menu, Menu ], - &[Content(Transport), Content(Transport) ], - &[Content(Arranger), Content(Arranger) ], - &[Content(PhrasePool), Content(PhraseEditor)], - ] - } - fn focus_update (&mut self) { - use AppViewFocus::*; - use ArrangerFocus::*; - let focused = self.focused(); - if let Some(mut status_bar) = self.status_bar { - status_bar.update(&( - self.focused(), - self.app.selected, - focused == Content(PhraseEditor) && self.entered - )) - } - } -} - -/// 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 - } - } -} - //pub fn track_next (&mut self, last_track: usize) { - //use ArrangerSelection::*; - //*self = match self { - //Mix => Track(0), - //Track(t) => Track(last_track.min(*t + 1)), - //Scene(s) => Clip(0, *s), - //Clip(t, s) => Clip(last_track.min(*t + 1), *s), - //} - //} - //pub fn track_prev (&mut self) { - //use ArrangerSelection::*; - //*self = match self { - //Mix => Mix, - //Scene(s) => Scene(*s), - //Track(t) => if *t == 0 { Mix } else { Track(*t - 1) }, - //Clip(t, s) => if *t == 0 { Scene(*s) } else { Clip(t.saturating_sub(1), *s) } - //} - //} - //pub fn scene_next (&mut self, last_scene: usize) { - //use ArrangerSelection::*; - //*self = match self { - //Mix => Scene(0), - //Track(t) => Clip(*t, 0), - //Scene(s) => Scene(last_scene.min(*s + 1)), - //Clip(t, s) => Clip(*t, last_scene.min(*s + 1)), - //} - //} - //pub fn scene_prev (&mut self) { - //use ArrangerSelection::*; - //*self = match self { - //Mix => Mix, - //Track(t) => Track(*t), - //Scene(s) => if *s == 0 { Mix } else { Scene(*s - 1) }, - //Clip(t, s) => if *s == 0 { Track(*t) } else { Clip(*t, s.saturating_sub(1)) } - //} - //} - -impl StatusBar for ArrangerStatusBar { - - type State = (AppViewFocus, ArrangerSelection, bool); - - fn hotkey_fg () -> Color where Self: Sized { - TuiTheme::hotkey_fg() - } - fn update (&mut self, (focused, selected, entered): &Self::State) { - use AppViewFocus::*; - if let Content(focused) = focused { - *self = match focused { - ArrangerFocus::Transport => ArrangerStatusBar::Transport, - ArrangerFocus::Arranger => match selected { - ArrangerSelection::Mix => ArrangerStatusBar::ArrangerMix, - ArrangerSelection::Track(_) => ArrangerStatusBar::ArrangerTrack, - ArrangerSelection::Scene(_) => ArrangerStatusBar::ArrangerScene, - ArrangerSelection::Clip(_, _) => ArrangerStatusBar::ArrangerClip, - }, - ArrangerFocus::PhrasePool => ArrangerStatusBar::PhrasePool, - ArrangerFocus::PhraseEditor => match entered { - true => ArrangerStatusBar::PhraseEdit, - false => ArrangerStatusBar::PhraseView, - }, - } - } - } -} - -impl Content for ArrangerStatusBar { - type Engine = Tui; - fn content (&self) -> impl Widget { - 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 = TuiStyle::bold(format!(" {label} "), true).bg(mode_bg).fg(mode_fg); - 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}")); - row!(mode, commands).fill_x().bg(status_bar_bg) - } -} - -//pub fn arranger_menu_bar () -> MenuBar { - //use ArrangerCommand as Cmd; - //use ArrangerCommand as Edit; - //use ArrangerSelection as Focus; - //use ArrangerTrackCommand as Track; - //use ArrangerClipCommand as Clip; - //use ArrangerSceneCommand as Scene; - //use TransportCommand as Transport; - //MenuBar::new() - //.add({ - //use ArrangerCommand::*; - //Menu::new("File") - //.cmd("n", "New project", ArrangerViewCommand::Arranger(New)) - //.cmd("l", "Load project", ArrangerViewCommand::Arranger(Load)) - //.cmd("s", "Save project", ArrangerViewCommand::Arranger(Save)) - //}) - //.add({ - //Menu::new("Transport") - //.cmd("p", "Play", TransportCommand::Transport(Play(None))) - //.cmd("P", "Play from start", TransportCommand::Transport(Play(Some(0)))) - //.cmd("s", "Pause", TransportCommand::Transport(Stop(None))) - //.cmd("S", "Stop and rewind", TransportCommand::Transport(Stop(Some(0)))) - //}) - //.add({ - //use ArrangerCommand::*; - //Menu::new("Track") - //.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("<", "Move down", ArrangerViewCommand::Arranger(AddTrack)) - //}) - //.add({ - //use ArrangerCommand::*; - //Menu::new("Scene") - //.cmd("a", "Append new", ArrangerViewCommand::Arranger(AddScene)) - //.cmd("i", "Insert new", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("n", "Rename", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("d", "Delete", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd(">", "Move up", ArrangerViewCommand::Arranger(AddTrack)) - //.cmd("<", "Move down", ArrangerViewCommand::Arranger(AddTrack)) - //}) - //.add({ - //use PhraseRenameCommand as Rename; - //use PhraseLengthCommand as Length; - //Menu::new("Phrase") - //.cmd("a", "Append new", PhrasePoolCommand::Phrases(Append)) - //.cmd("i", "Insert new", PhrasePoolCommand::Phrases(Insert)) - //.cmd("n", "Rename", PhrasePoolCommand::Phrases(Rename(Rename::Begin))) - //.cmd("t", "Set length", PhrasePoolCommand::Phrases(Length(Length::Begin))) - //.cmd("d", "Delete", PhrasePoolCommand::Phrases(Delete)) - //.cmd("l", "Load from MIDI...", PhrasePoolCommand::Phrases(Import)) - //.cmd("s", "Save to MIDI...", PhrasePoolCommand::Phrases(Export)) - //.cmd(">", "Move up", PhrasePoolCommand::Phrases(MoveUp)) - //.cmd("<", "Move down", PhrasePoolCommand::Phrases(MoveDown)) - //}) -//} - -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 -} - -pub fn arranger_content_vertical ( - view: &ArrangerView, - factor: usize -) -> impl Widget + use<'_> { - let timebase = view.timebase(); - let current = view.current(); - let tracks = view.tracks(); - let scenes = view.scenes(); - let cols = track_widths(tracks); - let rows = ArrangerScene::ppqs(scenes, factor); - let bg = view.color; - let clip_bg = TuiTheme::border_bg(); - let sep_fg = TuiTheme::separator_fg(false); - let header_h = 3u16;//5u16; - let scenes_w = 3 + ArrangerScene::longest_name(scenes) as u16; // x of 1st track - 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 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))) = track.phrase().as_ref() { - let length = phrase.read().unwrap().length; - let elapsed = track.pulses_since_start().unwrap(); - let elapsed = timebase.format_beats_1_short( - (elapsed as usize % length) as f64 - ); - format!("▎+{elapsed:>}") - } else { - String::from("▎") - }; - // beats until switchover - let until_next = track.next_phrase().as_ref().map(|(t, _)|{ - let target = t.pulse.get(); - let current = current.pulse.get(); - if target > current { - let remaining = target - current; - format!("▎-{:>}", timebase.format_beats_0_short(remaining)) - } else { - String::new() - } - }).unwrap_or(String::from("▎")); - // name of active MIDI input - let input = format!("▎>{}", track.midi_ins().get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into())); - // name of active MIDI output - let output = format!("▎<{}", track.midi_outs().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) - }); - // 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(&row!( - if playing { "▶ " } else { " " }, - TuiStyle::bold(scene.name.read().unwrap().as_str(), true), - ).fixed_xy(scenes_w, height).bg(scene.color.rgb))?; - // clip per track: - Ok(for (track, w) in cols.iter().map(|col|col.0).enumerate() { - add(&Layers::new(move |add|{ - let mut bg = clip_bg; - match (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; - add(&name.as_str()[0..max_w].push_x(1).fixed_x(w as u16))?; - bg = color.dark.rgb; - if let Some((_, Some(ref playing))) = track.phrase() { - if *playing.read().unwrap() == *phrase.read().unwrap() { - bg = color.light.rgb - } - }; - }, - _ => {} - }; - add(&Background(bg)) - }).fixed_xy(w as u16, height))?; - }) - }).fixed_y(height) - } - ).fixed_y((view.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 = view.focused; - let selected = view.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 { - 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(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 = TuiTheme::title_fg(view.focused); - let size = format!("{}x{}", view.size.w(), view.size.h()); - let lower_right = TuiStyle::fg(size, color).pull_x(1).align_se().fill_xy(); - lay!(arrangement, lower_right) -} - -pub fn arranger_content_horizontal ( - view: &ArrangerView, -) -> impl Widget + use<'_> { - let focused = view.focused; - let _tracks = view.tracks(); - lay!( - focused.then_some(Background(TuiTheme::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 [x, y, _, height] = to.area(); - let mut x2 = 0; - Ok(for (scene_index, scene) in view.scenes().iter().enumerate() { - let active_scene = view.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 = view.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_tui/src/tui_arranger_cmd.rs b/crates/tek_tui/src/tui_arranger_cmd.rs new file mode 100644 index 00000000..b2f18835 --- /dev/null +++ b/crates/tek_tui/src/tui_arranger_cmd.rs @@ -0,0 +1,221 @@ +use crate::*; + +/// Handle top-level events in standalone arranger. +impl Handle for ArrangerApp { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + ArrangerAppCommand::execute_with_state(self, i) + } +} + +pub type ArrangerAppCommand = AppViewCommand; + +#[derive(Clone, Debug)] +pub enum ArrangerViewCommand { + Clear, + Scene(ArrangerSceneCommand), + Track(ArrangerTrackCommand), + Clip(ArrangerClipCommand), + Select(ArrangerSelection), + Zoom(usize), + Clock(ClockCommand), + Playhead(PlayheadCommand), + Phrases(PhrasePoolViewCommand), + Editor(PhraseEditorCommand), + EditPhrase(Option>>), +} + +impl InputToCommand> for ArrangerAppCommand { + fn input_to_command (view: &ArrangerApp, input: &TuiInput) -> Option { + use AppViewFocus::*; + use FocusCommand::*; + use ArrangerViewCommand::*; + Some(match input.event() { + key!(KeyCode::Tab) => Self::Focus(Next), + key!(Shift-KeyCode::Tab) => Self::Focus(Prev), + key!(KeyCode::BackTab) => Self::Focus(Prev), + key!(Shift-KeyCode::BackTab) => Self::Focus(Prev), + key!(KeyCode::Up) => Self::Focus(Up), + key!(KeyCode::Down) => Self::Focus(Down), + key!(KeyCode::Left) => Self::Focus(Left), + key!(KeyCode::Right) => Self::Focus(Right), + key!(KeyCode::Enter) => Self::Focus(Enter), + key!(KeyCode::Esc) => Self::Focus(Exit), + key!(KeyCode::Char(' ')) => { + Self::App(Playhead(PlayheadCommand::Play(None))) + }, + _ => Self::App(match view.focused() { + Content(ArrangerFocus::Transport) => { + use TransportCommand::{Clock, Playhead}; + match TransportCommand::input_to_command(view, input)? { + Clock(command) => { + todo!() + }, + Playhead(command) => { + todo!() + }, + } + }, + Content(ArrangerFocus::PhraseEditor) => Editor( + PhraseEditorCommand::input_to_command(&view.app.editor, input)? + ), + Content(ArrangerFocus::PhrasePool) => match input.event() { + key!(KeyCode::Char('e')) => EditPhrase( + Some(view.app.phrase().clone()) + ), + _ => Phrases( + PhrasePoolViewCommand::input_to_command(view, input)? + ) + }, + Content(ArrangerFocus::Arranger) => { + use ArrangerSelection as Select; + use ArrangerTrackCommand as Track; + use ArrangerClipCommand as Clip; + use ArrangerSceneCommand as Scene; + match input.event() { + key!(KeyCode::Char('e')) => EditPhrase(view.phrase()), + _ => match input.event() { + // FIXME: boundary conditions + + key!(KeyCode::Up) => match view.app.selected { + Select::Mix => return None, + Select::Track(t) => return None, + Select::Scene(s) => Select(Select::Scene(s - 1)), + Select::Clip(t, s) => Select(Select::Clip(t, s - 1)), + }, + + key!(KeyCode::Down) => match view.app.selected { + Select::Mix => Select(Select::Scene(0)), + Select::Track(t) => Select(Select::Clip(t, 0)), + Select::Scene(s) => Select(Select::Scene(s + 1)), + Select::Clip(t, s) => Select(Select::Clip(t, s + 1)), + }, + + key!(KeyCode::Left) => match view.app.selected { + Select::Mix => return None, + Select::Track(t) => Select(Select::Track(t - 1)), + Select::Scene(s) => return None, + Select::Clip(t, s) => Select(Select::Clip(t - 1, s)), + }, + + key!(KeyCode::Right) => match view.app.selected { + Select::Mix => return None, + Select::Track(t) => Select(Select::Track(t + 1)), + Select::Scene(s) => Select(Select::Clip(0, s)), + Select::Clip(t, s) => Select(Select::Clip(t, s - 1)), + }, + + key!(KeyCode::Char('+')) => Zoom(0), + + key!(KeyCode::Char('=')) => Zoom(0), + + key!(KeyCode::Char('_')) => Zoom(0), + + key!(KeyCode::Char('-')) => Zoom(0), + + key!(KeyCode::Char('`')) => { todo!("toggle view mode") }, + + key!(KeyCode::Char(',')) => match view.app.selected { + Select::Mix => Zoom(0), + Select::Track(t) => Track(Track::Swap(t, t - 1)), + Select::Scene(s) => Scene(Scene::Swap(s, s - 1)), + Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), + }, + + key!(KeyCode::Char('.')) => match view.app.selected { + Select::Mix => Zoom(0), + Select::Track(t) => Track(Track::Swap(t, t + 1)), + Select::Scene(s) => Scene(Scene::Swap(s, s + 1)), + Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), + }, + + key!(KeyCode::Char('<')) => match view.app.selected { + Select::Mix => Zoom(0), + Select::Track(t) => Track(Track::Swap(t, t - 1)), + Select::Scene(s) => Scene(Scene::Swap(s, s - 1)), + Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), + }, + + key!(KeyCode::Char('>')) => match view.app.selected { + Select::Mix => Zoom(0), + Select::Track(t) => Track(Track::Swap(t, t + 1)), + Select::Scene(s) => Scene(Scene::Swap(s, s + 1)), + Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), + }, + + key!(KeyCode::Enter) => match view.app.selected { + Select::Mix => return None, + Select::Track(t) => return None, + Select::Scene(s) => Scene(Scene::Play(s)), + Select::Clip(t, s) => return None, + }, + + key!(KeyCode::Delete) => match view.app.selected { + Select::Mix => Clear, + Select::Track(t) => Track(Track::Delete(t)), + Select::Scene(s) => Scene(Scene::Delete(s)), + Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), + }, + + key!(KeyCode::Char('c')) => Clip(Clip::RandomColor), + + key!(KeyCode::Char('s')) => match view.app.selected { + Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), + _ => return None, + }, + + key!(KeyCode::Char('g')) => match view.app.selected { + Select::Clip(t, s) => Clip(Clip::Get(t, s)), + _ => return None, + }, + + key!(Ctrl-KeyCode::Char('a')) => Scene(Scene::Add), + + key!(Ctrl-KeyCode::Char('t')) => Track(Track::Add), + + key!(KeyCode::Char('l')) => Clip(Clip::SetLoop(false)), + + _ => return None + } + } + } + }) + }) + } +} + +impl Command> for ArrangerAppCommand { + fn execute (self, state: &mut ArrangerApp) -> Perhaps { + use AppViewCommand::*; + let undo = match self { + Focus(cmd) => { delegate(cmd, Focus, state) }, + App(cmd) => { delegate(cmd, App, state) } + _ => {todo!()} + }?; + state.show_phrase(); + state.update_status(); + return Ok(undo); + } +} + +impl Command> for ArrangerViewCommand { + fn execute (self, state: &mut ArrangerApp) -> Perhaps { + use ArrangerViewCommand::*; + match self { + Scene(cmd) => { delegate(cmd, Scene, &mut state.app) }, + Track(cmd) => { delegate(cmd, Track, &mut state.app) }, + Clip(cmd) => { delegate(cmd, Clip, &mut state.app) }, + Phrases(cmd) => { delegate(cmd, Phrases, &mut state.app) }, + Editor(cmd) => { delegate(cmd, Editor, &mut state.app) }, + Clock(cmd) => { delegate(cmd, Clock, &mut state.app) }, + Playhead(cmd) => { delegate(cmd, Playhead, &mut state.app) }, + Zoom(zoom) => { todo!(); }, + Select(selected) => { state.selected = selected; Ok(None) }, + EditPhrase(phrase) => { + state.editor.phrase = phrase.clone(); + state.focus(ArrangerFocus::PhraseEditor); + state.focus_enter(); + Ok(None) + } + } + } +} diff --git a/crates/tek_tui/src/tui_arranger_focus.rs b/crates/tek_tui/src/tui_arranger_focus.rs new file mode 100644 index 00000000..c17ed942 --- /dev/null +++ b/crates/tek_tui/src/tui_arranger_focus.rs @@ -0,0 +1,74 @@ +use crate::*; + +/// Sections in the arranger app that may be focused +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum ArrangerFocus { + /// The transport (toolbar) is focused + Transport, + /// The arrangement (grid) is focused + Arranger, + /// The phrase list (pool) is focused + PhrasePool, + /// The phrase editor (sequencer) is focused + PhraseEditor, +} + +impl FocusEnter for ArrangerApp { + fn focus_enter (&mut self) { + use AppViewFocus::*; + use ArrangerFocus::*; + let focused = self.focused(); + if !self.entered { + self.entered = focused == Content(Arranger); + self.app.editor.entered = focused == Content(PhraseEditor); + self.app.phrases.entered = focused == Content(PhrasePool); + } + } + fn focus_exit (&mut self) { + if self.entered { + self.entered = false; + self.app.editor.entered = false; + self.app.phrases.entered = false; + } + } + fn focus_entered (&self) -> Option { + if self.entered { + Some(self.focused()) + } else { + None + } + } +} + +/// Focus layout of arranger app +impl FocusGrid for ArrangerApp { + type Item = AppViewFocus; + fn focus_cursor (&self) -> (usize, usize) { + self.cursor + } + fn focus_cursor_mut (&mut self) -> &mut (usize, usize) { + &mut self.cursor + } + fn focus_layout (&self) -> &[&[Self::Item]] { + use AppViewFocus::*; + use ArrangerFocus::*; + &[ + &[Menu, Menu ], + &[Content(Transport), Content(Transport) ], + &[Content(Arranger), Content(Arranger) ], + &[Content(PhrasePool), Content(PhraseEditor)], + ] + } + fn focus_update (&mut self) { + use AppViewFocus::*; + use ArrangerFocus::*; + let focused = self.focused(); + if let Some(mut status_bar) = self.status_bar { + status_bar.update(&( + self.focused(), + self.app.selected, + focused == Content(PhraseEditor) && self.entered + )) + } + } +} diff --git a/crates/tek_tui/src/tui_arranger_scene.rs b/crates/tek_tui/src/tui_arranger_scene.rs index 062f19fd..e4864436 100644 --- a/crates/tek_tui/src/tui_arranger_scene.rs +++ b/crates/tek_tui/src/tui_arranger_scene.rs @@ -1,5 +1,27 @@ use crate::*; +impl HasScenes for ArrangerView { + 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(||ItemColor::random()), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok(&mut self.scenes_mut()[index]) + } +} + #[derive(Default, Debug, Clone)] pub struct ArrangerScene { /// Name of scene diff --git a/crates/tek_tui/src/tui_arranger_select.rs b/crates/tek_tui/src/tui_arranger_select.rs new file mode 100644 index 00000000..cf47899d --- /dev/null +++ b/crates/tek_tui/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_tui/src/tui_arranger_status.rs b/crates/tek_tui/src/tui_arranger_status.rs new file mode 100644 index 00000000..463f37c4 --- /dev/null +++ b/crates/tek_tui/src/tui_arranger_status.rs @@ -0,0 +1,125 @@ +use crate::*; + +/// Status bar for arranger app +#[derive(Copy, Clone, Debug)] +pub enum ArrangerStatusBar { + Transport, + ArrangerMix, + ArrangerTrack, + ArrangerScene, + ArrangerClip, + PhrasePool, + PhraseView, + PhraseEdit, +} + +impl StatusBar for ArrangerStatusBar { + + type State = (AppViewFocus, ArrangerSelection, bool); + + fn hotkey_fg () -> Color where Self: Sized { + TuiTheme::hotkey_fg() + } + fn update (&mut self, (focused, selected, entered): &Self::State) { + use AppViewFocus::*; + if let Content(focused) = focused { + *self = match focused { + ArrangerFocus::Transport => ArrangerStatusBar::Transport, + ArrangerFocus::Arranger => match selected { + ArrangerSelection::Mix => ArrangerStatusBar::ArrangerMix, + ArrangerSelection::Track(_) => ArrangerStatusBar::ArrangerTrack, + ArrangerSelection::Scene(_) => ArrangerStatusBar::ArrangerScene, + ArrangerSelection::Clip(_, _) => ArrangerStatusBar::ArrangerClip, + }, + ArrangerFocus::PhrasePool => ArrangerStatusBar::PhrasePool, + ArrangerFocus::PhraseEditor => match entered { + true => ArrangerStatusBar::PhraseEdit, + false => ArrangerStatusBar::PhraseView, + }, + } + } + } +} + +impl Content for ArrangerStatusBar { + type Engine = Tui; + fn content (&self) -> impl Widget { + 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 = TuiStyle::bold(format!(" {label} "), true).bg(mode_bg).fg(mode_fg); + 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}")); + row!(mode, commands).fill_x().bg(status_bar_bg) + } +} diff --git a/crates/tek_tui/src/tui_arranger_track.rs b/crates/tek_tui/src/tui_arranger_track.rs index f34a2539..174c1a45 100644 --- a/crates/tek_tui/src/tui_arranger_track.rs +++ b/crates/tek_tui/src/tui_arranger_track.rs @@ -1,5 +1,46 @@ use crate::*; +impl HasTracks for ArrangerView { + fn tracks (&self) -> &Vec { + &self.tracks + } + fn tracks_mut (&mut self) -> &mut Vec { + &mut self.tracks + } +} + +impl ArrangerTracksApi for ArrangerView { + 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(||ItemColor::random()), + midi_ins: vec![], + midi_outs: vec![], + reset: true, + recording: false, + monitoring: false, + overdub: false, + play_phrase: None, + next_phrase: None, + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + }; + 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 diff --git a/crates/tek_tui/src/tui_arranger_view.rs b/crates/tek_tui/src/tui_arranger_view.rs new file mode 100644 index 00000000..4406ea0d --- /dev/null +++ b/crates/tek_tui/src/tui_arranger_view.rs @@ -0,0 +1,482 @@ +use crate::*; + +/// Display mode of arranger +#[derive(Clone, PartialEq)] +pub enum ArrangerMode { + /// Tracks are rows + Horizontal, + /// Tracks are columns + Vertical(usize), +} + +/// Arranger display mode can be cycled +impl ArrangerMode { + /// Cycle arranger display mode + pub fn to_next (&mut self) { + *self = match self { + Self::Horizontal => Self::Vertical(1), + Self::Vertical(1) => Self::Vertical(2), + Self::Vertical(2) => Self::Vertical(2), + Self::Vertical(0) => Self::Horizontal, + Self::Vertical(_) => Self::Vertical(0), + } + } +} + +/// Layout for standalone arranger app. +impl Content for ArrangerView { + type Engine = Tui; + fn content (&self) -> impl Widget { + Split::up( + 1, + widget(&TransportRef(self)), + Split::down( + self.splits[0], + lay!( + Layers::new(move |add|{ + match self.mode { + ArrangerMode::Horizontal => + add(&arranger_content_horizontal(self))?, + ArrangerMode::Vertical(factor) => + add(&arranger_content_vertical(self, factor))? + }; + add(&self.size) + }) + .grow_y(1) + .border(Lozenge(Style::default() + .bg(TuiTheme::border_bg()) + .fg(TuiTheme::border_fg(self.focused)))), + widget(&self.size), + widget(&format!("[{}] Arranger", if self.entered { + "■" + } else { + " " + })) + .fg(TuiTheme::title_fg(self.focused)) + .push_x(1), + ), + Split::right( + self.splits[1], + widget(&self.phrases), + widget(&PhraseEditorRef(self)), + ) + ) + ) + } +} + +impl TransportViewState for ArrangerView { + fn focus (&self) -> TransportViewFocus { + self.focus + } + fn focused (&self) -> bool { + self.focused + } + fn transport_state (&self) -> Option { + *self.playing().read().unwrap() + } + fn bpm_value (&self) -> f64 { + self.bpm().get() + } + fn sync_value (&self) -> f64 { + self.sync().get() + } + fn format_beat (&self) -> String { + self.current().format_beat() + } + fn format_msu (&self) -> String { + self.current().usec.format_msu() + } +} + +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 +} + +pub fn arranger_content_vertical ( + view: &ArrangerView, + factor: usize +) -> impl Widget + use<'_> { + let timebase = view.timebase(); + let current = view.current(); + let tracks = view.tracks(); + let scenes = view.scenes(); + let cols = track_widths(tracks); + let rows = ArrangerScene::ppqs(scenes, factor); + let bg = view.color; + let clip_bg = TuiTheme::border_bg(); + let sep_fg = TuiTheme::separator_fg(false); + let header_h = 3u16;//5u16; + let scenes_w = 3 + ArrangerScene::longest_name(scenes) as u16; // x of 1st track + 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 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))) = track.phrase().as_ref() { + let length = phrase.read().unwrap().length; + let elapsed = track.pulses_since_start().unwrap(); + let elapsed = timebase.format_beats_1_short( + (elapsed as usize % length) as f64 + ); + format!("▎+{elapsed:>}") + } else { + String::from("▎") + }; + // beats until switchover + let until_next = track.next_phrase().as_ref().map(|(t, _)|{ + let target = t.pulse.get(); + let current = current.pulse.get(); + if target > current { + let remaining = target - current; + format!("▎-{:>}", timebase.format_beats_0_short(remaining)) + } else { + String::new() + } + }).unwrap_or(String::from("▎")); + // name of active MIDI input + let input = format!("▎>{}", track.midi_ins().get(0) + .map(|port|port.short_name()) + .transpose()? + .unwrap_or("(none)".into())); + // name of active MIDI output + let output = format!("▎<{}", track.midi_outs().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) + }); + // 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(&row!( + if playing { "▶ " } else { " " }, + TuiStyle::bold(scene.name.read().unwrap().as_str(), true), + ).fixed_xy(scenes_w, height).bg(scene.color.rgb))?; + // clip per track: + Ok(for (track, w) in cols.iter().map(|col|col.0).enumerate() { + add(&Layers::new(move |add|{ + let mut bg = clip_bg; + match (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; + add(&name.as_str()[0..max_w].push_x(1).fixed_x(w as u16))?; + bg = color.dark.rgb; + if let Some((_, Some(ref playing))) = track.phrase() { + if *playing.read().unwrap() == *phrase.read().unwrap() { + bg = color.light.rgb + } + }; + }, + _ => {} + }; + add(&Background(bg)) + }).fixed_xy(w as u16, height))?; + }) + }).fixed_y(height) + } + ).fixed_y((view.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 = view.focused; + let selected = view.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 { + 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(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 = TuiTheme::title_fg(view.focused); + let size = format!("{}x{}", view.size.w(), view.size.h()); + let lower_right = TuiStyle::fg(size, color).pull_x(1).align_se().fill_xy(); + lay!(arrangement, lower_right) +} + +pub fn arranger_content_horizontal ( + view: &ArrangerView, +) -> impl Widget + use<'_> { + let focused = view.focused; + let _tracks = view.tracks(); + lay!( + focused.then_some(Background(TuiTheme::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 [x, y, _, height] = to.area(); + let mut x2 = 0; + Ok(for (scene_index, scene) in view.scenes().iter().enumerate() { + let active_scene = view.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 = view.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; + }) + }), + ) + ) +}