From 9b996878c2b653ef323d3c24881bec9686ca115d Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 17 Nov 2024 17:29:46 +0100 Subject: [PATCH] wip: p.52, e=5, collecting tui by layer p.2 --- crates/tek_tui/src/lib.rs | 34 +- .../src/{tui_mixer.rs => todo_tui_mixer.rs} | 0 .../src/{tui_plugin.rs => todo_tui_plugin.rs} | 0 ...i_plugin_lv2.rs => todo_tui_plugin_lv2.rs} | 0 ..._lv2_gui.rs => todo_tui_plugin_lv2_gui.rs} | 0 ...plugin_vst2.rs => todo_tui_plugin_vst2.rs} | 0 ...plugin_vst3.rs => todo_tui_plugin_vst3.rs} | 0 .../{tui_sampler.rs => todo_tui_sampler.rs} | 0 ...sampler_cmd.rs => todo_tui_sampler_cmd.rs} | 0 crates/tek_tui/src/tui_apis.rs | 566 ++++++++++++++ crates/tek_tui/src/tui_arranger.rs | 277 ------- crates/tek_tui/src/tui_arranger_cmd.rs | 207 ----- crates/tek_tui/src/tui_arranger_scene.rs | 45 -- crates/tek_tui/src/tui_arranger_track.rs | 191 ----- crates/tek_tui/src/tui_cmd.rs | 738 ++++++++++++++++++ crates/tek_tui/src/tui_focus.rs | 28 + crates/tek_tui/src/tui_model.rs | 97 +++ crates/tek_tui/src/tui_phrase.rs | 85 -- crates/tek_tui/src/tui_phrase_cmd.rs | 149 ---- crates/tek_tui/src/tui_phrase_view.rs | 183 ----- crates/tek_tui/src/tui_pool.rs | 190 ----- crates/tek_tui/src/tui_pool_cmd.rs | 221 ------ crates/tek_tui/src/tui_pool_view.rs | 88 --- .../{tui_arranger_select.rs => tui_select.rs} | 0 crates/tek_tui/src/tui_sequencer.rs | 30 - crates/tek_tui/src/tui_sequencer_cmd.rs | 64 -- crates/tek_tui/src/tui_sequencer_view.rs | 89 --- crates/tek_tui/src/tui_transport.rs | 1 - crates/tek_tui/src/tui_transport_cmd.rs | 101 --- crates/tek_tui/src/tui_transport_view.rs | 85 -- .../src/{tui_arranger_view.rs => tui_view.rs} | 566 ++++++++++++++ 31 files changed, 2005 insertions(+), 2030 deletions(-) rename crates/tek_tui/src/{tui_mixer.rs => todo_tui_mixer.rs} (100%) rename crates/tek_tui/src/{tui_plugin.rs => todo_tui_plugin.rs} (100%) rename crates/tek_tui/src/{tui_plugin_lv2.rs => todo_tui_plugin_lv2.rs} (100%) rename crates/tek_tui/src/{tui_plugin_lv2_gui.rs => todo_tui_plugin_lv2_gui.rs} (100%) rename crates/tek_tui/src/{tui_plugin_vst2.rs => todo_tui_plugin_vst2.rs} (100%) rename crates/tek_tui/src/{tui_plugin_vst3.rs => todo_tui_plugin_vst3.rs} (100%) rename crates/tek_tui/src/{tui_sampler.rs => todo_tui_sampler.rs} (100%) rename crates/tek_tui/src/{tui_sampler_cmd.rs => todo_tui_sampler_cmd.rs} (100%) create mode 100644 crates/tek_tui/src/tui_apis.rs delete mode 100644 crates/tek_tui/src/tui_arranger_cmd.rs delete mode 100644 crates/tek_tui/src/tui_arranger_scene.rs delete mode 100644 crates/tek_tui/src/tui_arranger_track.rs create mode 100644 crates/tek_tui/src/tui_cmd.rs delete mode 100644 crates/tek_tui/src/tui_phrase_cmd.rs delete mode 100644 crates/tek_tui/src/tui_phrase_view.rs delete mode 100644 crates/tek_tui/src/tui_pool.rs delete mode 100644 crates/tek_tui/src/tui_pool_cmd.rs delete mode 100644 crates/tek_tui/src/tui_pool_view.rs rename crates/tek_tui/src/{tui_arranger_select.rs => tui_select.rs} (100%) delete mode 100644 crates/tek_tui/src/tui_sequencer_cmd.rs delete mode 100644 crates/tek_tui/src/tui_sequencer_view.rs delete mode 100644 crates/tek_tui/src/tui_transport.rs delete mode 100644 crates/tek_tui/src/tui_transport_cmd.rs delete mode 100644 crates/tek_tui/src/tui_transport_view.rs rename crates/tek_tui/src/{tui_arranger_view.rs => tui_view.rs} (51%) diff --git a/crates/tek_tui/src/lib.rs b/crates/tek_tui/src/lib.rs index 03a3fef5..a5dbdb7d 100644 --- a/crates/tek_tui/src/lib.rs +++ b/crates/tek_tui/src/lib.rs @@ -13,42 +13,28 @@ use std::fmt::Debug; submod! { - tui_model - tui_jack - tui_handle + tui_apis + tui_cmd tui_focus - tui_status + tui_handle + tui_jack tui_menu + tui_model + tui_status + tui_theme + tui_select - tui_arranger - tui_arranger_cmd - tui_arranger_scene - tui_arranger_select - tui_arranger_track - tui_arranger_view + tui_phrase + tui_sequencer //tui_mixer // TODO - tui_phrase - tui_phrase_cmd - tui_phrase_view //tui_plugin // TODO //tui_plugin_lv2 //tui_plugin_lv2_gui //tui_plugin_vst2 //tui_plugin_vst3 - tui_pool - tui_pool_cmd - tui_pool_view //tui_sampler // TODO //tui_sampler_cmd - tui_sequencer - tui_sequencer_cmd - tui_sequencer_view - - tui_theme - tui_transport - tui_transport_cmd - tui_transport_view } // TODO diff --git a/crates/tek_tui/src/tui_mixer.rs b/crates/tek_tui/src/todo_tui_mixer.rs similarity index 100% rename from crates/tek_tui/src/tui_mixer.rs rename to crates/tek_tui/src/todo_tui_mixer.rs diff --git a/crates/tek_tui/src/tui_plugin.rs b/crates/tek_tui/src/todo_tui_plugin.rs similarity index 100% rename from crates/tek_tui/src/tui_plugin.rs rename to crates/tek_tui/src/todo_tui_plugin.rs diff --git a/crates/tek_tui/src/tui_plugin_lv2.rs b/crates/tek_tui/src/todo_tui_plugin_lv2.rs similarity index 100% rename from crates/tek_tui/src/tui_plugin_lv2.rs rename to crates/tek_tui/src/todo_tui_plugin_lv2.rs diff --git a/crates/tek_tui/src/tui_plugin_lv2_gui.rs b/crates/tek_tui/src/todo_tui_plugin_lv2_gui.rs similarity index 100% rename from crates/tek_tui/src/tui_plugin_lv2_gui.rs rename to crates/tek_tui/src/todo_tui_plugin_lv2_gui.rs diff --git a/crates/tek_tui/src/tui_plugin_vst2.rs b/crates/tek_tui/src/todo_tui_plugin_vst2.rs similarity index 100% rename from crates/tek_tui/src/tui_plugin_vst2.rs rename to crates/tek_tui/src/todo_tui_plugin_vst2.rs diff --git a/crates/tek_tui/src/tui_plugin_vst3.rs b/crates/tek_tui/src/todo_tui_plugin_vst3.rs similarity index 100% rename from crates/tek_tui/src/tui_plugin_vst3.rs rename to crates/tek_tui/src/todo_tui_plugin_vst3.rs diff --git a/crates/tek_tui/src/tui_sampler.rs b/crates/tek_tui/src/todo_tui_sampler.rs similarity index 100% rename from crates/tek_tui/src/tui_sampler.rs rename to crates/tek_tui/src/todo_tui_sampler.rs diff --git a/crates/tek_tui/src/tui_sampler_cmd.rs b/crates/tek_tui/src/todo_tui_sampler_cmd.rs similarity index 100% rename from crates/tek_tui/src/tui_sampler_cmd.rs rename to crates/tek_tui/src/todo_tui_sampler_cmd.rs diff --git a/crates/tek_tui/src/tui_apis.rs b/crates/tek_tui/src/tui_apis.rs new file mode 100644 index 00000000..f4c1f884 --- /dev/null +++ b/crates/tek_tui/src/tui_apis.rs @@ -0,0 +1,566 @@ +use crate::*; + +impl HasPhrases for ArrangerTui { + fn phrases (&self) -> &Vec>> { + &self.phrases + } + fn phrases_mut (&mut self) -> &mut Vec>> { + &mut self.phrases + } +} + +impl HasPhrases for SequencerTui { + fn phrases (&self) -> &Vec>> { + &self.phrases + } + fn phrases_mut (&mut self) -> &mut Vec>> { + &mut self.phrases + } +} + +impl HasPhrase for SequencerTui { + fn reset (&self) -> bool { + self.reset + } + fn reset_mut (&mut self) -> &mut bool { + &mut self.reset + } + fn phrase (&self) -> &Option<(Instant, Option>>)> { + todo!() + } + fn phrase_mut (&self) -> &mut Option<(Instant, Option>>)> { + todo!() + } + fn next_phrase (&self) -> &Option<(Instant, Option>>)> { + todo!() + } + fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { + todo!() + } +} + +impl ArrangerSceneApi for ArrangerScene { + fn name (&self) -> &Arc> { + &self.name + } + fn clips (&self) -> &Vec>>> { + &self.clips + } + fn color (&self) -> ItemColor { + self.color + } +} + +impl HasScenes for ArrangerTui { + fn scenes (&self) -> &Vec { + &self.scenes + } + fn scenes_mut (&mut self) -> &mut Vec { + &mut self.scenes + } + fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerScene> + { + let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); + let scene = ArrangerScene { + name: Arc::new(name.into()), + clips: vec![None;self.tracks().len()], + color: color.unwrap_or_else(||ItemColor::random()), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok(&mut self.scenes_mut()[index]) + } +} + +impl HasTracks for ArrangerTui { + fn tracks (&self) -> &Vec { + &self.tracks + } + fn tracks_mut (&mut self) -> &mut Vec { + &mut self.tracks + } +} + +impl ArrangerTracksApi for ArrangerTui { + fn track_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerTrack> + { + let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); + let track = ArrangerTrack { + width: name.len() + 2, + name: Arc::new(name.into()), + color: color.unwrap_or_else(||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 ArrangerTrackApi for ArrangerTrack { + /// Name of track + fn name (&self) -> &Arc> { + &self.name + } + /// Preferred width of track column + fn width (&self) -> usize { + self.width + } + /// Preferred width of track column + fn width_mut (&mut self) -> &mut usize { + &mut self.width + } + /// Identifying color of track + fn color (&self) -> ItemColor { + self.color + } +} + +impl HasPhrase for ArrangerTrack { + fn reset (&self) -> bool { + self.reset + } + fn reset_mut (&mut self) -> &mut bool { + &mut self.reset + } + fn phrase (&self) -> &Option<(Instant, Option>>)> { + todo!() + } + fn phrase_mut (&self) -> &mut Option<(Instant, Option>>)> { + todo!() + } + fn next_phrase (&self) -> &Option<(Instant, Option>>)> { + todo!() + } + fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { + todo!() + } +} + +impl MidiInputApi for ArrangerTrack { + fn midi_ins (&self) -> &Vec> { + todo!() + } + fn midi_ins_mut (&self) -> &mut Vec> { + todo!() + } + fn recording (&self) -> bool { + todo!() + } + fn recording_mut (&mut self) -> &mut bool { + todo!() + } + fn monitoring (&self) -> bool { + todo!() + } + fn monitoring_mut (&mut self) -> &mut bool { + todo!() + } + fn overdub (&self) -> bool { + todo!() + } + fn overdub_mut (&mut self) -> &mut bool { + todo!() + } + fn notes_in (&self) -> &Arc> { + todo!() + } +} + +impl MidiOutputApi for ArrangerTrack { + fn midi_outs (&self) -> &Vec> { + todo!() + } + fn midi_outs_mut (&mut self) -> &mut Vec> { + todo!() + } + fn midi_note (&mut self) -> &mut Vec { + todo!() + } + fn notes_out (&self) -> &Arc> { + todo!() + } +} + +impl ClockApi for ArrangerTrack { + fn timebase (&self) -> &Arc { + todo!() + } + fn quant (&self) -> &Quantize { + todo!() + } + fn sync (&self) -> &LaunchSync { + todo!() + } +} + +impl PlayheadApi for ArrangerTrack { + fn current (&self) -> &Instant { + todo!() + } + fn transport (&self) -> &Transport { + todo!() + } + fn playing (&self) -> &RwLock> { + todo!() + } + fn started (&self) -> &RwLock> { + todo!() + } +} + +impl PlayerApi for ArrangerTrack {} + + +/// General methods for arranger +impl ArrangerTui { + 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) { + self.editor.show(self.selected_phrase().as_ref()); + } + + pub fn activate (&mut self) { + let scenes = self.scenes(); + let tracks = self.tracks_mut(); + match self.selected { + ArrangerSelection::Scene(s) => { + for (t, track) in tracks.iter_mut().enumerate() { + let player = &mut track.player; + let clip = scenes[s].clips[t].as_ref(); + if player.phrase.is_some() || clip.is_some() { + player.enqueue_next(clip); + } + } + // TODO make transport available here, so that + // activating a scene when stopped starts playback + //if self.is_stopped() { + //self.transport.toggle_play() + //} + }, + ArrangerSelection::Clip(t, s) => { + tracks[t].player.enqueue_next(scenes[s].clips[t]); + }, + _ => {} + } + } + + pub fn is_first_row (&self) -> bool { + let selected = self.selected; + selected.is_mix() || selected.is_track() + } + + pub fn is_last_row (&self) -> bool { + let selected = self.selected; + (self.scenes().len() == 0 && (selected.is_mix() || selected.is_track())) || match selected { + ArrangerSelection::Scene(s) => s == self.scenes().len() - 1, + ArrangerSelection::Clip(_, s) => s == self.scenes().len() - 1, + _ => false + } + } + + pub fn toggle_loop (&mut self) { + if let Some(phrase) = self.selected_phrase() { + phrase.write().unwrap().toggle_loop() + } + } + + pub fn randomize_color (&mut self) { + match self.selected { + ArrangerSelection::Mix => { + self.color = ItemColor::random_dark() + }, + ArrangerSelection::Track(t) => { + self.tracks_mut()[t].color = ItemColor::random() + }, + ArrangerSelection::Scene(s) => { + self.scenes_mut()[s].color = ItemColor::random() + }, + ArrangerSelection::Clip(t, s) => { + if let Some(phrase) = &self.scenes_mut()[s].clips[t] { + phrase.write().unwrap().color = ItemColorTriplet::random(); + } + } + } + } +} + +impl PhrasesTui { + pub fn new (phrases: Vec>>) -> Self { + Self { + scroll: 0, + phrase: 0, + mode: None, + focused: false, + entered: false, + phrases, + } + } + pub fn len (&self) -> usize { + self.phrases.len() + } + pub fn phrase (&self) -> &Arc> { + &self.phrases[self.phrase] + } + pub fn index_before (&self, index: usize) -> usize { + index.overflowing_sub(1).0.min(self.len() - 1) + } + pub fn index_after (&self, index: usize) -> usize { + (index + 1) % self.len() + } + pub fn index_of (&self, phrase: &Phrase) -> Option { + for i in 0..self.phrases.len() { + if *self.phrases[i].read().unwrap() == *phrase { return Some(i) } + } + return None + } + fn new_phrase (name: Option<&str>, color: Option) -> Arc> { + Arc::new(RwLock::new(Phrase::new( + String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color + ))) + } + pub fn delete_selected (&mut self) { + if self.phrase > 0 { + self.phrases.remove(self.phrase); + self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1)); + } + } + pub fn append_new (&mut self, name: Option<&str>, color: Option) { + self.phrases.push(Self::new_phrase(name, color)); + self.phrase = self.phrases.len() - 1; + } + pub fn insert_new (&mut self, name: Option<&str>, color: Option) { + self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color)); + self.phrase += 1; + } + pub fn insert_dup (&mut self) { + let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate(); + phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); + self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase))); + self.phrase += 1; + } + pub fn move_up (&mut self) { + if self.phrase > 1 { + self.phrases.swap(self.phrase - 1, self.phrase); + self.phrase -= 1; + } + } + pub fn move_down (&mut self) { + if self.phrase < self.phrases.len().saturating_sub(1) { + self.phrases.swap(self.phrase + 1, self.phrase); + self.phrase += 1; + } + } +} + + + + + + + + + + //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 { + //if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] { + //let phrases = self.model.phrases.read().unwrap(); + //let index = phrases.index_of(&*phrase.read().unwrap()); + //if let Some(index) = index { + //if index < phrases.len().saturating_sub(1) { + //*phrase = phrases[index + 1].clone(); + //} + //} + //} + //} + //} + //pub fn phrase_prev (&mut self) { + //if let ArrangerSelection::Clip(track, scene) = self.selected { + //if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] { + //let phrases = self.model.phrases.read().unwrap(); + //let index = phrases.index_of(&*phrase.read().unwrap()); + //if let Some(index) = index { + //if index > 0 { + //*phrase = phrases[index - 1].clone(); + //} + //} + //} + //} + //} + + //pub fn phrase_get (&mut self) { + //if let ArrangerSelection::Clip(track, scene) = self.selected { + //if let Some(phrase) = &self.model.scenes[scene].clips[track] { + //let mut phrases = self.model.phrases.write().unwrap(); + //if let Some(index) = &*phrases.index_of(&*phrase.read().unwrap()) { + //self.model.phrase = index; + //} + //} + //} + //} + + ///// Focus the editor with the current phrase + //pub fn edit_phrase (&mut self) { + //if self.arrangement.selected.is_clip() && self.arrangement.phrase().is_none() { + //self.phrases.append_new(None, Some(self.next_color().into())); + //self.arrangement.phrase_put(); + //} + //self.show_phrase(); + //self.focus(ArrangerFocus::PhraseEditor); + //self.editor.entered = true; + //} + + //pub fn next_color (&self) -> ItemColor { + //if let ArrangerSelection::Clip(track, scene) = self.arrangement.selected { + //let track_color = self.arrangement.model.tracks[track].color; + //let scene_color = self.arrangement.model.scenes[scene].color; + //track_color.mix(scene_color, 0.5).mix(ItemColor::random(), 0.25) + //} else { + //panic!("could not compute next color") + //} + //} + //pub fn phrase_del (&mut self) { + //let track_index = self.selected.track(); + //let scene_index = self.selected.scene(); + //track_index + //.and_then(|index|self.model.tracks.get_mut(index).map(|track|(index, track))) + //.map(|(track_index, _)|scene_index + //.and_then(|index|self.model.scenes.get_mut(index)) + //.map(|scene|scene.clips[track_index] = None)); + //} + //pub fn phrase_put (&mut self) { + //if let ArrangerSelection::Clip(track, scene) = self.selected { + //self.model.scenes[scene].clips[track] = self.selected_phrase().clone(); + //} + //} + //pub fn selected_scene (&self) -> Option<&ArrangerScene> { + //self.selected.scene().map(|s|self.model.scenes.get(s)).flatten() + //} + //pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { + //self.selected.scene().map(|s|self.model.scenes.get_mut(s)).flatten() + //} + //pub fn selected_phrase (&self) -> Option>> { + //self.selected_scene()?.clips.get(self.selected.track()?)?.clone() + //} diff --git a/crates/tek_tui/src/tui_arranger.rs b/crates/tek_tui/src/tui_arranger.rs index a467e679..c7b7e813 100644 --- a/crates/tek_tui/src/tui_arranger.rs +++ b/crates/tek_tui/src/tui_arranger.rs @@ -1,278 +1 @@ use crate::*; - -impl HasPhrases for ArrangerTui { - fn phrases (&self) -> &Vec>> { - &self.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.phrases - } -} - -/// General methods for arranger -impl ArrangerTui { - 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) { - self.editor.show(self.selected_phrase().as_ref()); - } - - pub fn activate (&mut self) { - let scenes = self.scenes(); - let tracks = self.tracks_mut(); - match self.selected { - ArrangerSelection::Scene(s) => { - for (t, track) in tracks.iter_mut().enumerate() { - let player = &mut track.player; - let clip = scenes[s].clips[t].as_ref(); - if player.phrase.is_some() || clip.is_some() { - player.enqueue_next(clip); - } - } - // TODO make transport available here, so that - // activating a scene when stopped starts playback - //if self.is_stopped() { - //self.transport.toggle_play() - //} - }, - ArrangerSelection::Clip(t, s) => { - tracks[t].player.enqueue_next(scenes[s].clips[t]); - }, - _ => {} - } - } - - pub fn is_first_row (&self) -> bool { - let selected = self.selected; - selected.is_mix() || selected.is_track() - } - - pub fn is_last_row (&self) -> bool { - let selected = self.selected; - (self.scenes().len() == 0 && (selected.is_mix() || selected.is_track())) || match selected { - ArrangerSelection::Scene(s) => s == self.scenes().len() - 1, - ArrangerSelection::Clip(_, s) => s == self.scenes().len() - 1, - _ => false - } - } - - pub fn toggle_loop (&mut self) { - if let Some(phrase) = self.selected_phrase() { - phrase.write().unwrap().toggle_loop() - } - } - - pub fn randomize_color (&mut self) { - match self.selected { - ArrangerSelection::Mix => { - self.color = ItemColor::random_dark() - }, - ArrangerSelection::Track(t) => { - self.tracks_mut()[t].color = ItemColor::random() - }, - ArrangerSelection::Scene(s) => { - self.scenes_mut()[s].color = ItemColor::random() - }, - ArrangerSelection::Clip(t, s) => { - if let Some(phrase) = &self.scenes_mut()[s].clips[t] { - phrase.write().unwrap().color = ItemColorTriplet::random(); - } - } - } - } -} - - - - - - - - - - //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 { - //if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] { - //let phrases = self.model.phrases.read().unwrap(); - //let index = phrases.index_of(&*phrase.read().unwrap()); - //if let Some(index) = index { - //if index < phrases.len().saturating_sub(1) { - //*phrase = phrases[index + 1].clone(); - //} - //} - //} - //} - //} - //pub fn phrase_prev (&mut self) { - //if let ArrangerSelection::Clip(track, scene) = self.selected { - //if let Some(ref mut phrase) = self.model.scenes[scene].clips[track] { - //let phrases = self.model.phrases.read().unwrap(); - //let index = phrases.index_of(&*phrase.read().unwrap()); - //if let Some(index) = index { - //if index > 0 { - //*phrase = phrases[index - 1].clone(); - //} - //} - //} - //} - //} - - //pub fn phrase_get (&mut self) { - //if let ArrangerSelection::Clip(track, scene) = self.selected { - //if let Some(phrase) = &self.model.scenes[scene].clips[track] { - //let mut phrases = self.model.phrases.write().unwrap(); - //if let Some(index) = &*phrases.index_of(&*phrase.read().unwrap()) { - //self.model.phrase = index; - //} - //} - //} - //} - - ///// Focus the editor with the current phrase - //pub fn edit_phrase (&mut self) { - //if self.arrangement.selected.is_clip() && self.arrangement.phrase().is_none() { - //self.phrases.append_new(None, Some(self.next_color().into())); - //self.arrangement.phrase_put(); - //} - //self.show_phrase(); - //self.focus(ArrangerFocus::PhraseEditor); - //self.editor.entered = true; - //} - - //pub fn next_color (&self) -> ItemColor { - //if let ArrangerSelection::Clip(track, scene) = self.arrangement.selected { - //let track_color = self.arrangement.model.tracks[track].color; - //let scene_color = self.arrangement.model.scenes[scene].color; - //track_color.mix(scene_color, 0.5).mix(ItemColor::random(), 0.25) - //} else { - //panic!("could not compute next color") - //} - //} - //pub fn phrase_del (&mut self) { - //let track_index = self.selected.track(); - //let scene_index = self.selected.scene(); - //track_index - //.and_then(|index|self.model.tracks.get_mut(index).map(|track|(index, track))) - //.map(|(track_index, _)|scene_index - //.and_then(|index|self.model.scenes.get_mut(index)) - //.map(|scene|scene.clips[track_index] = None)); - //} - //pub fn phrase_put (&mut self) { - //if let ArrangerSelection::Clip(track, scene) = self.selected { - //self.model.scenes[scene].clips[track] = self.selected_phrase().clone(); - //} - //} - //pub fn selected_scene (&self) -> Option<&ArrangerScene> { - //self.selected.scene().map(|s|self.model.scenes.get(s)).flatten() - //} - //pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { - //self.selected.scene().map(|s|self.model.scenes.get_mut(s)).flatten() - //} - //pub fn selected_phrase (&self) -> Option>> { - //self.selected_scene()?.clips.get(self.selected.track()?)?.clone() - //} diff --git a/crates/tek_tui/src/tui_arranger_cmd.rs b/crates/tek_tui/src/tui_arranger_cmd.rs deleted file mode 100644 index 26e2212d..00000000 --- a/crates/tek_tui/src/tui_arranger_cmd.rs +++ /dev/null @@ -1,207 +0,0 @@ -use crate::*; - -#[derive(Clone, Debug)] -pub enum ArrangerCommand { - Focus(FocusCommand), - Undo, - Redo, - Clear, - Clock(ClockCommand), - Playhead(PlayheadCommand), - Scene(ArrangerSceneCommand), - Track(ArrangerTrackCommand), - Clip(ArrangerClipCommand), - Select(ArrangerSelection), - Zoom(usize), - Phrases(PhrasePoolCommand), - Editor(PhraseCommand), - EditPhrase(Option>>), -} - -pub trait ArrangerControl { -} - -impl ArrangerControl for ArrangerTui { -} - -impl InputToCommand for ArrangerCommand { - fn input_to_command (view: &T, input: &TuiInput) -> Option { - use FocusCommand::*; - use ArrangerCommand::*; - 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() { - ArrangerFocus::Transport => { - use TransportCommand::{Clock, Playhead}; - match TransportCommand::input_to_command(view, input)? { - Clock(command) => { - todo!() - }, - Playhead(command) => { - todo!() - }, - } - }, - ArrangerFocus::PhraseEditor => Editor( - PhraseCommand::input_to_command(&view.editor, input)? - ), - ArrangerFocus::PhrasePool => match input.event() { - key!(KeyCode::Char('e')) => EditPhrase( - Some(view.phrase().clone()) - ), - _ => Phrases( - PhrasePoolCommand::input_to_command(view, input)? - ) - }, - 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.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.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.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.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.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.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.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.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.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.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.selected { - Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), - _ => return None, - }, - - key!(KeyCode::Char('g')) => match view.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 ArrangerCommand { - fn execute (self, state: &mut T) -> Perhaps { - use ArrangerCommand::*; - match self { - Focus(cmd) => { delegate(cmd, Focus, state) }, - Scene(cmd) => { delegate(cmd, Scene, &mut state) }, - Track(cmd) => { delegate(cmd, Track, &mut state) }, - Clip(cmd) => { delegate(cmd, Clip, &mut state) }, - Phrases(cmd) => { delegate(cmd, Phrases, &mut state) }, - Editor(cmd) => { delegate(cmd, Editor, &mut state) }, - Clock(cmd) => { delegate(cmd, Clock, &mut state) }, - Playhead(cmd) => { delegate(cmd, Playhead, &mut state) }, - 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_scene.rs b/crates/tek_tui/src/tui_arranger_scene.rs deleted file mode 100644 index a1bdcf7c..00000000 --- a/crates/tek_tui/src/tui_arranger_scene.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::*; - -impl HasScenes for ArrangerTui { - fn scenes (&self) -> &Vec { - &self.scenes - } - fn scenes_mut (&mut self) -> &mut Vec { - &mut self.scenes - } - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerScene> - { - let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); - let scene = ArrangerScene { - name: Arc::new(name.into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(||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 - pub name: Arc>, - /// Clips in scene, one per track - pub clips: Vec>>>, - /// Identifying color of scene - pub color: ItemColor, -} - -impl ArrangerSceneApi for ArrangerScene { - fn name (&self) -> &Arc> { - &self.name - } - fn clips (&self) -> &Vec>>> { - &self.clips - } - fn color (&self) -> ItemColor { - self.color - } -} diff --git a/crates/tek_tui/src/tui_arranger_track.rs b/crates/tek_tui/src/tui_arranger_track.rs deleted file mode 100644 index 92a7c295..00000000 --- a/crates/tek_tui/src/tui_arranger_track.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::*; - -impl HasTracks for ArrangerTui { - fn tracks (&self) -> &Vec { - &self.tracks - } - fn tracks_mut (&mut self) -> &mut Vec { - &mut self.tracks - } -} - -impl ArrangerTracksApi for ArrangerTui { - fn track_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerTrack> - { - let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); - let track = ArrangerTrack { - width: name.len() + 2, - name: Arc::new(name.into()), - color: color.unwrap_or_else(||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 - name: Arc>, - /// Preferred width of track column - width: usize, - /// Identifying color of track - color: ItemColor, - /// Start time and phrase being played - play_phrase: Option<(Instant, Option>>)>, - /// Start time and next phrase - next_phrase: Option<(Instant, Option>>)>, - /// Play input through output. - monitoring: bool, - /// Write input to sequence. - recording: bool, - /// Overdub input to sequence. - overdub: bool, - /// Send all notes off - reset: bool, // TODO?: after Some(nframes) - /// Record from MIDI ports to current sequence. - midi_ins: Vec>, - /// Play from current sequence to MIDI ports - midi_outs: Vec>, - /// Notes currently held at input - notes_in: Arc>, - /// Notes currently held at output - notes_out: Arc>, - ///// MIDI output buffer - //midi_note: Vec, - ///// MIDI output buffer - //midi_chunk: Vec>>, -} - -impl ArrangerTrackApi for ArrangerTrack { - /// Name of track - fn name (&self) -> &Arc> { - &self.name - } - /// Preferred width of track column - fn width (&self) -> usize { - self.width - } - /// Preferred width of track column - fn width_mut (&mut self) -> &mut usize { - &mut self.width - } - /// Identifying color of track - fn color (&self) -> ItemColor { - self.color - } -} - -impl HasPhrase for ArrangerTrack { - fn reset (&self) -> bool { - self.reset - } - fn reset_mut (&mut self) -> &mut bool { - &mut self.reset - } - fn phrase (&self) -> &Option<(Instant, Option>>)> { - todo!() - } - fn phrase_mut (&self) -> &mut Option<(Instant, Option>>)> { - todo!() - } - fn next_phrase (&self) -> &Option<(Instant, Option>>)> { - todo!() - } - fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { - todo!() - } -} - -impl MidiInputApi for ArrangerTrack { - fn midi_ins (&self) -> &Vec> { - todo!() - } - fn midi_ins_mut (&self) -> &mut Vec> { - todo!() - } - fn recording (&self) -> bool { - todo!() - } - fn recording_mut (&mut self) -> &mut bool { - todo!() - } - fn monitoring (&self) -> bool { - todo!() - } - fn monitoring_mut (&mut self) -> &mut bool { - todo!() - } - fn overdub (&self) -> bool { - todo!() - } - fn overdub_mut (&mut self) -> &mut bool { - todo!() - } - fn notes_in (&self) -> &Arc> { - todo!() - } -} - -impl MidiOutputApi for ArrangerTrack { - fn midi_outs (&self) -> &Vec> { - todo!() - } - fn midi_outs_mut (&mut self) -> &mut Vec> { - todo!() - } - fn midi_note (&mut self) -> &mut Vec { - todo!() - } - fn notes_out (&self) -> &Arc> { - todo!() - } -} - -impl ClockApi for ArrangerTrack { - fn timebase (&self) -> &Arc { - todo!() - } - fn quant (&self) -> &Quantize { - todo!() - } - fn sync (&self) -> &LaunchSync { - todo!() - } -} - -impl PlayheadApi for ArrangerTrack { - fn current (&self) -> &Instant { - todo!() - } - fn transport (&self) -> &Transport { - todo!() - } - fn playing (&self) -> &RwLock> { - todo!() - } - fn started (&self) -> &RwLock> { - todo!() - } -} - -impl PlayerApi for ArrangerTrack {} - diff --git a/crates/tek_tui/src/tui_cmd.rs b/crates/tek_tui/src/tui_cmd.rs new file mode 100644 index 00000000..4c706db8 --- /dev/null +++ b/crates/tek_tui/src/tui_cmd.rs @@ -0,0 +1,738 @@ +use crate::*; + +#[derive(Clone, Debug, PartialEq)] +pub enum TransportCommand { + Focus(FocusCommand), + Clock(ClockCommand), + Playhead(PlayheadCommand), +} + +impl InputToCommand for TransportCommand { + fn input_to_command (state: &T, input: &TuiInput) -> Option { + use KeyCode::Char; + use ClockCommand::{SetBpm, SetQuant, SetSync}; + use TransportFocus as Focused; + use TransportCommand::{Focus, Clock, Playhead}; + let focused = state.focused(); + Some(match input.event() { + key!(Left) => Focus(FocusCommand::Prev), + key!(Right) => Focus(FocusCommand::Next), + key!(Char('.')) => match focused { + Focused::Bpm => Clock(SetBpm(state.bpm().get() + 1.0)), + Focused::Quant => Clock(SetQuant(state.next_quant())), + Focused::Sync => Clock(SetSync(state.next_sync())), + Focused::PlayPause => Playhead(todo!()), + Focused::Clock => Playhead(todo!()), + _ => {todo!()} + }, + key!(KeyCode::Char(',')) => match focused { + Focused::Bpm => Clock(SetBpm(state.bpm().get() - 1.0)), + Focused::Quant => Clock(SetQuant(state.prev_quant())), + Focused::Sync => Clock(SetSync(state.prev_sync())), + Focused::PlayPause => Playhead(todo!()), + Focused::Clock => Playhead(todo!()), + _ => {todo!()} + }, + key!(KeyCode::Char('>')) => match focused { + Focused::Bpm => Clock(SetBpm(state.bpm().get() + 0.001)), + Focused::Quant => Clock(SetQuant(state.next_quant())), + Focused::Sync => Clock(SetSync(state.next_sync())), + Focused::PlayPause => Playhead(todo!()), + Focused::Clock => Playhead(todo!()), + _ => {todo!()} + }, + key!(KeyCode::Char('<')) => match focused { + Focused::Bpm => Clock(SetBpm(state.bpm().get() - 0.001)), + Focused::Quant => Clock(SetQuant(state.prev_quant())), + Focused::Sync => Clock(SetSync(state.prev_sync())), + Focused::PlayPause => Playhead(todo!()), + Focused::Clock => Playhead(todo!()), + _ => {todo!()} + }, + _ => return None + }) + } +} + +pub trait TransportControl { + fn quant (&self) -> &Quantize; + fn bpm (&self) -> &BeatsPerMinute; + fn next_quant (&self) -> f64 { + next_note_length(self.quant().get() as usize) as f64 + } + fn prev_quant (&self) -> f64 { + prev_note_length(self.quant().get() as usize) as f64 + } + fn sync (&self) -> &LaunchSync; + fn next_sync (&self) -> f64 { + next_note_length(self.sync().get() as usize) as f64 + } + fn prev_sync (&self) -> f64 { + prev_note_length(self.sync().get() as usize) as f64 + } +} + +impl TransportControl for TransportTui { + fn bpm (&self) -> &BeatsPerMinute { + self.bpm() + } + fn quant (&self) -> &Quantize { + self.quant() + } + fn sync (&self) -> &LaunchSync { + self.sync() + } +} + +impl Command for TransportCommand { + fn execute (self, state: &mut T) -> Perhaps { + use TransportCommand::{Focus, Clock, Playhead}; + use ClockCommand::{SetBpm, SetQuant, SetSync}; + Ok(Some(match self { + Focus(Next) => { todo!() } + Focus(Prev) => { todo!() }, + Focus(_) => { unimplemented!() }, + Clock(SetBpm(bpm)) => Clock(SetBpm(state.bpm().set(bpm))), + Clock(SetQuant(quant)) => Clock(SetQuant(state.quant().set(quant))), + Clock(SetSync(sync)) => Clock(SetSync(state.sync().set(sync))), + _ => return Ok(None) + })) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum SequencerCommand { + Focus(FocusCommand), + Undo, + Redo, + Clear, + Clock(ClockCommand), + Playhead(PlayheadCommand), + Phrases(PhrasesCommand), + Editor(PhraseCommand), +} + +impl InputToCommand for SequencerCommand { + fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { + use FocusCommand::*; + use SequencerCommand::*; + match input.event() { + key!(KeyCode::Tab) => Some(Self::Focus(Next)), + key!(Shift-KeyCode::Tab) => Some(Self::Focus(Prev)), + key!(KeyCode::BackTab) => Some(Self::Focus(Prev)), + key!(Shift-KeyCode::BackTab) => Some(Self::Focus(Prev)), + key!(KeyCode::Up) => Some(Self::Focus(Up)), + key!(KeyCode::Down) => Some(Self::Focus(Down)), + key!(KeyCode::Left) => Some(Self::Focus(Left)), + key!(KeyCode::Right) => Some(Self::Focus(Right)), + _ => Some(Self::App(match state.focused() { + SequencerFocus::Transport => + TransportCommand::input_to_command(&state, input).map(Transport), + SequencerFocus::Phrases => + PhrasesCommand::input_to_command(&state.phrases, input).map(Phrases), + SequencerFocus::PhraseEditor => + PhraseCommand::input_to_command(&state.editor, input).map(Editor), + _ => return None, + })) + } + } +} + +impl Command for SequencerCommand { + fn execute (self, state: &mut SequencerTui) -> Perhaps { + use SequencerCommand::*; + match self { + Focus(cmd) => delegate(cmd, Focus, state), + Phrases(cmd) => delegate(cmd, Phrases, &mut state.phrases), + Editor(cmd) => delegate(cmd, Editor, &mut state.editor), + Clock(cmd) => delegate(cmd, Clock, &mut state.transport), + Playhead(cmd) => delegate(cmd, Playhead, &mut state.transport) + } + } +} + +impl TransportControl for SequencerTui { + fn bpm (&self) -> &BeatsPerMinute { + self.app.bpm() + } + fn quant (&self) -> &Quantize { + self.app.quant() + } + fn sync (&self) -> &LaunchSync { + self.app.sync() + } +} + +#[derive(Clone, Debug)] +pub enum ArrangerCommand { + Focus(FocusCommand), + Undo, + Redo, + Clear, + Clock(ClockCommand), + Playhead(PlayheadCommand), + Scene(ArrangerSceneCommand), + Track(ArrangerTrackCommand), + Clip(ArrangerClipCommand), + Select(ArrangerSelection), + Zoom(usize), + Phrases(PhrasePoolCommand), + Editor(PhraseCommand), + EditPhrase(Option>>), +} + +pub trait ArrangerControl { +} + +impl ArrangerControl for ArrangerTui { +} + +impl InputToCommand for ArrangerCommand { + fn input_to_command (view: &T, input: &TuiInput) -> Option { + use FocusCommand::*; + use ArrangerCommand::*; + 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() { + ArrangerFocus::Transport => { + use TransportCommand::{Clock, Playhead}; + match TransportCommand::input_to_command(view, input)? { + Clock(command) => { + todo!() + }, + Playhead(command) => { + todo!() + }, + } + }, + ArrangerFocus::PhraseEditor => Editor( + PhraseCommand::input_to_command(&view.editor, input)? + ), + ArrangerFocus::PhrasePool => match input.event() { + key!(KeyCode::Char('e')) => EditPhrase( + Some(view.phrase().clone()) + ), + _ => Phrases( + PhrasePoolCommand::input_to_command(view, input)? + ) + }, + 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.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.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.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.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.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.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.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.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.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.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.selected { + Select::Clip(t, s) => Clip(Clip::Set(t, s, None)), + _ => return None, + }, + + key!(KeyCode::Char('g')) => match view.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 ArrangerCommand { + fn execute (self, state: &mut T) -> Perhaps { + use ArrangerCommand::*; + match self { + Focus(cmd) => { delegate(cmd, Focus, state) }, + Scene(cmd) => { delegate(cmd, Scene, &mut state) }, + Track(cmd) => { delegate(cmd, Track, &mut state) }, + Clip(cmd) => { delegate(cmd, Clip, &mut state) }, + Phrases(cmd) => { delegate(cmd, Phrases, &mut state) }, + Editor(cmd) => { delegate(cmd, Editor, &mut state) }, + Clock(cmd) => { delegate(cmd, Clock, &mut state) }, + Playhead(cmd) => { delegate(cmd, Playhead, &mut state) }, + 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) + } + } + } +} + +#[derive(Clone, PartialEq, Debug)] +pub enum PhrasesCommand { + Select(usize), + Edit(PhrasePoolCommand), + Rename(PhraseRenameCommand), + Length(PhraseLengthCommand), +} + +#[derive(Clone, Debug, PartialEq)] +pub enum PhraseRenameCommand { + Begin, + Set(String), + Confirm, + Cancel, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum PhraseLengthCommand { + Begin, + Next, + Prev, + Inc, + Dec, + Set(usize), + Cancel, +} + +impl InputToCommand for PhrasesCommand { + fn input_to_command (state: &PhrasesTui, input: &TuiInput) -> Option { + use PhrasesCommand as Cmd; + use PhrasePoolCommand as Edit; + use PhraseRenameCommand as Rename; + use PhraseLengthCommand as Length; + match input.event() { + key!(KeyCode::Up) => Some(Cmd::Select(0)), + key!(KeyCode::Down) => Some(Cmd::Select(0)), + key!(KeyCode::Char(',')) => Some(Cmd::Edit(Edit::Swap(0, 0))), + key!(KeyCode::Char('.')) => Some(Cmd::Edit(Edit::Swap(0, 0))), + key!(KeyCode::Delete) => Some(Cmd::Edit(Edit::Delete(0))), + key!(KeyCode::Char('a')) => Some(Cmd::Edit(Edit::Add(0))), + key!(KeyCode::Char('i')) => Some(Cmd::Edit(Edit::Add(0))), + key!(KeyCode::Char('d')) => Some(Cmd::Edit(Edit::Duplicate(0))), + key!(KeyCode::Char('c')) => Some(Cmd::Edit(Edit::RandomColor(0))), + key!(KeyCode::Char('n')) => Some(Cmd::Rename(Rename::Begin)), + key!(KeyCode::Char('t')) => Some(Cmd::Length(Length::Begin)), + _ => match state.mode { + Some(PhrasesMode::Rename(..)) => { + Rename::input_to_command(state, input).map(Cmd::Rename) + }, + Some(PhrasesMode::Length(..)) => { + Length::input_to_command(state, input).map(Cmd::Length) + }, + _ => None + } + } + } +} + +impl Command for PhrasesCommand { + fn execute (self, view: &mut PhrasesTui) -> Perhaps { + use PhraseRenameCommand as Rename; + use PhraseLengthCommand as Length; + match self { + Self::Select(phrase) => { + view.phrase = phrase + }, + Self::Edit(command) => { + return Ok(command.execute(&mut view)?.map(Self::Edit)) + } + Self::Rename(command) => match command { + Rename::Begin => { + view.mode = Some(PhrasesMode::Rename( + view.phrase, + view.phrases[view.phrase].read().unwrap().name.clone() + )) + }, + _ => { + return Ok(command.execute(view)?.map(Self::Rename)) + } + }, + Self::Length(command) => match command { + Length::Begin => { + view.mode = Some(PhrasesMode::Length( + view.phrase, + view.phrases[view.phrase].read().unwrap().length, + PhraseLengthFocus::Bar + )) + }, + _ => { + return Ok(command.execute(view)?.map(Self::Length)) + } + }, + } + Ok(None) + } +} + +impl InputToCommand for PhraseLengthCommand { + fn input_to_command (view: &PhrasesTui, from: &TuiInput) -> Option { + if let Some(PhrasesMode::Length(_, length, _)) = view.mode { + Some(match from.event() { + key!(KeyCode::Up) => Self::Inc, + key!(KeyCode::Down) => Self::Dec, + key!(KeyCode::Right) => Self::Next, + key!(KeyCode::Left) => Self::Prev, + key!(KeyCode::Enter) => Self::Set(length), + key!(KeyCode::Esc) => Self::Cancel, + _ => return None + }) + } else { + unreachable!() + } + } +} + +impl Command for PhraseLengthCommand { + fn execute (self, view: &mut PhrasesTui) -> Perhaps { + use PhraseLengthFocus::*; + use PhraseLengthCommand::*; + if let Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) = view.mode { + match self { + Self::Cancel => { + view.mode = None; + }, + Self::Prev => { + focus.prev() + }, + Self::Next => { + focus.next() + }, + Self::Inc => match focus { + Bar => { *length += 4 * PPQ }, + Beat => { *length += PPQ }, + Tick => { *length += 1 }, + }, + Self::Dec => match focus { + Bar => { *length = length.saturating_sub(4 * PPQ) }, + Beat => { *length = length.saturating_sub(PPQ) }, + Tick => { *length = length.saturating_sub(1) }, + }, + Self::Set(length) => { + let mut phrase = view.phrases[phrase].write().unwrap(); + let old_length = phrase.length; + phrase.length = length; + view.mode = None; + return Ok(Some(Self::Set(old_length))) + }, + _ => unreachable!() + } + Ok(None) + } else if self == Begin { + view.mode = Some(PhrasesMode::Length( + view.phrase, + view.phrases[view.phrase].read().unwrap().length, + PhraseLengthFocus::Bar + )); + Ok(None) + } else { + unreachable!() + } + } +} + +impl InputToCommand for PhraseRenameCommand { + fn input_to_command (view: &PhrasesTui, from: &TuiInput) -> Option { + if let Some(PhrasesMode::Rename(_, ref old_name)) = view.mode { + Some(match from.event() { + key!(KeyCode::Char(c)) => { + let mut new_name = old_name.clone(); + new_name.push(*c); + Self::Set(new_name) + }, + key!(KeyCode::Backspace) => { + let mut new_name = old_name.clone(); + new_name.pop(); + Self::Set(new_name) + }, + key!(KeyCode::Enter) => Self::Confirm, + key!(KeyCode::Esc) => Self::Cancel, + _ => return None + }) + } else { + unreachable!() + } + } +} + +impl Command for PhraseRenameCommand { + fn execute (self, view: &mut PhrasesTui) -> Perhaps { + use PhraseRenameCommand::*; + if let Some(PhrasesMode::Rename(phrase, ref mut old_name)) = view.mode { + match self { + Set(s) => { + view.phrases[phrase].write().unwrap().name = s.into(); + return Ok(Some(Self::Set(old_name.clone()))) + }, + Confirm => { + let old_name = old_name.clone(); + view.mode = None; + return Ok(Some(Self::Set(old_name))) + }, + Cancel => { + let mut phrase = view.phrases[phrase].write().unwrap(); + phrase.name = old_name.clone(); + }, + _ => unreachable!() + }; + Ok(None) + } else if self == Begin { + view.mode = Some(PhrasesMode::Rename( + view.phrase, + view.phrases[view.phrase].read().unwrap().name.clone() + )); + Ok(None) + } else { + unreachable!() + } + } +} + +#[derive(Clone, PartialEq, Debug)] +pub enum PhraseCommand { + // TODO: 1-9 seek markers that by default start every 8th of the phrase + ToggleDirection, + EnterEditMode, + ExitEditMode, + NoteAppend, + NoteSet, + NoteCursorSet(usize), + NoteLengthSet(usize), + NoteScrollSet(usize), + TimeCursorSet(usize), + TimeScrollSet(usize), + TimeZoomSet(usize), +} + +impl InputToCommand for PhraseCommand { + fn input_to_command (state: &PhraseTui, from: &TuiInput) -> Option { + use PhraseCommand::*; + Some(match from.event() { + key!(KeyCode::Char('`')) => ToggleDirection, + key!(KeyCode::Enter) => EnterEditMode, + key!(KeyCode::Esc) => ExitEditMode, + key!(KeyCode::Char('[')) => NoteLengthSet(0), + key!(KeyCode::Char(']')) => NoteLengthSet(0), + key!(KeyCode::Char('a')) => NoteAppend, + key!(KeyCode::Char('s')) => NoteSet, + key!(KeyCode::Char('-')) => TimeZoomSet(0), + key!(KeyCode::Char('_')) => TimeZoomSet(0), + key!(KeyCode::Char('=')) => TimeZoomSet(0), + key!(KeyCode::Char('+')) => TimeZoomSet(0), + key!(KeyCode::PageUp) => NoteScrollSet(0), + key!(KeyCode::PageDown) => NoteScrollSet(0), + key!(KeyCode::Up) => match state.entered { + true => NoteCursorSet(0), + false => NoteScrollSet(0), + }, + key!(KeyCode::Down) => match state.entered { + true => NoteCursorSet(0), + false => NoteScrollSet(0), + }, + key!(KeyCode::Left) => match state.entered { + true => TimeCursorSet(0), + false => TimeScrollSet(0), + }, + key!(KeyCode::Right) => match state.entered { + true => TimeCursorSet(0), + false => TimeScrollSet(0), + }, + _ => return None + }) + } +} + +impl Command for PhraseCommand { + //fn translate (self, state: &PhraseTui) -> Self { + //use PhraseCommand::*; + //match self { + //GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, }, + //GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, }, + //GoLeft => match state.entered { true => TimeCursorDec, false => TimeScrollDec, }, + //GoRight => match state.entered { true => TimeCursorInc, false => TimeScrollInc, }, + //_ => self + //} + //} + fn execute (self, state: &mut PhraseTui) -> Perhaps { + use PhraseCommand::*; + match self.translate(state) { + ToggleDirection => { + state.mode = !state.mode; + }, + EnterEditMode => { + state.entered = true; + }, + ExitEditMode => { + state.entered = false; + }, + TimeZoomOut => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().scale = next_note_length(scale) + }, + TimeZoomIn => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().scale = prev_note_length(scale) + }, + TimeCursorDec => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().point_dec(scale); + }, + TimeCursorInc => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().point_inc(scale); + }, + TimeScrollDec => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().start_dec(scale); + }, + TimeScrollInc => { + let scale = state.time_axis.read().unwrap().scale; + state.time_axis.write().unwrap().start_inc(scale); + }, + NoteCursorDec => { + let mut axis = state.note_axis.write().unwrap(); + axis.point_inc(1); + if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } } + }, + NoteCursorInc => { + let mut axis = state.note_axis.write().unwrap(); + axis.point_dec(1); + if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } } + }, + NoteScrollDec => { + state.note_axis.write().unwrap().start_inc(1); + }, + NoteScrollInc => { + state.note_axis.write().unwrap().start_dec(1); + }, + NoteLengthDec => { + state.note_len = prev_note_length(state.note_len) + }, + NoteLengthInc => { + state.note_len = next_note_length(state.note_len) + }, + NotePageUp => { + let mut axis = state.note_axis.write().unwrap(); + axis.start_dec(3); + axis.point_dec(3); + }, + NotePageDown => { + let mut axis = state.note_axis.write().unwrap(); + axis.start_inc(3); + axis.point_inc(3); + }, + NoteAppend => { + if state.entered { + state.put(); + state.time_cursor_advance(); + } + }, + NoteSet => { + if state.entered { state.put(); } + }, + _ => unreachable!() + } + Ok(None) + } +} diff --git a/crates/tek_tui/src/tui_focus.rs b/crates/tek_tui/src/tui_focus.rs index 17ad98e8..e05b7dd7 100644 --- a/crates/tek_tui/src/tui_focus.rs +++ b/crates/tek_tui/src/tui_focus.rs @@ -213,3 +213,31 @@ impl FocusGrid for ArrangerTui { } } } + +/// Focused field of `PhraseLength` +#[derive(Copy, Clone)] +pub enum PhraseLengthFocus { + /// Editing the number of bars + Bar, + /// Editing the number of beats + Beat, + /// Editing the number of ticks + Tick, +} + +impl PhraseLengthFocus { + pub fn next (&mut self) { + *self = match self { + Self::Bar => Self::Beat, + Self::Beat => Self::Tick, + Self::Tick => Self::Bar, + } + } + pub fn prev (&mut self) { + *self = match self { + Self::Bar => Self::Tick, + Self::Beat => Self::Bar, + Self::Tick => Self::Beat, + } + } +} diff --git a/crates/tek_tui/src/tui_model.rs b/crates/tek_tui/src/tui_model.rs index 2f6d6c72..82db3b67 100644 --- a/crates/tek_tui/src/tui_model.rs +++ b/crates/tek_tui/src/tui_model.rs @@ -98,3 +98,100 @@ pub struct ArrangerTui { pub status_bar: Option, pub history: Vec, } + +#[derive(Default, Debug, Clone)] +pub struct ArrangerScene { + /// Name of scene + pub name: Arc>, + /// Clips in scene, one per track + pub clips: Vec>>>, + /// Identifying color of scene + pub color: ItemColor, +} + +#[derive(Debug)] +pub struct ArrangerTrack { + /// Name of track + name: Arc>, + /// Preferred width of track column + width: usize, + /// Identifying color of track + color: ItemColor, + /// Start time and phrase being played + play_phrase: Option<(Instant, Option>>)>, + /// Start time and next phrase + next_phrase: Option<(Instant, Option>>)>, + /// Play input through output. + monitoring: bool, + /// Write input to sequence. + recording: bool, + /// Overdub input to sequence. + overdub: bool, + /// Send all notes off + reset: bool, // TODO?: after Some(nframes) + /// Record from MIDI ports to current sequence. + midi_ins: Vec>, + /// Play from current sequence to MIDI ports + midi_outs: Vec>, + /// Notes currently held at input + notes_in: Arc>, + /// Notes currently held at output + notes_out: Arc>, + ///// MIDI output buffer + //midi_note: Vec, + ///// MIDI output buffer + //midi_chunk: Vec>>, +} + +pub struct PhrasesTui { + /// Collection of phrases + pub phrases: Vec>>, + /// Selected phrase + pub phrase: usize, + /// Scroll offset + pub scroll: usize, + /// Mode switch + pub mode: Option, + /// Whether this widget is focused + pub focused: bool, + /// Whether this widget is entered + pub entered: bool, +} + +/// Modes for phrase pool +pub enum PhrasesMode { + /// Renaming a pattern + Rename(usize, String), + /// Editing the length of a pattern + Length(usize, usize, PhraseLengthFocus), +} + +/// Contains state for viewing and editing a phrase +pub struct PhraseTui { + /// Phrase being played + pub phrase: Option>>, + /// Length of note that will be inserted, in pulses + pub note_len: usize, + /// The full piano keys are rendered to this buffer + pub keys: Buffer, + /// The full piano roll is rendered to this buffer + pub buffer: BigBuffer, + /// Cursor/scroll/zoom in pitch axis + pub note_axis: RwLock>, + /// Cursor/scroll/zoom in time axis + pub time_axis: RwLock>, + /// Whether this widget is focused + pub focused: bool, + /// Whether note enter mode is enabled + pub entered: bool, + /// Display mode + pub mode: bool, + /// Notes currently held at input + pub notes_in: Arc>, + /// Notes currently held at output + pub notes_out: Arc>, + /// Current position of global playhead + pub now: Arc, + /// Width and height of notes area at last render + pub size: Measure +} diff --git a/crates/tek_tui/src/tui_phrase.rs b/crates/tek_tui/src/tui_phrase.rs index 4318bf03..d21d4223 100644 --- a/crates/tek_tui/src/tui_phrase.rs +++ b/crates/tek_tui/src/tui_phrase.rs @@ -1,45 +1,5 @@ use crate::*; -/// Contains state for viewing and editing a phrase -pub struct PhraseTui { - /// Phrase being played - pub phrase: Option>>, - /// Length of note that will be inserted, in pulses - pub note_len: usize, - /// The full piano keys are rendered to this buffer - pub keys: Buffer, - /// The full piano roll is rendered to this buffer - pub buffer: BigBuffer, - /// Cursor/scroll/zoom in pitch axis - pub note_axis: RwLock>, - /// Cursor/scroll/zoom in time axis - pub time_axis: RwLock>, - /// Whether this widget is focused - pub focused: bool, - /// Whether note enter mode is enabled - pub entered: bool, - /// Display mode - pub mode: bool, - /// Notes currently held at input - pub notes_in: Arc>, - /// Notes currently held at output - pub notes_out: Arc>, - /// Current position of global playhead - pub now: Arc, - /// Width and height of notes area at last render - pub size: Measure -} - -impl Widget for PhraseTui { - type Engine = Tui; - fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - PhraseView(&self, Default::default()).layout(to) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - PhraseView(&self, Default::default()).render(to) - } -} - impl PhraseTui { pub fn new () -> Self { Self { @@ -179,48 +139,3 @@ impl PhraseTui { } } } - -/// Colors of piano keys -const KEY_COLORS: [(Color, Color);6] = [ - (Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)), - (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), - (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), - (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), - (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), - (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), -]; - -pub(crate) fn keys_vert () -> Buffer { - let area = [0, 0, 5, 64]; - let mut buffer = Buffer::empty(Rect { - x: area.x(), y: area.y(), width: area.w(), height: area.h() - }); - buffer_update(&mut buffer, area, &|cell, x, y| { - let y = 63 - y; - match x { - 0 => { - cell.set_char('▀'); - let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize]; - cell.set_fg(fg); - cell.set_bg(bg); - }, - 1 => { - cell.set_char('▀'); - cell.set_fg(Color::White); - cell.set_bg(Color::White); - }, - 2 => if y % 6 == 0 { - cell.set_char('C'); - }, - 3 => if y % 6 == 0 { - cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); - }, - _ => {} - } - }); - buffer -} - -const NTH_OCTAVE: [&'static str; 11] = [ - "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", -]; diff --git a/crates/tek_tui/src/tui_phrase_cmd.rs b/crates/tek_tui/src/tui_phrase_cmd.rs deleted file mode 100644 index d22bcd46..00000000 --- a/crates/tek_tui/src/tui_phrase_cmd.rs +++ /dev/null @@ -1,149 +0,0 @@ -use crate::*; - -#[derive(Clone, PartialEq, Debug)] -pub enum PhraseCommand { - // TODO: 1-9 seek markers that by default start every 8th of the phrase - ToggleDirection, - EnterEditMode, - ExitEditMode, - NoteAppend, - NoteSet, - NoteCursorSet(usize), - NoteLengthSet(usize), - NoteScrollSet(usize), - TimeCursorSet(usize), - TimeScrollSet(usize), - TimeZoomSet(usize), -} - -impl InputToCommand for PhraseCommand { - fn input_to_command (state: &PhraseTui, from: &TuiInput) -> Option { - use PhraseCommand::*; - Some(match from.event() { - key!(KeyCode::Char('`')) => ToggleDirection, - key!(KeyCode::Enter) => EnterEditMode, - key!(KeyCode::Esc) => ExitEditMode, - key!(KeyCode::Char('[')) => NoteLengthSet(0), - key!(KeyCode::Char(']')) => NoteLengthSet(0), - key!(KeyCode::Char('a')) => NoteAppend, - key!(KeyCode::Char('s')) => NoteSet, - key!(KeyCode::Char('-')) => TimeZoomSet(0), - key!(KeyCode::Char('_')) => TimeZoomSet(0), - key!(KeyCode::Char('=')) => TimeZoomSet(0), - key!(KeyCode::Char('+')) => TimeZoomSet(0), - key!(KeyCode::PageUp) => NoteScrollSet(0), - key!(KeyCode::PageDown) => NoteScrollSet(0), - key!(KeyCode::Up) => match state.entered { - true => NoteCursorSet(0), - false => NoteScrollSet(0), - }, - key!(KeyCode::Down) => match state.entered { - true => NoteCursorSet(0), - false => NoteScrollSet(0), - }, - key!(KeyCode::Left) => match state.entered { - true => TimeCursorSet(0), - false => TimeScrollSet(0), - }, - key!(KeyCode::Right) => match state.entered { - true => TimeCursorSet(0), - false => TimeScrollSet(0), - }, - _ => return None - }) - } -} - -impl Command for PhraseCommand { - //fn translate (self, state: &PhraseTui) -> Self { - //use PhraseCommand::*; - //match self { - //GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, }, - //GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, }, - //GoLeft => match state.entered { true => TimeCursorDec, false => TimeScrollDec, }, - //GoRight => match state.entered { true => TimeCursorInc, false => TimeScrollInc, }, - //_ => self - //} - //} - fn execute (self, state: &mut PhraseTui) -> Perhaps { - use PhraseCommand::*; - match self.translate(state) { - ToggleDirection => { - state.mode = !state.mode; - }, - EnterEditMode => { - state.entered = true; - }, - ExitEditMode => { - state.entered = false; - }, - TimeZoomOut => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().scale = next_note_length(scale) - }, - TimeZoomIn => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().scale = prev_note_length(scale) - }, - TimeCursorDec => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().point_dec(scale); - }, - TimeCursorInc => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().point_inc(scale); - }, - TimeScrollDec => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().start_dec(scale); - }, - TimeScrollInc => { - let scale = state.time_axis.read().unwrap().scale; - state.time_axis.write().unwrap().start_inc(scale); - }, - NoteCursorDec => { - let mut axis = state.note_axis.write().unwrap(); - axis.point_inc(1); - if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } } - }, - NoteCursorInc => { - let mut axis = state.note_axis.write().unwrap(); - axis.point_dec(1); - if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } } - }, - NoteScrollDec => { - state.note_axis.write().unwrap().start_inc(1); - }, - NoteScrollInc => { - state.note_axis.write().unwrap().start_dec(1); - }, - NoteLengthDec => { - state.note_len = prev_note_length(state.note_len) - }, - NoteLengthInc => { - state.note_len = next_note_length(state.note_len) - }, - NotePageUp => { - let mut axis = state.note_axis.write().unwrap(); - axis.start_dec(3); - axis.point_dec(3); - }, - NotePageDown => { - let mut axis = state.note_axis.write().unwrap(); - axis.start_inc(3); - axis.point_inc(3); - }, - NoteAppend => { - if state.entered { - state.put(); - state.time_cursor_advance(); - } - }, - NoteSet => { - if state.entered { state.put(); } - }, - _ => unreachable!() - } - Ok(None) - } -} diff --git a/crates/tek_tui/src/tui_phrase_view.rs b/crates/tek_tui/src/tui_phrase_view.rs deleted file mode 100644 index fbaf9def..00000000 --- a/crates/tek_tui/src/tui_phrase_view.rs +++ /dev/null @@ -1,183 +0,0 @@ -use crate::*; - -pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T); - -pub trait PhraseViewState: Send + Sync { - fn focused (&self) -> bool; - fn entered (&self) -> bool; - fn keys (&self) -> &Buffer; - fn phrase (&self) -> &Option>>; - fn buffer (&self) -> &BigBuffer; - fn note_len (&self) -> usize; - fn note_axis (&self) -> &RwLock>; - fn time_axis (&self) -> &RwLock>; - fn size (&self) -> &Measure; - fn now (&self) -> &Arc; -} - -impl PhraseViewState for PhraseTui { - fn focused (&self) -> bool { - &self.focused - } - fn entered (&self) -> bool { - &self.entered - } - fn keys (&self) -> &Buffer { - &self.keys - } - fn phrase (&self) -> &Option>> { - &self.phrase - } - fn buffer (&self) -> &BigBuffer { - &self.buffer - } - fn note_len (&self) -> usize { - &self.note_len - } - fn note_axis (&self) -> &RwLock> { - &self.note_axis - } - fn time_axis (&self) -> &RwLock> { - &self.time_axis - } - fn size (&self) -> &Measure { - &self.size - } - fn now (&self) -> &Arc { - &self.now - } -} - -impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> { - type Engine = Tui; - fn content (&self) -> impl Widget { - let phrase = self.0.phrase(); - let size = self.0.size(); - let focused = self.0.focused(); - let entered = self.0.entered(); - let keys = self.0.keys(); - let buffer = self.0.buffer(); - let note_len = self.0.note_len(); - let note_axis = self.0.note_axis(); - let time_axis = self.0.time_axis(); - let FixedAxis { start: note_start, point: note_point, clamp: note_clamp } - = *note_axis.read().unwrap(); - let ScaledAxis { start: time_start, point: time_point, clamp: time_clamp, scale: time_scale } - = *time_axis.read().unwrap(); - //let color = Color::Rgb(0,255,0); - //let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color); - let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{ - Ok(if to.area().h() >= 2 { - to.buffer_update(to.area().set_w(5), &|cell, x, y|{ - let y = y + (note_start / 2) as u16; - if x < keys.area.width && y < keys.area.height { - *cell = keys.get(x, y).clone() - } - }); - }) - }).fill_y(); - let notes_bg_null = Color::Rgb(28, 35, 25); - let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ - let area = to.area(); - let h = area.h() as usize; - size.set_wh(area.w(), h); - let mut axis = note_axis.write().unwrap(); - if let Some(point) = axis.point { - if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) { - axis.start += 2; - } - } - Ok(if to.area().h() >= 2 { - let area = to.area(); - to.buffer_update(area, &move |cell, x, y|{ - cell.set_bg(notes_bg_null); - let src_x = (x as usize + time_start) * time_scale; - let src_y = y as usize + note_start / 2; - if src_x < buffer.width && src_y < buffer.height - 1 { - buffer.get(src_x, buffer.height - src_y - 2).map(|src|{ - cell.set_symbol(src.symbol()); - cell.set_fg(src.fg); - cell.set_bg(src.bg); - }); - } - }); - }) - }).fill_x(); - let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ - Ok(if focused && entered { - let area = to.area(); - if let (Some(time), Some(note)) = (time_point, note_point) { - let x1 = area.x() + (time / time_scale) as u16; - let x2 = x1 + (note_len / time_scale) as u16; - let y = area.y() + note.saturating_sub(note_start) as u16 / 2; - let c = if note % 2 == 0 { "▀" } else { "▄" }; - for x in x1..x2 { - to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0)))); - } - } - }) - }); - let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30)); - let playhead_active = playhead_inactive.clone().yellow().bold().not_dim(); - let playhead = CustomWidget::new( - |to:[u16;2]|Ok(Some(to.clip_h(1))), - move|to: &mut TuiOutput|{ - if let Some(_) = phrase { - let now = self.0.now().get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length; - let time_clamp = time_clamp - .expect("time_axis of sequencer expected to be clamped"); - for x in 0..(time_clamp/time_scale).saturating_sub(time_start) { - let this_step = time_start + (x + 0) * time_scale; - let next_step = time_start + (x + 1) * time_scale; - let x = to.area().x() + x as u16; - let active = this_step <= now && now < next_step; - let character = if active { "|" } else { "·" }; - let style = if active { playhead_active } else { playhead_inactive }; - to.blit(&character, x, to.area.y(), Some(style)); - } - } - Ok(()) - } - ).push_x(6).align_sw(); - let border_color = if focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)}; - let title_color = if focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)}; - let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); - let note_area = lay!(notes, cursor).fill_x(); - let piano_roll = row!(keys, note_area).fill_x(); - let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border); - let content = lay!(content, playhead); - let mut upper_left = format!("[{}] Sequencer", if entered {"■"} else {" "}); - if let Some(phrase) = phrase { - upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name); - } - let mut lower_right = format!("┤{}├", size.format()); - lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale)); - //lower_right = format!("Zoom: {} (+{}:{}*{}|{})", - //pulses_to_name(time_scale), - //time_start, time_point.unwrap_or(0), - //time_scale, time_clamp.unwrap_or(0), - //); - if focused && entered { - lower_right = format!("┤Note: {} {}├─{lower_right}", - note_axis.read().unwrap().point.unwrap(), - pulses_to_name(note_len)); - //lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}", - //pulses_to_name(*note_len), - //note_start, - //note_point.unwrap_or(0), - //note_clamp.unwrap_or(0), - //); - } - let upper_right = if let Some(phrase) = phrase { - format!("┤Length: {}├", phrase.read().unwrap().length) - } else { - String::new() - }; - lay!( - content, - TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), - TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), - TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(), - ) - } -} diff --git a/crates/tek_tui/src/tui_pool.rs b/crates/tek_tui/src/tui_pool.rs deleted file mode 100644 index 9b7ea537..00000000 --- a/crates/tek_tui/src/tui_pool.rs +++ /dev/null @@ -1,190 +0,0 @@ -use crate::*; - -pub struct PhrasesTui { - /// Collection of phrases - pub phrases: Vec>>, - /// Selected phrase - pub phrase: usize, - /// Scroll offset - pub scroll: usize, - /// Mode switch - pub mode: Option, - /// Whether this widget is focused - pub focused: bool, - /// Whether this widget is entered - pub entered: bool, -} - -/// Modes for phrase pool -pub enum PhrasesMode { - /// Renaming a pattern - Rename(usize, String), - /// Editing the length of a pattern - Length(usize, usize, PhraseLengthFocus), -} - -impl PhrasesTui { - pub fn new (phrases: Vec>>) -> Self { - Self { - scroll: 0, - phrase: 0, - mode: None, - focused: false, - entered: false, - phrases, - } - } - pub fn len (&self) -> usize { - self.phrases.len() - } - pub fn phrase (&self) -> &Arc> { - &self.phrases[self.phrase] - } - pub fn index_before (&self, index: usize) -> usize { - index.overflowing_sub(1).0.min(self.len() - 1) - } - pub fn index_after (&self, index: usize) -> usize { - (index + 1) % self.len() - } - pub fn index_of (&self, phrase: &Phrase) -> Option { - for i in 0..self.phrases.len() { - if *self.phrases[i].read().unwrap() == *phrase { return Some(i) } - } - return None - } - fn new_phrase (name: Option<&str>, color: Option) -> Arc> { - Arc::new(RwLock::new(Phrase::new( - String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color - ))) - } - pub fn delete_selected (&mut self) { - if self.phrase > 0 { - self.phrases.remove(self.phrase); - self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1)); - } - } - pub fn append_new (&mut self, name: Option<&str>, color: Option) { - self.phrases.push(Self::new_phrase(name, color)); - self.phrase = self.phrases.len() - 1; - } - pub fn insert_new (&mut self, name: Option<&str>, color: Option) { - self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color)); - self.phrase += 1; - } - pub fn insert_dup (&mut self) { - let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate(); - phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); - self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase))); - self.phrase += 1; - } - pub fn move_up (&mut self) { - if self.phrase > 1 { - self.phrases.swap(self.phrase - 1, self.phrase); - self.phrase -= 1; - } - } - pub fn move_down (&mut self) { - if self.phrase < self.phrases.len().saturating_sub(1) { - self.phrases.swap(self.phrase + 1, self.phrase); - self.phrase += 1; - } - } -} - -/// Displays and edits phrase length. -pub struct PhraseLength { - /// Pulses per beat (quaver) - pub ppq: usize, - /// Beats per bar - pub bpb: usize, - /// Length of phrase in pulses - pub pulses: usize, - /// Selected subdivision - pub focus: Option, -} - -impl PhraseLength { - pub fn new (pulses: usize, focus: Option) -> Self { - Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { - self.pulses / (self.bpb * self.ppq) - } - pub fn beats (&self) -> usize { - (self.pulses % (self.bpb * self.ppq)) / self.ppq - } - pub fn ticks (&self) -> usize { - self.pulses % self.ppq - } - pub fn bars_string (&self) -> String { - format!("{}", self.bars()) - } - pub fn beats_string (&self) -> String { - format!("{}", self.beats()) - } - pub fn ticks_string (&self) -> String { - format!("{:>02}", self.ticks()) - } -} - -impl Content for PhraseLength { - type Engine = Tui; - fn content (&self) -> impl Widget { - Layers::new(move|add|{ - match self.focus { - None => add(&row!( - " ", self.bars_string(), - ".", self.beats_string(), - ".", self.ticks_string(), - " " - )), - Some(PhraseLengthFocus::Bar) => add(&row!( - "[", self.bars_string(), - "]", self.beats_string(), - ".", self.ticks_string(), - " " - )), - Some(PhraseLengthFocus::Beat) => add(&row!( - " ", self.bars_string(), - "[", self.beats_string(), - "]", self.ticks_string(), - " " - )), - Some(PhraseLengthFocus::Tick) => add(&row!( - " ", self.bars_string(), - ".", self.beats_string(), - "[", self.ticks_string(), - "]" - )), - } - }) - } -} - -/// Focused field of `PhraseLength` -#[derive(Copy, Clone)] -pub enum PhraseLengthFocus { - /// Editing the number of bars - Bar, - /// Editing the number of beats - Beat, - /// Editing the number of ticks - Tick, -} - -impl PhraseLengthFocus { - pub fn next (&mut self) { - *self = match self { - Self::Bar => Self::Beat, - Self::Beat => Self::Tick, - Self::Tick => Self::Bar, - } - } - pub fn prev (&mut self) { - *self = match self { - Self::Bar => Self::Tick, - Self::Beat => Self::Bar, - Self::Tick => Self::Beat, - } - } -} diff --git a/crates/tek_tui/src/tui_pool_cmd.rs b/crates/tek_tui/src/tui_pool_cmd.rs deleted file mode 100644 index 007af379..00000000 --- a/crates/tek_tui/src/tui_pool_cmd.rs +++ /dev/null @@ -1,221 +0,0 @@ -use crate::*; - -#[derive(Clone, PartialEq, Debug)] -pub enum PhrasesCommand { - Select(usize), - Edit(PhrasePoolCommand), - Rename(PhraseRenameCommand), - Length(PhraseLengthCommand), -} - -#[derive(Clone, Debug, PartialEq)] -pub enum PhraseRenameCommand { - Begin, - Set(String), - Confirm, - Cancel, -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum PhraseLengthCommand { - Begin, - Next, - Prev, - Inc, - Dec, - Set(usize), - Cancel, -} - -impl InputToCommand for PhrasesCommand { - fn input_to_command (state: &PhrasesTui, input: &TuiInput) -> Option { - use PhrasesCommand as Cmd; - use PhrasePoolCommand as Edit; - use PhraseRenameCommand as Rename; - use PhraseLengthCommand as Length; - match input.event() { - key!(KeyCode::Up) => Some(Cmd::Select(0)), - key!(KeyCode::Down) => Some(Cmd::Select(0)), - key!(KeyCode::Char(',')) => Some(Cmd::Edit(Edit::Swap(0, 0))), - key!(KeyCode::Char('.')) => Some(Cmd::Edit(Edit::Swap(0, 0))), - key!(KeyCode::Delete) => Some(Cmd::Edit(Edit::Delete(0))), - key!(KeyCode::Char('a')) => Some(Cmd::Edit(Edit::Add(0))), - key!(KeyCode::Char('i')) => Some(Cmd::Edit(Edit::Add(0))), - key!(KeyCode::Char('d')) => Some(Cmd::Edit(Edit::Duplicate(0))), - key!(KeyCode::Char('c')) => Some(Cmd::Edit(Edit::RandomColor(0))), - key!(KeyCode::Char('n')) => Some(Cmd::Rename(Rename::Begin)), - key!(KeyCode::Char('t')) => Some(Cmd::Length(Length::Begin)), - _ => match state.mode { - Some(PhrasesMode::Rename(..)) => { - Rename::input_to_command(state, input).map(Cmd::Rename) - }, - Some(PhrasesMode::Length(..)) => { - Length::input_to_command(state, input).map(Cmd::Length) - }, - _ => None - } - } - } -} - -impl Command for PhrasesCommand { - fn execute (self, view: &mut PhrasesTui) -> Perhaps { - use PhraseRenameCommand as Rename; - use PhraseLengthCommand as Length; - match self { - Self::Select(phrase) => { - view.phrase = phrase - }, - Self::Edit(command) => { - return Ok(command.execute(&mut view)?.map(Self::Edit)) - } - Self::Rename(command) => match command { - Rename::Begin => { - view.mode = Some(PhrasesMode::Rename( - view.phrase, - view.phrases[view.phrase].read().unwrap().name.clone() - )) - }, - _ => { - return Ok(command.execute(view)?.map(Self::Rename)) - } - }, - Self::Length(command) => match command { - Length::Begin => { - view.mode = Some(PhrasesMode::Length( - view.phrase, - view.phrases[view.phrase].read().unwrap().length, - PhraseLengthFocus::Bar - )) - }, - _ => { - return Ok(command.execute(view)?.map(Self::Length)) - } - }, - } - Ok(None) - } -} - -impl InputToCommand for PhraseLengthCommand { - fn input_to_command (view: &PhrasesTui, from: &TuiInput) -> Option { - if let Some(PhrasesMode::Length(_, length, _)) = view.mode { - Some(match from.event() { - key!(KeyCode::Up) => Self::Inc, - key!(KeyCode::Down) => Self::Dec, - key!(KeyCode::Right) => Self::Next, - key!(KeyCode::Left) => Self::Prev, - key!(KeyCode::Enter) => Self::Set(length), - key!(KeyCode::Esc) => Self::Cancel, - _ => return None - }) - } else { - unreachable!() - } - } -} - -impl Command for PhraseLengthCommand { - fn execute (self, view: &mut PhrasesTui) -> Perhaps { - use PhraseLengthFocus::*; - use PhraseLengthCommand::*; - if let Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) = view.mode { - match self { - Self::Cancel => { - view.mode = None; - }, - Self::Prev => { - focus.prev() - }, - Self::Next => { - focus.next() - }, - Self::Inc => match focus { - Bar => { *length += 4 * PPQ }, - Beat => { *length += PPQ }, - Tick => { *length += 1 }, - }, - Self::Dec => match focus { - Bar => { *length = length.saturating_sub(4 * PPQ) }, - Beat => { *length = length.saturating_sub(PPQ) }, - Tick => { *length = length.saturating_sub(1) }, - }, - Self::Set(length) => { - let mut phrase = view.phrases[phrase].write().unwrap(); - let old_length = phrase.length; - phrase.length = length; - view.mode = None; - return Ok(Some(Self::Set(old_length))) - }, - _ => unreachable!() - } - Ok(None) - } else if self == Begin { - view.mode = Some(PhrasesMode::Length( - view.phrase, - view.phrases[view.phrase].read().unwrap().length, - PhraseLengthFocus::Bar - )); - Ok(None) - } else { - unreachable!() - } - } -} - -impl InputToCommand for PhraseRenameCommand { - fn input_to_command (view: &PhrasesTui, from: &TuiInput) -> Option { - if let Some(PhrasesMode::Rename(_, ref old_name)) = view.mode { - Some(match from.event() { - key!(KeyCode::Char(c)) => { - let mut new_name = old_name.clone(); - new_name.push(*c); - Self::Set(new_name) - }, - key!(KeyCode::Backspace) => { - let mut new_name = old_name.clone(); - new_name.pop(); - Self::Set(new_name) - }, - key!(KeyCode::Enter) => Self::Confirm, - key!(KeyCode::Esc) => Self::Cancel, - _ => return None - }) - } else { - unreachable!() - } - } -} - -impl Command for PhraseRenameCommand { - fn execute (self, view: &mut PhrasesTui) -> Perhaps { - use PhraseRenameCommand::*; - if let Some(PhrasesMode::Rename(phrase, ref mut old_name)) = view.mode { - match self { - Set(s) => { - view.phrases[phrase].write().unwrap().name = s.into(); - return Ok(Some(Self::Set(old_name.clone()))) - }, - Confirm => { - let old_name = old_name.clone(); - view.mode = None; - return Ok(Some(Self::Set(old_name))) - }, - Cancel => { - let mut phrase = view.phrases[phrase].write().unwrap(); - phrase.name = old_name.clone(); - }, - _ => unreachable!() - }; - Ok(None) - } else if self == Begin { - view.mode = Some(PhrasesMode::Rename( - view.phrase, - view.phrases[view.phrase].read().unwrap().name.clone() - )); - Ok(None) - } else { - unreachable!() - } - } -} diff --git a/crates/tek_tui/src/tui_pool_view.rs b/crates/tek_tui/src/tui_pool_view.rs deleted file mode 100644 index 0069bdfe..00000000 --- a/crates/tek_tui/src/tui_pool_view.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::*; - -impl Widget for PhrasesTui { - type Engine = Tui; - fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - PhrasesView(&self).layout(to) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - PhrasesView(&self).render(to) - } -} - -pub trait PhrasesViewState { - fn focused (&self) -> bool; - fn entered (&self) -> bool; - fn phrases (&self) -> Vec>>; - fn phrase (&self) -> usize; - fn mode (&self) -> Option<&PhrasesMode>; -} - -impl PhrasesViewState for PhrasesTui { - fn focused (&self) -> bool { - todo!() - } - fn entered (&self) -> bool { - todo!() - } - fn phrases (&self) -> Vec>> { - todo!() - } - fn phrase (&self) -> usize { - todo!() - } - fn mode (&self) -> Option<&PhrasesMode> { - &self.mode - } -} - -pub struct PhrasesView<'a, T: PhrasesViewState>(&'a T); - -// TODO: Display phrases always in order of appearance -impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> { - type Engine = Tui; - fn content (&self) -> impl Widget { - let focused = self.0.focused(); - let entered = self.0.entered(); - let phrases = self.0.phrases(); - let selected_phrase = self.0.phrase(); - let mode = self.0.mode(); - let content = col!( - (i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{ - let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); - let mut length = PhraseLength::new(length, None); - if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode { - if focused && i == *phrase { - length.pulses = *new_length; - length.focus = Some(*focus); - } - } - let length = length.align_e().fill_x(); - let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x(); - let mut row2 = format!(" {name}"); - if let Some(PhrasesMode::Rename(phrase, _)) = mode { - if focused && i == *phrase { - row2 = format!("{row2}▄"); - } - }; - let row2 = TuiStyle::bold(row2, true); - add(&col!(row1, row2).fill_x().bg(color.base.rgb))?; - if focused && i == selected_phrase { - add(&CORNERS)?; - } - Ok(()) - }) - ); - let border_color = if focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)}; - let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); - let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border); - let title_color = if focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)}; - let upper_left = format!("[{}] Phrases", if entered {"■"} else {" "}); - let upper_right = format!("({})", self.0.phrases().len()); - lay!( - content, - TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), - TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), - ) - } -} diff --git a/crates/tek_tui/src/tui_arranger_select.rs b/crates/tek_tui/src/tui_select.rs similarity index 100% rename from crates/tek_tui/src/tui_arranger_select.rs rename to crates/tek_tui/src/tui_select.rs diff --git a/crates/tek_tui/src/tui_sequencer.rs b/crates/tek_tui/src/tui_sequencer.rs index 028a7929..c7b7e813 100644 --- a/crates/tek_tui/src/tui_sequencer.rs +++ b/crates/tek_tui/src/tui_sequencer.rs @@ -1,31 +1 @@ use crate::*; - -impl HasPhrases for SequencerTui { - fn phrases (&self) -> &Vec>> { - &self.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.phrases - } -} - -impl HasPhrase for SequencerTui { - fn reset (&self) -> bool { - self.reset - } - fn reset_mut (&mut self) -> &mut bool { - &mut self.reset - } - fn phrase (&self) -> &Option<(Instant, Option>>)> { - todo!() - } - fn phrase_mut (&self) -> &mut Option<(Instant, Option>>)> { - todo!() - } - fn next_phrase (&self) -> &Option<(Instant, Option>>)> { - todo!() - } - fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { - todo!() - } -} diff --git a/crates/tek_tui/src/tui_sequencer_cmd.rs b/crates/tek_tui/src/tui_sequencer_cmd.rs deleted file mode 100644 index 40601a22..00000000 --- a/crates/tek_tui/src/tui_sequencer_cmd.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::*; - -#[derive(Clone, Debug, PartialEq)] -pub enum SequencerCommand { - Focus(FocusCommand), - Undo, - Redo, - Clear, - Clock(ClockCommand), - Playhead(PlayheadCommand), - Phrases(PhrasesCommand), - Editor(PhraseCommand), -} - -impl InputToCommand for SequencerCommand { - fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { - use FocusCommand::*; - use SequencerCommand::*; - match input.event() { - key!(KeyCode::Tab) => Some(Self::Focus(Next)), - key!(Shift-KeyCode::Tab) => Some(Self::Focus(Prev)), - key!(KeyCode::BackTab) => Some(Self::Focus(Prev)), - key!(Shift-KeyCode::BackTab) => Some(Self::Focus(Prev)), - key!(KeyCode::Up) => Some(Self::Focus(Up)), - key!(KeyCode::Down) => Some(Self::Focus(Down)), - key!(KeyCode::Left) => Some(Self::Focus(Left)), - key!(KeyCode::Right) => Some(Self::Focus(Right)), - _ => Some(Self::App(match state.focused() { - SequencerFocus::Transport => - TransportCommand::input_to_command(&state, input).map(Transport), - SequencerFocus::Phrases => - PhrasesCommand::input_to_command(&state.phrases, input).map(Phrases), - SequencerFocus::PhraseEditor => - PhraseCommand::input_to_command(&state.editor, input).map(Editor), - _ => return None, - })) - } - } -} - -impl Command for SequencerCommand { - fn execute (self, state: &mut SequencerTui) -> Perhaps { - use SequencerCommand::*; - match self { - Focus(cmd) => delegate(cmd, Focus, state), - Phrases(cmd) => delegate(cmd, Phrases, &mut state.phrases), - Editor(cmd) => delegate(cmd, Editor, &mut state.editor), - Clock(cmd) => delegate(cmd, Clock, &mut state.transport), - Playhead(cmd) => delegate(cmd, Playhead, &mut state.transport) - } - } -} - -impl TransportControl for SequencerTui { - fn bpm (&self) -> &BeatsPerMinute { - self.app.bpm() - } - fn quant (&self) -> &Quantize { - self.app.quant() - } - fn sync (&self) -> &LaunchSync { - self.app.sync() - } -} diff --git a/crates/tek_tui/src/tui_sequencer_view.rs b/crates/tek_tui/src/tui_sequencer_view.rs deleted file mode 100644 index 15e26772..00000000 --- a/crates/tek_tui/src/tui_sequencer_view.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::*; - -impl Content for SequencerTui { - type Engine = Tui; - fn content (&self) -> impl Widget { - col!( - widget(&TransportView(self)), - Split::right(20, - widget(&PhrasesView(self)), - widget(&PhraseView(self)), - ).min_y(20) - ) - } -} - -impl TransportViewState for SequencerTui { - fn focus (&self) -> TransportFocus { - self.focus - } - fn is_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 PhrasesViewState for SequencerTui { - fn focused (&self) -> bool { - todo!() - } - fn entered (&self) -> bool { - todo!() - } - fn phrases (&self) -> Vec>> { - todo!() - } - fn phrase (&self) -> usize { - todo!() - } - fn mode (&self) -> Option<&PhrasesMode> { - &self.mode - } -} - -impl PhraseViewState for SequencerTui { - fn focused (&self) -> bool { - todo!() - } - fn entered (&self) -> bool { - todo!() - } - fn keys (&self) -> &Buffer { - todo!() - } - fn phrase (&self) -> &Option>> { - todo!() - } - fn buffer (&self) -> &BigBuffer { - todo!() - } - fn note_len (&self) -> usize { - todo!() - } - fn note_axis (&self) -> &RwLock> { - todo!() - } - fn time_axis (&self) -> &RwLock> { - todo!() - } - fn size (&self) -> &Measure { - todo!() - } - fn now (&self) -> &Arc { - todo!() - } -} diff --git a/crates/tek_tui/src/tui_transport.rs b/crates/tek_tui/src/tui_transport.rs deleted file mode 100644 index c7b7e813..00000000 --- a/crates/tek_tui/src/tui_transport.rs +++ /dev/null @@ -1 +0,0 @@ -use crate::*; diff --git a/crates/tek_tui/src/tui_transport_cmd.rs b/crates/tek_tui/src/tui_transport_cmd.rs deleted file mode 100644 index 8d0c106d..00000000 --- a/crates/tek_tui/src/tui_transport_cmd.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::*; - -#[derive(Clone, Debug, PartialEq)] -pub enum TransportCommand { - Focus(FocusCommand), - Clock(ClockCommand), - Playhead(PlayheadCommand), -} - -impl InputToCommand for TransportCommand { - fn input_to_command (state: &T, input: &TuiInput) -> Option { - use KeyCode::Char; - use ClockCommand::{SetBpm, SetQuant, SetSync}; - use TransportFocus as Focused; - use TransportCommand::{Focus, Clock, Playhead}; - let focused = state.focused(); - Some(match input.event() { - key!(Left) => Focus(FocusCommand::Prev), - key!(Right) => Focus(FocusCommand::Next), - key!(Char('.')) => match focused { - Focused::Bpm => Clock(SetBpm(state.bpm().get() + 1.0)), - Focused::Quant => Clock(SetQuant(state.next_quant())), - Focused::Sync => Clock(SetSync(state.next_sync())), - Focused::PlayPause => Playhead(todo!()), - Focused::Clock => Playhead(todo!()), - _ => {todo!()} - }, - key!(KeyCode::Char(',')) => match focused { - Focused::Bpm => Clock(SetBpm(state.bpm().get() - 1.0)), - Focused::Quant => Clock(SetQuant(state.prev_quant())), - Focused::Sync => Clock(SetSync(state.prev_sync())), - Focused::PlayPause => Playhead(todo!()), - Focused::Clock => Playhead(todo!()), - _ => {todo!()} - }, - key!(KeyCode::Char('>')) => match focused { - Focused::Bpm => Clock(SetBpm(state.bpm().get() + 0.001)), - Focused::Quant => Clock(SetQuant(state.next_quant())), - Focused::Sync => Clock(SetSync(state.next_sync())), - Focused::PlayPause => Playhead(todo!()), - Focused::Clock => Playhead(todo!()), - _ => {todo!()} - }, - key!(KeyCode::Char('<')) => match focused { - Focused::Bpm => Clock(SetBpm(state.bpm().get() - 0.001)), - Focused::Quant => Clock(SetQuant(state.prev_quant())), - Focused::Sync => Clock(SetSync(state.prev_sync())), - Focused::PlayPause => Playhead(todo!()), - Focused::Clock => Playhead(todo!()), - _ => {todo!()} - }, - _ => return None - }) - } -} - -pub trait TransportControl { - fn quant (&self) -> &Quantize; - fn bpm (&self) -> &BeatsPerMinute; - fn next_quant (&self) -> f64 { - next_note_length(self.quant().get() as usize) as f64 - } - fn prev_quant (&self) -> f64 { - prev_note_length(self.quant().get() as usize) as f64 - } - fn sync (&self) -> &LaunchSync; - fn next_sync (&self) -> f64 { - next_note_length(self.sync().get() as usize) as f64 - } - fn prev_sync (&self) -> f64 { - prev_note_length(self.sync().get() as usize) as f64 - } -} - -impl TransportControl for TransportTui { - fn bpm (&self) -> &BeatsPerMinute { - self.bpm() - } - fn quant (&self) -> &Quantize { - self.quant() - } - fn sync (&self) -> &LaunchSync { - self.sync() - } -} - -impl Command for TransportCommand { - fn execute (self, state: &mut T) -> Perhaps { - use TransportCommand::{Focus, Clock, Playhead}; - use ClockCommand::{SetBpm, SetQuant, SetSync}; - Ok(Some(match self { - Focus(Next) => { todo!() } - Focus(Prev) => { todo!() }, - Focus(_) => { unimplemented!() }, - Clock(SetBpm(bpm)) => Clock(SetBpm(state.bpm().set(bpm))), - Clock(SetQuant(quant)) => Clock(SetQuant(state.quant().set(quant))), - Clock(SetSync(sync)) => Clock(SetSync(state.sync().set(sync))), - _ => return Ok(None) - })) - } -} diff --git a/crates/tek_tui/src/tui_transport_view.rs b/crates/tek_tui/src/tui_transport_view.rs deleted file mode 100644 index ef0691de..00000000 --- a/crates/tek_tui/src/tui_transport_view.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::*; - -impl Widget for TransportTui { - type Engine = Tui; - fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - TransportView(&self, Default::default()).layout(to) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - TransportView(&self, Default::default()).render(to) - } -} - -pub struct TransportView<'a, T: TransportViewState>(pub &'a T); - -pub trait TransportViewState: Send + Sync { - fn focus (&self) -> TransportFocus; - fn is_focused (&self) -> bool; - fn transport_state (&self) -> Option; - fn bpm_value (&self) -> f64; - fn sync_value (&self) -> f64; - fn format_beat (&self) -> String; - fn format_msu (&self) -> String; -} - -impl TransportViewState for TransportTui { - fn focus (&self) -> TransportFocus { - self.focus - } - fn is_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<'a, T: TransportViewState> Content for TransportView<'a, T> { - type Engine = Tui; - fn content (&self) -> impl Widget { - let state = self.0; - lay!( - state.focus().wrap(state.is_focused(), TransportFocus::PlayPause, &Styled( - None, - match state.transport_state() { - Some(TransportState::Rolling) => "▶ PLAYING", - Some(TransportState::Starting) => "READY ...", - Some(TransportState::Stopped) => "⏹ STOPPED", - _ => unreachable!(), - } - ).min_xy(11, 2).push_x(1)).align_x().fill_x(), - - row!( - state.focus().wrap(state.is_focused(), TransportFocus::Bpm, &Outset::X(1u16, { - let bpm = state.bpm_value(); - row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) } - })), - //let quant = state.focus().wrap(state.focused(), TransportFocus::Quant, &Outset::X(1u16, row! { - //"QUANT ", ppq_to_name(state.0.quant as usize) - //})), - state.focus().wrap(state.is_focused(), TransportFocus::Sync, &Outset::X(1u16, row! { - "SYNC ", pulses_to_name(state.sync_value() as usize) - })) - ).align_w().fill_x(), - - state.focus().wrap(state.is_focused(), TransportFocus::Clock, &{ - let time1 = state.format_beat(); - let time2 = state.format_msu(); - row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1) - }).align_e().fill_x(), - - ).fill_x().bg(Color::Rgb(40, 50, 30)) - } -} diff --git a/crates/tek_tui/src/tui_arranger_view.rs b/crates/tek_tui/src/tui_view.rs similarity index 51% rename from crates/tek_tui/src/tui_arranger_view.rs rename to crates/tek_tui/src/tui_view.rs index cf6a8a27..bb7a909e 100644 --- a/crates/tek_tui/src/tui_arranger_view.rs +++ b/crates/tek_tui/src/tui_view.rs @@ -1,5 +1,177 @@ use crate::*; +impl Widget for TransportTui { + type Engine = Tui; + fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + TransportView(&self, Default::default()).layout(to) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + TransportView(&self, Default::default()).render(to) + } +} + +pub struct TransportView<'a, T: TransportViewState>(pub &'a T); + +pub trait TransportViewState: Send + Sync { + fn focus (&self) -> TransportFocus; + fn is_focused (&self) -> bool; + fn transport_state (&self) -> Option; + fn bpm_value (&self) -> f64; + fn sync_value (&self) -> f64; + fn format_beat (&self) -> String; + fn format_msu (&self) -> String; +} + +impl TransportViewState for TransportTui { + fn focus (&self) -> TransportFocus { + self.focus + } + fn is_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<'a, T: TransportViewState> Content for TransportView<'a, T> { + type Engine = Tui; + fn content (&self) -> impl Widget { + let state = self.0; + lay!( + state.focus().wrap(state.is_focused(), TransportFocus::PlayPause, &Styled( + None, + match state.transport_state() { + Some(TransportState::Rolling) => "▶ PLAYING", + Some(TransportState::Starting) => "READY ...", + Some(TransportState::Stopped) => "⏹ STOPPED", + _ => unreachable!(), + } + ).min_xy(11, 2).push_x(1)).align_x().fill_x(), + + row!( + state.focus().wrap(state.is_focused(), TransportFocus::Bpm, &Outset::X(1u16, { + let bpm = state.bpm_value(); + row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) } + })), + //let quant = state.focus().wrap(state.focused(), TransportFocus::Quant, &Outset::X(1u16, row! { + //"QUANT ", ppq_to_name(state.0.quant as usize) + //})), + state.focus().wrap(state.is_focused(), TransportFocus::Sync, &Outset::X(1u16, row! { + "SYNC ", pulses_to_name(state.sync_value() as usize) + })) + ).align_w().fill_x(), + + state.focus().wrap(state.is_focused(), TransportFocus::Clock, &{ + let time1 = state.format_beat(); + let time2 = state.format_msu(); + row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1) + }).align_e().fill_x(), + + ).fill_x().bg(Color::Rgb(40, 50, 30)) + } +} + +impl Content for SequencerTui { + type Engine = Tui; + fn content (&self) -> impl Widget { + col!( + widget(&TransportView(self)), + Split::right(20, + widget(&PhrasesView(self)), + widget(&PhraseView(self)), + ).min_y(20) + ) + } +} + +impl TransportViewState for SequencerTui { + fn focus (&self) -> TransportFocus { + self.focus + } + fn is_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 PhrasesViewState for SequencerTui { + fn focused (&self) -> bool { + todo!() + } + fn entered (&self) -> bool { + todo!() + } + fn phrases (&self) -> Vec>> { + todo!() + } + fn phrase (&self) -> usize { + todo!() + } + fn mode (&self) -> Option<&PhrasesMode> { + &self.mode + } +} + +impl PhraseViewState for SequencerTui { + fn focused (&self) -> bool { + todo!() + } + fn entered (&self) -> bool { + todo!() + } + fn keys (&self) -> &Buffer { + todo!() + } + fn phrase (&self) -> &Option>> { + todo!() + } + fn buffer (&self) -> &BigBuffer { + todo!() + } + fn note_len (&self) -> usize { + todo!() + } + fn note_axis (&self) -> &RwLock> { + todo!() + } + fn time_axis (&self) -> &RwLock> { + todo!() + } + fn size (&self) -> &Measure { + todo!() + } + fn now (&self) -> &Arc { + todo!() + } +} + /// Display mode of arranger #[derive(Clone, PartialEq)] pub enum ArrangerMode { @@ -531,3 +703,397 @@ impl PhraseViewState for ArrangerTui { todo!() } } + +impl Widget for PhrasesTui { + type Engine = Tui; + fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + PhrasesView(&self).layout(to) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + PhrasesView(&self).render(to) + } +} + +pub trait PhrasesViewState { + fn focused (&self) -> bool; + fn entered (&self) -> bool; + fn phrases (&self) -> Vec>>; + fn phrase (&self) -> usize; + fn mode (&self) -> Option<&PhrasesMode>; +} + +impl PhrasesViewState for PhrasesTui { + fn focused (&self) -> bool { + todo!() + } + fn entered (&self) -> bool { + todo!() + } + fn phrases (&self) -> Vec>> { + todo!() + } + fn phrase (&self) -> usize { + todo!() + } + fn mode (&self) -> Option<&PhrasesMode> { + &self.mode + } +} + +pub struct PhrasesView<'a, T: PhrasesViewState>(&'a T); + +// TODO: Display phrases always in order of appearance +impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> { + type Engine = Tui; + fn content (&self) -> impl Widget { + let focused = self.0.focused(); + let entered = self.0.entered(); + let phrases = self.0.phrases(); + let selected_phrase = self.0.phrase(); + let mode = self.0.mode(); + let content = col!( + (i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{ + let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); + let mut length = PhraseLength::new(length, None); + if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode { + if focused && i == *phrase { + length.pulses = *new_length; + length.focus = Some(*focus); + } + } + let length = length.align_e().fill_x(); + let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x(); + let mut row2 = format!(" {name}"); + if let Some(PhrasesMode::Rename(phrase, _)) = mode { + if focused && i == *phrase { + row2 = format!("{row2}▄"); + } + }; + let row2 = TuiStyle::bold(row2, true); + add(&col!(row1, row2).fill_x().bg(color.base.rgb))?; + if focused && i == selected_phrase { + add(&CORNERS)?; + } + Ok(()) + }) + ); + let border_color = if focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)}; + let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); + let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border); + let title_color = if focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)}; + let upper_left = format!("[{}] Phrases", if entered {"■"} else {" "}); + let upper_right = format!("({})", self.0.phrases().len()); + lay!( + content, + TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), + TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), + ) + } +} + +pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T); + +pub trait PhraseViewState: Send + Sync { + fn focused (&self) -> bool; + fn entered (&self) -> bool; + fn keys (&self) -> &Buffer; + fn phrase (&self) -> &Option>>; + fn buffer (&self) -> &BigBuffer; + fn note_len (&self) -> usize; + fn note_axis (&self) -> &RwLock>; + fn time_axis (&self) -> &RwLock>; + fn size (&self) -> &Measure; + fn now (&self) -> &Arc; +} + +impl PhraseViewState for PhraseTui { + fn focused (&self) -> bool { + &self.focused + } + fn entered (&self) -> bool { + &self.entered + } + fn keys (&self) -> &Buffer { + &self.keys + } + fn phrase (&self) -> &Option>> { + &self.phrase + } + fn buffer (&self) -> &BigBuffer { + &self.buffer + } + fn note_len (&self) -> usize { + &self.note_len + } + fn note_axis (&self) -> &RwLock> { + &self.note_axis + } + fn time_axis (&self) -> &RwLock> { + &self.time_axis + } + fn size (&self) -> &Measure { + &self.size + } + fn now (&self) -> &Arc { + &self.now + } +} + +impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> { + type Engine = Tui; + fn content (&self) -> impl Widget { + let phrase = self.0.phrase(); + let size = self.0.size(); + let focused = self.0.focused(); + let entered = self.0.entered(); + let keys = self.0.keys(); + let buffer = self.0.buffer(); + let note_len = self.0.note_len(); + let note_axis = self.0.note_axis(); + let time_axis = self.0.time_axis(); + let FixedAxis { start: note_start, point: note_point, clamp: note_clamp } + = *note_axis.read().unwrap(); + let ScaledAxis { start: time_start, point: time_point, clamp: time_clamp, scale: time_scale } + = *time_axis.read().unwrap(); + //let color = Color::Rgb(0,255,0); + //let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color); + let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{ + Ok(if to.area().h() >= 2 { + to.buffer_update(to.area().set_w(5), &|cell, x, y|{ + let y = y + (note_start / 2) as u16; + if x < keys.area.width && y < keys.area.height { + *cell = keys.get(x, y).clone() + } + }); + }) + }).fill_y(); + let notes_bg_null = Color::Rgb(28, 35, 25); + let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ + let area = to.area(); + let h = area.h() as usize; + size.set_wh(area.w(), h); + let mut axis = note_axis.write().unwrap(); + if let Some(point) = axis.point { + if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) { + axis.start += 2; + } + } + Ok(if to.area().h() >= 2 { + let area = to.area(); + to.buffer_update(area, &move |cell, x, y|{ + cell.set_bg(notes_bg_null); + let src_x = (x as usize + time_start) * time_scale; + let src_y = y as usize + note_start / 2; + if src_x < buffer.width && src_y < buffer.height - 1 { + buffer.get(src_x, buffer.height - src_y - 2).map(|src|{ + cell.set_symbol(src.symbol()); + cell.set_fg(src.fg); + cell.set_bg(src.bg); + }); + } + }); + }) + }).fill_x(); + let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ + Ok(if focused && entered { + let area = to.area(); + if let (Some(time), Some(note)) = (time_point, note_point) { + let x1 = area.x() + (time / time_scale) as u16; + let x2 = x1 + (note_len / time_scale) as u16; + let y = area.y() + note.saturating_sub(note_start) as u16 / 2; + let c = if note % 2 == 0 { "▀" } else { "▄" }; + for x in x1..x2 { + to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0)))); + } + } + }) + }); + let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30)); + let playhead_active = playhead_inactive.clone().yellow().bold().not_dim(); + let playhead = CustomWidget::new( + |to:[u16;2]|Ok(Some(to.clip_h(1))), + move|to: &mut TuiOutput|{ + if let Some(_) = phrase { + let now = self.0.now().get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length; + let time_clamp = time_clamp + .expect("time_axis of sequencer expected to be clamped"); + for x in 0..(time_clamp/time_scale).saturating_sub(time_start) { + let this_step = time_start + (x + 0) * time_scale; + let next_step = time_start + (x + 1) * time_scale; + let x = to.area().x() + x as u16; + let active = this_step <= now && now < next_step; + let character = if active { "|" } else { "·" }; + let style = if active { playhead_active } else { playhead_inactive }; + to.blit(&character, x, to.area.y(), Some(style)); + } + } + Ok(()) + } + ).push_x(6).align_sw(); + let border_color = if focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)}; + let title_color = if focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)}; + let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); + let note_area = lay!(notes, cursor).fill_x(); + let piano_roll = row!(keys, note_area).fill_x(); + let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border); + let content = lay!(content, playhead); + let mut upper_left = format!("[{}] Sequencer", if entered {"■"} else {" "}); + if let Some(phrase) = phrase { + upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name); + } + let mut lower_right = format!("┤{}├", size.format()); + lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale)); + //lower_right = format!("Zoom: {} (+{}:{}*{}|{})", + //pulses_to_name(time_scale), + //time_start, time_point.unwrap_or(0), + //time_scale, time_clamp.unwrap_or(0), + //); + if focused && entered { + lower_right = format!("┤Note: {} {}├─{lower_right}", + note_axis.read().unwrap().point.unwrap(), + pulses_to_name(note_len)); + //lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}", + //pulses_to_name(*note_len), + //note_start, + //note_point.unwrap_or(0), + //note_clamp.unwrap_or(0), + //); + } + let upper_right = if let Some(phrase) = phrase { + format!("┤Length: {}├", phrase.read().unwrap().length) + } else { + String::new() + }; + lay!( + content, + TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), + TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), + TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(), + ) + } +} + +/// Colors of piano keys +const KEY_COLORS: [(Color, Color);6] = [ + (Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)), + (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), + (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), + (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), + (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), + (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), +]; + +pub(crate) fn keys_vert () -> Buffer { + let area = [0, 0, 5, 64]; + let mut buffer = Buffer::empty(Rect { + x: area.x(), y: area.y(), width: area.w(), height: area.h() + }); + buffer_update(&mut buffer, area, &|cell, x, y| { + let y = 63 - y; + match x { + 0 => { + cell.set_char('▀'); + let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize]; + cell.set_fg(fg); + cell.set_bg(bg); + }, + 1 => { + cell.set_char('▀'); + cell.set_fg(Color::White); + cell.set_bg(Color::White); + }, + 2 => if y % 6 == 0 { + cell.set_char('C'); + }, + 3 => if y % 6 == 0 { + cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); + }, + _ => {} + } + }); + buffer +} + +const NTH_OCTAVE: [&'static str; 11] = [ + "-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", +]; + +impl Content for PhraseLength { + type Engine = Tui; + fn content (&self) -> impl Widget { + Layers::new(move|add|{ + match self.focus { + None => add(&row!( + " ", self.bars_string(), + ".", self.beats_string(), + ".", self.ticks_string(), + " " + )), + Some(PhraseLengthFocus::Bar) => add(&row!( + "[", self.bars_string(), + "]", self.beats_string(), + ".", self.ticks_string(), + " " + )), + Some(PhraseLengthFocus::Beat) => add(&row!( + " ", self.bars_string(), + "[", self.beats_string(), + "]", self.ticks_string(), + " " + )), + Some(PhraseLengthFocus::Tick) => add(&row!( + " ", self.bars_string(), + ".", self.beats_string(), + "[", self.ticks_string(), + "]" + )), + } + }) + } +} + +/// Displays and edits phrase length. +pub struct PhraseLength { + /// Pulses per beat (quaver) + pub ppq: usize, + /// Beats per bar + pub bpb: usize, + /// Length of phrase in pulses + pub pulses: usize, + /// Selected subdivision + pub focus: Option, +} + +impl PhraseLength { + pub fn new (pulses: usize, focus: Option) -> Self { + Self { _engine: Default::default(), ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { + self.pulses / (self.bpb * self.ppq) + } + pub fn beats (&self) -> usize { + (self.pulses % (self.bpb * self.ppq)) / self.ppq + } + pub fn ticks (&self) -> usize { + self.pulses % self.ppq + } + pub fn bars_string (&self) -> String { + format!("{}", self.bars()) + } + pub fn beats_string (&self) -> String { + format!("{}", self.beats()) + } + pub fn ticks_string (&self) -> String { + format!("{:>02}", self.ticks()) + } +} + +impl Widget for PhraseTui { + type Engine = Tui; + fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + PhraseView(&self, Default::default()).layout(to) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + PhraseView(&self, Default::default()).render(to) + } +}