From 9ae35830c376a602675eb46ab0ceb4750fa58c85 Mon Sep 17 00:00:00 2001 From: okay stopped screaming Date: Mon, 23 Feb 2026 18:40:30 +0200 Subject: [PATCH] wip: removing Has trait --- Justfile | 6 +- app/tek.rs | 87 +---- app/tek_impls.rs | 866 ++++++++++++++++++++++++++--------------------- app/tek_trait.rs | 99 ++---- dizzle | 2 +- 5 files changed, 520 insertions(+), 540 deletions(-) diff --git a/Justfile b/Justfile index 0d60bdc7..ebb68b1c 100644 --- a/Justfile +++ b/Justfile @@ -4,6 +4,9 @@ export RUST_BACKTRACE := "1" default +ARGS="new": target/debug/tek {{ARGS}} +doc +ARGS="": + cargo doc --open -j4 --document-private-items {{ARGS}} + cloc: for src in {cli,edn/src,input/src,jack/src,midi/src,output/src,plugin/src,sampler/src,tek/src,time/src,tui/src}; do echo; echo $src; cloc --quiet $src; done @@ -43,9 +46,6 @@ run-init: prof: CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -- -doc: - cargo doc -j4 --workspace --document-private-items - release := "reset && cargo run --release --" release: {{release}} diff --git a/app/tek.rs b/app/tek.rs index 1b2b1a73..8cd455fc 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -384,12 +384,12 @@ pub fn device_kinds () -> &'static [&'static str] { ] } -impl>> HasDevices for T { +impl> + AsMut>> HasDevices for T { fn devices (&self) -> &Vec { - self.get() + self.as_ref() } fn devices_mut (&mut self) -> &mut Vec { - self.get_mut() + self.as_mut() } } @@ -1127,26 +1127,6 @@ mod view { Fixed::XY(20, 1 + ins, frame.enclose(Fixed::XY(20, 1 + ins, field))) } - pub fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Content + 'a { - Align::x(Tui::bg(Reset, Map::new(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - let width = (x2 - x1) as u16; - map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg( - track.color.lightest.rgb, - track.color.base.rgb, - callback(index, track))))}))) - } - - pub fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Content + 'a { - per_track_top(tracks, move|index, track|Fill::Y(Align::y(callback(index, track)))) - } - pub fn io_ports <'a, T: PortsSizes<'a>> ( fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a ) -> impl Content + 'a { @@ -1159,67 +1139,6 @@ mod view { Fill::Y(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, &connect.info))))))))) } - - #[cfg(feature = "lv2")] pub fn draw_header (state: &Lv2, to: &mut TuiOut, x: u16, y: u16, w: u16) { - let style = Style::default().gray(); - let label1 = format!(" {}", state.name); - to.blit(&label1, x + 1, y, Some(style.white().bold())); - if let Some(ref path) = state.path { - let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); - to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); - } - //Ok(Rect { x, y, width: w, height: 1 }) - } - - pub fn draw_sample ( - to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool - ) -> Usually { - let style = if focus { Style::default().green() } else { Style::default() }; - if focus { - to.blit(&"🬴", x+1, y, Some(style.bold())); - } - let label1 = format!("{:3} {:12}", - note.map(|n|n.to_string()).unwrap_or(String::default()), - sample.name); - let label2 = format!("{:>6} {:>6} +0.0", - sample.start, - sample.end); - to.blit(&label1, x+2, y, Some(style.bold())); - to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); - Ok(label1.len() + label2.len() + 4) - } -} - -pub(crate) fn sampler_jack_process ( - state: &mut Sampler, _: &Client, scope: &ProcessScope -) -> Control { - if let Some(midi_in) = &state.midi_in { - for midi in midi_in.port().iter(scope) { - sampler_midi_in(&state.samples, &state.voices, midi) - } - } - state.process_audio_out(scope); - state.process_audio_in(scope); - Control::Continue -} - -/// Create [Voice]s from [Sample]s in response to MIDI input. -pub(crate) fn sampler_midi_in ( - samples: &SampleKit<128>, voices: &Arc>>, RawMidi { time, bytes }: RawMidi -) { - if let Ok(LiveEvent::Midi { message, .. }) = LiveEvent::parse(bytes) { - match message { - MidiMessage::NoteOn { ref key, ref vel } => { - if let Some(sample) = samples.get(key.as_int() as usize) { - voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); - } - }, - MidiMessage::Controller { controller: _, value: _ } => { - // TODO - } - _ => {} - } - } } #[cfg(test)] mod test_view_meter { diff --git a/app/tek_impls.rs b/app/tek_impls.rs index 83fc76fe..12987408 100644 --- a/app/tek_impls.rs +++ b/app/tek_impls.rs @@ -2,350 +2,358 @@ use crate::*; use std::fmt::Write; use std::path::PathBuf; -impl Draw for App { - fn draw (&self, to: &mut TuiOut) { - if let Some(e) = self.error.read().unwrap().as_ref() { - to.place_at(to.area(), e); - } - for (index, dsl) in self.mode.view.iter().enumerate() { - if let Err(e) = self.understand(to, dsl) { - *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); - break; +mod app { + use crate::*; + + impl Draw for App { + fn draw (&self, to: &mut TuiOut) { + if let Some(e) = self.error.read().unwrap().as_ref() { + to.place_at(to.area(), e); + } + for (index, dsl) in self.mode.view.iter().enumerate() { + if let Err(e) = self.understand(to, dsl) { + *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); + break; + } } } } -} -handle!(TuiIn: |self: App, input|{ - let commands = collect_commands(self, input)?; - let history = execute_commands(self, commands)?; - self.history.extend(history.into_iter()); - Ok(None) -}); - -impl<'a> Namespace<'a, AppCommand> for App { - symbols!('a |app| -> AppCommand { - "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, - "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, - "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, - "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, - "confirm" => AppCommand::Confirm, - "cancel" => AppCommand::Cancel, + handle!(TuiIn: |self: App, input|{ + let commands = collect_commands(self, input)?; + let history = execute_commands(self, commands)?; + self.history.extend(history.into_iter()); + Ok(None) }); -} -impl Understand for App { - fn understand_expr <'a> (&'a self, to: &mut TuiOut, lang: &'a impl Expression) -> Usually<()> { - app_understand_expr(self, to, lang) + impl<'a> Namespace<'a, AppCommand> for App { + symbols!('a |app| -> AppCommand { + "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, + "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, + "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, + "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, + "confirm" => AppCommand::Confirm, + "cancel" => AppCommand::Cancel, + }); } - fn understand_word <'a> (&'a self, to: &mut TuiOut, lang: &'a impl Expression) -> Usually<()> { - app_understand_word(self, to, lang) - } -} -fn app_understand_expr (state: &App, to: &mut TuiOut, lang: &impl Expression) -> Usually<()> { - if evaluate_output_expression(state, to, lang)? - || evaluate_output_expression_tui(state, to, lang)? { - Ok(()) - } else { - Err(format!("App::understand_expr: unexpected: {lang:?}").into()) + impl Understand for App { + fn understand_expr <'a> (&'a self, to: &mut TuiOut, lang: &'a impl Expression) -> Usually<()> { + app_understand_expr(self, to, lang) + } + fn understand_word <'a> (&'a self, to: &mut TuiOut, lang: &'a impl Expression) -> Usually<()> { + app_understand_word(self, to, lang) + } } -} -fn app_understand_word (state: &App, to: &mut TuiOut, dsl: &impl Expression) -> Usually<()> { - let mut frags = dsl.src()?.unwrap().split("/"); - match frags.next() { - Some(":logo") => to.place(&view_logo()), - Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")), - Some(":meters") => match frags.next() { - Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))), - Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))), - _ => panic!() - }, - Some(":tracks") => match frags.next() { - None => to.place(&"TODO tracks"), - Some("names") => to.place(&state.project.view_track_names(state.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))), - Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))), - Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))), - Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))), - _ => panic!() - }, - Some(":scenes") => match frags.next() { - None => to.place(&"TODO scenes"), - Some(":scenes/names") => to.place(&"TODO Scene Names"), - _ => panic!() - }, - Some(":editor") => to.place(&"TODO Editor"), - Some(":dialog") => match frags.next() { - Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &state.dialog { - let items = items.clone(); - let selected = selected; - Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + fn app_understand_expr (state: &App, to: &mut TuiOut, lang: &impl Expression) -> Usually<()> { + if evaluate_output_expression(state, to, lang)? + || evaluate_output_expression_tui(state, to, lang)? { + Ok(()) + } else { + Err(format!("App::understand_expr: unexpected: {lang:?}").into()) + } + } + + fn app_understand_word (state: &App, to: &mut TuiOut, dsl: &impl Expression) -> Usually<()> { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + Some(":logo") => to.place(&view_logo()), + Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")), + Some(":meters") => match frags.next() { + Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))), + Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))), + _ => panic!() + }, + Some(":tracks") => match frags.next() { + None => to.place(&"TODO tracks"), + Some("names") => to.place(&state.project.view_track_names(state.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))), + Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))), + Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))), + Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))), + _ => panic!() + }, + Some(":scenes") => match frags.next() { + None => to.place(&"TODO scenes"), + Some(":scenes/names") => to.place(&"TODO Scene Names"), + _ => panic!() + }, + Some(":editor") => to.place(&"TODO Editor"), + Some(":dialog") => match frags.next() { + Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &state.dialog { + let items = items.clone(); + let selected = selected; + Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + to.place(&Push::Y((2 * index) as u16, + Tui::fg_bg( + if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, + if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, + Fixed::Y(2, Align::n(Fill::X(item))) + ))); + } + }))) + } else { + None + }), + _ => unimplemented!("App::understand_word: {dsl:?} ({frags:?})"), + }, + Some(":templates") => to.place(&{ + let modes = state.config.modes.clone(); + let height = (modes.read().unwrap().len() * 2) as u16; + Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut TuiOut|{ + for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { + let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; + let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); + let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); + let fg1 = Rgb(224, 192, 128); + let fg2 = Rgb(224, 128, 32); + let field_name = Fill::X(Align::w(Tui::fg(fg1, name))); + let field_id = Fill::X(Align::e(Tui::fg(fg2, id))); + let field_info = Fill::X(Align::w(info)); to.place(&Push::Y((2 * index) as u16, - Tui::fg_bg( - if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, - if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - Fixed::Y(2, Align::n(Fill::X(item))) - ))); + Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s( + Bsp::a(field_name, field_id), field_info)))))); } }))) - } else { - None }), - _ => unimplemented!("App::understand_word: {dsl:?} ({frags:?})"), - }, - Some(":templates") => to.place(&{ - let modes = state.config.modes.clone(); - let height = (modes.read().unwrap().len() * 2) as u16; - Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut TuiOut|{ - for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { - let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; - let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); - let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); - let fg1 = Rgb(224, 192, 128); - let fg2 = Rgb(224, 128, 32); - let field_name = Fill::X(Align::w(Tui::fg(fg1, name))); - let field_id = Fill::X(Align::e(Tui::fg(fg2, id))); - let field_info = Fill::X(Align::w(info)); + Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut TuiOut|{ + let fg = Rgb(224, 192, 128); + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; to.place(&Push::Y((2 * index) as u16, - Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s( - Bsp::a(field_name, field_id), field_info)))))); + &Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); } - }))) - }), - Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut TuiOut|{ - let fg = Rgb(224, 192, 128); - for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - to.place(&Push::Y((2 * index) as u16, - &Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); - } - })))), - Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(ItemColor::default(), - match state.dialog.browser_target().unwrap() { - BrowseTarget::SaveProject => "Save project:", - BrowseTarget::LoadProject => "Load project:", - BrowseTarget::ImportSample(_) => "Import sample:", - BrowseTarget::ExportSample(_) => "Export sample:", - BrowseTarget::ImportClip(_) => "Import clip:", - BrowseTarget::ExportClip(_) => "Export clip:", - }, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), Repeat::X("🭻")))))))), - Some(":device") => { - let selected = state.dialog.device_kind().unwrap(); - to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, - move||device_kinds().iter(), - move|_label: &&'static str, i|{ - let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let lb = if i == selected { "[ " } else { " " }; - let rb = if i == selected { " ]" } else { " " }; - Fill::X(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) - }, - Some(":debug") => to.place(&Fixed::Y(1, format!("[{:?}]", to.area()))), - Some(_) => { - let views = state.config.views.read().unwrap(); - if let Some(dsl) = views.get(dsl.src()?.unwrap()) { - let dsl = dsl.clone(); - std::mem::drop(views); - state.understand(to, &dsl)? - } else { - unimplemented!("{dsl:?}"); - } - }, - _ => unreachable!() - } - Ok(()) -} - -impl App { - /// Update memoized render of clock values. - /// ``` - /// tek::App::default().update_clock(); - /// ``` - pub fn update_clock (&self) { - ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } - - /// Set modal dialog. - /// - /// ``` - /// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome()); - /// ``` - pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { - std::mem::swap(&mut self.dialog, &mut dialog); - dialog - } - - /// FIXME: generalize. Set picked device in device pick dialog. - /// - /// ``` - /// tek::App::default().device_pick(0); - /// ``` - pub fn device_pick (&mut self, index: usize) { - self.dialog = Dialog::Device(index); - } - - /// FIXME: generalize. Add device to current track. - pub fn add_device (&mut self, index: usize) -> Usually<()> { - match index { - 0 => { - let name = self.jack.with_client(|c|c.name().to_string()); - let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); - let track = self.track().expect("no active track"); - let port = format!("{}/Sampler", &track.name); - let connect = Connect::exact(format!("{name}:{midi}")); - let sampler = if let Ok(sampler) = Sampler::new( - &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] - ) { - self.dialog = Dialog::None; - Device::Sampler(sampler) - } else { - self.dialog = Dialog::Message("Failed to add device.".into()); - return Err("failed to add device".into()) - }; - let track = self.track_mut().expect("no active track"); - track.devices.push(sampler); - Ok(()) - }, - 1 => { - todo!(); - //Ok(()) - }, - _ => unreachable!(), - } - } - - /// Return reference to content browser if open. - /// - /// ``` - /// assert_eq!(tek::App::default().browser(), None); - /// ``` - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } - } - - /// 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.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(editor) = &mut self.project.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - //Some(clip) - } else { - //None - } - } else if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() - { - // Remove clip from arrangement when exiting empty clip editor - let mut swapped = None; - if clip.read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - self.pool.delete_clip(&clip.read().unwrap()); - } - } - } -} - -impl Dialog { - /// ``` - /// let _ = tek::Dialog::welcome(); - /// ``` - pub fn welcome () -> Self { - Self::Menu(1, MenuItems([ - MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), - MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ - app.dialog = Dialog::None; - app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); })))), - MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), - ].into())) + Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(ItemColor::default(), + match state.dialog.browser_target().unwrap() { + BrowseTarget::SaveProject => "Save project:", + BrowseTarget::LoadProject => "Load project:", + BrowseTarget::ImportSample(_) => "Import sample:", + BrowseTarget::ExportSample(_) => "Export sample:", + BrowseTarget::ImportClip(_) => "Import clip:", + BrowseTarget::ExportClip(_) => "Export clip:", + }, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), Repeat::X("🭻")))))))), + Some(":device") => { + let selected = state.dialog.device_kind().unwrap(); + to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, + move||device_kinds().iter(), + move|_label: &&'static str, i|{ + let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let lb = if i == selected { "[ " } else { " " }; + let rb = if i == selected { " ]" } else { " " }; + Fill::X(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) + }, + Some(":debug") => to.place(&Fixed::Y(1, format!("[{:?}]", to.area()))), + Some(_) => { + let views = state.config.views.read().unwrap(); + if let Some(dsl) = views.get(dsl.src()?.unwrap()) { + let dsl = dsl.clone(); + std::mem::drop(views); + state.understand(to, &dsl)? + } else { + unimplemented!("{dsl:?}"); + } + }, + _ => unreachable!() + } + Ok(()) } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_selected(); - /// ``` - pub fn menu_selected (&self) -> Option { - if let Self::Menu(selected, _) = self { Some(*selected) } else { None } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_next(); - /// ``` - pub fn menu_next (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), - _ => Self::None + + impl App { + /// Update memoized render of clock values. + /// ``` + /// tek::App::default().update_clock(); + /// ``` + pub fn update_clock (&self) { + ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } + + /// Set modal dialog. + /// + /// ``` + /// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome()); + /// ``` + pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog + } + + /// FIXME: generalize. Set picked device in device pick dialog. + /// + /// ``` + /// tek::App::default().device_pick(0); + /// ``` + pub fn device_pick (&mut self, index: usize) { + self.dialog = Dialog::Device(index); + } + + /// FIXME: generalize. Add device to current track. + pub fn add_device (&mut self, index: usize) -> Usually<()> { + match index { + 0 => { + let name = self.jack.with_client(|c|c.name().to_string()); + let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); + let track = self.track().expect("no active track"); + let port = format!("{}/Sampler", &track.name); + let connect = Connect::exact(format!("{name}:{midi}")); + let sampler = if let Ok(sampler) = Sampler::new( + &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] + ) { + self.dialog = Dialog::None; + Device::Sampler(sampler) + } else { + self.dialog = Dialog::Message("Failed to add device.".into()); + return Err("failed to add device".into()) + }; + let track = self.track_mut().expect("no active track"); + track.devices.push(sampler); + Ok(()) + }, + 1 => { + todo!(); + //Ok(()) + }, + _ => unreachable!(), + } + } + + /// Return reference to content browser if open. + /// + /// ``` + /// assert_eq!(tek::App::default().browser(), None); + /// ``` + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } + + /// 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.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(editor) = &mut self.project.editor { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + //Some(clip) + } else { + //None + } + } else if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && let Some(clip) = slot.as_mut() + { + // Remove clip from arrangement when exiting empty clip editor + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + self.pool.delete_clip(&clip.read().unwrap()); + } + } } } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().menu_prev(); - /// ``` - pub fn menu_prev (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), - _ => Self::None +} + +mod dialog { + use crate::*; + + impl Dialog { + /// ``` + /// let _ = tek::Dialog::welcome(); + /// ``` + pub fn welcome () -> Self { + Self::Menu(1, MenuItems([ + MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ + app.dialog = Dialog::None; + app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); + })))), + MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), + ].into())) } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_selected(); + /// ``` + pub fn menu_selected (&self) -> Option { + if let Self::Menu(selected, _) = self { Some(*selected) } else { None } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_next(); + /// ``` + pub fn menu_next (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().menu_prev(); + /// ``` + pub fn menu_prev (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind(); + /// ``` + pub fn device_kind (&self) -> Option { + if let Self::Device(index) = self { Some(*index) } else { None } + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind_next(); + /// ``` + pub fn device_kind_next (&self) -> Option { + self.device_kind().map(|index|(index + 1) % device_kinds().len()) + } + /// FIXME: generalize + /// ``` + /// let _ = tek::Dialog::welcome().device_kind_prev(); + /// ``` + pub fn device_kind_prev (&self) -> Option { + self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) + } + /// FIXME: implement + pub fn message (&self) -> Option<&str> { todo!() } + /// FIXME: implement + pub fn browser (&self) -> Option<&Arc> { todo!() } + /// FIXME: implement + pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind(); - /// ``` - pub fn device_kind (&self) -> Option { - if let Self::Device(index) = self { Some(*index) } else { None } - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind_next(); - /// ``` - pub fn device_kind_next (&self) -> Option { - self.device_kind().map(|index|(index + 1) % device_kinds().len()) - } - /// FIXME: generalize - /// ``` - /// let _ = tek::Dialog::welcome().device_kind_prev(); - /// ``` - pub fn device_kind_prev (&self) -> Option { - self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) - } - /// FIXME: implement - pub fn message (&self) -> Option<&str> { todo!() } - /// FIXME: implement - pub fn browser (&self) -> Option<&Arc> { todo!() } - /// FIXME: implement - pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } } #[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} @@ -353,26 +361,14 @@ impl Dialog { mod arrange { use crate::*; - has!(Jack<'static>: |self: Arrangement|self.jack); - has!(Measure: |self: Arrangement|self.size); + impl_has!(Jack<'static>: |self: Arrangement|self.jack); + impl_has!(Measure: |self: Arrangement|self.size); - #[cfg(feature = "editor")] has!(Option: |self: Arrangement|self.editor); - #[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_ins); - #[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_outs); - #[cfg(feature = "clock")] has!(Clock: |self: Arrangement|self.clock); - #[cfg(feature = "select")] has!(Selection: |self: Arrangement|self.selection); - - #[cfg(feature = "track")] impl TracksView for Arrangement {} // -> to auto? - - #[cfg(all(feature = "select", feature = "track"))] has!(Vec: |self: Arrangement|self.tracks); - #[cfg(all(feature = "select", feature = "track"))] maybe_has!(Track: |self: Arrangement| - { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; - { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); - - #[cfg(all(feature = "select", feature = "scene"))] has!(Vec: |self: Arrangement|self.scenes); - #[cfg(all(feature = "select", feature = "scene"))] maybe_has!(Scene: |self: Arrangement| - { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; - { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); + #[cfg(feature = "editor")] impl_has!(Option: |self: Arrangement|self.editor); + #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement|self.midi_ins); + #[cfg(feature = "port")] impl_has!(Vec: |self: Arrangement|self.midi_outs); + #[cfg(feature = "clock")] impl_has!(Clock: |self: Arrangement|self.clock); + #[cfg(feature = "select")] impl_has!(Selection: |self: Arrangement|self.selection); #[cfg(feature = "select")] impl Arrangement { #[cfg(feature = "clip")] fn selected_clip (&self) -> Option { todo!() } @@ -748,10 +744,7 @@ mod browse { //take!(ClipCommand |state: Arrangement, iter|state.selected_clip().as_ref() //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); -impl> HasClock for T { - fn clock (&self) -> &Clock { self.get() } - fn clock_mut (&mut self) -> &mut Clock { self.get_mut() } -} +impl + AsMut> HasClock for T {} impl std::fmt::Debug for Clock { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { @@ -1030,9 +1023,9 @@ impl ClockView { } -impl>> HasEditor for T {} +impl>> HasEditor for T {} -has!(Measure: |self: MidiEditor|self.size); +impl_has!(Measure: |self: MidiEditor|self.size); impl std::fmt::Debug for MidiEditor { @@ -1200,7 +1193,7 @@ impl HasContent for MidiEditor { fn content (&self) -> impl Content { self.autoscroll(); /*self.autozoom();*/ self.size.of(&self.mode) } } -has!(Measure:|self:PianoHorizontal|self.size); +impl_has!(Measure:|self:PianoHorizontal|self.size); impl PianoHorizontal { pub fn new (clip: Option<&Arc>>) -> Self { @@ -1634,7 +1627,7 @@ impl Selection { } -impl> HasSelection for T {} +impl + AsMut> HasSelection for T {} /// Default is always empty map regardless if `E` and `C` implement [Default]. impl Default for Bind { @@ -1733,43 +1726,24 @@ impl> RegisterPorts for J { } } -#[cfg(feature = "scene")] impl AddScene for T {} -#[cfg(feature = "scene")] impl> + Send + Sync> HasScene for T {} -#[cfg(feature = "scene")] impl> + Send + Sync> HasScenes for T {} -#[cfg(feature = "scene")] impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } -#[cfg(feature = "scene")] impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } -#[cfg(feature = "scene")] has!(Vec: |self: App|self.project.scenes); -#[cfg(feature = "scene")] maybe_has!(Scene: |self: App| - { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); +#[cfg(feature = "clock")] impl_has!(Clock: |self: App|self.project.clock); +#[cfg(feature = "clock")] impl_has!(Clock: |self: Track|self.sequencer.clock); +#[cfg(feature = "clock")] impl_has!(Clock: |self: Sequencer|self.clock); -#[cfg(feature = "track")] impl> + Send + Sync> HasTracks for T {} -#[cfg(feature = "track")] impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } -#[cfg(feature = "track")] impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } -#[cfg(feature = "track")] has!(Vec: |self: App|self.project.tracks); -#[cfg(feature = "track")] maybe_has!(Track: |self: App| - { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); - -#[cfg(feature = "clock")] has!(Clock: |self: App|self.project.clock); -#[cfg(feature = "clock")] has!(Clock: |self: Track|self.sequencer.clock); -#[cfg(feature = "clock")] has!(Clock: |self: Sequencer|self.clock); - -#[cfg(feature = "port")] has!(Vec: |self: App|self.project.midi_ins); -#[cfg(feature = "port")] has!(Vec: |self: App|self.project.midi_outs); -#[cfg(feature = "port")] has!(Vec: |self: Sequencer|self.midi_ins); -#[cfg(feature = "port")] has!(Vec: |self: Sequencer|self.midi_outs); +#[cfg(feature = "port")] impl_has!(Vec: |self: App|self.project.midi_ins); +#[cfg(feature = "port")] impl_has!(Vec: |self: App|self.project.midi_outs); +#[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_ins); +#[cfg(feature = "port")] impl_has!(Vec: |self: Sequencer|self.midi_outs); audio!(App: tek_jack_process, tek_jack_event); -audio!(Sampler: sampler_jack_process); -has!(Jack<'static>: |self: App|self.jack); -has!(Dialog: |self: App|self.dialog); -has!(Measure: |self: App|self.size); -has!(Option: |self: App|self.project.editor); -has!(Pool: |self: App|self.pool); -has!(Selection: |self: App|self.project.selection); -has!(Sequencer: |self: Track|self.sequencer); +impl_has!(Jack<'static>: |self: App|self.jack); +impl_has!(Dialog: |self: App|self.dialog); +impl_has!(Measure: |self: App|self.size); +impl_has!(Option: |self: App|self.project.editor); +impl_has!(Pool: |self: App|self.pool); +impl_has!(Selection: |self: App|self.project.selection); +impl_has!(Sequencer: |self: Track|self.sequencer); has_clips!( |self: App|self.pool.clips); impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); @@ -2433,11 +2407,11 @@ mod midi { parse_midi_input(self.port().iter(scope)) } } - impl>> HasMidiIns for T { + impl>> HasMidiIns for T { fn midi_ins (&self) -> &Vec { self.get() } fn midi_ins_mut (&mut self) -> &mut Vec { self.get_mut() } } - impl>> HasMidiOuts for T { + impl>> HasMidiOuts for T { fn midi_outs (&self) -> &Vec { self.get() } fn midi_outs_mut (&mut self) -> &mut Vec { self.get_mut() } } @@ -2653,13 +2627,24 @@ mod audio { #[cfg(feature = "track")] mod track { use crate::*; + impl_as_ref!(Vec: |self: App| self.project.as_ref()); + impl_as_mut!(Vec: |self: App| self.project.as_mut()); + impl_as_ref!(Vec: |self: Arrangement| self.tracks); + impl_as_mut!(Vec: |self: Arrangement| self.tracks); + + #[cfg(all(feature = "select"))] impl_as_ref!(Option: |self: App| self.project.as_ref()); + #[cfg(all(feature = "select"))] impl_as_mut!(Option: |self: App| self.project.as_mut()); + + impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } + impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } + impl HasWidth for Track { const MIN_WIDTH: usize = 9; fn width_inc (&mut self) { self.width += 1; } fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } } - impl> HasTrack for T { + impl>> HasTrack for T { fn track (&self) -> Option<&Track> { self.get() } fn track_mut (&mut self) -> Option<&mut Track> { self.get_mut() } } @@ -2744,11 +2729,48 @@ mod audio { None } } + + pub fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Content + 'a { + per_track_top(tracks, move|index, track|Fill::Y(Align::y(callback(index, track)))) + } + + pub fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Content + 'a { + Align::x(Tui::bg(Reset, Map::new(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + let width = (x2 - x1) as u16; + map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg( + track.color.lightest.rgb, + track.color.base.rgb, + callback(index, track))))}))) + } } #[cfg(feature = "scene")] mod scene { use crate::*; + impl AddScene for T {} + impl> + AsMut> + Send + Sync> HasScenes for T {} + impl> + AsMut> + Send + Sync> HasScene for T {} + + impl_as_ref!(Vec: |self: App| self.project.as_ref()); + impl_as_mut!(Vec: |self: App| self.project.as_mut()); + impl_as_ref!(Vec: |self: Arrangement| self.scenes.as_ref()); + impl_as_mut!(Vec: |self: Arrangement| self.scenes.as_mut()); + #[cfg(all(feature = "select"))] impl_as_ref!(Option: |self: App| self.project.as_ref()); + #[cfg(all(feature = "select"))] impl_as_mut!(Option: |self: App| self.project.as_mut()); + #[cfg(all(feature = "select"))] impl_as_ref!(Option: |self: Arrangement| self.selected.scene()); + #[cfg(all(feature = "select"))] impl_as_mut!(Option: |self: Arrangement| self.selected.scene()); + + impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } + impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } + + impl ScenesView for App { fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) } fn w_side (&self) -> u16 { 20 } @@ -2792,10 +2814,9 @@ mod audio { #[cfg(feature = "sequencer")] mod sequencer { use crate::*; - impl> HasSequencer for T { - fn sequencer (&self) -> &Sequencer { self.get() } - fn sequencer_mut (&mut self) -> &mut Sequencer { self.get_mut() } - } + + impl + AsMut> HasSequencer for T {} + impl Default for Sequencer { fn default () -> Self { Self { @@ -2816,10 +2837,12 @@ mod audio { } } } + impl HasMidiBuffers for Sequencer { fn note_buf_mut (&mut self) -> &mut Vec { &mut self.note_buf } fn midi_buf_mut (&mut self) -> &mut Vec>> { &mut self.midi_buf } } + impl std::fmt::Debug for Sequencer { fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Sequencer") @@ -2829,6 +2852,7 @@ mod audio { .finish() } } + impl MidiMonitor for Sequencer { fn notes_in (&self) -> &Arc> { &self.notes_in @@ -2840,6 +2864,7 @@ mod audio { &mut self.monitoring } } + impl MidiRecord for Sequencer { fn recording (&self) -> bool { self.recording @@ -2854,6 +2879,7 @@ mod audio { &mut self.overdub } } + #[cfg(feature="clip")] impl HasPlayClip for Sequencer { fn reset (&self) -> bool { self.reset @@ -2874,6 +2900,7 @@ mod audio { &mut self.next_clip } } + /// JACK process callback for a sequencer's clip sequencer/recorder. impl Audio for Sequencer { fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { @@ -2884,6 +2911,7 @@ mod audio { } } } + impl Sequencer { pub fn new ( name: impl AsRef, @@ -3536,6 +3564,55 @@ mod audio { } }) } + + audio!(Sampler: sampler_jack_process); + pub(crate) fn sampler_jack_process (state: &mut Sampler, _: &Client, scope: &ProcessScope) -> Control { + if let Some(midi_in) = &state.midi_in { + for midi in midi_in.port().iter(scope) { + sampler_midi_in(&state.samples, &state.voices, midi) + } + } + state.process_audio_out(scope); + state.process_audio_in(scope); + Control::Continue + } + + /// Create [Voice]s from [Sample]s in response to MIDI input. + fn sampler_midi_in ( + samples: &SampleKit<128>, voices: &Arc>>, RawMidi { time, bytes }: RawMidi + ) { + if let Ok(LiveEvent::Midi { message, .. }) = LiveEvent::parse(bytes) { + match message { + MidiMessage::NoteOn { ref key, ref vel } => { + if let Some(sample) = samples.get(key.as_int() as usize) { + voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + } + }, + MidiMessage::Controller { controller: _, value: _ } => { + // TODO + } + _ => {} + } + } + } + + fn draw_sample ( + to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool + ) -> Usually { + let style = if focus { Style::default().green() } else { Style::default() }; + if focus { + to.blit(&"🬴", x+1, y, Some(style.bold())); + } + let label1 = format!("{:3} {:12}", + note.map(|n|n.to_string()).unwrap_or(String::default()), + sample.name); + let label2 = format!("{:>6} {:>6} +0.0", + sample.start, + sample.end); + to.blit(&label1, x+2, y, Some(style.bold())); + to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); + Ok(label1.len() + label2.len() + 4) + } } #[cfg(feature = "lv2")] mod lv2 { @@ -3680,6 +3757,17 @@ mod audio { } } + + fn draw_header (state: &Lv2, to: &mut TuiOut, x: u16, y: u16, w: u16) { + let style = Style::default().gray(); + let label1 = format!(" {}", state.name); + to.blit(&label1, x + 1, y, Some(style.white().bold())); + if let Some(ref path) = state.path { + let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); + to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); + } + //Ok(Rect { x, y, width: w, height: 1 }) + } } mod pool { diff --git a/app/tek_trait.rs b/app/tek_trait.rs index 4e7954ab..a7887992 100644 --- a/app/tek_trait.rs +++ b/app/tek_trait.rs @@ -115,10 +115,6 @@ pub trait JackPerfModel { } -//pub trait MaybeHas: Send + Sync { - //fn get (&self) -> Option<&T>; -//} - pub trait HasN: Send + Sync { fn get_nth (&self, key: usize) -> &T; fn get_nth_mut (&mut self, key: usize) -> &mut T; @@ -242,37 +238,44 @@ pub trait MidiRange: TimeRange + NoteRange {} /// can be clocked in microseconds with f64 without losing precision. pub trait TimeUnit: InteriorMutable {} -pub trait HasSceneScroll: HasScenes { - fn scene_scroll (&self) -> usize; -} - -pub trait HasTrackScroll: HasTracks { - fn track_scroll (&self) -> usize; -} +pub trait HasClipsSize { fn clips_size (&self) -> &Measure; } pub trait HasMidiClip { fn clip (&self) -> Option>>; } - -pub trait HasClipsSize { - fn clips_size (&self) -> &Measure; +pub trait HasClock: AsRef + AsMut { + fn clock_mut (&mut self) -> &mut Clock { self.as_mut() } + fn clock (&self) -> &Clock { self.as_ref() } } - -pub trait HasClock: Send + Sync { - fn clock (&self) -> &Clock; - fn clock_mut (&mut self) -> &mut Clock; +pub trait HasDevices: AsRef> + AsMut> { + fn devices_mut (&mut self) -> &mut Vec { self.as_mut() } + fn devices (&self) -> &Vec { self.as_reF() } } - -pub trait HasDevices { - fn devices (&self) -> &Vec; - fn devices_mut (&mut self) -> &mut Vec; +pub trait HasSelection: AsRef + AsMut { + fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } + fn selection (&self) -> &Selection { self.as_ref() } } - -pub trait HasSelection: Has { - fn selection (&self) -> &Selection { self.get() } - fn selection_mut (&mut self) -> &mut Selection { self.get_mut() } +pub trait HasSequencer: AsRef + AsMut { + fn sequencer_mut (&mut self) -> &mut Sequencer { self.as_mut() } + fn sequencer (&self) -> &Sequencer { self.as_ref() } +} +pub trait HasScene: AsRef> + AsMut> { + fn scene_mut (&mut self) -> &mut Option { self.as_mut() } + fn scene (&self) -> Option<&Scene> { self.as_ref() } +} +pub trait HasScenes: AsRef> + AsMut> { + fn scenes (&self) -> &Vec { self.as_reF() } + fn scenes_mut (&mut self) -> &mut Vec { self.as_mut() } + /// Generate the default name for a new scene + fn scene_default_name (&self) -> Arc { format!("s{:3>}", self.scenes().len() + 1).into() } + fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) } +} +pub trait HasSceneScroll: HasScenes { + fn scene_scroll (&self) -> usize; +} +pub trait HasTrackScroll: HasTracks { + fn track_scroll (&self) -> usize; } - pub trait HasWidth { const MIN_WIDTH: usize; /// Increment track width. @@ -280,48 +283,17 @@ pub trait HasWidth { /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. fn width_dec (&mut self); } - pub trait HasMidiBuffers { fn note_buf_mut (&mut self) -> &mut Vec; fn midi_buf_mut (&mut self) -> &mut Vec>>; } -pub trait HasSequencer { - fn sequencer (&self) -> &Sequencer; - fn sequencer_mut (&mut self) -> &mut Sequencer; -} - -pub trait HasScene: Has> + Send + Sync { - fn scene (&self) -> Option<&Scene> { - Has::>::get(self).as_ref() - } - fn scene_mut (&mut self) -> &mut Option { - Has::>::get_mut(self) - } -} - -pub trait HasScenes: Has> + Send + Sync { - fn scenes (&self) -> &Vec { - Has::>::get(self) - } - fn scenes_mut (&mut self) -> &mut Vec { - Has::>::get_mut(self) - } - /// Generate the default name for a new scene - fn scene_default_name (&self) -> Arc { - format!("s{:3>}", self.scenes().len() + 1).into() - } - fn scene_longest_name (&self) -> usize { - self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) - } -} - /// ``` /// use tek::{MidiEditor, HasEditor, tengri::Has}; /// /// let mut host = TestEditorHost(Some(MidiEditor::default())); /// struct TestEditorHost(Option); -/// impl Has> for TestEditorHost { +/// impl AsRef> for TestEditorHost { /// fn get (&self) -> &Option { &self.0 } /// fn get_mut (&mut self) -> &mut Option { &mut self.0 } /// } @@ -332,7 +304,7 @@ pub trait HasScenes: Has> + Send + Sync { /// let _ = host.editor_w(); /// let _ = host.editor_h(); /// ``` -pub trait HasEditor: Has> { +pub trait HasEditor: AsRef> { fn editor (&self) -> Option<&MidiEditor> { self.get().as_ref() } @@ -407,9 +379,10 @@ pub trait HasMidiOuts { } } -pub trait HasTracks: Has> + Send + Sync { - fn tracks (&self) -> &Vec { Has::>::get(self) } - fn tracks_mut (&mut self) -> &mut Vec { Has::>::get_mut(self) } +impl> + AsMut> + Send + Sync> HasTracks for T {} +pub trait HasTracks: AsRef> + AsMut> + Send + Sync { + fn tracks (&self) -> &Vec { self.as_ref() } + fn tracks_mut (&mut self) -> &mut Vec { self.as_mut() } /// Run audio callbacks for every track and every device fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { for track in self.tracks_mut().iter_mut() { diff --git a/dizzle b/dizzle index 7d1fbe3f..909b94cb 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit 7d1fbe3fbe53699a3e12eb5a3d55db79653d72d8 +Subproject commit 909b94cbd4beffb49be314724dc79db9374bcc99