tek/app/tek.rs
2026-02-21 04:08:56 +02:00

1337 lines
48 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![allow(clippy::unit_arg)]
#![feature(adt_const_params,
associated_type_defaults,
closure_lifetime_binder,
if_let_guard,
impl_trait_in_assoc_type,
trait_alias,
type_alias_impl_trait,
type_changing_struct_update)]
#[cfg(test)] mod tek_test;
mod tek_struct;
pub use self::tek_struct::*;
mod tek_trait;
pub use self::tek_trait::*;
mod tek_type;
pub use self::tek_type::*;
mod tek_impls;
pub(crate) use ConnectName::*;
pub(crate) use ConnectScope::*;
pub(crate) use ConnectStatus::*;
pub(crate) use JackState::*;
#[allow(unused)] pub(crate) use ::{
std::{
cmp::Ord,
collections::BTreeMap,
error::Error,
ffi::OsString,
fmt::{Write, Debug, Formatter},
fs::File,
ops::{Add, Sub, Mul, Div, Rem},
path::{Path, PathBuf},
sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}},
thread::{spawn, JoinHandle},
},
xdg::BaseDirectories,
atomic_float::*,
tengri::tui::ratatui::{
self,
prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}},
widgets::{Widget, canvas::{Canvas, Line}},
},
tengri::tui::crossterm::{
self,
event::{Event, KeyEvent, KeyCode::{self, *}},
},
};
pub extern crate tengri;
pub(crate) use tengri::{*, input::*, output::*, tui::*};
pub extern crate jack;
pub(crate) use ::jack::{*, contrib::{*, ClosureProcessHandler}};
pub extern crate midly;
pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*};
pub extern crate atomic_float;
pub(crate) use atomic_float::AtomicF64;
#[cfg(feature = "sampler")] pub(crate) use symphonia::{
core::{
formats::Packet,
codecs::{Decoder, CODEC_TYPE_NULL},
//errors::Error as SymphoniaError,
io::MediaSourceStream,
probe::Hint,
audio::SampleBuffer,
},
default::get_codecs,
};
#[cfg(feature = "lv2_gui")] use ::winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
window::{Window, WindowId},
platform::x11::EventLoopBuilderExtX11
};
/// Command-line entrypoint.
#[cfg(feature = "cli")] pub fn main () -> Usually<()> {
use clap::Parser;
Cli::parse().run()
}
/// Create a new application from a backend, project, config, and mode
///
/// ```
/// let jack = tek::Jack::new(&"test_tek").expect("failed to connect to jack");
/// let proj = Default::default();
/// let conf = Default::default();
/// let tek = tek::tek(&jack, proj, conf, "");
/// ```
pub fn tek (
jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef<str>
) -> App {
App {
color: ItemTheme::random(),
dialog: Dialog::welcome(),
jack: jack.clone(),
mode: config.get_mode(mode).expect("failed to find mode"),
config,
project,
..Default::default()
}
}
fn tek_confirm (state: &mut App) -> Perhaps<AppCommand> {
Ok(match &state.dialog {
Dialog::Menu(index, items) => {
let callback = items.0[*index].1.clone();
callback(state)?;
None
},
_ => todo!(),
})
}
fn tek_inc (state: &mut App, axis: &ControlAxis) -> Perhaps<AppCommand> {
Ok(match (&state.dialog, axis) {
(Dialog::None, _) => todo!(),
(Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_next() }
.execute(state)?,
_ => todo!()
})
}
fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps<AppCommand> {
Ok(match (&state.dialog, axis) {
(Dialog::None, _) => None,
(Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_prev() }
.execute(state)?,
_ => todo!()
})
}
pub fn load_view (views: &Views, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into());
Ok(())
}
pub fn load_mode (modes: &Modes, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
let mut mode = Mode::default();
body.each(|item|mode.add(item))?;
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
Ok(())
}
pub fn load_bind (binds: &Binds, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
let mut map = Bind::new();
body.each(|item|if item.expr().head() == Ok(Some("see")) {
// TODO
Ok(())
} else if let Ok(Some(_word)) = item.expr().head().word() {
if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? {
map.add(key, Binding {
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
condition: None,
description: None,
source: None
});
Ok(())
} else if Some(":char") == item.expr()?.head()? {
// TODO
return Ok(())
} else {
return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into())
}
} else {
return Err(format!("Config::load_bind: unexpected: {item:?}").into())
})?;
binds.write().unwrap().insert(name.as_ref().into(), map);
Ok(())
}
/// CLI banner.
pub(crate) const HEADER: &'static str = r#"
~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
█ █▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~
~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
fn collect_commands (
app: &App, input: &TuiIn
) -> Usually<Vec<AppCommand>> {
let mut commands = vec![];
for id in app.mode.keys.iter() {
if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref())
&& let Some(bindings) = event_map.query(input.event()) {
for binding in bindings {
for command in binding.commands.iter() {
if let Some(command) = app.namespace(command)? as Option<AppCommand> {
commands.push(command)
}
}
}
}
}
Ok(commands)
}
fn execute_commands (
app: &mut App, commands: Vec<AppCommand>
) -> Usually<Vec<(AppCommand, Option<AppCommand>)>> {
let mut history = vec![];
for command in commands.into_iter() {
let result = command.execute(app);
match result { Err(err) => { history.push((command, None)); return Err(err) }
Ok(undo) => { history.push((command, undo)); } };
}
Ok(history)
}
pub fn tek_jack_process (app: &mut App, client: &Client, scope: &ProcessScope) -> Control {
let t0 = app.perf.get_t0();
app.clock().update_from_scope(scope).unwrap();
let midi_in = app.project.midi_input_collect(scope);
if let Some(editor) = &app.editor() {
let mut pitch: Option<u7> = None;
for port in midi_in.iter() {
for event in port.iter() {
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..}))
= event
{
pitch = Some(key.clone());
}
}
}
if let Some(pitch) = pitch {
editor.set_note_pos(pitch.as_int() as usize);
}
}
let result = app.project.process_tracks(client, scope);
app.perf.update_from_jack_scope(t0, scope);
result
}
pub fn tek_jack_event (app: &mut App, event: JackEvent) {
use JackEvent::*;
match event {
SampleRate(sr) => { app.clock().timebase.sr.set(sr as f64); },
PortRegistration(_id, true) => {
//let port = app.jack().port_by_id(id);
//println!("\rport add: {id} {port:?}");
//println!("\rport add: {id}");
},
PortRegistration(_id, false) => {
/*println!("\rport del: {id}")*/
},
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
ClientRegistration(_id, true) => {},
ClientRegistration(_id, false) => {},
ThreadInit => {},
XRun => {},
GraphReorder => {},
_ => { panic!("{event:?}"); }
}
}
pub fn tek_show_version () {
println!("todo version");
}
pub fn tek_print_config (config: &Config) {
use ::ansi_term::Color::*;
println!("{:?}", config.dirs);
for (k, v) in config.views.read().unwrap().iter() {
println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")));
}
for (k, v) in config.binds.read().unwrap().iter() {
println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}")));
for (k, v) in v.0.iter() {
print!("{} ", &Yellow.paint(match &k.0 {
Event::Key(KeyEvent { modifiers, .. }) =>
format!("{:>16}", format!("{modifiers}")),
_ => unimplemented!()
}));
print!("{}", &Yellow.bold().paint(match &k.0 {
Event::Key(KeyEvent { code, .. }) =>
format!("{:<10}", format!("{code}")),
_ => unimplemented!()
}));
for v in v.iter() {
print!(" => {:?}", v.commands);
print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default());
println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default());
//println!(" {:?}", v.source);
}
}
}
for (k, v) in config.modes.read().unwrap().iter() {
println!();
for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); }
for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); }
print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}")));
print!("\n{}", Blue.paint("KEYS"));
for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); }
println!();
for (k, v) in v.modes.read().unwrap().iter() {
print!("{} {} {:?}",
Blue.paint("MODE"),
Green.bold().paint(format!("{k:<16}")),
v.name);
print!(" INFO={:?}",
v.info);
print!(" VIEW={:?}",
v.view);
println!(" KEYS={:?}",
v.keys);
}
print!("{}", Blue.paint("VIEW"));
for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); }
println!();
}
}
pub fn tek_print_status (project: &Arrangement) {
println!("Name: {:?}", &project.name);
println!("JACK: {:?}", &project.jack);
println!("Buffer: {:?}", &project.clock.chunk);
println!("Sample rate: {:?}", &project.clock.timebase.sr);
println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq);
println!("Tempo: {:?}", &project.clock.timebase.bpm);
println!("Quantize: {:?}", &project.clock.quant);
println!("Launch: {:?}", &project.clock.sync);
println!("Playhead: {:?}us", &project.clock.playhead.usec);
println!("Playhead: {:?}s", &project.clock.playhead.sample);
println!("Playhead: {:?}p", &project.clock.playhead.pulse);
println!("Started: {:?}", &project.clock.started);
println!("Tracks:");
for (i, t) in project.tracks.iter().enumerate() {
println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width,
&t.sequencer.play_clip, &t.sequencer.next_clip);
}
println!("Scenes:");
for (i, t) in project.scenes.iter().enumerate() {
println!(" Scene {i}: {} {:?}", &t.name, &t.clips);
}
println!("MIDI Ins: {:?}", &project.midi_ins);
println!("MIDI Outs: {:?}", &project.midi_outs);
println!("Audio Ins: {:?}", &project.audio_ins);
println!("Audio Outs: {:?}", &project.audio_outs);
// TODO git integration
// TODO dawvert integration
}
pub const DEFAULT_PPQ: f64 = 96.0;
/// FIXME: remove this and use PPQ from timebase everywhere:
pub const PPQ: usize = 96;
/// (pulses, name), assuming 96 PPQ
pub const NOTE_DURATIONS: [(usize, &str);26] = [
(1, "1/384"), (2, "1/192"),
(3, "1/128"), (4, "1/96"),
(6, "1/64"), (8, "1/48"),
(12, "1/32"), (16, "1/24"),
(24, "1/16"), (32, "1/12"),
(48, "1/8"), (64, "1/6"),
(96, "1/4"), (128, "1/3"),
(192, "1/2"), (256, "2/3"),
(384, "1/1"), (512, "4/3"),
(576, "3/2"), (768, "2/1"),
(1152, "3/1"), (1536, "4/1"),
(2304, "6/1"), (3072, "8/1"),
(3456, "9/1"), (6144, "16/1"),
];
pub const NOTE_NAMES: [&str; 128] = [
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9",
"C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10",
];
/// Return boxed iterator of MIDI events
pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>)
-> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a>
{
Box::new(input.map(|::jack::RawMidi { time, bytes }|(
time as usize,
LiveEvent::parse(bytes).unwrap(),
bytes
)))
}
/// Add "all notes off" to the start of a buffer.
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
let mut buf = vec![];
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
evt.write(&mut buf).unwrap();
output[0].push(buf);
}
/// Update notes_in array
pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) {
match message {
MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; }
MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; },
_ => {}
}
}
/// Returns the next shorter length
pub fn note_duration_prev (pulses: usize) -> usize {
for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } }
pulses
}
/// Returns the next longer length
pub fn note_duration_next (pulses: usize) -> usize {
for (length, _) in NOTE_DURATIONS.iter() { if *length > pulses { return *length } }
pulses
}
pub fn note_duration_to_name (pulses: usize) -> &'static str {
for (length, name) in NOTE_DURATIONS.iter() { if *length == pulses { return name } }
""
}
pub fn note_pitch_to_name (n: usize) -> &'static str {
if n > 127 {
panic!("to_note_name({n}): must be 0-127");
}
NOTE_NAMES[n]
}
//macro_rules! impl_port {
//($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => {
//#[derive(Debug)] pub struct $Name {
///// Handle to JACK client, for receiving reconnect events.
//jack: Jack<'static>,
///// Port name
//name: Arc<str>,
///// Port handle.
//port: Port<$Spec>,
///// List of ports to connect to.
//conn: Vec<PortConnect>
//}
//impl AsRef<Port<$Spec>> for $Name {
//fn as_ref (&self) -> &Port<$Spec> { &self.port }
//}
//impl $Name {
//pub fn new ($jack: &Jack, name: impl AsRef<str>, connect: &[PortConnect])
//-> Usually<Self>
//{
//let $name = name.as_ref();
//let jack = $jack.clone();
//let port = $port?;
//let name = $name.into();
//let conn = connect.to_vec();
//let port = Self { jack, port, name, conn };
//port.connect_to_matching()?;
//Ok(port)
//}
//pub fn name (&self) -> &Arc<str> { &self.name }
//pub fn port (&self) -> &Port<$Spec> { &self.port }
//pub fn port_mut (&mut self) -> &mut Port<$Spec> { &mut self.port }
//pub fn into_port (self) -> Port<$Spec> { self.port }
//pub fn close (self) -> Usually<()> {
//let Self { jack, port, .. } = self;
//Ok(jack.with_client(|client|client.unregister_port(port))?)
//}
//}
//impl HasJack<'static> for $Name {
//fn jack (&self) -> &'static Jack<'static> { &self.jack }
//}
//impl JackPort<'static> for $Name {
//type Port = $Spec;
//type Pair = $Pair;
//fn port (&self) -> &Port<$Spec> { &self.port }
//}
//impl ConnectTo<'static, &str> for $Name {
//fn connect_to (&self, to: &str) -> Usually<PortConnectStatus> {
//self.with_client(|c|if let Some(ref port) = c.port_by_name(to.as_ref()) {
//self.connect_to(port)
//} else {
//Ok(Missing)
//})
//}
//}
//impl ConnectTo<'static, &Port<Unowned>> for $Name {
//fn connect_to (&self, port: &Port<Unowned>) -> Usually<PortConnectStatus> {
//self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) {
//Connected
//} else if let Ok(_) = c.connect_ports(port, &self.port) {
//Connected
//} else {
//Mismatch
//}))
//}
//}
//impl ConnectTo<'static, &Port<$Pair>> for $Name {
//fn connect_to (&self, port: &Port<$Pair>) -> Usually<PortConnectStatus> {
//self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(&self.port, port) {
//Connected
//} else if let Ok(_) = c.connect_ports(port, &self.port) {
//Connected
//} else {
//Mismatch
//}))
//}
//}
//impl ConnectAuto<'static> for $Name {
//fn connections (&self) -> &[PortConnect] {
//&self.conn
//}
//}
//};
//}
pub fn swap_value <T: Clone + PartialEq, U> (
target: &mut T, value: &T, returned: impl Fn(T)->U
) -> Perhaps<U> {
if *target == *value {
Ok(None)
} else {
let mut value = value.clone();
std::mem::swap(target, &mut value);
Ok(Some(returned(value)))
}
}
pub fn toggle_bool <U> (
target: &mut bool, value: &Option<bool>, returned: impl Fn(Option<bool>)->U
) -> Perhaps<U> {
let mut value = value.unwrap_or(!*target);
if value == *target {
Ok(None)
} else {
std::mem::swap(target, &mut value);
Ok(Some(returned(Some(value))))
}
}
pub fn device_kinds () -> &'static [&'static str] {
&[
#[cfg(feature = "sampler")] "Sampler",
#[cfg(feature = "lv2")] "Plugin (LV2)",
]
}
impl<T: Has<Vec<Device>>> HasDevices for T {
fn devices (&self) -> &Vec<Device> {
self.get()
}
fn devices_mut (&mut self) -> &mut Vec<Device> {
self.get_mut()
}
}
impl Device {
pub fn name (&self) -> &str {
match self {
Self::Sampler(sampler) => sampler.name.as_ref(),
_ => todo!(),
}
}
pub fn midi_ins (&self) -> &[MidiInput] {
match self {
//Self::Sampler(Sampler { midi_in, .. }) => &[midi_in],
_ => todo!()
}
}
pub fn midi_outs (&self) -> &[MidiOutput] {
match self {
Self::Sampler(_) => &[],
_ => todo!()
}
}
pub fn audio_ins (&self) -> &[AudioInput] {
match self {
Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(),
_ => todo!()
}
}
pub fn audio_outs (&self) -> &[AudioOutput] {
match self {
Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(),
_ => todo!()
}
}
}
audio!(|self: DeviceAudio<'a>, client, scope|{
use Device::*;
match self.0 {
Mute => { Control::Continue },
Bypass => { /*TODO*/ Control::Continue },
#[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope),
#[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope),
#[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO
#[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO
#[cfg(feature = "clap")] Clap => { todo!() }, // TODO
#[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO
}
});
//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref()
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
#[macro_export] macro_rules! has_clip {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
}
}
}
fn to_key (note: usize) -> &'static str {
match note % 12 {
11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌",
10 | 8 | 6 | 3 | 1 => " ",
_ => unreachable!(),
}
}
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16)
-> impl Iterator<Item=(usize, u16, usize)>
{
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
}
#[cfg(feature = "lv2_gui")]
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
Ok(spawn(move||{
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
event_loop.set_control_flow(ControlFlow::Wait);
event_loop.run_app(&mut ui).unwrap()
}))
}
#[cfg(feature = "lv2_gui")]
fn lv2_ui_instantiate (kind: &str) {
//let host = Suil
}
pub fn mix_summing <const N: usize> (
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
) -> bool {
let channels = buffer.len();
for index in 0..frames {
if let Some(frame) = next() {
for (channel, sample) in frame.iter().enumerate() {
let channel = channel % channels;
buffer[channel][index] += sample * gain;
}
} else {
return false
}
}
true
}
pub fn mix_average <const N: usize> (
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
) -> bool {
let channels = buffer.len();
for index in 0..frames {
if let Some(frame) = next() {
for (channel, sample) in frame.iter().enumerate() {
let channel = channel % channels;
let value = buffer[channel][index];
buffer[channel][index] = (value + sample * gain) / 2.0;
}
} else {
return false
}
}
true
}
pub fn to_log10 (samples: &[f32]) -> f32 {
let total: f32 = samples.iter().map(|x|x.abs()).sum();
let count = samples.len() as f32;
10. * (total / count).log10()
}
pub fn to_rms (samples: &[f32]) -> f32 {
let sum = samples.iter()
.map(|s|*s)
.reduce(|sum, sample|sum + sample.abs())
.unwrap_or(0.0);
(sum / samples.len() as f32).sqrt()
}
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
todo!();
}
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
let entry = entry.expect("failed to read drectory entry");
let meta = entry.metadata().expect("failed to read entry metadata");
if meta.is_file() {
files.push(entry.file_name());
} else if meta.is_dir() {
subdirs.push(entry.file_name());
}
(subdirs, files)
});
subdirs.sort();
files.sort();
Ok((subdirs, files))
}
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
track.width as u16
}
#[cfg(feature = "vst2")] fn set_vst_plugin <E: Engine> (
host: &Arc<Mutex<Plugin<E>>>, _path: &str
) -> Usually<PluginKind> {
let mut loader = ::vst::host::PluginLoader::load(
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
host.clone()
)?;
Ok(PluginKind::VST2 {
instance: loader.instance()?
})
}
def_command!(AppCommand: |app: App| {
Nop => Ok(None),
Confirm => tek_confirm(app),
Cancel => todo!(), // TODO delegate:
Inc { axis: ControlAxis } => tek_inc(app, axis),
Dec { axis: ControlAxis } => tek_dec(app, axis),
SetDialog { dialog: Dialog } => {
swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog })
},
});
def_command!(ClockCommand: |clock: Clock| {
SeekUsec { usec: f64 } => {
clock.playhead.update_from_usec(*usec); Ok(None) },
SeekSample { sample: f64 } => {
clock.playhead.update_from_sample(*sample); Ok(None) },
SeekPulse { pulse: f64 } => {
clock.playhead.update_from_pulse(*pulse); Ok(None) },
SetBpm { bpm: f64 } => Ok(Some(
Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })),
SetQuant { quant: f64 } => Ok(Some(
Self::SetQuant { quant: clock.quant.set(*quant) })),
SetSync { sync: f64 } => Ok(Some(
Self::SetSync { sync: clock.sync.set(*sync) })),
Play { position: Option<u32> } => {
clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ },
Pause { position: Option<u32> } => {
clock.pause_at(*position)?; Ok(None) },
TogglePlayback { position: u32 } => Ok(if clock.is_rolling() {
clock.pause_at(Some(*position))?; None
} else {
clock.play_from(Some(*position))?; None
}),
});
def_command!(DeviceCommand: |device: Device| {});
def_command!(ClipCommand: |clip: MidiClip| {
SetColor { color: Option<ItemTheme> } => {
//(SetColor [t: usize, s: usize, c: ItemTheme]
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
//("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random())))
todo!()
},
SetLoop { looping: Option<bool> } => {
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
todo!()
}
});
def_command!(BrowseCommand: |browse: Browse| {
SetVisible => Ok(None),
SetPath { address: PathBuf } => Ok(None),
SetSearch { filter: Arc<str> } => Ok(None),
SetCursor { cursor: usize } => Ok(None),
});
def_command!(MidiEditCommand: |editor: MidiEditor| {
Show { clip: Option<Arc<RwLock<MidiClip>>> } => {
editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) },
DeleteNote => {
editor.redraw(); todo!() },
AppendNote { advance: bool } => {
editor.put_note(*advance); editor.redraw(); Ok(None) },
SetNotePos { pos: usize } => {
editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) },
SetNoteLen { len: usize } => {
editor.set_note_len(*len); editor.redraw(); Ok(None) },
SetNoteScroll { scroll: usize } => {
editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) },
SetTimePos { pos: usize } => {
editor.set_time_pos(*pos); editor.redraw(); Ok(None) },
SetTimeScroll { scroll: usize } => {
editor.set_time_start(*scroll); editor.redraw(); Ok(None) },
SetTimeZoom { zoom: usize } => {
editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) },
SetTimeLock { lock: bool } => {
editor.set_time_lock(*lock); editor.redraw(); Ok(None) },
// TODO: 1-9 seek markers that by default start every 8th of the clip
});
def_command!(PoolCommand: |pool: Pool| {
// Toggle visibility of pool
Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) },
// Select a clip from the clip pool
Select { index: usize } => { pool.set_clip_index(*index); Ok(None) },
// Update the contents of the clip pool
Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})),
// Rename a clip
Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?),
// Change the length of a clip
Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?),
// Import from file
Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
command.delegate(browse, |command|Self::Import{command})?
} else {
None
}),
// Export to file
Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
command.delegate(browse, |command|Self::Export{command})?
} else {
None
}),
});
def_command!(PoolClipCommand: |pool: Pool| {
Delete { index: usize } => {
let index = *index;
let clip = pool.clips_mut().remove(index).read().unwrap().clone();
Ok(Some(Self::Add { index, clip }))
},
Swap { index: usize, other: usize } => {
let index = *index;
let other = *other;
pool.clips_mut().swap(index, other);
Ok(Some(Self::Swap { index, other }))
},
Export { index: usize, path: PathBuf } => {
todo!("export clip to midi file");
},
Add { index: usize, clip: MidiClip } => {
let index = *index;
let mut index = index;
let clip = Arc::new(RwLock::new(clip.clone()));
let mut clips = pool.clips_mut();
if index >= clips.len() {
index = clips.len();
clips.push(clip)
} else {
clips.insert(index, clip);
}
Ok(Some(Self::Delete { index }))
},
Import { index: usize, path: PathBuf } => {
let index = *index;
let bytes = std::fs::read(&path)?;
let smf = Smf::parse(bytes.as_slice())?;
let mut t = 0u32;
let mut events = vec![];
for track in smf.tracks.iter() {
for event in track.iter() {
t += event.delta.as_int();
if let TrackEventKind::Midi { channel, message } = event.kind {
events.push((t, channel.as_int(), message));
}
}
}
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
for event in events.iter() {
clip.notes[event.0 as usize].push(event.2);
}
Ok(Self::Add { index, clip }.execute(pool)?)
},
SetName { index: usize, name: Arc<str> } => {
let index = *index;
let clip = &mut pool.clips_mut()[index];
let old_name = clip.read().unwrap().name.clone();
clip.write().unwrap().name = name.clone();
Ok(Some(Self::SetName { index, name: old_name }))
},
SetLength { index: usize, length: usize } => {
let index = *index;
let clip = &mut pool.clips_mut()[index];
let old_len = clip.read().unwrap().length;
clip.write().unwrap().length = *length;
Ok(Some(Self::SetLength { index, length: old_len }))
},
SetColor { index: usize, color: ItemColor } => {
let index = *index;
let mut color = ItemTheme::from(*color);
std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color);
Ok(Some(Self::SetColor { index, color: color.base }))
},
});
def_command!(RenameCommand: |pool: Pool| {
Begin => unreachable!(),
Cancel => {
if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() {
pool.clips()[clip].write().unwrap().name = old_name.clone().into();
}
Ok(None)
},
Confirm => {
if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() {
let old_name = old_name.clone(); *pool.mode_mut() = None; return Ok(Some(Self::Set { value: old_name }))
}
Ok(None)
},
Set { value: Arc<str> } => {
if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() {
pool.clips()[clip].write().unwrap().name = value.clone();
}
Ok(None)
},
});
def_command!(CropCommand: |pool: Pool| {
Begin => unreachable!(),
Cancel => { if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { *pool.mode_mut() = None; } Ok(None) },
Set { length: usize } => {
if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus))
= pool.mode_mut().clone()
{
let old_length;
{
let clip = pool.clips()[clip].clone();//.write().unwrap();
old_length = Some(clip.read().unwrap().length);
clip.write().unwrap().length = *length;
}
*pool.mode_mut() = None;
return Ok(old_length.map(|length|Self::Set { length }))
}
Ok(None)
},
Next => {
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.next() }; Ok(None)
},
Prev => {
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.prev() }; Ok(None)
},
Inc => {
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
match focus {
ClipLengthFocus::Bar => { *length += 4 * PPQ },
ClipLengthFocus::Beat => { *length += PPQ },
ClipLengthFocus::Tick => { *length += 1 },
}
}
Ok(None)
},
Dec => {
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
match focus {
ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) },
ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) },
ClipLengthFocus::Tick => { *length = length.saturating_sub(1) },
}
}
Ok(None)
}
});
def_command!(MidiInputCommand: |port: MidiInput| {
Close => todo!(),
Connect { midi_out: Arc<str> } => todo!(),
});
def_command!(MidiOutputCommand: |port: MidiOutput| {
Close => todo!(),
Connect { midi_in: Arc<str> } => todo!(),
});
def_command!(AudioInputCommand: |port: AudioInput| {
Close => todo!(),
Connect { audio_out: Arc<str> } => todo!(),
});
def_command!(AudioOutputCommand: |port: AudioOutput| {
Close => todo!(),
Connect { audio_in: Arc<str> } => todo!(),
});
def_command!(SamplerCommand: |sampler: Sampler| {
RecordToggle { slot: usize } => {
let slot = *slot;
let recording = sampler.recording.as_ref().map(|x|x.0);
let _ = Self::RecordFinish.execute(sampler)?;
// autoslice: continue recording at next slot
if recording != Some(slot) {
Self::RecordBegin { slot }.execute(sampler)
} else {
Ok(None)
}
},
RecordBegin { slot: usize } => {
let slot = *slot;
sampler.recording = Some((
slot,
Some(Arc::new(RwLock::new(Sample::new(
"Sample", 0, 0, vec![vec![];sampler.audio_ins.len()]
))))
));
Ok(None)
},
RecordFinish => {
let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{
std::mem::swap(sample, &mut sampler.samples.0[*index]);
sample
}); // TODO: undo
Ok(None)
},
RecordCancel => {
sampler.recording = None;
Ok(None)
},
PlaySample { slot: usize } => {
let slot = *slot;
if let Some(ref sample) = sampler.samples.0[slot] {
sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128)));
}
Ok(None)
},
StopSample { slot: usize } => {
let slot = *slot;
todo!();
Ok(None)
},
});
def_command!(FileBrowserCommand: |sampler: Sampler|{
//("begin" [] Some(Self::Begin))
//("cancel" [] Some(Self::Cancel))
//("confirm" [] Some(Self::Confirm))
//("select" [i: usize] Some(Self::Select(i.expect("no index"))))
//("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path"))))
//("filter" [f: Arc<str>] Some(Self::Filter(f.expect("no filter")))))
});
def_command!(SceneCommand: |scene: Scene| {
SetSize { size: usize } => { todo!() },
SetZoom { size: usize } => { todo!() },
SetName { name: Arc<str> } =>
swap_value(&mut scene.name, name, |name|Self::SetName{name}),
SetColor { color: ItemTheme } =>
swap_value(&mut scene.color, color, |color|Self::SetColor{color}),
});
def_command!(TrackCommand: |track: Track| {
Stop => { track.sequencer.enqueue_next(None); Ok(None) },
SetMute { mute: Option<bool> } => todo!(),
SetSolo { solo: Option<bool> } => todo!(),
SetSize { size: usize } => todo!(),
SetZoom { zoom: usize } => todo!(),
SetName { name: Arc<str> } =>
swap_value(&mut track.name, name, |name|Self::SetName { name }),
SetColor { color: ItemTheme } =>
swap_value(&mut track.color, color, |color|Self::SetColor { color }),
SetRec { rec: Option<bool> } =>
toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }),
SetMon { mon: Option<bool> } =>
toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }),
});
pub(crate) use self::size::*;
mod size {
use crate::*;
/// Define a type alias for iterators of sized items (columns).
macro_rules! def_sizes_iter {
($Type:ident => $($Item:ty),+) => {
pub trait $Type<'a> =
Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a;
}
}
def_sizes_iter!(InputsSizes => MidiInput);
def_sizes_iter!(OutputsSizes => MidiOutput);
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
def_sizes_iter!(ScenesSizes => Scene);
def_sizes_iter!(TracksSizes => Track);
}
pub(crate) use self::view::*;
mod view {
use crate::*;
pub fn view_logo () -> impl Content<TuiOut> {
Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{
Fixed::Y(1, ""),
Fixed::Y(1, ""),
Fixed::Y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"),
Fixed::Y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))),
Fixed::Y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"),
})))
}
pub fn view_transport (
play: bool,
bpm: Arc<RwLock<String>>,
beat: Arc<RwLock<String>>,
time: Arc<RwLock<String>>,
) -> impl Content<TuiOut> {
let theme = ItemTheme::G[96];
Tui::bg(Black, row!(Bsp::a(
Fill::XY(Align::w(button_play_pause(play))),
Fill::XY(Align::e(row!(
FieldH(theme, "BPM", bpm),
FieldH(theme, "Beat", beat),
FieldH(theme, "Time", time),
)))
)))
}
pub fn view_status (
sel: Option<Arc<str>>,
sr: Arc<RwLock<String>>,
buf: Arc<RwLock<String>>,
lat: Arc<RwLock<String>>,
) -> impl Content<TuiOut> {
let theme = ItemTheme::G[96];
Tui::bg(Black, row!(Bsp::a(
Fill::XY(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))),
Fill::XY(Align::e(row!(
FieldH(theme, "SR", sr),
FieldH(theme, "Buf", buf),
FieldH(theme, "Lat", lat),
)))
)))
}
pub fn button_play_pause (playing: bool) -> impl Content<TuiOut> {
let compact = true;//self.is_editing();
Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
Either::new(compact,
Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(9, Either::new(playing,
Tui::fg(Rgb(0, 255, 0), " PLAYING "),
Tui::fg(Rgb(255, 128, 0), " STOPPED ")))
)),
Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::X(5, Either::new(playing,
Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))
))
)
)
}
#[cfg(feature = "track")] pub fn view_track_row_section (
_theme: ItemTheme,
button: impl Content<TuiOut>,
button_add: impl Content<TuiOut>,
content: impl Content<TuiOut>,
) -> impl Content<TuiOut> {
Bsp::w(Fill::Y(Fixed::X(4, Align::nw(button_add))),
Bsp::e(Fixed::X(20, Fill::Y(Align::nw(button))), Fill::XY(Align::c(content))))
}
pub fn wrap (bg: Color, fg: Color, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
let left = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("")));
let right = Tui::fg_bg(bg, Reset, Fixed::X(1, Repeat::Y("")));
Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content)))
}
pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Content<TuiOut> + 'a {
col!(
FieldH(ItemTheme::G[128], label, format!("{:>+9.3}", value)),
Fixed::XY(if value >= 0.0 { 13 }
else if value >= -1.0 { 12 }
else if value >= -2.0 { 11 }
else if value >= -3.0 { 10 }
else if value >= -4.0 { 9 }
else if value >= -6.0 { 8 }
else if value >= -9.0 { 7 }
else if value >= -12.0 { 6 }
else if value >= -15.0 { 5 }
else if value >= -20.0 { 4 }
else if value >= -25.0 { 3 }
else if value >= -30.0 { 2 }
else if value >= -40.0 { 1 }
else { 0 }, 1, Tui::bg(if value >= 0.0 { Red }
else if value >= -3.0 { Yellow }
else { Green }, ())))
}
pub fn view_meters (values: &[f32;2]) -> impl Content<TuiOut> + use<'_> {
let left = format!("L/{:>+9.3}", values[0]);
let right = format!("R/{:>+9.3}", values[1]);
Bsp::s(left, right)
}
pub fn draw_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> {
When::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{
let sample = sample.unwrap().read().unwrap();
let theme = sample.color;
to.place(&row!(
FieldH(theme, "Name", format!("{:<10}", sample.name.clone())),
FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())),
FieldH(theme, "Start", format!("{:<8}", sample.start)),
FieldH(theme, "End", format!("{:<8}", sample.end)),
FieldH(theme, "Trans", "0"),
FieldH(theme, "Gain", format!("{}", sample.gain)),
))
}))
}
pub fn draw_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> + use<'_> {
Either::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{
let sample = sample.unwrap().read().unwrap();
let theme = sample.color;
to.place(&Fixed::X(20, col!(
Fill::X(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))),
Fill::X(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))),
Fill::X(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))),
Fill::X(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))),
Fill::X(Align::w(FieldH(theme, "Trans ", "0"))),
Fill::X(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))),
)))
}), Thunk::new(|to: &mut TuiOut|to.place(&Tui::fg(Red, col!(
Tui::bold(true, "× No sample."),
"[r] record",
"[Shift-F9] import",
)))))
}
pub fn draw_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Content<TuiOut> {
Tui::bold(true, Tui::fg(Tui::g(224), sample
.map(|sample|{
let sample = sample.read().unwrap();
format!("Sample {}-{}", sample.start, sample.end)
})
.unwrap_or_else(||"No sample".to_string())))
}
pub fn view_track_header (theme: ItemTheme, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
Fixed::X(12, Tui::bg(theme.darker.rgb, Fill::X(Align::e(content))))
}
pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
-> impl Content<TuiOut> + use<'a, T>
{
let ins = ports.len() as u16;
let frame = Outer(true, Style::default().fg(Tui::g(96)));
let iter = move||ports.iter();
let names = Map::south(1, iter, move|port, index|Fill::Y(Align::w(format!(" {index} {}", port.port_name()))));
let field = FieldV(theme, title, names);
Fixed::XY(20, 1 + ins, frame.enclose(Fixed::XY(20, 1 + ins, field)))
}
pub fn per_track_top <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Align::x(Tui::bg(Reset, Map::new(tracks,
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
let width = (x2 - x1) as u16;
map_east(x1 as u16, width, Fixed::X(width, Tui::fg_bg(
track.color.lightest.rgb,
track.color.base.rgb,
callback(index, track))))})))
}
pub fn per_track <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
per_track_top(tracks, move|index, track|Fill::Y(Align::y(callback(index, track))))
}
pub fn io_ports <'a, T: PortsSizes<'a>> (
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
) -> impl Content<TuiOut> + 'a {
Map::new(iter, move|(
_index, name, connections, y, y2
): (usize, &'a Arc<str>, &'a [Connect], usize, usize), _|
map_south(y as u16, (y2-y) as u16, Bsp::s(
Fill::Y(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(&" 󰣲 ", name))))),
Map::new(||connections.iter(), move|connect: &'a Connect, index|map_south(index as u16, 1,
Fill::Y(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg,
&connect.info)))))))))
}
pub fn draw_header (state: &Lv2, to: &mut TuiOut, x: u16, y: u16, w: u16) {
let style = Style::default().gray();
let label1 = format!(" {}", state.name);
to.blit(&label1, x + 1, y, Some(style.white().bold()));
if let Some(ref path) = state.path {
let label2 = format!("{}", &path[..((w as usize - 10).min(path.len()))]);
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
}
//Ok(Rect { x, y, width: w, height: 1 })
}
pub fn draw_sample (
to: &mut TuiOut, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
) -> Usually<usize> {
let style = if focus { Style::default().green() } else { Style::default() };
if focus {
to.blit(&"🬴", x+1, y, Some(style.bold()));
}
let label1 = format!("{:3} {:12}",
note.map(|n|n.to_string()).unwrap_or(String::default()),
sample.name);
let label2 = format!("{:>6} {:>6} +0.0",
sample.start,
sample.end);
to.blit(&label1, x+2, y, Some(style.bold()));
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
Ok(label1.len() + label2.len() + 4)
}
}