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::*;
/// 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` */
#[macro_export] macro_rules! edn_command {
($Command:ty : |$state:ident:$State:ty| { $((
@ -47,16 +51,3 @@ use crate::*;
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 EdnItem::*;
pub trait EdnInput: Input {
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 {
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,
};
mod from_jack; pub use self::from_jack::*;
mod has_jack; pub use self::has_jack::*;
mod jack_audio; pub use self::jack_audio::*;
mod jack_connect; pub use self::jack_connect::*;

View file

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

View file

@ -1,6 +1,6 @@
use crate::*;
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 MidiPool { clips, mode, .. } = self.1;
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 beats = ||self.beats_string();
let ticks = ||self.ticks_string();

View file

@ -1,17 +1,16 @@
use crate::*;
pub struct ClipSelected {
pub(crate) title: &'static str,
pub(crate) name: Arc<str>,
pub(crate) color: ItemPalette,
pub(crate) time: Arc<str>,
}
render!(TuiOut: (self: ClipSelected) =>
FieldV(self.color, self.title, format!("{} {}", self.time, self.name)));
content!(TuiOut: |self: ClipSelected|FieldV(
self.color,
self.title,
format!("{} {}", self.time, self.name)
));
impl ClipSelected {
/// Shows currently playing clip with beats elapsed
pub fn play_clip <T: HasPlayClip + HasClock> (state: &T) -> Self {
let (name, color) = if let Some((_, Some(clip))) = state.play_clip() {
@ -31,7 +30,6 @@ impl ClipSelected {
.unwrap_or_else(||String::from(" ")).into()
}
}
/// Shows next clip with beats remaining until switchover
pub fn next_clip <T: HasPlayClip> (state: &T) -> Self {
let mut time: Arc<str> = String::from("--.-.--").into();
@ -69,5 +67,4 @@ impl ClipSelected {
};
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)> {
(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(
Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())),
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>> {
fn try_from_edn (s: &'a T, head: &EdnItem<&str>, tail: &'a [EdnItem<&str>]) -> Option<Self> {
use EdnItem::*;
panic!("({head} {} {})", tail[0], tail[1]);
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/s"), [a, b]) => Self::s(s.get_content(a).expect("no a"), s.get(b).expect("no b")),

View file

@ -6,10 +6,8 @@ mod coordinate; pub use self::coordinate::*;
mod size; pub use self::size::*;
mod area; pub use self::area::*;
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 output; pub use self::output::*;
mod thunk; pub use self::thunk::*;
mod when; pub use self::when::*;
mod either; pub use self::either::*;

View file

@ -1,6 +1,6 @@
use crate::*;
/// Render target
use std::ops::Deref;
/// Render target.
pub trait Output: Send + Sync + Sized {
/// Unit of length
type Unit: Coordinate;
@ -20,3 +20,113 @@ pub trait Output: Send + Sync + Sized {
#[inline] fn h (&self) -> Self::Unit { self.area().h() }
#[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
}
}
render!(TuiOut: (self: SamplerTui) => {
content!(TuiOut: |self: SamplerTui| {
let keys_width = 5;
let keys = move||"";//SamplerKeys(self);
let fg = self.color.base.rgb;
@ -54,8 +54,8 @@ struct SamplesTui {
note_pt: usize,
height: usize,
}
render!(TuiOut: |self: SamplesTui, render|{
let x = render.area.x();
render!(TuiOut: |self: SamplesTui, to| {
let x = to.area.x();
let bg_base = self.color.darkest.rgb;
let bg_selected = self.color.darker.rgb;
let style_empty = Style::default().fg(self.color.base.rgb);
@ -64,7 +64,7 @@ render!(TuiOut: |self: SamplesTui, render|{
let note = self.note_hi - y as usize;
let bg = if note == self.note_pt { bg_selected } else { bg_base };
let style = Some(style_empty.bg(bg));
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 {

View file

@ -56,9 +56,7 @@ has_size!(<TuiOut>|self: App|&self.size);
has_clock!(|self: App|&self.clock);
has_clips!(|self: App|self.pool.as_ref().expect("no clip pool").clips);
//has_editor!(|self: App|self.editor.as_ref().expect("no editor"));
impl HasJack for App {
fn jack (&self) -> &Arc<RwLock<JackConnection>> { &self.jack }
}
has_jack!(|self: App|&self.jack);
impl HasSampler for App {
fn sampler (&self) -> &Option<Sampler> { &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,
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));
#[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)),
Fixed::xy(if self.1 >= 0.0 { 13 }
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 { Color::Green }, ()))));
#[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!("R/{:>+9.3}", self.0[1])
));
@ -182,7 +180,7 @@ pub trait HasSelection {
fn selected (&self) -> &Selection;
fn selected_mut (&mut self) -> &mut Selection;
}
#[derive(Debug, Default)] struct Track {
#[derive(Debug, Default)] pub struct Track {
/// Name of track
name: Arc<str>,
/// 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 {}
impl Device for Sampler {}
impl Device for Plugin {}
#[derive(Debug, Default)] struct Scene {
#[derive(Debug, Default)] pub struct Scene {
/// Name of scene
name: Arc<str>,
/// Clips in scene, one per track

View file

@ -2,10 +2,7 @@ use crate::*;
use KeyCode::*;
use ClockCommand::{Play, Pause};
/// Transport clock app.
pub struct ClockTui {
pub jack: Arc<RwLock<JackConnection>>,
pub clock: Clock,
}
pub struct ClockTui { pub jack: Arc<RwLock<JackConnection>>, pub clock: Clock, }
handle!(TuiIn: |self: ClockTui, input|ClockCommand::execute_with_state(self, input.event()));
keymap!(TRANSPORT_KEYS = |state: ClockTui, input: Event| ClockCommand {
key(Char(' ')) =>
@ -15,29 +12,20 @@ keymap!(TRANSPORT_KEYS = |state: ClockTui, input: Event| ClockCommand {
});
has_clock!(|self: ClockTui|&self.clock);
audio!(|self: ClockTui, client, scope|ClockAudio(self).process(client, scope));
render!(TuiOut: (self: ClockTui) => ClockView {
compact: false,
clock: &self.clock
});
content!(TuiOut:|self: ClockTui|ClockView { compact: false, clock: &self.clock });
pub struct ClockView<'a> { pub compact: bool, pub clock: &'a Clock }
impl<'a> ClockView<'a> {
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!(
content!(TuiOut: |self: ClockView<'a>| Outer(Style::default().fg(TuiTheme::g(255))).enclose(row!(
OutputStats::new(self.compact, self.clock),
" ",
PlayPause { compact: false, playing: self.clock.is_rolling() },
" ",
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 }
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)},
Either::new(self.compact,
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,
Tui::fg(Color::Rgb(0, 255, 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>, }
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 {
fn new (compact: bool, clock: &Clock) -> Self {
let bpm = format!("{:.3}", clock.timebase.bpm.get()).into();
@ -63,46 +57,27 @@ impl BeatStats {
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>, }
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 {
fn new (compact: bool, clock: &Clock) -> Self {
let rate = clock.timebase.sr.get();
let chunk = clock.chunk.load(Relaxed);
let sr = if compact {format!("{:.1}kHz", rate / 1000.)} else {format!("{:.0}Hz", rate)};
Self {
compact,
sample_rate: if compact {
format!("{:.1}kHz", rate / 1000.)
} else {
format!("{:.0}Hz", rate)
}.into(),
sample_rate: sr.into(),
buffer_size: format!("{chunk}").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:
//keymap!(TRANSPORT_BPM_KEYS = |state: Clock, input: Event| ClockCommand {

View file

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

View file

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