From 2858b01bd49e567fd5f91dc6cf13b26ce47f16e3 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 17 May 2025 20:58:02 +0300 Subject: [PATCH] arranger: now we're talkin --- config/keys_arranger.edn | 2 +- config/keys_global.edn | 12 +- crates/app/src/config.rs | 199 ---------------- crates/app/src/lib.rs | 1 - crates/app/src/model.rs | 239 +++++++++++++++++--- crates/app/src/view.rs | 96 +------- crates/device/src/arranger/arranger_view.rs | 28 ++- crates/device/src/browser/browser_model.rs | 10 + crates/device/src/device.rs | 7 + crates/device/src/dialog.rs | 113 +++++++++ crates/device/src/lib.rs | 61 ++--- deps/tengri | 2 +- 12 files changed, 379 insertions(+), 391 deletions(-) delete mode 100644 crates/app/src/config.rs create mode 100644 crates/device/src/dialog.rs diff --git a/config/keys_arranger.edn b/config/keys_arranger.edn index 6da6a560..76e1e6b4 100644 --- a/config/keys_arranger.edn +++ b/config/keys_arranger.edn @@ -9,7 +9,7 @@ (@shift-O project output-add) (@shift-S project scene-add) (@shift-T project track-add) -(@shift-D toggle-dialog :dialog-device) +(@shift-D dialog :dialog-device) (@up select :select-scene-prev) (@down select :select-scene-next) diff --git a/config/keys_global.edn b/config/keys_global.edn index 6ea6d748..8e9f4233 100644 --- a/config/keys_global.edn +++ b/config/keys_global.edn @@ -1,9 +1,9 @@ -(@esc dialog :dialog-none) -(@f1 dialog :dialog-help) -(@f6 dialog :dialog-save) -(@f8 dialog :dialog-options) -(@f9 dialog :dialog-load) -(@f10 dialog :dialog-quit) +(@esc dialog :dialog-none) +(@f1 dialog :dialog-help) +(@f6 dialog :dialog-save) +(@f8 dialog :dialog-options) +(@f9 dialog :dialog-load) +(@f10 dialog :dialog-quit) (@u undo 1) (@r redo 1) diff --git a/crates/app/src/config.rs b/crates/app/src/config.rs deleted file mode 100644 index 3b3b3cac..00000000 --- a/crates/app/src/config.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::*; -use std::path::PathBuf; - -/// Configuration -#[derive(Default, Debug)] -pub struct Configuration { - /// Path of configuration entrypoint - pub path: PathBuf, - /// Name of configuration - pub name: Option>, - /// Description of configuration - pub info: Option>, - /// View definition - pub view: TokenIter<'static>, - // Input keymap - pub keys: InputMap<'static, App, AppCommand, TuiIn, TokenIter<'static>> -} - -impl Configuration { - - pub fn new (path: &impl AsRef, _watch: bool) -> Usually { - let text = read_and_leak(path.as_ref())?; - let [name, info, view, keys] = Self::parse(TokenIter::from(text))?; - Ok(Self { - path: path.as_ref().into(), - info: info.map(Self::parse_info).flatten(), - name: name.map(Self::parse_name).flatten(), - view: Self::parse_view(view)?, - keys: Self::parse_keys(&path, keys)?, - }) - } - - fn parse (iter: TokenIter) -> Usually<[Option;4]> { - let mut name: Option = None; - let mut info: Option = None; - let mut view: Option = None; - let mut keys: Option = None; - for token in iter { - match token.value { - Value::Exp(_, mut exp) => { - let next = exp.next(); - match next { - Some(Token { value: Value::Key(sym), .. }) => match sym { - "name" => name = Some(exp), - "info" => info = Some(exp), - "keys" => keys = Some(exp), - "view" => view = Some(exp), - _ => return Err( - format!("(e3) unexpected symbol {sym:?}").into() - ) - }, - _ => return Err( - format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into() - ) - } - }, - t => return Err( - format!("(e1) unexpected token {token:?}").into() - ) - }; - } - Ok([name, info, view, keys]) - } - - fn parse_info (mut iter: TokenIter) -> Option> { - iter.next().and_then(|x|if let Value::Str(x) = x.value { - Some(x.into()) - } else { - None - }) - } - - fn parse_name (mut iter: TokenIter) -> Option> { - iter.next().and_then(|x|if let Value::Str(x) = x.value { - Some(x.into()) - } else { - None - }) - } - - fn parse_view (iter: Option) -> Usually { - if let Some(view) = iter { - Ok(view) - } else { - Err(format!("missing view definition").into()) - } - } - - fn parse_keys (base: &impl AsRef, iter: Option>) - -> Usually>> - { - if iter.is_none() { - return Err(format!("missing keys definition").into()) - } - let mut keys = iter.unwrap(); - let mut map = InputMap::default(); - while let Some(token) = keys.next() { - if let Value::Exp(_, mut exp) = token.value { - let next = exp.next(); - if let Some(Token { value: Value::Key(sym), .. }) = next { - match sym { - "layer" => { - let next = exp.next(); - if let Some(Token { value: Value::Str(path), .. }) = next { - let path = base.as_ref().parent().unwrap().join(unquote(path)); - if !std::fs::exists(&path)? { - return Err(format!("(e5) not found: {path:?}").into()) - } - map.add_layer(read_and_leak(path)?.into()); - } else { - return Err(format!("(e4) unexpected non-string {next:?}").into()) - } - }, - - "layer-if" => { - let mut cond = None; - - let next = exp.next(); - if let Some(Token { value: Value::Sym(sym), .. }) = next { - cond = Some(leak(sym)); - } else { - return Err(format!("(e4) unexpected non-symbol {next:?}").into()) - }; - - if let Some(Token { value: Value::Str(path), .. }) = exp.peek() { - let path = base.as_ref().parent().unwrap().join(unquote(path)); - if !std::fs::exists(&path)? { - return Err(format!("(e5) not found: {path:?}").into()) - } - print!("{path:?}..."); - let keys = read_and_leak(path)?.into(); - let cond = cond.unwrap(); - print!("{exp:?}..."); - println!("ok"); - map.add_layer_if( - Box::new(move |state|{ - let mut exp = exp.clone(); - Context::get(state, &mut exp).unwrap_or(false) - }), - keys - ); - } else { - return Err(format!("(e4) unexpected non-symbol {next:?}").into()) - } - }, - - _ => return Err(format!("(e3) unexpected symbol {sym:?}").into()) - } - } else { - return Err(format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into()) - } - } else { - return Err(format!("(e1) unexpected token {token:?}").into()) - } - } - Ok(map) - } - -} - -fn read_and_leak (path: impl AsRef) -> Usually<&'static str> { - Ok(leak(String::from_utf8(std::fs::read(path.as_ref())?)?)) -} - -fn leak (x: impl AsRef) -> &'static str { - Box::leak(x.as_ref().into()) -} - -fn unquote (x: &str) -> &str { - let mut chars = x.chars(); - chars.next(); - //chars.next_back(); - chars.as_str() -} - -macro_rules! default_config { ($path:literal) => { ($path, include_str!($path)) }; } -pub const DEFAULT_CONFIGS: &'static [(&'static str, &'static str)] = &[ - default_config!("../../../config/config_arranger.edn"), - default_config!("../../../config/config_groovebox.edn"), - default_config!("../../../config/config_sampler.edn"), - default_config!("../../../config/config_sequencer.edn"), - default_config!("../../../config/config_transport.edn"), - - default_config!("../../../config/keys_arranger.edn"), - default_config!("../../../config/keys_clip.edn"), - default_config!("../../../config/keys_clock.edn"), - default_config!("../../../config/keys_editor.edn"), - default_config!("../../../config/keys_global.edn"), - default_config!("../../../config/keys_groovebox.edn"), - default_config!("../../../config/keys_length.edn"), - default_config!("../../../config/keys_mix.edn"), - default_config!("../../../config/keys_pool.edn"), - default_config!("../../../config/keys_pool_file.edn"), - default_config!("../../../config/keys_rename.edn"), - default_config!("../../../config/keys_sampler.edn"), - default_config!("../../../config/keys_scene.edn"), - default_config!("../../../config/keys_sequencer.edn"), - default_config!("../../../config/keys_track.edn"), -]; diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index ee5f836d..cc811d61 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -36,7 +36,6 @@ pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}; mod api; pub use self::api::*; mod audio; pub use self::audio::*; -mod config; pub use self::config::*; mod model; pub use self::model::*; mod view; pub use self::view::*; diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index c6dabdf7..5ede40e6 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -1,4 +1,5 @@ use crate::*; +use std::path::PathBuf; #[derive(Default, Debug)] pub struct App { @@ -28,6 +29,7 @@ pub struct App { has!(Jack: |self: App|self.jack); has!(Pool: |self: App|self.pool); +has!(Option: |self: App|self.dialog); has!(Clock: |self: App|self.project.clock); has!(Option: |self: App|self.project.editor); has!(Selection: |self: App|self.project.selection); @@ -91,12 +93,6 @@ impl App { pub(crate) fn device_pick (&mut self, index: usize) { self.dialog = Some(Dialog::Device(index)); } - pub(crate) fn device_kinds (&self) -> &'static [&'static str] { - &[ - "Sampler", - "Plugin (LV2)", - ] - } pub(crate) fn device_add (&mut self, index: usize) -> Usually<()> { match index { 0 => self.device_add_sampler(), @@ -166,35 +162,6 @@ impl App { } } -/// Various possible dialog overlays -#[derive(Clone, Debug)] -pub enum Dialog { - Help(usize), - Menu(usize), - Device(usize), - Message(Message), - Browser(BrowserTarget, Browser), - Options, -} - -#[derive(Clone, Debug)] -pub enum BrowserTarget { - SaveProject, - LoadProject, - ImportSample(Arc>>), - ExportSample(Arc>>), - ImportClip(Arc>>), - ExportClip(Arc>>), -} - -/// Various possible messages -#[derive(PartialEq, Clone, Copy, Debug)] -pub enum Message { - FailedToAddDevice, -} - -content!(TuiOut: |self: Message| match self { Self::FailedToAddDevice => "Failed to add device." }); - #[tengri_proc::expose] impl App { fn _todo_isize_stub (&self) -> isize { @@ -332,16 +299,214 @@ impl App { } 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)) + index.overflowing_sub(1).0.min(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() + (index + 1) % device_kinds().len() } else { 0 } } } + +/// Configuration +#[derive(Default, Debug)] +pub struct Configuration { + /// Path of configuration entrypoint + pub path: PathBuf, + /// Name of configuration + pub name: Option>, + /// Description of configuration + pub info: Option>, + /// View definition + pub view: TokenIter<'static>, + // Input keymap + pub keys: InputMap<'static, App, AppCommand, TuiIn, TokenIter<'static>>, +} + +impl Configuration { + + pub fn new (path: &impl AsRef, _watch: bool) -> Usually { + let text = read_and_leak(path.as_ref())?; + let [name, info, view, keys] = Self::parse(TokenIter::from(text))?; + Ok(Self { + path: path.as_ref().into(), + info: info.map(Self::parse_info).flatten(), + name: name.map(Self::parse_name).flatten(), + view: Self::parse_view(view)?, + keys: Self::parse_keys(&path, keys)?, + }) + } + + fn parse (iter: TokenIter) -> Usually<[Option;4]> { + let mut name: Option = None; + let mut info: Option = None; + let mut view: Option = None; + let mut keys: Option = None; + for token in iter { + match token.value { + Value::Exp(_, mut exp) => { + let next = exp.next(); + match next { + Some(Token { value: Value::Key(sym), .. }) => match sym { + "name" => name = Some(exp), + "info" => info = Some(exp), + "keys" => keys = Some(exp), + "view" => view = Some(exp), + _ => return Err( + format!("(e3) unexpected symbol {sym:?}").into() + ) + }, + _ => return Err( + format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into() + ) + } + }, + t => return Err( + format!("(e1) unexpected token {token:?}").into() + ) + }; + } + Ok([name, info, view, keys]) + } + + fn parse_info (mut iter: TokenIter) -> Option> { + iter.next().and_then(|x|if let Value::Str(x) = x.value { + Some(x.into()) + } else { + None + }) + } + + fn parse_name (mut iter: TokenIter) -> Option> { + iter.next().and_then(|x|if let Value::Str(x) = x.value { + Some(x.into()) + } else { + None + }) + } + + fn parse_view (iter: Option) -> Usually { + if let Some(view) = iter { + Ok(view) + } else { + Err(format!("missing view definition").into()) + } + } + + fn parse_keys (base: &impl AsRef, iter: Option>) + -> Usually>> + { + if iter.is_none() { + return Err(format!("missing keys definition").into()) + } + let mut keys = iter.unwrap(); + let mut map = InputMap::default(); + while let Some(token) = keys.next() { + if let Value::Exp(_, mut exp) = token.value { + let next = exp.next(); + if let Some(Token { value: Value::Key(sym), .. }) = next { + match sym { + "layer" => { + if let Some(Token { value: Value::Str(path), .. }) = exp.peek() { + let path = base.as_ref().parent().unwrap().join(unquote(path)); + if !std::fs::exists(&path)? { + return Err(format!("(e5) not found: {path:?}").into()) + } + map.add_layer(read_and_leak(path)?.into()); + print!("layer:\n path: {:?}...", exp.0.0.trim()); + println!("ok"); + } else { + return Err(format!("(e4) unexpected non-string {next:?}").into()) + } + }, + + "layer-if" => { + let mut cond = None; + + let next = exp.next(); + if let Some(Token { value: Value::Sym(sym), .. }) = next { + cond = Some(leak(sym)); + } else { + return Err(format!("(e4) unexpected non-symbol {next:?}").into()) + }; + + if let Some(Token { value: Value::Str(path), .. }) = exp.peek() { + let path = base.as_ref().parent().unwrap().join(unquote(path)); + if !std::fs::exists(&path)? { + return Err(format!("(e5) not found: {path:?}").into()) + } + print!("layer-if:\n cond: {}\n path: {path:?}...", + cond.unwrap_or_default()); + let keys = read_and_leak(path)?.into(); + let cond = cond.unwrap(); + println!("ok"); + map.add_layer_if( + Box::new(move |state|{ + let mut exp = exp.clone(); + Context::get(state, &mut exp).unwrap_or(false) + }), + keys + ); + } else { + return Err(format!("(e4) unexpected non-symbol {next:?}").into()) + } + }, + + _ => return Err(format!("(e3) unexpected symbol {sym:?}").into()) + } + } else { + return Err(format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into()) + } + } else { + return Err(format!("(e1) unexpected token {token:?}").into()) + } + } + Ok(map) + } + +} + +fn read_and_leak (path: impl AsRef) -> Usually<&'static str> { + Ok(leak(String::from_utf8(std::fs::read(path.as_ref())?)?)) +} + +fn leak (x: impl AsRef) -> &'static str { + Box::leak(x.as_ref().into()) +} + +fn unquote (x: &str) -> &str { + let mut chars = x.chars(); + chars.next(); + //chars.next_back(); + chars.as_str() +} + +macro_rules! default_config { ($path:literal) => { ($path, include_str!($path)) }; } +pub const DEFAULT_CONFIGS: &'static [(&'static str, &'static str)] = &[ + default_config!("../../../config/config_arranger.edn"), + default_config!("../../../config/config_groovebox.edn"), + default_config!("../../../config/config_sampler.edn"), + default_config!("../../../config/config_sequencer.edn"), + default_config!("../../../config/config_transport.edn"), + + default_config!("../../../config/keys_arranger.edn"), + default_config!("../../../config/keys_clip.edn"), + default_config!("../../../config/keys_clock.edn"), + default_config!("../../../config/keys_editor.edn"), + default_config!("../../../config/keys_global.edn"), + default_config!("../../../config/keys_groovebox.edn"), + default_config!("../../../config/keys_length.edn"), + default_config!("../../../config/keys_mix.edn"), + default_config!("../../../config/keys_pool.edn"), + default_config!("../../../config/keys_pool_file.edn"), + default_config!("../../../config/keys_rename.edn"), + default_config!("../../../config/keys_sampler.edn"), + default_config!("../../../config/keys_scene.edn"), + default_config!("../../../config/keys_sequencer.edn"), + default_config!("../../../config/keys_track.edn"), +]; diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index 55afff65..1a67e193 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -123,7 +123,7 @@ impl App { self.project.view_scenes_clips() } pub fn view_tracks_inputs <'a> (&'a self) -> impl Content + use<'a> { - Fixed::y(2 + self.project.midi_ins.len() as u16, self.project.view_inputs(self.color)) + Fixed::y(1 + self.project.midi_ins.len() as u16, self.project.view_inputs(self.color)) } pub fn view_tracks_outputs <'a> (&'a self) -> impl Content + use<'a> { Fixed::y(1 + self.project.midi_outs.len() as u16, self.project.view_outputs(self.color)) @@ -163,100 +163,10 @@ impl App { self.project.sampler().map(|s|s.view_meters_output()) } pub fn view_dialog (&self) -> impl Content + use<'_> { - When(self.dialog.is_some(), Bsp::b( "", + self.dialog.as_ref().map(|dialog|Bsp::b("", 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(self.dialog.as_ref().map(|dialog|match dialog { - Dialog::Menu(_) => - self.view_dialog_menu().boxed(), - Dialog::Help(offset) => - self.view_dialog_help(*offset).boxed(), - Dialog::Browser(target, browser) => - self.view_dialog_browser(target, browser).boxed(), - Dialog::Options => - self.view_dialog_options().boxed(), - Dialog::Device(index) => - self.view_dialog_device(*index).boxed(), - Dialog::Message(message) => - self.view_dialog_message(message).boxed(), - })) - ))) - )) - } -} - -impl App { - pub fn view_dialog_menu (&self) -> impl Content { - 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 + use<'a> { - 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 = ||self.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 Message) -> impl Content + use<'a> { - Bsp::s(message, 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( - self.color, - 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" + .enclose(dialog)))))) } } diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index 3a4c4df0..55cc5198 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -2,9 +2,9 @@ use crate::*; impl Arrangement { pub fn view_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content + 'a { - let mut h = 1u16; + let mut h = 0; for track in self.tracks().iter() { - h = h.max(track.sequencer.midi_ins.len() as u16); + h = h.max(self.midi_ins.len() as u16); } let h = h + 1; self.view_track_row_section( @@ -153,12 +153,22 @@ pub trait TracksView: fn view_track_names (&self, theme: ItemTheme) -> impl Content { self.view_track_row_section( theme, - button_3("t", "rack ", if let Some(track) = self.selection().track() { - format!("{track}/{}", self.tracks().len()) - } else { - format!("{}", self.tracks().len()) - }, false), - button_2("T", "+", false), + Bsp::s( + button_3("t", "rack ", if let Some(track) = self.selection().track() { + format!("{track}/{}", self.tracks().len()) + } else { + format!("{}", self.tracks().len()) + }, false), + button_3("s", "cene ", if let Some(scene) = self.selection().scene() { + format!("{scene}/{}", self.scenes().len()) + } else { + format!("{}", self.scenes().len()) + }, false) + ), + Bsp::s( + button_2("T", "+", false), + button_2("S", "+", false), + ), Tui::bg(theme.darker.rgb, Fixed::y(2, Fill::x( Stack::east(move|add: &mut dyn FnMut(&dyn Render)|{ for (index, track, x1, x2) in self.tracks_with_sizes() { @@ -256,7 +266,7 @@ pub trait ScenesView: self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{ let active = editing && selected_track.is_some() && selected_scene == Some(s); let height = if active { larger } else { height }; - if y + height < self.clips_size().h() { + if y + height <= self.clips_size().h() { let data = (s, scene, y, y + height); y += height; Some(data) diff --git a/crates/device/src/browser/browser_model.rs b/crates/device/src/browser/browser_model.rs index 213d4276..fe0a0645 100644 --- a/crates/device/src/browser/browser_model.rs +++ b/crates/device/src/browser/browser_model.rs @@ -2,6 +2,16 @@ use crate::*; use std::path::PathBuf; use std::ffi::OsString; +#[derive(Clone, Debug)] +pub enum BrowserTarget { + SaveProject, + LoadProject, + ImportSample(Arc>>), + ExportSample(Arc>>), + ImportClip(Arc>>), + ExportClip(Arc>>), +} + /// Browses for phrase to import/export #[derive(Debug, Clone, Default)] pub struct Browser { diff --git a/crates/device/src/device.rs b/crates/device/src/device.rs index 0512e686..894aa2d2 100644 --- a/crates/device/src/device.rs +++ b/crates/device/src/device.rs @@ -1,5 +1,12 @@ use crate::*; +pub fn device_kinds () -> &'static [&'static str] { + &[ + "Sampler", + "Plugin (LV2)", + ] +} + impl> + Has> HasDevices for T { fn devices (&self) -> &Vec { self.get() diff --git a/crates/device/src/dialog.rs b/crates/device/src/dialog.rs new file mode 100644 index 00000000..f9f7c309 --- /dev/null +++ b/crates/device/src/dialog.rs @@ -0,0 +1,113 @@ +use crate::*; + +/// Various possible dialog overlays +#[derive(Clone, Debug)] +pub enum Dialog { + Help(usize), + Menu(usize), + Device(usize), + Message(Message), + Browser(BrowserTarget, Browser), + Options, +} + +/// Various possible messages +#[derive(PartialEq, Clone, Copy, Debug)] +pub enum Message { + FailedToAddDevice, +} + +content!(TuiOut: |self: Message| match self { + Self::FailedToAddDevice => "Failed to add device." +}); + +content!(TuiOut: |self: Dialog| match self { + Self::Menu(_) => + self.view_dialog_menu().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(), +}); + +impl Dialog { + pub fn view_dialog_menu (&self) -> impl Content { + 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 + use<'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 Message) -> impl Content + use<'a> { + Bsp::s(message, 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" + } +} diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 26131511..6041e16c 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -14,54 +14,27 @@ pub(crate) use std::error::Error; pub(crate) use std::ffi::OsString; pub(crate) use ::tengri::{from, has, maybe_has, Usually, Perhaps, Has, MaybeHas}; -pub(crate) use ::tengri::{dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}}; +pub(crate) use ::tengri::{dsl::*, input::*, output::{*, Margin}, tui::{*, ratatui::prelude::*}}; pub(crate) use ::tek_engine::*; pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage}; pub(crate) use ::tek_engine::jack::{Control, ProcessScope, MidiWriter, RawMidi}; pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}}; pub(crate) use Color::*; -mod device; -pub use self::device::*; +mod device; pub use self::device::*; +mod dialog; pub use self::dialog::*; -#[cfg(feature = "arranger")] mod arranger; -#[cfg(feature = "arranger")] pub use self::arranger::*; - -#[cfg(feature = "browser")] mod browser; -#[cfg(feature = "browser")] pub use self::browser::*; - -#[cfg(feature = "clock")] mod clock; -#[cfg(feature = "clock")] pub use self::clock::*; - -#[cfg(feature = "editor")] mod editor; -#[cfg(feature = "editor")] pub use self::editor::*; - -#[cfg(feature = "pool")] mod pool; -#[cfg(feature = "pool")] pub use self::pool::*; - -#[cfg(feature = "sequencer")] mod sequencer; -#[cfg(feature = "sequencer")] pub use self::sequencer::*; - -#[cfg(feature = "sampler")] mod sampler; -#[cfg(feature = "sampler")] pub use self::sampler::*; - -#[cfg(feature = "meter")] mod meter; -#[cfg(feature = "meter")] pub use self::meter::*; - -#[cfg(feature = "mixer")] mod mixer; -#[cfg(feature = "mixer")] pub use self::mixer::*; - -#[cfg(feature = "lv2")] mod lv2; -#[cfg(feature = "lv2")] pub use self::lv2::*; - -#[cfg(feature = "sf2")] mod sf2; -#[cfg(feature = "sf2")] pub use self::sf2::*; - -#[cfg(feature = "vst2")] mod vst2; -#[cfg(feature = "vst2")] pub use self::vst2::*; - -#[cfg(feature = "vst3")] mod vst3; -#[cfg(feature = "vst3")] pub use self::vst3::*; - -#[cfg(feature = "clap")] mod clap; -#[cfg(feature = "clap")] pub use self::clap::*; +#[cfg(feature = "arranger")] mod arranger; #[cfg(feature = "arranger")] pub use self::arranger::*; +#[cfg(feature = "browser")] mod browser; #[cfg(feature = "browser")] pub use self::browser::*; +#[cfg(feature = "clock")] mod clock; #[cfg(feature = "clock")] pub use self::clock::*; +#[cfg(feature = "editor")] mod editor; #[cfg(feature = "editor")] pub use self::editor::*; +#[cfg(feature = "pool")] mod pool; #[cfg(feature = "pool")] pub use self::pool::*; +#[cfg(feature = "sequencer")] mod sequencer; #[cfg(feature = "sequencer")] pub use self::sequencer::*; +#[cfg(feature = "sampler")] mod sampler; #[cfg(feature = "sampler")] pub use self::sampler::*; +#[cfg(feature = "meter")] mod meter; #[cfg(feature = "meter")] pub use self::meter::*; +#[cfg(feature = "mixer")] mod mixer; #[cfg(feature = "mixer")] pub use self::mixer::*; +#[cfg(feature = "lv2")] mod lv2; #[cfg(feature = "lv2")] pub use self::lv2::*; +#[cfg(feature = "sf2")] mod sf2; #[cfg(feature = "sf2")] pub use self::sf2::*; +#[cfg(feature = "vst2")] mod vst2; #[cfg(feature = "vst2")] pub use self::vst2::*; +#[cfg(feature = "vst3")] mod vst3; #[cfg(feature = "vst3")] pub use self::vst3::*; +#[cfg(feature = "clap")] mod clap; #[cfg(feature = "clap")] pub use self::clap::*; diff --git a/deps/tengri b/deps/tengri index 12998a94..f21781e8 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit 12998a94ea02bc84c1a490783bc76b10789ce37f +Subproject commit f21781e81664e1991e3985e2377becca9c1d58cf