use crate::*; //handle!(TuiIn:|self: App, input|self. impl App { fn handle_tui_key_with_history (&mut self, input: &TuiIn) -> Perhaps { panic!("{input:?}"); //Ok(if let Some(binding) = self.profile.as_ref() //.map(|c|c.keys.dispatch(input.event())).flatten() //{ //let binding = binding.clone(); //let undo = binding.command.clone().execute(self)?; //// FIXME failed commands are not persisted in undo history ////self.history.push((binding.command.clone(), undo)); //Some(true) //} else { //None //}) } } type MaybeClip = Option>>; macro_rules!dsl_expose(($Struct:ident { $($fn:ident: $ret:ty = |$self:ident|$body:expr);* $(;)? })=>{ #[tengri_proc::expose] impl $Struct { $(fn $fn (&$self) -> $ret { $body })* } }); dsl_expose!(App { _isize_stub: isize = |self|todo!(); _item_theme_stub: ItemTheme = |self|todo!(); w_sidebar: u16 = |self|self.project.w_sidebar(self.editor().is_some()); h_sample_detail: u16 = |self|6.max(self.height() as u16 * 3 / 9); focus_editor: bool = |self|self.project.editor.is_some(); focus_dialog: bool = |self|!matches!(self.dialog, Dialog::None); focus_message: bool = |self|matches!(self.dialog, Dialog::Message(..)); focus_add_device: bool = |self|matches!(self.dialog, Dialog::Device(..)); focus_browser: bool = |self|self.dialog.browser().is_some(); focus_clip: bool = |self|!self.focus_editor() && matches!(self.selection(), Selection::TrackClip{..}); focus_track: bool = |self|!self.focus_editor() && matches!(self.selection(), Selection::Track(..)); focus_scene: bool = |self|!self.focus_editor() && matches!(self.selection(), Selection::Scene(..)); focus_mix: bool = |self|!self.focus_editor() && matches!(self.selection(), Selection::Mix); focus_pool_import: bool = |self|matches!(self.pool.mode, Some(PoolMode::Import(..))); focus_pool_export: bool = |self|matches!(self.pool.mode, Some(PoolMode::Export(..))); focus_pool_rename: bool = |self|matches!(self.pool.mode, Some(PoolMode::Rename(..))); focus_pool_length: bool = |self|matches!(self.pool.mode, Some(PoolMode::Length(..))); dialog_none: Option = |self|None; dialog_device: Option = |self|Some(Dialog::Device(0)); // TODO dialog_device_prev: Option = |self|Some(Dialog::Device(0)); // TODO dialog_device_next: Option = |self|Some(Dialog::Device(0)); // TODO dialog_help: Option = |self|Some(Dialog::Help(0)); dialog_menu: Option = |self|Some(Dialog::Menu(0)); dialog_save: Option = |self|Some(Dialog::Browser(BrowserTarget::SaveProject, Browser::new(None).unwrap().into())); dialog_load: Option = |self|Some(Dialog::Browser(BrowserTarget::LoadProject, Browser::new(None).unwrap().into())); dialog_import_clip: Option = |self|Some(Dialog::Browser(BrowserTarget::ImportClip(Default::default()), Browser::new(None).unwrap().into())); dialog_export_clip: Option = |self|Some(Dialog::Browser(BrowserTarget::ExportClip(Default::default()), Browser::new(None).unwrap().into())); dialog_import_sample: Option = |self|Some(Dialog::Browser(BrowserTarget::ImportSample(Default::default()), Browser::new(None).unwrap().into())); dialog_export_sample: Option = |self|Some(Dialog::Browser(BrowserTarget::ExportSample(Default::default()), Browser::new(None).unwrap().into())); dialog_options: Option = |self|Some(Dialog::Options); editor_pitch: Option = |self|Some((self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()); scene_count: usize = |self|self.scenes().len(); scene_selected: Option = |self|self.selection().scene(); track_count: usize = |self|self.tracks().len(); track_selected: Option = |self|self.selection().track(); select_scene: Selection = |self|self.selection().select_scene(self.tracks().len()); select_scene_next: Selection = |self|self.selection().select_scene_next(self.scenes().len()); select_scene_prev: Selection = |self|self.selection().select_scene_prev(); select_track: Selection = |self|self.selection().select_track(self.tracks().len()); select_track_next: Selection = |self|self.selection().select_track_next(self.tracks().len()); select_track_prev: Selection = |self|self.selection().select_track_prev(); clip_selected: Option>> = |self|match self.selection() { Selection::TrackClip { track, scene } => self.scenes()[*scene].clips[*track].clone(), _ => None }; device_kind: usize = |self|if let Dialog::Device(index) = self.dialog { index } else { 0 }; device_kind_next: usize = |self|if let Dialog::Device(index) = self.dialog { (index + 1) % device_kinds().len() } else { 0 }; device_kind_prev: usize = |self|if let Dialog::Device(index) = self.dialog { index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)) } else { 0 }; }); macro_rules!dsl_bind(($Command:ident: $State:ident { $($fn:ident = |$state:ident $(, $arg:ident:$ty:ty)*|$body:expr);* $(;)? })=>{ handle!(TuiIn: |self: $State, input|self.handle_tui_key_with_history(input)); #[tengri_proc::command(App)] impl $Command { $(fn $fn ($state: &mut $State $(, $arg:$ty)*) -> Perhaps { $body })* } }); dsl_bind!(AppCommand: App { enqueue = |app, clip: Option>>| { todo!() }; history = |app, delta: isize| { todo!() }; zoom = |app, zoom: usize| { todo!() }; stop_all = |app| { app.tracks_stop_all(); Ok(None) }; //dialog = |app, command: DialogCommand| //Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?); project = |app, command: ArrangementCommand| Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?); clock = |app, command: ClockCommand| Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c})); sampler = |app, command: SamplerCommand| Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command})) .transpose()?.flatten()); pool = |app, command: PoolCommand| { let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; // update linked editor after pool action match command { // autoselect: automatically load selected clip in editor PoolCommand::Select { .. } | // autocolor: update color in all places simultaneously PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { let clip = app.pool.clip().clone(); app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) }, _ => None }; Ok(undo) }; select = |app, selection: Selection| { *app.project.selection_mut() = selection; //todo! //if let Some(ref mut editor) = app.editor_mut() { //editor.set_clip(match selection { //Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app //.project //.scenes.get(scene) //.map(|s|s.clips.get(track)) //=> //Some(clip), //_ => //None //}); //} Ok(None) //("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { //(0, 0) => Self::Select(Selection::Mix), //(t, 0) => Self::Select(Selection::Track(t)), //(0, s) => Self::Select(Selection::Scene(s)), //(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) // autoedit: load focused clip in editor. }; //fn color (app: &mut App, theme: ItemTheme) -> Perhaps { //Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) //} //fn launch (app: &mut App) -> Perhaps { //app.project.launch(); //Ok(None) //} toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; // update linked sampler after editor action app.project.sampler_mut().map(|sampler|match command { // autoselect: automatically select sample in sampler MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, _ => {} }); undo } else { None }); }); //take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); //take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); //take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); //take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); //take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); //take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); //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(); //}); #[tengri_proc::command(Option)] impl DialogCommand { fn open (dialog: &mut Option, new: Dialog) -> Perhaps { *dialog = Some(new); Ok(None) } fn close (dialog: &mut Option) -> Perhaps { *dialog = None; Ok(None) } } impl App { pub fn toggle_dialog (&mut self, mut dialog: Dialog) -> Dialog { 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 { // Create new clip in pool when entering empty cell 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 } } else 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() { // Remove clip from arrangement when exiting empty clip editor 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()); } } } pub fn browser (&self) -> Option<&Browser> { if let Dialog::Browser(_, ref b) = self.dialog { Some(b) } else { None } } pub fn device_pick (&mut self, index: usize) { self.dialog = Dialog::Device(index); } pub fn add_device (&mut self, index: usize) -> Usually<()> { match index { 0 => { let name = self.jack.with_client(|c|c.name().to_string()); let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); let track = self.track().expect("no active track"); let port = format!("{}/Sampler", &track.name); let connect = Connect::exact(format!("{name}:{midi}")); let sampler = if let Ok(sampler) = Sampler::new( &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] ) { self.dialog = Dialog::None; Device::Sampler(sampler) } else { self.dialog = Dialog::Message("Failed to add device.".into()); return Err("failed to add device".into()) }; let track = self.track_mut().expect("no active track"); track.devices.push(sampler); Ok(()) }, 1 => { todo!(); Ok(()) }, _ => unreachable!(), } } } /////////////////////////////////////////////////////////////////////////////////////////////////// //#[derive(Clone, Debug)] //pub enum DialogCommand { //Open { dialog: Dialog }, //Close //} //impl Command> for DialogCommand { //fn execute (self, state: &mut Option) -> Perhaps { //match self { //Self::Open { dialog } => { //*state = Some(dialog); //}, //Self::Close => { //*state = None; //} //}; //Ok(None) //} //} //dsl!(DialogCommand: |self: Dialog, iter|todo!()); //Dsl::take(&mut self.dialog, iter)); //#[tengri_proc::command(Option)]//Nope. //impl DialogCommand { //fn open (dialog: &mut Option, new: Dialog) -> Perhaps { //*dialog = Some(new); //Ok(None) //} //fn close (dialog: &mut Option) -> Perhaps { //*dialog = None; //Ok(None) //} //} //