diff --git a/config/config_arranger.edn b/config/config_arranger.edn index 6ee23998..a35182bf 100644 --- a/config/config_arranger.edn +++ b/config/config_arranger.edn @@ -13,10 +13,9 @@ (keys (layer-if :focus-message "./keys_message.edn") (layer-if :focus-device-add "./keys_device_add.edn") - (layer-if :focus-pool-import "./keys_pool_file.edn") - (layer-if :focus-pool-export "./keys_pool_file.edn") - (layer-if :focus-pool-rename "./keys_clip_rename.edn") - (layer-if :focus-pool-length "./keys_clip_length.edn") + (layer-if :focus-browser "./keys_browser") + (layer-if :focus-pool-rename "./keys_rename.edn") + (layer-if :focus-pool-length "./keys_length.edn") (layer "./keys_global.edn") (layer-if :focus-editor "./keys_editor.edn") (layer-if :focus-clip "./keys_clip.edn") diff --git a/config/config_groovebox.edn b/config/config_groovebox.edn index 3f63ce1d..67b0e5f9 100644 --- a/config/config_groovebox.edn +++ b/config/config_groovebox.edn @@ -15,10 +15,9 @@ (fill/y :view-editor))))))))))) (keys - (layer-if :focus-pool-import "./keys_pool_file.edn") - (layer-if :focus-pool-export "./keys_pool_file.edn") - (layer-if :focus-pool-rename "./keys_clip_rename.edn") - (layer-if :focus-pool-length "./keys_clip_length.edn") + (layer-if :focus-browser "./keys_browser") + (layer-if :focus-pool-rename "./keys_rename.edn") + (layer-if :focus-pool-length "./keys_length.edn") (layer "./keys_global.edn") (layer "./keys_clock.edn") (layer "./keys_editor.edn") diff --git a/config/config_sequencer.edn b/config/config_sequencer.edn index 5a6d4029..e09c0e7b 100644 --- a/config/config_sequencer.edn +++ b/config/config_sequencer.edn @@ -11,10 +11,9 @@ :view-editor))))) (keys - (layer-if :mode-pool-import "./keys_pool_file.edn") - (layer-if :mode-pool-export "./keys_pool_file.edn") - (layer-if :mode-pool-rename "./keys_clip_rename.edn") - (layer-if :mode-pool-length "./keys_clip_length.edn") + (layer-if :focus-browser "./keys_browser") + (layer-if :mode-pool-rename "./keys_rename.edn") + (layer-if :mode-pool-length "./keys_length.edn") (layer "./keys_global.edn") (layer "./keys_editor.edn") (layer "./keys_clock.edn") diff --git a/config/keys_browser.edn b/config/keys_browser.edn new file mode 100644 index 00000000..b950e989 --- /dev/null +++ b/config/keys_browser.edn @@ -0,0 +1,8 @@ +(@escape browser cancel) +(@return browser confirm) +(@up browser set-cursor :browser-cursor-prev) +(@down browser set-cursor :browser-cursor-next) +(@right browser set-address :browser-address-selected) +(@left browser set-address :browser-address-parent) +(:char browser append-to-search ;char) +(@backspace browser delete-from-search :last) diff --git a/config/keys_device_add.edn b/config/keys_device_add.edn index 139cf84e..6e557b0b 100644 --- a/config/keys_device_add.edn +++ b/config/keys_device_add.edn @@ -1,4 +1,3 @@ -(@esc device cancel) (@up device pick :device-kind-prev) (@down device pick :device-kind-next) (@enter device add :device-kind) diff --git a/config/keys_global.edn b/config/keys_global.edn index 773a139d..af89f616 100644 --- a/config/keys_global.edn +++ b/config/keys_global.edn @@ -1,4 +1,4 @@ -(@esc toggle-dialog :dialog-menu) +(@esc cancel-dialog) (@f1 toggle-dialog :dialog-help) (@f6 toggle-dialog :dialog-save) (@f8 toggle-dialog :dialog-options) diff --git a/config/keys_clip_length.edn b/config/keys_length.edn similarity index 100% rename from config/keys_clip_length.edn rename to config/keys_length.edn diff --git a/config/keys_clip_rename.edn b/config/keys_rename.edn similarity index 100% rename from config/keys_clip_rename.edn rename to config/keys_rename.edn diff --git a/crates/app/examples/midi_import.rs b/crates/app/examples/midi_import.rs index 339bb880..d2cceae2 100644 --- a/crates/app/examples/midi_import.rs +++ b/crates/app/examples/midi_import.rs @@ -11,7 +11,7 @@ impl HasClips for ExampleClips { } } fn main () -> Result<(), Box> { - let mut clips = MidiPool::default();//ExampleClips(Arc::new(vec![].into())); + let mut clips = Pool::default();//ExampleClips(Arc::new(vec![].into())); PoolClipCommand::Import { index: 0, path: std::path::PathBuf::from("./example.mid") diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 8e2e26fb..28076419 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -184,44 +184,15 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm } } -#[tengri_proc::expose] impl MidiPool { - fn _todo_bool_stub (&self) -> bool { - todo!() - } - fn _todo_path_buf_stub (&self) -> PathBuf { - todo!() - } - fn _todo_arc_str_stub (&self) -> Arc { - todo!() - } - fn clip_new (&self) -> MidiClip { - self.new_clip() - } - fn clip_cloned (&self) -> MidiClip { - self.cloned_clip() - } - fn clip_index_current (&self) -> usize { - 0 - } - fn clip_index_after (&self) -> usize { - 0 - } - fn clip_index_previous (&self) -> usize { - 0 - } - fn clip_index_next (&self) -> usize { - 0 - } - fn color_random (&self) -> ItemColor { - ItemColor::random() - } -} - #[tengri_proc::command(App)] impl AppCommand { fn toggle_dialog (app: &mut App, dialog: Dialog) -> Perhaps { app.toggle_dialog(Some(dialog)); Ok(None) } + fn cancel_dialog (app: &mut App) -> Perhaps { + app.toggle_dialog(None); + Ok(None) + } fn toggle_editor (app: &mut App, value: bool) -> Perhaps { app.toggle_editor(Some(value)); Ok(None) @@ -489,275 +460,3 @@ impl<'state> Context<'state, SamplerCommand> for App { todo!() } } - -#[tengri_proc::command(MidiPool)] impl PoolCommand { - /// Toggle visibility of pool - fn show (pool: &mut MidiPool, visible: bool) -> Perhaps { - pool.visible = visible; - Ok(Some(Self::Show { visible: !visible })) - } - /// Select a clip from the clip pool - fn select (pool: &mut MidiPool, index: usize) -> Perhaps { - pool.set_clip_index(index); - Ok(None) - } - /// Rename a clip - fn rename (pool: &mut MidiPool, command: ClipRenameCommand) -> Perhaps { - Ok(match command { - ClipRenameCommand::Begin => { - pool.begin_clip_rename(); - None - }, - _ => command.delegate(pool, |command|Self::Rename{command})? - }) - } - /// Change the length of a clip - fn length (pool: &mut MidiPool, command: ClipLengthCommand) -> Perhaps { - Ok(match command { - ClipLengthCommand::Begin => { - pool.begin_clip_length(); - None - }, - _ => command.delegate(pool, |command|Self::Length{command})? - }) - } - /// Import from file - fn import (pool: &mut MidiPool, command: FileBrowserCommand) -> Perhaps { - Ok(match command { - FileBrowserCommand::Begin => { - pool.begin_import(); - None - }, - _ => command.delegate(pool, |command|Self::Import{command})? - }) - } - /// Export to file - fn export (pool: &mut MidiPool, command: FileBrowserCommand) -> Perhaps { - Ok(match command { - FileBrowserCommand::Begin => { - pool.begin_export(); - None - }, - _ => command.delegate(pool, |command|Self::Export{command})? - }) - } - /// Update the contents of the clip pool - fn clip (pool: &mut MidiPool, command: PoolClipCommand) -> Perhaps { - Ok(command.execute(pool)?.map(|command|Self::Clip{command})) - } -} - -#[tengri_proc::command(MidiPool)] impl PoolClipCommand { - fn add (pool: &mut MidiPool, index: usize, clip: MidiClip) -> Perhaps { - let mut index = index; - let clip = Arc::new(RwLock::new(clip)); - let mut clips = pool.clips_mut(); - if index >= clips.len() { - index = clips.len(); - clips.push(clip) - } else { - clips.insert(index, clip); - } - Ok(Some(Self::Delete { index })) - } - fn delete (pool: &mut MidiPool, index: usize) -> Perhaps { - let clip = pool.clips_mut().remove(index).read().unwrap().clone(); - Ok(Some(Self::Add { index, clip })) - } - fn swap (pool: &mut MidiPool, index: usize, other: usize) -> Perhaps { - pool.clips_mut().swap(index, other); - Ok(Some(Self::Swap { index, other })) - } - fn import (pool: &mut MidiPool, index: usize, path: PathBuf) -> Perhaps { - 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); - } - Ok(Self::Add { index, clip }.execute(pool)?) - } - fn export (pool: &mut MidiPool, index: usize, path: PathBuf) -> Perhaps { - todo!("export clip to midi file"); - } - fn set_name (pool: &mut MidiPool, index: usize, name: Arc) -> Perhaps { - let clip = &mut pool.clips_mut()[index]; - let old_name = clip.read().unwrap().name.clone(); - clip.write().unwrap().name = name; - Ok(Some(Self::SetName { index, name: old_name })) - } - fn set_length (pool: &mut MidiPool, index: usize, length: usize) -> Perhaps { - let clip = &mut pool.clips_mut()[index]; - let old_len = clip.read().unwrap().length; - clip.write().unwrap().length = length; - Ok(Some(Self::SetLength { index, length: old_len })) - } - fn set_color (pool: &mut MidiPool, index: usize, color: ItemColor) -> Perhaps { - let mut color = ItemTheme::from(color); - std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color); - Ok(Some(Self::SetColor { index, color: color.base })) - } -} - -#[tengri_proc::command(MidiPool)] impl ClipRenameCommand { - fn begin (pool: &mut MidiPool) -> Perhaps { - unreachable!(); - } - fn cancel (pool: &mut MidiPool) -> Perhaps { - if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { - pool.clips()[clip].write().unwrap().name = old_name.clone().into(); - } - return Ok(None) - } - fn confirm (pool: &mut MidiPool) -> Perhaps { - if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { - let old_name = old_name.clone(); - *pool.mode_mut() = None; - return Ok(Some(Self::Set { value: old_name })) - } - return Ok(None) - } - fn set (pool: &mut MidiPool, value: Arc) -> Perhaps { - if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { - pool.clips()[clip].write().unwrap().name = value; - } - return Ok(None) - } -} - -#[tengri_proc::command(MidiPool)] impl ClipLengthCommand { - fn begin (pool: &mut MidiPool) -> Perhaps { - unreachable!() - } - fn cancel (pool: &mut MidiPool) -> Perhaps { - if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { - *pool.mode_mut() = None; - } - Ok(None) - } - fn set (pool: &mut MidiPool, length: usize) -> Perhaps { - if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) - = pool.mode_mut().clone() - { - let old_length; - { - let clip = pool.clips()[clip].clone();//.write().unwrap(); - old_length = Some(clip.read().unwrap().length); - clip.write().unwrap().length = *length; - } - *pool.mode_mut() = None; - return Ok(old_length.map(|length|Self::Set { length })) - } - Ok(None) - } - fn next (pool: &mut MidiPool) -> Perhaps { - if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) - = pool.mode_mut().clone() - { - focus.next() - } - Ok(None) - } - fn prev (pool: &mut MidiPool) -> Perhaps { - if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) - = pool.mode_mut().clone() - { - focus.prev() - } - Ok(None) - } - fn inc (pool: &mut MidiPool) -> Perhaps { - if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) - = pool.mode_mut().clone() - { - match focus { - ClipLengthFocus::Bar => { *length += 4 * PPQ }, - ClipLengthFocus::Beat => { *length += PPQ }, - ClipLengthFocus::Tick => { *length += 1 }, - } - } - Ok(None) - } - fn dec (pool: &mut MidiPool) -> Perhaps { - if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) - = pool.mode_mut().clone() - { - match focus { - ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) }, - ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) }, - ClipLengthFocus::Tick => { *length = length.saturating_sub(1) }, - } - } - Ok(None) - } -} - -#[tengri_proc::command(MidiPool)] impl FileBrowserCommand { - fn begin (pool: &mut MidiPool) -> Perhaps { - unreachable!(); - } - fn cancel (pool: &mut MidiPool) -> Perhaps { - pool.mode = None; - Ok(None) - } - fn confirm (pool: &mut MidiPool) -> Perhaps { - Ok(match pool.mode { - Some(PoolMode::Import(index, ref mut browser)) => { - if browser.is_file() { - let path = browser.path(); - pool.mode = None; - let _undo = PoolClipCommand::import(pool, index, path)?; - None - } else if browser.is_dir() { - pool.mode = Some(PoolMode::Import(index, browser.chdir()?)); - None - } else { - None - } - }, - Some(PoolMode::Export(index, ref mut browser)) => { - todo!() - }, - _ => unreachable!(), - }) - } - fn select (pool: &mut MidiPool, index: usize) -> Perhaps { - Ok(match pool.mode { - Some(PoolMode::Import(index, ref mut browser)) => { - browser.index = index; - None - }, - Some(PoolMode::Export(index, ref mut browser)) => { - browser.index = index; - None - }, - _ => unreachable!(), - }) - } - fn chdir (pool: &mut MidiPool, dir: PathBuf) -> Perhaps { - Ok(match pool.mode { - Some(PoolMode::Import(index, ref mut browser)) => { - pool.mode = Some(PoolMode::Import(index, FileBrowser::new(Some(dir))?)); - None - }, - Some(PoolMode::Export(index, ref mut browser)) => { - pool.mode = Some(PoolMode::Export(index, FileBrowser::new(Some(dir))?)); - None - }, - _ => unreachable!(), - }) - } - fn filter (pool: &mut MidiPool, filter: Arc) -> Perhaps { - todo!() - } -} diff --git a/crates/app/src/config.rs b/crates/app/src/config.rs index e65313bd..3b3b3cac 100644 --- a/crates/app/src/config.rs +++ b/crates/app/src/config.rs @@ -183,15 +183,15 @@ pub const DEFAULT_CONFIGS: &'static [(&'static str, &'static str)] = &[ default_config!("../../../config/keys_arranger.edn"), default_config!("../../../config/keys_clip.edn"), - default_config!("../../../config/keys_clip_length.edn"), - default_config!("../../../config/keys_clip_rename.edn"), default_config!("../../../config/keys_clock.edn"), default_config!("../../../config/keys_editor.edn"), default_config!("../../../config/keys_global.edn"), default_config!("../../../config/keys_groovebox.edn"), + default_config!("../../../config/keys_length.edn"), default_config!("../../../config/keys_mix.edn"), default_config!("../../../config/keys_pool.edn"), default_config!("../../../config/keys_pool_file.edn"), + default_config!("../../../config/keys_rename.edn"), default_config!("../../../config/keys_sampler.edn"), default_config!("../../../config/keys_scene.edn"), default_config!("../../../config/keys_sequencer.edn"), diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 46b52d58..074ca999 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -15,7 +15,7 @@ pub struct App { /// Theme pub color: ItemTheme, /// Contains all clips in the project - pub pool: Option, + pub pool: Option, /// Contains the currently edited MIDI clip pub editor: Option, /// Contains a render of the project arrangement, redrawn on update. @@ -275,7 +275,7 @@ impl App { } /// Get the clip pool, if present - pub(crate) fn pool (&self) -> Option<&MidiPool> { + pub(crate) fn pool (&self) -> Option<&Pool> { self.pool.as_ref() } diff --git a/crates/app/src/model/pool.rs b/crates/app/src/model/pool.rs index 5ef5e01b..e69de29b 100644 --- a/crates/app/src/model/pool.rs +++ b/crates/app/src/model/pool.rs @@ -1,203 +0,0 @@ -use crate::*; - -#[derive(Debug)] -pub struct MidiPool { - pub visible: bool, - /// Collection of clips - pub clips: Arc>>>>, - /// Selected clip - pub clip: AtomicUsize, - /// Mode switch - pub mode: Option, -} - -impl Default for MidiPool { - fn default () -> Self { - use PoolMode::*; - Self { - visible: true, - clips: Arc::from(RwLock::from(vec![])), - clip: 0.into(), - mode: None, - } - } -} - -has_clips!(|self: MidiPool|self.clips); - -has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone())); - -from!(|clip:&Arc>|MidiPool = { - let model = Self::default(); - model.clips.write().unwrap().push(clip.clone()); - model.clip.store(1, Relaxed); - model -}); - -impl MidiPool { - pub fn clip_index (&self) -> usize { - self.clip.load(Relaxed) - } - pub fn set_clip_index (&self, value: usize) { - self.clip.store(value, Relaxed); - } - pub fn mode (&self) -> &Option { - &self.mode - } - pub fn mode_mut (&mut self) -> &mut Option { - &mut self.mode - } - pub fn begin_clip_length (&mut self) { - let length = self.clips()[self.clip_index()].read().unwrap().length; - *self.mode_mut() = Some(PoolMode::Length( - self.clip_index(), - length, - ClipLengthFocus::Bar - )); - } - pub fn begin_clip_rename (&mut self) { - let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); - *self.mode_mut() = Some(PoolMode::Rename( - self.clip_index(), - name - )); - } - pub fn begin_import (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Import( - self.clip_index(), - FileBrowser::new(None)? - )); - Ok(()) - } - pub fn begin_export (&mut self) -> Usually<()> { - *self.mode_mut() = Some(PoolMode::Export( - self.clip_index(), - FileBrowser::new(None)? - )); - Ok(()) - } - pub fn new_clip (&self) -> MidiClip { - MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) - } - pub fn cloned_clip (&self) -> MidiClip { - let index = self.clip_index(); - let mut clip = self.clips()[index].read().unwrap().duplicate(); - clip.color = ItemTheme::random_near(clip.color, 0.25); - clip - } - pub fn add_new_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(self.new_clip())); - let index = { - let mut clips = self.clips.write().unwrap(); - clips.push(clip.clone()); - clips.len().saturating_sub(1) - }; - self.clip.store(index, Relaxed); - (index, clip) - } - pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { - let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); - if let Some(index) = index { - self.clips.write().unwrap().remove(index); - return true - } - false - } -} - -/// 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), -} - -/// Focused field of `ClipLength` -#[derive(Copy, Clone, Debug)] -pub enum ClipLengthFocus { - /// Editing the number of bars - Bar, - /// Editing the number of beats - Beat, - /// Editing the number of ticks - Tick, -} - -impl ClipLengthFocus { - pub fn next (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } - } - pub fn prev (&mut self) { - use ClipLengthFocus::*; - *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } - } -} - -/// Displays and edits clip length. -#[derive(Clone)] -pub struct ClipLength { - /// Pulses per beat (quaver) - ppq: usize, - /// Beats per bar - bpb: usize, - /// Length of clip in pulses - pulses: usize, - /// Selected subdivision - pub focus: Option, -} - -impl ClipLength { - pub fn _new (pulses: usize, focus: Option) -> Self { - Self { ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { - self.pulses / (self.bpb * self.ppq) - } - pub fn beats (&self) -> usize { - (self.pulses % (self.bpb * self.ppq)) / self.ppq - } - pub fn ticks (&self) -> usize { - self.pulses % self.ppq - } - pub fn bars_string (&self) -> Arc { - format!("{}", self.bars()).into() - } - pub fn beats_string (&self) -> Arc { - format!("{}", self.beats()).into() - } - pub fn ticks_string (&self) -> Arc { - format!("{:>02}", self.ticks()).into() - } -} - -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>; - fn add_clip (&self) -> (usize, Arc>) { - let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); - self.clips_mut().push(clip.clone()); - (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),*>)? { - fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { - $cb.read().unwrap() - } - fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { - $cb.write().unwrap() - } - } - } -} diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index 81ab2239..de290bcf 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -2,6 +2,7 @@ use crate::*; pub(crate) use std::fmt::Write; pub(crate) use ::tengri::tui::ratatui::prelude::Position; +mod view_dialog; pub use self::view_dialog::*; mod view_output; pub use self::view_output::*; #[tengri_proc::view(TuiOut)] @@ -49,85 +50,11 @@ impl App { self.sampler().map(|s|s.view_meters_output()) } pub fn view_dialog (&self) -> impl Content + use<'_> { - When::new(self.dialog.is_some(), Bsp::b( - Fill::xy(Tui::fg_bg(Rgb(64,64,64), Rgb(32,32,32), "")), - Fixed::xy(30, 15, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b( - Repeat(" "), - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(self.dialog.as_ref().map(|dialog|match dialog { - Dialog::Menu => self.view_dialog_menu().boxed(), - Dialog::Help => self.view_dialog_help().boxed(), - Dialog::Save => self.view_dialog_save().boxed(), - Dialog::Load => self.view_dialog_load().boxed(), - Dialog::Options => self.view_dialog_options().boxed(), - Dialog::Device(index) => self.view_dialog_device(*index).boxed(), - Dialog::Message(message) => self.view_dialog_message(message).boxed(), - })) - ))) - )) + view_dialog(self) } } impl App { - fn view_dialog_menu (&self) -> impl Content { - let options = ||["Projects", "Settings", "Help", "Quit"].iter(); - let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); - Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) - } - fn view_dialog_help (&self) -> impl Content + use<'_> { - let bindings = ||self.config.keys.layers.iter() - .filter_map(|a|(a.0)(self).then_some(a.1)) - .flat_map(|a|a) - .filter_map(|x|if let Value::Exp(_, iter)=x.value{ - Some(iter) - } else { - None - }); - //let binding = ;[> Bsp::e( - //Fixed::x(15, Align::w(Tui::bold(true, Tui::fg(Rgb(255,192,0), if let Some(Token { - //value: Value::Sym(key), .. - //}) = binding.next() { - //Some(key.to_string()) - //} else { - //None - //})))), - //Bsp::e(" ", Tui::fg(Rgb(255,255,255), if let Some(Token { - //value: Value::Key(command), .. - //}) = binding.next() { - //Some(command.to_string()) - //} else { - //None - //})), - //);*/ - Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1, bindings, |b,i|format!("{i}:{b:?}")))) - //|mut binding: TokenIter, _|Map::east(5, move||binding.clone(), |_,_|"kyp")))) - } - - fn view_dialog_device (&self, index: usize) -> impl Content + use<'_> { - let choices = ||self.device_kinds().iter(); - let choice = move|label, i| - Fill::x(Tui::bg(if i == index { Rgb(64,128,32) } else { Rgb(0,0,0) }, - Bsp::e(if i == index { "[ " } else { " " }, - Bsp::w(if i == index { " ]" } else { " " }, - label)))); - Bsp::s(Tui::bold(true, "Add device"), Map::south(1, choices, choice)) - } - - fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content + use<'a> { - Bsp::s(message, Bsp::s("", "[ OK ]")) - } - - fn view_dialog_save <'a> (&'a self) -> impl Content + use<'a> { - "WIP: SAVE" - } - - fn view_dialog_load <'a> (&'a self) -> impl Content + use<'a> { - "WIP: LOAD" - } - - fn view_dialog_options <'a> (&'a self) -> impl Content + use<'a> { - "WIP: OPTIONS" - } /// Spacing between tracks. pub(crate) const TRACK_SPACING: usize = 0; @@ -752,44 +679,3 @@ impl ViewCache { } } } - -pub struct PoolView<'a>(pub bool, pub &'a MidiPool); - -content!(TuiOut: |self: PoolView<'a>| { - let Self(compact, model) = self; - let MidiPool { clips, .. } = self.1; - //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::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(); - let height = clips.read().unwrap().len() as u16; - Tui::bg(Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc>, 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::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), - Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), - )))) - }))))) -}); - -content!(TuiOut: |self: ClipLength| { - use ClipLengthFocus::*; - let bars = ||self.bars_string(); - let beats = ||self.beats_string(); - let ticks = ||self.ticks_string(); - match self.focus { - None => row!(" ", bars(), ".", beats(), ".", ticks()), - Some(Bar) => row!("[", bars(), "]", beats(), ".", ticks()), - Some(Beat) => row!(" ", bars(), "[", beats(), "]", ticks()), - Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), - } -}); diff --git a/crates/app/src/view/view_dialog.rs b/crates/app/src/view/view_dialog.rs new file mode 100644 index 00000000..5ab8f1ef --- /dev/null +++ b/crates/app/src/view/view_dialog.rs @@ -0,0 +1,99 @@ +use crate::*; + +pub(crate) fn view_dialog (app: &App) -> impl Content + use<'_> { + When::new(app.dialog.is_some(), Bsp::b( + "", + Fixed::xy(70, 23, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b( + Repeat(" "), + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(app.dialog.as_ref().map(|dialog|match dialog { + Dialog::Menu => app.view_dialog_menu().boxed(), + Dialog::Help => app.view_dialog_help().boxed(), + Dialog::Save => app.view_dialog_save().boxed(), + Dialog::Load => app.view_dialog_load().boxed(), + Dialog::Options => app.view_dialog_options().boxed(), + Dialog::Device(index) => app.view_dialog_device(*index).boxed(), + Dialog::Message(message) => app.view_dialog_message(message).boxed(), + })) + ))) + )) +} + +impl App { + pub fn view_dialog_menu (&self) -> impl Content { + let options = ||["Projects", "Settings", "Help", "Quit"].iter(); + let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); + Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) + } + pub fn view_dialog_help <'a> (&'a self) -> impl Content + use<'a> { + //let bindings = ; + //let binding = ;[> Bsp::e( + //Fixed::x(15, Align::w(Tui::bold(true, Tui::fg(Rgb(255,192,0), if let Some(Token { + //value: Value::Sym(key), .. + //}) = binding.next() { + //Some(key.to_string()) + //} else { + //None + //})))), + //Bsp::e(" ", Tui::fg(Rgb(255,255,255), if let Some(Token { + //value: Value::Key(command), .. + //}) = binding.next() { + //Some(command.to_string()) + //} else { + //None + //})), + //);*/ + Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1, + ||self.config.keys.layers.iter() + .filter_map(|a|(a.0)(self).then_some(a.1)) + .flat_map(|a|a) + .filter_map(|x|if let Value::Exp(_, iter)=x.value{ + Some(iter) + } else { + None + }) + .take(20), + |mut b,i|Bsp::e( + Min::x(30, Max::x(60, format!("?"))), + b.next().map(|t|Min::x(16, Tui::fg(Rgb(224,64,0), format!("{}", t.value)))), + )))) + //format!("{b:?}"))))) + //|mut binding: TokenIter, _|Map::east(5, move||binding.clone(), |_,_|"kyp")))) + } + + pub fn view_dialog_device (&self, index: usize) -> impl Content + use<'_> { + let choices = ||self.device_kinds().iter(); + let choice = move|label, i| + Fill::x(Tui::bg(if i == index { Rgb(64,128,32) } else { Rgb(0,0,0) }, + Bsp::e(if i == index { "[ " } else { " " }, + Bsp::w(if i == index { " ]" } else { " " }, + label)))); + Bsp::s(Tui::bold(true, "Add device"), Map::south(1, choices, choice)) + } + + pub fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content + use<'a> { + Bsp::s(message, Bsp::s("", "[ OK ]")) + } + + pub fn view_dialog_save <'a> (&'a self) -> impl Content + use<'a> { + Bsp::s( + Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + Tui::bold(true, " Save project: "), + Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::xy("todo file browser"))) + } + + pub fn view_dialog_load <'a> (&'a self) -> impl Content + use<'a> { + Bsp::s( + Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + Tui::bold(true, " Load project: "), + Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::xy("todo file browser"))) + } + + pub fn view_dialog_options <'a> (&'a self) -> impl Content + use<'a> { + "TODO" + } +} diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index a6529854..eff1dea7 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -16,13 +16,15 @@ wavers = { workspace = true, optional = true } winit = { workspace = true, optional = true } [features] -default = [ "clock", "editor", "sequencer", "sampler", "lv2" ] +default = [ "browser", "clock", "editor", "sequencer", "sampler", "lv2" ] clock = [] editor = [] meter = [] mixer = [] -sequencer = [ "clock", "uuid" ] -sampler = [ "meter", "mixer", "symphonia", "wavers" ] +browser = [] +pool = [] +sequencer = [ "clock", "uuid", "pool" ] +sampler = [ "meter", "mixer", "browser", "symphonia", "wavers" ] lv2 = [ "livi", "winit" ] vst2 = [] vst3 = [] diff --git a/crates/device/src/browser.rs b/crates/device/src/browser.rs new file mode 100644 index 00000000..bba19e76 --- /dev/null +++ b/crates/device/src/browser.rs @@ -0,0 +1,3 @@ +mod browser_api; pub use self::browser_api::*; +mod browser_model; pub use self::browser_model::*; +mod browser_view; pub use self::browser_view::*; diff --git a/crates/device/src/browser/browser_api.rs b/crates/device/src/browser/browser_api.rs new file mode 100644 index 00000000..349a895b --- /dev/null +++ b/crates/device/src/browser/browser_api.rs @@ -0,0 +1,90 @@ +use crate::*; + +#[tengri_proc::expose] +impl Browser { +} + +#[tengri_proc::command(Browser)] +impl BrowserCommand { + //fn set_address (browser: &mut Browser, address: PathBuf) -> Perhaps { + //Ok(None) + //} + //fn set_search (browser: &mut Browser, filter: Arc) -> Perhaps { + //Ok(None) + //} + //fn set_cursor (browser: &mut Browser, cursor: usize) -> Perhaps { + //Ok(None) + //} +} + +// Commands supported by [Browser] +//#[derive(Debug, Clone, PartialEq)] +//pub enum BrowserCommand { + //Begin, + //Cancel, + //Confirm, + //Select(usize), + //Chdir(PathBuf), + //Filter(Arc), +//} + //fn begin (browser: &mut Browser) -> Perhaps { + //unreachable!(); + //} + //fn cancel (browser: &mut Browser) -> Perhaps { + //todo!() + ////browser.mode = None; + ////Ok(None) + //} + //fn confirm (browser: &mut Browser) -> Perhaps { + //todo!() + ////Ok(match browser.mode { + ////Some(PoolMode::Import(index, ref mut browser)) => { + ////if browser.is_file() { + ////let path = browser.path(); + ////browser.mode = None; + ////let _undo = PoolClipCommand::import(browser, index, path)?; + ////None + ////} else if browser.is_dir() { + ////browser.mode = Some(PoolMode::Import(index, browser.chdir()?)); + ////None + ////} else { + ////None + ////} + ////}, + ////Some(PoolMode::Export(index, ref mut browser)) => { + ////todo!() + ////}, + ////_ => unreachable!(), + ////}) + //} + //fn select (browser: &mut Browser, index: usize) -> Perhaps { + //todo!() + ////Ok(match browser.mode { + ////Some(PoolMode::Import(index, ref mut browser)) => { + ////browser.index = index; + ////None + ////}, + ////Some(PoolMode::Export(index, ref mut browser)) => { + ////browser.index = index; + ////None + ////}, + ////_ => unreachable!(), + ////}) + //} + //fn chdir (browser: &mut Browser, dir: PathBuf) -> Perhaps { + //todo!() + ////Ok(match browser.mode { + ////Some(PoolMode::Import(index, ref mut browser)) => { + ////browser.mode = Some(PoolMode::Import(index, Browser::new(Some(dir))?)); + ////None + ////}, + ////Some(PoolMode::Export(index, ref mut browser)) => { + ////browser.mode = Some(PoolMode::Export(index, Browser::new(Some(dir))?)); + ////None + ////}, + ////_ => unreachable!(), + ////}) + //} + //fn filter (browser: &mut Browser, filter: Arc) -> Perhaps { + //todo!() + //} diff --git a/crates/device/src/browser/browser_model.rs b/crates/device/src/browser/browser_model.rs new file mode 100644 index 00000000..053ecbce --- /dev/null +++ b/crates/device/src/browser/browser_model.rs @@ -0,0 +1,69 @@ +use crate::*; + +/// Browses for phrase to import/export +#[derive(Debug, Clone)] +pub struct Browser { + pub cwd: PathBuf, + pub dirs: Vec<(OsString, String)>, + pub files: Vec<(OsString, String)>, + pub filter: String, + pub index: usize, + pub scroll: usize, + pub size: Measure, +} + +impl Browser { + + pub fn new (cwd: Option) -> Usually { + let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; + let mut dirs = vec![]; + let mut files = vec![]; + for entry in std::fs::read_dir(&cwd)? { + let entry = entry?; + let name = entry.file_name(); + let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); + let meta = entry.metadata()?; + if meta.is_dir() { + dirs.push((name, format!("📁 {decoded}"))); + } else if meta.is_file() { + files.push((name, format!("📄 {decoded}"))); + } + } + Ok(Self { + cwd, + dirs, + files, + filter: "".to_string(), + index: 0, + scroll: 0, + size: Measure::new(), + }) + } + + pub fn len (&self) -> usize { + self.dirs.len() + self.files.len() + } + + pub fn is_dir (&self) -> bool { + self.index < self.dirs.len() + } + + pub fn is_file (&self) -> bool { + self.index >= self.dirs.len() + } + + pub fn path (&self) -> PathBuf { + self.cwd.join(if self.is_dir() { + &self.dirs[self.index].0 + } else if self.is_file() { + &self.files[self.index - self.dirs.len()].0 + } else { + unreachable!() + }) + } + + pub fn chdir (&self) -> Usually { + Self::new(Some(self.path())) + } + +} diff --git a/crates/device/src/browser/browser_view.rs b/crates/device/src/browser/browser_view.rs new file mode 100644 index 00000000..ae02f3c3 --- /dev/null +++ b/crates/device/src/browser/browser_view.rs @@ -0,0 +1,19 @@ +use crate::*; + +content!(TuiOut: |self: Browser| /*Stack::down(|add|{ + let mut i = 0; + for (_, name) in self.dirs.iter() { + if i >= self.scroll { + add(&Tui::bold(i == self.index, name.as_str()))?; + } + i += 1; + } + for (_, name) in self.files.iter() { + if i >= self.scroll { + add(&Tui::bold(i == self.index, name.as_str()))?; + } + i += 1; + } + add(&format!("{}/{i}", self.index))?; + Ok(()) +})*/"todo"); diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index bb773305..7bc424a2 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -20,12 +20,18 @@ pub(crate) use Color::*; mod device; pub use self::device::*; +#[cfg(feature = "browser")] mod browser; +#[cfg(feature = "browser")] pub use self::browser::*; + #[cfg(feature = "clock")] mod clock; #[cfg(feature = "clock")] pub use self::clock::*; #[cfg(feature = "editor")] mod editor; #[cfg(feature = "editor")] pub use self::editor::*; +#[cfg(feature = "pool")] mod pool; +#[cfg(feature = "pool")] pub use self::pool::*; + #[cfg(feature = "sequencer")] mod sequencer; #[cfg(feature = "sequencer")] pub use self::sequencer::*; diff --git a/crates/device/src/pool.rs b/crates/device/src/pool.rs new file mode 100644 index 00000000..eaf9a753 --- /dev/null +++ b/crates/device/src/pool.rs @@ -0,0 +1,3 @@ +mod pool_api; pub use self::pool_api::*; +mod pool_model; pub use self::pool_model::*; +mod pool_view; pub use self::pool_view::*; diff --git a/crates/device/src/pool/pool_api.rs b/crates/device/src/pool/pool_api.rs new file mode 100644 index 00000000..27349f2f --- /dev/null +++ b/crates/device/src/pool/pool_api.rs @@ -0,0 +1,255 @@ +use crate::*; + +#[tengri_proc::expose] +impl Pool { + fn _todo_usize_ (&self) -> usize { todo!() } + fn _todo_bool_ (&self) -> bool { todo!() } + fn _todo_clip_ (&self) -> MidiClip { todo!() } + fn _todo_path_ (&self) -> PathBuf { todo!() } + fn _todo_color_ (&self) -> ItemColor { todo!() } + fn _todo_str_ (&self) -> Arc { todo!() } + fn clip_new (&self) -> MidiClip { + self.new_clip() + } + fn clip_cloned (&self) -> MidiClip { + self.cloned_clip() + } + fn clip_index_current (&self) -> usize { + 0 + } + fn clip_index_after (&self) -> usize { + 0 + } + fn clip_index_previous (&self) -> usize { + 0 + } + fn clip_index_next (&self) -> usize { + 0 + } + fn color_random (&self) -> ItemColor { + ItemColor::random() + } +} + +#[tengri_proc::command(Pool)] +impl PoolCommand { + + /// Toggle visibility of pool + fn show (pool: &mut Pool, visible: bool) -> Perhaps { + pool.visible = visible; + Ok(Some(Self::Show { visible: !visible })) + } + + /// Select a clip from the clip pool + fn select (pool: &mut Pool, index: usize) -> Perhaps { + pool.set_clip_index(index); + Ok(None) + } + + /// Rename a clip + fn rename (pool: &mut Pool, command: RenameCommand) -> Perhaps { + Ok(command.delegate(pool, |command|Self::Rename{command})?) + } + + /// Change the length of a clip + fn length (pool: &mut Pool, command: CropCommand) -> Perhaps { + Ok(command.delegate(pool, |command|Self::Length{command})?) + } + + /// Import from file + fn import (pool: &mut Pool, command: BrowserCommand) -> Perhaps { + Ok(if let Some(browser) = pool.browser.as_mut() { + command.delegate(browser, |command|Self::Import{command})? + } else { + None + }) + } + + /// Export to file + fn export (pool: &mut Pool, command: BrowserCommand) -> Perhaps { + Ok(if let Some(browser) = pool.browser.as_mut() { + command.delegate(browser, |command|Self::Export{command})? + } else { + None + }) + } + + /// Update the contents of the clip pool + fn clip (pool: &mut Pool, command: PoolClipCommand) -> Perhaps { + Ok(command.execute(pool)?.map(|command|Self::Clip{command})) + } + +} + +impl<'state> Context<'state, BrowserCommand> for Pool { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + self.browser.as_ref().map(|p|Context::get(p, iter)).flatten() + } +} + +#[tengri_proc::command(Pool)] +impl PoolClipCommand { + + fn add (pool: &mut Pool, index: usize, clip: MidiClip) -> Perhaps { + let mut index = index; + let clip = Arc::new(RwLock::new(clip)); + let mut clips = pool.clips_mut(); + if index >= clips.len() { + index = clips.len(); + clips.push(clip) + } else { + clips.insert(index, clip); + } + Ok(Some(Self::Delete { index })) + } + + fn delete (pool: &mut Pool, index: usize) -> Perhaps { + let clip = pool.clips_mut().remove(index).read().unwrap().clone(); + Ok(Some(Self::Add { index, clip })) + } + + fn swap (pool: &mut Pool, index: usize, other: usize) -> Perhaps { + pool.clips_mut().swap(index, other); + Ok(Some(Self::Swap { index, other })) + } + + fn import (pool: &mut Pool, index: usize, path: PathBuf) -> Perhaps { + 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); + } + Ok(Self::Add { index, clip }.execute(pool)?) + } + + fn export (pool: &mut Pool, index: usize, path: PathBuf) -> Perhaps { + todo!("export clip to midi file"); + } + + fn set_name (pool: &mut Pool, index: usize, name: Arc) -> Perhaps { + let clip = &mut pool.clips_mut()[index]; + let old_name = clip.read().unwrap().name.clone(); + clip.write().unwrap().name = name; + Ok(Some(Self::SetName { index, name: old_name })) + } + + fn set_length (pool: &mut Pool, index: usize, length: usize) -> Perhaps { + let clip = &mut pool.clips_mut()[index]; + let old_len = clip.read().unwrap().length; + clip.write().unwrap().length = length; + Ok(Some(Self::SetLength { index, length: old_len })) + } + + fn set_color (pool: &mut Pool, index: usize, color: ItemColor) -> Perhaps { + let mut color = ItemTheme::from(color); + std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color); + Ok(Some(Self::SetColor { index, color: color.base })) + } + +} + +#[tengri_proc::command(Pool)] +impl RenameCommand { + fn begin (pool: &mut Pool) -> Perhaps { + unreachable!(); + } + fn cancel (pool: &mut Pool) -> Perhaps { + if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { + pool.clips()[clip].write().unwrap().name = old_name.clone().into(); + } + return Ok(None) + } + fn confirm (pool: &mut Pool) -> Perhaps { + if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { + let old_name = old_name.clone(); + *pool.mode_mut() = None; + return Ok(Some(Self::Set { value: old_name })) + } + return Ok(None) + } + fn set (pool: &mut Pool, value: Arc) -> Perhaps { + if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { + pool.clips()[clip].write().unwrap().name = value; + } + return Ok(None) + } +} + +#[tengri_proc::command(Pool)] +impl CropCommand { + fn begin (pool: &mut Pool) -> Perhaps { + unreachable!() + } + fn cancel (pool: &mut Pool) -> Perhaps { + if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { + *pool.mode_mut() = None; + } + Ok(None) + } + fn set (pool: &mut Pool, length: usize) -> Perhaps { + if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) + = pool.mode_mut().clone() + { + let old_length; + { + let clip = pool.clips()[clip].clone();//.write().unwrap(); + old_length = Some(clip.read().unwrap().length); + clip.write().unwrap().length = *length; + } + *pool.mode_mut() = None; + return Ok(old_length.map(|length|Self::Set { length })) + } + Ok(None) + } + fn next (pool: &mut Pool) -> Perhaps { + if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) + = pool.mode_mut().clone() + { + focus.next() + } + Ok(None) + } + fn prev (pool: &mut Pool) -> Perhaps { + if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) + = pool.mode_mut().clone() + { + focus.prev() + } + Ok(None) + } + fn inc (pool: &mut Pool) -> Perhaps { + if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) + = pool.mode_mut().clone() + { + match focus { + ClipLengthFocus::Bar => { *length += 4 * PPQ }, + ClipLengthFocus::Beat => { *length += PPQ }, + ClipLengthFocus::Tick => { *length += 1 }, + } + } + Ok(None) + } + fn dec (pool: &mut Pool) -> Perhaps { + if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) + = pool.mode_mut().clone() + { + match focus { + ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) }, + ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) }, + ClipLengthFocus::Tick => { *length = length.saturating_sub(1) }, + } + } + Ok(None) + } +} diff --git a/crates/device/src/pool/pool_model.rs b/crates/device/src/pool/pool_model.rs new file mode 100644 index 00000000..8347ddb7 --- /dev/null +++ b/crates/device/src/pool/pool_model.rs @@ -0,0 +1,206 @@ +use crate::*; + +#[derive(Debug)] +pub struct Pool { + pub visible: bool, + /// Collection of clips + pub clips: Arc>>>>, + /// Selected clip + pub clip: AtomicUsize, + /// Mode switch + pub mode: Option, + /// Embedded file browser + pub browser: Option, +} + +impl Default for Pool { + fn default () -> Self { + use PoolMode::*; + Self { + visible: true, + clips: Arc::from(RwLock::from(vec![])), + clip: 0.into(), + mode: None, + browser: None, + } + } +} + +impl Pool { + pub fn clip_index (&self) -> usize { + self.clip.load(Relaxed) + } + pub fn set_clip_index (&self, value: usize) { + self.clip.store(value, Relaxed); + } + pub fn mode (&self) -> &Option { + &self.mode + } + pub fn mode_mut (&mut self) -> &mut Option { + &mut self.mode + } + pub fn begin_clip_length (&mut self) { + let length = self.clips()[self.clip_index()].read().unwrap().length; + *self.mode_mut() = Some(PoolMode::Length( + self.clip_index(), + length, + ClipLengthFocus::Bar + )); + } + pub fn begin_clip_rename (&mut self) { + let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); + *self.mode_mut() = Some(PoolMode::Rename( + self.clip_index(), + name + )); + } + pub fn begin_import (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Import( + self.clip_index(), + Browser::new(None)? + )); + Ok(()) + } + pub fn begin_export (&mut self) -> Usually<()> { + *self.mode_mut() = Some(PoolMode::Export( + self.clip_index(), + Browser::new(None)? + )); + Ok(()) + } + pub fn new_clip (&self) -> MidiClip { + MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemTheme::random())) + } + pub fn cloned_clip (&self) -> MidiClip { + let index = self.clip_index(); + let mut clip = self.clips()[index].read().unwrap().duplicate(); + clip.color = ItemTheme::random_near(clip.color, 0.25); + clip + } + pub fn add_new_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(self.new_clip())); + let index = { + let mut clips = self.clips.write().unwrap(); + clips.push(clip.clone()); + clips.len().saturating_sub(1) + }; + self.clip.store(index, Relaxed); + (index, clip) + } + pub fn delete_clip (&mut self, clip: &MidiClip) -> bool { + let index = self.clips.read().unwrap().iter().position(|x|*x.read().unwrap()==*clip); + if let Some(index) = index { + self.clips.write().unwrap().remove(index); + return true + } + false + } +} + +/// 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, Browser), + /// Save clip to disk + Export(usize, Browser), +} + +/// Focused field of `ClipLength` +#[derive(Copy, Clone, Debug)] +pub enum ClipLengthFocus { + /// Editing the number of bars + Bar, + /// Editing the number of beats + Beat, + /// Editing the number of ticks + Tick, +} + +impl ClipLengthFocus { + pub fn next (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Beat, Beat => Tick, Tick => Bar, } + } + pub fn prev (&mut self) { + use ClipLengthFocus::*; + *self = match self { Bar => Tick, Beat => Bar, Tick => Beat, } + } +} + +/// Displays and edits clip length. +#[derive(Clone)] +pub struct ClipLength { + /// Pulses per beat (quaver) + ppq: usize, + /// Beats per bar + bpb: usize, + /// Length of clip in pulses + pulses: usize, + /// Selected subdivision + pub focus: Option, +} + +impl ClipLength { + pub fn _new (pulses: usize, focus: Option) -> Self { + Self { ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { + self.pulses / (self.bpb * self.ppq) + } + pub fn beats (&self) -> usize { + (self.pulses % (self.bpb * self.ppq)) / self.ppq + } + pub fn ticks (&self) -> usize { + self.pulses % self.ppq + } + pub fn bars_string (&self) -> Arc { + format!("{}", self.bars()).into() + } + pub fn beats_string (&self) -> Arc { + format!("{}", self.beats()).into() + } + pub fn ticks_string (&self) -> Arc { + format!("{:>02}", self.ticks()).into() + } +} + +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>; + fn add_clip (&self) -> (usize, Arc>) { + let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); + self.clips_mut().push(clip.clone()); + (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),*>)? { + fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { + $cb.read().unwrap() + } + fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { + $cb.write().unwrap() + } + } + } +} + +has_clips!(|self: Pool|self.clips); + +has_clip!(|self: Pool|self.clips().get(self.clip_index()).map(|c|c.clone())); + +from!(|clip:&Arc>|Pool = { + let model = Self::default(); + model.clips.write().unwrap().push(clip.clone()); + model.clip.store(1, Relaxed); + model +}); diff --git a/crates/device/src/pool/pool_view.rs b/crates/device/src/pool/pool_view.rs new file mode 100644 index 00000000..82e74ae0 --- /dev/null +++ b/crates/device/src/pool/pool_view.rs @@ -0,0 +1,42 @@ +use crate::*; + +pub struct PoolView<'a>(pub bool, pub &'a Pool); + +content!(TuiOut: |self: PoolView<'a>| { + let Self(compact, model) = self; + let Pool { clips, .. } = self.1; + //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||Tui::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(); + let height = clips.read().unwrap().len() as u16; + Tui::bg(Reset, Fixed::y(height, on_bg(border(Map::new(iter, move|clip: Arc>, 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::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "▶"))))), + Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(Tui::g(255), "◀"))))), + )))) + }))))) +}); + +content!(TuiOut: |self: ClipLength| { + use ClipLengthFocus::*; + let bars = ||self.bars_string(); + let beats = ||self.beats_string(); + let ticks = ||self.ticks_string(); + match self.focus { + None => row!(" ", bars(), ".", beats(), ".", ticks()), + Some(Bar) => row!("[", bars(), "]", beats(), ".", ticks()), + Some(Beat) => row!(" ", bars(), "[", beats(), "]", ticks()), + Some(Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), + } +}); diff --git a/crates/device/src/sampler.rs b/crates/device/src/sampler.rs index b201fb54..47ac6366 100644 --- a/crates/device/src/sampler.rs +++ b/crates/device/src/sampler.rs @@ -11,7 +11,7 @@ pub(crate) use symphonia::{ }; mod sampler_api; pub use self::sampler_api::*; -mod sampler_audio; pub use self::sampler_audio::*; +mod sampler_audio; mod sampler_browse; pub use self::sampler_browse::*; mod sampler_midi; pub use self::sampler_midi::*; mod sampler_model; pub use self::sampler_model::*; diff --git a/crates/device/src/sampler/sampler_api.rs b/crates/device/src/sampler/sampler_api.rs index 514ccf5f..ebd62ac7 100644 --- a/crates/device/src/sampler/sampler_api.rs +++ b/crates/device/src/sampler/sampler_api.rs @@ -63,7 +63,8 @@ impl SamplerCommand { Arc::new(RwLock::new(Sample::new( "Sample", 0, 0, vec![vec![];sampler.audio_ins.len()] ))) - )); + +)); Ok(None) } fn record_finish (sampler: &mut Sampler) -> Perhaps { diff --git a/crates/device/src/sampler/sampler_model.rs b/crates/device/src/sampler/sampler_model.rs index 1d9ec18e..bf5c624d 100644 --- a/crates/device/src/sampler/sampler_model.rs +++ b/crates/device/src/sampler/sampler_model.rs @@ -174,5 +174,5 @@ pub struct Voice { #[derive(Debug)] pub enum SamplerMode { // Load sample from path - Import(usize, FileBrowser), + Import(usize, Browser), } diff --git a/deps/tengri b/deps/tengri index faecc2c3..b45ac8f4 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit faecc2c304ad2c0ebd78d21170a02c172fd356bf +Subproject commit b45ac8f417b2f4e83e116a9ee5fe4bf3ad57a726