From f303a8d5524164bada988f0d5d317bf9d10e8720 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 20 Jul 2024 19:47:42 +0300 Subject: [PATCH] wip: sample selector --- Cargo.lock | 1 + Cargo.toml | 1 + src/control.rs | 23 +-- src/core.rs | 20 ++- src/core/handle.rs | 2 +- src/core/render.rs | 14 ++ src/devices.rs | 2 +- src/devices/arranger.rs | 25 +-- src/{view => devices}/chain.rs | 0 src/devices/help.rs | 23 +-- src/devices/sampler.rs | 272 ++++++++++++++++++++++++--------- src/model.rs | 14 +- src/view.rs | 6 +- 13 files changed, 278 insertions(+), 125 deletions(-) rename src/{view => devices}/chain.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index b4a9c676..d2433687 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1058,6 +1058,7 @@ dependencies = [ "microxdg", "midly", "music-math", + "once_cell", "r8brain-rs", "ratatui", "rlsf", diff --git a/Cargo.toml b/Cargo.toml index bd327bae..874c26d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ fraction = "0.15.3" rlsf = "0.2.1" r8brain-rs = "0.3.5" clojure-reader = "0.1.0" +once_cell = "1.19.0" diff --git a/src/control.rs b/src/control.rs index e9f96119..1c16676c 100644 --- a/src/control.rs +++ b/src/control.rs @@ -1,10 +1,10 @@ //! Handling of input events. -use crate::{core::*, handle, App, AppFocus}; +use crate::{core::*, handle, App, AppFocus, model::MODAL}; handle!{ App |self, e| { - if handle_modal(self, e)? { + if handle_modal(e)? { return Ok(true) } Ok(if self.entered { @@ -19,16 +19,21 @@ handle!{ } } -fn handle_modal (state: &mut App, e: &AppEvent) -> Usually { - if let Some(ref mut modal) = state.modal { +fn handle_modal (e: &AppEvent) -> Usually { + let mut handled = false; + let mut close = false; + if let Some(ref mut modal) = *MODAL.lock().unwrap() { if modal.handle(e)? { + handled = true; if modal.exited() { - state.modal = None; + close = true; } - return Ok(true) }; } - Ok(false) + if close { + *MODAL.lock().unwrap() = None; + } + Ok(handled) } fn handle_focused (state: &mut App, e: &AppEvent) -> Usually { @@ -160,8 +165,8 @@ pub const KEYMAP_CHAIN: &'static [KeyBinding] = keymap!(App { /// Generic key bindings for views that support focus. pub const KEYMAP_FOCUS: &'static [KeyBinding] = keymap!(App { - [Char(';'), NONE, "command", "open command palette", |app: &mut App| { - app.modal = Some(Box::new(crate::devices::help::HelpModal::new())); + [Char(';'), NONE, "command", "open command palette", |_: &mut App| { + *MODAL.lock().unwrap() = Some(Box::new(crate::devices::help::HelpModal::new())); Ok(true) }], [Tab, NONE, "focus_next", "focus next area", focus_next], diff --git a/src/core.rs b/src/core.rs index d34ceed4..77fcc4ce 100644 --- a/src/core.rs +++ b/src/core.rs @@ -8,6 +8,9 @@ pub(crate) use std::time::Duration; pub(crate) use std::collections::BTreeMap; pub(crate) use std::sync::atomic::{Ordering, AtomicBool}; pub(crate) use std::sync::{Arc, Mutex, RwLock, LockResult, RwLockReadGuard, RwLockWriteGuard}; +pub(crate) use std::path::PathBuf; +pub(crate) use std::fs::read_dir; +pub(crate) use std::ffi::OsString; // Non-stdlib dependencies: pub(crate) use microxdg::XdgApp; @@ -51,6 +54,19 @@ pub trait Exit: Component { } } +#[macro_export] macro_rules! exit { + ($T:ty) => { + impl Exit for $T { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } + } + } +} + /// Anything that implements `Render` + `Handle` can be used as a UI component. impl Component for T {} @@ -66,7 +82,9 @@ pub trait Device: Render + Handle + Process + Send + Sync { impl Device for T {} // Reexport macros: -pub use crate::{submod, pubmod, render, handle, process, phrase, keymap, ports}; +pub use crate::{ + submod, pubmod, render, handle, process, phrase, keymap, ports, exit +}; // Reexport JACK proto-lib: pub use crate::jack::*; diff --git a/src/core/handle.rs b/src/core/handle.rs index c17a5597..ad1bf993 100644 --- a/src/core/handle.rs +++ b/src/core/handle.rs @@ -15,7 +15,7 @@ pub trait Handle { ($T:ty) => { impl Handle for $T {} }; - ($T:ty |$self:ident, $e:ident|$block:tt) => { + ($T:ty |$self:ident, $e:ident|$block:expr) => { impl Handle for $T { fn handle (&mut $self, $e: &AppEvent) -> Usually { $block diff --git a/src/core/render.rs b/src/core/render.rs index 3e0576fa..6929dec0 100644 --- a/src/core/render.rs +++ b/src/core/render.rs @@ -5,6 +5,20 @@ pub(crate) use ratatui::layout::Rect; pub(crate) use ratatui::buffer::{Buffer, Cell}; use ratatui::widgets::WidgetRef; +pub fn make_dim (buf: &mut Buffer) { + for cell in buf.content.iter_mut() { + cell.bg = ratatui::style::Color::Rgb(30,30,30); + cell.fg = ratatui::style::Color::Rgb(100,100,100); + cell.modifier = ratatui::style::Modifier::DIM; + } +} +pub fn center_box (area: Rect, w: u16, h: u16) -> Rect { + let width = w.min(area.width * 3 / 5); + let height = h.min(area.width * 3 / 5); + let x = area.x + (area.width - width) / 2; + let y = area.y + (area.height - height) / 2; + Rect { x, y, width, height } +} pub fn buffer_update ( buf: &mut Buffer, area: Rect, callback: &impl Fn(&mut Cell, u16, u16) ) { diff --git a/src/devices.rs b/src/devices.rs index cf20ebe7..f217255e 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -1,2 +1,2 @@ //! Music-making apparatuses. -crate::core::pubmod!{arranger help looper mixer plugin sampler setup sequencer transport} +crate::core::pubmod!{arranger chain help looper mixer plugin sampler setup sequencer transport} diff --git a/src/devices/arranger.rs b/src/devices/arranger.rs index 53a563fa..b6375b20 100644 --- a/src/devices/arranger.rs +++ b/src/devices/arranger.rs @@ -338,7 +338,7 @@ mod draw_vertical { y, width: *w as u16, height: area.height, - }, Color::Rgb(40,80,50)); + }, Color::Rgb(60,100,50)); } } } @@ -353,17 +353,18 @@ mod draw_horizontal { pub fn draw (state: &Arranger, buf: &mut Buffer, mut area: Rect) -> Usually { area.height = area.height.min((2 + state.tracks.len() * 2) as u16); - let area = Split::right([ - &track_name_column(state), - &track_mon_column(state), - &track_rec_column(state), - &track_ovr_column(state), - &track_del_column(state), - &track_gain_column(state), - &track_scenes_column(state), - ]).render(buf, area)?; - fill_bg(buf, area, Nord::bg_lo(state.focused, state.entered)); - Ok(area) + Layered([ + &to_fill_bg(Nord::bg_lo(state.focused, state.entered)), + &Split::right([ + &track_name_column(state), + &track_mon_column(state), + &track_rec_column(state), + &track_ovr_column(state), + &track_del_column(state), + &track_gain_column(state), + &track_scenes_column(state), + ]), + ]).render(buf, area) } fn track_name_column <'a> (state: &'a Arranger) -> impl Render + 'a { diff --git a/src/view/chain.rs b/src/devices/chain.rs similarity index 100% rename from src/view/chain.rs rename to src/devices/chain.rs diff --git a/src/devices/help.rs b/src/devices/help.rs index 8d460bc6..f2795fec 100644 --- a/src/devices/help.rs +++ b/src/devices/help.rs @@ -13,26 +13,11 @@ impl HelpModal { Self { cursor: 0, search: None, exited: false } } } -impl Exit for HelpModal { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true; - } -} +exit!(HelpModal); render!(HelpModal |self, buf, area|{ - for cell in buf.content.iter_mut() { - cell.bg = ratatui::style::Color::Rgb(30,30,30); - cell.fg = ratatui::style::Color::Rgb(100,100,100); - cell.modifier = ratatui::style::Modifier::DIM; - } - let width = 64.min(area.width * 3 / 5); - let height = 20.min(area.width * 3 / 5); - let x = area.x + (area.width - width) / 2; - let y = area.y + (area.height - height) / 2; - let area = Rect { x, y, width, height }; + make_dim(buf); + let area = center_box(area, 64, 20); fill_fg(buf, area, Color::Reset); fill_bg(buf, area, Nord::bg_lo(true, true)); fill_char(buf, area, ' '); @@ -48,7 +33,7 @@ render!(HelpModal |self, buf, area|{ let y = y + 1; fill_char(buf, Rect { y, height: 1, ..area }, '-'); let y = y + 1; - for i in 0..height-3 { + for i in 0..area.height-3 { let y = y + i; if let Some(command) = crate::control::KEYMAP_FOCUS.get(i as usize) { format!("{:?}", command.0).blit(buf, x, y, Some(Style::default().white().bold()))?; diff --git a/src/devices/sampler.rs b/src/devices/sampler.rs index c6c25539..2dfc9424 100644 --- a/src/devices/sampler.rs +++ b/src/devices/sampler.rs @@ -1,95 +1,100 @@ //! Sampler (currently 16bit WAVs at system rate; TODO convert/resample) -use crate::core::*; - -/// Key bindings for sampler device. -pub const KEYMAP_SAMPLER: &'static [KeyBinding] = keymap!(Sampler { - [Up, NONE, "cursor_up", "move cursor up", cursor_up], - [Down, NONE, "cursor_down", "move cursor down", cursor_down], - [Char('t'), NONE, "sample_play", "play current sample", trigger], - [Char('a'), NONE, "sample_add", "add a new sample", add_sample], - [Enter, NONE, "sample_edit", "edit selected sample", edit_sample], -}); - -fn cursor_up (state: &mut Sampler) -> Usually { - state.cursor.0 = if state.cursor.0 == 0 { - state.mapped.len() - 1 - } else { - state.cursor.0 - 1 - }; - Ok(true) -} - -fn cursor_down (state: &mut Sampler) -> Usually { - state.cursor.0 = (state.cursor.0 + 1) % state.mapped.len(); - Ok(true) -} - -fn trigger (state: &mut Sampler) -> Usually { - if let Some(sample) = state.sample() { - state.voices.push(sample.play(0, &100.into())) - } - Ok(true) -} - -fn add_sample (state: &mut Sampler) -> Usually { - state.unmapped.push(Sample::new("", 0, 0, vec![])); - Ok(true) -} - -fn edit_sample (state: &mut Sampler) -> Usually { - if let Some(sample) = state.sample() { - state.editing = Some(sample.clone()); - } - Ok(true) -} +use crate::{core::*, view::*, model::MODAL}; /// The sampler plugin plays sounds. pub struct Sampler { - pub name: String, - pub cursor: (usize, usize), - pub editing: Option>, - pub mapped: BTreeMap>, - pub unmapped: Vec>, - pub voices: Vec, - pub ports: JackPorts, - pub buffer: Vec>, + pub name: String, + pub cursor: (usize, usize), + pub editing: Option>, + pub mapped: BTreeMap>, + pub unmapped: Vec>, + pub voices: Vec, + pub ports: JackPorts, + pub buffer: Vec>, pub output_gain: f32 } -process!(Sampler = Sampler::process); -handle!(Sampler |self, event| { - handle_keymap(self, event, KEYMAP_SAMPLER) -}); + render!(Sampler |self, buf, area| { let Rect { x, y, height, .. } = area; let style = Style::default().gray(); let title = format!(" {} ({})", self.name, self.voices.len()); title.blit(buf, x+1, y, Some(style.white().bold().not_dim()))?; let mut width = title.len() + 2; - for (i, (note, sample)) in self.mapped.iter().enumerate() { - let style = if i == self.cursor.0 { - Style::default().green() - } else { - Style::default() - }; - let i = i as u16; - let y1 = y+1+i; - if y1 >= y + height { + let mut y1 = 1; + let mut j = 0; + for (note, sample) in self.mapped.iter() + .map(|(note, sample)|(Some(note), sample)) + .chain(self.unmapped.iter().map(|sample|(None, sample))) + { + if y1 >= height { break } - if i as usize == self.cursor.0 { - "⯈".blit(buf, x+1, y1, Some(style.bold()))?; - } - let label1 = format!("{note:3} {:12}", sample.name); - let label2 = format!("{:>6} {:>6} +0.0", sample.start, sample.end); - label1.blit(buf, x+2, y1, Some(style.bold()))?; - label2.blit(buf, x+3+label1.len()as u16, y1, Some(style))?; - width = width.max(label1.len() + label2.len() + 4); + let active = j == self.cursor.0; + width = width.max(draw_sample(buf, x, y + y1, note, sample, active)?); + y1 = y1 + 1; + j = j + 1; } - let height = ((2 + self.mapped.len()) as u16).min(height); + let height = ((2 + y1) as u16).min(height); Ok(Rect { x, y, width: (width as u16).min(area.width), height }) }); +fn draw_sample ( + buf: &mut Buffer, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool +) -> Usually { + let style = if focus { Style::default().green() } else { Style::default() }; + if focus { + "🬴".blit(buf, x+1, y, Some(style.bold()))?; + } + let label1 = format!("{:3} {:12}", + note.map(|n|n.to_string()).unwrap_or(String::default()), + sample.name); + let label2 = format!("{:>6} {:>6} +0.0", + sample.start, + sample.end); + label1.blit(buf, x+2, y, Some(style.bold()))?; + label2.blit(buf, x+3+label1.len()as u16, y, Some(style))?; + Ok(label1.len() + label2.len() + 4) +} + +handle!(Sampler |self, event| handle_keymap(self, event, KEYMAP_SAMPLER)); + +/// Key bindings for sampler device. +pub const KEYMAP_SAMPLER: &'static [KeyBinding] = keymap!(Sampler { + [Up, NONE, "cursor_up", "move cursor up", |state: &mut Sampler| { + state.cursor.0 = if state.cursor.0 == 0 { + state.mapped.len() + state.unmapped.len() - 1 + } else { + state.cursor.0 - 1 + }; + Ok(true) + }], + [Down, NONE, "cursor_down", "move cursor down", |state: &mut Sampler| { + state.cursor.0 = (state.cursor.0 + 1) % (state.mapped.len() + state.unmapped.len()); + Ok(true) + }], + [Char('t'), NONE, "sample_play", "play current sample", |state: &mut Sampler| { + if let Some(sample) = state.sample() { + state.voices.push(sample.play(0, &100.into())) + } + Ok(true) + }], + [Char('a'), NONE, "sample_add", "add a new sample", |state: &mut Sampler| { + let sample = Sample::new("", 0, 0, vec![]); + *MODAL.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample)?)); + state.unmapped.push(sample); + Ok(true) + }], + [Enter, NONE, "sample_edit", "edit selected sample", |state: &mut Sampler| { + if let Some(sample) = state.sample() { + state.editing = Some(sample.clone()); + } + Ok(true) + }], +}); + +process!(Sampler = Sampler::process); + impl Sampler { pub fn new (name: &str, mapped: Option>>) -> Usually { Jack::new(name)? @@ -257,3 +262,124 @@ impl Iterator for Voice { None } } + +pub struct AddSampleModal { + exited: bool, + dir: PathBuf, + subdirs: Vec, + files: Vec, + cursor: usize, + offset: usize, + sample: Arc, + search: Option, +} +exit!(AddSampleModal); +render!(AddSampleModal |self,buf,area|{ + make_dim(buf); + let area = center_box(area, 64, 20); + fill_fg(buf, area, Color::Reset); + fill_bg(buf, area, Nord::bg_lo(true, true)); + fill_char(buf, area, ' '); + format!("{}", &self.dir.to_string_lossy()) + .blit(buf, area.x+2, area.y+1, Some(Style::default().bold()))?; + "Select sample:" + .blit(buf, area.x+2, area.y+2, Some(Style::default().bold()))?; + for (i, (is_dir, name)) in self.subdirs.iter() + .map(|path|(true, path)) + .chain(self.files.iter().map(|path|(false, path))) + .enumerate() + .skip(self.offset) + { + let t = if is_dir { "" } else { "" }; + format!("{t} {}", name.to_string_lossy()) + .blit(buf, area.x + 2, area.y + 3 + i as u16, Some(if i == self.cursor { + Style::default().green() + } else { + Style::default().white() + }))?; + } + Lozenge(Style::default()).draw(buf, area) +}); +handle!(AddSampleModal |self,e|{ + if handle_keymap(self, e, KEYMAP_ADD_SAMPLE)? { + return Ok(true) + } + Ok(true) +}); +impl AddSampleModal { + fn new (sample: &Arc) -> Usually { + let dir = std::env::current_dir()?; + let (subdirs, files) = scan(&dir)?; + Ok(Self { + exited: false, + dir, + subdirs, + files, + cursor: 0, + offset: 0, + sample: sample.clone(), + search: None + }) + } +} +fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { + Ok(read_dir(dir)?.fold( + (vec!["..".into()], vec![]), + |(mut subdirs, mut files), entry|{ + let entry = entry.expect("failed to read drectory entry"); + let meta = entry.metadata().expect("failed to read entry metadata"); + if meta.is_file() { + files.push(entry.file_name()); + } else if meta.is_dir() { + subdirs.push(entry.file_name()); + } + (subdirs, files) + })) +} +impl AddSampleModal { + fn rescan (&mut self) -> Usually<()> { + scan(&self.dir).map(|(subdirs, files)|{ + self.subdirs = subdirs; + self.files = files; + }) + } + fn prev (&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + fn next (&mut self) { + self.cursor = self.cursor + 1; + } + fn pick (&mut self) -> Usually<()> { + if self.cursor == 0 { + if let Some(parent) = self.dir.parent() { + self.dir = parent.into(); + self.rescan()?; + } + } else if self.cursor < self.subdirs.len() { + self.dir = self.dir.join(&self.subdirs[self.cursor]); + self.rescan()?; + } else if (self.cursor - self.subdirs.len()) < self.files.len() { + + } else { + } + Ok(()) + } +} +pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { + [Esc, NONE, "add_sample_close", "close help dialog", |modal: &mut AddSampleModal|{ + modal.exit(); + Ok(true) + }], + [Up, NONE, "add_sample_prev", "select previous entry", |modal: &mut AddSampleModal|{ + modal.prev(); + Ok(true) + }], + [Down, NONE, "add_sample_next", "select next entry", |modal: &mut AddSampleModal|{ + modal.next(); + Ok(true) + }], + [Enter, NONE, "add_sample_enter", "activate selected entry", |modal: &mut AddSampleModal|{ + modal.pick()?; + Ok(true) + }], +}); diff --git a/src/model.rs b/src/model.rs index 54935202..5bb68d61 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,11 +1,14 @@ //! Application state. use crate::{core::*, devices::{arranger::*, sequencer::*, transport::*}}; +use once_cell::sync::Lazy; + +/// Global modal dialog +pub static MODAL: Lazy>>>> = + Lazy::new(||Arc::new(Mutex::new(None))); /// Root of application state. pub struct App { - /// Optional modal dialog - pub modal: Option>, /// Whether the currently focused section has input priority pub entered: bool, /// Currently focused section @@ -38,11 +41,10 @@ impl App { let xdg = Arc::new(microxdg::XdgApp::new("tek")?); let first_run = crate::config::AppPaths::new(&xdg)?.should_create(); let jack = JackClient::Inactive(Client::new("tek", ClientOptions::NO_START_SERVER)?.0); + *MODAL.lock().unwrap() = first_run.then(||{ + Exit::boxed(crate::devices::setup::SetupModal(Some(xdg.clone()), false)) + }); Ok(Self { - modal: first_run.then(||{ - Exit::boxed(crate::devices::setup::SetupModal(Some(xdg.clone()), false)) - }), - entered: true, section: AppFocus::default(), transport: TransportToolbar::new(Some(jack.transport())), diff --git a/src/view.rs b/src/view.rs index e1fb3cc3..3600b71e 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,8 +1,8 @@ //! Rendering of application to display. -use crate::{render, App, core::*}; +use crate::{render, App, core::*, devices::chain::ChainView, model::MODAL}; -submod! { border chain split theme } +submod! { border split theme } render!(App |self, buf, area| { Split::down([ @@ -13,7 +13,7 @@ render!(App |self, buf, area| { &self.sequencer, ])) ]).render(buf, area)?; - if let Some(ref modal) = self.modal { + if let Some(ref modal) = *MODAL.lock().unwrap() { modal.render(buf, area)?; } Ok(area)