groovebox: spiffy sidebar

This commit is contained in:
🪞👃🪞 2025-05-14 20:19:44 +03:00
parent 0f16c89248
commit d45bd2122e
6 changed files with 137 additions and 44 deletions

View file

@ -2,18 +2,6 @@
(info "A sequencer with built-in sampler.") (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 (keys
(layer-if :focus-browser "./keys_browser.edn") (layer-if :focus-browser "./keys_browser.edn")
(layer-if :focus-pool-rename "./keys_rename.edn") (layer-if :focus-pool-rename "./keys_rename.edn")
@ -22,3 +10,11 @@
(layer "./keys_editor.edn") (layer "./keys_editor.edn")
(layer "./keys_sampler.edn") (layer "./keys_sampler.edn")
(layer "./keys_global.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)))))))

View file

@ -14,6 +14,36 @@ impl App {
pub fn view_nil (&self) -> impl Content<TuiOut> + use<'_> { pub fn view_nil (&self) -> impl Content<TuiOut> + use<'_> {
"nil" "nil"
} }
pub fn view_status_v (&self) -> impl Content<TuiOut> + 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<TuiOut> + use<'_> { pub fn view_status (&self) -> impl Content<TuiOut> + use<'_> {
self.update_clock(); self.update_clock();
let cache = self.view_cache.read().unwrap(); 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()) cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone())
} }
pub fn view_editor (&self) -> impl Content<TuiOut> + use<'_> { pub fn view_editor (&self) -> impl Content<TuiOut> + 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<TuiOut> + use<'_> {
Fixed::y(5, self.editor().map(|e|Bsp::s(e.clip_status(), e.edit_status())))
}
pub fn view_ports_status (&self) -> impl Content<TuiOut> + 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<TuiOut> + use<'_> { pub fn view_arranger (&self) -> impl Content<TuiOut> + use<'_> {
ArrangerView::new(&self.project, self.editor.as_ref()) ArrangerView::new(&self.project, self.editor.as_ref())
} }
pub fn view_pool (&self) -> impl Content<TuiOut> + use<'_> { pub fn view_pool (&self) -> impl Content<TuiOut> + 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<TuiOut> + use<'_> { pub fn view_samples_keys (&self) -> impl Content<TuiOut> + use<'_> {
self.project.sampler().map(|s|s.view_list(false, self.editor().unwrap())) 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<TuiOut> + use<'_> { pub fn view_sample_info (&self) -> impl Content<TuiOut> + use<'_> {
self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos()))
} }
pub fn view_sample_status (&self) -> impl Content<TuiOut> + use<'_> {
self.project.sampler().map(|s|s.view_sample_status(self.editor().unwrap().get_note_pos()))
}
pub fn view_meters_input (&self) -> impl Content<TuiOut> + use<'_> { pub fn view_meters_input (&self) -> impl Content<TuiOut> + use<'_> {
self.project.sampler().map(|s|s.view_meters_input()) self.project.sampler().map(|s|s.view_meters_input())
} }
@ -63,9 +117,9 @@ impl App {
Dialog::Help(offset) => Dialog::Help(offset) =>
self.view_dialog_help(*offset).boxed(), self.view_dialog_help(*offset).boxed(),
Dialog::Save(browser) => Dialog::Save(browser) =>
self.view_dialog_save().boxed(), self.view_dialog_save(browser).boxed(),
Dialog::Load(browser) => Dialog::Load(browser) =>
self.view_dialog_load().boxed(), self.view_dialog_load(browser).boxed(),
Dialog::Options => Dialog::Options =>
self.view_dialog_options().boxed(), self.view_dialog_options().boxed(),
Dialog::Device(index) => Dialog::Device(index) =>
@ -108,21 +162,21 @@ impl App {
pub fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content<TuiOut> + use<'a> { pub fn view_dialog_message <'a> (&'a self, message: &'a Message) -> impl Content<TuiOut> + use<'a> {
Bsp::s(message, Bsp::s("", "[ OK ]")) Bsp::s(message, Bsp::s("", "[ OK ]"))
} }
pub fn view_dialog_save <'a> (&'a self) -> impl Content<TuiOut> + use<'a> { pub fn view_dialog_save <'a> (&'a self, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
Bsp::s( Bsp::s(
Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( Fill::x(Align::w(Margin::xy(1, 1, Bsp::e(
Tui::bold(true, " Save project: "), Tui::bold(true, " Save project: "),
Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))),
Outer(true, Style::default().fg(Tui::g(96))) 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<TuiOut> + use<'a> { pub fn view_dialog_load <'a> (&'a self, browser: &'a Browser) -> impl Content<TuiOut> + use<'a> {
Bsp::s( Bsp::s(
Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( Fill::x(Align::w(Margin::xy(1, 1, Bsp::e(
Tui::bold(true, " Load project: "), Tui::bold(true, " Load project: "),
Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))),
Outer(true, Style::default().fg(Tui::g(96))) 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<TuiOut> + use<'a> { pub fn view_dialog_options <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
"TODO" "TODO"

View file

@ -111,27 +111,27 @@ impl Arrangement {
//1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0) //1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
} }
/// Get the active track /// Get the active track
fn get_track (&self) -> Option<&Track> { pub fn get_track (&self) -> Option<&Track> {
let index = self.selection().track()?; let index = self.selection().track()?;
Has::<Vec<Track>>::get(self).get(index) Has::<Vec<Track>>::get(self).get(index)
} }
/// Get a mutable reference to the active track /// 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()?; let index = self.selection().track()?;
Has::<Vec<Track>>::get_mut(self).get_mut(index) Has::<Vec<Track>>::get_mut(self).get_mut(index)
} }
/// Get the active scene /// Get the active scene
fn get_scene (&self) -> Option<&Scene> { pub fn get_scene (&self) -> Option<&Scene> {
let index = self.selection().scene()?; let index = self.selection().scene()?;
Has::<Vec<Scene>>::get(self).get(index) Has::<Vec<Scene>>::get(self).get(index)
} }
/// Get a mutable reference to the active scene /// 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()?; let index = self.selection().scene()?;
Has::<Vec<Scene>>::get_mut(self).get_mut(index) Has::<Vec<Scene>>::get_mut(self).get_mut(index)
} }
/// Get the active clip /// Get the active clip
fn get_clip (&self) -> Option<Arc<RwLock<MidiClip>>> { pub fn get_clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
self.get_scene()?.clips.get(self.selection().track()?)?.clone() self.get_scene()?.clips.get(self.selection().track()?)?.clone()
} }
/// Put a clip in a slot /// Put a clip in a slot

View file

@ -12,10 +12,11 @@ impl MidiEditor {
let (color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { 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) (clip.color, clip.name.clone(), clip.length, clip.looped)
} else { (ItemTheme::G[64], String::new().into(), 0, false) }; } else { (ItemTheme::G[64], String::new().into(), 0, false) };
Bsp::e( Fixed::x(20, col!(
FieldH(color, "Edit", format!("{name} ({length})")), Fill::x(Align::w(FieldV(color, "Clip ", format!("{name}")))),
FieldH(color, "Loop", looped.to_string()) 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<TuiOut> + '_ { pub fn edit_status (&self) -> impl Content<TuiOut> + '_ {
@ -29,10 +30,10 @@ impl MidiEditor {
let note_name = format!("{:4}", Note::pitch_to_name(note_pos)); let note_name = format!("{:4}", Note::pitch_to_name(note_pos));
let note_pos = format!("{:>3}", note_pos); let note_pos = format!("{:>3}", note_pos);
let note_len = format!("{:>4}", self.get_note_len()); let note_len = format!("{:>4}", self.get_note_len());
Bsp::e( Fixed::x(20, Bsp::s(
FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")), Fill::x(Align::w(FieldH(color, "Time", format!("{length}/{time_zoom}+{time_pos} {time_lock}")))),
FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")), Fill::x(Align::w(FieldH(color, "Note", format!("{note_name} {note_pos} {note_len}")))),
) ))
} }
} }

View file

@ -146,11 +146,20 @@ pub struct Sample {
pub channels: Vec<Vec<f32>>, pub channels: Vec<Vec<f32>>,
pub rate: Option<usize>, pub rate: Option<usize>,
pub gain: f32, pub gain: f32,
pub color: ItemTheme,
} }
impl Sample { impl Sample {
pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self { pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> 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<RwLock<Self>>, after: usize, velocity: &u7) -> Voice { pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
Voice { Voice {

View file

@ -66,10 +66,18 @@ impl Sampler {
//let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); //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 bg = if note == note_pt { Tui::g(64) } else { Color::Reset };
let mut fg = Tui::g(160); let mut fg = Tui::g(160);
let mapped: &Option<Arc<RwLock<Sample>>> = &self.mapped[note]; if let Some(mapped) = &self.mapped[note] {
if mapped.is_some() { let sample = mapped.read().unwrap();
fg = Tui::g(224); fg = if note == note_pt {
bg = Color::Rgb(0, if note == note_pt { 96 } else { 64 }, 0); 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 let Some((index, _)) = self.recording {
if note == index { if note == index {
@ -110,6 +118,16 @@ impl Sampler {
}))) })))
} }
pub fn view_sample_status (&self, note_pt: usize) -> impl Content<TuiOut> + 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<TuiOut> { pub fn view_status (&self, index: usize) -> impl Content<TuiOut> {
draw_status(self.mapped[index].as_ref()) draw_status(self.mapped[index].as_ref())
} }
@ -189,11 +207,11 @@ fn draw_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> +
.y_bounds([0.0, height as f64]) .y_bounds([0.0, height as f64])
.paint(|ctx| { .paint(|ctx| {
let text = "press record to begin sampling"; let text = "press record to begin sampling";
ctx.print( //ctx.print(
(width - text.len() as u16) as f64 / 2.0, //(width - text.len() as u16) as f64 / 2.0,
height as f64 / 2.0, //height as f64 / 2.0,
text.red() //text.red()
); //);
}) })
.render(area, &mut to.buffer); .render(area, &mut to.buffer);
} }
@ -203,7 +221,7 @@ fn draw_viewer (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> +
fn draw_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> { fn draw_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> {
When(sample.is_some(), Thunk::new(move||{ When(sample.is_some(), Thunk::new(move||{
let sample = sample.unwrap().read().unwrap(); let sample = sample.unwrap().read().unwrap();
let theme = ItemTheme::G[96]; let theme = sample.color;
row!( row!(
FieldH(theme, "Name", format!("{:<10}", sample.name.clone())), FieldH(theme, "Name", format!("{:<10}", sample.name.clone())),
FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())), FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())),
@ -215,6 +233,21 @@ fn draw_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + us
})) }))
} }
fn draw_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + 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<RwLock<Sample>>>) -> impl Content<TuiOut> { fn draw_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> {
Tui::bold(true, Tui::fg(Tui::g(224), sample Tui::bold(true, Tui::fg(Tui::g(224), sample
.map(|sample|{ .map(|sample|{