diff --git a/crates/tek_core/src/space.rs b/crates/tek_core/src/space.rs index 21e70a4a..1081d800 100644 --- a/crates/tek_core/src/space.rs +++ b/crates/tek_core/src/space.rs @@ -903,6 +903,16 @@ impl, B: Widget> Widget for Split(PhantomData, AtomicUsize, AtomicUsize); +impl Clone for Measure { + fn clone (&self) -> Self { + Self( + Default::default(), + AtomicUsize::from(self.1.load(Ordering::Relaxed)), + AtomicUsize::from(self.2.load(Ordering::Relaxed)), + ) + } +} + impl std::fmt::Debug for Measure { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Measure") diff --git a/crates/tek_core/src/tui.rs b/crates/tek_core/src/tui.rs index 20e1baaa..ae05b31e 100644 --- a/crates/tek_core/src/tui.rs +++ b/crates/tek_core/src/tui.rs @@ -654,3 +654,37 @@ pub const fn shift (key: KeyEvent) -> KeyEvent { })) } } +#[macro_export] macro_rules! key_lit { + ($code:expr) => { + TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { + code: $code, + modifiers: crossterm::event::KeyModifiers::NONE, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE + })) + }; + (Ctrl-$code:expr) => { + TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { + code: $code, + modifiers: crossterm::event::KeyModifiers::CONTROL, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE + })) + }; + (Alt-$code:expr) => { + TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { + code: $code, + modifiers: crossterm::event::KeyModifiers::ALT, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE + })) + }; + (Shift-$code:expr) => { + TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { + code: $code, + modifiers: crossterm::event::KeyModifiers::SHIFT, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE + })) + } +} diff --git a/crates/tek_tui/src/tui_command.rs b/crates/tek_tui/src/tui_command.rs index 48d8bd91..546f13b4 100644 --- a/crates/tek_tui/src/tui_command.rs +++ b/crates/tek_tui/src/tui_command.rs @@ -117,6 +117,8 @@ pub enum PhrasesCommand { Phrase(PhrasePoolCommand), Rename(PhraseRenameCommand), Length(PhraseLengthCommand), + Import(FileBrowserCommand), + Export(FileBrowserCommand), } impl Command for PhrasesCommand { @@ -124,8 +126,44 @@ impl Command for PhrasesCommand { use PhrasesCommand::*; Ok(match self { Phrase(command) => command.execute(state)?.map(Phrase), - Rename(command) => command.execute(state)?.map(Rename), - Length(command) => command.execute(state)?.map(Length), + 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 + }, + _ => command.execute(state)?.map(Import) + }, + Export(command) => match command { + FileBrowserCommand::Begin => { + *state.phrases_mode_mut() = Some( + PhrasesMode::Export(state.phrase_index(), FileBrowser::new()) + ); + None + }, + _ => command.execute(state)?.map(Export) + }, Select(phrase) => { state.set_phrase_index(phrase); None @@ -137,21 +175,20 @@ impl Command for PhrasesCommand { #[derive(Copy, Clone, Debug, PartialEq)] pub enum PhraseLengthCommand { Begin, + Cancel, + Set(usize), Next, Prev, Inc, Dec, - Set(usize), - Cancel, } impl Command for PhraseLengthCommand { fn execute (self, state: &mut T) -> Perhaps { use PhraseLengthFocus::*; use PhraseLengthCommand::*; - let mut mode = state.phrases_mode_mut().clone(); - if let Some(PhrasesMode::Length(phrase, ref mut length, ref mut focus)) = mode { - match self { + 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() }, @@ -174,15 +211,9 @@ impl Command for PhraseLengthCommand { return Ok(Some(Self::Set(old_length))) }, _ => unreachable!() - } - } else if self == Begin { - let length = state.phrases()[state.phrase_index()].read().unwrap().length; - *state.phrases_mode_mut() = Some( - PhrasesMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar) - ); - } else { - unreachable!() - } + }, + _ => unreachable!() + }; Ok(None) } } @@ -190,20 +221,16 @@ impl Command for PhraseLengthCommand { #[derive(Clone, Debug, PartialEq)] pub enum PhraseRenameCommand { Begin, - Set(String), - Confirm, Cancel, + Confirm, + Set(String), } -impl Command for PhraseRenameCommand -where - T: PhrasesControl -{ +impl Command for PhraseRenameCommand { fn execute (self, state: &mut T) -> Perhaps { use PhraseRenameCommand::*; - let mut mode = state.phrases_mode_mut().clone(); - if let Some(PhrasesMode::Rename(phrase, ref mut old_name)) = mode { - match self { + match state.phrases_mode_mut().clone() { + Some(PhrasesMode::Rename(phrase, ref mut old_name)) => match self { Set(s) => { state.phrases()[phrase].write().unwrap().name = s.into(); return Ok(Some(Self::Set(old_name.clone()))) @@ -217,15 +244,40 @@ where state.phrases()[phrase].write().unwrap().name = old_name.clone(); }, _ => unreachable!() - }; - } else if self == Begin { - let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone(); - *state.phrases_mode_mut() = Some( - PhrasesMode::Rename(state.phrase_index(), name) - ); - } else { - unreachable!() - } + }, + _ => unreachable!() + }; + Ok(None) + } +} + +/// Commands supported by [FileBrowser] +#[derive(Debug, Clone, PartialEq)] +pub enum FileBrowserCommand { + Begin, + Cancel, + Confirm, + Select(usize), + Chdir(PathBuf) +} + +impl Command for FileBrowserCommand { + fn execute (self, state: &mut T) -> Perhaps { + use FileBrowserCommand::*; + match state.phrases_mode_mut().clone() { + Some(PhrasesMode::Import(index, browser)) => { + todo!() + }, + Some(PhrasesMode::Export(index, browser)) => { + todo!() + }, + _ => match self { + Begin => { + todo!() + }, + _ => unreachable!() + } + }; Ok(None) } } diff --git a/crates/tek_tui/src/tui_input.rs b/crates/tek_tui/src/tui_input.rs index 78bca48d..00b5da25 100644 --- a/crates/tek_tui/src/tui_input.rs +++ b/crates/tek_tui/src/tui_input.rs @@ -247,73 +247,79 @@ fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option InputToCommand for PhrasesCommand { fn input_to_command (state: &T, input: &TuiInput) -> Option { - use PhrasePoolCommand as Pool; use PhraseRenameCommand as Rename; use PhraseLengthCommand as Length; - use KeyCode::{Up, Down, Delete, Char}; - let index = state.phrase_index(); - let count = state.phrases().len(); - Some(match input.event() { - key!(Up) => Self::Select( - state.phrase_index().overflowing_sub(1).0.min(state.phrases().len() - 1) - ), - key!(Down) => Self::Select( - state.phrase_index().saturating_add(1) % state.phrases().len() - ), - key!(Char(',')) => if index > 1 { - state.set_phrase_index(state.phrase_index().saturating_sub(1)); - Self::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); - Self::Phrase(Pool::Swap(index + 1, index)) - } else { - return None - }, - key!(Delete) => if index > 0 { - state.set_phrase_index(index.min(count.saturating_sub(1))); - Self::Phrase(Pool::Delete(index)) - } else { - return None - }, - key!(Char('a')) => Self::Phrase(Pool::Add( - count, - Phrase::new( - String::from("(new)"), true, 4 * PPQ, None, Some(ItemColorTriplet::random()) - ) - )), - key!(Char('i')) => Self::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); - Self::Phrase(Pool::Add(index + 1, phrase)) - }, - key!(Char('c')) => Self::Phrase(Pool::SetColor( - index, - ItemColor::random() - )), - key!(Char('n')) => Self::Rename(Rename::Begin), - key!(Char('t')) => Self::Length(Length::Begin), - _ => 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)?) - }, - _ => return None - } + use FileBrowserCommand as Browse; + 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: &T, input: &TuiInput) -> Option { + use KeyCode::{Up, Down, Delete, Char}; + use PhrasesCommand as Cmd; + use PhrasePoolCommand as Pool; + use PhraseRenameCommand as Rename; + use PhraseLengthCommand as Length; + use FileBrowserCommand as Browse; + 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!(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('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())), + 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 + }) +} + +impl InputToCommand for FileBrowserCommand { + fn input_to_command (state: &T, from: &TuiInput) -> Option { + todo!() + } +} + impl InputToCommand for PhraseLengthCommand { fn input_to_command (state: &T, from: &TuiInput) -> Option { use KeyCode::{Up, Down, Right, Left, Enter, Esc}; diff --git a/crates/tek_tui/src/tui_model.rs b/crates/tek_tui/src/tui_model.rs index 41b47178..e530e76a 100644 --- a/crates/tek_tui/src/tui_model.rs +++ b/crates/tek_tui/src/tui_model.rs @@ -155,6 +155,72 @@ impl Default for PhrasesModel { } } +/// 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), +} + +/// Browses for phrase to import/export +#[derive(Debug, Clone)] +pub struct FileBrowser { + pub cwd: PathBuf, + pub dirs: Vec, + pub files: Vec, + pub index: usize, + pub scroll: usize, + pub size: Measure +} + +impl FileBrowser { + pub fn new () -> Self { + todo!() + } +} + +/// 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()) + } +} + impl HasScenes for ArrangerTui { fn scenes (&self) -> &Vec { &self.scenes @@ -267,48 +333,3 @@ impl ArrangerTrackApi for ArrangerTrack { self.color } } - -/// 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), -} - -/// 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()) - } -}