From d45bd2122e07697b7368008e954bc64f867f4fd2 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 14 May 2025 20:19:44 +0300 Subject: [PATCH] groovebox: spiffy sidebar --- config/config_groovebox.edn | 20 +++--- crates/app/src/view.rs | 70 +++++++++++++++++--- crates/device/src/arranger/arranger_model.rs | 10 +-- crates/device/src/editor/editor_view.rs | 17 ++--- crates/device/src/sampler/sampler_model.rs | 11 ++- crates/device/src/sampler/sampler_view.rs | 53 ++++++++++++--- 6 files changed, 137 insertions(+), 44 deletions(-) diff --git a/config/config_groovebox.edn b/config/config_groovebox.edn index c313319f..ce8495fe 100644 --- a/config/config_groovebox.edn +++ b/config/config_groovebox.edn @@ -2,18 +2,6 @@ (info "A sequencer with built-in sampler.") -(view - (bsp/a :view-dialog - (bsp/s (fixed/y 1 :view-transport) - (bsp/n (fixed/y 1 :view-status) - (bsp/w :view-meters-output - (bsp/e :view-meters-input - (bsp/n :view-sample-info - (bsp/n (fixed/y 5 :view-sample-viewer) - (bsp/w :view-pool - (bsp/e :view-samples-keys - (fill/y :view-editor))))))))))) - (keys (layer-if :focus-browser "./keys_browser.edn") (layer-if :focus-pool-rename "./keys_rename.edn") @@ -22,3 +10,11 @@ (layer "./keys_editor.edn") (layer "./keys_sampler.edn") (layer "./keys_global.edn")) + +(view (bsp/a :view-dialog + (bsp/w :view-meters-output (bsp/e :view-meters-input + (bsp/n (fixed/y 6 (bsp/e :view-sample-status :view-sample-viewer)) + (bsp/e + (fill/y (align/n (bsp/s :view-status-v (bsp/s :view-ports-status + (bsp/s :view-editor-status :view-pool))))) + (bsp/e :view-samples-keys :view-editor))))))) diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index 3ffbd439..f7209be3 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -14,6 +14,36 @@ impl App { pub fn view_nil (&self) -> impl Content + use<'_> { "nil" } + pub fn view_status_v (&self) -> impl Content + use<'_> { + self.update_clock(); + let cache = self.view_cache.read().unwrap(); + let theme = self.color; + let playing = self.clock().is_rolling(); + Fixed::xy(20, 7, col!( + Fill::x(Align::w(Bsp::e( + Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + Either::new(false, // TODO + Thunk::new(move||Fixed::x(9, Either::new(playing, + Tui::fg(Rgb(0, 255, 0), " PLAYING "), + Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + ), + Thunk::new(move||Fixed::x(5, Either::new(playing, + Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) + ) + ) + )), + Bsp::s( + FieldH(theme, "Beat", cache.beat.view.clone()), + FieldH(theme, "Time", cache.time.view.clone()), + ), + ))), + Fill::x(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + Fill::x(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + Fill::x(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), + Fill::x(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), + )) + } pub fn view_status (&self) -> impl Content + use<'_> { self.update_clock(); let cache = self.view_cache.read().unwrap(); @@ -27,13 +57,34 @@ impl App { cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) } pub fn view_editor (&self) -> impl Content + use<'_> { - self.editor().map(|e|Bsp::n(Bsp::e(e.clip_status(), e.edit_status()), e)) + self.editor() + } + pub fn view_editor_status (&self) -> impl Content + use<'_> { + Fixed::y(5, self.editor().map(|e|Bsp::s(e.clip_status(), e.edit_status()))) + } + pub fn view_ports_status (&self) -> impl Content + use<'_> { + self.project.get_track().map(|track|Bsp::s( + Fixed::y(4.max(track.sequencer.midi_ins.len() as u16), Align::n(Map::south(1, + ||track.sequencer.midi_ins.iter(), + |port, index|Fixed::x(20, FieldV( + self.color, + format!("IN {index}: "), + format!("{}", port.name())))))), + Fixed::y(4.max(track.sequencer.midi_outs.len() as u16), Align::n(Map::south(1, + ||track.sequencer.midi_outs.iter(), + |port, index|Fixed::x(20, FieldV( + self.color, + format!("OUT {index}: "), + format!("{}", port.name())))))))) } pub fn view_arranger (&self) -> impl Content + use<'_> { ArrangerView::new(&self.project, self.editor.as_ref()) } pub fn view_pool (&self) -> impl Content + use<'_> { - PoolView(&self.project.pool) + Fixed::x(20, Bsp::s( + Fill::x(Align::w(Bsp::s("", FieldH(self.color, "Pool", "")))), + Fill::y(Align::n(Tui::bg(Black, PoolView(&self.project.pool)))), + )) } pub fn view_samples_keys (&self) -> impl Content + use<'_> { self.project.sampler().map(|s|s.view_list(false, self.editor().unwrap())) @@ -47,6 +98,9 @@ impl App { pub fn view_sample_info (&self) -> impl Content + use<'_> { self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) } + pub fn view_sample_status (&self) -> impl Content + use<'_> { + self.project.sampler().map(|s|s.view_sample_status(self.editor().unwrap().get_note_pos())) + } pub fn view_meters_input (&self) -> impl Content + use<'_> { self.project.sampler().map(|s|s.view_meters_input()) } @@ -63,9 +117,9 @@ impl App { Dialog::Help(offset) => self.view_dialog_help(*offset).boxed(), Dialog::Save(browser) => - self.view_dialog_save().boxed(), + self.view_dialog_save(browser).boxed(), Dialog::Load(browser) => - self.view_dialog_load().boxed(), + self.view_dialog_load(browser).boxed(), Dialog::Options => self.view_dialog_options().boxed(), Dialog::Device(index) => @@ -108,21 +162,21 @@ impl App { pub fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content + use<'a> { Bsp::s(message, Bsp::s("", "[ OK ]")) } - pub fn view_dialog_save <'a> (&'a self) -> impl Content + use<'a> { + pub fn view_dialog_save <'a> (&'a self, browser: &'a Browser) -> impl Content + use<'a> { Bsp::s( Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( Tui::bold(true, " Save project: "), Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::xy("todo file browser"))) + .enclose(Fill::xy(browser))) } - pub fn view_dialog_load <'a> (&'a self) -> impl Content + use<'a> { + pub fn view_dialog_load <'a> (&'a self, browser: &'a Browser) -> impl Content + use<'a> { Bsp::s( Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( Tui::bold(true, " Load project: "), Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::xy("todo file browser"))) + .enclose(Fill::xy(browser))) } pub fn view_dialog_options <'a> (&'a self) -> impl Content + use<'a> { "TODO" diff --git a/crates/device/src/arranger/arranger_model.rs b/crates/device/src/arranger/arranger_model.rs index 46687ab1..5f38e61e 100644 --- a/crates/device/src/arranger/arranger_model.rs +++ b/crates/device/src/arranger/arranger_model.rs @@ -111,27 +111,27 @@ impl Arrangement { //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) } /// Get the active track - fn get_track (&self) -> Option<&Track> { + pub fn get_track (&self) -> Option<&Track> { let index = self.selection().track()?; Has::>::get(self).get(index) } /// Get a mutable reference to the active track - fn get_track_mut (&mut self) -> Option<&mut Track> { + pub fn get_track_mut (&mut self) -> Option<&mut Track> { let index = self.selection().track()?; Has::>::get_mut(self).get_mut(index) } /// Get the active scene - fn get_scene (&self) -> Option<&Scene> { + pub fn get_scene (&self) -> Option<&Scene> { let index = self.selection().scene()?; Has::>::get(self).get(index) } /// Get a mutable reference to the active scene - fn get_scene_mut (&mut self) -> Option<&mut Scene> { + pub fn get_scene_mut (&mut self) -> Option<&mut Scene> { let index = self.selection().scene()?; Has::>::get_mut(self).get_mut(index) } /// Get the active clip - fn get_clip (&self) -> Option>> { + pub fn get_clip (&self) -> Option>> { self.get_scene()?.clips.get(self.selection().track()?)?.clone() } /// Put a clip in a slot diff --git a/crates/device/src/editor/editor_view.rs b/crates/device/src/editor/editor_view.rs index c91de260..ab0ca78f 100644 --- a/crates/device/src/editor/editor_view.rs +++ b/crates/device/src/editor/editor_view.rs @@ -12,10 +12,11 @@ impl MidiEditor { let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { (clip.color, clip.name.clone(), clip.length, clip.looped) } else { (ItemTheme::G[64], String::new().into(), 0, false) }; - Bsp::e( - FieldH(color, "Edit", format!("{name} ({length})")), - FieldH(color, "Loop", looped.to_string()) - ) + Fixed::x(20, col!( + Fill::x(Align::w(FieldV(color, "Clip ", format!("{name}")))), + Fill::x(Align::w(FieldH(color, "Length", format!("{length}")))), + Fill::x(Align::w(FieldH(color, "Loop ", looped.to_string()))), + )) } pub fn edit_status (&self) -> impl Content + '_ { @@ -29,10 +30,10 @@ impl MidiEditor { let note_name = format!("{:4}", Note::pitch_to_name(note_pos)); let note_pos = format!("{:>3}", note_pos); let note_len = format!("{:>4}", self.get_note_len()); - Bsp::e( - FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")), - FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")), - ) + Fixed::x(20, Bsp::s( + Fill::x(Align::w(FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")))), + Fill::x(Align::w(FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")))), + )) } } diff --git a/crates/device/src/sampler/sampler_model.rs b/crates/device/src/sampler/sampler_model.rs index bf5c624d..b9137636 100644 --- a/crates/device/src/sampler/sampler_model.rs +++ b/crates/device/src/sampler/sampler_model.rs @@ -146,11 +146,20 @@ pub struct Sample { pub channels: Vec>, pub rate: Option, pub gain: f32, + pub color: ItemTheme, } impl Sample { pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { - Self { name: name.as_ref().into(), start, end, channels, rate: None, gain: 1.0 } + Self { + name: name.as_ref().into(), + start, + end, + channels, + rate: None, + gain: 1.0, + color: ItemTheme::random(), + } } pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { Voice { diff --git a/crates/device/src/sampler/sampler_view.rs b/crates/device/src/sampler/sampler_view.rs index ee67807b..cf2192f5 100644 --- a/crates/device/src/sampler/sampler_view.rs +++ b/crates/device/src/sampler/sampler_view.rs @@ -66,10 +66,18 @@ impl Sampler { //let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); let mut bg = if note == note_pt { Tui::g(64) } else { Color::Reset }; let mut fg = Tui::g(160); - let mapped: &Option>> = &self.mapped[note]; - if mapped.is_some() { - fg = Tui::g(224); - bg = Color::Rgb(0, if note == note_pt { 96 } else { 64 }, 0); + if let Some(mapped) = &self.mapped[note] { + let sample = mapped.read().unwrap(); + fg = if note == note_pt { + sample.color.lightest.rgb + } else { + Tui::g(224) + }; + bg = if note == note_pt { + sample.color.light.rgb + } else { + sample.color.base.rgb + }; } if let Some((index, _)) = self.recording { if note == index { @@ -110,6 +118,16 @@ impl Sampler { }))) } + pub fn view_sample_status (&self, note_pt: usize) -> impl Content + use<'_> { + Fixed::x(20, draw_info_v(if let Some((_, sample)) = &self.recording { + Some(sample) + } else if let Some(sample) = &self.mapped[note_pt] { + Some(sample) + } else { + None + })) + } + pub fn view_status (&self, index: usize) -> impl Content { draw_status(self.mapped[index].as_ref()) } @@ -189,11 +207,11 @@ fn draw_viewer (sample: Option<&Arc>>) -> impl Content + .y_bounds([0.0, height as f64]) .paint(|ctx| { let text = "press record to begin sampling"; - ctx.print( - (width - text.len() as u16) as f64 / 2.0, - height as f64 / 2.0, - text.red() - ); + //ctx.print( + //(width - text.len() as u16) as f64 / 2.0, + //height as f64 / 2.0, + //text.red() + //); }) .render(area, &mut to.buffer); } @@ -203,7 +221,7 @@ fn draw_viewer (sample: Option<&Arc>>) -> impl Content + fn draw_info (sample: Option<&Arc>>) -> impl Content + use<'_> { When(sample.is_some(), Thunk::new(move||{ let sample = sample.unwrap().read().unwrap(); - let theme = ItemTheme::G[96]; + let theme = sample.color; row!( FieldH(theme, "Name", format!("{:<10}", sample.name.clone())), FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())), @@ -215,6 +233,21 @@ fn draw_info (sample: Option<&Arc>>) -> impl Content + us })) } +fn draw_info_v (sample: Option<&Arc>>) -> impl Content + use<'_> { + When(sample.is_some(), Thunk::new(move||{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + Fixed::x(20, col!( + Fill::x(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))), + Fill::x(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))), + Fill::x(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))), + Fill::x(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))), + Fill::x(Align::w(FieldH(theme, "Trans ", "0 "))), + Fill::x(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))), + )) + })) +} + fn draw_status (sample: Option<&Arc>>) -> impl Content { Tui::bold(true, Tui::fg(Tui::g(224), sample .map(|sample|{