diff --git a/crates/app/app.rs b/crates/app/app.rs index f2c58c88..1bbec61d 100644 --- a/crates/app/app.rs +++ b/crates/app/app.rs @@ -63,6 +63,153 @@ pub struct App { /// Base color. pub color: ItemTheme, } +/// Configuration +#[derive(Default, Debug)] +pub struct Config { + /// XDG basedirs + pub dirs: BaseDirectories, + /// Available view profiles + pub profiles: Arc, Profile>>>, + /// Available input bindings + pub bindings: Arc, Arc>>>, +} +/// Profile +#[derive(Default, Debug)] +pub struct Profile { + /// Path of configuration entrypoint + pub path: PathBuf, + /// Name of configuration + pub name: Option>, + /// Description of configuration + pub info: Option>, + /// View definition + pub view: Arc, + // Input keymap + pub keys: EventMap, +} +/// Various possible dialog modes. +#[derive(Debug, Clone)] +pub enum Dialog { + Help(usize), + Menu(usize), + Device(usize), + Message(Arc), + Browser(BrowserTarget, Browser), + Options, +} +impl App { + pub fn view (&self) -> impl Content + '_ { + Fill::xy(Bsp::a( + Fill::xy(ErrorBoundary::new(Ok(Some(Tui::bg(Black, self.view_dialog()))))), + Fill::xy(ErrorBoundary::new(Ok(Some(self.view_nil())))), + )) + } + fn handle_tui_key_with_history (&mut self, input: &TuiIn) -> Perhaps { + 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 + }) + } + pub fn update_clock (&self) { + ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } + 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 fn device_pick (&mut self, index: usize) { + self.dialog = Some(Dialog::Device(index)); + } + pub 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].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 = None; + Device::Sampler(sampler) + } else { + self.dialog = Some(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(()) + } + // 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()); + } + } + } +} macro_rules!dsl_expose(($Struct:ident { $($fn:ident: $ret:ty = |$self:ident|$body:expr);* $(;)? })=>{ #[tengri_proc::expose] impl $Struct { $(fn $fn (&$self) -> $ret { $body })* } }); @@ -110,7 +257,8 @@ dsl_expose!(App { 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 }; + _ => None + }; device_kind: usize = |self|if let Some(Dialog::Device(index)) = self.dialog { index } else { 0 }; device_kind_next: usize = |self|if let Some(Dialog::Device(index)) = self.dialog { @@ -119,136 +267,53 @@ dsl_expose!(App { index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)) } else { 0 }; }); -/// Various possible dialog modes. -#[derive(Debug, Clone)] -pub enum Dialog { - Help(usize), - Menu(usize), - Device(usize), - Message(Arc), - Browser(BrowserTarget, Browser), - Options, -} -impl App { - pub fn view <'a> (&'a self) -> impl Content + 'a { - Fill::xy(Bsp::a(self.view_overlay(), self.view_content())) - } - pub fn update_clock (&self) { - ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } -} -macro_rules!dsl_view(($Struct:ident { $($fn:ident = |$self:ident $(, $arg:ident:$ty:ty)*|$body:expr);* $(;)? })=>{ - content!(TuiOut: |self: $Struct| { - ErrorBoundary::new(Ok(Some(Tui::bg(Black, self.view())))) - }); - #[tengri_proc::view(TuiOut)] impl $Struct { $( - fn $fn (&$self $(, $arg: $ty)*) -> impl Content + '_ { $body } - )* } +macro_rules!dsl_view(($Struct:ident { $($fn:ident = |$self:ident|$body:expr);* $(;)? })=>{ + content!(TuiOut: |self: $Struct| { ErrorBoundary::new(Ok(Some(Tui::bg(Black, self.view())))) }); + #[tengri_proc::view(TuiOut)] impl $Struct { $(fn $fn (&$self) -> impl Content + '_ { $body })* } }); dsl_view!(App { - view_nil = |self|"·"; - view_content = |self|Fill::xy(ErrorBoundary::new(Ok(Some(self.view_nil())))); - view_overlay = |self|Fill::xy(ErrorBoundary::new(Ok(Some(Tui::bg(Black, ThunkRender::new(|to| - match self.dialog.as_ref() { - Some(Dialog::Help(scrolled)) => Render::render(&self.view_help(*scrolled), to), - Some(Dialog::Menu(selected)) => Render::render(&self.view_menu(*selected), to), - Some(Dialog::Device(selected)) => Render::render(&self.view_device(*selected), to), - Some(Dialog::Message(message)) => Render::render(&self.view_message(message.clone()), to), - Some(Dialog::Browser(target, browser)) => Render::render(&self.view_browser(target.clone(), browser.clone()), to), - Some(Dialog::Options) => Render::render(&self.view_options(), to), - _ => unimplemented!() })))))); - view_menu = |self, selected: usize|{ - let profiles = self.config.profiles.clone(); - wrap_dialog(wrap_dialog_menu(Stack::south(move|add: &mut dyn FnMut(&dyn Render)|{ - ////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))) - for (index, (id, profile)) in profiles.read().unwrap().iter().enumerate() { - let bg = if index == 0 { Rgb(64,64,64) } else { Rgb(32,32,32) }; - let name = profile.name.as_ref().map(|x|unquote(unquote(x.as_ref()))); - let info = profile.info.as_ref().map(|x|unquote(unquote(x.as_ref()))); - add(&Fixed::y(3, Tui::bg(bg, Bsp::s( - Fill::x(Bsp::a( - Fill::x(Align::w(Tui::fg(Rgb(224,192,128), name))), - Fill::x(Align::e(Tui::fg(Rgb(224,128,32), &id))) - )), - Fill::x(Align::w(info)))))); } }))) }; - view_help = |self, scrolled: usize|wrap_dialog(Bsp::s(Tui::bold(true, "Help"), "TODO")); - view_options = |self|wrap_dialog("TODO"); - view_message = |self, message: Arc|wrap_dialog(Bsp::s(message.clone(), Bsp::s("", "[ OK ]"))); - view_browser = |self, target: BrowserTarget, browser: Browser|wrap_dialog(Bsp::s( - Padding::xy(3, 1, Fill::x(Align::w(FieldV( - Default::default(), - match target { - BrowserTarget::SaveProject => "Save project:", - BrowserTarget::LoadProject => "Load project:", - BrowserTarget::ImportSample(_) => "Import sample:", - BrowserTarget::ExportSample(_) => "Export sample:", - BrowserTarget::ImportClip(_) => "Import clip:", - BrowserTarget::ExportClip(_) => "Export clip:", - }, - Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))), - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::xy(browser)))); - view_device = |self, selected: usize|wrap_dialog(Bsp::s( - Tui::bold(true, "Add device"), - Map::south(1, - move||device_kinds().iter(), - move|label: &&'static str, i| - Fill::x(Tui::bg(if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }, - Bsp::e(if i == selected { "[ " } else { " " }, - Bsp::w(if i == selected { " ]" } else { " " }, - "FIXME device name"))))))); - //Bsp::s("", - //Map::south(1, - //move||self.config.bindings.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 }) - //.skip(offset) - //.take(20), - //|mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e( - //b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), - //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))), - - //Dialog::Browser(BrowserTarget::Load, browser) => { - //"bobcat".boxed() - ////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(browser))) - //}, - //Dialog::Browser(BrowserTarget::Export, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - ////Tui::bold(true, " Export: "), - ////Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::xy(browser))) - //}, - //Dialog::Browser(BrowserTarget::Import, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - ////Tui::bold(true, " Import: "), - ////Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::xy(browser))) - //}, + view_nil = |self|"·"; + view_dialog = |self|self.dialog.as_ref().map(|dialog|wrap_dialog(match dialog { + Dialog::Menu(selected) => wrap_dialog_menu({ + let profiles = self.config.profiles.clone(); + Stack::south(move|add: &mut dyn FnMut(&dyn Render)|{ + for (index, (id, profile)) in profiles.read().unwrap().iter().enumerate() { + let bg = if index == 0 { Rgb(64,64,64) } else { Rgb(32,32,32) }; + let name = profile.name.as_ref().map(|x|unquote(unquote(x.as_ref()))); + let info = profile.info.as_ref().map(|x|unquote(unquote(x.as_ref()))); + add(&Fixed::y(3, Tui::bg(bg, Bsp::s( + Fill::x(Bsp::a( + Fill::x(Align::w(Tui::fg(Rgb(224,192,128), name))), + Fill::x(Align::e(Tui::fg(Rgb(224,128,32), &id))) + )), + Fill::x(Align::w(info)) + )))); + } + }) + }).boxed(), + //Self::Help(offset) => + //self.view_dialog_help(*offset).boxed(), + //Self::Browser(target, browser) => + //self.view_dialog_browser(target, browser).boxed(), + //Self::Options => + //self.view_dialog_options().boxed(), + //Self::Device(index) => + //self.view_dialog_device(*index).boxed(), + //Self::Message(message) => + //self.view_dialog_message(message).boxed(), + _ => "kyp".boxed() + })); }); -fn wrap_dialog <'a> (dialog: impl Content + 'a) -> Box + 'a> { +fn wrap_dialog (dialog: Box>) -> impl Content { 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(dialog)))).boxed() + Repeat(" "), Outer(true, Style::default().fg(Tui::g(96))).enclose(dialog)))) } -fn wrap_dialog_menu <'a> (content: impl Content + 'a) -> Box + 'a> { +fn wrap_dialog_menu (content: impl Content) -> impl Content { Tui::bg(Rgb(0,0,0), Bsp::s( Fill::x(Fixed::y(3, Tui::bg(Rgb(33,33,33), Tui::bold(true, "tek 0.3.0-rc0")))), Bsp::n( Fill::x(Fixed::y(3, Tui::bg(Rgb(33,33,33), Bsp::e(Tui::fg(Rgb(255,192,48), "[Enter]"), " new session")))), - Fill::y(Align::n(Fill::x(content)))))).boxed() + Fill::y(Align::n(Fill::x(content)))))) } has!(Jack<'static>: |self: App|self.jack); has!(Pool: |self: App|self.pool); @@ -261,28 +326,34 @@ 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); -has_clips!( |self: App|self.pool.clips); -maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); -maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); -impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.inner_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() } } -/// Profile -#[derive(Default, Debug)] -pub struct Profile { - /// Path of configuration entrypoint - pub path: PathBuf, - /// Name of configuration - pub name: Option>, - /// Description of configuration - pub info: Option>, - /// View definition - pub view: Arc, - // Input keymap - pub keys: EventMap, -} +has_clips!(|self: App|self.pool.clips); +impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.inner_size } } +//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(); +//}); impl Profile { fn from_dsl (dsl: impl Dsl) -> Usually { let mut profile = Self { ..Default::default() }; @@ -298,16 +369,6 @@ impl Profile { Ok(profile) } } -/// Configuration -#[derive(Default, Debug)] -pub struct Config { - /// XDG basedirs - pub dirs: BaseDirectories, - /// Available view profiles - pub profiles: Arc, Profile>>>, - /// Available input bindings - pub bindings: Arc, EventMap>>>, -} impl Config { const PROFILES: &'static str = "profiles.edn"; const BINDINGS: &'static str = "bindings.edn"; @@ -317,8 +378,8 @@ impl Config { let mut dirs = BaseDirectories::with_profile("tek", "v0"); let mut cfgs = Self { dirs, ..Default::default() }; cfgs.init_file(Self::PROFILES, Self::DEFAULT_PROFILES)?; - cfgs.load_file(Self::PROFILES, |cfgs, dsl|Ok( - if dsl.exp().head().key() == Ok(Some("module")) { + cfgs.load_file(Self::PROFILES, |cfgs, dsl|{ + Ok(if dsl.exp().head().key() == Ok(Some("module")) { let exp = dsl.exp()?; let tail = exp.tail()?; let head = tail.head()?; @@ -330,30 +391,26 @@ impl Config { } } else { return Err("unexpected: {exp:?}".into()); - } - ))?; - cfgs.init_file(Self::BINDINGS, Self::DEFAULT_BINDINGS)?; - cfgs.load_file(Self::BINDINGS, |cfgs, dsl|Ok( - if dsl.exp().head().key() == Ok(Some("module")) { - let exp = dsl.exp()?; - let tail = exp.tail()?; - let head = tail.head()?; - if let Some(id) = head.sym()? { - cfgs.bindings.write().unwrap().insert( - id.into(), - EventMap::from_dsl(&mut tail.tail()?)? - ); - } - } else { - return Err("unexpected: {exp:?}".into()); - } - ))?; + }) + })?; + //cfgs.init_file(Self::BINDINGS, Self::DEFAULT_BINDINGS)?; + //cfgs.load_file(Self::BINDINGS, |cfgs, dsl|Ok( + //if let Some(exp) = dsl.head()?.exp()? && exp.head()?.key()? == Some("module") { + //let name = exp.tail()?.head()?.unwrap_or_default().into(); + //println!("name = {name}"); + //let body = exp.tail()?.tail()?.unwrap_or_default().into(); + //println!("body = {body}"); + //cfgs.bindings.write().unwrap().insert(name, body); + //} else { + //return Err("unexpected: {exp:?}".into()); + //} + //))?; println!("{cfgs:#?}"); Ok(cfgs) } fn init_file (&mut self, path: &str, val: &str) -> Usually<()> { if self.dirs.find_config_file(path).is_none() { - std::fs::write(self.dirs.place_config_file(path)?, val); + std::fs::write(self.dirs.place_config_file("profiles.edn")?, Self::DEFAULT_PROFILES); } Ok(()) } @@ -419,57 +476,124 @@ impl ScenesView for App { } } -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 - }) +impl Dialog { + pub fn view_dialog_menu <'a> (selected: usize) -> impl Content + 'a { + //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, offset: usize) -> impl Content + 'a { + Bsp::s(Tui::bold(true, "Help"), "FIXME") + //Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1, + //move||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 }) + //.skip(offset) + //.take(20), + //|mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e( + //b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), + //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim())))))))))) + } + pub fn view_dialog_device (&self, index: usize) -> impl Content + use<'_> { + let choices = ||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 Arc) + -> impl Content + use<'a> + { + Bsp::s(message.as_ref(), Bsp::s("", "[ OK ]")) + } + pub fn view_dialog_browser <'a> (&'a self, target: &BrowserTarget, browser: &'a Browser) -> impl Content + use<'a> { + Bsp::s( + Padding::xy(3, 1, Fill::x(Align::w(FieldV( + Default::default(), + match target { + BrowserTarget::SaveProject => "Save project:", + BrowserTarget::LoadProject => "Load project:", + BrowserTarget::ImportSample(_) => "Import sample:", + BrowserTarget::ExportSample(_) => "Export sample:", + BrowserTarget::ImportClip(_) => "Import clip:", + BrowserTarget::ExportClip(_) => "Export clip:", + }, + Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))), + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::xy(browser))) + } + pub fn view_dialog_load <'a> (&'a self, browser: &'a Browser) -> 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(browser))) + } + pub fn view_dialog_export <'a> (&'a self, browser: &'a Browser) -> impl Content + use<'a> { + Bsp::s( + Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + Tui::bold(true, " Export: "), + Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::xy(browser))) + } + pub fn view_dialog_import <'a> (&'a self, browser: &'a Browser) -> impl Content + use<'a> { + Bsp::s( + Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + Tui::bold(true, " Import: "), + Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::xy(browser))) + } + pub fn view_dialog_options <'a> (&'a self) -> impl Content + use<'a> { + "TODO" } } - 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 }}; } - -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 })* +handle!(TuiIn: |self: App, input|self.handle_tui_key_with_history(input)); +#[tengri_proc::command(App)] impl AppCommand { + fn toggle_editor (app: &mut App, value: bool) -> Perhaps { + app.toggle_editor(Some(value)); + Ok(None) } -}); - -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, |command|Self::Dialog{command})?); - project = |app, command: ArrangementCommand|Ok( - command.delegate(&mut app.project, |command|Self::Project{command})?); - clock = |app, command: ClockCommand|Ok( - command.execute(app.clock_mut())?.map(|command|Self::Clock{command})); - sampler = |app, command: SamplerCommand|Ok(app.project.sampler_mut() - .map(|s|command.delegate(s, |command|Self::Sampler{command})) - .transpose()? - .flatten()); - pool = |app, command: PoolCommand| { + fn editor (app: &mut App, command: MidiEditCommand) -> Perhaps { + 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 + }) + } + fn dialog (app: &mut App, command: DialogCommand) -> Perhaps { + panic!("dialog"); + Ok(command.delegate(&mut app.dialog, |command|Self::Dialog{command})?) + } + fn project (app: &mut App, command: ArrangementCommand) -> Perhaps { + Ok(command.delegate(&mut app.project, |command|Self::Project{command})?) + } + fn clock (app: &mut App, command: ClockCommand) -> Perhaps { + Ok(command.execute(app.clock_mut())?.map(|command|Self::Clock{command})) + } + fn sampler (app: &mut App, command: SamplerCommand) -> Perhaps { + Ok(app.project.sampler_mut() + .map(|s|command.delegate(s, |command|Self::Sampler{command})) + .transpose()? + .flatten()) + } + fn pool (app: &mut App, command: PoolCommand) -> Perhaps { let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; // update linked editor after pool action match command { @@ -483,8 +607,17 @@ dsl_bind!(AppCommand: App { _ => None }; Ok(undo) - }; - select = |app, selection: Selection| { + } + 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 select (app: &mut App, selection: Selection) -> Perhaps { *app.project.selection_mut() = selection; //todo! //if let Some(ref mut editor) = app.editor_mut() { @@ -506,7 +639,11 @@ dsl_bind!(AppCommand: App { //(0, s) => Self::Select(Selection::Scene(s)), //(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) // autoedit: load focused clip in editor. - }; + } + fn stop_all (app: &mut App) -> Perhaps { + app.tracks_stop_all(); + Ok(None) + } //fn color (app: &mut App, theme: ItemTheme) -> Perhaps { //Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) //} @@ -514,39 +651,10 @@ dsl_bind!(AppCommand: App { //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 { +} + +#[tengri_proc::command(Option)] +impl DialogCommand { fn open (dialog: &mut Option, new: Dialog) -> Perhaps { *dialog = Some(new); Ok(None) @@ -611,88 +719,6 @@ audio!( } ); -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 { - // 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> { - self.dialog.as_ref().and_then(|dialog|match dialog { - Dialog::Browser(_, b) => Some(b), - _ => None - }) - } - pub fn device_pick (&mut self, index: usize) { - self.dialog = Some(Dialog::Device(index)); - } - pub fn device_add (&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 = None; - Device::Sampler(sampler) - } else { - self.dialog = Some(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)]