diff --git a/crates/tek_tui/src/lib.rs b/crates/tek_tui/src/lib.rs index 72937bf6..8c317545 100644 --- a/crates/tek_tui/src/lib.rs +++ b/crates/tek_tui/src/lib.rs @@ -9,23 +9,27 @@ pub(crate) use std::path::PathBuf; pub(crate) use std::ffi::OsString; pub(crate) use std::fs::read_dir; -use std::fmt::Debug; - submod! { - tui_apps - tui_command - tui_control - tui_debug tui_focus - tui_handle - tui_init - tui_input - tui_impls - tui_jack tui_menu - tui_select tui_status - tui_theme + + tui_app_arranger + tui_app_sequencer + tui_app_transport + + tui_jack_transport + tui_jack_sequencer + tui_jack_arranger + + tui_control_arranger + tui_control_file_browser + tui_control_phrase_editor + tui_control_phrase_length + tui_control_phrase_list + tui_control_phrase_rename + tui_control_sequencer + tui_control_transport tui_model_arranger tui_model_clock @@ -43,3 +47,196 @@ submod! { tui_view_sequencer tui_view_transport } + +pub fn to_focus_command (input: &TuiInput) -> Option { + use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc}; + Some(match input.event() { + key!(Tab) => FocusCommand::Next, + key!(Shift-Tab) => FocusCommand::Prev, + key!(BackTab) => FocusCommand::Prev, + key!(Shift-BackTab) => FocusCommand::Prev, + key!(Up) => FocusCommand::Up, + key!(Down) => FocusCommand::Down, + key!(Left) => FocusCommand::Left, + key!(Right) => FocusCommand::Right, + key!(Enter) => FocusCommand::Enter, + key!(Esc) => FocusCommand::Exit, + _ => return None + }) +} + +pub struct TuiTheme; + +impl TuiTheme { + pub fn border_bg () -> Color { + Color::Rgb(40, 50, 30) + } + pub fn border_fg (focused: bool) -> Color { + if focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) } + } + pub fn title_fg (focused: bool) -> Color { + if focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) } + } + pub fn separator_fg (_: bool) -> Color { + Color::Rgb(0, 0, 0) + } + pub const fn hotkey_fg () -> Color { + Color::Rgb(255, 255, 0) + } + pub fn mode_bg () -> Color { + Color::Rgb(150, 160, 90) + } + pub fn mode_fg () -> Color { + Color::Rgb(255, 255, 255) + } + pub fn status_bar_bg () -> Color { + Color::Rgb(28, 35, 25) + } +} + +macro_rules! impl_clock_api { + ($Struct:ident $(:: $field:ident)*) => { + impl ClockApi for $Struct { + fn quant (&self) -> &Arc { + &self$(.$field)*.quant + } + fn sync (&self) -> &Arc { + &self$(.$field)*.sync + } + fn current (&self) -> &Arc { + &self$(.$field)*.current + } + fn transport_handle (&self) -> &Arc { + &self$(.$field)*.transport + } + fn transport_state (&self) -> &Arc>> { + &self$(.$field)*.playing + } + fn transport_offset (&self) -> &Arc>> { + &self$(.$field)*.started + } + } + } +} +macro_rules! impl_midi_player { + ($Struct:ident $(:: $field:ident)*) => { + impl HasPhrase for $Struct { + fn reset (&self) -> bool { + self$(.$field)*.reset + } + fn reset_mut (&mut self) -> &mut bool { + &mut self$(.$field)*.reset + } + fn play_phrase (&self) -> &Option<(Instant, Option>>)> { + &self$(.$field)*.play_phrase + } + fn play_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { + &mut self$(.$field)*.play_phrase + } + fn next_phrase (&self) -> &Option<(Instant, Option>>)> { + &self$(.$field)*.next_phrase + } + fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { + &mut self$(.$field)*.next_phrase + } + } + impl MidiInputApi for $Struct { + fn midi_ins (&self) -> &Vec> { + &self$(.$field)*.midi_ins + } + fn midi_ins_mut (&mut self) -> &mut Vec> { + &mut self$(.$field)*.midi_ins + } + fn recording (&self) -> bool { + self$(.$field)*.recording + } + fn recording_mut (&mut self) -> &mut bool { + &mut self$(.$field)*.recording + } + fn monitoring (&self) -> bool { + self$(.$field)*.monitoring + } + fn monitoring_mut (&mut self) -> &mut bool { + &mut self$(.$field)*.monitoring + } + fn overdub (&self) -> bool { + self$(.$field)*.overdub + } + fn overdub_mut (&mut self) -> &mut bool { + &mut self$(.$field)*.overdub + } + fn notes_in (&self) -> &Arc> { + &self$(.$field)*.notes_in + } + } + impl MidiOutputApi for $Struct { + fn midi_outs (&self) -> &Vec> { + &self$(.$field)*.midi_outs + } + fn midi_outs_mut (&mut self) -> &mut Vec> { + &mut self$(.$field)*.midi_outs + } + fn midi_note (&mut self) -> &mut Vec { + &mut self$(.$field)*.note_buf + } + fn notes_out (&self) -> &Arc> { + &self$(.$field)*.notes_in + } + } + impl MidiPlayerApi for $Struct {} + } +} + +impl_clock_api!(TransportTui::clock); +impl_clock_api!(SequencerTui::clock); +impl_clock_api!(ArrangerTui::clock); +impl_clock_api!(PhrasePlayerModel::clock); +impl_clock_api!(ArrangerTrack::player::clock); + +impl_midi_player!(SequencerTui::player); +impl_midi_player!(ArrangerTrack::player); +impl_midi_player!(PhrasePlayerModel); + +use std::fmt::{Debug, Formatter, Error}; +type DebugResult = std::result::Result<(), Error>; + +impl Debug for ClockModel { + fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult { + f.debug_struct("editor") + .field("playing", &self.playing) + .field("started", &self.started) + .field("current", &self.current) + .field("quant", &self.quant) + .field("sync", &self.sync) + .finish() + } +} + +impl Debug for TransportTui { + fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult { + f.debug_struct("Measure") + .field("jack", &self.jack) + .field("size", &self.size) + .field("cursor", &self.cursor) + .finish() + } +} + +impl Debug for PhraseEditorModel { + fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult { + f.debug_struct("editor") + .field("note_axis", &self.time_axis) + .field("time_axis", &self.note_axis) + .finish() + } +} + +impl Debug for PhrasePlayerModel { + fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult { + f.debug_struct("editor") + .field("clock", &self.clock) + .field("play_phrase", &self.play_phrase) + .field("next_phrase", &self.next_phrase) + .finish() + } +} diff --git a/crates/tek_tui/src/tui_app_arranger.rs b/crates/tek_tui/src/tui_app_arranger.rs new file mode 100644 index 00000000..56081273 --- /dev/null +++ b/crates/tek_tui/src/tui_app_arranger.rs @@ -0,0 +1,68 @@ +use crate::*; + +/// Root view for standalone `tek_arranger` +pub struct ArrangerTui { + pub jack: Arc>, + pub clock: ClockModel, + pub phrases: PhrasesModel, + pub tracks: Vec, + pub scenes: Vec, + pub name: Arc>, + pub splits: [u16;2], + pub selected: ArrangerSelection, + pub mode: ArrangerMode, + pub color: ItemColor, + pub entered: bool, + pub size: Measure, + pub cursor: (usize, usize), + pub menu_bar: Option>, + pub status_bar: Option, + pub history: Vec, + pub note_buf: Vec, + pub midi_buf: Vec>>, + pub editor: PhraseEditorModel, + pub focus: FocusState>, + pub perf: PerfModel, +} + +impl TryFrom<&Arc>> for ArrangerTui { + type Error = Box; + fn try_from (jack: &Arc>) -> Usually { + Ok(Self { + jack: jack.clone(), + clock: ClockModel::from(&Arc::new(jack.read().unwrap().transport())), + phrases: PhrasesModel::default(), + editor: PhraseEditorModel::default(), + selected: ArrangerSelection::Clip(0, 0), + scenes: vec![], + tracks: vec![], + color: Color::Rgb(28, 35, 25).into(), + history: vec![], + mode: ArrangerMode::Vertical(2), + name: Arc::new(RwLock::new(String::new())), + size: Measure::new(), + cursor: (0, 0), + splits: [20, 20], + entered: false, + menu_bar: None, + status_bar: None, + midi_buf: vec![vec![];65536], + note_buf: vec![], + focus: FocusState::Entered(AppFocus::Content(ArrangerFocus::Transport(TransportFocus::Bpm))), + perf: PerfModel::default(), + }) + } +} + +/// Sections in the arranger app that may be focused +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum ArrangerFocus { + /// The transport (toolbar) is focused + Transport(TransportFocus), + /// The arrangement (grid) is focused + Arranger, + /// The phrase list (pool) is focused + Phrases, + /// The phrase editor (sequencer) is focused + PhraseEditor, +} diff --git a/crates/tek_tui/src/tui_app_sequencer.rs b/crates/tek_tui/src/tui_app_sequencer.rs new file mode 100644 index 00000000..d33ad9f6 --- /dev/null +++ b/crates/tek_tui/src/tui_app_sequencer.rs @@ -0,0 +1,51 @@ +use crate::*; + +/// Root view for standalone `tek_sequencer`. +pub struct SequencerTui { + pub jack: Arc>, + pub clock: ClockModel, + pub phrases: PhrasesModel, + pub player: PhrasePlayerModel, + pub editor: PhraseEditorModel, + pub size: Measure, + pub cursor: (usize, usize), + pub split: u16, + pub entered: bool, + pub note_buf: Vec, + pub midi_buf: Vec>>, + pub focus: FocusState>, + pub perf: PerfModel, +} + +impl TryFrom<&Arc>> for SequencerTui { + type Error = Box; + fn try_from (jack: &Arc>) -> Usually { + let clock = ClockModel::from(&Arc::new(jack.read().unwrap().transport())); + Ok(Self { + jack: jack.clone(), + phrases: PhrasesModel::default(), + player: PhrasePlayerModel::from(&clock), + editor: PhraseEditorModel::default(), + size: Measure::new(), + cursor: (0, 0), + entered: false, + split: 20, + midi_buf: vec![vec![];65536], + note_buf: vec![], + clock, + focus: FocusState::Entered(AppFocus::Content(SequencerFocus::Transport(TransportFocus::Bpm))), + perf: PerfModel::default(), + }) + } +} + +/// Sections in the sequencer app that may be focused +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum SequencerFocus { + /// The transport (toolbar) is focused + Transport(TransportFocus), + /// The phrase list (pool) is focused + Phrases, + /// The phrase editor (sequencer) is focused + PhraseEditor, +} diff --git a/crates/tek_tui/src/tui_app_transport.rs b/crates/tek_tui/src/tui_app_transport.rs new file mode 100644 index 00000000..1bf1ef18 --- /dev/null +++ b/crates/tek_tui/src/tui_app_transport.rs @@ -0,0 +1,56 @@ +use crate::*; + +/// Stores and displays time-related info. +pub struct TransportTui { + pub jack: Arc>, + pub clock: ClockModel, + pub size: Measure, + pub cursor: (usize, usize), + pub focus: FocusState>, +} + +/// Create app state from JACK handle. +impl TryFrom<&Arc>> for TransportTui { + type Error = Box; + fn try_from (jack: &Arc>) -> Usually { + Ok(Self { + jack: jack.clone(), + clock: ClockModel::from(&Arc::new(jack.read().unwrap().transport())), + size: Measure::new(), + cursor: (0, 0), + focus: FocusState::Entered(AppFocus::Content(TransportFocus::Bpm)) + }) + } +} + +/// Which item of the transport toolbar is focused +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TransportFocus { + Bpm, + Sync, + PlayPause, + Clock, + Quant, +} + +impl FocusWrap for TransportFocus { + fn wrap <'a, W: Widget> (self, focus: TransportFocus, content: &'a W) + -> impl Widget + 'a + { + let focused = focus == self; + let corners = focused.then_some(CORNERS); + let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50))); + lay!(corners, highlight, *content) + } +} + +impl FocusWrap for Option { + fn wrap <'a, W: Widget> (self, focus: TransportFocus, content: &'a W) + -> impl Widget + 'a + { + let focused = Some(focus) == self; + let corners = focused.then_some(CORNERS); + let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50))); + lay!(corners, highlight, *content) + } +} diff --git a/crates/tek_tui/src/tui_apps.rs b/crates/tek_tui/src/tui_apps.rs deleted file mode 100644 index 5bd4a9f6..00000000 --- a/crates/tek_tui/src/tui_apps.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::*; - -/// Stores and displays time-related info. -pub struct TransportTui { - pub jack: Arc>, - pub clock: ClockModel, - pub size: Measure, - pub cursor: (usize, usize), - pub focus: FocusState>, -} - -/// Root view for standalone `tek_sequencer`. -pub struct SequencerTui { - pub jack: Arc>, - pub clock: ClockModel, - pub phrases: PhrasesModel, - pub player: PhrasePlayerModel, - pub editor: PhraseEditorModel, - pub size: Measure, - pub cursor: (usize, usize), - pub split: u16, - pub entered: bool, - pub note_buf: Vec, - pub midi_buf: Vec>>, - pub focus: FocusState>, - pub perf: PerfModel, -} - -/// Root view for standalone `tek_arranger` -pub struct ArrangerTui { - pub jack: Arc>, - pub clock: ClockModel, - pub phrases: PhrasesModel, - pub tracks: Vec, - pub scenes: Vec, - pub name: Arc>, - pub splits: [u16;2], - pub selected: ArrangerSelection, - pub mode: ArrangerMode, - pub color: ItemColor, - pub entered: bool, - pub size: Measure, - pub cursor: (usize, usize), - pub menu_bar: Option>, - pub status_bar: Option, - pub history: Vec, - pub note_buf: Vec, - pub midi_buf: Vec>>, - pub editor: PhraseEditorModel, - pub focus: FocusState>, - pub perf: PerfModel, -} diff --git a/crates/tek_tui/src/tui_command.rs b/crates/tek_tui/src/tui_command.rs deleted file mode 100644 index 4e4f8250..00000000 --- a/crates/tek_tui/src/tui_command.rs +++ /dev/null @@ -1,370 +0,0 @@ -use crate::*; - -#[derive(Clone, Debug, PartialEq)] -pub enum TransportCommand { - Focus(FocusCommand), - Clock(ClockCommand), -} - -impl Command for TransportCommand { - fn execute (self, state: &mut T) -> Perhaps { - use TransportCommand::{Focus, Clock}; - use FocusCommand::{Next, Prev}; - use ClockCommand::{SetBpm, SetQuant, SetSync}; - Ok(match self { - Focus(cmd) => cmd.execute(state)?.map(Focus), - Clock(SetBpm(bpm)) => Some(Clock(SetBpm(state.bpm().set(bpm)))), - Clock(SetQuant(quant)) => Some(Clock(SetQuant(state.quant().set(quant)))), - Clock(SetSync(sync)) => Some(Clock(SetSync(state.sync().set(sync)))), - _ => return Ok(None) - }) - } -} - -#[derive(Clone, Debug)] -pub enum SequencerCommand { - Focus(FocusCommand), - Undo, - Redo, - Clear, - Clock(ClockCommand), - Phrases(PhrasesCommand), - Editor(PhraseCommand), -} - -impl Command for SequencerCommand { - fn execute (self, state: &mut SequencerTui) -> Perhaps { - use SequencerCommand::*; - Ok(match self { - Focus(cmd) => cmd.execute(state)?.map(Focus), - Phrases(cmd) => cmd.execute(state)?.map(Phrases), - Editor(cmd) => cmd.execute(state)?.map(Editor), - Clock(cmd) => cmd.execute(state)?.map(Clock), - Undo => { todo!() }, - Redo => { todo!() }, - Clear => { todo!() }, - }) - } -} - -#[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(state)?.map(Phrases), - Editor(cmd) => cmd.execute(state)?.map(Editor), - Clock(cmd) => cmd.execute(state)?.map(Clock), - Zoom(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) - } -} - -#[derive(Clone, PartialEq, Debug)] -pub enum PhrasesCommand { - Select(usize), - Phrase(PhrasePoolCommand), - Rename(PhraseRenameCommand), - Length(PhraseLengthCommand), - Import(FileBrowserCommand), - Export(FileBrowserCommand), -} - -impl Command for PhrasesCommand { - fn execute (self, state: &mut T) -> 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 - }, - }) - } -} - -#[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 T) -> Perhaps { - use PhraseLengthFocus::*; - use PhraseLengthCommand::*; - 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) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum PhraseRenameCommand { - Begin, - Cancel, - Confirm, - Set(String), -} - -impl Command for PhraseRenameCommand { - fn execute (self, state: &mut T) -> Perhaps { - use PhraseRenameCommand::*; - 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()))) - }, - Confirm => { - let old_name = old_name.clone(); - *state.phrases_mode_mut() = None; - return Ok(Some(Self::Set(old_name))) - }, - Cancel => { - state.phrases()[phrase].write().unwrap().name = old_name.clone(); - }, - _ => unreachable!() - }, - _ => unreachable!() - }; - Ok(None) - } -} - -/// Commands supported by [FileBrowser] -#[derive(Debug, Clone, PartialEq)] -pub enum FileBrowserCommand { - Begin, - Cancel, - Confirm, - Select(usize), - Chdir(PathBuf), - Filter(String), -} - -impl Command for FileBrowserCommand { - fn execute (self, state: &mut T) -> Perhaps { - use FileBrowserCommand::*; - use PhrasesMode::{Import, Export}; - let mode = state.phrases_mode_mut(); - match mode { - Some(Import(index, ref mut browser)) => match self { - Cancel => { - *mode = None; - }, - Chdir(cwd) => { - *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); - }, - Select(index) => { - browser.index = index; - }, - Confirm => { - if browser.is_file() { - let index = *index; - let path = browser.path(); - *mode = None; - PhrasePoolCommand::Import(index, path).execute(state)?; - } else if browser.is_dir() { - *mode = Some(Import(*index, browser.chdir()?)); - } - }, - _ => todo!(), - _ => unreachable!() - }, - Some(PhrasesMode::Export(index, ref mut browser)) => match self { - Cancel => { - *mode = None; - }, - Chdir(cwd) => { - *mode = Some(PhrasesMode::Export(*index, FileBrowser::new(Some(cwd))?)); - }, - Select(index) => { - browser.index = index; - }, - _ => unreachable!() - }, - _ => unreachable!(), - }; - Ok(None) - } -} - -#[derive(Clone, Debug)] -pub enum PhraseCommand { - // TODO: 1-9 seek markers that by default start every 8th of the phrase - ToggleDirection, - EnterEditMode, - ExitEditMode, - NoteAppend, - NoteSet, - NoteCursorSet(Option), - NoteLengthSet(usize), - NoteScrollSet(usize), - TimeCursorSet(Option), - TimeScrollSet(usize), - TimeZoomSet(usize), - Show(Option>>), -} - -impl Command for PhraseCommand { - fn execute (self, state: &mut T) -> Perhaps { - use PhraseCommand::*; - Ok(match self { - Show(phrase) => { - state.edit_phrase(phrase); - None - }, - ToggleDirection => { todo!() }, - EnterEditMode => { - state.focus_enter(); - None - }, - ExitEditMode => { - state.focus_exit(); - None - }, - NoteAppend => { - if state.phrase_editor_entered() { - state.put_note(); - state.time_cursor_advance(); - } - None - }, - NoteSet => { if state.phrase_editor_entered() { state.put_note(); } None }, - TimeCursorSet(time) => { state.time_axis().write().unwrap().point_set(time); None }, - TimeScrollSet(time) => { state.time_axis().write().unwrap().start_set(time); None }, - TimeZoomSet(zoom) => { state.time_axis().write().unwrap().scale_set(zoom); None }, - NoteScrollSet(note) => { state.note_axis().write().unwrap().start_set(note); None }, - NoteLengthSet(time) => { *state.note_len_mut() = time; None }, - NoteCursorSet(note) => { - let mut axis = state.note_axis().write().unwrap(); - axis.point_set(note); - if let Some(point) = axis.point { - if point > 73 { - axis.point = Some(73); - } - if point < axis.start { - axis.start = (point / 2) * 2; - } - } - None - }, - _ => unreachable!() - }) - } -} diff --git a/crates/tek_tui/src/tui_control.rs b/crates/tek_tui/src/tui_control.rs deleted file mode 100644 index 519e9278..00000000 --- a/crates/tek_tui/src/tui_control.rs +++ /dev/null @@ -1,131 +0,0 @@ -use crate::*; - -pub trait TransportControl: ClockApi + FocusGrid + HasEnter { - fn transport_focused (&self) -> Option; -} - -pub trait SequencerControl: TransportControl {} - -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 TransportControl for TransportTui { - fn transport_focused (&self) -> Option { - if let AppFocus::Content(focus) = self.focus.inner() { - Some(focus) - } else { - None - } - } -} - -impl TransportControl for SequencerTui { - fn transport_focused (&self) -> Option { - if let AppFocus::Content(SequencerFocus::Transport(focus)) = self.focus.inner() { - Some(focus) - } else { - None - } - } -} - -impl TransportControl for ArrangerTui { - fn transport_focused (&self) -> Option { - if let AppFocus::Content(ArrangerFocus::Transport(focus)) = self.focus.inner() { - Some(focus) - } else { - None - } - } -} - -impl SequencerControl for SequencerTui {} - -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.enqueue_next(phrase.as_ref()); - } - } - if self.is_stopped() { - self.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].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(); - } - } - } - } -} - -pub trait PhrasesControl: HasPhrases { - fn phrase_index (&self) -> usize; - fn set_phrase_index (&self, index: usize); - fn phrases_mode (&self) -> &Option; - fn phrases_mode_mut (&mut self) -> &mut Option; - fn index_of (&self, phrase: &Phrase) -> Option { - for i in 0..self.phrases().len() { - if *self.phrases()[i].read().unwrap() == *phrase { return Some(i) } - } - return None - } -} - -pub trait PhraseEditorControl: HasFocus { - fn edit_phrase (&mut self, phrase: Option>>); - fn phrase_to_edit (&self) -> Option>>; - fn phrase_editing (&self) -> &Option>>; - fn phrase_editor_entered (&self) -> bool; - fn time_axis (&self) -> &RwLock>; - fn note_axis (&self) -> &RwLock>; - fn note_len (&self) -> usize; - fn note_len_mut (&mut self) -> &mut usize; - fn put_note (&mut self); - fn time_cursor_advance (&self) { - let point = self.time_axis().read().unwrap().point; - let length = self.phrase_editing().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - let forward = |time|(time + self.note_len()) % length; - self.time_axis().write().unwrap().point = point.map(forward); - } -} diff --git a/crates/tek_tui/src/tui_control_arranger.rs b/crates/tek_tui/src/tui_control_arranger.rs new file mode 100644 index 00000000..13e87da3 --- /dev/null +++ b/crates/tek_tui/src/tui_control_arranger.rs @@ -0,0 +1,260 @@ +use crate::*; + +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(state)?.map(Phrases), + Editor(cmd) => cmd.execute(state)?.map(Editor), + Clock(cmd) => cmd.execute(state)?.map(Clock), + Zoom(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.enqueue_next(phrase.as_ref()); + } + } + if self.is_stopped() { + self.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].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(state.phrase_to_edit().clone())), + _ => match state.focused() { + AppFocus::Menu => { todo!() }, + AppFocus::Content(focused) => match focused { + ArrangerFocus::Transport(_) => { + use TransportCommand::{Clock, Focus}; + match TransportCommand::input_to_command(state, input)? { + Clock(_) => { todo!() }, + Focus(command) => Cmd::Focus(command) + } + }, + ArrangerFocus::PhraseEditor => { + Cmd::Editor(PhraseCommand::input_to_command(state, input)?) + }, + ArrangerFocus::Phrases => { + Cmd::Phrases(PhrasesCommand::input_to_command(state, 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_tui/src/tui_control_file_browser.rs b/crates/tek_tui/src/tui_control_file_browser.rs new file mode 100644 index 00000000..c5a96235 --- /dev/null +++ b/crates/tek_tui/src/tui_control_file_browser.rs @@ -0,0 +1,116 @@ +use crate::*; + +/// Commands supported by [FileBrowser] +#[derive(Debug, Clone, PartialEq)] +pub enum FileBrowserCommand { + Begin, + Cancel, + Confirm, + Select(usize), + Chdir(PathBuf), + Filter(String), +} + +impl Command for FileBrowserCommand { + fn execute (self, state: &mut T) -> Perhaps { + use FileBrowserCommand::*; + use PhrasesMode::{Import, Export}; + let mode = state.phrases_mode_mut(); + match mode { + Some(Import(index, ref mut browser)) => match self { + Cancel => { + *mode = None; + }, + Chdir(cwd) => { + *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); + }, + Select(index) => { + browser.index = index; + }, + Confirm => { + if browser.is_file() { + let index = *index; + let path = browser.path(); + *mode = None; + PhrasePoolCommand::Import(index, path).execute(state)?; + } else if browser.is_dir() { + *mode = Some(Import(*index, browser.chdir()?)); + } + }, + _ => todo!(), + _ => unreachable!() + }, + Some(PhrasesMode::Export(index, ref mut browser)) => match self { + Cancel => { + *mode = None; + }, + Chdir(cwd) => { + *mode = Some(PhrasesMode::Export(*index, FileBrowser::new(Some(cwd))?)); + }, + Select(index) => { + browser.index = index; + }, + _ => unreachable!() + }, + _ => unreachable!(), + }; + Ok(None) + } +} + +impl InputToCommand for FileBrowserCommand { + fn input_to_command (state: &T, from: &TuiInput) -> Option { + use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace}; + use FileBrowserCommand::*; + if let Some(PhrasesMode::Import(index, browser)) = state.phrases_mode() { + Some(match from.event() { + key!(Up) => Select( + browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1)) + ), + key!(Down) => Select( + browser.index.saturating_add(1) % browser.len() + ), + key!(Right) => Chdir(browser.cwd.clone()), + key!(Left) => Chdir(browser.cwd.clone()), + key!(Enter) => Confirm, + key!(Char(c)) => { todo!() }, + key!(Backspace) => { todo!() }, + key!(Esc) => Self::Cancel, + _ => return None + }) + } else if let Some(PhrasesMode::Export(index, browser)) = state.phrases_mode() { + Some(match from.event() { + key!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())), + key!(Down) => Select(browser.index.saturating_add(1) % browser.len()), + key!(Right) => Chdir(browser.cwd.clone()), + key!(Left) => Chdir(browser.cwd.clone()), + key!(Enter) => Confirm, + key!(Char(c)) => { todo!() }, + key!(Backspace) => { todo!() }, + key!(Esc) => Self::Cancel, + _ => return None + }) + } else { + unreachable!() + } + } +} + +impl InputToCommand for PhraseLengthCommand { + fn input_to_command (state: &T, from: &TuiInput) -> Option { + use KeyCode::{Up, Down, Right, Left, Enter, Esc}; + if let Some(PhrasesMode::Length(_, length, _)) = state.phrases_mode() { + Some(match from.event() { + key!(Up) => Self::Inc, + key!(Down) => Self::Dec, + key!(Right) => Self::Next, + key!(Left) => Self::Prev, + key!(Enter) => Self::Set(*length), + key!(Esc) => Self::Cancel, + _ => return None + }) + } else { + unreachable!() + } + } +} diff --git a/crates/tek_tui/src/tui_control_phrase_editor.rs b/crates/tek_tui/src/tui_control_phrase_editor.rs new file mode 100644 index 00000000..d98857fe --- /dev/null +++ b/crates/tek_tui/src/tui_control_phrase_editor.rs @@ -0,0 +1,183 @@ +use crate::*; + +#[derive(Clone, Debug)] +pub enum PhraseCommand { + // TODO: 1-9 seek markers that by default start every 8th of the phrase + ToggleDirection, + EnterEditMode, + ExitEditMode, + NoteAppend, + NoteSet, + NoteCursorSet(Option), + NoteLengthSet(usize), + NoteScrollSet(usize), + TimeCursorSet(Option), + TimeScrollSet(usize), + TimeZoomSet(usize), + Show(Option>>), +} + +impl InputToCommand for PhraseCommand { + fn input_to_command (state: &T, from: &TuiInput) -> Option { + use PhraseCommand::*; + use KeyCode::{Char, Enter, Esc, Up, Down, PageUp, PageDown, Left, Right}; + Some(match from.event() { + key!(Char('`')) => ToggleDirection, + key!(Enter) => EnterEditMode, + key!(Esc) => ExitEditMode, + key!(Char('a')) => NoteAppend, + key!(Char('s')) => NoteSet, + key!(Char('[')) => NoteLengthSet(prev_note_length(state.note_len())), + key!(Char(']')) => NoteLengthSet(next_note_length(state.note_len())), + key!(Char('-')) => TimeZoomSet(next_note_length(state.time_axis().read().unwrap().scale)), + key!(Char('_')) => TimeZoomSet(next_note_length(state.time_axis().read().unwrap().scale)), + key!(Char('=')) => TimeZoomSet(prev_note_length(state.time_axis().read().unwrap().scale)), + key!(Char('+')) => TimeZoomSet(prev_note_length(state.time_axis().read().unwrap().scale)), + key!(Up) => match state.phrase_editor_entered() { + true => NoteCursorSet(state.note_axis().write().unwrap().point_plus(1)), + false => NoteScrollSet(state.note_axis().write().unwrap().start_plus(1)), + }, + key!(Down) => match state.phrase_editor_entered() { + true => NoteCursorSet(state.note_axis().write().unwrap().point_minus(1)), + false => NoteScrollSet(state.note_axis().write().unwrap().start_minus(1)), + }, + key!(PageUp) => match state.phrase_editor_entered() { + true => NoteCursorSet(state.note_axis().write().unwrap().point_plus(3)), + false => NoteScrollSet(state.note_axis().write().unwrap().start_plus(3)), + }, + key!(PageDown) => match state.phrase_editor_entered() { + true => NoteCursorSet(state.note_axis().write().unwrap().point_minus(3)), + false => NoteScrollSet(state.note_axis().write().unwrap().start_minus(3)), + }, + key!(Left) => match state.phrase_editor_entered() { + true => TimeCursorSet(state.note_axis().write().unwrap().point_minus(1)), + false => TimeScrollSet(state.note_axis().write().unwrap().start_minus(1)), + }, + key!(Right) => match state.phrase_editor_entered() { + true => TimeCursorSet(state.note_axis().write().unwrap().point_plus(1)), + false => TimeScrollSet(state.note_axis().write().unwrap().start_plus(1)), + }, + _ => return None + }) + } +} + +impl Command for PhraseCommand { + fn execute (self, state: &mut T) -> Perhaps { + use PhraseCommand::*; + Ok(match self { + Show(phrase) => { + state.edit_phrase(phrase); + None + }, + ToggleDirection => { todo!() }, + EnterEditMode => { + state.focus_enter(); + None + }, + ExitEditMode => { + state.focus_exit(); + None + }, + NoteAppend => { + if state.phrase_editor_entered() { + state.put_note(); + state.time_cursor_advance(); + } + None + }, + NoteSet => { if state.phrase_editor_entered() { state.put_note(); } None }, + TimeCursorSet(time) => { state.time_axis().write().unwrap().point_set(time); None }, + TimeScrollSet(time) => { state.time_axis().write().unwrap().start_set(time); None }, + TimeZoomSet(zoom) => { state.time_axis().write().unwrap().scale_set(zoom); None }, + NoteScrollSet(note) => { state.note_axis().write().unwrap().start_set(note); None }, + NoteLengthSet(time) => { *state.note_len_mut() = time; None }, + NoteCursorSet(note) => { + let mut axis = state.note_axis().write().unwrap(); + axis.point_set(note); + if let Some(point) = axis.point { + if point > 73 { + axis.point = Some(73); + } + if point < axis.start { + axis.start = (point / 2) * 2; + } + } + None + }, + _ => unreachable!() + }) + } +} + +pub trait PhraseEditorControl: HasFocus { + fn edit_phrase (&mut self, phrase: Option>>); + fn phrase_to_edit (&self) -> Option>>; + fn phrase_editing (&self) -> &Option>>; + fn phrase_editor_entered (&self) -> bool; + fn time_axis (&self) -> &RwLock>; + fn note_axis (&self) -> &RwLock>; + fn note_len (&self) -> usize; + fn note_len_mut (&mut self) -> &mut usize; + fn put_note (&mut self); + fn time_cursor_advance (&self) { + let point = self.time_axis().read().unwrap().point; + let length = self.phrase_editing().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + let forward = |time|(time + self.note_len()) % length; + self.time_axis().write().unwrap().point = point.map(forward); + } +} + +macro_rules! impl_phrase_editor_control { + ( + $Struct:ident $(:: $field:ident)* + [$Focus:expr] + [$self1:ident: $phrase_to_edit:expr] + [$self2:ident, $phrase:ident: $edit_phrase:expr] + ) => { + impl PhraseEditorControl for $Struct { + fn phrase_to_edit (&$self1) -> Option>> { + $phrase_to_edit + } + fn edit_phrase (&mut $self2, $phrase: Option>>) { + $edit_phrase + //self.editor.show(self.selected_phrase().as_ref()); + //state.editor.phrase = phrase.clone(); + //state.focus(ArrangerFocus::PhraseEditor); + //state.focus_enter(); + //todo!("edit_phrase") + } + fn phrase_editing (&self) -> &Option>> { + todo!("phrase_editing") + } + fn phrase_editor_entered (&self) -> bool { + self.entered && self.focused() == $Focus + } + fn time_axis (&self) -> &RwLock> { + &self.editor.time_axis + } + fn note_axis (&self) -> &RwLock> { + &self.editor.note_axis + } + fn note_len (&self) -> usize { + self.editor.note_len + } + fn note_len_mut (&mut self) -> &mut usize { + &mut self.editor.note_len + } + fn put_note (&mut self) { + todo!("put_note") + } + } + } +} +impl_phrase_editor_control!(SequencerTui + [AppFocus::Content(SequencerFocus::PhraseEditor)] + [self: Some(self.phrases.phrases[self.phrases.phrase.load(Ordering::Relaxed)].clone())] + [self, phrase: self.editor.show(phrase)] +); +impl_phrase_editor_control!(ArrangerTui + [AppFocus::Content(ArrangerFocus::PhraseEditor)] + [self: todo!()] + [self, phrase: todo!()] +); diff --git a/crates/tek_tui/src/tui_control_phrase_length.rs b/crates/tek_tui/src/tui_control_phrase_length.rs new file mode 100644 index 00000000..0cbcd9e5 --- /dev/null +++ b/crates/tek_tui/src/tui_control_phrase_length.rs @@ -0,0 +1,47 @@ +use crate::*; + +#[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 T) -> Perhaps { + use PhraseLengthFocus::*; + use PhraseLengthCommand::*; + 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_tui/src/tui_control_phrase_list.rs b/crates/tek_tui/src/tui_control_phrase_list.rs new file mode 100644 index 00000000..5ede5e02 --- /dev/null +++ b/crates/tek_tui/src/tui_control_phrase_list.rs @@ -0,0 +1,181 @@ +use crate::*; + +#[derive(Clone, PartialEq, Debug)] +pub enum PhrasesCommand { + Select(usize), + Phrase(PhrasePoolCommand), + Rename(PhraseRenameCommand), + Length(PhraseLengthCommand), + Import(FileBrowserCommand), + Export(FileBrowserCommand), +} + +impl Command for PhrasesCommand { + fn execute (self, state: &mut T) -> 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 + }, + }) + } +} + +pub trait PhrasesControl: HasPhrases { + fn phrase_index (&self) -> usize; + fn set_phrase_index (&self, index: usize); + fn phrases_mode (&self) -> &Option; + fn phrases_mode_mut (&mut self) -> &mut Option; + fn index_of (&self, phrase: &Phrase) -> Option { + for i in 0..self.phrases().len() { + if *self.phrases()[i].read().unwrap() == *phrase { return Some(i) } + } + return None + } +} +macro_rules! impl_phrases_control { + ($Struct:ident $(:: $field:ident)*) => { + impl PhrasesControl for $Struct { + fn phrase_index (&self) -> usize { + self.phrases.phrase.load(Ordering::Relaxed) + } + fn set_phrase_index (&self, value: usize) { + self.phrases.phrase.store(value, Ordering::Relaxed); + } + fn phrases_mode (&self) -> &Option { + &self.phrases.mode + } + fn phrases_mode_mut (&mut self) -> &mut Option { + &mut self.phrases.mode + } + } + } +} + +impl_phrases_control!(SequencerTui); +impl_phrases_control!(ArrangerTui); + +macro_rules! impl_has_phrases { + ($Struct:ident $(:: $field:ident)*) => { + impl HasPhrases for $Struct { + fn phrases (&self) -> &Vec>> { + &self$(.$field)*.phrases + } + fn phrases_mut (&mut self) -> &mut Vec>> { + &mut self$(.$field)*.phrases + } + } + } +} +impl_has_phrases!(PhrasesModel); +impl_has_phrases!(SequencerTui::phrases); +impl_has_phrases!(ArrangerTui::phrases); + +impl InputToCommand for PhrasesCommand { + fn input_to_command (state: &T, input: &TuiInput) -> Option { + use PhraseRenameCommand as Rename; + use PhraseLengthCommand as Length; + 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 + }) +} diff --git a/crates/tek_tui/src/tui_control_phrase_rename.rs b/crates/tek_tui/src/tui_control_phrase_rename.rs new file mode 100644 index 00000000..dcd509fd --- /dev/null +++ b/crates/tek_tui/src/tui_control_phrase_rename.rs @@ -0,0 +1,60 @@ +use crate::*; + +#[derive(Clone, Debug, PartialEq)] +pub enum PhraseRenameCommand { + Begin, + Cancel, + Confirm, + Set(String), +} + +impl Command for PhraseRenameCommand { + fn execute (self, state: &mut T) -> Perhaps { + use PhraseRenameCommand::*; + 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()))) + }, + Confirm => { + let old_name = old_name.clone(); + *state.phrases_mode_mut() = None; + return Ok(Some(Self::Set(old_name))) + }, + Cancel => { + state.phrases()[phrase].write().unwrap().name = old_name.clone(); + }, + _ => unreachable!() + }, + _ => unreachable!() + }; + Ok(None) + } +} + +impl InputToCommand for PhraseRenameCommand { + fn input_to_command (state: &T, from: &TuiInput) -> Option { + use KeyCode::{Char, Backspace, Enter, Esc}; + if let Some(PhrasesMode::Rename(_, ref old_name)) = state.phrases_mode() { + Some(match from.event() { + key!(Char(c)) => { + let mut new_name = old_name.clone(); + new_name.push(*c); + Self::Set(new_name) + }, + key!(Backspace) => { + let mut new_name = old_name.clone(); + new_name.pop(); + Self::Set(new_name) + }, + key!(Enter) => Self::Confirm, + key!(Esc) => Self::Cancel, + _ => return None + }) + } else { + unreachable!() + } + } +} + diff --git a/crates/tek_tui/src/tui_control_sequencer.rs b/crates/tek_tui/src/tui_control_sequencer.rs new file mode 100644 index 00000000..709e1340 --- /dev/null +++ b/crates/tek_tui/src/tui_control_sequencer.rs @@ -0,0 +1,88 @@ +use crate::*; + +impl Handle for SequencerTui { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + SequencerCommand::execute_with_state(self, i) + } +} + +pub trait SequencerControl: TransportControl {} + +impl SequencerControl for SequencerTui {} + +#[derive(Clone, Debug)] +pub enum SequencerCommand { + Focus(FocusCommand), + Undo, + Redo, + Clear, + Clock(ClockCommand), + Phrases(PhrasesCommand), + Editor(PhraseCommand), +} + +impl Command for SequencerCommand { + fn execute (self, state: &mut SequencerTui) -> Perhaps { + use SequencerCommand::*; + Ok(match self { + Focus(cmd) => cmd.execute(state)?.map(Focus), + Phrases(cmd) => cmd.execute(state)?.map(Phrases), + Editor(cmd) => cmd.execute(state)?.map(Editor), + Clock(cmd) => cmd.execute(state)?.map(Clock), + Undo => { todo!() }, + Redo => { todo!() }, + Clear => { todo!() }, + }) + } +} + +impl InputToCommand for SequencerCommand { + fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { + to_sequencer_command(state, input) + .or_else(||to_focus_command(input).map(SequencerCommand::Focus)) + } +} + +pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option { + use SequencerCommand::*; + use KeyCode::Char; + if !state.entered() { + return None + } + Some(match input.event() { + key!(Char('e')) => Editor( + PhraseCommand::Show(state.phrase_to_edit().clone()) + ), + key!(Char(' ')) => Clock( + if let Some(TransportState::Stopped) = *state.clock.playing.read().unwrap() { + ClockCommand::Play(None) + } else { + ClockCommand::Pause(None) + } + ), + key!(Shift-Char(' ')) => Clock( + if let Some(TransportState::Stopped) = *state.clock.playing.read().unwrap() { + ClockCommand::Play(Some(0)) + } else { + ClockCommand::Pause(Some(0)) + } + ), + _ => match state.focused() { + AppFocus::Menu => { todo!() }, + AppFocus::Content(focused) => match focused { + SequencerFocus::Transport(_) => { + match TransportCommand::input_to_command(state, input)? { + TransportCommand::Clock(_) => { todo!() }, + TransportCommand::Focus(command) => Focus(command), + } + }, + SequencerFocus::Phrases => Phrases( + PhrasesCommand::input_to_command(state, input)? + ), + SequencerFocus::PhraseEditor => Editor( + PhraseCommand::input_to_command(state, input)? + ), + } + } + }) +} diff --git a/crates/tek_tui/src/tui_control_transport.rs b/crates/tek_tui/src/tui_control_transport.rs new file mode 100644 index 00000000..26a7682d --- /dev/null +++ b/crates/tek_tui/src/tui_control_transport.rs @@ -0,0 +1,119 @@ +use crate::*; + +impl Handle for TransportTui { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + TransportCommand::execute_with_state(self, from) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum TransportCommand { + Focus(FocusCommand), + Clock(ClockCommand), +} + +impl Command for TransportCommand { + fn execute (self, state: &mut T) -> Perhaps { + use TransportCommand::{Focus, Clock}; + use FocusCommand::{Next, Prev}; + use ClockCommand::{SetBpm, SetQuant, SetSync}; + Ok(match self { + Focus(cmd) => cmd.execute(state)?.map(Focus), + Clock(SetBpm(bpm)) => Some(Clock(SetBpm(state.bpm().set(bpm)))), + Clock(SetQuant(quant)) => Some(Clock(SetQuant(state.quant().set(quant)))), + Clock(SetSync(sync)) => Some(Clock(SetSync(state.sync().set(sync)))), + _ => return Ok(None) + }) + } +} + +pub trait TransportControl: ClockApi + FocusGrid + HasEnter { + fn transport_focused (&self) -> Option; +} + +impl TransportControl for TransportTui { + fn transport_focused (&self) -> Option { + if let AppFocus::Content(focus) = self.focus.inner() { + Some(focus) + } else { + None + } + } +} + +impl TransportControl for SequencerTui { + fn transport_focused (&self) -> Option { + if let AppFocus::Content(SequencerFocus::Transport(focus)) = self.focus.inner() { + Some(focus) + } else { + None + } + } +} + +impl TransportControl for ArrangerTui { + fn transport_focused (&self) -> Option { + if let AppFocus::Content(ArrangerFocus::Transport(focus)) = self.focus.inner() { + Some(focus) + } else { + None + } + } +} + +impl InputToCommand for TransportCommand { + fn input_to_command (state: &T, input: &TuiInput) -> Option { + to_transport_command(state, input) + .or_else(||to_focus_command(input).map(TransportCommand::Focus)) + } +} + +pub fn to_transport_command (state: &T, input: &TuiInput) -> Option +where + T: TransportControl +{ + use ClockCommand::{SetBpm, SetQuant, SetSync}; + use TransportCommand::{Focus, Clock}; + use KeyCode::{Enter, Left, Right, Char}; + Some(match input.event() { + key!(Left) => Focus(FocusCommand::Prev), + key!(Right) => Focus(FocusCommand::Next), + key!(Char(' ')) => todo!("toolbar space"), + key!(Shift-Char(' ')) => todo!("toolbar shift-space"), + _ => match state.transport_focused().unwrap() { + TransportFocus::Bpm => match input.event() { + key!(Char(',')) => Clock(SetBpm(state.bpm().get() - 1.0)), + key!(Char('.')) => Clock(SetBpm(state.bpm().get() + 1.0)), + key!(Char('<')) => Clock(SetBpm(state.bpm().get() - 0.001)), + key!(Char('>')) => Clock(SetBpm(state.bpm().get() + 0.001)), + _ => return None, + }, + TransportFocus::Quant => match input.event() { + key!(Char(',')) => Clock(SetQuant(state.quant().prev())), + key!(Char('.')) => Clock(SetQuant(state.quant().next())), + key!(Char('<')) => Clock(SetQuant(state.quant().prev())), + key!(Char('>')) => Clock(SetQuant(state.quant().next())), + _ => return None, + }, + TransportFocus::Sync => match input.event() { + key!(Char(',')) => Clock(SetSync(state.sync().prev())), + key!(Char('.')) => Clock(SetSync(state.sync().next())), + key!(Char('<')) => Clock(SetSync(state.sync().prev())), + key!(Char('>')) => Clock(SetSync(state.sync().next())), + _ => return None, + }, + TransportFocus::Clock => match input.event() { + key!(Char(',')) => todo!("transport seek bar"), + key!(Char('.')) => todo!("transport seek bar"), + key!(Char('<')) => todo!("transport seek beat"), + key!(Char('>')) => todo!("transport seek beat"), + _ => return None, + }, + TransportFocus::PlayPause => match input.event() { + key!(Enter) => todo!("transport play toggle"), + key!(Shift-Enter) => todo!("transport shift-play toggle"), + _ => return None, + }, + } + }) +} diff --git a/crates/tek_tui/src/tui_debug.rs b/crates/tek_tui/src/tui_debug.rs deleted file mode 100644 index 1228bbd3..00000000 --- a/crates/tek_tui/src/tui_debug.rs +++ /dev/null @@ -1,47 +0,0 @@ -// Not all fields are included here. Add as needed. - -use crate::*; - -use std::fmt::{Debug, Formatter, Error}; -type DebugResult = std::result::Result<(), Error>; - -impl Debug for ClockModel { - fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult { - f.debug_struct("editor") - .field("playing", &self.playing) - .field("started", &self.started) - .field("current", &self.current) - .field("quant", &self.quant) - .field("sync", &self.sync) - .finish() - } -} - -impl Debug for TransportTui { - fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult { - f.debug_struct("Measure") - .field("jack", &self.jack) - .field("size", &self.size) - .field("cursor", &self.cursor) - .finish() - } -} - -impl Debug for PhraseEditorModel { - fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult { - f.debug_struct("editor") - .field("note_axis", &self.time_axis) - .field("time_axis", &self.note_axis) - .finish() - } -} - -impl Debug for PhrasePlayerModel { - fn fmt (&self, f: &mut Formatter<'_>) -> DebugResult { - f.debug_struct("editor") - .field("clock", &self.clock) - .field("play_phrase", &self.play_phrase) - .field("next_phrase", &self.next_phrase) - .finish() - } -} diff --git a/crates/tek_tui/src/tui_focus.rs b/crates/tek_tui/src/tui_focus.rs index 767231f9..3bdc73de 100644 --- a/crates/tek_tui/src/tui_focus.rs +++ b/crates/tek_tui/src/tui_focus.rs @@ -8,67 +8,11 @@ pub enum AppFocus { Content(T) } -/// Which item of the transport toolbar is focused -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum TransportFocus { - Bpm, - Sync, - PlayPause, - Clock, - Quant, -} - -/// Sections in the sequencer app that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum SequencerFocus { - /// The transport (toolbar) is focused - Transport(TransportFocus), - /// The phrase list (pool) is focused - Phrases, - /// The phrase editor (sequencer) is focused - PhraseEditor, -} - -/// Sections in the arranger app that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum ArrangerFocus { - /// The transport (toolbar) is focused - Transport(TransportFocus), - /// The arrangement (grid) is focused - Arranger, - /// The phrase list (pool) is focused - Phrases, - /// The phrase editor (sequencer) is focused - PhraseEditor, -} - pub trait FocusWrap { fn wrap <'a, W: Widget> (self, focus: T, content: &'a W) -> impl Widget + 'a; } -impl FocusWrap for TransportFocus { - fn wrap <'a, W: Widget> (self, focus: TransportFocus, content: &'a W) - -> impl Widget + 'a - { - let focused = focus == self; - let corners = focused.then_some(CORNERS); - let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50))); - lay!(corners, highlight, *content) - } -} - -impl FocusWrap for Option { - fn wrap <'a, W: Widget> (self, focus: TransportFocus, content: &'a W) - -> impl Widget + 'a - { - let focused = Some(focus) == self; - let corners = focused.then_some(CORNERS); - let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50))); - lay!(corners, highlight, *content) - } -} - macro_rules! impl_focus { ($Struct:ident $Focus:ident $Grid:expr) => { impl HasFocus for $Struct { @@ -175,31 +119,3 @@ impl_focus!(ArrangerTui ArrangerFocus [ Content(PhraseEditor), ], ]); - -/// 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_tui/src/tui_handle.rs b/crates/tek_tui/src/tui_handle.rs deleted file mode 100644 index 51623103..00000000 --- a/crates/tek_tui/src/tui_handle.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::*; - -impl Handle for TransportTui { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - TransportCommand::execute_with_state(self, from) - } -} -impl Handle for SequencerTui { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - SequencerCommand::execute_with_state(self, i) - } -} -impl Handle for ArrangerTui { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - ArrangerCommand::execute_with_state(self, i) - } -} -//impl Handle for PhrasesModel { - //fn handle (&mut self, from: &TuiInput) -> Perhaps { - //PhrasesCommand::execute_with_state(self, from) - //} -//} -//impl Handle for PhraseEditorModel { - //fn handle (&mut self, from: &TuiInput) -> Perhaps { - //PhraseCommand::execute_with_state(self, from) - //} -//} diff --git a/crates/tek_tui/src/tui_impls.rs b/crates/tek_tui/src/tui_impls.rs index b4175dad..c7b7e813 100644 --- a/crates/tek_tui/src/tui_impls.rs +++ b/crates/tek_tui/src/tui_impls.rs @@ -1,208 +1 @@ use crate::*; - -macro_rules! impl_jack_api { - ($Struct:ident $(:: $field:ident)*) => { - impl JackApi for $Struct { - fn jack (&self) -> &Arc> { - &self$(.$field)* - } - } - } -} -macro_rules! impl_clock_api { - ($Struct:ident $(:: $field:ident)*) => { - impl ClockApi for $Struct { - fn quant (&self) -> &Arc { - &self$(.$field)*.quant - } - fn sync (&self) -> &Arc { - &self$(.$field)*.sync - } - fn current (&self) -> &Arc { - &self$(.$field)*.current - } - fn transport_handle (&self) -> &Arc { - &self$(.$field)*.transport - } - fn transport_state (&self) -> &Arc>> { - &self$(.$field)*.playing - } - fn transport_offset (&self) -> &Arc>> { - &self$(.$field)*.started - } - } - } -} -macro_rules! impl_has_phrases { - ($Struct:ident $(:: $field:ident)*) => { - impl HasPhrases for $Struct { - fn phrases (&self) -> &Vec>> { - &self$(.$field)*.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self$(.$field)*.phrases - } - } - } -} -macro_rules! impl_midi_player { - ($Struct:ident $(:: $field:ident)*) => { - impl HasPhrase for $Struct { - fn reset (&self) -> bool { - self$(.$field)*.reset - } - fn reset_mut (&mut self) -> &mut bool { - &mut self$(.$field)*.reset - } - fn play_phrase (&self) -> &Option<(Instant, Option>>)> { - &self$(.$field)*.play_phrase - } - fn play_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { - &mut self$(.$field)*.play_phrase - } - fn next_phrase (&self) -> &Option<(Instant, Option>>)> { - &self$(.$field)*.next_phrase - } - fn next_phrase_mut (&mut self) -> &mut Option<(Instant, Option>>)> { - &mut self$(.$field)*.next_phrase - } - } - impl MidiInputApi for $Struct { - fn midi_ins (&self) -> &Vec> { - &self$(.$field)*.midi_ins - } - fn midi_ins_mut (&mut self) -> &mut Vec> { - &mut self$(.$field)*.midi_ins - } - fn recording (&self) -> bool { - self$(.$field)*.recording - } - fn recording_mut (&mut self) -> &mut bool { - &mut self$(.$field)*.recording - } - fn monitoring (&self) -> bool { - self$(.$field)*.monitoring - } - fn monitoring_mut (&mut self) -> &mut bool { - &mut self$(.$field)*.monitoring - } - fn overdub (&self) -> bool { - self$(.$field)*.overdub - } - fn overdub_mut (&mut self) -> &mut bool { - &mut self$(.$field)*.overdub - } - fn notes_in (&self) -> &Arc> { - &self$(.$field)*.notes_in - } - } - impl MidiOutputApi for $Struct { - fn midi_outs (&self) -> &Vec> { - &self$(.$field)*.midi_outs - } - fn midi_outs_mut (&mut self) -> &mut Vec> { - &mut self$(.$field)*.midi_outs - } - fn midi_note (&mut self) -> &mut Vec { - &mut self$(.$field)*.note_buf - } - fn notes_out (&self) -> &Arc> { - &self$(.$field)*.notes_in - } - } - impl MidiPlayerApi for $Struct {} - } -} -macro_rules! impl_phrases_control { - ($Struct:ident $(:: $field:ident)*) => { - impl PhrasesControl for $Struct { - fn phrase_index (&self) -> usize { - self.phrases.phrase.load(Ordering::Relaxed) - } - fn set_phrase_index (&self, value: usize) { - self.phrases.phrase.store(value, Ordering::Relaxed); - } - fn phrases_mode (&self) -> &Option { - &self.phrases.mode - } - fn phrases_mode_mut (&mut self) -> &mut Option { - &mut self.phrases.mode - } - } - } -} -macro_rules! impl_phrase_editor_control { - ( - $Struct:ident $(:: $field:ident)* - [$Focus:expr] - [$self1:ident: $phrase_to_edit:expr] - [$self2:ident, $phrase:ident: $edit_phrase:expr] - ) => { - impl PhraseEditorControl for $Struct { - fn phrase_to_edit (&$self1) -> Option>> { - $phrase_to_edit - } - fn edit_phrase (&mut $self2, $phrase: Option>>) { - $edit_phrase - //self.editor.show(self.selected_phrase().as_ref()); - //state.editor.phrase = phrase.clone(); - //state.focus(ArrangerFocus::PhraseEditor); - //state.focus_enter(); - //todo!("edit_phrase") - } - fn phrase_editing (&self) -> &Option>> { - todo!("phrase_editing") - } - fn phrase_editor_entered (&self) -> bool { - self.entered && self.focused() == $Focus - } - fn time_axis (&self) -> &RwLock> { - &self.editor.time_axis - } - fn note_axis (&self) -> &RwLock> { - &self.editor.note_axis - } - fn note_len (&self) -> usize { - self.editor.note_len - } - fn note_len_mut (&mut self) -> &mut usize { - &mut self.editor.note_len - } - fn put_note (&mut self) { - todo!("put_note") - } - } - } -} - -impl_jack_api!(TransportTui::jack); -impl_jack_api!(SequencerTui::jack); -impl_jack_api!(ArrangerTui::jack); - -impl_clock_api!(TransportTui::clock); -impl_clock_api!(SequencerTui::clock); -impl_clock_api!(ArrangerTui::clock); -impl_clock_api!(PhrasePlayerModel::clock); -impl_clock_api!(ArrangerTrack::player::clock); - -impl_has_phrases!(PhrasesModel); -impl_has_phrases!(SequencerTui::phrases); -impl_has_phrases!(ArrangerTui::phrases); - -impl_midi_player!(SequencerTui::player); -impl_midi_player!(ArrangerTrack::player); -impl_midi_player!(PhrasePlayerModel); - -impl_phrases_control!(SequencerTui); -impl_phrases_control!(ArrangerTui); - -impl_phrase_editor_control!(SequencerTui - [AppFocus::Content(SequencerFocus::PhraseEditor)] - [self: Some(self.phrases.phrases[self.phrases.phrase.load(Ordering::Relaxed)].clone())] - [self, phrase: self.editor.show(phrase)] -); -impl_phrase_editor_control!(ArrangerTui - [AppFocus::Content(ArrangerFocus::PhraseEditor)] - [self: todo!()] - [self, phrase: todo!()] -); diff --git a/crates/tek_tui/src/tui_init.rs b/crates/tek_tui/src/tui_init.rs deleted file mode 100644 index 238f04d4..00000000 --- a/crates/tek_tui/src/tui_init.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::*; - -/// Create app state from JACK handle. -impl TryFrom<&Arc>> for TransportTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - Ok(Self { - jack: jack.clone(), - clock: ClockModel::from(&Arc::new(jack.read().unwrap().transport())), - size: Measure::new(), - cursor: (0, 0), - focus: FocusState::Entered(AppFocus::Content(TransportFocus::Bpm)) - }) - } -} - -impl TryFrom<&Arc>> for SequencerTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - let clock = ClockModel::from(&Arc::new(jack.read().unwrap().transport())); - Ok(Self { - jack: jack.clone(), - phrases: PhrasesModel::default(), - player: PhrasePlayerModel::from(&clock), - editor: PhraseEditorModel::default(), - size: Measure::new(), - cursor: (0, 0), - entered: false, - split: 20, - midi_buf: vec![vec![];65536], - note_buf: vec![], - clock, - focus: FocusState::Entered(AppFocus::Content(SequencerFocus::Transport(TransportFocus::Bpm))), - perf: PerfModel::default(), - }) - } -} - -impl TryFrom<&Arc>> for ArrangerTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - Ok(Self { - jack: jack.clone(), - clock: ClockModel::from(&Arc::new(jack.read().unwrap().transport())), - phrases: PhrasesModel::default(), - editor: PhraseEditorModel::default(), - selected: ArrangerSelection::Clip(0, 0), - scenes: vec![], - tracks: vec![], - color: Color::Rgb(28, 35, 25).into(), - history: vec![], - mode: ArrangerMode::Vertical(2), - name: Arc::new(RwLock::new(String::new())), - size: Measure::new(), - cursor: (0, 0), - splits: [20, 20], - entered: false, - menu_bar: None, - status_bar: None, - midi_buf: vec![vec![];65536], - note_buf: vec![], - focus: FocusState::Entered(AppFocus::Content(ArrangerFocus::Transport(TransportFocus::Bpm))), - perf: PerfModel::default(), - }) - } -} diff --git a/crates/tek_tui/src/tui_input.rs b/crates/tek_tui/src/tui_input.rs deleted file mode 100644 index d68f9735..00000000 --- a/crates/tek_tui/src/tui_input.rs +++ /dev/null @@ -1,455 +0,0 @@ -use crate::*; - -impl InputToCommand for TransportCommand { - fn input_to_command (state: &T, input: &TuiInput) -> Option { - to_transport_command(state, input) - .or_else(||to_focus_command(input).map(TransportCommand::Focus)) - } -} - -impl InputToCommand for SequencerCommand { - fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { - to_sequencer_command(state, input) - .or_else(||to_focus_command(input).map(SequencerCommand::Focus)) - } -} - -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_focus_command (input: &TuiInput) -> Option { - use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc}; - Some(match input.event() { - key!(Tab) => FocusCommand::Next, - key!(Shift-Tab) => FocusCommand::Prev, - key!(BackTab) => FocusCommand::Prev, - key!(Shift-BackTab) => FocusCommand::Prev, - key!(Up) => FocusCommand::Up, - key!(Down) => FocusCommand::Down, - key!(Left) => FocusCommand::Left, - key!(Right) => FocusCommand::Right, - key!(Enter) => FocusCommand::Enter, - key!(Esc) => FocusCommand::Exit, - _ => return None - }) -} - -fn to_transport_command (state: &T, input: &TuiInput) -> Option -where - T: TransportControl -{ - use ClockCommand::{SetBpm, SetQuant, SetSync}; - use TransportCommand::{Focus, Clock}; - use KeyCode::{Enter, Left, Right, Char}; - Some(match input.event() { - key!(Left) => Focus(FocusCommand::Prev), - key!(Right) => Focus(FocusCommand::Next), - key!(Char(' ')) => todo!("toolbar space"), - key!(Shift-Char(' ')) => todo!("toolbar shift-space"), - _ => match state.transport_focused().unwrap() { - TransportFocus::Bpm => match input.event() { - key!(Char(',')) => Clock(SetBpm(state.bpm().get() - 1.0)), - key!(Char('.')) => Clock(SetBpm(state.bpm().get() + 1.0)), - key!(Char('<')) => Clock(SetBpm(state.bpm().get() - 0.001)), - key!(Char('>')) => Clock(SetBpm(state.bpm().get() + 0.001)), - _ => return None, - }, - TransportFocus::Quant => match input.event() { - key!(Char(',')) => Clock(SetQuant(state.quant().prev())), - key!(Char('.')) => Clock(SetQuant(state.quant().next())), - key!(Char('<')) => Clock(SetQuant(state.quant().prev())), - key!(Char('>')) => Clock(SetQuant(state.quant().next())), - _ => return None, - }, - TransportFocus::Sync => match input.event() { - key!(Char(',')) => Clock(SetSync(state.sync().prev())), - key!(Char('.')) => Clock(SetSync(state.sync().next())), - key!(Char('<')) => Clock(SetSync(state.sync().prev())), - key!(Char('>')) => Clock(SetSync(state.sync().next())), - _ => return None, - }, - TransportFocus::Clock => match input.event() { - key!(Char(',')) => todo!("transport seek bar"), - key!(Char('.')) => todo!("transport seek bar"), - key!(Char('<')) => todo!("transport seek beat"), - key!(Char('>')) => todo!("transport seek beat"), - _ => return None, - }, - TransportFocus::PlayPause => match input.event() { - key!(Enter) => todo!("transport play toggle"), - key!(Shift-Enter) => todo!("transport shift-play toggle"), - _ => return None, - }, - } - }) -} - -fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option { - use SequencerCommand::*; - use KeyCode::Char; - if !state.entered() { - return None - } - Some(match input.event() { - key!(Char('e')) => Editor( - PhraseCommand::Show(state.phrase_to_edit().clone()) - ), - key!(Char(' ')) => Clock( - if let Some(TransportState::Stopped) = *state.clock.playing.read().unwrap() { - ClockCommand::Play(None) - } else { - ClockCommand::Pause(None) - } - ), - key!(Shift-Char(' ')) => Clock( - if let Some(TransportState::Stopped) = *state.clock.playing.read().unwrap() { - ClockCommand::Play(Some(0)) - } else { - ClockCommand::Pause(Some(0)) - } - ), - _ => match state.focused() { - AppFocus::Menu => { todo!() }, - AppFocus::Content(focused) => match focused { - SequencerFocus::Transport(_) => { - match TransportCommand::input_to_command(state, input)? { - TransportCommand::Clock(_) => { todo!() }, - TransportCommand::Focus(command) => Focus(command), - } - }, - SequencerFocus::Phrases => Phrases( - PhrasesCommand::input_to_command(state, input)? - ), - SequencerFocus::PhraseEditor => Editor( - PhraseCommand::input_to_command(state, input)? - ), - } - } - }) -} - -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(state.phrase_to_edit().clone())), - _ => match state.focused() { - AppFocus::Menu => { todo!() }, - AppFocus::Content(focused) => match focused { - ArrangerFocus::Transport(_) => { - use TransportCommand::{Clock, Focus}; - match TransportCommand::input_to_command(state, input)? { - Clock(_) => { todo!() }, - Focus(command) => Cmd::Focus(command) - } - }, - ArrangerFocus::PhraseEditor => { - Cmd::Editor(PhraseCommand::input_to_command(state, input)?) - }, - ArrangerFocus::Phrases => { - Cmd::Phrases(PhrasesCommand::input_to_command(state, 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 - }) -} - -impl InputToCommand for PhrasesCommand { - fn input_to_command (state: &T, input: &TuiInput) -> Option { - use PhraseRenameCommand as Rename; - use PhraseLengthCommand as Length; - 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 { - use KeyCode::{Up, Down, Right, Left, Enter, Esc, Char, Backspace}; - use FileBrowserCommand::*; - if let Some(PhrasesMode::Import(index, browser)) = state.phrases_mode() { - Some(match from.event() { - key!(Up) => Select( - browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1)) - ), - key!(Down) => Select( - browser.index.saturating_add(1) % browser.len() - ), - key!(Right) => Chdir(browser.cwd.clone()), - key!(Left) => Chdir(browser.cwd.clone()), - key!(Enter) => Confirm, - key!(Char(c)) => { todo!() }, - key!(Backspace) => { todo!() }, - key!(Esc) => Self::Cancel, - _ => return None - }) - } else if let Some(PhrasesMode::Export(index, browser)) = state.phrases_mode() { - Some(match from.event() { - key!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())), - key!(Down) => Select(browser.index.saturating_add(1) % browser.len()), - key!(Right) => Chdir(browser.cwd.clone()), - key!(Left) => Chdir(browser.cwd.clone()), - key!(Enter) => Confirm, - key!(Char(c)) => { todo!() }, - key!(Backspace) => { todo!() }, - key!(Esc) => Self::Cancel, - _ => return None - }) - } else { - unreachable!() - } - } -} - -impl InputToCommand for PhraseLengthCommand { - fn input_to_command (state: &T, from: &TuiInput) -> Option { - use KeyCode::{Up, Down, Right, Left, Enter, Esc}; - if let Some(PhrasesMode::Length(_, length, _)) = state.phrases_mode() { - Some(match from.event() { - key!(Up) => Self::Inc, - key!(Down) => Self::Dec, - key!(Right) => Self::Next, - key!(Left) => Self::Prev, - key!(Enter) => Self::Set(*length), - key!(Esc) => Self::Cancel, - _ => return None - }) - } else { - unreachable!() - } - } -} - -impl InputToCommand for PhraseRenameCommand { - fn input_to_command (state: &T, from: &TuiInput) -> Option { - use KeyCode::{Char, Backspace, Enter, Esc}; - if let Some(PhrasesMode::Rename(_, ref old_name)) = state.phrases_mode() { - Some(match from.event() { - key!(Char(c)) => { - let mut new_name = old_name.clone(); - new_name.push(*c); - Self::Set(new_name) - }, - key!(Backspace) => { - let mut new_name = old_name.clone(); - new_name.pop(); - Self::Set(new_name) - }, - key!(Enter) => Self::Confirm, - key!(Esc) => Self::Cancel, - _ => return None - }) - } else { - unreachable!() - } - } -} - -impl InputToCommand for PhraseCommand { - fn input_to_command (state: &T, from: &TuiInput) -> Option { - use PhraseCommand::*; - use KeyCode::{Char, Enter, Esc, Up, Down, PageUp, PageDown, Left, Right}; - Some(match from.event() { - key!(Char('`')) => ToggleDirection, - key!(Enter) => EnterEditMode, - key!(Esc) => ExitEditMode, - key!(Char('a')) => NoteAppend, - key!(Char('s')) => NoteSet, - key!(Char('[')) => NoteLengthSet(prev_note_length(state.note_len())), - key!(Char(']')) => NoteLengthSet(next_note_length(state.note_len())), - key!(Char('-')) => TimeZoomSet(next_note_length(state.time_axis().read().unwrap().scale)), - key!(Char('_')) => TimeZoomSet(next_note_length(state.time_axis().read().unwrap().scale)), - key!(Char('=')) => TimeZoomSet(prev_note_length(state.time_axis().read().unwrap().scale)), - key!(Char('+')) => TimeZoomSet(prev_note_length(state.time_axis().read().unwrap().scale)), - key!(Up) => match state.phrase_editor_entered() { - true => NoteCursorSet(state.note_axis().write().unwrap().point_plus(1)), - false => NoteScrollSet(state.note_axis().write().unwrap().start_plus(1)), - }, - key!(Down) => match state.phrase_editor_entered() { - true => NoteCursorSet(state.note_axis().write().unwrap().point_minus(1)), - false => NoteScrollSet(state.note_axis().write().unwrap().start_minus(1)), - }, - key!(PageUp) => match state.phrase_editor_entered() { - true => NoteCursorSet(state.note_axis().write().unwrap().point_plus(3)), - false => NoteScrollSet(state.note_axis().write().unwrap().start_plus(3)), - }, - key!(PageDown) => match state.phrase_editor_entered() { - true => NoteCursorSet(state.note_axis().write().unwrap().point_minus(3)), - false => NoteScrollSet(state.note_axis().write().unwrap().start_minus(3)), - }, - key!(Left) => match state.phrase_editor_entered() { - true => TimeCursorSet(state.note_axis().write().unwrap().point_minus(1)), - false => TimeScrollSet(state.note_axis().write().unwrap().start_minus(1)), - }, - key!(Right) => match state.phrase_editor_entered() { - true => TimeCursorSet(state.note_axis().write().unwrap().point_plus(1)), - false => TimeScrollSet(state.note_axis().write().unwrap().start_plus(1)), - }, - _ => return None - }) - } -} diff --git a/crates/tek_tui/src/tui_jack.rs b/crates/tek_tui/src/tui_jack.rs index 711940e2..c7b7e813 100644 --- a/crates/tek_tui/src/tui_jack.rs +++ b/crates/tek_tui/src/tui_jack.rs @@ -1,91 +1 @@ use crate::*; - -impl Audio for TransportTui { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - ClockAudio(self).process(client, scope) - } -} - -impl Audio for SequencerTui { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - // Start profiling cycle - let t0 = self.perf.get_t0(); - - // Update transport clock - if ClockAudio(self).process(client, scope) == Control::Quit { - return Control::Quit - } - - // Update MIDI sequencer - if PlayerAudio( - &mut self.player, - &mut self.note_buf, - &mut self.midi_buf, - ).process(client, scope) == Control::Quit { - return Control::Quit - } - - // Update sequencer playhead indicator - //self.now().set(0.); - //if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase { - //let phrase = phrase.read().unwrap(); - //if *playing.read().unwrap() == *phrase { - //let pulse = self.current().pulse.get(); - //let start = started_at.pulse.get(); - //let now = (pulse - start) % phrase.length as f64; - //self.now().set(now); - //} - //} - - // End profiling cycle - self.perf.update(t0, scope); - - Control::Continue - } -} - -impl Audio for ArrangerTui { - #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - // Start profiling cycle - let t0 = self.perf.get_t0(); - - // Update transport clock - if ClockAudio(self).process(client, scope) == Control::Quit { - return Control::Quit - } - - // Update MIDI sequencers - if TracksAudio( - &mut self.tracks, - &mut self.note_buf, - &mut self.midi_buf, - Default::default(), - ).process(client, scope) == Control::Quit { - return Control::Quit - } - - // FIXME: one of these per playing track - self.now().set(0.); - if let ArrangerSelection::Clip(t, s) = self.selected { - let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t)); - if let Some(Some(Some(phrase))) = phrase { - if let Some(track) = self.tracks().get(t) { - if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase { - let phrase = phrase.read().unwrap(); - if *playing.read().unwrap() == *phrase { - let pulse = self.current().pulse.get(); - let start = started_at.pulse.get(); - let now = (pulse - start) % phrase.length as f64; - self.now().set(now); - } - } - } - } - } - - // End profiling cycle - self.perf.update(t0, scope); - - return Control::Continue - } -} diff --git a/crates/tek_tui/src/tui_jack_arranger.rs b/crates/tek_tui/src/tui_jack_arranger.rs new file mode 100644 index 00000000..73615148 --- /dev/null +++ b/crates/tek_tui/src/tui_jack_arranger.rs @@ -0,0 +1,53 @@ +use crate::*; + +impl JackApi for ArrangerTui { + fn jack (&self) -> &Arc> { + &self.jack + } +} + +impl Audio for ArrangerTui { + #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + // Start profiling cycle + let t0 = self.perf.get_t0(); + + // Update transport clock + if ClockAudio(self).process(client, scope) == Control::Quit { + return Control::Quit + } + + // Update MIDI sequencers + if TracksAudio( + &mut self.tracks, + &mut self.note_buf, + &mut self.midi_buf, + Default::default(), + ).process(client, scope) == Control::Quit { + return Control::Quit + } + + // FIXME: one of these per playing track + self.now().set(0.); + if let ArrangerSelection::Clip(t, s) = self.selected { + let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t)); + if let Some(Some(Some(phrase))) = phrase { + if let Some(track) = self.tracks().get(t) { + if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase { + let phrase = phrase.read().unwrap(); + if *playing.read().unwrap() == *phrase { + let pulse = self.current().pulse.get(); + let start = started_at.pulse.get(); + let now = (pulse - start) % phrase.length as f64; + self.now().set(now); + } + } + } + } + } + + // End profiling cycle + self.perf.update(t0, scope); + + return Control::Continue + } +} diff --git a/crates/tek_tui/src/tui_jack_sequencer.rs b/crates/tek_tui/src/tui_jack_sequencer.rs new file mode 100644 index 00000000..a590564d --- /dev/null +++ b/crates/tek_tui/src/tui_jack_sequencer.rs @@ -0,0 +1,45 @@ +use crate::*; + +impl JackApi for SequencerTui { + fn jack (&self) -> &Arc> { + &self.jack + } +} + +impl Audio for SequencerTui { + fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + // Start profiling cycle + let t0 = self.perf.get_t0(); + + // Update transport clock + if ClockAudio(self).process(client, scope) == Control::Quit { + return Control::Quit + } + + // Update MIDI sequencer + if PlayerAudio( + &mut self.player, + &mut self.note_buf, + &mut self.midi_buf, + ).process(client, scope) == Control::Quit { + return Control::Quit + } + + // Update sequencer playhead indicator + //self.now().set(0.); + //if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase { + //let phrase = phrase.read().unwrap(); + //if *playing.read().unwrap() == *phrase { + //let pulse = self.current().pulse.get(); + //let start = started_at.pulse.get(); + //let now = (pulse - start) % phrase.length as f64; + //self.now().set(now); + //} + //} + + // End profiling cycle + self.perf.update(t0, scope); + + Control::Continue + } +} diff --git a/crates/tek_tui/src/tui_jack_transport.rs b/crates/tek_tui/src/tui_jack_transport.rs new file mode 100644 index 00000000..ec9843cd --- /dev/null +++ b/crates/tek_tui/src/tui_jack_transport.rs @@ -0,0 +1,13 @@ +use crate::*; + +impl JackApi for TransportTui { + fn jack (&self) -> &Arc> { + &self.jack + } +} + +impl Audio for TransportTui { + fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + ClockAudio(self).process(client, scope) + } +} diff --git a/crates/tek_tui/src/tui_model_arranger.rs b/crates/tek_tui/src/tui_model_arranger.rs index 493d1dc3..79898e6b 100644 --- a/crates/tek_tui/src/tui_model_arranger.rs +++ b/crates/tek_tui/src/tui_model_arranger.rs @@ -112,3 +112,72 @@ impl ArrangerTrackApi for ArrangerTrack { 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_tui/src/tui_model_phrase_length.rs b/crates/tek_tui/src/tui_model_phrase_length.rs index a9979787..091f02ee 100644 --- a/crates/tek_tui/src/tui_model_phrase_length.rs +++ b/crates/tek_tui/src/tui_model_phrase_length.rs @@ -35,3 +35,31 @@ impl PhraseLength { 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_tui/src/tui_select.rs b/crates/tek_tui/src/tui_select.rs deleted file mode 100644 index cf47899d..00000000 --- a/crates/tek_tui/src/tui_select.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::*; - -#[derive(PartialEq, Clone, Copy, Debug)] -/// Represents the current user selection in the arranger -pub enum ArrangerSelection { - /// The whole mix is selected - Mix, - /// A track is selected. - Track(usize), - /// A scene is selected. - Scene(usize), - /// A clip (track × scene) is selected. - Clip(usize, usize), -} - -/// Focus identification methods -impl ArrangerSelection { - pub fn description ( - &self, - tracks: &Vec, - scenes: &Vec, - ) -> String { - format!("Selected: {}", match self { - Self::Mix => format!("Everything"), - Self::Track(t) => match tracks.get(*t) { - Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), - None => format!("T??"), - }, - Self::Scene(s) => match scenes.get(*s) { - Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), - None => format!("S??"), - }, - Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { - (Some(_), Some(scene)) => match scene.clip(*t) { - Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), - None => format!("T{t} S{s}: Empty") - }, - _ => format!("T{t} S{s}: Empty"), - } - }) - } - pub fn is_mix (&self) -> bool { - match self { Self::Mix => true, _ => false } - } - pub fn is_track (&self) -> bool { - match self { Self::Track(_) => true, _ => false } - } - pub fn is_scene (&self) -> bool { - match self { Self::Scene(_) => true, _ => false } - } - pub fn is_clip (&self) -> bool { - match self { Self::Clip(_, _) => true, _ => false } - } - pub fn track (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(t, _) => Some(*t), - Track(t) => Some(*t), - _ => None - } - } - pub fn scene (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(_, s) => Some(*s), - Scene(s) => Some(*s), - _ => None - } - } -} diff --git a/crates/tek_tui/src/tui_theme.rs b/crates/tek_tui/src/tui_theme.rs index da2f5ab5..c7b7e813 100644 --- a/crates/tek_tui/src/tui_theme.rs +++ b/crates/tek_tui/src/tui_theme.rs @@ -1,30 +1 @@ use crate::*; - -pub struct TuiTheme; - -impl TuiTheme { - pub fn border_bg () -> Color { - Color::Rgb(40, 50, 30) - } - pub fn border_fg (focused: bool) -> Color { - if focused { Color::Rgb(100, 110, 40) } else { Color::Rgb(70, 80, 50) } - } - pub fn title_fg (focused: bool) -> Color { - if focused { Color::Rgb(150, 160, 90) } else { Color::Rgb(120, 130, 100) } - } - pub fn separator_fg (_: bool) -> Color { - Color::Rgb(0, 0, 0) - } - pub const fn hotkey_fg () -> Color { - Color::Rgb(255, 255, 0) - } - pub fn mode_bg () -> Color { - Color::Rgb(150, 160, 90) - } - pub fn mode_fg () -> Color { - Color::Rgb(255, 255, 255) - } - pub fn status_bar_bg () -> Color { - Color::Rgb(28, 35, 25) - } -}