/// Represents the current user selection in the arranger #[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { #[default] /// Nothing is selected Nothing, /// The whole mix is selected Mix, /// A MIDI input is selected. Input(usize), /// A MIDI output is selected. Output(usize), /// A scene is selected. #[cfg(feature = "scene")] Scene(usize), /// A track is selected. #[cfg(feature = "track")] Track(usize), /// A clip (track × scene) is selected. #[cfg(feature = "track")] TrackClip { track: usize, scene: usize }, /// A track's MIDI input connection is selected. #[cfg(feature = "track")] TrackInput { track: usize, port: usize }, /// A track's MIDI output connection is selected. #[cfg(feature = "track")] TrackOutput { track: usize, port: usize }, /// A track device slot is selected. #[cfg(feature = "track")] TrackDevice { track: usize, device: usize }, } /// A scene consists of a set of clips to play together. /// /// ``` /// let scene: tek::Scene = Default::default(); /// let _ = scene.pulses(); /// let _ = scene.is_playing(&[]); /// ``` #[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>>>, } /// Arranger. /// /// ``` /// let arranger = tek::Arrangement::default(); /// ``` #[derive(Default, Debug)] pub struct Arrangement { /// Project name. pub name: Arc, /// Base color. pub color: ItemTheme, /// JACK client handle. pub jack: Jack<'static>, /// FIXME a render of the project arrangement, redrawn on update. /// TODO rename to "render_cache" or smth pub arranger: Arc>, /// Display size pub size: Measure, /// Display size of clips area pub size_inner: Measure, /// Source of time #[cfg(feature = "clock")] pub clock: Clock, /// Allows one MIDI clip to be edited #[cfg(feature = "editor")] pub editor: Option, /// List of global midi inputs #[cfg(feature = "port")] pub midi_ins: Vec, /// List of global midi outputs #[cfg(feature = "port")] pub midi_outs: Vec, /// List of global audio inputs #[cfg(feature = "port")] pub audio_ins: Vec, /// List of global audio outputs #[cfg(feature = "port")] pub audio_outs: Vec, /// Selected UI element #[cfg(feature = "select")] pub selection: Selection, /// Last track number (to avoid duplicate port names) #[cfg(feature = "track")] pub track_last: usize, /// List of tracks #[cfg(feature = "track")] pub tracks: Vec, /// Scroll offset of tracks #[cfg(feature = "track")] pub track_scroll: usize, /// List of scenes #[cfg(feature = "scene")] pub scenes: Vec, /// Scroll offset of scenes #[cfg(feature = "scene")] pub scene_scroll: usize, } pub trait ClipsView: TracksView + ScenesView { fn view_scenes_clips <'a> (&'a self) -> impl Draw + 'a { self.clips_size().of(wh_full(above( wh_full(origin_se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), Thunk::new(|to: &mut Tui|for ( track_index, track, _, _ ) in self.tracks_with_sizes() { to.place(&w_exact(track.width as u16, h_full(self.view_track_clips(track_index, track)))) })))) } fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { Thunk::new(move|to: &mut Tui|for ( scene_index, scene, .. ) in self.scenes_with_sizes() { let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { let clip = clip.read().unwrap(); (format!(" ⏹ {}", &clip.name).into(), clip.color) } else { (" ⏹ -- ".into(), 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.lighter.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 }; let w = if self.selection().track() == Some(track_index) && let Some(editor) = self.editor () { (editor.measure_width() as usize).max(24).max(track.width) } else { track.width } as u16; let y = if self.selection().scene() == Some(scene_index) && let Some(editor) = self.editor () { (editor.measure_height() as usize).max(12) } else { Self::H_SCENE as usize } as u16; to.place(&wh_exact(w, y, below( wh_full(Outer(true, Style::default().fg(outline))), wh_full(below( below( Tui::fg_bg(outline, bg, wh_full("")), wh_full(origin_nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), ), wh_full(when(self.selection().track() == Some(track_index) && self.selection().scene() == Some(scene_index) && self.is_editing(), self.editor()))))))); }) } } pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll + Measured { fn tracks_width_available (&self) -> u16 { (self.measure_width() as u16).saturating_sub(40) } /// Iterate over tracks with their corresponding sizes. fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { let _editor_width = self.editor().map(|e|e.measure_width()); let _active_track = self.selection().track(); let mut x = 0; self.tracks().iter().enumerate().map_while(move |(index, track)|{ let width = track.width.max(8); if x + width < self.clips_size().w() as usize { let data = (index, track, x, x + width); x += width + Self::TRACK_SPACING; Some(data) } else { None } }) } fn view_track_names (&self, theme: ItemTheme) -> impl Draw { let track_count = self.tracks().len(); let scene_count = self.scenes().len(); let selected = self.selection(); let button = south( button_3("t", "rack ", format!("{}{track_count}", selected.track() .map(|track|format!("{track}/")).unwrap_or_default()), false), button_3("s", "cene ", format!("{}{scene_count}", selected.scene() .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); let button_2 = south( button_2("T", "+", false), button_2("S", "+", false)); view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb, h_exact(2, Thunk::new(|to: &mut Tui|{ for (index, track, x1, _x2) in self.tracks_with_sizes() { to.place(&x_push(x1 as u16, w_exact(track_width(index, track), Tui::bg(if selected.track() == Some(index) { track.color.light.rgb } else { track.color.base.rgb }, south(w_full(origin_nw(east( format!("·t{index:02} "), Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name)) ))), ""))) ));}})))) } fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw { view_track_row_section(theme, south(w_full(origin_w(button_2("o", "utput", false))), Thunk::new(|to: &mut Tui|for port in self.midi_outs().iter() { to.place(&w_full(origin_w(port.port_name()))); })), button_2("O", "+", false), Tui::bg(theme.darker.rgb, origin_w(Thunk::new(|to: &mut Tui|{ for (index, track, _x1, _x2) in self.tracks_with_sizes() { to.place(&w_exact(track_width(index, track), origin_nw(h_full(iter_south(1, ||track.sequencer.midi_outs.iter(), |port, index|Tui::fg(Rgb(255, 255, 255), h_exact(1, Tui::bg(track.color.dark.rgb, w_full(origin_w( format!("·o{index:02} {}", port.port_name())))))))))));}})))) } fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw { let mut h = 0u16; for track in self.tracks().iter() { h = h.max(track.sequencer.midi_ins.len() as u16); } let content = Thunk::new(move|to: &mut Tui|for (index, track, _x1, _x2) in self.tracks_with_sizes() { to.place(&wh_exact(track_width(index, track), h + 1, origin_nw(south( Tui::bg(track.color.base.rgb, w_full(origin_w(east!( either(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "), either(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "), either(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "), )))), iter_south(1, ||track.sequencer.midi_ins.iter(), |port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb, w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))); }); view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), Tui::bg(theme.darker.rgb, origin_w(content))) } } pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize + Send + Sync { /// Default scene height. const H_SCENE: usize = 2; /// Default editor height. const H_EDITOR: usize = 15; fn h_scenes (&self) -> u16; fn w_side (&self) -> u16; fn w_mid (&self) -> u16; fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { let mut y = 0; self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{ let height = if self.selection().scene() == Some(s) && self.editor().is_some() { 8 } else { Self::H_SCENE }; if y + height <= self.clips_size().h() as usize { let data = (s, scene, y, y + height); y += height; Some(data) } else { None } }) } fn view_scenes_names (&self) -> impl Draw { w_exact(20, Thunk::new(|to: &mut Tui|for (index, scene, ..) in self.scenes_with_sizes() { to.place(&self.view_scene_name(index, scene)); })) } fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw + 'a { let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() { 7 } else { Self::H_SCENE as u16 }; let bg = if self.selection().scene() == Some(index) { scene.color.light.rgb } else { scene.color.base.rgb }; let a = w_full(origin_w(east(format!("·s{index:02} "), Tui::fg(Tui::g(255), Tui::bold(true, &scene.name))))); let b = when(self.selection().scene() == Some(index) && self.is_editing(), wh_full(origin_nw(south( self.editor().as_ref().map(|e|e.clip_status()), self.editor().as_ref().map(|e|e.edit_status()))))); wh_exact(20, h, Tui::bg(bg, origin_nw(south(a, b)))) } } pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; } pub trait HasTrackScroll: HasTracks { fn track_scroll (&self) -> usize; } pub trait HasScene: AsRefOpt + AsMutOpt { fn scene_mut (&mut self) -> Option<&mut Scene> { self.as_mut_opt() } fn scene (&self) -> Option<&Scene> { self.as_ref_opt() } } pub trait HasSelection: AsRef + AsMut { fn selection (&self) -> &Selection { self.as_ref() } fn selection_mut (&mut self) -> &mut Selection { self.as_mut() } /// Get the active track #[cfg(feature = "track")] fn selected_track (&self) -> Option<&Track> where Self: HasTracks { let index = self.selection().track()?; self.tracks().get(index) } /// Get a mutable reference to the active track #[cfg(feature = "track")] fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks { let index = self.selection().track()?; self.tracks_mut().get_mut(index) } /// Get the active scene #[cfg(feature = "scene")] fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes { let index = self.selection().scene()?; self.scenes().get(index) } /// Get a mutable reference to the active scene #[cfg(feature = "scene")] fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes { let index = self.selection().scene()?; self.scenes_mut().get_mut(index) } /// Get the active clip #[cfg(feature = "clip")] fn selected_clip (&self) -> Option>> where Self: HasScenes + HasTracks { self.selected_scene()?.clips.get(self.selection().track()?)?.clone() } } pub trait HasScenes: AsRef> + AsMut> { fn scenes (&self) -> &Vec { self.as_ref() } fn scenes_mut (&mut self) -> &mut Vec { self.as_mut() } /// Generate the default name for a new scene fn scene_default_name (&self) -> Arc { format!("s{:3>}", self.scenes().len() + 1).into() } fn scene_longest_name (&self) -> usize { self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) } /// Add multiple scenes fn scenes_add (&mut self, n: usize) -> Usually<()> where Self: HasTracks { let scene_color_1 = ItemColor::random(); let scene_color_2 = ItemColor::random(); for i in 0..n { let _ = self.scene_add(None, Some( scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() ))?; } Ok(()) } /// Add a scene fn scene_add (&mut self, name: Option<&str>, color: Option) -> Usually<(usize, &mut Scene)> where Self: HasTracks { let scene = Scene { name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), clips: vec![None;self.tracks().len()], color: color.unwrap_or_else(ItemTheme::random), }; self.scenes_mut().push(scene); let index = self.scenes().len() - 1; Ok((index, &mut self.scenes_mut()[index])) } } pub trait HasTracks: AsRef> + AsMut> { fn tracks (&self) -> &Vec { self.as_ref() } fn tracks_mut (&mut self) -> &mut Vec { self.as_mut() } /// Run audio callbacks for every track and every device fn process_tracks (&mut self, client: &Client, scope: &ProcessScope) -> Control { for track in self.tracks_mut().iter_mut() { if Control::Quit == Audio::process(&mut track.sequencer, client, scope) { return Control::Quit } for device in track.devices.iter_mut() { if Control::Quit == DeviceAudio(device).process(client, scope) { return Control::Quit } } } Control::Continue } fn track_longest_name (&self) -> usize { self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) } /// Stop all playing clips fn tracks_stop_all (&mut self) { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } } /// Stop all playing clips fn tracks_launch (&mut self, clips: Option>>>>) { if let Some(clips) = clips { for (clip, track) in clips.iter().zip(self.tracks_mut()) { track.sequencer.enqueue_next(clip.as_ref()); } } else { for track in self.tracks_mut().iter_mut() { track.sequencer.enqueue_next(None); } } } /// Spacing between tracks. const TRACK_SPACING: usize = 0; } pub trait HasTrack: AsRefOpt + AsMutOpt { fn track (&self) -> Option<&Track> { self.as_ref_opt() } fn track_mut (&mut self) -> Option<&mut Track> { self.as_mut_opt() } #[cfg(feature = "port")] fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw + 'a { self.track().map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) } #[cfg(feature = "port")] fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw + '_ { self.track().map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) } #[cfg(feature = "port")] fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { self.track().map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) } #[cfg(feature = "port")] fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { self.track().map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) } }