diff --git a/app/tek.edn b/app/tek.edn index 4e1c14a8..5ecbdcff 100644 --- a/app/tek.edn +++ b/app/tek.edn @@ -10,8 +10,8 @@ (keys :axis/i2 (@lt i2/dec) (@gt z2/inc)) (keys :axis/w (@openbracket w/dec) (@closebracket w/inc)) (keys :axis/w2 (@openbrace w2/dec) (@closebrace w2/inc)) -(mode :menu (name Menu) (info Mode selector.) (keys :axis/y :confirm) (bg (g 0) - (bsp/s :ports/out (bsp/n :ports/in (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill :dialog/menu))))))) +(mode :menu (name Menu) (info Mode selector.) (keys :axis/y :confirm) + (bg (g 0) (bsp/s :ports/out (bsp/n :ports/in (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill :dialog/menu))))))) (view :ports/out (fill/x (fixed/y 3 (bsp/a (fill/x (align/w (text L-AUDIO-OUT))) (bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R)))))))) diff --git a/app/tek.rs b/app/tek.rs index 90792cbe..9b474c77 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -1,5 +1,4 @@ #![allow(clippy::unit_arg)] - #![feature(adt_const_params, associated_type_defaults, closure_lifetime_binder, @@ -8,9 +7,7 @@ trait_alias, type_alias_impl_trait, type_changing_struct_update)] - #[cfg(test)] mod tek_test; - #[allow(unused)] pub(crate) use ::{ std::path::{Path, PathBuf}, std::sync::{Arc, RwLock}, @@ -38,7 +35,6 @@ event::{Event, KeyEvent, KeyCode::{self, *}}, }, }; - pub mod model { use super::{*, gui::*}; /// Total state @@ -63,6 +59,8 @@ pub mod model { pub pool: Pool, /// Contains the currently edited musical arrangement pub project: Arrangement, + /// Error, if any + pub error: Arc>>> } /// Configuration: mode, view, and bind definitions. #[derive(Default, Debug)] pub struct Config { @@ -83,7 +81,7 @@ pub mod model { pub type Views = Arc, Arc>>>; /// Group of view and keys definitions. #[derive(Default, Debug)] - pub struct Mode { + pub struct Mode { pub path: PathBuf, pub name: Vec, pub info: Vec, @@ -158,11 +156,14 @@ pub mod core { } } impl Config { - const CONFIG: &'static str = "tek.edn"; - const DEFAULTS: &'static str = include_str!("./tek.edn"); + const CONFIG_DIR: &'static str = "tek"; + const CONFIG_SUB: &'static str = "v0"; + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("./tek.edn"); /// Create a new app configuration from a set of XDG base directories, pub fn new (dirs: Option) -> Self { - let dirs = dirs.unwrap_or_else(||BaseDirectories::with_profile("tek", "v0")); + let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); + let dirs = dirs.unwrap_or_else(default); Self { dirs, ..Default::default() } } /// Write initial contents of configuration. @@ -187,10 +188,10 @@ pub mod core { }) } /// Add statements to configuration from [Dsl] source. - pub fn add (&mut self, dsl: impl Dsl) -> Usually<()> { + pub fn add (&mut self, dsl: impl Language) -> Usually<()> { dsl.each(|item|self.add_one(item)) } - fn add_one (&self, item: impl Dsl) -> Usually<()> { + fn add_one (&self, item: impl Language) -> Usually<()> { if let Some(expr) = item.expr()? { let head = expr.head()?; let tail = expr.tail()?; @@ -212,17 +213,17 @@ pub mod core { self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() } } - pub fn load_view (views: &Views, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { + pub fn load_view (views: &Views, name: &impl AsRef, body: &impl Language) -> Usually<()> { views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); Ok(()) } - pub fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { + pub fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Language) -> Usually<()> { let mut mode = Mode::default(); body.each(|item|mode.add(item))?; modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); Ok(()) } - pub fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { + pub fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Language) -> Usually<()> { let mut map = Bind::new(); body.each(|item|if item.expr().head() == Ok(Some("see")) { // TODO @@ -249,7 +250,7 @@ pub mod core { Ok(()) } impl Mode> { - fn add (&mut self, dsl: impl Dsl) -> Usually<()> { + fn add (&mut self, dsl: impl Language) -> Usually<()> { Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { //println!("Mode::add: {head} {:?}", expr.tail()); let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); @@ -304,7 +305,7 @@ pub mod core { } } impl Binding { - pub fn from_dsl (dsl: impl Dsl) -> Usually { + pub fn from_dsl (dsl: impl Language) -> Usually { let command: Option = None; let condition: Option = None; let description: Option> = None; @@ -317,289 +318,28 @@ pub mod core { } } } -pub mod ns { - // TODO make these enumerable: - // - // impl Ns> for App { - // const NS: To> = To::new(|_, _|Default::default()) - // .key(":foo", |_|"bar".into()) - // .key(":bar", |_|"baz".into()); - // } - // - // - use super::{*, model::*, gui::*}; - // Allow source to be read as Literal string - dsl_ns!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); - // Provide boolean values. - dsl_ns!(App: bool { - // TODO literal = ... - word = |app| { - ":mode/editor" => app.project.editor.is_some(), - ":focused/dialog" => !matches!(app.dialog, Dialog::None), - ":focused/message" => matches!(app.dialog, Dialog::Message(..)), - ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), - ":focused/browser" => app.dialog.browser().is_some(), - ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), - ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), - ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), - ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), - ":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}), - ":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)), - ":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)), - ":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix), - }; - }); - // TODO: provide colors here - dsl_ns!(App: ItemTheme {}); - dsl_ns!(App: Dialog { - word = |app| { - ":dialog/none" => Dialog::None, - ":dialog/options" => Dialog::Options, - ":dialog/device" => Dialog::Device(0), - ":dialog/device/prev" => Dialog::Device(0), - ":dialog/device/next" => Dialog::Device(0), - ":dialog/help" => Dialog::Help(0), - ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, - Browse::new(None).unwrap().into()), - ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, - Browse::new(None).unwrap().into()), - ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), - Browse::new(None).unwrap().into()), - }; - }); - dsl_ns!(App: Selection { - word = |app| { - ":select/scene" => app.selection().select_scene(app.tracks().len()), - ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), - ":select/scene/prev" => app.selection().select_scene_prev(), - ":select/track" => app.selection().select_track(app.tracks().len()), - ":select/track/next" => app.selection().select_track_next(app.tracks().len()), - ":select/track/prev" => app.selection().select_track_prev(), - }; - }); - dsl_ns!(App: Color { - word = |app| { - ":color/bg" => Color::Rgb(28, 32, 36), - }; - expr = |app| { - "g" (n: u8) => Color::Rgb(n, n, n), - "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), - }; - }); - dsl_ns!(App: Option { - word = |app| { - ":editor/pitch" => Some( - (app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into() - ) - }; - }); - dsl_ns!(App: Option { - word = |app| { - ":selected/scene" => app.selection().scene(), - ":selected/track" => app.selection().track(), - }; - }); - dsl_ns!(App: Option>> { - word = |app| { - ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { - app.scenes()[*scene].clips[*track].clone() - } else { - None - } - }; - }); - dsl_ns!(App: u8 { - literal = |dsl|Ok(if let Some(src) = dsl.src()? { - Some(to_number(src)? as u8) - } else { - None - }); - }); - dsl_ns!(App: u16 { - literal = |dsl|Ok(if let Some(src) = dsl.src()? { - Some(to_number(src)? as u16) - } else { - None - }); - word = |app| { - ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), - ":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9), - }; - }); - dsl_ns!(App: usize { - literal = |dsl|Ok(if let Some(src) = dsl.src()? { - Some(to_number(src)? as usize) - } else { - None - }); - word = |app| { - ":scene-count" => app.scenes().len(), - ":track-count" => app.tracks().len(), - ":device-kind" => app.dialog.device_kind().unwrap_or(0), - ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), - ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), - }; - }); - dsl_ns!(App: isize { - literal = |dsl|Ok(if let Some(src) = dsl.src()? { - Some(to_number(src)? as isize) - } else { - None - }); - }); - impl<'a> DslNs<'a, AppCommand> for App {} - impl<'a> DslNsExprs<'a, AppCommand> for App {} - impl<'a> DslNsWords<'a, AppCommand> for App { - dsl_words!('a |app| -> AppCommand { - "x/inc" => AppCommand::Inc { axis: Axis::X }, - "x/dec" => AppCommand::Dec { axis: Axis::X }, - "y/inc" => AppCommand::Inc { axis: Axis::Y }, - "y/dec" => AppCommand::Dec { axis: Axis::Y }, - "confirm" => AppCommand::Confirm, - "cancel" => AppCommand::Cancel, - }); - } - impl App { - pub fn update_clock (&self) { - ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } - /// Set modal dialog. - pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { - std::mem::swap(&mut self.dialog, &mut dialog); - dialog - } - /// Set picked device in device pick dialog. - 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!(), - } - } - /// Return reference to content browser if open. - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } - } - /// Is a MIDI editor currently focused? - pub fn editor_focused (&self) -> bool { false } - /// Toggle MIDI editor. - 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(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 mod tui { use super::{*, model::*, gui::*}; - handle!(TuiIn: |self: App, input|{ - let mut commands = vec![]; - for id in self.mode.keys.iter() { - if let Some(event_map) = self.config.binds.clone().read().unwrap().get(id.as_ref()) { - if let Some(bindings) = event_map.query(input.event()) { - for binding in bindings { - for command in binding.commands.iter() { - if let Some(command) = self.from(command)? as Option { - commands.push(command) - } - } - } - } - } - } - for command in commands.into_iter() { - let result = command.execute(self); - match result { - Ok(undo) => { - self.history.push((command, undo)); - }, - Err(e) => { - self.history.push((command, None)); - return Err(e) - } - } - } - Ok(None) - }); impl Draw for App { fn draw (&self, to: &mut TuiOut) { - for (index, dsl) in self.mode.view.iter().enumerate() { + if let Some(e) = self.error.read().unwrap().as_ref() { + to.place_at(to.area(), e); + } + for (_index, dsl) in self.mode.view.iter().enumerate() { if let Err(e) = self.view(to, dsl) { - panic!("render #{index} failed ({e}): {dsl}"); + *self.error.write().unwrap() = Some(format!("{e}").into()); + break; } } } } - impl Draw for Mode { + impl Draw for Mode { fn draw (&self, _to: &mut TuiOut) { //self.content().draw(to) } } impl View for App { - fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl DslExpr) -> Usually<()> { + fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl Expression) -> Usually<()> { if evaluate_output_expression(self, to, expr)? || evaluate_output_expression_tui(self, to, expr)? { Ok(()) @@ -607,7 +347,7 @@ pub mod tui { Err(format!("App::view_expr: unexpected: {expr:?}").into()) } } - fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl DslExpr) -> Usually<()> { + fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl Expression) -> Usually<()> { let mut frags = dsl.src()?.unwrap().split("/"); match frags.next() { Some(":logo") => to.place(&view_logo()), @@ -721,6 +461,135 @@ pub mod tui { Fixed::Y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), }))) } + handle!(TuiIn: |self: App, input|{ + let commands = collect_commands(self, input)?; + let history = execute_commands(self, commands)?; + self.history.extend(history.into_iter()); + Ok(None) + }); + fn collect_commands ( + app: &App, input: &TuiIn + ) -> Usually> { + let mut commands = vec![]; + for id in app.mode.keys.iter() { + if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) + && let Some(bindings) = event_map.query(input.event()) { + for binding in bindings { + for command in binding.commands.iter() { + if let Some(command) = app.resolve(command)? as Option { + commands.push(command) + } + } + } + } + } + Ok(commands) + } + fn execute_commands ( + app: &mut App, commands: Vec + ) -> Usually)>> { + let mut history = vec![]; + for command in commands.into_iter() { + let result = command.execute(app); + match result { Err(err) => { history.push((command, None)); return Err(err) } + Ok(undo) => { history.push((command, undo)); } }; + } + Ok(history) + } +} +pub mod ns { + use super::{*, model::*, gui::*}; + macro_rules!primitive(($T:ty: $name:ident)=>{ + fn $name (src: impl Language) -> Perhaps<$T> { + Ok(if let Some(src) = src.src()? { Some(to_number(src)? as $T) } else { None }) } }); + primitive!(u8: try_to_u8); + primitive!(u16: try_to_u16); + primitive!(usize: try_to_usize); + primitive!(isize: try_to_isize); + namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); + namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); + namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { + ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), + ":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9), }; }); + namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); + namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { + ":scene-count" => app.scenes().len(), + ":track-count" => app.tracks().len(), + ":device-kind" => app.dialog.device_kind().unwrap_or(0), + ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), + ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; }); + // Provide boolean values. + namespace!(App: bool { symbol = |app| { + ":mode/editor" => app.project.editor.is_some(), + ":focused/dialog" => !matches!(app.dialog, Dialog::None), + ":focused/message" => matches!(app.dialog, Dialog::Message(..)), + ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), + ":focused/browser" => app.dialog.browser().is_some(), + ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), + ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), + ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), + ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), + ":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}), + ":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)), + ":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)), + ":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix), + }; }); + // TODO: provide colors here + namespace!(App: ItemTheme {}); + namespace!(App: Dialog { symbol = |app| { + ":dialog/none" => Dialog::None, + ":dialog/options" => Dialog::Options, + ":dialog/device" => Dialog::Device(0), + ":dialog/device/prev" => Dialog::Device(0), + ":dialog/device/next" => Dialog::Device(0), + ":dialog/help" => Dialog::Help(0), + ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, + Browse::new(None).unwrap().into()), + ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, + Browse::new(None).unwrap().into()), + ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), + Browse::new(None).unwrap().into()), + }; }); + namespace!(App: Selection { symbol = |app| { + ":select/scene" => app.selection().select_scene(app.tracks().len()), + ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), + ":select/scene/prev" => app.selection().select_scene_prev(), + ":select/track" => app.selection().select_track(app.tracks().len()), + ":select/track/next" => app.selection().select_track_next(app.tracks().len()), + ":select/track/prev" => app.selection().select_track_prev(), + }; }); + namespace!(App: Color { + symbol = |app| { + ":color/bg" => Color::Rgb(28, 32, 36), + }; + expression = |app| { + "g" (n: u8) => Color::Rgb(n, n, n), + "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), + }; + }); + namespace!(App: Option { symbol = |app| { + ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) + }; }); + namespace!(App: Option { symbol = |app| { + ":selected/scene" => app.selection().scene(), + ":selected/track" => app.selection().track(), + }; }); + namespace!(App: Option>> { + symbol = |app| { ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { app.scenes()[*scene].clips[*track].clone() } else { None } }; + }); + impl<'a> Namespace<'a, AppCommand> for App { symbols!('a |app| -> AppCommand { + "x/inc" => AppCommand::Inc { axis: Axis::X }, + "x/dec" => AppCommand::Dec { axis: Axis::X }, + "y/inc" => AppCommand::Inc { axis: Axis::Y }, + "y/dec" => AppCommand::Dec { axis: Axis::Y }, + "confirm" => AppCommand::Confirm, + "cancel" => AppCommand::Cancel, }); } } pub mod gui { use super::{*, model::*}; @@ -745,6 +614,93 @@ pub mod gui { /// Callback pub ArcUsually<()> + Send + Sync>> ); + impl App { + pub fn update_clock (&self) { + ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } + /// Set modal dialog. + pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog + } + /// Set picked device in device pick dialog. + 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!(), + } + } + /// Return reference to content browser if open. + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } + /// Is a MIDI editor currently focused? + pub fn editor_focused (&self) -> bool { false } + /// Toggle MIDI editor. + 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(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()); + } + } + } + } impl Dialog { pub fn welcome () -> Self { Self::Menu(1, MenuItems([ diff --git a/deps/dizzle b/deps/dizzle index 1ce18223..b5fdda4f 160000 --- a/deps/dizzle +++ b/deps/dizzle @@ -1 +1 @@ -Subproject commit 1ce18223c60eac427da617d948ad18808d2f5b38 +Subproject commit b5fdda4fdbd04dba982c47943ddbf9bf38f21242 diff --git a/deps/tengri b/deps/tengri index a933cbe2..8a5bc7b6 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit a933cbe2857613a439761e38a9634cf85dc17461 +Subproject commit 8a5bc7b6ea446d337834417f22f68291c5ab2a51