diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index f2fe97f5..10bbefed 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -43,16 +43,16 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm todo!() } fn launch (app: &mut App) -> Perhaps { - app.launch(); + app.project.launch(); Ok(None) } fn select (app: &mut App, selection: Selection) -> Perhaps { - app.arranger.select(selection); + app.project.select(selection); if let Some(ref mut editor) = app.editor { - editor.set_clip(match app.arranger.selected { + editor.set_clip(match app.project.selected { Some(Selection::TrackClip { track, scene }) if let Some(Some(Some(clip))) = app - .arranger + .project .scenes.get(scene) .map(|s|s.clips.get(track)) => @@ -70,11 +70,11 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm // autoedit: load focused clip in editor. } fn stop_all (app: &mut App) -> Perhaps { - app.stop_all(); + app.tracks_stop_all(); Ok(None) } fn sampler (app: &mut App, command: SamplerCommand) -> Perhaps { - Ok(app.sampler_mut() + Ok(app.project.sampler_mut() .map(|s|command.delegate(s, |command|Self::Sampler{command})) .transpose()? .flatten()) @@ -83,7 +83,7 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm Ok(command.delegate(app, |command|Self::Arrange{command})?) } fn clock (app: &mut App, command: ClockCommand) -> Perhaps { - Ok(command.execute(&mut app.clock)?.map(|command|Self::Clock{command})) + Ok(command.execute(&mut app.clock())?.map(|command|Self::Clock{command})) } fn device (app: &mut App, command: DeviceCommand) -> Perhaps { Ok(command.delegate(app, |command|Self::Device{command})?) @@ -95,7 +95,7 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm Ok(if let Some(editor) = app.editor.as_mut() { let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; // update linked sampler after editor action - app.sampler_mut().map(|sampler|match command { + app.project.sampler_mut().map(|sampler|match command { // autoselect: automatically select sample in sampler MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, _ => {} @@ -106,7 +106,7 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm }) } fn pool (app: &mut App, command: PoolCommand) -> Perhaps { - Ok(if let Some(pool) = app.pool.as_mut() { + Ok(if let Some(pool) = app.project.pool.as_mut() { let undo = command.clone().delegate(pool, |command|AppCommand::Pool{command})?; // update linked editor after pool action app.editor.as_mut().map(|editor|match command { @@ -126,7 +126,7 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm impl<'state> Context<'state, ClockCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { - Context::get(&self.clock, iter) + Context::get(&self.clock(), iter) } } @@ -138,13 +138,13 @@ impl<'state> Context<'state, MidiEditCommand> for App { impl<'state> Context<'state, PoolCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { - self.pool().map(|p|Context::get(p, iter)).flatten() + self.project.pool.map(|p|Context::get(p, iter)).flatten() } } impl<'state> Context<'state, SamplerCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { - self.sampler().map(|p|Context::get(p, iter)).flatten() + self.project.sampler().map(|p|Context::get(p, iter)).flatten() } } diff --git a/crates/app/src/audio.rs b/crates/app/src/audio.rs index 15d15232..e12c2b80 100644 --- a/crates/app/src/audio.rs +++ b/crates/app/src/audio.rs @@ -10,7 +10,7 @@ audio!( |self: App, client, scope|{ let t0 = self.perf.get_t0(); self.clock().update_from_scope(scope).unwrap(); - let midi_in = self.arranger.midi_input_collect(scope); + let midi_in = self.project.midi_input_collect(scope); if let Some(editor) = &self.editor { let mut pitch: Option = None; for port in midi_in.iter() { @@ -26,14 +26,16 @@ audio!( editor.set_note_pos(pitch.as_int() as usize); } } - let result = self.arranger.tracks_render(client, scope); + let result = self.project.process_tracks(client, scope); self.perf.update_from_jack_scope(t0, scope); result }; |self, event|{ use JackEvent::*; match event { - SampleRate(sr) => { self.clock.timebase.sr.set(sr as f64); }, + SampleRate(sr) => { + self.clock().timebase.sr.set(sr as f64); + }, PortRegistration(id, true) => { //let port = self.jack().port_by_id(id); //println!("\rport add: {id} {port:?}"); diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index 6cd4f34e..24cdcb4b 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -28,12 +28,26 @@ pub struct App { pub color: ItemTheme, } -has!(Option: |self: App|self.project.selected); +has!(Option: |self: App|self.project.selection); has!(Vec: |self: App|self.project.midi_ins); has!(Vec: |self: App|self.project.midi_outs); has!(Vec: |self: App|self.project.scenes); has!(Vec: |self: App|self.project.tracks); has!(Clock: |self: App|self.project.clock); +has_size!(|self: App|&self.size); +has_clips!(|self: App|self.project.pool.clips); +has_editor!(|self: App|{ + editor = self.editor; + editor_w = { + let size = self.size.w(); + let editor = self.editor.as_ref().expect("missing editor"); + let time_len = editor.time_len().get(); + let time_zoom = editor.time_zoom().get().max(1); + (5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) + }; + editor_h = 15; + is_editing = self.editing.load(Relaxed); +}); impl App { pub fn toggle_dialog (&mut self, dialog: Option) { @@ -92,14 +106,13 @@ impl App { } // Create new clip in pool when entering empty cell fn clip_auto_create (&mut self) -> Option>> { - if let Some(pool) = Has::>::get(self) - && let Some(Selection::TrackClip { track, scene }) = self.get() - && let Some(scene) = Has::>::get_mut(self).get_mut(*scene) - && let Some(slot) = scene.clips.get_mut(*track) - && slot.is_none() - && let Some(track) = Has::>::get_mut(self).get_mut(*track) + if let Some(Selection::TrackClip { track, scene }) = self.get() + && let Some(scene) = Has::>::get_mut(self).get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && slot.is_none() + && let Some(track) = Has::>::get_mut(self).get_mut(track) { - let (index, mut clip) = pool.add_new_clip(); + let (index, mut clip) = self.arranger.pool.add_new_clip(); // autocolor: new clip colors from scene and track color let color = track.color.base.mix(scene.color.base, 0.5); clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); @@ -144,7 +157,7 @@ pub trait AutoRemove: /// Various possible dialog overlays -#[derive(PartialEq, Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub enum Dialog { Help, Menu, @@ -162,21 +175,6 @@ pub enum Message { } content!(TuiOut: |self: Message| match self { Self::FailedToAddDevice => "Failed to add device." }); -has_size!(|self: App|&self.size); -has!(Clock: |self: App|self.clock); -has_clips!(|self: App|self.pool.as_ref().expect("no clip pool").clips); -has_editor!(|self: App|{ - editor = self.editor; - editor_w = { - let size = self.size.w(); - let editor = self.editor.as_ref().expect("missing editor"); - let time_len = editor.time_len().get(); - let time_zoom = editor.time_zoom().get().max(1); - (5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) - }; - editor_h = 15; - is_editing = self.editing.load(Relaxed); -}); #[tengri_proc::expose] impl App { @@ -202,28 +200,28 @@ impl App { self.browser.is_visible } fn focus_clip (&self) -> bool { - !self.is_editing() && self.selected.is_clip() + !self.is_editing() && self.selection().is_clip() } fn focus_track (&self) -> bool { - !self.is_editing() && self.selected.is_track() + !self.is_editing() && self.selection().is_track() } fn focus_scene (&self) -> bool { - !self.is_editing() && self.selected.is_scene() + !self.is_editing() && self.selection().is_scene() } fn focus_mix (&self) -> bool { - !self.is_editing() && self.selected.is_mix() + !self.is_editing() && self.selection().is_mix() } fn focus_pool_import (&self) -> bool { - matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Import(..))) + matches!(self.project.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Import(..))) } fn focus_pool_export (&self) -> bool { - matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Export(..))) + matches!(self.project.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Export(..))) } fn focus_pool_rename (&self) -> bool { - matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Rename(..))) + matches!(self.project.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Rename(..))) } fn focus_pool_length (&self) -> bool { - matches!(self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..))) + matches!(self.project.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..))) } fn dialog_device (&self) -> Dialog { Dialog::Device(0) // TODO @@ -250,38 +248,38 @@ impl App { Dialog::Options } fn editor_pitch (&self) -> Option { - Some((self.editor().map(|e|e.get_note_pos()).unwrap() as u8).into()) + Some((self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) } fn scene_count (&self) -> usize { self.scenes.len() } - fn scene_selected (&self) -> Option { - self.selected.scene() + fn scene_selection (&self) -> Option { + self.selection().scene() } fn track_count (&self) -> usize { self.tracks.len() } - fn track_selected (&self) -> Option { - self.selected.track() + fn track_selection (&self) -> Option { + self.selection().track() } fn select_scene_next (&self) -> Selection { - self.selected.scene_next(self.scenes.len()) + self.selection().scene_next(self.scenes().len()) } fn select_scene_prev (&self) -> Selection { - self.selected.scene_prev() + self.selection().scene_prev() } fn select_track_header (&self) -> Selection { - self.selected.track_header(self.tracks.len()) + self.selection().track_header(self.tracks().len()) } fn select_track_next (&self) -> Selection { - self.selected.track_next(self.tracks.len()) + self.selection().track_next(self.tracks().len()) } fn select_track_prev (&self) -> Selection { - self.selected.track_prev() + self.selection().track_prev() } - fn clip_selected (&self) -> Option>> { - match self.selected { - Selection::TrackClip { track, scene } => self.scenes[scene].clips[track].clone(), + fn clip_selection (&self) -> Option>> { + match self.selection() { + Selection::TrackClip { track, scene } => self.scenes()[scene].clips[track].clone(), _ => None } } diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index d9d5fd0e..86886df1 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -17,41 +17,43 @@ impl App { pub fn view_status (&self) -> impl Content + use<'_> { self.update_clock(); let cache = self.view_cache.read().unwrap(); - view_status(self.selected.describe(self.tracks(), self.scenes()), + view_status(self.project.selected.map(|x|x.describe(self.tracks(), self.scenes())), cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) } pub fn view_transport (&self) -> impl Content + use<'_> { self.update_clock(); let cache = self.view_cache.read().unwrap(); - view_transport(self.clock.is_rolling(), + view_transport(self.project.clock.is_rolling(), cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) } - pub fn view_arranger (&self) -> impl Content + use<'_> { - ArrangerView::new(self, self.editor.as_ref()) - } - pub fn view_pool (&self) -> impl Content + use<'_> { - self.pool().map(|p|Fixed::x(self.w_sidebar(), PoolView(self.is_editing(), p))) - } pub fn view_editor (&self) -> impl Content + use<'_> { self.editor().map(|e|Bsp::n(Bsp::e(e.clip_status(), e.edit_status()), e)) } + pub fn view_arranger (&self) -> impl Content + use<'_> { + ArrangerView::new(&self.project, self.editor.as_ref()) + } + pub fn view_pool (&self) -> impl Content + use<'_> { + let is_editing = self.is_editing(); + Fixed::x(self.project.w_sidebar(is_editing), + PoolView(is_editing, &self.project.pool)) + } pub fn view_samples_keys (&self) -> impl Content + use<'_> { - self.sampler().map(|s|s.view_list(false, self.editor().unwrap())) + self.project.sampler().map(|s|s.view_list(false, self.editor().unwrap())) } pub fn view_samples_grid (&self) -> impl Content + use<'_> { - self.sampler().map(|s|s.view_grid()) + self.project.sampler().map(|s|s.view_grid()) } pub fn view_sample_viewer (&self) -> impl Content + use<'_> { - self.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) + self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) } pub fn view_sample_info (&self) -> impl Content + use<'_> { - self.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) + self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) } pub fn view_meters_input (&self) -> impl Content + use<'_> { - self.sampler().map(|s|s.view_meters_input()) + self.project.sampler().map(|s|s.view_meters_input()) } pub fn view_meters_output (&self) -> impl Content + use<'_> { - self.sampler().map(|s|s.view_meters_output()) + self.project.sampler().map(|s|s.view_meters_output()) } pub fn view_dialog (&self) -> impl Content + use<'_> { When(self.dialog.is_some(), Bsp::b( "", @@ -76,6 +78,9 @@ impl App { ))) )) } +} + +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)); diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs index 7d59ed67..2fa8556c 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -53,33 +53,33 @@ has!(Clock: |self: Arrangement|self.clock); impl Arrangement { /// Width of display - pub(crate) fn w (&self) -> u16 { + pub fn w (&self) -> u16 { self.size.w() as u16 } /// Width allocated for sidebar. - pub(crate) fn w_sidebar (&self, is_editing: bool) -> u16 { + pub fn w_sidebar (&self, is_editing: bool) -> u16 { self.w() / if is_editing { 16 } else { 8 } as u16 } /// Width taken by all tracks. - pub(crate) fn w_tracks (&self) -> u16 { + pub fn w_tracks (&self) -> u16 { self.tracks_with_sizes(&self.selected, None).last() .map(|(_, _, _, x)|x as u16).unwrap_or(0) } /// Width available to display tracks. - pub(crate) fn w_tracks_area (&self, is_editing: bool) -> u16 { + pub fn w_tracks_area (&self, is_editing: bool) -> u16 { self.w().saturating_sub(2 * self.w_sidebar(is_editing)) } /// Height of display - pub(crate) fn h (&self) -> u16 { + pub fn h (&self) -> u16 { self.size.h() as u16 } /// Height available to display track headers. - pub(crate) fn h_tracks_area (&self) -> u16 { + pub fn h_tracks_area (&self) -> u16 { 5 // FIXME //self.h().saturating_sub(self.h_inputs() + self.h_outputs()) } /// Height available to display tracks. - pub(crate) fn h_scenes_area (&self) -> u16 { + pub fn h_scenes_area (&self) -> u16 { //15 self.h().saturating_sub( self.h_inputs() + @@ -89,7 +89,7 @@ impl Arrangement { ) } /// Height taken by all scenes. - pub(crate) fn h_scenes (&self, is_editing: bool) -> u16 { + pub fn h_scenes (&self, is_editing: bool) -> u16 { let (selected_track, selected_scene) = match Has::>::get(self) { Some(Selection::Track(t)) => (Some(*t), None), Some(Selection::Scene(s)) => (None, Some(*s)), @@ -107,15 +107,15 @@ impl Arrangement { .map(|(_, _, _, y)|y as u16).unwrap_or(0) } /// Height taken by all inputs. - pub(crate) fn h_inputs (&self) -> u16 { + pub fn h_inputs (&self) -> u16 { self.midi_ins_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } /// Height taken by all outputs. - pub(crate) fn h_outputs (&self) -> u16 { + pub fn h_outputs (&self) -> u16 { self.midi_outs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } /// Height taken by visible device slots. - pub(crate) fn h_devices (&self) -> u16 { + pub fn h_devices (&self) -> u16 { 2 //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } @@ -144,7 +144,7 @@ impl Arrangement { self.get_scene()?.clips.get(self.selection()?.track()?)?.clone() } /// Put a clip in a slot - pub(crate) fn clip_put ( + pub fn clip_put ( &mut self, track: usize, scene: usize, clip: Option>> ) -> Option>> { let old = self.scenes[scene].clips[track].clone(); @@ -152,7 +152,7 @@ impl Arrangement { old } /// Change the color of a clip, returning the previous one - pub(crate) fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) + pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) -> Option { self.scenes[scene].clips[track].as_ref().map(|clip|{ @@ -164,7 +164,7 @@ impl Arrangement { }) } /// Toggle looping for the active clip - pub(crate) fn toggle_loop (&mut self) { + pub fn toggle_loop (&mut self) { if let Some(clip) = self.get_clip() { clip.write().unwrap().toggle_loop() } diff --git a/crates/device/src/arranger/arranger_select.rs b/crates/device/src/arranger/arranger_select.rs index 81a2f7a1..856e53eb 100644 --- a/crates/device/src/arranger/arranger_select.rs +++ b/crates/device/src/arranger/arranger_select.rs @@ -17,8 +17,11 @@ impl Arrangement { /// Represents the current user selection in the arranger #[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { + #[default] + /// Nothing is selected + Nothing, /// The whole mix is selected - #[default] Mix, + Mix, /// A MIDI input is selected. Input(usize), /// A MIDI output is selected. diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index 0375819b..b05905af 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -1,6 +1,6 @@ use crate::*; -pub(crate) struct ArrangerView<'a> { +pub struct ArrangerView<'a> { pub arrangement: &'a Arrangement, pub is_editing: bool, diff --git a/crates/device/src/clock/clock_view.rs b/crates/device/src/clock/clock_view.rs index ab2acc3e..92e778eb 100644 --- a/crates/device/src/clock/clock_view.rs +++ b/crates/device/src/clock/clock_view.rs @@ -18,14 +18,14 @@ pub fn view_transport ( } pub fn view_status ( - sel: Arc, + sel: Option>, sr: Arc>, buf: Arc>, lat: Arc>, ) -> impl Content { let theme = ItemTheme::G[96]; Tui::bg(Black, row!(Bsp::a( - Fill::xy(Align::w(FieldH(theme, "Selected", sel))), + Fill::xy(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))), Fill::xy(Align::e(row!( FieldH(theme, "SR", sr), FieldH(theme, "Buf", buf), diff --git a/crates/device/src/editor/editor_model.rs b/crates/device/src/editor/editor_model.rs index 648e70c5..9d8d31f6 100644 --- a/crates/device/src/editor/editor_model.rs +++ b/crates/device/src/editor/editor_model.rs @@ -102,8 +102,8 @@ impl MidiViewer for MidiEditor { } pub trait HasEditor { - fn editor (&self) -> &Option; - fn editor_mut (&mut self) -> &Option; + fn editor (&self) -> Option<&MidiEditor>; + fn editor_mut (&mut self) -> Option<&mut MidiEditor>; fn is_editing (&self) -> bool { true } fn editor_w (&self) -> usize { 0 } fn editor_h (&self) -> usize { 0 } @@ -117,17 +117,12 @@ pub trait HasEditor { is_editing = $e3:expr; }) => { impl HasEditor for $Struct { - fn editor (&$self) -> &Option { &$e0 } - fn editor_mut (&mut $self) -> &Option { &mut $e0 } + fn editor (&$self) -> Option<&MidiEditor> { $e0.as_ref() } + fn editor_mut (&mut $self) -> Option<&mut MidiEditor> { $e0.as_mut() } fn editor_w (&$self) -> usize { $e1 } fn editor_h (&$self) -> usize { $e2 } fn is_editing (&$self) -> bool { $e3 } } }; - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? { - fn editor (&$self) -> &MidiEditor { &$cb } - } - }; }