From f10fa309e5b61c5b9d47bc5d5b37f57fd015baf7 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Sat, 19 Sep 2026 10:11:55 +0300 Subject: [PATCH] more reorg; newtype Binds and Views --- Justfile | 3 +- src/app.rs | 104 ++++++---- src/binds.rs | 38 ---- src/cli.rs | 11 +- src/clip.rs | 136 ------------ src/config.rs | 39 ++-- src/{bind.rs => config_bind.rs} | 58 ++++++ src/config_mode.rs | 162 +++++++++++++++ src/{views.rs => config_view.rs} | 52 +++-- src/dialog.rs | 38 ++-- src/midi_clip.rs | 60 ++++-- src/midi_clips.rs | 87 ++++++++ src/{editor.rs => midi_edit.rs} | 185 +++++++++-------- src/mode.rs | 72 ------- src/mode_stack.rs | 46 ----- src/modes.rs | 45 ---- src/project.rs | 17 +- src/sample.rs | 144 +++++++++++++ src/sample_add.rs | 128 ++++++++++++ src/sample_kit.rs | 26 +++ src/sample_voice.rs | 30 +++ src/sampler.rs | 345 ++----------------------------- src/scene.rs | 57 ++++- src/tek.edn | 6 +- src/tek.rs | 16 +- src/track.rs | 2 +- tengri | 2 +- 27 files changed, 1017 insertions(+), 892 deletions(-) delete mode 100644 src/binds.rs delete mode 100644 src/clip.rs rename src/{bind.rs => config_bind.rs} (60%) create mode 100644 src/config_mode.rs rename src/{views.rs => config_view.rs} (92%) rename src/{editor.rs => midi_edit.rs} (86%) delete mode 100644 src/mode.rs delete mode 100644 src/mode_stack.rs delete mode 100644 src/modes.rs create mode 100644 src/sample.rs create mode 100644 src/sample_add.rs create mode 100644 src/sample_kit.rs create mode 100644 src/sample_voice.rs diff --git a/Justfile b/Justfile index 4cf8f59d..d32780ea 100644 --- a/Justfile +++ b/Justfile @@ -1,6 +1,5 @@ -#export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold" export RUST_BACKTRACE := "1" -export RUSTFLAGS := "-Zmacro-backtrace -Clink-arg=-fuse-ld=mold -Clink-arg=-Wl,--no-rosegment -Cforce-frame-pointers=yes" +export RUSTFLAGS := "-Clink-arg=-fuse-ld=mold -Clink-arg=-Wl,--no-rosegment -Cforce-frame-pointers=yes" [default] list: diff --git a/src/app.rs b/src/app.rs index 7a354268..5197c086 100644 --- a/src/app.rs +++ b/src/app.rs @@ -24,28 +24,32 @@ use crate::*; /// let _ = app.project.h_scenes(); /// ``` #[derive(Default, Debug)] -#[namespace(MidiClip)] -#[namespace(ItemColor)] -#[namespace(ItemTheme)] -#[namespace(isize)] -#[namespace(u8)] -#[namespace(bool get_bool)] -#[namespace(u16 get_u16)] -#[namespace(usize get_usize)] #[namespace(Arc<[Connect]> get_arc_array_connect)] #[namespace(Arc get_arc_str)] #[namespace(Color get_color)] #[namespace(ControlAxis get_axis)] #[namespace(Dialog get_dialog)] +#[namespace(ItemColor)] +#[namespace(ItemTheme)] +#[namespace(MidiClip)] #[namespace(Option> get_opt_arc_array_connect)] #[namespace(Option> get_opt_arc_str)] #[namespace(Option get_opt_itemtheme)] +#[namespace(Option>> get_opt_arc_rwlock_midiclip)] #[namespace(Option>>>> get_opt_vec_opt_arc_rwlock_midiclip)] #[namespace(Option get_opt_bool)] #[namespace(Option get_opt_u16)] +#[namespace(Option get_opt_u32)] #[namespace(Option get_opt_usize)] #[namespace(PathBuf get_path_buf)] #[namespace(Vec> get_vec_arc_str)] +#[namespace(bool get_bool)] +#[namespace(f64 get_f64)] +#[namespace(isize)] +#[namespace(u16 get_u16)] +#[namespace(u32)] +#[namespace(u8)] +#[namespace(usize get_usize)] pub struct App { /// Exit flag pub exit: Exit, @@ -83,7 +87,7 @@ impl App { /// ``` /// let mut proj = tek::Project::default(); /// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); - /// let mut conf = tek::config_load(tek::Config::default(), "(mode hello)").unwrap(); + /// let mut conf = tek::Config::load(tek::Config::default(), "(mode hello)").unwrap(); /// let tek = tek::App::new(None, proj, conf.into(), "hello"); /// ``` pub fn new ( @@ -128,6 +132,10 @@ fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps { })) } +fn get_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps { + todo!() +} + fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps { src.word()?.map(|word|Ok(match word { "Y" => true, @@ -145,7 +153,7 @@ fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps { ":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)), ":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)), ":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix), - _ => return Err(format!("not bool: {word}").into()) + _ => return err!("not bool: {word}") })).transpose() } @@ -158,7 +166,7 @@ fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps { ":select/track" => state.selection().select_track(state.tracks().len()), ":select/track/next" => state.selection().select_track_next(state.tracks().len()), ":select/track/prev" => state.selection().select_track_prev(), - _ => return Err(format!("not selection: {word}").into()) + _ => return err!("not selection: {word}") })).transpose() } @@ -179,64 +187,64 @@ fn get_color <'a> (_: &App, src: impl Language) -> Perhaps { .ok_or(LanguageError::domain("not blue"))?; Ok(Some(Color::Rgb(r, g, b))) }, - (Some(_), _) => return Err(format!("not a color expression: {expr}").into()), - (None, _) => return Err(format!("not a color expression: {expr}").into()), + (Some(_), _) => return err!("not a color expression: {expr}"), + (None, _) => return err!("not a color expression: {expr}"), } } else if let Ok(Some(sym)) = src.word() { Ok(match sym { ":color/bg" => Some(Color::Rgb(28, 32, 36)), ":color/fg" => Some(Color::Rgb(98, 92, 96)), - _ => return Err(format!("not a color: {sym}").into()) + _ => return err!("not a color: {sym}") }) } else { - return Err(format!("not a color: {:?}", src.src()?).into()) + return err!("not a color: {:?}", src.src()?) } } #[allow(unused)] fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps> { - src.word()?.map(|word|Ok(match word { + eval_word(src, |word|Ok(match word { + ":editor/pitch" => Some(( state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8 ).into()), - _ => return Err(format!("unknown midi note: {word}").into()) - })).transpose() + + _ => return err!("unknown midi note: {word}") + + })) } fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps> { - src.word()?.map(|word|Ok(match word { + eval_word(src, |word|Ok(match word { + ":selected/scene" => state.selection().scene(), ":selected/track" => state.selection().track(), - _ => return Err(format!("unknown opt: {word}").into()) - })).transpose() -} -#[allow(unused)] -fn get_clip <'a> (state: &App, src: impl Language) -> Perhaps>>> { - src.word()?.map(|word|Ok(match word { - ":selected/clip" if let Selection::TrackClip { track, scene } = state.selection() => - state.scenes()[*scene].clips[*track].clone(), - _ => return Err(format!("not a clip: {word}").into()) - })).transpose() + _ => return err!("unknown opt: {word}") + + })) } fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps { - Ok(src.word()?.map(|word|Ok(match word { + eval_word(src, |word|Ok(match word { + ":x" => ControlAxis::X, ":y" => ControlAxis::Y, - _ => return Err(format!("unknown axis {word}")) - })).transpose()?) + + _ => return err!("unknown axis {word}") + + })) } fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps> { Ok(None) } -fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps> { - todo!() +fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps> { + Ok(None) } -fn get_opt_vec_opt_arc_rwlock_midiclip <'a> (_: &App, _: impl Language) -> Perhaps>>>>> { +fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps> { todo!() } @@ -264,14 +272,14 @@ fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps>> { todo!() } -impl_has!(Clock: |self: App|self.project.clock); -impl_has!(Dialog: |self: App|self.dialog); -impl_has!(Jack<'static>: |self: App|self.jack); -impl_has!(Pool: |self: App|self.pool); -impl_has!(Selection: |self: App|self.project.selection); -impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); -impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); -impl_has_clips!( |self: App|self.pool.clips); +impl_has!(Clock: |self: App|self.project.clock); +impl_has!(Dialog: |self: App|self.dialog); +impl_has!(Jack<'static>: |self: App|self.jack); +impl_has!(Pool: |self: App|self.pool); +impl_has!(Selection: |self: App|self.project.selection); +impl_has_clips!( |self: App|self.pool.clips); +impl_as_ref!(Option: |self: App|self.project.as_ref()); +impl_as_mut!(Option: |self: App|self.project.as_mut()); primitive!(u8: try_to_u8); primitive!(u16: try_to_u16); @@ -298,7 +306,7 @@ impl App { if let Some(mode) = mode { let binds = state.config.binds.clone(); for id in mode.keys.iter() { - if let Some(event_map) = binds.try_read().unwrap().get(id.as_ref()) + if let Some(event_map) = binds.find(id) && let Some(bindings) = event_map.query(input) { return Some(bindings.into()) } @@ -379,7 +387,7 @@ impl App { /// Swap currently active modal dialog. /// /// ``` - /// let _ = tek::App::default().set_dialog(tek::Dialog::welcome()); + /// let _ = tek::App::default().command_dialog(tek::Dialog::welcome()); /// ``` #[command(Dialog = "dialog")] pub fn command_dialog (&mut self, dialog: Dialog) -> Perhaps { @@ -413,6 +421,11 @@ impl App { Ok(command.dispatch(self)?.map(Into::into)) } + #[command(MidiEdit = "edit")] + pub fn command_midi_edit (&mut self, command: MidiEditCommand) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + #[command(Pool = "pool")] pub fn command_pool (&mut self, command: PoolCommand) -> Perhaps { Ok(command.dispatch(self)?.map(Into::into)) @@ -480,6 +493,7 @@ impl App { impl_from!(AppCommand: |x: AxisCommand| AppCommand::Axis { command: x }); impl_from!(AppCommand: |x: BrowseCommand| AppCommand::Browse { command: x }); impl_from!(AppCommand: |x: MidiClipCommand| AppCommand::MidiClip { command: x }); +impl_from!(AppCommand: |x: MidiEditCommand| AppCommand::MidiEdit { command: x }); impl_from!(AppCommand: |x: ModeCommand| AppCommand::Mode { command: x }); impl_from!(AppCommand: |x: PoolCommand| AppCommand::Pool { command: x }); impl_from!(AppCommand: |x: ProjectCommand| AppCommand::Project { command: x }); diff --git a/src/binds.rs b/src/binds.rs deleted file mode 100644 index 5df8ac0b..00000000 --- a/src/binds.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::*; - -/// Collection of input bindings. -pub type Binds = Arc, Bind>>>>; - -pub fn binds_add <'a> (binds: &Binds, expr: impl Language) -> UsuallyRef<'a, ()> { - //println!("\n\rbinds_add: {expr:?}"); - let name = expr.head()?.ok_or("bind: missing name")?; - let body = expr.tail()?.unwrap_or(""); - binds.try_write().unwrap().insert(name.into(), { - let mut map = Bind::new(); - body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) { - // TODO - Ok(()) - } else if let Ok(Some(_word)) = item.expr().head().word() { - let expr = item.expr()?; - let head = expr.head()?; - if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() { - map.add(TuiEvent(event), Binding { - commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), - condition: None, - description: None, - source: None - }); - Ok(()) - } else if Some(":char") == item.expr()?.head()? { - // TODO - return Ok(()) - } else { - return Err(format!("binds_add: invalid key: {:?}", item.expr()?.head()?).into()) - } - } else { - return Err(format!("binds_add: unexpected: {item:?}").into()) - })?; - map - }); - Ok(()) -} diff --git a/src/cli.rs b/src/cli.rs index 6f70fb47..fc679ad3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -215,10 +215,12 @@ pub fn print_status (project: &Project) { pub fn print_config (config: &Config) { use ::ansi_term::Color::*; println!("{:?}", config.dirs); - for (k, v) in config.views.try_read().unwrap().iter() { + + config.views.for_each(|k, v|{ println!("{} {} {}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")), v.source); - } - for (k, v) in config.binds.try_read().unwrap().iter() { + }); + + config.binds.for_each(|k, v|{ println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}"))); for (k, v) in v.0.iter() { print!("{} ", &Yellow.paint(match &k.0 { @@ -238,7 +240,8 @@ pub fn print_config (config: &Config) { //println!(" {:?}", v.source); } } - } + }); + config.modes.for_each(|k, v|{ println!(); for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); } diff --git a/src/clip.rs b/src/clip.rs deleted file mode 100644 index 7c9394ec..00000000 --- a/src/clip.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::*; - -impl Project { - /// Toggle looping for the active clip - pub fn toggle_loop (&mut self) { - if let Some(clip) = self.selected_clip() { - clip.try_write().unwrap().toggle_loop() - } - } - - /// Put a clip in a slot - pub fn clip_put ( - &mut self, track: usize, scene: usize, clip: Option>> - ) -> Option>> { - let old = self.scenes[scene].clips[track].clone(); - self.scenes[scene].clips[track] = clip; - old - } - - /// Change the color of a clip, returning the previous one - pub fn clip_set_color ( - &self, track: usize, scene: usize, color: ItemTheme - ) -> Option { - self.scenes[scene].clips[track].as_ref().map(|clip|{ - let mut clip = clip.try_write().unwrap(); - let old = clip.color.clone(); - clip.color = color.clone(); - panic!("{color:?} {old:?}"); - //old - }) - } -} - -pub trait HasClipsSize { - fn clips_size (&self) -> &Sizer; -} - -impl HasClipsSize for App { - fn clips_size (&self) -> &Sizer { &self.project.size_inner } -} - -impl HasClipsSize for Project { - fn clips_size (&self) -> &Sizer { &self.size_inner } -} - -impl ClipsView for T {} - -pub trait ClipsView: TracksView + ScenesView { - - /// Draw clips per scene - fn view_scenes_clips (&self) -> impl Draw { - let select = self.selection(); - let editor = self.editor(); - let editing = self.is_editing(); - with_clips_size(true, self.clips_size(), iter_east(move||self.tracks_with_sizes() - .map(move|(track_index, track, _, _)|iter_south(move||self.scenes_with_sizes() - .map(move|(scene_index, scene, _, _)|{ - let (name, theme): (Arc, ItemTheme) = view_scene_name_theme(scene, track_index); - let f = theme.lightest.term; - let (b, o) = view_scene_bg(theme, select, track_index, scene_index); - let is_selected = view_scene_sel(select, track_index, scene_index, editing); - below( - Outer(true, Style::default().fg(o)).full_wh(), - below( - below( - fg_bg(o, b, "".full_wh()), - fg_bg(f, b, bold(true, name)).align_nw().full_wh(), - ), - when(is_selected, editor).full_wh() - ).full_wh() - ).exact_wh( - view_scene_w(track, select, track_index, editor), - view_scene_y(select, scene_index, editor), - ) - }) - ) - .exact_w(track.width as u16)) - )).align_c() - } - -} - -fn view_scene_name_theme (scene: &Scene, track_index: usize) -> (Arc, ItemTheme) { - if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.try_read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - } -} - -fn with_clips_size (show: bool, size: &Sizer, content: impl Draw) -> impl Draw { - above( - when(show, fg(Green, east!(size.w() as usize, "x", size.h() as usize)).align_se()), - size.of(content.align_c().full_wh()) - ) -} - -fn view_scene_bg ( - theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize -) -> (Color, Color) { - let mut outline = theme.base.term; - (if select.track() == Some(track_index) && select.scene() == Some(scene_index) { - outline = theme.lighter.term; - theme.light.term - } else if select.track() == Some(track_index) || select.scene() == Some(scene_index) { - outline = theme.darkest.term; - theme.base.term - } else { - theme.dark.term - }, outline) -} - -fn view_scene_w ( - track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor> -) -> u16 { - if select.track() == Some(track_index) && let Some(editor) = editor { - (editor.size.w() as usize).max(24).max(track.width) as u16 - } else { - track.width as u16 - } -} - -fn view_scene_y ( - select: &Selection, scene_index: usize, editor: Option<&MidiEditor> -) -> u16 { - if select.scene() == Some(scene_index) && let Some(editor) = editor { - editor.size.h().max(12) - } else { - Scene::DEFAULT_HEIGHT as u16 - } -} - -fn view_scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool { - editing && select.track() == Some(track_index) && select.scene() == Some(scene_index) -} diff --git a/src/config.rs b/src/config.rs index 99327030..2eb14ab3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,7 +5,7 @@ use notify_debouncer_full::notify::{RecursiveMode, PollWatcher, Config as Notify /// Configuration: mode, view, and bind definitions. /// /// ``` -/// use ::{std::sync::Arc, tek::{Config, Modes::add, Mode}}; +/// use ::{std::sync::Arc, tek::{Config, Modes, Mode}}; /// let config: Config = Default::default(); /// Modes::add(&config.modes, stringify!( /// (mode :menu (name Menu) @@ -58,24 +58,18 @@ impl Config { pub fn load_item , L: Language> (config: C, src: L) -> Usually { if let Some(expr) = src.expr()? { - Config::load_kind(config, expr) + match expr.head()? { + Some("mode") => Modes::add(&config.as_ref().modes, expr.tail()), + Some("keys") => Binds::add(&config.as_ref().binds, expr.tail()), + Some("view") => Views::add(&config.as_ref().views, expr.tail()), + _ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into()) + }?; + Ok(config) } else { Err(format!("Config::add_one: tried to add empty item").into()) } } - pub fn load_kind , L: Language> (config: C, src: L) -> Usually { - //if let Some(tail) = src.tail()? { - match src.head()? { - Some("mode") => Modes::add(&config.as_ref().modes, src.tail()), - Some("keys") => binds_add(&config.as_ref().binds, src.tail()), - Some("view") => views_add(&config.as_ref().views, src.tail()), - _ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into()) - }?; - //} - Ok(config) - } - /// Watch a config's file for changes. pub fn watch ( config: Arc, poll: Option @@ -158,14 +152,14 @@ impl Config { } /// Make this configuration empty. - fn clear (&self) { + pub fn clear (&self) { *self.modes.0.try_write().unwrap() = Default::default(); - *self.views.try_write().unwrap() = Default::default(); - *self.binds.try_write().unwrap() = Default::default(); + self.views.clear(); + self.binds.clear(); } pub fn get_view (&self, name: impl AsRef) -> Option>> { - self.views.try_read().map(|views|views.get(name.as_ref()).cloned()).flatten() + self.views.get(name) } /// Default configuration directory. @@ -181,3 +175,12 @@ impl Config { const DEFAULTS: &'static str = include_str!("tek.edn"); } + +pub fn eval_word (src: impl Language, eval: impl Fn(&str)->Usually) -> Perhaps { + src.word()?.map(eval).transpose() +} + +//pub fn eval_expr (src: impl Language, eval: impl Fn(&str)->Usually) -> Perhaps { + //Ok(match expr.head()? { + //}) +//} diff --git a/src/bind.rs b/src/config_bind.rs similarity index 60% rename from src/bind.rs rename to src/config_bind.rs index a1b75a5e..38c6f69c 100644 --- a/src/bind.rs +++ b/src/config_bind.rs @@ -1,5 +1,63 @@ use crate::*; +/// Collection of input bindings. +#[derive(Default, Clone, Debug)] +pub struct Binds(pub Arc, + Arc>> +>>>); + +impl Binds { + pub fn add <'a> (binds: &Binds, expr: impl Language) -> UsuallyRef<'a, ()> { + let name = expr.head()?.ok_or("bind: missing name")?; + let body = expr.tail()?.unwrap_or(""); + binds.0.try_write().unwrap().insert(name.into(), { + let mut map = Bind::new(); + body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) { + // TODO + Ok(()) + } else if let Ok(Some(_word)) = item.expr().head().word() { + let expr = item.expr()?; + let head = expr.head()?; + if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() { + map.add(TuiEvent(event), Binding { + commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), + condition: None, + description: None, + source: None + }); + Ok(()) + } else if Some(":char") == item.expr()?.head()? { + // TODO + return Ok(()) + } else { + return err!("Binds::add: invalid key: {:?}", item.expr()?.head()?) + } + } else { + return err!("Binds::add: unexpected: {item:?}") + })?; + map.into() + }); + Ok(()) + } + + pub fn clear (&self) { + *self.0.write() = Default::default(); + } + + pub fn find (&self, id: impl AsRef) -> Option>>> { + self.0.read().get(id.as_ref()).cloned() + } + + pub fn for_each ( + &self, mut cb: impl FnMut(&Arc, &Bind>)->T + ) { + for (k, v) in self.0.read().iter() { + cb(k, v); + } + } +} + /// An map of input events (e.g. [TuiEvent]) to [Binding]s. /// /// ``` diff --git a/src/config_mode.rs b/src/config_mode.rs new file mode 100644 index 00000000..9a163c43 --- /dev/null +++ b/src/config_mode.rs @@ -0,0 +1,162 @@ +use crate::*; + +/// Collection of UI modes. +/// +/// ``` +/// let mut modes = tek::Modes::default(); +/// assert_eq!(modes.len(), 0); +/// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))"); +/// assert_eq!(modes.len(), 1); +/// ``` +#[derive(Default, Debug, Clone)] +pub struct Modes(pub Arc, + Arc +>>>); + +impl Modes { + + /// Register a mode. + pub fn add <'a> (&self, expr: impl Language) -> UsuallyRef<'a, ()> { + let name = expr.head()?.ok_or("mode: missing name")?; + let body = expr.tail()?.ok_or("mode: missing body")?; + let mode = Mode::default(); + let mode = body.each(mode, |c, s|Mode::add(c, s))?; + self.0.try_write().unwrap().insert(name.into(), Arc::new(mode)); + Ok(()) + } + + /// Get a mode by name. + pub fn get (&self, name: impl AsRef) -> Option> { + self.0.try_read().unwrap().get(name.as_ref()).cloned() + } + + /// Run something for each mode. + pub fn for_each (&self, mut ator: impl FnMut(&str, &Mode)->T) { + for (k, v) in self.0.try_read().unwrap().iter() { + let _ = ator(k.as_ref(), v.as_ref()); + } + } + + /// Count modes. + pub fn len (&self) -> usize { + self.0.try_read().unwrap().len() + } + +} + +/// Group of view and keys definitions. +/// +/// ``` +/// let mode = tek::Mode::default(); +/// ``` +#[derive(Default, Debug)] +pub struct Mode { + pub path: PathBuf, + pub name: Vec>, + pub info: Vec>, + pub view: Vec>>, + pub keys: Vec>, + pub modes: Modes, +} + +impl Mode { + + /// Add a definition to the mode. + /// + /// Supported definitions: + /// + /// - (name ...) -> name + /// - (info ...) -> description + /// - (keys ...) -> key bindings + /// - (mode ...) -> submode + /// - ... -> view + /// + /// ``` + /// let mut mode: tek::Mode = tek::Mode::add(tek::Mode::default(), "(name hello)").unwrap(); + /// ``` + pub fn add > (mut mode: T, 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(""); + match head { + "mode" => { + let name = tail.head()?.ok_or("submode: missing name")?; + let body = tail.tail()?.ok_or("submode: missing body")?; + let submode = Mode::default(); + let submode = body.each(submode, |c,s|Mode::add(c,s))?; + let modes = mode.as_mut().modes.clone(); + modes.0.try_write().unwrap().insert(name.into(), Arc::new(submode)); + mode + }, + "keys" => { + tail.each(mode, |mut mode: T, expr: &str|{ + mode.as_mut().keys.push(expr.trim().into()); + Ok(mode) + })? + }, + "name" => { mode.as_mut().name.push(tail.into()); mode }, + "info" => { mode.as_mut().info.push(tail.into()); mode }, + "view" => { mode.as_mut().view.push(View::new(tail)?.into()); mode }, + _ => { mode.as_mut().view.push(View::new(tail)?.into()); mode }, + } + } else if let Ok(Some(word)) = dsl.word() { + mode.as_mut().view.push(View::new(word)?.into()); + mode + } else { + return Err(format!("Mode::add: unexpected: {dsl:?}").into()); + }) + } + +} + +impl AsMut for Mode { + fn as_mut (&mut self) -> &mut Self { + self + } +} + +pub trait HasModeStack { + fn modes (&self) -> RwLockReadGuard<'_, Vec>>; + fn modes_mut (&mut self) -> RwLockWriteGuard<'_, Vec>>; +} + +impl HasModeStack for App { + fn modes (&self) -> RwLockReadGuard<'_, Vec>> { + self.modes.try_read().unwrap() + } + fn modes_mut (&mut self) -> RwLockWriteGuard<'_, Vec>> { + self.modes.try_write().unwrap() + } +} + +impl ModeController for T where T: HasModeStack + + Namespace>> + + Namespace> + + Namespace +{} + +#[tek_proc::commands(ModeCommand)] +pub trait ModeController: HasModeStack + + Namespace>> + + Namespace> + + Namespace +{ + #[command(Reset = "reset")] + fn mode_reset (&mut self, modes: Vec>) -> Perhaps { + *self.modes_mut() = modes.into(); + Ok(None) + } + + #[command(Push = "push")] + fn mode_push (&mut self, mode: Arc) -> Perhaps { + self.modes_mut().push(mode); + Ok(Some(ModeCommand::Pop)) + } + + #[command(Pop = "pop")] + fn mode_pop (&mut self) -> Perhaps { + let mode = self.modes_mut().pop(); + Ok(mode.map(|mode|ModeCommand::Push { mode })) + } +} diff --git a/src/views.rs b/src/config_view.rs similarity index 92% rename from src/views.rs rename to src/config_view.rs index a293e47c..2ad96110 100644 --- a/src/views.rs +++ b/src/config_view.rs @@ -1,7 +1,36 @@ use crate::*; /// Collection of custom view definitions. -pub type Views = Arc, Arc>>>>; +#[derive(Default, Clone, Debug)] +pub struct Views(pub Arc, + Arc> +>>>); + +impl Views { + /// Load custom view definition. + pub fn add <'a> (views: &Views, expr: impl Language) -> UsuallyRef<'a, ()> { + views.0.write().insert( + expr.head()?.ok_or("view: missing name")?.into(), + View::new(expr.tail()?.ok_or("view: missing body")?)?.into() + ); + Ok(()) + } + + pub fn clear (&self) { + *self.0.try_write().unwrap() = Default::default(); + } + + pub fn get (&self, name: impl AsRef) -> Option>> { + self.0.try_read().map(|views|views.get(name.as_ref()).cloned()).flatten() + } + + pub fn for_each (&self, cb: impl Fn(&Arc, &Arc>)->T) -> () { + for (k, v) in self.0.read().iter() { + cb(k, v); + } + } +} /// Custom view definition is a boxed closure emitting a [Draw]able from state `S`. pub struct View { @@ -37,7 +66,7 @@ impl View { } else if let Ok(Some(word)) = source.word() { Self::compile_word(word.into())? } else { - return Err(format!("not word/expr:\n{source:?}").into()) + return err!("not word/expr:\n{source:?}") }; Ok(Arc::new(Box::new(move|state, screen|layer(state, screen)))) } @@ -94,7 +123,7 @@ impl View { Some("w") => Split::West, Some("a") => Split::Above, Some("b") => Split::Below, - _ => return Err(format!("invalid split: {split:?}").into()) + _ => return err!("invalid split: {split:?}") }; let a = Self::compile(arg!(expr, head, 1, "content A"))?; let b = Self::compile(arg!(expr, head, 2, "content B"))?; @@ -113,7 +142,7 @@ impl View { Some("w") => Split::West, Some("a") => Split::Above, Some("b") => Split::Below, - _ => return Err(format!("invalid split: {split:?}").into()) + _ => return err!("invalid split: {split:?}") }; let size = Arc::from(arg!(expr, head, 1, "size")); let size = move|state: &App|state.namespace(&size)?.ok_or_else(||Box::::from("bar: no size")); @@ -140,7 +169,7 @@ impl View { Some("c") => Azimuth::C, Some("x") => Azimuth::X, Some("y") => Azimuth::Y, - _ => return Err(format!("invalid azimuth: {azimuth:?}").into()) + _ => return err!("invalid azimuth: {azimuth:?}") }; let thunk = Self::compile(arg!(expr, head, 1, "content"))?; Self::boxed(move|state, screen|{ @@ -239,12 +268,12 @@ impl View { }) }, - _ => return Err(format!("compile_expr: unexpected: {expr:?}").into()) + _ => return err!("compile_expr: unexpected: {expr:?}") } } else { - return Err(format!("compile_expr: invalid expression: {expr:?}").into()) + return err!("compile_expr: invalid expression: {expr:?}") })) } @@ -310,12 +339,3 @@ impl View { })) } } - -/// Load custom view definition. -pub fn views_add <'a> (views: &Views, expr: impl Language) -> UsuallyRef<'a, ()> { - views.try_write().unwrap().insert( - expr.head()?.ok_or("view: missing name")?.into(), - View::new(expr.tail()?.ok_or("view: missing body")?)?.into() - ); - Ok(()) -} diff --git a/src/dialog.rs b/src/dialog.rs index 22f68924..af13adc0 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -1,35 +1,43 @@ use crate::*; pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps { - src.word()?.map(|word|Ok(match word { - ":none" => Dialog::None, - ":options" => Dialog::Options, - ":device" => Dialog::Device(0), - ":device/prev" => Dialog::Device(0), - ":device/next" => Dialog::Device(0), - ":help" => Dialog::Help(0), - ":save" => Dialog::Browse( + + eval_word(&src, |word|Ok(match word { + + ":none" => Dialog::None, + + ":options" => Dialog::Options, + + ":device" => Dialog::Device(0), + ":device/prev" => Dialog::Device(0), + ":device/next" => Dialog::Device(0), + + ":help" => Dialog::Help(0), + ":save" => Dialog::Browse( BrowseTarget::SaveProject, Browse::new(None).unwrap().into()), - ":load" => Dialog::Browse( + ":load" => Dialog::Browse( BrowseTarget::LoadProject, Browse::new(None).unwrap().into()), - ":import/clip" => Dialog::Browse( + + ":import/clip" => Dialog::Browse( BrowseTarget::ImportClip(Default::default()), Browse::new(None).unwrap().into()), - ":export/clip" => Dialog::Browse( + ":export/clip" => Dialog::Browse( BrowseTarget::ExportClip(Default::default()), Browse::new(None).unwrap().into()), + ":import/sample" => Dialog::Browse( BrowseTarget::ImportSample(Default::default()), Browse::new(None).unwrap().into()), ":export/sample" => Dialog::Browse( BrowseTarget::ExportSample(Default::default()), Browse::new(None).unwrap().into()), - _ => return Err( - format!("not a dialog: {}", src.src()?.unwrap_or("")).into() - ) - })).transpose() + + _ => return Err(format!("not a dialog: {}", src.src()?.unwrap_or("")).into()) + + })) + } /// Various possible dialog modes. diff --git a/src/midi_clip.rs b/src/midi_clip.rs index 9d83b97c..08529843 100644 --- a/src/midi_clip.rs +++ b/src/midi_clip.rs @@ -1,5 +1,37 @@ use crate::*; +pub fn get_opt_arc_rwlock_midiclip <'a> (state: &App, src: impl Language) + -> Perhaps>>> +{ + eval_word(src, |word|Ok(match word { + + ":selected" if let Selection::TrackClip { + track, scene + } = state.selection() => { + state.scenes()[*scene].clips[*track].clone() + }, + + //":selected-or-create" if let Selection::TrackClip { + //track, scene + //} = state.selection() => Some(if let Some(Some(clip)) = state.scenes()[*scene].clips.get(*track) { + //clip.clone() + //} else { + //let clip: Arc> = Arc::new(RwLock::new(Default::default())); + //state.scenes()[*scene].clips[*track] = Some(clip.clone()); + //clip + //}), + + _ => return err!("not a clip: {word}") + + })) +} + +pub fn get_opt_vec_opt_arc_rwlock_midiclip <'a> (_: &App, _: impl Language) + -> Perhaps>>>>> +{ + todo!() +} + /// A MIDI sequence. /// /// ``` @@ -29,11 +61,11 @@ use crate::*; impl MidiClip { pub fn new ( - name: impl AsRef, - looped: bool, - length: usize, - notes: Option, - color: Option, + name: impl AsRef, + looped: bool, + length: usize, + notes: Option, + color: Option, ) -> Self { Self { uuid: uuid::Uuid::new_v4(), @@ -93,6 +125,8 @@ impl MidiClip { } pub type MidiData = Vec>; +impl PartialEq for MidiClip { fn eq (&self, other: &Self) -> bool { self.uuid == other.uuid } } +impl Eq for MidiClip {} #[macro_export] macro_rules! has_clip { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { @@ -122,8 +156,12 @@ pub trait MidiClipController: HasMidiClip + Namespace> + Namespace> { + #[command(Upsert = "upsert")] + fn clip_upsert (&mut self) -> Perhaps { + todo!() + } - #[command(SetColor = "clip/color")] + #[command(SetColor = "color")] fn clip_set_color (&mut self, _color: Option) -> Perhaps { //(SetColor [t: usize, s: usize, c: ItemTheme] //clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); @@ -131,7 +169,7 @@ pub trait MidiClipController: HasMidiClip todo!() } - #[command(SetLoop = "clip/loop")] + #[command(SetLoop = "loop")] fn clip_toggle_loop (&mut self, _looping: Option) -> Perhaps { //(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) //("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) @@ -139,11 +177,3 @@ pub trait MidiClipController: HasMidiClip } } - -impl PartialEq for MidiClip { - fn eq (&self, other: &Self) -> bool { - self.uuid == other.uuid - } -} - -impl Eq for MidiClip {} diff --git a/src/midi_clips.rs b/src/midi_clips.rs index 9ff62652..ba2523c7 100644 --- a/src/midi_clips.rs +++ b/src/midi_clips.rs @@ -26,3 +26,90 @@ pub trait HasClips { } } } + +impl Project { + /// Toggle looping for the active clip + pub fn toggle_loop (&mut self) { + if let Some(clip) = self.selected_clip() { + clip.try_write().unwrap().toggle_loop() + } + } + + /// Put a clip in a slot + pub fn clip_put ( + &mut self, track: usize, scene: usize, clip: Option>> + ) -> Option>> { + let old = self.scenes[scene].clips[track].clone(); + self.scenes[scene].clips[track] = clip; + old + } + + /// Change the color of a clip, returning the previous one + pub fn clip_set_color ( + &self, track: usize, scene: usize, color: ItemTheme + ) -> Option { + self.scenes[scene].clips[track].as_ref().map(|clip|{ + let mut clip = clip.try_write().unwrap(); + let old = clip.color.clone(); + clip.color = color.clone(); + panic!("{color:?} {old:?}"); + //old + }) + } +} + +pub trait HasClipsSize { + fn clips_size (&self) -> &Sizer; +} + +impl HasClipsSize for App { + fn clips_size (&self) -> &Sizer { &self.project.size_inner } +} + +impl HasClipsSize for Project { + fn clips_size (&self) -> &Sizer { &self.size_inner } +} + +impl ClipsView for T {} + +pub trait ClipsView: TracksView + ScenesView { + + /// Draw clips per scene + fn view_scenes_clips (&self) -> impl Draw { + let select = self.selection(); + let editor = self.editor(); + let editing = self.is_editing(); + with_clips_size(true, self.clips_size(), iter_east(move||self.tracks_with_sizes() + .map(move|(track_index, track, _, _)|iter_south(move||self.scenes_with_sizes() + .map(move|(scene_index, scene, _, _)|{ + let (name, theme): (Arc, ItemTheme) = view_scene_name_theme(scene, track_index); + let f = theme.lightest.term; + let (b, o) = view_scene_bg(theme, select, track_index, scene_index); + let is_selected = view_scene_sel(select, track_index, scene_index, editing); + below( + Outer(true, Style::default().fg(o)).full_wh(), + below( + below( + fg_bg(o, b, "".full_wh()), + fg_bg(f, b, bold(true, name)).align_nw().full_wh(), + ), + when(is_selected, editor).full_wh() + ).full_wh() + ).exact_wh( + view_scene_w(track, select, track_index, editor.as_ref()), + view_scene_y(select, scene_index, editor.as_ref()), + ) + }) + ) + .exact_w(track.width as u16)) + )).align_c() + } + +} + +fn with_clips_size (show: bool, size: &Sizer, content: impl Draw) -> impl Draw { + above( + when(show, fg(Green, east!(size.w() as usize, "x", size.h() as usize)).align_se()), + size.of(content.align_c().full_wh()) + ) +} diff --git a/src/editor.rs b/src/midi_edit.rs similarity index 86% rename from src/editor.rs rename to src/midi_edit.rs index dc784d43..7c7fb7f8 100644 --- a/src/editor.rs +++ b/src/midi_edit.rs @@ -1,70 +1,27 @@ #![allow(unused)] use crate::*; -impl App { - /// Is a MIDI editor currently focused? - /// - /// ``` - /// tek::App::default().editor_focused(); - /// ``` - pub fn editor_focused (&self) -> bool { - false +pub trait HasEditor: AsRef> + AsMut> { + fn editor (&self) -> &Option { + self.as_ref() } - - /// Toggle MIDI editor. - /// - /// ``` - /// tek::App::default().toggle_editor(None); - /// ``` - 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, 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.try_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.try_read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - self.pool.delete_clip(&clip.try_read().unwrap()); - } - } + fn editor_mut (&mut self) -> &mut Option { + self.as_mut() + } + fn is_editing (&self) -> bool { + self.editor().is_some() + } + fn editor_w (&self) -> usize { + self.editor().as_ref().map(|e|e.size.w()).unwrap_or(0) as usize + } + fn editor_h (&self) -> usize { + self.editor().as_ref().map(|e|e.size.h()).unwrap_or(0) as usize } } -pub trait HasEditor: AsRefOpt + AsMutOpt { - fn editor (&self) -> Option<&MidiEditor> { self.as_ref_opt() } - fn editor_mut (&mut self) -> Option<&mut MidiEditor> { self.as_mut_opt() } - fn is_editing (&self) -> bool { self.editor().is_some() } - fn editor_w (&self) -> usize { self.editor().map(|e|e.size.w()).unwrap_or(0) as usize } - fn editor_h (&self) -> usize { self.editor().map(|e|e.size.h()).unwrap_or(0) as usize } -} - -impl+AsMutOpt> HasEditor for T {} +impl HasEditor for T where T: + AsRef> + + AsMut> {} impl @@ -75,7 +32,7 @@ impl>>> > MidiEditController for T {} -#[tek_proc::commands(MidiEditCommand = "edit")] +#[tek_proc::commands(MidiEditCommand)] pub trait MidiEditController: HasEditor + Namespace + Namespace @@ -84,9 +41,19 @@ pub trait MidiEditController: HasEditor + Namespace> + Namespace>>> { + #[command(Toggle = "toggle")] + fn edit_toggle (&mut self) -> Perhaps { + if self.is_editing() { + *self.editor_mut() = None; + } else { + todo!() + } + Ok(Some(MidiEditCommand::Toggle)) + } + #[command(Show = "show")] - fn show (&mut self, clip: Option>>) -> Perhaps { - Ok(self.editor_mut().map(|editor|{ + fn edit_show (&mut self, clip: Option>>) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|mut editor|{ editor.set_clip(clip.as_ref()); editor.redraw(); None @@ -94,16 +61,16 @@ pub trait MidiEditController: HasEditor } #[command(DeleteNote = "delete")] - fn note_delete (&mut self) -> Perhaps { - Ok(self.editor_mut().map(|editor|{ + fn edit_note_delete (&mut self) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ editor.redraw(); todo!() }).flatten()) } #[command(AppendNote = "append")] - fn note_append (&mut self, advance: bool) -> Perhaps { - Ok(self.editor_mut().map(|editor|{ + fn edit_note_append (&mut self, advance: bool) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|mut editor|{ editor.put_note(advance); editor.redraw(); None @@ -111,8 +78,8 @@ pub trait MidiEditController: HasEditor } #[command(SetNotePos = "note-pos")] - fn note_set_pos (&mut self, pos: usize) -> Perhaps { - Ok(self.editor_mut().map(|editor|{ + fn edit_note_set_pos (&mut self, pos: usize) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ editor.set_note_pos((pos).min(127)); editor.redraw(); None @@ -120,8 +87,8 @@ pub trait MidiEditController: HasEditor } #[command(SetNoteLen = "note-len")] - fn note_set_len (&mut self, len: usize) -> Perhaps { - Ok(self.editor_mut().map(|editor|{ + fn edit_note_set_len (&mut self, len: usize) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ editor.set_note_len(len); editor.redraw(); None @@ -129,8 +96,8 @@ pub trait MidiEditController: HasEditor } #[command(SetNoteScroll = "note-scroll")] - fn note_set_scroll (&mut self, scroll: usize) -> Perhaps { - Ok(self.editor_mut().map(|editor|{ + fn edit_note_set_scroll (&mut self, scroll: usize) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ editor.set_note_lo((scroll).min(127)); editor.redraw(); None @@ -138,8 +105,8 @@ pub trait MidiEditController: HasEditor } #[command(SetTimePos = "time-pos")] - fn time_set_pos (&mut self, pos: usize) -> Perhaps { - Ok(self.editor_mut().map(|editor|{ + fn edit_time_set_pos (&mut self, pos: usize) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ editor.set_time_pos(pos); editor.redraw(); None @@ -147,8 +114,8 @@ pub trait MidiEditController: HasEditor } #[command(SetTimeScroll = "time-scroll")] - fn time_set_scroll (&mut self, scroll: usize) -> Perhaps { - Ok(self.editor_mut().map(|editor|{ + fn edit_time_set_scroll (&mut self, scroll: usize) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ editor.set_time_start(scroll); editor.redraw(); None @@ -156,8 +123,8 @@ pub trait MidiEditController: HasEditor } #[command(SetTimeZoom = "time-zoom")] - fn time_set_zoom (&mut self, zoom: usize) -> Perhaps { - Ok(self.editor_mut().map(|editor|{ + fn edit_time_set_zoom (&mut self, zoom: usize) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ editor.set_time_zoom(zoom); editor.redraw(); None @@ -165,13 +132,14 @@ pub trait MidiEditController: HasEditor } #[command(SetTimeLock = "time-lock")] - fn time_set_lock (&mut self, lock: bool) -> Perhaps { - Ok(self.editor_mut().map(|editor|{ + fn edit_time_set_lock (&mut self, lock: bool) -> Perhaps { + Ok(self.editor_mut().as_mut().map(|editor|{ editor.set_time_lock(lock); editor.redraw(); None }).flatten()) } + // TODO: 1-9 seek markers that by default start every 8th of the clip } @@ -432,3 +400,58 @@ impl Draw for MidiEditor { self.size.of(&self.mode).draw(to) } } + +impl App { + /// Is a MIDI editor currently focused? + /// + /// ``` + /// tek::App::default().editor_focused(); + /// ``` + pub fn editor_focused (&self) -> bool { + false + } + + /// Toggle MIDI editor. + /// + /// ``` + /// tek::App::default().toggle_editor(None); + /// ``` + 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, 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.try_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.try_read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + self.pool.delete_clip(&clip.try_read().unwrap()); + } + } + } +} diff --git a/src/mode.rs b/src/mode.rs deleted file mode 100644 index 4402a462..00000000 --- a/src/mode.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::*; - -/// Group of view and keys definitions. -/// -/// ``` -/// let mode = tek::Mode::default(); -/// ``` -#[derive(Default, Debug)] -pub struct Mode { - pub path: PathBuf, - pub name: Vec>, - pub info: Vec>, - pub view: Vec>>, - pub keys: Vec>, - pub modes: Modes, -} - -impl Mode { - - /// Add a definition to the mode. - /// - /// Supported definitions: - /// - /// - (name ...) -> name - /// - (info ...) -> description - /// - (keys ...) -> key bindings - /// - (mode ...) -> submode - /// - ... -> view - /// - /// ``` - /// let mut mode: tek::Mode = tek::Mode::add(tek::Mode::default(), "(name hello)").unwrap(); - /// ``` - pub fn add > (mut mode: T, 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(""); - match head { - "mode" => { - let name = tail.head()?.ok_or("submode: missing name")?; - let body = tail.tail()?.ok_or("submode: missing body")?; - let submode = Mode::default(); - let submode = body.each(submode, |c,s|Mode::add(c,s))?; - let modes = mode.as_mut().modes.clone(); - modes.0.try_write().unwrap().insert(name.into(), Arc::new(submode)); - mode - }, - "keys" => { - tail.each(mode, |mut mode: T, expr: &str|{ - mode.as_mut().keys.push(expr.trim().into()); - Ok(mode) - })? - }, - "name" => { mode.as_mut().name.push(tail.into()); mode }, - "info" => { mode.as_mut().info.push(tail.into()); mode }, - "view" => { mode.as_mut().view.push(View::new(tail)?.into()); mode }, - _ => { mode.as_mut().view.push(View::new(tail)?.into()); mode }, - } - } else if let Ok(Some(word)) = dsl.word() { - mode.as_mut().view.push(View::new(word)?.into()); - mode - } else { - return Err(format!("Mode::add: unexpected: {dsl:?}").into()); - }) - } - -} - -impl AsMut for Mode { - fn as_mut (&mut self) -> &mut Self { - self - } -} diff --git a/src/mode_stack.rs b/src/mode_stack.rs deleted file mode 100644 index 3d381a3d..00000000 --- a/src/mode_stack.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::*; - -pub trait HasModeStack { - fn modes (&self) -> RwLockReadGuard>>; - fn modes_mut (&mut self) -> RwLockWriteGuard>>; -} - -impl HasModeStack for App { - fn modes (&self) -> RwLockReadGuard>> { - self.modes.try_read().unwrap() - } - fn modes_mut (&mut self) -> RwLockWriteGuard>> { - self.modes.try_write().unwrap() - } -} - -impl ModeController for T where T: HasModeStack - + Namespace>> - + Namespace> - + Namespace -{} - -#[tek_proc::commands(ModeCommand)] -pub trait ModeController: HasModeStack - + Namespace>> - + Namespace> - + Namespace -{ - #[command(Reset = "reset")] - fn mode_reset (&mut self, modes: Vec>) -> Perhaps { - *self.modes_mut() = modes.into(); - Ok(None) - } - - #[command(Push = "push")] - fn mode_push (&mut self, mode: Arc) -> Perhaps { - self.modes_mut().push(mode); - Ok(Some(ModeCommand::Pop)) - } - - #[command(Pop = "pop")] - fn mode_pop (&mut self) -> Perhaps { - let mode = self.modes_mut().pop(); - Ok(mode.map(|mode|ModeCommand::Push { mode })) - } -} diff --git a/src/modes.rs b/src/modes.rs deleted file mode 100644 index 7a718106..00000000 --- a/src/modes.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::*; - -/// Collection of UI modes. -/// -/// ``` -/// let mut modes = tek::Modes::default(); -/// assert_eq!(modes.len(), 0); -/// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))"); -/// assert_eq!(modes.len(), 1); -/// ``` -#[derive(Default, Debug, Clone)] -pub struct Modes( - pub Arc, Arc>>> -); - -impl Modes { - - /// Register a mode. - pub fn add <'a> (modes: &Modes, expr: impl Language) -> UsuallyRef<'a, ()> { - let name = expr.head()?.ok_or("mode: missing name")?; - let body = expr.tail()?.ok_or("mode: missing body")?; - let mode = Mode::default(); - let mode = body.each(mode, |c, s|Mode::add(c, s))?; - modes.0.try_write().unwrap().insert(name.into(), Arc::new(mode)); - Ok(()) - } - - /// Get a mode by name. - pub fn get (&self, name: impl AsRef) -> Option> { - self.0.try_read().unwrap().get(name.as_ref()).cloned() - } - - /// Run something for each mode. - pub fn for_each (&self, mut ator: impl FnMut(&str, &Mode)->T) { - for (k, v) in self.0.try_read().unwrap().iter() { - let _ = ator(k.as_ref(), v.as_ref()); - } - } - - /// Count modes. - pub fn len (&self) -> usize { - self.0.try_read().unwrap().len() - } - -} diff --git a/src/project.rs b/src/project.rs index 35afbbe0..3ac54d71 100644 --- a/src/project.rs +++ b/src/project.rs @@ -54,7 +54,7 @@ pub struct Project { /// Display size of clips area pub size_inner: Sizer, /// Selected UI element - pub selection: Selection, + pub selection: Selection, /// Source of time #[cfg(feature = "clock")] pub clock: Clock, /// Allows one MIDI clip to be edited @@ -138,12 +138,15 @@ impl Project { } impl HasJack<'static> for Project { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl_has!(Jack<'static>: |self: Project| self.jack); -impl_has!(Sizer: |self: Project| self.size); -impl_has!(Clock: |self: Project| self.clock); -impl_has!(Selection: |self: Project| self.selection); -impl_as_ref_opt!(MidiEditor: |self: Project| self.editor.as_ref()); -impl_as_mut_opt!(MidiEditor: |self: Project| self.editor.as_mut()); +impl_has!(Jack<'static>: |self: Project| self.jack); +impl_has!(Sizer: |self: Project| self.size); +impl_has!(Clock: |self: Project| self.clock); +impl_has!(Selection: |self: Project| self.selection); +impl_has!(Vec: |self: Project| self.scenes); +impl_as_ref_opt!(Scene: |self: Project| self.selected_scene()); +impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut()); +impl_as_ref!(Option: |self: Project| &self.editor); +impl_as_mut!(Option: |self: Project| &mut self.editor); pub fn swap_value ( target: &mut T, value: &T, returned: impl Fn(T)->U diff --git a/src/sample.rs b/src/sample.rs new file mode 100644 index 00000000..0b034cda --- /dev/null +++ b/src/sample.rs @@ -0,0 +1,144 @@ +use crate::*; + +/// A sound cut. +/// +/// ``` +/// let sample = tek::Sample::default(); +/// let sample = tek::Sample::new("test", 0, 0, vec![]); +/// ``` +#[derive(Default, Debug)] pub struct Sample { + pub name: Arc, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, + pub gain: f32, + pub color: ItemTheme, +} + +impl Sample { + pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { + Self { + name: name.as_ref().into(), + start, + end, + channels, + rate: None, + gain: 1.0, + color: ItemTheme::random(), + } + } + pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { + Voice { + sample: sample.clone(), + after, + position: sample.try_read().unwrap().start, + velocity: velocity.as_int() as f32 / 127.0, + } + } + pub fn handle_cc (&mut self, controller: u7, value: u7) { + let percentage = value.as_int() as f64 / 127.; + match controller.as_int() { + 20 => { + self.start = (percentage * self.end as f64) as usize; + }, + 21 => { + let length = self.channels[0].len(); + self.end = length.min( + self.start + (percentage * (length as f64 - self.start as f64)) as usize + ); + }, + 22 => { /*attack*/ }, + 23 => { /*decay*/ }, + 24 => { + self.gain = percentage as f32 * 2.0; + }, + 26 => { /* pan */ } + 25 => { /* pitch */ } + _ => {} + } + } + /// Read WAV from file + pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) + } + pub fn from_file (path: &PathBuf) -> Usually { + let name = path.file_name().unwrap().to_string_lossy().into(); + let mut sample = Self { name, ..Default::default() }; + // Use file extension if present + let mut hint = Hint::new(); + if let Some(ext) = path.extension() { + hint.with_extension(&ext.to_string_lossy()); + } + let probed = symphonia::default::get_probe().format( + &hint, + MediaSourceStream::new( + Box::new(File::open(path)?), + Default::default(), + ), + &Default::default(), + &Default::default() + )?; + let mut format = probed.format; + let params = &format.tracks().iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no tracks found") + .codec_params; + let mut decoder = get_codecs().make(params, &Default::default())?; + loop { + match format.next_packet() { + Ok(packet) => sample.decode_packet(&mut decoder, packet)?, + Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), + Err(err) => return Err(err.into()), + }; + }; + sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); + Ok(sample) + } + fn decode_packet ( + &mut self, decoder: &mut Box, packet: Packet + ) -> Usually<()> { + // Decode a packet + let decoded = decoder + .decode(&packet) + .map_err(|e|Box::::from(e))?; + // Determine sample rate + let spec = *decoded.spec(); + if let Some(rate) = self.rate { + if rate != spec.rate as usize { + panic!("sample rate changed"); + } + } else { + self.rate = Some(spec.rate as usize); + } + // Determine channel count + while self.channels.len() < spec.channels.count() { + self.channels.push(vec![]); + } + // Load sample + let mut samples = SampleBuffer::new( + decoded.frames() as u64, + spec + ); + if samples.capacity() > 0 { + samples.copy_interleaved_ref(decoded); + for frame in samples.samples().chunks(spec.channels.count()) { + for (chan, frame) in frame.iter().enumerate() { + self.channels[chan].push(*frame) + } + } + } + Ok(()) + } +} diff --git a/src/sample_add.rs b/src/sample_add.rs new file mode 100644 index 00000000..5088d3de --- /dev/null +++ b/src/sample_add.rs @@ -0,0 +1,128 @@ +use crate::*; + +#[derive(Default, Debug)] pub struct SampleAdd { + pub exited: bool, + pub dir: PathBuf, + pub subdirs: Vec, + pub files: Vec, + pub cursor: usize, + pub offset: usize, + pub sample: Arc>, + pub voices: Arc>>, + pub _search: Option, +} + +impl_draw!(|self: SampleAdd, to: Tui|{ todo!() }); + +#[allow(unused)] +impl SampleAdd { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } + pub fn new ( + sample: &Arc>, + voices: &Arc>> + ) -> Usually { + let dir = std::env::current_dir()?; + let (subdirs, files) = scan(&dir)?; + Ok(Self { + exited: false, + dir, + subdirs, + files, + cursor: 0, + offset: 0, + sample: sample.clone(), + voices: voices.clone(), + _search: None + }) + } + fn rescan (&mut self) -> Usually<()> { + scan(&self.dir).map(|(subdirs, files)|{ + self.subdirs = subdirs; + self.files = files; + }) + } + fn prev (&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + fn next (&mut self) { + self.cursor = self.cursor + 1; + } + fn try_preview (&mut self) -> Usually<()> { + if let Some(path) = self.cursor_file() { + if let Ok(sample) = Sample::from_file(&path) { + *self.sample.try_write().unwrap() = sample; + self.voices.try_write().unwrap().push( + Sample::play(&self.sample, 0, &u7::from(100u8)) + ); + } + //load_sample(&path)?; + //let src = std::fs::File::open(&path)?; + //let mss = MediaSourceStream::new(Box::new(src), Default::default()); + //let mut hint = Hint::new(); + //if let Some(ext) = path.extension() { + //hint.with_extension(&ext.to_string_lossy()); + //} + //let meta_opts: MetadataOptions = Default::default(); + //let fmt_opts: FormatOptions = Default::default(); + //if let Ok(mut probed) = symphonia::default::get_probe() + //.format(&hint, mss, &fmt_opts, &meta_opts) + //{ + //panic!("{:?}", probed.format.metadata()); + //}; + } + Ok(()) + } + fn cursor_dir (&self) -> Option { + if self.cursor < self.subdirs.len() { + Some(self.dir.join(&self.subdirs[self.cursor])) + } else { + None + } + } + fn cursor_file (&self) -> Option { + if self.cursor < self.subdirs.len() { + return None + } + let index = self.cursor.saturating_sub(self.subdirs.len()); + if index < self.files.len() { + Some(self.dir.join(&self.files[index])) + } else { + None + } + } + fn pick (&mut self) -> Usually { + if self.cursor == 0 { + if let Some(parent) = self.dir.parent() { + self.dir = parent.into(); + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + } + if let Some(dir) = self.cursor_dir() { + self.dir = dir; + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + if let Some(path) = self.cursor_file() { + let (end, channels) = read_sample_data(&path.to_string_lossy())?; + let mut sample = self.sample.try_write().unwrap(); + sample.name = path.file_name().unwrap().to_string_lossy().into(); + sample.end = end; + sample.channels = channels; + return Ok(true) + } + return Ok(false) + } +} + +#[allow(unused)] +fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { + todo!(); +} diff --git a/src/sample_kit.rs b/src/sample_kit.rs new file mode 100644 index 00000000..382ce678 --- /dev/null +++ b/src/sample_kit.rs @@ -0,0 +1,26 @@ +use crate::*; + +/// Collection of samples, one per slot, fixed number of slots. +/// +/// History: Separated to cleanly implement [Default]. +/// +/// ``` +/// let samples = tek::SampleKit([None, None, None, None]); +/// ``` +#[derive(Debug)] pub struct SampleKit ( + pub [Option>>;N] +); + +impl Default for SampleKit { + fn default () -> Self { Self([const { None }; N]) } +} + +impl SampleKit { + pub fn get (&self, index: usize) -> &Option>> { + if index < self.0.len() { + &self.0[index] + } else { + &None + } + } +} diff --git a/src/sample_voice.rs b/src/sample_voice.rs new file mode 100644 index 00000000..75be5ae9 --- /dev/null +++ b/src/sample_voice.rs @@ -0,0 +1,30 @@ +use crate::*; + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after -= 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.try_read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position += 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity * sample.gain, + sample.channels[0][position] * self.velocity * sample.gain, + ]) + } + None + } +} + diff --git a/src/sampler.rs b/src/sampler.rs index 55d35da7..63947297 100644 --- a/src/sampler.rs +++ b/src/sampler.rs @@ -8,18 +8,6 @@ pub(crate) use symphonia::{ }, }; -/// Get the first sampler of the active track -impl Project { - pub fn sampler (&self) -> Option<&Sampler> { - self.selected_track()?.sampler(0) - } - - /// Get the first sampler of the active track - pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.selected_track_mut()?.sampler_mut(0) - } -} - pub trait HasSampler: AsRef + AsMut { fn sampler (&self) -> &Sampler { self.as_ref() @@ -441,327 +429,6 @@ fn draw_sample ( Ok(label1.len() + label2.len() + 4) } -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after -= 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.try_read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position += 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity * sample.gain, - sample.channels[0][position] * self.velocity * sample.gain, - ]) - } - None - } -} - -/// Collection of samples, one per slot, fixed number of slots. -/// -/// History: Separated to cleanly implement [Default]. -/// -/// ``` -/// let samples = tek::SampleKit([None, None, None, None]); -/// ``` -#[derive(Debug)] pub struct SampleKit ( - pub [Option>>;N] -); - -impl Default for SampleKit { - fn default () -> Self { Self([const { None }; N]) } -} - -impl SampleKit { - pub fn get (&self, index: usize) -> &Option>> { - if index < self.0.len() { - &self.0[index] - } else { - &None - } - } -} - -/// A sound cut. -/// -/// ``` -/// let sample = tek::Sample::default(); -/// let sample = tek::Sample::new("test", 0, 0, vec![]); -/// ``` -#[derive(Default, Debug)] pub struct Sample { - pub name: Arc, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, - pub gain: f32, - pub color: ItemTheme, -} - -impl Sample { - pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { - Self { - name: name.as_ref().into(), - start, - end, - channels, - rate: None, - gain: 1.0, - color: ItemTheme::random(), - } - } - pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { - Voice { - sample: sample.clone(), - after, - position: sample.try_read().unwrap().start, - velocity: velocity.as_int() as f32 / 127.0, - } - } - pub fn handle_cc (&mut self, controller: u7, value: u7) { - let percentage = value.as_int() as f64 / 127.; - match controller.as_int() { - 20 => { - self.start = (percentage * self.end as f64) as usize; - }, - 21 => { - let length = self.channels[0].len(); - self.end = length.min( - self.start + (percentage * (length as f64 - self.start as f64)) as usize - ); - }, - 22 => { /*attack*/ }, - 23 => { /*decay*/ }, - 24 => { - self.gain = percentage as f32 * 2.0; - }, - 26 => { /* pan */ } - 25 => { /* pitch */ } - _ => {} - } - } - /// Read WAV from file - pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path(src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - Ok((end, data)) - } - pub fn from_file (path: &PathBuf) -> Usually { - let name = path.file_name().unwrap().to_string_lossy().into(); - let mut sample = Self { name, ..Default::default() }; - // Use file extension if present - let mut hint = Hint::new(); - if let Some(ext) = path.extension() { - hint.with_extension(&ext.to_string_lossy()); - } - let probed = symphonia::default::get_probe().format( - &hint, - MediaSourceStream::new( - Box::new(File::open(path)?), - Default::default(), - ), - &Default::default(), - &Default::default() - )?; - let mut format = probed.format; - let params = &format.tracks().iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .expect("no tracks found") - .codec_params; - let mut decoder = get_codecs().make(params, &Default::default())?; - loop { - match format.next_packet() { - Ok(packet) => sample.decode_packet(&mut decoder, packet)?, - Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), - Err(err) => return Err(err.into()), - }; - }; - sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); - Ok(sample) - } - fn decode_packet ( - &mut self, decoder: &mut Box, packet: Packet - ) -> Usually<()> { - // Decode a packet - let decoded = decoder - .decode(&packet) - .map_err(|e|Box::::from(e))?; - // Determine sample rate - let spec = *decoded.spec(); - if let Some(rate) = self.rate { - if rate != spec.rate as usize { - panic!("sample rate changed"); - } - } else { - self.rate = Some(spec.rate as usize); - } - // Determine channel count - while self.channels.len() < spec.channels.count() { - self.channels.push(vec![]); - } - // Load sample - let mut samples = SampleBuffer::new( - decoded.frames() as u64, - spec - ); - if samples.capacity() > 0 { - samples.copy_interleaved_ref(decoded); - for frame in samples.samples().chunks(spec.channels.count()) { - for (chan, frame) in frame.iter().enumerate() { - self.channels[chan].push(*frame) - } - } - } - Ok(()) - } -} - -#[derive(Default, Debug)] pub struct SampleAdd { - pub exited: bool, - pub dir: PathBuf, - pub subdirs: Vec, - pub files: Vec, - pub cursor: usize, - pub offset: usize, - pub sample: Arc>, - pub voices: Arc>>, - pub _search: Option, -} - -impl_draw!(|self: SampleAdd, to: Tui|{ todo!() }); - -impl SampleAdd { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true - } - pub fn new ( - sample: &Arc>, - voices: &Arc>> - ) -> Usually { - let dir = std::env::current_dir()?; - let (subdirs, files) = scan(&dir)?; - Ok(Self { - exited: false, - dir, - subdirs, - files, - cursor: 0, - offset: 0, - sample: sample.clone(), - voices: voices.clone(), - _search: None - }) - } - fn rescan (&mut self) -> Usually<()> { - scan(&self.dir).map(|(subdirs, files)|{ - self.subdirs = subdirs; - self.files = files; - }) - } - fn prev (&mut self) { - self.cursor = self.cursor.saturating_sub(1); - } - fn next (&mut self) { - self.cursor = self.cursor + 1; - } - fn try_preview (&mut self) -> Usually<()> { - if let Some(path) = self.cursor_file() { - if let Ok(sample) = Sample::from_file(&path) { - *self.sample.try_write().unwrap() = sample; - self.voices.try_write().unwrap().push( - Sample::play(&self.sample, 0, &u7::from(100u8)) - ); - } - //load_sample(&path)?; - //let src = std::fs::File::open(&path)?; - //let mss = MediaSourceStream::new(Box::new(src), Default::default()); - //let mut hint = Hint::new(); - //if let Some(ext) = path.extension() { - //hint.with_extension(&ext.to_string_lossy()); - //} - //let meta_opts: MetadataOptions = Default::default(); - //let fmt_opts: FormatOptions = Default::default(); - //if let Ok(mut probed) = symphonia::default::get_probe() - //.format(&hint, mss, &fmt_opts, &meta_opts) - //{ - //panic!("{:?}", probed.format.metadata()); - //}; - } - Ok(()) - } - fn cursor_dir (&self) -> Option { - if self.cursor < self.subdirs.len() { - Some(self.dir.join(&self.subdirs[self.cursor])) - } else { - None - } - } - fn cursor_file (&self) -> Option { - if self.cursor < self.subdirs.len() { - return None - } - let index = self.cursor.saturating_sub(self.subdirs.len()); - if index < self.files.len() { - Some(self.dir.join(&self.files[index])) - } else { - None - } - } - fn pick (&mut self) -> Usually { - if self.cursor == 0 { - if let Some(parent) = self.dir.parent() { - self.dir = parent.into(); - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - } - if let Some(dir) = self.cursor_dir() { - self.dir = dir; - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - if let Some(path) = self.cursor_file() { - let (end, channels) = read_sample_data(&path.to_string_lossy())?; - let mut sample = self.sample.try_write().unwrap(); - sample.name = path.file_name().unwrap().to_string_lossy().into(); - sample.end = end; - sample.channels = channels; - return Ok(true) - } - return Ok(false) - } -} - -fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { - todo!(); -} - pub fn view_sample_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { when(sample.is_some(), draw(move|to: &mut Tui|{ let sample = sample.unwrap().try_read().unwrap(); @@ -809,6 +476,18 @@ pub fn view_sample_status (sample: Option<&Arc>>) -> impl Draw Option<&Sampler> { + self.selected_track()?.sampler(0) + } + + /// Get the first sampler of the active track + pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { + self.selected_track_mut()?.sampler_mut(0) + } +} + #[cfg(feature = "track")] impl Track { /// Create a new track connecting the [Sequencer] to a [Sampler]. diff --git a/src/scene.rs b/src/scene.rs index f0798ca0..d7925caf 100644 --- a/src/scene.rs +++ b/src/scene.rs @@ -55,10 +55,6 @@ impl Scene { } -impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); -impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); -impl_as_ref_opt!(Scene: |self: Project| self.selected_scene()); -impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut()); impl + AsMutOpt + Send + Sync> HasScene for T {} @@ -123,9 +119,10 @@ pub trait SceneController: HasScene pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T); def_sizes_iter!(ScenesSizes => Scene); -impl_has!(Vec: |self: Project| self.scenes); impl_as_ref!(Vec: |self: App| self.project.as_ref()); impl_as_mut!(Vec: |self: App| self.project.as_mut()); +impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); +impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); impl> + AsMut>> HasScenes for T {} @@ -236,7 +233,7 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + let editor = self.editor(); let editing = self.is_editing(); iter_south(move||self.scenes_with_sizes().map(move|(index, scene, ..)|{ - view_scene_name(select, editor, index, scene, editing) + view_scene_name(select, editor.as_ref(), index, scene, editing) })) } @@ -322,3 +319,51 @@ impl HasSceneScroll for App { self.project.scene_scroll() } } + +pub fn view_scene_name_theme (scene: &Scene, track_index: usize) -> (Arc, ItemTheme) { + if let Some(Some(clip)) = &scene.clips.get(track_index) { + let clip = clip.try_read().unwrap(); + (format!(" ⏹ {}", &clip.name).into(), clip.color) + } else { + (" ⏹ -- ".into(), ItemTheme::G[32]) + } +} + +pub fn view_scene_bg ( + theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize +) -> (Color, Color) { + let mut outline = theme.base.term; + (if select.track() == Some(track_index) && select.scene() == Some(scene_index) { + outline = theme.lighter.term; + theme.light.term + } else if select.track() == Some(track_index) || select.scene() == Some(scene_index) { + outline = theme.darkest.term; + theme.base.term + } else { + theme.dark.term + }, outline) +} + +pub fn view_scene_w ( + track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor> +) -> u16 { + if select.track() == Some(track_index) && let Some(editor) = editor { + (editor.size.w() as usize).max(24).max(track.width) as u16 + } else { + track.width as u16 + } +} + +pub fn view_scene_y ( + select: &Selection, scene_index: usize, editor: Option<&MidiEditor> +) -> u16 { + if select.scene() == Some(scene_index) && let Some(editor) = editor { + editor.size.h().max(12) + } else { + Scene::DEFAULT_HEIGHT as u16 + } +} + +pub fn view_scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool { + editing && select.track() == Some(track_index) && select.scene() == Some(scene_index) +} diff --git a/src/tek.edn b/src/tek.edn index e530469e..63fe7fc6 100644 --- a/src/tek.edn +++ b/src/tek.edn @@ -43,7 +43,7 @@ (bar/n 2 :status (bar/s 8 (bar/e 16 (align/nw (pad/x 1 :transport)) - (bar/w 16 (bar/s 2 (bar/w 4 :tracks/outputs/add :tracks/outputs/count) + (bar/w 20 (bar/s 2 (bar/w 4 :tracks/outputs/add :tracks/outputs/count) (bar/s 2 (bar/w 4 :tracks/devices/add :tracks/devices/count) (bar/s 2 (bar/w 4 :tracks/names/add :tracks/names/count) (bar/w 4 :tracks/inputs/add :tracks/inputs/count)))) @@ -52,7 +52,7 @@ (bar/s 2 :tracks/devices/cells (bar/s 2 :tracks/names/cells :tracks/inputs/cells)))))) - (bar/w 16 (text) + (bar/w 20 (text) (bar/e 16 :scenes/names :scenes/clips)))))) @@ -89,7 +89,7 @@ (keys :clip (see :color :launch :axis/z :axis/z2 :delete) (@l (toggle :loop)) - (@tab (toggle :edit))) + (@tab (clip upsert) (edit toggle))) (view :ports/out (bsp/s (align/w (text L-AUDIO-OUT)) diff --git a/src/tek.rs b/src/tek.rs index 8974c5cc..d7cb1679 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -9,26 +9,22 @@ macro_rules! def_sizes_iter { } mod app; pub use self::app::*; -mod bind; pub use self::bind::*; -mod binds; pub use self::binds::*; mod browse; pub use self::browse::*; mod cli; pub use self::cli::*; -mod clip; pub use self::clip::*; mod clock; pub use self::clock::*; mod clock_view; pub use self::clock_view::*; mod config; pub use self::config::*; +mod config_bind; pub use self::config_bind::*; +mod config_mode; pub use self::config_mode::*; +mod config_view; pub use self::config_view::*; mod deps; pub use self::deps::*; mod device; pub use self::device::*; mod dialog; pub use self::dialog::*; -mod editor; pub use self::editor::*; mod field; pub use self::field::*; mod memo; pub use self::memo::*; mod menu_item; pub use self::menu_item::*; mod meter; pub use self::meter::*; mod mix; pub use self::mix::*; -mod mode; pub use self::mode::*; -mod mode_stack; pub use self::mode_stack::*; -mod modes; pub use self::modes::*; mod moment; pub use self::moment::*; mod octave; pub use self::octave::*; mod piano; pub use self::piano::*; @@ -38,10 +34,15 @@ mod port; pub use self::port::*; mod project; pub use self::project::*; mod range; pub use self::range::*; mod sampler; pub use self::sampler::*; +mod sample; pub use self::sample::*; +mod sample_add; pub use self::sample_add::*; +mod sample_kit; pub use self::sample_kit::*; +mod sample_voice; pub use self::sample_voice::*; mod scene; pub use self::scene::*; mod select; pub use self::select::*; mod midi_clip; pub use self::midi_clip::*; mod midi_clips; pub use self::midi_clips::*; +mod midi_edit; pub use self::midi_edit::*; mod midi_mon; pub use self::midi_mon::*; mod midi_play; pub use self::midi_play::*; mod midi_rec; pub use self::midi_rec::*; @@ -49,7 +50,6 @@ mod midi_seq; pub use self::midi_seq::*; mod ticker; pub use self::ticker::*; mod timebase; pub use self::timebase::*; mod track; pub use self::track::*; -mod views; pub use self::views::*; #[cfg(feature = "lv2")] mod lv2; #[cfg(feature = "lv2")] pub use self::lv2::*; diff --git a/src/track.rs b/src/track.rs index 16bb0f4a..11e37cdc 100644 --- a/src/track.rs +++ b/src/track.rs @@ -352,7 +352,7 @@ pub trait TracksView: HasTracks + ScenesView + HasMidiIns + HasMidiOuts + HasTra /// Iterate over tracks with their corresponding sizes. fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { - let _editor_width = self.editor().map(|e|e.size.w()); + let _editor_width = self.editor().as_ref().map(|e|e.size.w()); let _active_track = self.selection().track(); let mut x = 0; let w = self.clips_size().w() as usize; diff --git a/tengri b/tengri index a8ee66a8..79f9cfcb 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit a8ee66a8cbdc33eb162b31b8da36b4ec203fd5ff +Subproject commit 79f9cfcbf93db626d031ab86abbbdf5c29d80a48