diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 9a0640f2..23270cc0 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -1,4 +1,12 @@ -use crate::*; +use crate::{ + *, + api::{ + ArrangerTrackCommand, + ArrangerSceneCommand, + ArrangerClipCommand + } +}; + impl TryFrom<&Arc>> for ArrangerTui { type Error = Box; @@ -853,3 +861,451 @@ pub fn arranger_content_horizontal ( //) //) //} + +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]) + } + fn selected_scene (&self) -> Option<&ArrangerScene> { + self.selected.scene().map(|s|self.scenes().get(s)).flatten() + } + fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { + self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() + } +} + +#[derive(Default, Debug, Clone)] +pub struct ArrangerScene { + /// Name of scene + pub(crate) name: Arc>, + /// Clips in scene, one per track + pub(crate) clips: Vec>>>, + /// Identifying color of scene + pub(crate) color: ItemColor, +} + +impl ArrangerSceneApi for ArrangerScene { + fn name (&self) -> &Arc> { + &self.name + } + fn clips (&self) -> &Vec>>> { + &self.clips + } + fn color (&self) -> ItemColor { + self.color + } +} + +impl HasTracks for ArrangerTui { + fn tracks (&self) -> &Vec { + &self.tracks + } + fn tracks_mut (&mut self) -> &mut Vec { + &mut self.tracks + } +} + +impl ArrangerTracksApi for ArrangerTui { + fn track_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerTrack> + { + let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); + let track = ArrangerTrack { + width: name.len() + 2, + name: Arc::new(name.into()), + color: color.unwrap_or_else(||ItemColor::random()), + player: PhrasePlayerModel::from(&self.clock), + }; + self.tracks_mut().push(track); + let index = self.tracks().len() - 1; + Ok(&mut self.tracks_mut()[index]) + } + fn track_del (&mut self, index: usize) { + self.tracks_mut().remove(index); + for scene in self.scenes_mut().iter_mut() { + scene.clips.remove(index); + } + } +} + +#[derive(Debug)] +pub struct ArrangerTrack { + /// Name of track + pub(crate) name: Arc>, + /// Preferred width of track column + pub(crate) width: usize, + /// Identifying color of track + pub(crate) color: ItemColor, + /// MIDI player state + pub(crate) player: PhrasePlayerModel, +} + +impl HasPlayer for ArrangerTrack { + fn player (&self) -> &impl MidiPlayerApi { + &self.player + } + fn player_mut (&mut self) -> &mut impl MidiPlayerApi { + &mut self.player + } +} + +impl ArrangerTrackApi for ArrangerTrack { + /// Name of track + fn name (&self) -> &Arc> { + &self.name + } + /// Preferred width of track column + fn width (&self) -> usize { + self.width + } + /// Preferred width of track column + fn width_mut (&mut self) -> &mut usize { + &mut self.width + } + /// Identifying color of track + fn color (&self) -> ItemColor { + self.color + } +} + +#[derive(PartialEq, Clone, Copy, Debug)] +/// Represents the current user selection in the arranger +pub enum ArrangerSelection { + /// The whole mix is selected + Mix, + /// A track is selected. + Track(usize), + /// A scene is selected. + Scene(usize), + /// A clip (track × scene) is selected. + Clip(usize, usize), +} + +/// Focus identification methods +impl ArrangerSelection { + pub fn description ( + &self, + tracks: &Vec, + scenes: &Vec, + ) -> String { + format!("Selected: {}", match self { + Self::Mix => format!("Everything"), + Self::Track(t) => match tracks.get(*t) { + Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), + None => format!("T??"), + }, + Self::Scene(s) => match scenes.get(*s) { + Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), + None => format!("S??"), + }, + Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { + (Some(_), Some(scene)) => match scene.clip(*t) { + Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), + None => format!("T{t} S{s}: Empty") + }, + _ => format!("T{t} S{s}: Empty"), + } + }) + } + pub fn is_mix (&self) -> bool { + match self { Self::Mix => true, _ => false } + } + pub fn is_track (&self) -> bool { + match self { Self::Track(_) => true, _ => false } + } + pub fn is_scene (&self) -> bool { + match self { Self::Scene(_) => true, _ => false } + } + pub fn is_clip (&self) -> bool { + match self { Self::Clip(_, _) => true, _ => false } + } + pub fn track (&self) -> Option { + use ArrangerSelection::*; + match self { + Clip(t, _) => Some(*t), + Track(t) => Some(*t), + _ => None + } + } + pub fn scene (&self) -> Option { + use ArrangerSelection::*; + match self { + Clip(_, s) => Some(*s), + Scene(s) => Some(*s), + _ => None + } + } +} + +impl Handle for ArrangerTui { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + ArrangerCommand::execute_with_state(self, i) + } +} + +#[derive(Clone, Debug)] +pub enum ArrangerCommand { + Focus(FocusCommand), + Undo, + Redo, + Clear, + Color(ItemColor), + Clock(ClockCommand), + Scene(ArrangerSceneCommand), + Track(ArrangerTrackCommand), + Clip(ArrangerClipCommand), + Select(ArrangerSelection), + Zoom(usize), + Phrases(PhrasesCommand), + Editor(PhraseCommand), +} + +impl Command for ArrangerCommand { + fn execute (self, state: &mut ArrangerTui) -> Perhaps { + use ArrangerCommand::*; + Ok(match self { + Focus(cmd) => cmd.execute(state)?.map(Focus), + Scene(cmd) => cmd.execute(state)?.map(Scene), + Track(cmd) => cmd.execute(state)?.map(Track), + Clip(cmd) => cmd.execute(state)?.map(Clip), + Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), + Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), + Clock(cmd) => cmd.execute(state)?.map(Clock), + Zoom(_) => { todo!(); }, + Select(selected) => { + *state.selected_mut() = selected; + None + }, + _ => { todo!() } + }) + } +} + +impl Command for ArrangerSceneCommand { + fn execute (self, _state: &mut ArrangerTui) -> Perhaps { + //todo!(); + Ok(None) + } +} + +impl Command for ArrangerTrackCommand { + fn execute (self, _state: &mut ArrangerTui) -> Perhaps { + //todo!(); + Ok(None) + } +} + +impl Command for ArrangerClipCommand { + fn execute (self, _state: &mut ArrangerTui) -> Perhaps { + //todo!(); + Ok(None) + } +} + +pub trait ArrangerControl: TransportControl { + fn selected (&self) -> ArrangerSelection; + fn selected_mut (&mut self) -> &mut ArrangerSelection; + fn activate (&mut self) -> Usually<()>; + fn selected_phrase (&self) -> Option>>; + fn toggle_loop (&mut self); + fn randomize_color (&mut self); +} + +impl ArrangerControl for ArrangerTui { + fn selected (&self) -> ArrangerSelection { + self.selected + } + fn selected_mut (&mut self) -> &mut ArrangerSelection { + &mut self.selected + } + fn activate (&mut self) -> Usually<()> { + if let ArrangerSelection::Scene(s) = self.selected { + for (t, track) in self.tracks.iter_mut().enumerate() { + let phrase = self.scenes[s].clips[t].clone(); + if track.player.play_phrase.is_some() || phrase.is_some() { + track.player.enqueue_next(phrase.as_ref()); + } + } + if self.clock().is_stopped() { + self.clock().play_from(Some(0))?; + } + } else if let ArrangerSelection::Clip(t, s) = self.selected { + let phrase = self.scenes()[s].clips[t].clone(); + self.tracks_mut()[t].player.enqueue_next(phrase.as_ref()); + }; + Ok(()) + } + fn selected_phrase (&self) -> Option>> { + self.selected_scene()?.clips.get(self.selected.track()?)?.clone() + } + fn toggle_loop (&mut self) { + if let Some(phrase) = self.selected_phrase() { + phrase.write().unwrap().toggle_loop() + } + } + 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 InputToCommand for ArrangerCommand { + fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option { + to_arranger_command(state, input) + .or_else(||to_focus_command(input).map(ArrangerCommand::Focus)) + } +} + + +fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { + use ArrangerCommand as Cmd; + use KeyCode::Char; + if !state.entered() { + return None + } + Some(match input.event() { + key!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some( + state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() + ))), + _ => match state.focused() { + ArrangerFocus::Transport(_) => { + match TransportCommand::input_to_command(state, input)? { + TransportCommand::Clock(command) => Cmd::Clock(command), + _ => return None, + } + }, + ArrangerFocus::PhraseEditor => { + Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?) + }, + ArrangerFocus::Phrases => { + Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?) + }, + ArrangerFocus::Arranger => { + use ArrangerSelection::*; + match input.event() { + key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), + key!(Char('+')) => Cmd::Zoom(0), // TODO + key!(Char('=')) => Cmd::Zoom(0), // TODO + key!(Char('_')) => Cmd::Zoom(0), // TODO + key!(Char('-')) => Cmd::Zoom(0), // TODO + key!(Char('`')) => { todo!("toggle state mode") }, + key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), + key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), + _ => match state.selected() { + Mix => to_arranger_mix_command(input)?, + Track(t) => to_arranger_track_command(input, t)?, + Scene(s) => to_arranger_scene_command(input, s)?, + Clip(t, s) => to_arranger_clip_command(input, t, s)?, + } + } + } + } + }) +} + +fn to_arranger_mix_command (input: &TuiInput) -> Option { + use KeyCode::{Char, Down, Right, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + Some(match input.event() { + key!(Down) => Cmd::Select(Select::Scene(0)), + key!(Right) => Cmd::Select(Select::Track(0)), + key!(Char(',')) => Cmd::Zoom(0), + key!(Char('.')) => Cmd::Zoom(0), + key!(Char('<')) => Cmd::Zoom(0), + key!(Char('>')) => Cmd::Zoom(0), + key!(Delete) => Cmd::Clear, + key!(Char('c')) => Cmd::Color(ItemColor::random()), + _ => return None + }) +} + +fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { + use KeyCode::{Char, Down, Left, Right, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerTrackCommand as Track; + Some(match input.event() { + key!(Down) => Cmd::Select(Select::Clip(t, 0)), + key!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }), + key!(Right) => Cmd::Select(Select::Track(t + 1)), + key!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)), + key!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)), + key!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)), + key!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), + key!(Delete) => Cmd::Track(Track::Delete(t)), + //key!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())), + _ => return None + }) +} + +fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { + use KeyCode::{Char, Up, Down, Right, Enter, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerSceneCommand as Scene; + Some(match input.event() { + key!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }), + key!(Down) => Cmd::Select(Select::Scene(s + 1)), + key!(Right) => Cmd::Select(Select::Clip(0, s)), + key!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)), + key!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)), + key!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)), + key!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)), + key!(Enter) => Cmd::Scene(Scene::Play(s)), + key!(Delete) => Cmd::Scene(Scene::Delete(s)), + //key!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())), + _ => return None + }) +} + +fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option { + use KeyCode::{Char, Up, Down, Left, Right, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerClipCommand as Clip; + Some(match input.event() { + key!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }), + key!(Down) => Cmd::Select(Select::Clip(t, s + 1)), + key!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }), + key!(Right) => Cmd::Select(Select::Clip(t + 1, s)), + key!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)), + key!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)), + key!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)), + key!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)), + key!(Delete) => Cmd::Clip(Clip::Set(t, s, None)), + //key!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())), + //key!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))), + //key!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))), + _ => return None + }) +} diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index eea3070e..1862af0c 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -1,17 +1,30 @@ -use crate::*; +use crate::{*, api::ClockCommand::{Play, Pause}}; +use super::phrase_editor::PhraseCommand::Show; +use super::app_transport::TransportCommand; +use KeyCode::{Char, Enter}; +use SequencerCommand::*; use SequencerFocus::*; -use super::app_transport::TransportFocus::*; /// Create app state from JACK handle. impl TryFrom<&Arc>> for SequencerTui { type Error = Box; fn try_from (jack: &Arc>) -> Usually { let clock = ClockModel::from(jack); + let mut phrase = Phrase::default(); + phrase.name = "New".into(); + phrase.color = ItemColor::random().into(); + phrase.set_length(384); + let mut phrases = PhraseListModel::default(); + let phrase = Arc::new(RwLock::new(phrase)); + phrases.phrases.push(phrase.clone()); + phrases.phrase.store(1, Ordering::Relaxed); + let mut editor = PhraseEditorModel::default(); + editor.show_phrase(Some(phrase)); Ok(Self { jack: jack.clone(), - phrases: PhraseListModel::default(), + phrases: phrases, player: PhrasePlayerModel::from(&clock), - editor: PhraseEditorModel::default(), + editor: editor, size: Measure::new(), cursor: (0, 0), entered: false, @@ -20,7 +33,7 @@ impl TryFrom<&Arc>> for SequencerTui { note_buf: vec![], clock, perf: PerfModel::default(), - focus: FocusState::Focused(SequencerFocus::Transport(TransportFocus::PlayPause)) + focus: FocusState::Focused(SequencerFocus::PhraseEditor) }) } } @@ -216,6 +229,7 @@ impl StatusBar for SequencerStatusBar { impl From<&SequencerTui> for SequencerStatusBar { fn from (state: &SequencerTui) -> Self { + use super::app_transport::TransportFocus::*; let samples = state.clock.chunk.load(Ordering::Relaxed); let rate = state.clock.timebase.sr.get() as f64; let buffer = samples as f64 / rate; @@ -358,3 +372,95 @@ render!(|self:SequencerStats<'a>|{ Tui::fg(orange, size), ])) }); + +impl Handle for SequencerTui { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + SequencerCommand::execute_with_state(self, i) + } +} + +#[derive(Clone, Debug)] +pub enum SequencerCommand { + Focus(FocusCommand), + Clock(ClockCommand), + Phrases(PhrasesCommand), + Editor(PhraseCommand), + Enqueue(Option>>), + Clear, + Undo, + Redo, +} + +impl Command for SequencerCommand { + fn execute (self, state: &mut SequencerTui) -> Perhaps { + Ok(match self { + Self::Focus(cmd) => cmd.execute(state)?.map(Focus), + Self::Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), + Self::Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), + Self::Clock(cmd) => cmd.execute(state)?.map(Clock), + Self::Enqueue(phrase) => { + state.player.enqueue_next(phrase.as_ref()); + None + }, + Self::Undo => { todo!() }, + Self::Redo => { todo!() }, + Self::Clear => { todo!() }, + }) + } +} + +impl InputToCommand for SequencerCommand { + fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { + if state.entered() { + to_sequencer_command(state, input) + .or_else(||to_focus_command(input).map(SequencerCommand::Focus)) + } else { + to_focus_command(input).map(SequencerCommand::Focus) + .or_else(||to_sequencer_command(state, input)) + } + } +} + +pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option { + Some(match input.event() { + // Play/pause + key!(Char(' ')) => Clock( + if state.clock().is_stopped() { Play(None) } else { Pause(None) } + ), + // Play from start/rewind to start + key!(Shift-Char(' ')) => Clock( + if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } + ), + // Edit phrase + key!(Char('e')) => match state.focused() { + SequencerFocus::PhrasePlay => Editor(Show( + state.player.play_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) + )), + SequencerFocus::PhraseNext => Editor(Show( + state.player.next_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) + )), + SequencerFocus::PhraseList => Editor(Show( + Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()) + )), + _ => return None, + }, + _ => match state.focused() { + SequencerFocus::Transport(_) => match TransportCommand::input_to_command(state, input)? { + TransportCommand::Clock(command) => Clock(command), + _ => return None, + }, + SequencerFocus::PhraseEditor => Editor( + PhraseCommand::input_to_command(&state.editor, input)? + ), + SequencerFocus::PhraseList => match input.event() { + key!(Enter) => Enqueue(Some( + state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() + )), + _ => Phrases( + PhrasesCommand::input_to_command(&state.phrases, input)? + ), + } + _ => return None + } + }) +} diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index a399a989..1237a88b 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -82,10 +82,10 @@ impl From<&T> for TransportView { bpm, ppq, started: true, - global_sample: format!("{:.0}", started.sample.get()), - global_second: format!("{:.0}", started.usec.get()), - current_sample: format!("{:.0}", clock.global.sample.get() - started.sample.get()), - current_second: format!("{:.0}", clock.global.usec.get() - started.usec.get()), + global_sample: format!("{:.0}k", started.sample.get()/1000.), + global_second: format!("{:.1}s", started.usec.get()/1000.), + current_sample: format!("{:.0}k", (clock.global.sample.get() - started.sample.get())/1000.), + current_second: format!("{:.1}s", (clock.global.usec.get() - started.usec.get())/1000000.), } } else { Self { @@ -93,8 +93,8 @@ impl From<&T> for TransportView { bpm, ppq, started: false, - global_sample: format!("{:.0}", clock.global.sample.get()), - global_second: format!("{:.0}", clock.global.usec.get()), + global_sample: format!("{:.0}k", clock.global.sample.get()/1000.), + global_second: format!("{:.1}s", clock.global.usec.get()/1000000.), current_sample: "".to_string(), current_second: "".to_string(), } @@ -102,37 +102,43 @@ impl From<&T> for TransportView { } } +struct TransportField<'a>(&'a str, &'a str); +render!(|self: TransportField<'a>|{ + col!([ + Tui::fg(Color::Rgb(150, 150, 150), self.0), + Tui::bold(true, Tui::fg(Color::Rgb(200, 200, 200), self.1)), + ]) +}); + render!(|self: TransportView|{ let bg = TuiTheme::border_bg(); let border_style = Style::default().bg(bg).fg(TuiTheme::border_fg(false)); lay!([ Tui::fill_x(Lozenge(border_style)), - Tui::bg(bg, Tui::outset_xy(1, 1, row!([ + Tui::outset_x(1, row!([ row!([ - col!(["SR", self.sr ]), " ", - col!(["BPM", self.bpm]), " ", - col!(["PPQ", self.ppq]), " ", - ]), - row!([ - col!(["Sample", self.global_sample]), " ", - col!(["Second", self.global_second]), " ", + TransportField("SR ", self.sr.as_str()), + " ", + TransportField("BPM ", self.bpm.as_str()), + " ", + TransportField("PPQ ", self.ppq.as_str()), ]), lay!(|add|{ if self.started { add(&row!([ col!(["", Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING ")]), " ", - col!(["Sample", self.current_sample]), + TransportField("Beat", "00X+0/0B+00/00P"), " ", - col!(["Second", self.current_second]), + TransportField("Second", self.current_second.as_str()), " ", - col!(["Beat", "00B 0b 00/00"]), + TransportField("Sample", self.current_sample.as_str()), ])) } else { add(&col!([Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED "), ""])) } }), - ]))) + ])) ]) }); diff --git a/crates/tek/src/tui/ctrl_arranger.rs b/crates/tek/src/tui/ctrl_arranger.rs deleted file mode 100644 index 63bc6dac..00000000 --- a/crates/tek/src/tui/ctrl_arranger.rs +++ /dev/null @@ -1,266 +0,0 @@ -use super::model_arranger::ArrangerSelection; -use crate::{ - *, - api::{ - ArrangerTrackCommand, - ArrangerSceneCommand, - ArrangerClipCommand - } -}; - -impl Handle for ArrangerTui { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - ArrangerCommand::execute_with_state(self, i) - } -} - -#[derive(Clone, Debug)] -pub enum ArrangerCommand { - Focus(FocusCommand), - Undo, - Redo, - Clear, - Color(ItemColor), - Clock(ClockCommand), - Scene(ArrangerSceneCommand), - Track(ArrangerTrackCommand), - Clip(ArrangerClipCommand), - Select(ArrangerSelection), - Zoom(usize), - Phrases(PhrasesCommand), - Editor(PhraseCommand), -} - -impl Command for ArrangerCommand { - fn execute (self, state: &mut ArrangerTui) -> Perhaps { - use ArrangerCommand::*; - Ok(match self { - Focus(cmd) => cmd.execute(state)?.map(Focus), - Scene(cmd) => cmd.execute(state)?.map(Scene), - Track(cmd) => cmd.execute(state)?.map(Track), - Clip(cmd) => cmd.execute(state)?.map(Clip), - Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), - Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), - Clock(cmd) => cmd.execute(state)?.map(Clock), - Zoom(_) => { todo!(); }, - Select(selected) => { - *state.selected_mut() = selected; - None - }, - _ => { todo!() } - }) - } -} - -impl Command for ArrangerSceneCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} - -impl Command for ArrangerTrackCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} - -impl Command for ArrangerClipCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} - -pub trait ArrangerControl: TransportControl { - fn selected (&self) -> ArrangerSelection; - fn selected_mut (&mut self) -> &mut ArrangerSelection; - fn activate (&mut self) -> Usually<()>; - fn selected_phrase (&self) -> Option>>; - fn toggle_loop (&mut self); - fn randomize_color (&mut self); -} - -impl ArrangerControl for ArrangerTui { - fn selected (&self) -> ArrangerSelection { - self.selected - } - fn selected_mut (&mut self) -> &mut ArrangerSelection { - &mut self.selected - } - fn activate (&mut self) -> Usually<()> { - if let ArrangerSelection::Scene(s) = self.selected { - for (t, track) in self.tracks.iter_mut().enumerate() { - let phrase = self.scenes[s].clips[t].clone(); - if track.player.play_phrase.is_some() || phrase.is_some() { - track.player.enqueue_next(phrase.as_ref()); - } - } - if self.clock().is_stopped() { - self.clock().play_from(Some(0))?; - } - } else if let ArrangerSelection::Clip(t, s) = self.selected { - let phrase = self.scenes()[s].clips[t].clone(); - self.tracks_mut()[t].player.enqueue_next(phrase.as_ref()); - }; - Ok(()) - } - fn selected_phrase (&self) -> Option>> { - self.selected_scene()?.clips.get(self.selected.track()?)?.clone() - } - fn toggle_loop (&mut self) { - if let Some(phrase) = self.selected_phrase() { - phrase.write().unwrap().toggle_loop() - } - } - 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 InputToCommand for ArrangerCommand { - fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option { - to_arranger_command(state, input) - .or_else(||to_focus_command(input).map(ArrangerCommand::Focus)) - } -} - - -fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { - use ArrangerCommand as Cmd; - use KeyCode::Char; - if !state.entered() { - return None - } - Some(match input.event() { - key!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some( - state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() - ))), - _ => match state.focused() { - ArrangerFocus::Transport(_) => { - match TransportCommand::input_to_command(state, input)? { - TransportCommand::Clock(command) => Cmd::Clock(command), - _ => return None, - } - }, - ArrangerFocus::PhraseEditor => { - Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?) - }, - ArrangerFocus::Phrases => { - Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?) - }, - ArrangerFocus::Arranger => { - use ArrangerSelection::*; - match input.event() { - key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), - key!(Char('+')) => Cmd::Zoom(0), // TODO - key!(Char('=')) => Cmd::Zoom(0), // TODO - key!(Char('_')) => Cmd::Zoom(0), // TODO - key!(Char('-')) => Cmd::Zoom(0), // TODO - key!(Char('`')) => { todo!("toggle state mode") }, - key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), - key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), - _ => match state.selected() { - Mix => to_arranger_mix_command(input)?, - Track(t) => to_arranger_track_command(input, t)?, - Scene(s) => to_arranger_scene_command(input, s)?, - Clip(t, s) => to_arranger_clip_command(input, t, s)?, - } - } - } - } - }) -} - -fn to_arranger_mix_command (input: &TuiInput) -> Option { - use KeyCode::{Char, Down, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - Some(match input.event() { - key!(Down) => Cmd::Select(Select::Scene(0)), - key!(Right) => Cmd::Select(Select::Track(0)), - key!(Char(',')) => Cmd::Zoom(0), - key!(Char('.')) => Cmd::Zoom(0), - key!(Char('<')) => Cmd::Zoom(0), - key!(Char('>')) => Cmd::Zoom(0), - key!(Delete) => Cmd::Clear, - key!(Char('c')) => Cmd::Color(ItemColor::random()), - _ => return None - }) -} - -fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { - use KeyCode::{Char, Down, Left, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerTrackCommand as Track; - Some(match input.event() { - key!(Down) => Cmd::Select(Select::Clip(t, 0)), - key!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }), - key!(Right) => Cmd::Select(Select::Track(t + 1)), - key!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)), - key!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)), - key!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)), - key!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), - key!(Delete) => Cmd::Track(Track::Delete(t)), - //key!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())), - _ => return None - }) -} - -fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { - use KeyCode::{Char, Up, Down, Right, Enter, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerSceneCommand as Scene; - Some(match input.event() { - key!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }), - key!(Down) => Cmd::Select(Select::Scene(s + 1)), - key!(Right) => Cmd::Select(Select::Clip(0, s)), - key!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)), - key!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)), - key!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)), - key!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)), - key!(Enter) => Cmd::Scene(Scene::Play(s)), - key!(Delete) => Cmd::Scene(Scene::Delete(s)), - //key!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())), - _ => return None - }) -} - -fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option { - use KeyCode::{Char, Up, Down, Left, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerClipCommand as Clip; - Some(match input.event() { - key!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }), - key!(Down) => Cmd::Select(Select::Clip(t, s + 1)), - key!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }), - key!(Right) => Cmd::Select(Select::Clip(t + 1, s)), - key!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)), - key!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)), - key!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)), - key!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)), - key!(Delete) => Cmd::Clip(Clip::Set(t, s, None)), - //key!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())), - //key!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))), - //key!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))), - _ => return None - }) -} diff --git a/crates/tek/src/tui/ctrl_phrase_editor.rs b/crates/tek/src/tui/ctrl_phrase_editor.rs deleted file mode 100644 index 6209ffe7..00000000 --- a/crates/tek/src/tui/ctrl_phrase_editor.rs +++ /dev/null @@ -1,129 +0,0 @@ -use crate::*; - -#[derive(Clone, Debug)] -pub enum PhraseCommand { - // TODO: 1-9 seek markers that by default start every 8th of the phrase - AppendNote, - PutNote, - SetNoteCursor(usize), - SetNoteLength(usize), - SetNoteScroll(usize), - SetTimeCursor(usize), - SetTimeScroll(usize), - SetTimeZoom(usize), - Show(Option>>), - SetEditMode(PhraseEditMode), - ToggleDirection, -} - -impl InputToCommand for PhraseCommand { - fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option { - use PhraseCommand::*; - use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right}; - let note_lo = state.note_lo.load(Ordering::Relaxed); - let note_point = state.note_point.load(Ordering::Relaxed); - let time_start = state.time_start.load(Ordering::Relaxed); - let time_point = state.time_point.load(Ordering::Relaxed); - let time_zoom = state.view_mode.time_zoom(); - let length = state.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - Some(match from.event() { - key!(Char('`')) => ToggleDirection, - key!(Esc) => SetEditMode(PhraseEditMode::Scroll), - key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)), - key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)), - key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)), - key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)), - key!(Char('a')) => AppendNote, - key!(Char('s')) => PutNote, - key!(Char('[')) => SetNoteLength(prev_note_length(state.note_len)), - key!(Char(']')) => SetNoteLength(next_note_length(state.note_len)), - key!(Char('n')) => { todo!("toggle keys vs notes") }, - _ => match state.edit_mode { - PhraseEditMode::Scroll => match from.event() { - key!(Char('e')) => SetEditMode(PhraseEditMode::Note), - key!(Up) => SetNoteScroll(note_lo + 1), - key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)), - key!(PageUp) => SetNoteScroll(note_lo + 3), - key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)), - key!(Left) => SetTimeScroll(time_start.saturating_sub(1)), - key!(Right) => SetTimeScroll(time_start + 1), - _ => return None - }, - PhraseEditMode::Note => match from.event() { - key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll), - key!(Up) => SetNoteCursor(note_point + 1), - key!(Down) => SetNoteCursor(note_point.saturating_sub(1)), - key!(PageUp) => SetNoteCursor(note_point + 3), - key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)), - key!(Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)), - key!(Right) => SetTimeCursor((time_point + time_zoom) % length), - _ => return None - }, - } - }) - } -} - -impl Command for PhraseCommand { - fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { - use PhraseCommand::*; - Ok(match self { - Show(phrase) => { - state.show_phrase(phrase); - None - }, - ToggleDirection => { - todo!() - }, - SetEditMode(mode) => { - state.edit_mode = mode; - None - } - AppendNote => { - state.put_note(); - state.time_cursor_advance(); - None - }, - PutNote => { - state.put_note(); - None - }, - SetTimeCursor(time) => { - state.time_point.store(time, Ordering::Relaxed); - None - }, - SetTimeScroll(time) => { - state.time_start.store(time, Ordering::Relaxed); - None - }, - SetTimeZoom(zoom) => { - state.view_mode.set_time_zoom(zoom); - state.show_phrase(state.phrase.clone()); - None - }, - SetNoteScroll(note) => { - state.note_lo.store(note, Ordering::Relaxed); - None - }, - SetNoteLength(time) => { - state.note_len = time; - None - }, - SetNoteCursor(note) => { - let note = 127.min(note); - let start = state.note_lo.load(Ordering::Relaxed); - state.note_point.store(note, Ordering::Relaxed); - if note < start { - state.note_lo.store(note, Ordering::Relaxed); - } - None - }, - }) - } -} - -#[derive(Copy, Clone, Debug)] -pub enum PhraseEditMode { - Note, - Scroll, -} diff --git a/crates/tek/src/tui/ctrl_phrase_length.rs b/crates/tek/src/tui/ctrl_phrase_length.rs deleted file mode 100644 index 79668c95..00000000 --- a/crates/tek/src/tui/ctrl_phrase_length.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::*; -use super::model_phrase_list::{PhraseListModel, PhrasesMode}; -use super::model_phrase_length::PhraseLengthFocus::*; -use PhraseLengthCommand::*; - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum PhraseLengthCommand { - Begin, - Cancel, - Set(usize), - Next, - Prev, - Inc, - Dec, -} - -impl Command for PhraseLengthCommand { - fn execute (self, state: &mut PhraseListModel) -> Perhaps { - match state.phrases_mode_mut().clone() { - Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) => match self { - Cancel => { *state.phrases_mode_mut() = None; }, - Prev => { focus.prev() }, - Next => { focus.next() }, - Inc => match focus { - Bar => { *length += 4 * PPQ }, - Beat => { *length += PPQ }, - Tick => { *length += 1 }, - }, - Dec => match focus { - Bar => { *length = length.saturating_sub(4 * PPQ) }, - Beat => { *length = length.saturating_sub(PPQ) }, - Tick => { *length = length.saturating_sub(1) }, - }, - Set(length) => { - let mut phrase = state.phrases()[phrase].write().unwrap(); - let old_length = phrase.length; - phrase.length = length; - std::mem::drop(phrase); - *state.phrases_mode_mut() = None; - return Ok(Some(Self::Set(old_length))) - }, - _ => unreachable!() - }, - _ => unreachable!() - }; - Ok(None) - } -} diff --git a/crates/tek/src/tui/ctrl_phrase_list.rs b/crates/tek/src/tui/ctrl_phrase_list.rs deleted file mode 100644 index f21c712d..00000000 --- a/crates/tek/src/tui/ctrl_phrase_list.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::{ - *, - api::PhrasePoolCommand as Pool, - tui::{ - ctrl_phrase_rename::PhraseRenameCommand as Rename, - ctrl_phrase_length::PhraseLengthCommand as Length, - ctrl_file_browser::FileBrowserCommand as Browse, - } -}; - -#[derive(Clone, PartialEq, Debug)] -pub enum PhrasesCommand { - Select(usize), - Phrase(Pool), - Rename(Rename), - Length(Length), - Import(Browse), - Export(Browse), -} - -impl Command for PhrasesCommand { - fn execute (self, state: &mut PhraseListModel) -> Perhaps { - use PhrasesCommand::*; - Ok(match self { - Phrase(command) => command.execute(state)?.map(Phrase), - Rename(command) => match command { - PhraseRenameCommand::Begin => { - let length = state.phrases()[state.phrase_index()].read().unwrap().length; - *state.phrases_mode_mut() = Some( - PhrasesMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar) - ); - None - }, - _ => command.execute(state)?.map(Rename) - }, - Length(command) => match command { - PhraseLengthCommand::Begin => { - let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone(); - *state.phrases_mode_mut() = Some( - PhrasesMode::Rename(state.phrase_index(), name) - ); - None - }, - _ => command.execute(state)?.map(Length) - }, - Import(command) => match command { - FileBrowserCommand::Begin => { - *state.phrases_mode_mut() = Some( - PhrasesMode::Import(state.phrase_index(), FileBrowser::new(None)?) - ); - None - }, - _ => command.execute(state)?.map(Import) - }, - Export(command) => match command { - FileBrowserCommand::Begin => { - *state.phrases_mode_mut() = Some( - PhrasesMode::Export(state.phrase_index(), FileBrowser::new(None)?) - ); - None - }, - _ => command.execute(state)?.map(Export) - }, - Select(phrase) => { - state.set_phrase_index(phrase); - None - }, - }) - } -} - -impl HasPhrases for PhraseListModel { - fn phrases (&self) -> &Vec>> { - &self.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.phrases - } -} - -impl InputToCommand for PhrasesCommand { - fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option { - Some(match state.phrases_mode() { - Some(PhrasesMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?), - Some(PhrasesMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?), - Some(PhrasesMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?), - Some(PhrasesMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?), - _ => to_phrases_command(state, input)? - }) - } -} - -fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option { - use KeyCode::{Up, Down, Delete, Char}; - use PhrasesCommand as Cmd; - let index = state.phrase_index(); - let count = state.phrases().len(); - Some(match input.event() { - key!(Char('n')) => Cmd::Rename(Rename::Begin), - key!(Char('t')) => Cmd::Length(Length::Begin), - key!(Char('m')) => Cmd::Import(Browse::Begin), - key!(Char('x')) => Cmd::Export(Browse::Begin), - key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())), - key!(Up) => Cmd::Select( - index.overflowing_sub(1).0.min(state.phrases().len() - 1) - ), - key!(Down) => Cmd::Select( - index.saturating_add(1) % state.phrases().len() - ), - key!(Char(',')) => if index > 1 { - state.set_phrase_index(state.phrase_index().saturating_sub(1)); - Cmd::Phrase(Pool::Swap(index - 1, index)) - } else { - return None - }, - key!(Char('.')) => if index < count.saturating_sub(1) { - state.set_phrase_index(state.phrase_index() + 1); - Cmd::Phrase(Pool::Swap(index + 1, index)) - } else { - return None - }, - key!(Delete) => if index > 0 { - state.set_phrase_index(index.min(count.saturating_sub(1))); - Cmd::Phrase(Pool::Delete(index)) - } else { - return None - }, - key!(Char('a')) => Cmd::Phrase(Pool::Add(count, Phrase::new( - String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) - ))), - key!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new( - String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) - ))), - key!(Char('d')) => { - let mut phrase = state.phrases()[index].read().unwrap().duplicate(); - phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); - Cmd::Phrase(Pool::Add(index + 1, phrase)) - }, - _ => return None - }) -} diff --git a/crates/tek/src/tui/ctrl_sequencer.rs b/crates/tek/src/tui/ctrl_sequencer.rs deleted file mode 100644 index b9b13259..00000000 --- a/crates/tek/src/tui/ctrl_sequencer.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crate::{*, api::ClockCommand::{Play, Pause}}; -use super::ctrl_phrase_editor::PhraseCommand::Show; -use KeyCode::{Char, Enter}; -use SequencerCommand::*; -use super::app_transport::TransportCommand; - -impl Handle for SequencerTui { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - SequencerCommand::execute_with_state(self, i) - } -} - -#[derive(Clone, Debug)] -pub enum SequencerCommand { - Focus(FocusCommand), - Clock(ClockCommand), - Phrases(PhrasesCommand), - Editor(PhraseCommand), - Enqueue(Option>>), - Clear, - Undo, - Redo, -} - -impl Command for SequencerCommand { - fn execute (self, state: &mut SequencerTui) -> Perhaps { - Ok(match self { - Focus(cmd) => cmd.execute(state)?.map(Focus), - Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), - Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), - Clock(cmd) => cmd.execute(state)?.map(Clock), - Enqueue(phrase) => { - state.player.enqueue_next(phrase.as_ref()); - None - }, - Undo => { todo!() }, - Redo => { todo!() }, - Clear => { todo!() }, - }) - } -} - -impl InputToCommand for SequencerCommand { - fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { - if state.entered() { - to_sequencer_command(state, input) - .or_else(||to_focus_command(input).map(SequencerCommand::Focus)) - } else { - to_focus_command(input).map(SequencerCommand::Focus) - .or_else(||to_sequencer_command(state, input)) - } - } -} - -pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option { - Some(match input.event() { - // Play/pause - key!(Char(' ')) => Clock( - if state.clock().is_stopped() { Play(None) } else { Pause(None) } - ), - // Play from start/rewind to start - key!(Shift-Char(' ')) => Clock( - if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } - ), - // Edit phrase - key!(Char('e')) => match state.focused() { - SequencerFocus::PhrasePlay => Editor(Show( - state.player.play_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) - )), - SequencerFocus::PhraseNext => Editor(Show( - state.player.next_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) - )), - SequencerFocus::PhraseList => Editor(Show( - Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()) - )), - _ => return None, - }, - _ => match state.focused() { - SequencerFocus::Transport(_) => match TransportCommand::input_to_command(state, input)? { - TransportCommand::Clock(command) => Clock(command), - _ => return None, - }, - SequencerFocus::PhraseEditor => Editor( - PhraseCommand::input_to_command(&state.editor, input)? - ), - SequencerFocus::PhraseList => match input.event() { - key!(Enter) => Enqueue(Some( - state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() - )), - _ => Phrases( - PhrasesCommand::input_to_command(&state.phrases, input)? - ), - } - _ => return None - } - }) -} diff --git a/crates/tek/src/tui/ctrl_transport.rs b/crates/tek/src/tui/ctrl_transport.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/tek/src/tui/ctrl_file_browser.rs b/crates/tek/src/tui/file_browser.rs similarity index 63% rename from crates/tek/src/tui/ctrl_file_browser.rs rename to crates/tek/src/tui/file_browser.rs index 4e60c784..436a3220 100644 --- a/crates/tek/src/tui/ctrl_file_browser.rs +++ b/crates/tek/src/tui/file_browser.rs @@ -1,7 +1,88 @@ use crate::*; use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace}; use FileBrowserCommand::*; -use super::model_phrase_list::PhrasesMode::{Import, Export}; +use super::phrase_list::PhrasesMode::{Import, Export}; + +/// Browses for phrase to import/export +#[derive(Debug, Clone)] +pub struct FileBrowser { + pub cwd: PathBuf, + pub dirs: Vec<(OsString, String)>, + pub files: Vec<(OsString, String)>, + pub filter: String, + pub index: usize, + pub scroll: usize, + pub size: Measure +} + +render!(|self: FileBrowser|{ + Stack::down(|add|{ + let mut i = 0; + for (_, name) in self.dirs.iter() { + if i >= self.scroll { + add(&Tui::bold(i == self.index, name.as_str()))?; + } + i += 1; + } + for (_, name) in self.files.iter() { + if i >= self.scroll { + add(&Tui::bold(i == self.index, name.as_str()))?; + } + i += 1; + } + add(&format!("{}/{i}", self.index))?; + Ok(()) + }) +}); + +impl FileBrowser { + pub fn new (cwd: Option) -> Usually { + let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; + let mut dirs = vec![]; + let mut files = vec![]; + for entry in std::fs::read_dir(&cwd)? { + let entry = entry?; + let name = entry.file_name(); + let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); + let meta = entry.metadata()?; + if meta.is_dir() { + dirs.push((name, format!("📁 {decoded}"))); + } else if meta.is_file() { + files.push((name, format!("📄 {decoded}"))); + } + } + Ok(Self { + cwd, + dirs, + files, + filter: "".to_string(), + index: 0, + scroll: 0, + size: Measure::new(), + }) + } + pub fn len (&self) -> usize { + self.dirs.len() + self.files.len() + } + pub fn is_dir (&self) -> bool { + self.index < self.dirs.len() + } + pub fn is_file (&self) -> bool { + self.index >= self.dirs.len() + } + pub fn path (&self) -> PathBuf { + self.cwd.join(if self.is_dir() { + &self.dirs[self.index].0 + } else if self.is_file() { + &self.files[self.index - self.dirs.len()].0 + } else { + unreachable!() + }) + } + pub fn chdir (&self) -> Usually { + Self::new(Some(self.path())) + } +} /// Commands supported by [FileBrowser] #[derive(Debug, Clone, PartialEq)] diff --git a/crates/tek/src/tui/model_arranger.rs b/crates/tek/src/tui/model_arranger.rs deleted file mode 100644 index 33319bf9..00000000 --- a/crates/tek/src/tui/model_arranger.rs +++ /dev/null @@ -1,192 +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]) - } - fn selected_scene (&self) -> Option<&ArrangerScene> { - self.selected.scene().map(|s|self.scenes().get(s)).flatten() - } - fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { - self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() - } -} - -#[derive(Default, Debug, Clone)] -pub struct ArrangerScene { - /// Name of scene - pub(crate) name: Arc>, - /// Clips in scene, one per track - pub(crate) clips: Vec>>>, - /// Identifying color of scene - pub(crate) color: ItemColor, -} - -impl ArrangerSceneApi for ArrangerScene { - fn name (&self) -> &Arc> { - &self.name - } - fn clips (&self) -> &Vec>>> { - &self.clips - } - fn color (&self) -> ItemColor { - self.color - } -} - -impl HasTracks for ArrangerTui { - fn tracks (&self) -> &Vec { - &self.tracks - } - fn tracks_mut (&mut self) -> &mut Vec { - &mut self.tracks - } -} - -impl ArrangerTracksApi for ArrangerTui { - fn track_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerTrack> - { - let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); - let track = ArrangerTrack { - width: name.len() + 2, - name: Arc::new(name.into()), - color: color.unwrap_or_else(||ItemColor::random()), - player: PhrasePlayerModel::from(&self.clock), - }; - self.tracks_mut().push(track); - let index = self.tracks().len() - 1; - Ok(&mut self.tracks_mut()[index]) - } - fn track_del (&mut self, index: usize) { - self.tracks_mut().remove(index); - for scene in self.scenes_mut().iter_mut() { - scene.clips.remove(index); - } - } -} - -#[derive(Debug)] -pub struct ArrangerTrack { - /// Name of track - pub(crate) name: Arc>, - /// Preferred width of track column - pub(crate) width: usize, - /// Identifying color of track - pub(crate) color: ItemColor, - /// MIDI player state - pub(crate) player: PhrasePlayerModel, -} - -impl HasPlayer for ArrangerTrack { - fn player (&self) -> &impl MidiPlayerApi { - &self.player - } - fn player_mut (&mut self) -> &mut impl MidiPlayerApi { - &mut self.player - } -} - -impl ArrangerTrackApi for ArrangerTrack { - /// Name of track - fn name (&self) -> &Arc> { - &self.name - } - /// Preferred width of track column - fn width (&self) -> usize { - self.width - } - /// Preferred width of track column - fn width_mut (&mut self) -> &mut usize { - &mut self.width - } - /// Identifying color of track - fn color (&self) -> ItemColor { - self.color - } -} - -#[derive(PartialEq, Clone, Copy, Debug)] -/// Represents the current user selection in the arranger -pub enum ArrangerSelection { - /// The whole mix is selected - Mix, - /// A track is selected. - Track(usize), - /// A scene is selected. - Scene(usize), - /// A clip (track × scene) is selected. - Clip(usize, usize), -} - -/// Focus identification methods -impl ArrangerSelection { - pub fn description ( - &self, - tracks: &Vec, - scenes: &Vec, - ) -> String { - format!("Selected: {}", match self { - Self::Mix => format!("Everything"), - Self::Track(t) => match tracks.get(*t) { - Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), - None => format!("T??"), - }, - Self::Scene(s) => match scenes.get(*s) { - Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), - None => format!("S??"), - }, - Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { - (Some(_), Some(scene)) => match scene.clip(*t) { - Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), - None => format!("T{t} S{s}: Empty") - }, - _ => format!("T{t} S{s}: Empty"), - } - }) - } - pub fn is_mix (&self) -> bool { - match self { Self::Mix => true, _ => false } - } - pub fn is_track (&self) -> bool { - match self { Self::Track(_) => true, _ => false } - } - pub fn is_scene (&self) -> bool { - match self { Self::Scene(_) => true, _ => false } - } - pub fn is_clip (&self) -> bool { - match self { Self::Clip(_, _) => true, _ => false } - } - pub fn track (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(t, _) => Some(*t), - Track(t) => Some(*t), - _ => None - } - } - pub fn scene (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(_, s) => Some(*s), - Scene(s) => Some(*s), - _ => None - } - } -} diff --git a/crates/tek/src/tui/model_file_browser.rs b/crates/tek/src/tui/model_file_browser.rs deleted file mode 100644 index e539d01c..00000000 --- a/crates/tek/src/tui/model_file_browser.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::*; - -/// Browses for phrase to import/export -#[derive(Debug, Clone)] -pub struct FileBrowser { - pub cwd: PathBuf, - pub dirs: Vec<(OsString, String)>, - pub files: Vec<(OsString, String)>, - pub filter: String, - pub index: usize, - pub scroll: usize, - pub size: Measure -} - -impl FileBrowser { - pub fn new (cwd: Option) -> Usually { - let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; - let mut dirs = vec![]; - let mut files = vec![]; - for entry in std::fs::read_dir(&cwd)? { - let entry = entry?; - let name = entry.file_name(); - let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); - let meta = entry.metadata()?; - if meta.is_dir() { - dirs.push((name, format!("📁 {decoded}"))); - } else if meta.is_file() { - files.push((name, format!("📄 {decoded}"))); - } - } - Ok(Self { - cwd, - dirs, - files, - filter: "".to_string(), - index: 0, - scroll: 0, - size: Measure::new(), - }) - } - pub fn len (&self) -> usize { - self.dirs.len() + self.files.len() - } - pub fn is_dir (&self) -> bool { - self.index < self.dirs.len() - } - pub fn is_file (&self) -> bool { - self.index >= self.dirs.len() - } - pub fn path (&self) -> PathBuf { - self.cwd.join(if self.is_dir() { - &self.dirs[self.index].0 - } else if self.is_file() { - &self.files[self.index - self.dirs.len()].0 - } else { - unreachable!() - }) - } - pub fn chdir (&self) -> Usually { - Self::new(Some(self.path())) - } -} diff --git a/crates/tek/src/tui/model_phrase_editor.rs b/crates/tek/src/tui/model_phrase_editor.rs deleted file mode 100644 index 8e5a7d3d..00000000 --- a/crates/tek/src/tui/model_phrase_editor.rs +++ /dev/null @@ -1,134 +0,0 @@ -use crate::*; - -/// Contains state for viewing and editing a phrase -pub struct PhraseEditorModel { - /// Phrase being played - pub(crate) phrase: Option>>, - /// Length of note that will be inserted, in pulses - pub(crate) note_len: usize, - /// The full piano roll is rendered to this buffer - pub(crate) buffer: BigBuffer, - /// Notes currently held at input - pub(crate) notes_in: Arc>, - /// Notes currently held at output - pub(crate) notes_out: Arc>, - /// Current position of global playhead - pub(crate) now: Arc, - /// Width and height of notes area at last render - pub(crate) size: Measure, - - pub(crate) note_lo: AtomicUsize, - pub(crate) note_point: AtomicUsize, - - pub(crate) time_start: AtomicUsize, - pub(crate) time_point: AtomicUsize, - pub(crate) time_scale: AtomicUsize, - - pub(crate) edit_mode: PhraseEditMode, - pub(crate) view_mode: PhraseViewMode, -} - -impl std::fmt::Debug for PhraseEditorModel { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("PhraseEditorModel") - .field("note_axis", &format!("{} {}", - self.note_lo.load(Ordering::Relaxed), - self.note_point.load(Ordering::Relaxed), - )) - .field("time_axis", &format!("{} {} {}", - self.time_start.load(Ordering::Relaxed), - self.time_point.load(Ordering::Relaxed), - self.time_scale.load(Ordering::Relaxed), - )) - .finish() - } -} - -impl Default for PhraseEditorModel { - fn default () -> Self { - Self { - phrase: None, - note_len: 24, - buffer: Default::default(), - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - now: Pulse::default().into(), - size: Measure::new(), - edit_mode: PhraseEditMode::Scroll, - note_lo: 0.into(), - note_point: 0.into(), - time_start: 0.into(), - time_point: 0.into(), - time_scale: 24.into(), - view_mode: PhraseViewMode::PianoHorizontal { - time_zoom: 24, - note_zoom: PhraseViewNoteZoom::N(1) - }, - } - } -} - -impl PhraseEditorModel { - /// Put note at current position - pub fn put_note (&mut self) { - if let Some(phrase) = &self.phrase { - let time = self.time_point.load(Ordering::Relaxed); - let note = self.note_point.load(Ordering::Relaxed); - let mut phrase = phrase.write().unwrap(); - let key: u7 = u7::from((127 - note) as u8); - let vel: u7 = 100.into(); - let start = time; - let end = (start + self.note_len) % phrase.length; - phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); - phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); - self.buffer = self.view_mode.draw(&phrase); - } - } - /// Move time cursor forward by current note length - pub fn time_cursor_advance (&self) { - let point = self.time_point.load(Ordering::Relaxed); - let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - let forward = |time|(time + self.note_len) % length; - self.time_point.store(forward(point), Ordering::Relaxed); - } - /// Select which pattern to display. This pre-renders it to the buffer at full resolution. - pub fn show_phrase (&mut self, phrase: Option>>) { - if phrase.is_some() { - self.buffer = self.view_mode.draw(&*phrase.as_ref().unwrap().read().unwrap()); - self.phrase = phrase; - } else { - self.buffer = Default::default(); - self.phrase = None; - } - } -} - -pub trait HasEditor { - fn editor (&self) -> &PhraseEditorModel; - fn editor_focused (&self) -> bool; - fn editor_entered (&self) -> bool; -} - -impl HasEditor for SequencerTui { - fn editor (&self) -> &PhraseEditorModel { - &self.editor - } - fn editor_focused (&self) -> bool { - self.focused() == SequencerFocus::PhraseEditor - } - fn editor_entered (&self) -> bool { - self.entered() && self.editor_focused() - } -} - -impl HasEditor for ArrangerTui { - fn editor (&self) -> &PhraseEditorModel { - &self.editor - } - fn editor_focused (&self) -> bool { - self.focused() == ArrangerFocus::PhraseEditor - } - fn editor_entered (&self) -> bool { - self.entered() && self.editor_focused() - } -} diff --git a/crates/tek/src/tui/model_phrase_length.rs b/crates/tek/src/tui/model_phrase_length.rs deleted file mode 100644 index 091f02ee..00000000 --- a/crates/tek/src/tui/model_phrase_length.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::*; - -/// 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 { 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()) - } -} - -/// Focused field of `PhraseLength` -#[derive(Copy, Clone, Debug)] -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/src/tui/model_phrase_list.rs b/crates/tek/src/tui/model_phrase_list.rs deleted file mode 100644 index 65e66e58..00000000 --- a/crates/tek/src/tui/model_phrase_list.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::*; -use super::*; - -#[derive(Debug)] -pub struct PhraseListModel { - /// Collection of phrases - pub(crate) phrases: Vec>>, - /// Selected phrase - pub(crate) phrase: AtomicUsize, - /// Scroll offset - pub(crate) scroll: usize, - /// Mode switch - pub(crate) mode: Option, -} - -impl Default for PhraseListModel { - fn default () -> Self { - Self { - phrases: vec![RwLock::new(Phrase::default()).into()], - phrase: 0.into(), - scroll: 0, - mode: None, - } - } -} - -impl PhraseListModel { - pub(crate) fn phrase_index (&self) -> usize { - self.phrase.load(Ordering::Relaxed) - } - pub(crate) fn set_phrase_index (&self, value: usize) { - self.phrase.store(value, Ordering::Relaxed); - } - pub(crate) fn phrases_mode (&self) -> &Option { - &self.mode - } - pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option { - &mut self.mode - } -} - -/// Modes for phrase pool -#[derive(Debug, Clone)] -pub enum PhrasesMode { - /// Renaming a pattern - Rename(usize, String), - /// Editing the length of a pattern - Length(usize, usize, PhraseLengthFocus), - /// Load phrase from disk - Import(usize, FileBrowser), - /// Save phrase to disk - Export(usize, FileBrowser), -} - -pub trait HasPhraseList: HasPhrases { - fn phrases_focused (&self) -> bool; - fn phrases_entered (&self) -> bool; - fn phrases_mode (&self) -> &Option; - fn phrase_index (&self) -> usize; -} - -impl HasPhraseList for SequencerTui { - fn phrases_focused (&self) -> bool { - self.focused() == SequencerFocus::PhraseList - } - fn phrases_entered (&self) -> bool { - self.entered() && self.phrases_focused() - } - fn phrases_mode (&self) -> &Option { - &self.phrases.mode - } - fn phrase_index (&self) -> usize { - self.phrases.phrase.load(Ordering::Relaxed) - } -} - -impl HasPhraseList for ArrangerTui { - fn phrases_focused (&self) -> bool { - self.focused() == ArrangerFocus::Phrases - } - fn phrases_entered (&self) -> bool { - self.entered() && self.phrases_focused() - } - fn phrases_mode (&self) -> &Option { - &self.phrases.mode - } - fn phrase_index (&self) -> usize { - self.phrases.phrase.load(Ordering::Relaxed) - } -} diff --git a/crates/tek/src/tui/view_phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs similarity index 65% rename from crates/tek/src/tui/view_phrase_editor.rs rename to crates/tek/src/tui/phrase_editor.rs index 81be8ff9..a3b10c34 100644 --- a/crates/tek/src/tui/view_phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -1,5 +1,138 @@ use crate::*; +/// Contains state for viewing and editing a phrase +pub struct PhraseEditorModel { + /// Phrase being played + pub(crate) phrase: Option>>, + /// Length of note that will be inserted, in pulses + pub(crate) note_len: usize, + /// The full piano roll is rendered to this buffer + pub(crate) buffer: BigBuffer, + /// Notes currently held at input + pub(crate) notes_in: Arc>, + /// Notes currently held at output + pub(crate) notes_out: Arc>, + /// Current position of global playhead + pub(crate) now: Arc, + /// Width and height of notes area at last render + pub(crate) size: Measure, + + pub(crate) note_lo: AtomicUsize, + pub(crate) note_point: AtomicUsize, + + pub(crate) time_start: AtomicUsize, + pub(crate) time_point: AtomicUsize, + pub(crate) time_scale: AtomicUsize, + + pub(crate) edit_mode: PhraseEditMode, + pub(crate) view_mode: PhraseViewMode, +} + +impl std::fmt::Debug for PhraseEditorModel { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("PhraseEditorModel") + .field("note_axis", &format!("{} {}", + self.note_lo.load(Ordering::Relaxed), + self.note_point.load(Ordering::Relaxed), + )) + .field("time_axis", &format!("{} {} {}", + self.time_start.load(Ordering::Relaxed), + self.time_point.load(Ordering::Relaxed), + self.time_scale.load(Ordering::Relaxed), + )) + .finish() + } +} + +impl Default for PhraseEditorModel { + fn default () -> Self { + Self { + phrase: None, + note_len: 24, + buffer: Default::default(), + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + now: Pulse::default().into(), + size: Measure::new(), + edit_mode: PhraseEditMode::Scroll, + note_lo: 0.into(), + note_point: 0.into(), + time_start: 0.into(), + time_point: 0.into(), + time_scale: 24.into(), + view_mode: PhraseViewMode::PianoHorizontal { + time_zoom: 24, + note_zoom: PhraseViewNoteZoom::N(1) + }, + } + } +} + +impl PhraseEditorModel { + /// Put note at current position + pub fn put_note (&mut self) { + if let Some(phrase) = &self.phrase { + let time = self.time_point.load(Ordering::Relaxed); + let note = self.note_point.load(Ordering::Relaxed); + let mut phrase = phrase.write().unwrap(); + let key: u7 = u7::from((127 - note) as u8); + let vel: u7 = 100.into(); + let start = time; + let end = (start + self.note_len) % phrase.length; + phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); + phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); + self.buffer = self.view_mode.draw(&phrase); + } + } + /// Move time cursor forward by current note length + pub fn time_cursor_advance (&self) { + let point = self.time_point.load(Ordering::Relaxed); + let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + let forward = |time|(time + self.note_len) % length; + self.time_point.store(forward(point), Ordering::Relaxed); + } + /// Select which pattern to display. This pre-renders it to the buffer at full resolution. + pub fn show_phrase (&mut self, phrase: Option>>) { + if phrase.is_some() { + self.buffer = self.view_mode.draw(&*phrase.as_ref().unwrap().read().unwrap()); + self.phrase = phrase; + } else { + self.buffer = Default::default(); + self.phrase = None; + } + } +} + +pub trait HasEditor { + fn editor (&self) -> &PhraseEditorModel; + fn editor_focused (&self) -> bool; + fn editor_entered (&self) -> bool; +} + +impl HasEditor for SequencerTui { + fn editor (&self) -> &PhraseEditorModel { + &self.editor + } + fn editor_focused (&self) -> bool { + self.focused() == SequencerFocus::PhraseEditor + } + fn editor_entered (&self) -> bool { + self.entered() && self.editor_focused() + } +} + +impl HasEditor for ArrangerTui { + fn editor (&self) -> &PhraseEditorModel { + &self.editor + } + fn editor_focused (&self) -> bool { + self.focused() == ArrangerFocus::PhraseEditor + } + fn editor_entered (&self) -> bool { + self.entered() && self.editor_focused() + } +} + pub struct PhraseView<'a> { focused: bool, entered: bool, @@ -473,3 +606,131 @@ impl PhraseViewMode { //////"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", ////]; + +#[derive(Clone, Debug)] +pub enum PhraseCommand { + // TODO: 1-9 seek markers that by default start every 8th of the phrase + AppendNote, + PutNote, + SetNoteCursor(usize), + SetNoteLength(usize), + SetNoteScroll(usize), + SetTimeCursor(usize), + SetTimeScroll(usize), + SetTimeZoom(usize), + Show(Option>>), + SetEditMode(PhraseEditMode), + ToggleDirection, +} + +impl InputToCommand for PhraseCommand { + fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option { + use PhraseCommand::*; + use KeyCode::{Char, Esc, Up, Down, PageUp, PageDown, Left, Right}; + let note_lo = state.note_lo.load(Ordering::Relaxed); + let note_point = state.note_point.load(Ordering::Relaxed); + let time_start = state.time_start.load(Ordering::Relaxed); + let time_point = state.time_point.load(Ordering::Relaxed); + let time_zoom = state.view_mode.time_zoom(); + let length = state.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + Some(match from.event() { + key!(Char('`')) => ToggleDirection, + key!(Esc) => SetEditMode(PhraseEditMode::Scroll), + key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)), + key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)), + key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)), + key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)), + key!(Char('a')) => AppendNote, + key!(Char('s')) => PutNote, + key!(Char('[')) => SetNoteLength(prev_note_length(state.note_len)), + key!(Char(']')) => SetNoteLength(next_note_length(state.note_len)), + key!(Char('n')) => { todo!("toggle keys vs notes") }, + _ => match state.edit_mode { + PhraseEditMode::Scroll => match from.event() { + key!(Char('e')) => SetEditMode(PhraseEditMode::Note), + key!(Up) => SetNoteScroll(note_lo + 1), + key!(Down) => SetNoteScroll(note_lo.saturating_sub(1)), + key!(PageUp) => SetNoteScroll(note_lo + 3), + key!(PageDown) => SetNoteScroll(note_lo.saturating_sub(3)), + key!(Left) => SetTimeScroll(time_start.saturating_sub(1)), + key!(Right) => SetTimeScroll(time_start + 1), + _ => return None + }, + PhraseEditMode::Note => match from.event() { + key!(Char('e')) => SetEditMode(PhraseEditMode::Scroll), + key!(Up) => SetNoteCursor(note_point + 1), + key!(Down) => SetNoteCursor(note_point.saturating_sub(1)), + key!(PageUp) => SetNoteCursor(note_point + 3), + key!(PageDown) => SetNoteCursor(note_point.saturating_sub(3)), + key!(Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)), + key!(Right) => SetTimeCursor((time_point + time_zoom) % length), + _ => return None + }, + } + }) + } +} + +impl Command for PhraseCommand { + fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { + use PhraseCommand::*; + Ok(match self { + Show(phrase) => { + state.show_phrase(phrase); + None + }, + ToggleDirection => { + todo!() + }, + SetEditMode(mode) => { + state.edit_mode = mode; + None + } + AppendNote => { + state.put_note(); + state.time_cursor_advance(); + None + }, + PutNote => { + state.put_note(); + None + }, + SetTimeCursor(time) => { + state.time_point.store(time, Ordering::Relaxed); + None + }, + SetTimeScroll(time) => { + state.time_start.store(time, Ordering::Relaxed); + None + }, + SetTimeZoom(zoom) => { + state.view_mode.set_time_zoom(zoom); + state.show_phrase(state.phrase.clone()); + None + }, + SetNoteScroll(note) => { + state.note_lo.store(note, Ordering::Relaxed); + None + }, + SetNoteLength(time) => { + state.note_len = time; + None + }, + SetNoteCursor(note) => { + let note = 127.min(note); + let start = state.note_lo.load(Ordering::Relaxed); + state.note_point.store(note, Ordering::Relaxed); + if note < start { + state.note_lo.store(note, Ordering::Relaxed); + } + None + }, + }) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum PhraseEditMode { + Note, + Scroll, +} diff --git a/crates/tek/src/tui/phrase_length.rs b/crates/tek/src/tui/phrase_length.rs new file mode 100644 index 00000000..f6527cf4 --- /dev/null +++ b/crates/tek/src/tui/phrase_length.rs @@ -0,0 +1,129 @@ +use crate::*; +use super::phrase_list::{PhraseListModel, PhrasesMode}; +use PhraseLengthFocus::*; +use PhraseLengthCommand::*; + +/// 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 { 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()) + } +} + +/// Focused field of `PhraseLength` +#[derive(Copy, Clone, Debug)] +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, + } + } +} + + +render!(|self: PhraseLength|{ + let bars = ||self.bars_string(); + let beats = ||self.beats_string(); + let ticks = ||self.ticks_string(); + row!(move|add|match self.focus { + None => + add(&row!([" ", bars(), "B", beats(), "b", ticks(), "T"])), + Some(PhraseLengthFocus::Bar) => + add(&row!(["[", bars(), "]", beats(), "b", ticks(), "T"])), + Some(PhraseLengthFocus::Beat) => + add(&row!([" ", bars(), "[", beats(), "]", ticks(), "T"])), + Some(PhraseLengthFocus::Tick) => + add(&row!([" ", bars(), "B", beats(), "[", ticks(), "]"])), + }) +}); + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum PhraseLengthCommand { + Begin, + Cancel, + Set(usize), + Next, + Prev, + Inc, + Dec, +} + +impl Command for PhraseLengthCommand { + fn execute (self, state: &mut PhraseListModel) -> Perhaps { + match state.phrases_mode_mut().clone() { + Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) => match self { + Cancel => { *state.phrases_mode_mut() = None; }, + Prev => { focus.prev() }, + Next => { focus.next() }, + Inc => match focus { + Bar => { *length += 4 * PPQ }, + Beat => { *length += PPQ }, + Tick => { *length += 1 }, + }, + Dec => match focus { + Bar => { *length = length.saturating_sub(4 * PPQ) }, + Beat => { *length = length.saturating_sub(PPQ) }, + Tick => { *length = length.saturating_sub(1) }, + }, + Set(length) => { + let mut phrase = state.phrases()[phrase].write().unwrap(); + let old_length = phrase.length; + phrase.length = length; + std::mem::drop(phrase); + *state.phrases_mode_mut() = None; + return Ok(Some(Self::Set(old_length))) + }, + _ => unreachable!() + }, + _ => unreachable!() + }; + Ok(None) + } +} diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs new file mode 100644 index 00000000..f9159ede --- /dev/null +++ b/crates/tek/src/tui/phrase_list.rs @@ -0,0 +1,303 @@ +use super::*; +use crate::{ + *, + api::PhrasePoolCommand as Pool, + tui::{ + phrase_rename::PhraseRenameCommand as Rename, + phrase_length::PhraseLengthCommand as Length, + file_browser::FileBrowserCommand as Browse, + } +}; + +#[derive(Debug)] +pub struct PhraseListModel { + /// Collection of phrases + pub(crate) phrases: Vec>>, + /// Selected phrase + pub(crate) phrase: AtomicUsize, + /// Scroll offset + pub(crate) scroll: usize, + /// Mode switch + pub(crate) mode: Option, +} + +impl Default for PhraseListModel { + fn default () -> Self { + Self { + phrases: vec![RwLock::new(Phrase::default()).into()], + phrase: 0.into(), + scroll: 0, + mode: None, + } + } +} + +impl PhraseListModel { + pub(crate) fn phrase_index (&self) -> usize { + self.phrase.load(Ordering::Relaxed) + } + pub(crate) fn set_phrase_index (&self, value: usize) { + self.phrase.store(value, Ordering::Relaxed); + } + pub(crate) fn phrases_mode (&self) -> &Option { + &self.mode + } + pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option { + &mut self.mode + } +} + +/// Modes for phrase pool +#[derive(Debug, Clone)] +pub enum PhrasesMode { + /// Renaming a pattern + Rename(usize, String), + /// Editing the length of a pattern + Length(usize, usize, PhraseLengthFocus), + /// Load phrase from disk + Import(usize, FileBrowser), + /// Save phrase to disk + Export(usize, FileBrowser), +} + +pub trait HasPhraseList: HasPhrases { + fn phrases_focused (&self) -> bool; + fn phrases_entered (&self) -> bool; + fn phrases_mode (&self) -> &Option; + fn phrase_index (&self) -> usize; +} + +impl HasPhraseList for SequencerTui { + fn phrases_focused (&self) -> bool { + self.focused() == SequencerFocus::PhraseList + } + fn phrases_entered (&self) -> bool { + self.entered() && self.phrases_focused() + } + fn phrases_mode (&self) -> &Option { + &self.phrases.mode + } + fn phrase_index (&self) -> usize { + self.phrases.phrase.load(Ordering::Relaxed) + } +} + +impl HasPhraseList for ArrangerTui { + fn phrases_focused (&self) -> bool { + self.focused() == ArrangerFocus::Phrases + } + fn phrases_entered (&self) -> bool { + self.entered() && self.phrases_focused() + } + fn phrases_mode (&self) -> &Option { + &self.phrases.mode + } + fn phrase_index (&self) -> usize { + self.phrases.phrase.load(Ordering::Relaxed) + } +} + +pub struct PhraseListView<'a> { + pub(crate) title: &'static str, + pub(crate) focused: bool, + pub(crate) entered: bool, + pub(crate) phrases: &'a Vec>>, + pub(crate) index: usize, + pub(crate) mode: &'a Option +} + +impl<'a, T: HasPhraseList> From<&'a T> for PhraseListView<'a> { + fn from (state: &'a T) -> Self { + Self { + title: "Phrases", + focused: state.phrases_focused(), + entered: state.phrases_entered(), + phrases: state.phrases(), + index: state.phrase_index(), + mode: state.phrases_mode(), + } + } +} + +// TODO: Display phrases always in order of appearance +render!(|self: PhraseListView<'a>|{ + let Self { title, focused, entered, phrases, index, mode } = self; + 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 upper_left = format!("[{}] {title}", if *entered {"■"} else {" "}); + let upper_right = format!("({})", phrases.len()); + lay!([ + Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)) + .wrap(Tui::bg(Color::Rgb(28, 35, 25), Tui::fill_xy(col!(move|add|match mode { + Some(PhrasesMode::Import(_, ref browser)) => { + add(browser) + }, + Some(PhrasesMode::Export(_, ref browser)) => { + add(browser) + }, + _ => { + for (i, phrase) in phrases.iter().enumerate() { + add(&lay!(|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 = Tui::fill_x(Tui::at_e(length)); + let row1 = Tui::fill_x(lay!([Tui::fill_x(Tui::at_w(format!(" {i}"))), length])); + let mut row2 = format!(" {name}"); + if let Some(PhrasesMode::Rename(phrase, _)) = mode { + if *focused && i == *phrase { + row2 = format!("{row2}▄"); + } + }; + let row2 = Tui::bold(true, row2); + add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([row1, row2]))))?; + if *entered && i == *index { + add(&CORNERS)?; + } + Ok(()) + }))?; + } + Ok(()) + } + })))), + Tui::fill_xy(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))), + Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))), + ]) +}); + +#[derive(Clone, PartialEq, Debug)] +pub enum PhrasesCommand { + Select(usize), + Phrase(Pool), + Rename(Rename), + Length(Length), + Import(Browse), + Export(Browse), +} + +impl Command for PhrasesCommand { + fn execute (self, state: &mut PhraseListModel) -> Perhaps { + use PhrasesCommand::*; + Ok(match self { + Phrase(command) => command.execute(state)?.map(Phrase), + Rename(command) => match command { + PhraseRenameCommand::Begin => { + let length = state.phrases()[state.phrase_index()].read().unwrap().length; + *state.phrases_mode_mut() = Some( + PhrasesMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar) + ); + None + }, + _ => command.execute(state)?.map(Rename) + }, + Length(command) => match command { + PhraseLengthCommand::Begin => { + let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone(); + *state.phrases_mode_mut() = Some( + PhrasesMode::Rename(state.phrase_index(), name) + ); + None + }, + _ => command.execute(state)?.map(Length) + }, + Import(command) => match command { + FileBrowserCommand::Begin => { + *state.phrases_mode_mut() = Some( + PhrasesMode::Import(state.phrase_index(), FileBrowser::new(None)?) + ); + None + }, + _ => command.execute(state)?.map(Import) + }, + Export(command) => match command { + FileBrowserCommand::Begin => { + *state.phrases_mode_mut() = Some( + PhrasesMode::Export(state.phrase_index(), FileBrowser::new(None)?) + ); + None + }, + _ => command.execute(state)?.map(Export) + }, + Select(phrase) => { + state.set_phrase_index(phrase); + None + }, + }) + } +} + +impl HasPhrases for PhraseListModel { + fn phrases (&self) -> &Vec>> { + &self.phrases + } + fn phrases_mut (&mut self) -> &mut Vec>> { + &mut self.phrases + } +} + +impl InputToCommand for PhrasesCommand { + fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option { + Some(match state.phrases_mode() { + Some(PhrasesMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?), + Some(PhrasesMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?), + Some(PhrasesMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?), + Some(PhrasesMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?), + _ => to_phrases_command(state, input)? + }) + } +} + +fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option { + use KeyCode::{Up, Down, Delete, Char}; + use PhrasesCommand as Cmd; + let index = state.phrase_index(); + let count = state.phrases().len(); + Some(match input.event() { + key!(Char('n')) => Cmd::Rename(Rename::Begin), + key!(Char('t')) => Cmd::Length(Length::Begin), + key!(Char('m')) => Cmd::Import(Browse::Begin), + key!(Char('x')) => Cmd::Export(Browse::Begin), + key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())), + key!(Up) => Cmd::Select( + index.overflowing_sub(1).0.min(state.phrases().len() - 1) + ), + key!(Down) => Cmd::Select( + index.saturating_add(1) % state.phrases().len() + ), + key!(Char(',')) => if index > 1 { + state.set_phrase_index(state.phrase_index().saturating_sub(1)); + Cmd::Phrase(Pool::Swap(index - 1, index)) + } else { + return None + }, + key!(Char('.')) => if index < count.saturating_sub(1) { + state.set_phrase_index(state.phrase_index() + 1); + Cmd::Phrase(Pool::Swap(index + 1, index)) + } else { + return None + }, + key!(Delete) => if index > 0 { + state.set_phrase_index(index.min(count.saturating_sub(1))); + Cmd::Phrase(Pool::Delete(index)) + } else { + return None + }, + key!(Char('a')) => Cmd::Phrase(Pool::Add(count, Phrase::new( + String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) + ))), + key!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new( + String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) + ))), + key!(Char('d')) => { + let mut phrase = state.phrases()[index].read().unwrap().duplicate(); + phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); + Cmd::Phrase(Pool::Add(index + 1, phrase)) + }, + _ => return None + }) +} diff --git a/crates/tek/src/tui/model_phrase_player.rs b/crates/tek/src/tui/phrase_player.rs similarity index 100% rename from crates/tek/src/tui/model_phrase_player.rs rename to crates/tek/src/tui/phrase_player.rs diff --git a/crates/tek/src/tui/ctrl_phrase_rename.rs b/crates/tek/src/tui/phrase_rename.rs similarity index 100% rename from crates/tek/src/tui/ctrl_phrase_rename.rs rename to crates/tek/src/tui/phrase_rename.rs diff --git a/crates/tek/src/tui/view_phrase_selector.rs b/crates/tek/src/tui/phrase_select.rs similarity index 100% rename from crates/tek/src/tui/view_phrase_selector.rs rename to crates/tek/src/tui/phrase_select.rs diff --git a/crates/tek/src/tui/view_status_bar.rs b/crates/tek/src/tui/status_bar.rs similarity index 100% rename from crates/tek/src/tui/view_status_bar.rs rename to crates/tek/src/tui/status_bar.rs diff --git a/crates/tek/src/tui/view_file_browser.rs b/crates/tek/src/tui/view_file_browser.rs deleted file mode 100644 index 748907b0..00000000 --- a/crates/tek/src/tui/view_file_browser.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::*; - -render!(|self: FileBrowser|{ - Stack::down(|add|{ - let mut i = 0; - for (_, name) in self.dirs.iter() { - if i >= self.scroll { - add(&Tui::bold(i == self.index, name.as_str()))?; - } - i += 1; - } - for (_, name) in self.files.iter() { - if i >= self.scroll { - add(&Tui::bold(i == self.index, name.as_str()))?; - } - i += 1; - } - add(&format!("{}/{i}", self.index))?; - Ok(()) - }) -}); diff --git a/crates/tek/src/tui/view_phrase_length.rs b/crates/tek/src/tui/view_phrase_length.rs deleted file mode 100644 index c20423d3..00000000 --- a/crates/tek/src/tui/view_phrase_length.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::*; - -render!(|self: PhraseLength|{ - let bars = ||self.bars_string(); - let beats = ||self.beats_string(); - let ticks = ||self.ticks_string(); - row!(move|add|match self.focus { - None => - add(&row!([" ", bars(), "B", beats(), "b", ticks(), "T"])), - Some(PhraseLengthFocus::Bar) => - add(&row!(["[", bars(), "]", beats(), "b", ticks(), "T"])), - Some(PhraseLengthFocus::Beat) => - add(&row!([" ", bars(), "[", beats(), "]", ticks(), "T"])), - Some(PhraseLengthFocus::Tick) => - add(&row!([" ", bars(), "B", beats(), "[", ticks(), "]"])), - }) -}); diff --git a/crates/tek/src/tui/view_phrase_list.rs b/crates/tek/src/tui/view_phrase_list.rs deleted file mode 100644 index 02a6c9ad..00000000 --- a/crates/tek/src/tui/view_phrase_list.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::*; - -pub struct PhraseListView<'a> { - pub(crate) title: &'static str, - pub(crate) focused: bool, - pub(crate) entered: bool, - pub(crate) phrases: &'a Vec>>, - pub(crate) index: usize, - pub(crate) mode: &'a Option -} - -impl<'a, T: HasPhraseList> From<&'a T> for PhraseListView<'a> { - fn from (state: &'a T) -> Self { - Self { - title: "Phrases", - focused: state.phrases_focused(), - entered: state.phrases_entered(), - phrases: state.phrases(), - index: state.phrase_index(), - mode: state.phrases_mode(), - } - } -} - -// TODO: Display phrases always in order of appearance -render!(|self: PhraseListView<'a>|{ - let Self { title, focused, entered, phrases, index, mode } = self; - 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 upper_left = format!("[{}] {title}", if *entered {"■"} else {" "}); - let upper_right = format!("({})", phrases.len()); - lay!([ - Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)) - .wrap(Tui::bg(Color::Rgb(28, 35, 25), Tui::fill_xy(col!(move|add|match mode { - Some(PhrasesMode::Import(_, ref browser)) => { - add(browser) - }, - Some(PhrasesMode::Export(_, ref browser)) => { - add(browser) - }, - _ => { - for (i, phrase) in phrases.iter().enumerate() { - add(&lay!(|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 = Tui::fill_x(Tui::at_e(length)); - let row1 = Tui::fill_x(lay!([Tui::fill_x(Tui::at_w(format!(" {i}"))), length])); - let mut row2 = format!(" {name}"); - if let Some(PhrasesMode::Rename(phrase, _)) = mode { - if *focused && i == *phrase { - row2 = format!("{row2}▄"); - } - }; - let row2 = Tui::bold(true, row2); - add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([row1, row2]))))?; - if *entered && i == *index { - add(&CORNERS)?; - } - Ok(()) - }))?; - } - Ok(()) - } - })))), - Tui::fill_xy(Tui::at_nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))), - Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))), - ]) -});