mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
unify some modules and implement edn_command for sampler
This commit is contained in:
parent
3a6202464c
commit
d4f962fbfa
19 changed files with 1008 additions and 1084 deletions
|
|
@ -185,8 +185,8 @@ impl MidiViewer for MidiEditor {
|
|||
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { 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")))
|
||||
|
|
|
|||
|
|
@ -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<str>] Self::Filter(f.expect("no filter")))
|
||||
});
|
||||
command!(|self: FileBrowserCommand, state: MidiPool|{
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait HasSampler {
|
||||
fn sampler (&self) -> &Option<Sampler>;
|
||||
fn sampler_mut (&mut self) -> &mut Option<Sampler>;
|
||||
fn sample_index (&self) -> usize;
|
||||
fn view_sample <'a> (&'a self, compact: bool) -> impl Content<TuiOut> + '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<MidiEditor>) -> impl Content<TuiOut> + '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<Sampler> { &$e0 }
|
||||
fn sampler_mut (&mut $self) -> &mut Option<Sampler> { &mut $e0 }
|
||||
fn sample_index (&$self) -> usize { $e1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
0
sampler/src/keys_sampler.edn
Normal file
0
sampler/src/keys_sampler.edn
Normal file
|
|
@ -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![]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,35 @@
|
|||
use crate::*;
|
||||
|
||||
/// The sampler plugin plays sounds.
|
||||
#[derive(Debug)]
|
||||
pub struct Sampler {
|
||||
pub trait HasSampler {
|
||||
fn sampler (&self) -> &Option<Sampler>;
|
||||
fn sampler_mut (&mut self) -> &mut Option<Sampler>;
|
||||
fn sample_index (&self) -> usize;
|
||||
fn view_sample <'a> (&'a self, compact: bool) -> impl Content<TuiOut> + '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<MidiEditor>) -> impl Content<TuiOut> + '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<Sampler> { &$e0 }
|
||||
fn sampler_mut (&mut $self) -> &mut Option<Sampler> { &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<RwLock<JackConnection>>,
|
||||
pub name: String,
|
||||
pub mapped: [Option<Arc<RwLock<Sample>>>;128],
|
||||
|
|
@ -16,6 +43,32 @@ pub struct Sampler {
|
|||
pub buffer: Vec<Vec<f32>>,
|
||||
pub output_gain: f32
|
||||
}
|
||||
/// A sound sample.
|
||||
#[derive(Default, Debug)] pub struct Sample {
|
||||
pub name: Arc<str>,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub channels: Vec<Vec<f32>>,
|
||||
pub rate: Option<usize>,
|
||||
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<RwLock<Sample>>,
|
||||
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<u7>, Arc<RwLock<crate::Sample>>);
|
||||
|
||||
from_edn!("sampler" => |jack: &Arc<RwLock<JackConnection>>, 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<RwLock<JackConnection>>, &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<str>,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub channels: Vec<Vec<f32>>,
|
||||
pub rate: Option<usize>,
|
||||
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<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> 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<RwLock<Sample>>,
|
||||
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<u7>, Arc<RwLock<crate::Sample>>);
|
||||
from_edn!("sampler" => |jack: &Arc<RwLock<JackConnection>>, 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<RwLock<JackConnection>>, &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<Self::Item> {
|
||||
|
|
@ -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<RwLock<Vec<Voice>>>,
|
||||
_search: Option<String>,
|
||||
}
|
||||
|
||||
impl AddSampleModal {
|
||||
fn exited (&self) -> bool {
|
||||
self.exited
|
||||
|
|
@ -435,7 +447,6 @@ impl AddSampleModal {
|
|||
self.exited = true
|
||||
}
|
||||
}
|
||||
|
||||
impl AddSampleModal {
|
||||
pub fn new (
|
||||
sample: &Arc<RwLock<Sample>>,
|
||||
|
|
@ -536,11 +547,9 @@ impl AddSampleModal {
|
|||
return Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||
todo!();
|
||||
}
|
||||
|
||||
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||||
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<OsString>, Vec<OsString>)> {
|
|||
files.sort();
|
||||
Ok((subdirs, files))
|
||||
}
|
||||
|
||||
fn draw_sample (
|
||||
to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
|
||||
) -> Usually<usize> {
|
||||
|
|
@ -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<TuiOut> for AddSampleModal {
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
todo!()
|
||||
|
|
@ -612,7 +619,6 @@ impl Content<TuiOut> for AddSampleModal {
|
|||
//Lozenge(Style::default()).draw(to)
|
||||
}
|
||||
}
|
||||
|
||||
//impl Handle<TuiIn> for AddSampleModal {
|
||||
//fn handle (&mut self, from: &TuiIn) -> Perhaps<bool> {
|
||||
//if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? {
|
||||
|
|
@ -621,7 +627,6 @@ impl Content<TuiOut> for AddSampleModal {
|
|||
//Ok(Some(true))
|
||||
//}
|
||||
//}
|
||||
|
||||
//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding<AddSampleModal>] = keymap!(AddSampleModal {
|
||||
//[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{
|
||||
//modal.exit();
|
||||
|
|
@ -646,9 +651,288 @@ impl Content<TuiOut> 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<str>: |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<str>] Self::Filter(f.expect("no filter")))
|
||||
});
|
||||
#[derive(Clone, Debug)] pub enum SamplerCommand {
|
||||
RecordBegin(u7),
|
||||
RecordCancel,
|
||||
RecordFinish,
|
||||
SetSample(u7, Option<Arc<RwLock<Sample>>>),
|
||||
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<Arc<RwLock<Sample>>>]
|
||||
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<Arc<RwLock<Sample>>>: |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<Arc<RwLock<Sample>>>,
|
||||
pub mode: Option<SamplerMode>,
|
||||
/// Size of actual notes area
|
||||
pub size: Measure<TuiOut>,
|
||||
/// 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<RwLock<Sample>>> {
|
||||
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<TuiOut> + '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<TuiOut> {
|
||||
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<Line>) =
|
||||
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<TuiOut> {
|
||||
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())))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SamplerTui> for SamplerTuiCommand {
|
||||
fn from_edn <'a> (state: &SamplerTui, head: &EdnItem<&str>, tail: &'a [EdnItem<&str>]) -> Option<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)] pub enum SamplerCommand {
|
||||
RecordBegin(u7),
|
||||
RecordCancel,
|
||||
RecordFinish,
|
||||
SetSample(u7, Option<Arc<RwLock<Sample>>>),
|
||||
SetStart(u7, usize),
|
||||
SetGain(f32),
|
||||
NoteOn(u7, u7),
|
||||
NoteOff(u7),
|
||||
}
|
||||
impl EdnCommand<Sampler> for SamplerCommand {
|
||||
fn from_edn <'a> (state: &Sampler, head: &EdnItem<&str>, tail: &'a [EdnItem<&str>]) -> Option<Self> {
|
||||
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!()
|
||||
});
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
use crate::*;
|
||||
pub struct SamplerTui {
|
||||
pub state: Sampler,
|
||||
pub cursor: (usize, usize),
|
||||
pub editing: Option<Arc<RwLock<Sample>>>,
|
||||
pub mode: Option<SamplerMode>,
|
||||
/// Size of actual notes area
|
||||
pub size: Measure<TuiOut>,
|
||||
/// 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<RwLock<Sample>>> {
|
||||
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<TuiOut> + '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<TuiOut> {
|
||||
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<Line>) =
|
||||
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<TuiOut> {
|
||||
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())))
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -1,253 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct Bordered<S: BorderStyle, W: Content<TuiOut>>(pub bool, pub S, pub W);
|
||||
content!(TuiOut: |self: Bordered<S: BorderStyle, W: Content<TuiOut>>|Fill::xy(
|
||||
lay!(When::new(self.0, Border(self.0, self.1)), Padding::xy(1, 1, &self.2))
|
||||
));
|
||||
|
||||
pub struct Border<S: BorderStyle>(pub bool, pub S);
|
||||
render!(TuiOut: |self: Border<S: BorderStyle>, 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 <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
|
||||
Bsp::b(Fill::xy(Border(self.enabled(), self)), w)
|
||||
}
|
||||
fn enclose2 <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
|
||||
Bsp::b(Margin::xy(1, 1, Fill::xy(Border(self.enabled(), self))), w)
|
||||
}
|
||||
fn enclose_bg <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
|
||||
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<Style>
|
||||
) -> Usually<[u16;4]> {
|
||||
let area = to.area();
|
||||
let style = style.or_else(||self.style_horizontal());
|
||||
let [x, x2, y, y2] = area.lrtb();
|
||||
for x in x..x2.saturating_sub(1) {
|
||||
to.blit(&Self::N, x, y, style);
|
||||
to.blit(&Self::S, x, y2.saturating_sub(1), style)
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
#[inline] fn draw_vertical (
|
||||
&self, to: &mut TuiOut, style: Option<Style>
|
||||
) -> Usually<[u16;4]> {
|
||||
let area = to.area();
|
||||
let style = style.or_else(||self.style_vertical());
|
||||
let [x, x2, y, y2] = area.lrtb();
|
||||
let h = y2 - y;
|
||||
if h > 1 {
|
||||
for y in y..y2.saturating_sub(1) {
|
||||
to.blit(&Self::W, x, y, style);
|
||||
to.blit(&Self::E, x2.saturating_sub(1), y, style);
|
||||
}
|
||||
} else if h > 0 {
|
||||
to.blit(&Self::W0, x, y, style);
|
||||
to.blit(&Self::E0, x2.saturating_sub(1), y, style);
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
#[inline] fn draw_corners (
|
||||
&self, to: &mut TuiOut, style: Option<Style>
|
||||
) -> Usually<[u16;4]> {
|
||||
let area = to.area();
|
||||
let style = style.or_else(||self.style_corners());
|
||||
let [x, y, width, height] = area.xywh();
|
||||
if width > 1 && height > 1 {
|
||||
to.blit(&Self::NW, x, y, style);
|
||||
to.blit(&Self::NE, x + width - 1, y, style);
|
||||
to.blit(&Self::SW, x, y + height - 1, style);
|
||||
to.blit(&Self::SE, x + width - 1, y + height - 1, style);
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
#[inline] fn style (&self) -> Option<Style> { None }
|
||||
#[inline] fn style_horizontal (&self) -> Option<Style> { self.style() }
|
||||
#[inline] fn style_vertical (&self) -> Option<Style> { self.style() }
|
||||
#[inline] fn style_corners (&self) -> Option<Style> { self.style() }
|
||||
}
|
||||
|
||||
macro_rules! border {
|
||||
($($T:ident {
|
||||
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
||||
$($x:tt)*
|
||||
}),+) => {$(
|
||||
impl BorderStyle for $T {
|
||||
const NW: &'static str = $nw;
|
||||
const N: &'static str = $n;
|
||||
const NE: &'static str = $ne;
|
||||
const W: &'static str = $w;
|
||||
const E: &'static str = $e;
|
||||
const SW: &'static str = $sw;
|
||||
const S: &'static str = $s;
|
||||
const SE: &'static str = $se;
|
||||
$($x)*
|
||||
fn enabled (&self) -> bool { self.0 }
|
||||
}
|
||||
#[derive(Copy, Clone)] pub struct $T(pub bool, pub Style);
|
||||
impl Content<TuiOut> for $T {
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
if self.enabled() { let _ = self.draw(to); }
|
||||
}
|
||||
}
|
||||
)+}
|
||||
}
|
||||
|
||||
border! {
|
||||
Square {
|
||||
"┌" "─" "┐"
|
||||
"│" "│"
|
||||
"└" "─" "┘" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
SquareBold {
|
||||
"┏" "━" "┓"
|
||||
"┃" "┃"
|
||||
"┗" "━" "┛" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
TabLike {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"│" " " "│" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Lozenge {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"╰" "─" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Brace {
|
||||
"╭" "" "╮"
|
||||
"│" "│"
|
||||
"╰" "" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
LozengeDotted {
|
||||
"╭" "┅" "╮"
|
||||
"┇" "┇"
|
||||
"╰" "┅" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Quarter {
|
||||
"▎" "▔" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "▁" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
QuarterV {
|
||||
"▎" "" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Chamfer {
|
||||
"🭂" "▔" "🭍"
|
||||
"▎" "🮇"
|
||||
"🭓" "▁" "🭞" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Corners {
|
||||
"🬆" "" "🬊" // 🬴 🬸
|
||||
"" ""
|
||||
"🬱" "" "🬵" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
CornersTall {
|
||||
"🭽" "" "🭾"
|
||||
"" ""
|
||||
"🭼" "" "🭿" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Outer {
|
||||
"🭽" "▔" "🭾"
|
||||
"▏" "▕"
|
||||
"🭼" "▁" "🭿"
|
||||
const W0: &'static str = "[";
|
||||
const E0: &'static str = "]";
|
||||
const N0: &'static str = "⎴";
|
||||
const S0: &'static str = "⎵";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Phat {
|
||||
"▄" "▄" "▄"
|
||||
"█" "█"
|
||||
"▀" "▀" "▀"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Rugged {
|
||||
"▄" "▂" "▄"
|
||||
"▐" "▌"
|
||||
"▀" "🮂" "▀"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Skinny {
|
||||
"▗" "▄" "▖"
|
||||
"▐" "▌"
|
||||
"▝" "▀" "▘"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Brackets {
|
||||
"⎡" "" "⎤"
|
||||
"" ""
|
||||
"⎣" "" "⎦"
|
||||
const W0: &'static str = "[";
|
||||
const E0: &'static str = "]";
|
||||
const N0: &'static str = "⎴";
|
||||
const S0: &'static str = "⎵";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Reticle {
|
||||
"⎡" "" "⎤"
|
||||
"" ""
|
||||
"⎣" "" "⎦"
|
||||
const W0: &'static str = "╟";
|
||||
const E0: &'static str = "╢";
|
||||
const N0: &'static str = "┯";
|
||||
const S0: &'static str = "┷";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
use crate::*;
|
||||
|
||||
pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||
for row in 0..area.h() {
|
||||
let y = area.y() + row;
|
||||
|
|
@ -11,14 +10,11 @@ pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut C
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BigBuffer {
|
||||
#[derive(Default)] pub struct BigBuffer {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub content: Vec<Cell>
|
||||
}
|
||||
|
||||
impl BigBuffer {
|
||||
pub fn new (width: usize, height: usize) -> Self {
|
||||
Self { width, height, content: vec![Cell::default(); width*height] }
|
||||
|
|
@ -35,5 +31,4 @@ impl BigBuffer {
|
|||
y * self.width + x
|
||||
}
|
||||
}
|
||||
|
||||
from!(|size:(usize, usize)| BigBuffer = Self::new(size.0, size.1));
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
use crate::*;
|
||||
use rand::{thread_rng, distributions::uniform::UniformSampler};
|
||||
|
||||
pub trait HasColor {
|
||||
fn color (&self) -> ItemColor;
|
||||
}
|
||||
|
||||
pub trait HasColor { fn color (&self) -> ItemColor; }
|
||||
#[macro_export] macro_rules! has_color {
|
||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
|
||||
|
|
@ -12,16 +8,13 @@ pub trait HasColor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A color in OKHSL and RGB representations.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
||||
pub struct ItemColor {
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemColor {
|
||||
pub okhsl: Okhsl<f32>,
|
||||
pub rgb: Color,
|
||||
}
|
||||
/// A color in OKHSL and RGB with lighter and darker variants.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
||||
pub struct ItemPalette {
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemPalette {
|
||||
pub base: ItemColor,
|
||||
pub light: ItemColor,
|
||||
pub lighter: ItemColor,
|
||||
|
|
@ -62,7 +55,6 @@ from!(|base: ItemColor|ItemPalette = {
|
|||
lighter.lightness = (lighter.lightness * 1.3).min(Okhsl::<f32>::max_lightness());
|
||||
let mut lightest = base.okhsl;
|
||||
lightest.lightness = 0.95;
|
||||
|
||||
let mut dark = base.okhsl;
|
||||
dark.lightness = (dark.lightness * 0.75).max(Okhsl::<f32>::min_lightness());
|
||||
dark.saturation = (dark.saturation * 0.75).max(Okhsl::<f32>::min_saturation());
|
||||
|
|
@ -72,7 +64,6 @@ from!(|base: ItemColor|ItemPalette = {
|
|||
let mut darkest = darker;
|
||||
darkest.lightness = 0.1;
|
||||
darkest.saturation = (darkest.saturation * 0.50).max(Okhsl::<f32>::min_saturation());
|
||||
|
||||
Self {
|
||||
base,
|
||||
light: light.into(),
|
||||
|
|
@ -84,9 +75,7 @@ from!(|base: ItemColor|ItemPalette = {
|
|||
}
|
||||
});
|
||||
impl ItemPalette {
|
||||
pub fn random () -> Self {
|
||||
ItemColor::random().into()
|
||||
}
|
||||
pub fn random () -> Self { ItemColor::random().into() }
|
||||
pub fn random_near (color: Self, distance: f32) -> Self {
|
||||
color.base.mix(ItemColor::random(), distance).into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,101 @@ impl Content<TuiOut> for Arc<str> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct Field<T, U>(pub ItemPalette, pub T, pub U)
|
||||
where T: AsRef<str> + Send + Sync, U: AsRef<str> + Send + Sync;
|
||||
|
||||
impl<T, U> Content<TuiOut> for Field<T, U>
|
||||
where T: AsRef<str> + Send + Sync, U: AsRef<str> + Send + Sync
|
||||
{
|
||||
fn content (&self) -> impl Render<TuiOut> {
|
||||
let ItemPalette { darkest, dark, lighter, lightest, .. } = self.0;
|
||||
row!(
|
||||
Tui::fg_bg(dark.rgb, darkest.rgb, "▐"),
|
||||
Tui::fg_bg(lighter.rgb, dark.rgb, Tui::bold(true, format!("{}", self.1.as_ref()))),
|
||||
Tui::fg_bg(dark.rgb, darkest.rgb, "▌"),
|
||||
Tui::fg_bg(lightest.rgb, darkest.rgb, format!("{} ", self.2.as_ref()))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FieldV<T, U>(pub ItemPalette, pub T, pub U)
|
||||
where T: AsRef<str> + Send + Sync, U: AsRef<str> + Send + Sync;
|
||||
|
||||
impl<T, U> Content<TuiOut> for FieldV<T, U>
|
||||
where T: AsRef<str> + Send + Sync, U: AsRef<str> + Send + Sync
|
||||
{
|
||||
fn content (&self) -> impl Render<TuiOut> {
|
||||
let ItemPalette { darkest, dark, lighter, lightest, .. } = self.0;
|
||||
let sep1 = Tui::bg(darkest.rgb, Tui::fg(dark.rgb, "▐"));
|
||||
let sep2 = Tui::bg(darkest.rgb, Tui::fg(dark.rgb, "▌"));
|
||||
let name = Tui::bg(dark.rgb, Tui::fg(lighter.rgb,
|
||||
Tui::bold(true, format!("{}", self.1.as_ref()))));
|
||||
let value = Tui::bg(darkest.rgb, Tui::fg(lightest.rgb,
|
||||
format!(" {} ", self.2.as_ref())));
|
||||
Bsp::e(Bsp::s(row!(sep1, name, sep2), value), " ")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy,Clone)]
|
||||
pub struct TuiTheme;
|
||||
|
||||
impl Theme for TuiTheme {}
|
||||
|
||||
pub trait Theme {
|
||||
const HOTKEY_FG: Color = Color::Rgb(255, 255, 0);
|
||||
fn null () -> Color {
|
||||
Color::Reset
|
||||
}
|
||||
fn bg0 () -> Color {
|
||||
Color::Rgb(20, 20, 20)
|
||||
}
|
||||
fn bg () -> Color {
|
||||
Color::Rgb(28, 35, 25)
|
||||
}
|
||||
fn border_bg () -> Color {
|
||||
Color::Rgb(40, 50, 30)
|
||||
}
|
||||
fn border_fg (focused: bool) -> Color {
|
||||
if focused { Self::bo1() } else { Self::bo2() }
|
||||
}
|
||||
fn title_fg (focused: bool) -> Color {
|
||||
if focused { Self::ti1() } else { Self::ti2() }
|
||||
}
|
||||
fn separator_fg (_: bool) -> Color {
|
||||
Color::Rgb(0, 0, 0)
|
||||
}
|
||||
fn mode_bg () -> Color {
|
||||
Color::Rgb(150, 160, 90)
|
||||
}
|
||||
fn mode_fg () -> Color {
|
||||
Color::Rgb(255, 255, 255)
|
||||
}
|
||||
fn status_bar_bg () -> Color {
|
||||
Color::Rgb(28, 35, 25)
|
||||
}
|
||||
fn bo1 () -> Color {
|
||||
Color::Rgb(100, 110, 40)
|
||||
}
|
||||
fn bo2 () -> Color {
|
||||
Color::Rgb(70, 80, 50)
|
||||
}
|
||||
fn ti1 () -> Color {
|
||||
Color::Rgb(150, 160, 90)
|
||||
}
|
||||
fn ti2 () -> Color {
|
||||
Color::Rgb(120, 130, 100)
|
||||
}
|
||||
fn orange () -> Color {
|
||||
Color::Rgb(255,128,0)
|
||||
}
|
||||
fn yellow () -> Color {
|
||||
Color::Rgb(255,255,0)
|
||||
}
|
||||
fn g (g: u8) -> Color {
|
||||
Color::Rgb(g, g, g)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Repeat<'a>(pub &'a str);
|
||||
|
||||
impl Content<TuiOut> for Repeat<'_> {
|
||||
|
|
@ -122,3 +217,338 @@ pub fn phat_sel_3 <T: Content<TuiOut>> (
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
pub trait TuiStyle {
|
||||
fn fg <R: Content<TuiOut>> (color: Color, w: R) -> Foreground<R> {
|
||||
Foreground(color, w)
|
||||
}
|
||||
fn bg <R: Content<TuiOut>> (color: Color, w: R) -> Background<R> {
|
||||
Background(color, w)
|
||||
}
|
||||
fn fg_bg <R: Content<TuiOut>> (fg: Color, bg: Color, w: R) -> Background<Foreground<R>> {
|
||||
Background(bg, Foreground(fg, w))
|
||||
}
|
||||
fn bold <R: Content<TuiOut>> (enable: bool, w: R) -> Bold<R> {
|
||||
Bold(enable, w)
|
||||
}
|
||||
fn border <R: Content<TuiOut>, S: BorderStyle> (enable: bool, style: S, w: R) -> Bordered<S, R> {
|
||||
Bordered(enable, style, w)
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiStyle for Tui {}
|
||||
|
||||
pub struct Bold<R: Content<TuiOut>>(pub bool, R);
|
||||
impl<R: Content<TuiOut>> Content<TuiOut> for Bold<R> {
|
||||
fn content (&self) -> impl Render<TuiOut> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.fill_bold(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Foreground<R: Content<TuiOut>>(pub Color, R);
|
||||
impl<R: Content<TuiOut>> Content<TuiOut> for Foreground<R> {
|
||||
fn content (&self) -> impl Render<TuiOut> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.fill_fg(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Background<R: Content<TuiOut>>(pub Color, R);
|
||||
impl<R: Content<TuiOut>> Content<TuiOut> for Background<R> {
|
||||
fn content (&self) -> impl Render<TuiOut> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.fill_bg(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Styled<R: Content<TuiOut>>(pub Option<Style>, pub R);
|
||||
impl<R: Content<TuiOut>> Content<TuiOut> for Styled<R> {
|
||||
fn content (&self) -> impl Render<TuiOut> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.place(self.content().layout(to.area()), &self.content());
|
||||
// TODO write style over area
|
||||
}
|
||||
}
|
||||
|
||||
//pub trait TuiStyle: Render<TuiOut> + Sized {
|
||||
//fn fg (self, color: Color) -> impl Render<TuiOut> {
|
||||
//Layers::new(move |add|{ add(&Foreground(color))?; add(&self) })
|
||||
//}
|
||||
//fn bg (self, color: Color) -> impl Render<TuiOut> {
|
||||
//Layers::new(move |add|{ add(&Background(color))?; add(&self) })
|
||||
//}
|
||||
//fn bold (self, on: bool) -> impl Render<TuiOut> {
|
||||
//Layers::new(move |add|{ add(&Bold(on))?; add(&self) })
|
||||
//}
|
||||
//fn border <S: BorderStyle> (self, style: S) -> impl Render<TuiOut> {
|
||||
//Bordered(style, self)
|
||||
//}
|
||||
//}
|
||||
|
||||
//impl<R: Content<TuiOut>> TuiStyle for R {}
|
||||
|
||||
//impl<S: BorderStyle> Content<TuiOut> for Border<S> {
|
||||
//}
|
||||
|
||||
//impl<S: BorderStyle, R: Content<TuiOut>> Content<TuiOut> for Bordered<S, R> {
|
||||
//fn content (&self) -> impl Render<TuiOut> {
|
||||
//let content: &dyn Content<TuiOut> = &self.1;
|
||||
//lay! { content.padding_xy(1, 1), Border(self.0) }.fill_xy()
|
||||
//}
|
||||
//}
|
||||
|
||||
pub struct Bordered<S: BorderStyle, W: Content<TuiOut>>(pub bool, pub S, pub W);
|
||||
content!(TuiOut: |self: Bordered<S: BorderStyle, W: Content<TuiOut>>|Fill::xy(
|
||||
lay!(When::new(self.0, Border(self.0, self.1)), Padding::xy(1, 1, &self.2))
|
||||
));
|
||||
|
||||
pub struct Border<S: BorderStyle>(pub bool, pub S);
|
||||
render!(TuiOut: |self: Border<S: BorderStyle>, 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 <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
|
||||
Bsp::b(Fill::xy(Border(self.enabled(), self)), w)
|
||||
}
|
||||
fn enclose2 <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
|
||||
Bsp::b(Margin::xy(1, 1, Fill::xy(Border(self.enabled(), self))), w)
|
||||
}
|
||||
fn enclose_bg <W: Content<TuiOut>> (self, w: W) -> impl Content<TuiOut> {
|
||||
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<Style>
|
||||
) -> Usually<[u16;4]> {
|
||||
let area = to.area();
|
||||
let style = style.or_else(||self.style_horizontal());
|
||||
let [x, x2, y, y2] = area.lrtb();
|
||||
for x in x..x2.saturating_sub(1) {
|
||||
to.blit(&Self::N, x, y, style);
|
||||
to.blit(&Self::S, x, y2.saturating_sub(1), style)
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
#[inline] fn draw_vertical (
|
||||
&self, to: &mut TuiOut, style: Option<Style>
|
||||
) -> Usually<[u16;4]> {
|
||||
let area = to.area();
|
||||
let style = style.or_else(||self.style_vertical());
|
||||
let [x, x2, y, y2] = area.lrtb();
|
||||
let h = y2 - y;
|
||||
if h > 1 {
|
||||
for y in y..y2.saturating_sub(1) {
|
||||
to.blit(&Self::W, x, y, style);
|
||||
to.blit(&Self::E, x2.saturating_sub(1), y, style);
|
||||
}
|
||||
} else if h > 0 {
|
||||
to.blit(&Self::W0, x, y, style);
|
||||
to.blit(&Self::E0, x2.saturating_sub(1), y, style);
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
#[inline] fn draw_corners (
|
||||
&self, to: &mut TuiOut, style: Option<Style>
|
||||
) -> Usually<[u16;4]> {
|
||||
let area = to.area();
|
||||
let style = style.or_else(||self.style_corners());
|
||||
let [x, y, width, height] = area.xywh();
|
||||
if width > 1 && height > 1 {
|
||||
to.blit(&Self::NW, x, y, style);
|
||||
to.blit(&Self::NE, x + width - 1, y, style);
|
||||
to.blit(&Self::SW, x, y + height - 1, style);
|
||||
to.blit(&Self::SE, x + width - 1, y + height - 1, style);
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
#[inline] fn style (&self) -> Option<Style> { None }
|
||||
#[inline] fn style_horizontal (&self) -> Option<Style> { self.style() }
|
||||
#[inline] fn style_vertical (&self) -> Option<Style> { self.style() }
|
||||
#[inline] fn style_corners (&self) -> Option<Style> { self.style() }
|
||||
}
|
||||
|
||||
macro_rules! border {
|
||||
($($T:ident {
|
||||
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
||||
$($x:tt)*
|
||||
}),+) => {$(
|
||||
impl BorderStyle for $T {
|
||||
const NW: &'static str = $nw;
|
||||
const N: &'static str = $n;
|
||||
const NE: &'static str = $ne;
|
||||
const W: &'static str = $w;
|
||||
const E: &'static str = $e;
|
||||
const SW: &'static str = $sw;
|
||||
const S: &'static str = $s;
|
||||
const SE: &'static str = $se;
|
||||
$($x)*
|
||||
fn enabled (&self) -> bool { false }
|
||||
}
|
||||
#[derive(Copy, Clone)] pub struct $T(pub bool, pub Style);
|
||||
impl Content<TuiOut> for $T {
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
if self.enabled() { let _ = self.draw(to); }
|
||||
}
|
||||
}
|
||||
)+}
|
||||
}
|
||||
|
||||
border! {
|
||||
Square {
|
||||
"┌" "─" "┐"
|
||||
"│" "│"
|
||||
"└" "─" "┘" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
SquareBold {
|
||||
"┏" "━" "┓"
|
||||
"┃" "┃"
|
||||
"┗" "━" "┛" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
TabLike {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"│" " " "│" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Lozenge {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"╰" "─" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Brace {
|
||||
"╭" "" "╮"
|
||||
"│" "│"
|
||||
"╰" "" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
LozengeDotted {
|
||||
"╭" "┅" "╮"
|
||||
"┇" "┇"
|
||||
"╰" "┅" "╯" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Quarter {
|
||||
"▎" "▔" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "▁" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
QuarterV {
|
||||
"▎" "" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "" "🮇" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Chamfer {
|
||||
"🭂" "▔" "🭍"
|
||||
"▎" "🮇"
|
||||
"🭓" "▁" "🭞" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Corners {
|
||||
"🬆" "" "🬊" // 🬴 🬸
|
||||
"" ""
|
||||
"🬱" "" "🬵" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
CornersTall {
|
||||
"🭽" "" "🭾"
|
||||
"" ""
|
||||
"🭼" "" "🭿" fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Outer {
|
||||
"🭽" "▔" "🭾"
|
||||
"▏" "▕"
|
||||
"🭼" "▁" "🭿"
|
||||
const W0: &'static str = "[";
|
||||
const E0: &'static str = "]";
|
||||
const N0: &'static str = "⎴";
|
||||
const S0: &'static str = "⎵";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Phat {
|
||||
"▄" "▄" "▄"
|
||||
"█" "█"
|
||||
"▀" "▀" "▀"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Rugged {
|
||||
"▄" "▂" "▄"
|
||||
"▐" "▌"
|
||||
"▀" "🮂" "▀"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Skinny {
|
||||
"▗" "▄" "▖"
|
||||
"▐" "▌"
|
||||
"▝" "▀" "▘"
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Brackets {
|
||||
"⎡" "" "⎤"
|
||||
"" ""
|
||||
"⎣" "" "⎦"
|
||||
const W0: &'static str = "[";
|
||||
const E0: &'static str = "]";
|
||||
const N0: &'static str = "⎴";
|
||||
const S0: &'static str = "⎵";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
},
|
||||
Reticle {
|
||||
"⎡" "" "⎤"
|
||||
"" ""
|
||||
"⎣" "" "⎦"
|
||||
const W0: &'static str = "╟";
|
||||
const E0: &'static str = "╢";
|
||||
const N0: &'static str = "┯";
|
||||
const S0: &'static str = "┷";
|
||||
fn style (&self) -> Option<Style> { Some(self.1) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::*;
|
||||
|
||||
use std::time::Duration;
|
||||
pub struct Tui {
|
||||
pub exited: Arc<AtomicBool>,
|
||||
pub backend: CrosstermBackend<Stdout>,
|
||||
|
|
@ -7,7 +7,6 @@ pub struct Tui {
|
|||
pub area: [u16;4],
|
||||
pub perf: PerfModel,
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
/// Construct a new TUI engine and wrap it for shared ownership.
|
||||
pub fn new () -> Usually<Arc<RwLock<Self>>> {
|
||||
|
|
@ -59,3 +58,25 @@ impl Tui {
|
|||
disable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
pub trait TuiRun<R: Render<TuiOut> + Handle<TuiIn> + 'static> {
|
||||
/// Run an app in the main loop.
|
||||
fn run (&self, state: &Arc<RwLock<R>>) -> Usually<()>;
|
||||
}
|
||||
impl<T: Render<TuiOut> + Handle<TuiIn> + 'static> TuiRun<T> for Arc<RwLock<Tui>> {
|
||||
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()> {
|
||||
let _input_thread = TuiIn::run_input(self, state, Duration::from_millis(100));
|
||||
self.write().unwrap().setup()?;
|
||||
let render_thread = TuiOut::run_output(self, state, Duration::from_millis(10));
|
||||
match render_thread.join() {
|
||||
Ok(result) => {
|
||||
self.write().unwrap().teardown()?;
|
||||
println!("\n\rRan successfully: {result:?}\n\r");
|
||||
},
|
||||
Err(error) => {
|
||||
self.write().unwrap().teardown()?;
|
||||
panic!("\n\rRender thread failed: {error:?}.\n\r")
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub struct Field<T, U>(pub ItemPalette, pub T, pub U)
|
||||
where T: AsRef<str> + Send + Sync, U: AsRef<str> + Send + Sync;
|
||||
|
||||
impl<T, U> Content<TuiOut> for Field<T, U>
|
||||
where T: AsRef<str> + Send + Sync, U: AsRef<str> + Send + Sync
|
||||
{
|
||||
fn content (&self) -> impl Render<TuiOut> {
|
||||
let ItemPalette { darkest, dark, lighter, lightest, .. } = self.0;
|
||||
row!(
|
||||
Tui::fg_bg(dark.rgb, darkest.rgb, "▐"),
|
||||
Tui::fg_bg(lighter.rgb, dark.rgb, Tui::bold(true, format!("{}", self.1.as_ref()))),
|
||||
Tui::fg_bg(dark.rgb, darkest.rgb, "▌"),
|
||||
Tui::fg_bg(lightest.rgb, darkest.rgb, format!("{} ", self.2.as_ref()))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FieldV<T, U>(pub ItemPalette, pub T, pub U)
|
||||
where T: AsRef<str> + Send + Sync, U: AsRef<str> + Send + Sync;
|
||||
|
||||
impl<T, U> Content<TuiOut> for FieldV<T, U>
|
||||
where T: AsRef<str> + Send + Sync, U: AsRef<str> + Send + Sync
|
||||
{
|
||||
fn content (&self) -> impl Render<TuiOut> {
|
||||
let ItemPalette { darkest, dark, lighter, lightest, .. } = self.0;
|
||||
let sep1 = Tui::bg(darkest.rgb, Tui::fg(dark.rgb, "▐"));
|
||||
let sep2 = Tui::bg(darkest.rgb, Tui::fg(dark.rgb, "▌"));
|
||||
let name = Tui::bg(dark.rgb, Tui::fg(lighter.rgb,
|
||||
Tui::bold(true, format!("{}", self.1.as_ref()))));
|
||||
let value = Tui::bg(darkest.rgb, Tui::fg(lightest.rgb,
|
||||
format!(" {} ", self.2.as_ref())));
|
||||
Bsp::e(Bsp::s(row!(sep1, name, sep2), value), " ")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
use crate::*;
|
||||
use std::time::Duration;
|
||||
pub trait TuiRun<R: Render<TuiOut> + Handle<TuiIn> + 'static> {
|
||||
/// Run an app in the main loop.
|
||||
fn run (&self, state: &Arc<RwLock<R>>) -> Usually<()>;
|
||||
}
|
||||
impl<T: Render<TuiOut> + Handle<TuiIn> + 'static> TuiRun<T> for Arc<RwLock<Tui>> {
|
||||
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()> {
|
||||
let _input_thread = TuiIn::run_input(self, state, Duration::from_millis(100));
|
||||
self.write().unwrap().setup()?;
|
||||
let render_thread = TuiOut::run_output(self, state, Duration::from_millis(10));
|
||||
match render_thread.join() {
|
||||
Ok(result) => {
|
||||
self.write().unwrap().teardown()?;
|
||||
println!("\n\rRan successfully: {result:?}\n\r");
|
||||
},
|
||||
Err(error) => {
|
||||
self.write().unwrap().teardown()?;
|
||||
panic!("\n\rRender thread failed: {error:?}.\n\r")
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait TuiStyle {
|
||||
fn fg <R: Content<TuiOut>> (color: Color, w: R) -> Foreground<R> {
|
||||
Foreground(color, w)
|
||||
}
|
||||
fn bg <R: Content<TuiOut>> (color: Color, w: R) -> Background<R> {
|
||||
Background(color, w)
|
||||
}
|
||||
fn fg_bg <R: Content<TuiOut>> (fg: Color, bg: Color, w: R) -> Background<Foreground<R>> {
|
||||
Background(bg, Foreground(fg, w))
|
||||
}
|
||||
fn bold <R: Content<TuiOut>> (enable: bool, w: R) -> Bold<R> {
|
||||
Bold(enable, w)
|
||||
}
|
||||
fn border <R: Content<TuiOut>, S: BorderStyle> (enable: bool, style: S, w: R) -> Bordered<S, R> {
|
||||
Bordered(enable, style, w)
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiStyle for Tui {}
|
||||
|
||||
pub struct Bold<R: Content<TuiOut>>(pub bool, R);
|
||||
impl<R: Content<TuiOut>> Content<TuiOut> for Bold<R> {
|
||||
fn content (&self) -> impl Render<TuiOut> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.fill_bold(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Foreground<R: Content<TuiOut>>(pub Color, R);
|
||||
impl<R: Content<TuiOut>> Content<TuiOut> for Foreground<R> {
|
||||
fn content (&self) -> impl Render<TuiOut> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.fill_fg(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Background<R: Content<TuiOut>>(pub Color, R);
|
||||
impl<R: Content<TuiOut>> Content<TuiOut> for Background<R> {
|
||||
fn content (&self) -> impl Render<TuiOut> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.fill_bg(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Styled<R: Content<TuiOut>>(pub Option<Style>, pub R);
|
||||
impl<R: Content<TuiOut>> Content<TuiOut> for Styled<R> {
|
||||
fn content (&self) -> impl Render<TuiOut> { &self.1 }
|
||||
fn render (&self, to: &mut TuiOut) {
|
||||
to.place(self.content().layout(to.area()), &self.content());
|
||||
// TODO write style over area
|
||||
}
|
||||
}
|
||||
|
||||
//pub trait TuiStyle: Render<TuiOut> + Sized {
|
||||
//fn fg (self, color: Color) -> impl Render<TuiOut> {
|
||||
//Layers::new(move |add|{ add(&Foreground(color))?; add(&self) })
|
||||
//}
|
||||
//fn bg (self, color: Color) -> impl Render<TuiOut> {
|
||||
//Layers::new(move |add|{ add(&Background(color))?; add(&self) })
|
||||
//}
|
||||
//fn bold (self, on: bool) -> impl Render<TuiOut> {
|
||||
//Layers::new(move |add|{ add(&Bold(on))?; add(&self) })
|
||||
//}
|
||||
//fn border <S: BorderStyle> (self, style: S) -> impl Render<TuiOut> {
|
||||
//Bordered(style, self)
|
||||
//}
|
||||
//}
|
||||
|
||||
//impl<R: Content<TuiOut>> TuiStyle for R {}
|
||||
|
||||
//impl<S: BorderStyle> Content<TuiOut> for Border<S> {
|
||||
//}
|
||||
|
||||
//impl<S: BorderStyle, R: Content<TuiOut>> Content<TuiOut> for Bordered<S, R> {
|
||||
//fn content (&self) -> impl Render<TuiOut> {
|
||||
//let content: &dyn Content<TuiOut> = &self.1;
|
||||
//lay! { content.padding_xy(1, 1), Border(self.0) }.fill_xy()
|
||||
//}
|
||||
//}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
#[derive(Copy,Clone)]
|
||||
pub struct TuiTheme;
|
||||
|
||||
impl Theme for TuiTheme {}
|
||||
|
||||
pub trait Theme {
|
||||
const HOTKEY_FG: Color = Color::Rgb(255, 255, 0);
|
||||
fn null () -> Color {
|
||||
Color::Reset
|
||||
}
|
||||
fn bg0 () -> Color {
|
||||
Color::Rgb(20, 20, 20)
|
||||
}
|
||||
fn bg () -> Color {
|
||||
Color::Rgb(28, 35, 25)
|
||||
}
|
||||
fn border_bg () -> Color {
|
||||
Color::Rgb(40, 50, 30)
|
||||
}
|
||||
fn border_fg (focused: bool) -> Color {
|
||||
if focused { Self::bo1() } else { Self::bo2() }
|
||||
}
|
||||
fn title_fg (focused: bool) -> Color {
|
||||
if focused { Self::ti1() } else { Self::ti2() }
|
||||
}
|
||||
fn separator_fg (_: bool) -> Color {
|
||||
Color::Rgb(0, 0, 0)
|
||||
}
|
||||
fn mode_bg () -> Color {
|
||||
Color::Rgb(150, 160, 90)
|
||||
}
|
||||
fn mode_fg () -> Color {
|
||||
Color::Rgb(255, 255, 255)
|
||||
}
|
||||
fn status_bar_bg () -> Color {
|
||||
Color::Rgb(28, 35, 25)
|
||||
}
|
||||
fn bo1 () -> Color {
|
||||
Color::Rgb(100, 110, 40)
|
||||
}
|
||||
fn bo2 () -> Color {
|
||||
Color::Rgb(70, 80, 50)
|
||||
}
|
||||
fn ti1 () -> Color {
|
||||
Color::Rgb(150, 160, 90)
|
||||
}
|
||||
fn ti2 () -> Color {
|
||||
Color::Rgb(120, 130, 100)
|
||||
}
|
||||
fn orange () -> Color {
|
||||
Color::Rgb(255,128,0)
|
||||
}
|
||||
fn yellow () -> Color {
|
||||
Color::Rgb(255,255,0)
|
||||
}
|
||||
fn g (g: u8) -> Color {
|
||||
Color::Rgb(g, g, g)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue