diff --git a/config/config_arranger.edn b/config/config_arranger.edn index 2b67db1c..408dab55 100644 --- a/config/config_arranger.edn +++ b/config/config_arranger.edn @@ -21,10 +21,11 @@ (bsp/s (fixed/y 8 (bsp/e (fixed/x 20 (fill/y (align/n (bsp/s :view-status-v - (bsp/s :view-audio-ins-status :view-audio-outs-status))))) + (bsp/s (bsp/s :view-audio-ins-status :view-audio-outs-status) + :view-editor-status))))) (fill/xy (align/n (bsp/s :view-arranger-track-names (bsp/s :view-arranger-track-outputs (bsp/s :view-arranger-track-devices :view-arranger-track-inputs))))))) - (fill/xy (bsp/e - (bsp/n (max/y 9 :view-editor-status) (fixed/x 20 (align/nw :view-arranger-scenes-names))) - :view-arranger-scenes-clips))))) + (fill/xy (align/w (bsp/e + (align/w (fixed/x 20 (fill/xy (align/nw :view-arranger-scenes-names)))) + (align/w (fill/xy (align/nw :view-arranger-scenes-clips))))))))) diff --git a/config/keys_arranger.edn b/config/keys_arranger.edn index 0199a07b..40ffc742 100644 --- a/config/keys_arranger.edn +++ b/config/keys_arranger.edn @@ -1,7 +1,7 @@ (@c color) (@q launch) (@t select :select-track-header) -(@tab edit :clip-selected) +(@tab edit :clip) (@shift-I input add) (@shift-O output add) (@shift-S scene add) diff --git a/config/keys_sampler.edn b/config/keys_sampler.edn index 596671c4..bb4b5b57 100644 --- a/config/keys_sampler.edn +++ b/config/keys_sampler.edn @@ -8,5 +8,5 @@ (@p sampler play-sample :sample-selected) (@P sampler stop-sample :sample-selected) -(@shift-f6 dialog :dialog-export-sample) -(@shift-f9 dialog :dialog-import-sample) +(@shift-f6 dialog :dialog-import-sample) +(@shift-f9 dialog :dialog-export-sample) diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 3a4516dd..6b04fe75 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -18,20 +18,6 @@ handle!(TuiIn: |self: App, input|Ok(if let Some(command) = self.config.keys.comm })); #[tengri_proc::command(App)] impl AppCommand { - fn edit (app: &mut App) -> Perhaps { - let selection = app.selection().clone(); - Ok(match selection { - Selection::TrackClip { track, scene } => { - let clip = &mut app.scenes_mut()[scene].clips[track]; - if clip.is_none() { - *clip = Some(Default::default()); - } - app.editor = clip.as_ref().map(|c|c.into()); - None - } - _ => None - }) - } fn dialog (app: &mut App, dialog: Option) -> Perhaps { app.toggle_dialog(dialog); Ok(None) diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index ee5f836d..602ce67a 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -18,8 +18,7 @@ #![feature(closure_lifetime_binder)] pub use ::tek_engine:: *; pub use ::tek_device::{self, *}; -pub use ::tengri::{Usually, Perhaps, Has, MaybeHas}; -pub use ::tengri::{has, maybe_has}; +pub use ::tengri::{Usually, Perhaps, Has}; pub use ::tengri::dsl::*; pub use ::tengri::input::*; pub use ::tengri::output::*; diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index df8b5ce8..1138f078 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -3,25 +3,25 @@ use crate::*; #[derive(Default, Debug)] pub struct App { /// Must not be dropped for the duration of the process - pub jack: Jack, + pub jack: Jack, /// Port handles pub ports: std::collections::BTreeMap>, /// Display size pub size: Measure, /// Performance counter pub perf: PerfModel, + // View and input definition - pub config: Configuration, - /// Contains all recently created clips. - pub pool: Pool, + pub config: Configuration, /// Contains the currently edited musical arrangement pub project: Arrangement, /// Undo history pub history: Vec, // Dialog overlay - pub dialog: Option, + pub dialog: Option, /// Contains the currently edited MIDI clip - pub editor: Option, + pub editor: Option, + // Cache of formatted strings pub view_cache: Arc>, /// Base color. @@ -29,7 +29,6 @@ pub struct App { } has!(Jack: |self: App|self.jack); -has!(Pool: |self: App|self.pool); has!(Clock: |self: App|self.project.clock); has!(Selection: |self: App|self.project.selection); has!(Vec: |self: App|self.project.midi_ins); @@ -37,18 +36,7 @@ has!(Vec: |self: App|self.project.midi_outs); has!(Vec: |self: App|self.project.scenes); has!(Vec: |self: App|self.project.tracks); has!(Measure: |self: App|self.size); -maybe_has!(Track: |self: App| - { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); -impl HasTrackScroll for App { - fn track_scroll (&self) -> usize { self.project.track_scroll() } -} -maybe_has!(Scene: |self: App| - { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); -impl HasSceneScroll for App { - fn scene_scroll (&self) -> usize { self.project.scene_scroll() } -} + has_clips!(|self: App|self.project.pool.clips); has_editor!(|self: App|{ editor = self.editor; @@ -312,7 +300,7 @@ impl App { fn select_track_prev (&self) -> Selection { self.selection().track_prev() } - fn clip_selected (&self) -> Option>> { + fn clip_selection (&self) -> Option>> { match self.selection() { Selection::TrackClip { track, scene } => self.scenes()[*scene].clips[*track].clone(), _ => None diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index 4ea5322c..d5723f74 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -61,41 +61,164 @@ impl App { self.editor() } pub fn view_editor_status (&self) -> impl Content + use<'_> { - self.editor().map(|e|Fixed::x(20, Outer(true, Style::default().fg(Tui::g(96))).enclose( - Fill::y(Align::n(Bsp::s(e.clip_status(), e.edit_status())))))) + self.editor().map(|e|Bsp::s(e.clip_status(), e.edit_status())) } pub fn view_midi_ins_status (&self) -> impl Content + use<'_> { - self.project.view_midi_ins_status(self.color) + self.project.get_track().map(|track|{ + let ins = track.sequencer.midi_ins.len() as u16; + Fixed::xy(20, 1 + ins, Outer(true, Style::default().fg(Tui::g(96))).enclose( + Fixed::xy(20, 1 + ins, FieldV(self.color, format!("MIDI ins: "), + Map::south(1, ||track.sequencer.midi_ins.iter(), + |port, index|Fill::x(Align::w(format!(" {index} {}", port.name())))))))) + }) } pub fn view_midi_outs_status (&self) -> impl Content + use<'_> { - self.project.view_midi_outs_status(self.color) + self.project.get_track().map(|track|{ + let outs = track.sequencer.midi_outs.len() as u16; + Fixed::xy(20, 1 + outs, Outer(true, Style::default().fg(Tui::g(96))).enclose( + Fixed::xy(20, 1 + outs, FieldV(self.color, format!("MIDI outs: "), + Map::south(1, ||track.sequencer.midi_outs.iter(), + |port, index|Fill::x(Align::w(format!(" {index} {}", port.name())))))))) + }) } pub fn view_audio_ins_status (&self) -> impl Content + use<'_> { - self.project.view_audio_ins_status(self.color) + self.project.get_track() + .and_then(|track|track.devices.get(0)) + .map(|device|{ + let ins = device.audio_ins().len() as u16; + Fixed::xy(20, 1 + ins, Outer(true, Style::default().fg(Tui::g(96))).enclose( + Fixed::xy(20, 1 + ins, FieldV(self.color, format!("Audio ins: "), + Map::south(1, ||device.audio_ins().iter(), + |port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))}) } pub fn view_audio_outs_status (&self) -> impl Content + use<'_> { - self.project.view_audio_outs_status(self.color) + self.project.get_track() + .and_then(|track|track.devices.last()) + .map(|device|{ + let outs = device.audio_outs().len() as u16; + Fixed::xy(20, 1 + outs, Outer(true, Style::default().fg(Tui::g(96))).enclose( + Fixed::xy(20, 1 + outs, FieldV(self.color, format!("Audio outs: "), + Map::south(1, ||device.audio_outs().iter(), + |port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))}) } pub fn view_arranger (&self) -> impl Content + use<'_> { - &self.project + ArrangerView::new(&self.project, self.editor.as_ref()) + } + pub fn view_arranger_scenes (&self) -> impl Content + use<'_> { + self.scenes_view(&self.editor) } pub fn view_arranger_scenes_names (&self) -> impl Content + use<'_> { - self.project.view_scenes_names() + self.scenes_names() } pub fn view_arranger_scenes_clips (&self) -> impl Content + use<'_> { - self.project.view_scenes_clips(&self.editor) + self.scenes_clips(&self.editor) + } + pub fn view_arranger_scene_names <'a> (&'a self) -> impl Content + use<'a> { + let h = self.project.scenes.len() as u16 * 2; + let bg = self.color.darker.rgb; + Fixed::y(h, Tui::bg(bg, Align::w(Fill::x(Map::new( + ||self.project.scenes.iter().skip(self.project.scene_scroll), + move|scene: &Scene, index| + Push::y(index as u16 * 2u16, Fixed::xy(20, 2, + Tui::bg(scene.color.dark.rgb, Align::nw(Bsp::e( + format!(" {index:2} "), + Tui::fg(Rgb(255, 255, 255), + Tui::bold(true, format!("{}", scene.name))))))))))))) + } + pub fn view_arranger_scene_clips <'a> (&'a self) -> impl Content + use<'a> { + let h = self.project.scenes.len() as u16 * 2; + let bg = self.color.darker.rgb; + Fixed::y(h, Tui::bg(bg, Align::w(Fill::x(Map::new( + ||self.project.scenes.iter().skip(self.project.scene_scroll), + move|scene: &'a Scene, index1| + Push::y(index1 as u16 * 2u16, Fixed::xy(20, 2, + Map::new( + move||scene.clips.iter().skip(self.project.track_scroll), + move|clip: &'a Option>>, index2|{ + let (theme, text) = if let Some(clip) = clip { + let clip = clip.read().unwrap(); + (clip.color, clip.name.clone()) + } else { + (scene.color, Default::default()) + }; + Push::x(index2 as u16 * 14, Tui::bg(theme.dark.rgb, Bsp::e( + format!(" {index1:2} {index2:2} "), + Tui::fg(Rgb(255, 255, 255), + Tui::bold(true, format!("{}", text)))))) + })))))))) } pub fn view_arranger_track_names (&self) -> impl Content + use<'_> { - self.project.view_track_names(self.color) + let mut max_outputs = 0u16; + for track in self.project.tracks.iter() { + max_outputs = max_outputs.max(track.sequencer.midi_outs.len() as u16); + } + Bsp::w( + Fixed::x(20, Tui::bg(self.color.darkest.rgb, + col!(Tui::bold(true, "[t]rack"), "[T] Add"))), + Align::w(Fixed::y(max_outputs + 1, Tui::bg(self.color.darker.rgb, Align::w(Fill::x(Map::new( + ||self.project.tracks_with_sizes(&self.project.selection, None) + .skip(self.project.track_scroll), + move|(index, track, x1, x2): (usize, &Track, usize, usize), _| + Push::x(index as u16 * 14, Fixed::xy(track.width as u16, max_outputs + 1, + Tui::bg(track.color.dark.rgb, Align::nw(Bsp::s( + Tui::fg(Rgb(255, 255, 255), Tui::bold(true, format!("{}", track.name))), + format!("{index} {x1} {x2}"))))))))))))) } pub fn view_arranger_track_outputs <'a> (&'a self) -> impl Content + use<'a> { - self.project.view_track_outputs(self.color) + let mut max_outputs = 0u16; + for track in self.project.tracks.iter() { + max_outputs = max_outputs.max(track.sequencer.midi_outs.len() as u16); + } + Bsp::w( + Fixed::x(20, Tui::bg(self.color.darkest.rgb, + col!(Tui::bold(true, "[o]utput"), "[O] Add"))), + Align::w(Fixed::y(max_outputs + 1, Tui::bg(self.color.darker.rgb, Align::w(Fill::x(Map::new( + ||self.project.tracks_with_sizes(&self.project.selection, None) + .skip(self.project.track_scroll), + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _| + Push::x(x2 as u16, Tui::bg(track.color.dark.rgb, Fixed::xy( + track.width as u16, + max_outputs + 1, + Align::nw(Bsp::s( + format!("[mut] [sol]"), + Map::south(1, ||track.sequencer.midi_outs.iter(), + |port, index|Tui::fg(Rgb(255, 255, 255), + format!("{index}: {}", port.name()))))))))))))))) } pub fn view_arranger_track_inputs <'a> (&'a self) -> impl Content + use<'a> { - self.project.view_track_inputs(self.color) + let mut max_inputs = 0u16; + for track in self.project.tracks.iter() { + max_inputs = max_inputs.max(track.sequencer.midi_ins.len() as u16); + } + Bsp::w( + Fixed::x(20, Tui::bg(self.color.darkest.rgb, + col!(Tui::bold(true, "[i]nputs"), "[I] Add"))), + Fill::x(Align::w(Fixed::y(max_inputs + 1, Tui::bg(self.color.darker.rgb, Align::w(Fill::x(Map::new( + ||self.project.tracks_with_sizes(&self.project.selection, None) + .skip(self.project.track_scroll), + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _| + Push::x(x2 as u16, Fixed::xy(track.width as u16, max_inputs + 1, + Tui::bg(track.color.dark.rgb, Align::nw(Bsp::s( + format!("[rec] [mon]"), + Map::south(1, ||track.sequencer.midi_ins.iter(), + |port, index|Tui::fg(Rgb(255, 255, 255), + format!("{index}: {}", port.name())))))))))))))))) } pub fn view_arranger_track_devices <'a> (&'a self) -> impl Content + use<'a> { - self.project.view_track_devices(self.color) + let mut max_devices = 2u16; + for track in self.project.tracks.iter() { + max_devices = max_devices.max(track.devices.len() as u16); + } + Bsp::w( + Fixed::x(20, Tui::bg(self.color.darkest.rgb, + col!(Tui::bold(true, "[d]evice"), "[D] Add"))), + Fixed::y(max_devices, Tui::bg(self.color.darker.rgb, Align::w(Fill::x(Map::new( + ||self.project.tracks_with_sizes(&self.project.selection, None) + .skip(self.project.track_scroll), + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _| + Push::x(x2 as u16, Fixed::xy(track.width as u16, max_devices + 1, + Tui::bg(track.color.dark.rgb, Align::nw(Map::south(1, move||0..max_devices, + |_, index|format!("{index}: {}", "--------")))))))))))) } pub fn view_arranger_track_scenes <'a> (&'a self) -> impl Content + use<'a> { let mut max_devices = 0u16; @@ -132,8 +255,7 @@ impl App { self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) } pub fn view_sample_status (&self) -> impl Content + use<'_> { - self.project.sampler().map(|s|Outer(true, Style::default().fg(Tui::g(96))).enclose( - Fill::y(Align::n(s.view_sample_status(self.editor().unwrap().get_note_pos()))))) + self.project.sampler().map(|s|s.view_sample_status(self.editor().unwrap().get_note_pos())) } pub fn view_meters_input (&self) -> impl Content + use<'_> { self.project.sampler().map(|s|s.view_meters_input()) @@ -239,7 +361,7 @@ impl App { } } -impl ScenesView for App { +impl ArrangerSceneRows for App { fn arrangement (&self) -> &Arrangement { &self.project } @@ -255,9 +377,15 @@ impl ScenesView for App { fn scene_selected (&self) -> Option { self.project.selection.scene() } + fn scene_last (&self) -> usize { + self.project.scenes.len().saturating_sub(1) + } fn track_selected (&self) -> Option { self.project.selection.track() } + fn is_editing (&self) -> bool { + self.editor.is_some() + } } pub(crate) fn heading <'a> ( diff --git a/crates/device/src/arranger.rs b/crates/device/src/arranger.rs index 94a7f295..38f5b313 100644 --- a/crates/device/src/arranger.rs +++ b/crates/device/src/arranger.rs @@ -11,8 +11,11 @@ macro_rules! def_sizes_iter { mod arranger_api; pub use self::arranger_api::*; mod arranger_clip; pub use self::arranger_clip::*; mod arranger_model; pub use self::arranger_model::*; +mod arranger_port; pub use self::arranger_port::*; +mod arranger_scene; pub use self::arranger_scene::*; mod arranger_scenes; pub use self::arranger_scenes::*; mod arranger_select; pub use self::arranger_select::*; +mod arranger_track; pub use self::arranger_track::*; mod arranger_tracks; pub use self::arranger_tracks::*; mod arranger_view; pub use self::arranger_view::*; @@ -27,3 +30,48 @@ pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content) -> impl let right = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("▌"))); Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content))) } + +pub(crate) fn io_ports <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a +) -> impl Content + 'a { + Map::new(iter, move|( + index, name, connections, y, y2 + ): (usize, &'a Arc, &'a [PortConnect], usize, usize), _| + map_south(y as u16, (y2-y) as u16, Bsp::s( + Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(" 󰣲 ", name))))), + Map::new(||connections.iter(), move|connect: &'a PortConnect, index|map_south(index as u16, 1, + Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, + &connect.info))))))))) +} + +pub(crate) fn io_conns <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a +) -> impl Content + 'a { + Map::new(iter, move|( + index, name, connections, y, y2 + ): (usize, &'a Arc, &'a [PortConnect], usize, usize), _| + map_south(y as u16, (y2-y) as u16, Bsp::s( + Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w("▞▞▞▞ ▞▞▞▞"))))), + Map::new(||connections.iter(), move|connect, index|map_south(index as u16, 1, + Fill::x(Align::w(Tui::bold(false, wrap(bg, fg, Fill::x("")))))))))) +} + +pub trait HasWidth { + const MIN_WIDTH: usize; + /// Increment track width. + fn width_inc (&mut self); + /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. + fn width_dec (&mut self); +} + +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; + } + } +} diff --git a/crates/device/src/arranger/arranger_api.rs b/crates/device/src/arranger/arranger_api.rs index aba398e9..daa31c67 100644 --- a/crates/device/src/arranger/arranger_api.rs +++ b/crates/device/src/arranger/arranger_api.rs @@ -192,12 +192,6 @@ impl ArrangementCommand { } } -impl<'state> Context<'state, TrackCommand> for Arrangement { - fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { - Context::get(&self, iter) - } -} - impl<'state> Context<'state, MidiInputCommand> for Arrangement { fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { Context::get(&self, iter) @@ -215,15 +209,3 @@ impl<'state> Context<'state, DeviceCommand> for Arrangement { Context::get(&self, iter) } } - -impl<'state> Context<'state, SceneCommand> for Arrangement { - fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { - Context::get(&self, iter) - } -} - -impl<'state> Context<'state, ClipCommand> for Arrangement { - fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { - Context::get(&self, iter) - } -} diff --git a/crates/device/src/arranger/arranger_clip.rs b/crates/device/src/arranger/arranger_clip.rs index a856c47b..98dd7ba7 100644 --- a/crates/device/src/arranger/arranger_clip.rs +++ b/crates/device/src/arranger/arranger_clip.rs @@ -1,5 +1,11 @@ use crate::*; +impl<'state> Context<'state, ClipCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self, iter) + } +} + #[tengri_proc::expose] impl MidiClip { fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } @@ -24,3 +30,7 @@ impl ClipCommand { todo!() } } + +impl Arrangement { + +} diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs index a1c8a407..a34134d7 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -47,12 +47,6 @@ has!(Vec: |self: Arrangement|self.midi_outs); has!(Vec: |self: Arrangement|self.scenes); has!(Vec: |self: Arrangement|self.tracks); has!(Measure: |self: Arrangement|self.size); -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() }); -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() }); impl Arrangement { /// Width of display @@ -76,6 +70,18 @@ impl Arrangement { pub fn h (&self) -> u16 { self.size.h() as u16 } + /// Height taken by all scenes. + pub fn h_scenes (&self, is_editing: bool) -> u16 { + self.scenes_with_sizes( + is_editing, + ArrangerView::H_SCENE, + ArrangerView::H_EDITOR, + self.selection().track(), + self.selection().scene(), + ) + .last() + .map(|(_, _, _, y)|y as u16).unwrap_or(0) + } /// Height taken by all inputs. pub fn h_inputs (&self) -> u16 { self.midi_ins_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) @@ -139,64 +145,6 @@ impl Arrangement { clip.write().unwrap().toggle_loop() } } - /// Add multiple tracks - pub fn tracks_add ( - &mut self, - count: usize, - width: Option, - mins: &[PortConnect], - mouts: &[PortConnect], - ) -> Usually<()> { - let jack = self.jack().clone(); - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..count { - let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); - let mut track = self.track_add(None, Some(color), mins, mouts)?.1; - if let Some(width) = width { - track.width = width; - } - } - Ok(()) - } - - /// Add a track - pub fn track_add ( - &mut self, - name: Option<&str>, - color: Option, - mins: &[PortConnect], - mouts: &[PortConnect], - ) -> Usually<(usize, &mut Track)> { - let name: Arc = name.map_or_else( - ||format!("t{:02}", self.track_last).into(), - |x|x.to_string().into() - ); - self.track_last += 1; - let mut track = Track { - width: (name.len() + 2).max(12), - color: color.unwrap_or_else(ItemTheme::random), - sequencer: Sequencer::new( - &format!("{name}"), - self.jack(), - Some(self.clock()), - None, - mins, - mouts - )?, - name, - ..Default::default() - }; - self.tracks_mut().push(track); - let len = self.tracks().len(); - let index = len - 1; - for scene in self.scenes_mut().iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok((index, &mut self.tracks_mut()[index])) - } } #[cfg(feature = "sampler")] @@ -210,24 +158,3 @@ impl Arrangement { self.get_track_mut()?.sampler_mut(0) } } - -impl ScenesView for Arrangement { - fn arrangement (&self) -> &Arrangement { - self - } - fn scenes_height (&self) -> u16 { - (self.height() as u16).saturating_sub(20) - } - fn width_side (&self) -> u16 { - (self.width() as u16 * 2 / 10).max(20) - } - fn width_mid (&self) -> u16 { - (self.width() as u16).saturating_sub(2 * self.width_side()).max(40) - } - fn scene_selected (&self) -> Option { - self.selection().scene() - } - fn track_selected (&self) -> Option { - self.selection().track() - } -} diff --git a/crates/device/src/arranger/arranger_port.rs b/crates/device/src/arranger/arranger_port.rs new file mode 100644 index 00000000..cf7a65b4 --- /dev/null +++ b/crates/device/src/arranger/arranger_port.rs @@ -0,0 +1,135 @@ +use crate::*; + +impl<'a> ArrangerView<'a> { + + pub(crate) fn input_routes (&'a self) -> impl Content + 'a { + Tryptich::top(self.arrangement.h_inputs()) + .left(self.width_side, + io_ports(Tui::g(224), Tui::g(32), ||self.arrangement.midi_ins_with_sizes())) + .middle(self.width_mid, + per_track_top(||self.tracks_with_sizes_scrolled(), + move|_, &Track { color, .. }|io_conns( + color.dark.rgb, + color.darker.rgb, + ||self.arrangement.midi_ins_with_sizes() + ))) + } + + pub(crate) fn input_ports (&'a self) -> impl Content + 'a { + Tryptich::top(1) + .left(self.width_side, + button_3("i", "midi ins", format!("{}", self.arrangement.midi_ins().len()), self.is_editing)) + .right(self.width_side, + button_2("I", "add midi in", self.is_editing)) + .middle(self.width_mid, + per_track_top(||self.tracks_with_sizes_scrolled(), + move|t, track|{ + let rec = track.sequencer.recording; + let mon = track.sequencer.monitoring; + let rec = if rec { White } else { track.color.darkest.rgb }; + let mon = if mon { White } else { track.color.darkest.rgb }; + let bg = if self.track_selected == Some(t) { + track.color.light.rgb + } else { + track.color.base.rgb + }; + //let bg2 = if t > 0 { track.color.base.rgb } else { Reset }; + wrap(bg, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e( + Tui::fg_bg(rec, bg, "Rec "), + Tui::fg_bg(mon, bg, "Mon "), + )))) + })) + } + + pub(crate) fn input_intos (&'a self) -> impl Content + 'a { + Tryptich::top(2) + .left(self.width_side, + Bsp::s(Align::e("Input:"), Align::e("Into clip:"))) + .middle(self.width_mid, + per_track_top(||self.tracks_with_sizes_scrolled(), + |_, _|Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ "))))) + } + +} + +impl<'a> ArrangerView<'a> { + + pub(crate) fn output_nexts (&self) -> impl Content + '_ { + Tryptich::top(2) + .left(self.width_side, Align::ne("From clip:")) + .middle(self.width_mid, per_track_top(||self.tracks_with_sizes_scrolled(), + |_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default()))))) + } + + pub(crate) fn output_froms (&'a self) -> impl Content + 'a { + let label = Align::ne("Next clip:"); + Tryptich::top(2).left(self.width_side, label).middle(self.width_mid, per_track_top( + ||self.tracks_with_sizes_scrolled(), |t, track|{ + let queued = track.sequencer.next_clip.is_some(); + let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ ")); + let queued_clip = Thunk::new(||{ + Tui::bg(Reset, if let Some((_, clip)) = track.sequencer.next_clip.as_ref() { + if let Some(clip) = clip { + clip.read().unwrap().name.clone() + } else { + "Stop".into() + } + } else { + "".into() + }) + }); + Either(queued, queued_clip, queued_blank) + })) + } + + pub(crate) fn output_ports (&'a self) -> impl Content + 'a { + Tryptich::top(1) + .left(self.width_side, self.output_count()) + .right(self.width_side, self.output_add()) + .middle(self.width_mid, self.output_map()) + } + + pub(crate) fn output_count (&'a self) -> impl Content + 'a { + button_3( + "o", + "midi outs", + format!("{}", self.arrangement.midi_outs().len()), + self.is_editing + ) + } + + pub(crate) fn output_add (&'a self) -> impl Content + 'a { + button_2("O", "add midi out", self.is_editing) + } + + pub(crate) fn output_map (&'a self) -> impl Content + 'a { + per_track_top(||self.tracks_with_sizes_scrolled(), move|i, t|{ + let mute = false; + let solo = false; + let mute = if mute { White } else { t.color.darkest.rgb }; + let solo = if solo { White } else { t.color.darkest.rgb }; + let bg_1 = if self.track_selected == Some(i) { + t.color.light.rgb + } else { + t.color.base.rgb + }; + let bg_2 = if i > 0 { t.color.base.rgb } else { Reset }; + let mute = Tui::fg_bg(mute, bg_1, "Play "); + let solo = Tui::fg_bg(solo, bg_1, "Solo "); + wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo)))) + }) + } + + pub(crate) fn output_conns (&'a self) -> impl Content + 'a { + Tryptich::top(self.arrangement.h_outputs()) + .left(self.width_side, io_ports( + Tui::g(224), Tui::g(32), ||self.arrangement.midi_outs_with_sizes())) + .middle(self.width_mid, per_track_top(||self.tracks_with_sizes_scrolled(), + |_, t|io_conns( + t.color.dark.rgb, + t.color.darker.rgb, + ||self.arrangement.midi_outs_with_sizes() + ))) + } + +} diff --git a/crates/device/src/arranger/arranger_scene.rs b/crates/device/src/arranger/arranger_scene.rs new file mode 100644 index 00000000..209d5397 --- /dev/null +++ b/crates/device/src/arranger/arranger_scene.rs @@ -0,0 +1,84 @@ +use crate::*; + +impl<'state> Context<'state, SceneCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self, iter) + } +} + +#[tengri_proc::expose] +impl Scene { + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } +} + +#[tengri_proc::command(Scene)] +impl SceneCommand { + fn set_name (scene: &mut Scene, mut name: Arc) -> Perhaps { + std::mem::swap(&mut scene.name, &mut name); + Ok(Some(Self::SetName { name })) + } + fn set_color (scene: &mut Scene, mut color: ItemTheme) -> Perhaps { + std::mem::swap(&mut scene.color, &mut color); + Ok(Some(Self::SetColor { color })) + } + fn set_size (scene: &mut Scene, size: usize) -> Perhaps { + todo!() + } + fn set_zoom (scene: &mut Scene, zoom: usize) -> Perhaps { + todo!() + } +} + +impl> + Send + Sync> HasScene for T {} + +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) + } +} + +#[derive(Debug, Default)] +pub struct Scene { + /// Name of scene + pub name: Arc, + /// Identifying color of scene + pub color: ItemTheme, + /// Clips in scene, one per track + pub clips: Vec>>>, +} + +impl Scene { + /// Returns the pulse length of the longest clip in the scene + pub fn pulses (&self) -> usize { + self.clips.iter().fold(0, |a, p|{ + a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) + }) + } + /// Returns true if all clips in the scene are + /// currently playing on the given collection of tracks. + pub fn is_playing (&self, tracks: &[Track]) -> bool { + self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(c) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(clip))) = track.sequencer().play_clip() { + *clip.read().unwrap() == *c.read().unwrap() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } + pub fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } + } +} diff --git a/crates/device/src/arranger/arranger_scenes.rs b/crates/device/src/arranger/arranger_scenes.rs index 0aca0946..f14d686f 100644 --- a/crates/device/src/arranger/arranger_scenes.rs +++ b/crates/device/src/arranger/arranger_scenes.rs @@ -1,5 +1,162 @@ use crate::*; +pub type SceneWith<'a, T: Send + Sync> = (usize, &'a Scene, usize, usize, T); + +pub trait ArrangerSceneRows: Send + Sync { + /// Default scene height. + const H_SCENE: usize = 2; + /// Default editor height. + const H_EDITOR: usize = 15; + /// Render scenes with clips + fn scenes_view <'a> (&'a self, editor: &'a Option) -> impl Content + 'a { + Tryptich::center(self.scenes_height()) + .left(self.width_side(), self.scenes_names()) + .middle(self.width_mid(), self.scenes_clips(editor)) + } + fn is_editing (&self) -> bool; + fn arrangement (&self) -> &Arrangement; + fn scene_last (&self) -> usize; + fn scene_selected (&self) -> Option; + fn track_selected (&self) -> Option; + fn scenes_height (&self) -> u16; + fn width_side (&self) -> u16; + fn width_mid (&self) -> u16; + fn scenes_names (&self) -> impl Content { + let h = self.scenes_with_prev_color().last().map(|(_,_,_,h,_)|h as u16).unwrap_or(0); + Fixed::y(h, Map::new(move||self.scenes_with_prev_color(), + move|(s, scene, y1, y2, previous): SceneWith<'_, Option>, _|{ + let height = (1 + y2 - y1) as u16; + let name = Some(scene.name.clone()); + let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⯈ ", name)))); + let selected = self.scene_selected() == Some(s); + let neighbor = s > 0 && self.scene_selected() == Some(s - 1); + let is_last = self.scene_last() == s; + let theme = scene.color; + let fg = theme.lightest.rgb; + let bg = if selected { theme.light } else { theme.base }.rgb; + let hi = if let Some(previous) = previous { + if neighbor { previous.light.rgb } else { previous.base.rgb } + } else { + Reset + }; + let lo = if is_last { Reset } else if selected { + theme.light.rgb + } else { + theme.base.rgb + }; + Fill::x(map_south(y1 as u16, height, Fixed::y(height, Phat { + width: 0, height: 0, content, colors: [fg, bg, hi, lo] + }))) + })) + } + fn scenes_with_prev_color (&self) -> impl Iterator>> + Send + Sync { + self.scenes_iter().map(|(s, scene, y1, y2)|(s, scene, y1, y2, + (s>0).then(||self.arrangement().scenes()[s-1].color))) + } + 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 { + Map::new(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + Tui::fg_bg(track.color.lightest.rgb, track.color.darker.rgb, + map_east(x1 as u16, (x2 - x1) as u16, callback(index, track))) }) + } + fn scenes_clips <'a> (&'a self, editor: &'a Option) + -> impl Content + 'a + { + let h = self.scenes_with_prev_color().last().map(|(_,_,_,h,_)|h as u16).unwrap_or(0); + Fixed::y(h, Self::per_track(||self.tracks_with_sizes_scrolled(), + move|track_index, track|Map::new(move||self.scenes_with_clip(track_index), + move|(s, scene, y1, y2, previous): SceneWith<'_, Option>, _|{ + let (name, theme) = if let Some(clip) = &scene.clips[track_index] { + let clip = clip.read().unwrap(); + (Some(clip.name.clone()), clip.color) + } else { + (None, ItemTheme::G[32]) + }; + let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⏹ ", name)))); + let same_track = self.track_selected() == Some(track_index); + let selected = same_track && self.scene_selected() == Some(s); + let neighbor = same_track && s > 0 && self.scene_selected() == Some(s - 1); + let is_last = self.scene_last() == s; + let fg = theme.lightest.rgb; + let bg = if selected { theme.light } else { theme.base }.rgb; + let hi = if let Some(previous) = previous { + if neighbor { previous.light.rgb } else { previous.base.rgb } + } else { + Reset + }; + let lo = if is_last { + Reset + } else if selected { + theme.light.rgb + } else { + theme.base.rgb + }; + let height = (1 + y2 - y1) as u16; + map_south(y1 as u16, height, Bsp::b(Fixed::y(height, Phat { + width: 0, height: 0, content, colors: [fg, bg, hi, lo] + }), When( + self.is_editing() && same_track && self.scene_selected() == Some(s), + editor + ))) + }))) + } + fn scenes_with_clip (&self, track_index: usize) -> impl Iterator>> + Send + Sync { + self.scenes_iter().map(move|(s, scene, y1, y2)|(s, scene, y1, y2, + (s>0).then(||self.arrangement().scenes()[s-1].clips[track_index].as_ref() + .map(|c|c.read().unwrap().color) + .unwrap_or(ItemTheme::G[32])))) + } + /// A scene with size and color. + fn scenes_iter (&self) -> impl Iterator + Send + Sync { + let selection = Has::::get(self.arrangement()); + self.arrangement().scenes_with_sizes( + self.is_editing(), + Self::H_SCENE, Self::H_EDITOR, + selection.track(), selection.scene(), + ).map_while(|(s, scene, y1, y2)|(y2<=self.scenes_height() as usize) + .then_some((s, scene, y1, y2))) + } + fn tracks_with_sizes_scrolled <'t> (&'t self) -> impl TracksSizes<'t> { + self.arrangement() + .tracks_with_sizes( + &self.arrangement().selection(), + self.is_editing().then_some(20/*FIXME*/) + ) + .map_while(move|(t, track, x1, x2)|{ + (self.width_mid() > x2 as u16).then_some((t, track, x1, x2)) + }) + } +} + +impl<'a> ArrangerSceneRows for ArrangerView<'a> { + fn arrangement (&self) -> &Arrangement { + self.arrangement + } + fn scenes_height (&self) -> u16 { + self.scenes_height + } + fn width_side (&self) -> u16 { + self.width_side + } + fn width_mid (&self) -> u16 { + self.width_mid + } + fn scene_selected (&self) -> Option { + self.arrangement.selection.scene() + } + fn scene_last (&self) -> usize { + self.scene_last + } + fn track_selected (&self) -> Option { + self.arrangement.selection.track() + } + fn is_editing (&self) -> bool { + self.is_editing + } +} + impl> + Send + Sync> HasScenes for T {} pub trait HasScenes: Has> + Send + Sync { @@ -28,22 +185,13 @@ pub trait HasScenes: Has> + Send + Sync { } /// Generate the default name for a new scene fn scene_default_name (&self) -> Arc { - format!("s{:3>}", self.scenes().len() + 1).into() + format!("Sc{: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; -} -impl HasSceneScroll for Arrangement { - fn scene_scroll (&self) -> usize { self.scene_scroll } -} - -pub type SceneWith<'a, T: Send + Sync> = (usize, &'a Scene, usize, usize, T); - impl AddScene for T {} pub trait AddScene: HasScenes + HasTracks { @@ -72,85 +220,3 @@ pub trait AddScene: HasScenes + HasTracks { Ok((index, &mut self.scenes_mut()[index])) } } - -#[tengri_proc::expose] -impl Scene { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } -} - -#[tengri_proc::command(Scene)] -impl SceneCommand { - fn set_name (scene: &mut Scene, mut name: Arc) -> Perhaps { - std::mem::swap(&mut scene.name, &mut name); - Ok(Some(Self::SetName { name })) - } - fn set_color (scene: &mut Scene, mut color: ItemTheme) -> Perhaps { - std::mem::swap(&mut scene.color, &mut color); - Ok(Some(Self::SetColor { color })) - } - fn set_size (scene: &mut Scene, size: usize) -> Perhaps { - todo!() - } - fn set_zoom (scene: &mut Scene, zoom: usize) -> Perhaps { - todo!() - } -} - -impl> + Send + Sync> HasScene for T {} - -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) - } -} - -#[derive(Debug, Default)] -pub struct Scene { - /// Name of scene - pub name: Arc, - /// Identifying color of scene - pub color: ItemTheme, - /// Clips in scene, one per track - pub clips: Vec>>>, -} - -impl Scene { - /// Returns the pulse length of the longest clip in the scene - pub fn pulses (&self) -> usize { - self.clips.iter().fold(0, |a, p|{ - a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) - }) - } - /// Returns true if all clips in the scene are - /// currently playing on the given collection of tracks. - pub fn is_playing (&self, tracks: &[Track]) -> bool { - self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(c) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(clip))) = track.sequencer().play_clip() { - *clip.read().unwrap() == *c.read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } - pub fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } - } -} - //scene_scroll: Fill::y(Fixed::x(1, ScrollbarV { - //offset: arrangement.scene_scroll, - //length: h_scenes_area as usize, - //total: h_scenes as usize, - //})), diff --git a/crates/device/src/arranger/arranger_track.rs b/crates/device/src/arranger/arranger_track.rs new file mode 100644 index 00000000..20b1157d --- /dev/null +++ b/crates/device/src/arranger/arranger_track.rs @@ -0,0 +1,175 @@ +use crate::*; + +impl<'state> Context<'state, TrackCommand> for Arrangement { + fn get <'source> (&'state self, iter: &mut TokenIter<'source>) -> Option { + Context::get(&self, iter) + } +} + +#[tengri_proc::expose] +impl Track { + fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } + fn _todo_usize_stub_ (&self) -> usize { todo!() } + fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } + fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } +} + +#[tengri_proc::command(Track)] +impl TrackCommand { + fn set_name (track: &mut Track, mut name: Arc) -> Perhaps { + std::mem::swap(&mut name, &mut track.name); + Ok(Some(Self::SetName { name })) + } + fn set_color (track: &mut Track, mut color: ItemTheme) -> Perhaps { + std::mem::swap(&mut color, &mut track.color); + Ok(Some(Self::SetColor { color })) + } + fn set_mute (track: &mut Track, value: Option) -> Perhaps { + todo!() + } + fn set_solo (track: &mut Track, value: Option) -> Perhaps { + todo!() + } + fn set_rec (track: &mut Track, value: Option) -> Perhaps { + let current = track.sequencer.recording; + let value = value.unwrap_or(!current); + Ok((value != current).then_some(Self::SetRec { value: Some(current) })) + } + fn set_mon (track: &mut Track, value: Option) -> Perhaps { + let current = track.sequencer.monitoring; + let value = value.unwrap_or(!current); + Ok((value != current).then_some(Self::SetRec { value: Some(current) })) + } + fn set_size (track: &mut Track, size: usize) -> Perhaps { + todo!() + } + fn set_zoom (track: &mut Track, zoom: usize) -> Perhaps { + todo!() + } + fn stop (track: &mut Track) -> Perhaps { + track.sequencer.enqueue_next(None); + Ok(None) + } +} + +#[derive(Debug, Default)] +pub struct Track { + /// Name of track + pub name: Arc, + /// Identifying color of track + pub color: ItemTheme, + /// Preferred width of track column + pub width: usize, + /// MIDI sequencer state + pub sequencer: Sequencer, + /// Device chain + pub devices: Vec, +} + +has!(Clock: |self: Track|self.sequencer.clock); +has!(Sequencer: |self: Track|self.sequencer); + +impl Track { + /// Create a new track with only the default [Sequencer]. + pub fn new ( + name: &impl AsRef, + color: Option, + jack: &Jack, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[PortConnect], + midi_to: &[PortConnect], + ) -> Usually { + Ok(Self { + name: name.as_ref().into(), + color: color.unwrap_or_default(), + sequencer: Sequencer::new( + format!("{}/sequencer", name.as_ref()), + jack, + clock, + clip, + midi_from, + midi_to + )?, + ..Default::default() + }) + } + /// Create a new track connecting the [Sequencer] to a [Sampler]. + pub fn new_with_sampler ( + name: &impl AsRef, + color: Option, + jack: &Jack, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[PortConnect], + midi_to: &[PortConnect], + audio_from: &[&[PortConnect];2], + audio_to: &[&[PortConnect];2], + ) -> Usually { + let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; + track.devices.push(Device::Sampler(Sampler::new( + jack, + &format!("{}/sampler", name.as_ref()), + &[PortConnect::exact(format!("{}:{}", + jack.with_client(|c|c.name().to_string()), + track.sequencer.midi_outs[0].name() + ))], + audio_from, + audio_to + )?)); + Ok(track) + } + #[cfg(feature = "sampler")] + pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { + for device in self.devices.iter() { + match device { + Device::Sampler(s) => if nth == 0 { + return Some(s); + } else { + nth -= 1; + }, + _ => {} + } + } + None + } + #[cfg(feature = "sampler")] + pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { + for device in self.devices.iter_mut() { + match device { + Device::Sampler(s) => if nth == 0 { + return Some(s); + } else { + nth -= 1; + }, + _ => {} + } + } + None + } +} + +pub trait HasTrack { + fn track (&self) -> Option<&Track>; + fn track_mut (&mut self) -> Option<&mut Track>; +} + +//impl>> HasTrack for T { + //fn track (&self) -> Option<&Track> { + //self.get().as_ref() + //} + //fn track_mut (&mut self) -> Option<&mut Track> { + //self.get_mut().as_mut() + //} +//} + +impl> + Has> HasTrack for T { + fn track (&self) -> Option<&Track> { + let index = Has::::get(self).track()?; + Has::>::get(self).get(index) + } + fn track_mut (&mut self) -> Option<&mut Track> { + let index = Has::::get(self).track()?; + Has::>::get_mut(self).get_mut(index) + } +} diff --git a/crates/device/src/arranger/arranger_tracks.rs b/crates/device/src/arranger/arranger_tracks.rs index 08d9a121..471c0b88 100644 --- a/crates/device/src/arranger/arranger_tracks.rs +++ b/crates/device/src/arranger/arranger_tracks.rs @@ -45,9 +45,11 @@ pub trait HasTracks: Has> + Send + Sync { } } /// Iterate over tracks with their corresponding sizes. - fn tracks_with_sizes (&self, selection: &Selection, editor_width: Option) - -> impl TracksSizes<'_> - { + fn tracks_with_sizes ( + &self, + selection: &Selection, + editor_width: Option + ) -> impl TracksSizes<'_> { let mut x = 0; let active_track = if let Some(width) = editor_width { selection.track() @@ -67,263 +69,63 @@ pub trait HasTracks: Has> + Send + Sync { const TRACK_SPACING: usize = 0; } -pub trait HasTrackScroll: HasTracks { - fn track_scroll (&self) -> usize; -} -impl HasTrackScroll for Arrangement { - fn track_scroll (&self) -> usize { self.track_scroll } -} +impl Arrangement { + /// Add multiple tracks + pub fn tracks_add ( + &mut self, + count: usize, + width: Option, + mins: &[PortConnect], + mouts: &[PortConnect], + ) -> Usually<()> { + let jack = self.jack().clone(); + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let mut track = self.track_add(None, Some(color), mins, mouts)?.1; + if let Some(width) = width { + track.width = width; + } + } + Ok(()) + } -impl> HasTrack for T { - fn track (&self) -> Option<&Track> { - self.get() - } - fn track_mut (&mut self) -> Option<&mut Track> { - self.get_mut() - } -} - -#[tengri_proc::expose] -impl Track { - fn _todo_opt_bool_stub_ (&self) -> Option { todo!() } - fn _todo_usize_stub_ (&self) -> usize { todo!() } - fn _todo_arc_str_stub_ (&self) -> Arc { todo!() } - fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } -} - -#[tengri_proc::command(Track)] -impl TrackCommand { - fn set_name (track: &mut Track, mut name: Arc) -> Perhaps { - std::mem::swap(&mut name, &mut track.name); - Ok(Some(Self::SetName { name })) - } - fn set_color (track: &mut Track, mut color: ItemTheme) -> Perhaps { - std::mem::swap(&mut color, &mut track.color); - Ok(Some(Self::SetColor { color })) - } - fn set_mute (track: &mut Track, value: Option) -> Perhaps { - todo!() - } - fn set_solo (track: &mut Track, value: Option) -> Perhaps { - todo!() - } - fn set_rec (track: &mut Track, value: Option) -> Perhaps { - let current = track.sequencer.recording; - let value = value.unwrap_or(!current); - Ok((value != current).then_some(Self::SetRec { value: Some(current) })) - } - fn set_mon (track: &mut Track, value: Option) -> Perhaps { - let current = track.sequencer.monitoring; - let value = value.unwrap_or(!current); - Ok((value != current).then_some(Self::SetRec { value: Some(current) })) - } - fn set_size (track: &mut Track, size: usize) -> Perhaps { - todo!() - } - fn set_zoom (track: &mut Track, zoom: usize) -> Perhaps { - todo!() - } - fn stop (track: &mut Track) -> Perhaps { - track.sequencer.enqueue_next(None); - Ok(None) - } -} - -#[derive(Debug, Default)] -pub struct Track { - /// Name of track - pub name: Arc, - /// Identifying color of track - pub color: ItemTheme, - /// Preferred width of track column - pub width: usize, - /// MIDI sequencer state - pub sequencer: Sequencer, - /// Device chain - pub devices: Vec, -} - -has!(Clock: |self: Track|self.sequencer.clock); -has!(Sequencer: |self: Track|self.sequencer); - -impl Track { - /// Create a new track with only the default [Sequencer]. - pub fn new ( - name: &impl AsRef, - color: Option, - jack: &Jack, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[PortConnect], - midi_to: &[PortConnect], - ) -> Usually { - Ok(Self { - name: name.as_ref().into(), - color: color.unwrap_or_default(), + /// Add a track + pub fn track_add ( + &mut self, + name: Option<&str>, + color: Option, + mins: &[PortConnect], + mouts: &[PortConnect], + ) -> Usually<(usize, &mut Track)> { + self.track_last += 1; + let name: Arc = name.map_or_else( + ||format!("Track{:02}", self.track_last).into(), + |x|x.to_string().into() + ); + let mut track = Track { + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemTheme::random), sequencer: Sequencer::new( - format!("{}/sequencer", name.as_ref()), - jack, - clock, - clip, - midi_from, - midi_to + &format!("{name}"), + self.jack(), + Some(self.clock()), + None, + mins, + mouts )?, + name, ..Default::default() - }) - } - /// Create a new track connecting the [Sequencer] to a [Sampler]. - pub fn new_with_sampler ( - name: &impl AsRef, - color: Option, - jack: &Jack, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[PortConnect], - midi_to: &[PortConnect], - audio_from: &[&[PortConnect];2], - audio_to: &[&[PortConnect];2], - ) -> Usually { - let mut track = Self::new(name, color, jack, clock, clip, midi_from, midi_to)?; - track.devices.push(Device::Sampler(Sampler::new( - jack, - &format!("{}/sampler", name.as_ref()), - &[PortConnect::exact(format!("{}:{}", - jack.with_client(|c|c.name().to_string()), - track.sequencer.midi_outs[0].name() - ))], - audio_from, - audio_to - )?)); - Ok(track) - } - #[cfg(feature = "sampler")] - pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { - for device in self.devices.iter() { - match device { - Device::Sampler(s) => if nth == 0 { - return Some(s); - } else { - nth -= 1; - }, - _ => {} + }; + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); } } - None - } - #[cfg(feature = "sampler")] - pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { - for device in self.devices.iter_mut() { - match device { - Device::Sampler(s) => if nth == 0 { - return Some(s); - } else { - nth -= 1; - }, - _ => {} - } - } - None + Ok((index, &mut self.tracks_mut()[index])) } } - -pub trait HasTrack { - fn track (&self) -> Option<&Track>; - fn track_mut (&mut self) -> Option<&mut Track>; - fn view_midi_ins_status (&self, theme: ItemTheme) -> impl Content { - self.track().map(|track|{ - let ins = track.sequencer.midi_ins.len() as u16; - Fixed::xy(20, 1 + ins, Outer(true, Style::default().fg(Tui::g(96))).enclose( - Fixed::xy(20, 1 + ins, FieldV(theme, format!("MIDI ins: "), - Map::south(1, ||track.sequencer.midi_ins.iter(), - |port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))}) - } - fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Content { - self.track().map(|track|{ - let outs = track.sequencer.midi_outs.len() as u16; - Fixed::xy(20, 1 + outs, Outer(true, Style::default().fg(Tui::g(96))).enclose( - Fixed::xy(20, 1 + outs, FieldV(theme, format!("MIDI outs: "), - Map::south(1, ||track.sequencer.midi_outs.iter(), - |port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))}) - } - fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Content { - self.track().and_then(|track|track.devices.get(0)).map(|device|{ - let ins = device.audio_ins().len() as u16; - Fixed::xy(20, 1 + ins, Outer(true, Style::default().fg(Tui::g(96))).enclose( - Fixed::xy(20, 1 + ins, FieldV(theme, format!("Audio ins: "), - Map::south(1, ||device.audio_ins().iter(), - |port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))}) - } - fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Content { - self.track().and_then(|track|track.devices.last()).map(|device|{ - let outs = device.audio_outs().len() as u16; - Fixed::xy(20, 1 + outs, Outer(true, Style::default().fg(Tui::g(96))).enclose( - Fixed::xy(20, 1 + outs, FieldV(theme, format!("Audio outs: "), - Map::south(1, ||device.audio_outs().iter(), - |port, index|Fill::x(Align::w(format!(" {index} {}", port.name()))))))))}) - } -} - -impl Track { - pub fn per <'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 { - 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(crate) 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(crate) 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(crate) fn io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a -) -> impl Content + 'a { - Map::new(iter, move|( - index, name, connections, y, y2 - ): (usize, &'a Arc, &'a [PortConnect], usize, usize), _| - map_south(y as u16, (y2-y) as u16, Bsp::s( - Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(" 󰣲 ", name))))), - Map::new(||connections.iter(), move|connect: &'a PortConnect, index|map_south(index as u16, 1, - Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, - &connect.info))))))))) -} - -pub(crate) fn io_conns <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a -) -> impl Content + 'a { - Map::new(iter, move|( - index, name, connections, y, y2 - ): (usize, &'a Arc, &'a [PortConnect], usize, usize), _| - map_south(y as u16, (y2-y) as u16, Bsp::s( - Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w("▞▞▞▞ ▞▞▞▞"))))), - Map::new(||connections.iter(), move|connect, index|map_south(index as u16, 1, - Fill::x(Align::w(Tui::bold(false, wrap(bg, fg, Fill::x("")))))))))) -} - //track_scroll: Fill::x(Fixed::y(1, ScrollbarH { - //offset: arrangement.track_scroll, - //length: h_tracks_area as usize, - //total: h_scenes as usize, - //})), diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs index ac132a9f..b464a870 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -1,159 +1,94 @@ use crate::*; -impl Content for Arrangement { - fn content (&self) -> impl Render { - let ins = |x|Bsp::n(self.view_inputs_0(), x); - let tracks = |x|Bsp::s(self.view_tracks_0(), x); - let devices = |x|Bsp::s(self.view_devices_0(), x); - let outs = |x|Bsp::s(self.view_outputs_0(), x); - let bg = |x|Tui::bg(Reset, x); - //let track_scroll = |x|Bsp::s(&self.track_scroll, x); - //let scene_scroll = |x|Bsp::e(&self.scene_scroll, x); - self.size.of(outs(tracks(devices(ins(bg(self.view_scenes_clips(&None))))))) +pub struct ArrangerView<'a> { + pub arrangement: &'a Arrangement, + pub is_editing: bool, + pub width: u16, + pub width_mid: u16, + pub width_side: u16, + pub scene_last: usize, + pub scene_scroll: Fill>, + pub scene_selected: Option, + /// Height available to display scene/track content. + pub scenes_height: u16, + pub track_scroll: Fill>, + pub track_selected: Option, + /// Height available to display track headers. + pub tracks_height: u16, +} + +impl<'a> ArrangerView<'a> { + pub fn new ( + arrangement: &'a Arrangement, + editor: Option<&'a MidiEditor> + ) -> Self { + let is_editing = editor.is_some(); + let h_tracks_area = 5; + let h_scenes_area = (arrangement.height() as u16).saturating_sub(20); + let h_scenes = arrangement.h_scenes(is_editing); + Self { + arrangement, + is_editing, + width: arrangement.w_tracks_area(is_editing), + width_mid: arrangement.w_tracks_area(is_editing).saturating_sub(20), + width_side: 20, + scenes_height: h_scenes_area, + scene_selected: arrangement.selection().scene(), + scene_last: arrangement.scenes.len().saturating_sub(1), + scene_scroll: Fill::y(Fixed::x(1, ScrollbarV { + offset: arrangement.scene_scroll, + length: h_scenes_area as usize, + total: h_scenes as usize, + })), + tracks_height: h_tracks_area, + track_selected: arrangement.selection().track(), + track_scroll: Fill::x(Fixed::y(1, ScrollbarH { + offset: arrangement.track_scroll, + length: h_tracks_area as usize, + total: h_scenes as usize, + })), + } } } -impl Arrangement { +impl<'a> Content for ArrangerView<'a> { + fn content (&self) -> impl Render { + let ins = |x|Bsp::n(self.inputs(), x); + let tracks = |x|Bsp::s(self.tracks(), x); + let devices = |x|Bsp::s(self.devices(), x); + let outs = |x|Bsp::s(self.outputs(), x); + let bg = |x|Tui::bg(Reset, x); + //let track_scroll = |x|Bsp::s(&self.track_scroll, x); + //let scene_scroll = |x|Bsp::e(&self.scene_scroll, x); + self.arrangement.size.of(outs(tracks(devices(ins(bg(self.scenes_view(&None))))))) + } +} + +impl<'a> ArrangerView<'a> { /// Render input matrix. - fn view_inputs_0 (&self) -> impl Content + '_ { + pub(crate) fn inputs (&'a self) -> impl Content + 'a { Tui::bg(Reset, Bsp::s( - self.view_input_intos(), - Bsp::s(self.view_input_routes(), self.view_input_ports()), + self.input_intos(), + Bsp::s(self.input_routes(), self.input_ports()), )) } - fn view_input_ports (&self) -> impl Content + '_ { - let is_editing = false; //FIXME - Tryptich::top(1) - .left(20, button_3("i", "midi ins", format!("{}", - self.midi_ins().len()), is_editing)) - .right(20, button_2("I", "add midi in", is_editing)) - .middle(self.width().saturating_sub(40) as u16, - per_track_top(||self.tracks_with_sizes_scrolled(), - move|t, track|{ - let rec = track.sequencer.recording; - let mon = track.sequencer.monitoring; - let rec = if rec { White } else { track.color.darkest.rgb }; - let mon = if mon { White } else { track.color.darkest.rgb }; - let bg = if self.track_selected() == Some(t) { - track.color.light.rgb - } else { - track.color.base.rgb - }; - //let bg2 = if t > 0 { track.color.base.rgb } else { Reset }; - wrap(bg, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e( - Tui::fg_bg(rec, bg, "Rec "), - Tui::fg_bg(mon, bg, "Mon "), - )))) - })) - } - fn view_input_routes (&self) -> impl Content + '_ { - Tryptich::top(self.h_inputs()) - .left(self.width_side(), - io_ports(Tui::g(224), Tui::g(32), ||self.midi_ins_with_sizes())) - .middle(self.width_mid(), - per_track_top(||self.tracks_with_sizes_scrolled(), - move|_, &Track { color, .. }|io_conns( - color.dark.rgb, color.darker.rgb, ||self.midi_ins_with_sizes()))) - } - fn view_input_intos (&self) -> impl Content + '_ { - Tryptich::top(2) - .left(self.width_side(), - Bsp::s(Align::e("Input:"), Align::e("Into clip:"))) - .middle(self.width_mid(), - per_track_top(||self.tracks_with_sizes_scrolled(), - |_, _|Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ "))))) - } /// Render output matrix. - fn view_outputs_0 (&self) -> impl Content + '_ { + pub(crate) fn outputs (&'a self) -> impl Content + 'a { Tui::bg(Reset, Align::n(Bsp::s( - Bsp::s(self.view_output_ports(), self.view_output_conns()), - Bsp::s(self.view_output_nexts(), self.view_output_froms()), + Bsp::s(self.output_ports(), self.output_conns()), + Bsp::s(self.output_nexts(), self.output_froms()), ))) } - fn view_output_ports (&self) -> impl Content + '_ { - Tryptich::top(1) - .left(self.width_side(), self.view_output_count()) - .right(self.width_side(), self.view_output_add()) - .middle(self.width_mid(), self.view_output_map()) - } - fn view_output_count (&self) -> impl Content { - button_3( - "o", - "midi outs", - format!("{}", self.midi_outs().len()), - false // self.is_editing() - ) - } - fn view_output_add (&self) -> impl Content { - button_2("O", "add midi out", false /* is_editing */) - } - fn view_output_map (&self) -> impl Content + '_ { - per_track_top(||self.tracks_with_sizes_scrolled(), move|i, t|{ - let mute = false; - let solo = false; - let mute = if mute { White } else { t.color.darkest.rgb }; - let solo = if solo { White } else { t.color.darkest.rgb }; - let bg_1 = if self.track_selected() == Some(i) { - t.color.light.rgb - } else { - t.color.base.rgb - }; - let bg_2 = if i > 0 { t.color.base.rgb } else { Reset }; - let mute = Tui::fg_bg(mute, bg_1, "Play "); - let solo = Tui::fg_bg(solo, bg_1, "Solo "); - wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo)))) - }) - } - fn view_output_conns (&self) -> impl Content + '_ { - Tryptich::top(self.h_outputs()) - .left(self.width_side(), io_ports( - Tui::g(224), Tui::g(32), ||self.midi_outs_with_sizes())) - .middle(self.width_mid(), per_track_top(||self.tracks_with_sizes_scrolled(), - |_, t|io_conns( - t.color.dark.rgb, - t.color.darker.rgb, - ||self.midi_outs_with_sizes() - ))) - } - fn view_output_nexts (&self) -> impl Content + '_ { - Tryptich::top(2).left(self.width_side(), Align::ne("From clip:")) - .middle(self.width_mid(), per_track_top(||self.tracks_with_sizes_scrolled(), - |_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default()))))) - } - fn view_output_froms (&self) -> impl Content + '_ { - let label = Align::ne("Next clip:"); - Tryptich::top(2).left(self.width_side(), label) - .middle(self.width_mid(), per_track_top( - ||self.tracks_with_sizes_scrolled(), |t, track|{ - let queued = track.sequencer.next_clip.is_some(); - let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ ")); - let queued_clip = Thunk::new(||{ - Tui::bg(Reset, if let Some((_, clip)) = track.sequencer.next_clip.as_ref() { - if let Some(clip) = clip { - clip.read().unwrap().name.clone() - } else { - "Stop".into() - } - } else { - "".into() - }) - }); - Either(queued, queued_clip, queued_blank) - })) - } /// Render track headers - fn view_tracks_0 (&self) -> impl Content + '_ { - let width_side = self.width_side(); - let width_mid = self.width_mid(); - let is_editing = false; // FIXME - let track_selected = self.track_selected(); + pub(crate) fn tracks (&'a self) -> impl Content + 'a { + let Self { width_side, width_mid, track_selected, is_editing, .. } = self; Tryptich::center(3) - .left(width_side, - button_3("t", "track", format!("{}", self.tracks().len()), is_editing)) - .right(width_side, button_2("T", "add track", is_editing)) - .middle(width_mid, per_track(||self.tracks_with_sizes_scrolled(), - move|index, track|wrap( - if track_selected == Some(index) { + .left(*width_side, + button_3("t", "track", format!("{}", self.arrangement.tracks.len()), *is_editing)) + .right(*width_side, button_2("T", "add track", *is_editing)) + .middle(*width_mid, per_track(||self.tracks_with_sizes_scrolled(), + |index, track|wrap( + if *track_selected == Some(index) { track.color.light } else { track.color.base @@ -163,17 +98,14 @@ impl Arrangement { ))) } /// Render device switches. - fn view_devices_0 (&self) -> impl Content + '_ { - let width_side = self.width_side(); - let width_mid = self.width_mid(); - let is_editing = false; // FIXME - let track_selected = self.track_selected(); + pub(crate) fn devices (&'a self) -> impl Content + 'a { + let Self { width_side, width_mid, track_selected, is_editing, .. } = self; Tryptich::top(1) - .left(width_side, button_3("d", "devices", format!("{}", 0), is_editing)) - .right(width_side, button_2("D", "add device", is_editing)) - .middle(width_mid, per_track_top(||self.tracks_with_sizes_scrolled(), + .left(*width_side, button_3("d", "devices", format!("{}", 0), *is_editing)) + .right(*width_side, button_2("D", "add device", *is_editing)) + .middle(*width_mid, per_track_top(||self.tracks_with_sizes_scrolled(), move|index, track|{ - let bg = if track_selected == Some(index) { + let bg = if *track_selected == Some(index) { track.color.light } else { track.color.base @@ -184,363 +116,22 @@ impl Arrangement { } } -impl TracksView for T -where T: HasSize + HasTrackScroll + HasSelection + HasMidiIns {} - -impl ClipsView for Arrangement {} - -pub trait TracksView: HasSize + HasTrackScroll + HasSelection + HasMidiIns { - fn is_editing (&self) -> bool { false } - fn tracks_width_available (&self) -> u16 { - (self.width() as u16).saturating_sub(40) - } - fn tracks_with_sizes_scrolled <'t> (&'t self) -> impl TracksSizes<'t> { - self.tracks_with_sizes(&self.selection(), self.is_editing().then_some(20/*FIXME*/)) - .map_while(move|(t, track, x1, x2)| - ((x2 as u16) < self.tracks_width_available()) - .then_some((t, track, x1, x2))) - } - fn view_track_names (&self, theme: ItemTheme) -> impl Content { - let content = Fixed::y(1, Align::w(Tui::bg(theme.darker.rgb, Align::w(Fill::x( - Stack::east(move|add: &mut dyn FnMut(&dyn Render)|{ - for (index, track, x1, x2) in self - .tracks_with_sizes(&self.selection(), None) - .skip(self.track_scroll()) - { - add(&Fixed::x(track.width as u16, - Tui::bg(if self.selection().track() == Some(index) { - track.color.light.rgb - } else { - track.color.base.rgb - }, Align::nw(Tui::fg( - Rgb(255, 255, 255), Tui::bold(true, - format!("{}", track.name))))))); - } - })))))); - Bsp::w( - self.view_track_header(theme, row!( - Tui::bold(true, button_2("t", "rack ", false)), - button_2("T", "+", false) - )), - content - ) - } - fn view_track_outputs <'a> (&'a self, theme: ItemTheme) -> impl Content { - let mut max_outputs = 0u16; - for track in self.tracks().iter() { - max_outputs = max_outputs.max(track.sequencer.midi_outs.len() as u16); - } - let content = Align::w(Fixed::y(max_outputs + 1, - Tui::bg(theme.darker.rgb, Align::w(Fill::x( - Stack::east(move|add: &mut dyn FnMut(&dyn Render)|{ - for (index, track, x1, x2) in self - .tracks_with_sizes(&self.selection(), None) - .skip(self.track_scroll()) - { - (add)(&Fixed::x(track.width as u16, Align::nw(Bsp::s( - Tui::bg(track.color.base.rgb, Fill::x(Align::w(format!("[mut] [sol]")))), - Map::south(1, ||track.sequencer.midi_outs.iter(), - |port, index|Tui::fg(Rgb(255, 255, 255), - Tui::bg(track.color.dark.rgb, Fill::x(Align::w( - format!("{index}: {}", port.name())))))))))); - } - })))))); - Bsp::w( - self.view_track_header(theme, row!( - Tui::bold(true, button_2("o", "utput", false)), - button_2("O", "+", false) - )), - content - ) - } - fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content { - let mut h = 0u16; - for track in self.tracks().iter() { - h = h.max(track.sequencer.midi_ins.len() as u16); - } - let content = Tui::bg(theme.darker.rgb, Align::w(Fill::x( - Stack::east(move|add: &mut dyn FnMut(&dyn Render)|{ - for (index, track, x1, x2) in self - .tracks_with_sizes(&self.selection(), None) - .skip(self.track_scroll()) - { - add(&Fixed::xy(track.width as u16, h + 1, - Align::nw(Bsp::s( - Tui::bg(track.color.base.rgb, - Fill::x(Align::w(format!("[rec] [mon]")))), - Map::south(1, ||track.sequencer.midi_ins.iter(), - |port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb, - Fill::x(Align::w(format!("{index}: {}", port.name()))))))))); - } - - })))); - Bsp::w( - self.view_track_header(theme, row!( - Tui::bold(true, button_2("i", "nputs", false)), - button_2("I", "+", false) - )), - Fixed::y(h, Fill::x(Align::w(Fixed::y(h + 1, content)))), - ) - } - fn view_track_devices <'a> (&'a self, theme: ItemTheme) -> impl Content { - let mut h = 2u16; - for track in self.tracks().iter() { - h = h.max(track.devices.len() as u16); - } - Bsp::w( - self.view_track_header(theme, row!( - Tui::bold(true, button_2("d", "evice", false)), - button_2("D", "+", false) - )), - Fixed::y(h, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Stack::east( - move|add: &mut dyn FnMut(&dyn Render)|{ - for (index, track, x1, x2) in self - .tracks_with_sizes(&self.selection(), None) - .skip(self.track_scroll()) - { - add(&Fixed::xy(track.width as u16, h + 1, - Tui::bg(track.color.dark.rgb, Align::nw(Map::south(1, move||0..h, - |_, index|format!("{index}: {}", "--------")))))); - } - })))))) - } - fn view_track_header <'a, T: Content> ( - &'a self, theme: ItemTheme, content: T - ) -> impl Content { - Fixed::x(20, Tui::bg(theme.darker.rgb, Fill::x(Align::e(content)))) - } +pub(crate) 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 trait ScenesView: HasSelection + HasSceneScroll + Send + Sync { - /// Default scene height. - const H_SCENE: usize = 2; - /// Default editor height. - const H_EDITOR: usize = 15; - fn arrangement (&self) -> &Arrangement; - fn scene_selected (&self) -> Option; - fn track_selected (&self) -> Option; - fn scenes_height (&self) -> u16; - fn width_side (&self) -> u16; - fn width_mid (&self) -> u16; - fn view_scenes_names (&self) -> impl Content { - Stack::south(move|add: &mut dyn FnMut(&dyn Render)|{ - for (index, scene) in self.scenes().iter().enumerate().skip(self.scene_scroll()) { - add(&self.view_scene_name(index, scene)); - } - }) - } - fn view_scene_name (&self, index: usize, scene: &Scene) -> impl Content { - Fixed::xy(20, 2, Tui::bg(if self.selection().scene() == Some(index) { - scene.color.light.rgb - } else { - scene.color.base.rgb - }, Align::nw(Bsp::e( - format!(" {index:2} "), - Tui::fg(Rgb(255, 255, 255), - Tui::bold(true, format!("{}", scene.name))))))) - //let height = (1 + y2 - y1) as u16; - //let name = Some(scene.name.clone()); - //let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⯈ ", name)))); - //let selected = self.scene_selected() == Some(s); - //let neighbor = s > 0 && self.scene_selected() == Some(s - 1); - //let is_last = self.scenes().len().saturating_sub(1) == s; - //let theme = scene.color; - //let fg = theme.lightest.rgb; - //let bg = if selected { theme.light } else { theme.base }.rgb; - //let hi = if let Some(previous) = previous { - //if neighbor { previous.light.rgb } else { previous.base.rgb } - //} else { - //Reset - //}; - //let lo = if is_last { Reset } else if selected { - //theme.light.rgb - //} else { - //theme.base.rgb - //}; - //add(&Fill::x(map_south(y1 as u16, height, Fixed::y(height, Phat { - //width: 0, height: 0, content, colors: [fg, bg, hi, lo] - //})))) - //} - } - fn scenes_with_prev_color (&self) -> impl Iterator>> + Send + Sync { - self.scenes_iter().map(|(s, scene, y1, y2)|(s, scene, y1, y2, - (s>0).then(||self.arrangement().scenes()[s-1].color))) - } - 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 { - Map::new(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - Tui::fg_bg(track.color.lightest.rgb, track.color.darker.rgb, - map_east(x1 as u16, (x2 - x1) as u16, callback(index, track))) }) - } - fn scenes_with_clip (&self, track_index: usize) -> impl Iterator>> + Send + Sync { - self.scenes_iter().map(move|(s, scene, y1, y2)|(s, scene, y1, y2, - (s>0).then(||self.arrangement().scenes()[s-1].clips[track_index].as_ref() - .map(|c|c.read().unwrap().color) - .unwrap_or(ItemTheme::G[32])))) - } - /// A scene with size and color. - fn scenes_iter (&self) -> impl Iterator + Send + Sync { - let selection = Has::::get(self.arrangement()); - self.arrangement().scenes_with_sizes( - false, // FIXME self.is_editing(), - Self::H_SCENE, Self::H_EDITOR, - selection.track(), selection.scene(), - ).map_while(|(s, scene, y1, y2)|(y2<=self.scenes_height() as usize) - .then_some((s, scene, y1, y2))) - } - /// Height required to display all scenes. - fn scenes_height_total (&self, is_editing: bool) -> u16 { - self.scenes_with_sizes( - is_editing, - Self::H_SCENE, - Self::H_EDITOR, - self.selection().track(), - self.selection().scene(), - ) - .last() - .map(|(_, _, _, y)|y as u16).unwrap_or(0) - } -} - -pub trait ClipsView: TracksView + ScenesView + Send + Sync { - fn view_scenes_clips <'a> (&'a self, editor: &'a Option) - -> impl Content + 'a - { - Fill::xy(Stack::::east(move|column: &mut dyn FnMut(&dyn Render)|{ - for (track_index, track, _, _) in self - .tracks_with_sizes(&self.selection(), None) - .skip(self.track_scroll()) - { - //column(&Fixed::x(5, Fill::xy(Tui::bg(Green, "kyp")))); - column(&Fixed::x( - track.width as u16, - Fill::y(self.view_track_clips(track_index, track)) - )) - } - })) - } - - fn view_track_clips <'a> (&'a self, track_index: usize, track: &Track) -> impl Content { - Stack::south(move|cell: &mut dyn FnMut(&dyn Render)|{ - for (scene_index, scene) in self.scenes().iter().enumerate().skip(self.scene_scroll()) { - let (name, theme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.read().unwrap(); - (Some(clip.name.clone()), clip.color) - } else { - (None, ItemTheme::G[32]) - }; - let fg = theme.lightest.rgb; - let mut outline = theme.base.rgb; - let bg = if self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - { - outline = theme.lightest.rgb; - theme.light.rgb - } else if self.selection().track() == Some(track_index) - || self.selection().scene() == Some(scene_index) - { - outline = theme.darkest.rgb; - theme.base.rgb - } else { - theme.dark.rgb - }; - cell(&Fixed::xy(track.width as u16, 2, Bsp::b( - Fill::xy(Outer(true, Style::default().fg(outline))), - Fill::xy(Align::nw(Tui::fg_bg(fg, bg, Align::nw(name.unwrap_or(" ---- ".into())))))))); - //let (name, theme) = if let Some(clip) = &scene.clips.get(track_index).flatten() { - //let clip = clip.read().unwrap(); - //(Some(clip.name.clone()), clip.color) - //} else { - //(None, ItemTheme::G[32]) - //}; - //let content = Fill::x(Align::w(Tui::bold(true, Bsp::e(" ⏹ ", name)))); - //let same_track = self.track_selected() == Some(track_index); - //let selected = same_track && self.scene_selected() == Some(s); - //let neighbor = same_track && s > 0 && self.scene_selected() == Some(s - 1); - //let is_last = self.scenes().len().saturating_sub(1) == s; - //let fg = theme.lightest.rgb; - //let bg = if selected { theme.light } else { theme.base }.rgb; - //let hi = if let Some(previous) = previous { - //if neighbor { previous.light.rgb } else { previous.base.rgb } - //} else { - //Reset - //}; - //let lo = if is_last { - //Reset - //} else if selected { - //theme.light.rgb - //} else { - //theme.base.rgb - //}; - //let height = (1 + y2 - y1) as u16; - //let is_editing = false; //FIXME - //let editor = (); //FIXME - //cell(&Fixed::xy(track.width as u16, 2, Bsp::b(Fixed::y(height, Phat { - //width: 0, height: 0, content, colors: [fg, bg, hi, lo] - //}), When( - //is_editing && same_track && self.scene_selected() == Some(s), - //editor - //)))) - } - }) - } - - fn scenes_clips_2 <'a> ( - &'a self, - theme: ItemTheme - ) -> impl Content + 'a { - Fixed::y(self.scenes().len() as u16 * 2, Tui::bg(theme.darker.rgb, - Align::w(Fill::x(Map::new(||self.scenes().iter().skip(self.scene_scroll()), - move|scene: &'a Scene, index|self.track_scenes(index, scene)))))) - } - fn track_scenes <'a> ( - &'a self, - scene_index: usize, - scene: &'a Scene - ) -> impl Content + 'a { - Push::y(scene_index as u16 * 2u16, Fixed::xy(20, 2, Map::new( - move||scene.clips.iter().skip(self.track_scroll()), - move|clip: &'a Option>>, track_index| - self.track_scene_clip(scene_index, scene, track_index, clip)))) - } - fn track_scene_clip ( - &self, - scene_index: usize, - scene: &Scene, - track_index: usize, - clip: &Option>> - ) -> impl Content { - let (theme, text) = if let Some(clip) = clip { - let clip = clip.read().unwrap(); - (clip.color, clip.name.clone()) - } else { - (scene.color, Default::default()) - }; - Push::x(track_index as u16 * 14, Tui::bg(theme.dark.rgb, Bsp::e( - format!(" {scene_index:2} {track_index:2} "), - Tui::fg(Rgb(255, 255, 255), - Tui::bold(true, format!("{}", text)))))) - } -} - -pub trait HasWidth { - const MIN_WIDTH: usize; - /// Increment track width. - fn width_inc (&mut self); - /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. - fn width_dec (&mut self); -} - -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; - } - } +pub(crate) 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)))) } diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 26131511..a9f2686e 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -13,7 +13,7 @@ pub(crate) use std::path::PathBuf; pub(crate) use std::error::Error; pub(crate) use std::ffi::OsString; -pub(crate) use ::tengri::{from, has, maybe_has, Usually, Perhaps, Has, MaybeHas}; +pub(crate) use ::tengri::{from, Usually, Perhaps, Has}; pub(crate) use ::tengri::{dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}}; pub(crate) use ::tek_engine::*; pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage}; diff --git a/crates/device/src/sampler/sampler_view.rs b/crates/device/src/sampler/sampler_view.rs index 7b21233d..bfb344ff 100644 --- a/crates/device/src/sampler/sampler_view.rs +++ b/crates/device/src/sampler/sampler_view.rs @@ -119,13 +119,13 @@ impl Sampler { } pub fn view_sample_status (&self, note_pt: usize) -> impl Content + use<'_> { - Fixed::x(20, draw_info_v(if let Some((_, sample)) = &self.recording { + Fixed::x(20, Outer(true, Style::default().fg(Tui::g(96))).enclose(draw_info_v(if let Some((_, sample)) = &self.recording { Some(sample) } else if let Some(sample) = &self.mapped[note_pt] { Some(sample) } else { None - })) + }))) } pub fn view_status (&self, index: usize) -> impl Content { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 31eb7e92..9acdc1cb 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -9,7 +9,7 @@ pub(crate) use std::sync::{Arc, atomic::{AtomicUsize, AtomicBool, Ordering::Rela pub(crate) use std::fmt::Debug; pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; -pub(crate) use ::tengri::{from, Usually, Perhaps, Has, has, maybe_has, tui::*}; +pub(crate) use ::tengri::{from, Usually, Perhaps, Has, tui::*}; pub use ::atomic_float; pub(crate) use atomic_float::*; @@ -23,6 +23,15 @@ pub use ::atomic_float; pub(crate) use atomic_float::*; //} //} +#[macro_export] macro_rules! has { + ($T:ty: |$self:ident : $S:ty| $x:expr) => { + impl Has<$T> for $S { + fn get (&$self) -> &$T { &$x } + fn get_mut (&mut $self) -> &mut $T { &mut $x } + } + }; +} + #[macro_export] macro_rules! as_ref { ($T:ty: |$self:ident : $S:ty| $x:expr) => { impl AsRef<$T> for $S { diff --git a/deps/tengri b/deps/tengri index a55e84c2..4ff4ea81 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit a55e84c29f51606e0996f7f88b7664ca0d37365b +Subproject commit 4ff4ea81735548f808302c61b619ad7804e1eec0