mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-09 13:16:44 +01:00
unify edn_view entrypoint
This commit is contained in:
parent
df50bb9f47
commit
9cd6e9f195
16 changed files with 603 additions and 541 deletions
|
|
@ -0,0 +1 @@
|
||||||
|
fn main () {}
|
||||||
|
|
@ -4,7 +4,9 @@ use EdnItem::*;
|
||||||
pub struct EdnKeymap(pub Vec<EdnItem<String>>);
|
pub struct EdnKeymap(pub Vec<EdnItem<String>>);
|
||||||
|
|
||||||
impl EdnKeymap {
|
impl EdnKeymap {
|
||||||
pub fn command <C, D: Command<C>, E: EdnCommand<C>, I: EdnInput> (&self, state: &C, input: &I) -> Option<E> {
|
pub fn command <C, D: Command<C>, E: EdnCommand<C>, I: EdnInput> (
|
||||||
|
&self, state: &C, input: &I
|
||||||
|
) -> Option<E> {
|
||||||
for item in self.0.iter() {
|
for item in self.0.iter() {
|
||||||
if let Exp(items) = item {
|
if let Exp(items) = item {
|
||||||
match items.as_slice() {
|
match items.as_slice() {
|
||||||
|
|
@ -20,3 +22,13 @@ impl EdnKeymap {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<str>> From<T> for EdnKeymap {
|
||||||
|
fn from (source: T) -> Self {
|
||||||
|
Self(EdnItem::<String>::read_all(source.as_ref()).expect("failed to load keymap"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)] #[test] fn test_edn_keymap () {
|
||||||
|
let keymap = EdnKeymap::from("");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
use tek::*;
|
use tek_midi::*;
|
||||||
|
use std::sync::*;
|
||||||
|
|
||||||
struct ExampleClips(Vec<Arc<RwLock<Clip>>>);
|
struct ExampleClips(Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>);
|
||||||
|
|
||||||
impl HasClips for ExampleClips {
|
impl HasClips for ExampleClips {
|
||||||
fn phrases (&self) -> &Vec<Arc<RwLock<Clip>>> {
|
fn clips (&self) -> RwLockReadGuard<'_, Vec<Arc<RwLock<MidiClip>>>> {
|
||||||
&self.0
|
self.0.read().unwrap()
|
||||||
}
|
}
|
||||||
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Clip>>> {
|
fn clips_mut (&self) -> RwLockWriteGuard<'_, Vec<Arc<RwLock<MidiClip>>>> {
|
||||||
&mut self.0
|
self.0.write().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main () -> Usually<()> {
|
fn main () -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut phrases = ExampleClips(vec![]);
|
let mut clips = ExampleClips(Arc::new(vec![].into()));
|
||||||
PoolClipCommand::Import(0, String::from("./example.mid")).execute(&mut phrases)?;
|
PoolClipCommand::Import(0, String::from("./example.mid")).execute(&mut clips)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,25 @@ pub trait HasEditor {
|
||||||
fn editor_h (&self) -> usize { 0 }
|
fn editor_h (&self) -> usize { 0 }
|
||||||
}
|
}
|
||||||
#[macro_export] macro_rules! has_editor {
|
#[macro_export] macro_rules! has_editor {
|
||||||
|
(|$self:ident: $Struct:ident|{
|
||||||
|
editor = $e0:expr;
|
||||||
|
editor_w = $e1:expr;
|
||||||
|
editor_h = $e2:expr;
|
||||||
|
is_editing = $e3:expr;
|
||||||
|
}) => {
|
||||||
|
impl HasEditor for $Struct {
|
||||||
|
fn editor (&$self) -> &Option<MidiEditor> { &$e0 }
|
||||||
|
fn editor_mut (&mut $self) -> &Option<MidiEditor> { &mut $e0 }
|
||||||
|
fn editor_w (&$self) -> usize { $e1 }
|
||||||
|
fn editor_h (&$self) -> usize { $e2 }
|
||||||
|
fn is_editing (&$self) -> bool { $e3 }
|
||||||
|
}
|
||||||
|
};
|
||||||
(|$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)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
|
impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
|
||||||
fn editor (&$self) -> &MidiEditor { &$cb }
|
fn editor (&$self) -> &MidiEditor { &$cb }
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
/// Contains state for viewing and editing a clip
|
/// Contains state for viewing and editing a clip
|
||||||
pub struct MidiEditor {
|
pub struct MidiEditor {
|
||||||
|
|
@ -32,7 +46,7 @@ impl Default for MidiEditor {
|
||||||
mode: PianoHorizontal::new(None),
|
mode: PianoHorizontal::new(None),
|
||||||
size: Measure::new(),
|
size: Measure::new(),
|
||||||
keymap: EdnKeymap(
|
keymap: EdnKeymap(
|
||||||
EdnItem::<String>::read_all(include_str!("../edn/midi-keys.edn"))
|
EdnItem::<String>::read_all(include_str!("midi_editor_keys.edn"))
|
||||||
.expect("failed to load keymap for MidiEditor")
|
.expect("failed to load keymap for MidiEditor")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use tek::*;
|
use tek_tui::{*, tek_input::*, tek_output::*};
|
||||||
use tek_edn::*;
|
use tek_edn::*;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use crossterm::event::{*, KeyCode::*};
|
use crossterm::event::{*, KeyCode::*};
|
||||||
|
use crate::ratatui::style::Color;
|
||||||
|
|
||||||
const EDN: &'static [&'static str] = &[
|
const EDN: &'static [&'static str] = &[
|
||||||
include_str!("edn01.edn"),
|
include_str!("edn01.edn"),
|
||||||
|
|
@ -25,26 +26,15 @@ fn main () -> Usually<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Example(usize, Measure<TuiOut>);
|
#[derive(Debug)] pub struct Example(usize, Measure<TuiOut>);
|
||||||
|
|
||||||
impl EdnViewData<TuiOut> for &Example {
|
edn_provide_content!(|self: Example|{
|
||||||
fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
|
":title" => Tui::bg(Color::Rgb(60,10,10), Push::y(1, Align::n(format!("Example {}/{}:", self.0 + 1, EDN.len())))).boxed(),
|
||||||
use EdnItem::*;
|
":code" => Tui::bg(Color::Rgb(10,60,10), Push::y(2, Align::n(format!("{}", EDN[self.0])))).boxed(),
|
||||||
match item {
|
":hello-world" => "Hello world!".boxed(),
|
||||||
Nil => Box::new(()),
|
":hello" => Tui::bg(Color::Rgb(10, 100, 10), "Hello").boxed(),
|
||||||
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
|
":world" => Tui::bg(Color::Rgb(100, 10, 10), "world").boxed(),
|
||||||
//Sym(name) => self.get_sym(name), TODO
|
});
|
||||||
Sym(":title") => Tui::bg(Color::Rgb(60,10,10), Push::y(1,
|
|
||||||
Align::n(format!("Example {}/{}:", self.0 + 1, EDN.len())))).boxed(),
|
|
||||||
Sym(":code") => Tui::bg(Color::Rgb(10,60,10), Push::y(2,
|
|
||||||
Align::n(format!("{}", EDN[self.0])))).boxed(),
|
|
||||||
Sym(":hello-world") => "Hello world!".boxed(),
|
|
||||||
Sym(":hello") => Tui::bg(Color::Rgb(10, 100, 10), "Hello").boxed(),
|
|
||||||
Sym(":world") => Tui::bg(Color::Rgb(100, 10, 10), "world").boxed(),
|
|
||||||
_ => panic!("no content for {item:?}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Content<TuiOut> for Example {
|
impl Content<TuiOut> for Example {
|
||||||
fn content (&self) -> impl Render<TuiOut> {
|
fn content (&self) -> impl Render<TuiOut> {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,42 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use EdnItem::*;
|
use EdnItem::*;
|
||||||
|
|
||||||
|
#[macro_export] macro_rules! edn_view {
|
||||||
|
($Output:ty: |$self:ident: $App:ty| $content:expr; {
|
||||||
|
$( $type:ty { $($sym:literal => $value:expr),* } );*
|
||||||
|
}) => {
|
||||||
|
impl Content<$Output> for $App {
|
||||||
|
fn content(&$self) -> impl Render<$Output> { $content }
|
||||||
|
}
|
||||||
|
$(
|
||||||
|
impl<'a> EdnProvide<'a, $type> for $App {
|
||||||
|
fn get <S: AsRef<str>> (&'a $self, edn: &'a EdnItem<S>) -> Option<$type> {
|
||||||
|
Some(match edn.to_ref() {
|
||||||
|
$(EdnItem::Sym($sym) => $value,)*
|
||||||
|
_ => return None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Implements `EdnProvide` for content components and expressions
|
/// Implements `EdnProvide` for content components and expressions
|
||||||
#[macro_export] macro_rules! edn_provide_content {
|
#[macro_export] macro_rules! edn_provide_content {
|
||||||
(|$self:ident:$Stat:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => {
|
(|$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||||
impl<'a> EdnProvide<'a, Box<dyn Render<TuiOut> + 'a>> for $State {
|
impl<'a, E: Output> EdnProvide<'a, Box<dyn Render<E> + 'a>> for $State {
|
||||||
fn get <S: AsRef<str>> (&'a $self, edn: &'a EdnItem<S>) -> Option<Box<dyn Render<TuiOut> + 'a>> {
|
fn get <S: AsRef<str>> (&'a $self, edn: &'a EdnItem<S>) -> Option<Box<dyn Render<E> + 'a>> {
|
||||||
|
Some(match edn.to_ref() {
|
||||||
|
$(EdnItem::Sym($pat) => $expr),*,
|
||||||
|
_ => return None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($Output:ty: |$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||||
|
impl<'a> EdnProvide<'a, Box<dyn Render<$Output> + 'a>> for $State {
|
||||||
|
fn get <S: AsRef<str>> (&'a $self, edn: &'a EdnItem<S>) -> Option<Box<dyn Render<$Output> + 'a>> {
|
||||||
Some(match edn.to_ref() {
|
Some(match edn.to_ref() {
|
||||||
$(EdnItem::Sym($pat) => $expr),*,
|
$(EdnItem::Sym($pat) => $expr),*,
|
||||||
_ => return None
|
_ => return None
|
||||||
|
|
@ -15,21 +46,23 @@ use EdnItem::*;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Renders from EDN source and context.
|
/// Renders from EDN source and context.
|
||||||
#[derive(Default)]
|
#[derive(Default)] pub enum EdnView<'a, E: Output, T: EdnViewData<'a, E> + std::fmt::Debug> {
|
||||||
pub enum EdnView<'a, E: Output, T: EdnViewData<'a, E> + std::fmt::Debug> {
|
#[default] Inert,
|
||||||
#[default]
|
|
||||||
Inert,
|
|
||||||
_Unused(PhantomData<&'a E>),
|
|
||||||
Ok(T, EdnItem<&'a str>),
|
Ok(T, EdnItem<&'a str>),
|
||||||
//render: Box<dyn Fn(&'a T)->Box<dyn Render<E> + Send + Sync + 'a> + Send + Sync + 'a>
|
//render: Box<dyn Fn(&'a T)->Box<dyn Render<E> + Send + Sync + 'a> + Send + Sync + 'a>
|
||||||
Err(String)
|
Err(String),
|
||||||
|
_Unused(PhantomData<&'a E>),
|
||||||
}
|
}
|
||||||
impl<'a, E: Output, T: EdnViewData<'a, E> + std::fmt::Debug> std::fmt::Debug for EdnView<'a, E, T> {
|
impl<'a, E: Output, T: EdnViewData<'a, E> + std::fmt::Debug> std::fmt::Debug for EdnView<'a, E, T> {
|
||||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
match self {
|
match self {
|
||||||
Self::Inert | Self::_Unused(_) => write!(f, "EdnView::Inert"),
|
Self::Inert | Self::_Unused(_) =>
|
||||||
Self::Ok(state, view) => write!(f, "EdnView::Ok(state={state:?} view={view:?}"),
|
write!(f, "EdnView::Inert"),
|
||||||
Self::Err(error) => write!(f, "EdnView::Err({error})"),
|
Self::Ok(state, view) =>
|
||||||
|
write!(f, "EdnView::Ok(state={state:?} view={view:?}"),
|
||||||
|
Self::Err(error) =>
|
||||||
|
write!(f, "EdnView::Err({error})"),
|
||||||
|
_ => unreachable!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,42 +66,35 @@ pub type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
|
||||||
#[cfg(test)] #[test] fn test_layout () -> Usually<()> {
|
#[cfg(test)] #[test] fn test_layout () -> Usually<()> {
|
||||||
use ::tek_tui::{*, tek_output::*};
|
use ::tek_tui::{*, tek_output::*};
|
||||||
let area: [u16;4] = [10, 10, 20, 20];
|
let area: [u16;4] = [10, 10, 20, 20];
|
||||||
|
|
||||||
let unit = ();
|
let unit = ();
|
||||||
|
fn test (area: [u16;4], item: &impl Content<TuiOut>, expected: [u16;4]) {
|
||||||
assert_eq!(Content::<TuiOut>::layout(&unit, area), [20, 20, 0, 0]);
|
assert_eq!(Content::layout(item, area), expected);
|
||||||
|
assert_eq!(Render::layout(item, area), expected);
|
||||||
assert_eq!(Content::<TuiOut>::layout(&Fill::<()>::x(unit), area), [10, 20, 20, 0]);
|
};
|
||||||
assert_eq!(Render::<TuiOut>::layout(&Fill::<()>::x(unit), area), [10, 20, 20, 0]);
|
test(area, &(), [20, 20, 0, 0]);
|
||||||
|
test(area, &Fill::xy(()), area);
|
||||||
assert_eq!(Fill::<()>::y(unit).layout(area), [20, 10, 0, 20]);
|
test(area, &Fill::x(()), [10, 20, 20, 0]);
|
||||||
assert_eq!(Fill::<()>::xy(unit).layout(area), area);
|
test(area, &Fill::y(()), [20, 10, 0, 20]);
|
||||||
|
test(area, &Fixed::x(4, unit), [18, 20, 4, 0]);
|
||||||
assert_eq!(Fixed::<TuiOut, u16>::x(4, unit).layout(area), [18, 20, 4, 0]);
|
test(area, &Fixed::y(4, unit), [20, 18, 0, 4]);
|
||||||
assert_eq!(Fixed::<TuiOut, u16>::y(4, unit).layout(area), [20, 18, 0, 4]);
|
test(area, &Fixed::xy(4, 4, unit), [18, 18, 4, 4]);
|
||||||
assert_eq!(Fixed::<TuiOut, u16>::xy(4, 4, unit).layout(area), [18, 18, 4, 4]);
|
let four = ||Fixed::<TuiOut, _, _>::xy(4, 4, unit);
|
||||||
|
test(area, &Align::nw(four()), [10, 10, 4, 4]);
|
||||||
let four = ||Fixed::<TuiOut, _>::xy(4, 4, unit);
|
test(area, &Align::n(four()), [18, 10, 4, 4]);
|
||||||
|
test(area, &Align::ne(four()), [26, 10, 4, 4]);
|
||||||
assert_eq!(Align::nw(four()).layout(area), [10, 10, 4, 4]);
|
test(area, &Align::e(four()), [26, 18, 4, 4]);
|
||||||
assert_eq!(Align::n(four()).layout(area), [18, 10, 4, 4]);
|
test(area, &Align::se(four()), [26, 26, 4, 4]);
|
||||||
assert_eq!(Align::ne(four()).layout(area), [26, 10, 4, 4]);
|
test(area, &Align::s(four()), [18, 26, 4, 4]);
|
||||||
assert_eq!(Align::e(four()).layout(area), [26, 18, 4, 4]);
|
test(area, &Align::sw(four()), [10, 26, 4, 4]);
|
||||||
assert_eq!(Align::se(four()).layout(area), [26, 26, 4, 4]);
|
test(area, &Align::w(four()), [10, 18, 4, 4]);
|
||||||
assert_eq!(Align::s(four()).layout(area), [18, 26, 4, 4]);
|
let two_by_four = ||Fixed::<TuiOut, _, _>::xy(4, 2, unit);
|
||||||
assert_eq!(Align::sw(four()).layout(area), [10, 26, 4, 4]);
|
test(area, &Align::nw(two_by_four()), [10, 10, 4, 2]);
|
||||||
assert_eq!(Align::w(four()).layout(area), [10, 18, 4, 4]);
|
test(area, &Align::n(two_by_four()), [18, 10, 4, 2]);
|
||||||
|
test(area, &Align::ne(two_by_four()), [26, 10, 4, 2]);
|
||||||
let two_by_four = ||Fixed::<TuiOut, _>::xy(4, 2, unit);
|
test(area, &Align::e(two_by_four()), [26, 19, 4, 2]);
|
||||||
|
test(area, &Align::se(two_by_four()), [26, 28, 4, 2]);
|
||||||
assert_eq!(Align::nw(two_by_four()).layout(area), [10, 10, 4, 2]);
|
test(area, &Align::s(two_by_four()), [18, 28, 4, 2]);
|
||||||
assert_eq!(Align::n(two_by_four()).layout(area), [18, 10, 4, 2]);
|
test(area, &Align::sw(two_by_four()), [10, 28, 4, 2]);
|
||||||
assert_eq!(Align::ne(two_by_four()).layout(area), [26, 10, 4, 2]);
|
test(area, &Align::w(two_by_four()), [10, 19, 4, 2]);
|
||||||
assert_eq!(Align::e(two_by_four()).layout(area), [26, 19, 4, 2]);
|
|
||||||
assert_eq!(Align::se(two_by_four()).layout(area), [26, 28, 4, 2]);
|
|
||||||
assert_eq!(Align::s(two_by_four()).layout(area), [18, 28, 4, 2]);
|
|
||||||
assert_eq!(Align::sw(two_by_four()).layout(area), [10, 28, 4, 2]);
|
|
||||||
assert_eq!(Align::w(two_by_four()).layout(area), [10, 19, 4, 2]);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
(bsp/s (fixed/y 2 :toolbar)
|
(bsp/s (fixed/y 2 :toolbar)
|
||||||
(fill/x (align/c (bsp/w :pool
|
(fill/x (align/c (bsp/w :pool
|
||||||
(bsp/n :outputs (bsp/n :inputs (bsp/n :tracks :scenes)))))))
|
(bsp/s :outputs (bsp/s :inputs (bsp/s :tracks :scenes)))))))
|
||||||
|
|
|
||||||
0
tek/src/groovebox-keys.edn
Normal file
0
tek/src/groovebox-keys.edn
Normal file
938
tek/src/lib.rs
938
tek/src/lib.rs
|
|
@ -62,74 +62,434 @@ impl HasSampler for App {
|
||||||
fn sampler_mut (&mut self) -> &mut Option<Sampler> { &mut self.sampler }
|
fn sampler_mut (&mut self) -> &mut Option<Sampler> { &mut self.sampler }
|
||||||
fn sample_index (&self) -> usize { self.editor.as_ref().map(|e|e.note_point()).unwrap_or(0) }
|
fn sample_index (&self) -> usize { self.editor.as_ref().map(|e|e.note_point()).unwrap_or(0) }
|
||||||
}
|
}
|
||||||
impl HasEditor for App {
|
has_editor!(|self: App|{
|
||||||
fn editor (&self) -> &Option<MidiEditor> { &self.editor }
|
editor = self.editor;
|
||||||
fn editor_mut (&mut self) -> &Option<MidiEditor> { &mut self.editor }
|
editor_w = {
|
||||||
|
let size = self.size.w();
|
||||||
|
let editor = self.editor.as_ref().expect("missing editor");
|
||||||
|
let time_len = editor.time_len().get();
|
||||||
|
let time_zoom = editor.time_zoom().get().max(1);
|
||||||
|
(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16)
|
||||||
|
};
|
||||||
|
editor_h = 15;
|
||||||
|
is_editing = self.editing.load(Relaxed);
|
||||||
|
});
|
||||||
|
edn_view!(TuiOut: |self: App| self.size.of(EdnView::from_source(self, self.edn.as_ref())); {
|
||||||
|
bool {};
|
||||||
|
usize {};
|
||||||
|
isize {};
|
||||||
|
Color {};
|
||||||
|
Selection {};
|
||||||
|
Arc<RwLock<MidiClip>> {};
|
||||||
|
Option<Arc<RwLock<MidiClip>>> {};
|
||||||
|
|
||||||
|
u16 {
|
||||||
|
":sidebar-w" => self.sidebar_w(),
|
||||||
|
":sample-h" => if self.compact() { 0 } else { 5 },
|
||||||
|
":samples-w" => if self.compact() { 4 } else { 11 },
|
||||||
|
":samples-y" => if self.compact() { 1 } else { 0 },
|
||||||
|
":pool-w" => if self.compact() { 5 } else {
|
||||||
|
let w = self.size.w();
|
||||||
|
if w > 60 { 20 } else if w > 40 { 15 } else { 10 } } };
|
||||||
|
|
||||||
|
Box<dyn Render<TuiOut> + 'a> {
|
||||||
|
":editor" => (&self.editor).boxed(),
|
||||||
|
":pool" => self.pool.as_ref().map(|pool|PoolView(self.compact(), pool)).boxed(),
|
||||||
|
":sample" => self.view_sample(self.is_editing()).boxed(),
|
||||||
|
":sampler" => self.view_sampler(self.is_editing(), &self.editor).boxed(),
|
||||||
|
":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(),
|
||||||
|
":toolbar" => ClockView::new(true, &self.clock).boxed(),
|
||||||
|
":tracks" => self.row(self.w(), 3, self.track_header(), self.track_cells()).boxed(),
|
||||||
|
":inputs" => self.row(self.w(), 3, self.input_header(), self.input_cells()).boxed(),
|
||||||
|
":outputs" => self.row(self.w(), 3, self.output_header(), self.output_cells()).boxed(),
|
||||||
|
":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16,
|
||||||
|
self.scene_header(), self.scene_cells(self.is_editing())).boxed() }});
|
||||||
|
|
||||||
|
//content!(TuiOut:|self: App|self.size.of(EdnView::from_source(self, self.edn.as_ref())));
|
||||||
|
//edn_provide!(bool: |self: App| { _ => return None });
|
||||||
|
//edn_provide!(usize: |self: App| { _ => return None });
|
||||||
|
//edn_provide!(isize: |self: App| { _ => return None });
|
||||||
|
//edn_provide!(Color: |self: App| { _ => return None });
|
||||||
|
//edn_provide!(Selection: |self: App| { _ => return None });
|
||||||
|
//edn_provide!(Arc<RwLock<MidiClip>>: |self: App| { _ => return None });
|
||||||
|
//edn_provide!(Option<Arc<RwLock<MidiClip>>>: |self: App| { _ => return None });
|
||||||
|
//edn_provide!('a: Box<dyn Render<TuiOut> + 'a>: |self: App|{
|
||||||
|
//":editor" => (&self.editor).boxed(),
|
||||||
|
//":pool" => self.pool.as_ref().map(|pool|PoolView(self.compact(), pool)).boxed(),
|
||||||
|
//":sample" => self.view_sample(self.is_editing()).boxed(),
|
||||||
|
//":sampler" => self.view_sampler(self.is_editing(), &self.editor).boxed(),
|
||||||
|
//":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(),
|
||||||
|
//":toolbar" => ClockView::new(true, &self.clock).boxed(),
|
||||||
|
//":tracks" => self.row(self.w(), 3, self.track_header(), self.track_cells()).boxed(),
|
||||||
|
//":inputs" => self.row(self.w(), 3, self.input_header(), self.input_cells()).boxed(),
|
||||||
|
//":outputs" => self.row(self.w(), 3, self.output_header(), self.output_cells()).boxed(),
|
||||||
|
//":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16,
|
||||||
|
//self.scene_header(), self.scene_cells(self.is_editing())).boxed() });
|
||||||
|
//edn_provide!(u16: |self: App|{
|
||||||
|
//":sidebar-w" => self.sidebar_w(),
|
||||||
|
//":sample-h" => if self.compact() { 0 } else { 5 },
|
||||||
|
//":samples-w" => if self.compact() { 4 } else { 11 },
|
||||||
|
//":samples-y" => if self.compact() { 1 } else { 0 },
|
||||||
|
//":pool-w" => if self.compact() { 5 } else {
|
||||||
|
//let w = self.size.w();
|
||||||
|
//if w > 60 { 20 } else if w > 40 { 15 } else { 10 }
|
||||||
|
//}
|
||||||
|
//});
|
||||||
|
impl App {
|
||||||
|
pub fn sequencer (
|
||||||
|
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
|
||||||
|
player: Option<MidiPlayer>, midi_froms: &[PortConnection], midi_tos: &[PortConnection],
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
edn: include_str!("./sequencer-view.edn").to_string(),
|
||||||
|
jack: jack.clone(),
|
||||||
|
pool: Some(pool),
|
||||||
|
editor: Some(editor),
|
||||||
|
player: player,
|
||||||
|
editing: false.into(),
|
||||||
|
midi_buf: vec![vec![];65536],
|
||||||
|
color: ItemPalette::random(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn groovebox (
|
||||||
|
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
|
||||||
|
player: Option<MidiPlayer>, midi_froms: &[PortConnection], midi_tos: &[PortConnection],
|
||||||
|
sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]],
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
edn: include_str!("./groovebox-view.edn").to_string(),
|
||||||
|
sampler: Some(sampler),
|
||||||
|
..Self::sequencer(
|
||||||
|
jack, pool, editor,
|
||||||
|
player, midi_froms, midi_tos
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn arranger (
|
||||||
|
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
|
||||||
|
midi_froms: &[PortConnection], midi_tos: &[PortConnection],
|
||||||
|
sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]],
|
||||||
|
scenes: usize, tracks: usize, track_width: usize,
|
||||||
|
) -> Self {
|
||||||
|
let mut arranger = Self {
|
||||||
|
edn: include_str!("./arranger-view.edn").to_string(),
|
||||||
|
..Self::groovebox(
|
||||||
|
jack, pool, editor,
|
||||||
|
None, midi_froms, midi_tos,
|
||||||
|
sampler, audio_froms, audio_tos
|
||||||
|
)
|
||||||
|
};
|
||||||
|
arranger.scenes_add(scenes);
|
||||||
|
arranger.tracks_add(tracks, track_width, &[], &[]);
|
||||||
|
arranger
|
||||||
|
}
|
||||||
|
fn compact (&self) -> bool { false }
|
||||||
fn is_editing (&self) -> bool { self.editing.load(Relaxed) }
|
fn is_editing (&self) -> bool { self.editing.load(Relaxed) }
|
||||||
fn editor_h (&self) -> usize { 15 }
|
fn editor (&self) -> impl Content<TuiOut> + '_ { &self.editor }
|
||||||
fn editor_w (&self) -> usize {
|
fn w (&self) -> u16 { self.tracks_sizes(self.is_editing(), self.editor_w()).last().map(|x|x.3 as u16).unwrap_or(0) }
|
||||||
let editor = self.editor.as_ref().expect("missing editor");
|
fn pool (&self) -> impl Content<TuiOut> + use<'_> { self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) }
|
||||||
(5 + (editor.time_len().get() / editor.time_zoom().get()))
|
fn sidebar_w (&self) -> u16 {
|
||||||
.min(self.size.w().saturating_sub(20))
|
let w = self.size.w();
|
||||||
.max(16)
|
let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||||
|
let w = if self.is_editing() { 8 } else { w };
|
||||||
|
w
|
||||||
|
}
|
||||||
|
fn row <'a> (
|
||||||
|
&'a self, w: u16, h: u16, a: impl Content<TuiOut> + 'a, b: impl Content<TuiOut> + 'a
|
||||||
|
) -> impl Content<TuiOut> + 'a {
|
||||||
|
Fixed::y(h, Bsp::e(
|
||||||
|
Fixed::xy(self.sidebar_w() as u16, h, a),
|
||||||
|
Fill::x(Align::c(Fixed::xy(w, h, b)))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
||||||
|
self.scene()?.clips.get(self.selected().track()?)?.clone()
|
||||||
|
}
|
||||||
|
fn toggle_loop (&mut self) {
|
||||||
|
if let Some(clip) = self.clip() {
|
||||||
|
clip.write().unwrap().toggle_loop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//fn randomize_color (&mut self) {
|
||||||
|
//match self.selected {
|
||||||
|
//Selection::Mix => { self.color = ItemPalette::random() },
|
||||||
|
//Selection::Track(t) => { self.tracks[t].color = ItemPalette::random() },
|
||||||
|
//Selection::Scene(s) => { self.scenes[s].color = ItemPalette::random() },
|
||||||
|
//Selection::Clip(t, s) => if let Some(clip) = &self.scenes[s].clips[t] {
|
||||||
|
//clip.write().unwrap().color = ItemPalette::random();
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
fn track_add (
|
||||||
|
&mut self,
|
||||||
|
name: Option<&str>,
|
||||||
|
color: Option<ItemPalette>
|
||||||
|
) -> Usually<&mut Track> {
|
||||||
|
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
|
||||||
|
let track = Track {
|
||||||
|
width: (name.len() + 2).max(9),
|
||||||
|
color: color.unwrap_or_else(ItemPalette::random),
|
||||||
|
player: MidiPlayer::from(self.clock()),
|
||||||
|
name,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
self.tracks_mut().push(track);
|
||||||
|
let len = self.tracks().len();
|
||||||
|
let index = len - 1;
|
||||||
|
for scene in self.scenes_mut().iter_mut() {
|
||||||
|
while scene.clips.len() < len {
|
||||||
|
scene.clips.push(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(&mut self.tracks_mut()[index])
|
||||||
|
}
|
||||||
|
fn tracks_add (
|
||||||
|
&mut self,
|
||||||
|
count: usize,
|
||||||
|
width: usize,
|
||||||
|
midi_from: &[PortConnection],
|
||||||
|
midi_to: &[PortConnection],
|
||||||
|
) -> Usually<()> {
|
||||||
|
let jack = self.jack().clone();
|
||||||
|
let track_color_1 = ItemColor::random();
|
||||||
|
let track_color_2 = ItemColor::random();
|
||||||
|
for i in 0..count {
|
||||||
|
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
|
||||||
|
let mut track = self.track_add(None, Some(color))?;
|
||||||
|
track.width = width;
|
||||||
|
let port = JackPort::<MidiIn>::new(&jack, &format!("{}I", &track.name), midi_from)?;
|
||||||
|
track.player.midi_ins.push(port);
|
||||||
|
let port = JackPort::<MidiOut>::new(&jack, &format!("{}O", &track.name), midi_to)?;
|
||||||
|
track.player.midi_outs.push(port);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn track_del (&mut self, index: usize) {
|
||||||
|
self.tracks_mut().remove(index);
|
||||||
|
for scene in self.scenes_mut().iter_mut() {
|
||||||
|
scene.clips.remove(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
||||||
|
-> Usually<&mut Scene>
|
||||||
|
{
|
||||||
|
let scene = Scene {
|
||||||
|
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
||||||
|
clips: vec![None;self.tracks().len()],
|
||||||
|
color: color.unwrap_or_else(ItemPalette::random),
|
||||||
|
};
|
||||||
|
self.scenes_mut().push(scene);
|
||||||
|
let index = self.scenes().len() - 1;
|
||||||
|
Ok(&mut self.scenes_mut()[index])
|
||||||
|
}
|
||||||
|
fn scenes_add (&mut self, n: usize) -> Usually<()> {
|
||||||
|
let scene_color_1 = ItemColor::random();
|
||||||
|
let scene_color_2 = ItemColor::random();
|
||||||
|
for i in 0..n {
|
||||||
|
let _scene = self.scene_add(None, Some(
|
||||||
|
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn scene_cells <'a> (&'a self, editing: bool) -> BoxThunk<'a, TuiOut> {
|
||||||
|
let tracks = move||self.tracks_sizes(self.is_editing(), self.editor_w());
|
||||||
|
let scenes = ||self.scenes_sizes(self.is_editing(), 2, 15);
|
||||||
|
let selected_track = self.selected().track();
|
||||||
|
let selected_scene = self.selected().scene();
|
||||||
|
(move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| {
|
||||||
|
let w = (x2 - x1) as u16;
|
||||||
|
let color: ItemPalette = track.color.dark.into();
|
||||||
|
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
||||||
|
let cells = Map::new(scenes, move|(_, scene, y1, y2), s| {
|
||||||
|
let h = (y2 - y1) as u16;
|
||||||
|
let color = scene.color;
|
||||||
|
let (name, fg, bg) = if let Some(c) = &scene.clips[t] {
|
||||||
|
let c = c.read().unwrap();
|
||||||
|
(c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb)
|
||||||
|
} else {
|
||||||
|
("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32))
|
||||||
|
};
|
||||||
|
let last = last_color.read().unwrap().clone();
|
||||||
|
let active = editing && selected_scene == Some(s) && selected_track == Some(t);
|
||||||
|
let editor = Thunk::new(||self.editor());
|
||||||
|
let cell = Thunk::new(move||phat_sel_3(
|
||||||
|
selected_track == Some(t) && selected_scene == Some(s),
|
||||||
|
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
||||||
|
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
||||||
|
if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(bg.into())
|
||||||
|
},
|
||||||
|
bg.into(),
|
||||||
|
bg.into(),
|
||||||
|
));
|
||||||
|
let cell = Either::new(active, editor, cell);
|
||||||
|
*last_color.write().unwrap() = bg.into();
|
||||||
|
map_south(
|
||||||
|
y1 as u16,
|
||||||
|
h + 1,
|
||||||
|
Fill::x(Fixed::y(h + 1, cell))
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed());
|
||||||
|
Fixed::x(w, map_east(x1 as u16, w, column))
|
||||||
|
}))).boxed()).into()
|
||||||
|
}
|
||||||
|
fn activate (&mut self) -> Usually<()> {
|
||||||
|
let selected = self.selected().clone();
|
||||||
|
match selected {
|
||||||
|
Selection::Scene(s) => {
|
||||||
|
let mut clips = vec![];
|
||||||
|
for (t, _) in self.tracks().iter().enumerate() {
|
||||||
|
clips.push(self.scenes()[s].clips[t].clone());
|
||||||
|
}
|
||||||
|
for (t, track) in self.tracks_mut().iter_mut().enumerate() {
|
||||||
|
if track.player.play_clip.is_some() || clips[t].is_some() {
|
||||||
|
track.player.enqueue_next(clips[t].as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.clock().is_stopped() {
|
||||||
|
self.clock().play_from(Some(0))?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Selection::Clip(t, s) => {
|
||||||
|
let clip = self.scenes()[s].clips[t].clone();
|
||||||
|
self.tracks_mut()[t].player.enqueue_next(clip.as_ref());
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
edn_provide!(u16: |self: App|{
|
handle!(TuiIn: |self: App, input| Ok(None));
|
||||||
":sidebar-w" => self.sidebar_w(),
|
#[derive(Clone, Debug)] pub enum AppCommand {
|
||||||
":sample-h" => if self.compact() { 0 } else { 5 },
|
Clear,
|
||||||
":samples-w" => if self.compact() { 4 } else { 11 },
|
Clip(ClipCommand),
|
||||||
":samples-y" => if self.compact() { 1 } else { 0 },
|
Clock(ClockCommand),
|
||||||
":pool-w" => if self.compact() { 5 } else {
|
Color(ItemPalette),
|
||||||
let w = self.size.w();
|
Compact(Option<bool>),
|
||||||
if w > 60 { 20 } else if w > 40 { 15 } else { 10 }
|
Editor(MidiEditCommand),
|
||||||
|
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
||||||
|
History(isize),
|
||||||
|
Pool(PoolCommand),
|
||||||
|
Sampler(SamplerCommand),
|
||||||
|
Scene(SceneCommand),
|
||||||
|
Select(Selection),
|
||||||
|
StopAll,
|
||||||
|
Track(TrackCommand),
|
||||||
|
Zoom(Option<usize>),
|
||||||
|
}
|
||||||
|
edn_command!(AppCommand: |state: App| {
|
||||||
|
("clear" [] Self::Clear)
|
||||||
|
("stop-all" [] Self::StopAll)
|
||||||
|
("compact" [c: bool ] Self::Compact(c))
|
||||||
|
("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default()))
|
||||||
|
("history" [d: isize] Self::History(d.unwrap_or(0)))
|
||||||
|
("zoom" [z: usize] Self::Zoom(z))
|
||||||
|
("select" [s: Selection] Self::Select(s.expect("no selection")))
|
||||||
|
("enqueue" [c: Arc<RwLock<MidiClip>>] Self::Enqueue(c))
|
||||||
|
("clock" [a, ..b] Self::Clock(ClockCommand::from_edn(state, &a.to_ref(), b)))
|
||||||
|
("track" [a, ..b] Self::Track(TrackCommand::from_edn(state, &a.to_ref(), b)))
|
||||||
|
("scene" [a, ..b] Self::Scene(SceneCommand::from_edn(state, &a.to_ref(), b)))
|
||||||
|
("clip" [a, ..b] Self::Clip(ClipCommand::from_edn(state, &a.to_ref(), b)))
|
||||||
|
("pool" [a, ..b] Self::Pool(PoolCommand::from_edn(state.pool.as_ref().expect("no pool"), &a.to_ref(), b)))
|
||||||
|
("editor" [a, ..b] Self::Editor(MidiEditCommand::from_edn(state.editor.as_ref().expect("no editor"), &a.to_ref(), b)))
|
||||||
|
("sampler" [a, ..b] Self::Sampler(SamplerCommand::from_edn(state.sampler.as_ref().expect("no sampler"), &a.to_ref(), b)))
|
||||||
|
});
|
||||||
|
command!(|self: AppCommand, state: App|match self {
|
||||||
|
Self::Clear => { todo!() },
|
||||||
|
Self::Zoom(_) => { todo!(); },
|
||||||
|
Self::History(delta) => { todo!("undo/redo") },
|
||||||
|
Self::Select(s) => { state.selected = s; None },
|
||||||
|
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
||||||
|
Self::Scene(cmd) => match cmd {
|
||||||
|
SceneCommand::Add => { state.scene_add(None, None)?; None }
|
||||||
|
SceneCommand::Del(index) => { state.scene_del(index); None },
|
||||||
|
SceneCommand::SetColor(index, color) => {
|
||||||
|
let old = state.scenes[index].color;
|
||||||
|
state.scenes[index].color = color;
|
||||||
|
Some(SceneCommand::SetColor(index, old))
|
||||||
|
},
|
||||||
|
SceneCommand::Enqueue(scene) => {
|
||||||
|
for track in 0..state.tracks.len() {
|
||||||
|
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
},
|
||||||
|
_ => None
|
||||||
|
}.map(Self::Scene),
|
||||||
|
Self::Track(cmd) => match cmd {
|
||||||
|
TrackCommand::Add => { state.track_add(None, None)?; None },
|
||||||
|
TrackCommand::Del(index) => { state.track_del(index); None },
|
||||||
|
TrackCommand::Stop(track) => { state.tracks[track].player.enqueue_next(None); None },
|
||||||
|
TrackCommand::SetColor(index, color) => {
|
||||||
|
let old = state.tracks[index].color;
|
||||||
|
state.tracks[index].color = color;
|
||||||
|
Some(TrackCommand::SetColor(index, old))
|
||||||
|
},
|
||||||
|
_ => None
|
||||||
|
}.map(Self::Track),
|
||||||
|
Self::Clip(cmd) => match cmd {
|
||||||
|
ClipCommand::Get(track, scene) => { todo!() },
|
||||||
|
ClipCommand::Put(track, scene, clip) => {
|
||||||
|
let old = state.scenes[scene].clips[track].clone();
|
||||||
|
state.scenes[scene].clips[track] = clip;
|
||||||
|
Some(ClipCommand::Put(track, scene, old))
|
||||||
|
},
|
||||||
|
ClipCommand::Enqueue(track, scene) => {
|
||||||
|
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
|
||||||
|
None
|
||||||
|
},
|
||||||
|
_ => None
|
||||||
|
}.map(Self::Clip),
|
||||||
|
Self::Editor(cmd) =>
|
||||||
|
state.editor.as_mut().map(|editor|cmd.delegate(editor, Self::Editor)).transpose()?.flatten(),
|
||||||
|
Self::Sampler(cmd) =>
|
||||||
|
state.sampler.as_mut().map(|sampler|cmd.delegate(sampler, Self::Sampler)).transpose()?.flatten(),
|
||||||
|
Self::Enqueue(clip) =>
|
||||||
|
state.player.as_mut().map(|player|{player.enqueue_next(clip.as_ref());None}).flatten(),
|
||||||
|
Self::StopAll => {
|
||||||
|
for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); }
|
||||||
|
None
|
||||||
|
},
|
||||||
|
Self::Color(palette) => {
|
||||||
|
let old = state.color;
|
||||||
|
state.color = palette;
|
||||||
|
Some(Self::Color(old))
|
||||||
|
},
|
||||||
|
Self::Pool(cmd) => if let Some(pool) = state.pool.as_mut() {
|
||||||
|
let undo = cmd.clone().delegate(pool, Self::Pool)?;
|
||||||
|
if let Some(editor) = state.editor.as_mut() {
|
||||||
|
match cmd {
|
||||||
|
// autoselect: automatically load selected clip in editor
|
||||||
|
// autocolor: update color in all places simultaneously
|
||||||
|
PoolCommand::Select(_) | PoolCommand::Clip(PoolClipCommand::SetColor(_, _)) =>
|
||||||
|
editor.set_clip(pool.clip().as_ref()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
undo
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
Self::Compact(compact) => match compact {
|
||||||
|
Some(compact) => {
|
||||||
|
if state.compact != compact {
|
||||||
|
state.compact = compact;
|
||||||
|
Some(Self::Compact(Some(!compact)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
state.compact = !state.compact;
|
||||||
|
Some(Self::Compact(Some(!state.compact)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
edn_provide!(bool: |self: App| { _ => return None });
|
|
||||||
edn_provide!(usize: |self: App| { _ => return None });
|
|
||||||
edn_provide!(isize: |self: App| { _ => return None });
|
|
||||||
edn_provide!(Color: |self: App| { _ => return None });
|
|
||||||
edn_provide!(Selection: |self: App| { _ => return None });
|
|
||||||
edn_provide!(Arc<RwLock<MidiClip>>: |self: App| { _ => return None });
|
|
||||||
edn_provide!(Option<Arc<RwLock<MidiClip>>>: |self: App| { _ => return None });
|
|
||||||
edn_provide!('a: Box<dyn Render<TuiOut> + 'a>: |self: App|{
|
|
||||||
":editor" => (&self.editor).boxed(),
|
|
||||||
":pool" => self.pool.as_ref().map(|pool|PoolView(self.compact(), pool)).boxed(),
|
|
||||||
":sample" => self.view_sample(self.is_editing()).boxed(),
|
|
||||||
":sampler" => self.view_sampler(self.is_editing(), &self.editor).boxed(),
|
|
||||||
":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(),
|
|
||||||
":toolbar" => ClockView::new(true, &self.clock).boxed(),
|
|
||||||
":tracks" => self.row(self.w(), 3, self.track_header(), self.track_cells()).boxed(),
|
|
||||||
":inputs" => self.row(self.w(), 3, self.input_header(), self.input_cells()).boxed(),
|
|
||||||
":outputs" => self.row(self.w(), 3, self.output_header(), self.output_cells()).boxed(),
|
|
||||||
":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16,
|
|
||||||
self.scene_header(), self.scene_cells(self.is_editing())).boxed(),
|
|
||||||
});
|
|
||||||
content!(TuiOut: |self: App| self.size.of(EdnView::from_source(self, self.edn.as_ref())));
|
|
||||||
handle!(TuiIn: |self: App, input| Ok(None));
|
|
||||||
#[derive(Debug, Default)] struct Meter<'a>(pub &'a str, pub f32);
|
|
||||||
content!(TuiOut: |self: Meter<'a>| col!(
|
|
||||||
Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)),
|
|
||||||
Fixed::xy(if self.1 >= 0.0 { 13 }
|
|
||||||
else if self.1 >= -1.0 { 12 }
|
|
||||||
else if self.1 >= -2.0 { 11 }
|
|
||||||
else if self.1 >= -3.0 { 10 }
|
|
||||||
else if self.1 >= -4.0 { 9 }
|
|
||||||
else if self.1 >= -6.0 { 8 }
|
|
||||||
else if self.1 >= -9.0 { 7 }
|
|
||||||
else if self.1 >= -12.0 { 6 }
|
|
||||||
else if self.1 >= -15.0 { 5 }
|
|
||||||
else if self.1 >= -20.0 { 4 }
|
|
||||||
else if self.1 >= -25.0 { 3 }
|
|
||||||
else if self.1 >= -30.0 { 2 }
|
|
||||||
else if self.1 >= -40.0 { 1 }
|
|
||||||
else { 0 }, 1, Tui::bg(if self.1 >= 0.0 { Color::Red }
|
|
||||||
else if self.1 >= -3.0 { Color::Yellow }
|
|
||||||
else { Color::Green }, ()))));
|
|
||||||
#[derive(Debug, Default)] struct Meters<'a>(pub &'a[f32]);
|
|
||||||
content!(TuiOut: |self: Meters<'a>| col!(
|
|
||||||
format!("L/{:>+9.3}", self.0[0]),
|
|
||||||
format!("R/{:>+9.3}", self.0[1])
|
|
||||||
));
|
|
||||||
/// Represents the current user selection in the arranger
|
/// Represents the current user selection in the arranger
|
||||||
#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection {
|
#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection {
|
||||||
/// The whole mix is selected
|
/// The whole mix is selected
|
||||||
|
|
@ -209,6 +569,25 @@ impl Track {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[derive(Clone, Debug)] pub enum TrackCommand {
|
||||||
|
Add,
|
||||||
|
Del(usize),
|
||||||
|
Stop(usize),
|
||||||
|
Swap(usize, usize),
|
||||||
|
SetSize(usize),
|
||||||
|
SetZoom(usize),
|
||||||
|
SetColor(usize, ItemPalette),
|
||||||
|
}
|
||||||
|
edn_command!(TrackCommand: |state: App| {
|
||||||
|
("add" [] Self::Add)
|
||||||
|
("size" [a: usize] Self::SetSize(a.unwrap()))
|
||||||
|
("zoom" [a: usize] Self::SetZoom(a.unwrap()))
|
||||||
|
("color" [a: usize] Self::SetColor(a.unwrap(), ItemPalette::random()))
|
||||||
|
("del" [a: usize] Self::Del(a.unwrap()))
|
||||||
|
("stop" [a: usize] Self::Stop(a.unwrap()))
|
||||||
|
("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap()))
|
||||||
|
});
|
||||||
|
command!(|self: TrackCommand, state: App|match self { _ => todo!("track command") });
|
||||||
impl HasTracks for App {
|
impl HasTracks for App {
|
||||||
fn midi_ins (&self) -> &Vec<JackPort<MidiIn>> { &self.midi_ins }
|
fn midi_ins (&self) -> &Vec<JackPort<MidiIn>> { &self.midi_ins }
|
||||||
fn midi_outs (&self) -> &Vec<JackPort<MidiOut>> { &self.midi_outs }
|
fn midi_outs (&self) -> &Vec<JackPort<MidiOut>> { &self.midi_outs }
|
||||||
|
|
@ -449,202 +828,6 @@ pub trait HasScenes: HasSelection + HasEditor + Send + Sync {
|
||||||
}).into()
|
}).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl App {
|
|
||||||
pub fn sequencer (
|
|
||||||
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
|
|
||||||
player: Option<MidiPlayer>, midi_froms: &[PortConnection], midi_tos: &[PortConnection],
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
edn: include_str!("./sequencer-view.edn").to_string(),
|
|
||||||
jack: jack.clone(),
|
|
||||||
pool: Some(pool),
|
|
||||||
editor: Some(editor),
|
|
||||||
player: player,
|
|
||||||
editing: false.into(),
|
|
||||||
midi_buf: vec![vec![];65536],
|
|
||||||
color: ItemPalette::random(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn groovebox (
|
|
||||||
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
|
|
||||||
player: Option<MidiPlayer>, midi_froms: &[PortConnection], midi_tos: &[PortConnection],
|
|
||||||
sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]],
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
edn: include_str!("./groovebox-view.edn").to_string(),
|
|
||||||
sampler: Some(sampler),
|
|
||||||
..Self::sequencer(
|
|
||||||
jack, pool, editor,
|
|
||||||
player, midi_froms, midi_tos
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn arranger (
|
|
||||||
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
|
|
||||||
midi_froms: &[PortConnection], midi_tos: &[PortConnection],
|
|
||||||
sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]],
|
|
||||||
scenes: usize, tracks: usize, track_width: usize,
|
|
||||||
) -> Self {
|
|
||||||
let mut arranger = Self {
|
|
||||||
edn: include_str!("./arranger-view.edn").to_string(),
|
|
||||||
..Self::groovebox(
|
|
||||||
jack, pool, editor,
|
|
||||||
None, midi_froms, midi_tos,
|
|
||||||
sampler, audio_froms, audio_tos
|
|
||||||
)
|
|
||||||
};
|
|
||||||
arranger.scenes_add(scenes);
|
|
||||||
arranger.tracks_add(tracks, track_width, &[], &[]);
|
|
||||||
arranger
|
|
||||||
}
|
|
||||||
fn compact (&self) -> bool { false }
|
|
||||||
fn editor (&self) -> impl Content<TuiOut> + '_ { &self.editor }
|
|
||||||
fn w (&self) -> u16 { self.tracks_sizes(self.is_editing(), self.editor_w()).last().map(|x|x.3 as u16).unwrap_or(0) }
|
|
||||||
fn pool (&self) -> impl Content<TuiOut> + use<'_> { self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) }
|
|
||||||
fn row <'a> (
|
|
||||||
&'a self, w: u16, h: u16, a: impl Content<TuiOut> + 'a, b: impl Content<TuiOut> + 'a
|
|
||||||
) -> impl Content<TuiOut> + 'a {
|
|
||||||
Fixed::y(h, Bsp::e(
|
|
||||||
Fixed::xy(self.sidebar_w() as u16, h, a),
|
|
||||||
Fill::x(Align::c(Fixed::xy(w, h, b)))
|
|
||||||
))
|
|
||||||
}
|
|
||||||
fn is_editing (&self) -> bool {
|
|
||||||
self.editing.load(Relaxed)
|
|
||||||
}
|
|
||||||
fn sidebar_w (&self) -> u16 {
|
|
||||||
let w = self.size.w();
|
|
||||||
let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
|
||||||
let w = if self.is_editing() { 8 } else { w };
|
|
||||||
w
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug)] pub enum AppCommand {
|
|
||||||
Clear,
|
|
||||||
Clip(ClipCommand),
|
|
||||||
Clock(ClockCommand),
|
|
||||||
Color(ItemPalette),
|
|
||||||
Compact(Option<bool>),
|
|
||||||
Editor(MidiEditCommand),
|
|
||||||
Enqueue(Option<Arc<RwLock<MidiClip>>>),
|
|
||||||
History(isize),
|
|
||||||
Pool(PoolCommand),
|
|
||||||
Sampler(SamplerCommand),
|
|
||||||
Scene(SceneCommand),
|
|
||||||
Select(Selection),
|
|
||||||
StopAll,
|
|
||||||
Track(TrackCommand),
|
|
||||||
Zoom(Option<usize>),
|
|
||||||
}
|
|
||||||
edn_command!(AppCommand: |state: App| {
|
|
||||||
("clear" [] Self::Clear)
|
|
||||||
("stop-all" [] Self::StopAll)
|
|
||||||
("compact" [c: bool ] Self::Compact(c))
|
|
||||||
("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default()))
|
|
||||||
("history" [d: isize] Self::History(d.unwrap_or(0)))
|
|
||||||
("zoom" [z: usize] Self::Zoom(z))
|
|
||||||
("select" [s: Selection] Self::Select(s.expect("no selection")))
|
|
||||||
("enqueue" [c: Arc<RwLock<MidiClip>>] Self::Enqueue(c))
|
|
||||||
("clock" [a, ..b] Self::Clock(ClockCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("track" [a, ..b] Self::Track(TrackCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("scene" [a, ..b] Self::Scene(SceneCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("clip" [a, ..b] Self::Clip(ClipCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("pool" [a, ..b] Self::Pool(PoolCommand::from_edn(state.pool.as_ref().expect("no pool"), &a.to_ref(), b)))
|
|
||||||
("editor" [a, ..b] Self::Editor(MidiEditCommand::from_edn(state.editor.as_ref().expect("no editor"), &a.to_ref(), b)))
|
|
||||||
("sampler" [a, ..b] Self::Sampler(SamplerCommand::from_edn(state.sampler.as_ref().expect("no sampler"), &a.to_ref(), b)))
|
|
||||||
});
|
|
||||||
command!(|self: AppCommand, state: App|match self {
|
|
||||||
Self::Clear => { todo!() },
|
|
||||||
Self::Zoom(_) => { todo!(); },
|
|
||||||
Self::History(delta) => { todo!("undo/redo") },
|
|
||||||
Self::Select(s) => { state.selected = s; None },
|
|
||||||
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
|
|
||||||
Self::Scene(cmd) => match cmd {
|
|
||||||
SceneCommand::Add => { state.scene_add(None, None)?; None }
|
|
||||||
SceneCommand::Del(index) => { state.scene_del(index); None },
|
|
||||||
SceneCommand::SetColor(index, color) => {
|
|
||||||
let old = state.scenes[index].color;
|
|
||||||
state.scenes[index].color = color;
|
|
||||||
Some(SceneCommand::SetColor(index, old))
|
|
||||||
},
|
|
||||||
SceneCommand::Enqueue(scene) => {
|
|
||||||
for track in 0..state.tracks.len() {
|
|
||||||
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
|
|
||||||
}
|
|
||||||
None
|
|
||||||
},
|
|
||||||
_ => None
|
|
||||||
}.map(Self::Scene),
|
|
||||||
Self::Track(cmd) => match cmd {
|
|
||||||
TrackCommand::Add => { state.track_add(None, None)?; None },
|
|
||||||
TrackCommand::Del(index) => { state.track_del(index); None },
|
|
||||||
TrackCommand::Stop(track) => { state.tracks[track].player.enqueue_next(None); None },
|
|
||||||
TrackCommand::SetColor(index, color) => {
|
|
||||||
let old = state.tracks[index].color;
|
|
||||||
state.tracks[index].color = color;
|
|
||||||
Some(TrackCommand::SetColor(index, old))
|
|
||||||
},
|
|
||||||
_ => None
|
|
||||||
}.map(Self::Track),
|
|
||||||
Self::Clip(cmd) => match cmd {
|
|
||||||
ClipCommand::Get(track, scene) => { todo!() },
|
|
||||||
ClipCommand::Put(track, scene, clip) => {
|
|
||||||
let old = state.scenes[scene].clips[track].clone();
|
|
||||||
state.scenes[scene].clips[track] = clip;
|
|
||||||
Some(ClipCommand::Put(track, scene, old))
|
|
||||||
},
|
|
||||||
ClipCommand::Enqueue(track, scene) => {
|
|
||||||
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
|
|
||||||
None
|
|
||||||
},
|
|
||||||
_ => None
|
|
||||||
}.map(Self::Clip),
|
|
||||||
Self::Editor(cmd) =>
|
|
||||||
state.editor.as_mut().map(|editor|cmd.delegate(editor, Self::Editor)).transpose()?.flatten(),
|
|
||||||
Self::Sampler(cmd) =>
|
|
||||||
state.sampler.as_mut().map(|sampler|cmd.delegate(sampler, Self::Sampler)).transpose()?.flatten(),
|
|
||||||
Self::Enqueue(clip) =>
|
|
||||||
state.player.as_mut().map(|player|{player.enqueue_next(clip.as_ref());None}).flatten(),
|
|
||||||
Self::StopAll => {
|
|
||||||
for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); }
|
|
||||||
None
|
|
||||||
},
|
|
||||||
Self::Color(palette) => {
|
|
||||||
let old = state.color;
|
|
||||||
state.color = palette;
|
|
||||||
Some(Self::Color(old))
|
|
||||||
},
|
|
||||||
Self::Pool(cmd) => if let Some(pool) = state.pool.as_mut() {
|
|
||||||
let undo = cmd.clone().delegate(pool, Self::Pool)?;
|
|
||||||
if let Some(editor) = state.editor.as_mut() {
|
|
||||||
match cmd {
|
|
||||||
// autoselect: automatically load selected clip in editor
|
|
||||||
// autocolor: update color in all places simultaneously
|
|
||||||
PoolCommand::Select(_) | PoolCommand::Clip(PoolClipCommand::SetColor(_, _)) =>
|
|
||||||
editor.set_clip(pool.clip().as_ref()),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
undo
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
Self::Compact(compact) => match compact {
|
|
||||||
Some(compact) => {
|
|
||||||
if state.compact != compact {
|
|
||||||
state.compact = compact;
|
|
||||||
Some(Self::Compact(Some(!compact)))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
state.compact = !state.compact;
|
|
||||||
Some(Self::Compact(Some(!state.compact)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
#[derive(Clone, Debug)] pub enum ClipCommand {
|
#[derive(Clone, Debug)] pub enum ClipCommand {
|
||||||
Get(usize, usize),
|
Get(usize, usize),
|
||||||
Put(usize, usize, Option<Arc<RwLock<MidiClip>>>),
|
Put(usize, usize, Option<Arc<RwLock<MidiClip>>>),
|
||||||
|
|
@ -654,45 +837,20 @@ command!(|self: AppCommand, state: App|match self {
|
||||||
SetColor(usize, usize, ItemPalette),
|
SetColor(usize, usize, ItemPalette),
|
||||||
}
|
}
|
||||||
edn_command!(ClipCommand: |state: App| {
|
edn_command!(ClipCommand: |state: App| {
|
||||||
("get" [a: usize
|
("get" [a: usize ,b: usize]
|
||||||
,b: usize] Self::Get(a.unwrap(), b.unwrap()))
|
Self::Get(a.unwrap(), b.unwrap()))
|
||||||
|
("put" [a: usize, b: usize, c: Option<Arc<RwLock<MidiClip>>>]
|
||||||
("put" [a: usize
|
Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))
|
||||||
,b: usize
|
("enqueue" [a: usize, b: usize]
|
||||||
,c: Option<Arc<RwLock<MidiClip>>>] Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))
|
Self::Enqueue(a.unwrap(), b.unwrap()))
|
||||||
|
("edit" [a: Option<Arc<RwLock<MidiClip>>>]
|
||||||
("enqueue" [a: usize
|
Self::Edit(a.unwrap()))
|
||||||
,b: usize] Self::Enqueue(a.unwrap(), b.unwrap()))
|
("loop" [a: usize, b: usize, c: bool]
|
||||||
|
Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))
|
||||||
("edit" [a: Option<Arc<RwLock<MidiClip>>>] Self::Edit(a.unwrap()))
|
("color" [a: usize, b: usize]
|
||||||
|
Self::SetColor(a.unwrap(), b.unwrap(), ItemPalette::random()))
|
||||||
("loop" [a: usize
|
|
||||||
,b: usize
|
|
||||||
,c: bool] Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))
|
|
||||||
|
|
||||||
("color" [a: usize
|
|
||||||
,b: usize] Self::SetColor(a.unwrap(), b.unwrap(), ItemPalette::random()))
|
|
||||||
});
|
});
|
||||||
command!(|self: ClipCommand, state: App|match self { _ => todo!("clip command") });
|
command!(|self: ClipCommand, state: App|match self { _ => todo!("clip command") });
|
||||||
#[derive(Clone, Debug)] pub enum TrackCommand {
|
|
||||||
Add,
|
|
||||||
Del(usize),
|
|
||||||
Stop(usize),
|
|
||||||
Swap(usize, usize),
|
|
||||||
SetSize(usize),
|
|
||||||
SetZoom(usize),
|
|
||||||
SetColor(usize, ItemPalette),
|
|
||||||
}
|
|
||||||
edn_command!(TrackCommand: |state: App| {
|
|
||||||
("add" [] Self::Add)
|
|
||||||
("size" [a: usize] Self::SetSize(a.unwrap()))
|
|
||||||
("zoom" [a: usize] Self::SetZoom(a.unwrap()))
|
|
||||||
("color" [a: usize] Self::SetColor(a.unwrap(), ItemPalette::random()))
|
|
||||||
("del" [a: usize] Self::Del(a.unwrap()))
|
|
||||||
("stop" [a: usize] Self::Stop(a.unwrap()))
|
|
||||||
("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap()))
|
|
||||||
});
|
|
||||||
command!(|self: TrackCommand, state: App|match self { _ => todo!("track command") });
|
|
||||||
audio!(|self: App, client, scope|{
|
audio!(|self: App, client, scope|{
|
||||||
// Start profiling cycle
|
// Start profiling cycle
|
||||||
let t0 = self.perf.get_t0();
|
let t0 = self.perf.get_t0();
|
||||||
|
|
@ -815,170 +973,30 @@ fn help_tag <'a> (before: &'a str, key: &'a str, after: &'a str) -> impl Content
|
||||||
let hi = TuiTheme::orange();
|
let hi = TuiTheme::orange();
|
||||||
Tui::bold(true, row!(Tui::fg(lo, before), Tui::fg(hi, key), Tui::fg(lo, after)))
|
Tui::bold(true, row!(Tui::fg(lo, before), Tui::fg(hi, key), Tui::fg(lo, after)))
|
||||||
}
|
}
|
||||||
impl<T> Arrangement for T where T:
|
#[derive(Debug, Default)] struct Meter<'a>(pub &'a str, pub f32);
|
||||||
HasEditor + HasTracks + HasScenes + HasSelection + HasClock + HasJack {}
|
content!(TuiOut: |self: Meter<'a>| col!(
|
||||||
pub trait Arrangement: HasEditor + HasTracks + HasScenes + HasSelection + HasClock + HasJack {
|
Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)),
|
||||||
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
|
Fixed::xy(if self.1 >= 0.0 { 13 }
|
||||||
self.scene()?.clips.get(self.selected().track()?)?.clone()
|
else if self.1 >= -1.0 { 12 }
|
||||||
}
|
else if self.1 >= -2.0 { 11 }
|
||||||
fn toggle_loop (&mut self) {
|
else if self.1 >= -3.0 { 10 }
|
||||||
if let Some(clip) = self.clip() {
|
else if self.1 >= -4.0 { 9 }
|
||||||
clip.write().unwrap().toggle_loop()
|
else if self.1 >= -6.0 { 8 }
|
||||||
}
|
else if self.1 >= -9.0 { 7 }
|
||||||
}
|
else if self.1 >= -12.0 { 6 }
|
||||||
//fn randomize_color (&mut self) {
|
else if self.1 >= -15.0 { 5 }
|
||||||
//match self.selected {
|
else if self.1 >= -20.0 { 4 }
|
||||||
//Selection::Mix => { self.color = ItemPalette::random() },
|
else if self.1 >= -25.0 { 3 }
|
||||||
//Selection::Track(t) => { self.tracks[t].color = ItemPalette::random() },
|
else if self.1 >= -30.0 { 2 }
|
||||||
//Selection::Scene(s) => { self.scenes[s].color = ItemPalette::random() },
|
else if self.1 >= -40.0 { 1 }
|
||||||
//Selection::Clip(t, s) => if let Some(clip) = &self.scenes[s].clips[t] {
|
else { 0 }, 1, Tui::bg(if self.1 >= 0.0 { Color::Red }
|
||||||
//clip.write().unwrap().color = ItemPalette::random();
|
else if self.1 >= -3.0 { Color::Yellow }
|
||||||
//}
|
else { Color::Green }, ()))));
|
||||||
//}
|
#[derive(Debug, Default)] struct Meters<'a>(pub &'a[f32]);
|
||||||
//}
|
content!(TuiOut: |self: Meters<'a>| col!(
|
||||||
fn track_add (
|
format!("L/{:>+9.3}", self.0[0]),
|
||||||
&mut self,
|
format!("R/{:>+9.3}", self.0[1])
|
||||||
name: Option<&str>,
|
));
|
||||||
color: Option<ItemPalette>
|
|
||||||
) -> Usually<&mut Track> {
|
|
||||||
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
|
|
||||||
let track = Track {
|
|
||||||
width: (name.len() + 2).max(9),
|
|
||||||
color: color.unwrap_or_else(ItemPalette::random),
|
|
||||||
player: MidiPlayer::from(self.clock()),
|
|
||||||
name,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
self.tracks_mut().push(track);
|
|
||||||
let len = self.tracks().len();
|
|
||||||
let index = len - 1;
|
|
||||||
for scene in self.scenes_mut().iter_mut() {
|
|
||||||
while scene.clips.len() < len {
|
|
||||||
scene.clips.push(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(&mut self.tracks_mut()[index])
|
|
||||||
}
|
|
||||||
fn tracks_add (
|
|
||||||
&mut self,
|
|
||||||
count: usize,
|
|
||||||
width: usize,
|
|
||||||
midi_from: &[PortConnection],
|
|
||||||
midi_to: &[PortConnection],
|
|
||||||
) -> Usually<()> {
|
|
||||||
let jack = self.jack().clone();
|
|
||||||
let track_color_1 = ItemColor::random();
|
|
||||||
let track_color_2 = ItemColor::random();
|
|
||||||
for i in 0..count {
|
|
||||||
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
|
|
||||||
let mut track = self.track_add(None, Some(color))?;
|
|
||||||
track.width = width;
|
|
||||||
let port = JackPort::<MidiIn>::new(&jack, &format!("{}I", &track.name), midi_from)?;
|
|
||||||
track.player.midi_ins.push(port);
|
|
||||||
let port = JackPort::<MidiOut>::new(&jack, &format!("{}O", &track.name), midi_to)?;
|
|
||||||
track.player.midi_outs.push(port);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
fn track_del (&mut self, index: usize) {
|
|
||||||
self.tracks_mut().remove(index);
|
|
||||||
for scene in self.scenes_mut().iter_mut() {
|
|
||||||
scene.clips.remove(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
|
|
||||||
-> Usually<&mut Scene>
|
|
||||||
{
|
|
||||||
let scene = Scene {
|
|
||||||
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
|
|
||||||
clips: vec![None;self.tracks().len()],
|
|
||||||
color: color.unwrap_or_else(ItemPalette::random),
|
|
||||||
};
|
|
||||||
self.scenes_mut().push(scene);
|
|
||||||
let index = self.scenes().len() - 1;
|
|
||||||
Ok(&mut self.scenes_mut()[index])
|
|
||||||
}
|
|
||||||
fn scenes_add (&mut self, n: usize) -> Usually<()> {
|
|
||||||
let scene_color_1 = ItemColor::random();
|
|
||||||
let scene_color_2 = ItemColor::random();
|
|
||||||
for i in 0..n {
|
|
||||||
let _scene = self.scene_add(None, Some(
|
|
||||||
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
fn scene_cells <'a> (&'a self, editing: bool) -> BoxThunk<'a, TuiOut> {
|
|
||||||
let tracks = move||self.tracks_sizes(self.is_editing(), self.editor_w());
|
|
||||||
let scenes = ||self.scenes_sizes(self.is_editing(), 2, 15);
|
|
||||||
let selected_track = self.selected().track();
|
|
||||||
let selected_scene = self.selected().scene();
|
|
||||||
(move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| {
|
|
||||||
let w = (x2 - x1) as u16;
|
|
||||||
let color: ItemPalette = track.color.dark.into();
|
|
||||||
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
|
|
||||||
let cells = Map::new(scenes, move|(_, scene, y1, y2), s| {
|
|
||||||
let h = (y2 - y1) as u16;
|
|
||||||
let color = scene.color;
|
|
||||||
let (name, fg, bg) = if let Some(c) = &scene.clips[t] {
|
|
||||||
let c = c.read().unwrap();
|
|
||||||
(c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb)
|
|
||||||
} else {
|
|
||||||
("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32))
|
|
||||||
};
|
|
||||||
let last = last_color.read().unwrap().clone();
|
|
||||||
let active = editing && selected_scene == Some(s) && selected_track == Some(t);
|
|
||||||
let editor = Thunk::new(||self.editor());
|
|
||||||
let cell = Thunk::new(move||phat_sel_3(
|
|
||||||
selected_track == Some(t) && selected_scene == Some(s),
|
|
||||||
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
|
||||||
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
|
|
||||||
if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(bg.into())
|
|
||||||
},
|
|
||||||
bg.into(),
|
|
||||||
bg.into(),
|
|
||||||
));
|
|
||||||
let cell = Either::new(active, editor, cell);
|
|
||||||
*last_color.write().unwrap() = bg.into();
|
|
||||||
map_south(
|
|
||||||
y1 as u16,
|
|
||||||
h + 1,
|
|
||||||
Fill::x(Fixed::y(h + 1, cell))
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed());
|
|
||||||
Fixed::x(w, map_east(x1 as u16, w, column))
|
|
||||||
}))).boxed()).into()
|
|
||||||
}
|
|
||||||
fn activate (&mut self) -> Usually<()> {
|
|
||||||
let selected = self.selected().clone();
|
|
||||||
match selected {
|
|
||||||
Selection::Scene(s) => {
|
|
||||||
let mut clips = vec![];
|
|
||||||
for (t, _) in self.tracks().iter().enumerate() {
|
|
||||||
clips.push(self.scenes()[s].clips[t].clone());
|
|
||||||
}
|
|
||||||
for (t, track) in self.tracks_mut().iter_mut().enumerate() {
|
|
||||||
if track.player.play_clip.is_some() || clips[t].is_some() {
|
|
||||||
track.player.enqueue_next(clips[t].as_ref());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.clock().is_stopped() {
|
|
||||||
self.clock().play_from(Some(0))?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Selection::Clip(t, s) => {
|
|
||||||
let clip = self.scenes()[s].clips[t].clone();
|
|
||||||
self.tracks_mut()[t].player.enqueue_next(clip.as_ref());
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(test)] fn test_tek () {
|
#[cfg(test)] fn test_tek () {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
|
||||||
0
tek/src/sequencer-keys.edn
Normal file
0
tek/src/sequencer-keys.edn
Normal file
Loading…
Add table
Add a link
Reference in a new issue