From 93fa3c26b430deaf785e49203beef646ceec9cb8 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Mon, 13 Jan 2025 20:23:10 +0100 Subject: [PATCH] app trait impls --- midi/src/midi_editor.rs | 6 +- sampler/src/has_sampler.rs | 22 ++++ sampler/src/lib.rs | 1 + tek/src/lib.rs | 264 ++++++++++++++++++------------------- 4 files changed, 156 insertions(+), 137 deletions(-) create mode 100644 sampler/src/has_sampler.rs diff --git a/midi/src/midi_editor.rs b/midi/src/midi_editor.rs index 4070858d..c50efcab 100644 --- a/midi/src/midi_editor.rs +++ b/midi/src/midi_editor.rs @@ -1,6 +1,10 @@ use crate::*; pub trait HasEditor { - fn editor (&self) -> &MidiEditor; + fn editor (&self) -> &Option; + fn editor_mut (&mut self) -> &Option; + fn is_editing (&self) -> bool { true } + fn editor_w (&self) -> usize { 0 } + fn editor_h (&self) -> usize { 0 } } #[macro_export] macro_rules! has_editor { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { diff --git a/sampler/src/has_sampler.rs b/sampler/src/has_sampler.rs new file mode 100644 index 00000000..6ae84abe --- /dev/null +++ b/sampler/src/has_sampler.rs @@ -0,0 +1,22 @@ +use crate::*; + +pub trait HasSampler { + fn sampler (&self) -> &Option; + fn sampler_mut (&mut self) -> &mut Option; + fn sample_index (&self) -> usize; + fn view_sample <'a> (&'a self, compact: bool) -> impl Content + 'a { + self.sampler().as_ref().map(|sampler|Max::y( + if compact { 0u16 } else { 5 }.into(), + Fill::x(sampler.viewer(self.sample_index())) + )) + } + fn view_sampler <'a> (&'a self, compact: bool, editor: &Option) -> impl Content + 'a { + self.sampler().as_ref().map(|sampler|Fixed::x( + if compact { 4u16 } else { 40 }.into(), + Push::y( + if compact { 1u16 } else { 0 }.into(), + editor.as_ref().map(|e|Fill::y(sampler.list(compact, e))) + ) + )) + } +} diff --git a/sampler/src/lib.rs b/sampler/src/lib.rs index df1d1f3e..5f6311f0 100644 --- a/sampler/src/lib.rs +++ b/sampler/src/lib.rs @@ -1,6 +1,7 @@ mod sampler; pub use self::sampler::*; mod sampler_tui; pub use self::sampler_tui::*; mod sampler_cmd; pub use self::sampler_cmd::*; +mod has_sampler; pub use self::has_sampler::*; pub(crate) use ::tek_jack::{*, jack::*}; pub(crate) use ::tek_midi::{*, midly::{*, live::*, num::*}}; diff --git a/tek/src/lib.rs b/tek/src/lib.rs index 3e7a0af1..ce620d5e 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -52,13 +52,38 @@ pub(crate) use std::sync::{Arc, RwLock}; pub perf: PerfModel, pub compact: bool, } -impl HasJack for App { fn jack (&self) -> &Arc> { &self.jack } } has_size!(|self: App|&self.size); has_clock!(|self: App|&self.clock); has_clips!(|self: App|self.pool.as_ref().expect("no clip pool").clips); -has_clock!(|self:Track|self.player.clock()); -has_player!(|self:Track|self.player); -has_editor!(|self: App|self.editor.as_ref().expect("no editor")); +//has_editor!(|self: App|self.editor.as_ref().expect("no editor")); +impl HasJack for App { + fn jack (&self) -> &Arc> { &self.jack } +} +impl HasSampler for App { + fn sampler (&self) -> &Option { &self.sampler } + fn sampler_mut (&mut self) -> &mut Option { &mut self.sampler } + fn sample_index (&self) -> usize { self.editor.as_ref().map(|e|e.note_point()).unwrap_or(0) } +} +impl HasEditor for App { + fn editor (&self) -> &Option { + &self.editor + } + fn editor_mut (&mut self) -> &Option { + &mut self.editor + } + fn is_editing (&self) -> bool { + self.editing.load(Relaxed) + } + fn editor_w (&self) -> usize { + let editor = self.editor.as_ref().expect("missing editor"); + (5 + (editor.time_len().get() / editor.time_zoom().get())) + .min(self.size.w().saturating_sub(20)) + .max(16) + } + fn editor_h (&self) -> usize { + 15 + } +} edn_provide!(u16: |self: App|{ ":sample-h" => if self.compact() { 0 } else { 5 }, ":samples-w" => if self.compact() { 4 } else { 11 }, @@ -68,28 +93,25 @@ edn_provide!(u16: |self: App|{ if w > 60 { 20 } else if w > 40 { 15 } else { 10 } } }); -edn_provide!(Color: |self: App| { _ => return None }); +edn_provide!(bool: |self: App| { _ => return None }); edn_provide!(usize: |self: App| { _ => return None }); edn_provide!(isize: |self: App| { _ => return None }); -edn_provide!(bool: |self: App| { _ => return None }); +edn_provide!(Color: |self: App| { _ => return None }); edn_provide!(Selection: |self: App| { _ => return None }); edn_provide!(Arc>: |self: App| { _ => return None }); edn_provide!(Option>>: |self: App| { _ => return None }); edn_provide!('a: Box + 'a>: |self: App|{ ":editor" => (&self.editor).boxed(), ":pool" => self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))).boxed(), - ":sample" => self.sample().boxed(), - ":sampler" => self.sampler().boxed(), + ":sample" => self.view_sample(self.is_editing()).boxed(), + ":sampler" => self.view_sampler(self.is_editing(), &self.editor).boxed(), ":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(), ":toolbar" => Fill::x(Fixed::y(2, Align::x(ClockView::new(true, &self.clock)))).boxed(), - ":tracks" => self.row(self.w(), 3, - self.track_header(), self.track_cells()).boxed(), - ":inputs" => self.row(self.w(), 3, - input_header(&self), input_cells(&self)).boxed(), - ":outputs" => self.row(self.w(), 3, - output_header(&self), output_cells(&self)).boxed(), + ":tracks" => self.row(self.w(), 3, self.track_header(), self.track_cells()).boxed(), + ":inputs" => self.row(self.w(), 3, self.input_header(), self.input_cells()).boxed(), + ":outputs" => self.row(self.w(), 3, self.output_header(), self.output_cells()).boxed(), ":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16, - self.scene_header(), self.scene_cells()).boxed(), + self.scene_header(), self.scene_cells(self.is_editing())).boxed(), }); render!(TuiOut: (self: App) => self.size.of(EdnView::from_source(self, self.edn.as_ref()))); handle!(TuiIn: |self: App, input| Ok(None)); @@ -184,8 +206,10 @@ pub trait HasSelection { /// Outputs of last device audio_outs: Vec>, /// Device chain - devices: Vec, + devices: Vec>, } +has_clock!(|self: Track|self.player.clock()); +has_player!(|self: Track|self.player); impl Track { const MIN_WIDTH: usize = 9; fn longest_name (tracks: &[Self]) -> usize { @@ -204,14 +228,12 @@ impl HasTracks for App { fn tracks (&self) -> &Vec { &self.tracks } fn tracks_mut (&mut self) -> &mut Vec { &mut self.tracks } } -pub trait HasTracks: HasSelection + HasClock + HasJack { +pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync { fn tracks (&self) -> &Vec; fn tracks_mut (&mut self) -> &mut Vec; - fn tracks_sizes ( - &self, - editing: bool, - bigger: usize - ) -> impl Iterator { + fn tracks_sizes <'a> (&'a self, editing: bool, bigger: usize) + -> impl Iterator + Send + Sync + 'a + { let mut x = 0; let active = match self.selected() { Selection::Track(t) if editing => Some(t), @@ -241,7 +263,7 @@ pub trait HasTracks: HasSelection + HasClock + HasJack { )).boxed()).into() } fn track_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - let iter = ||self.tracks_with_sizes(); + let iter = ||self.tracks_sizes(self.is_editing(), self.editor_w()); (move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| { let name = Push::x(1, &track.name); let color = track.color; @@ -256,7 +278,7 @@ pub trait HasTracks: HasSelection + HasClock + HasJack { })).boxed()).into() } } -trait Device {} +trait Device: Send + Sync + std::fmt::Debug {} impl Device for Sampler {} impl Device for Plugin {} #[derive(Debug, Default)] struct Scene { @@ -321,15 +343,12 @@ impl HasScenes for App { fn scenes (&self) -> &Vec { &self.scenes } fn scenes_mut (&mut self) -> &mut Vec { &mut self.scenes } } -pub trait HasScenes: HasSelection { +pub trait HasScenes: HasSelection + HasEditor + Send + Sync { fn scenes (&self) -> &Vec; fn scenes_mut (&mut self) -> &mut Vec; - fn scenes_sizes( - &self, - editing: bool, - scene_height: usize, - scene_larger: usize, - ) -> impl Iterator { + fn scenes_sizes (&self, editing: bool, height: usize, larger: usize,) + -> impl Iterator + Send + Sync + { let mut y = 0; let (selected_track, selected_scene) = match self.selected() { Selection::Clip(t, s) => (Some(t), Some(s)), @@ -337,7 +356,7 @@ pub trait HasScenes: HasSelection { }; self.scenes().iter().enumerate().map(move|(s, scene)|{ let active = editing && selected_track.is_some() && selected_scene == Some(&s); - let height = if active { scene_larger } else { scene_height }; + let height = if active { larger } else { height }; let data = (s, scene, y, y + height); y += height; data @@ -353,13 +372,14 @@ pub trait HasScenes: HasSelection { self.selected().scene().and_then(|s|self.scenes_mut().get_mut(s)) } fn scene_del (&mut self, index: usize) { - todo!("delete scene"); + self.selected().scene().and_then(|s|Some(self.scenes_mut().remove(index))); } fn scene_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (||{ + (move||{ let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); - let selected = self.selected().scene(); - Fill::y(Align::c(Map::new(||self.scenes_with_sizes(2), move|(_, scene, y1, y2), i| { + let selected = self.selected().scene(); + let iter = ||self.scenes_sizes(self.is_editing(), 2, 15); + Fill::y(Align::c(Map::new(iter, move|(_, scene, y1, y2), i| { let h = (y2 - y1) as u16; let name = format!("🭬{}", &scene.name); let color = scene.color; @@ -431,27 +451,8 @@ impl App { } fn compact (&self) -> bool { false } fn editor (&self) -> impl Content + '_ { &self.editor } - fn w (&self) -> u16 { self.tracks_with_sizes().last().map(|x|x.3 as u16).unwrap_or(0) } + fn w (&self) -> u16 { self.tracks_sizes(self.is_editing(), self.editor_w()).last().map(|x|x.3 as u16).unwrap_or(0) } fn pool (&self) -> impl Content + use<'_> { self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) } - fn sample <'a> (&'a self) -> impl Content + 'a { - let compact = self.is_editing(); - if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) { - let note_pt = editor.note_point(); - let sample_h = if compact { 0 } else { 5 }; - return Some(Max::y(sample_h, Fill::x(sampler.viewer(note_pt)))) - } - None - } - fn sampler (&self) -> impl Content + use<'_> { - let compact = self.is_editing(); - if let (Some(editor), Some(sampler)) = (&self.editor, &self.sampler) { - let note_pt = editor.note_point(); - let w = if compact { 4 } else { 40 }; - let y = if compact { 1 } else { 0 }; - return Some(Fixed::x(w, Push::y(y, Fill::y(sampler.list(compact, editor))))) - } - None - } fn row <'a> ( &'a self, w: u16, h: u16, a: impl Content + 'a, b: impl Content + 'a ) -> impl Content + 'a { @@ -460,27 +461,69 @@ impl App { Fill::x(Align::c(Fixed::xy(w, h, b))) )) } - fn tracks_with_sizes (&self) -> impl Iterator { - self.tracks_sizes(self.is_editing(), self.editor_w()) - } - fn scenes_with_sizes (&self, h: usize) -> impl Iterator { - self.scenes_sizes(self.is_editing(), 2, 15) - } fn is_editing (&self) -> bool { self.editing.load(Relaxed) } - fn editor_w (&self) -> usize { - let editor = self.editor.as_ref().expect("missing editor"); - (5 + (editor.time_len().get() / editor.time_zoom().get())) - .min(self.size.w().saturating_sub(20)) - .max(16) - } fn sidebar_w (&self) -> u16 { let w = self.size.w(); let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; let w = if self.is_editing() { 8 } else { w }; w } + fn input_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + let fg = TuiTheme::g(224); + let bg = TuiTheme::g(64); + (move||Bsp::s(help_tag("midi ", "I", "ns"), self.midi_ins.get(0).map(|inp|Bsp::s( + Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(inp.name.clone())))), + inp.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, + Tui::fg_bg(fg, bg, connect.info()))))), + ))).boxed()).into() + } + fn input_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (move||Align::x(Map::new(||self.tracks_sizes(self.is_editing(), self.editor_w()), move|(_, track, x1, x2), i| { + let w = (x2 - x1) as u16; + let color: ItemPalette = track.color.dark.into(); + map_east(x1 as u16, w, Fixed::x(w, cell(color, Bsp::n( + Self::rec_mon(color.base.rgb, false, false), + phat_hi(color.base.rgb, color.dark.rgb) + )))) + })).boxed()).into() + } + fn rec_mon (bg: Color, rec: bool, mon: bool) -> impl Content { + row!( + Tui::fg_bg(if rec { Color::Red } else { bg }, bg, "▐"), + Tui::fg_bg(if rec { Color::White } else { Color::Rgb(0,0,0) }, bg, "REC"), + Tui::fg_bg(if rec { Color::White } else { bg }, bg, "▐"), + Tui::fg_bg(if mon { Color::White } else { Color::Rgb(0,0,0) }, bg, "MON"), + Tui::fg_bg(if mon { Color::White } else { bg }, bg, "▌"), + ) + } + fn output_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + let fg = TuiTheme::g(224); + let bg = TuiTheme::g(64); + (move||Bsp::s(help_tag("midi ", "O", "uts"), self.midi_outs.get(0).map(|out|Bsp::s( + Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(out.name.clone())))), + out.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, + Tui::fg_bg(fg, bg, connect.info()))))), + ))).boxed()).into() + } + fn output_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (move||Align::x(Map::new(||self.tracks_sizes(self.is_editing(), self.editor_w()), move|(_, track, x1, x2), i| { + let w = (x2 - x1) as u16; + let color: ItemPalette = track.color.dark.into(); + map_east(x1 as u16, w, Fixed::x(w, cell(color, Bsp::n( + Self::mute_solo(color.base.rgb, false, false), + phat_hi(color.dark.rgb, color.darker.rgb) + )))) + })).boxed()).into() + } + fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content { + row!( + Tui::fg_bg(if mute { Color::White } else { Color::Rgb(0,0,0) }, bg, "MUTE"), + Tui::fg_bg(if mute { Color::White } else { bg }, bg, "▐"), + Tui::fg_bg(if solo { Color::White } else { Color::Rgb(0,0,0) }, bg, "SOLO"), + ) + } } #[derive(Clone, Debug)] pub enum AppCommand { Clear, @@ -777,44 +820,6 @@ fn help_tag <'a> (before: &'a str, key: &'a str, after: &'a str) -> impl Content let hi = TuiTheme::orange(); Tui::bold(true, row!(Tui::fg(lo, before), Tui::fg(hi, key), Tui::fg(lo, after))) } -fn input_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - let fg = TuiTheme::g(224); - let bg = TuiTheme::g(64); - (move||Bsp::s(help_tag("midi ", "I", "ns"), state.midi_ins.get(0).map(|inp|Bsp::s( - Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(inp.name.clone())))), - inp.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, - Tui::fg_bg(fg, bg, connect.info()))))), - ))).boxed()).into() -} -fn input_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - (move||Align::x(Map::new(||state.tracks_with_sizes(), move|(_, track, x1, x2), i| { - let w = (x2 - x1) as u16; - let color: ItemPalette = track.color.dark.into(); - map_east(x1 as u16, w, Fixed::x(w, cell(color, Bsp::n( - rec_mon(color.base.rgb, false, false), - phat_hi(color.base.rgb, color.dark.rgb) - )))) - })).boxed()).into() -} -fn output_header <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - let fg = TuiTheme::g(224); - let bg = TuiTheme::g(64); - (move||Bsp::s(help_tag("midi ", "O", "uts"), state.midi_outs.get(0).map(|out|Bsp::s( - Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(out.name.clone())))), - out.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, - Tui::fg_bg(fg, bg, connect.info()))))), - ))).boxed()).into() -} -fn output_cells <'a> (state: &'a App) -> BoxThunk<'a, TuiOut> { - (move||Align::x(Map::new(||state.tracks_with_sizes(), move|(_, track, x1, x2), i| { - let w = (x2 - x1) as u16; - let color: ItemPalette = track.color.dark.into(); - map_east(x1 as u16, w, Fixed::x(w, cell(color, Bsp::n( - mute_solo(color.base.rgb, false, false), - phat_hi(color.dark.rgb, color.darker.rgb) - )))) - })).boxed()).into() -} fn cell_clip <'a> ( scene: &'a Scene, index: usize, track: &'a Track, w: u16, h: u16 ) -> impl Content + use<'a> { @@ -833,36 +838,24 @@ fn cell_clip <'a> ( Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w])))); })) } -fn rec_mon (bg: Color, rec: bool, mon: bool) -> impl Content { - row!( - Tui::fg_bg(if rec { Color::Red } else { bg }, bg, "▐"), - Tui::fg_bg(if rec { Color::White } else { Color::Rgb(0,0,0) }, bg, "REC"), - Tui::fg_bg(if rec { Color::White } else { bg }, bg, "▐"), - Tui::fg_bg(if mon { Color::White } else { Color::Rgb(0,0,0) }, bg, "MON"), - Tui::fg_bg(if mon { Color::White } else { bg }, bg, "▌"), - ) -} -fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content { - row!( - Tui::fg_bg(if mute { Color::White } else { Color::Rgb(0,0,0) }, bg, "MUTE"), - Tui::fg_bg(if mute { Color::White } else { bg }, bg, "▐"), - Tui::fg_bg(if solo { Color::White } else { Color::Rgb(0,0,0) }, bg, "SOLO"), - ) -} fn cell > (color: ItemPalette, field: T) -> impl Content { Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field)) } -impl Arrangement for App {} -pub trait Arrangement: HasTracks + HasScenes + HasSelection + HasClock + HasJack { - fn track_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut Track> - { +impl Arrangement for T where T: + HasEditor + HasTracks + HasScenes + HasSelection + HasClock + HasJack {} +pub trait Arrangement: HasEditor + HasTracks + HasScenes + HasSelection + HasClock + HasJack { + fn track_add ( + &mut self, + name: Option<&str>, + color: Option + ) -> Usually<&mut Track> { let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into()); let track = Track { width: (name.len() + 2).max(9), color: color.unwrap_or_else(ItemPalette::random), player: MidiPlayer::from(self.clock()), name, + ..Default::default() }; self.tracks_mut().push(track); let len = self.tracks().len(); @@ -876,8 +869,8 @@ pub trait Arrangement: HasTracks + HasScenes + HasSelection + HasClock + HasJack } fn tracks_add ( &mut self, - count: usize, - width: usize, + count: usize, + width: usize, midi_from: &[PortConnection], midi_to: &[PortConnection], ) -> Usually<()> { @@ -923,12 +916,11 @@ pub trait Arrangement: HasTracks + HasScenes + HasSelection + HasClock + HasJack } Ok(()) } - fn scene_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - let editing = self.is_editing(); - let tracks = move||self.tracks_with_sizes(); - let scenes = ||self.scenes_with_sizes(2); - let selected_track = self.selected.track(); - let selected_scene = self.selected.scene(); + fn scene_cells <'a> (&'a self, editing: bool) -> BoxThunk<'a, TuiOut> { + let tracks = move||self.tracks_sizes(self.is_editing(), self.editor_w()); + let scenes = ||self.scenes_sizes(self.is_editing(), 2, 15); + let selected_track = self.selected().track(); + let selected_scene = self.selected().scene(); (move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| { let w = (x2 - x1) as u16; let color: ItemPalette = track.color.dark.into(); @@ -944,7 +936,7 @@ pub trait Arrangement: HasTracks + HasScenes + HasSelection + HasClock + HasJack }; let last = last_color.read().unwrap().clone(); let active = editing && selected_scene == Some(s) && selected_track == Some(t); - let editor = Thunk::new(||&self.editor); + let editor = Thunk::new(||self.editor()); let cell = Thunk::new(move||phat_sel_3( selected_track == Some(t) && selected_scene == Some(s), Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),