mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-01 08:36:42 +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) }
|
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
|
||||||
}
|
}
|
||||||
edn_command!(MidiEditCommand: |state: MidiEditor| {
|
edn_command!(MidiEditCommand: |state: MidiEditor| {
|
||||||
("note/put" [_a: bool] Self::PutNote)
|
("note/put" [_a: bool] Self::PutNote)
|
||||||
("note/del" [_a: bool] Self::PutNote)
|
("note/del" [_a: bool] Self::PutNote)
|
||||||
("note/pos" [a: usize] Self::SetNoteCursor(a.expect("no note cursor")))
|
("note/pos" [a: usize] Self::SetNoteCursor(a.expect("no note cursor")))
|
||||||
("note/len" [a: usize] Self::SetNoteLength(a.expect("no note length")))
|
("note/len" [a: usize] Self::SetNoteLength(a.expect("no note length")))
|
||||||
("time/pos" [a: usize] Self::SetTimeCursor(a.expect("no time cursor")))
|
("time/pos" [a: usize] Self::SetTimeCursor(a.expect("no time cursor")))
|
||||||
|
|
|
||||||
|
|
@ -479,8 +479,8 @@ edn_command!(FileBrowserCommand: |state: MidiPool| {
|
||||||
("begin" [] Self::Begin)
|
("begin" [] Self::Begin)
|
||||||
("cancel" [] Self::Cancel)
|
("cancel" [] Self::Cancel)
|
||||||
("confirm" [] Self::Confirm)
|
("confirm" [] Self::Confirm)
|
||||||
("select" [i: usize] Self::Select(i.expect("no index")))
|
("select" [i: usize] Self::Select(i.expect("no index")))
|
||||||
("chdir" [p: PathBuf] Self::Chdir(p.expect("no path")))
|
("chdir" [p: PathBuf] Self::Chdir(p.expect("no path")))
|
||||||
("filter" [f: Arc<str>] Self::Filter(f.expect("no filter")))
|
("filter" [f: Arc<str>] Self::Filter(f.expect("no filter")))
|
||||||
});
|
});
|
||||||
command!(|self: FileBrowserCommand, state: MidiPool|{
|
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; 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_jack::{*, jack::*};
|
||||||
pub(crate) use ::tek_midi::{*, midly::{*, live::*, num::*}};
|
pub(crate) use ::tek_midi::{*, midly::{*, live::*, num::*}};
|
||||||
pub(crate) use ::tek_tui::{
|
pub(crate) use ::tek_tui::*;
|
||||||
*,
|
pub(crate) use ::tek_tui::tek_output::*;
|
||||||
tek_output::*,
|
pub(crate) use ::tek_tui::tek_input::*;
|
||||||
tek_input::*,
|
pub(crate) use ::tek_tui::tek_edn::*;
|
||||||
tek_edn::*,
|
pub(crate) use ::tek_tui::ratatui::prelude::*;
|
||||||
ratatui::prelude::*,
|
pub(crate) use ::tek_tui::crossterm::event::*;
|
||||||
crossterm::event::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}};
|
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}};
|
||||||
pub(crate) use std::fs::File;
|
pub(crate) use std::fs::File;
|
||||||
pub(crate) use std::path::PathBuf;
|
pub(crate) use std::path::PathBuf;
|
||||||
pub(crate) use std::error::Error;
|
pub(crate) use std::error::Error;
|
||||||
pub(crate) use std::ffi::OsString;
|
pub(crate) use std::ffi::OsString;
|
||||||
pub(crate) use KeyCode::Char;
|
|
||||||
pub(crate) use symphonia::{
|
pub(crate) use symphonia::{
|
||||||
core::{
|
core::{
|
||||||
formats::Packet,
|
formats::Packet,
|
||||||
|
|
@ -31,8 +23,8 @@ pub(crate) use symphonia::{
|
||||||
},
|
},
|
||||||
default::get_codecs,
|
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 () {
|
#[cfg(test)] #[test] fn test_sampler () {
|
||||||
// TODO!
|
// TODO!
|
||||||
|
let sample = Sample::new("test", 0, 0, vec![]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,35 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
pub trait HasSampler {
|
||||||
/// The sampler plugin plays sounds.
|
fn sampler (&self) -> &Option<Sampler>;
|
||||||
#[derive(Debug)]
|
fn sampler_mut (&mut self) -> &mut Option<Sampler>;
|
||||||
pub struct 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 jack: Arc<RwLock<JackConnection>>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub mapped: [Option<Arc<RwLock<Sample>>>;128],
|
pub mapped: [Option<Arc<RwLock<Sample>>>;128],
|
||||||
|
|
@ -16,6 +43,32 @@ pub struct Sampler {
|
||||||
pub buffer: Vec<Vec<f32>>,
|
pub buffer: Vec<Vec<f32>>,
|
||||||
pub output_gain: 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 {
|
impl Default for Sampler {
|
||||||
fn default () -> Self {
|
fn default () -> Self {
|
||||||
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 {
|
impl Sample {
|
||||||
pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
|
pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
|
||||||
Self { name: name.as_ref().into(), start, end, channels, rate: None, gain: 1.0 }
|
Self { name: name.as_ref().into(), start, end, channels, rate: None, gain: 1.0 }
|
||||||
|
|
@ -385,16 +247,168 @@ impl Sample {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
audio!(|self: SamplerTui, client, scope|SamplerAudio(&mut self.state).process(client, scope));
|
||||||
/// A currently playing instance of a sample.
|
pub struct SamplerAudio<'a>(pub &'a mut Sampler);
|
||||||
#[derive(Default, Debug, Clone)]
|
audio!(|self: SamplerAudio<'a>, _client, scope|{
|
||||||
pub struct Voice {
|
self.0.process_midi_in(scope);
|
||||||
pub sample: Arc<RwLock<Sample>>,
|
self.0.clear_output_buffer();
|
||||||
pub after: usize,
|
self.0.process_audio_out(scope);
|
||||||
pub position: usize,
|
self.0.write_output_buffer(scope);
|
||||||
pub velocity: f32,
|
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 {
|
impl Iterator for Voice {
|
||||||
type Item = [f32;2];
|
type Item = [f32;2];
|
||||||
fn next (&mut self) -> Option<Self::Item> {
|
fn next (&mut self) -> Option<Self::Item> {
|
||||||
|
|
@ -414,7 +428,6 @@ impl Iterator for Voice {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AddSampleModal {
|
pub struct AddSampleModal {
|
||||||
exited: bool,
|
exited: bool,
|
||||||
dir: PathBuf,
|
dir: PathBuf,
|
||||||
|
|
@ -426,7 +439,6 @@ pub struct AddSampleModal {
|
||||||
voices: Arc<RwLock<Vec<Voice>>>,
|
voices: Arc<RwLock<Vec<Voice>>>,
|
||||||
_search: Option<String>,
|
_search: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AddSampleModal {
|
impl AddSampleModal {
|
||||||
fn exited (&self) -> bool {
|
fn exited (&self) -> bool {
|
||||||
self.exited
|
self.exited
|
||||||
|
|
@ -435,7 +447,6 @@ impl AddSampleModal {
|
||||||
self.exited = true
|
self.exited = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AddSampleModal {
|
impl AddSampleModal {
|
||||||
pub fn new (
|
pub fn new (
|
||||||
sample: &Arc<RwLock<Sample>>,
|
sample: &Arc<RwLock<Sample>>,
|
||||||
|
|
@ -536,11 +547,9 @@ impl AddSampleModal {
|
||||||
return Ok(false)
|
return Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||||||
todo!();
|
todo!();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||||||
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
||||||
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
||||||
|
|
@ -557,7 +566,6 @@ fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||||||
files.sort();
|
files.sort();
|
||||||
Ok((subdirs, files))
|
Ok((subdirs, files))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_sample (
|
fn draw_sample (
|
||||||
to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
|
to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
|
||||||
) -> Usually<usize> {
|
) -> Usually<usize> {
|
||||||
|
|
@ -575,7 +583,6 @@ fn draw_sample (
|
||||||
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
|
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
|
||||||
Ok(label1.len() + label2.len() + 4)
|
Ok(label1.len() + label2.len() + 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Content<TuiOut> for AddSampleModal {
|
impl Content<TuiOut> for AddSampleModal {
|
||||||
fn render (&self, to: &mut TuiOut) {
|
fn render (&self, to: &mut TuiOut) {
|
||||||
todo!()
|
todo!()
|
||||||
|
|
@ -612,7 +619,6 @@ impl Content<TuiOut> for AddSampleModal {
|
||||||
//Lozenge(Style::default()).draw(to)
|
//Lozenge(Style::default()).draw(to)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//impl Handle<TuiIn> for AddSampleModal {
|
//impl Handle<TuiIn> for AddSampleModal {
|
||||||
//fn handle (&mut self, from: &TuiIn) -> Perhaps<bool> {
|
//fn handle (&mut self, from: &TuiIn) -> Perhaps<bool> {
|
||||||
//if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? {
|
//if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? {
|
||||||
|
|
@ -621,7 +627,6 @@ impl Content<TuiOut> for AddSampleModal {
|
||||||
//Ok(Some(true))
|
//Ok(Some(true))
|
||||||
//}
|
//}
|
||||||
//}
|
//}
|
||||||
|
|
||||||
//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding<AddSampleModal>] = keymap!(AddSampleModal {
|
//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding<AddSampleModal>] = keymap!(AddSampleModal {
|
||||||
//[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{
|
//[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{
|
||||||
//modal.exit();
|
//modal.exit();
|
||||||
|
|
@ -646,9 +651,288 @@ impl Content<TuiOut> for AddSampleModal {
|
||||||
//Ok(true)
|
//Ok(true)
|
||||||
//}]
|
//}]
|
||||||
//});
|
//});
|
||||||
|
|
||||||
|
|
||||||
pub enum SamplerMode {
|
pub enum SamplerMode {
|
||||||
// Load sample from path
|
// Load sample from path
|
||||||
Import(usize, FileBrowser),
|
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 right_tos = PortConnection::collect(&self.right_to, empty, empty);
|
||||||
let audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
|
let audio_froms = &[left_froms.as_slice(), right_froms.as_slice()];
|
||||||
let audio_tos = &[left_tos.as_slice(), right_tos.as_slice() ];
|
let audio_tos = &[left_tos.as_slice(), right_tos.as_slice() ];
|
||||||
Ok(match self.mode {
|
engine.run(&jack.activate_with(|jack|match self.mode {
|
||||||
TekMode::Clock =>
|
TekMode::Clock => Tek::new_clock(
|
||||||
engine.run(&jack.activate_with(|jack|Tek::new_clock(
|
jack, self.bpm, self.sync_lead, self.sync_follow,
|
||||||
jack, self.bpm, self.sync_lead, self.sync_follow,
|
&midi_froms, &midi_tos),
|
||||||
&midi_froms, &midi_tos))?)?,
|
TekMode::Sequencer => Tek::new_sequencer(
|
||||||
TekMode::Sequencer =>
|
jack, self.bpm, self.sync_lead, self.sync_follow,
|
||||||
engine.run(&jack.activate_with(|jack|Tek::new_sequencer(
|
&midi_froms, &midi_tos),
|
||||||
jack, self.bpm, self.sync_lead, self.sync_follow,
|
TekMode::Groovebox => Tek::new_groovebox(
|
||||||
&midi_froms, &midi_tos))?)?,
|
jack, self.bpm, self.sync_lead, self.sync_follow,
|
||||||
TekMode::Groovebox =>
|
&midi_froms, &midi_tos,
|
||||||
engine.run(&jack.activate_with(|jack|Tek::new_groovebox(
|
&audio_froms, &audio_tos),
|
||||||
jack, self.bpm, self.sync_lead, self.sync_follow,
|
TekMode::Arranger { scenes, tracks, track_width, .. } => Tek::new_arranger(
|
||||||
&midi_froms, &midi_tos,
|
jack, self.bpm, self.sync_lead, self.sync_follow,
|
||||||
&audio_froms, &audio_tos))?)?,
|
&midi_froms, &midi_tos,
|
||||||
TekMode::Arranger { scenes, tracks, track_width, .. } =>
|
&audio_froms, &audio_tos,
|
||||||
engine.run(&jack.activate_with(|jack|Tek::new_arranger(
|
scenes, tracks, track_width),
|
||||||
jack, self.bpm, self.sync_lead, self.sync_follow,
|
|
||||||
&midi_froms, &midi_tos,
|
|
||||||
&audio_froms, &audio_tos,
|
|
||||||
scenes, tracks, track_width
|
|
||||||
))?)?,
|
|
||||||
_ => todo!()
|
_ => todo!()
|
||||||
})
|
})?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Default, Debug)] struct Tek {
|
#[derive(Default, Debug)] struct Tek {
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,32 @@
|
||||||
pub use ::tek_input;
|
mod tui_buffer; pub use self::tui_buffer::*;
|
||||||
pub(crate) use tek_input::*;
|
mod tui_color; pub use self::tui_color::*;
|
||||||
|
|
||||||
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_content; pub use self::tui_content::*;
|
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_input; pub use self::tui_input::*;
|
||||||
mod tui_output; pub use self::tui_output::*;
|
mod tui_output; pub use self::tui_output::*;
|
||||||
mod tui_run; pub use self::tui_run::*;
|
pub use ::tek_edn; pub(crate) use ::tek_edn::*;
|
||||||
|
pub use ::tek_time; pub(crate) use ::tek_time::*;
|
||||||
mod tui_color; pub use self::tui_color::*;
|
pub use ::tek_input; pub(crate) use tek_input::*;
|
||||||
mod tui_style; pub use self::tui_style::*;
|
pub use ::tek_output; pub(crate) use tek_output::*;
|
||||||
mod tui_theme; pub use self::tui_theme::*;
|
pub use ::better_panic; pub(crate) use better_panic::{Settings, Verbosity};
|
||||||
mod tui_border; pub use self::tui_border::*;
|
pub use ::palette; pub(crate) use ::palette::{*, convert::*, okhsl::*};
|
||||||
mod tui_field; pub use self::tui_field::*;
|
pub use ::crossterm; pub(crate) use crossterm::{
|
||||||
mod tui_buffer; pub use self::tui_buffer::*;
|
ExecutableCommand,
|
||||||
mod tui_file; pub use self::tui_file::*;
|
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::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}};
|
||||||
pub(crate) use std::io::{stdout, Stdout};
|
pub(crate) use std::io::{stdout, Stdout};
|
||||||
pub(crate) use std::path::PathBuf;
|
pub(crate) use std::path::PathBuf;
|
||||||
pub(crate) use std::ffi::OsString;
|
pub(crate) use std::ffi::OsString;
|
||||||
|
|
||||||
/// Prototypal case of implementor macro.
|
|
||||||
/// Saves 4loc per data pats.
|
|
||||||
#[macro_export] macro_rules! from {
|
#[macro_export] macro_rules! from {
|
||||||
($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => {
|
($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => {
|
||||||
impl $(<$($lt),+>)? From<$Source> for $Target {
|
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<()> {
|
#[cfg(test)] #[test] fn test_tui_engine () -> Usually<()> {
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use std::sync::{Arc, RwLock};
|
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::*;
|
use crate::*;
|
||||||
|
|
||||||
pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||||
for row in 0..area.h() {
|
for row in 0..area.h() {
|
||||||
let y = area.y() + row;
|
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 width: usize,
|
||||||
pub height: usize,
|
pub height: usize,
|
||||||
pub content: Vec<Cell>
|
pub content: Vec<Cell>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BigBuffer {
|
impl BigBuffer {
|
||||||
pub fn new (width: usize, height: usize) -> Self {
|
pub fn new (width: usize, height: usize) -> Self {
|
||||||
Self { width, height, content: vec![Cell::default(); width*height] }
|
Self { width, height, content: vec![Cell::default(); width*height] }
|
||||||
|
|
@ -35,5 +31,4 @@ impl BigBuffer {
|
||||||
y * self.width + x
|
y * self.width + x
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
from!(|size:(usize, usize)| BigBuffer = Self::new(size.0, size.1));
|
from!(|size:(usize, usize)| BigBuffer = Self::new(size.0, size.1));
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use rand::{thread_rng, distributions::uniform::UniformSampler};
|
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 {
|
#[macro_export] macro_rules! has_color {
|
||||||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||||||
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
|
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
|
||||||
|
|
@ -12,16 +8,13 @@ pub trait HasColor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A color in OKHSL and RGB representations.
|
/// A color in OKHSL and RGB representations.
|
||||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemColor {
|
||||||
pub struct ItemColor {
|
|
||||||
pub okhsl: Okhsl<f32>,
|
pub okhsl: Okhsl<f32>,
|
||||||
pub rgb: Color,
|
pub rgb: Color,
|
||||||
}
|
}
|
||||||
/// A color in OKHSL and RGB with lighter and darker variants.
|
/// A color in OKHSL and RGB with lighter and darker variants.
|
||||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
#[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct ItemPalette {
|
||||||
pub struct ItemPalette {
|
|
||||||
pub base: ItemColor,
|
pub base: ItemColor,
|
||||||
pub light: ItemColor,
|
pub light: ItemColor,
|
||||||
pub lighter: ItemColor,
|
pub lighter: ItemColor,
|
||||||
|
|
@ -62,7 +55,6 @@ from!(|base: ItemColor|ItemPalette = {
|
||||||
lighter.lightness = (lighter.lightness * 1.3).min(Okhsl::<f32>::max_lightness());
|
lighter.lightness = (lighter.lightness * 1.3).min(Okhsl::<f32>::max_lightness());
|
||||||
let mut lightest = base.okhsl;
|
let mut lightest = base.okhsl;
|
||||||
lightest.lightness = 0.95;
|
lightest.lightness = 0.95;
|
||||||
|
|
||||||
let mut dark = base.okhsl;
|
let mut dark = base.okhsl;
|
||||||
dark.lightness = (dark.lightness * 0.75).max(Okhsl::<f32>::min_lightness());
|
dark.lightness = (dark.lightness * 0.75).max(Okhsl::<f32>::min_lightness());
|
||||||
dark.saturation = (dark.saturation * 0.75).max(Okhsl::<f32>::min_saturation());
|
dark.saturation = (dark.saturation * 0.75).max(Okhsl::<f32>::min_saturation());
|
||||||
|
|
@ -72,7 +64,6 @@ from!(|base: ItemColor|ItemPalette = {
|
||||||
let mut darkest = darker;
|
let mut darkest = darker;
|
||||||
darkest.lightness = 0.1;
|
darkest.lightness = 0.1;
|
||||||
darkest.saturation = (darkest.saturation * 0.50).max(Okhsl::<f32>::min_saturation());
|
darkest.saturation = (darkest.saturation * 0.50).max(Okhsl::<f32>::min_saturation());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
base,
|
base,
|
||||||
light: light.into(),
|
light: light.into(),
|
||||||
|
|
@ -84,9 +75,7 @@ from!(|base: ItemColor|ItemPalette = {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
impl ItemPalette {
|
impl ItemPalette {
|
||||||
pub fn random () -> Self {
|
pub fn random () -> Self { ItemColor::random().into() }
|
||||||
ItemColor::random().into()
|
|
||||||
}
|
|
||||||
pub fn random_near (color: Self, distance: f32) -> Self {
|
pub fn random_near (color: Self, distance: f32) -> Self {
|
||||||
color.base.mix(ItemColor::random(), distance).into()
|
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);
|
pub struct Repeat<'a>(pub &'a str);
|
||||||
|
|
||||||
impl Content<TuiOut> for Repeat<'_> {
|
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 crate::*;
|
||||||
|
use std::time::Duration;
|
||||||
pub struct Tui {
|
pub struct Tui {
|
||||||
pub exited: Arc<AtomicBool>,
|
pub exited: Arc<AtomicBool>,
|
||||||
pub backend: CrosstermBackend<Stdout>,
|
pub backend: CrosstermBackend<Stdout>,
|
||||||
|
|
@ -7,7 +7,6 @@ pub struct Tui {
|
||||||
pub area: [u16;4],
|
pub area: [u16;4],
|
||||||
pub perf: PerfModel,
|
pub perf: PerfModel,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tui {
|
impl Tui {
|
||||||
/// Construct a new TUI engine and wrap it for shared ownership.
|
/// Construct a new TUI engine and wrap it for shared ownership.
|
||||||
pub fn new () -> Usually<Arc<RwLock<Self>>> {
|
pub fn new () -> Usually<Arc<RwLock<Self>>> {
|
||||||
|
|
@ -59,3 +58,25 @@ impl Tui {
|
||||||
disable_raw_mode().map_err(Into::into)
|
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