diff --git a/midi/src/midi_in.rs b/midi/src/midi_in.rs index 3452367c..2a9efb65 100644 --- a/midi/src/midi_in.rs +++ b/midi/src/midi_in.rs @@ -9,7 +9,7 @@ pub trait HasMidiIns { } } -pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns { +pub trait MidiRecordApi: HasClock + HasPlayClip + HasMidiIns { fn notes_in (&self) -> &Arc>; fn recording (&self) -> bool; diff --git a/midi/src/midi_launch.rs b/midi/src/midi_launch.rs index 66a21596..6359ae5d 100644 --- a/midi/src/midi_launch.rs +++ b/midi/src/midi_launch.rs @@ -1,6 +1,6 @@ use crate::*; -pub trait HasPlayPhrase: HasClock { +pub trait HasPlayClip: HasClock { fn reset (&self) -> bool; fn reset_mut (&mut self) -> &mut bool; fn play_clip (&self) -> &Option<(Moment, Option>>)>; diff --git a/midi/src/midi_out.rs b/midi/src/midi_out.rs index 3b312de5..3ac74eb5 100644 --- a/midi/src/midi_out.rs +++ b/midi/src/midi_out.rs @@ -11,7 +11,7 @@ pub trait HasMidiOuts { fn midi_note (&mut self) -> &mut Vec; } -pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts { +pub trait MidiPlaybackApi: HasPlayClip + HasClock + HasMidiOuts { fn notes_out (&self) -> &Arc>; @@ -117,7 +117,7 @@ pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts { ) { // Source clip from which the MIDI events will be taken. let clip = clip.read().unwrap(); - // Phrase with zero length is not processed + // Clip with zero length is not processed if clip.length > 0 { // Current pulse index in source clip let pulse = pulse % clip.length; diff --git a/midi/src/midi_player.rs b/midi/src/midi_player.rs index 271fa4ae..9a526ace 100644 --- a/midi/src/midi_player.rs +++ b/midi/src/midi_player.rs @@ -186,7 +186,7 @@ impl MidiPlaybackApi for MidiPlayer { } } -impl HasPlayPhrase for MidiPlayer { +impl HasPlayClip for MidiPlayer { fn reset (&self) -> bool { self.reset } @@ -212,9 +212,9 @@ impl HasPlayPhrase for MidiPlayer { ///// Global timebase //pub clock: Arc, ///// Start time and clip being played - //pub play_clip: Option<(Moment, Option>>)>, + //pub play_clip: Option<(Moment, Option>>)>, ///// Start time and next clip - //pub next_clip: Option<(Moment, Option>>)>, + //pub next_clip: Option<(Moment, Option>>)>, ///// Play input through output. //pub monitoring: bool, ///// Write input to sequence. diff --git a/midi/src/midi_select.rs b/midi/src/midi_select.rs index 81ac72f4..20776579 100644 --- a/midi/src/midi_select.rs +++ b/midi/src/midi_select.rs @@ -13,7 +13,7 @@ render!(TuiOut: (self: ClipSelected) => impl ClipSelected { /// Shows currently playing clip with beats elapsed - pub fn play_clip (state: &T) -> Self { + pub fn play_clip (state: &T) -> Self { let (name, color) = if let Some((_, Some(clip))) = state.play_clip() { let MidiClip { ref name, color, .. } = *clip.read().unwrap(); (name.clone().into(), color) @@ -33,7 +33,7 @@ impl ClipSelected { } /// Shows next clip with beats remaining until switchover - pub fn next_clip (state: &T) -> Self { + pub fn next_clip (state: &T) -> Self { let mut time: Arc = String::from("--.-.--").into(); let mut name: Arc = String::from("").into(); let mut color = ItemPalette::from(TuiTheme::g(64)); diff --git a/tek/examples/midi_import.rs.fixme b/tek/examples/midi_import.rs.fixme index abaabfe3..c422c86c 100644 --- a/tek/examples/midi_import.rs.fixme +++ b/tek/examples/midi_import.rs.fixme @@ -1,18 +1,18 @@ use tek::*; -struct ExamplePhrases(Vec>>); +struct ExampleClips(Vec>>); -impl HasClips for ExamplePhrases { - fn phrases (&self) -> &Vec>> { +impl HasClips for ExampleClips { + fn phrases (&self) -> &Vec>> { &self.0 } - fn phrases_mut (&mut self) -> &mut Vec>> { + fn phrases_mut (&mut self) -> &mut Vec>> { &mut self.0 } } fn main () -> Usually<()> { - let mut phrases = ExamplePhrases(vec![]); + let mut phrases = ExampleClips(vec![]); MidiPoolCommand::Import(0, String::from("./example.mid")).execute(&mut phrases)?; Ok(()) } diff --git a/tek/src/arranger.rs b/tek/src/arranger.rs index 1ea57cd6..3c12a750 100644 --- a/tek/src/arranger.rs +++ b/tek/src/arranger.rs @@ -1,560 +1,6 @@ use crate::*; use ClockCommand::{Play, Pause}; use self::ArrangerCommand as Cmd; -/// Root view for standalone `tek_arranger` -pub struct Arranger { - 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: AtomicBool, - pub perf: PerfModel, - pub compact: bool, -} -render!(TuiOut: (self: Arranger) => self.size.of(EdnView::from_source(self, Self::EDN))); -impl EdnViewData for &Arranger { - fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { - use EdnItem::*; - let tracks_w = self.tracks_with_sizes().last().unwrap().3 as u16; - match item { - Nil => Box::new(()), - Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), - Sym(":editor") => (&self.editor).boxed(), - Sym(":pool") => self.pool().boxed(), - Sym(":status") => self.status().boxed(), - Sym(":toolbar") => self.toolbar().boxed(), - Sym(":tracks") => self.track_row(tracks_w).boxed(), - Sym(":scenes") => self.scene_row(tracks_w).boxed(), - Sym(":inputs") => self.input_row(tracks_w).boxed(), - Sym(":outputs") => self.output_row(tracks_w).boxed(), - _ => panic!("no content for {item:?}") - } - } -} -impl Arranger { - const EDN: &'static str = include_str!("arranger.edn"); - pub const LEFT_SEP: char = '▎'; - pub const TRACK_MIN_WIDTH: usize = 9; - - fn toolbar (&self) -> impl Content + use<'_> { - Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock)))) - } - fn pool (&self) -> impl Content + use<'_> { - Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact, &self.pool))) - } - fn status (&self) -> impl Content + use<'_> { - Bsp::e(self.editor.clip_status(), self.editor.edit_status()) - } - 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 - { - tracks_with_sizes(self.tracks.iter(), match self.selected { - ArrangerSelection::Track(t) if self.is_editing() => Some(t), - ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t), - _ => None - }, self.editor_w()) - } - - 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_header()), - Fill::x(Align::c(Fixed::xy(tracks_w, h, self.play_cells()))) - )) - } - fn play_header (&self) -> BoxThunk { - (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Playing")).boxed()).into() - } - fn play_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_header()), - Fill::x(Align::c(Fixed::xy(tracks_w, h, self.next_cells()))) - )) - } - fn next_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Next")).boxed()).into() - } - fn next_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|x;//Rugged(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_header()), - Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.track_cells())))) - )) - } - fn track_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (||Tui::bold(true, Bsp::s( - row!( - Tui::fg(TuiTheme::g(128), "add "), - Tui::fg(TuiTheme::orange(), "t"), - Tui::fg(TuiTheme::g(128), "rack"), - ), - row!( - Tui::fg(TuiTheme::orange(), "a"), - Tui::fg(TuiTheme::g(128), "dd scene"), - ), - ).boxed())).into() - } - fn track_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; - let active = self.selected.track() == Some(i); - let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) }; - let border = Style::default().fg(bfg).bg(bg); - Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16, - Outer(border).enclose(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 = 2 + self.midi_ins[0].connect.len() as u16; - Fixed::y(h, Bsp::e( - Fixed::xy(self.sidebar_w() as u16, h, self.input_header()), - Fill::x(Align::c(Fixed::xy(tracks_w, h, self.input_cells()))) - )) - } - fn input_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (||Bsp::s( - Tui::bold(true, row!( - Tui::fg(TuiTheme::g(128), "midi "), - Tui::fg(TuiTheme::orange(), "I"), - Tui::fg(TuiTheme::g(128), "ns"), - )), - 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_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(); - map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n( - Self::rec_mon(color.base.rgb, false, false), - phat_hi(color.base.rgb, color.dark.rgb) - )))) - })).boxed()).into() - } - fn rec_mon (bg: Color, rec: bool, mon: bool) -> impl Content { - row!( - Tui::fg_bg(if rec { Color::Red } else { bg }, bg, "▐"), - Tui::fg_bg(if rec { Color::White } else { Color::Rgb(0,0,0) }, bg, "REC"), - Tui::fg_bg(if rec { Color::White } else { bg }, bg, "▐"), - Tui::fg_bg(if mon { Color::White } else { Color::Rgb(0,0,0) }, bg, "MON"), - Tui::fg_bg(if mon { Color::White } else { bg }, bg, "▌"), - ) - } - - fn output_row (&self, tracks_w: u16) -> impl Content + '_ { - let h = 2 + self.midi_outs[0].connect.len() as u16; - Fixed::y(h, Bsp::e( - Fixed::xy(self.sidebar_w() as u16, h, self.output_header()), - Fill::x(Align::c(Fixed::xy(tracks_w, h, self.output_cells()))) - )) - } - fn output_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (||Bsp::s( - Tui::bold(true, row!( - Tui::fg(TuiTheme::g(128), "midi "), - Tui::fg(TuiTheme::orange(), "O"), - Tui::fg(TuiTheme::g(128), "uts"), - )), - 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_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(); - map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n( - Self::mute_solo(color.base.rgb, false, false), - phat_hi(color.dark.rgb, color.darker.rgb) - )))) - })).boxed()).into() - } - fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content { - row!( - Tui::fg_bg(if mute { Color::White } else { Color::Rgb(0,0,0) }, bg, "MUTE"), - Tui::fg_bg(if mute { Color::White } else { bg }, bg, "▐"), - Tui::fg_bg(if solo { Color::White } else { Color::Rgb(0,0,0) }, bg, "SOLO"), - ) - } - - fn scene_row (&self, tracks_w: u16) -> impl Content + '_ { - let h = (self.size.h() as u16).saturating_sub(8).max(8); - let border = |x|x;//Skinny(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x); - Bsp::e( - Tui::bg(Color::Reset, Fixed::xy(self.sidebar_w() as u16, h, self.scene_headers())), - Tui::bg(Color::Reset, Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.scene_cells()))))) - ) - } - fn scene_headers <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - (||{ - let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); - let selected = self.selected.scene(); - 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 active = selected == Some(i); - let mid = if active { color.light } else { color.base }; - let top = Some(last_color.read().unwrap().base.rgb); - 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_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { - let editing = self.is_editing(); - let tracks = move||self.tracks_with_sizes(); - let scenes = ||self.scenes_with_sizes(2); - let selected_track = self.selected.track(); - let selected_scene = self.selected.scene(); - (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, fg, bg) = if let Some(c) = &scene.clips[t] { - let c = c.read().unwrap(); - (c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb) - } else { - ("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32)) - }; - let last = last_color.read().unwrap().clone(); - 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(fg, Push::x(1, Tui::bold(true, name.to_string()))), - Tui::fg(fg, 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(bg.into()) - }, - bg.into(), - bg.into(), - )); - let cell = Either(active, editor, cell); - *last_color.write().unwrap() = bg.into(); - map_south( - y1 as u16, - h + 1, - Fill::x(Fixed::y(h + 1, 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 -pub fn phat_lo (fg: Color, bg: Color) -> impl Content { - Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"▄"))) -} -/// A phat line -pub 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. -pub 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))), - ) - ) -} -pub 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))), - ) - ) -} -pub 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)), - ) - ) - ) -} has_clock!(|self: Arranger|&self.clock); has_clips!(|self: Arranger|self.pool.clips); has_editor!(|self: Arranger|self.editor); diff --git a/tek/src/control.rs b/tek/src/control.rs index 0ec430da..86bbb1b2 100644 --- a/tek/src/control.rs +++ b/tek/src/control.rs @@ -4,6 +4,7 @@ use KeyCode::{Tab, Char}; use SequencerCommand as SeqCmd; use GrooveboxCommand as GrvCmd; use ArrangerCommand as ArrCmd; +use SamplerCommand as SmplCmd; use MidiEditCommand as EditCmd; use MidiPoolCommand as PoolCmd; @@ -88,10 +89,11 @@ handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, } command!(|self: SequencerCommand, state: Sequencer|match self { - Self::Enqueue(clip) => { - state.player.enqueue_next(clip.as_ref()); - None - }, + Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, + Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, + Self::Enqueue(clip) => { state.player.enqueue_next(clip.as_ref()); None }, + Self::History(delta) => { todo!("undo/redo") }, + Self::Pool(cmd) => match cmd { // autoselect: automatically load selected clip in editor PoolCommand::Select(_) => { @@ -100,18 +102,13 @@ command!(|self: SequencerCommand, state: Sequencer|match self { undo }, // update color in all places simultaneously - PoolCommand::Phrase(PoolCmd::SetColor(index, _)) => { + PoolCommand::Clip(PoolCmd::SetColor(index, _)) => { let undo = cmd.delegate(&mut state.pool, Self::Pool)?; state.editor.set_clip(state.pool.clip().as_ref()); undo }, _ => cmd.delegate(&mut state.pool, Self::Pool)? }, - Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, - Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, - Self::History(delta) => { - todo!("undo/redo") - }, Self::Compact(compact) => if state.compact != compact { state.compact = compact; Some(Self::Compact(!compact)) @@ -120,10 +117,12 @@ command!(|self: SequencerCommand, state: Sequencer|match self { }, }); command!(|self: GrooveboxCommand, state: Groovebox|match self { - Self::Enqueue(clip) => { - state.player.enqueue_next(clip.as_ref()); - None - }, + Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, + Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, + Self::Enqueue(clip) => { state.player.enqueue_next(clip.as_ref()); None }, + Self::History(delta) => { todo!("undo/redo") }, + Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?, + Self::Pool(cmd) => match cmd { // autoselect: automatically load selected clip in editor PoolCommand::Select(_) => { @@ -132,17 +131,13 @@ command!(|self: GrooveboxCommand, state: Groovebox|match self { undo }, // update color in all places simultaneously - PoolCommand::Phrase(PoolCmd::SetColor(index, _)) => { + PoolCommand::Clip(PoolCmd::SetColor(index, _)) => { let undo = cmd.delegate(&mut state.pool, Self::Pool)?; state.editor.set_clip(state.pool.clip().as_ref()); undo }, _ => cmd.delegate(&mut state.pool, Self::Pool)? }, - Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?, - Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, - Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, - Self::History(delta) => { todo!("undo/redo") }, Self::Compact(compact) => if state.compact != compact { state.compact = compact; Some(Self::Compact(!compact)) @@ -151,25 +146,26 @@ command!(|self: GrooveboxCommand, state: Groovebox|match self { }, }); command!(|self: ArrangerCommand, state: Arranger|match self { - Self::Clear => { todo!() }, - Self::History(_) => { todo!() }, - Self::Zoom(_) => { todo!(); }, - 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::Select(selected) => { state.selected = selected; None }, - Self::StopAll => { + Self::Clear => { todo!() }, + Self::Clip(cmd) => cmd.delegate(state, Self::Clip)?, + Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, + Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, + Self::History(_) => { todo!() }, + Self::Scene(cmd) => cmd.delegate(state, Self::Scene)?, + Self::Select(s) => { state.selected = s; None }, + Self::Track(cmd) => cmd.delegate(state, Self::Track)?, + Self::Zoom(_) => { todo!(); }, + + Self::StopAll => { for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); } None }, - Self::Color(palette) => { + Self::Color(palette) => { let old = state.color; state.color = palette; Some(Self::Color(old)) }, - Self::Pool(cmd) => { + Self::Pool(cmd) => { match cmd { // autoselect: automatically load selected clip in editor PoolCommand::Select(_) => { @@ -178,7 +174,7 @@ command!(|self: ArrangerCommand, state: Arranger|match self { undo }, // reload clip in editor to update color - PoolCommand::Phrase(MidiPoolCommand::SetColor(index, _)) => { + PoolCommand::Clip(MidiPoolCommand::SetColor(index, _)) => { let undo = cmd.delegate(&mut state.pool, Self::Pool)?; state.editor.set_clip(state.pool.clip().as_ref()); undo @@ -188,14 +184,8 @@ command!(|self: ArrangerCommand, state: Arranger|match self { }, }); command!(|self: ArrangerSceneCommand, state: Arranger|match self { - Self::Add => { - state.scene_add(None, None)?; - None - } - Self::Delete(index) => { - state.scene_del(index); - None - }, + 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; @@ -210,19 +200,14 @@ command!(|self: ArrangerSceneCommand, state: Arranger|match self { _ => None }); command!(|self: ArrangerTrackCommand, state: Arranger|match self { - Self::Add => { - state.track_add(None, None)?; - None - }, + Self::Add => { state.track_add(None, None)?; None }, + Self::Delete(index) => { state.track_del(index); None }, + Self::Stop(track) => { state.tracks[track].player.enqueue_next(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 }); command!(|self: ArrangerClipCommand, state: Arranger|match self { @@ -289,13 +274,13 @@ keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand ), // Shift-R: toggle recording shift(key(Char('R'))) => GrvCmd::Sampler(if state.sampler.recording.is_some() { - SamplerCommand::RecordFinish + SmplCmd::RecordFinish } else { - SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8)) + SmplCmd::RecordBegin(u7::from(state.editor.note_point() as u8)) }), // Shift-Del: delete sample shift(key(Delete)) => GrvCmd::Sampler( - SamplerCommand::SetSample(u7::from(state.editor.note_point() as u8), None) + SmplCmd::SetSample(u7::from(state.editor.note_point() as u8), None) ), // e: Toggle between editing currently playing or other clip //shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_clip() { @@ -349,9 +334,9 @@ keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand { kpat!(Char('c')) => Some(ArrCmd::Color(ItemPalette::random())), kpat!(Up) => return None, - kpat!(Down) => Some( ArrCmd::Select(Selected::Scene(0))), + kpat!(Down) => Some(ArrCmd::Select(Selected::Scene(0))), kpat!(Left) => return None, - kpat!(Right) => Some( ArrCmd::Select(Selected::Track(0))), + kpat!(Right) => Some(ArrCmd::Select(Selected::Track(0))), _ => None }, diff --git a/tek/src/groovebox.rs b/tek/src/groovebox.rs index e6efe7d8..8d9650d2 100644 --- a/tek/src/groovebox.rs +++ b/tek/src/groovebox.rs @@ -2,202 +2,4 @@ use crate::*; use super::*; use self::GrooveboxCommand as Cmd; use EdnItem::*; -use ClockCommand::{Play, Pause}; -use MidiEditCommand::*; -use MidiPoolCommand::*; -use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right}; use std::marker::ConstParamTy; -pub struct Groovebox { - pub _jack: Arc>, - pub player: MidiPlayer, - pub pool: PoolModel, - pub editor: MidiEditor, - pub sampler: Sampler, - - pub compact: bool, - pub size: Measure, - pub status: bool, - pub note_buf: Vec, - pub midi_buf: Vec>>, - pub perf: PerfModel, -} -render!(TuiOut: (self: Groovebox) => self.size.of(EdnView::from_source(self, Self::EDN))); -impl EdnViewData for &Groovebox { - fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { - use EdnItem::*; - match item { - Nil => Box::new(()), - Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), - Sym(":editor") => (&self.editor).boxed(), - Sym(":pool") => self.pool().boxed(), - Sym(":status") => self.status().boxed(), - Sym(":toolbar") => self.toolbar().boxed(), - Sym(":sampler") => self.sampler().boxed(), - Sym(":sample") => self.sample().boxed(), - _ => panic!("no content for {item:?}") - } - } - fn get_unit (&self, item: EdnItem<&str>) -> u16 { - use EdnItem::*; - match item.to_str() { - ":sample-h" => if self.compact { 0 } else { 5 }, - ":samples-w" => if self.compact { 4 } else { 11 }, - ":samples-y" => if self.compact { 1 } else { 0 }, - ":pool-w" => if self.compact { 5 } else { - let w = self.size.w(); - if w > 60 { 20 } else if w > 40 { 15 } else { 10 } - }, - _ => 0 - } - } -} -impl Groovebox { - const EDN: &'static str = include_str!("groovebox.edn"); - fn toolbar (&self) -> impl Content + use<'_> { - Fill::x(Fixed::y(2, lay!( - Fill::x(Align::w(Meter("L/", self.sampler.input_meter[0]))), - Fill::x(Align::e(Meter("R/", self.sampler.input_meter[1]))), - Align::x(TransportView::new(true, &self.player.clock)), - ))) - } - fn status (&self) -> impl Content + use<'_> { - row!( - self.player.play_status(), - self.player.next_status(), - self.editor.clip_status(), - self.editor.edit_status(), - ) - } - fn sample (&self) -> impl Content + use<'_> { - let note_pt = self.editor.note_point(); - let sample_h = if self.compact { 0 } else { 5 }; - Max::y(sample_h, Fill::xy( - Bsp::a( - Fill::x(Align::w(Fixed::y(1, self.sampler.status(note_pt)))), - self.sampler.viewer(note_pt)))) - } - fn pool (&self) -> impl Content + use<'_> { - let w = self.size.w(); - let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - Fixed::x(if self.compact { 5 } else { pool_w }, - PoolView(self.compact, &self.pool)) - } - fn sampler (&self) -> impl Content + use<'_> { - let note_pt = self.editor.note_point(); - let sampler_w = if self.compact { 4 } else { 40 }; - let sampler_y = if self.compact { 1 } else { 0 }; - Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(self.sampler.list(self.compact, &self.editor)))) - } -} -has_clock!(|self: Groovebox|self.player.clock()); - -///// Status bar for sequencer app -//#[derive(Clone)] -//pub struct GrooveboxStatus { - //pub(crate) width: usize, - //pub(crate) cpu: Option, - //pub(crate) size: String, - //pub(crate) playing: bool, -//} -//from!(|state: &Groovebox|GrooveboxStatus = { - //let samples = state.clock().chunk.load(Relaxed); - //let rate = state.clock().timebase.sr.get(); - //let buffer = samples as f64 / rate; - //let width = state.size.w(); - //Self { - //width, - //playing: state.clock().is_rolling(), - //cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), - //size: format!("{}x{}│", width, state.size.h()), - //} -//}); -//render!(TuiOut: (self: GrooveboxStatus) => Fixed::y(2, lay!( - //Self::help(), - //Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))), -//))); -//impl GrooveboxStatus { - //fn help () -> impl Content { - //let single = |binding, command|row!(" ", col!( - //Tui::fg(TuiTheme::yellow(), binding), - //command - //)); - //let double = |(b1, c1), (b2, c2)|col!( - //row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,), - //row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,), - //); - //Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!( - //single("SPACE", "play/pause"), - //double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ), - //double(("a", "append"), ("s", "set note"),), - //double((",.", "length"), ("<>", "triplet"), ), - //double(("[]", "phrase"), ("{}", "order"), ), - //double(("q", "enqueue"), ("e", "edit"), ), - //double(("c", "color"), ("", ""),), - //)) - //} - //fn stats (&self) -> impl Content + use<'_> { - //row!(&self.cpu, &self.size) - //} -//} -//macro_rules! edn_context { - //($Struct:ident |$l:lifetime, $state:ident| { - //$($key:literal = $field:ident: $Type:ty => $expr:expr,)* - //}) => { - - //#[derive(Default)] - //pub struct EdnView<$l> { $($field: Option<$Type>),* } - - //impl<$l> EdnView<$l> { - //pub fn parse <'e> (edn: &[Edn<'e>]) -> impl Fn(&$Struct) + use<'e> { - //let imports = Self::imports_all(edn); - //move |state| { - //let mut context = EdnView::default(); - //for import in imports.iter() { - //context.import(state, import) - //} - //} - //} - //fn imports_all <'e> (edn: &[Edn<'e>]) -> Vec<&'e str> { - //let mut imports = vec![]; - //for edn in edn.iter() { - //for import in Self::imports_one(edn) { - //imports.push(import); - //} - //} - //imports - //} - //fn imports_one <'e> (edn: &Edn<'e>) -> Vec<&'e str> { - //match edn { - //Edn::Symbol(import) => vec![import], - //Edn::List(edn) => Self::imports_all(edn.as_slice()), - //_ => vec![], - //} - //} - //pub fn import (&mut self, $state: &$l$Struct, key: &str) { - //match key { - //$($key => self.$field = Some($expr),)* - //_ => {} - //} - //} - //} - //} -//} - -////impl Groovebox { - ////fn status (&self) -> impl Content + use<'_> { - ////let note_pt = self.editor.note_point(); - ////Align::w(Fixed::y(1, )) - ////} - ////fn pool (&self) -> impl Content + use<'_> { - ////let w = self.size.w(); - ////let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - ////Fixed::x(if self.compact { 5 } else { pool_w }, - ////) - ////} - ////fn sampler (&self) -> impl Content + use<'_> { - ////let sampler_w = if self.compact { 4 } else { 11 }; - ////let sampler_y = if self.compact { 1 } else { 0 }; - ////Fixed::x(sampler_w, Push::y(sampler_y, Fill::y( - ////SampleList::new(self.compact, &self.sampler, &self.editor)))) - ////} -////} diff --git a/tek/src/lib.rs b/tek/src/lib.rs index 1734cf94..b1ffe52f 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -10,7 +10,7 @@ pub type Usually = std::result::Result>; /// Standard optional result type. pub type Perhaps = std::result::Result, Box>; -pub mod app; pub use self::app::*; +pub mod model; pub use self::model::*; pub mod view; pub use self::view::*; pub mod control; pub use self::control::*; pub mod audio; pub use self::audio::*; diff --git a/tek/src/app.rs b/tek/src/model.rs similarity index 67% rename from tek/src/app.rs rename to tek/src/model.rs index fb995297..260a8601 100644 --- a/tek/src/app.rs +++ b/tek/src/model.rs @@ -87,3 +87,49 @@ impl App { } } } +pub struct Sequencer { + pub jack: Arc>, + pub compact: bool, + pub editor: MidiEditor, + pub midi_buf: Vec>>, + pub note_buf: Vec, + pub perf: PerfModel, + pub player: MidiPlayer, + pub pool: PoolModel, + pub selectors: bool, + pub size: Measure, + pub status: bool, + pub transport: bool, +} +pub struct Groovebox { + pub jack: Arc>, + pub compact: bool, + pub editor: MidiEditor, + pub midi_buf: Vec>>, + pub note_buf: Vec, + pub perf: PerfModel, + pub player: MidiPlayer, + pub pool: PoolModel, + pub sampler: Sampler, + pub size: Measure, + pub status: bool, +} +pub struct Arranger { + pub clock: Clock, + pub color: ItemPalette, + pub compact: bool, + pub editing: AtomicBool, + pub editor: MidiEditor, + pub jack: Arc>, + pub midi_buf: Vec>>, + pub midi_ins: Vec>, + pub midi_outs: Vec>, + pub note_buf: Vec, + pub perf: PerfModel, + pub pool: PoolModel, + pub scenes: Vec, + pub selected: ArrangerSelection, + pub size: Measure, + pub splits: [u16;2], + pub tracks: Vec, +} diff --git a/tek/src/pool.rs b/tek/src/pool.rs index 98237709..926a8122 100644 --- a/tek/src/pool.rs +++ b/tek/src/pool.rs @@ -1,7 +1,7 @@ use crate::*; use super::*; -use PhraseLengthFocus::*; -use PhraseLengthCommand::*; +use ClipLengthFocus::*; +use ClipLengthCommand::*; use KeyCode::{Up, Down, Left, Right, Enter, Esc}; use super::*; @@ -71,7 +71,7 @@ pub enum PoolMode { /// Renaming a pattern Rename(usize, Arc), /// Editing the length of a pattern - Length(usize, usize, PhraseLengthFocus), + Length(usize, usize, ClipLengthFocus), /// Load clip from disk Import(usize, FileBrowser), /// Save clip to disk @@ -82,13 +82,13 @@ pub enum PoolMode { pub enum PoolCommand { Show(bool), /// Update the contents of the clip pool - Phrase(MidiPoolCommand), + Clip(MidiPoolCommand), /// Select a clip from the clip pool Select(usize), /// Rename a clip - Rename(PhraseRenameCommand), + Rename(ClipRenameCommand), /// Change the length of a clip - Length(PhraseLengthCommand), + Length(ClipLengthCommand), /// Import from file Import(FileBrowserCommand), /// Export to file @@ -103,17 +103,17 @@ command!(|self:PoolCommand, state: PoolModel|{ Some(Self::Show(!visible)) } Rename(command) => match command { - PhraseRenameCommand::Begin => { + ClipRenameCommand::Begin => { let length = state.clips()[state.clip_index()].read().unwrap().length; *state.clips_mode_mut() = Some( - PoolMode::Length(state.clip_index(), length, PhraseLengthFocus::Bar) + PoolMode::Length(state.clip_index(), length, ClipLengthFocus::Bar) ); None }, _ => command.execute(state)?.map(Rename) }, Length(command) => match command { - PhraseLengthCommand::Begin => { + ClipLengthCommand::Begin => { let name = state.clips()[state.clip_index()].read().unwrap().name.clone(); *state.clips_mode_mut() = Some( PoolMode::Rename(state.clip_index(), name) @@ -144,13 +144,13 @@ command!(|self:PoolCommand, state: PoolModel|{ state.set_clip_index(clip); None }, - Phrase(command) => command.execute(state)?.map(Phrase), + Clip(command) => command.execute(state)?.map(Clip), } }); input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.clips_mode() { - Some(PoolMode::Rename(..)) => Self::Rename(PhraseRenameCommand::input_to_command(state, input)?), - Some(PoolMode::Length(..)) => Self::Length(PhraseLengthCommand::input_to_command(state, input)?), + Some(PoolMode::Rename(..)) => Self::Rename(ClipRenameCommand::input_to_command(state, input)?), + Some(PoolMode::Length(..)) => Self::Length(ClipLengthCommand::input_to_command(state, input)?), Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?), _ => to_clips_command(state, input)? @@ -162,11 +162,11 @@ fn to_clips_command (state: &PoolModel, input: &Event) -> Option { let index = state.clip_index(); let count = state.clips().len(); Some(match input { - kpat!(Char('n')) => Cmd::Rename(PhraseRenameCommand::Begin), - kpat!(Char('t')) => Cmd::Length(PhraseLengthCommand::Begin), + kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin), + kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin), kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin), kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin), - kpat!(Char('c')) => Cmd::Phrase(MidiPoolCommand::SetColor(index, ItemColor::random())), + kpat!(Char('c')) => Cmd::Clip(MidiPoolCommand::SetColor(index, ItemColor::random())), kpat!(Char('[')) | kpat!(Up) => Cmd::Select( index.overflowing_sub(1).0.min(state.clips().len() - 1) ), @@ -175,32 +175,32 @@ fn to_clips_command (state: &PoolModel, input: &Event) -> Option { ), kpat!(Char('<')) => if index > 1 { state.set_clip_index(state.clip_index().saturating_sub(1)); - Cmd::Phrase(MidiPoolCommand::Swap(index - 1, index)) + Cmd::Clip(MidiPoolCommand::Swap(index - 1, index)) } else { return None }, kpat!(Char('>')) => if index < count.saturating_sub(1) { state.set_clip_index(state.clip_index() + 1); - Cmd::Phrase(MidiPoolCommand::Swap(index + 1, index)) + Cmd::Clip(MidiPoolCommand::Swap(index + 1, index)) } else { return None }, kpat!(Delete) => if index > 0 { state.set_clip_index(index.min(count.saturating_sub(1))); - Cmd::Phrase(MidiPoolCommand::Delete(index)) + Cmd::Clip(MidiPoolCommand::Delete(index)) } else { return None }, - kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Phrase(MidiPoolCommand::Add(count, MidiClip::new( + kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(MidiPoolCommand::Add(count, MidiClip::new( "Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) ))), - kpat!(Char('i')) => Cmd::Phrase(MidiPoolCommand::Add(index + 1, MidiClip::new( + kpat!(Char('i')) => Cmd::Clip(MidiPoolCommand::Add(index + 1, MidiClip::new( "Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) ))), kpat!(Char('d')) | kpat!(Shift-Char('D')) => { let mut clip = state.clips()[index].read().unwrap().duplicate(); clip.color = ItemPalette::random_near(clip.color, 0.25); - Cmd::Phrase(MidiPoolCommand::Add(index + 1, clip)) + Cmd::Clip(MidiPoolCommand::Add(index + 1, clip)) }, _ => return None }) @@ -295,7 +295,7 @@ input_to_command!(FileBrowserCommand: |state: PoolModel, input: Event|{ /// Displays and edits clip length. #[derive(Clone)] -pub struct PhraseLength { +pub struct ClipLength { /// Pulses per beat (quaver) pub ppq: usize, /// Beats per bar @@ -303,11 +303,11 @@ pub struct PhraseLength { /// Length of clip in pulses pub pulses: usize, /// Selected subdivision - pub focus: Option, + pub focus: Option, } -impl PhraseLength { - pub fn new (pulses: usize, focus: Option) -> Self { +impl ClipLength { + pub fn new (pulses: usize, focus: Option) -> Self { Self { ppq: PPQ, bpb: 4, pulses, focus } } pub fn bars (&self) -> usize { @@ -330,9 +330,9 @@ impl PhraseLength { } } -/// Focused field of `PhraseLength` +/// Focused field of `ClipLength` #[derive(Copy, Clone, Debug)] -pub enum PhraseLengthFocus { +pub enum ClipLengthFocus { /// Editing the number of bars Bar, /// Editing the number of beats @@ -341,7 +341,7 @@ pub enum PhraseLengthFocus { Tick, } -impl PhraseLengthFocus { +impl ClipLengthFocus { pub fn next (&mut self) { *self = match self { Self::Bar => Self::Beat, @@ -358,24 +358,24 @@ impl PhraseLengthFocus { } } -render!(TuiOut: (self: PhraseLength) => { +render!(TuiOut: (self: ClipLength) => { let bars = ||self.bars_string(); let beats = ||self.beats_string(); let ticks = ||self.ticks_string(); match self.focus { None => row!(" ", bars(), ".", beats(), ".", ticks()), - Some(PhraseLengthFocus::Bar) => + Some(ClipLengthFocus::Bar) => row!("[", bars(), "]", beats(), ".", ticks()), - Some(PhraseLengthFocus::Beat) => + Some(ClipLengthFocus::Beat) => row!(" ", bars(), "[", beats(), "]", ticks()), - Some(PhraseLengthFocus::Tick) => + Some(ClipLengthFocus::Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), } }); #[derive(Copy, Clone, Debug, PartialEq)] -pub enum PhraseLengthCommand { +pub enum ClipLengthCommand { Begin, Cancel, Set(usize), @@ -385,7 +385,7 @@ pub enum PhraseLengthCommand { Dec, } -command!(|self:PhraseLengthCommand,state:PoolModel|{ +command!(|self:ClipLengthCommand,state:PoolModel|{ match state.clips_mode_mut().clone() { Some(PoolMode::Length(clip, ref mut length, ref mut focus)) => match self { Cancel => { *state.clips_mode_mut() = None; }, @@ -418,7 +418,7 @@ command!(|self:PhraseLengthCommand,state:PoolModel|{ None }); -input_to_command!(PhraseLengthCommand: |state: PoolModel, input: Event|{ +input_to_command!(ClipLengthCommand: |state: PoolModel, input: Event|{ if let Some(PoolMode::Length(_, length, _)) = state.clips_mode() { match input { kpat!(Up) => Self::Inc, @@ -437,16 +437,16 @@ use crate::*; use super::*; #[derive(Clone, Debug, PartialEq)] -pub enum PhraseRenameCommand { +pub enum ClipRenameCommand { Begin, Cancel, Confirm, Set(Arc), } -impl Command for PhraseRenameCommand { +impl Command for ClipRenameCommand { fn execute (self, state: &mut PoolModel) -> Perhaps { - use PhraseRenameCommand::*; + use ClipRenameCommand::*; match state.clips_mode_mut().clone() { Some(PoolMode::Rename(clip, ref mut old_name)) => match self { Set(s) => { @@ -469,7 +469,7 @@ impl Command for PhraseRenameCommand { } } -impl InputToCommand for PhraseRenameCommand { +impl InputToCommand for ClipRenameCommand { fn input_to_command (state: &PoolModel, input: &Event) -> Option { use KeyCode::{Char, Backspace, Enter, Esc}; if let Some(PoolMode::Rename(_, ref old_name)) = state.clips_mode() { diff --git a/tek/src/sequencer.rs b/tek/src/sequencer.rs index 5478cf44..825de2d8 100644 --- a/tek/src/sequencer.rs +++ b/tek/src/sequencer.rs @@ -4,83 +4,6 @@ use KeyCode::{Tab, Char}; use SequencerCommand as Cmd; use MidiEditCommand::*; use MidiPoolCommand::*; -render!(TuiOut: (self: Sequencer) => self.size.of(EdnView::from_source(self, Self::EDN))); -/// Root view for standalone `tek_sequencer`. -pub struct Sequencer { - pub _jack: Arc>, - - pub pool: PoolModel, - pub editor: MidiEditor, - pub player: MidiPlayer, - - pub transport: bool, - pub selectors: bool, - pub compact: bool, - - pub size: Measure, - pub status: bool, - pub note_buf: Vec, - pub midi_buf: Vec>>, - pub perf: PerfModel, -} -impl EdnViewData for &Sequencer { - fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { - use EdnItem::*; - match item { - Nil => Box::new(()), - Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), - Sym(":editor") => (&self.editor).boxed(), - Sym(":pool") => self.pool_view().boxed(), - Sym(":status") => self.status_view().boxed(), - Sym(":toolbar") => self.toolbar_view().boxed(), - _ => panic!("no content for {item:?}") - } - } -} -impl Sequencer { - const EDN: &'static str = include_str!("sequencer.edn"); - fn toolbar_view (&self) -> impl Content + use<'_> { - Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.player.clock)))) - } - fn status_view (&self) -> impl Content + use<'_> { - Bsp::e( - When(self.selectors, Bsp::e( - self.player.play_status(), - self.player.next_status(), - )), - Bsp::e( - self.editor.clip_status(), - self.editor.edit_status(), - ) - ) - } - fn pool_view (&self) -> impl Content + use<'_> { - let w = self.size.w(); - let clip_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let pool_w = if self.pool.visible { clip_w } else { 0 }; - let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool)))); - Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool)))) - } - fn help () -> impl Content { - let single = |binding, command|row!(" ", col!( - Tui::fg(TuiTheme::yellow(), binding), - command - )); - let double = |(b1, c1), (b2, c2)|col!( - row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,), - row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,), - ); - Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!( - single("SPACE", "play/pause"), - double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ), - double(("a", "append"), ("s", "set note"),), - double((",.", "length"), ("<>", "triplet"), ), - double(("[]", "clip"), ("{}", "order"), ), - double(("q", "enqueue"), ("e", "edit"), ), - double(("c", "color"), ("", ""),), - )) - } -} has_size!(|self:Sequencer|&self.size); has_clock!(|self:Sequencer|&self.player.clock); has_clips!(|self:Sequencer|self.pool.clips); diff --git a/tek/src/view.rs b/tek/src/view.rs index a8c1135d..f8dba3e6 100644 --- a/tek/src/view.rs +++ b/tek/src/view.rs @@ -336,3 +336,740 @@ fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content { fn cell > (color: ItemPalette, field: T) -> impl Content { Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field)) } + +/////////////////////////////////////////////////////////////////////////////////////////////////// +render!(TuiOut: (self: Sequencer) => self.size.of(EdnView::from_source(self, Self::EDN))); +impl EdnViewData for &Sequencer { + fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { + use EdnItem::*; + match item { + Nil => Box::new(()), + Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), + Sym(":editor") => (&self.editor).boxed(), + Sym(":pool") => self.pool_view().boxed(), + Sym(":status") => self.status_view().boxed(), + Sym(":toolbar") => self.toolbar_view().boxed(), + _ => panic!("no content for {item:?}") + } + } +} +impl Sequencer { + const EDN: &'static str = include_str!("sequencer.edn"); + fn toolbar_view (&self) -> impl Content + use<'_> { + Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.player.clock)))) + } + fn status_view (&self) -> impl Content + use<'_> { + Bsp::e( + When(self.selectors, Bsp::e( + self.player.play_status(), + self.player.next_status(), + )), + Bsp::e( + self.editor.clip_status(), + self.editor.edit_status(), + ) + ) + } + fn pool_view (&self) -> impl Content + use<'_> { + let w = self.size.w(); + let clip_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + let pool_w = if self.pool.visible { clip_w } else { 0 }; + let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool)))); + Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool)))) + } + fn help () -> impl Content { + let single = |binding, command|row!(" ", col!( + Tui::fg(TuiTheme::yellow(), binding), + command + )); + let double = |(b1, c1), (b2, c2)|col!( + row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,), + row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,), + ); + Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!( + single("SPACE", "play/pause"), + double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ), + double(("a", "append"), ("s", "set note"),), + double((",.", "length"), ("<>", "triplet"), ), + double(("[]", "clip"), ("{}", "order"), ), + double(("q", "enqueue"), ("e", "edit"), ), + double(("c", "color"), ("", ""),), + )) + } +} +/////////////////////////////////////////////////////////////////////////////////////////////////// +render!(TuiOut: (self: Groovebox) => self.size.of(EdnView::from_source(self, Self::EDN))); +impl EdnViewData for &Groovebox { + fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { + use EdnItem::*; + match item { + Nil => Box::new(()), + Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), + Sym(":editor") => (&self.editor).boxed(), + Sym(":pool") => self.pool().boxed(), + Sym(":status") => self.status().boxed(), + Sym(":toolbar") => self.toolbar().boxed(), + Sym(":sampler") => self.sampler().boxed(), + Sym(":sample") => self.sample().boxed(), + _ => panic!("no content for {item:?}") + } + } + fn get_unit (&self, item: EdnItem<&str>) -> u16 { + use EdnItem::*; + match item.to_str() { + ":sample-h" => if self.compact { 0 } else { 5 }, + ":samples-w" => if self.compact { 4 } else { 11 }, + ":samples-y" => if self.compact { 1 } else { 0 }, + ":pool-w" => if self.compact { 5 } else { + let w = self.size.w(); + if w > 60 { 20 } else if w > 40 { 15 } else { 10 } + }, + _ => 0 + } + } +} +impl Groovebox { + const EDN: &'static str = include_str!("groovebox.edn"); + fn toolbar (&self) -> impl Content + use<'_> { + Fill::x(Fixed::y(2, lay!( + Fill::x(Align::w(Meter("L/", self.sampler.input_meter[0]))), + Fill::x(Align::e(Meter("R/", self.sampler.input_meter[1]))), + Align::x(TransportView::new(true, &self.player.clock)), + ))) + } + fn status (&self) -> impl Content + use<'_> { + row!( + self.player.play_status(), + self.player.next_status(), + self.editor.clip_status(), + self.editor.edit_status(), + ) + } + fn sample (&self) -> impl Content + use<'_> { + let note_pt = self.editor.note_point(); + let sample_h = if self.compact { 0 } else { 5 }; + Max::y(sample_h, Fill::xy( + Bsp::a( + Fill::x(Align::w(Fixed::y(1, self.sampler.status(note_pt)))), + self.sampler.viewer(note_pt)))) + } + fn pool (&self) -> impl Content + use<'_> { + let w = self.size.w(); + let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + Fixed::x(if self.compact { 5 } else { pool_w }, + PoolView(self.compact, &self.pool)) + } + fn sampler (&self) -> impl Content + use<'_> { + let note_pt = self.editor.note_point(); + let sampler_w = if self.compact { 4 } else { 40 }; + let sampler_y = if self.compact { 1 } else { 0 }; + Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(self.sampler.list(self.compact, &self.editor)))) + } +} +has_clock!(|self: Groovebox|self.player.clock()); + +///// Status bar for sequencer app +//#[derive(Clone)] +//pub struct GrooveboxStatus { + //pub(crate) width: usize, + //pub(crate) cpu: Option, + //pub(crate) size: String, + //pub(crate) playing: bool, +//} +//from!(|state: &Groovebox|GrooveboxStatus = { + //let samples = state.clock().chunk.load(Relaxed); + //let rate = state.clock().timebase.sr.get(); + //let buffer = samples as f64 / rate; + //let width = state.size.w(); + //Self { + //width, + //playing: state.clock().is_rolling(), + //cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), + //size: format!("{}x{}│", width, state.size.h()), + //} +//}); +//render!(TuiOut: (self: GrooveboxStatus) => Fixed::y(2, lay!( + //Self::help(), + //Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))), +//))); +//impl GrooveboxStatus { + //fn help () -> impl Content { + //let single = |binding, command|row!(" ", col!( + //Tui::fg(TuiTheme::yellow(), binding), + //command + //)); + //let double = |(b1, c1), (b2, c2)|col!( + //row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,), + //row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,), + //); + //Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!( + //single("SPACE", "play/pause"), + //double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ), + //double(("a", "append"), ("s", "set note"),), + //double((",.", "length"), ("<>", "triplet"), ), + //double(("[]", "phrase"), ("{}", "order"), ), + //double(("q", "enqueue"), ("e", "edit"), ), + //double(("c", "color"), ("", ""),), + //)) + //} + //fn stats (&self) -> impl Content + use<'_> { + //row!(&self.cpu, &self.size) + //} +//} +//macro_rules! edn_context { + //($Struct:ident |$l:lifetime, $state:ident| { + //$($key:literal = $field:ident: $Type:ty => $expr:expr,)* + //}) => { + + //#[derive(Default)] + //pub struct EdnView<$l> { $($field: Option<$Type>),* } + + //impl<$l> EdnView<$l> { + //pub fn parse <'e> (edn: &[Edn<'e>]) -> impl Fn(&$Struct) + use<'e> { + //let imports = Self::imports_all(edn); + //move |state| { + //let mut context = EdnView::default(); + //for import in imports.iter() { + //context.import(state, import) + //} + //} + //} + //fn imports_all <'e> (edn: &[Edn<'e>]) -> Vec<&'e str> { + //let mut imports = vec![]; + //for edn in edn.iter() { + //for import in Self::imports_one(edn) { + //imports.push(import); + //} + //} + //imports + //} + //fn imports_one <'e> (edn: &Edn<'e>) -> Vec<&'e str> { + //match edn { + //Edn::Symbol(import) => vec![import], + //Edn::List(edn) => Self::imports_all(edn.as_slice()), + //_ => vec![], + //} + //} + //pub fn import (&mut self, $state: &$l$Struct, key: &str) { + //match key { + //$($key => self.$field = Some($expr),)* + //_ => {} + //} + //} + //} + //} +//} + +////impl Groovebox { + ////fn status (&self) -> impl Content + use<'_> { + ////let note_pt = self.editor.note_point(); + ////Align::w(Fixed::y(1, )) + ////} + ////fn pool (&self) -> impl Content + use<'_> { + ////let w = self.size.w(); + ////let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + ////Fixed::x(if self.compact { 5 } else { pool_w }, + ////) + ////} + ////fn sampler (&self) -> impl Content + use<'_> { + ////let sampler_w = if self.compact { 4 } else { 11 }; + ////let sampler_y = if self.compact { 1 } else { 0 }; + ////Fixed::x(sampler_w, Push::y(sampler_y, Fill::y( + ////SampleList::new(self.compact, &self.sampler, &self.editor)))) + ////} +////} +/////////////////////////////////////////////////////////////////////////////////////////////////// +render!(TuiOut: (self: Arranger) => self.size.of(EdnView::from_source(self, Self::EDN))); +impl EdnViewData for &Arranger { + fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { + use EdnItem::*; + let tracks_w = self.tracks_with_sizes().last().unwrap().3 as u16; + match item { + Nil => Box::new(()), + Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), + Sym(":editor") => (&self.editor).boxed(), + Sym(":pool") => self.pool().boxed(), + Sym(":status") => self.status().boxed(), + Sym(":toolbar") => self.toolbar().boxed(), + Sym(":tracks") => self.track_row(tracks_w).boxed(), + Sym(":scenes") => self.scene_row(tracks_w).boxed(), + Sym(":inputs") => self.input_row(tracks_w).boxed(), + Sym(":outputs") => self.output_row(tracks_w).boxed(), + _ => panic!("no content for {item:?}") + } + } +} +impl Arranger { + const EDN: &'static str = include_str!("arranger.edn"); + pub const LEFT_SEP: char = '▎'; + pub const TRACK_MIN_WIDTH: usize = 9; + + fn toolbar (&self) -> impl Content + use<'_> { + Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock)))) + } + fn pool (&self) -> impl Content + use<'_> { + Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact, &self.pool))) + } + fn status (&self) -> impl Content + use<'_> { + Bsp::e(self.editor.clip_status(), self.editor.edit_status()) + } + 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 + { + tracks_with_sizes(self.tracks.iter(), match self.selected { + ArrangerSelection::Track(t) if self.is_editing() => Some(t), + ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t), + _ => None + }, self.editor_w()) + } + + 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_header()), + Fill::x(Align::c(Fixed::xy(tracks_w, h, self.play_cells()))) + )) + } + fn play_header (&self) -> BoxThunk { + (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Playing")).boxed()).into() + } + fn play_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_header()), + Fill::x(Align::c(Fixed::xy(tracks_w, h, self.next_cells()))) + )) + } + fn next_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Next")).boxed()).into() + } + fn next_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|x;//Rugged(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_header()), + Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.track_cells())))) + )) + } + fn track_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (||Tui::bold(true, Bsp::s( + row!( + Tui::fg(TuiTheme::g(128), "add "), + Tui::fg(TuiTheme::orange(), "t"), + Tui::fg(TuiTheme::g(128), "rack"), + ), + row!( + Tui::fg(TuiTheme::orange(), "a"), + Tui::fg(TuiTheme::g(128), "dd scene"), + ), + ).boxed())).into() + } + fn track_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; + let active = self.selected.track() == Some(i); + let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) }; + let border = Style::default().fg(bfg).bg(bg); + Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16, + Outer(border).enclose(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 = 2 + self.midi_ins[0].connect.len() as u16; + Fixed::y(h, Bsp::e( + Fixed::xy(self.sidebar_w() as u16, h, self.input_header()), + Fill::x(Align::c(Fixed::xy(tracks_w, h, self.input_cells()))) + )) + } + fn input_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (||Bsp::s( + Tui::bold(true, row!( + Tui::fg(TuiTheme::g(128), "midi "), + Tui::fg(TuiTheme::orange(), "I"), + Tui::fg(TuiTheme::g(128), "ns"), + )), + 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_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(); + map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n( + Self::rec_mon(color.base.rgb, false, false), + phat_hi(color.base.rgb, color.dark.rgb) + )))) + })).boxed()).into() + } + fn rec_mon (bg: Color, rec: bool, mon: bool) -> impl Content { + row!( + Tui::fg_bg(if rec { Color::Red } else { bg }, bg, "▐"), + Tui::fg_bg(if rec { Color::White } else { Color::Rgb(0,0,0) }, bg, "REC"), + Tui::fg_bg(if rec { Color::White } else { bg }, bg, "▐"), + Tui::fg_bg(if mon { Color::White } else { Color::Rgb(0,0,0) }, bg, "MON"), + Tui::fg_bg(if mon { Color::White } else { bg }, bg, "▌"), + ) + } + + fn output_row (&self, tracks_w: u16) -> impl Content + '_ { + let h = 2 + self.midi_outs[0].connect.len() as u16; + Fixed::y(h, Bsp::e( + Fixed::xy(self.sidebar_w() as u16, h, self.output_header()), + Fill::x(Align::c(Fixed::xy(tracks_w, h, self.output_cells()))) + )) + } + fn output_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (||Bsp::s( + Tui::bold(true, row!( + Tui::fg(TuiTheme::g(128), "midi "), + Tui::fg(TuiTheme::orange(), "O"), + Tui::fg(TuiTheme::g(128), "uts"), + )), + 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_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(); + map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n( + Self::mute_solo(color.base.rgb, false, false), + phat_hi(color.dark.rgb, color.darker.rgb) + )))) + })).boxed()).into() + } + fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content { + row!( + Tui::fg_bg(if mute { Color::White } else { Color::Rgb(0,0,0) }, bg, "MUTE"), + Tui::fg_bg(if mute { Color::White } else { bg }, bg, "▐"), + Tui::fg_bg(if solo { Color::White } else { Color::Rgb(0,0,0) }, bg, "SOLO"), + ) + } + + fn scene_row (&self, tracks_w: u16) -> impl Content + '_ { + let h = (self.size.h() as u16).saturating_sub(8).max(8); + let border = |x|x;//Skinny(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x); + Bsp::e( + Tui::bg(Color::Reset, Fixed::xy(self.sidebar_w() as u16, h, self.scene_headers())), + Tui::bg(Color::Reset, Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.scene_cells()))))) + ) + } + fn scene_headers <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + (||{ + let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); + let selected = self.selected.scene(); + 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 active = selected == Some(i); + let mid = if active { color.light } else { color.base }; + let top = Some(last_color.read().unwrap().base.rgb); + 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_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + let editing = self.is_editing(); + let tracks = move||self.tracks_with_sizes(); + let scenes = ||self.scenes_with_sizes(2); + let selected_track = self.selected.track(); + let selected_scene = self.selected.scene(); + (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, fg, bg) = if let Some(c) = &scene.clips[t] { + let c = c.read().unwrap(); + (c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb) + } else { + ("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32)) + }; + let last = last_color.read().unwrap().clone(); + 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(fg, Push::x(1, Tui::bold(true, name.to_string()))), + Tui::fg(fg, 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(bg.into()) + }, + bg.into(), + bg.into(), + )); + let cell = Either(active, editor, cell); + *last_color.write().unwrap() = bg.into(); + map_south( + y1 as u16, + h + 1, + Fill::x(Fixed::y(h + 1, 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)) + } +} diff --git a/tui/src/tui_content.rs b/tui/src/tui_content.rs index 9a0ad1ad..2ff25470 100644 --- a/tui/src/tui_content.rs +++ b/tui/src/tui_content.rs @@ -80,3 +80,44 @@ impl Content for RepeatH<'_> { } } } + +/// A phat line +pub fn phat_lo (fg: Color, bg: Color) -> impl Content { + Fixed::y(1, Tui::fg_bg(fg, bg, RepeatH(&"▄"))) +} +/// A phat line +pub 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. +pub 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))), + ) + ) +} +pub 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))), + ) + ) +} +pub 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)), + ) + ) + ) +}