diff --git a/src/arranger.rs b/src/arranger.rs index 15f85078..8b11c808 100644 --- a/src/arranger.rs +++ b/src/arranger.rs @@ -17,7 +17,7 @@ mod arranger_v_sep; pub(crate) use self::arranger_v_sep::*; pub struct ArrangerTui { jack: Arc>, pub clock: Clock, - pub phrases: PoolModel, + pub pool: PoolModel, pub tracks: Vec, pub scenes: Vec, pub splits: [u16;2], @@ -81,7 +81,7 @@ from_jack!(|jack| ArrangerTui { ))); Self { clock, - phrases: (&phrase).into(), + pool: (&phrase).into(), editor: (&phrase).into(), selected: ArrangerSelection::Clip(0, 0), scenes: vec![], @@ -105,8 +105,8 @@ impl ArrangerTui { } } render!(Tui: (self: ArrangerTui) => { - let pool_size = if self.phrases.visible { self.splits[1] } else { 0 }; - let with_pool = |x|Bsp::w(Fixed::x(pool_size, PoolView(&self.phrases)), x); + let pool_size = if self.pool.visible { self.splits[1] } else { 0 }; + let with_pool = |x|Bsp::w(Fixed::x(pool_size, PoolView(self.pool.visible, &self.pool)), x); let status = ArrangerStatus::from(self); let with_editbar = |x|Bsp::n(Fixed::y(1, MidiEditStatus(&self.editor)), x); let with_status = |x|Bsp::n(Fixed::y(2, status), x); @@ -162,6 +162,6 @@ audio!(|self: ArrangerTui, client, scope|{ return Control::Continue }); has_clock!(|self: ArrangerTui|&self.clock); -has_phrases!(|self: ArrangerTui|self.phrases.phrases); +has_phrases!(|self: ArrangerTui|self.pool.phrases); has_editor!(|self: ArrangerTui|self.editor); handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); diff --git a/src/arranger/arranger_command.rs b/src/arranger/arranger_command.rs index b50fb419..6d76447a 100644 --- a/src/arranger/arranger_command.rs +++ b/src/arranger/arranger_command.rs @@ -57,14 +57,14 @@ input_to_command!(ArrangerCommand: |state: ArrangerTui, input|match input.e key_pat!(Shift-Char(' ')) => Self::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), key_pat!(Char('e')) => - Self::Editor(MidiEditCommand::Show(Some(state.phrases.phrase().clone()))), + Self::Editor(MidiEditCommand::Show(Some(state.pool.phrase().clone()))), key_pat!(Ctrl-Left) => Self::Scene(ArrangerSceneCommand::Add), key_pat!(Ctrl-Char('t')) => Self::Track(ArrangerTrackCommand::Add), // Tab: Toggle visibility of phrase pool column key_pat!(Tab) => - Self::Phrases(PoolCommand::Show(!state.phrases.visible)), + Self::Phrases(PoolCommand::Show(!state.pool.visible)), _ => { use ArrangerCommand as Cmd; use ArrangerSelection as Selected; @@ -81,7 +81,7 @@ input_to_command!(ArrangerCommand: |state: ArrangerTui, input|match input.e key_pat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))), key_pat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))), key_pat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))), - key_pat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.phrases.phrase().clone())))), + key_pat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.pool.phrase().clone())))), key_pat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))), key_pat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))), @@ -154,106 +154,19 @@ input_to_command!(ArrangerCommand: |state: ArrangerTui, input|match input.e } }.or_else(||if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { Some(Self::Editor(command)) - } else if let Some(command) = PoolCommand::input_to_command(&state.phrases, input) { + } else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { Some(Self::Phrases(command)) } else { None })? }); -fn to_arrangement_command (state: &ArrangerTui, input: &TuiIn) -> Option { - use ArrangerCommand as Cmd; - 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.event() { - key_pat!(Char('g')) => Some(Cmd::Phrases(PoolCommand::Select(0))), - key_pat!(Char('q')) => Some(Cmd::Clip(Clip::Enqueue(t, s))), - key_pat!(Char(',')) => Some(Cmd::Clip(Clip::Put(t, s, None))), - key_pat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))), - key_pat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))), - key_pat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))), - key_pat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.phrases.phrase().clone())))), - key_pat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))), - key_pat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))), - key_pat!(Up) => Some(Cmd::Select( - if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) })), - key_pat!(Down) => Some(Cmd::Select( - Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1))))), - key_pat!(Left) => Some(Cmd::Select( - if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) })), - key_pat!(Right) => Some(Cmd::Select( - Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s))), - - _ => None - }, - Selected::Scene(s) => match input.event() { - key_pat!(Char(',')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))), - key_pat!(Char('.')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))), - key_pat!(Char('<')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))), - key_pat!(Char('>')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))), - key_pat!(Char('q')) => Some(Cmd::Scene(Scene::Enqueue(s))), - key_pat!(Delete) => Some(Cmd::Scene(Scene::Delete(s))), - key_pat!(Char('c')) => Some(Cmd::Scene(Scene::SetColor(s, ItemPalette::random()))), - - key_pat!(Up) => Some( - Cmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix })), - key_pat!(Down) => Some( - Cmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1))))), - key_pat!(Left) => - None, - key_pat!(Right) => Some( - Cmd::Select(Selected::Clip(0, s))), - - _ => None - }, - Selected::Track(t) => match input.event() { - key_pat!(Char(',')) => Some(Cmd::Track(Track::Swap(t, t - 1))), - key_pat!(Char('.')) => Some(Cmd::Track(Track::Swap(t, t + 1))), - key_pat!(Char('<')) => Some(Cmd::Track(Track::Swap(t, t - 1))), - key_pat!(Char('>')) => Some(Cmd::Track(Track::Swap(t, t + 1))), - key_pat!(Delete) => Some(Cmd::Track(Track::Delete(t))), - key_pat!(Char('c')) => Some(Cmd::Track(Track::SetColor(t, ItemPalette::random()))), - - key_pat!(Up) => - None, - key_pat!(Down) => Some( - Cmd::Select(Selected::Clip(t, 0))), - key_pat!(Left) => Some( - Cmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix })), - key_pat!(Right) => Some( - Cmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1))))), - - _ => None - }, - Selected::Mix => match input.event() { - key_pat!(Delete) => Some(Cmd::Clear), - key_pat!(Char('0')) => Some(Cmd::StopAll), - key_pat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())), - - key_pat!(Up) => - None, - key_pat!(Down) => Some( - Cmd::Select(Selected::Scene(0))), - key_pat!(Left) => - None, - key_pat!(Right) => Some( - Cmd::Select(Selected::Track(0))), - - _ => None - }, - } -} command!(|self: ArrangerCommand, state: ArrangerTui|match self { - Self::Scene(cmd) => cmd.execute(state)?.map(Self::Scene), - Self::Track(cmd) => cmd.execute(state)?.map(Self::Track), - Self::Clip(cmd) => cmd.execute(state)?.map(Self::Clip), - Self::Editor(cmd) => cmd.execute(&mut state.editor)?.map(Self::Editor), - Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock), + 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; @@ -265,23 +178,20 @@ command!(|self: ArrangerCommand, state: ArrangerTui|match self { Some(Self::Color(old)) }, Self::Phrases(cmd) => { - let mut default = |cmd: PoolCommand|{ - cmd.execute(&mut state.phrases).map(|x|x.map(Self::Phrases)) - }; match cmd { // autoselect: automatically load selected phrase in editor PoolCommand::Select(_) => { - let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); + let undo = cmd.delegate(&mut state.pool, Self::Phrases)?; + state.editor.set_phrase(Some(state.pool.phrase())); undo }, // reload phrase in editor to update color PoolCommand::Phrase(PhrasePoolCommand::SetColor(index, _)) => { - let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); + let undo = cmd.delegate(&mut state.pool, Self::Phrases)?; + state.editor.set_phrase(Some(state.pool.phrase())); undo }, - _ => default(cmd)? + _ => cmd.delegate(&mut state.pool, Self::Phrases)? } }, Self::History(_) => { todo!() }, diff --git a/src/groovebox.rs b/src/groovebox.rs index dfac7977..1aee904b 100644 --- a/src/groovebox.rs +++ b/src/groovebox.rs @@ -14,6 +14,7 @@ pub struct Groovebox { pub editor: MidiEditor, pub sampler: Sampler, + pub compact: bool, pub size: Measure, pub status: bool, pub note_buf: Vec, @@ -47,6 +48,7 @@ impl Groovebox { note_buf: vec![], perf: PerfModel::default(), status: true, + compact: true, }) } } @@ -64,7 +66,7 @@ audio!(|self: Groovebox, client, scope|{ if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) { return Control::Quit } - // TODO move this to sampler: + // TODO move these to editor and sampler: for RawMidi { time, bytes } in self.player.midi_ins[0].iter(scope) { if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { match message { @@ -73,22 +75,7 @@ audio!(|self: Groovebox, client, scope|{ }, MidiMessage::Controller { controller, value } => { if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] { - let mut sample = sample.write().unwrap(); - let percentage = value.as_int() as f64 / 127.; - match controller.as_int() { - 20 => { - sample.start = (percentage * sample.end as f64) as usize; - }, - 21 => { - let length = sample.channels[0].len(); - sample.end = sample.start + (percentage * (length as f64 - sample.start as f64)) as usize; - sample.end = sample.end.min(length); - }, - 24 => { - sample.gain = percentage as f32 * 2.0; - }, - _ => {} - } + sample.write().unwrap().handle_cc(controller, value) } } _ => {} @@ -101,23 +88,23 @@ audio!(|self: Groovebox, client, scope|{ render!(Tui: (self: Groovebox) => { let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let pool_w = if self.pool.visible { phrase_w } else { 0 }; - let sampler_w = if self.pool.visible { phrase_w } else { 4 }; - let sample_h = if self.pool.visible { 8 } else { 0 }; + let pool_w = if !self.compact { phrase_w } else { 5 }; + let sampler_w = if !self.compact { phrase_w } else { 4 }; + let sample_h = if !self.compact { 5 } else { 0 }; let note_pt = self.editor.note_point(); let color = self.player.play_phrase().as_ref() .and_then(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)) .clone(); let sampler = - Fixed::x(sampler_w, Push::y(1, Align::w(Fill::y(SampleList::new(&self.sampler, &self.editor))))); + Fixed::x(sampler_w, Push::y(1, Align::w(Fill::y(SampleList::new(self.compact, &self.sampler, &self.editor))))); let selectors = Bsp::e(ClipSelected::play_phrase(&self.player), ClipSelected::next_phrase(&self.player)); let edit_clip = MidiEditClip(&self.editor); self.size.of(Bsp::s( - Fill::x(Fixed::y(if self.pool.visible { 3 } else { 1 }, lay!( + Fill::x(Fixed::y(2, lay!( Align::w(Meter("L/", self.sampler.input_meter[0])), Align::e(Meter("R/", self.sampler.input_meter[1])), Align::x(Tui::bg(TuiTheme::g(32), TransportView::new(true, &self.player.clock))), @@ -141,7 +128,7 @@ render!(Tui: (self: Groovebox) => { Align::x(Fixed::y(1, MidiEditStatus(&self.editor))), ), Bsp::w( - Fixed::x(pool_w, Align::e(Fill::y(PoolView(&self.pool)))), + Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool)))), Fill::xy(Bsp::e(sampler, &self.editor)), ), ) @@ -151,6 +138,7 @@ render!(Tui: (self: Groovebox) => { }); pub enum GrooveboxCommand { + Compact(bool), History(isize), Clock(ClockCommand), Pool(PoolCommand), @@ -172,8 +160,8 @@ input_to_command!(GrooveboxCommand: |state: Groovebox, input|match input.ev if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } ), - // Tab: Toggle visibility of phrase pool column - key_pat!(Tab) => Cmd::Pool(PoolCommand::Show(!state.pool.visible)), + // Tab: Toggle visibility of sidebars + key_pat!(Tab) => Cmd::Compact(!state.compact), // q: Enqueue currently edited phrase key_pat!(Char('q')) => Cmd::Enqueue(Some(state.pool.phrase().clone())), @@ -211,9 +199,6 @@ input_to_command!(GrooveboxCommand: |state: Groovebox, input|match input.ev }); command!(|self: GrooveboxCommand, state: Groovebox|match self { - Self::History(delta) => { - todo!("undo/redo") - }, Self::Enqueue(phrase) => { state.player.enqueue_next(phrase.as_ref()); None @@ -235,7 +220,14 @@ command!(|self: GrooveboxCommand, state: Groovebox|match self { _ => 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::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)) + } else { + None + }, }); diff --git a/src/piano/piano_h.rs b/src/piano/piano_h.rs index 53427c66..66516417 100644 --- a/src/piano/piano_h.rs +++ b/src/piano/piano_h.rs @@ -22,7 +22,7 @@ render!(Tui: (self: PianoHorizontal) => Bsp::s( impl PianoHorizontal { /// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄ fn draw_bg (buf: &mut BigBuffer, phrase: &MidiClip, zoom: usize, note_len: usize) { - for (y, note) in (0..127).rev().enumerate() { + for (y, note) in (0..=127).rev().enumerate() { for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { let cell = buf.get_mut(x, y).unwrap(); cell.set_bg(phrase.color.darkest.rgb); @@ -47,7 +47,7 @@ impl PianoHorizontal { let mut notes_on = [false;128]; for (x, time_start) in (0..phrase.length).step_by(zoom).enumerate() { - for (y, note) in (0..127).rev().enumerate() { + for (y, note) in (0..=127).rev().enumerate() { if let Some(cell) = buf.get_mut(x, note) { if notes_on[note] { cell.set_char('▂'); diff --git a/src/pool.rs b/src/pool.rs index f4c0b2d5..edba6b46 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -1,14 +1,10 @@ +mod pool_tui; pub use self::pool_tui::*; +mod clip_length; pub use self::clip_length::*; +mod clip_rename; pub use self::clip_rename::*; +mod clip_select; pub use self::clip_select::*; + use super::*; -pub mod phrase_length; -pub(crate) use phrase_length::*; - -pub mod phrase_rename; -pub(crate) use phrase_rename::*; - -pub mod phrase_selector; -pub(crate) use phrase_selector::*; - #[derive(Debug)] pub struct PoolModel { pub(crate) visible: bool, @@ -205,23 +201,3 @@ impl PoolModel { } } } -pub struct PoolView<'a>(pub(crate) &'a PoolModel); -render!(Tui: (self: PoolView<'a>) => { - let PoolModel { phrases, mode, .. } = self.0; - let color = self.0.phrase().read().unwrap().color; - Outer( - Style::default().fg(color.dark.rgb).bg(color.darkest.rgb) - ).enclose(Tui::map(||self.0.phrases().iter(), |clip, i|{ - let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); - let item_height = 1; - let item_offset = i as u16 * item_height; - let selected = i == self.0.phrase_index(); - let offset = |a|Push::y(item_offset, Align::n(Fixed::y(item_height, Fill::x(a)))); - offset(Tui::bg(if selected { color.light.rgb } else { color.base.rgb }, lay!( - Align::w(Tui::fg(color.lightest.rgb, Tui::bold(selected, format!(" {i:>3} {name}")))), - Align::e(Tui::fg(color.lightest.rgb, Tui::bold(selected, format!("{length} ")))), - Align::w(Tui::when(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶")))), - Align::e(Tui::when(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀")))), - ))) - })) -}); diff --git a/src/pool/phrase_length.rs b/src/pool/clip_length.rs similarity index 100% rename from src/pool/phrase_length.rs rename to src/pool/clip_length.rs diff --git a/src/pool/phrase_rename.rs b/src/pool/clip_rename.rs similarity index 100% rename from src/pool/phrase_rename.rs rename to src/pool/clip_rename.rs diff --git a/src/pool/phrase_selector.rs b/src/pool/clip_select.rs similarity index 100% rename from src/pool/phrase_selector.rs rename to src/pool/clip_select.rs diff --git a/src/pool/pool_tui.rs b/src/pool/pool_tui.rs new file mode 100644 index 00000000..614cd450 --- /dev/null +++ b/src/pool/pool_tui.rs @@ -0,0 +1,27 @@ +use crate::*; + +pub struct PoolView<'a>(pub bool, pub &'a PoolModel); +render!(Tui: (self: PoolView<'a>) => { + let Self(compact, model) = self; + let PoolModel { phrases, mode, .. } = self.1; + let color = self.1.phrase().read().unwrap().color; + Outer( + Style::default().fg(color.dark.rgb).bg(color.darkest.rgb) + ).enclose(Tui::map(||model.phrases().iter(), |clip, i|{ + let item_height = 1; + let item_offset = i as u16 * item_height; + let selected = i == model.phrase_index(); + let offset = |a|Push::y(item_offset, Align::n(Fixed::y(item_height, Fill::x(a)))); + + 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} ") }; + + offset(Tui::bg(if selected { color.light.rgb } else { color.base.rgb }, lay!( + Align::w(Tui::fg(color.lightest.rgb, Tui::bold(selected, name))), + Align::e(Tui::fg(color.lightest.rgb, Tui::bold(selected, length))), + Align::w(Tui::when(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶")))), + Align::e(Tui::when(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀")))), + ))) + })) +}); diff --git a/src/sampler/sample.rs b/src/sampler/sample.rs index 39f6d9b6..08ece69b 100644 --- a/src/sampler/sample.rs +++ b/src/sampler/sample.rs @@ -118,4 +118,26 @@ impl Sample { } Ok(()) } + pub fn handle_cc (&mut self, controller: u7, value: u7) { + let percentage = value.as_int() as f64 / 127.; + match controller.as_int() { + 20 => { + self.start = (percentage * self.end as f64) as usize; + }, + 21 => { + let length = self.channels[0].len(); + self.end = length.min( + self.start + (percentage * (length as f64 - self.start as f64)) as usize + ); + }, + 22 => { /*attack*/ }, + 23 => { /*decay*/ }, + 24 => { + self.gain = percentage as f32 * 2.0; + }, + 26 => { /* pan */ } + 25 => { /* pitch */ } + _ => {} + } + } } diff --git a/src/sampler/sample_list.rs b/src/sampler/sample_list.rs index 253db240..56b4ac0f 100644 --- a/src/sampler/sample_list.rs +++ b/src/sampler/sample_list.rs @@ -1,19 +1,26 @@ use crate::*; -pub struct SampleList<'a>(&'a Sampler, &'a MidiEditor); +pub struct SampleList<'a> { + compact: bool, + sampler: &'a Sampler, + editor: &'a MidiEditor +} impl<'a> SampleList<'a> { - pub fn new (sampler: &'a Sampler, editor: &'a MidiEditor) -> Self { - Self(sampler, editor) + pub fn new (compact: bool, sampler: &'a Sampler, editor: &'a MidiEditor) -> Self { + Self { compact, sampler, editor } } } render!(Tui: (self: SampleList<'a>) => { - let Self(sampler, editor) = self; + let Self { compact, sampler, editor } = self; let note_lo = editor.note_lo().load(Relaxed); let note_pt = editor.note_point(); let note_hi = editor.note_hi(); Fill::xy(Tui::map(move||(note_lo..=note_hi).rev(), move|note, i| { + + let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); + let mut bg = if note == note_pt { TuiTheme::g(64) } else { Color::Reset }; let mut fg = TuiTheme::g(160); if let Some((index, _)) = sampler.recording { @@ -24,11 +31,14 @@ render!(Tui: (self: SampleList<'a>) => { } else if sampler.mapped[note].is_some() { fg = TuiTheme::g(224); } - let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); - offset(Tui::bg(bg, if let Some(sample) = &sampler.mapped[note] { - Tui::fg(fg, format!("{note:3} ?????? ")) + + offset(Tui::fg_bg(fg, bg, format!("{note:3} {}", if *compact { + "" + } else if let Some(sample) = &sampler.mapped[note] { + "??????" } else { - Tui::fg(fg, format!("{note:3} (none) ")) - })) + "(none)" + }))) + })) }); diff --git a/src/sequencer.rs b/src/sequencer.rs index c0eb9759..3816bfa2 100644 --- a/src/sequencer.rs +++ b/src/sequencer.rs @@ -7,12 +7,14 @@ use PhrasePoolCommand::*; /// Root view for standalone `tek_sequencer`. pub struct SequencerTui { _jack: Arc>, + + pub pool: PoolModel, + pub editor: MidiEditor, + pub player: MidiPlayer, + pub transport: bool, pub selectors: bool, pub clock: Clock, - pub pool: PoolModel, - pub player: MidiPlayer, - pub editor: MidiEditor, pub size: Measure, pub status: bool, pub note_buf: Vec, @@ -26,12 +28,14 @@ from_jack!(|jack|SequencerTui { None, Some(ItemColor::random().into()) ))); Self { - _jack: jack.clone(), + _jack: jack.clone(), + + pool: PoolModel::from(&phrase), + editor: MidiEditor::from(&phrase), + player: MidiPlayer::from((&clock, &phrase)), + transport: true, selectors: true, - pool: PoolModel::from(&phrase), - editor: MidiEditor::from(&phrase), - player: MidiPlayer::from((&clock, &phrase)), size: Measure::new(), midi_buf: vec![vec![];65536], note_buf: vec![], @@ -44,7 +48,7 @@ render!(Tui: (self: SequencerTui) => { let w = self.size.w(); let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; let pool_w = if self.pool.visible { phrase_w } else { 0 }; - let pool = Pull::y(1, Fill::y(Align::e(PoolView(&self.pool)))); + let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool)))); let with_pool = move|x|Bsp::w(Fixed::x(pool_w, pool), x); let status = SequencerStatus::from(self); let with_status = |x|Bsp::n(Fixed::x(if self.status { 2 } else { 0 }, status), x);