separate render/content macros; add has_jack

This commit is contained in:
🪞👃🪞 2025-01-14 12:41:27 +01:00
parent 08184f9906
commit e62e36d558
19 changed files with 183 additions and 226 deletions

View file

@ -1,4 +1,8 @@
use crate::*; use crate::*;
/// Turns an EDN item sequence into a command enum variant.
pub trait EdnCommand<C>: Command<C> {
fn from_edn <'a> (state: &C, head: &EdnItem<&str>, tail: &'a [EdnItem<String>]) -> Self;
}
/** Implement `EdnCommand` for given `State` and `Command` */ /** Implement `EdnCommand` for given `State` and `Command` */
#[macro_export] macro_rules! edn_command { #[macro_export] macro_rules! edn_command {
($Command:ty : |$state:ident:$State:ty| { $(( ($Command:ty : |$state:ident:$State:ty| { $((
@ -47,16 +51,3 @@ use crate::*;
let $arg: $type = EdnProvide::<$type>::get_or_fail($state, $arg); let $arg: $type = EdnProvide::<$type>::get_or_fail($state, $arg);
}; };
} }
/// Turns an EDN symbol sequence into a command enum variant.
pub trait EdnCommand<C>: Command<C> {
fn from_edn <'a> (state: &C, head: &EdnItem<&str>, tail: &'a [EdnItem<String>]) -> Self;
fn get_isize (state: &C, item: &EdnItem<&str>) -> Option<isize> {
None
}
fn get_usize (state: &C, item: &EdnItem<&str>) -> Option<usize> {
None
}
fn get_bool (state: &C, item: &EdnItem<&str>) -> Option<usize> {
None
}
}

View file

@ -1,5 +1,4 @@
use crate::*; use crate::*;
use EdnItem::*;
pub trait EdnInput: Input { pub trait EdnInput: Input {
fn matches (&self, token: &str) -> bool; fn matches (&self, token: &str) -> bool;

View file

@ -1,13 +0,0 @@
use crate::*;
/// Implement [TryFrom<&Arc<RwLock<JackConnection>>>]: create app state from wrapped JACK handle.
#[macro_export] macro_rules! from_jack {
(|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc<RwLock<JackConnection>>> for $Struct $(<$($L),*$($T),*>)? {
type Error = Box<dyn std::error::Error>;
fn try_from ($jack: &Arc<RwLock<JackConnection>>) -> Usually<Self> {
Ok($cb)
}
}
};
}

View file

@ -3,3 +3,22 @@ use crate::*;
pub trait HasJack { pub trait HasJack {
fn jack (&self) -> &Arc<RwLock<JackConnection>>; fn jack (&self) -> &Arc<RwLock<JackConnection>>;
} }
/// Implement [HasJack].
#[macro_export] macro_rules! has_jack {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasJack for $Struct $(<$($L),*$($T),*>)? {
fn jack (&$self) -> &Arc<RwLock<JackConnection>> { $cb }
}
};
}
/// Implement [TryFrom<&Arc<RwLock<JackConnection>>>]: create app state from wrapped JACK handle.
#[macro_export] macro_rules! from_jack {
(|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc<RwLock<JackConnection>>> for $Struct $(<$($L),*$($T),*>)? {
type Error = Box<dyn std::error::Error>;
fn try_from ($jack: &Arc<RwLock<JackConnection>>) -> Usually<Self> { Ok($cb) }
}
};
}

View file

@ -10,7 +10,6 @@ pub(crate) use ::jack::{
Unowned, MidiIn, MidiOut, AudioIn, AudioOut, Unowned, MidiIn, MidiOut, AudioIn, AudioOut,
}; };
mod from_jack; pub use self::from_jack::*;
mod has_jack; pub use self::has_jack::*; mod has_jack; pub use self::has_jack::*;
mod jack_audio; pub use self::jack_audio::*; mod jack_audio; pub use self::jack_audio::*;
mod jack_connect; pub use self::jack_connect::*; mod jack_connect; pub use self::jack_connect::*;

View file

@ -1,6 +1,6 @@
use crate::*; use crate::*;
has_size!(<TuiOut>|self: MidiEditor|&self.size); has_size!(<TuiOut>|self: MidiEditor|&self.size);
render!(TuiOut: (self: MidiEditor) => { content!(TuiOut: |self: MidiEditor| {
self.autoscroll(); self.autoscroll();
//self.autozoom(); //self.autozoom();
self.size.of(&self.mode) self.size.of(&self.mode)
@ -17,7 +17,6 @@ impl MidiEditor {
FieldV(color, "Loop", looped.to_string()) FieldV(color, "Loop", looped.to_string())
) )
} }
pub fn edit_status (&self) -> impl Content<TuiOut> + '_ { pub fn edit_status (&self) -> impl Content<TuiOut> + '_ {
let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { let (color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) {
(clip.color, clip.length) (clip.color, clip.length)

View file

@ -1,6 +1,6 @@
use crate::*; use crate::*;
pub struct PoolView<'a>(pub bool, pub &'a MidiPool); pub struct PoolView<'a>(pub bool, pub &'a MidiPool);
render!(TuiOut: (self: PoolView<'a>) => { content!(TuiOut: |self: PoolView<'a>| {
let Self(compact, model) = self; let Self(compact, model) = self;
let MidiPool { clips, mode, .. } = self.1; let MidiPool { clips, mode, .. } = self.1;
let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||TuiTheme::g(32).into()); let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||TuiTheme::g(32).into());
@ -24,7 +24,7 @@ render!(TuiOut: (self: PoolView<'a>) => {
)))) ))))
}))))) })))))
}); });
render!(TuiOut: (self: ClipLength) => { content!(TuiOut: |self: ClipLength| {
let bars = ||self.bars_string(); let bars = ||self.bars_string();
let beats = ||self.beats_string(); let beats = ||self.beats_string();
let ticks = ||self.ticks_string(); let ticks = ||self.ticks_string();

View file

@ -1,17 +1,16 @@
use crate::*; use crate::*;
pub struct ClipSelected { pub struct ClipSelected {
pub(crate) title: &'static str, pub(crate) title: &'static str,
pub(crate) name: Arc<str>, pub(crate) name: Arc<str>,
pub(crate) color: ItemPalette, pub(crate) color: ItemPalette,
pub(crate) time: Arc<str>, pub(crate) time: Arc<str>,
} }
content!(TuiOut: |self: ClipSelected|FieldV(
render!(TuiOut: (self: ClipSelected) => self.color,
FieldV(self.color, self.title, format!("{} {}", self.time, self.name))); self.title,
format!("{} {}", self.time, self.name)
));
impl ClipSelected { impl ClipSelected {
/// Shows currently playing clip with beats elapsed /// Shows currently playing clip with beats elapsed
pub fn play_clip <T: HasPlayClip + HasClock> (state: &T) -> Self { pub fn play_clip <T: HasPlayClip + HasClock> (state: &T) -> Self {
let (name, color) = if let Some((_, Some(clip))) = state.play_clip() { let (name, color) = if let Some((_, Some(clip))) = state.play_clip() {
@ -31,7 +30,6 @@ impl ClipSelected {
.unwrap_or_else(||String::from(" ")).into() .unwrap_or_else(||String::from(" ")).into()
} }
} }
/// Shows next clip with beats remaining until switchover /// Shows next clip with beats remaining until switchover
pub fn next_clip <T: HasPlayClip> (state: &T) -> Self { pub fn next_clip <T: HasPlayClip> (state: &T) -> Self {
let mut time: Arc<str> = String::from("--.-.--").into(); let mut time: Arc<str> = String::from("--.-.--").into();
@ -69,5 +67,4 @@ impl ClipSelected {
}; };
Self { title: "Next", time, name, color, } Self { title: "Next", time, name, color, }
} }
} }

View file

@ -40,7 +40,7 @@ impl PianoHorizontal {
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator<Item=(usize, u16, usize)> { 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)) (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
} }
render!(TuiOut: (self: PianoHorizontal) => Tui::bg(TuiTheme::g(40), Bsp::s( content!(TuiOut:|self: PianoHorizontal| Tui::bg(TuiTheme::g(40), Bsp::s(
Bsp::e( Bsp::e(
Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())), Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())),
self.timeline() self.timeline()

View file

@ -1,46 +0,0 @@
use crate::*;
/// Build a [Render]able out of other [Render]ables,
/// then apply optional custom render/layout on top.
pub trait Content<E: Output>: Send + Sync + Sized {
fn content (&self) -> impl Render<E> { () }
fn layout (&self, area: E::Area) -> E::Area { self.content().layout(area) }
fn render (&self, output: &mut E) { self.content().render(output) }
}
impl<E: Output, C: Content<E>> Content<E> for &C {
fn content (&self) -> impl Render<E> { (*self).content() }
fn layout (&self, area: E::Area) -> E::Area { (*self).layout(area) }
fn render (&self, output: &mut E) { (*self).render(output) }
}
/// The platonic ideal unit of [Content]: total emptiness at dead center.
impl<E: Output> Content<E> for () {
fn layout (&self, area: E::Area) -> E::Area { area.center().to_area_pos().into() }
fn render (&self, _: &mut E) {}
}
impl<E: Output, T: Content<E>> Content<E> for Option<T> {
fn content (&self) -> impl Render<E> {
self.as_ref()
}
fn layout (&self, area: E::Area) -> E::Area {
self.as_ref()
.map(|content|content.layout(area))
.unwrap_or([0.into(), 0.into(), 0.into(), 0.into(),].into())
}
fn render (&self, output: &mut E) {
self.as_ref()
.map(|content|content.render(output));
}
}
//impl<E: Output, T: Content<E>, E: Content<E>> Content<E> for Option<T> {
//fn content (&self) -> impl Render<E> {
//self.as_ref()
//}
//fn layout (&self, area: E::Area) -> E::Area {
//self.as_ref()
//.map(|content|content.layout(area))
//.unwrap_or([0.into(), 0.into(), 0.into(), 0.into(),].into())
//}
//fn render (&self, output: &mut E) {
//self.as_ref()
//.map(|content|content.render(output));
//}
//}

View file

@ -94,7 +94,6 @@ impl<E: Output, A: Content<E>, B: Content<E>> BspAreas<E, A, B> for Bsp<E, A, B>
impl<'a, E: Output + 'a, T: EdnViewData<'a, E>> TryFromEdn<'a, T> for Bsp<E, RenderBox<'a, E>, RenderBox<'a, E>> { impl<'a, E: Output + 'a, T: EdnViewData<'a, E>> TryFromEdn<'a, T> for Bsp<E, RenderBox<'a, E>, RenderBox<'a, E>> {
fn try_from_edn (s: &'a T, head: &EdnItem<&str>, tail: &'a [EdnItem<&str>]) -> Option<Self> { fn try_from_edn (s: &'a T, head: &EdnItem<&str>, tail: &'a [EdnItem<&str>]) -> Option<Self> {
use EdnItem::*; use EdnItem::*;
panic!("({head} {} {})", tail[0], tail[1]);
Some(match (head, tail) { Some(match (head, tail) {
(Key("bsp/n"), [a, b]) => Self::n(s.get_content(a).expect("no a"), s.get(b).expect("no b")), (Key("bsp/n"), [a, b]) => Self::n(s.get_content(a).expect("no a"), s.get(b).expect("no b")),
(Key("bsp/s"), [a, b]) => Self::s(s.get_content(a).expect("no a"), s.get(b).expect("no b")), (Key("bsp/s"), [a, b]) => Self::s(s.get_content(a).expect("no a"), s.get(b).expect("no b")),

View file

@ -7,8 +7,6 @@ mod size; pub use self::size::*;
mod area; pub use self::area::*; mod area; pub use self::area::*;
mod output; pub use self::output::*; mod output; pub use self::output::*;
mod content; pub use self::content::*;
mod render; pub use self::render::*;
mod thunk; pub use self::thunk::*; mod thunk; pub use self::thunk::*;
mod when; pub use self::when::*; mod when; pub use self::when::*;

View file

@ -1,6 +1,6 @@
use crate::*; use crate::*;
use std::ops::Deref;
/// Render target /// Render target.
pub trait Output: Send + Sync + Sized { pub trait Output: Send + Sync + Sized {
/// Unit of length /// Unit of length
type Unit: Coordinate; type Unit: Coordinate;
@ -20,3 +20,113 @@ pub trait Output: Send + Sync + Sized {
#[inline] fn h (&self) -> Self::Unit { self.area().h() } #[inline] fn h (&self) -> Self::Unit { self.area().h() }
#[inline] fn wh (&self) -> Self::Size { self.area().wh().into() } #[inline] fn wh (&self) -> Self::Size { self.area().wh().into() }
} }
/// Renderable with dynamic dispatch.
pub trait Render<E: Output>: Send + Sync {
/// Compute layout.
fn layout (&self, area: E::Area) -> E::Area;
/// Write data to display.
fn render (&self, output: &mut E);
/// Perform type erasure, turning `self` into an opaque [RenderBox].
fn boxed <'a> (self) -> RenderBox<'a, E> where Self: Sized + 'a {
Box::new(self) as RenderBox<'a, E>
}
}
/// Most importantly, every [Content] is also a [Render].
///
/// However, the converse does not hold true.
/// Instead, the [Content::content] method returns an
/// opaque [Render] pointer.
impl<E: Output, C: Content<E>> Render<E> for C {
fn layout (&self, area: E::Area) -> E::Area { Content::layout(self, area) }
fn render (&self, output: &mut E) { Content::render(self, output) }
}
/// Opaque pointer to a renderable living on the heap.
///
/// Return this from [Content::content] to use dynamic dispatch.
pub type RenderBox<'a, E> = Box<RenderDyn<'a, E>>;
/// You can render from a box.
impl<'a, E: Output> Content<E> for RenderBox<'a, E> {
fn content (&self) -> impl Render<E> { self.deref() }
//fn boxed <'b> (self) -> RenderBox<'b, E> where Self: Sized + 'b { self }
}
/// Opaque pointer to a renderable.
pub type RenderDyn<'a, E> = dyn Render<E> + 'a;
/// You can render from an opaque pointer.
impl<'a, E: Output> Content<E> for &RenderDyn<'a, E> where Self: Sized {
fn content (&self) -> impl Render<E> { self.deref() }
fn layout (&self, area: E::Area) -> E::Area { Render::layout(self.deref(), area) }
fn render (&self, output: &mut E) { Render::render(self.deref(), output) }
}
/// Composable renderable with static dispatch.
pub trait Content<E: Output>: Send + Sync + Sized {
/// Return a [Render]able of a specific type.
fn content (&self) -> impl Render<E> { () }
/// Perform layout. By default, delegates to [Self::content].
fn layout (&self, area: E::Area) -> E::Area { self.content().layout(area) }
/// Draw to output. By default, delegates to [Self::content].
fn render (&self, output: &mut E) { self.content().render(output) }
}
/// Every pointer to [Content] is a [Content].
impl<E: Output, C: Content<E>> Content<E> for &C {
fn content (&self) -> impl Render<E> { (*self).content() }
fn layout (&self, area: E::Area) -> E::Area { (*self).layout(area) }
fn render (&self, output: &mut E) { (*self).render(output) }
}
/// The platonic ideal unit of [Content]: total emptiness at dead center (e=1vg^sqrt(-1))
impl<E: Output> Content<E> for () {
fn layout (&self, area: E::Area) -> E::Area { area.center().to_area_pos().into() }
fn render (&self, _: &mut E) {}
}
impl<E: Output, T: Content<E>> Content<E> for Option<T> {
fn content (&self) -> impl Render<E> {
self.as_ref()
}
fn layout (&self, area: E::Area) -> E::Area {
self.as_ref()
.map(|content|content.layout(area))
.unwrap_or([0.into(), 0.into(), 0.into(), 0.into(),].into())
}
fn render (&self, output: &mut E) {
self.as_ref()
.map(|content|content.render(output));
}
}
/// Implement [Content] with composable content for a struct.
#[macro_export] macro_rules! content {
// Implement for all [Output]s.
(|$self:ident:$Struct:ty| $content:expr) => {
impl<E: Output> Content<E> for $Struct {
fn content (&$self) -> impl Render<E> { Some($content) }
}
};
// Implement for specific [Output].
($Output:ty:|
$self:ident:
$Struct:ident$(<$($($L:lifetime)? $($T:ident)? $(:$Trait:path)?),+>)?
|$content:expr) => {
impl $(<$($($L)? $($T)? $(:$Trait)?),+>)? Content<$Output>
for $Struct $(<$($($L)? $($T)?),+>)? {
fn content (&$self) -> impl Render<$Output> { $content }
}
};
}
/// Implement [Content] with custom rendering for a struct.
#[macro_export] macro_rules! render {
(|$self:ident:$Struct:ident $(<
$($L:lifetime),* $($T:ident $(:$Trait:path)?),*
>)?, $to:ident | $render:expr) => {
impl <$($($L),*)? E: Output, $($T$(:$Trait)?),*> Content<E>
for $Struct $(<$($L),* $($T),*>>)? {
fn render (&$self, $to: &mut E) { $render }
}
};
($Output:ty:|
$self:ident:
$Struct:ident $(<$($($L:lifetime)? $($T:ident)? $(:$Trait:path)?),+>)?, $to:ident
|$render:expr) => {
impl $(<$($($L)? $($T)? $(:$Trait)?),+>)? Content<$Output>
for $Struct $(<$($($L)? $($T)?),+>)? {
fn render (&$self, $to: &mut $Output) { $render }
}
};
}

View file

@ -1,66 +0,0 @@
use crate::*;
use std::ops::Deref;
/// Custom layout and rendering.
pub trait Render<E: Output>: Send + Sync {
fn layout (&self, area: E::Area) -> E::Area;
fn render (&self, output: &mut E);
fn boxed <'a> (self) -> RenderBox<'a, E> where Self: Sized + 'a {
Box::new(self) as RenderBox<'a, E>
}
}
pub type RenderDyn<'a, Output> = dyn Render<Output> + 'a;
impl<'a, E: Output> Content<E> for &RenderDyn<'a, E> where Self: Sized {
fn content (&self) -> impl Render<E> { self.deref() }
fn layout (&self, area: E::Area) -> E::Area { Render::layout(self.deref(), area) }
fn render (&self, output: &mut E) { Render::render(self.deref(), output) }
}
pub type RenderBox<'a, E: Output> = Box<RenderDyn<'a, E>>;
impl<'a, E: Output> Content<E> for RenderBox<'a, E> {
fn content (&self) -> impl Render<E> { self.deref() }
//fn boxed <'b> (self) -> RenderBox<'b, E> where Self: Sized + 'b { self }
}
impl<E: Output, C: Content<E>> Render<E> for C {
fn layout (&self, area: E::Area) -> E::Area { Content::layout(self, area) }
fn render (&self, output: &mut E) { Content::render(self, output) }
}
#[macro_export] macro_rules! render {
(($self:ident:$Struct:ty) => $content:expr) => {
impl <E: Output> Content<E> for $Struct {
fn content (&$self) -> impl Render<E> { Some($content) }
}
};
(|$self:ident:$Struct:ident $(<
$($L:lifetime),* $($T:ident $(:$Trait:path)?),*
>)?, $to:ident | $render:expr) => {
impl <$($($L),*)? E: Output, $($T$(:$Trait)?),*> Content<E>
for $Struct $(<$($L),* $($T),*>>)? {
fn render (&$self, $to: &mut E) { $render }
}
};
($Output:ty:
($self:ident:$Struct:ident $(<$(
$($L:lifetime)? $($T:ident)? $(:$Trait:path)?
),+>)?) => $content:expr
) => {
impl $(<$($($L)? $($T)? $(:$Trait)?),+>)? Content<$Output>
for $Struct $(<$($($L)? $($T)?),+>)? {
fn content (&$self) -> impl Render<$Output> { $content }
}
};
($Output:ty:
|$self:ident : $Struct:ident $(<$(
$($L:lifetime)? $($T:ident)? $(:$Trait:path)?
),+>)?, $to:ident| $render:expr
) => {
impl $(<$($($L)? $($T)? $(:$Trait)?),+>)? Content<$Output>
for $Struct $(<$($($L)? $($T)?),+>)? {
fn render (&$self, $to: &mut $Output) { $render }
}
};
}

View file

@ -27,7 +27,7 @@ impl SamplerTui {
None None
} }
} }
render!(TuiOut: (self: SamplerTui) => { content!(TuiOut: |self: SamplerTui| {
let keys_width = 5; let keys_width = 5;
let keys = move||"";//SamplerKeys(self); let keys = move||"";//SamplerKeys(self);
let fg = self.color.base.rgb; let fg = self.color.base.rgb;
@ -54,8 +54,8 @@ struct SamplesTui {
note_pt: usize, note_pt: usize,
height: usize, height: usize,
} }
render!(TuiOut: |self: SamplesTui, render|{ render!(TuiOut: |self: SamplesTui, to| {
let x = render.area.x(); let x = to.area.x();
let bg_base = self.color.darkest.rgb; let bg_base = self.color.darkest.rgb;
let bg_selected = self.color.darker.rgb; let bg_selected = self.color.darker.rgb;
let style_empty = Style::default().fg(self.color.base.rgb); let style_empty = Style::default().fg(self.color.base.rgb);
@ -64,7 +64,7 @@ render!(TuiOut: |self: SamplesTui, render|{
let note = self.note_hi - y as usize; let note = self.note_hi - y as usize;
let bg = if note == self.note_pt { bg_selected } else { bg_base }; let bg = if note == self.note_pt { bg_selected } else { bg_base };
let style = Some(style_empty.bg(bg)); let style = Some(style_empty.bg(bg));
render.blit(&" (no sample) ", x, render.area.y() + y as u16, style); to.blit(&" (no sample) ", x, to.area.y() + y as u16, style);
} }
}); });
impl NoteRange for SamplerTui { impl NoteRange for SamplerTui {

View file

@ -56,9 +56,7 @@ has_size!(<TuiOut>|self: App|&self.size);
has_clock!(|self: App|&self.clock); has_clock!(|self: App|&self.clock);
has_clips!(|self: App|self.pool.as_ref().expect("no clip pool").clips); has_clips!(|self: App|self.pool.as_ref().expect("no clip pool").clips);
//has_editor!(|self: App|self.editor.as_ref().expect("no editor")); //has_editor!(|self: App|self.editor.as_ref().expect("no editor"));
impl HasJack for App { has_jack!(|self: App|&self.jack);
fn jack (&self) -> &Arc<RwLock<JackConnection>> { &self.jack }
}
impl HasSampler for App { impl HasSampler for App {
fn sampler (&self) -> &Option<Sampler> { &self.sampler } fn sampler (&self) -> &Option<Sampler> { &self.sampler }
fn sampler_mut (&mut self) -> &mut Option<Sampler> { &mut self.sampler } fn sampler_mut (&mut self) -> &mut Option<Sampler> { &mut self.sampler }
@ -106,10 +104,10 @@ edn_provide!('a: Box<dyn Render<TuiOut> + 'a>: |self: App|{
":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16, ":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16,
self.scene_header(), self.scene_cells(self.is_editing())).boxed(), self.scene_header(), self.scene_cells(self.is_editing())).boxed(),
}); });
render!(TuiOut: (self: App) => self.size.of(EdnView::from_source(self, self.edn.as_ref()))); content!(TuiOut: |self: App| self.size.of(EdnView::from_source(self, self.edn.as_ref())));
handle!(TuiIn: |self: App, input| Ok(None)); handle!(TuiIn: |self: App, input| Ok(None));
#[derive(Debug, Default)] struct Meter<'a>(pub &'a str, pub f32); #[derive(Debug, Default)] struct Meter<'a>(pub &'a str, pub f32);
render!(TuiOut: (self: Meter<'a>) => col!( content!(TuiOut: |self: Meter<'a>| col!(
Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)), Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)),
Fixed::xy(if self.1 >= 0.0 { 13 } Fixed::xy(if self.1 >= 0.0 { 13 }
else if self.1 >= -1.0 { 12 } else if self.1 >= -1.0 { 12 }
@ -128,7 +126,7 @@ render!(TuiOut: (self: Meter<'a>) => col!(
else if self.1 >= -3.0 { Color::Yellow } else if self.1 >= -3.0 { Color::Yellow }
else { Color::Green }, ())))); else { Color::Green }, ()))));
#[derive(Debug, Default)] struct Meters<'a>(pub &'a[f32]); #[derive(Debug, Default)] struct Meters<'a>(pub &'a[f32]);
render!(TuiOut: (self: Meters<'a>) => col!( content!(TuiOut: |self: Meters<'a>| col!(
format!("L/{:>+9.3}", self.0[0]), format!("L/{:>+9.3}", self.0[0]),
format!("R/{:>+9.3}", self.0[1]) format!("R/{:>+9.3}", self.0[1])
)); ));
@ -182,7 +180,7 @@ pub trait HasSelection {
fn selected (&self) -> &Selection; fn selected (&self) -> &Selection;
fn selected_mut (&mut self) -> &mut Selection; fn selected_mut (&mut self) -> &mut Selection;
} }
#[derive(Debug, Default)] struct Track { #[derive(Debug, Default)] pub struct Track {
/// Name of track /// Name of track
name: Arc<str>, name: Arc<str>,
/// Preferred width of track column /// Preferred width of track column
@ -332,7 +330,7 @@ pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync
trait Device: Send + Sync + std::fmt::Debug {} trait Device: Send + Sync + std::fmt::Debug {}
impl Device for Sampler {} impl Device for Sampler {}
impl Device for Plugin {} impl Device for Plugin {}
#[derive(Debug, Default)] struct Scene { #[derive(Debug, Default)] pub struct Scene {
/// Name of scene /// Name of scene
name: Arc<str>, name: Arc<str>,
/// Clips in scene, one per track /// Clips in scene, one per track

View file

@ -2,10 +2,7 @@ use crate::*;
use KeyCode::*; use KeyCode::*;
use ClockCommand::{Play, Pause}; use ClockCommand::{Play, Pause};
/// Transport clock app. /// Transport clock app.
pub struct ClockTui { pub struct ClockTui { pub jack: Arc<RwLock<JackConnection>>, pub clock: Clock, }
pub jack: Arc<RwLock<JackConnection>>,
pub clock: Clock,
}
handle!(TuiIn: |self: ClockTui, input|ClockCommand::execute_with_state(self, input.event())); handle!(TuiIn: |self: ClockTui, input|ClockCommand::execute_with_state(self, input.event()));
keymap!(TRANSPORT_KEYS = |state: ClockTui, input: Event| ClockCommand { keymap!(TRANSPORT_KEYS = |state: ClockTui, input: Event| ClockCommand {
key(Char(' ')) => key(Char(' ')) =>
@ -15,29 +12,20 @@ keymap!(TRANSPORT_KEYS = |state: ClockTui, input: Event| ClockCommand {
}); });
has_clock!(|self: ClockTui|&self.clock); has_clock!(|self: ClockTui|&self.clock);
audio!(|self: ClockTui, client, scope|ClockAudio(self).process(client, scope)); audio!(|self: ClockTui, client, scope|ClockAudio(self).process(client, scope));
render!(TuiOut: (self: ClockTui) => ClockView { content!(TuiOut:|self: ClockTui|ClockView { compact: false, clock: &self.clock });
compact: false,
clock: &self.clock
});
pub struct ClockView<'a> { pub compact: bool, pub clock: &'a Clock } pub struct ClockView<'a> { pub compact: bool, pub clock: &'a Clock }
impl<'a> ClockView<'a> { content!(TuiOut: |self: ClockView<'a>| Outer(Style::default().fg(TuiTheme::g(255))).enclose(row!(
pub fn new (compact: bool, clock: &'a Clock) -> Self {
Self { compact, clock }
}
}
render!(TuiOut: (self: ClockView<'a>) => Outer(
Style::default().fg(TuiTheme::g(255))
).enclose(row!(
OutputStats::new(self.compact, self.clock), OutputStats::new(self.compact, self.clock),
" ", " ",
PlayPause { compact: false, playing: self.clock.is_rolling() }, PlayPause { compact: false, playing: self.clock.is_rolling() },
" ", " ",
BeatStats::new(self.compact, self.clock), BeatStats::new(self.compact, self.clock),
))); )));
impl<'a> ClockView<'a> {
pub fn new (compact: bool, clock: &'a Clock) -> Self { Self { compact, clock } }
}
pub struct PlayPause { pub compact: bool, pub playing: bool } pub struct PlayPause { pub compact: bool, pub playing: bool }
render!(TuiOut: (self: PlayPause) => Tui::bg( content!(TuiOut: |self: PlayPause| Tui::bg(
if self.playing{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}, if self.playing{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)},
Either::new(self.compact, Either::new(self.compact,
Thunk::new(||Fixed::x(9, Either::new(self.playing, Thunk::new(||Fixed::x(9, Either::new(self.playing,
@ -46,8 +34,14 @@ render!(TuiOut: (self: PlayPause) => Tui::bg(
Thunk::new(||Fixed::x(5, Either::new(self.playing, Thunk::new(||Fixed::x(5, Either::new(self.playing,
Tui::fg(Color::Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), Tui::fg(Color::Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
Tui::fg(Color::Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))))))); Tui::fg(Color::Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))))));
pub struct BeatStats { compact: bool, bpm: Arc<str>, beat: Arc<str>, time: Arc<str>, } pub struct BeatStats { compact: bool, bpm: Arc<str>, beat: Arc<str>, time: Arc<str>, }
content!(TuiOut: |self: BeatStats| Either::new(self.compact,
row!(FieldV(TuiTheme::g(128).into(), "BPM", &self.bpm),
FieldV(TuiTheme::g(128).into(), "Beat", &self.beat),
FieldV(TuiTheme::g(128).into(), "Time", &self.time),),
col!(Bsp::e(Tui::fg(TuiTheme::g(255), &self.bpm), " BPM"),
Bsp::e("Beat ", Tui::fg(TuiTheme::g(255), &self.beat)),
Bsp::e("Time ", Tui::fg(TuiTheme::g(255), &self.time)))));
impl BeatStats { impl BeatStats {
fn new (compact: bool, clock: &Clock) -> Self { fn new (compact: bool, clock: &Clock) -> Self {
let bpm = format!("{:.3}", clock.timebase.bpm.get()).into(); let bpm = format!("{:.3}", clock.timebase.bpm.get()).into();
@ -63,46 +57,27 @@ impl BeatStats {
Self { compact, bpm, beat, time } Self { compact, bpm, beat, time }
} }
} }
render!(TuiOut: (self: BeatStats) => Either::new(self.compact,
row!(
FieldV(TuiTheme::g(128).into(), "BPM", &self.bpm),
FieldV(TuiTheme::g(128).into(), "Beat", &self.beat),
FieldV(TuiTheme::g(128).into(), "Time", &self.time),
),
col!(
Bsp::e(Tui::fg(TuiTheme::g(255), &self.bpm), " BPM"),
Bsp::e("Beat ", Tui::fg(TuiTheme::g(255), &self.beat)),
Bsp::e("Time ", Tui::fg(TuiTheme::g(255), &self.time)),
)));
pub struct OutputStats { compact: bool, sample_rate: Arc<str>, buffer_size: Arc<str>, latency: Arc<str>, } pub struct OutputStats { compact: bool, sample_rate: Arc<str>, buffer_size: Arc<str>, latency: Arc<str>, }
content!(TuiOut: |self: OutputStats| Either::new(self.compact,
row!(FieldV(TuiTheme::g(128).into(), "SR", &self.sample_rate),
FieldV(TuiTheme::g(128).into(), "Buf", &self.buffer_size),
FieldV(TuiTheme::g(128).into(), "Lat", &self.latency)),
col!(Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", self.sample_rate)), " sample rate"),
Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", self.buffer_size)), " sample buffer"),
Bsp::e(Tui::fg(TuiTheme::g(255), format!("{:.3}ms", self.latency)), " latency"))));
impl OutputStats { impl OutputStats {
fn new (compact: bool, clock: &Clock) -> Self { fn new (compact: bool, clock: &Clock) -> Self {
let rate = clock.timebase.sr.get(); let rate = clock.timebase.sr.get();
let chunk = clock.chunk.load(Relaxed); let chunk = clock.chunk.load(Relaxed);
let sr = if compact {format!("{:.1}kHz", rate / 1000.)} else {format!("{:.0}Hz", rate)};
Self { Self {
compact, compact,
sample_rate: if compact { sample_rate: sr.into(),
format!("{:.1}kHz", rate / 1000.)
} else {
format!("{:.0}Hz", rate)
}.into(),
buffer_size: format!("{chunk}").into(), buffer_size: format!("{chunk}").into(),
latency: format!("{:.1}ms", chunk as f64 / rate * 1000.).into(), latency: format!("{:.1}ms", chunk as f64 / rate * 1000.).into(),
} }
} }
} }
render!(TuiOut: (self: OutputStats) => Either::new(self.compact,
row!(
FieldV(TuiTheme::g(128).into(), "SR", &self.sample_rate),
FieldV(TuiTheme::g(128).into(), "Buf", &self.buffer_size),
FieldV(TuiTheme::g(128).into(), "Lat", &self.latency),
),
col!(
Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", self.sample_rate)), " sample rate"),
Bsp::e(Tui::fg(TuiTheme::g(255), format!("{}", self.buffer_size)), " sample buffer"),
Bsp::e(Tui::fg(TuiTheme::g(255), format!("{:.3}ms", self.latency)), " latency"),
)));
// TODO: // TODO:
//keymap!(TRANSPORT_BPM_KEYS = |state: Clock, input: Event| ClockCommand { //keymap!(TRANSPORT_BPM_KEYS = |state: Clock, input: Event| ClockCommand {

View file

@ -1,13 +1,11 @@
use crate::*; use crate::*;
pub struct Bordered<S: BorderStyle, W: Content<TuiOut>>(pub S, pub W); pub struct Bordered<S: BorderStyle, W: Content<TuiOut>>(pub S, pub W);
content!(TuiOut: |self: Bordered<S: BorderStyle, W: Content<TuiOut>>|Fill::xy(
render!(TuiOut: (self: Bordered<S: BorderStyle, W: Content<TuiOut>>) => { lay!(Border(self.0), Padding::xy(1, 1, &self.1))
Fill::xy(lay!(Border(self.0), Padding::xy(1, 1, &self.1))) ));
});
pub struct Border<S: BorderStyle>(pub S); pub struct Border<S: BorderStyle>(pub S);
render!(TuiOut: |self: Border<S: BorderStyle>, to| { render!(TuiOut: |self: Border<S: BorderStyle>, to| {
let area = to.area(); let area = to.area();
if area.w() > 0 && area.y() > 0 { if area.w() > 0 && area.y() > 0 {

View file

@ -20,7 +20,7 @@ pub enum FileBrowserCommand {
Chdir(PathBuf), Chdir(PathBuf),
Filter(Arc<str>), Filter(Arc<str>),
} }
render!(TuiOut: (self: FileBrowser) => /*Stack::down(|add|{ content!(TuiOut: |self: FileBrowser| /*Stack::down(|add|{
let mut i = 0; let mut i = 0;
for (_, name) in self.dirs.iter() { for (_, name) in self.dirs.iter() {
if i >= self.scroll { if i >= self.scroll {