use crate::*; #[derive(Default, Debug)] pub struct App { /// Must not be dropped for the duration of the process pub jack: Jack, /// Port handles pub ports: std::collections::BTreeMap>, /// Display size pub size: Measure, /// Performance counter pub perf: PerfModel, // View and input definition pub config: Configuration, /// Contains all recently created clips. pub pool: Pool, /// Contains the currently edited musical arrangement pub project: Arrangement, /// Undo history pub history: Vec, // Dialog overlay pub dialog: Option, // Cache of formatted strings pub view_cache: Arc>, /// Base color. pub color: ItemTheme, } has!(Jack: |self: App|self.jack); has!(Pool: |self: App|self.pool); has!(Clock: |self: App|self.project.clock); has!(Option: |self: App|self.project.editor); has!(Selection: |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!(Measure: |self: App|self.size); maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; { MaybeHas::::get_mut(&mut self.project) }); impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; { MaybeHas::::get_mut(&mut self.project) }); impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } has_clips!(|self: App|self.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.editor.is_some(); //}); impl App { pub fn toggle_dialog (&mut self, mut dialog: Option) -> Option { std::mem::swap(&mut self.dialog, &mut dialog); dialog } pub fn toggle_editor (&mut self, value: Option) { //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); let value = value.unwrap_or_else(||!self.editor().is_some()); if value { self.clip_auto_create(); } else { self.clip_auto_remove(); } } pub fn browser (&self) -> Option<&Browser> { self.dialog.as_ref().and_then(|dialog|match dialog { Dialog::Browser(_, b) => Some(b), _ => None }) } pub(crate) fn device_pick (&mut self, index: usize) { self.dialog = Some(Dialog::Device(index)); } pub(crate) fn device_kinds (&self) -> &'static [&'static str] { &[ "Sampler", "Plugin (LV2)", ] } pub(crate) fn device_add (&mut self, index: usize) -> Usually<()> { match index { 0 => self.device_add_sampler(), 1 => self.device_add_lv2(), _ => unreachable!(), } } fn device_add_lv2 (&mut self) -> Usually<()> { todo!(); Ok(()) } fn device_add_sampler (&mut self) -> Usually<()> { let name = self.jack.with_client(|c|c.name().to_string()); let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].name(); let track = self.track().expect("no active track"); let port = format!("{}/Sampler", &track.name); let connect = PortConnect::exact(format!("{name}:{midi}")); let sampler = if let Ok(sampler) = Sampler::new( &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] ) { self.dialog = None; Device::Sampler(sampler) } else { self.dialog = Some(Dialog::Message(Message::FailedToAddDevice)); return Err("failed to add device".into()) }; let track = self.track_mut().expect("no active track"); track.devices.push(sampler); Ok(()) } // Create new clip in pool when entering empty cell fn clip_auto_create (&mut self) -> Option>> { if let Selection::TrackClip { track, scene } = *self.selection() && let Some(scene) = self.project.scenes.get_mut(scene) && let Some(slot) = scene.clips.get_mut(track) && slot.is_none() && let Some(track) = self.project.tracks.get_mut(track) { let (index, mut clip) = self.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(); if let Some(ref mut editor) = &mut self.project.editor { editor.set_clip(Some(&clip)); } *slot = Some(clip.clone()); Some(clip) } else { None } } // Remove clip from arrangement when exiting empty clip editor fn clip_auto_remove (&mut self) { if let Selection::TrackClip { track, scene } = *self.selection() && let Some(scene) = self.project.scenes.get_mut(scene) && let Some(slot) = scene.clips.get_mut(track) && let Some(clip) = slot.as_mut() { let mut swapped = None; if clip.read().unwrap().count_midi_messages() == 0 { std::mem::swap(&mut swapped, slot); } if let Some(clip) = swapped { self.pool.delete_clip(&clip.read().unwrap()); } } } } /// Various possible dialog overlays #[derive(Clone, Debug)] pub enum Dialog { Help(usize), Menu(usize), Device(usize), Message(Message), Browser(BrowserTarget, Browser), Options, } #[derive(Clone, Debug)] pub enum BrowserTarget { SaveProject, LoadProject, ImportSample(Arc>>), ExportSample(Arc>>), ImportClip(Arc>>), ExportClip(Arc>>), } /// Various possible messages #[derive(PartialEq, Clone, Copy, Debug)] pub enum Message { FailedToAddDevice, } content!(TuiOut: |self: Message| match self { Self::FailedToAddDevice => "Failed to add device." }); #[tengri_proc::expose] impl App { fn _todo_isize_stub (&self) -> isize { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } fn w_sidebar (&self) -> u16 { self.project.w_sidebar(self.editor().is_some()) } fn h_sample_detail (&self) -> u16 { 6.max(self.height() as u16 * 3 / 9) } fn focus_editor (&self) -> bool { self.project.editor.is_some() } fn is_editing (&self) -> bool { self.project.editor.is_some() } fn focus_message (&self) -> bool { matches!(self.dialog, Some(Dialog::Message(..))) } fn focus_device_add (&self) -> bool { matches!(self.dialog, Some(Dialog::Device(..))) } fn focus_browser (&self) -> bool { self.browser().is_some() } fn focus_clip (&self) -> bool { !self.is_editing() && self.selection().is_clip() } fn focus_track (&self) -> bool { !self.is_editing() && self.selection().is_track() } fn focus_scene (&self) -> bool { !self.is_editing() && self.selection().is_scene() } fn focus_mix (&self) -> bool { !self.is_editing() && self.selection().is_mix() } fn focus_pool_import (&self) -> bool { matches!(self.pool.mode, Some(PoolMode::Import(..))) } fn focus_pool_export (&self) -> bool { matches!(self.pool.mode, Some(PoolMode::Export(..))) } fn focus_pool_rename (&self) -> bool { matches!(self.pool.mode, Some(PoolMode::Rename(..))) } fn focus_pool_length (&self) -> bool { matches!(self.pool.mode, Some(PoolMode::Length(..))) } fn dialog_none (&self) -> Option { None } fn dialog_device (&self) -> Option { Some(Dialog::Device(0)) // TODO } fn dialog_device_prev (&self) -> Option { Some(Dialog::Device(0)) // TODO } fn dialog_device_next (&self) -> Option { Some(Dialog::Device(0)) // TODO } fn dialog_help (&self) -> Option { Some(Dialog::Help(0)) } fn dialog_menu (&self) -> Option { Some(Dialog::Menu(0)) } fn dialog_save (&self) -> Option { Some(Dialog::Browser(BrowserTarget::SaveProject, Browser::new(None).unwrap())) } fn dialog_load (&self) -> Option { Some(Dialog::Browser(BrowserTarget::LoadProject, Browser::new(None).unwrap())) } fn dialog_import_clip (&self) -> Option { Some(Dialog::Browser(BrowserTarget::ImportClip(Default::default()), Browser::new(None).unwrap())) } fn dialog_export_clip (&self) -> Option { Some(Dialog::Browser(BrowserTarget::ExportClip(Default::default()), Browser::new(None).unwrap())) } fn dialog_import_sample (&self) -> Option { Some(Dialog::Browser(BrowserTarget::ImportSample(Default::default()), Browser::new(None).unwrap())) } fn dialog_export_sample (&self) -> Option { Some(Dialog::Browser(BrowserTarget::ExportSample(Default::default()), Browser::new(None).unwrap())) } fn dialog_options (&self) -> Option { Some(Dialog::Options) } fn editor_pitch (&self) -> Option { 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_selection (&self) -> Option { self.selection().scene() } fn track_count (&self) -> usize { self.tracks().len() } fn track_selection (&self) -> Option { self.selection().track() } fn select_scene_next (&self) -> Selection { self.selection().scene_next(self.scenes().len()) } fn select_scene_prev (&self) -> Selection { self.selection().scene_prev() } fn select_track_header (&self) -> Selection { self.selection().track_header(self.tracks().len()) } fn select_track_next (&self) -> Selection { self.selection().track_next(self.tracks().len()) } fn select_track_prev (&self) -> Selection { self.selection().track_prev() } fn clip_selected (&self) -> Option>> { match self.selection() { Selection::TrackClip { track, scene } => self.scenes()[*scene].clips[*track].clone(), _ => None } } fn device_kind (&self) -> usize { if let Some(Dialog::Device(index)) = self.dialog { index } else { 0 } } fn device_kind_prev (&self) -> usize { if let Some(Dialog::Device(index)) = self.dialog { index.overflowing_sub(1).0.min(self.device_kinds().len().saturating_sub(1)) } else { 0 } } fn device_kind_next (&self) -> usize { if let Some(Dialog::Device(index)) = self.dialog { (index + 1) % self.device_kinds().len() } else { 0 } } }