From 19ed6a24b818b53fbf39802ed1f6e72dd272b705 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 12 Jan 2025 02:23:39 +0100 Subject: [PATCH] but how to pass arbitrary chars to the config --- edn/src/edn_token.rs | 10 +- input/src/edn_cmd.rs | 2 +- midi/edn/midi-keys.edn | 26 +-- midi/src/lib.rs | 10 +- midi/src/midi_edit_cmd.rs | 77 +++++++ midi/src/midi_edit_tui.rs | 38 ++++ midi/src/midi_editor.rs | 205 ++++------------- midi/src/midi_pool.rs | 459 ++------------------------------------ midi/src/midi_pool_cmd.rs | 363 ++++++++++++++++++++++++++++++ midi/src/midi_pool_tui.rs | 41 ++++ tek/src/arranger.rs | 40 ++-- 11 files changed, 627 insertions(+), 644 deletions(-) create mode 100644 midi/src/midi_edit_cmd.rs create mode 100644 midi/src/midi_edit_tui.rs create mode 100644 midi/src/midi_pool_cmd.rs create mode 100644 midi/src/midi_pool_tui.rs diff --git a/edn/src/edn_token.rs b/edn/src/edn_token.rs index 3343ef98..2eeb9c76 100644 --- a/edn/src/edn_token.rs +++ b/edn/src/edn_token.rs @@ -9,8 +9,10 @@ pub enum Token<'a> { Exp(&'a str, usize, usize, usize), } +pub type ChompResult<'a> = Result<(&'a str, Token<'a>), ParseError>; + impl<'a> Token<'a> { - pub const fn chomp (source: &'a str) -> Result<(&'a str, Self), ParseError> { + pub const fn chomp (source: &'a str) -> ChompResult<'a> { use Token::*; use konst::string::{split_at, char_indices}; let mut state = Self::Nil; @@ -20,15 +22,17 @@ impl<'a> Token<'a> { // must begin expression Nil => match c { ' '|'\n'|'\r'|'\t' => Nil, - '(' => Exp(source, index, 1, 1), - ':' => Sym(source, index, 1), + '(' => Exp(source, index, 1, 1), + ':'|'@' => Sym(source, index, 1), '0'..='9' => Num(source, index, 1), 'a'..='z' => Key(source, index, 1), + //'\'' => Chr(source, index, 1), _ => return Err(ParseError::Unexpected(c)) }, Num(_, _, 0) => unreachable!(), Sym(_, _, 0) => unreachable!(), Key(_, _, 0) => unreachable!(), + //Chr(_, _, 0) => unreachable!(), Num(source, index, length) => match c { '0'..='9' => Num(source, index, length + 1), ' '|'\n'|'\r'|'\t' => return Ok(( diff --git a/input/src/edn_cmd.rs b/input/src/edn_cmd.rs index 0cb70726..2fbd9d39 100644 --- a/input/src/edn_cmd.rs +++ b/input/src/edn_cmd.rs @@ -18,7 +18,7 @@ impl EdnKeymap { for item in self.0.iter() { if let Exp(items) = item { match items.as_slice() { - [Key(a), b, c @ ..] => if input.matches(a.as_str()) { + [Sym(a), b, c @ ..] => if input.matches(a.as_str()) { return Some(E::from_edn(state, &b.to_ref(), c)) }, _ => unreachable!() diff --git a/midi/edn/midi-keys.edn b/midi/edn/midi-keys.edn index ac461cb0..bf4c9958 100644 --- a/midi/edn/midi-keys.edn +++ b/midi/edn/midi-keys.edn @@ -1,17 +1,17 @@ -(@enter note/put) -(@del note/del) -(@"," note/duration/dec) -(@"." note/duration/inc) -(@"+" note/scale/inc) -(@"-" note/scale/dec) -(@up note/cursor/inc) -(@down note/cursor/dec) +(enter note/put) +(del note/del) +(',' note/dur/dec) +('.' note/dur/inc) +('+' note/range/inc) +('-' note/range/dec) +(up note/pos/inc) +(down note/pos/dec) -(@left time/cursor/dec) -(@right time/cursor/inc) -(@"z" time/zoom/lock) -(@"=" time/zoom/in) -(@"-" time/zoom/out) +(left time/pos/dec) +(right time/pos/inc) +('z' time/zoom/lock) +('=' time/zoom/in) +('-' time/zoom/out) ;(@ctrl-k (midi/kbd/toggle)) ;(@space (clock/toggle)) diff --git a/midi/src/lib.rs b/midi/src/lib.rs index edb2e276..666d402d 100644 --- a/midi/src/lib.rs +++ b/midi/src/lib.rs @@ -1,4 +1,3 @@ -mod midi_pool; pub use midi_pool::*; mod midi_clip; pub use midi_clip::*; mod midi_launch; pub use midi_launch::*; mod midi_player; pub use midi_player::*; @@ -9,9 +8,16 @@ mod midi_pitch; pub use midi_pitch::*; mod midi_range; pub use midi_range::*; mod midi_point; pub use midi_point::*; mod midi_view; pub use midi_view::*; -mod midi_editor; pub use midi_editor::*; mod midi_select; pub use midi_select::*; +mod midi_pool; pub use midi_pool::*; +mod midi_pool_tui; pub use midi_pool_tui::*; +mod midi_pool_cmd; pub use midi_pool_cmd::*; + +mod midi_editor; pub use midi_editor::*; +mod midi_edit_cmd; pub use midi_edit_cmd::*; +mod midi_edit_tui; pub use midi_edit_tui::*; + mod piano_h; pub use self::piano_h::*; pub(crate) use ::tek_time::*; diff --git a/midi/src/midi_edit_cmd.rs b/midi/src/midi_edit_cmd.rs new file mode 100644 index 00000000..68c022f8 --- /dev/null +++ b/midi/src/midi_edit_cmd.rs @@ -0,0 +1,77 @@ +use crate::*; +use self::MidiEditCommand::*; +use KeyCode::*; +#[derive(Clone, Debug)] +pub enum MidiEditCommand { + // TODO: 1-9 seek markers that by default start every 8th of the clip + AppendNote, + PutNote, + SetNoteCursor(usize), + SetNoteLength(usize), + SetNoteScroll(usize), + SetTimeCursor(usize), + SetTimeScroll(usize), + SetTimeZoom(usize), + SetTimeLock(bool), + Show(Option>>), +} +handle!(TuiIn: |self: MidiEditor, input|MidiEditCommand::execute_with_state(self, input.event())); +keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand { + key(Up) => SetNoteCursor(s.note_point() + 1), + key(Char('w')) => SetNoteCursor(s.note_point() + 1), + key(Down) => SetNoteCursor(s.note_point().saturating_sub(1)), + key(Char('s')) => SetNoteCursor(s.note_point().saturating_sub(1)), + key(Left) => SetTimeCursor(s.time_point().saturating_sub(s.note_len())), + key(Char('a')) => SetTimeCursor(s.time_point().saturating_sub(s.note_len())), + key(Right) => SetTimeCursor((s.time_point() + s.note_len()) % s.clip_length()), + ctrl(alt(key(Up))) => SetNoteScroll(s.note_point() + 3), + ctrl(alt(key(Down))) => SetNoteScroll(s.note_point().saturating_sub(3)), + ctrl(alt(key(Left))) => SetTimeScroll(s.time_point().saturating_sub(s.time_zoom().get())), + ctrl(alt(key(Right))) => SetTimeScroll((s.time_point() + s.time_zoom().get()) % s.clip_length()), + ctrl(key(Up)) => SetNoteScroll(s.note_lo().get() + 1), + ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(1)), + ctrl(key(Left)) => SetTimeScroll(s.time_start().get().saturating_sub(s.note_len())), + ctrl(key(Right)) => SetTimeScroll(s.time_start().get() + s.note_len()), + alt(key(Up)) => SetNoteCursor(s.note_point() + 3), + alt(key(Down)) => SetNoteCursor(s.note_point().saturating_sub(3)), + alt(key(Left)) => SetTimeCursor(s.time_point().saturating_sub(s.time_zoom().get())), + alt(key(Right)) => SetTimeCursor((s.time_point() + s.time_zoom().get()) % s.clip_length()), + key(Char('d')) => SetTimeCursor((s.time_point() + s.note_len()) % s.clip_length()), + key(Char('z')) => SetTimeLock(!s.time_lock().get()), + key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }), + key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }), + key(Char('=')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }), + key(Char('+')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }), + key(Enter) => PutNote, + ctrl(key(Enter)) => AppendNote, + key(Char(',')) => SetNoteLength(NoteDuration::prev(s.note_len())), + key(Char('.')) => SetNoteLength(NoteDuration::next(s.note_len())), + key(Char('<')) => SetNoteLength(NoteDuration::prev(s.note_len())), + key(Char('>')) => SetNoteLength(NoteDuration::next(s.note_len())), + //// TODO: kpat!(Char('/')) => // toggle 3plet + //// TODO: kpat!(Char('?')) => // toggle dotted +}); +impl Command for MidiEditCommand { + fn execute (self, state: &mut MidiEditor) -> Perhaps { + use MidiEditCommand::*; + match self { + Show(clip) => { state.set_clip(clip.as_ref()); }, + PutNote => { state.put_note(false); }, + AppendNote => { state.put_note(true); }, + SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); }, + SetTimeLock(x) => { state.time_lock().set(x); }, + SetTimeScroll(x) => { state.time_start().set(x); }, + SetNoteScroll(x) => { state.note_lo().set(x.min(127)); }, + SetNoteLength(x) => { state.set_note_len(x); }, + SetTimeCursor(x) => { state.set_time_point(x); }, + SetNoteCursor(note) => { state.set_note_point(note.min(127)); }, + _ => todo!("{:?}", self) + } + Ok(None) + } +} +impl EdnCommand for MidiEditCommand { + fn from_edn <'a> (state: &MidiEditor, head: &EdnItem<&str>, tail: &'a [EdnItem]) -> Self { + todo!() + } +} diff --git a/midi/src/midi_edit_tui.rs b/midi/src/midi_edit_tui.rs new file mode 100644 index 00000000..af07e9cf --- /dev/null +++ b/midi/src/midi_edit_tui.rs @@ -0,0 +1,38 @@ +use crate::*; +has_size!(|self: MidiEditor|&self.size); +render!(TuiOut: (self: MidiEditor) => { + self.autoscroll(); + //self.autozoom(); + self.size.of(&self.mode) +}); +impl MidiEditor { + pub fn clip_status (&self) -> impl Content + '_ { + let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.name.clone(), clip.length, clip.looped) + } else { + (ItemPalette::from(TuiTheme::g(64)), String::new().into(), 0, false) + }; + row!( + FieldV(color, "Edit", format!("{name} ({length})")), + FieldV(color, "Loop", looped.to_string()) + ) + } + + pub fn edit_status (&self) -> impl Content + '_ { + let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { + (clip.color, clip.length) + } else { + (ItemPalette::from(TuiTheme::g(64)), 0) + }; + let time_point = self.time_point(); + let time_zoom = self.time_zoom().get(); + let time_lock = if self.time_lock().get() { "[lock]" } else { " " }; + let note_point = format!("{:>3}", self.note_point()); + let note_name = format!("{:4}", Note::pitch_to_name(self.note_point())); + let note_len = format!("{:>4}", self.note_len()); + Bsp::e( + FieldV(color, "Time", format!("{length}/{time_zoom}+{time_point} {time_lock}")), + FieldV(color, "Note", format!("{note_name} {note_point} {note_len}")), + ) + } +} diff --git a/midi/src/midi_editor.rs b/midi/src/midi_editor.rs index ef2693de..4070858d 100644 --- a/midi/src/midi_editor.rs +++ b/midi/src/midi_editor.rs @@ -1,7 +1,4 @@ use crate::*; -use KeyCode::{Char, Up, Down, Left, Right, Enter}; -use MidiEditCommand::*; - pub trait HasEditor { fn editor (&self) -> &MidiEditor; } @@ -18,17 +15,13 @@ pub struct MidiEditor { pub size: Measure, pub keymap: EdnKeymap, } -from!(|clip: &Arc>|MidiEditor = { - let model = Self::from(Some(clip.clone())); - model.redraw(); - model -}); -from!(|clip: Option>>|MidiEditor = { - let mut model = Self::default(); - *model.clip_mut() = clip; - model.redraw(); - model -}); +impl std::fmt::Debug for MidiEditor { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("MidiEditor") + .field("mode", &self.mode) + .finish() + } +} impl Default for MidiEditor { fn default () -> Self { Self { @@ -41,41 +34,10 @@ impl Default for MidiEditor { } } } -has_size!(|self: MidiEditor|&self.size); -render!(TuiOut: (self: MidiEditor) => { - self.autoscroll(); - //self.autozoom(); - self.size.of(&self.mode) -}); -impl TimeRange for MidiEditor { - fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } - fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } -} -impl NoteRange for MidiEditor { - fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } -} -impl NotePoint for MidiEditor { - fn note_len (&self) -> usize { self.mode.note_len() } - fn set_note_len (&self, x: usize) { self.mode.set_note_len(x) } - fn note_point (&self) -> usize { self.mode.note_point() } - fn set_note_point (&self, x: usize) { self.mode.set_note_point(x) } -} -impl TimePoint for MidiEditor { - fn time_point (&self) -> usize { self.mode.time_point() } - fn set_time_point (&self, x: usize) { self.mode.set_time_point(x) } -} -impl MidiViewer for MidiEditor { - fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } - fn redraw (&self) { self.mode.redraw() } - fn clip (&self) -> &Option>> { self.mode.clip() } - fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } - fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } -} impl MidiEditor { + pub fn clip_length (&self) -> usize { + self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) + } /// Put note at current position pub fn put_note (&mut self, advance: bool) { let mut redraw = false; @@ -106,120 +68,43 @@ impl MidiEditor { self.mode.redraw(); } } - - pub fn clip_status (&self) -> impl Content + '_ { - let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.name.clone(), clip.length, clip.looped) - } else { - (ItemPalette::from(TuiTheme::g(64)), String::new().into(), 0, false) - }; - row!( - FieldV(color, "Edit", format!("{name} ({length})")), - FieldV(color, "Loop", looped.to_string()) - ) - } - - pub fn edit_status (&self) -> impl Content + '_ { - let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { - (clip.color, clip.length) - } else { - (ItemPalette::from(TuiTheme::g(64)), 0) - }; - let time_point = self.time_point(); - let time_zoom = self.time_zoom().get(); - let time_lock = if self.time_lock().get() { "[lock]" } else { " " }; - let note_point = format!("{:>3}", self.note_point()); - let note_name = format!("{:4}", Note::pitch_to_name(self.note_point())); - let note_len = format!("{:>4}", self.note_len()); - Bsp::e( - FieldV(color, "Time", format!("{length}/{time_zoom}+{time_point} {time_lock}")), - FieldV(color, "Note", format!("{note_name} {note_point} {note_len}")), - ) - } } -impl std::fmt::Debug for MidiEditor { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("MidiEditor") - .field("mode", &self.mode) - .finish() - } -} -#[derive(Clone, Debug)] -pub enum MidiEditCommand { - // TODO: 1-9 seek markers that by default start every 8th of the clip - AppendNote, - PutNote, - SetNoteCursor(usize), - SetNoteLength(usize), - SetNoteScroll(usize), - SetTimeCursor(usize), - SetTimeScroll(usize), - SetTimeZoom(usize), - SetTimeLock(bool), - Show(Option>>), -} -handle!(TuiIn: |self: MidiEditor, input|MidiEditCommand::execute_with_state(self, input.event())); -keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand { - key(Up) => SetNoteCursor(s.note_point() + 1), - key(Char('w')) => SetNoteCursor(s.note_point() + 1), - key(Down) => SetNoteCursor(s.note_point().saturating_sub(1)), - key(Char('s')) => SetNoteCursor(s.note_point().saturating_sub(1)), - key(Left) => SetTimeCursor(s.time_point().saturating_sub(s.note_len())), - key(Char('a')) => SetTimeCursor(s.time_point().saturating_sub(s.note_len())), - key(Right) => SetTimeCursor((s.time_point() + s.note_len()) % s.clip_length()), - ctrl(alt(key(Up))) => SetNoteScroll(s.note_point() + 3), - ctrl(alt(key(Down))) => SetNoteScroll(s.note_point().saturating_sub(3)), - ctrl(alt(key(Left))) => SetTimeScroll(s.time_point().saturating_sub(s.time_zoom().get())), - ctrl(alt(key(Right))) => SetTimeScroll((s.time_point() + s.time_zoom().get()) % s.clip_length()), - ctrl(key(Up)) => SetNoteScroll(s.note_lo().get() + 1), - ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(1)), - ctrl(key(Left)) => SetTimeScroll(s.time_start().get().saturating_sub(s.note_len())), - ctrl(key(Right)) => SetTimeScroll(s.time_start().get() + s.note_len()), - alt(key(Up)) => SetNoteCursor(s.note_point() + 3), - alt(key(Down)) => SetNoteCursor(s.note_point().saturating_sub(3)), - alt(key(Left)) => SetTimeCursor(s.time_point().saturating_sub(s.time_zoom().get())), - alt(key(Right)) => SetTimeCursor((s.time_point() + s.time_zoom().get()) % s.clip_length()), - key(Char('d')) => SetTimeCursor((s.time_point() + s.note_len()) % s.clip_length()), - key(Char('z')) => SetTimeLock(!s.time_lock().get()), - key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }), - key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }), - key(Char('=')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }), - key(Char('+')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }), - key(Enter) => PutNote, - ctrl(key(Enter)) => AppendNote, - key(Char(',')) => SetNoteLength(NoteDuration::prev(s.note_len())), - key(Char('.')) => SetNoteLength(NoteDuration::next(s.note_len())), - key(Char('<')) => SetNoteLength(NoteDuration::prev(s.note_len())), - key(Char('>')) => SetNoteLength(NoteDuration::next(s.note_len())), - //// TODO: kpat!(Char('/')) => // toggle 3plet - //// TODO: kpat!(Char('?')) => // toggle dotted +from!(|clip: &Arc>|MidiEditor = { + let model = Self::from(Some(clip.clone())); + model.redraw(); + model }); -impl MidiEditor { - fn clip_length (&self) -> usize { - self.clip().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) - } +from!(|clip: Option>>|MidiEditor = { + let mut model = Self::default(); + *model.clip_mut() = clip; + model.redraw(); + model +}); +impl TimeRange for MidiEditor { + fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } + fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } + fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } + fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } + fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } } -impl Command for MidiEditCommand { - fn execute (self, state: &mut MidiEditor) -> Perhaps { - use MidiEditCommand::*; - match self { - Show(clip) => { state.set_clip(clip.as_ref()); }, - PutNote => { state.put_note(false); }, - AppendNote => { state.put_note(true); }, - SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); }, - SetTimeLock(x) => { state.time_lock().set(x); }, - SetTimeScroll(x) => { state.time_start().set(x); }, - SetNoteScroll(x) => { state.note_lo().set(x.min(127)); }, - SetNoteLength(x) => { state.set_note_len(x); }, - SetTimeCursor(x) => { state.set_time_point(x); }, - SetNoteCursor(note) => { state.set_note_point(note.min(127)); }, - _ => todo!("{:?}", self) - } - Ok(None) - } +impl NoteRange for MidiEditor { + fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } + fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } } -impl EdnCommand for MidiEditCommand { - fn from_edn <'a> (state: &MidiEditor, head: &EdnItem<&str>, tail: &'a [EdnItem]) -> Self { - todo!() - } +impl NotePoint for MidiEditor { + fn note_len (&self) -> usize { self.mode.note_len() } + fn set_note_len (&self, x: usize) { self.mode.set_note_len(x) } + fn note_point (&self) -> usize { self.mode.note_point() } + fn set_note_point (&self, x: usize) { self.mode.set_note_point(x) } +} +impl TimePoint for MidiEditor { + fn time_point (&self) -> usize { self.mode.time_point() } + fn set_time_point (&self, x: usize) { self.mode.set_time_point(x) } +} +impl MidiViewer for MidiEditor { + fn buffer_size (&self, clip: &MidiClip) -> (usize, usize) { self.mode.buffer_size(clip) } + fn redraw (&self) { self.mode.redraw() } + fn clip (&self) -> &Option>> { self.mode.clip() } + fn clip_mut (&mut self) -> &mut Option>> { self.mode.clip_mut() } + fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } } diff --git a/midi/src/midi_pool.rs b/midi/src/midi_pool.rs index 1eba2460..72263c7d 100644 --- a/midi/src/midi_pool.rs +++ b/midi/src/midi_pool.rs @@ -1,7 +1,5 @@ use crate::*; - pub type ClipPool = Vec>>; - pub trait HasClips { fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>; @@ -11,7 +9,6 @@ pub trait HasClips { (self.clips().len() - 1, clip) } } - #[macro_export] macro_rules! has_clips { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { @@ -24,7 +21,6 @@ pub trait HasClips { } } } - #[derive(Debug)] pub struct PoolModel { pub visible: bool, @@ -35,6 +31,18 @@ pub struct PoolModel { /// Mode switch pub mode: Option, } +/// Modes for clip pool +#[derive(Debug, Clone)] +pub enum PoolMode { + /// Renaming a pattern + Rename(usize, Arc), + /// Editing the length of a pattern + Length(usize, usize, ClipLengthFocus), + /// Load clip from disk + Import(usize, FileBrowser), + /// Save clip to disk + Export(usize, FileBrowser), +} impl Default for PoolModel { fn default () -> Self { Self { @@ -51,262 +59,6 @@ from!(|clip:&Arc>|PoolModel = { model.clip.store(1, Relaxed); model }); - -#[derive(Clone, PartialEq, Debug)] -pub enum PoolCommand { - Show(bool), - /// Update the contents of the clip pool - Clip(PoolClipCommand), - /// Select a clip from the clip pool - Select(usize), - /// Rename a clip - Rename(ClipRenameCommand), - /// Change the length of a clip - Length(ClipLengthCommand), - /// Import from file - Import(FileBrowserCommand), - /// Export to file - Export(FileBrowserCommand), -} -impl EdnCommand for PoolCommand { - fn from_edn <'a> (state: &PoolModel, head: &EdnItem<&str>, tail: &'a [EdnItem]) -> Self { - todo!() - } -} -#[derive(Clone, Debug, PartialEq)] -pub enum PoolClipCommand { - Add(usize, MidiClip), - Delete(usize), - Swap(usize, usize), - Import(usize, PathBuf), - Export(usize, PathBuf), - SetName(usize, Arc), - SetLength(usize, usize), - SetColor(usize, ItemColor), -} -impl PoolClipCommand { - pub fn from_edn <'a> (head: &EdnItem<&str>, tail: &'a [EdnItem]) -> Self { - todo!() - } -} - -/// Modes for clip pool -#[derive(Debug, Clone)] -pub enum PoolMode { - /// Renaming a pattern - Rename(usize, Arc), - /// Editing the length of a pattern - Length(usize, usize, ClipLengthFocus), - /// Load clip from disk - Import(usize, FileBrowser), - /// Save clip to disk - Export(usize, FileBrowser), -} - - -impl Command for PoolClipCommand { - fn execute (self, model: &mut T) -> Perhaps { - use PoolClipCommand::*; - Ok(match self { - Add(mut index, clip) => { - let clip = Arc::new(RwLock::new(clip)); - let mut clips = model.clips_mut(); - if index >= clips.len() { - index = clips.len(); - clips.push(clip) - } else { - clips.insert(index, clip); - } - Some(Self::Delete(index)) - }, - Delete(index) => { - let clip = model.clips_mut().remove(index).read().unwrap().clone(); - Some(Self::Add(index, clip)) - }, - Swap(index, other) => { - model.clips_mut().swap(index, other); - Some(Self::Swap(index, other)) - }, - Import(index, path) => { - let bytes = std::fs::read(&path)?; - let smf = Smf::parse(bytes.as_slice())?; - let mut t = 0u32; - let mut events = vec![]; - for track in smf.tracks.iter() { - for event in track.iter() { - t += event.delta.as_int(); - if let TrackEventKind::Midi { channel, message } = event.kind { - events.push((t, channel.as_int(), message)); - } - } - } - let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); - for event in events.iter() { - clip.notes[event.0 as usize].push(event.2); - } - Self::Add(index, clip).execute(model)? - }, - Export(_index, _path) => { - todo!("export clip to midi file"); - }, - SetName(index, name) => { - let clip = &mut model.clips_mut()[index]; - let old_name = clip.read().unwrap().name.clone(); - clip.write().unwrap().name = name; - Some(Self::SetName(index, old_name)) - }, - SetLength(index, length) => { - let clip = &mut model.clips_mut()[index]; - let old_len = clip.read().unwrap().length; - clip.write().unwrap().length = length; - Some(Self::SetLength(index, old_len)) - }, - SetColor(index, color) => { - let mut color = ItemPalette::from(color); - std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color); - Some(Self::SetColor(index, color.base)) - }, - }) - } -} - - -pub struct PoolView<'a>(pub bool, pub &'a PoolModel); -render!(TuiOut: (self: PoolView<'a>) => { - let Self(compact, model) = self; - let PoolModel { clips, mode, .. } = self.1; - let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||TuiTheme::g(32).into()); - let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); - let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); - let iter = | |model.clips().clone().into_iter(); - Tui::bg(Color::Reset, Fixed::y(clips.read().unwrap().len() as u16, on_bg(border(Map::new(iter, move|clip, i|{ - let item_height = 1; - let item_offset = i as u16 * item_height; - let selected = i == model.clip_index(); - let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); - let bg = if selected { color.light.rgb } else { color.base.rgb }; - let fg = color.lightest.rgb; - let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; - let length = if *compact { String::default() } else { format!("{length} ") }; - Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( - Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), - Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), - Fill::x(Align::w(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶"))))), - Fill::x(Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀"))))), - )))) - }))))) -}); - -command!(|self:PoolCommand, state: PoolModel|{ - use PoolCommand::*; - match self { - Show(visible) => { - state.visible = visible; - Some(Self::Show(!visible)) - } - Rename(command) => match command { - ClipRenameCommand::Begin => { - let length = state.clips()[state.clip_index()].read().unwrap().length; - *state.clips_mode_mut() = Some( - PoolMode::Length(state.clip_index(), length, ClipLengthFocus::Bar) - ); - None - }, - _ => command.execute(state)?.map(Rename) - }, - Length(command) => match command { - ClipLengthCommand::Begin => { - let name = state.clips()[state.clip_index()].read().unwrap().name.clone(); - *state.clips_mode_mut() = Some( - PoolMode::Rename(state.clip_index(), name) - ); - None - }, - _ => command.execute(state)?.map(Length) - }, - Import(command) => match command { - FileBrowserCommand::Begin => { - *state.clips_mode_mut() = Some( - PoolMode::Import(state.clip_index(), FileBrowser::new(None)?) - ); - None - }, - _ => command.execute(state)?.map(Import) - }, - Export(command) => match command { - FileBrowserCommand::Begin => { - *state.clips_mode_mut() = Some( - PoolMode::Export(state.clip_index(), FileBrowser::new(None)?) - ); - None - }, - _ => command.execute(state)?.map(Export) - }, - Select(clip) => { - state.set_clip_index(clip); - None - }, - Clip(command) => command.execute(state)?.map(Clip), - } -}); - -input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.clips_mode() { - Some(PoolMode::Rename(..)) => Self::Rename(ClipRenameCommand::input_to_command(state, input)?), - Some(PoolMode::Length(..)) => Self::Length(ClipLengthCommand::input_to_command(state, input)?), - Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), - Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?), - _ => to_clips_command(state, input)? -}); - -fn to_clips_command (state: &PoolModel, input: &Event) -> Option { - use KeyCode::{Up, Down, Delete, Char}; - use PoolCommand as Cmd; - let index = state.clip_index(); - let count = state.clips().len(); - Some(match input { - kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin), - kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin), - kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin), - kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin), - kpat!(Char('c')) => Cmd::Clip(PoolClipCommand::SetColor(index, ItemColor::random())), - kpat!(Char('[')) | kpat!(Up) => Cmd::Select( - index.overflowing_sub(1).0.min(state.clips().len() - 1) - ), - kpat!(Char(']')) | kpat!(Down) => Cmd::Select( - index.saturating_add(1) % state.clips().len() - ), - kpat!(Char('<')) => if index > 1 { - state.set_clip_index(state.clip_index().saturating_sub(1)); - Cmd::Clip(PoolClipCommand::Swap(index - 1, index)) - } else { - return None - }, - kpat!(Char('>')) => if index < count.saturating_sub(1) { - state.set_clip_index(state.clip_index() + 1); - Cmd::Clip(PoolClipCommand::Swap(index + 1, index)) - } else { - return None - }, - kpat!(Delete) => if index > 0 { - state.set_clip_index(index.min(count.saturating_sub(1))); - Cmd::Clip(PoolClipCommand::Delete(index)) - } else { - return None - }, - kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(PoolClipCommand::Add(count, MidiClip::new( - "Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) - ))), - kpat!(Char('i')) => Cmd::Clip(PoolClipCommand::Add(index + 1, MidiClip::new( - "Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) - ))), - kpat!(Char('d')) | kpat!(Shift-Char('D')) => { - let mut clip = state.clips()[index].read().unwrap().duplicate(); - clip.color = ItemPalette::random_near(clip.color, 0.25); - Cmd::Clip(PoolClipCommand::Add(index + 1, clip)) - }, - _ => return None - }) -} has_clips!(|self: PoolModel|self.clips); has_clip!(|self: PoolModel|self.clips().get(self.clip_index()).map(|c|c.clone())); impl PoolModel { @@ -330,71 +82,6 @@ impl PoolModel { } } } -command!(|self: FileBrowserCommand, state: PoolModel|{ - use PoolMode::*; - use FileBrowserCommand::*; - let mode = &mut state.mode; - 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; - PoolClipCommand::Import(index, path).execute(state)?; - } else if browser.is_dir() { - *mode = Some(Import(*index, browser.chdir()?)); - }, - _ => todo!(), - }, - Some(Export(index, ref mut browser)) => match self { - Cancel => { *mode = None; }, - Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); }, - Select(index) => { browser.index = index; }, - _ => unreachable!() - }, - _ => unreachable!(), - }; - None -}); -input_to_command!(FileBrowserCommand: |state: PoolModel, input: Event|{ - use FileBrowserCommand::*; - use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char}; - if let Some(PoolMode::Import(_index, browser)) = &state.mode { - match input { - kpat!(Up) => Select(browser.index.overflowing_sub(1).0 - .min(browser.len().saturating_sub(1))), - kpat!(Down) => Select(browser.index.saturating_add(1) - % browser.len()), - kpat!(Right) => Chdir(browser.cwd.clone()), - kpat!(Left) => Chdir(browser.cwd.clone()), - kpat!(Enter) => Confirm, - kpat!(Char(_)) => { todo!() }, - kpat!(Backspace) => { todo!() }, - kpat!(Esc) => Cancel, - _ => return None - } - } else if let Some(PoolMode::Export(_index, browser)) = &state.mode { - match input { - kpat!(Up) => Select(browser.index.overflowing_sub(1).0 - .min(browser.len())), - kpat!(Down) => Select(browser.index.saturating_add(1) - % browser.len()), - kpat!(Right) => Chdir(browser.cwd.clone()), - kpat!(Left) => Chdir(browser.cwd.clone()), - kpat!(Enter) => Confirm, - kpat!(Char(_)) => { todo!() }, - kpat!(Backspace) => { todo!() }, - kpat!(Esc) => Cancel, - _ => return None - } - } else { - unreachable!() - } -}); - /// Displays and edits clip length. #[derive(Clone)] pub struct ClipLength { @@ -407,7 +94,6 @@ pub struct ClipLength { /// Selected subdivision pub focus: Option, } - impl ClipLength { pub fn new (pulses: usize, focus: Option) -> Self { Self { ppq: PPQ, bpb: 4, pulses, focus } @@ -431,7 +117,6 @@ impl ClipLength { format!("{:>02}", self.ticks()).into() } } - /// Focused field of `ClipLength` #[derive(Copy, Clone, Debug)] pub enum ClipLengthFocus { @@ -442,104 +127,14 @@ pub enum ClipLengthFocus { /// Editing the number of ticks Tick, } - impl ClipLengthFocus { pub fn next (&mut self) { - *self = match self { - Self::Bar => Self::Beat, - Self::Beat => Self::Tick, - Self::Tick => Self::Bar, - } + *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, - } + *self = match self { Self::Bar => Self::Tick, Self::Beat => Self::Bar, Self::Tick => Self::Beat, } } } - -render!(TuiOut: (self: ClipLength) => { - let bars = ||self.bars_string(); - let beats = ||self.beats_string(); - let ticks = ||self.ticks_string(); - match self.focus { - None => - row!(" ", bars(), ".", beats(), ".", ticks()), - Some(ClipLengthFocus::Bar) => - row!("[", bars(), "]", beats(), ".", ticks()), - Some(ClipLengthFocus::Beat) => - row!(" ", bars(), "[", beats(), "]", ticks()), - Some(ClipLengthFocus::Tick) => - row!(" ", bars(), ".", beats(), "[", ticks()), - } -}); - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum ClipLengthCommand { - Begin, - Cancel, - Set(usize), - Next, - Prev, - Inc, - Dec, -} - -command!(|self: ClipLengthCommand,state:PoolModel|{ - use ClipLengthCommand::*; - use ClipLengthFocus::*; - match state.clips_mode_mut().clone() { - Some(PoolMode::Length(clip, ref mut length, ref mut focus)) => match self { - Cancel => { *state.clips_mode_mut() = None; }, - Self::Prev => { focus.prev() }, - Self::Next => { focus.next() }, - Self::Inc => match focus { - Bar => { *length += 4 * PPQ }, - Beat => { *length += PPQ }, - Tick => { *length += 1 }, - }, - Self::Dec => match focus { - Bar => { *length = length.saturating_sub(4 * PPQ) }, - Beat => { *length = length.saturating_sub(PPQ) }, - Tick => { *length = length.saturating_sub(1) }, - }, - Self::Set(length) => { - let mut old_length = None; - { - let mut clip = state.clips()[clip].clone();//.write().unwrap(); - old_length = Some(clip.read().unwrap().length); - clip.write().unwrap().length = length; - } - *state.clips_mode_mut() = None; - return Ok(old_length.map(Self::Set)) - }, - _ => unreachable!() - }, - _ => unreachable!() - }; - None -}); - -input_to_command!(ClipLengthCommand: |state: PoolModel, input: Event|{ - if let Some(PoolMode::Length(_, length, _)) = state.clips_mode() { - match input { - kpat!(Up) => Self::Inc, - kpat!(Down) => Self::Dec, - kpat!(Right) => Self::Next, - kpat!(Left) => Self::Prev, - kpat!(Enter) => Self::Set(*length), - kpat!(Esc) => Self::Cancel, - _ => return None - } - } else { - unreachable!() - } -}); -use crate::*; -use super::*; - #[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand { Begin, @@ -547,7 +142,6 @@ pub enum ClipRenameCommand { Confirm, Set(Arc), } - impl Command for ClipRenameCommand { fn execute (self, state: &mut PoolModel) -> Perhaps { use ClipRenameCommand::*; @@ -572,28 +166,3 @@ impl Command for ClipRenameCommand { Ok(None) } } - -impl InputToCommand for ClipRenameCommand { - fn input_to_command (state: &PoolModel, input: &Event) -> Option { - use KeyCode::{Char, Backspace, Enter, Esc}; - if let Some(PoolMode::Rename(_, ref old_name)) = state.clips_mode() { - Some(match input { - kpat!(Char(c)) => { - let mut new_name = old_name.clone().to_string(); - new_name.push(*c); - Self::Set(new_name.into()) - }, - kpat!(Backspace) => { - let mut new_name = old_name.clone().to_string(); - new_name.pop(); - Self::Set(new_name.into()) - }, - kpat!(Enter) => Self::Confirm, - kpat!(Esc) => Self::Cancel, - _ => return None - }) - } else { - unreachable!() - } - } -} diff --git a/midi/src/midi_pool_cmd.rs b/midi/src/midi_pool_cmd.rs new file mode 100644 index 00000000..88895601 --- /dev/null +++ b/midi/src/midi_pool_cmd.rs @@ -0,0 +1,363 @@ +use crate::*; +#[derive(Clone, PartialEq, Debug)] +pub enum PoolCommand { + Show(bool), + /// Update the contents of the clip pool + Clip(PoolClipCommand), + /// Select a clip from the clip pool + Select(usize), + /// Rename a clip + Rename(ClipRenameCommand), + /// Change the length of a clip + Length(ClipLengthCommand), + /// Import from file + Import(FileBrowserCommand), + /// Export to file + Export(FileBrowserCommand), +} +impl EdnCommand for PoolCommand { + fn from_edn <'a> (state: &PoolModel, head: &EdnItem<&str>, tail: &'a [EdnItem]) -> Self { + todo!() + } +} +#[derive(Clone, Debug, PartialEq)] +pub enum PoolClipCommand { + Add(usize, MidiClip), + Delete(usize), + Swap(usize, usize), + Import(usize, PathBuf), + Export(usize, PathBuf), + SetName(usize, Arc), + SetLength(usize, usize), + SetColor(usize, ItemColor), +} +impl PoolClipCommand { + pub fn from_edn <'a> (head: &EdnItem<&str>, tail: &'a [EdnItem]) -> Self { + todo!() + } +} +impl Command for PoolClipCommand { + fn execute (self, model: &mut T) -> Perhaps { + use PoolClipCommand::*; + Ok(match self { + Add(mut index, clip) => { + let clip = Arc::new(RwLock::new(clip)); + let mut clips = model.clips_mut(); + if index >= clips.len() { + index = clips.len(); + clips.push(clip) + } else { + clips.insert(index, clip); + } + Some(Self::Delete(index)) + }, + Delete(index) => { + let clip = model.clips_mut().remove(index).read().unwrap().clone(); + Some(Self::Add(index, clip)) + }, + Swap(index, other) => { + model.clips_mut().swap(index, other); + Some(Self::Swap(index, other)) + }, + Import(index, path) => { + let bytes = std::fs::read(&path)?; + let smf = Smf::parse(bytes.as_slice())?; + let mut t = 0u32; + let mut events = vec![]; + for track in smf.tracks.iter() { + for event in track.iter() { + t += event.delta.as_int(); + if let TrackEventKind::Midi { channel, message } = event.kind { + events.push((t, channel.as_int(), message)); + } + } + } + let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); + for event in events.iter() { + clip.notes[event.0 as usize].push(event.2); + } + Self::Add(index, clip).execute(model)? + }, + Export(_index, _path) => { + todo!("export clip to midi file"); + }, + SetName(index, name) => { + let clip = &mut model.clips_mut()[index]; + let old_name = clip.read().unwrap().name.clone(); + clip.write().unwrap().name = name; + Some(Self::SetName(index, old_name)) + }, + SetLength(index, length) => { + let clip = &mut model.clips_mut()[index]; + let old_len = clip.read().unwrap().length; + clip.write().unwrap().length = length; + Some(Self::SetLength(index, old_len)) + }, + SetColor(index, color) => { + let mut color = ItemPalette::from(color); + std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color); + Some(Self::SetColor(index, color.base)) + }, + }) + } +} +command!(|self:PoolCommand, state: PoolModel|{ + use PoolCommand::*; + match self { + Show(visible) => { + state.visible = visible; + Some(Self::Show(!visible)) + } + Rename(command) => match command { + ClipRenameCommand::Begin => { + let length = state.clips()[state.clip_index()].read().unwrap().length; + *state.clips_mode_mut() = Some( + PoolMode::Length(state.clip_index(), length, ClipLengthFocus::Bar) + ); + None + }, + _ => command.execute(state)?.map(Rename) + }, + Length(command) => match command { + ClipLengthCommand::Begin => { + let name = state.clips()[state.clip_index()].read().unwrap().name.clone(); + *state.clips_mode_mut() = Some( + PoolMode::Rename(state.clip_index(), name) + ); + None + }, + _ => command.execute(state)?.map(Length) + }, + Import(command) => match command { + FileBrowserCommand::Begin => { + *state.clips_mode_mut() = Some( + PoolMode::Import(state.clip_index(), FileBrowser::new(None)?) + ); + None + }, + _ => command.execute(state)?.map(Import) + }, + Export(command) => match command { + FileBrowserCommand::Begin => { + *state.clips_mode_mut() = Some( + PoolMode::Export(state.clip_index(), FileBrowser::new(None)?) + ); + None + }, + _ => command.execute(state)?.map(Export) + }, + Select(clip) => { + state.set_clip_index(clip); + None + }, + Clip(command) => command.execute(state)?.map(Clip), + } +}); +input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.clips_mode() { + Some(PoolMode::Rename(..)) => Self::Rename(ClipRenameCommand::input_to_command(state, input)?), + Some(PoolMode::Length(..)) => Self::Length(ClipLengthCommand::input_to_command(state, input)?), + Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), + Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?), + _ => to_clips_command(state, input)? +}); +fn to_clips_command (state: &PoolModel, input: &Event) -> Option { + use KeyCode::{Up, Down, Delete, Char}; + use PoolCommand as Cmd; + let index = state.clip_index(); + let count = state.clips().len(); + Some(match input { + kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin), + kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin), + kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin), + kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin), + kpat!(Char('c')) => Cmd::Clip(PoolClipCommand::SetColor(index, ItemColor::random())), + kpat!(Char('[')) | kpat!(Up) => Cmd::Select( + index.overflowing_sub(1).0.min(state.clips().len() - 1) + ), + kpat!(Char(']')) | kpat!(Down) => Cmd::Select( + index.saturating_add(1) % state.clips().len() + ), + kpat!(Char('<')) => if index > 1 { + state.set_clip_index(state.clip_index().saturating_sub(1)); + Cmd::Clip(PoolClipCommand::Swap(index - 1, index)) + } else { + return None + }, + kpat!(Char('>')) => if index < count.saturating_sub(1) { + state.set_clip_index(state.clip_index() + 1); + Cmd::Clip(PoolClipCommand::Swap(index + 1, index)) + } else { + return None + }, + kpat!(Delete) => if index > 0 { + state.set_clip_index(index.min(count.saturating_sub(1))); + Cmd::Clip(PoolClipCommand::Delete(index)) + } else { + return None + }, + kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(PoolClipCommand::Add(count, MidiClip::new( + "Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) + ))), + kpat!(Char('i')) => Cmd::Clip(PoolClipCommand::Add(index + 1, MidiClip::new( + "Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) + ))), + kpat!(Char('d')) | kpat!(Shift-Char('D')) => { + let mut clip = state.clips()[index].read().unwrap().duplicate(); + clip.color = ItemPalette::random_near(clip.color, 0.25); + Cmd::Clip(PoolClipCommand::Add(index + 1, clip)) + }, + _ => return None + }) +} +command!(|self: FileBrowserCommand, state: PoolModel|{ + use PoolMode::*; + use FileBrowserCommand::*; + let mode = &mut state.mode; + 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; + PoolClipCommand::Import(index, path).execute(state)?; + } else if browser.is_dir() { + *mode = Some(Import(*index, browser.chdir()?)); + }, + _ => todo!(), + }, + Some(Export(index, ref mut browser)) => match self { + Cancel => { *mode = None; }, + Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); }, + Select(index) => { browser.index = index; }, + _ => unreachable!() + }, + _ => unreachable!(), + }; + None +}); +input_to_command!(FileBrowserCommand: |state: PoolModel, input: Event|{ + use FileBrowserCommand::*; + use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char}; + if let Some(PoolMode::Import(_index, browser)) = &state.mode { + match input { + kpat!(Up) => Select(browser.index.overflowing_sub(1).0 + .min(browser.len().saturating_sub(1))), + kpat!(Down) => Select(browser.index.saturating_add(1) + % browser.len()), + kpat!(Right) => Chdir(browser.cwd.clone()), + kpat!(Left) => Chdir(browser.cwd.clone()), + kpat!(Enter) => Confirm, + kpat!(Char(_)) => { todo!() }, + kpat!(Backspace) => { todo!() }, + kpat!(Esc) => Cancel, + _ => return None + } + } else if let Some(PoolMode::Export(_index, browser)) = &state.mode { + match input { + kpat!(Up) => Select(browser.index.overflowing_sub(1).0 + .min(browser.len())), + kpat!(Down) => Select(browser.index.saturating_add(1) + % browser.len()), + kpat!(Right) => Chdir(browser.cwd.clone()), + kpat!(Left) => Chdir(browser.cwd.clone()), + kpat!(Enter) => Confirm, + kpat!(Char(_)) => { todo!() }, + kpat!(Backspace) => { todo!() }, + kpat!(Esc) => Cancel, + _ => return None + } + } else { + unreachable!() + } +}); +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ClipLengthCommand { + Begin, + Cancel, + Set(usize), + Next, + Prev, + Inc, + Dec, +} +command!(|self: ClipLengthCommand,state:PoolModel|{ + use ClipLengthCommand::*; + use ClipLengthFocus::*; + match state.clips_mode_mut().clone() { + Some(PoolMode::Length(clip, ref mut length, ref mut focus)) => match self { + Cancel => { *state.clips_mode_mut() = None; }, + Self::Prev => { focus.prev() }, + Self::Next => { focus.next() }, + Self::Inc => match focus { + Bar => { *length += 4 * PPQ }, + Beat => { *length += PPQ }, + Tick => { *length += 1 }, + }, + Self::Dec => match focus { + Bar => { *length = length.saturating_sub(4 * PPQ) }, + Beat => { *length = length.saturating_sub(PPQ) }, + Tick => { *length = length.saturating_sub(1) }, + }, + Self::Set(length) => { + let mut old_length = None; + { + let mut clip = state.clips()[clip].clone();//.write().unwrap(); + old_length = Some(clip.read().unwrap().length); + clip.write().unwrap().length = length; + } + *state.clips_mode_mut() = None; + return Ok(old_length.map(Self::Set)) + }, + _ => unreachable!() + }, + _ => unreachable!() + }; + None +}); + +input_to_command!(ClipLengthCommand: |state: PoolModel, input: Event|{ + if let Some(PoolMode::Length(_, length, _)) = state.clips_mode() { + match input { + kpat!(Up) => Self::Inc, + kpat!(Down) => Self::Dec, + kpat!(Right) => Self::Next, + kpat!(Left) => Self::Prev, + kpat!(Enter) => Self::Set(*length), + kpat!(Esc) => Self::Cancel, + _ => return None + } + } else { + unreachable!() + } +}); +use crate::*; +use super::*; + + +impl InputToCommand for ClipRenameCommand { + fn input_to_command (state: &PoolModel, input: &Event) -> Option { + use KeyCode::{Char, Backspace, Enter, Esc}; + if let Some(PoolMode::Rename(_, ref old_name)) = state.clips_mode() { + Some(match input { + kpat!(Char(c)) => { + let mut new_name = old_name.clone().to_string(); + new_name.push(*c); + Self::Set(new_name.into()) + }, + kpat!(Backspace) => { + let mut new_name = old_name.clone().to_string(); + new_name.pop(); + Self::Set(new_name.into()) + }, + kpat!(Enter) => Self::Confirm, + kpat!(Esc) => Self::Cancel, + _ => return None + }) + } else { + unreachable!() + } + } +} diff --git a/midi/src/midi_pool_tui.rs b/midi/src/midi_pool_tui.rs new file mode 100644 index 00000000..03e846e9 --- /dev/null +++ b/midi/src/midi_pool_tui.rs @@ -0,0 +1,41 @@ +use crate::*; +pub struct PoolView<'a>(pub bool, pub &'a PoolModel); +render!(TuiOut: (self: PoolView<'a>) => { + let Self(compact, model) = self; + let PoolModel { clips, mode, .. } = self.1; + let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||TuiTheme::g(32).into()); + let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); + let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); + let iter = | |model.clips().clone().into_iter(); + Tui::bg(Color::Reset, Fixed::y(clips.read().unwrap().len() as u16, on_bg(border(Map::new(iter, move|clip, i|{ + let item_height = 1; + let item_offset = i as u16 * item_height; + let selected = i == model.clip_index(); + let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); + let bg = if selected { color.light.rgb } else { color.base.rgb }; + let fg = color.lightest.rgb; + let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; + let length = if *compact { String::default() } else { format!("{length} ") }; + Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( + Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), + Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), + Fill::x(Align::w(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶"))))), + Fill::x(Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀"))))), + )))) + }))))) +}); +render!(TuiOut: (self: ClipLength) => { + let bars = ||self.bars_string(); + let beats = ||self.beats_string(); + let ticks = ||self.ticks_string(); + match self.focus { + None => + row!(" ", bars(), ".", beats(), ".", ticks()), + Some(ClipLengthFocus::Bar) => + row!("[", bars(), "]", beats(), ".", ticks()), + Some(ClipLengthFocus::Beat) => + row!(" ", bars(), "[", beats(), "]", ticks()), + Some(ClipLengthFocus::Tick) => + row!(" ", bars(), ".", beats(), "[", ticks()), + } +}); diff --git a/tek/src/arranger.rs b/tek/src/arranger.rs index 8b3a1daa..410aed32 100644 --- a/tek/src/arranger.rs +++ b/tek/src/arranger.rs @@ -17,12 +17,12 @@ command!(|self: TrackCommand, state: App|match self { _ => todo!("track command" impl EdnCommand for ClipCommand { fn from_edn <'a> (state: &App, head: &EdnItem<&str>, tail: &'a [EdnItem]) -> Self { match (head, tail) { - (Key("get"), [a, b ]) => Self::Get(0, 0), - (Key("put"), [a, b, c ]) => Self::Put(0, 0, None), - (Key("enqueue"), [a, b ]) => Self::Enqueue(0, 0), - (Key("edit"), [a ]) => Self::Edit(None), - (Key("loop"), [a, b, c ]) => Self::SetLoop(0, 0, true), - (Key("color"), [a, b, c ]) => Self::SetColor(0, 0, ItemPalette::random()), + (Sym("get"), [a, b ]) => Self::Get(0, 0), + (Sym("put"), [a, b, c ]) => Self::Put(0, 0, None), + (Sym("enqueue"), [a, b ]) => Self::Enqueue(0, 0), + (Sym("edit"), [a ]) => Self::Edit(None), + (Sym("loop"), [a, b, c ]) => Self::SetLoop(0, 0, true), + (Sym("color"), [a, b, c ]) => Self::SetColor(0, 0, ItemPalette::random()), _ => panic!(), } } @@ -39,13 +39,13 @@ impl EdnCommand for ClipCommand { impl EdnCommand for SceneCommand { fn from_edn <'a> (state: &App, head: &EdnItem<&str>, tail: &'a [EdnItem]) -> Self { match (head, tail) { - (Key("add"), [ ]) => Self::Add, - (Key("del"), [a ]) => Self::Del(0), - (Key("swap"), [a, b ]) => Self::Swap(0, 0), - (Key("size"), [a ]) => Self::SetSize(0), - (Key("zoom"), [a, ]) => Self::SetZoom(0), - (Key("color"), [a, b, ]) => Self::SetColor(0, ItemPalette::random()), - (Key("enqueue"), [a, ]) => Self::Enqueue(0), + (Sym("add"), [ ]) => Self::Add, + (Sym("del"), [a ]) => Self::Del(0), + (Sym("swap"), [a, b ]) => Self::Swap(0, 0), + (Sym("size"), [a ]) => Self::SetSize(0), + (Sym("zoom"), [a, ]) => Self::SetZoom(0), + (Sym("color"), [a, b, ]) => Self::SetColor(0, ItemPalette::random()), + (Sym("enqueue"), [a, ]) => Self::Enqueue(0), _ => panic!(), } } @@ -62,13 +62,13 @@ impl EdnCommand for SceneCommand { impl EdnCommand for TrackCommand { fn from_edn <'a> (state: &App, head: &EdnItem<&str>, tail: &'a [EdnItem]) -> Self { match (head, tail) { - (Key("add"), [ ]) => Self::Add, - (Key("del"), [a ]) => Self::Del(0), - (Key("stop"), [a ]) => Self::Stop(0), - (Key("swap"), [a, b ]) => Self::Swap(0, 0), - (Key("size"), [a ]) => Self::SetSize(0), - (Key("zoom"), [a, ]) => Self::SetZoom(0), - (Key("color"), [a, b, ]) => Self::SetColor(0, ItemPalette::random()), + (Sym("add"), [ ]) => Self::Add, + (Sym("del"), [a ]) => Self::Del(0), + (Sym("stop"), [a ]) => Self::Stop(0), + (Sym("swap"), [a, b ]) => Self::Swap(0, 0), + (Sym("size"), [a ]) => Self::SetSize(0), + (Sym("zoom"), [a, ]) => Self::SetZoom(0), + (Sym("color"), [a, b, ]) => Self::SetColor(0, ItemPalette::random()), _ => panic!(), } }