use crate::*; #[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, } impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } has!(Jack<'static>: |self: Arrangement|self.jack); has!(Measure: |self: Arrangement|self.size); #[cfg(feature = "editor")] has!(Option: |self: Arrangement|self.editor); #[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_ins); #[cfg(feature = "port")] has!(Vec: |self: Arrangement|self.midi_outs); #[cfg(feature = "clock")] has!(Clock: |self: Arrangement|self.clock); #[cfg(feature = "select")] has!(Selection: |self: Arrangement|self.selection); #[cfg(all(feature = "select", feature = "track"))] has!(Vec: |self: Arrangement|self.tracks); #[cfg(all(feature = "select", feature = "track"))] maybe_has!(Track: |self: Arrangement| { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); #[cfg(all(feature = "select", feature = "scene"))] has!(Vec: |self: Arrangement|self.scenes); #[cfg(all(feature = "select", feature = "scene"))] maybe_has!(Scene: |self: Arrangement| { Has::::get(self).track().map(|index|Has::>::get(self).get(index)).flatten() }; { Has::::get(self).track().map(|index|Has::>::get_mut(self).get_mut(index)).flatten() }); #[cfg(feature = "select")] impl Arrangement { #[cfg(feature = "clip")] fn selected_clip (&self) -> Option { todo!() } #[cfg(feature = "scene")] fn selected_scene (&self) -> Option { todo!() } #[cfg(feature = "track")] fn selected_track (&self) -> Option { todo!() } #[cfg(feature = "port")] fn selected_midi_in (&self) -> Option { todo!() } #[cfg(feature = "port")] fn selected_midi_out (&self) -> Option { todo!() } fn selected_device (&self) -> Option { todo!() } fn unselect (&self) -> Selection { Selection::Nothing } } impl Arrangement { /// Width of display pub fn w (&self) -> u16 { self.size.w() as u16 } /// Width allocated for sidebar. pub fn w_sidebar (&self, is_editing: bool) -> u16 { self.w() / if is_editing { 16 } else { 8 } as u16 } /// Width available to display tracks. pub fn w_tracks_area (&self, is_editing: bool) -> u16 { self.w().saturating_sub(self.w_sidebar(is_editing)) } /// Height of display pub fn h (&self) -> u16 { self.size.h() as u16 } /// Height taken by visible device slots. pub fn h_devices (&self) -> u16 { 2 //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } } #[cfg(feature = "track")] impl TracksView for Arrangement {} #[cfg(feature = "track")] impl Arrangement { /// Get the active track pub fn get_track (&self) -> Option<&Track> { let index = self.selection().track()?; Has::>::get(self).get(index) } /// Get a mutable reference to the active track pub fn get_track_mut (&mut self) -> Option<&mut Track> { let index = self.selection().track()?; Has::>::get_mut(self).get_mut(index) } /// Add multiple tracks pub fn tracks_add ( &mut self, count: usize, width: Option, mins: &[Connect], mouts: &[Connect], ) -> Usually<()> { 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 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: &[Connect], mouts: &[Connect], ) -> Usually<(usize, &mut Track)> { let name: Arc = name.map_or_else( ||format!("trk{:02}", self.track_last).into(), |x|x.to_string().into() ); self.track_last += 1; let 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])) } pub fn view_inputs (&self, _theme: ItemTheme) -> impl Content + '_ { Bsp::s( Fixed::Y(1, self.view_inputs_header()), Thunk::new(|to: &mut TuiOut|{ for (index, port) in self.midi_ins().iter().enumerate() { to.place(&Push::X(index as u16 * 10, Fixed::Y(1, self.view_inputs_row(port)))) } }) ) } fn view_inputs_header (&self) -> impl Content + '_ { Bsp::e(Fixed::X(20, Align::w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), Bsp::w(Fixed::X(4, button_2("I", "+", false)), Thunk::new(move|to: &mut TuiOut|for (_index, track, x1, _x2) in self.tracks_with_sizes() { #[cfg(feature = "track")] to.place(&Push::X(x1 as u16, Tui::bg(track.color.dark.rgb, Align::w(Fixed::X(track.width as u16, row!( Either::new(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), Either::new(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), Either::new(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), )))))) }))) } fn view_inputs_row (&self, port: &MidiInput) -> impl Content { Bsp::e(Fixed::X(20, Align::w(Bsp::e(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), Bsp::w(Fixed::X(4, ()), Thunk::new(move|to: &mut TuiOut|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { #[cfg(feature = "track")] to.place(&Tui::bg(track.color.darker.rgb, Align::w(Fixed::X(track.width as u16, row!( Either::new(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), Either::new(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), Either::new(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), ))))) }))) } pub fn view_outputs (&self, theme: ItemTheme) -> impl Content { let mut h = 1; for output in self.midi_outs().iter() { h += 1 + output.connections.len(); } let h = h as u16; let list = Bsp::s( Fixed::Y(1, Fill::X(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), Fixed::Y(h - 1, Fill::XY(Align::nw(Thunk::new(|to: &mut TuiOut|{ for (_index, port) in self.midi_outs().iter().enumerate() { to.place(&Fixed::Y(1,Fill::X(Bsp::e( Align::w(Bsp::e(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), Fill::X(Align::e(format!("{}/{} ", port.port().get_connections().len(), port.connections.len()))))))); for (index, conn) in port.connections.iter().enumerate() { to.place(&Fixed::Y(1, Fill::X(Align::w(format!(" c{index:02}{}", conn.info()))))); } } }))))); Fixed::Y(h, view_track_row_section(theme, list, button_2("O", "+", false), Tui::bg(theme.darker.rgb, Align::w(Fill::X( Thunk::new(|to: &mut TuiOut|{ for (index, track, _x1, _x2) in self.tracks_with_sizes() { to.place(&Fixed::X(track_width(index, track), Thunk::new(|to: &mut TuiOut|{ to.place(&Fixed::Y(1, Align::w(Bsp::e( Either::new(true, Tui::fg(Green, "play "), "play "), Either::new(false, Tui::fg(Yellow, "solo "), "solo "), )))); for (_index, port) in self.midi_outs().iter().enumerate() { to.place(&Fixed::Y(1, Align::w(Bsp::e( Either::new(true, Tui::fg(Green, " ● "), " · "), Either::new(false, Tui::fg(Yellow, " ● "), " · "), )))); for (_index, _conn) in port.connections.iter().enumerate() { to.place(&Fixed::Y(1, Fill::X(""))); } }})))}})))))) } pub fn view_track_devices (&self, theme: ItemTheme) -> impl Content { let mut h = 2u16; for track in self.tracks().iter() { h = h.max(track.devices.len() as u16 * 2); } view_track_row_section(theme, button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), button_2("D", "+", false), Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { to.place(&Fixed::XY(track_width(index, track), h + 1, Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h, |_, _index|Fixed::XY(track.width as u16, 2, Tui::fg_bg( ItemTheme::G[32].lightest.rgb, ItemTheme::G[32].dark.rgb, Align::nw(format!(" · {}", "--"))))))))); })) } } #[cfg(feature = "track")] pub fn view_track_row_section ( _theme: ItemTheme, button: impl Content, button_add: impl Content, content: impl Content, ) -> impl Content { Bsp::w(Fill::Y(Fixed::X(4, Align::nw(button_add))), Bsp::e(Fixed::X(20, Fill::Y(Align::nw(button))), Fill::XY(Align::c(content)))) } #[cfg(feature = "scene")] impl Arrangement { /// Get the active scene pub fn get_scene (&self) -> Option<&Scene> { let index = self.selection().scene()?; Has::>::get(self).get(index) } /// Get a mutable reference to the active scene pub fn get_scene_mut (&mut self) -> Option<&mut Scene> { let index = self.selection().scene()?; Has::>::get_mut(self).get_mut(index) } } #[cfg(feature = "scene")] impl ScenesView for Arrangement { fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) } fn w_side (&self) -> u16 { (self.width() as u16 * 2 / 10).max(20) } fn w_mid (&self) -> u16 { (self.width() as u16).saturating_sub(2 * self.w_side()).max(40) } } #[cfg(feature = "clip")] impl Arrangement { /// Get the active clip pub fn get_clip (&self) -> Option>> { self.get_scene()?.clips.get(self.selection().track()?)?.clone() } /// Put a clip in a slot pub fn clip_put ( &mut self, track: usize, scene: usize, clip: Option>> ) -> Option>> { let old = self.scenes[scene].clips[track].clone(); self.scenes[scene].clips[track] = clip; old } /// Change the color of a clip, returning the previous one pub fn clip_set_color (&self, track: usize, scene: usize, color: ItemTheme) -> Option { self.scenes[scene].clips[track].as_ref().map(|clip|{ let mut clip = clip.write().unwrap(); let old = clip.color.clone(); clip.color = color.clone(); panic!("{color:?} {old:?}"); old }) } /// Toggle looping for the active clip pub fn toggle_loop (&mut self) { if let Some(clip) = self.get_clip() { clip.write().unwrap().toggle_loop() } } } #[cfg(feature = "sampler")] impl Arrangement { /// Get the first sampler of the active track pub fn sampler (&self) -> Option<&Sampler> { self.get_track()?.sampler(0) } /// Get the first sampler of the active track pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { self.get_track_mut()?.sampler_mut(0) } } pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content) -> impl Content { let left = Tui::fg_bg(bg, Reset, Fixed::X(1, RepeatV("▐"))); let right = Tui::fg_bg(bg, Reset, Fixed::X(1, RepeatV("▌"))); Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content))) } pub trait HasClipsSize { fn clips_size (&self) -> &Measure; } impl HasClipsSize for Arrangement { fn clips_size (&self) -> &Measure { &self.size_inner } } 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); } //def_command!(ArrangementCommand: |arranger: Arrangement| { //Home => { arranger.editor = None; Ok(None) }, //Edit => { //let selection = arranger.selection().clone(); //arranger.editor = if arranger.editor.is_some() { //None //} else { //match selection { //Selection::TrackClip { track, scene } => { //let clip = &mut arranger.scenes_mut()[scene].clips[track]; //if clip.is_none() { ////app.clip_auto_create(); //*clip = Some(Arc::new(RwLock::new(MidiClip::new( //&format!("t{track:02}s{scene:02}"), //false, 384, None, Some(ItemTheme::random()) //)))); //} //clip.as_ref().map(|c|c.into()) //} //_ => { //None //} //} //}; //if let Some(editor) = arranger.editor.as_mut() { //if let Some(clip) = editor.clip() { //let length = clip.read().unwrap().length.max(1); //let width = arranger.size_inner.w().saturating_sub(20).max(1); //editor.set_time_zoom(length / width); //editor.redraw(); //} //} //Ok(None) //}, ////// Set the selection //Select { selection: Selection } => { *arranger.selection_mut() = *selection; Ok(None) }, ////// Launch the selected clip or scene //Launch => { //match *arranger.selection() { //Selection::Track(t) => { //arranger.tracks[t].sequencer.enqueue_next(None) //}, //Selection::TrackClip { track, scene } => { //arranger.tracks[track].sequencer.enqueue_next(arranger.scenes[scene].clips[track].as_ref()) //}, //Selection::Scene(s) => { //for t in 0..arranger.tracks.len() { //arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()) //} //}, //_ => {} //}; //Ok(None) //}, ////// Set the color of the selected entity //SetColor { palette: Option } => { //let mut palette = palette.unwrap_or_else(||ItemTheme::random()); //let selection = *arranger.selection(); //Ok(Some(Self::SetColor { palette: Some(match selection { //Selection::Mix => { //std::mem::swap(&mut palette, &mut arranger.color); //palette //}, //Selection::Scene(s) => { //std::mem::swap(&mut palette, &mut arranger.scenes[s].color); //palette //} //Selection::Track(t) => { //std::mem::swap(&mut palette, &mut arranger.tracks[t].color); //palette //} //Selection::TrackClip { track, scene } => { //if let Some(ref clip) = arranger.scenes[scene].clips[track] { //let mut clip = clip.write().unwrap(); //std::mem::swap(&mut palette, &mut clip.color); //palette //} else { //return Ok(None) //} //}, //_ => todo!() //}) })) //}, //Track { track: TrackCommand } => { todo!("delegate") }, //TrackAdd => { //let index = arranger.track_add(None, None, &[], &[])?.0; //*arranger.selection_mut() = match arranger.selection() { //Selection::Track(_) => Selection::Track(index), //Selection::TrackClip { track: _, scene } => Selection::TrackClip { //track: index, scene: *scene //}, //_ => *arranger.selection() //}; //Ok(Some(Self::TrackDelete { index })) //}, //TrackSwap { index: usize, other: usize } => { //let index = *index; //let other = *other; //Ok(Some(Self::TrackSwap { index, other })) //}, //TrackDelete { index: usize } => { //let index = *index; //let exists = arranger.tracks().get(index).is_some(); //if exists { //let track = arranger.tracks_mut().remove(index); //let Track { sequencer: Sequencer { midi_ins, midi_outs, .. }, .. } = track; //for port in midi_ins.into_iter() { //port.close()?; //} //for port in midi_outs.into_iter() { //port.close()?; //} //for scene in arranger.scenes_mut().iter_mut() { //scene.clips.remove(index); //} //} //Ok(None) ////TODO:Ok(Some(Self::TrackAdd ( index, track: Some(deleted_track) }) //}, //MidiIn { input: MidiInputCommand } => { //todo!("delegate"); Ok(None) //}, //MidiInAdd => { //arranger.midi_in_add()?; //Ok(None) //}, //MidiOut { output: MidiOutputCommand } => { //todo!("delegate"); //Ok(None) //}, //MidiOutAdd => { //arranger.midi_out_add()?; //Ok(None) //}, //Device { command: DeviceCommand } => { //todo!("delegate"); //Ok(None) //}, //DeviceAdd { index: usize } => { //todo!("delegate"); //Ok(None) //}, //Scene { scene: SceneCommand } => { //todo!("delegate"); //Ok(None) //}, //OutputAdd => { //arranger.midi_outs.push(MidiOutput::new( //arranger.jack(), //&format!("/M{}", arranger.midi_outs.len() + 1), //&[] //)?); //Ok(None) //}, //InputAdd => { //arranger.midi_ins.push(MidiInput::new( //arranger.jack(), //&format!("M{}/", arranger.midi_ins.len() + 1), //&[] //)?); //Ok(None) //}, //SceneAdd => { //let index = arranger.scene_add(None, None)?.0; //*arranger.selection_mut() = match arranger.selection() { //Selection::Scene(_) => Selection::Scene(index), //Selection::TrackClip { track, scene } => Selection::TrackClip { //track: *track, //scene: index //}, //_ => *arranger.selection() //}; //Ok(None) // TODO //}, //SceneSwap { index: usize, other: usize } => { //let index = *index; //let other = *other; //Ok(Some(Self::SceneSwap { index, other })) //}, //SceneDelete { index: usize } => { //let index = *index; //let scenes = arranger.scenes_mut(); //Ok(if scenes.get(index).is_some() { //let _scene = scenes.remove(index); //None //} else { //None //}) //}, //SceneLaunch { index: usize } => { //let index = *index; //for track in 0..arranger.tracks.len() { //let clip = arranger.scenes[index].clips[track].as_ref(); //arranger.tracks[track].sequencer.enqueue_next(clip); //} //Ok(None) //}, //Clip { scene: ClipCommand } => { //todo!("delegate") //}, //ClipGet { a: usize, b: usize } => { ////(Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}")) ////("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap()))) //todo!() //}, //ClipPut { a: usize, b: usize } => { ////(Put [t: usize, s: usize, c: MaybeClip] ////Some(Self::Put(t, s, arranger.clip_put(t, s, c)))) ////("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))) //todo!() //}, //ClipDel { a: usize, b: usize } => { ////("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None)))) //todo!() //}, //ClipEnqueue { a: usize, b: usize } => { ////(Enqueue [t: usize, s: usize] ////cmd!(arranger.tracks[t].sequencer.enqueue_next(arranger.scenes[s].clips[t].as_ref()))) ////("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap()))) //todo!() //}, //ClipSwap { a: usize, b: usize }=> { ////(Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}")) ////("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap()))) //todo!() //}, //});