use crate::*; use std::path::PathBuf; type MaybeClip = Option>>; macro_rules! ns { ($C:ty, $s:expr, $a:expr, $W:expr) => { <$C>::try_from_expr($s, $a).map($W) } } macro_rules! cmd { ($cmd:expr) => {{ $cmd; None }}; } macro_rules! cmd_todo { ($msg:literal) => {{ println!($msg); None }}; } handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.command(self, input) { let undo = command.execute(self)?; if let Some(undo) = undo { self.history.push(undo); } Some(true) } else { None })); #[tengri_proc::expose] impl App { fn _todo_isize_stub (&self) -> isize { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } fn focus_editor (&self) -> bool { self.is_editing() } 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_clip (&self) -> bool { !self.is_editing() && self.selected.is_clip() } fn focus_track (&self) -> bool { !self.is_editing() && self.selected.is_track() } fn focus_scene (&self) -> bool { !self.is_editing() && self.selected.is_scene() } fn focus_mix (&self) -> bool { !self.is_editing() && self.selected.is_mix() } fn focus_pool_import (&self) -> bool { matches!(self.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(..))) } fn focus_pool_rename (&self) -> bool { matches!(self.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(..))) } fn editor_pitch (&self) -> Option { Some((self.editor().map(|e|e.get_note_pos()).unwrap() as u8).into()) } /// Width of display pub(crate) fn w (&self) -> u16 { self.size.w() as u16 } /// Width allocated for sidebar. pub(crate) fn w_sidebar (&self) -> u16 { self.w() / if self.is_editing() { 16 } else { 8 } as u16 } /// Width taken by all tracks. pub(crate) fn w_tracks (&self) -> u16 { self.tracks_with_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0) } /// Width available to display tracks. pub(crate) fn w_tracks_area (&self) -> u16 { self.w().saturating_sub(2 * self.w_sidebar()) } /// Height of display pub(crate) fn h (&self) -> u16 { self.size.h() as u16 } /// Height available to display track headers. pub(crate) 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 { //15 self.h().saturating_sub( self.h_inputs() + self.h_outputs() + self.h_devices() + 13 // FIXME ) } /// Height taken by all scenes. pub(crate) fn h_scenes (&self) -> u16 { self.scenes_with_sizes(self.is_editing(), Self::H_SCENE, Self::H_EDITOR).last() .map(|(_, _, _, y)|y as u16).unwrap_or(0) } /// Height taken by all inputs. pub(crate) fn h_inputs (&self) -> u16 { self.inputs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } /// Height taken by all outputs. pub(crate) fn h_outputs (&self) -> u16 { self.outputs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } /// Height taken by visible device slots. pub(crate) fn h_devices (&self) -> u16 { 2 //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } fn scene_count (&self) -> usize { self.scenes.len() } fn scene_selected (&self) -> Option { self.selected.scene() } fn track_count (&self) -> usize { self.tracks.len() } fn track_selected (&self) -> Option { self.selected.track() } fn select_scene_next (&self) -> Selection { self.selected.scene_next(self.scenes.len()) } fn select_scene_prev (&self) -> Selection { self.selected.scene_prev() } fn select_track_header (&self) -> Selection { self.selected.track_header(self.tracks.len()) } fn select_track_next (&self) -> Selection { self.selected.track_next(self.tracks.len()) } fn select_track_prev (&self) -> Selection { self.selected.track_prev() } fn clip_selected (&self) -> Option>> { match self.selected { 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 } } } #[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_help (app: &mut App, value: bool) -> Perhaps { app.toggle_dialog(Some(Dialog::Help)); Ok(None) } fn toggle_menu (app: &mut App, value: bool) -> Perhaps { app.toggle_dialog(Some(Dialog::Menu)); Ok(None) } fn toggle_edit (app: &mut App, value: bool) -> Perhaps { app.toggle_editor(Some(value)); Ok(None) } fn color (app: &mut App, theme: ItemTheme) -> Perhaps { Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) } fn enqueue (app: &mut App, clip: Option>>) -> Perhaps { todo!() } fn history (app: &mut App, delta: isize) -> Perhaps { todo!() } fn zoom (app: &mut App, zoom: usize) -> Perhaps { todo!() } fn launch (app: &mut App) -> Perhaps { app.launch(); Ok(None) } fn select (app: &mut App, selection: Selection) -> Perhaps { app.select(selection); 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 }) }))) } fn stop_all (app: &mut App) -> Perhaps { app.stop_all(); Ok(None) } fn sampler (app: &mut App, command: SamplerCommand) -> Perhaps { Ok(app.sampler_mut() .map(|s|command.delegate(s, |command|Self::Sampler{command})) .transpose()? .flatten()) } fn scene (app: &mut App, command: SceneCommand) -> Perhaps { Ok(command.delegate(app, |command|Self::Scene{command})?) } fn track (app: &mut App, command: TrackCommand) -> Perhaps { Ok(command.delegate(app, |command|Self::Track{command})?) } fn input (app: &mut App, command: InputCommand) -> Perhaps { Ok(command.delegate(app, |command|Self::Input{command})?) } fn output (app: &mut App, command: OutputCommand) -> Perhaps { Ok(command.delegate(app, |command|Self::Output{command})?) } fn clip (app: &mut App, command: ClipCommand) -> Perhaps { Ok(command.delegate(app, |command|Self::Clip{command})?) } fn clock (app: &mut App, command: ClockCommand) -> Perhaps { 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})?) } fn message (app: &mut App, command: MessageCommand) -> Perhaps { Ok(command.delegate(app, |command|Self::Message{command})?) } fn editor (app: &mut App, command: MidiEditCommand) -> Perhaps { 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 { // autoselect: automatically select sample in sampler MidiEditCommand::NotePos { pos } => { sampler.set_note_pos(pos); }, _ => {} }); undo } else { None }) } fn pool (app: &mut App, command: PoolCommand) -> Perhaps { Ok(if let Some(pool) = app.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 { // autoselect: automatically load selected clip in editor PoolCommand::Select { .. } | // autocolor: update color in all places simultaneously PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => editor.set_clip(pool.clip().as_ref()), _ => {} }); undo } else { None }) } } impl<'state> Context<'state, ClockCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { Context::get(&self.clock, iter) } } impl<'state> Context<'state, MidiEditCommand> for App { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { self.editor().map(|e|Context::get(e, iter)).flatten() } } 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() } } 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() } } #[tengri_proc::command(App)] impl InputCommand { fn add (app: &mut App) -> Perhaps { app.midi_in_add()?; Ok(None) } } #[tengri_proc::command(App)] impl OutputCommand { fn add (app: &mut App) -> Perhaps { app.midi_out_add()?; Ok(None) } } #[tengri_proc::command(App)] impl DeviceCommand { fn picker (app: &mut App) -> Perhaps { app.device_picker_show(); Ok(None) } fn pick (app: &mut App, i: usize) -> Perhaps { app.device_pick(i); Ok(None) } fn add (app: &mut App, i: usize) -> Perhaps { app.device_add(i); Ok(None) } } #[tengri_proc::command(App)] impl MessageCommand { fn dismiss (app: &mut App) -> Perhaps { app.message_dismiss(); Ok(None) } } #[tengri_proc::command(App)] impl TrackCommand { fn toggle_play (app: &mut App) -> Perhaps { todo!() } fn toggle_solo (app: &mut App) -> Perhaps { todo!() } fn toggle_rec (app: &mut App) -> Perhaps { app.track_toggle_record(); Ok(Some(Self::ToggleRec)) } fn toggle_mon (app: &mut App) -> Perhaps { app.track_toggle_monitor(); Ok(Some(Self::ToggleMon)) } fn set_size (app: &mut App, size: usize) -> Perhaps { todo!() } fn set_zoom (app: &mut App, zoom: usize) -> Perhaps { todo!() } fn swap (app: &mut App, index: usize, other: usize) -> Perhaps { todo!(); Ok(Some(Self::Swap { index, other })) } fn del (app: &mut App, index: usize) -> Perhaps { app.track_del(index); Ok(None) } fn stop (app: &mut App, index: usize) -> Perhaps { app.tracks[index].sequencer.enqueue_next(None); Ok(None) } fn add (app: &mut App) -> Perhaps { Ok(Some(Self::Del { index: app.track_add_focus()? })) } fn set_color (app: &mut App, index: usize, color: ItemTheme) -> Perhaps { Ok(Some(Self::SetColor { index, color: app.track_set_color(index, color) })) } } #[tengri_proc::command(App)] impl SceneCommand { fn add (app: &mut App) -> Perhaps { todo!() } fn del (app: &mut App, index: usize) -> Perhaps { app.scene_del(index); Ok(None) } fn enqueue (app: &mut App, index: usize) -> Perhaps { app.scene_enqueue(index); Ok(None) } fn set_color (app: &mut App, index: usize, color: ItemTheme) -> Perhaps { Ok(Some(Self::SetColor { index, color: app.scene_set_color(index, color) })) } fn set_size (app: &mut App, index: usize, size: usize) -> Perhaps { todo!() } fn set_zoom (app: &mut App, index: usize, zoom: usize) -> Perhaps { todo!() } fn swap (app: &mut App, index: usize, other: usize) -> Perhaps { todo!(); Ok(Some(Self::Swap { index, other })) } } #[tengri_proc::command(App)] impl ClipCommand { fn get (app: &mut App, a: usize, b: usize) -> Perhaps { //(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}")) //("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap()))) todo!() } fn edit (app: &mut App, a: usize, b: usize) -> Perhaps { //(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}")) //("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap()))) todo!() } fn set_loop (app: &mut App, a: usize, b: usize) -> Perhaps { //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) todo!() } fn put (app: &mut App, a: usize, b: usize) -> Perhaps { //(Put [t: usize, s: usize, c: MaybeClip] //Some(Self::Put(t, s, app.clip_put(t, s, c)))) //("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))) todo!() } fn del (app: &mut App, a: usize, b: usize) -> Perhaps { //("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None)))) todo!() } fn enqueue (app: &mut App, a: usize, b: usize) -> Perhaps { //(Enqueue [t: usize, s: usize] //cmd!(app.tracks[t].sequencer.enqueue_next(app.scenes[s].clips[t].as_ref()))) //("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap()))) todo!() } fn set_color (app: &mut App, a: usize, b: usize) -> Perhaps { //(SetColor [t: usize, s: usize, c: ItemTheme] //app.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); //("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) 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!() } }