diff --git a/.gitmodules b/.gitmodules index c4e9da5..34ea4da 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = ssh://git@codeberg.org/unspeaker/dizzle.git [submodule "rust-jack"] path = rust-jack - url = https://codeberg.org/unspeaker/rust-jack + url = ssh://git@codeberg.org/unspeaker/rust-jack diff --git a/PAPER.qmd b/PAPER.qmd new file mode 100644 index 0000000..e69de29 diff --git a/dizzle b/dizzle index e984fbb..af2a107 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit e984fbb9fe8e588399744d50841d006454c16657 +Subproject commit af2a10751f87caef8e5526d1f692c941b8de8357 diff --git a/examples/tui_00.rs b/examples/tui_00.rs index 730efa4..8aedd9f 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| { - todo!() + Ok(()) }); 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), y_push(1, align_n(heading))); - let code = bg(Color::Rgb(10, 60, 10), y_push(2, align_n(format!("{}", src)))); + 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 content = ;//();//bg(Color::Rgb(10, 10, 60), View(self, CstIter::new(src))); - let widget = thunk(move|to: &mut Tui|self.interpret(to, &src)); + let widget = thunk(move|to: &mut Tui|self.interpret(to, &src).map(Some)); self.size.of(south(title, north(code, widget))) }); @@ -32,7 +32,7 @@ struct State { /** User-controllable value. */ cursor: usize, /** Rendered window size. */ - size: crate::space::Size, + size: Sizer, } impl Interpret> for State { diff --git a/shell.nix b/shell.nix index dba3faf..6f44581 100644 --- a/shell.nix +++ b/shell.nix @@ -1,13 +1,28 @@ #!/usr/bin/env nix-shell {pkgs?import{}}:let - 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 []; + name = "tengri"; + stdenv = pkgs.clang19Stdenv; + LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath []; in pkgs.mkShell.override { inherit stdenv; } { - inherit name nativeBuildInputs buildInputs LIBCLANG_PATH LD_LIBRARY_PATH; + 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" + ''; + })) + ]; } diff --git a/src/draw.rs b/src/draw.rs index ad47922..64738bd 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -1,4 +1,53 @@ -use crate::{*, lang::*, color::*, space::*}; +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 + } } +); /// Drawable that supports dynamic dispatch. /// @@ -11,36 +60,47 @@ use crate::{*, lang::*, color::*, space::*}; /// 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::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; +/// use tengri::*; +/// struct MyWidget(bool); +/// impl Draw for MyWidget { +/// fn draw (self, to: &mut Tui) -> Perhaps> { +/// todo!("your draw logic") /// } /// } -/// 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 T) -> Usually>; -} -impl> Draw for &D { - fn draw (self, to: &mut T) -> Usually> { - todo!() +pub trait Draw { + fn draw (self, to: &mut S) -> Drawn; + fn layout (&self, area: XYWH) -> Drawn { + Ok(Some(area)) } } -impl> Draw for Option { - 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!(,>|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? @@ -48,74 +108,29 @@ pub trait View { 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 View for () { + fn view (&self) -> impl Draw { + () } } -/// 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()) }) -} - -/// 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!() +impl> Draw for &V { + fn draw (self, to: &mut T) -> Perhaps> { + self.view().draw(to) } } + +features! { + "draw": [ + color, + coord, + iter, + layout, + lrtb, + sizer, + space, + split, + thunk, + xywh + ] +} diff --git a/src/color.rs b/src/draw/color.rs similarity index 99% rename from src/color.rs rename to src/draw/color.rs index f16358a..ccea5e6 100644 --- a/src/color.rs +++ b/src/draw/color.rs @@ -1,10 +1,10 @@ -use ::ratatui::style::Color; use crate::lang::impl_from; +use ::ratatui::style::Color; +use ::rand::distributions::uniform::UniformSampler; 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,6 +29,7 @@ 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),*>)? { @@ -42,8 +43,11 @@ 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 { @@ -80,8 +84,11 @@ 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/coord.rs b/src/draw/coord.rs new file mode 100644 index 0000000..40d784d --- /dev/null +++ b/src/draw/coord.rs @@ -0,0 +1,35 @@ +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 new file mode 100644 index 0000000..1bbdcfa --- /dev/null +++ b/src/draw/iter.rs @@ -0,0 +1,86 @@ +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 new file mode 100644 index 0000000..71dca39 --- /dev/null +++ b/src/draw/layout.rs @@ -0,0 +1,339 @@ +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 new file mode 100644 index 0000000..7f8b182 --- /dev/null +++ b/src/draw/lrtb.rs @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000..7ff5ed5 --- /dev/null +++ b/src/draw/sizer.rs @@ -0,0 +1,31 @@ +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 new file mode 100644 index 0000000..6576b44 --- /dev/null +++ b/src/draw/space.rs @@ -0,0 +1,2 @@ +use crate::*; + diff --git a/src/draw/split.rs b/src/draw/split.rs new file mode 100644 index 0000000..37ef914 --- /dev/null +++ b/src/draw/split.rs @@ -0,0 +1,143 @@ +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 new file mode 100644 index 0000000..af27f5b --- /dev/null +++ b/src/draw/thunk.rs @@ -0,0 +1,48 @@ +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 new file mode 100644 index 0000000..5b308d1 --- /dev/null +++ b/src/draw/xywh.rs @@ -0,0 +1,115 @@ +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 9cb0048..423cb64 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,183 +1,229 @@ -use crate::{*, term::*, draw::*, ratatui::prelude::*}; -use dizzle::*; +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) + } +}}); /// Interpret layout operation. /// /// ``` -/// # 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) {} -/// } +/// # use tengri::{*, lang::*}; /// /// struct State {/*app-specific*/} -/// impl<'b> Namespace<'b, bool> for State {} /// impl<'b> Namespace<'b, u16> for State {} -/// impl Interpret for State {} +/// impl<'b> Namespace<'b, bool> for State {} +/// impl<'b> Namespace<'b, Option> for State {} +/// impl Interpret>> for State {} /// /// # fn main () -> tengri::Usually<()> { /// let state = State {}; -/// let mut target = Target { xywh: Default::default() }; +/// let mut target = Tui::new(80, 25); /// eval_view(&state, &mut target, &"")?; -/// eval_view(&state, &mut target, &"(when true (text hello))")?; +/// eval_view(&state, &mut target, &"(whe true (text hello))")?; /// eval_view(&state, &mut target, &"(either true (text hello) (text world))")?; /// // TODO test all /// # Ok(()) } /// ``` -#[cfg(feature = "dsl")] pub fn eval_view <'a, O: Screen + 'a, S> ( +pub fn eval_view <'a, O: Screen + 'a, S> ( state: &S, output: &mut O, expr: &'a impl Expression -) -> Usually where - S: Interpret - + for<'b>Namespace<'b, bool> - + for<'b>Namespace<'b, O::Unit> +) -> Perhaps> where + S: Interpret>> + + for<'b> Namespace<'b, bool> + + for<'b> Namespace<'b, O::Unit> + + for<'b> Namespace<'b, Option> { - // First element of expression is used for dispatch. - // Dispatch is proto-namespaced using separator character + // First element of expression is name of the operation. + // These are quasi-namespaced using the 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(); - // And we also have to do the above binding dance - // so that the Perhapss remain in scope. + + // First `frags.next()` calls returns the namespace. match frags.next() { - Some("when") => output.place(&when( + Some("when") => when( state.namespace(arg0?)?.unwrap(), - move|output: &mut O|state.interpret(output, &arg1) - )), + thunk(move|output: &mut O|{state.interpret(output, &arg1)}) + ).draw(output), - Some("either") => output.place(&either( + Some("either") => either( state.namespace(arg0?)?.unwrap(), - move|output: &mut O|state.interpret(output, &arg1), - move|output: &mut O|state.interpret(output, &arg2), - )), + thunk(move|output: &mut O|{state.interpret(output, &arg1)}), + thunk(move|output: &mut O|{state.interpret(output, &arg2)}), + ).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) - }), + // 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("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("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("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("exact") => eval_xy!( + "exact", expr, head, output, state, frags.next(), + arg0, arg1, arg2, wh_exact, w_exact, h_exact + ), - 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("min") => eval_xy!( + "min", expr, head, output, state, frags.next(), + arg0, arg1, arg2, wh_min, w_min, h_min + ), - 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("max") => eval_xy!( + "max", expr, head, output, state, frags.next(), + arg0, arg1, arg2, wh_max, w_max, h_max + ), - 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:?}") - } - }), + Some("push") => eval_xy!( + "push", expr, head, output, state, frags.next(), + arg0, arg1, arg2, xy_push, x_push, y_push + ), - 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(None) - _ => return Ok(false) - - }; - Ok(true) + } } /// Interpret TUI-specific layout operation. /// /// ``` -/// use tengri::{Namespace, Interpret, Tui, ratatui::prelude::Color}; +/// use tengri::{*, lang::*, 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; -/// 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 {} +/// 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) +/// } +/// } +/// /// # fn main () -> tengri::Usually<()> { /// let state = State; -/// 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!)))")?; +/// 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!)))")?; /// # Ok(()) } /// ``` pub fn eval_view_tui <'a, S> ( state: &S, output: &mut Tui, expr: impl Expression + 'a -) -> Usually where - S: Interpret +) -> Perhaps> where + S: Interpret>> + for<'b>Namespace<'b, bool> + for<'b>Namespace<'b, u16> + for<'b>Namespace<'b, Color> @@ -189,36 +235,43 @@ 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.place(&src) } + if let Some(src) = args?.src()? { + output.show(src) + } else { + return Ok(None) + } }, Some("fg") => { - 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()) - }))) + 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()) + } }, Some("bg") => { - 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()) - }))) + 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()) + } }, - _ => return Ok(false) + _ => return Ok(None) - }; - Ok(true) + } } diff --git a/src/exit.rs b/src/exit.rs index 0b79708..787e3e5 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 Fn(Self)->Usually) -> Usually { + pub fn run (run: impl FnOnce(Self)->Usually) -> Usually { run(Self(Arc::new(AtomicBool::new(false)))) } } @@ -14,4 +14,3 @@ impl AsRef> for Exit { &self.0 } } - diff --git a/src/keys.rs b/src/keys.rs deleted file mode 100644 index 9cc54e1..0000000 --- a/src/keys.rs +++ /dev/null @@ -1,110 +0,0 @@ -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 deleted file mode 100644 index 8266bae..0000000 --- a/src/lang.rs +++ /dev/null @@ -1,40 +0,0 @@ -#[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 e7d22ac..f42030b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,24 @@ #![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, @@ -21,29 +26,32 @@ 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 }; -#[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; +macro_rules! features { + ($($feature:literal: [ $($module:ident),* ]),*) => { + $( + $( + #[cfg(feature = $feature)] mod $module; + #[cfg(feature = $feature)] pub use $module::*; + )* + )* + } +} -#[cfg(feature = "time")] pub mod time; +#[cfg(feature = "lang")] pub use ::dizzle::{Usually, Perhaps, impl_default}; -#[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; +features! { + "lang": [ eval ], + "time": [ time ], + "play": [ exit, task ], + "sing": [ sing ], + "text": [ text ], + "term": [ term ], + "draw": [ draw ] +} /// Define a trait an implement it for various mutation-enabled wrapper types. */ #[macro_export] macro_rules! flex_trait_mut ( @@ -86,7 +94,7 @@ pub(crate) use ::{ // FIXME: support attrs (docstrings) $($Variant $({ $($arg: $Arg),* })?),* } - impl ::tengri::dizzle::Act<$State> for $Command { + impl ::tengri::lang::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 new file mode 100644 index 0000000..e69de29 diff --git a/src/sing.rs b/src/sing.rs index a7903b8..ce776a6 100644 --- a/src/sing.rs +++ b/src/sing.rs @@ -1,6 +1,9 @@ pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; use crate::{*, time::PerfModel}; -use JackState::*; + +mod jack; pub use self::jack::*; +mod jack_event; pub use self::jack_event::*; +mod jack_perf; pub use self::jack_perf::*; /// Trait for thing that has a JACK process callback. pub trait Audio { @@ -22,58 +25,6 @@ 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. @@ -92,138 +43,10 @@ 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::{Jack, HasJack}; +/// use tengri::*; /// /// let jack: &Jack = Jacked::default().jack(); /// @@ -266,10 +89,6 @@ 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 new file mode 100644 index 0000000..0100fed --- /dev/null +++ b/src/sing/jack.rs @@ -0,0 +1,112 @@ +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 new file mode 100644 index 0000000..cfeae6c --- /dev/null +++ b/src/sing/jack_event.rs @@ -0,0 +1,72 @@ +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 new file mode 100644 index 0000000..ee0d7d6 --- /dev/null +++ b/src/sing/jack_perf.rs @@ -0,0 +1,22 @@ +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 deleted file mode 100644 index 2d2033f..0000000 --- a/src/space.rs +++ /dev/null @@ -1,515 +0,0 @@ -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 2251432..a289e66 100644 --- a/src/term.rs +++ b/src/term.rs @@ -1,102 +1,74 @@ -use crate::{*, lang::*, draw::*, space::{*, Split::*}, color::*, text::*, task::*}; +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 unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; //use rand::distributions::uniform::UniformSampler; -use ::{ +pub(crate) use ::{ std::{ io::{stdout, Write}, time::Duration, ops::{Deref, DerefMut}, }, - better_panic::{Settings, Verbosity}, ratatui::{ - prelude::{Style, Buffer as ScreenBuffer, Position, Backend, Color}, + prelude::{Style, 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}, - } }; -#[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(()) - }) +/// 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(); } } + 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() { @@ -127,588 +99,57 @@ 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; - }) - })) -} -/// TUI works in u16 coordinates. -impl Coord for u16 { - fn plus (self, other: Self) -> Self { - self.saturating_add(other) +/// 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))) } } } -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_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()) }) } -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