diff --git a/cli/tek.rs b/cli/tek.rs index 2dd01a0d..109d8a04 100644 --- a/cli/tek.rs +++ b/cli/tek.rs @@ -174,6 +174,8 @@ pub fn main () -> Usually<()> { let clock = default_clock(jack); let mut app = Arranger { jack: jack.clone(), + midi_ins: vec![JackPort::::new(jack, format!("M/{name}"), &midi_froms)?,], + midi_outs: vec![JackPort::::new(jack, format!("{name}/M"), &midi_tos)?, ], clock, pool: (&clip).into(), editor: (&clip).into(), diff --git a/jack/src/jack_port.rs b/jack/src/jack_port.rs index 502731ce..c73ce730 100644 --- a/jack/src/jack_port.rs +++ b/jack/src/jack_port.rs @@ -2,6 +2,8 @@ use crate::*; #[derive(Debug)] pub struct JackPort { + /// Port name + pub name: String, /// Handle to JACK client, for receiving reconnect events. pub jack: Arc>, /// Port handle. @@ -10,7 +12,7 @@ pub struct JackPort { pub connect: Vec } impl JackPort { - pub fn connect_to_matching (&mut self) { + pub fn connect_to_matching (&mut self) -> Usually<()> { use PortConnectionName::*; use PortConnectionScope::*; use PortConnectionStatus::*; @@ -21,7 +23,8 @@ impl JackPort { if port.as_str() == &**name { if let Some(port) = self.jack.port_by_name(port.as_str()) { let port_status = Self::try_both_ways(&self.jack, &port, &self.port); - status.push((port, port_status)); + let name = port.name()?; + status.push((port, name, port_status)); if port_status == Connected { break } @@ -31,7 +34,8 @@ impl JackPort { RegExp(re) => for port in self.jack.ports(Some(&re), None, PortFlags::empty()).iter() { if let Some(port) = self.jack.port_by_name(port.as_str()) { let port_status = Self::try_both_ways(&self.jack, &port, &self.port); - status.push((port, port_status)); + let name = port.name()?; + status.push((port, name, port_status)); if port_status == Connected && connect.scope == One { break } @@ -40,6 +44,7 @@ impl JackPort { } connect.status = status } + Ok(()) } fn try_both_ways ( jack: &impl ConnectPort, port_a: &Port, port_b: &Port @@ -59,7 +64,7 @@ impl JackPort { pub struct PortConnection { pub name: PortConnectionName, pub scope: PortConnectionScope, - pub status: Vec<(Port, PortConnectionStatus)>, + pub status: Vec<(Port, String, PortConnectionStatus)>, } impl PortConnection { pub fn collect (exact: &[impl AsRef], re: &[impl AsRef], re_all: &[impl AsRef]) @@ -84,11 +89,22 @@ impl PortConnection { let name = PortConnectionName::RegExp(name.as_ref().into()); Self { name, scope: PortConnectionScope::All, status: vec![] } } + pub fn info (&self) -> String { + format!("{} {} {}", match self.scope { + PortConnectionScope::One => " ", + PortConnectionScope::All => "*", + }, match &self.name { + PortConnectionName::Exact(name) => format!("= {name}"), + PortConnectionName::RegExp(name) => format!("~ {name}"), + }, self.status.len()) + } } #[derive(Clone, Debug, PartialEq)] pub enum PortConnectionName { - /** Exact match */ Exact(Arc), - /** Match regular expression */ RegExp(Arc), + /** Exact match */ + Exact(Arc), + /** Match regular expression */ + RegExp(Arc), } #[derive(Clone, Copy, Debug, PartialEq)] pub enum PortConnectionScope { One, All } @@ -106,10 +122,11 @@ impl JackPort { ) -> Usually { let mut port = JackPort { jack: jack.clone(), - port: jack.midi_in(name)?, + port: jack.midi_in(name.as_ref())?, + name: name.as_ref().to_string(), connect: connect.to_vec() }; - port.connect_to_matching(); + port.connect_to_matching()?; Ok(port) } } @@ -119,10 +136,11 @@ impl JackPort { ) -> Usually { let mut port = Self { jack: jack.clone(), - port: jack.midi_out(name)?, + port: jack.midi_out(name.as_ref())?, + name: name.as_ref().to_string(), connect: connect.to_vec() }; - port.connect_to_matching(); + port.connect_to_matching()?; Ok(port) } } @@ -132,10 +150,11 @@ impl JackPort { ) -> Usually { let mut port = Self { jack: jack.clone(), - port: jack.audio_in(name)?, + port: jack.audio_in(name.as_ref())?, + name: name.as_ref().to_string(), connect: connect.to_vec() }; - port.connect_to_matching(); + port.connect_to_matching()?; Ok(port) } } @@ -145,10 +164,11 @@ impl JackPort { ) -> Usually { let mut port = Self { jack: jack.clone(), - port: jack.audio_out(name)?, + port: jack.audio_out(name.as_ref())?, + name: name.as_ref().to_string(), connect: connect.to_vec() }; - port.connect_to_matching(); + port.connect_to_matching()?; Ok(port) } } diff --git a/output/src/map.rs b/output/src/map.rs index 6d15d8e1..18428485 100644 --- a/output/src/map.rs +++ b/output/src/map.rs @@ -5,7 +5,7 @@ pub fn map_south( item_height: O::Unit, item: impl Content ) -> impl Content { - Push::y(item_offset, Align::n(Fixed::y(item_height, Align::n(Fill::x(item))))) + Push::y(item_offset, Fixed::y(item_height, Fill::x(item))) } pub fn map_south_west( @@ -52,8 +52,7 @@ impl<'a, E, A, B, I, F, G> Content for Map<'a, A, B, I, F, G> where let [mut min_x, mut min_y] = area.center(); let [mut max_x, mut max_y] = area.center(); for item in get_iterator() { - let area = callback(item, index).layout(area).xywh(); - let [x,y,w,h] = area.xywh(); + let [x,y,w,h] = callback(item, index).layout(area).xywh(); min_x = min_x.min(x.into()); min_y = min_y.min(y.into()); max_x = max_x.max((x + w).into()); @@ -68,11 +67,11 @@ impl<'a, E, A, B, I, F, G> Content for Map<'a, A, B, I, F, G> where fn render (&self, to: &mut E) { let Self(_, get_iterator, callback) = self; let mut index = 0; - //let area = self.layout(to.area()); + let area = Content::layout(self, to.area()); for item in get_iterator() { let item = callback(item, index); //to.place(area.into(), &item); - to.place(item.layout(to.area()), &item); + to.place(item.layout(area), &item); index += 1; } } diff --git a/tek/src/arranger.rs b/tek/src/arranger.rs index ecb8bafe..099c6370 100644 --- a/tek/src/arranger.rs +++ b/tek/src/arranger.rs @@ -1,28 +1,531 @@ use crate::*; - -mod arranger_command; pub use self::arranger_command::*; mod arranger_scene; pub use self::arranger_scene::*; mod arranger_select; pub use self::arranger_select::*; mod arranger_track; pub use self::arranger_track::*; mod arranger_h; - +use ClockCommand::{Play, Pause}; +use self::ArrangerCommand as Cmd; /// Root view for standalone `tek_arranger` pub struct Arranger { - pub jack: Arc>, - pub clock: Clock, - pub pool: PoolModel, - pub tracks: Vec, - pub scenes: Vec, - pub splits: [u16;2], - pub selected: ArrangerSelection, - pub color: ItemPalette, - pub size: Measure, - pub note_buf: Vec, - pub midi_buf: Vec>>, - pub editor: MidiEditor, - pub editing: bool, - pub perf: PerfModel, - pub compact: bool, + pub jack: Arc>, + pub midi_ins: Vec>, + pub midi_outs: Vec>, + pub clock: Clock, + pub pool: PoolModel, + pub tracks: Vec, + pub scenes: Vec, + pub splits: [u16;2], + pub selected: ArrangerSelection, + pub color: ItemPalette, + pub size: Measure, + pub note_buf: Vec, + pub midi_buf: Vec>>, + pub editor: MidiEditor, + pub editing: bool, + pub perf: PerfModel, + pub compact: bool, +} +pub(crate) const HEADER_H: u16 = 0; // 5 +pub(crate) const SCENES_W_OFFSET: u16 = 0; +render!(TuiOut: (self: Arranger) => { + let scenes_w = self.sidebar_w(); + let tracks_w = self.tracks_with_sizes().last().unwrap().3 as u16; + let h = self.size.h() as u16; + let ins = 1 + self.midi_ins[0].connect.len() as u16; + let outs = 1 + self.midi_outs[0].connect.len() as u16; + let toolbar = |x|Bsp::s(self.toolbar_view(), x); + let pool = |x|Bsp::w(self.pool_view(), x); + let editing = |x|Bsp::n(Bsp::e(self.editor.clip_status(), self.editor.edit_status()), x); + let playing = |x|x;//Bsp::s(self.play_row(tracks_w), Fill::y(x)); + let next = |x|x;//Bsp::s(self.next_row(tracks_w), Fill::y(x)); + let tracks = |x|Bsp::s(self.track_row(tracks_w), Fill::y(Align::c(x))); + let outputs = |x|Bsp::s(self.output_row(tracks_w), Fill::y(x)); + let scenes = |x|Bsp::s(self.scene_row(tracks_w), x); + let inputs = |x|Bsp::n(self.input_row(tracks_w), Fill::y(x)); + self.size.of(toolbar(Fill::xy(Align::c( + editing(pool(inputs(outputs(playing(next(tracks(scenes(Fill::xy(""))))))))) + )))) +}); +impl Arranger { + pub const LEFT_SEP: char = 'โ–Ž'; + pub const TRACK_MIN_WIDTH: usize = 4; + + fn toolbar_view (&self) -> impl Content + use<'_> { + Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock)))) + } + fn pool_view (&self) -> impl Content + use<'_> { + Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact, &self.pool))) + } + fn is_editing (&self) -> bool { + !self.pool.visible + } + fn sidebar_w (&self) -> u16 { + let w = self.size.w(); + let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + let w = if self.pool.visible { w } else { 8 }; + w + } + fn editor_w (&self) -> usize { + //self.editor.note_len() / self.editor.note_zoom().get() + (5 + (self.editor.time_len().get() / self.editor.time_zoom().get())) + .min(self.size.w().saturating_sub(20)) + .max(16) + //self.editor.time_axis().get().max(16) + //50 + } + pub fn scenes_with_sizes (&self, h: usize) -> impl Iterator { + let mut y = 0; + let editing = self.is_editing(); + let (selected_track, selected_scene) = match self.selected { + ArrangerSelection::Clip(t, s) => (Some(t), Some(s)), + _ => (None, None) + }; + self.scenes.iter().enumerate().map(move|(s, scene)|{ + let active = editing && selected_track.is_some() && selected_scene == Some(s); + let height = if active { 15 } else { h }; + let data = (s, scene, y, y + height); + y += height; + data + }) + } + pub fn tracks_with_sizes (&self) + -> impl Iterator + { + let active = match self.selected { + ArrangerSelection::Track(t) if self.is_editing() => Some(t), + ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t), + _ => None + }; + let big = self.editor_w(); + let mut x = 0; + self.tracks.iter().enumerate().map(move |(index, track)|{ + let width = if Some(index) == active { big } else { track.width.max(8) }; + let data = (index, track, x, x + width); + x += width; + data + }) + } + + fn play_row (&self, tracks_w: u16) -> impl Content + '_ { + let h = 2; + Fixed::y(h, Bsp::e( + Fixed::xy(self.sidebar_w() as u16, h, self.play_row_header()), + Fill::x(Align::c(Fixed::xy(tracks_w, h, self.play_row_cells()))) + )) + } + fn play_row_header (&self) -> BoxThunk { + (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Playing")).boxed()).into() + } + fn play_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { + //let color = track.color(); + let color: ItemPalette = track.color().dark.into(); + let timebase = self.clock().timebase(); + let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb, + if let Some((_, Some(clip))) = track.player.play_clip().as_ref() { + let length = clip.read().unwrap().length; + let elapsed = track.player.pulses_since_start().unwrap() as usize; + format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64)) + } else { + String::new() + }); + let cell = Bsp::s(value, phat_hi(color.dark.rgb, color.darker.rgb)); + Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell)) + })).boxed()).into() + } + + fn next_row (&self, tracks_w: u16) -> impl Content + '_ { + let h = 2; + Fixed::y(h, Bsp::e( + Fixed::xy(self.sidebar_w() as u16, h, self.next_row_header()), + Fill::x(Align::c(Fixed::xy(tracks_w, h, self.next_row_cells()))) + )) + } + fn next_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Next")).boxed()).into() + } + fn next_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { + let color: ItemPalette = track.color(); + let color: ItemPalette = track.color().dark.into(); + let current = &self.clock().playhead; + let timebase = ¤t.timebase; + let cell = Self::cell(color, Tui::bold(true, { + let mut result = String::new(); + if let Some((t, _)) = track.player.next_clip().as_ref() { + let target = t.pulse.get(); + let current = current.pulse.get(); + if target > current { + result = format!("-{:>}", timebase.format_beats_0_short(target - current)) + } + } + result + })); + let cell = Tui::fg_bg(color.lightest.rgb, color.base.rgb, cell); + let cell = Bsp::s(cell, phat_hi(color.dark.rgb, color.darker.rgb)); + Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell)) + })).boxed()).into() + } + /// beats until switchover + fn cell_until_next (track: &ArrangerTrack, current: &Arc) + -> Option> + { + let timebase = ¤t.timebase; + let mut result = String::new(); + if let Some((t, _)) = track.player.next_clip().as_ref() { + let target = t.pulse.get(); + let current = current.pulse.get(); + if target > current { + result = format!("-{:>}", timebase.format_beats_0_short(target - current)) + } + } + Some(result) + } + + fn track_row (&self, tracks_w: u16) -> impl Content + '_ { + let h = 3; + let border = |x|Skinny(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x); + Fixed::y(h, Bsp::e( + Fixed::xy(self.sidebar_w() as u16, h, self.track_row_header()), + Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.track_row_cells())))) + )) + } + fn track_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Track")).boxed()).into() + } + fn track_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + let iter = ||self.tracks_with_sizes(); + (move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| { + let name = Push::x(1, &track.name); + let color = track.color(); + let fg = color.lightest.rgb; + let bg = color.base.rgb; + Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16, + Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name)))) + )) + })).boxed()).into() + } + + fn input_row (&self, tracks_w: u16) -> impl Content + '_ { + let h = 1 + self.midi_ins[0].connect.len() as u16; + Fixed::y(h, Bsp::e( + Fixed::xy(self.sidebar_w() as u16, h, self.input_row_header()), + Fill::x(Align::c(Fixed::xy(tracks_w, h, self.input_row_cells()))) + )) + } + fn input_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (||Bsp::s( + Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), + Align::w(&self.midi_ins[0].name)))), + self.midi_ins.get(0) + .and_then(|midi_in|midi_in.connect.get(0)) + .map(|connect|Fill::x(Align::w( + Tui::bold(false, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), connect.info()))))) + ).boxed()).into() + } + fn input_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { + let w = (x2 - x1) as u16; + let cell = Bsp::s("[Rec]", "[Mon]"); + let color: ItemPalette = track.color().dark.into(); + map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, cell))) + })).boxed()).into() + } + + fn output_row (&self, tracks_w: u16) -> impl Content + '_ { + let h = 1 + self.midi_outs[0].connect.len() as u16; + Fixed::y(h, Bsp::e( + Fixed::xy(self.sidebar_w() as u16, h, self.output_row_header()), + Fill::x(Align::c(Fixed::xy(tracks_w, h, self.output_row_cells()))) + )) + } + fn output_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (||Bsp::s( + Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), + Align::w(&self.midi_outs[0].name)))), + self.midi_outs.get(0) + .and_then(|midi_out|midi_out.connect.get(0)) + .map(|connect|Fill::x(Align::w( + Tui::bold(false, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), connect.info()))))) + ).boxed()).into() + } + fn output_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { + let w = (x2 - x1) as u16; + let color: ItemPalette = track.color().dark.into(); + let cell = Bsp::s(format!(" M S "), phat_hi(color.dark.rgb, color.darker.rgb)); + map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, cell))) + })).boxed()).into() + } + + fn scene_row (&self, tracks_w: u16) -> impl Content + '_ { + let h = self.size.h() as u16; + let border = |x|Skinny(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x); + Fill::y(Bsp::e( + Tui::bg(Color::Reset, Fixed::x(self.sidebar_w() as u16, self.scene_row_headers())), + Tui::bg(Color::Reset, Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.scene_row_cells()))))) + )) + } + fn scene_row_headers <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (||{ + let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); + let selected_scene = match self.selected { + ArrangerSelection::Scene(s) => Some(s), + _ => None + }; + Fill::y(Align::c(Map::new(||self.scenes_with_sizes(2), move|(_, scene, y1, y2), i| { + let h = (y2 - y1) as u16; + let name = format!("๐Ÿญฌ{}", &scene.name); + let color = scene.color(); + let top = (selected_scene.map(|s|s + 1) == Some(i)) + .then(||last_color.read().unwrap().base.rgb); + let active = selected_scene == Some(i); + let mid = if active { color.light } else { color.base }; + let cell = phat_sel_3( + active, + Tui::bold(true, name.clone()), + Tui::bold(true, name), + top, + mid.rgb, + Color::Rgb(0, 0, 0) + ); + *last_color.write().unwrap() = color; + map_south(y1 as u16, h + 1, Fixed::y(h + 1, cell)) + }))).boxed() + }).into() + } + fn scene_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + let editing = self.is_editing(); + let (selected_track, selected_scene) = match self.selected { + ArrangerSelection::Clip(t, s) => (Some(t), Some(s)), + _ => (None, None) + }; + let tracks = move||self.tracks_with_sizes(); + let scenes = ||self.scenes_with_sizes(2); + (move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| { + let w = (x2 - x1) as u16; + let color: ItemPalette = track.color().dark.into(); + let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); + let cells = Map::new(scenes, move|(_, scene, y1, y2), s| { + let h = (y2 - y1) as u16; + let color = scene.color(); + let name = "โน "; + let last = last_color.read().unwrap().clone(); + //let cell = phat_sel_3( + //selected_track == Some(i) && selected_scene == Some(j), + //Tui::fg(TuiTheme::g(64), Push::x(1, name)), + //Tui::fg(TuiTheme::g(64), Push::x(1, name)), + //if selected_track == Some(i) && selected_scene.map(|s|s+1) == Some(j) { + //None + //} else { + //Some(TuiTheme::g(32).into()) + //}, + //TuiTheme::g(32).into(), + //TuiTheme::g(32).into(), + //); + let active = editing && selected_scene == Some(s) && selected_track == Some(t); + let editor = Thunk::new(||&self.editor); + let cell = Thunk::new(move||phat_sel_3( + selected_track == Some(t) && selected_scene == Some(s), + Tui::fg(TuiTheme::g(64), Push::x(1, Tui::bold(true, name.to_string()))), + Tui::fg(TuiTheme::g(64), Push::x(1, Tui::bold(true, name.to_string()))), + if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) { + None + } else { + Some(TuiTheme::g(32).into()) + }, + TuiTheme::g(32).into(), + TuiTheme::g(32).into(), + )); + let cell = Either(active, editor, cell); + map_south( + y1 as u16, + h + 1, + Fill::x(cell) + ) + }); + let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed()); + Fixed::x(w, map_east(x1 as u16, w, column)) + }))).boxed()).into() + } + fn cell_clip <'a> ( + scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16 + ) -> impl Content + use<'a> { + scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{ + let clip = clip.read().unwrap(); + let mut bg = TuiTheme::border_bg(); + let name = clip.name.to_string(); + let max_w = name.len().min((w as usize).saturating_sub(2)); + let color = clip.color; + bg = color.dark.rgb; + if let Some((_, Some(ref playing))) = track.player.play_clip() { + if *playing.read().unwrap() == *clip { + bg = color.light.rgb + } + }; + Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w])))); + })) + } + + fn track_column_separators <'a> (&'a self) -> impl Content + 'a { + let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); + let fg = Color::Rgb(64,64,64); + Map::new(move||self.tracks_with_sizes(), move|(_n, _track, x1, x2), _i|{ + Push::x(scenes_w, map_east(x1 as u16, (x2 - x1) as u16, + Fixed::x((x2 - x1) as u16, Tui::fg(fg, RepeatV(&"ยท"))))) + }) + } + + pub fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { + let mut widths = vec![]; + let mut total = 0; + for track in tracks.iter() { + let width = track.width; + widths.push((width, total)); + total += width; + } + widths.push((0, total)); + widths + } + + fn scene_row_sep <'a> (&'a self) -> impl Content + 'a { + let fg = Color::Rgb(255,255,255); + Map::new(move||self.scenes_with_sizes(1), |_, _|"") + //Map(||rows.iter(), |(_n, _scene, y1, _y2), _i| { + //let y = to.area().y() + (y / PPQ) as u16 + 1; + //if y >= to.buffer.area.height { break } + //for x in to.area().x()..to.area().x2().saturating_sub(2) { + ////if x < to.buffer.area.x && y < to.buffer.area.y { + //if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((x, y))) { + //cell.modifier = Modifier::UNDERLINED; + //cell.underline_color = fg; + //} + ////} + //} + //}) + } + + //pub fn scene_heights (scenes: &[ArrangerScene], factor: usize) -> Vec<(usize, usize)> { + //let mut total = 0; + //if factor == 0 { + //scenes.iter().map(|scene|{ + //let pulses = scene.pulses().max(PPQ); + //total += pulses; + //(pulses, total - pulses) + //}).collect() + //} else { + //(0..=scenes.len()).map(|i|{ + //(factor*PPQ, factor*PPQ*i) + //}).collect() + //} + //} + //fn cursor (&self) -> impl Content + '_ { + //let color = self.color; + //let bg = color.lighter.rgb;//Color::Rgb(0, 255, 0); + //let selected = self.selected(); + //let cols = Arranger::track_widths(&self.tracks); + //let rows = Arranger::scene_heights(&self.scenes, 1); + //let scenes_w = 16.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); + //let focused = true; + //let reticle = Reticle(Style { + //fg: Some(self.color.lighter.rgb), + //bg: None, + //underline_color: None, + //add_modifier: Modifier::empty(), + //sub_modifier: Modifier::DIM + //}); + //RenderThunk::new(move|to: &mut TuiOut|{ + //let area = to.area(); + //let [x, y, w, h] = area.xywh(); + //let mut track_area: Option<[u16;4]> = match selected { + //ArrangerSelection::Track(t) | ArrangerSelection::Clip(t, _) => Some([ + //x + scenes_w + cols[t].1 as u16, y, + //cols[t].0 as u16, h, + //]), + //_ => None + //}; + //let mut scene_area: Option<[u16;4]> = match selected { + //ArrangerSelection::Scene(s) | ArrangerSelection::Clip(_, s) => Some([ + //x, y + HEADER_H + (rows[s].1 / PPQ) as u16, + //w, (rows[s].0 / PPQ) as u16 + //]), + //_ => None + //}; + //let mut clip_area: Option<[u16;4]> = match selected { + //ArrangerSelection::Clip(t, s) => Some([ + //(scenes_w + x + cols[t].1 as u16).saturating_sub(1), + //HEADER_H + y + (rows[s].1/PPQ) as u16, + //cols[t].0 as u16 + 2, + //(rows[s].0 / PPQ) as u16 + //]), + //_ => None + //}; + //if let Some([x, y, width, height]) = track_area { + //to.fill_fg([x, y, 1, height], bg); + //to.fill_fg([x + width, y, 1, height], bg); + //} + //if let Some([_, y, _, height]) = scene_area { + //to.fill_ul([x, y - 1, w, 1], bg); + //to.fill_ul([x, y + height - 1, w, 1], bg); + //} + //if focused { + //to.place(if let Some(clip_area) = clip_area { + //clip_area + //} else if let Some(track_area) = track_area { + //track_area.clip_h(HEADER_H) + //} else if let Some(scene_area) = scene_area { + //scene_area.clip_w(scenes_w) + //} else { + //area.clip_w(scenes_w).clip_h(HEADER_H) + //}, &reticle) + //}; + //}) + //} + + /// A 1-row cell. + fn cell > (color: ItemPalette, field: T) -> impl Content { + Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field)) + } +} +/// A phat line +fn phat_lo (fg: Color, bg: Color) -> impl Content { + Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"โ–„"))) +} +/// A phat line +fn phat_hi (fg: Color, bg: Color) -> impl Content { + Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"โ–€"))) +} +/// A cell that is 3-row on its own, but stacks, giving (N+1)*2 rows per N cells. +fn phat_cell > ( + color: ItemPalette, last: ItemPalette, field: T +) -> impl Content { + Bsp::s(phat_lo(color.base.rgb, last.base.rgb), + Bsp::n(phat_hi(color.base.rgb, last.base.rgb), + Fixed::y(1, Fill::x(Tui::fg_bg(color.lightest.rgb, color.base.rgb, field))), + ) + ) +} +fn phat_cell_3 > ( + field: T, top: Color, middle: Color, bottom: Color +) -> impl Content { + Bsp::s(phat_lo(middle, top), + Bsp::n(phat_hi(middle, bottom), + Fill::y(Fill::x(Tui::bg(middle, field))), + ) + ) +} +fn phat_sel_3 > ( + selected: bool, field_1: T, field_2: T, top: Option, middle: Color, bottom: Color +) -> impl Content { + let border = Style::default().fg(Color::Rgb(255,255,255)).bg(middle); + Either(selected, + Tui::bg(middle, Outer(border).enclose(Align::w(Bsp::s("", Bsp::n("", Fill::y(field_1)))))), + Bsp::s(Fixed::y(1, top.map(|top|phat_lo(middle, top))), + Bsp::n(Fixed::y(1, phat_hi(middle, bottom)), + Fill::xy(Tui::bg(middle, field_2)), + ) + ) + ) } audio!(|self: Arranger, client, scope|{ // Start profiling cycle @@ -107,488 +610,198 @@ impl Arranger { } } } -pub(crate) const HEADER_H: u16 = 0; // 5 -pub(crate) const SCENES_W_OFFSET: u16 = 0; -render!(TuiOut: (self: Arranger) => { - let scenes_w = self.sidebar_w(); - let tracks_w = self.tracks_with_widths().last().unwrap().3 as u16; - let row_head = |h, head|Fixed::x(scenes_w, head); - let row_cells = |h, cells|Fill::x(Align::c(Fixed::xy(tracks_w, h, cells))); - let row = move|h, head, cells|Fixed::y(h, Bsp::e(row_head(h, head), row_cells(h, cells))); - let big = move|h, head, cells|Fill::y(Bsp::e( - Tui::bg(Color::Reset, row_head(h, head)), - Tui::bg(Color::Reset, row_cells(h, cells)) - )); - //let row = move|h, header, cells|Align::w(Bsp::e( - //Align::w(Fixed::xy(scenes_w, h, header)), - //Fixed::xy(self.tracks.len() as u16*12, h, cells) - //)); - let h = self.size.h() as u16; - let toolbar = |x|Bsp::s(self.toolbar_view(), x); - let pool = |x|Bsp::w(self.pool_view(), x); - let editing = |x|Bsp::n(Bsp::e(self.editor.clip_status(), self.editor.edit_status()), x); - let outputs = |x|Bsp::s(row(2, self.output_row_header(), self.output_row_cells()), Fill::y(x)); - let playing = |x|Bsp::s(row(2, self.elapsed_row_header(), self.elapsed_row_cells()), Fill::y(x)); - let next = |x|Bsp::s(row(2, self.next_row_header(), self.next_row_cells()), Fill::y(x)); - let tracks = |x|Bsp::s(row(3, self.track_row_header(), self.track_row_cells()), Fill::y(Align::c(x))); - let scenes = |x|Bsp::s(big(h.saturating_sub(13), self.scene_row_headers(), self.scene_row_cells()), x); - let inputs = |x|Bsp::n(row(2, self.input_row_header(), self.input_row_cells()), Fill::y(x)); - self.size.of(toolbar(Fill::xy(Align::c(editing(pool(inputs(outputs(playing(next(tracks(scenes(Fill::xy(""))))))))))))) - //let enclosed = |x|Outer(Style::default().fg(Color::Rgb(72,72,72))).enclose(x); - //.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); - //Bsp::s(arrrrrr, enclosed(&self.editor)) -}); -impl Arranger { - pub const LEFT_SEP: char = 'โ–Ž'; - pub const TRACK_MIN_WIDTH: usize = 4; - pub fn scenes_with_heights (&self, h: usize) -> impl Iterator { - let mut y = 0; - let editing = self.editing; - let (selected_track, selected_scene) = match self.selected { - ArrangerSelection::Clip(t, s) => (Some(t), Some(s)), - _ => (None, None) - }; - self.scenes.iter().enumerate().map(move|(s, scene)|{ - let active = editing && selected_track.is_some() && selected_scene == Some(s); - let height = if active { 15 } else { h }; - let data = (s, scene, y, y + height); - y += height; - data - }) - } - pub fn tracks_with_widths (&self) - -> impl Iterator - { - let active = match self.selected { - ArrangerSelection::Track(t) => Some(t), - ArrangerSelection::Clip(t, _) => Some(t), +#[derive(Clone, Debug)] pub enum ArrangerCommand { + History(isize), + Color(ItemPalette), + Clock(ClockCommand), + Scene(ArrangerSceneCommand), + Track(ArrangerTrackCommand), + Clip(ArrangerClipCommand), + Select(ArrangerSelection), + Zoom(usize), + Phrases(PoolCommand), + Editor(MidiEditCommand), + StopAll, + Clear, +} + +#[derive(Clone, Debug)] +pub enum ArrangerClipCommand { + Get(usize, usize), + Put(usize, usize, Option>>), + Enqueue(usize, usize), + Edit(Option>>), + SetLoop(usize, usize, bool), + SetColor(usize, usize, ItemPalette), +} + +//handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event())); +//input_to_command!(ArrangerCommand: |state: Arranger, input: Event|{KEYS_ARRANGER.handle(state, input)?}); + +keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand { + key(Char('u')) => Cmd::History(-1), + key(Char('U')) => Cmd::History(1), + // TODO: k: toggle on-screen keyboard + ctrl(key(Char('k'))) => { todo!("keyboard") }, + // Transport: Play/pause + key(Char(' ')) => Cmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), + // Transport: Play from start or rewind to start + shift(key(Char(' '))) => Cmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), + key(Char('e')) => Cmd::Editor(MidiEditCommand::Show(Some(state.pool.clip().clone()))), + ctrl(key(Char('a'))) => Cmd::Scene(ArrangerSceneCommand::Add), + ctrl(key(Char('t'))) => Cmd::Track(ArrangerTrackCommand::Add), + // Tab: Toggle visibility of clip pool column + key(Tab) => Cmd::Phrases(PoolCommand::Show(!state.pool.visible)), +}, { + use ArrangerSelection as Selected; + use ArrangerSceneCommand as Scene; + use ArrangerTrackCommand as Track; + use ArrangerClipCommand as Clip; + let t_len = state.tracks.len(); + let s_len = state.scenes.len(); + match state.selected() { + Selected::Clip(t, s) => match input { + kpat!(Char('g')) => Some(Cmd::Phrases(PoolCommand::Select(0))), + kpat!(Char('q')) => Some(Cmd::Clip(Clip::Enqueue(t, s))), + kpat!(Char(',')) => Some(Cmd::Clip(Clip::Put(t, s, None))), + kpat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))), + kpat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))), + kpat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))), + kpat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.pool.clip().clone())))), + kpat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))), + kpat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))), + + kpat!(Up) => Some(Cmd::Select( + if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) })), + kpat!(Down) => Some(Cmd::Select( + Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1))))), + kpat!(Left) => Some(Cmd::Select( + if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) })), + kpat!(Right) => Some(Cmd::Select( + Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s))), + _ => None - }; - Self::tracks_with_widths_static(self.tracks.as_slice(), active) - } - fn tracks_with_widths_static (tracks: &[ArrangerTrack], active: Option) - -> impl Iterator - { - let mut x = 0; - tracks.iter().enumerate().map(move |(index, track)|{ - let width = if Some(index) == active { 40 } else { track.width.max(8) }; - let data = (index, track, x, x + width); - x += width; - data - }) - } + }, + Selected::Scene(s) => match input { + kpat!(Char(',')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))), + kpat!(Char('.')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))), + kpat!(Char('<')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))), + kpat!(Char('>')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))), + kpat!(Char('q')) => Some(Cmd::Scene(Scene::Enqueue(s))), + kpat!(Delete) => Some(Cmd::Scene(Scene::Delete(s))), + kpat!(Char('c')) => Some(Cmd::Scene(Scene::SetColor(s, ItemPalette::random()))), - fn output_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - let fg = TuiTheme::g(192); - let bg = TuiTheme::g(48); - (move||Tui::bold(true, Tui::fg_bg(fg, bg, "[ ] Out 1: NI")).boxed()).into() - } - fn output_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - //let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); - (move||Align::x(Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| { - let w = (x2 - x1) as u16; - let color: ItemPalette = track.color().dark.into(); - let cell = Bsp::s(format!(" M S "), phat_hi(color.dark.rgb, color.darker.rgb)); - map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, cell))) - })).boxed()).into() - } + kpat!(Up) => Some( + Cmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix })), + kpat!(Down) => Some( + Cmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1))))), + kpat!(Left) => + return None, + kpat!(Right) => Some( + Cmd::Select(Selected::Clip(0, s))), - fn elapsed_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Playing")).boxed()).into() - } - fn elapsed_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (move||Align::x(Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| { - //let color = track.color(); - let color: ItemPalette = track.color().dark.into(); - let timebase = self.clock().timebase(); - let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb, - if let Some((_, Some(clip))) = track.player.play_clip().as_ref() { - let length = clip.read().unwrap().length; - let elapsed = track.player.pulses_since_start().unwrap() as usize; - format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64)) - } else { - String::new() - }); - let cell = Bsp::s(value, phat_hi(color.dark.rgb, color.darker.rgb)); - Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell)) - })).boxed()).into() - } + _ => None + }, + Selected::Track(t) => match input { + kpat!(Char(',')) => Some(Cmd::Track(Track::Swap(t, t - 1))), + kpat!(Char('.')) => Some(Cmd::Track(Track::Swap(t, t + 1))), + kpat!(Char('<')) => Some(Cmd::Track(Track::Swap(t, t - 1))), + kpat!(Char('>')) => Some(Cmd::Track(Track::Swap(t, t + 1))), + kpat!(Delete) => Some(Cmd::Track(Track::Delete(t))), + kpat!(Char('c')) => Some(Cmd::Track(Track::SetColor(t, ItemPalette::random()))), - fn next_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Next")).boxed()).into() - } - fn next_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (move||Align::x(Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| { - let color: ItemPalette = track.color(); - let color: ItemPalette = track.color().dark.into(); - let cell = Self::cell_until_next(track, &self.clock().playhead); - let cell = Self::cell(color, Tui::bold(true, cell)); - let cell = Tui::fg_bg(color.lightest.rgb, color.base.rgb, cell); - let cell = Bsp::s(cell, phat_hi(color.dark.rgb, color.darker.rgb)); - Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell)) - })).boxed()).into() - } + kpat!(Up) => + return None, + kpat!(Down) => Some( + Cmd::Select(Selected::Clip(t, 0))), + kpat!(Left) => Some( + Cmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix })), + kpat!(Right) => Some( + Cmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1))))), - fn track_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Track")).boxed()).into() - } - fn track_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - let iter = ||self.tracks_with_widths(); - (move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| { - let color = track.color(); - let name = Push::x(1, &track.name); - Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, - Tui::fg_bg(color.lightest.rgb, color.base.rgb, - //phat_cell(color, color.darkest.rgb.into(), - Tui::bold(true, Fill::x(Align::x(name)))))) })).boxed()).into() - } + _ => None + }, + Selected::Mix => match input { + kpat!(Delete) => Some(Cmd::Clear), + kpat!(Char('0')) => Some(Cmd::StopAll), + kpat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())), - fn input_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (||Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(0), TuiTheme::g(200), "[ ] In 1: Korg"))).boxed()).into() - } - fn input_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (move||Align::x(Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| { - let w = (x2 - x1) as u16; - let cell = Bsp::s("[Rec]", "[Mon]"); - let color: ItemPalette = track.color().dark.into(); - map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, cell))) - })).boxed()).into() - } + kpat!(Up) => + return None, + kpat!(Down) => Some( + Cmd::Select(Selected::Scene(0))), + kpat!(Left) => + return None, + kpat!(Right) => Some( + Cmd::Select(Selected::Track(0))), - fn scene_row_headers <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (||{ - let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); - let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); - let selected_scene = match self.selected { - ArrangerSelection::Scene(s) => Some(s), - _ => None - }; - Fill::y(Align::y(Map::new( - ||self.scenes_with_heights(2), - move|(_, scene, y1, y2), i| { - let h = (y2 - y1) as u16; - let name = format!("๐Ÿญฌ{}", &scene.name); - let color = scene.color(); - let cell = phat_sel_3( - selected_scene == Some(i), - Push::x(1, Tui::bold(true, name.clone())), - Push::x(1, Tui::bold(true, name)), - if selected_scene.map(|s|s + 1) == Some(i) { - None - } else { - Some(last_color.read().unwrap().base.rgb) - }, - if selected_scene == Some(i) { - color.light.rgb - } else { - color.base.rgb - }, - Color::Rgb(0, 0, 0) - ); - *last_color.write().unwrap() = color; - map_south(y1 as u16, h + 1, Fixed::y(h + 1, cell)) - } - ))).boxed() - }).into() - } - fn scene_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - let editing = self.editing; - let (selected_track, selected_scene) = match self.selected { - ArrangerSelection::Clip(t, s) => (Some(t), Some(s)), - _ => (None, None) - }; - let tracks = move||self.tracks_with_widths(); - let scenes = ||self.scenes_with_heights(2); - (move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| { - let w = (x2 - x1) as u16; - let color: ItemPalette = track.color().dark.into(); - let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); - let cells = Map::new(scenes, move|(_, scene, y1, y2), s| { - let h = (y2 - y1) as u16; - let color = scene.color(); - let name = "โน "; - let last = last_color.read().unwrap().clone(); - //let cell = phat_sel_3( - //selected_track == Some(i) && selected_scene == Some(j), - //Tui::fg(TuiTheme::g(64), Push::x(1, name)), - //Tui::fg(TuiTheme::g(64), Push::x(1, name)), - //if selected_track == Some(i) && selected_scene.map(|s|s+1) == Some(j) { - //None - //} else { - //Some(TuiTheme::g(32).into()) - //}, - //TuiTheme::g(32).into(), - //TuiTheme::g(32).into(), - //); - let active = editing && selected_scene == Some(s) && selected_track == Some(t); - let editor = Thunk::new(||&self.editor); - let cell = Thunk::new(move||phat_sel_3( - selected_track == Some(t) && selected_scene == Some(s), - Tui::fg(TuiTheme::g(64), Push::x(1, Tui::bold(true, name.to_string()))), - Tui::fg(TuiTheme::g(64), Push::x(1, Tui::bold(true, name.to_string()))), - if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) { - None - } else { - Some(TuiTheme::g(32).into()) - }, - TuiTheme::g(32).into(), - TuiTheme::g(32).into(), - )); - let cell = Either(active, editor, cell); - map_south( - y1 as u16, - h + 1, - Fill::x(cell) - ) - }); - map_east( - x1 as u16, - w, - Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed()) - ) - }))).boxed()).into() + _ => None + }, } +}.or_else(||if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { + Some(Cmd::Editor(command)) +} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { + Some(Cmd::Phrases(command)) +} else { + None +})?); - fn track_column_separators <'a> (&'a self) -> impl Content + 'a { - let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); - let fg = Color::Rgb(64,64,64); - Map::new(move||self.tracks_with_widths(), move|(_n, _track, x1, x2), _i|{ - Push::x(scenes_w, map_east(x1 as u16, (x2 - x1) as u16, - Fixed::x((x2 - x1) as u16, Tui::fg(fg, RepeatV(&"ยท"))))) - }) - } - /// name and width of track - fn cell_name (track: &ArrangerTrack, _w: usize) -> impl Content { - Tui::bold(true, Tui::fg(track.color.lightest.rgb, track.name().clone())) - } - /// beats until switchover - fn cell_until_next (track: &ArrangerTrack, current: &Arc) - -> Option> - { - let timebase = ¤t.timebase; - let mut result = String::new(); - if let Some((t, _)) = track.player.next_clip().as_ref() { - let target = t.pulse.get(); - let current = current.pulse.get(); - if target > current { - result = format!("-{:>}", timebase.format_beats_0_short(target - current)) - } +command!(|self: ArrangerCommand, state: Arranger|match self { + Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, + Self::Clip(cmd) => cmd.delegate(state, Self::Clip)?, + Self::Scene(cmd) => cmd.delegate(state, Self::Scene)?, + Self::Track(cmd) => cmd.delegate(state, Self::Track)?, + Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, + Self::Zoom(_) => { todo!(); }, + Self::Select(selected) => { + *state.selected_mut() = selected; + None + }, + Self::Color(palette) => { + let old = state.color; + state.color = palette; + Some(Self::Color(old)) + }, + Self::Phrases(cmd) => { + match cmd { + // autoselect: automatically load selected clip in editor + PoolCommand::Select(_) => { + let undo = cmd.delegate(&mut state.pool, Self::Phrases)?; + state.editor.set_clip(Some(state.pool.clip())); + undo + }, + // reload clip in editor to update color + PoolCommand::Phrase(MidiPoolCommand::SetColor(index, _)) => { + let undo = cmd.delegate(&mut state.pool, Self::Phrases)?; + state.editor.set_clip(Some(state.pool.clip())); + undo + }, + _ => cmd.delegate(&mut state.pool, Self::Phrases)? } - Some(result) - } - - fn scene_rows (&self) -> impl Content + use<'_> { - let scenes_w = 16.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); - Map::new(||self.scenes_with_heights(1), move|(_, scene, y1, y2), i| { - let h = (y2 - y1) as u16; - let color = scene.color(); - let cell = Self::cell(color, scene.name.clone()); - let cell = Fixed::y(h, Fixed::x(scenes_w, cell)); - map_south(y1 as u16, 1, cell) - }) - } - pub fn scene_heights (scenes: &[ArrangerScene], factor: usize) -> Vec<(usize, usize)> { - let mut total = 0; - if factor == 0 { - scenes.iter().map(|scene|{ - let pulses = scene.pulses().max(PPQ); - total += pulses; - (pulses, total - pulses) - }).collect() - } else { - (0..=scenes.len()).map(|i|{ - (factor*PPQ, factor*PPQ*i) - }).collect() + }, + Self::History(_) => { todo!() }, + Self::StopAll => { + for track in 0..state.tracks.len() { + state.tracks[track].player.enqueue_next(None); } - } - - fn cell_scene <'a> ( - tracks: &'a [ArrangerTrack], scene: &'a ArrangerScene, pulses: usize - ) -> impl Content + use<'a> { - let height = 1.max((pulses / PPQ) as u16); - let playing = scene.is_playing(tracks); - let icon = Tui::bg( - scene.color.base.rgb, if playing { "โ–ถ " } else { " " } - ); - let name = Tui::fg_bg(scene.color.lightest.rgb, scene.color.base.rgb, - Expand::x(1, Tui::bold(true, scene.name.clone())) - ); - let clips = Map::new(||Arranger::tracks_with_widths_static(tracks, None), move|(index, track, x1, x2), _| - Push::x((x2 - x1) as u16, Self::cell_clip(scene, index, track, (x2 - x1) as u16, height)) - ); - Fixed::y(height, Bsp::e(icon, Bsp::e(name, clips))) - } - - fn cell_clip <'a> ( - scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16 - ) -> impl Content + use<'a> { - scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{ - let clip = clip.read().unwrap(); - let mut bg = TuiTheme::border_bg(); - let name = clip.name.to_string(); - let max_w = name.len().min((w as usize).saturating_sub(2)); - let color = clip.color; - bg = color.dark.rgb; - if let Some((_, Some(ref playing))) = track.player.play_clip() { - if *playing.read().unwrap() == *clip { - bg = color.light.rgb - } - }; - Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w])))); - })) - } - fn toolbar_view (&self) -> impl Content + use<'_> { - Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock)))) - } - fn sidebar_w (&self) -> u16 { - let w = self.size.w(); - let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let w = if self.pool.visible { w } else { 0 }; - w - } - fn pool_view (&self) -> impl Content + use<'_> { - let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool)))); - Fixed::x(self.sidebar_w(), Align::e(Fill::y(PoolView(self.compact, &self.pool)))) - } - - pub fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { - let mut widths = vec![]; - let mut total = 0; - for track in tracks.iter() { - let width = track.width; - widths.push((width, total)); - total += width; - } - widths.push((0, total)); - widths - } - - fn scene_row_sep <'a> (&'a self) -> impl Content + 'a { - let fg = Color::Rgb(255,255,255); - Map::new(move||self.scenes_with_heights(1), |_, _|"") - //Map(||rows.iter(), |(_n, _scene, y1, _y2), _i| { - //let y = to.area().y() + (y / PPQ) as u16 + 1; - //if y >= to.buffer.area.height { break } - //for x in to.area().x()..to.area().x2().saturating_sub(2) { - ////if x < to.buffer.area.x && y < to.buffer.area.y { - //if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((x, y))) { - //cell.modifier = Modifier::UNDERLINED; - //cell.underline_color = fg; - //} - ////} - //} - //}) - } - - fn cursor (&self) -> impl Content + '_ { - let color = self.color; - let bg = color.lighter.rgb;//Color::Rgb(0, 255, 0); - let selected = self.selected(); - let cols = Arranger::track_widths(&self.tracks); - let rows = Arranger::scene_heights(&self.scenes, 1); - let scenes_w = 16.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); - let focused = true; - let reticle = Reticle(Style { - fg: Some(self.color.lighter.rgb), - bg: None, - underline_color: None, - add_modifier: Modifier::empty(), - sub_modifier: Modifier::DIM - }); - RenderThunk::new(move|to: &mut TuiOut|{ - let area = to.area(); - let [x, y, w, h] = area.xywh(); - let mut track_area: Option<[u16;4]> = match selected { - ArrangerSelection::Track(t) | ArrangerSelection::Clip(t, _) => Some([ - x + scenes_w + cols[t].1 as u16, y, - cols[t].0 as u16, h, - ]), - _ => None - }; - let mut scene_area: Option<[u16;4]> = match selected { - ArrangerSelection::Scene(s) | ArrangerSelection::Clip(_, s) => Some([ - x, y + HEADER_H + (rows[s].1 / PPQ) as u16, - w, (rows[s].0 / PPQ) as u16 - ]), - _ => None - }; - let mut clip_area: Option<[u16;4]> = match selected { - ArrangerSelection::Clip(t, s) => Some([ - (scenes_w + x + cols[t].1 as u16).saturating_sub(1), - HEADER_H + y + (rows[s].1/PPQ) as u16, - cols[t].0 as u16 + 2, - (rows[s].0 / PPQ) as u16 - ]), - _ => None - }; - if let Some([x, y, width, height]) = track_area { - to.fill_fg([x, y, 1, height], bg); - to.fill_fg([x + width, y, 1, height], bg); - } - if let Some([_, y, _, height]) = scene_area { - to.fill_ul([x, y - 1, w, 1], bg); - to.fill_ul([x, y + height - 1, w, 1], bg); - } - if focused { - to.place(if let Some(clip_area) = clip_area { - clip_area - } else if let Some(track_area) = track_area { - track_area.clip_h(HEADER_H) - } else if let Some(scene_area) = scene_area { - scene_area.clip_w(scenes_w) - } else { - area.clip_w(scenes_w).clip_h(HEADER_H) - }, &reticle) - }; - }) - } - - /// A 1-row cell. - fn cell > (color: ItemPalette, field: T) -> impl Content { - Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field)) - } -} -/// A phat line -fn phat_lo (fg: Color, bg: Color) -> impl Content { - Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"โ–„"))) -} -/// A phat line -fn phat_hi (fg: Color, bg: Color) -> impl Content { - Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"โ–€"))) -} -/// A cell that is 3-row on its own, but stacks, giving (N+1)*2 rows per N cells. -fn phat_cell > ( - color: ItemPalette, last: ItemPalette, field: T -) -> impl Content { - Bsp::s(phat_lo(color.base.rgb, last.base.rgb), - Bsp::n(phat_hi(color.base.rgb, last.base.rgb), - Fixed::y(1, Fill::x(Tui::fg_bg(color.lightest.rgb, color.base.rgb, field))), - ) - ) -} -fn phat_cell_3 > ( - field: T, top: Color, middle: Color, bottom: Color -) -> impl Content { - Bsp::s(phat_lo(middle, top), - Bsp::n(phat_hi(middle, bottom), - Fill::y(Fill::x(Tui::bg(middle, field))), - ) - ) -} -fn phat_sel_3 > ( - selected: bool, field_1: T, field_2: T, top: Option, middle: Color, bottom: Color -) -> impl Content { - let border = Style::default().fg(Color::Rgb(255,255,255)).bg(middle); - Either(selected, - Tui::bg(middle, Outer(border).enclose(Align::w(Bsp::s("", Bsp::n("", Fill::y(field_1)))))), - Bsp::s(Fixed::y(1, top.map(|top|phat_lo(middle, top))), - Bsp::n(Fixed::y(1, phat_hi(middle, bottom)), - Fill::xy(Tui::bg(middle, field_2)), - ) - ) - ) -} + None + }, + Self::Clear => { todo!() }, +}); +command!(|self: ArrangerClipCommand, state: Arranger|match self { + Self::Get(track, scene) => { todo!() }, + Self::Put(track, scene, clip) => { + let old = state.scenes[scene].clips[track].clone(); + state.scenes[scene].clips[track] = clip; + Some(Self::Put(track, scene, old)) + }, + Self::Enqueue(track, scene) => { + state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); + None + }, + _ => None +}); //pub struct ArrangerVCursor { //cols: Vec<(usize, usize)>, diff --git a/tek/src/arranger/arranger_scene.rs b/tek/src/arranger/arranger_scene.rs index e1fb875b..4aba7fcf 100644 --- a/tek/src/arranger/arranger_scene.rs +++ b/tek/src/arranger/arranger_scene.rs @@ -1,4 +1,36 @@ use crate::*; +#[derive(Clone, Debug)] +pub enum ArrangerSceneCommand { + Add, + Delete(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), + SetColor(usize, ItemPalette), + Enqueue(usize), +} +command!(|self: ArrangerSceneCommand, state: Arranger|match self { + Self::Add => { + state.scene_add(None, None)?; + None + } + Self::Delete(index) => { + state.scene_del(index); + None + }, + Self::SetColor(index, color) => { + let old = state.scenes[index].color; + state.scenes[index].color = color; + Some(Self::SetColor(index, old)) + }, + Self::Enqueue(scene) => { + for track in 0..state.tracks.len() { + state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); + } + None + }, + _ => None +}); impl Arranger { pub fn scene_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut ArrangerScene> diff --git a/tek/src/arranger/arranger_track.rs b/tek/src/arranger/arranger_track.rs index a24f8c78..64a55f5c 100644 --- a/tek/src/arranger/arranger_track.rs +++ b/tek/src/arranger/arranger_track.rs @@ -1,4 +1,30 @@ use crate::*; +#[derive(Clone, Debug)] +pub enum ArrangerTrackCommand { + Add, + Delete(usize), + Stop(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), + SetColor(usize, ItemPalette), +} +command!(|self: ArrangerTrackCommand, state: Arranger|match self { + Self::Add => { + state.track_add(None, None)?; + None + }, + Self::SetColor(index, color) => { + let old = state.tracks[index].color; + state.tracks[index].color = color; + Some(Self::SetColor(index, old)) + }, + Self::Stop(track) => { + state.tracks[track].player.enqueue_next(None); + None + }, + _ => None +}); impl Arranger { pub fn track_next_name (&self) -> Arc { format!("Tr{:02}", self.tracks.len() + 1).into() diff --git a/tek/src/pool.rs b/tek/src/pool.rs index f8abaf5d..6f8685f4 100644 --- a/tek/src/pool.rs +++ b/tek/src/pool.rs @@ -1,4 +1,3 @@ -mod pool_tui; pub use self::pool_tui::*; mod clip_length; pub use self::clip_length::*; mod clip_rename; pub use self::clip_rename::*; @@ -19,6 +18,31 @@ pub struct PoolModel { scroll: usize, } +pub struct PoolView<'a>(pub bool, pub &'a PoolModel); +render!(TuiOut: (self: PoolView<'a>) => { + let Self(compact, model) = self; + let PoolModel { clips, mode, .. } = self.1; + let color = self.1.clip().read().unwrap().color; + let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); + let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); + Tui::bg(Color::Green, Fixed::y(clips.len() as u16, on_bg(border(Map::new(||model.clips().iter(), move|clip, i|{ + let item_height = 1; + let item_offset = i as u16 * item_height; + let selected = i == model.clip_index(); + let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); + let bg = if selected { color.light.rgb } else { color.base.rgb }; + let fg = color.lightest.rgb; + let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; + let length = if *compact { String::default() } else { format!("{length} ") }; + Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( + Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), + Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), + Fill::x(Align::w(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "โ–ถ"))))), + Fill::x(Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "โ—€"))))), + )))) + }))))) +}); + /// Modes for clip pool #[derive(Debug, Clone)] pub enum PoolMode { diff --git a/tek/src/pool/pool_tui.rs b/tek/src/pool/pool_tui.rs deleted file mode 100644 index bd7bbfc2..00000000 --- a/tek/src/pool/pool_tui.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::*; - -pub struct PoolView<'a>(pub bool, pub &'a PoolModel); -render!(TuiOut: (self: PoolView<'a>) => { - let Self(compact, model) = self; - let PoolModel { clips, mode, .. } = self.1; - let color = self.1.clip().read().unwrap().color; - let on_bg = |x|Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); - let border = |x|Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); - on_bg(border(Map::new(||model.clips().iter(), |clip, i|{ - let item_height = 1; - let item_offset = i as u16 * item_height; - let selected = i == model.clip_index(); - let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); - let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; - let length = if *compact { String::default() } else { format!("{length} ") }; - map_south(item_offset, item_height, Tui::bg(if selected { color.light.rgb } else { color.base.rgb }, lay!( - Fill::x(Align::w(Tui::fg(color.lightest.rgb, Tui::bold(selected, name)))), - Fill::x(Align::e(Tui::fg(color.lightest.rgb, Tui::bold(selected, length)))), - Fill::x(Align::w(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "โ–ถ"))))), - Fill::x(Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "โ—€"))))), - ))) - }))) -}); diff --git a/tui/src/tui_border.rs b/tui/src/tui_border.rs index 96e4ee3c..264e9ec3 100644 --- a/tui/src/tui_border.rs +++ b/tui/src/tui_border.rs @@ -33,6 +33,9 @@ pub trait BorderStyle: Send + Sync + Copy { fn enclose > (self, w: W) -> impl Content { lay!(Fill::xy(Border(self)), w) } + fn enclose2 > (self, w: W) -> impl Content { + Bsp::b(Margin::xy(1, 1, Fill::xy(Border(self))), w) + } fn enclose_bg > (self, w: W) -> impl Content { Tui::bg(self.style().unwrap().bg.unwrap_or(Color::Reset), lay!( Fill::xy(Border(self)), @@ -209,6 +212,24 @@ border! { const S0: &'static str = "โŽต"; fn style (&self) -> Option