diff --git a/Cargo.lock b/Cargo.lock index c73f9036..762b7603 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1389,6 +1389,7 @@ name = "tek_engine" version = "0.2.0" dependencies = [ "better-panic", + "clojure-reader", "crossterm", "ratatui", ] diff --git a/engine/Cargo.toml b/engine/Cargo.toml index b72a07cb..f9005149 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -7,3 +7,4 @@ version = "0.2.0" crossterm = "0.28.1" ratatui = { version = "0.29.0", features = [ "unstable-widget-ref", "underline-color" ] } better-panic = "0.3.0" +clojure-reader = "0.3.0" diff --git a/engine/src/edn.rs b/engine/src/edn.rs new file mode 100644 index 00000000..2a84d991 --- /dev/null +++ b/engine/src/edn.rs @@ -0,0 +1,36 @@ +use crate::*; + +use std::sync::{Arc, RwLock}; +use std::collections::BTreeMap; + +pub use clojure_reader::edn::Edn; + +/// EDN parsing helper. +#[macro_export] macro_rules! edn { + ($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => { + match $edn { $($pat => $expr),* } + }; + ($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => { + for $edn in $args { + edn!($edn { $($pat => $expr),* }) + } + }; +} + +pub trait FromEdn: Sized { + const ID: &'static str; + fn from_edn (context: C, expr: &[Edn<'_>]) -> Usually; +} + +/// Implements the [FromEdn] trait. +#[macro_export] macro_rules! from_edn { + ($id:expr => |$context:tt:$Context:ty, $args:ident| -> $T:ty $body:block) => { + impl FromEdn<$Context> for $T { + const ID: &'static str = $id; + fn from_edn <'e> ($context: $Context, $args: &[Edn<'e>]) -> Usually { + $body + } + } + } +} + diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 9a89169b..e792e371 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -4,6 +4,7 @@ mod input; pub use self::input::*; mod output; pub use self::output::*; pub mod tui; +pub mod edn; pub use std::error::Error; diff --git a/layout/src/align.rs b/layout/src/align.rs index 7135643a..919443a8 100644 --- a/layout/src/align.rs +++ b/layout/src/align.rs @@ -3,23 +3,23 @@ use crate::*; #[derive(Debug, Copy, Clone, Default)] pub enum Alignment { #[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W } -pub struct Align>(Alignment, T, PhantomData); +pub struct Align(Alignment, T); -impl> Align { - pub fn c (a: T) -> Self { Self(Alignment::Center, a, Default::default()) } - pub fn x (a: T) -> Self { Self(Alignment::X, a, Default::default()) } - pub fn y (a: T) -> Self { Self(Alignment::Y, a, Default::default()) } - pub fn n (a: T) -> Self { Self(Alignment::N, a, Default::default()) } - pub fn s (a: T) -> Self { Self(Alignment::S, a, Default::default()) } - pub fn e (a: T) -> Self { Self(Alignment::E, a, Default::default()) } - pub fn w (a: T) -> Self { Self(Alignment::W, a, Default::default()) } - pub fn nw (a: T) -> Self { Self(Alignment::NW, a, Default::default()) } - pub fn sw (a: T) -> Self { Self(Alignment::SW, a, Default::default()) } - pub fn ne (a: T) -> Self { Self(Alignment::NE, a, Default::default()) } - pub fn se (a: T) -> Self { Self(Alignment::SE, a, Default::default()) } +impl Align { + pub fn c (a: T) -> Self { Self(Alignment::Center, a) } + pub fn x (a: T) -> Self { Self(Alignment::X, a) } + pub fn y (a: T) -> Self { Self(Alignment::Y, a) } + pub fn n (a: T) -> Self { Self(Alignment::N, a) } + pub fn s (a: T) -> Self { Self(Alignment::S, a) } + pub fn e (a: T) -> Self { Self(Alignment::E, a) } + pub fn w (a: T) -> Self { Self(Alignment::W, a) } + pub fn nw (a: T) -> Self { Self(Alignment::NW, a) } + pub fn sw (a: T) -> Self { Self(Alignment::SW, a) } + pub fn ne (a: T) -> Self { Self(Alignment::NE, a) } + pub fn se (a: T) -> Self { Self(Alignment::SE, a) } } -impl> Content for Align { +impl> Content for Align { fn content (&self) -> impl Content { &self.1 } @@ -45,52 +45,8 @@ impl> Content for Align { Y => [it.x(), centered.y()], }; [x, y, centered.w(), centered.h()].into() - //let [cfx, cfy, ..] = on.center(); - //let [cmx, cmy, ..] = it.center(); - ////let center = |cf, cm, m|if cf >= cm { m + (cf - cm) } else { m.minus(cm - cf) }; - //let center_x = center(cfx, cmx, it.x()); - //let center_y = center(cfy, cmy, it.y()); - //let east_x = on.x() + on.w().minus(it.w()); - //let south_y = on.y() + on.h().minus(it.h()); - //let [x, y] = match self.0 { - //Alignment::X => [center_x, it.y(), ], - //Alignment::Y => [it.x(), center_y,], - - //Alignment::NW => [on.x(), on.y(), ], - //Alignment::N => [center_x, on.y(), ], - //Alignment::NE => [east_x, on.y(), ], - //Alignment::E => [east_x, center_y,], - //Alignment::SE => [east_x, south_y, ], - //Alignment::S => [center_x, south_y, ], - //Alignment::SW => [on.x(), south_y, ], - //Alignment::W => [on.x(), center_y,], - //}; - //[x, y, it.w(), it.h()].into() } fn render (&self, render: &mut E::Output) { render.place(self.layout(render.area()), &self.content()) } } - -//fn align, N: Coordinate, R: Area + From<[N;4]>> (align: &Align, outer: R, content: R) -> Option { - //if outer.w() < content.w() || outer.h() < content.h() { - //None - //} else { - //let [ox, oy, ow, oh] = outer.xywh(); - //let [ix, iy, iw, ih] = content.xywh(); - //Some(match align { - //Align::Center(_) => [ox + (ow - iw) / 2.into(), oy + (oh - ih) / 2.into(), iw, ih,].into(), - //Align::X(_) => [ox + (ow - iw) / 2.into(), iy, iw, ih,].into(), - //Align::Y(_) => [ix, oy + (oh - ih) / 2.into(), iw, ih,].into(), - //Align::NW(_) => [ox, oy, iw, ih,].into(), - //Align::N(_) => [ox + (ow - iw) / 2.into(), oy, iw, ih,].into(), - //Align::NE(_) => [ox + ow - iw, oy, iw, ih,].into(), - //Align::W(_) => [ox, oy + (oh - ih) / 2.into(), iw, ih,].into(), - //Align::E(_) => [ox + ow - iw, oy + (oh - ih) / 2.into(), iw, ih,].into(), - //Align::SW(_) => [ox, oy + oh - ih, iw, ih,].into(), - //Align::S(_) => [ox + (ow - iw) / 2.into(), oy + oh - ih, iw, ih,].into(), - //Align::SE(_) => [ox + ow - iw, oy + oh - ih, iw, ih,].into(), - //_ => unreachable!() - //}) - //} -//} diff --git a/layout/src/direction.rs b/layout/src/direction.rs index 8cab09c8..78f9f477 100644 --- a/layout/src/direction.rs +++ b/layout/src/direction.rs @@ -18,9 +18,9 @@ impl Direction { } } -pub struct Bsp, Y: Content>(Direction, X, Y, PhantomData); +pub struct Bsp(Direction, X, Y); -impl, B: Content> Content for Bsp { +impl, B: Content> Content for Bsp { fn layout (&self, outer: E::Area) -> E::Area { let [_, _, c] = self.areas(outer); c @@ -35,26 +35,31 @@ impl, B: Content> Content for Bsp { } } -impl, B: Content> Bsp { - pub fn n (a: A, b: B) -> Self { Self(North, a, b, Default::default()) } - pub fn s (a: A, b: B) -> Self { Self(South, a, b, Default::default()) } - pub fn e (a: A, b: B) -> Self { Self(East, a, b, Default::default()) } - pub fn w (a: A, b: B) -> Self { Self(West, a, b, Default::default()) } - pub fn a (a: A, b: B) -> Self { Self(Above, a, b, Default::default()) } - pub fn b (a: A, b: B) -> Self { Self(Below, a, b, Default::default()) } - pub fn contents (&self) -> (&A, &B) { (&self.1, &self.2) } - pub fn areas (&self, outer: E::Area) -> [E::Area;3] { +impl Bsp { + pub fn n (a: A, b: B) -> Self { Self(North, a, b) } + pub fn s (a: A, b: B) -> Self { Self(South, a, b) } + pub fn e (a: A, b: B) -> Self { Self(East, a, b) } + pub fn w (a: A, b: B) -> Self { Self(West, a, b) } + pub fn a (a: A, b: B) -> Self { Self(Above, a, b) } + pub fn b (a: A, b: B) -> Self { Self(Below, a, b) } +} + +pub trait BspAreas, B: Content> { + fn direction (&self) -> Direction; + fn contents (&self) -> (&A, &B); + fn areas (&self, outer: E::Area) -> [E::Area;3] { + let direction = self.direction(); let [x, y, w, h] = outer.xywh(); let (a, b) = self.contents(); let [ax, ay, aw, ah] = a.layout(outer).xywh(); - let [bx, by, bw, bh] = b.layout(match self.0 { + let [bx, by, bw, bh] = b.layout(match direction { Above | Below => outer, South => [x, y + ah, w, h.minus(ah)].into(), North => [x, y, w, h.minus(ah)].into(), East => [x + aw, y, w.minus(aw), h].into(), West => [x, y, w.minus(aw), h].into(), }).xywh(); - match self.0 { + match direction { Above | Below => { let x = ax.min(bx); let w = (ax+aw).max(bx+bw).minus(x); @@ -90,6 +95,11 @@ impl, B: Content> Bsp { } } +impl, B: Content> BspAreas for Bsp { + fn direction (&self) -> Direction { self. 0 } + fn contents (&self) -> (&A, &B) { (&self.1, &self.2) } +} + /// Renders multiple things on top of each other, #[macro_export] macro_rules! lay { ($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)*; bsp }} diff --git a/layout/src/layout_edn.rs b/layout/src/layout_edn.rs new file mode 100644 index 00000000..e466483e --- /dev/null +++ b/layout/src/layout_edn.rs @@ -0,0 +1,47 @@ +use crate::*; +use ::tek_engine::edn::*; + +macro_rules! edn_module { + ($name:literal = $Host:ident<$E:ident: $Engine:path> { $( + (defn $fn:ident + $(<$($G:ident: $Generic:ty),+>)? + $Struct:ident($($arg:ident : $Arg:ty),*)) + )* }) => { + pub trait $Host<$E: Engine> { + pub fn read_one <'e> (edn: &[Edn<'e>]) -> impl Content<$E> { + if let Some(Edn::Symbol(name)) = edn.get(0) { + match name { + $( + stringify!($fn) => + ),* + } + } else { + panic!("invalid edn") + } + } + $( + + )* + } + }; +} + +//edn_module! { + //(host LayoutEdn) + //(name "layout") + + //(defn when > + //When(cond: bool, item: A)) + + //(defn either , B: Content> + //Either(cond: bool, a: A, b: B)) + + //(defn map < + //A: Content, + //B: Content, + //I: Iterator + Send + Sync, + //F: Fn() -> I + Send + Sync, + //G: Fn(A, usize)->B + Send + Sync + //> + //Map(get_iterator: I, callback: G)) +//} diff --git a/layout/src/lib.rs b/layout/src/lib.rs index b41d9f0b..56f2a87d 100644 --- a/layout/src/lib.rs +++ b/layout/src/lib.rs @@ -2,6 +2,7 @@ mod align; pub use self::align::*; mod direction; pub use self::direction::*; +mod layout_edn; pub use self::edn::*; mod measure; pub use self::measure::*; mod ops; pub use self::ops::*; mod transform_xy; pub use self::transform_xy::*; diff --git a/layout/src/measure.rs b/layout/src/measure.rs index 98ccf85f..b1bdf2e1 100644 --- a/layout/src/measure.rs +++ b/layout/src/measure.rs @@ -65,7 +65,7 @@ impl Measure { y: Arc::new(0.into()), } } - pub fn of > (&self, item: T) -> Bsp, T> { + pub fn of > (&self, item: T) -> Bsp, T> { Bsp::b(Fill::xy(&self), item) } } diff --git a/layout/src/ops.rs b/layout/src/ops.rs index 117a77fc..06539e6b 100644 --- a/layout/src/ops.rs +++ b/layout/src/ops.rs @@ -1,55 +1,11 @@ use crate::*; -impl Layout for E {} +/// Show an item only when a condition is true. +pub struct When(pub bool, pub A); -pub trait Layout { - /// Content `item` when `cond` is true. - fn when (cond: bool, item: A) -> When where - A: Content - { - When(cond, item, Default::default()) - } - /// Content `item` if `cond` is true, otherwise render `other`. - fn either (cond: bool, a: A, b: B) -> Either where - A: Content, - B: Content, - { - Either(cond, a, b, Default::default()) - } - /// Maps an [Option] through a callback `F` - fn opt (option: Option, cb: F) -> Opt where - F: Fn(A) -> R, - R: Content - { - Opt(option, cb, Default::default()) - } - fn map (iterator: J, callback: F) -> Map where - E: Engine, - I: Iterator + Send + Sync, - J: Fn() -> I + Send + Sync, - R: Content, - F: Fn(T, usize)->R + Send + Sync - { - Map(Default::default(), iterator, callback) - } - //pub fn reduce (iterator: I, callback: F) -> Reduce where - //E: Engine, - //I: Iterator + Send + Sync, - //R: Content, - //F: Fn(R, T, usize) -> R + Send + Sync - //{ - //Reduce(Default::default(), iterator, callback) - //} -} - -pub struct OptR, R: Content>(Option, F, PhantomData); - -/// Contents `self.1` when `self.0` is true. -pub struct When(bool, A, PhantomData); - -impl> Content for When { +impl> Content for When { fn layout (&self, to: E::Area) -> E::Area { - let Self(cond, item, ..) = self; + let Self(cond, item) = self; let mut area = E::Area::zero(); if *cond { let item_area = item.layout(to); @@ -61,45 +17,44 @@ impl> Content for When { area.into() } fn render (&self, to: &mut E::Output) { - let Self(cond, item, ..) = self; + let Self(cond, item) = self; if *cond { item.render(to) } } } -/// Contents `self.1` when `self.0` is true, otherwise renders `self.2` -pub struct Either(bool, A, B, PhantomData); +/// Show one item if a condition is true and another if the condition is false +pub struct Either(pub bool, pub A, pub B); -impl, B: Content> Content for Either { +impl, B: Content> Content for Either { fn layout (&self, to: E::Area) -> E::Area { - let Self(cond, a, b, ..) = self; + let Self(cond, a, b) = self; if *cond { a.layout(to) } else { b.layout(to) } } fn render (&self, to: &mut E::Output) { - let Self(cond, a, b, ..) = self; + let Self(cond, a, b) = self; if *cond { a.render(to) } else { b.render(to) } } } -pub struct Map(PhantomData, J, F) where - E: Engine, - I: Iterator + Send + Sync, - J: Fn()->I + Send + Sync, - R: Content, - F: Fn(T, usize)->R + Send + Sync; +pub struct Map(pub F, pub G) where + I: Iterator + Send + Sync, + F: Fn() -> I + Send + Sync, + G: Fn(A, usize)->B + Send + Sync; -impl Content for Map where +impl Content for Map where E: Engine, - I: Iterator + Send + Sync, - J: Fn()->I + Send + Sync, - R: Content, - F: Fn(T, usize)->R + Send + Sync + B: Content, + I: Iterator + Send + Sync, + F: Fn() -> I + Send + Sync, + G: Fn(A, usize)->B + Send + Sync { fn layout (&self, area: E::Area) -> E::Area { + let Self(get_iterator, callback) = self; let mut index = 0; let [mut min_x, mut min_y] = area.center(); let [mut max_x, mut max_y] = area.center(); - for item in (self.1)() { - let area = (self.2)(item, index).layout(area).xywh(); + for item in get_iterator() { + let area = callback(item, index).layout(area).xywh(); let [x,y,w,h] = area.xywh(); min_x = min_x.min(x.into()); min_y = min_y.min(y.into()); @@ -113,10 +68,11 @@ impl Content for Map where area.center_xy([w.into(), h.into()].into()).into() } fn render (&self, to: &mut E::Output) { + let Self(get_iterator, callback) = self; let mut index = 0; //let area = self.layout(to.area()); - for item in (self.1)() { - let item = (self.2)(item, index); + for item in get_iterator() { + let item = callback(item, index); //to.place(area.into(), &item); to.place(to.area().into(), &item); index += 1; @@ -125,6 +81,15 @@ impl Content for Map where } /* + + //pub fn reduce (iterator: I, callback: F) -> Reduce where + //E: Engine, + //I: Iterator + Send + Sync, + //R: Content, + //F: Fn(R, T, usize) -> R + Send + Sync + //{ + //Reduce(Default::default(), iterator, callback) + //} pub struct Reduce(PhantomData<(E, R)>, I, F) where E: Engine, I: Iterator + Send + Sync, @@ -141,3 +106,42 @@ impl Content for Reduce where } } */ + +//macro_rules! define_ops { + //($Trait:ident<$E:ident:$Engine:path> { $( + //$(#[$attr:meta $($attr_args:tt)*])* + //( + //$fn:ident + //$(<$($G:ident$(:$Gen:path)?, )+>)? + //$Op:ident + //($($arg:ident:$Arg:ty),*) + //) + //)* }) => { + //impl<$E: $Engine> $Trait for E {} + //pub trait $Trait<$E: $Engine> { + //$( + //$(#[$attr $($attr_args)*])* + //fn $fn $(<$($G),+>)? + //($($arg:$Arg),*)-> $Op<$($(, $G)+)?> + //$(where $($G: $($Gen + Send + Sync)?),+)? + //{ $Op($($arg),*) } + //)* + //} + //} +//} + +//define_ops! { + //Layout { + //(when ,> + //When(cond: bool, item: A)) + ///// When `cond` is `true`, render `a`, otherwise render `b`. + //(either , B: Content,> + //Either(cond: bool, a: A, b: B)) + ///// If `opt` is `Some(T)` renders `cb(t)`, otherwise nothing. + //(opt B, B: Content,> + //Opt(option: Option, cb: F)) + ///// Maps items of iterator through callback. + //(map , I: Iterator, F: Fn() -> I, G: Fn(A, usize)->B,> + //Map(get_iterator: F, callback: G)) + //} +//} diff --git a/src/arranger/arranger_v_clips.rs b/src/arranger/arranger_v_clips.rs index 97520739..6dcc0d86 100644 --- a/src/arranger/arranger_v_clips.rs +++ b/src/arranger/arranger_v_clips.rs @@ -20,7 +20,7 @@ impl<'a> ArrangerVClips<'a> { impl<'a> Content for ArrangerVClips<'a> { fn content (&self) -> impl Content { let iter = ||self.scenes.iter().zip(self.rows.iter().map(|row|row.0)); - let col = Tui::map(iter, |(scene, pulses), i|Self::format_scene(self.tracks, scene, pulses)); + let col = Map(iter, |(scene, pulses), i|Self::format_scene(self.tracks, scene, pulses)); Fill::xy(col) } } @@ -37,7 +37,7 @@ impl<'a> ArrangerVClips<'a> { let name = Tui::fg_bg(scene.color.lightest.rgb, scene.color.base.rgb, Expand::x(1, Tui::bold(true, scene.name.read().unwrap().clone())) ); - let clips = Tui::map(||ArrangerTrack::with_widths(tracks), move|(index, track, x1, x2), _| + let clips = Map(||ArrangerTrack::with_widths(tracks), move|(index, track, x1, x2), _| Push::x((x2 - x1) as u16, Self::format_clip(scene, index, track, (x2 - x1) as u16, height)) ); Fixed::y(height, row!(icon, name, clips)) diff --git a/src/arranger/arranger_v_head.rs b/src/arranger/arranger_v_head.rs index 0a9a68a3..cfef2096 100644 --- a/src/arranger/arranger_v_head.rs +++ b/src/arranger/arranger_v_head.rs @@ -20,7 +20,7 @@ render!(Tui: (self: ArrangerVHead<'a>) => { row!(Tui::fg(color.light.rgb, "▎"), Tui::fg(color.lightest.rgb, field)) } Some(Push::x(self.scenes_w, - Tui::map(||ArrangerTrack::with_widths(self.tracks), |(_, track, x1, x2), i| { + Map(||ArrangerTrack::with_widths(self.tracks), |(_, track, x1, x2), i| { let (w, h) = (ArrangerTrack::MIN_WIDTH.max(x2 - x1), HEADER_H); let color = track.color(); let input = Self::format_input(track); diff --git a/src/clock/clock_tui.rs b/src/clock/clock_tui.rs index 5221b2bc..80d9c0c4 100644 --- a/src/clock/clock_tui.rs +++ b/src/clock/clock_tui.rs @@ -74,11 +74,11 @@ render!(Tui: (self: TransportView<'a>) => Outer( pub struct PlayPause { pub compact: bool, pub playing: bool } render!(Tui: (self: PlayPause) => Tui::bg( if self.playing{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}, - Tui::either(self.compact, - Thunk::new(||Fixed::x(9, Tui::either(self.playing, + Either(self.compact, + Thunk::new(||Fixed::x(9, Either(self.playing, Tui::fg(Color::Rgb(0, 255, 0), " PLAYING "), Tui::fg(Color::Rgb(255, 128, 0), " STOPPED ")))), - Thunk::new(||Fixed::x(5, Tui::either(self.playing, + Thunk::new(||Fixed::x(5, Either(self.playing, Tui::fg(Color::Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), Tui::fg(Color::Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))))))); @@ -95,7 +95,7 @@ impl BeatStats { Self { compact, bpm: format!("{:.3}", clock.timebase.bpm.get()), beat, time } } } -render!(Tui: (self: BeatStats) => Tui::either(self.compact, +render!(Tui: (self: BeatStats) => Either(self.compact, row!( FieldV(TuiTheme::g(128).into(), "BPM", &self.bpm), FieldV(TuiTheme::g(128).into(), "Beat", &self.beat), @@ -124,7 +124,7 @@ impl OutputStats { } } } -render!(Tui: (self: OutputStats) => Tui::either(self.compact, +render!(Tui: (self: OutputStats) => Either(self.compact, row!( FieldV(TuiTheme::g(128).into(), "SR", &self.sample_rate), FieldV(TuiTheme::g(128).into(), "Buf", &self.buffer_size), diff --git a/src/edn.rs b/src/edn.rs index 289d3e81..931fcdc3 100644 --- a/src/edn.rs +++ b/src/edn.rs @@ -1,37 +1,4 @@ use crate::*; -use std::sync::{Arc, RwLock}; -use std::collections::BTreeMap; -pub use clojure_reader::edn::Edn; -//pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError}; - -/// EDN parsing helper. -#[macro_export] macro_rules! edn { - ($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => { - match $edn { $($pat => $expr),* } - }; - ($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => { - for $edn in $args { - edn!($edn { $($pat => $expr),* }) - } - }; -} - -pub trait FromEdn: Sized { - const ID: &'static str; - fn from_edn (context: C, expr: &[Edn<'_>]) -> Usually; -} - -/// Implements the [FromEdn] trait. -#[macro_export] macro_rules! from_edn { - ($id:expr => |$context:tt:$Context:ty, $args:ident| -> $T:ty $body:block) => { - impl FromEdn<$Context> for $T { - const ID: &'static str = $id; - fn from_edn <'e> ($context: $Context, $args: &[Edn<'e>]) -> Usually { - $body - } - } - } -} from_edn!("sampler" => |jack: &Arc>, args| -> crate::Sampler { let mut name = String::new(); diff --git a/src/lib.rs b/src/lib.rs index a60d3762..84d9c50b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ pub(crate) use ::tek_layout::{ Output, Content, Thunk, render, Input, Handle, handle, kexp, kpat, + edn::*, tui::{ Tui, TuiIn, key, ctrl, shift, alt, diff --git a/src/midi/midi_launch.rs b/src/midi/midi_launch.rs index 49ec833e..c196b5a4 100644 --- a/src/midi/midi_launch.rs +++ b/src/midi/midi_launch.rs @@ -15,12 +15,13 @@ pub trait HasPlayPhrase: HasClock { None } } - fn pulses_since_start_looped (&self) -> Option { + fn pulses_since_start_looped (&self) -> Option<(f64, f64)> { if let Some((started, Some(phrase))) = self.play_phrase().as_ref() { let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); let length = phrase.read().unwrap().length.max(1); // prevent div0 on empty phrase + let times = (elapsed as usize / length) as f64; let elapsed = (elapsed as usize % length) as f64; - Some(elapsed) + Some((times, elapsed)) } else { None } diff --git a/src/pool/clip_select.rs b/src/pool/clip_select.rs index 44a2b3d7..5d07f341 100644 --- a/src/pool/clip_select.rs +++ b/src/pool/clip_select.rs @@ -25,7 +25,9 @@ impl ClipSelected { name, color, time: state.pulses_since_start_looped() - .map(|elapsed|format!("+{:>}", state.clock().timebase.format_beats_0(elapsed))) + .map(|(times, time)|format!("{:>3}x {:>}", + times+1.0, + state.clock().timebase.format_beats_1(time))) .unwrap_or_else(||String::from(" ")) } } @@ -44,7 +46,7 @@ impl ClipSelected { let current = state.clock().playhead.pulse.get(); if target > current { let remaining = target - current; - format!("-{:>}", state.clock().timebase.format_beats_0(remaining)) + format!("-{:>}", state.clock().timebase.format_beats_1(remaining)) } else { String::new() } diff --git a/src/pool/pool_tui.rs b/src/pool/pool_tui.rs index 614cd450..4fdb8166 100644 --- a/src/pool/pool_tui.rs +++ b/src/pool/pool_tui.rs @@ -7,7 +7,7 @@ render!(Tui: (self: PoolView<'a>) => { let color = self.1.phrase().read().unwrap().color; Outer( Style::default().fg(color.dark.rgb).bg(color.darkest.rgb) - ).enclose(Tui::map(||model.phrases().iter(), |clip, i|{ + ).enclose(Map(||model.phrases().iter(), |clip, i|{ let item_height = 1; let item_offset = i as u16 * item_height; let selected = i == model.phrase_index(); @@ -20,8 +20,8 @@ render!(Tui: (self: PoolView<'a>) => { offset(Tui::bg(if selected { color.light.rgb } else { color.base.rgb }, lay!( Align::w(Tui::fg(color.lightest.rgb, Tui::bold(selected, name))), Align::e(Tui::fg(color.lightest.rgb, Tui::bold(selected, length))), - Align::w(Tui::when(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶")))), - Align::e(Tui::when(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀")))), + Align::w(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶")))), + Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀")))), ))) })) }); diff --git a/src/sampler/sample_list.rs b/src/sampler/sample_list.rs index ad46d003..8ce01bd7 100644 --- a/src/sampler/sample_list.rs +++ b/src/sampler/sample_list.rs @@ -17,7 +17,7 @@ render!(Tui: (self: SampleList<'a>) => { let note_lo = editor.note_lo().load(Relaxed); let note_pt = editor.note_point(); let note_hi = editor.note_hi(); - Outer(Style::default().fg(TuiTheme::g(96))).enclose(Tui::map(move||(note_lo..=note_hi).rev(), move|note, i| { + Outer(Style::default().fg(TuiTheme::g(96))).enclose(Map(move||(note_lo..=note_hi).rev(), move|note, i| { let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); diff --git a/src/sequencer.rs b/src/sequencer.rs index e0656c6c..3e84f468 100644 --- a/src/sequencer.rs +++ b/src/sequencer.rs @@ -47,38 +47,27 @@ from_jack!(|jack|SequencerTui { clock, } }); -render!(Tui: (self: SequencerTui) => { - let w = - self.size.w(); - let phrase_w = - if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let color = self.player.play_phrase().as_ref().map(|(_,p)| - p.as_ref().map(|p|p.read().unwrap().color) - ).flatten().clone(); - let toolbar = Tui::when(self.transport, - TransportView::new(true, &self.clock)); - let selectors = Tui::when(self.selectors, - Bsp::e(ClipSelected::play_phrase(&self.player), ClipSelected::next_phrase(&self.player))); - let pool_w = - if self.pool.visible { phrase_w } else { 0 }; - let pool = - Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool)))); - let edit_clip = - MidiEditClip(&self.editor); - self.size.of(Bsp::s( - toolbar, - Bsp::s( - lay!(Align::w(edit_clip), Align::e(selectors)), - Bsp::n( - Align::x(Fixed::y(1, MidiEditStatus(&self.editor))), - Bsp::w( - Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool)))), - Fill::xy(&self.editor), - ), - ) - ) - )) -}); +render!(Tui: (self: SequencerTui) => self.size.of( + Bsp::s(self.toolbar_view(), + Bsp::n(self.status_view(), + Bsp::w(self.pool_view(), Fill::xy(&self.editor)))))); +impl SequencerTui { + fn toolbar_view (&self) -> impl Content + use<'_> { + self.transport.then(||TransportView::new(true, &self.clock)) + } + fn status_view (&self) -> impl Content + use<'_> { + let edit_clip = MidiEditClip(&self.editor); + let selectors = When(self.selectors, Bsp::e(ClipSelected::play_phrase(&self.player), ClipSelected::next_phrase(&self.player))); + row!(selectors, edit_clip, MidiEditStatus(&self.editor)) + } + fn pool_view (&self) -> impl Content + use<'_> { + let w = self.size.w(); + let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + let pool_w = if self.pool.visible { phrase_w } else { 0 }; + let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool)))); + Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool)))) + } +} audio!(|self:SequencerTui, client, scope|{ // Start profiling cycle let t0 = self.perf.get_t0();