diff --git a/.gitmodules b/.gitmodules index 34ea4da..c4e9da5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = ssh://git@codeberg.org/unspeaker/dizzle.git [submodule "rust-jack"] path = rust-jack - url = ssh://git@codeberg.org/unspeaker/rust-jack + url = https://codeberg.org/unspeaker/rust-jack diff --git a/PAPER.qmd b/PAPER.qmd deleted file mode 100644 index e69de29..0000000 diff --git a/dizzle b/dizzle index af2a107..e984fbb 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit af2a10751f87caef8e5526d1f692c941b8de8357 +Subproject commit e984fbb9fe8e588399744d50841d006454c16657 diff --git a/examples/tui_00.rs b/examples/tui_00.rs index 8aedd9f..730efa4 100644 --- a/examples/tui_00.rs +++ b/examples/tui_00.rs @@ -1,7 +1,7 @@ use ::{ std::{io::stdout, sync::{Arc, RwLock}}, + tengri::{*, term::*, lang::*, keys::*, draw::*, space::*, dizzle::*}, ratatui::style::Color, - tengri::{*, lang::*}, }; tui_main!(State { @@ -10,7 +10,7 @@ tui_main!(State { }); tui_keys!(|self: State, input| { - Ok(()) + todo!() }); tui_view!(|self: State| { @@ -18,10 +18,10 @@ tui_view!(|self: State| { let wh = (self.size.w(), self.size.h()); let src = VIEWS.get(self.cursor).unwrap_or(&""); let heading = format!("State {}/{} in {:?}", index, VIEWS.len(), &wh); - let title = bg(Color::Rgb(60, 10, 10), heading.align_n().push_y(1)); - let code = bg(Color::Rgb(10, 60, 10), format!("{}", src).align_n().push_y(2)); + let title = bg(Color::Rgb(60, 10, 10), y_push(1, align_n(heading))); + let code = bg(Color::Rgb(10, 60, 10), y_push(2, align_n(format!("{}", src)))); //let content = ;//();//bg(Color::Rgb(10, 10, 60), View(self, CstIter::new(src))); - let widget = thunk(move|to: &mut Tui|self.interpret(to, &src).map(Some)); + let widget = thunk(move|to: &mut Tui|self.interpret(to, &src)); self.size.of(south(title, north(code, widget))) }); @@ -32,7 +32,7 @@ struct State { /** User-controllable value. */ cursor: usize, /** Rendered window size. */ - size: Sizer, + size: crate::space::Size, } impl Interpret> for State { diff --git a/shell.nix b/shell.nix index 6f44581..dba3faf 100644 --- a/shell.nix +++ b/shell.nix @@ -1,28 +1,13 @@ #!/usr/bin/env nix-shell {pkgs?import{}}:let - name = "tengri"; - stdenv = pkgs.clang19Stdenv; - LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; - LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath []; + stdenv = pkgs.clang19Stdenv; + name = "tengri"; + nativeBuildInputs = [ pkgs.pkg-config pkgs.clang pkgs.libclang pkgs.mold pkgs.bacon ]; + buildInputs = [ pkgs.libclang pkgs.jack2 ]; + LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath []; in pkgs.mkShell.override { inherit stdenv; } { - inherit name LIBCLANG_PATH LD_LIBRARY_PATH; - buildInputs = [ - pkgs.libclang - pkgs.jack2 - ]; - nativeBuildInputs = [ - pkgs.pkg-config - pkgs.clang - pkgs.libclang - pkgs.mold - pkgs.bacon - (pkgs.quarto.overrideAttrs (oldAttrs: { # https://github.com/NixOS/nixpkgs/issues/519484 - postPatch = (oldAttrs.postPatch or "") + '' - substituteInPlace bin/quarto.js \ - --replace-fail "syntax-highlighting" "highlight-style" - ''; - })) - ]; + inherit name nativeBuildInputs buildInputs LIBCLANG_PATH LD_LIBRARY_PATH; } diff --git a/src/draw/color.rs b/src/color.rs similarity index 99% rename from src/draw/color.rs rename to src/color.rs index ccea5e6..f16358a 100644 --- a/src/draw/color.rs +++ b/src/color.rs @@ -1,10 +1,10 @@ -use crate::lang::impl_from; use ::ratatui::style::Color; -use ::rand::distributions::uniform::UniformSampler; +use crate::lang::impl_from; pub(crate) use ::palette::{ Okhsl, Srgb, OklabHue, Mix, okhsl::UniformOkhsl, convert::{FromColor, FromColorUnclamped} }; +use rand::distributions::uniform::UniformSampler; pub fn rgb (r: u8, g: u8, b: u8) -> ItemColor { let term = Color::Rgb(r, g, b); @@ -29,7 +29,6 @@ pub fn rgb_to_okhsl (color: Color) -> Okhsl { } pub trait HasColor { fn color (&self) -> ItemColor; } - #[macro_export] macro_rules! has_color { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? { @@ -43,11 +42,8 @@ pub struct ItemColor { pub term: Color, pub okhsl: Okhsl } - impl_from!(ItemColor: |term: Color| Self { term, okhsl: rgb_to_okhsl(term) }); - impl_from!(ItemColor: |okhsl: Okhsl| Self { okhsl, term: okhsl_to_rgb(okhsl) }); - // A single color within item theme parameters, in OKHSL and RGB representations. impl ItemColor { #[cfg(feature = "term")] pub const fn from_tui (term: Color) -> Self { @@ -84,11 +80,8 @@ pub struct ItemTheme { pub darker: ItemColor, pub darkest: ItemColor, } - impl_from!(ItemTheme: |base: ItemColor| Self::from_item_color(base)); - impl_from!(ItemTheme: |base: Color| Self::from_tui_color(base)); - impl ItemTheme { #[cfg(feature = "term")] pub const G: [Self;256] = { let mut builder = dizzle::konst::array::ArrayBuilder::new(); diff --git a/src/draw.rs b/src/draw.rs index 64738bd..ad47922 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -1,53 +1,4 @@ -use crate::*; - -/// Output target. -/// -/// ``` -/// use tengri::*; -/// struct TestOut { w: u16, h: u16 }; -/// impl Wide for TestOut {} -/// impl Tall for TestOut {} -/// impl Xy for TestOut { fn x (&self) -> u16 { 0 } fn y (&self) -> u16 { 0 } } -/// impl Screen for TestOut { -/// type Unit = u16; -/// fn show > (&mut self, _: D) -> Drawn { -/// println!("placed"); -/// Ok(None) -/// } -/// } -/// -/// impl_draw!(|self: String, to: TestOut|{ -/// to.w = self.len() as u16; -/// Ok(None) -/// }); -/// ``` -pub trait Screen: Xy + Wh + Send + Sync + Sized { - type Unit: Coord; - /// Render drawable in subarea specified by `area` - fn show > (&mut self, content: T) -> Perhaps>; -} - -/// Implement the [Draw] trait for a particular drawable and [Screen]. -/// -/// ``` -/// use tengri::*; -/// struct MyDrawable; -/// impl_draw!(|self: MyDrawable, to: Tui|{ -/// todo!("your draw logic") -/// }); -/// ``` -#[macro_export] macro_rules! impl_draw ( - ($(<$($T:ident: $Trait:path,)+>)?| - $self:ident:$Self:path, $to:ident:$To:ty - |$draw:block)=>{ impl$(<$($T:$Trait),+>)? Draw<$To> for $Self { - fn draw ($self, $to: &mut $To) -> Perhaps::Unit>> $draw - } }; - ($(<$($T:ident: $Trait:path,)+>)?| - $self:ident:$Self:ty, $to:ident:$To:ty - |$draw:block)=>{ impl$(<$($T:$Trait),+>)? Draw<$To> for $Self { - fn draw ($self, $to: &mut $To) -> Perhaps::Unit>> $draw - } } -); +use crate::{*, lang::*, color::*, space::*}; /// Drawable that supports dynamic dispatch. /// @@ -60,47 +11,36 @@ pub trait Screen: Xy + Wh + Send + Sync + Sized { /// To draw a thing multiple times, instead of explicitly constructing it /// every time, implement the [View] trait instead, which will construct /// a [Draw]able. -/// +/// /// ``` -/// use tengri::*; -/// struct MyWidget(bool); -/// impl Draw for MyWidget { -/// fn draw (self, to: &mut Tui) -> Perhaps> { -/// todo!("your draw logic") +/// use tengri::draw::*; +/// struct TestScreen(bool); +/// impl Screen for TestScreen { type Unit = u16; } +/// struct TestWidget(bool); +/// impl Draw for TestWidget { +/// fn draw (&self, screen: &mut T) -> Usually> { +/// screen.0 |= self.0; /// } /// } +/// let mut screen = TestScreen(false); +/// TestWidget(false).draw(&mut screen); +/// TestWidget(true).draw(&mut screen); +/// TestWidget(false).draw(&mut screen); /// ``` -pub trait Draw { - fn draw (self, to: &mut S) -> Drawn; - fn layout (&self, area: XYWH) -> Drawn { - Ok(Some(area)) +pub trait Draw { + fn draw (self, to: &mut T) -> Usually>; +} +impl> Draw for &D { + fn draw (self, to: &mut T) -> Usually> { + todo!() } } - -pub type Drawn = Perhaps>; - -impl Draw for () { - fn draw (self, _: &mut S) -> Drawn { - Ok(None) +impl> Draw for Option { + fn draw (self, to: &mut T) -> Usually> { + todo!() } } -impl_draw!(,>|self: Option, to: S|{ - self.map(|it|it.draw(to)).transpose().map(Option::unwrap_or_default) -}); - -//impl> Draw for RwLock { - //fn draw (self, __: &mut S) -> Drawn { - //todo!() - //} -//} - -//impl> Draw for Arc { - //fn draw (self, __: &mut T) -> Perhaps> { - //todo!() - //} -//} - /// Emit a [Draw]able. /// /// Speculative. How to avoid conflicts with [Draw] proper? @@ -108,29 +48,74 @@ pub trait View { fn view (&self) -> impl Draw; } -impl View for () { - fn view (&self) -> impl Draw { - () +pub const fn thunk Usually>> (draw: F) -> Thunk { + Thunk(draw, std::marker::PhantomData) +} + +/// Because we can't implement [Draw] for `F: FnOnce...` without conflicts. +pub struct ThunkUsually>>( + pub F, + std::marker::PhantomData +); +implUsually>> Draw for Thunk { + fn draw (self, to: &mut T) -> Usually> { + (self.0)(to) } } -impl> Draw for &V { - fn draw (self, to: &mut T) -> Perhaps> { - self.view().draw(to) - } +/// Only render when condition is true. +/// +/// ``` +/// # fn test () -> impl tengri::Draw { +/// tengri::when(true, "Yes") +/// # } +/// ``` +pub const fn when (condition: bool, draw: impl Draw) -> impl Draw { + thunk(move|to: &mut T|if condition { draw.draw(to) } else { Ok(Default::default()) }) } -features! { - "draw": [ - color, - coord, - iter, - layout, - lrtb, - sizer, - space, - split, - thunk, - xywh - ] +/// Render one thing if a condition is true and another false. +/// +/// ``` +/// # fn test () -> impl tengri::Draw { +/// tengri::either(true, "Yes", "No") +/// # } +/// ``` +pub const fn either (condition: bool, a: impl Draw, b: impl Draw) -> impl Draw { + thunk(move|to: &mut T|if condition { a.draw(to) } else { b.draw(to) }) +} + +/// Output target. +/// +/// ``` +/// use tengri::*; +/// +/// struct TestOut(impl TwoD); +/// +/// impl Screen for TestOut { +/// type Unit = u16; +/// fn place_at + ?Sized> (&mut self, area: impl TwoD, _: &T) { +/// println!("placed: {area:?}"); +/// () +/// } +/// } +/// +/// impl Draw for String { +/// fn draw (&self, to: &mut TestOut) -> Usually> { +/// to.area_mut().set_w(self.len() as u16); +/// } +/// } +/// ``` +pub trait Screen: Space + Send + Sync + Sized { + type Unit: Coord; + /// Render drawable in area. + fn place <'t, T: Draw + ?Sized> (&mut self, content: &'t T) { + self.place_at(self.xywh(), content) + } + /// Render drawable in subarea specified by `area` + fn place_at <'t, T: Draw + ?Sized> ( + &mut self, area: XYWH, content: &'t T + ) { + unimplemented!() + } } diff --git a/src/draw/coord.rs b/src/draw/coord.rs deleted file mode 100644 index 40d784d..0000000 --- a/src/draw/coord.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::*; - -/// A numeric type that can be used as coordinate. -/// -/// FIXME: Replace with `num` crate? -/// FIXME: Use AsRef/AsMut? -/// -/// ``` -/// use tengri::*; -/// let a: u16 = Coord::zero(); -/// let b: u16 = a.plus(1); -/// let c: u16 = a.minus(2); -/// let d = a.atomic(); -/// ``` -pub trait Coord: Send + Sync + Copy - + Add - + Sub - + Mul - + Div - + Ord + PartialEq + Eq - + Debug + Display + Default - + From + Into - + Into - + Into - + std::iter::Step -{ - /// Zero in own type. - fn zero () -> Self { 0.into() } - /// Addition. - fn plus (self, other: Self) -> Self; - /// Saturating subtraction. - fn minus (self, other: Self) -> Self { if self >= other { self - other } else { 0.into() } } - /// Convert to [AtomicUsize]. - fn atomic (self) -> AtomicUsize { AtomicUsize::new(self.into()) } -} diff --git a/src/draw/iter.rs b/src/draw/iter.rs deleted file mode 100644 index 1bbdcfa..0000000 --- a/src/draw/iter.rs +++ /dev/null @@ -1,86 +0,0 @@ -use super::*; - -/// Iterate over a collection of the same kind of [Draw]able: -pub fn iter <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( - _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, -) -> impl Draw { - thunk(move|_to: &mut S|{ todo!() }) -} - -/// Iterate over a collection of the same kind of [Draw]able: -pub fn iter_once <'a, S: Screen, D: 'a, U: Draw> ( - _iter: impl Iterator, _draw: impl Fn(D, usize)->U, -) -> impl Draw { - thunk(move|_to: &mut S|{ todo!() }) -} - -/// Iterate over a collection of various [Draw]ables: -pub fn iter_dyn < - S: Screen, // Target screen - D, // Input doesn't need to be [Draw]able, output does - V: Fn()->I, // Function that returns the iterator - I: Iterator, // Type of the iterator - F: Fn(&D, usize)->dyn Draw, // Function that returns [Draw]able from iterator item -> (_items: V, _cb: F) -> impl Draw { - thunk(move|_to: &mut S|{ todo!() }) -} - -pub fn iter_north <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( - _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, -) -> impl Draw { - thunk(move|_to: &mut S|{ todo!() }) -} - -pub fn iter_east <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( - _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, -) -> impl Draw { - thunk(move|_to: &mut S|{ todo!() }) -} - -pub fn iter_east_fixed <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( - _height: S::Unit, _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, -) -> impl Draw { - thunk(move|_to: &mut S|{ todo!() }) -} - -pub fn iter_south <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( - _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, -) -> impl Draw { - thunk(move|_to: &mut S|{ todo!() }) -} - -pub fn iter_south_fixed <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( - _height: S::Unit, _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, -) -> impl Draw { - thunk(move|_to: &mut S|{ todo!() }) -} - -pub fn iter_west <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( - _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, -) -> impl Draw { - thunk(move|_to: &mut S|{ todo!() }) -} - -pub fn row_south (south: S::Unit, height: S::Unit, content: impl Draw) - -> impl Draw -{ - content.exact_h(height).push_y(south) -} - -pub fn row_north (north: S::Unit, height: S::Unit, content: impl Draw) - -> impl Draw -{ - content.exact_h(height).pull_y(north) -} - -pub fn col_east (east: S::Unit, width: S::Unit, content: impl Draw) - -> impl Draw -{ - content.exact_w(width).push_x(east) -} - -pub fn col_west (west: S::Unit, width: S::Unit, content: impl Draw) - -> impl Draw -{ - content.exact_w(width).pull_x(west) -} diff --git a/src/draw/layout.rs b/src/draw/layout.rs deleted file mode 100644 index 71dca39..0000000 --- a/src/draw/layout.rs +++ /dev/null @@ -1,339 +0,0 @@ -use crate::*; - -pub trait Layout: Draw + Sized { - fn full_w (self) -> impl Draw { - Full::W(self) - } - fn full_h (self) -> impl Draw { - Full::H(self) - } - fn full_wh (self) -> impl Draw { - Full::WH(self) - } - - fn exact_w >> (self, x: N) -> impl Draw { - Exact::W(self, x.into()) - } - fn exact_h >> (self, y: N) -> impl Draw { - Exact::H(self, y.into()) - } - fn exact_wh >> (self, x: N, y: N) -> impl Draw { - Exact::WH(self, x.into(), y.into()) - } - - fn min_w >> (self, x: N) -> impl Draw { - Min::W(self, x.into()) - } - fn min_h >> (self, y: N) -> impl Draw { - Min::H(self, y.into()) - } - fn min_wh >> (self, x: N, y: N) -> impl Draw { - Min::WH(self, x.into(), y.into()) - } - - fn max_w >> (self, x: N) -> impl Draw { - Max::W(self, x.into()) - } - fn max_h >> (self, y: N) -> impl Draw { - Max::H(self, y.into()) - } - fn max_wh >> (self, x: N, y: N) -> impl Draw { - Max::WH(self, x.into(), y.into()) - } - - fn pad_w >> (self, x: N) -> impl Draw { - Pad::W(self, x.into()) - } - fn pad_h >> (self, y: N) -> impl Draw { - Pad::H(self, y.into()) - } - fn pad_wh >> (self, x: N, y: N) -> impl Draw { - Pad::WH(self, x.into(), y.into()) - } - - fn pull_x >> (self, x: N) -> impl Draw { - Pull::X(self, x.into()) - } - fn pull_y >> (self, y: N) -> impl Draw { - Pull::X(self, y.into()) - } - fn pull_xy >> (self, x: N, y: N) -> impl Draw { - Pull::XY(self, x.into(), y.into()) - } - - fn push_x >> (self, x: N) -> impl Draw { - Push::X(self, x.into()) - } - fn push_y >> (self, y: N) -> impl Draw { - Push::X(self, y.into()) - } - fn push_xy >> (self, x: N, y: N) -> impl Draw { - Push::XY(self, x.into(), y.into()) - } - - fn align (self, azimuth: impl Into>) -> impl Draw { - Align(azimuth.into(), self) - } - fn align_c (self) -> impl Draw { - Align(Some(Azimuth::C), self) - } - fn align_x (self) -> impl Draw { - Align(Some(Azimuth::X), self) - } - fn align_y (self) -> impl Draw { - Align(Some(Azimuth::Y), self) - } - - fn align_n (self) -> impl Draw { - Align(Some(Azimuth::N), self) - } - fn align_s (self) -> impl Draw { - Align(Some(Azimuth::S), self) - } - fn align_e (self) -> impl Draw { - Align(Some(Azimuth::E), self) - } - fn align_w (self) -> impl Draw { - Align(Some(Azimuth::W), self) - } - - fn align_ne (self) -> impl Draw { - Align(Some(Azimuth::NE), self) - } - fn align_se (self) -> impl Draw { - Align(Some(Azimuth::SE), self) - } - fn align_nw (self) -> impl Draw { - Align(Some(Azimuth::NW), self) - } - fn align_sw (self) -> impl Draw { - Align(Some(Azimuth::SW), self) - } - - fn origin (self, azimuth: impl Into>) -> impl Draw { - Origin(azimuth.into(), self) - } - fn origin_c (self) -> impl Draw { - Origin(Some(Azimuth::C), self) - } - fn origin_x (self) -> impl Draw { - Origin(Some(Azimuth::X), self) - } - fn origin_y (self) -> impl Draw { - Origin(Some(Azimuth::Y), self) - } - - fn origin_n (self) -> impl Draw { - Origin(Some(Azimuth::N), self) - } - fn origin_s (self) -> impl Draw { - Origin(Some(Azimuth::S), self) - } - fn origin_e (self) -> impl Draw { - Origin(Some(Azimuth::E), self) - } - fn origin_w (self) -> impl Draw { - Origin(Some(Azimuth::W), self) - } - - fn origin_ne (self) -> impl Draw { - Origin(Some(Azimuth::NE), self) - } - fn origin_se (self) -> impl Draw { - Origin(Some(Azimuth::SE), self) - } - fn origin_nw (self) -> impl Draw { - Origin(Some(Azimuth::NW), self) - } - fn origin_sw (self) -> impl Draw { - Origin(Some(Azimuth::SW), self) - } -} - -impl> Layout for T {} - -/// Use whole drawing area along one or both axes. -/// -/// ``` -/// use tengri::Layout; -/// let _ = "".full_w(); -/// let _ = "".full_h(); -/// let _ = "".full_wh(); -/// ``` -pub enum Full> { - __(PhantomData), - W(I), - H(I), - WH(I), -} - -impl_draw!(,>|self: Full, _to: T|{ - todo!() -}); - -/// Move content in the positive direction of one or both axes. -/// -/// ``` -/// use tengri::Layout; -/// let _ = "".push_x(1); -/// let _ = "".push_y(1); -/// let _ = "".push_xy(1, 1); -/// ``` -pub enum Push, X: Into>> { - __(PhantomData), - X(I, X), - Y(I, X), - XY(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Push, _to: T|{ - todo!() -}); - -/// Move content in the negative direction of one or both axes. -/// -/// ``` -/// use tengri::Layout; -/// let _ = "".pull_x(1); -/// let _ = "".pull_y(1); -/// let _ = "".pull_xy(1, 1); -/// ``` -pub enum Pull, X: Into>> { - __(PhantomData), - X(I, X), - Y(I, X), - XY(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Pull, _to: T|{ - todo!() -}); - -/// Only draw content if area is above a certain size. -/// -/// ``` -/// use tengri::Layout; -/// let _ = "".min_w(1); -/// let _ = "".min_h(1); -/// let _ = "".min_wh(1, 1); -/// ``` -pub enum Min, X: Into>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Min, _to: T|{ - todo!() -}); - -/// Set maximum size of of drawing area. -/// -/// ``` -/// use tengri::Layout; -/// let _ = "".max_w(1); -/// let _ = "".max_h(1); -/// let _ = "".max_wh(1, 1); -/// ``` -pub enum Max, X: Into>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Max, _to: T|{ - todo!() -}); - -/// Set size of of drawing area. -/// -/// ``` -/// use tengri::Layout; -/// let _ = "".exact_w(1); -/// let _ = "".exact_h(1); -/// let _ = "".exact_wh(1, 1); -/// ``` -pub enum Exact, X: Into>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Exact, _to: T|{ - todo!() -}); - -/// Define inner drawing area. -/// -/// ``` -/// use tengri::Layout; -/// let _ = "".pad_w(1); -/// let _ = "".pad_h(1); -/// let _ = "".pad_wh(1, 1); -/// ``` -pub enum Pad, X: Into>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Pad, _to: T|{ - todo!() -}); - -pub struct Align(Option, T); - -impl_draw!(,>|self: Align, _to: S|{ - todo!() -}); - -/// Where is [0, 0] located? -/// -/// ``` -/// use tengri::*; -/// use Azimuth::*; -/// let _ = "".align(NW); -/// ``` -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Debug, Copy, Clone, Default)] pub enum Azimuth { - #[default] C, X, Y, NW, N, NE, E, SE, S, SW, W -} - -/// ``` -/// use tengri::*; -/// let _ = area(None, "unareaed"); -/// let _ = area(XYWH(1, 2, 3, 4), "southeast"); -/// let _ = area(Some(XYWH(1, 2, 3, 4)), "southeast"); -/// ``` -pub fn area , U: Into>>> ( - origin: U, it: T -) -> Area { - Area(origin.into(), it) -} - -pub struct Area>(Option>, T); - -impl_draw!(,>|self: Area, _to: S|{ - todo!() -}); - -pub struct Origin(Option, T); - -impl_draw!(,>|self: Origin, _to: S|{ - todo!() -}); - -/// Something that has `[0, 0]` at a particular point. -pub trait HasOrigin { - fn origin (&self) -> Azimuth; -} - -impl> HasOrigin for T { - fn origin (&self) -> Azimuth { - *self.as_ref() - } -} diff --git a/src/draw/lrtb.rs b/src/draw/lrtb.rs deleted file mode 100644 index 7f8b182..0000000 --- a/src/draw/lrtb.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::*; -use Azimuth::*; - -impl> Lrtb for T {} - -pub trait Lrtb: Xywh { - fn lrtb (&self) -> [N;4] { - // FIXME: factor origin - [self.x(), self.y(), self.x()+self.w(), self.y()+self.h()] - } - fn iter_x (&self) -> impl Iterator where Self: HasOrigin { - self.x_west()..self.x_east() - } - fn x_west (&self) -> N where Self: HasOrigin { - let w = self.w(); - let a = self.origin(); - let d = match a { NW|W|SW => 0.into(), N|X|C|Y|S => w/2.into(), NE|E|SE => w }; - self.x().minus(d) - } - fn x_east (&self) -> N where Self: HasOrigin { - let w = self.w(); - let a = self.origin(); - let d = match a { NW|W|SW => w, N|X|C|Y|S => w/2.into(), NE|E|SE => 0.into() }; - self.x().plus(d) - } - fn x_center (&self) -> N where Self: HasOrigin { - todo!() - } - fn iter_y (&self) -> impl Iterator where Self: HasOrigin { - self.y_north()..self.y_south() - } - fn y_north (&self) -> N where Self: HasOrigin { - let a = self.origin(); - let h = self.h(); - let d = match a { NW|N|NE => 0.into(), W|X|C|Y|E => h/2.into(), SW|S|SE => h }; - self.y().minus(d) - } - fn y_south (&self) -> N where Self: HasOrigin { - let a = self.origin(); - let h = self.h(); - let d = match a { NW|N|NE => h, W|X|C|Y|E => h/2.into(), SW|S|SE => 0.into() }; - self.y().plus(d) - } - fn y_center (&self) -> N where Self: HasOrigin { - todo!() - } -} diff --git a/src/draw/sizer.rs b/src/draw/sizer.rs deleted file mode 100644 index 7ff5ed5..0000000 --- a/src/draw/sizer.rs +++ /dev/null @@ -1,31 +0,0 @@ -use super::*; -use std::sync::atomic::Ordering; - -/// Uses [AtomicUsize] to measure size during\ -/// rendering (which is normally read-only). -#[derive(Default, Debug, Clone)] -pub struct Sizer( - /// Width - pub Arc, - /// Height - pub Arc, -); - -impl Xy for Sizer { - fn x (&self) -> u16 { self.0.load(Ordering::Relaxed) as u16 } - fn y (&self) -> u16 { self.1.load(Ordering::Relaxed) as u16 } -} -impl Wide for Sizer { fn w (&self) -> u16 { self.0.load(Relaxed) as u16 } } -impl Tall for Sizer { fn h (&self) -> u16 { self.1.load(Relaxed) as u16 } } -impl PartialEq for Sizer { fn eq (&self, _: &Self) -> bool { todo!() } } - -impl Sizer { - pub const fn of (&self, of: impl Draw) -> impl Draw { - thunk(move|to: &mut T|{ - let area = of.draw(to)?; - self.0.store(area.map(|a|a.w()).unwrap_or(T::Unit::zero()).into(), Relaxed); - self.1.store(area.map(|a|a.h()).unwrap_or(T::Unit::zero()).into(), Relaxed); - Ok(area) - }) - } -} diff --git a/src/draw/space.rs b/src/draw/space.rs deleted file mode 100644 index 6576b44..0000000 --- a/src/draw/space.rs +++ /dev/null @@ -1,2 +0,0 @@ -use crate::*; - diff --git a/src/draw/split.rs b/src/draw/split.rs deleted file mode 100644 index 37ef914..0000000 --- a/src/draw/split.rs +++ /dev/null @@ -1,143 +0,0 @@ -use super::*; -use Azimuth::*; - -pub const fn east (a: impl Draw, b: impl Draw) -> impl Draw { - Split::East.half(a, b) -} - -pub const fn north (a: impl Draw, b: impl Draw) -> impl Draw { - Split::North.half(a, b) -} - -pub const fn west (a: impl Draw, b: impl Draw) -> impl Draw { - Split::West.half(a, b) -} - -pub const fn south (a: impl Draw, b: impl Draw) -> impl Draw { - Split::South.half(a, b) -} - -pub const fn above (a: impl Draw, b: impl Draw) -> impl Draw { - Split::Above.half(a, b) -} - -pub const fn below (a: impl Draw, b: impl Draw) -> impl Draw { - Split::Below.half(a, b) -} - -/// Split along an axis. Direction determines order. -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Copy, Clone, PartialEq, Debug, Default)] pub enum Split { - North, - South, - East, - West, - Above, - #[default] Below -} - -impl Split { - - /// ``` - /// use tengri::*; - /// let _ = Split::Above.half("", ""); - /// let _ = Split::Below.half("", ""); - /// let _ = Split::North.half("", ""); - /// let _ = Split::South.half("", ""); - /// let _ = Split::East.half("", ""); - /// let _ = Split::West.half("", ""); - /// ``` - pub const fn half (&self, a: impl Draw, b: impl Draw) -> impl Draw { - thunk(move|to: &mut T|{ - let (area_a, area_b) = to.xywh().split_half(self); - let (origin_a, origin_b) = self.origins(); - let a = a.align(origin_a); - let b = b.align(origin_b); - match self { - Self::Below => { - to.show(area(area_b, b))?; - to.show(area(area_b, a))?; - }, - _ => { - to.show(area(area_a, a))?; - to.show(area(area_b, b))?; - } - } - Ok(Some(to.xywh())) // FIXME: compute and return actually used area - }) - } - - /// Newly split areas begin at the center of the split - /// to maintain centeredness in the user's field of view. - /// - /// Use [align] to override that and always start - /// at the top, bottom, etc. - /// - /// ``` - /// /* - /// - /// Split east: Split south: - /// | | | | A | - /// | <-A|B-> | |---------| - /// | | | | B | - /// - /// */ - /// ``` - const fn origins (&self) -> (Azimuth, Azimuth) { - use Azimuth::*; - match self { - Self::South => (S, N), - Self::East => (E, W), - Self::North => (N, S), - Self::West => (W, E), - Self::Above => (C, C), - Self::Below => (C, C), - } - } - - ///// ``` - ///// use tengri::*; - ///// let _ = Split::Below.iter([ - ///// "Leftbar" - ///// .min_w(10).max_w(15).align(Azimuth::NW), - ///// "Rightbar" - ///// .min_w(10).max_w(12).align(Azimuth::NE), - ///// "Center" - ///// .min_w(20).max_w(40).align(Azimuth::C), - ///// ].iter()); - ///// ``` - //pub fn iter > (&self, _: impl Iterator) { - //todo!() - //} - -} - -#[macro_export] macro_rules! north { - ($head:expr $(,)?) => { $head }; - ($head:expr, $($tail:expr),* $(,)?) => { north($head, north!($($tail,)*)) }; -} - -#[macro_export] macro_rules! south { - ($head:expr $(,)?) => { $head }; - ($head:expr, $($tail:expr),* $(,)?) => { south($head, south!($($tail,)*)) }; -} - -#[macro_export] macro_rules! east { - ($head:expr $(,)?) => { $head }; - ($head:expr, $($tail:expr),* $(,)?) => { east($head, east!($($tail,)*)) }; -} - -#[macro_export] macro_rules! west { - ($head:expr $(,)?) => { $head }; - ($head:expr $(, $tail:expr)* $(,)?) => { west($head, west!($($tail,)*)) }; -} - -#[macro_export] macro_rules! above { - ($head:expr $(,)?) => { $head }; - ($head:expr $(, $tail:expr)* $(,)?) => { above($head, above!($($tail,)*)) }; -} - -#[macro_export] macro_rules! below { - ($head:expr $(,)?) => { $head }; - ($head:expr, $($tail:expr),* $(,)?) => { below($head, below!($($tail,)*)) }; -} diff --git a/src/draw/thunk.rs b/src/draw/thunk.rs deleted file mode 100644 index af27f5b..0000000 --- a/src/draw/thunk.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::*; - -/// Because we can't implement [Draw] for `F: FnOnce...` without conflicts. -pub struct Thunk(pub F, std::marker::PhantomData); - -implPerhaps>> Draw for Thunk { - fn draw (self, to: &mut T) -> Perhaps> { - (self.0)(to) - } -} - -/// Basic [Draw]able closure. -/// -/// ``` -/// # use tengri::*; -/// # fn test () -> impl Draw { -/// thunk(|to: &mut Tui|Ok(Some(to.1))) -/// # } -/// ``` -pub const fn thunk Perhaps>> ( - draw: F -) -> Thunk { - Thunk(draw, std::marker::PhantomData) -} - -/// Only render when condition is true. -/// -/// ``` -/// # use tengri::*; -/// # fn test () -> impl Draw { -/// when(true, "Yes") -/// # } -/// ``` -pub const fn when (condition: bool, draw: impl Draw) -> impl Draw { - thunk(move|to: &mut T|if condition { draw.draw(to) } else { Ok(Default::default()) }) -} - -/// Render one thing if a condition is true and another false. -/// -/// ``` -/// # use tengri::*; -/// # fn test () -> impl Draw { -/// either(true, "Yes", "No") -/// # } -/// ``` -pub const fn either (condition: bool, a: impl Draw, b: impl Draw) -> impl Draw { - thunk(move|to: &mut T|if condition { a.draw(to) } else { b.draw(to) }) -} diff --git a/src/draw/xywh.rs b/src/draw/xywh.rs deleted file mode 100644 index 5b308d1..0000000 --- a/src/draw/xywh.rs +++ /dev/null @@ -1,115 +0,0 @@ -use super::*; -use Split::*; - -pub trait Xy { - fn x (&self) -> N; - fn y (&self) -> N; -} - -pub trait Wh: Wide + Tall { - fn wh (&self) -> [N;2]; -} - -pub trait Xywh: Xy + Wh { - fn xywh (&self) -> XYWH { - XYWH(self.x(), self.y(), self.w(), self.h()) - } -} - -pub trait Wide: Xy { - fn w (&self) -> N { N::zero() } - fn w_min (&self) -> N { self.w() } - fn w_max (&self) -> N { self.w() } -} - -pub trait Tall { - fn h (&self) -> N { N::zero() } - fn h_min (&self) -> N { self.h() } - fn h_max (&self) -> N { self.h() } -} - -/// Point with size. -/// -/// ``` -/// # use tengri::*; -/// let xywh = XYWH(0u16, 0, 0, 0); -/// assert_eq!(XYWH(10u16, 10, 20, 20).center(), (20, 20)); -/// ``` -/// -/// * [ ] TODO: origin field (determines at which corner/side is X0 Y0) -/// -#[cfg_attr(test, derive(Arbitrary))] #[derive(Copy, Clone, Debug, Default, PartialEq)] -pub struct XYWH(pub N, pub N, pub N, pub N); - -impl Xy for XYWH { - fn x (&self) -> N { self.0 } - fn y (&self) -> N { self.1 } -} - -impl Wide for XYWH { fn w (&self) -> N { self.2 } } - -impl Tall for XYWH { fn h (&self) -> N { self.3 } } - -impl XYWH { - - pub fn zero () -> Self { - Self(0.into(), 0.into(), 0.into(), 0.into()) - } - - pub fn center (&self) -> (N, N) { - let Self(x, y, w, h) = *self; - (x.plus(w/2.into()), y.plus(h/2.into())) - } - - pub fn centered (&self) -> (N, N) { - let Self(x, y, w, h) = *self; - (x.minus(w/2.into()), y.minus(h/2.into())) - } - - pub fn centered_x (&self, n: N) -> Self { - let Self(x, y, w, h) = *self; - let x_center = (x.plus(w / 2.into())).minus(n / 2.into()); - let y_center = y.plus(h / 2.into()); - XYWH(x_center, y_center, n, 1.into()) - } - - pub fn centered_y (&self, n: N) -> Self { - let Self(x, y, w, h) = *self; - let x_center = x.plus(w / 2.into()); - let y_corner = (y.plus(h / 2.into())).minus(n / 2.into()); - XYWH(x_center, y_corner, 1.into(), n) - } - - pub fn centered_xy (&self, [n, m]: [N;2]) -> Self { - let Self(x, y, w, h) = *self; - let x_center = (x.plus(w / 2.into())).minus(n / 2.into()); - let y_corner = (y.plus(h / 2.into())).minus(m / 2.into()); - XYWH(x_center, y_corner, n, m) - } - - pub fn split_half (&self, direction: &Split) -> (Self, Self) { - let XYWH(x, y, w, h) = self.xywh(); - match direction { - South => (XYWH(x, y, w, h - h / 2.into()), XYWH(x, y + h / 2.into(), w, h / 2.into())), - East => (XYWH(x, y, w - w / 2.into(), h), XYWH(x + w / 2.into(), y, w / 2.into(), h)), - North => (XYWH(x, y + h / 2.into(), w, h - h / 2.into()), XYWH(x, y, w, h / 2.into())), - West => (XYWH(x + w / 2.into(), y, w - w / 2.into(), h), XYWH(x, y, w / 2.into(), h)), - Above | Below => (XYWH(x, y, w, h), XYWH(x, y, w, h)) - } - } - -} - -impl From<&ratatui::prelude::Rect> for XYWH { - fn from (rect: &ratatui::prelude::Rect) -> Self { - Self(rect.x, rect.y, rect.width, rect.height) - } -} - -impl + Tall> Wh for T { - fn wh (&self) -> [N;2] { - [self.w(), self.h()] - } -} - -impl + Wh> Xywh for T {} diff --git a/src/eval.rs b/src/eval.rs index 423cb64..9cb0048 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,229 +1,183 @@ -use crate::{*, lang::*}; -use ratatui::style::Color; - -/// Some layout operations exist in XY, X, and Y variants that take 3 or 2 arguments. -/// Their handling in [eval_view] is uniform and goes like this: -macro_rules! eval_xy (( - $name:expr, $expr:expr, $head:expr, $output:ident, $state:ident, - $variant:expr, $arg0: ident, $arg1: ident, $arg2: ident, - $xy:ident, $x:ident, $y:ident -) => {{ - let variant = $variant; - let cb = thunk(move|output: &mut O|$state.interpret(output, &match variant { - Some("x") | Some("y") => $arg1, - Some("xy") | None => $arg2, - _ => panic!("{}: unsupported axis {variant:?}; try /x, /y, /xy", $name) - })); - - match variant { - - // XY variant (can be omitted) - Some("xy") | None => cb.push_xy( - $state.namespace($arg0?)?, - $state.namespace($arg1?)?, - ).draw($output), - - // X variant - Some("x") => cb.push_x( - $state.namespace($arg0?)?, - ).draw($output), - - // Y variant - Some("y") => cb.push_y( - $state.namespace($arg0?)?, - ).draw($output), - - // Other namespace members are invalid - frag => { - let name = $name; - let expr = $expr; - let head = $head; - unimplemented!( - "{name}/{frag:?} ({expr:?}) ({head:?}) ({:?})", - head.src()?.unwrap_or_default().split("/").next() - ) - } - - } -}}); - -/// Some layout operations exist in multiple variants that take a single argument. -/// Their handling in [eval_view] is uniform and goes like this: -macro_rules! eval_enum (( - $name:literal, $output:ident, $state:ident, $value:expr, $arg0: ident, $Enum:ident { - $($v:literal => $V:ident),* $(,)? - } -) => {{ - match $value { - $(Some($v) => $Enum::$V,)* - frag => unimplemented!("{}/{frag:?}", $name) - } -}}); +use crate::{*, term::*, draw::*, ratatui::prelude::*}; +use dizzle::*; /// Interpret layout operation. /// /// ``` -/// # use tengri::{*, lang::*}; +/// # use tengri::*; +/// struct Target { xywh: XYWH /*platform-specific*/} +/// impl tengri::Out for Target { +/// type Unit = u16; +/// fn area (&self) -> XYWH { self.xywh } +/// fn area_mut (&mut self) -> &mut XYWH { &mut self.xywh } +/// fn show <'t, T: Draw + ?Sized> (&mut self, area: XYWH, content: &'t T) {} +/// } /// /// struct State {/*app-specific*/} -/// impl<'b> Namespace<'b, u16> for State {} /// impl<'b> Namespace<'b, bool> for State {} -/// impl<'b> Namespace<'b, Option> for State {} -/// impl Interpret>> for State {} +/// impl<'b> Namespace<'b, u16> for State {} +/// impl Interpret for State {} /// /// # fn main () -> tengri::Usually<()> { /// let state = State {}; -/// let mut target = Tui::new(80, 25); +/// let mut target = Target { xywh: Default::default() }; /// eval_view(&state, &mut target, &"")?; -/// eval_view(&state, &mut target, &"(whe true (text hello))")?; +/// eval_view(&state, &mut target, &"(when true (text hello))")?; /// eval_view(&state, &mut target, &"(either true (text hello) (text world))")?; /// // TODO test all /// # Ok(()) } /// ``` -pub fn eval_view <'a, O: Screen + 'a, S> ( +#[cfg(feature = "dsl")] pub fn eval_view <'a, O: Screen + 'a, S> ( state: &S, output: &mut O, expr: &'a impl Expression -) -> Perhaps> where - S: Interpret>> - + for<'b> Namespace<'b, bool> - + for<'b> Namespace<'b, O::Unit> - + for<'b> Namespace<'b, Option> +) -> Usually where + S: Interpret + + for<'b>Namespace<'b, bool> + + for<'b>Namespace<'b, O::Unit> { - // First element of expression is name of the operation. - // These are quasi-namespaced using the separator character, `/`. + // First element of expression is used for dispatch. + // Dispatch is proto-namespaced using separator character let head = expr.head()?; let mut frags = head.src()?.unwrap_or_default().split("/"); - // The rest of the tokens in the expr are arguments. // Their meanings depend on the dispatched operation - // Here we just reference them, so that they are in scope. - // Dereferencing them happens in the dispatch branch. let args = expr.tail(); let arg0 = args.head(); let tail0 = args.tail(); let arg1 = tail0.head(); let tail1 = tail0.tail(); let arg2 = tail1.head(); - - // First `frags.next()` calls returns the namespace. + // And we also have to do the above binding dance + // so that the Perhapss remain in scope. match frags.next() { - Some("when") => when( + Some("when") => output.place(&when( state.namespace(arg0?)?.unwrap(), - thunk(move|output: &mut O|{state.interpret(output, &arg1)}) - ).draw(output), + move|output: &mut O|state.interpret(output, &arg1) + )), - Some("either") => either( + Some("either") => output.place(&either( state.namespace(arg0?)?.unwrap(), - thunk(move|output: &mut O|{state.interpret(output, &arg1)}), - thunk(move|output: &mut O|{state.interpret(output, &arg2)}), - ).draw(output), + move|output: &mut O|state.interpret(output, &arg1), + move|output: &mut O|state.interpret(output, &arg2), + )), - // Second `frags.next()` calls returns the namespace member. - Some("bsp") => { - eval_enum!("bsp", output, state, frags.next(), arg0, Split { - "n" => North, - "s" => South, - "e" => East, - "w" => West, - "a" => Above, - "b" => Below - }).half( - thunk(move|output: &mut O|{state.interpret(output, &arg0)}), - thunk(move|output: &mut O|{state.interpret(output, &arg1)}), - ).draw(output) - }, + Some("bsp") => output.place(&{ + let a = move|output: &mut O|state.interpret(output, &arg0); + let b = move|output: &mut O|state.interpret(output, &arg1); + bsp(match frags.next() { + Some("n") => Alignment::N, + Some("s") => Alignment::S, + Some("e") => Alignment::E, + Some("w") => Alignment::W, + Some("a") => Alignment::A, + Some("b") => Alignment::B, + frag => unimplemented!("bsp/{frag:?}") + }, a, b) + }), - Some("align") => { - let content = thunk(move|output: &mut O|{state.interpret(output, &arg0)}); - content.align(eval_enum!("align", output, state, frags.next(), arg0, Azimuth { - "c" => C, - "n" => N, - "s" => S, - "e" => E, - "w" => W, - "x" => X, - "y" => Y - })).draw(output) - }, + Some("align") => output.place(&{ + let a = move|output: &mut O|state.interpret(output, &arg0).unwrap(); + align(match frags.next() { + Some("c") => Alignment::Center, + Some("n") => Alignment::N, + Some("s") => Alignment::S, + Some("e") => Alignment::E, + Some("w") => Alignment::W, + Some("x") => Alignment::X, + Some("y") => Alignment::Y, + frag => unimplemented!("align/{frag:?}") + }, a) + }), - Some("exact") => eval_xy!( - "exact", expr, head, output, state, frags.next(), - arg0, arg1, arg2, wh_exact, w_exact, h_exact - ), + Some("fill") => output.place(&{ + let a = move|output: &mut O|state.interpret(output, &arg0).unwrap(); + match frags.next() { + Some("xy") | None => fill_wh(a), + Some("x") => fill_w(a), + Some("y") => fill_h(a), + frag => unimplemented!("fill/{frag:?}") + } + }), - Some("min") => eval_xy!( - "min", expr, head, output, state, frags.next(), - arg0, arg1, arg2, wh_min, w_min, h_min - ), + Some("exact") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("exact: unsupported axis {axis:?}") }; + let cb = move|output: &mut O|state.interpret(output, &arg).unwrap(); + match axis { + Some("xy") | None => exact_wh(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => exact_w(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => exact_h(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("exact/{frag:?} ({expr:?}) ({head:?}) ({:?})", + head.src()?.unwrap_or_default().split("/").next()) + } + }), - Some("max") => eval_xy!( - "max", expr, head, output, state, frags.next(), - arg0, arg1, arg2, wh_max, w_max, h_max - ), + Some("min") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("min: unsupported axis {axis:?}") }; + let cb = move|output: &mut O|state.interpret(output, &arg).unwrap(); + match axis { + Some("xy") | None => min_wh(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => min_w(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => min_h(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("min/{frag:?}") + } + }), - Some("push") => eval_xy!( - "push", expr, head, output, state, frags.next(), - arg0, arg1, arg2, xy_push, x_push, y_push - ), + Some("max") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("max: unsupported axis {axis:?}") }; + let cb = move|output: &mut O|state.interpret(output, &arg).unwrap(); + match axis { + Some("xy") | None => max_wh(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => max_w(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => max_h(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("max/{frag:?}") + } + }), - _ => return Ok(None) + Some("push") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("push: unsupported axis {axis:?}") }; + let cb = move|output: &mut O|state.interpret(output, &arg); + match axis { + Some("xy") | None => push_xy(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => push_x(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => push_y(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("push/{frag:?}") + } + }), - } + _ => return Ok(false) + + }; + Ok(true) } /// Interpret TUI-specific layout operation. /// /// ``` -/// use tengri::{*, lang::*, ratatui::prelude::Color}; +/// use tengri::{Namespace, Interpret, Tui, ratatui::prelude::Color}; /// -/// #[namespace(bool {})] -/// #[namespace(u8: try_to_u8)] -/// #[namespace(u16: try_to_u16)] -/// #[namespace(Color: try_to_color)] -/// #[interpret(Tui -> Option>: try_eval_tui)] /// struct State; -/// tengri::lang::primitive!(u8: try_to_u8); -/// tengri::lang::primitive!(u16: try_to_u16); -/// tengri::lang::namespace!(State: bool { -/// }); -/// tengri::lang::namespace!(State: u8 { -/// literal = |x|try_to_u8(x); -/// }); -/// tengri::lang::namespace!(State: u16 { -/// literal = |x|try_to_u16(x); -/// }); -/// tengri::lang::namespace!(State: Color { -/// expression = |_state| { -/// "g" (x: u8) => { ItemTheme::G[x as usize].base.term } -/// }; -/// }); -/// tengri::lang::interpret!(|self: State, context: Tui, lang|->Option>{ -/// expression = { -/// "text" (...rest) => { todo!() } -/// } -/// }); -/// impl Interpret>> for State { -/// fn interpret_expr <'a> (&'a self, _: &mut Tui, lang: &'a impl Expression) -/// -> Usually>> -/// { -/// Ok(None) -/// } -/// } -/// +/// impl<'b> Namespace<'b, bool> for State {} +/// impl<'b> Namespace<'b, u16> for State {} +/// impl<'b> Namespace<'b, Color> for State {} +/// impl Interpret for State {} /// # fn main () -> tengri::Usually<()> { /// let state = State; -/// let mut out = Tui::new(80, 25); -/// eval_view_tui(&state, &mut out, "")?; -/// eval_view_tui(&state, &mut out, "text Hello world!")?; -/// eval_view_tui(&state, &mut out, "fg (g 0) (text Hello world!)")?; -/// eval_view_tui(&state, &mut out, "bg (g 2) (text Hello world!)")?; -/// eval_view_tui(&state, &mut out, "(bg (g 3) (fg (g 4) (text Hello world!)))")?; +/// let mut out = Tui::default(); +/// tengri::eval_view_tui(&state, &mut out, "")?; +/// tengri::eval_view_tui(&state, &mut out, "text Hello world!")?; +/// tengri::eval_view_tui(&state, &mut out, "fg (g 0) (text Hello world!)")?; +/// tengri::eval_view_tui(&state, &mut out, "bg (g 2) (text Hello world!)")?; +/// tengri::eval_view_tui(&state, &mut out, "(bg (g 3) (fg (g 4) (text Hello world!)))")?; /// # Ok(()) } /// ``` pub fn eval_view_tui <'a, S> ( state: &S, output: &mut Tui, expr: impl Expression + 'a -) -> Perhaps> where - S: Interpret>> +) -> Usually where + S: Interpret + for<'b>Namespace<'b, bool> + for<'b>Namespace<'b, u16> + for<'b>Namespace<'b, Color> @@ -235,43 +189,36 @@ pub fn eval_view_tui <'a, S> ( let arg0 = args.head(); let tail0 = args.tail(); let arg1 = tail0.head(); + let tail1 = tail0.tail(); + let arg2 = tail1.head(); match frags.next() { Some("text") => { - if let Some(src) = args?.src()? { - output.show(src) - } else { - return Ok(None) - } + if let Some(src) = args?.src()? { output.place(&src) } }, Some("fg") => { - let arg0 = arg0?.expect("fg: expected arg 0 (color)"); - if let Some(color) = Namespace::namespace(state, arg0)? { - output.show(fg(color, thunk(move|output: &mut Tui|{ - state.interpret(output, &arg1)?; - // FIXME?: don't max out the used area? - Ok(Some(output.area().into())) - }))) - } else { - return Err(format!("fg: {arg0:?}: not a color").into()) - } + let arg0 = arg0?.expect("fg: expected arg 0 (color)"); + let color = Namespace::namespace(state, arg0)?.unwrap_or_else(||panic!("fg: {arg0:?}: not a color")); + output.place(&fg(color, thunk(move|output: &mut Tui|{ + state.interpret(output, &arg1)?; + // FIXME?: don't max out the used area? + Ok(output.area().into()) + }))) }, Some("bg") => { - let arg0 = arg0?.expect("bg: expected arg 0 (color)"); - if let Some(color) = Namespace::namespace(state, arg0)? { - output.show(bg(color, thunk(move|output: &mut Tui|{ - state.interpret(output, &arg1)?; - // FIXME?: don't max out the used area? - Ok(Some(output.area().into())) - }))) - } else { - return Err(format!("bg: {arg0:?}: not a color").into()) - } + let arg0 = arg0?.expect("bg: expected arg 0 (color)"); + let color = Namespace::namespace(state, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color")); + output.place(&bg(color, thunk(move|output: &mut Tui|{ + state.interpret(output, &arg1)?; + // FIXME?: don't max out the used area? + Ok(output.area().into()) + }))) }, - _ => return Ok(None) + _ => return Ok(false) - } + }; + Ok(true) } diff --git a/src/exit.rs b/src/exit.rs index 787e3e5..0b79708 100644 --- a/src/exit.rs +++ b/src/exit.rs @@ -4,7 +4,7 @@ use crate::Usually; #[derive(Clone)] pub struct Exit(Arc); impl Exit { - pub fn run (run: impl FnOnce(Self)->Usually) -> Usually { + pub fn run (run: impl Fn(Self)->Usually) -> Usually { run(Self(Arc::new(AtomicBool::new(false)))) } } @@ -14,3 +14,4 @@ impl AsRef> for Exit { &self.0 } } + diff --git a/src/keys.rs b/src/keys.rs new file mode 100644 index 0000000..9cc54e1 --- /dev/null +++ b/src/keys.rs @@ -0,0 +1,110 @@ +use crate::task::Task; + +use ::std::sync::{Arc, RwLock, atomic::{AtomicBool, Ordering::*}}; +use ::std::time::Duration; +use ::dizzle::{Usually, Apply, impl_from}; +use ::crossterm::event::{ + read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState +}; + +/// Spawn the TUI input thread which reads keys from the terminal. +pub fn tui_input > + Send + Sync + 'static> ( + exited: &Arc, state: &Arc>, poll: Duration +) -> Result { + let exited = exited.clone(); + let state = state.clone(); + Task::new_poll(exited.clone(), poll, move |_| { + let event = read().unwrap(); + match event { + + // Hardcoded exit. + Event::Key(KeyEvent { + modifiers: KeyModifiers::CONTROL, + code: KeyCode::Char('c'), + kind: KeyEventKind::Press, + state: KeyEventState::NONE + }) => { exited.store(true, Relaxed); }, + + // Handle all other events by the state: + event => { + if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) { + panic!("{e}") + } + }, + + } + }) +} + +/// TUI input loop event. +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] +pub struct TuiEvent(pub Event); +impl_from!(TuiEvent: |e: Event| TuiEvent(e)); +impl_from!(TuiEvent: |c: char| TuiEvent(Event::Key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)))); +impl Ord for TuiEvent { + fn cmp (&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other) + .unwrap_or_else(||format!("{:?}", self).cmp(&format!("{other:?}"))) // FIXME perf + } +} + +/// TUI key spec. +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] +pub struct TuiKey(pub Option, pub KeyModifiers); +impl TuiKey { + const SPLIT: char = '/'; + pub fn from_crossterm (event: KeyEvent) -> Self { + Self(Some(event.code), event.modifiers) + } + pub fn to_crossterm (&self) -> Option { + self.0.map(|code|Event::Key(KeyEvent { + code, + modifiers: self.1, + kind: KeyEventKind::Press, + state: KeyEventState::NONE, + })) + } + pub fn named (token: &str) -> Option { + use KeyCode::*; + Some(match token { + "up" => Up, + "down" => Down, + "left" => Left, + "right" => Right, + "esc" | "escape" => Esc, + "enter" | "return" => Enter, + "delete" | "del" => Delete, + "backspace" => Backspace, + "tab" => Tab, + "space" => Char(' '), + "comma" => Char(','), + "period" => Char('.'), + "plus" => Char('+'), + "minus" | "dash" => Char('-'), + "equal" | "equals" => Char('='), + "underscore" => Char('_'), + "backtick" => Char('`'), + "lt" => Char('<'), + "gt" => Char('>'), + "cbopen" | "openbrace" => Char('{'), + "cbclose" | "closebrace" => Char('}'), + "bropen" | "openbracket" => Char('['), + "brclose" | "closebracket" => Char(']'), + "pgup" | "pageup" => PageUp, + "pgdn" | "pagedown" => PageDown, + "f1" => F(1), + "f2" => F(2), + "f3" => F(3), + "f4" => F(4), + "f5" => F(5), + "f6" => F(6), + "f7" => F(7), + "f8" => F(8), + "f9" => F(9), + "f10" => F(10), + "f11" => F(11), + "f12" => F(12), + _ => return None, + }) + } +} diff --git a/src/lang.rs b/src/lang.rs new file mode 100644 index 0000000..8266bae --- /dev/null +++ b/src/lang.rs @@ -0,0 +1,40 @@ +#[cfg(feature = "term")] +impl crate::term::TuiKey { + #[cfg(feature = "lang")] + pub fn from_dsl (dsl: impl Language) -> Usually { + if let Some(word) = dsl.word()? { + let word = word.trim(); + Ok(if word == ":char" { + Self(None, KeyModifiers::NONE) + } else if word.chars().nth(0) == Some('@') { + let mut key = None; + let mut modifiers = KeyModifiers::NONE; + let mut tokens = word[1..].split(Self::SPLIT).peekable(); + while let Some(token) = tokens.next() { + if tokens.peek().is_some() { + match token { + "ctrl" | "Ctrl" | "c" | "C" => modifiers |= KeyModifiers::CONTROL, + "alt" | "Alt" | "m" | "M" => modifiers |= KeyModifiers::ALT, + "shift" | "Shift" | "s" | "S" => { + modifiers |= KeyModifiers::SHIFT; + // + TODO normalize character case, BackTab, etc. + }, + _ => panic!("unknown modifier {token}"), + } + } else { + key = if token.len() == 1 { + Some(KeyCode::Char(token.chars().next().unwrap())) + } else { + Some(named_key(token).unwrap_or_else(||panic!("unknown character {token}"))) + } + } + } + Self(key, modifiers) + } else { + return Err(format!("TuiKey: unexpected: {word}").into()) + }) + } else { + return Err(format!("TuiKey: unspecified").into()) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index f42030b..e7d22ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,24 +1,19 @@ #![feature(anonymous_lifetime_in_impl_trait)] -//#![feature(associated_type_defaults)] -//#![feature(const_default)] -//#![feature(const_option_ops)] +#![feature(associated_type_defaults)] +#![feature(const_default)] +#![feature(const_option_ops)] #![feature(const_precise_live_drops)] #![feature(const_trait_impl)] -//#![feature(impl_trait_in_assoc_type)] +#![feature(impl_trait_in_assoc_type)] #![feature(step_trait)] -//#![feature(trait_alias)] -//#![feature(type_alias_impl_trait)] -//#![feature(type_changing_struct_update)] +#![feature(trait_alias)] +#![feature(type_alias_impl_trait)] +#![feature(type_changing_struct_update)] pub extern crate atomic_float; pub extern crate palette; pub extern crate better_panic; pub extern crate unicode_width; -#[cfg(feature = "sing")] pub extern crate jack; -#[cfg(feature = "term")] pub extern crate ratatui; -#[cfg(feature = "term")] pub extern crate crossterm; -#[cfg(feature = "lang")] pub extern crate dizzle as lang; #[cfg(test)] #[macro_use] pub extern crate proptest; -#[cfg(test)] pub(crate) use proptest_derive::Arbitrary; pub(crate) use ::{ atomic_float::AtomicF64, @@ -26,32 +21,29 @@ pub(crate) use ::{ std::ops::{Add, Sub, Mul, Div}, std::sync::{Arc, RwLock}, std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::*}, - std::error::Error, - std::marker::PhantomData }; -macro_rules! features { - ($($feature:literal: [ $($module:ident),* ]),*) => { - $( - $( - #[cfg(feature = $feature)] mod $module; - #[cfg(feature = $feature)] pub use $module::*; - )* - )* - } -} +#[cfg(feature = "lang")] pub extern crate dizzle as lang; +#[cfg(feature = "lang")] pub use ::dizzle::{self, Usually, Perhaps, impl_default}; +#[cfg(feature = "lang")] pub mod eval; -#[cfg(feature = "lang")] pub use ::dizzle::{Usually, Perhaps, impl_default}; +#[cfg(feature = "time")] pub mod time; -features! { - "lang": [ eval ], - "time": [ time ], - "play": [ exit, task ], - "sing": [ sing ], - "text": [ text ], - "term": [ term ], - "draw": [ draw ] -} +#[cfg(feature = "play")] pub mod exit; +#[cfg(feature = "play")] pub mod task; + +#[cfg(feature = "sing")] pub extern crate jack; +#[cfg(feature = "sing")] pub mod sing; + +#[cfg(feature = "draw")] pub mod draw; +#[cfg(feature = "draw")] pub mod space; +#[cfg(feature = "draw")] pub mod color; + +#[cfg(feature = "text")] pub mod text; +#[cfg(feature = "term")] pub mod term; +#[cfg(feature = "term")] pub mod keys; +#[cfg(feature = "term")] pub extern crate ratatui; +#[cfg(feature = "term")] pub extern crate crossterm; /// Define a trait an implement it for various mutation-enabled wrapper types. */ #[macro_export] macro_rules! flex_trait_mut ( @@ -94,7 +86,7 @@ features! { // FIXME: support attrs (docstrings) $($Variant $({ $($arg: $Arg),* })?),* } - impl ::tengri::lang::Act<$State> for $Command { + impl ::tengri::dizzle::Act<$State> for $Command { fn act (&self, $state: &mut $State) -> Perhaps { match self { $(Self::$Variant $({ $($arg),* })? => $body,)* diff --git a/src/origin.rs b/src/origin.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/sing.rs b/src/sing.rs index ce776a6..a7903b8 100644 --- a/src/sing.rs +++ b/src/sing.rs @@ -1,9 +1,6 @@ pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; use crate::{*, time::PerfModel}; - -mod jack; pub use self::jack::*; -mod jack_event; pub use self::jack_event::*; -mod jack_perf; pub use self::jack_perf::*; +use JackState::*; /// Trait for thing that has a JACK process callback. pub trait Audio { @@ -25,6 +22,58 @@ pub trait Audio { } } +/// Wraps [JackState], and through it [jack::Client] when connected. +/// +/// ``` +/// let jack = tengri::Jack::default(); +/// ``` +#[derive(Clone, Debug, Default)] pub struct Jack<'j> ( + pub(crate) Arc>> +); + +/// This is a connection which may be [Inactive], [Activating], or [Active]. +/// In the [Active] and [Inactive] states, [JackState::client] returns a +/// [jack::Client], which you can use to talk to the JACK API. +/// +/// ``` +/// let state = tengri::JackState::default(); +/// ``` +#[derive(Debug, Default)] pub enum JackState<'j> { + /// Unused + #[default] Inert, + /// Before activation. + Inactive(Client), + /// During activation. + Activating, + /// After activation. Must not be dropped for JACK thread to persist. + Active(DynamicAsyncClient<'j>), +} + +/// Event enum for JACK events. +/// +/// ``` +/// let event = tengri::JackEvent::XRun; +/// ``` +#[derive(Debug, Clone, PartialEq)] pub enum JackEvent { + ThreadInit, + Shutdown(ClientStatus, Arc), + Freewheel(bool), + SampleRate(Frames), + ClientRegistration(Arc, bool), + PortRegistration(PortId, bool), + PortRename(PortId, Arc, Arc), + PortsConnected(PortId, PortId, bool), + GraphReorder, + XRun, +} + +/// Generic notification handler that emits [JackEvent] +/// +/// ``` +/// let notify = tengri::JackNotify(|_|{}); +/// ``` +pub struct JackNotify(pub T); + /// Running JACK [AsyncClient] with maximum type erasure. /// /// One [Box] contains function that handles [JackEvent]s. @@ -43,10 +92,138 @@ pub type DynamicAudioHandler<'j> = pub type BoxedAudioHandler<'j> = Box Control + Send + Sync + 'j>; +/// Notification handler wrapper for [BoxedJackEventHandler]. +pub type DynamicNotifications<'j> = + JackNotify>; + +/// Boxed [JackEvent] callback. +pub type BoxedJackEventHandler<'j> = + Box; + +impl<'j> HasJack<'j> for Jack<'j> { fn jack (&self) -> &Jack<'j> { self } } + +impl<'j> HasJack<'j> for &Jack<'j> { fn jack (&self) -> &Jack<'j> { self } } + +/// Implement [Jack] constructor and methods +impl<'j> Jack<'j> { + /// Register new [Client] and wrap it for shared use. + pub fn new_run + Audio + Send + Sync + 'static> ( + name: &impl AsRef, + init: impl FnOnce(Jack<'j>)->Usually + ) -> Usually>> { + Jack::new(name)?.run(init) + } + + pub fn new (name: &impl AsRef) -> Usually { + let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0; + Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client))))) + } + + pub fn run + Audio + Send + Sync + 'static> + (self, init: impl FnOnce(Self)->Usually) -> Usually>> + { + let client_state = self.0.clone(); + let app: Arc> = Arc::new(RwLock::new(init(self)?)); + let mut state = Activating; + std::mem::swap(&mut*client_state.write().unwrap(), &mut state); + if let Inactive(client) = state { + // This is the misc notifications handler. It's a struct that wraps a [Box] + // which performs type erasure on a callback that takes [JackEvent], which is + // one of the available misc notifications. + let notify = JackNotify(Box::new({ + let app = app.clone(); + move|event|(&mut*app.write().unwrap()).handle(event) + }) as BoxedJackEventHandler); + // This is the main processing handler. It's a struct that wraps a [Box] + // which performs type erasure on a callback that takes [Client] and [ProcessScope] + // and passes them down to the `app`'s `process` callback, which in turn + // implements audio and MIDI input and output on a realtime basis. + let process = ::jack::contrib::ClosureProcessHandler::new(Box::new({ + let app = app.clone(); + move|c: &_, s: &_|if let Ok(mut app) = app.write() { + app.process(c, s) + } else { + Control::Quit + } + }) as BoxedAudioHandler); + // Launch a client with the two handlers. + *client_state.write().unwrap() = Active( + client.activate_async(notify, process)? + ); + } else { + unreachable!(); + } + Ok(app) + } + + /// Run something with the client. + pub fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { + match &*self.0.read().unwrap() { + Inert => panic!("jack client not activated"), + Inactive(client) => op(client), + Activating => panic!("jack client has not finished activation"), + Active(client) => op(client.as_client()), + } + } +} + +impl NotificationHandler for JackNotify { + fn thread_init(&self, _: &Client) { + self.0(JackEvent::ThreadInit); + } + unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { + self.0(JackEvent::Shutdown(status, reason.into())); + } + fn freewheel(&mut self, _: &Client, enabled: bool) { + self.0(JackEvent::Freewheel(enabled)); + } + fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { + self.0(JackEvent::SampleRate(frames)); + Control::Quit + } + fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { + self.0(JackEvent::ClientRegistration(name.into(), reg)); + } + fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { + self.0(JackEvent::PortRegistration(id, reg)); + } + fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + self.0(JackEvent::PortRename(id, old.into(), new.into())); + Control::Continue + } + fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { + self.0(JackEvent::PortsConnected(a, b, are)); + } + fn graph_reorder(&mut self, _: &Client) -> Control { + self.0(JackEvent::GraphReorder); + Control::Continue + } + fn xrun(&mut self, _: &Client) -> Control { + self.0(JackEvent::XRun); + Control::Continue + } +} + +impl JackPerfModel for PerfModel { + fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope) { + if let Some(t0) = t0 { + let t1 = self.clock.raw(); + self.used.store( + self.clock.delta_as_nanos(t0, t1) as f64, + Relaxed, + ); + self.window.store( + scope.cycle_times().unwrap().period_usecs as f64, + Relaxed, + ); + } + } +} + /// Things that can provide a [jack::Client] reference. /// /// ``` -/// use tengri::*; +/// use tengri::{Jack, HasJack}; /// /// let jack: &Jack = Jacked::default().jack(); /// @@ -89,6 +266,10 @@ pub trait HasJack<'j>: Send + Sync { } } +pub trait JackPerfModel { + fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope); +} + /// Implement [Audio]: provide JACK callbacks. #[macro_export] macro_rules! impl_audio { diff --git a/src/sing/jack.rs b/src/sing/jack.rs deleted file mode 100644 index 0100fed..0000000 --- a/src/sing/jack.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::{*, PerfModel}; -pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; - -use JackState::*; - -/// Wraps [JackState], and through it [jack::Client] when connected. -/// -/// ``` -/// let jack = tengri::Jack::default(); -/// ``` -#[derive(Clone, Debug, Default)] pub struct Jack<'j> ( - pub(crate) Arc>> -); - -/// This is a connection which may be [Inactive], [Activating], or [Active]. -/// In the [Active] and [Inactive] states, [JackState::client] returns a -/// [jack::Client], which you can use to talk to the JACK API. -/// -/// ``` -/// let state = tengri::JackState::default(); -/// ``` -#[derive(Debug, Default)] pub enum JackState<'j> { - /// Unused - #[default] Inert, - /// Before activation. - Inactive(Client), - /// During activation. - Activating, - /// After activation. Must not be dropped for JACK thread to persist. - Active(DynamicAsyncClient<'j>), -} - -/// Implement [Jack] constructor and methods -impl<'j> Jack<'j> { - /// Register new [Client] and wrap it for shared use. - pub fn new_run + Audio + Send + Sync + 'static> ( - name: impl AsRef, - init: impl FnOnce(Jack<'j>)->Usually - ) -> Usually>> { - Jack::new(name)?.run(init) - } - - pub fn new (name: impl AsRef) -> Usually { - let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0; - Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client))))) - } - - pub fn run + Audio + Send + Sync + 'static> - (self, init: impl FnOnce(Self)->Usually) -> Usually>> - { - let client_state = self.0.clone(); - let app: Arc> = Arc::new(RwLock::new(init(self)?)); - let mut state = Activating; - std::mem::swap(&mut*client_state.write().unwrap(), &mut state); - if let Inactive(client) = state { - // This is the misc notifications handler. It's a struct that wraps a [Box] - // which performs type erasure on a callback that takes [JackEvent], which is - // one of the available misc notifications. - let notify = JackNotify(Box::new({ - let app = app.clone(); - move|event|(&mut*app.write().unwrap()).handle(event) - }) as BoxedJackEventHandler); - // This is the main processing handler. It's a struct that wraps a [Box] - // which performs type erasure on a callback that takes [Client] and [ProcessScope] - // and passes them down to the `app`'s `process` callback, which in turn - // implements audio and MIDI input and output on a realtime basis. - let process = ::jack::contrib::ClosureProcessHandler::new(Box::new({ - let app = app.clone(); - move|c: &_, s: &_|if let Ok(mut app) = app.write() { - app.process(c, s) - } else { - Control::Quit - } - }) as BoxedAudioHandler); - // Launch a client with the two handlers. - *client_state.write().unwrap() = Active( - client.activate_async(notify, process)? - ); - } else { - unreachable!(); - } - Ok(app) - } - - /// Run something with the client. - pub fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { - match &*self.0.read().unwrap() { - Inert => panic!("jack client not activated"), - Inactive(client) => op(client), - Activating => panic!("jack client has not finished activation"), - Active(client) => op(client.as_client()), - } - } -} - -impl<'j> HasJack<'j> for Jack<'j> { - fn jack (&self) -> &Jack<'j> { - self - } -} - -impl<'j> HasJack<'j> for &Jack<'j> { - fn jack (&self) -> &Jack<'j> { - self - } -} - -impl<'j, T: HasJack<'j>> HasJack<'j> for Arc { - fn jack (&self) -> &Jack<'j> { - (&**self).jack() - } -} diff --git a/src/sing/jack_event.rs b/src/sing/jack_event.rs deleted file mode 100644 index cfeae6c..0000000 --- a/src/sing/jack_event.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::{*, time::PerfModel}; -pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; - -/// Event enum for JACK events. -/// -/// ``` -/// let event = tengri::JackEvent::XRun; // kerpop -/// ``` -#[derive(Debug, Clone, PartialEq)] pub enum JackEvent { - ThreadInit, - Shutdown(ClientStatus, Arc), - Freewheel(bool), - SampleRate(Frames), - ClientRegistration(Arc, bool), - PortRegistration(PortId, bool), - PortRename(PortId, Arc, Arc), - PortsConnected(PortId, PortId, bool), - GraphReorder, - XRun, -} - -/// Generic notification handler that emits [JackEvent] -/// -/// ``` -/// let notify = tengri::JackNotify(|_|{}); -/// ``` -pub struct JackNotify(pub T); - -/// Notification handler wrapper for [BoxedJackEventHandler]. -pub type DynamicNotifications<'j> = - JackNotify>; - -/// Boxed [JackEvent] callback. -pub type BoxedJackEventHandler<'j> = - Box; - -impl NotificationHandler for JackNotify { - fn thread_init(&self, _: &Client) { - self.0(JackEvent::ThreadInit); - } - unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { - self.0(JackEvent::Shutdown(status, reason.into())); - } - fn freewheel(&mut self, _: &Client, enabled: bool) { - self.0(JackEvent::Freewheel(enabled)); - } - fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { - self.0(JackEvent::SampleRate(frames)); - Control::Quit - } - fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { - self.0(JackEvent::ClientRegistration(name.into(), reg)); - } - fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { - self.0(JackEvent::PortRegistration(id, reg)); - } - fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - self.0(JackEvent::PortRename(id, old.into(), new.into())); - Control::Continue - } - fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { - self.0(JackEvent::PortsConnected(a, b, are)); - } - fn graph_reorder(&mut self, _: &Client) -> Control { - self.0(JackEvent::GraphReorder); - Control::Continue - } - fn xrun(&mut self, _: &Client) -> Control { - self.0(JackEvent::XRun); - Control::Continue - } -} diff --git a/src/sing/jack_perf.rs b/src/sing/jack_perf.rs deleted file mode 100644 index ee0d7d6..0000000 --- a/src/sing/jack_perf.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::{*, time::PerfModel}; -pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; - -pub trait JackPerfModel { - fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope); -} - -impl JackPerfModel for PerfModel { - fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope) { - if let Some(t0) = t0 { - let t1 = self.clock.raw(); - self.used.store( - self.clock.delta_as_nanos(t0, t1) as f64, - Relaxed, - ); - self.window.store( - scope.cycle_times().unwrap().period_usecs as f64, - Relaxed, - ); - } - } -} diff --git a/src/space.rs b/src/space.rs new file mode 100644 index 0000000..2d2033f --- /dev/null +++ b/src/space.rs @@ -0,0 +1,515 @@ +use crate::{*, draw::*}; +#[cfg(test)] use proptest_derive::Arbitrary; + +/// Point with size. +/// +/// ``` +/// let xywh = tengri::XYWH(0u16, 0, 0, 0); +/// assert_eq!(tengri::XYWH(10u16, 10, 20, 20).center(), tengri::XY(20, 20)); +/// ``` +/// +/// * [ ] TODO: origin field (determines at which corner/side is X0 Y0) +/// +#[cfg_attr(test, derive(Arbitrary))] #[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct XYWH(pub N, pub N, pub N, pub N); +impl From<&ratatui::prelude::Rect> for XYWH { + fn from (rect: &ratatui::prelude::Rect) -> Self { + Self(rect.x, rect.y, rect.width, rect.height) + } +} +impl X for XYWH { fn x (&self) -> N { self.0 } fn w (&self) -> N { self.2 } } +impl Y for XYWH { fn y (&self) -> N { self.0 } fn h (&self) -> N { self.2 } } +impl XYWH { + pub fn zero () -> Self { + Self(0.into(), 0.into(), 0.into(), 0.into()) + } + pub fn center (&self) -> (N, N) { + let Self(x, y, w, h) = *self; + (x.plus(w/2.into()), y.plus(h/2.into())) + } + pub fn centered (&self) -> (N, N) { + let Self(x, y, w, h) = *self; + (x.minus(w/2.into()), y.minus(h/2.into())) + } + pub fn centered_x (&self, n: N) -> Self { + let Self(x, y, w, h) = *self; + let x_center = (x.plus(w / 2.into())).minus(n / 2.into()); + let y_center = y.plus(h / 2.into()); + XYWH(x_center, y_center, n, 1.into()) + } + pub fn centered_y (&self, n: N) -> Self { + let Self(x, y, w, h) = *self; + let x_center = x.plus(w / 2.into()); + let y_corner = (y.plus(h / 2.into())).minus(n / 2.into()); + XYWH(x_center, y_corner, 1.into(), n) + } + pub fn centered_xy (&self, [n, m]: [N;2]) -> Self { + let Self(x, y, w, h) = *self; + let x_center = (x.plus(w / 2.into())).minus(n / 2.into()); + let y_corner = (y.plus(h / 2.into())).minus(m / 2.into()); + XYWH(x_center, y_corner, n, m) + } + pub fn split_half (&self, direction: &Split) -> (Self, Self) { + use Split::*; + let XYWH(x, y, w, h) = self.xywh(); + match direction { + South => (XYWH(x, y, w, h - h / 2.into()), XYWH(x, y + h / 2.into(), w, h / 2.into())), + East => (XYWH(x, y, w - w / 2.into(), h), XYWH(x + w / 2.into(), y, w / 2.into(), h)), + North => (XYWH(x, y + h / 2.into(), w, h - h / 2.into()), XYWH(x, y, w, h / 2.into())), + West => (XYWH(x + w / 2.into(), y, w - w / 2.into(), h), XYWH(x, y, w / 2.into(), h)), + Above | Below => (XYWH(x, y, w, h), XYWH(x, y, w, h)) + } + } +} + +/// Something that has `[0, 0]` at a particular point. +pub trait HasOrigin { fn origin (&self) -> Origin; +} +impl> HasOrigin for T { fn origin (&self) -> Origin { *self.as_ref() } } +pub struct Anchor(Origin, T); +impl AsRef for Anchor { + fn as_ref (&self) -> &Origin { + &self.0 + } +} +impl> Draw for Anchor { + fn draw (self, to: &mut T) -> Usually> { + todo!() + } +} + +pub const fn origin_c (a: impl Draw) -> impl Draw { + Anchor(Origin::C, a) +} +pub const fn origin_x (a: impl Draw) -> impl Draw { + Anchor(Origin::X, a) +} +pub const fn origin_y (a: impl Draw) -> impl Draw { + Anchor(Origin::Y, a) +} +pub const fn origin_nw (a: impl Draw) -> impl Draw { + Anchor(Origin::NW, a) +} +pub const fn origin_n (a: impl Draw) -> impl Draw { + Anchor(Origin::N, a) +} +pub const fn origin_ne (a: impl Draw) -> impl Draw { + Anchor(Origin::NE, a) +} +pub const fn origin_w (a: impl Draw) -> impl Draw { + Anchor(Origin::W, a) +} +pub const fn origin_e (a: impl Draw) -> impl Draw { + Anchor(Origin::E, a) +} +pub const fn origin_sw (a: impl Draw) -> impl Draw { + Anchor(Origin::SW, a) +} +pub const fn origin_s (a: impl Draw) -> impl Draw { + Anchor(Origin::S, a) +} +pub const fn origin_se (a: impl Draw) -> impl Draw { + Anchor(Origin::SE, a) +} + +/// Where is [0, 0] located? +/// +/// ``` +/// use tengri::draw::Origin; +/// let _ = Origin::NW.align(()) +/// ``` +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Debug, Copy, Clone, Default)] pub enum Origin { + #[default] C, X, Y, NW, N, NE, E, SE, S, SW, W +} +impl Origin { + pub fn align (&self, a: impl Draw) -> impl Draw { + align(*self, a) + } +} + +/// ``` +/// use tengri::draw::{align, Origin::*}; +/// let _ = align(NW, "test"); +/// let _ = align(SE, "test"); +/// ``` +pub fn align (origin: Origin, a: impl Draw) -> impl Draw { + thunk(move|to: &mut T| { todo!() }) +} +pub fn align_n (a: impl Draw) -> impl Draw { + align(Origin::N, a) +} + +/// A numeric type that can be used as coordinate. +/// +/// FIXME: Replace with `num` crate? +/// FIXME: Use AsRef/AsMut? +/// +/// ``` +/// use tengri::draw::Coord; +/// let a: u16 = Coord::zero(); +/// let b: u16 = a.plus(1); +/// let c: u16 = a.minus(2); +/// let d = a.atomic(); +/// ``` +pub trait Coord: Send + Sync + Copy + + Add + + Sub + + Mul + + Div + + Ord + PartialEq + Eq + + Debug + Display + Default + + From + Into + + Into + + Into + + std::iter::Step +{ + /// Zero in own type. + fn zero () -> Self { 0.into() } + /// Addition. + fn plus (self, other: Self) -> Self; + /// Saturating subtraction. + fn minus (self, other: Self) -> Self { if self >= other { self - other } else { 0.into() } } + /// Convert to [AtomicUsize]. + fn atomic (self) -> AtomicUsize { AtomicUsize::new(self.into()) } +} + +/// A cardinal direction. +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Copy, Clone, PartialEq, Debug, Default)] pub enum Split { + North, South, East, West, Above, #[default] Below +} + +pub const fn east (a: impl Draw, b: impl Draw) -> impl Draw { + Split::East.half(a, b) +} +pub const fn north (a: impl Draw, b: impl Draw) -> impl Draw { + Split::North.half(a, b) +} +pub const fn west (a: impl Draw, b: impl Draw) -> impl Draw { + Split::West.half(a, b) +} +pub const fn south (a: impl Draw, b: impl Draw) -> impl Draw { + Split::South.half(a, b) +} +pub const fn above (a: impl Draw, b: impl Draw) -> impl Draw { + Split::Above.half(a, b) +} +pub const fn below (a: impl Draw, b: impl Draw) -> impl Draw { + Split::Below.half(a, b) +} +impl Split { + /// ``` + /// use tengri::draw::Split::*; + /// let _ = Above.bsp((), ()); + /// let _ = Below.bsp((), ()); + /// let _ = North.bsp((), ()); + /// let _ = South.bsp((), ()); + /// let _ = East.bsp((), ()); + /// let _ = West.bsp((), ()); + /// ``` + pub const fn half (&self, a: impl Draw, b: impl Draw) -> impl Draw { + thunk(move|to: &mut T|{ + let (area_a, area_b) = to.xywh().split_half(self); + let (origin_a, origin_b) = self.origins(); + let a = origin_a.align(a); + let b = origin_b.align(b); + match self { + Self::Below => { + to.place_at(area_b, &b); + to.place_at(area_b, &a); + }, + _ => { + to.place_at(area_a, &a); + to.place_at(area_a, &b); + } + } + Ok(to.xywh()) // FIXME: compute and return actually used area + }) + } + /// Newly split areas begin at the center of the split + /// to maintain centeredness in the user's field of view. + /// + /// Use [align] to override that and always start + /// at the top, bottom, etc. + /// + /// ``` + /// /* + /// + /// Split east: Split south: + /// | | | | A | + /// | <-A|B-> | |---------| + /// | | | | B | + /// + /// */ + /// ``` + const fn origins (&self) -> (Origin, Origin) { + use Origin::*; + match self { + Self::South => (S, N), + Self::East => (E, W), + Self::North => (N, S), + Self::West => (W, E), + Self::Above => (C, C), + Self::Below => (C, C), + } + } +} + +/// Horizontal axis. +pub trait X { + fn x (&self) -> N; + fn w (&self) -> N { N::zero() } + fn w_min (&self) -> N { self.w() } + fn w_max (&self) -> N { self.w() } + fn iter_x (&self) -> impl Iterator where Self: HasOrigin { + self.x_west()..self.x_east() + } + fn x_west (&self) -> N where Self: HasOrigin { + use Origin::*; + let w = self.w(); + let a = self.origin(); + let d = match a { NW|W|SW => 0.into(), N|X|C|Y|S => w/2.into(), NE|E|SE => w }; + self.x().minus(d) + } + fn x_east (&self) -> N where Self: HasOrigin { + use Origin::*; + let w = self.w(); + let a = self.origin(); + let d = match a { NW|W|SW => w, N|X|C|Y|S => w/2.into(), NE|E|SE => 0.into() }; + self.x().plus(d) + } + fn x_center (&self) -> N where Self: HasOrigin { + todo!() + } +} +pub const fn x_push (x: T::Unit, a: impl Draw) -> impl Draw { + a +} +pub const fn x_pull (x: T::Unit, a: impl Draw) -> impl Draw { + a +} +pub const fn w_max (w: Option, a: impl Draw) -> impl Draw { + wh_max(w, None, a) +} +pub const fn w_min (w: Option, a: impl Draw) -> impl Draw { + wh_min(w, None, a) +} +pub const fn w_exact (w: T::Unit, c: impl Draw) -> impl Draw { + wh_exact(Some(w), None, c) +} +/// Shrink drawing area symmetrically. +/// +/// ``` +/// let padded = tengri::W(3).pad("Hello"); +/// ``` +pub const fn w_pad (x: T::Unit, draw: impl Draw) -> impl Draw { + thunk(move|to: &mut T|draw.draw(todo!())) +} + +pub trait Y { + fn y (&self) -> N; + fn h (&self) -> N { N::zero() } + fn h_min (&self) -> N { self.h() } + fn h_max (&self) -> N { self.h() } + fn iter_y (&self) -> impl Iterator where Self: HasOrigin { + self.y_north()..self.y_south() + } + fn y_north (&self) -> N where Self: HasOrigin { + let a = self.origin(); + let h = self.h(); + use Origin::*; + let d = match a { NW|N|NE => 0.into(), W|X|C|Y|E => h/2.into(), SW|S|SE => h }; + self.y().minus(d) + } + fn y_south (&self) -> N where Self: HasOrigin { + let a = self.origin(); + let h = self.h(); + use Origin::*; + let d = match a { NW|N|NE => h, W|X|C|Y|E => h/2.into(), SW|S|SE => 0.into() }; + self.y().plus(d) + } + fn y_center (&self) -> N where Self: HasOrigin { + todo!() + } +} +pub const fn y_push (x: T::Unit, a: impl Draw) -> impl Draw { + a +} +pub const fn y_pull (x: T::Unit, a: impl Draw) -> impl Draw { + a +} +pub const fn h_max (h: Option, a: impl Draw) -> impl Draw { + wh_max(None, h, a) +} +pub const fn h_min (h: Option, a: impl Draw) -> impl Draw { + wh_min(None, h, a) +} +pub const fn h_exact (h: T::Unit, c: impl Draw) -> impl Draw { + wh_exact(None, Some(h), c) +} +/// Shrink drawing area symmetrically. +/// +/// ``` +/// let padded = tengri::W::pad(3, "Hello"); +/// ``` +pub const fn h_pad (x: T::Unit, draw: impl Draw) -> impl Draw { + thunk(move|to: &mut T|draw.draw(todo!())) +} + +pub trait Space: X + Y { + fn xywh (&self) -> XYWH { XYWH(self.x(), self.y(), self.w(), self.h()) } + // FIXME: factor origin + fn lrtb (&self) -> [N;4] { [self.x(), self.y(), self.x()+self.w(), self.y()+self.h()] } +} +impl + Y> Space for T {} +pub const fn xy_push (x: T::Unit, y: T::Unit, a: impl Draw) -> impl Draw { + a +} +pub const fn xy_pull (x: T::Unit, y: T::Unit, a: impl Draw) -> impl Draw { + a +} +/// Shrink drawing area symmetrically. +/// +/// ``` +/// let padded = tengri::WH(3, 5).pad("Hello"); +/// ``` +pub const fn wh_pad (w: T::Unit, h: T::Unit, draw: impl Draw) + -> impl Draw +{ + thunk(move|to: &mut T|draw.draw(todo!())) +} +/// Only draw content if area is above a certain size. +/// +/// ``` +/// let min = tengri::wh_min(3, 5, "Hello"); // 5x5 +/// ``` +pub const fn wh_min (w: Option, h: Option, draw: impl Draw) + -> impl Draw +{ + thunk(move|to: &mut T|draw.draw(todo!())) +} + +/// Set the maximum width and/or height of the content. +/// +/// ``` +/// let max = tengri::wh_max(Some(3), Some(5), "Hello"); +/// ``` +pub const fn wh_max (w: Option, h: Option, draw: impl Draw) + -> impl Draw +{ + thunk(move|to: &mut T|draw.draw(todo!())) +} + +/// Set the maximum width and/or height of the content. +/// +/// ``` +/// let exact = tengri::wh_exact(Some(3), Some(5), "Hello"); +/// ``` +pub const fn wh_exact (w: Option, h: Option, draw: impl Draw) + -> impl Draw +{ + thunk(move|to: &mut T|draw.draw(todo!())) +} + +/// Limit size of drawing area +/// ``` +/// let clipped = tengri::wh_clip(Some(3), Some(5), "Hello"); +/// ``` +pub const fn wh_clip ( + w: Option, h: Option, draw: impl Draw +) -> impl Draw { + thunk(move|to: &mut T|draw.draw(todo!())) +} + +pub const fn wh_full (a: impl Draw) -> impl Draw { a } +pub const fn w_full (a: impl Draw) -> impl Draw { a } +pub const fn h_full (a: impl Draw) -> impl Draw { a } + +#[macro_export] macro_rules! north { + ($head:expr $(,)?) => { $head }; + ($head:expr, $($tail:expr),* $(,)?) => { north($head, north!($($tail,)*)) }; +} +#[macro_export] macro_rules! south { + ($head:expr $(,)?) => { $head }; + ($head:expr, $($tail:expr),* $(,)?) => { south($head, south!($($tail,)*)) }; +} +#[macro_export] macro_rules! east { + ($head:expr $(,)?) => { $head }; + ($head:expr, $($tail:expr),* $(,)?) => { east($head, east!($($tail,)*)) }; +} +#[macro_export] macro_rules! west { + ($head:expr $(, $tail:expr)* $(,)?) => { west($head, west!($($tail,)*)) }; +} +#[macro_export] macro_rules! above { + ($head:expr $(, $tail:expr)* $(,)?) => { above($head, above!($($tail,)*)) }; +} +#[macro_export] macro_rules! below { + ($head:expr $(,)?) => { $head }; + ($head:expr, $($tail:expr),* $(,)?) => { below($head, below!($($tail,)*)) }; +} + +/// Iterate over a collection of renderables: +/// +/// ``` +/// use tengri::draw::{Origin::*, Split::*}; +/// let _ = Below.iter([ +/// NW.align(W(15).max(W(10).min("Leftbar"))), +/// NE.align(W(12).max(W(10).min("Rightbar"))), +/// Center.align(W(40).max(H(20.max("Center")))) +/// ].iter(), |x|x); +/// ``` +pub fn iter < + S: Screen, + D: Draw, + V: Fn()->I, + I: Iterator, + F: Fn(&D)->dyn Draw, +> (_items: V, _cb: F) -> impl Draw { + thunk(move|_to: &mut S|{ todo!() }) +} + +pub fn iter_north <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( + _iter: impl Fn()->I, _draw: impl Fn(D)->U, +) -> impl Draw { + thunk(move|_to: &mut S|{ todo!() }) +} + +pub fn iter_east <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( + _iter: impl Fn()->I, _draw: impl Fn(D)->U, +) -> impl Draw { + thunk(move|_to: &mut S|{ todo!() }) +} + +pub fn iter_south <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( + _iter: impl Fn()->I, _draw: impl Fn(D)->U, +) -> impl Draw { + thunk(move|_to: &mut S|{ todo!() }) +} + +pub fn iter_west <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( + _iter: impl Fn()->I, _draw: impl Fn(D)->U, +) -> impl Draw { + thunk(move|_to: &mut S|{ todo!() }) +} + +#[derive(Default, Debug)] +pub struct Size(AtomicUsize, AtomicUsize); +impl X for Size { + fn x (&self) -> u16 { 0 } + fn w (&self) -> u16 { self.0.load(Relaxed) as u16 } +} +impl Y for Size { + fn y (&self) -> u16 { 0 } + fn h (&self) -> u16 { self.1.load(Relaxed) as u16 } +} +impl Size { + pub const fn of (&self, of: impl Draw) -> impl Draw { + thunk(move|to: &mut T|{ + let area = of.draw(to)?; + self.0.store(area.w().into(), Relaxed); + self.1.store(area.h().into(), Relaxed); + Ok(area) + }) + } +} diff --git a/src/term.rs b/src/term.rs index a289e66..2251432 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,74 +1,102 @@ -use crate::{*, lang::*}; - -mod border; pub use self::border::*; -mod event; pub use self::event::*; -mod keys; pub use self::keys::*; -mod buffer; pub use self::buffer::*; -mod input; pub use self::input::*; -mod output; pub use self::output::*; - +use crate::{*, lang::*, draw::*, space::{*, Split::*}, color::*, text::*, task::*}; //use unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; //use rand::distributions::uniform::UniformSampler; -pub(crate) use ::{ +use ::{ std::{ io::{stdout, Write}, time::Duration, ops::{Deref, DerefMut}, }, + better_panic::{Settings, Verbosity}, ratatui::{ - prelude::{Style, Position, Backend, Color}, + prelude::{Style, Buffer as ScreenBuffer, Position, Backend, Color}, style::{Modifier, Color::*}, backend::{CrosstermBackend, ClearType}, layout::{Size, Rect}, buffer::{Buffer, Cell}, - crossterm::{ - ExecutableCommand, - terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, - //event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, - } - }, - crossterm::event::{ - read, Event, KeyEvent, KeyModifiers, KeyCode, KeyEventKind, KeyEventState }, + crossterm::{ + ExecutableCommand, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, + //event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, + } }; -/// Terminal output. -pub struct Tui( - /// Ratatui buffer; area is screen size - pub Buffer, - /// Current draw area - pub XYWH -); - -impl Deref for Tui { type Target = Buffer; fn deref (&self) -> &Buffer { &self.0 } } -impl DerefMut for Tui { fn deref_mut (&mut self) -> &mut Buffer { &mut self.0 } } -impl AsMut for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } } -impl Wide for Tui { fn w (&self) -> u16 { self.1.2 } } -impl Tall for Tui { fn h (&self) -> u16 { self.1.3 } } -impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } } -impl Xy for Tui { fn x (&self) -> u16 { self.1.0 } fn y (&self) -> u16 { self.1.1 } } - -impl Tui { - pub fn new (width: u16, height: u16) -> Self { - Self(Buffer::empty(Rect { x: 0, y: 0, width, height }), XYWH(0, 0, width, height)) - } - pub fn resize (&mut self, back: &mut CrosstermBackend, width: u16, height: u16) { - let size = Rect { x: 0, y: 0, width, height }; - if self.0.area != size { - back.clear_region(ClearType::All).unwrap(); - self.0.resize(size); - self.0.reset(); +#[macro_export] macro_rules! tui_main { + ($state:expr) => { + pub fn main () -> Usually<()> { + tengri::exit::Exit::run(|exit|{ + let state = Arc::new(RwLock::new($state)); + let input = ::tengri::keys::tui_input( + exit.as_ref(), + &state, + ::std::time::Duration::from_millis(100) + )?; + let output = ::tengri::term::tui_output( + stdout(), + exit.as_ref(), + &state, + ::std::time::Duration::from_millis(10) + )?; + Ok(()) + }) } } - pub fn redraw <'b, W: Write> (&'b mut self, back: &mut CrosstermBackend, mut next: &'b mut Self) { - let updates = self.0.diff(&next.0); - back.draw(updates.into_iter()).expect("failed to render"); - Backend::flush(back).expect("failed to flush output new"); - std::mem::swap(self, &mut next); - next.0.reset(); - } } +#[macro_export] macro_rules! tui_keys { + (|$self:ident:$State:ty,$input:ident|$body:block) => { + impl Apply> for $State { + fn apply (&mut $self, $input: &TuiEvent) -> Usually $body + } + }; +} + +#[macro_export] macro_rules! tui_view { + (|$self:ident: $State:ty|$body:block) => { + impl View for $State { + fn view (&$self) -> impl Draw $body + } + } +} + +/// Spawn the TUI output thread which writes colored characters to the terminal. +pub fn tui_output + Send + Sync + 'static> ( + output: W, + exited: &Arc, + state: &Arc>, + sleep: Duration +) -> Usually { + let state = state.clone(); + tui_setup()?; + let mut backend = CrosstermBackend::new(output); + let Size { width, height } = backend.size().expect("get size failed"); + let mut buffer_a = Buffer::empty(Rect { x: 0, y: 0, width, height }); + let mut buffer_b = Buffer::empty(Rect { x: 0, y: 0, width, height }); + Ok(Task::new_sleep(exited.clone(), sleep, move |perf| { + let Size { width, height } = backend.size().expect("get size failed"); + if let Ok(state) = state.try_read() { + tui_resize(&mut backend, &mut buffer_a, Rect { x: 0, y: 0, width, height }); + tui_redraw(&mut backend, &mut buffer_a, &mut buffer_b); + } + let timer = format!("{:>3.3}ms", perf.used.load(Relaxed)); + buffer_a.set_string(0, 0, &timer, Style::default()); + })?) +} + +pub struct Tui(pub Buffer, pub XYWH); +impl Screen for Tui { type Unit = u16; } +impl Deref for Tui { type Target = Buffer; fn deref (&self) -> &Buffer { &self.0 } } +impl DerefMut for Tui { fn deref_mut (&mut self) -> &mut Buffer { &mut self.0 } } +impl HasOrigin for Tui { fn origin (&self) -> Origin { Origin::NW } } +impl X for Tui { + fn x (&self) -> u16 { self.1.0 } + fn w (&self) -> u16 { self.1.2 } +} +impl Y for Tui { + fn y (&self) -> u16 { self.1.1 } + fn h (&self) -> u16 { self.1.3 } +} impl Tui { pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH { for row in 0..self.h() { @@ -99,57 +127,588 @@ impl Tui { } } } +/// Apply foreground color. +pub const fn fg (fg: Color, draw: impl Draw) -> impl Draw { + thunk(move|to: &mut Tui|{ + to.update(&|cell,_,_|{ cell.set_fg(fg); }); + draw.draw(to) + }) +} +/// Apply background color. +pub const fn bg (bg: Color, draw: impl Draw) -> impl Draw { + thunk(move|to: &mut Tui|{ + to.update(&|cell,_,_|{ cell.set_bg(bg); }); + draw.draw(to) + }) +} +pub const fn fg_bg (fg: Color, bg: Color, draw: impl Draw) -> impl Draw { + thunk(move|to: &mut Tui|{ + to.update(&|cell,_,_|{ cell.set_fg(fg); cell.set_bg(bg); }); + draw.draw(to) + }) +} +pub const fn fill_char (c: char) -> impl Draw { + thunk(move|to: &mut Tui|Ok(to.update(&|cell,_,_|{ + cell.set_char(c); + }))) +} +/// Draw contents with modifier applied. +pub const fn modify (on: bool, modifier: Modifier, draw: impl Draw) -> impl Draw { + thunk(move|to: &mut Tui|{ + fill_mod(on, modifier).draw(to)?; + draw.draw(to) + }) +} +pub const fn fill_mod (on: bool, modifier: Modifier) -> impl Draw { + thunk(move|to: &mut Tui|Ok({ + if on { + to.update(&|cell,_,_|cell.modifier.insert(modifier)) + } else { + to.update(&|cell,_,_|cell.modifier.remove(modifier)) + } + })) +} +/// Draw contents with bold modifier applied. +pub const fn bold (on: bool, draw: impl Draw) -> impl Draw { + modify(on, Modifier::BOLD, draw) +} +pub const fn fill_ul (color: Option) -> impl Draw { + thunk(move|to: &mut Tui|Ok(if let Some(color) = color { + to.update(&|cell,_,_|{ + cell.modifier.insert(Modifier::UNDERLINED); + cell.underline_color = color; + }) + } else { + to.update(&|cell,_,_|{ + cell.modifier.remove(Modifier::UNDERLINED); + cell.underline_color = Reset; + }) + })) +} -/// Implement standard [main] entrypoint for TUI apps. -#[macro_export] macro_rules! tui_main { - ($state:expr) => { - pub fn main () -> Usually<()> { tui_run_main(Arc::new(RwLock::new($state))) } +/// TUI works in u16 coordinates. +impl Coord for u16 { + fn plus (self, other: Self) -> Self { + self.saturating_add(other) } } -pub fn tui_run_main (state: Arc>) -> Usually<()> where - T: View + Apply> + Send + Sync + 'static -{ - Exit::run(|exit|{ - let scan = Duration::from_millis(100); - let frame = Duration::from_millis(10); - let (input, output) = tui_io(exit.as_ref(), &state, scan, frame, std::io::stdout())?; - output.join(); - tui_teardown(&mut stdout()) +impl Draw for u64 { + fn draw (self, _to: &mut Tui) -> Usually> { todo!() } +} +impl Draw for f64 { + fn draw (self, _to: &mut Tui) -> Usually> { todo!() } +} + +mod phat { + use super::*; + pub const LO: &'static str = "▄"; + pub const HI: &'static str = "▀"; + /// A phat line + pub fn lo (fg: Color, bg: Color) -> impl Draw { + h_exact(1, fg_bg(fg, bg, x_repeat(self::phat::LO))) + } + /// A phat line + pub fn hi (fg: Color, bg: Color) -> impl Draw { + h_exact(1, fg_bg(fg, bg, x_repeat(self::phat::HI))) + } +} + +mod scroll { + pub const ICON_DEC_V: &[char] = &['▲']; + pub const ICON_INC_V: &[char] = &['▼']; + pub const ICON_DEC_H: &[char] = &[' ', '🞀', ' ']; + pub const ICON_INC_H: &[char] = &[' ', '🞂', ' ']; +} + +pub const fn x_repeat (c: &str) -> impl Draw { + thunk(move|to: &mut Tui|{ + let XYWH(x, y, w, h) = to.xywh(); + for x in x..x+w { + if let Some(cell) = to.0.cell_mut(Position::from((x, y))) { + cell.set_symbol(&c); + } + } + Ok(XYWH(x, y, w, 1)) }) } -pub fn tui_teardown (backend: &mut W) -> Usually<()> { - use ::ratatui::backend::Backend; +pub const fn y_repeat (c: &str) -> impl Draw { + thunk(move|to: &mut Tui|{ + let XYWH(x, y, w, h) = to.xywh(); + for y in y..y+h { + if let Some(cell) = to.0.cell_mut(Position::from((x, y))) { + cell.set_symbol(&c); + } + } + Ok(XYWH(x, y, 1, h)) + }) +} + +pub const fn xy_repeat (c: &str) -> impl Draw { + thunk(move|to: &mut Tui|{ + let XYWH(x, y, w, h) = to.xywh(); + let a = c.len(); + for (_v, y) in (y..y+h).enumerate() { + for (u, x) in (x..x+w).enumerate() { + if let Some(cell) = to.0.cell_mut(Position::from((x, y))) { + let u = u % a; + cell.set_symbol(&c[u..u+1]); + } + } + } + Ok(XYWH(x, y, w, h)) + }) +} + +/// ``` +/// let _ = tengri::button_2("", "", true); +/// let _ = tengri::button_2("", "", false); +/// ``` +pub const fn button_2 <'a> (key: impl Draw, label: impl Draw, hide: bool) -> impl Draw { + let c1 = tui_orange(); + let c2 = tui_g(0); + let c3 = tui_g(96); + let c4 = tui_g(255); + bold(true, fg_bg(c1, c2, east(fg(c2, east(key, fg(c3, "▐"))), when(!hide, fg_bg(c4, c3, label))))) +} + +/// ``` +/// let _ = tengri::button_3("", "", "", true); +/// let _ = tengri::button_3("", "", "", false); +/// ``` +pub const fn button_3 <'a> ( + key: impl Draw, label: impl Draw, value: impl Draw, editing: bool, +) -> impl Draw { + bold(true, east( + fg_bg(tui_orange(), tui_g(0), + east(fg(tui_g(0), "▐"), east(key, fg(if editing { tui_g(128) } else { tui_g(96) }, "▐")))), + east( + when(!editing, east(fg_bg(tui_g(255), tui_g(96), label), fg_bg(tui_g(128), tui_g(96), "▐"),)), + east(fg_bg(tui_g(224), tui_g(128), value), fg_bg(tui_g(128), Reset, "▌"), )))) +} + +macro_rules! border { + ($($T:ident { + $nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal + $($x:tt)* + }),+) => {$( + impl BorderStyle for $T { + const NW: &'static str = $nw; + const N: &'static str = $n; + const NE: &'static str = $ne; + const W: &'static str = $w; + const E: &'static str = $e; + const SW: &'static str = $sw; + const S: &'static str = $s; + const SE: &'static str = $se; + $($x)* + fn enabled (&self) -> bool { self.0 } + } + #[derive(Copy, Clone)] pub struct $T(pub bool, pub Style); + //impl Layout for $T {} + impl Draw for $T { + fn draw (self, to: &mut Tui) -> Usually> { + when(self.enabled(), thunk(|to: &mut Tui|BorderStyle::draw(self, to))).draw(to) + } + } + )+} +} + +border! { + Square { + "┌" "─" "┐" + "│" "│" + "└" "─" "┘" fn style (&self) -> Option