diff --git a/crates/app/app.rs b/crates/app/app.rs index 1bbec61d..f2c58c88 100644 --- a/crates/app/app.rs +++ b/crates/app/app.rs @@ -63,153 +63,6 @@ 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 })* } }); @@ -257,8 +110,7 @@ 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 { @@ -267,53 +119,136 @@ dsl_expose!(App { index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)) } else { 0 }; }); -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 })* } +/// 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 } + )* } }); dsl_view!(App { - 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() - })); + 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))) + //}, }); -fn wrap_dialog (dialog: Box>) -> impl Content { +fn wrap_dialog <'a> (dialog: impl Content + 'a) -> Box + 'a> { 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)))) + Repeat(" "), Outer(true, Style::default().fg(Tui::g(96))).enclose(dialog)))).boxed() } -fn wrap_dialog_menu (content: impl Content) -> impl Content { +fn wrap_dialog_menu <'a> (content: impl Content + 'a) -> Box + 'a> { 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)))))) + Fill::y(Align::n(Fill::x(content)))))).boxed() } has!(Jack<'static>: |self: App|self.jack); has!(Pool: |self: App|self.pool); @@ -326,34 +261,28 @@ 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) }); +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 } } 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); -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(); -//}); +/// 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, +} impl Profile { fn from_dsl (dsl: impl Dsl) -> Usually { let mut profile = Self { ..Default::default() }; @@ -369,6 +298,16 @@ 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"; @@ -378,8 +317,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()?; @@ -391,26 +330,30 @@ 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 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()); - //} - //))?; + } + ))?; + 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()); + } + ))?; 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("profiles.edn")?, Self::DEFAULT_PROFILES); + std::fs::write(self.dirs.place_config_file(path)?, val); } Ok(()) } @@ -476,124 +419,57 @@ impl ScenesView for App { } } -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 }}; } -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) - } - 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 +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 }) } - fn dialog (app: &mut App, command: DialogCommand) -> Perhaps { - panic!("dialog"); - Ok(command.delegate(&mut app.dialog, |command|Self::Dialog{command})?) +} + +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 })* } - 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 { +}); + +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| { let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; // update linked editor after pool action match command { @@ -607,17 +483,8 @@ handle!(TuiIn: |self: App, input|self.handle_tui_key_with_history(input)); _ => None }; Ok(undo) - } - 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 { + }; + select = |app, selection: Selection| { *app.project.selection_mut() = selection; //todo! //if let Some(ref mut editor) = app.editor_mut() { @@ -639,11 +506,7 @@ handle!(TuiIn: |self: App, input|self.handle_tui_key_with_history(input)); //(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})) //} @@ -651,10 +514,39 @@ handle!(TuiIn: |self: App, input|self.handle_tui_key_with_history(input)); //app.project.launch(); //Ok(None) //} -} - -#[tengri_proc::command(Option)] -impl DialogCommand { + 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) @@ -719,6 +611,88 @@ 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)]