From a7f37e52cfb50cccd617f9023286bcd2fda9379e Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 17 May 2025 09:02:33 +0300 Subject: [PATCH] remove ArrangerView<'a> --- config/config_arranger.edn | 4 +- crates/app/src/view.rs | 15 +- crates/device/src/arranger.rs | 48 -- crates/device/src/arranger/arranger_api.rs | 12 + crates/device/src/arranger/arranger_clip.rs | 49 -- crates/device/src/arranger/arranger_model.rs | 91 ++- crates/device/src/arranger/arranger_port.rs | 135 ----- crates/device/src/arranger/arranger_scene.rs | 84 --- crates/device/src/arranger/arranger_scenes.rs | 252 +++------ crates/device/src/arranger/arranger_track.rs | 199 ------- crates/device/src/arranger/arranger_tracks.rs | 368 +++++++----- crates/device/src/arranger/arranger_view.rs | 535 ++++++++++++++---- 12 files changed, 843 insertions(+), 949 deletions(-) delete mode 100644 crates/device/src/arranger/arranger_port.rs delete mode 100644 crates/device/src/arranger/arranger_scene.rs delete mode 100644 crates/device/src/arranger/arranger_track.rs diff --git a/config/config_arranger.edn b/config/config_arranger.edn index 408dab55..1eb5e376 100644 --- a/config/config_arranger.edn +++ b/config/config_arranger.edn @@ -26,6 +26,4 @@ (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 (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))))))))) + (fill/xy (align/w :view-arranger))))) diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index a1e2ad1b..4ea5322c 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -77,16 +77,13 @@ impl App { self.project.view_audio_outs_status(self.color) } pub fn view_arranger (&self) -> impl Content + use<'_> { - ArrangerView::new(&self.project, self.editor.as_ref()) - } - pub fn view_arranger_scenes (&self) -> impl Content + use<'_> { - self.scenes_view(&self.editor) + &self.project } pub fn view_arranger_scenes_names (&self) -> impl Content + use<'_> { - self.scenes_names() + self.project.view_scenes_names() } pub fn view_arranger_scenes_clips (&self) -> impl Content + use<'_> { - self.scenes_clips(&self.editor) + self.project.view_scenes_clips(&self.editor) } pub fn view_arranger_track_names (&self) -> impl Content + use<'_> { self.project.view_track_names(self.color) @@ -258,15 +255,9 @@ 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 38f5b313..94a7f295 100644 --- a/crates/device/src/arranger.rs +++ b/crates/device/src/arranger.rs @@ -11,11 +11,8 @@ 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::*; @@ -30,48 +27,3 @@ 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 1b6343a2..aba398e9 100644 --- a/crates/device/src/arranger/arranger_api.rs +++ b/crates/device/src/arranger/arranger_api.rs @@ -215,3 +215,15 @@ 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 3b6ab878..a856c47b 100644 --- a/crates/device/src/arranger/arranger_clip.rs +++ b/crates/device/src/arranger/arranger_clip.rs @@ -1,11 +1,5 @@ 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!() } @@ -30,46 +24,3 @@ impl ClipCommand { todo!() } } - -impl ClipsView for Arrangement {} - -impl<'a> ClipsView for ArrangerView<'a> {} - -pub trait ClipsView: HasTrackScroll + HasSceneScroll + Send + Sync { - 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)))))) - } -} diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs index 32551797..3a9b5f56 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -76,18 +76,6 @@ 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) @@ -151,6 +139,64 @@ 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)> { + 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!("{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")] @@ -164,3 +210,24 @@ 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 deleted file mode 100644 index cf7a65b4..00000000 --- a/crates/device/src/arranger/arranger_port.rs +++ /dev/null @@ -1,135 +0,0 @@ -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 deleted file mode 100644 index 209d5397..00000000 --- a/crates/device/src/arranger/arranger_scene.rs +++ /dev/null @@ -1,84 +0,0 @@ -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 3fbbf41e..8420e23a 100644 --- a/crates/device/src/arranger/arranger_scenes.rs +++ b/crates/device/src/arranger/arranger_scenes.rs @@ -41,179 +41,9 @@ pub trait HasSceneScroll: HasScenes { impl HasSceneScroll for Arrangement { fn scene_scroll (&self) -> usize { self.scene_scroll } } -impl<'a> HasSceneScroll for ArrangerView<'a> { - fn scene_scroll (&self) -> usize { self.arrangement.scene_scroll() } -} pub type SceneWith<'a, T: Send + Sync> = (usize, &'a Scene, usize, usize, T); -pub trait ScenesView: HasSceneScroll + 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_names_2 (&self, theme: ItemTheme) -> impl Content { - let h = self.scenes().len() as u16 * 2; - let bg = theme.darker.rgb; - Fixed::y(h, Tui::bg(bg, Align::w(Fill::x(Map::new( - ||self.scenes().iter().skip(self.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))))))))))))) - } - 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> ScenesView 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 AddScene for T {} pub trait AddScene: HasScenes + HasTracks { @@ -242,3 +72,85 @@ 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 deleted file mode 100644 index 09fb56e2..00000000 --- a/crates/device/src/arranger/arranger_track.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::*; - -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(), - 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>; - 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>> 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() - //} -//} diff --git a/crates/device/src/arranger/arranger_tracks.rs b/crates/device/src/arranger/arranger_tracks.rs index 5ad832c5..08d9a121 100644 --- a/crates/device/src/arranger/arranger_tracks.rs +++ b/crates/device/src/arranger/arranger_tracks.rs @@ -73,143 +73,257 @@ pub trait HasTrackScroll: HasTracks { impl HasTrackScroll for Arrangement { fn track_scroll (&self) -> usize { self.track_scroll } } -impl<'a> HasTrackScroll for ArrangerView<'a> { - fn track_scroll (&self) -> usize { self.arrangement.track_scroll() } -} -impl TracksView for T {} - -pub trait TracksView: HasTrackScroll + HasSelection { - fn view_track_names (&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); - } - Bsp::w( - Fixed::x(20, Tui::bg(theme.darkest.rgb, - col!(Tui::bold(true, "[t]rack"), "[T] Add"))), - Align::w(Fixed::y(max_outputs + 1, Tui::bg(theme.darker.rgb, - Align::w(Fill::x(Map::new( - ||self.tracks_with_sizes(&self.selection(), None).skip(self.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}"))))))))))))) +impl> HasTrack for T { + fn track (&self) -> Option<&Track> { + self.get() } - 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); - } - Bsp::w( - Fixed::x(20, Tui::bg(theme.darkest.rgb, - col!(Tui::bold(true, "[o]utput"), "[O] Add"))), - Align::w(Fixed::y(max_outputs + 1, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Map::new( - ||self.tracks_with_sizes(&self.selection(), None).skip(self.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()))))))))))))))) - } - fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content { - let mut max_inputs = 0u16; - for track in self.tracks().iter() { - max_inputs = max_inputs.max(track.sequencer.midi_ins.len() as u16); - } - Bsp::w( - Fixed::x(20, Tui::bg(theme.darkest.rgb, - col!(Tui::bold(true, "[i]nputs"), "[I] Add"))), - Fill::x(Align::w(Fixed::y(max_inputs + 1, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Map::new( - ||self.tracks_with_sizes(&self.selection(), None).skip(self.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())))))))))))))))) - } - fn view_track_devices <'a> (&'a self, theme: ItemTheme) -> impl Content { - let mut max_devices = 2u16; - for track in self.tracks().iter() { - max_devices = max_devices.max(track.devices.len() as u16); - } - Bsp::w( - Fixed::x(20, Tui::bg(theme.darkest.rgb, - col!(Tui::bold(true, "[d]evice"), "[D] Add"))), - Fixed::y(max_devices, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Map::new( - ||self.tracks_with_sizes(&self.selection(), None).skip(self.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}: {}", "--------")))))))))))) + fn track_mut (&mut self) -> Option<&mut Track> { + self.get_mut() } } -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(()) - } +#[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!() } +} - /// 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), +#[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!("{name}"), - self.jack(), - Some(self.clock()), - None, - mins, - mouts + format!("{}/sequencer", name.as_ref()), + jack, + clock, + clip, + midi_from, + midi_to )?, - 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); + }) + } + /// 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; + }, + _ => {} } } - Ok((index, &mut self.tracks_mut()[index])) + 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>; + 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 e17cf0a0..9aa36721 100644 --- a/crates/device/src/arranger/arranger_view.rs +++ b/crates/device/src/arranger/arranger_view.rs @@ -1,110 +1,159 @@ use crate::*; -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> Has> for ArrangerView<'a> { - fn get (&self) -> &Vec { - &self.arrangement.scenes - } - fn get_mut (&mut self) -> &mut Vec { - unreachable!() - } -} -impl<'a> Has> for ArrangerView<'a> { - fn get (&self) -> &Vec { - &self.arrangement.tracks - } - fn get_mut (&mut self) -> &mut Vec { - unreachable!() - } -} - -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<'a> Content for ArrangerView<'a> { +impl Content for Arrangement { 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 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.arrangement.size.of(outs(tracks(devices(ins(bg(self.scenes_view(&None))))))) + self.size.of(outs(tracks(devices(ins(bg(self.view_scenes_clips(&None))))))) } } -impl<'a> ArrangerView<'a> { +impl Arrangement { /// Render input matrix. - pub(crate) fn inputs (&'a self) -> impl Content + 'a { + fn view_inputs_0 (&self) -> impl Content + '_ { Tui::bg(Reset, Bsp::s( - self.input_intos(), - Bsp::s(self.input_routes(), self.input_ports()), + self.view_input_intos(), + Bsp::s(self.view_input_routes(), self.view_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. - pub(crate) fn outputs (&'a self) -> impl Content + 'a { + fn view_outputs_0 (&self) -> impl Content + '_ { Tui::bg(Reset, Align::n(Bsp::s( - Bsp::s(self.output_ports(), self.output_conns()), - Bsp::s(self.output_nexts(), self.output_froms()), + Bsp::s(self.view_output_ports(), self.view_output_conns()), + Bsp::s(self.view_output_nexts(), self.view_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 - pub(crate) fn tracks (&'a self) -> impl Content + 'a { - let Self { width_side, width_mid, track_selected, is_editing, .. } = self; + 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(); Tryptich::center(3) - .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) { + .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) { track.color.light } else { track.color.base @@ -114,14 +163,17 @@ impl<'a> ArrangerView<'a> { ))) } /// Render device switches. - pub(crate) fn devices (&'a self) -> impl Content + 'a { - let Self { width_side, width_mid, track_selected, is_editing, .. } = self; + 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(); 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 @@ -132,22 +184,285 @@ impl<'a> ArrangerView<'a> { } } -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))))}))) +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 mut max_outputs = 0u16; + for track in self.tracks().iter() { + max_outputs = max_outputs.max(track.sequencer.midi_outs.len() as u16); + } + Bsp::w( + Fixed::x(20, Tui::bg(theme.darkest.rgb, + col!(Tui::bold(true, "[t]rack"), "[T] Add"))), + Align::w(Fixed::y(max_outputs + 1, Tui::bg(theme.darker.rgb, + Align::w(Fill::x(Map::new( + ||self.tracks_with_sizes(&self.selection(), None).skip(self.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}"))))))))))))) + } + 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); + } + Bsp::w( + Fixed::x(20, Tui::bg(theme.darkest.rgb, + col!(Tui::bold(true, "[o]utput"), "[O] Add"))), + Align::w(Fixed::y(max_outputs + 1, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Map::new( + ||self.tracks_with_sizes(&self.selection(), None).skip(self.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()))))))))))))))) + } + fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content { + let mut max_inputs = 0u16; + for track in self.tracks().iter() { + max_inputs = max_inputs.max(track.sequencer.midi_ins.len() as u16); + } + Bsp::w( + Fixed::x(20, Tui::bg(theme.darkest.rgb, + col!(Tui::bold(true, "[i]nputs"), "[I] Add"))), + Fill::x(Align::w(Fixed::y(max_inputs + 1, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Map::new( + ||self.tracks_with_sizes(&self.selection(), None).skip(self.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())))))))))))))))) + } + fn view_track_devices <'a> (&'a self, theme: ItemTheme) -> impl Content { + let mut max_devices = 2u16; + for track in self.tracks().iter() { + max_devices = max_devices.max(track.devices.len() as u16); + } + Bsp::w( + Fixed::x(20, Tui::bg(theme.darkest.rgb, + col!(Tui::bold(true, "[d]evice"), "[D] Add"))), + Fixed::y(max_devices, Tui::bg(theme.darker.rgb, Align::w(Fill::x(Map::new( + ||self.tracks_with_sizes(&self.selection(), None).skip(self.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(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 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 { + 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.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 + }; + Fill::x(map_south(y1 as u16, height, Fixed::y(height, Phat { + width: 0, height: 0, content, colors: [fg, bg, hi, lo] + }))) + })) + } + fn scenes_names_2 (&self, theme: ItemTheme) -> impl Content { + let h = self.scenes().len() as u16 * 2; + let bg = theme.darker.rgb; + Fixed::y(h, Tui::bg(bg, Align::w(Fill::x(Map::new( + ||self.scenes().iter().skip(self.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))))))))))))) + } + 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 + { + let is_editing = false; //FIXME + 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.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; + map_south(y1 as u16, height, 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; + } + } }