diff --git a/midi/src/midi_edit.rs b/midi/src/midi_edit.rs index 6ff67149..0cb77c83 100644 --- a/midi/src/midi_edit.rs +++ b/midi/src/midi_edit.rs @@ -185,8 +185,8 @@ impl MidiViewer for MidiEditor { fn set_clip (&mut self, p: Option<&Arc>>) { self.mode.set_clip(p) } } edn_command!(MidiEditCommand: |state: MidiEditor| { - ("note/put" [_a: bool] Self::PutNote) - ("note/del" [_a: bool] Self::PutNote) + ("note/put" [_a: bool] Self::PutNote) + ("note/del" [_a: bool] Self::PutNote) ("note/pos" [a: usize] Self::SetNoteCursor(a.expect("no note cursor"))) ("note/len" [a: usize] Self::SetNoteLength(a.expect("no note length"))) ("time/pos" [a: usize] Self::SetTimeCursor(a.expect("no time cursor"))) diff --git a/midi/src/midi_pool.rs b/midi/src/midi_pool.rs index 4ff52fb7..2fb73f39 100644 --- a/midi/src/midi_pool.rs +++ b/midi/src/midi_pool.rs @@ -479,8 +479,8 @@ edn_command!(FileBrowserCommand: |state: MidiPool| { ("begin" [] Self::Begin) ("cancel" [] Self::Cancel) ("confirm" [] Self::Confirm) - ("select" [i: usize] Self::Select(i.expect("no index"))) - ("chdir" [p: PathBuf] Self::Chdir(p.expect("no path"))) + ("select" [i: usize] Self::Select(i.expect("no index"))) + ("chdir" [p: PathBuf] Self::Chdir(p.expect("no path"))) ("filter" [f: Arc] Self::Filter(f.expect("no filter"))) }); command!(|self: FileBrowserCommand, state: MidiPool|{ diff --git a/sampler/src/has_sampler.rs b/sampler/src/has_sampler.rs deleted file mode 100644 index a2e3bb8f..00000000 --- a/sampler/src/has_sampler.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::*; - -pub trait HasSampler { - fn sampler (&self) -> &Option; - fn sampler_mut (&mut self) -> &mut Option; - fn sample_index (&self) -> usize; - fn view_sample <'a> (&'a self, compact: bool) -> impl Content + 'a { - self.sampler().as_ref().map(|sampler|Max::y( - if compact { 0u16 } else { 5 }.into(), - Fill::x(sampler.viewer(self.sample_index())) - )) - } - fn view_sampler <'a> (&'a self, compact: bool, editor: &Option) -> impl Content + 'a { - self.sampler().as_ref().map(|sampler|Fixed::x( - if compact { 4u16 } else { 40 }.into(), - Push::y( - if compact { 1u16 } else { 0 }.into(), - editor.as_ref().map(|e|Fill::y(sampler.list(compact, e))) - ) - )) - } -} - -#[macro_export] macro_rules! has_sampler { - (|$self:ident:$Struct:ty| { sampler = $e0:expr; index = $e1:expr; }) => { - impl HasSampler for $Struct { - fn sampler (&$self) -> &Option { &$e0 } - fn sampler_mut (&mut $self) -> &mut Option { &mut $e0 } - fn sample_index (&$self) -> usize { $e1 } - } - } -} diff --git a/sampler/src/keys_sampler.edn b/sampler/src/keys_sampler.edn new file mode 100644 index 00000000..e69de29b diff --git a/sampler/src/lib.rs b/sampler/src/lib.rs index 5f6311f0..89bba172 100644 --- a/sampler/src/lib.rs +++ b/sampler/src/lib.rs @@ -1,25 +1,17 @@ mod sampler; pub use self::sampler::*; -mod sampler_tui; pub use self::sampler_tui::*; -mod sampler_cmd; pub use self::sampler_cmd::*; -mod has_sampler; pub use self::has_sampler::*; - pub(crate) use ::tek_jack::{*, jack::*}; pub(crate) use ::tek_midi::{*, midly::{*, live::*, num::*}}; -pub(crate) use ::tek_tui::{ - *, - tek_output::*, - tek_input::*, - tek_edn::*, - ratatui::prelude::*, - crossterm::event::*, -}; - +pub(crate) use ::tek_tui::*; +pub(crate) use ::tek_tui::tek_output::*; +pub(crate) use ::tek_tui::tek_input::*; +pub(crate) use ::tek_tui::tek_edn::*; +pub(crate) use ::tek_tui::ratatui::prelude::*; +pub(crate) use ::tek_tui::crossterm::event::*; pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}}; pub(crate) use std::fs::File; pub(crate) use std::path::PathBuf; pub(crate) use std::error::Error; pub(crate) use std::ffi::OsString; -pub(crate) use KeyCode::Char; pub(crate) use symphonia::{ core::{ formats::Packet, @@ -31,8 +23,8 @@ pub(crate) use symphonia::{ }, default::get_codecs, }; -pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Points, Line}}}; - +pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}}; #[cfg(test)] #[test] fn test_sampler () { // TODO! + let sample = Sample::new("test", 0, 0, vec![]); } diff --git a/sampler/src/sampler.rs b/sampler/src/sampler.rs index f5c4cfcc..ba4682e8 100644 --- a/sampler/src/sampler.rs +++ b/sampler/src/sampler.rs @@ -1,8 +1,35 @@ use crate::*; - -/// The sampler plugin plays sounds. -#[derive(Debug)] -pub struct Sampler { +pub trait HasSampler { + fn sampler (&self) -> &Option; + fn sampler_mut (&mut self) -> &mut Option; + fn sample_index (&self) -> usize; + fn view_sample <'a> (&'a self, compact: bool) -> impl Content + 'a { + self.sampler().as_ref().map(|sampler|Max::y( + if compact { 0u16 } else { 5 }.into(), + Fill::x(sampler.viewer(self.sample_index())) + )) + } + fn view_sampler <'a> (&'a self, compact: bool, editor: &Option) -> impl Content + 'a { + self.sampler().as_ref().map(|sampler|Fixed::x( + if compact { 4u16 } else { 40 }.into(), + Push::y( + if compact { 1u16 } else { 0 }.into(), + editor.as_ref().map(|e|Fill::y(sampler.list(compact, e))) + ) + )) + } +} +#[macro_export] macro_rules! has_sampler { + (|$self:ident:$Struct:ty| { sampler = $e0:expr; index = $e1:expr; }) => { + impl HasSampler for $Struct { + fn sampler (&$self) -> &Option { &$e0 } + fn sampler_mut (&mut $self) -> &mut Option { &mut $e0 } + fn sample_index (&$self) -> usize { $e1 } + } + } +} +/// The sampler device plays sounds in response to MIDI notes. +#[derive(Debug)] pub struct Sampler { pub jack: Arc>, pub name: String, pub mapped: [Option>>;128], @@ -16,6 +43,32 @@ pub struct Sampler { pub buffer: Vec>, pub output_gain: f32 } +/// A sound sample. +#[derive(Default, Debug)] pub struct Sample { + pub name: Arc, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, + pub gain: f32, +} +/// Load sample from WAV and assign to MIDI note. +#[macro_export] macro_rules! sample { + ($note:expr, $name:expr, $src:expr) => {{ + let (end, data) = read_sample_data($src)?; + ( + u7::from_int_lossy($note).into(), + Sample::new($name, 0, end, data).into() + ) + }}; +} +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} impl Default for Sampler { fn default () -> Self { Self { @@ -76,197 +129,6 @@ impl Sampler { } } } -audio!(|self: SamplerTui, client, scope|{ - SamplerAudio(&mut self.state).process(client, scope) -}); -pub struct SamplerAudio<'a>(pub &'a mut Sampler); -audio!(|self: SamplerAudio<'a>, _client, scope|{ - self.0.process_midi_in(scope); - self.0.clear_output_buffer(); - self.0.process_audio_out(scope); - self.0.write_output_buffer(scope); - self.0.process_audio_in(scope); - Control::Continue -}); -impl Sampler { - pub fn process_audio_in (&mut self, scope: &ProcessScope) { - let Sampler { audio_ins, input_meter, recording, .. } = self; - if audio_ins.len() != input_meter.len() { - *input_meter = vec![0.0;audio_ins.len()]; - } - if let Some((_, sample)) = recording { - let mut sample = sample.write().unwrap(); - if sample.channels.len() != audio_ins.len() { - panic!("channel count mismatch"); - } - let iterator = audio_ins.iter().zip(input_meter).zip(sample.channels.iter_mut()); - let mut length = 0; - for ((input, meter), channel) in iterator { - let slice = input.port.as_slice(scope); - length = length.max(slice.len()); - let total: f32 = slice.iter().map(|x|x.abs()).sum(); - let count = slice.len() as f32; - *meter = 10. * (total / count).log10(); - channel.extend_from_slice(slice); - } - sample.end += length; - } else { - for (input, meter) in audio_ins.iter().zip(input_meter) { - let slice = input.port.as_slice(scope); - let total: f32 = slice.iter().map(|x|x.abs()).sum(); - let count = slice.len() as f32; - *meter = 10. * (total / count).log10(); - } - } - } - /// Create [Voice]s from [Sample]s in response to MIDI input. - pub fn process_midi_in (&mut self, scope: &ProcessScope) { - let Sampler { midi_in, mapped, voices, .. } = self; - if let Some(ref midi_in) = midi_in { - for RawMidi { time, bytes } in midi_in.port.iter(scope) { - if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { - match message { - MidiMessage::NoteOn { ref key, ref vel } => { - if let Some(ref sample) = mapped[key.as_int() as usize] { - voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); - } - }, - MidiMessage::Controller { controller, value } => { - // TODO - } - _ => {} - } - } - } - } - } - /// Zero the output buffer. - pub fn clear_output_buffer (&mut self) { - for buffer in self.buffer.iter_mut() { - buffer.fill(0.0); - } - } - /// Mix all currently playing samples into the output. - pub fn process_audio_out (&mut self, scope: &ProcessScope) { - let Sampler { ref mut buffer, voices, output_gain, .. } = self; - let channel_count = buffer.len(); - voices.write().unwrap().retain_mut(|voice|{ - for index in 0..scope.n_frames() as usize { - if let Some(frame) = voice.next() { - for (channel, sample) in frame.iter().enumerate() { - // Averaging mixer: - //self.buffer[channel % channel_count][index] = ( - //(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0 - //); - buffer[channel % channel_count][index] += sample * *output_gain; - } - } else { - return false - } - } - true - }); - } - /// Write output buffer to output ports. - pub fn write_output_buffer (&mut self, scope: &ProcessScope) { - let Sampler { ref mut audio_outs, buffer, .. } = self; - for (i, port) in audio_outs.iter_mut().enumerate() { - let buffer = &buffer[i]; - for (i, value) in port.port.as_mut_slice(scope).iter_mut().enumerate() { - *value = *buffer.get(i).unwrap_or(&0.0); - } - } - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -type MidiSample = (Option, Arc>); - -from_edn!("sampler" => |jack: &Arc>, args| -> crate::Sampler { - let mut name = String::new(); - let mut dir = String::new(); - let mut samples = BTreeMap::new(); - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) { - dir = String::from(*n); - } - }, - Edn::List(args) => match args.first() { - Some(Edn::Symbol("sample")) => { - let (midi, sample) = MidiSample::from_edn((jack, &dir), &args[1..])?; - if let Some(midi) = midi { - samples.insert(midi, sample); - } else { - panic!("sample without midi binding: {}", sample.read().unwrap().name); - } - }, - _ => panic!("unexpected in sampler {name}: {args:?}") - }, - _ => panic!("unexpected in sampler {name}: {edn:?}") - }); - Self::new(jack, &name) -}); - -from_edn!("sample" => |(_jack, dir): (&Arc>, &str), args| -> MidiSample { - let mut name = String::new(); - let mut file = String::new(); - let mut midi = None; - let mut start = 0usize; - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) { - file = String::from(*f); - } - if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { - start = *i as usize; - } - if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { - midi = Some(u7::from(*m as u8)); - } - }, - _ => panic!("unexpected in sample {name}"), - }); - let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; - Ok((midi, Arc::new(RwLock::new(crate::Sample { - name, - start, - end, - channels: data, - rate: None, - gain: 1.0 - })))) -}); - -/// A sound sample. -#[derive(Default, Debug)] -pub struct Sample { - pub name: Arc, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, - pub gain: f32, -} - -/// Load sample from WAV and assign to MIDI note. -#[macro_export] macro_rules! sample { - ($note:expr, $name:expr, $src:expr) => {{ - let (end, data) = read_sample_data($src)?; - ( - u7::from_int_lossy($note).into(), - Sample::new($name, 0, end, data).into() - ) - }}; -} - 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 } @@ -385,16 +247,168 @@ impl Sample { } } } - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] -pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, +audio!(|self: SamplerTui, client, scope|SamplerAudio(&mut self.state).process(client, scope)); +pub struct SamplerAudio<'a>(pub &'a mut Sampler); +audio!(|self: SamplerAudio<'a>, _client, scope|{ + self.0.process_midi_in(scope); + self.0.clear_output_buffer(); + self.0.process_audio_out(scope); + self.0.write_output_buffer(scope); + self.0.process_audio_in(scope); + Control::Continue +}); +impl Sampler { + pub fn process_audio_in (&mut self, scope: &ProcessScope) { + let Sampler { audio_ins, input_meter, recording, .. } = self; + if audio_ins.len() != input_meter.len() { + *input_meter = vec![0.0;audio_ins.len()]; + } + if let Some((_, sample)) = recording { + let mut sample = sample.write().unwrap(); + if sample.channels.len() != audio_ins.len() { + panic!("channel count mismatch"); + } + let iterator = audio_ins.iter().zip(input_meter).zip(sample.channels.iter_mut()); + let mut length = 0; + for ((input, meter), channel) in iterator { + let slice = input.port.as_slice(scope); + length = length.max(slice.len()); + let total: f32 = slice.iter().map(|x|x.abs()).sum(); + let count = slice.len() as f32; + *meter = 10. * (total / count).log10(); + channel.extend_from_slice(slice); + } + sample.end += length; + } else { + for (input, meter) in audio_ins.iter().zip(input_meter) { + let slice = input.port.as_slice(scope); + let total: f32 = slice.iter().map(|x|x.abs()).sum(); + let count = slice.len() as f32; + *meter = 10. * (total / count).log10(); + } + } + } + /// Create [Voice]s from [Sample]s in response to MIDI input. + pub fn process_midi_in (&mut self, scope: &ProcessScope) { + let Sampler { midi_in, mapped, voices, .. } = self; + if let Some(ref midi_in) = midi_in { + for RawMidi { time, bytes } in midi_in.port.iter(scope) { + if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { + match message { + MidiMessage::NoteOn { ref key, ref vel } => { + if let Some(ref sample) = mapped[key.as_int() as usize] { + voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + } + }, + MidiMessage::Controller { controller, value } => { + // TODO + } + _ => {} + } + } + } + } + } + /// Zero the output buffer. + pub fn clear_output_buffer (&mut self) { + for buffer in self.buffer.iter_mut() { + buffer.fill(0.0); + } + } + /// Mix all currently playing samples into the output. + pub fn process_audio_out (&mut self, scope: &ProcessScope) { + let Sampler { ref mut buffer, voices, output_gain, .. } = self; + let channel_count = buffer.len(); + voices.write().unwrap().retain_mut(|voice|{ + for index in 0..scope.n_frames() as usize { + if let Some(frame) = voice.next() { + for (channel, sample) in frame.iter().enumerate() { + // Averaging mixer: + //self.buffer[channel % channel_count][index] = ( + //(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0 + //); + buffer[channel % channel_count][index] += sample * *output_gain; + } + } else { + return false + } + } + true + }); + } + /// Write output buffer to output ports. + pub fn write_output_buffer (&mut self, scope: &ProcessScope) { + let Sampler { ref mut audio_outs, buffer, .. } = self; + for (i, port) in audio_outs.iter_mut().enumerate() { + let buffer = &buffer[i]; + for (i, value) in port.port.as_mut_slice(scope).iter_mut().enumerate() { + *value = *buffer.get(i).unwrap_or(&0.0); + } + } + } } - +/////////////////////////////////////////////////////////////////////////////////////////////////// +type MidiSample = (Option, Arc>); +from_edn!("sampler" => |jack: &Arc>, args| -> crate::Sampler { + let mut name = String::new(); + let mut dir = String::new(); + let mut samples = BTreeMap::new(); + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) { + dir = String::from(*n); + } + }, + Edn::List(args) => match args.first() { + Some(Edn::Symbol("sample")) => { + let (midi, sample) = MidiSample::from_edn((jack, &dir), &args[1..])?; + if let Some(midi) = midi { + samples.insert(midi, sample); + } else { + panic!("sample without midi binding: {}", sample.read().unwrap().name); + } + }, + _ => panic!("unexpected in sampler {name}: {args:?}") + }, + _ => panic!("unexpected in sampler {name}: {edn:?}") + }); + Self::new(jack, &name) +}); +from_edn!("sample" => |(_jack, dir): (&Arc>, &str), args| -> MidiSample { + let mut name = String::new(); + let mut file = String::new(); + let mut midi = None; + let mut start = 0usize; + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) { + file = String::from(*f); + } + if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { + start = *i as usize; + } + if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { + midi = Some(u7::from(*m as u8)); + } + }, + _ => panic!("unexpected in sample {name}"), + }); + let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; + Ok((midi, Arc::new(RwLock::new(crate::Sample { + name, + start, + end, + channels: data, + rate: None, + gain: 1.0 + })))) +}); impl Iterator for Voice { type Item = [f32;2]; fn next (&mut self) -> Option { @@ -414,7 +428,6 @@ impl Iterator for Voice { None } } - pub struct AddSampleModal { exited: bool, dir: PathBuf, @@ -426,7 +439,6 @@ pub struct AddSampleModal { voices: Arc>>, _search: Option, } - impl AddSampleModal { fn exited (&self) -> bool { self.exited @@ -435,7 +447,6 @@ impl AddSampleModal { self.exited = true } } - impl AddSampleModal { pub fn new ( sample: &Arc>, @@ -536,11 +547,9 @@ impl AddSampleModal { return Ok(false) } } - fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { todo!(); } - fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { let (mut subdirs, mut files) = std::fs::read_dir(dir)? .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ @@ -557,7 +566,6 @@ fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { files.sort(); Ok((subdirs, files)) } - fn draw_sample ( to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool ) -> Usually { @@ -575,7 +583,6 @@ fn draw_sample ( to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); Ok(label1.len() + label2.len() + 4) } - impl Content for AddSampleModal { fn render (&self, to: &mut TuiOut) { todo!() @@ -612,7 +619,6 @@ impl Content for AddSampleModal { //Lozenge(Style::default()).draw(to) } } - //impl Handle for AddSampleModal { //fn handle (&mut self, from: &TuiIn) -> Perhaps { //if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { @@ -621,7 +627,6 @@ impl Content for AddSampleModal { //Ok(Some(true)) //} //} - //pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { //[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ //modal.exit(); @@ -646,9 +651,288 @@ impl Content for AddSampleModal { //Ok(true) //}] //}); - - pub enum SamplerMode { // Load sample from path Import(usize, FileBrowser), } +//handle!(TuiIn: |self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input.event())); +#[derive(Clone, Debug)] pub enum SamplerTuiCommand { + Import(FileBrowserCommand), + Select(usize), + Sample(SamplerCommand), +} +edn_command!(SamplerTuiCommand: |state: SamplerTui| { + ("select" [i: usize] Self::Select(i.expect("no index"))) + ("import" [a, ..b] if let Some(command) = FileBrowserCommand::from_edn(state, a, b) { + Self::Import(command) + } else { + return None + }) + ("sample" [a, ..b] if let Some(command) = SamplerCommand::from_edn(&state.state, a, b) { + Self::Sample(command) + } else { + return None + }) +}); +edn_provide!(usize: |self: SamplerTui| {}); +edn_provide!(PathBuf: |self: SamplerTui| {}); +edn_provide!(Arc: |self: SamplerTui| {}); +edn_command!(FileBrowserCommand: |state: SamplerTui| { + ("begin" [] Self::Begin) + ("cancel" [] Self::Cancel) + ("confirm" [] Self::Confirm) + ("select" [i: usize] Self::Select(i.expect("no index"))) + ("chdir" [p: PathBuf] Self::Chdir(p.expect("no path"))) + ("filter" [f: Arc] Self::Filter(f.expect("no filter"))) +}); +#[derive(Clone, Debug)] pub enum SamplerCommand { + RecordBegin(u7), + RecordCancel, + RecordFinish, + SetSample(u7, Option>>), + SetStart(u7, usize), + SetGain(u7, f32), + NoteOn(u7, u7), + NoteOff(u7), +} +edn_command!(SamplerCommand: |state: Sampler| { + ("record/begin" [i: u7] + Self::RecordBegin(i.expect("no index"))) + ("record/cancel" [] + Self::RecordCancel) + ("record/finish" [] + Self::RecordFinish) + ("set/sample" [i: u7, s: Option>>] + Self::SetSample(i.expect("no index"), s.expect("no sampler"))) + ("set/start" [i: u7, s: usize] + Self::SetStart(i.expect("no index"), s.expect("no start"))) + ("set/gain" [i: u7, g: f32] + Self::SetGain(i.expect("no index"), g.expect("no garin"))) + ("note/on" [p: u7, v: u7] + Self::NoteOn(p.expect("no pitch"), v.expect("no velocity"))) + ("note/off" [p: u7] + Self::NoteOff(p.expect("no pitch"))) +}); +edn_provide!(u7: |self: Sampler| {}); +edn_provide!(Option>>: |self: Sampler| {}); +edn_provide!(usize: |self: Sampler| {}); +edn_provide!(f32: |self: Sampler| {}); +input_to_command!(FileBrowserCommand: |state:SamplerTui, input: Event|match input { _ => return None }); +command!(|self: FileBrowserCommand,state:SamplerTui|match self { _ => todo!() }); +//input_to_command!(SamplerTuiCommand: |state: SamplerTui, input: Event|match state.mode{ + //Some(SamplerMode::Import(..)) => Self::Import( + //FileBrowserCommand::input_to_command(state, input)? + //), + //_ => match input { + //// load sample + //kpat!(Shift-Char('L')) => Self::Import(FileBrowserCommand::Begin), + //kpat!(KeyCode::Up) => Self::Select(state.note_pos().overflowing_add(1).0.min(127)), + //kpat!(KeyCode::Down) => Self::Select(state.note_pos().overflowing_sub(1).0.min(127)), + //_ => return None + //} +//}); +command!(|self: SamplerTuiCommand, state: SamplerTui|match self { + Self::Import(FileBrowserCommand::Begin) => { + //let voices = &state.state.voices; + //let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); + None + }, + Self::Select(index) => { + let old = state.note_pos(); + state.set_note_pos(index); + Some(Self::Select(old)) + }, + Self::Sample(cmd) => cmd.execute(&mut state.state)?.map(Self::Sample), + _ => todo!("{self:?}") +}); +command!(|self: SamplerCommand, state: Sampler|match self { + Self::RecordBegin(index) => { state.begin_recording(index.as_int() as usize); None }, + Self::RecordCancel => { state.cancel_recording(); None }, + Self::RecordFinish => { state.finish_recording(); None }, + Self::SetSample(index, sample) => { + let i = index.as_int() as usize; + let old = state.mapped[i].clone(); + state.mapped[i] = sample; + Some(Self::SetSample(index, old)) + }, + _ => todo!("{self:?}") +}); +pub struct SamplerTui { + pub state: Sampler, + pub cursor: (usize, usize), + pub editing: Option>>, + pub mode: Option, + /// Size of actual notes area + pub size: Measure, + /// Lowest note displayed + pub note_lo: AtomicUsize, + pub note_pt: AtomicUsize, + pub color: ItemPalette +} +impl SamplerTui { + /// Immutable reference to sample at cursor. + pub fn sample (&self) -> Option<&Arc>> { + for (i, sample) in self.state.mapped.iter().enumerate() { + if i == self.cursor.0 { + return sample.as_ref() + } + } + for (i, sample) in self.state.unmapped.iter().enumerate() { + if i + self.state.mapped.len() == self.cursor.0 { + return Some(sample) + } + } + None + } +} +content!(TuiOut: |self: SamplerTui| { + let keys_width = 5; + let keys = move||"";//SamplerKeys(self); + let fg = self.color.base.rgb; + let bg = self.color.darkest.rgb; + let border = Fill::xy(Outer(true, Style::default().fg(fg).bg(bg))); + let with_border = |x|lay!(border, Fill::xy(x)); + let with_size = |x|lay!(self.size.clone(), x); + Tui::bg(bg, Fill::xy(with_border(Bsp::s( + Tui::fg(self.color.light.rgb, Tui::bold(true, &"Sampler")), + with_size(Shrink::y(1, Bsp::e( + Fixed::x(keys_width, keys()), + Fill::xy(SamplesTui { + color: self.color, + note_hi: self.note_hi(), + note_pt: self.note_pos(), + height: self.size.h(), + }), + ))), + )))) +}); +struct SamplesTui { + color: ItemPalette, + note_hi: usize, + note_pt: usize, + height: usize, +} +render!(TuiOut: |self: SamplesTui, to| { + let x = to.area.x(); + let bg_base = self.color.darkest.rgb; + let bg_selected = self.color.darker.rgb; + let style_empty = Style::default().fg(self.color.base.rgb); + let style_full = Style::default().fg(self.color.lighter.rgb); + for y in 0..self.height { + let note = self.note_hi - y as usize; + let bg = if note == self.note_pt { bg_selected } else { bg_base }; + let style = Some(style_empty.bg(bg)); + to.blit(&" (no sample) ", x, to.area.y() + y as u16, style); + } +}); +impl NoteRange for SamplerTui { + fn note_lo (&self) -> &AtomicUsize { &self.note_lo } + fn note_axis (&self) -> &AtomicUsize { &self.size.y } +} +impl NotePoint for SamplerTui { + fn note_len (&self) -> usize {0/*TODO*/} + fn set_note_len (&self, x: usize) {} + fn note_pos (&self) -> usize { self.note_pt.load(Relaxed) } + fn set_note_pos (&self, x: usize) { self.note_pt.store(x, Relaxed); } +} +impl Sampler { + const EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; + pub fn list <'a> (&'a self, compact: bool, editor: &MidiEditor) -> impl Content + 'a { + let note_lo = editor.note_lo().load(Relaxed); + let note_pt = editor.note_pos(); + let note_hi = editor.note_hi(); + Outer(true, Style::default().fg(TuiTheme::g(96))).enclose(Map::new(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 self.mapped[note].is_some() { + fg = TuiTheme::g(224); + bg = Color::Rgb(0, if note == note_pt { 96 } else { 64 }, 0); + } + if let Some((index, _)) = self.recording { + if note == index { + bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; + fg = Color::Rgb(224,64,32) + } + } + offset(Tui::fg_bg(fg, bg, format!("{note:3} {}", self.list_item(note, compact)))) + })) + } + pub fn list_item (&self, note: usize, compact: bool) -> String { + if compact { + String::default() + } else if let Some(sample) = &self.mapped[note] { + let sample = sample.read().unwrap(); + format!("{:8} {:3} {:6}-{:6}/{:6}", + sample.name, + sample.gain, + sample.start, + sample.end, + sample.channels[0].len() + ) + } else { + String::from("(none)") + } + } + pub fn viewer (&self, note_pt: usize) -> impl Content { + let sample = if let Some((_, sample)) = &self.recording { + Some(sample.clone()) + } else if let Some(sample) = &self.mapped[note_pt] { + Some(sample.clone()) + } else { + None + }; + let min_db = -40.0; + RenderThunk::new(move|to: &mut TuiOut|{ + let [x, y, width, height] = to.area(); + let area = Rect { x, y, width, height }; + let (x_bounds, y_bounds, lines): ([f64;2], [f64;2], Vec) = + if let Some(sample) = &sample { + let sample = sample.read().unwrap(); + let start = sample.start as f64; + let end = sample.end as f64; + let length = end - start; + let step = length / width as f64; + let mut t = start; + let mut lines = vec![]; + while t < end { + let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; + let total: f32 = chunk.iter().map(|x|x.abs()).sum(); + let count = chunk.len() as f32; + let meter = 10. * (total / count).log10(); + let x = t as f64; + let y = meter as f64; + lines.push(Line::new(x, min_db, x, y, Color::Green)); + t += step / 2.; + } + ( + [sample.start as f64, sample.end as f64], + [min_db, 0.], + lines + ) + } else { + ( + [0.0, width as f64], + [0.0, height as f64], + vec![ + Line::new(0.0, 0.0, width as f64, height as f64, Color::Red), + Line::new(width as f64, 0.0, 0.0, height as f64, Color::Red), + ] + ) + }; + Canvas::default() + .x_bounds(x_bounds) + .y_bounds(y_bounds) + .paint(|ctx| { for line in lines.iter() { ctx.draw(line) } }) + .render(area, &mut to.buffer); + }) + } + pub fn status (&self, index: usize) -> impl Content { + Tui::bold(true, Tui::fg(TuiTheme::g(224), self.mapped[index].as_ref().map(|sample|format!( + "Sample {}-{}", + sample.read().unwrap().start, + sample.read().unwrap().end, + )).unwrap_or_else(||"No sample".to_string()))) + } +} diff --git a/sampler/src/sampler_cmd.rs b/sampler/src/sampler_cmd.rs deleted file mode 100644 index f403da63..00000000 --- a/sampler/src/sampler_cmd.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::*; - -//handle!(TuiIn: |self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input.event())); - -#[derive(Clone, Debug)] pub enum SamplerTuiCommand { - Import(FileBrowserCommand), - Select(usize), - Sample(SamplerCommand), -} -impl EdnCommand for SamplerTuiCommand { - fn from_edn <'a> (state: &SamplerTui, head: &EdnItem<&str>, tail: &'a [EdnItem<&str>]) -> Option { - todo!() - } -} - -#[derive(Clone, Debug)] pub enum SamplerCommand { - RecordBegin(u7), - RecordCancel, - RecordFinish, - SetSample(u7, Option>>), - SetStart(u7, usize), - SetGain(f32), - NoteOn(u7, u7), - NoteOff(u7), -} -impl EdnCommand for SamplerCommand { - fn from_edn <'a> (state: &Sampler, head: &EdnItem<&str>, tail: &'a [EdnItem<&str>]) -> Option { - todo!() - } -} - -input_to_command!(FileBrowserCommand: |state:SamplerTui, input: Event|match input { - _ => return None -}); - -command!(|self:FileBrowserCommand,state:SamplerTui|match self { - _ => todo!() -}); - -//input_to_command!(SamplerTuiCommand: |state: SamplerTui, input: Event|match state.mode{ - //Some(SamplerMode::Import(..)) => Self::Import( - //FileBrowserCommand::input_to_command(state, input)? - //), - //_ => match input { - //// load sample - //kpat!(Shift-Char('L')) => Self::Import(FileBrowserCommand::Begin), - //kpat!(KeyCode::Up) => Self::Select(state.note_pos().overflowing_add(1).0.min(127)), - //kpat!(KeyCode::Down) => Self::Select(state.note_pos().overflowing_sub(1).0.min(127)), - //_ => return None - //} -//}); - -command!(|self: SamplerTuiCommand, state: SamplerTui|match self { - Self::Import(FileBrowserCommand::Begin) => { - let voices = &state.state.voices; - let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); - None - }, - Self::Select(index) => { - let old = state.note_pos(); - state.set_note_pos(index); - Some(Self::Select(old)) - }, - Self::Sample(cmd) => cmd.execute(&mut state.state)?.map(Self::Sample), - _ => todo!() -}); - -command!(|self: SamplerCommand, state: Sampler|match self { - Self::SetSample(index, sample) => { - let i = index.as_int() as usize; - let old = state.mapped[i].clone(); - state.mapped[i] = sample; - Some(Self::SetSample(index, old)) - }, - Self::RecordBegin(index) => { - state.begin_recording(index.as_int() as usize); - None - }, - Self::RecordCancel => { - state.cancel_recording(); - None - }, - Self::RecordFinish => { - state.finish_recording(); - None - }, - _ => todo!() -}); diff --git a/sampler/src/sampler_tui.rs b/sampler/src/sampler_tui.rs deleted file mode 100644 index c37d9b06..00000000 --- a/sampler/src/sampler_tui.rs +++ /dev/null @@ -1,180 +0,0 @@ -use crate::*; -pub struct SamplerTui { - pub state: Sampler, - pub cursor: (usize, usize), - pub editing: Option>>, - pub mode: Option, - /// Size of actual notes area - pub size: Measure, - /// Lowest note displayed - pub note_lo: AtomicUsize, - pub note_pt: AtomicUsize, - pub color: ItemPalette -} -impl SamplerTui { - /// Immutable reference to sample at cursor. - pub fn sample (&self) -> Option<&Arc>> { - for (i, sample) in self.state.mapped.iter().enumerate() { - if i == self.cursor.0 { - return sample.as_ref() - } - } - for (i, sample) in self.state.unmapped.iter().enumerate() { - if i + self.state.mapped.len() == self.cursor.0 { - return Some(sample) - } - } - None - } -} -content!(TuiOut: |self: SamplerTui| { - let keys_width = 5; - let keys = move||"";//SamplerKeys(self); - let fg = self.color.base.rgb; - let bg = self.color.darkest.rgb; - let border = Fill::xy(Outer(true, Style::default().fg(fg).bg(bg))); - let with_border = |x|lay!(border, Fill::xy(x)); - let with_size = |x|lay!(self.size.clone(), x); - Tui::bg(bg, Fill::xy(with_border(Bsp::s( - Tui::fg(self.color.light.rgb, Tui::bold(true, &"Sampler")), - with_size(Shrink::y(1, Bsp::e( - Fixed::x(keys_width, keys()), - Fill::xy(SamplesTui { - color: self.color, - note_hi: self.note_hi(), - note_pt: self.note_pos(), - height: self.size.h(), - }), - ))), - )))) -}); -struct SamplesTui { - color: ItemPalette, - note_hi: usize, - note_pt: usize, - height: usize, -} -render!(TuiOut: |self: SamplesTui, to| { - let x = to.area.x(); - let bg_base = self.color.darkest.rgb; - let bg_selected = self.color.darker.rgb; - let style_empty = Style::default().fg(self.color.base.rgb); - let style_full = Style::default().fg(self.color.lighter.rgb); - for y in 0..self.height { - let note = self.note_hi - y as usize; - let bg = if note == self.note_pt { bg_selected } else { bg_base }; - let style = Some(style_empty.bg(bg)); - to.blit(&" (no sample) ", x, to.area.y() + y as u16, style); - } -}); -impl NoteRange for SamplerTui { - fn note_lo (&self) -> &AtomicUsize { &self.note_lo } - fn note_axis (&self) -> &AtomicUsize { &self.size.y } -} -impl NotePoint for SamplerTui { - fn note_len (&self) -> usize {0/*TODO*/} - fn set_note_len (&self, x: usize) {} - fn note_pos (&self) -> usize { self.note_pt.load(Relaxed) } - fn set_note_pos (&self, x: usize) { self.note_pt.store(x, Relaxed); } -} -impl Sampler { - const EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)]; - pub fn list <'a> (&'a self, compact: bool, editor: &MidiEditor) -> impl Content + 'a { - let note_lo = editor.note_lo().load(Relaxed); - let note_pt = editor.note_pos(); - let note_hi = editor.note_hi(); - Outer(true, Style::default().fg(TuiTheme::g(96))).enclose(Map::new(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 self.mapped[note].is_some() { - fg = TuiTheme::g(224); - bg = Color::Rgb(0, if note == note_pt { 96 } else { 64 }, 0); - } - if let Some((index, _)) = self.recording { - if note == index { - bg = if note == note_pt { Color::Rgb(96,24,0) } else { Color::Rgb(64,16,0) }; - fg = Color::Rgb(224,64,32) - } - } - offset(Tui::fg_bg(fg, bg, format!("{note:3} {}", self.list_item(note, compact)))) - })) - } - pub fn list_item (&self, note: usize, compact: bool) -> String { - if compact { - String::default() - } else if let Some(sample) = &self.mapped[note] { - let sample = sample.read().unwrap(); - format!("{:8} {:3} {:6}-{:6}/{:6}", - sample.name, - sample.gain, - sample.start, - sample.end, - sample.channels[0].len() - ) - } else { - String::from("(none)") - } - } - pub fn viewer (&self, note_pt: usize) -> impl Content { - let sample = if let Some((_, sample)) = &self.recording { - Some(sample.clone()) - } else if let Some(sample) = &self.mapped[note_pt] { - Some(sample.clone()) - } else { - None - }; - let min_db = -40.0; - RenderThunk::new(move|to: &mut TuiOut|{ - let [x, y, width, height] = to.area(); - let area = Rect { x, y, width, height }; - let (x_bounds, y_bounds, lines): ([f64;2], [f64;2], Vec) = - if let Some(sample) = &sample { - let sample = sample.read().unwrap(); - let start = sample.start as f64; - let end = sample.end as f64; - let length = end - start; - let step = length / width as f64; - let mut t = start; - let mut lines = vec![]; - while t < end { - let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)]; - let total: f32 = chunk.iter().map(|x|x.abs()).sum(); - let count = chunk.len() as f32; - let meter = 10. * (total / count).log10(); - let x = t as f64; - let y = meter as f64; - lines.push(Line::new(x, min_db, x, y, Color::Green)); - t += step / 2.; - } - ( - [sample.start as f64, sample.end as f64], - [min_db, 0.], - lines - ) - } else { - ( - [0.0, width as f64], - [0.0, height as f64], - vec![ - Line::new(0.0, 0.0, width as f64, height as f64, Color::Red), - Line::new(width as f64, 0.0, 0.0, height as f64, Color::Red), - ] - ) - }; - - Canvas::default() - .x_bounds(x_bounds) - .y_bounds(y_bounds) - .paint(|ctx| { for line in lines.iter() { ctx.draw(line) } }) - .render(area, &mut to.buffer); - }) - } - pub fn status (&self, index: usize) -> impl Content { - Tui::bold(true, Tui::fg(TuiTheme::g(224), self.mapped[index].as_ref().map(|sample|format!( - "Sample {}-{}", - sample.read().unwrap().start, - sample.read().unwrap().end, - )).unwrap_or_else(||"No sample".to_string()))) - } -} diff --git a/tek/src/lib.rs b/tek/src/lib.rs index 9be02611..000b53bb 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -95,29 +95,24 @@ impl TekCli { let right_tos = PortConnection::collect(&self.right_to, empty, empty); let audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; let audio_tos = &[left_tos.as_slice(), right_tos.as_slice() ]; - Ok(match self.mode { - TekMode::Clock => - engine.run(&jack.activate_with(|jack|Tek::new_clock( - jack, self.bpm, self.sync_lead, self.sync_follow, - &midi_froms, &midi_tos))?)?, - TekMode::Sequencer => - engine.run(&jack.activate_with(|jack|Tek::new_sequencer( - jack, self.bpm, self.sync_lead, self.sync_follow, - &midi_froms, &midi_tos))?)?, - TekMode::Groovebox => - engine.run(&jack.activate_with(|jack|Tek::new_groovebox( - jack, self.bpm, self.sync_lead, self.sync_follow, - &midi_froms, &midi_tos, - &audio_froms, &audio_tos))?)?, - TekMode::Arranger { scenes, tracks, track_width, .. } => - engine.run(&jack.activate_with(|jack|Tek::new_arranger( - jack, self.bpm, self.sync_lead, self.sync_follow, - &midi_froms, &midi_tos, - &audio_froms, &audio_tos, - scenes, tracks, track_width - ))?)?, + engine.run(&jack.activate_with(|jack|match self.mode { + TekMode::Clock => Tek::new_clock( + jack, self.bpm, self.sync_lead, self.sync_follow, + &midi_froms, &midi_tos), + TekMode::Sequencer => Tek::new_sequencer( + jack, self.bpm, self.sync_lead, self.sync_follow, + &midi_froms, &midi_tos), + TekMode::Groovebox => Tek::new_groovebox( + jack, self.bpm, self.sync_lead, self.sync_follow, + &midi_froms, &midi_tos, + &audio_froms, &audio_tos), + TekMode::Arranger { scenes, tracks, track_width, .. } => Tek::new_arranger( + jack, self.bpm, self.sync_lead, self.sync_follow, + &midi_froms, &midi_tos, + &audio_froms, &audio_tos, + scenes, tracks, track_width), _ => todo!() - }) + })?) } } #[derive(Default, Debug)] struct Tek { diff --git a/tui/src/lib.rs b/tui/src/lib.rs index d997312a..7e00ae59 100644 --- a/tui/src/lib.rs +++ b/tui/src/lib.rs @@ -1,35 +1,32 @@ -pub use ::tek_input; -pub(crate) use tek_input::*; - -pub use ::tek_output; -pub(crate) use tek_output::*; - -pub use ::tek_edn; -pub(crate) use ::tek_edn::*; - -pub use ::tek_time; -pub(crate) use ::tek_time::*; - -mod tui_engine; pub use self::tui_engine::*; +mod tui_buffer; pub use self::tui_buffer::*; +mod tui_color; pub use self::tui_color::*; mod tui_content; pub use self::tui_content::*; +mod tui_engine; pub use self::tui_engine::*; +mod tui_file; pub use self::tui_file::*; mod tui_input; pub use self::tui_input::*; mod tui_output; pub use self::tui_output::*; -mod tui_run; pub use self::tui_run::*; - -mod tui_color; pub use self::tui_color::*; -mod tui_style; pub use self::tui_style::*; -mod tui_theme; pub use self::tui_theme::*; -mod tui_border; pub use self::tui_border::*; -mod tui_field; pub use self::tui_field::*; -mod tui_buffer; pub use self::tui_buffer::*; -mod tui_file; pub use self::tui_file::*; +pub use ::tek_edn; pub(crate) use ::tek_edn::*; +pub use ::tek_time; pub(crate) use ::tek_time::*; +pub use ::tek_input; pub(crate) use tek_input::*; +pub use ::tek_output; pub(crate) use tek_output::*; +pub use ::better_panic; pub(crate) use better_panic::{Settings, Verbosity}; +pub use ::palette; pub(crate) use ::palette::{*, convert::*, okhsl::*}; +pub use ::crossterm; pub(crate) use crossterm::{ + ExecutableCommand, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, + event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, +}; +pub use ::ratatui; pub(crate) use ratatui::{ + prelude::{Color, Style, Buffer}, + style::Modifier, + backend::{Backend, CrosstermBackend, ClearType}, + layout::{Size, Rect}, + buffer::Cell +}; pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}}; pub(crate) use std::io::{stdout, Stdout}; pub(crate) use std::path::PathBuf; pub(crate) use std::ffi::OsString; - -/// Prototypal case of implementor macro. -/// Saves 4loc per data pats. #[macro_export] macro_rules! from { ($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => { impl $(<$($lt),+>)? From<$Source> for $Target { @@ -37,26 +34,6 @@ pub(crate) use std::ffi::OsString; } }; } - -pub use ::better_panic; pub(crate) use better_panic::{Settings, Verbosity}; -pub use ::palette; pub(crate) use ::palette::{*, convert::*, okhsl::*}; - -pub use ::crossterm; -pub(crate) use crossterm::{ - ExecutableCommand, - terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, - event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, -}; - -pub use ::ratatui; -pub(crate) use ratatui::{ - prelude::{Color, Style, Buffer}, - style::Modifier, - backend::{Backend, CrosstermBackend, ClearType}, - layout::{Size, Rect}, - buffer::Cell -}; - #[cfg(test)] #[test] fn test_tui_engine () -> Usually<()> { use crate::*; use std::sync::{Arc, RwLock}; diff --git a/tui/src/tui_border.rs b/tui/src/tui_border.rs deleted file mode 100644 index c0ff7617..00000000 --- a/tui/src/tui_border.rs +++ /dev/null @@ -1,253 +0,0 @@ -use crate::*; - -pub struct Bordered>(pub bool, pub S, pub W); -content!(TuiOut: |self: Bordered>|Fill::xy( - lay!(When::new(self.0, Border(self.0, self.1)), Padding::xy(1, 1, &self.2)) -)); - -pub struct Border(pub bool, pub S); -render!(TuiOut: |self: Border, to| { - if self.0 { - let area = to.area(); - if area.w() > 0 && area.y() > 0 { - to.blit(&self.1.nw(), area.x(), area.y(), self.1.style()); - to.blit(&self.1.ne(), area.x() + area.w() - 1, area.y(), self.1.style()); - to.blit(&self.1.sw(), area.x(), area.y() + area.h() - 1, self.1.style()); - to.blit(&self.1.se(), area.x() + area.w() - 1, area.y() + area.h() - 1, self.1.style()); - for x in area.x()+1..area.x()+area.w()-1 { - to.blit(&self.1.n(), x, area.y(), self.1.style()); - to.blit(&self.1.s(), x, area.y() + area.h() - 1, self.1.style()); - } - for y in area.y()+1..area.y()+area.h()-1 { - to.blit(&self.1.w(), area.x(), y, self.1.style()); - to.blit(&self.1.e(), area.x() + area.w() - 1, y, self.1.style()); - } - } - } -}); - -pub trait BorderStyle: Send + Sync + Copy { - fn enabled (&self) -> bool; - fn enclose > (self, w: W) -> impl Content { - Bsp::b(Fill::xy(Border(self.enabled(), self)), w) - } - fn enclose2 > (self, w: W) -> impl Content { - Bsp::b(Margin::xy(1, 1, Fill::xy(Border(self.enabled(), self))), w) - } - fn enclose_bg > (self, w: W) -> impl Content { - Tui::bg(self.style().unwrap().bg.unwrap_or(Color::Reset), - Bsp::b(Fill::xy(Border(self.enabled(), self)), w)) - } - const NW: &'static str = ""; - const N: &'static str = ""; - const NE: &'static str = ""; - const E: &'static str = ""; - const SE: &'static str = ""; - const S: &'static str = ""; - const SW: &'static str = ""; - const W: &'static str = ""; - - const N0: &'static str = ""; - const S0: &'static str = ""; - const W0: &'static str = ""; - const E0: &'static str = ""; - - fn n (&self) -> &str { Self::N } - fn s (&self) -> &str { Self::S } - fn e (&self) -> &str { Self::E } - fn w (&self) -> &str { Self::W } - fn nw (&self) -> &str { Self::NW } - fn ne (&self) -> &str { Self::NE } - fn sw (&self) -> &str { Self::SW } - fn se (&self) -> &str { Self::SE } - #[inline] fn draw <'a> ( - &self, to: &mut TuiOut - ) -> Usually<()> { - if self.enabled() { - self.draw_horizontal(to, None)?; - self.draw_vertical(to, None)?; - self.draw_corners(to, None)?; - } - Ok(()) - } - #[inline] fn draw_horizontal ( - &self, to: &mut TuiOut, style: Option