diff --git a/Cargo.toml b/Cargo.toml index 96c6bf9..37bdf64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,15 +5,12 @@ version = "0.15.0" description = "UI metaframework." [features] -default = ["lang", "sing", "draw", "tui"] -lang = ["dep:dizzle"] -sing = ["dep:jack"] -draw = [] -tui = ["draw", "dep:ratatui", "dep:crossterm"] -gui = ["draw", "dep:winit"] - +default = ["tui", "jack", "dsl"] bumpalo = ["dep:bumpalo"] +tui = ["dep:ratatui", "dep:crossterm"] +gui = ["dep:winit"] dsl = ["dep:dizzle"] +jack = ["dep:jack"] [dependencies] anyhow = { version = "1.0" } diff --git a/README.md b/README.md index 0bf1e39..a155820 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,31 @@ -# tengri [![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page) +# tengri -***tengri*** is the skygod in whose dream the [**`tek`**](https://codeberg.org/unspeaker/tek) -is happening. it looks at us from its [**`perch`**](https://codeberg.org/unspeaker/perch). +***tengri*** is a metaframework for building interactive applications with rust. (aren't we all?) -## features +tengri is published under [**AGPL3**](../LICENSE). -it is here to do the following: +## tengri and tek -### sing +tengri is developed as part of [***tek***](https://codeberg.org/unspeaker/tek), +a music program for terminals. -connect to jack audio connection kit to process chunks of audio and midi. +## tengri and dizzle -### draw +tengri integrates with [***dizzle***](./dizzle), +a framework for defining domain-specific languages. -abstract interface layout system for defining interface layouts abstractly. +## what's in the box -### play +excluding the reexports, tengri is a flat namespace containing the following kinds of things: -the input handling system. +### logical -### tui +### temporal -uses `ratatui` to run in a terminal. +### aural -### gui (todo-todo-todo) +### spatial -opens windows, runs shaders in them and/or delegates them (to e.g. plugin guis). +### textual -### lang - -uses `dizzle` to let you livecode all of the above. - -## license - -here and now, the blessings of `tengri` are invokable under the [**`AGPL3`**](./LICENSE). +checkout the repo and generate docs with `cargo doc` to explore! diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..41cfef5 --- /dev/null +++ b/src/README.md @@ -0,0 +1,56 @@ +***tengri*** is a metaframework for building interactive applications with rust. (aren't we all?) + +tengri is developed as part of [***tek***](https://codeberg.org/unspeaker/tek), +a music program for terminals. + +tengri contains: +* [***dizzle***](./dsl), a framework for defining domain-specific languages +* [***output***](./output), an abstract UI layout framework +* [***input***](./input), an abstract UI event framework +* [***tui***](./tui), an implementation of tengri over [***ratatui***](https://ratatui.rs/). + +as well as: +* [***core***](./core), the shared definitions ("utils") module +* [***proc***](./proc), the space for procedural macros +* [***tengri***](./tengri), the top-level reexport crate + +tengri is published under [**AGPL3**](./LICENSE). + +## Input + +***tengri_input*** is where tengri's input handling is defined. + +the following items are provided: +* `Input` trait, for defining for input sources +* `Handle` trait and `handle!` macro, for defining input handlers +* `Command` trait and the `command!` macro, for defining commands that inputs may result in + +## Output + +***tengri_output*** is an abstract interface layout framework. + +it expresses the following notions: + +* [**space:**](./src/space.rs) `Direction`, `Coordinate`, `Area`, `Size`, `Measure` + +* [**output:**](./src/output.rs) `Out`, `Draw`, `Content` + * the layout operators are generic over `Draw` and/or `Content` + * the traits `Draw` and `Content` are generic over `Out` + * implement `Out` to bring a layout to a new backend: + [see `TuiOut` in `tengri_tui`](../tui/src/tui_engine/tui_output.rs) + +* [**layout:**](./src/layout.rs) + * conditionals: `When`, `Either` + * iteration: `Map` + * concatenation: `Bsp` + * positioning: `Align`, `Push`, `Pull` + * sizing: `Fill`, `Fixed`, `Expand`, `Shrink`, `Min`, `Max` + * implement custom components (that may be backend-dependent): + [see `tui_content` in `tengri_tui`](../tui/src/tui_content) + +## TUI + +***tengri_tui*** implements [tengri_output](../output) and [tengri_input](../input) +on top of [ratatui](https://ratatui.rs/) and [crossterm](https://github.com/crossterm-rs/crossterm). + +tengri is published under [**AGPL3**](../LICENSE). diff --git a/src/draw.rs b/src/draw.rs deleted file mode 100644 index 8af6098..0000000 --- a/src/draw.rs +++ /dev/null @@ -1,407 +0,0 @@ -use crate::*; - -/// A numeric type that can be used as coordinate. -/// -/// FIXME: Replace this ad-hoc trait with `num` crate. -pub trait Coord: Send + Sync + Copy - + Add - + Sub - + Mul - + Div - + Ord + PartialEq + Eq - + Debug + Display + Default - + From + Into - + Into - + Into -{ - fn zero () -> Self { 0.into() } - fn plus (self, other: Self) -> Self; - fn minus (self, other: Self) -> Self { if self >= other { self - other } else { 0.into() } } - fn atomic (self) -> AtomicUsize { AtomicUsize::new(self.into()) } -} - -/// Point along horizontal axis. -pub trait X { fn x (&self) -> N { N::zero() } } - -/// Point along vertical axis. -pub trait Y { fn y (&self) -> N { N::zero() } } - -/// Length along horizontal axis. -pub trait W { fn w (&self) -> N { N::zero() } } - -/// Length along vertical axis. -pub trait H { fn h (&self) -> N { N::zero() } } - -/// Which corner/side of a box is 0, 0 -pub trait Anchor { fn anchor (&self) -> Alignment; } - -/// Area along (X, Y). -pub trait Area: W + H { fn wh (&self) -> WH { WH(self.w(), self.h()) } } - -/// Point along (X, Y). -pub trait Point: X + Y { fn xy (&self) -> XY { XY(self.x(), self.y()) } } - -// Something that has a 2D bounding box (X, Y, W, H). -pub trait Bounded: Point + Area + Anchor { - fn x2 (&self) -> N { self.x().plus(self.w()) } - fn y2 (&self) -> N { self.y().plus(self.h()) } - fn xywh (&self) -> [N;4] { [..self.xy(), ..self.wh()] } - fn expect_min (&self, w: N, h: N) -> Usually<&Self> { - if self.w() < w || self.h() < h { - Err(format!("min {w}x{h}").into()) - } else { - Ok(self) - } - } -} - -/// A point (X, Y). -/// -/// ``` -/// let xy = tengri::XY(0u16, 0); -/// ``` -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XY(pub C, pub C); - -/// A size (Width, Height). -/// -/// ``` -/// let wh = tengri::WH(0u16, 0); -/// ``` -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct WH(pub C, pub C); - -/// 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: anchor field (determines at which corner/side is X0 Y0) -/// -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XYWH( - pub C, pub C, pub C, pub C -); - -/// A cardinal direction. -/// -/// ``` -/// let direction = tengri::Direction::Above; -/// ``` -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Copy, Clone, PartialEq, Debug)] pub enum Direction { - North, South, East, West, Above, Below -} - -/// 9th of area to place. -/// -/// ``` -/// let alignment = tengri::Alignment::Center; -/// ``` -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Debug, Copy, Clone, Default)] pub enum Alignment { - #[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W -} - -/// Drawable with dynamic dispatch. -pub trait Draw: Fn(&mut T)->Usually<()> { fn draw (&self, to: &mut T) -> Usually<()>; } - -/// Drawable thunk. -impl()> Draw for F { fn draw (&self, to: &mut T) -> Usually<()> { self(to) } } - -impl Draw for () { fn draw (&self, _: &mut S) -> Usually<()> { Ok(()) } } -impl Draw for Box> { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } } - -impl> Draw for &D { fn draw (&self, to: &mut S) -> Usually<()> { (*self).draw(to) } } -impl> Draw for &mut D { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } } -impl> Draw for Arc { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } } -impl> Draw for RwLock { fn draw (&self, to: &mut S) -> Usually<()> { self.read().unwrap().draw(to) } } -impl> Draw for Option { fn draw (&self, to: &mut S) { if let Some(draw) = self { draw.draw(to) } } } - -/// Draw the content or its error message. -/// -/// ``` -/// let _ = tengri::Catcher::::new(Ok(Some("hello"))); -/// let _ = tengri::Catcher::::new(Ok(None)); -/// let _ = tengri::Catcher::::new(Err("draw fail".into())); -/// ``` -pub fn catcher (error: Perhaps, draw: impl Draw) -> impl Draw { - move|to|match self.0.as_ref() { - Ok(Some(content)) => draw(to), - Ok(None) => to.blit(&"", 0, 0, Some(Style::default().yellow())), - Err(e) => { - let err_fg = Color::Rgb(255,224,244); - let err_bg = Color::Rgb(96,24,24); - let title = Bsp::e(Tui::bold(true, "oopsie daisy. "), "rendering failed."); - let error = Bsp::e("\"why?\" ", Tui::bold(true, format!("{e}"))); - to.place(&Tui::fg_bg(err_fg, err_bg, Bsp::s(title, error))) - } - } -} -/// Only render when condition is true. -/// -/// ``` -/// fn test () -> impl tengri::Draw { -/// tengri::when(true, "Yes") -/// } -/// ``` -pub fn when (condition: bool, draw: impl Draw) -> impl Draw { - move|to|Ok(if condition { draw(to)? } }) -} - -/// Render one thing if a condition is true and another false. -/// -/// ``` -/// fn test () -> impl tengri::Draw { -/// tengri::either(true, "Yes", "No") -/// } -/// ``` -pub fn either (condition: bool, a: impl Draw, b: impl Draw) -> impl Draw { - move|to|Ok(if condition { a(to)? } else { b(to)? }) -} - -/// Set maximum width and/or height of drawing area. -pub fn clip (w: Option, h: Option, draw: impl Draw) -> impl Draw { - move|to|draw(to.clip(w, h)) -} - -/// Shrink drawing area symmetrically. -pub fn pad (w: Option, h: Option, draw: impl Draw) -> impl Draw { - move|to|draw(to.pad(w, h)) -} - -/// Only draw content if area is above a certain size. -/// -/// ``` -/// let minimum = tengri::Min::XY(3, 5, "Hello"); // 5x5 -/// ``` -pub fn min (w: N, draw: impl Draw) -> impl Draw { - move|to|draw(to.min(w, h)) -} - -/// Set the maximum width and/or height of the content. -/// -/// ``` -/// let maximum = tengri::Max::XY(3, 5, "Hello"); // 3x1 -/// ``` -pub fn max (x: N, draw: impl Draw) -> impl Draw { - move|to|draw(to.max(w, h)) -} - -// pub fn displace ... - -/// Shrink by amount on each axis. -/// -/// ``` -/// /// TODO -/// ``` -pub fn border (on: bool, style: S, draw: impl Draw) -> impl Draw { - fill_wh(above(when(on, |output|{/*TODO*/}), pad(Some(1), Some(1), draw))) -} - -/// Stackably padded. -/// -/// ``` -/// /// TODO -/// ``` -pub fn phat ( - w: N, h: N, [fg, bg, hi, lo]: [Color;4], draw: impl Draw -) -> impl Draw { - let top = exact_w(1, Self::lo(bg, hi)); - let low = exact_h(1, Self::hi(bg, lo)); - let draw = TuiOut::fg_bg(fg, bg, draw); - min_wh(w, h, bsp_s(top, bsp_n(low, fill_wh(draw)))) -} - -pub fn iter (items: impl Iterator>) -> impl Draw { - todo!() -} - -/// Split screen between two items, or layer them atop each other. -/// -/// ``` -/// use tengri::Direction::*; -/// let _ = tengri::draw::bsp(Above, (), ()); -/// let _ = tengri::draw::bsp(Below, (), ()); -/// let _ = tengri::draw::bsp(North, (), ()); -/// let _ = tengri::draw::bsp(South, (), ()); -/// let _ = tengri::draw::bsp(East, (), ()); -/// let _ = tengri::draw::bsp(West, (), ()); -/// ``` -pub fn bsp (dir: Direction, a: impl Draw, b: impl Draw) -> impl Draw { - move|to|{ - //fn split_cardinal ( - //direction: Direction, area: XYWH, a: N - //) -> (XYWH, XYWH) { - //let XYWH(x, y, w, h) = area; - //match direction { - //North => (XYWH(x, y.plus(h).minus(a), w, a), XYWH(x, y, w, h.minus(a))), - //South => (XYWH(x, y, w, a), XYWH(x, y.plus(a), w, h.minus(a))), - //East => (XYWH(x, y, a, h), XYWH(x.plus(a), y, w.minus(a), h)), - //West => (XYWH(x.plus(w).minus(a), y, a, h), XYWH(x, y, w.minus(a), h)), - //Above | Below => (area, area) - //} - //} - //fn bsp_areas , B: TwoD> ( - //area: [N;4], - //direction: Direction, - //a: &A, - //b: &B, - //) -> [XYWH;3] { - //let XYWH(x, y, w, h) = area; - //let WH(aw, ah) = a.layout(area).wh(); - //let WH(bw, bh) = b.layout(match direction { - //South => XYWH(x, y + ah, w, h.minus(ah)), - //North => XYWH(x, y, w, h.minus(ah)), - //East => XYWH(x + aw, y, w.minus(aw), h), - //West => XYWH(x, y, w.minus(aw), h), - //Above => area, - //Below => area, - //}).wh(); - //match direction { - //Above | Below => { - //let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah.max(bh)]); - //let a = XYWH((x + w/2.into()).minus(aw/2.into()), (y + h/2.into()).minus(ah/2.into()), aw, ah); - //let b = XYWH((x + w/2.into()).minus(bw/2.into()), (y + h/2.into()).minus(bh/2.into()), bw, bh); - //[a.into(), b.into(), XYWH(x, y, w, h)] - //}, - //South => { - //let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]); - //let a = XYWH((x + w/2.into()).minus(aw/2.into()), y, aw, ah); - //let b = XYWH((x + w/2.into()).minus(bw/2.into()), y + ah, bw, bh); - //[a.into(), b.into(), XYWH(x, y, w, h)] - //}, - //North => { - //let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]); - //let a = XYWH((x + (w/2.into())).minus(aw/2.into()), y + bh, aw, ah); - //let b = XYWH((x + (w/2.into())).minus(bw/2.into()), y, bw, bh); - //[a.into(), b.into(), XYWH(x, y, w, h)] - //}, - //East => { - //let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]); - //let a = XYWH(x, (y + h/2.into()).minus(ah/2.into()), aw, ah); - //let b = XYWH(x + aw, (y + h/2.into()).minus(bh/2.into()), bw, bh); - //[a.into(), b.into(), XYWH(x, y, w, h)] - //}, - //West => { - //let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]); - //let a = XYWH(x + bw, (y + h/2.into()).minus(ah/2.into()), aw, ah); - //let b = XYWH(x, (y + h/2.into()).minus(bh/2.into()), bw, bh); - //[a.into(), b.into(), XYWH(x, y, w, h)] - //}, - //} - //} - //pub fn iter - } -} - -/// 3-column layout with center priority. -/// -/// ``` -/// /// TODO -/// ``` -pub fn tryptich ( - top: boolean, [(w_a, ref a), (w_b, ref b), (w_c, ref c)]: [(N, impl Draw);3] -) -> impl Draw { - let Self { top, h, left: (w_a, ref a), middle: (w_b, ref b), right: (w_c, ref c) } = *self; - let a = exact_w(w_a, a); - let b = exact_w(w_b, align_w(b)); - let c = exact_w(w_c, c); - exact_h(h, if top { - bsp_above(fill_w(align_n(b)), bsp_above(fill_w(align_nw(a)), fill_w(align_ne(c)))) - } else { - bsp_above(fill_wh(align_c(b)), bsp_above(fill_wh(align_w(a)), fill_wh(align_e(c)))) - }) -} - -impl> Draw for Measure { - fn draw (&self, to: &mut O) { - // TODO: 🡘 🡙 ←🡙→ indicator to expand window when too small - self.x.store(to.area().w().into(), Relaxed); - self.y.store(to.area().h().into(), Relaxed); - } -} - -impl> Draw for Bounded { - fn draw (&self, to: &mut O) { - let area = to.area(); - *to.area_mut() = self.0; - self.1.draw(to); - *to.area_mut() = area; - } -} - -impl> Draw for Align { - fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), &self.1).draw(to) } -} - -impl> Draw for Pad { - fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } -} - -impl, Tail: Draw> Draw for Bsp { - fn draw (&self, to: &mut O) { - let [a, b, _] = bsp_areas(to.area(), self.0, &self.1, &self.2); - //panic!("{a:?} {b:?}"); - if self.0 == Below { - to.show(a, &self.1); - to.show(b, &self.2); - } else { - to.show(b, &self.2); - to.show(a, &self.1); - } - } -} - -/// Clear a pre-allocated buffer, then write into it. -#[macro_export] macro_rules! rewrite { - ($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } } -} - -/// FIXME: This macro should be some variant of `eval`, too. -/// But taking into account the different signatures (resolving them into 1?) -#[cfg(feature = "lang")] #[macro_export] macro_rules! draw { - ($State:ident: $Output:ident: $layers:expr) => { - impl Draw<$Output> for $State { - fn draw (&self, to: &mut $Output) { - for layer in $layers { layer(self, to) } - } - } - } -} - -/// FIXME: This is generic: should be called `eval` and be part of [dizzle]. -#[cfg(feature = "lang")] #[macro_export] macro_rules! view { - ($State:ident: $Output:ident: $namespaces:expr) => { - impl Understand<$Output, ()> for $State { - fn understand_expr <'a> (&'a self, to: &mut $Output, expr: &'a impl Expression) -> Usually<()> { - for namespace in $namespaces { if namespace(self, to, expr)? { return Ok(()) } } - Err(format!("{}::<{}, ()>::understand_expr: unexpected: {expr:?}", - stringify! { $State }, - stringify! { $Output }).into()) - } - } - } -} - -/// Stack things on top of each other, -#[macro_export] macro_rules! lay (($($expr:expr),* $(,)?) => {{ - let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)* bsp -}}); - -/// Stack southward. -#[macro_export] macro_rules! col (($($expr:expr),* $(,)?) => {{ - let bsp = (); $(let bsp = Bsp::s(bsp, $expr);)* bsp -}}); - -/// Stack northward. -#[macro_export] macro_rules! col_up (($($expr:expr),* $(,)?) => {{ - let bsp = (); $(let bsp = Bsp::n(bsp, $expr);)* bsp -}}); - -/// Stack eastward. -#[macro_export] macro_rules! row (($($expr:expr),* $(,)?) => {{ - let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)* bsp -}}); diff --git a/src/play.rs b/src/play.rs deleted file mode 100644 index 686ab86..0000000 --- a/src/play.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::*; - -/// Define an enum containing commands, and implement [Command] trait for over given `State`. -#[macro_export] macro_rules! def_command ( - ($Command:ident: |$state:ident: $State:ty| { - // FIXME: support attrs (docstrings) - $($Variant:ident$({$($arg:ident:$Arg:ty),+ $(,)?})?=>$body:expr),* $(,)? - })=>{ - #[derive(Debug)] pub enum $Command { - // FIXME: support attrs (docstrings) - $($Variant $({ $($arg: $Arg),* })?),* - } - impl ::tengri::Command<$State> for $Command { - fn execute (&self, $state: &mut $State) -> Perhaps { - match self { - $(Self::$Variant $({ $($arg),* })? => $body,)* - _ => unimplemented!("Command<{}>: {self:?}", stringify!($State)), - } - } - } - }); - -/// Implement [Handle] for given `State` and `handler`. -#[macro_export] macro_rules! handle { - (|$self:ident:$State:ty,$input:ident|$handler:expr) => { - impl ::tengri::Handle for $State { - fn handle (&mut $self, $input: &E) -> Perhaps { - $handler - } - } - }; - ($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => { - impl ::tengri::Handle<$E> for $State { - fn handle (&mut $self, $input: &$E) -> - Perhaps<<$E as ::tengri::Input>::Handled> - { - $handler - } - } - } -} diff --git a/src/sing.rs b/src/sing.rs deleted file mode 100644 index 08c0bcb..0000000 --- a/src/sing.rs +++ /dev/null @@ -1,201 +0,0 @@ -use crate::*; -use ::jack::*; -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>), -} - -/// 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. -/// -/// Another [Box] containing a function that handles realtime IO. -/// -/// That's all it knows about them. -pub type DynamicAsyncClient<'j> - = AsyncClient, DynamicAudioHandler<'j>>; - -/// Notification handler wrapper for [BoxedAudioHandler]. -pub type DynamicAudioHandler<'j> = - ::jack::contrib::ClosureProcessHandler<(), BoxedAudioHandler<'j>>; - -/// Boxed realtime callback. -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, - ); - } - } -} diff --git a/src/tengri.rs b/src/tengri.rs index 5e9e25e..7c1968b 100644 --- a/src/tengri.rs +++ b/src/tengri.rs @@ -9,56 +9,502 @@ #![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; -mod tengri_macros; mod tengri_struct; pub use self::tengri_struct::*; mod tengri_trait; pub use self::tengri_trait::*; mod tengri_impl; pub use self::tengri_impl::*; -mod tengri_fns; pub use self::tengri_fns::*; - #[cfg(test)] pub(crate) use proptest_derive::Arbitrary; - -pub(crate) use ::{ - atomic_float::AtomicF64, - palette::{*, convert::*, okhsl::*}, - better_panic::{Settings, Verbosity}, - unicode_width::*, - std::{ - io::{stdout, Stdout, Write}, - sync::{Arc, Weak, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}}, - fmt::{Debug, Display}, - ops::{Add, Sub, Mul, Div}, - marker::PhantomData, - time::Duration, - thread::{spawn, JoinHandle} - } -}; - -#[cfg(feature = "lang")] extern crate dizzle; -#[cfg(feature = "lang")] pub use ::dizzle::self as lang; - -#[cfg(feature = "draw")] pub mod draw; - +pub extern crate dizzle; pub use dizzle::*; +pub extern crate atomic_float; pub(crate) use atomic_float::AtomicF64; +pub extern crate palette; pub(crate) use ::palette::{*, convert::*, okhsl::*}; +pub extern crate better_panic; pub(crate) use better_panic::{Settings, Verbosity}; +pub extern crate unicode_width; pub(crate) use unicode_width::*; +#[cfg(feature = "jack")] pub extern crate jack; +#[cfg(feature = "jack")] pub use jack::{*, contrib::{*, ClosureProcessHandler}}; #[cfg(feature = "tui")] pub extern crate ratatui; +#[cfg(feature = "tui")] pub(crate) use ::ratatui::{ + prelude::{Color, Style, Buffer, Position}, + style::{Stylize, Modifier, Color::*}, + backend::{Backend, CrosstermBackend, ClearType}, + layout::{Size, Rect}, + buffer::Cell +}; #[cfg(feature = "tui")] pub extern crate crossterm; -#[cfg(feature = "tui")] pub(crate) use ::{ - ratatui::{ - prelude::{Color, Style, Buffer, Position}, - style::{Stylize, Modifier, Color::*}, - backend::{Backend, CrosstermBackend, ClearType}, - layout::{Size, Rect}, - buffer::Cell - }, - crossterm::{ - ExecutableCommand, - terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, - event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, - } +#[cfg(feature = "tui")] pub(crate) use ::crossterm::{ + ExecutableCommand, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, + event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, +}; +pub(crate) use ::std::{ + io::{stdout, Stdout, Write}, + sync::{Arc, Weak, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}}, + fmt::{Debug, Display}, + ops::{Add, Sub, Mul, Div}, + marker::PhantomData, + time::Duration, + thread::{spawn, JoinHandle} }; -#[cfg(feature = "sing")] pub extern crate jack; -#[cfg(feature = "sing")] pub mod sing; -#[cfg(feature = "sing")] pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; +// Define macros first, so that private macros are available in private modules: + +/// Clear a pre-allocated buffer, then write into it. +#[macro_export] macro_rules! rewrite { + ($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } } +} + +/// FIXME: This macro should be some variant of `eval`, too. +/// But taking into account the different signatures (resolving them into 1?) +#[cfg(feature = "dsl")] #[macro_export] macro_rules! draw { + ($State:ident: $Output:ident: $layers:expr) => { + impl Draw<$Output> for $State { + fn draw (&self, to: &mut $Output) { + for layer in $layers { layer(self, to) } + } + } + } +} + +/// FIXME: This is generic: should be called `eval` and be part of [dizzle]. +#[cfg(feature = "dsl")] #[macro_export] macro_rules! view { + ($State:ident: $Output:ident: $namespaces:expr) => { + impl Understand<$Output, ()> for $State { + fn understand_expr <'a> (&'a self, to: &mut $Output, expr: &'a impl Expression) -> Usually<()> { + for namespace in $namespaces { if namespace(self, to, expr)? { return Ok(()) } } + Err(format!("{}::<{}, ()>::understand_expr: unexpected: {expr:?}", + stringify! { $State }, + stringify! { $Output }).into()) + } + } + } +} + +/// Stack things on top of each other, +#[macro_export] macro_rules! lay (($($expr:expr),* $(,)?) => {{ + let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)* bsp +}}); + +/// Stack southward. +#[macro_export] macro_rules! col (($($expr:expr),* $(,)?) => {{ + let bsp = (); $(let bsp = Bsp::s(bsp, $expr);)* bsp +}}); + +/// Stack northward. +#[macro_export] macro_rules! col_up (($($expr:expr),* $(,)?) => {{ + let bsp = (); $(let bsp = Bsp::n(bsp, $expr);)* bsp +}}); + +/// Stack eastward. +#[macro_export] macro_rules! row (($($expr:expr),* $(,)?) => {{ + let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)* bsp +}}); + +/// Define an enum containing commands, and implement [Command] trait for over given `State`. +#[macro_export] macro_rules! def_command ( + ($Command:ident: |$state:ident: $State:ty| { + // FIXME: support attrs (docstrings) + $($Variant:ident$({$($arg:ident:$Arg:ty),+ $(,)?})?=>$body:expr),* $(,)? + })=>{ + #[derive(Debug)] pub enum $Command { + // FIXME: support attrs (docstrings) + $($Variant $({ $($arg: $Arg),* })?),* + } + impl ::tengri::Command<$State> for $Command { + fn execute (&self, $state: &mut $State) -> Perhaps { + match self { + $(Self::$Variant $({ $($arg),* })? => $body,)* + _ => unimplemented!("Command<{}>: {self:?}", stringify!($State)), + } + } + } + }); + +/// Implement [Handle] for given `State` and `handler`. +#[macro_export] macro_rules! handle { + (|$self:ident:$State:ty,$input:ident|$handler:expr) => { + impl ::tengri::Handle for $State { + fn handle (&mut $self, $input: &E) -> Perhaps { + $handler + } + } + }; + ($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => { + impl ::tengri::Handle<$E> for $State { + fn handle (&mut $self, $input: &$E) -> + Perhaps<<$E as ::tengri::Input>::Handled> + { + $handler + } + } + } +} + +#[macro_export] macro_rules! tui_main { + ($expr:expr) => { + fn main () -> Usually<()> { + let engine = ::tengri::Tui::new(Box::new(stdout()))?; + let state = ::std::sync::Arc::new(std::sync::RwLock::new($expr)); + engine.run(true, &state)?; + Ok(()) + } + }; +} + +#[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),*>)? { + fn color (&$self) -> ItemColor { $cb } + } + } +} + +/// Define 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 place_at <'t, T: Draw + ?Sized> (&mut self, area: XYWH, content: &'t T) {} +/// } +/// +/// struct State {/*app-specific*/} +/// impl<'b> Namespace<'b, bool> for State {} +/// impl<'b> Namespace<'b, u16> for State {} +/// impl Understand for State {} +/// +/// # fn main () -> tengri::Usually<()> { +/// let state = State {}; +/// let mut target = Target { xywh: Default::default() }; +/// evaluate_output_expression(&state, &mut target, &"")?; +/// evaluate_output_expression(&state, &mut target, &"(when true (text hello))")?; +/// evaluate_output_expression(&state, &mut target, &"(either true (text hello) (text world))")?; +/// // TODO test all +/// # Ok(()) } +/// ``` +#[cfg(feature = "dsl")] pub fn evaluate_output_expression <'a, O: Out + 'a, S> ( + state: &S, output: &mut O, expr: &'a impl Expression +) -> Usually where + S: Understand + + for<'b>Namespace<'b, bool> + + for<'b>Namespace<'b, O::Unit> +{ + // First element of expression is used for dispatch. + // Dispatch is proto-namespaced using separator character + let head = expr.head()?; + let mut frags = head.src()?.unwrap_or_default().split("/"); + // The rest of the tokens in the expr are arguments. + // Their meanings depend on the dispatched operation + 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. + match frags.next() { + + Some("when") => output.place(&When::new( + state.namespace(arg0?)?.unwrap(), + Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()) + )), + + Some("either") => output.place(&Either::new( + state.namespace(arg0?)?.unwrap(), + Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()), + Thunk::new(move|output: &mut O|state.understand(output, &arg2).unwrap()) + )), + + Some("bsp") => output.place(&{ + let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); + let b = Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()); + match frags.next() { + Some("n") => Bsp::n(a, b), + Some("s") => Bsp::s(a, b), + Some("e") => Bsp::e(a, b), + Some("w") => Bsp::w(a, b), + Some("a") => Bsp::a(a, b), + Some("b") => Bsp::b(a, b), + frag => unimplemented!("bsp/{frag:?}") + } + }), + + Some("align") => output.place(&{ + let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); + match frags.next() { + Some("n") => Align::n(a), + Some("s") => Align::s(a), + Some("e") => Align::e(a), + Some("w") => Align::w(a), + Some("x") => Align::x(a), + Some("y") => Align::y(a), + Some("c") => Align::c(a), + frag => unimplemented!("align/{frag:?}") + } + }), + + Some("fill") => output.place(&{ + let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); + match frags.next() { + Some("xy") | None => Fill::XY(a), + Some("x") => Fill::X(a), + Some("y") => Fill::Y(a), + frag => unimplemented!("fill/{frag:?}") + } + }), + + Some("fixed") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); + match axis { + Some("xy") | None => Fixed::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => Fixed::X(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => Fixed::Y(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})", + head.src()?.unwrap_or_default().split("/").next()) + } + }), + + Some("min") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); + match axis { + Some("xy") | None => Min::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => Min::X(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => Min::Y(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("min/{frag:?}") + } + }), + + Some("max") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); + match axis { + Some("xy") | None => Max::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => Max::X(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => Max::Y(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("max/{frag:?}") + } + }), + + Some("push") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); + match axis { + Some("xy") | None => Push::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => Push::X(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => Push::Y(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("push/{frag:?}") + } + }), + + _ => return Ok(false) + + }; + Ok(true) +} + + +/// Trim string with [unicode_width]. +pub fn trim_string (max_width: usize, input: impl AsRef) -> String { + let input = input.as_ref(); + let mut output = Vec::with_capacity(input.len()); + let mut width: usize = 1; + let mut chars = input.chars(); + while let Some(c) = chars.next() { + if width > max_width { + break + } + output.push(c); + width += c.width().unwrap_or(0); + } + return output.into_iter().collect() +} + +pub(crate) fn width_chars_max (max: u16, text: impl AsRef) -> u16 { + let mut width: u16 = 0; + let mut chars = text.as_ref().chars(); + while let Some(c) = chars.next() { + width += c.width().unwrap_or(0) as u16; + if width > max { + break + } + } + return width +} + +#[inline] pub fn map_south( + item_offset: O::Unit, + item_height: O::Unit, + item: impl Content +) -> impl Content { + Push::Y(item_offset, Fixed::Y(item_height, Fill::X(item))) +} + +#[inline] pub fn map_south_west( + item_offset: O::Unit, + item_height: O::Unit, + item: impl Content +) -> impl Content { + Push::Y(item_offset, Align::nw(Fixed::Y(item_height, Fill::X(item)))) +} + +#[inline] pub fn map_east( + item_offset: O::Unit, + item_width: O::Unit, + item: impl Content +) -> impl Content { + Push::X(item_offset, Align::w(Fixed::X(item_width, Fill::Y(item)))) +} + +#[cfg(test)] mod test { + use proptest::{prelude::*, option::of}; + use proptest_derive::Arbitrary; + use crate::*; + use Direction::*; + + proptest! { + #[test] fn proptest_direction ( + d in prop_oneof![ + Just(North), Just(South), + Just(East), Just(West), + Just(Above), Just(Below) + ], + x in u16::MIN..u16::MAX, + y in u16::MIN..u16::MAX, + w in u16::MIN..u16::MAX, + h in u16::MIN..u16::MAX, + a in u16::MIN..u16::MAX, + ) { + let _ = d.split_fixed(XYWH(x, y, w, h), a); + } + } + + proptest! { + #[test] fn proptest_area ( + x in u16::MIN..u16::MAX, + y in u16::MIN..u16::MAX, + w in u16::MIN..u16::MAX, + h in u16::MIN..u16::MAX, + a in u16::MIN..u16::MAX, + b in u16::MIN..u16::MAX, + ) { + let _: XYWH = XYWH::zero(); + //let _: XYWH = XYWH::from_position([a, b]); + //let _: XYWH = XYWH::from_size([a, b]); + let area: XYWH = XYWH(x, y, w, h); + //let _ = area.expect_min(a, b); + let _ = area.xy(); + let _ = area.wh(); + //let _ = area.xywh(); + let _ = area.clipped_h(a); + let _ = area.clipped_w(b); + let _ = area.clipped(WH(a, b)); + //let _ = area.set_w(a); + //let _ = area.set_h(b); + let _ = area.x2(); + let _ = area.y2(); + let _ = area.lrtb(); + let _ = area.center(); + let _ = area.centered(); + let _ = area.centered_x(a); + let _ = area.centered_y(b); + let _ = area.centered_xy([a, b]); + } + } + + proptest! { + #[test] fn proptest_size ( + x in u16::MIN..u16::MAX, + y in u16::MIN..u16::MAX, + a in u16::MIN..u16::MAX, + b in u16::MIN..u16::MAX, + ) { + let size = WH(x, y); + let _ = size.w(); + let _ = size.h(); + let _ = size.wh(); + let _ = size.clip_w(a); + let _ = size.clip_h(b); + //let _ = size.expect_min(a, b); + //let _ = size.to_area_pos(); + //let _ = size.to_area_size(); + } + } + + macro_rules! test_op_transform { + ($fn:ident, $Op:ident) => { + proptest! { + #[test] fn $fn ( + op_x in of(u16::MIN..u16::MAX), + op_y in of(u16::MIN..u16::MAX), + content in "\\PC*", + x in u16::MIN..u16::MAX, + y in u16::MIN..u16::MAX, + w in u16::MIN..u16::MAX, + h in u16::MIN..u16::MAX, + ) { + if let Some(op) = match (op_x, op_y) { + (Some(x), Some(y)) => Some($Op::XY(x, y, content)), + (Some(x), None) => Some($Op::X(x, content)), + (None, Some(y)) => Some($Op::Y(y, content)), + _ => None + } { + //assert_eq!(Content::layout(&op, [x, y, w, h]), + //Draw::layout(&op, [x, y, w, h])); + } + } + } + } + } + test_op_transform!(proptest_op_fixed, Fixed); + test_op_transform!(proptest_op_min, Min); + test_op_transform!(proptest_op_max, Max); + test_op_transform!(proptest_op_push, Push); + test_op_transform!(proptest_op_pull, Pull); + test_op_transform!(proptest_op_shrink, Shrink); + test_op_transform!(proptest_op_expand, Expand); + test_op_transform!(proptest_op_padding, Pad); + + proptest! { + #[test] fn proptest_op_bsp ( + d in prop_oneof![ + Just(North), Just(South), + Just(East), Just(West), + Just(Above), Just(Below) + ], + a in "\\PC*", + b in "\\PC*", + x in u16::MIN..u16::MAX, + y in u16::MIN..u16::MAX, + w in u16::MIN..u16::MAX, + h in u16::MIN..u16::MAX, + ) { + let bsp = Bsp(d, a, b); + //assert_eq!( + //Content::layout(&bsp, [x, y, w, h]), + //Draw::layout(&bsp, [x, y, w, h]), + //); + } + } + + //#[test] fn test_tui_engine () -> Usually<()> { + ////use std::sync::{Arc, RwLock}; + //struct TestComponent(String); + //impl Draw for TestComponent { + //fn draw (&self, _to: &mut TuiOut) {} + //} + //impl Handle for TestComponent { + //fn handle (&mut self, _from: &TuiIn) -> Perhaps { Ok(None) } + //} + //let engine = Tui::new(Box::<&mut Vec>::new(vec![0u8;0].as_mut()))?; + //let state = engine.run(false, &Arc::new(RwLock::new(TestComponent("hello world".into()))))?; + //state.read().unwrap().exited.store(true, std::sync::atomic::Ordering::Relaxed); + //Ok(()) + //} +} diff --git a/src/tengri_fns.rs b/src/tengri_fns.rs deleted file mode 100644 index 7a4e6d4..0000000 --- a/src/tengri_fns.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::*; - -/// 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) {} -/// } -/// -/// struct State {/*app-specific*/} -/// impl<'b> Namespace<'b, bool> for State {} -/// impl<'b> Namespace<'b, u16> for State {} -/// impl Understand for State {} -/// -/// # fn main () -> tengri::Usually<()> { -/// let state = State {}; -/// let mut target = Target { xywh: Default::default() }; -/// eval_view(&state, &mut target, &"")?; -/// eval_view(&state, &mut target, &"(when 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> ( - state: &S, output: &mut O, expr: &'a impl Expression -) -> Usually where - S: Understand - + for<'b>Namespace<'b, bool> - + for<'b>Namespace<'b, O::Unit> -{ - // First element of expression is used for dispatch. - // Dispatch is proto-namespaced using separator character - let head = expr.head()?; - let mut frags = head.src()?.unwrap_or_default().split("/"); - // The rest of the tokens in the expr are arguments. - // Their meanings depend on the dispatched operation - 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. - match frags.next() { - - Some("when") => output.place(&When::new( - state.namespace(arg0?)?.unwrap(), - Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()) - )), - - Some("either") => output.place(&Either::new( - state.namespace(arg0?)?.unwrap(), - Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()), - Thunk::new(move|output: &mut O|state.understand(output, &arg2).unwrap()) - )), - - Some("bsp") => output.place(&{ - let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); - let b = Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()); - match frags.next() { - Some("n") => Bsp::n(a, b), - Some("s") => Bsp::s(a, b), - Some("e") => Bsp::e(a, b), - Some("w") => Bsp::w(a, b), - Some("a") => Bsp::a(a, b), - Some("b") => Bsp::b(a, b), - frag => unimplemented!("bsp/{frag:?}") - } - }), - - Some("align") => output.place(&{ - let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); - match frags.next() { - Some("n") => Align::n(a), - Some("s") => Align::s(a), - Some("e") => Align::e(a), - Some("w") => Align::w(a), - Some("x") => Align::x(a), - Some("y") => Align::y(a), - Some("c") => Align::c(a), - frag => unimplemented!("align/{frag:?}") - } - }), - - Some("fill") => output.place(&{ - let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); - match frags.next() { - Some("xy") | None => Fill::XY(a), - Some("x") => Fill::X(a), - Some("y") => Fill::Y(a), - frag => unimplemented!("fill/{frag:?}") - } - }), - - Some("fixed") => output.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; - let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); - match axis { - Some("xy") | None => Fixed::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => Fixed::X(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => Fixed::Y(state.namespace(arg0?)?.unwrap(), cb), - frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})", - head.src()?.unwrap_or_default().split("/").next()) - } - }), - - Some("min") => output.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; - let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); - match axis { - Some("xy") | None => Min::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => Min::X(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => Min::Y(state.namespace(arg0?)?.unwrap(), cb), - frag => unimplemented!("min/{frag:?}") - } - }), - - Some("max") => output.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; - let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); - match axis { - Some("xy") | None => Max::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => Max::X(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => Max::Y(state.namespace(arg0?)?.unwrap(), cb), - frag => unimplemented!("max/{frag:?}") - } - }), - - Some("push") => output.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; - let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); - match axis { - Some("xy") | None => Push::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => Push::X(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => Push::Y(state.namespace(arg0?)?.unwrap(), cb), - frag => unimplemented!("push/{frag:?}") - } - }), - - _ => return Ok(false) - - }; - Ok(true) -} - -/// Trim string with [unicode_width]. -pub fn trim_string (max_width: usize, input: impl AsRef) -> String { - let input = input.as_ref(); - let mut output = Vec::with_capacity(input.len()); - let mut width: usize = 1; - let mut chars = input.chars(); - while let Some(c) = chars.next() { - if width > max_width { - break - } - output.push(c); - width += c.width().unwrap_or(0); - } - return output.into_iter().collect() -} - -pub(crate) fn width_chars_max (max: u16, text: impl AsRef) -> u16 { - let mut width: u16 = 0; - let mut chars = text.as_ref().chars(); - while let Some(c) = chars.next() { - width += c.width().unwrap_or(0) as u16; - if width > max { - break - } - } - return width -} diff --git a/src/tengri_impl.rs b/src/tengri_impl.rs index 476c657..0a865c2 100644 --- a/src/tengri_impl.rs +++ b/src/tengri_impl.rs @@ -1,9 +1,65 @@ use crate::*; +use Alignment::*; use Direction::*; use rand::{thread_rng, distributions::uniform::UniformSampler}; +impl + Layout> Content for T {} +impl<'a, O: Out> AsRef + 'a> for dyn Content + 'a { + fn as_ref (&self) -> &(dyn Draw + 'a) { self } +} +impl<'a, O: Out> AsRef + 'a> for dyn Content + 'a { + fn as_ref (&self) -> &(dyn Layout + 'a) { self } +} +impl Draw for () { + fn draw (&self, _: &mut O) {} +} +impl Draw for fn(&mut O) { + fn draw (&self, to: &mut O) { (*self)(to) } +} +impl Draw for Box> { + fn draw (&self, to: &mut O) { (**self).draw(to) } +} +impl> Draw for &D { + fn draw (&self, to: &mut O) { (*self).draw(to) } +} +impl> Draw for &mut D { + fn draw (&self, to: &mut O) { (**self).draw(to) } +} +impl> Draw for Arc { + fn draw (&self, to: &mut O) { (**self).draw(to) } +} +impl> Draw for RwLock { + fn draw (&self, to: &mut O) { self.read().unwrap().draw(to) } +} +impl> Draw for Option { + fn draw (&self, to: &mut O) { if let Some(draw) = self { draw.draw(to) } } +} impl<'a, T: AsRef> TrimString { fn as_ref (&self) -> TrimStringRef<'_, T> { TrimStringRef(self.0, &self.1) } } +impl> ErrorBoundary { + pub fn new (content: Perhaps) -> Self { Self(content, Default::default()) } +} +impl, V: Content> HasContent for FieldH { + fn content (&self) -> impl Content { Bsp::e(&self.1, &self.2) } +} +impl, V: Content> Layout for FieldH { + fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } +} +impl, V: Content> Draw for FieldH { + fn draw (&self, to: &mut O) { self.content().draw(to) } +} +impl, V: Content> HasContent for FieldV { + fn content (&self) -> impl Content { Bsp::s(&self.1, &self.2) } +} +impl, V: Content> Layout for FieldV { + fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } +} +impl, V: Content> Draw for FieldV { + fn draw (&self, to: &mut O) { self.content().draw(to) } +} +impl> Layout for Border { + fn layout (&self, area: XYWH) -> XYWH { self.1.layout(area) } +} impl Field { pub fn new (direction: Direction) -> Field { Field:: { @@ -23,9 +79,32 @@ impl Field { Field:: { value, value_fg: fg, value_bg: bg, value_align: align, ..self } } } -impl> Action for Option { fn action (&self, _: &mut S) -> Perhaps { Ok(None) } } -impl Clone for Measure { fn clone (&self) -> Self { Self { __: Default::default(), x: self.x.clone(), y: self.y.clone(), } } } -impl Thunk { pub const fn new (draw: F) -> Self { Self(draw, PhantomData) } } +impl> Command for Option { + fn execute (&self, _: &mut S) -> Perhaps { + Ok(None) + } + fn delegate (&self, _: &mut S, _: impl Fn(Self)->U) -> Perhaps + where Self: Sized + { + Ok(None) + } +} +impl Layout for Measure {} +impl>> Measured for T { + fn measure (&self) -> &Measure { self.as_ref() } +} +impl Clone for Measure { + fn clone (&self) -> Self { + Self { __: Default::default(), x: self.x.clone(), y: self.y.clone(), } + } +} +impl Thunk { + pub const fn new (draw: F) -> Self { Self(draw, PhantomData) } +} +impl Draw for Thunk { + fn draw (&self, to: &mut O) { (self.0)(to) } +} +impl Layout for Thunk {} impl Memo { pub fn new (value: T, view: U) -> Self { Self { value, view: Arc::new(view.into()) } @@ -39,11 +118,37 @@ impl Memo { None } } -impl PartialEq for Measure { fn eq (&self, other: &Self) -> bool { self.w() == other.w() && self.h() == other.h() } } -impl W for Measure { fn w (&self) -> O::Unit { (self.x.load(Relaxed) as u16).into() } } -impl H for Measure { fn h (&self) -> O::Unit { (self.y.load(Relaxed) as u16).into() } } -impl Debug for Measure { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Measure").field("width", &self.x).field("height", &self.y).finish() } } -impl Measure { +impl Direction { + pub fn split_fixed (self, area: XYWH, a: N) -> (XYWH, XYWH) { + let XYWH(x, y, w, h) = area; + match self { + North => (XYWH(x, y.plus(h).minus(a), w, a), XYWH(x, y, w, h.minus(a))), + South => (XYWH(x, y, w, a), XYWH(x, y.plus(a), w, h.minus(a))), + East => (XYWH(x, y, a, h), XYWH(x.plus(a), y, w.minus(a), h)), + West => (XYWH(x.plus(w).minus(a), y, a, h), XYWH(x, y, w.minus(a), h)), + Above | Below => (area, area) + } + } +} +impl PartialEq for Measure { + fn eq (&self, other: &Self) -> bool { + self.x.load(Relaxed) == other.x.load(Relaxed) && + self.y.load(Relaxed) == other.y.load(Relaxed) + } +} +// TODO: 🡘 🡙 ←🡙→ indicator to expand window when too small +impl Draw for Measure { + fn draw (&self, to: &mut O) { + self.x.store(to.area().w().into(), Relaxed); + self.y.store(to.area().h().into(), Relaxed); + } +} +impl Debug for Measure { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Measure").field("width", &self.x).field("height", &self.y).finish() + } +} +impl Measure { pub fn set_w (&self, w: impl Into) -> &Self { self.x.store(w.into(), Relaxed); self } pub fn set_h (&self, h: impl Into) -> &Self { self.y.store(h.into(), Relaxed); self } pub fn set_wh (&self, w: impl Into, h: impl Into) -> &Self { self.set_w(w); self.set_h(h); self } @@ -53,7 +158,142 @@ impl Measure { Self { __: PhantomData::default(), x: Arc::new(x.atomic()), y: Arc::new(y.atomic()), } } } -impl From> for Measure { fn from (WH(x, y): WH) -> Self { Self::new(x, y) } } +impl From> for Measure { + fn from (WH(x, y): WH) -> Self { Self::new(x, y) } +} +impl HasWH for Measure { /// FIXME don't convert to u16 specifically + fn w (&self) -> O::Unit { (self.x.load(Relaxed) as u16).into() } + fn h (&self) -> O::Unit { (self.y.load(Relaxed) as u16).into() } +} +impl Layout for () { + fn layout_x (&self, a: XYWH) -> O::Unit { a.x() } + fn layout_y (&self, a: XYWH) -> O::Unit { a.y() } + fn layout_w (&self, _: XYWH) -> O::Unit { 0.into() } + fn layout_w_min (&self, _: XYWH) -> O::Unit { 0.into() } + fn layout_w_max (&self, _: XYWH) -> O::Unit { 0.into() } + fn layout_h (&self, _: XYWH) -> O::Unit { 0.into() } + fn layout_h_min (&self, _: XYWH) -> O::Unit { 0.into() } + fn layout_h_max (&self, _: XYWH) -> O::Unit { 0.into() } + fn layout (&self, a: XYWH) -> XYWH { XYWH(a.x(), a.y(), 0.into(), 0.into()) } +} +impl> Layout for &L { + fn layout_x (&self, a: XYWH) -> O::Unit { (*self).layout_x(a) } + fn layout_y (&self, a: XYWH) -> O::Unit { (*self).layout_y(a) } + fn layout_w (&self, a: XYWH) -> O::Unit { (*self).layout_w(a) } + fn layout_w_min (&self, a: XYWH) -> O::Unit { (*self).layout_w_min(a) } + fn layout_w_max (&self, a: XYWH) -> O::Unit { (*self).layout_w_max(a) } + fn layout_h (&self, a: XYWH) -> O::Unit { (*self).layout_h(a) } + fn layout_h_min (&self, a: XYWH) -> O::Unit { (*self).layout_h_min(a) } + fn layout_h_max (&self, a: XYWH) -> O::Unit { (*self).layout_h_max(a) } + fn layout (&self, a: XYWH) -> XYWH { (*self).layout(a) } +} + +impl> Layout for &mut L { + fn layout_x (&self, a: XYWH) -> O::Unit { (**self).layout_x(a) } + fn layout_y (&self, a: XYWH) -> O::Unit { (**self).layout_y(a) } + fn layout_w (&self, a: XYWH) -> O::Unit { (**self).layout_w(a) } + fn layout_w_min (&self, a: XYWH) -> O::Unit { (**self).layout_w_min(a) } + fn layout_w_max (&self, a: XYWH) -> O::Unit { (**self).layout_w_max(a) } + fn layout_h (&self, a: XYWH) -> O::Unit { (**self).layout_h(a) } + fn layout_h_min (&self, a: XYWH) -> O::Unit { (**self).layout_h_min(a) } + fn layout_h_max (&self, a: XYWH) -> O::Unit { (**self).layout_h_max(a) } + fn layout (&self, a: XYWH) -> XYWH { (**self).layout(a) } +} + +impl> Layout for Arc { + fn layout_x (&self, a: XYWH) -> O::Unit { (**self).layout_x(a) } + fn layout_y (&self, a: XYWH) -> O::Unit { (**self).layout_y(a) } + fn layout_w (&self, a: XYWH) -> O::Unit { (**self).layout_w(a) } + fn layout_w_min (&self, a: XYWH) -> O::Unit { (**self).layout_w_min(a) } + fn layout_w_max (&self, a: XYWH) -> O::Unit { (**self).layout_w_max(a) } + fn layout_h (&self, a: XYWH) -> O::Unit { (**self).layout_h(a) } + fn layout_h_min (&self, a: XYWH) -> O::Unit { (**self).layout_h_min(a) } + fn layout_h_max (&self, a: XYWH) -> O::Unit { (**self).layout_h_max(a) } + fn layout (&self, a: XYWH) -> XYWH { (**self).layout(a) } +} +impl Layout for Box> { + fn layout_x (&self, a: XYWH) -> O::Unit { (**self).layout_x(a) } + fn layout_y (&self, a: XYWH) -> O::Unit { (**self).layout_y(a) } + fn layout_w (&self, a: XYWH) -> O::Unit { (**self).layout_w(a) } + fn layout_w_min (&self, a: XYWH) -> O::Unit { (**self).layout_w_min(a) } + fn layout_w_max (&self, a: XYWH) -> O::Unit { (**self).layout_w_max(a) } + fn layout_h (&self, a: XYWH) -> O::Unit { (**self).layout_h(a) } + fn layout_h_min (&self, a: XYWH) -> O::Unit { (**self).layout_h_min(a) } + fn layout_h_max (&self, a: XYWH) -> O::Unit { (**self).layout_h_max(a) } + fn layout (&self, a: XYWH) -> XYWH { (**self).layout(a) } +} +impl> Layout for RwLock { + fn layout_x (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_x(a) } + fn layout_y (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_y(a) } + fn layout_w (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_w(a) } + fn layout_w_min (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_w_min(a) } + fn layout_w_max (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_w_max(a) } + fn layout_h (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_h(a) } + fn layout_h_min (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_h_min(a) } + fn layout_h_max (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_h_max(a) } + fn layout (&self, a: XYWH) -> XYWH { self.read().unwrap().layout(a) } +} +impl> Layout for Option { + fn layout_x (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_x(to)).unwrap_or(to.x()) } + fn layout_y (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_y(to)).unwrap_or(to.y()) } + fn layout_w_min (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_w_min(to)).unwrap_or(0.into()) } + fn layout_w_max (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_w_max(to)).unwrap_or(0.into()) } + fn layout_w (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_w(to)).unwrap_or(0.into()) } + fn layout_h_min (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_h_min(to)).unwrap_or(0.into()) } + fn layout_h_max (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_h_max(to)).unwrap_or(0.into()) } + fn layout_h (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_h(to)).unwrap_or(0.into()) } + fn layout (&self, to: XYWH) -> XYWH { + let xywh = XYWH(self.layout_x(to), self.layout_y(to), self.layout_w(to), self.layout_h(to)); + self.as_ref().map(|c|c.layout(xywh)).unwrap_or(XYWH(to.x(), to.y(), 0.into(), 0.into())) + } +} +impl> HasContent for Bounded { + fn content (&self) -> impl Content { &self.1 } +} +impl> Draw for Bounded { + fn draw (&self, to: &mut O) { + let area = to.area(); + *to.area_mut() = self.0; + self.1.draw(to); + *to.area_mut() = area; + } +} +impl> When { + /// Create a binary condition. + pub const fn new (c: bool, a: T) -> Self { Self(c, a, PhantomData) } +} +impl> Layout for When { + fn layout (&self, to: XYWH) -> XYWH { + let Self(cond, item, ..) = self; + if *cond { item.layout(to) } else { XYWH::::zero().into() } + } +} +impl> Draw for When { + fn draw (&self, to: &mut O) { + let Self(cond, item, ..) = self; + if *cond { Bounded(self.layout(to.area()), item).draw(to) } + } +} +impl, B: Content> Either { + /// Create a ternary view condition. + pub const fn new (c: bool, a: A, b: B) -> Self { + Self(c, a, b, PhantomData) + } +} +impl, B: Layout> Layout for Either { + fn layout (&self, to: XYWH) -> XYWH { + let Self(cond, a, b, ..) = self; + if *cond { a.layout(to) } else { b.layout(to) } + } +} +impl, B: Content> Draw for Either { + fn draw (&self, to: &mut E) { + let Self(cond, a, b, ..) = self; + let area = self.layout(to.area()); + if *cond { Bounded(area, a).draw(to) } else { Bounded(area, b).draw(to) } + } +} + macro_rules! layout_op_xy ( // Variant for layout ops that take no coordinates (0: $T: ident) => { @@ -62,7 +302,7 @@ macro_rules! layout_op_xy ( match self { Self::X(c) | Self::Y(c) | Self::XY(c) => c } } } - impl> Draw for $T { + impl> Draw for $T { fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } } }; @@ -73,7 +313,7 @@ macro_rules! layout_op_xy ( match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, } } } - impl> Draw for $T { + impl> Draw for $T { fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } } impl $T { @@ -91,7 +331,7 @@ macro_rules! layout_op_xy ( match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, } } } - impl> Draw for $T { + impl> Draw for $T { fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } } impl $T { @@ -107,7 +347,7 @@ macro_rules! layout_op_xy ( // Implement layout op that increments X and/or Y by fixed amount. macro_rules! push_pull(($T:ident: $method: ident)=>{ layout_op_xy!(1: $T); - impl> Layout for $T { + impl> Layout for $T { fn layout_x (&self, area: XYWH) -> O::Unit { area.x().$method(self.dx()) } fn layout_y (&self, area: XYWH) -> O::Unit { area.y().$method(self.dy()) } } @@ -117,7 +357,7 @@ push_pull!(Pull: minus); layout_op_xy!(0: Fill); -impl> Layout for Fill { +impl> Layout for Fill { fn layout_x (&self, area: XYWH) -> O::Unit { if self.dx() { area.x() } else { self.inner().layout_x(area) } } fn layout_y (&self, area: XYWH) -> O::Unit { if self.dy() { area.y() } else { self.inner().layout_y(area) } } fn layout_w (&self, area: XYWH) -> O::Unit { if self.dx() { area.w() } else { self.inner().layout_w(area) } } @@ -135,7 +375,7 @@ impl Fill { layout_op_xy!(1 opt: Fixed); -impl> Layout for Fixed { +impl> Layout for Fixed { fn layout_w (&self, area: XYWH) -> O::Unit { self.dx().unwrap_or(self.inner().layout_w(area)) } fn layout_w_min (&self, area: XYWH) -> O::Unit { self.dx().unwrap_or(self.inner().layout_w_min(area)) } fn layout_w_max (&self, area: XYWH) -> O::Unit { self.dx().unwrap_or(self.inner().layout_w_max(area)) } @@ -146,7 +386,7 @@ impl> Layout for Fixed { layout_op_xy!(1 opt: Max); -impl> Layout for Max { +impl> Layout for Max { fn layout (&self, area: XYWH) -> XYWH { let XYWH(x, y, w, h) = self.inner().layout(area); match self { @@ -159,7 +399,7 @@ impl> Layout for Max { layout_op_xy!(1 opt: Min); -impl> Layout for Min { +impl> Layout for Min { fn layout (&self, area: XYWH) -> XYWH { let XYWH(x, y, w, h) = self.inner().layout(area); match self { @@ -172,7 +412,7 @@ impl> Layout for Min { layout_op_xy!(1 opt: Expand); -impl> Layout for Expand { +impl> Layout for Expand { fn layout_w (&self, to: XYWH) -> O::Unit { self.inner().layout_w(to).plus(self.dx().unwrap_or_default()) } @@ -185,7 +425,7 @@ impl> Layout for Expand { layout_op_xy!(1 opt: Shrink); -impl> Layout for Shrink { +impl> Layout for Shrink { fn layout (&self, to: XYWH) -> XYWH { let area = self.inner().layout(to); let dx = self.dx().unwrap_or_default(); @@ -208,9 +448,12 @@ impl Align { #[inline] pub const fn se (a: T) -> Self { Self(Alignment::SE, a) } } -impl> Layout for Align { +impl> Draw for Align { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), &self.1).draw(to) } +} + +impl> Layout for Align { fn layout_x (&self, to: XYWH) -> O::Unit { - use Alignment::*; match self.0 { NW | W | SW => to.x(), N | Center | S => to.x().plus(to.w() / 2.into()).minus(self.1.layout_w(to) / 2.into()), @@ -219,7 +462,6 @@ impl> Layout for Align { } } fn layout_y (&self, to: XYWH) -> O::Unit { - use Alignment::*; match self.0 { NW | N | NE => to.y(), W | Center | E => to.y().plus(to.h() / 2.into()).minus(self.1.layout_h(to) / 2.into()), @@ -247,7 +489,11 @@ impl Pad { } } -impl> Layout for Pad { +impl> Draw for Pad { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } +} + +impl> Layout for Pad { fn layout_x (&self, area: XYWH) -> O::Unit { area.x().plus(self.dx()) } fn layout_y (&self, area: XYWH) -> O::Unit { area.x().plus(self.dx()) } fn layout_w (&self, area: XYWH) -> O::Unit { area.w().minus(self.dx() * 2.into()) } @@ -263,7 +509,21 @@ impl Bsp { #[inline] pub const fn b (a: Head, b: Tail) -> Self { Self(Below, a, b) } } -impl, Tail: Layout> Layout for Bsp { +impl, Tail: Content> Draw for Bsp { + fn draw (&self, to: &mut O) { + let [a, b, _] = bsp_areas(to.area(), self.0, &self.1, &self.2); + //panic!("{a:?} {b:?}"); + if self.0 == Below { + to.place_at(a, &self.1); + to.place_at(b, &self.2); + } else { + to.place_at(b, &self.2); + to.place_at(a, &self.1); + } + } +} + +impl, Tail: Layout> Layout for Bsp { fn layout_w (&self, area: XYWH) -> O::Unit { match self.0 { Above | Below | North | South => self.1.layout_w(area).max(self.2.layout_w(area)), @@ -305,6 +565,56 @@ impl, Tail: Layout> Layout for Bsp } } +fn bsp_areas , B: Layout> ( + area: XYWH, + direction: Direction, + a: &A, + b: &B, +) -> [XYWH;3] { + let XYWH(x, y, w, h) = area; + let WH(aw, ah) = a.layout(area).wh(); + let WH(bw, bh) = b.layout(match direction { + South => XYWH(x, y + ah, w, h.minus(ah)), + North => XYWH(x, y, w, h.minus(ah)), + East => XYWH(x + aw, y, w.minus(aw), h), + West => XYWH(x, y, w.minus(aw), h), + Above => area, + Below => area, + }).wh(); + match direction { + Above | Below => { + let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah.max(bh)]); + let a = XYWH((x + w/2.into()).minus(aw/2.into()), (y + h/2.into()).minus(ah/2.into()), aw, ah); + let b = XYWH((x + w/2.into()).minus(bw/2.into()), (y + h/2.into()).minus(bh/2.into()), bw, bh); + [a.into(), b.into(), XYWH(x, y, w, h)] + }, + South => { + let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]); + let a = XYWH((x + w/2.into()).minus(aw/2.into()), y, aw, ah); + let b = XYWH((x + w/2.into()).minus(bw/2.into()), y + ah, bw, bh); + [a.into(), b.into(), XYWH(x, y, w, h)] + }, + North => { + let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]); + let a = XYWH((x + (w/2.into())).minus(aw/2.into()), y + bh, aw, ah); + let b = XYWH((x + (w/2.into())).minus(bw/2.into()), y, bw, bh); + [a.into(), b.into(), XYWH(x, y, w, h)] + }, + East => { + let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]); + let a = XYWH(x, (y + h/2.into()).minus(ah/2.into()), aw, ah); + let b = XYWH(x + aw, (y + h/2.into()).minus(bh/2.into()), bw, bh); + [a.into(), b.into(), XYWH(x, y, w, h)] + }, + West => { + let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]); + let a = XYWH(x + bw, (y + h/2.into()).minus(ah/2.into()), aw, ah); + let b = XYWH(x, (y + h/2.into()).minus(bh/2.into()), bw, bh); + [a.into(), b.into(), XYWH(x, y, w, h)] + }, + } +} + impl<'a, O, A, B, I, F, G> Map where I: Iterator + Send + Sync + 'a, F: Fn() -> I + Send + Sync + 'a, @@ -316,28 +626,13 @@ impl<'a, O, A, B, I, F, G> Map where get_item } } - const fn to_south ( - item_offset: S::Unit, item_height: S::Unit, item: impl Draw - ) -> impl Draw { - Push::Y(item_offset, Fixed::Y(item_height, Fill::X(item))) - } - const fn to_south_west ( - item_offset: S::Unit, item_height: S::Unit, item: impl Draw - ) -> impl Draw { - Push::Y(item_offset, Align::nw(Fixed::Y(item_height, Fill::X(item)))) - } - const fn to_east ( - item_offset: S::Unit, item_width: S::Unit, item: impl Draw - ) -> impl Draw { - Push::X(item_offset, Align::w(Fixed::X(item_width, Fill::Y(item)))) - } } macro_rules! impl_map_direction (($name:ident, $axis:ident, $align:ident)=>{ impl<'a, O, A, B, I, F> Map< O, A, Push>>>, I, F, fn(A, usize)->B > where - O: Screen, + O: Out, B: Draw, I: Iterator + Send + Sync + 'a, F: Fn() -> I + Send + Sync + 'a @@ -373,7 +668,7 @@ impl_map_direction!(west, X, e); impl_map_direction!(north, Y, s); impl<'a, O, A, B, I, F, G> Layout for Map where - O: Screen, + O: Out, B: Layout, I: Iterator + Send + Sync + 'a, F: Fn() -> I + Send + Sync + 'a, @@ -400,8 +695,8 @@ impl<'a, O, A, B, I, F, G> Layout for Map where } impl<'a, O, A, B, I, F, G> Draw for Map where - O: Screen, - B: Draw, + O: Out, + B: Content, I: Iterator + Send + Sync + 'a, F: Fn() -> I + Send + Sync + 'a, G: Fn(A, usize)->B + Send + Sync @@ -412,8 +707,8 @@ impl<'a, O, A, B, I, F, G> Draw for Map where let area = self.layout(to.area()); for item in get_iter() { let item = get_item(item, index); - //to.show(area.into(), &item); - to.show(item.layout(area), &item); + //to.place_at(area.into(), &item); + to.place_at(item.layout(area), &item); index += 1; } } @@ -465,19 +760,34 @@ impl PerfModel { mod xywh { use crate::*; - impl X for XY { fn x (&self) -> N { self.0 } } - impl Y for XY { fn y (&self) -> N { self.1 } } - impl W for WH { fn w (&self) -> N { self.0 } } - impl H for WH { fn h (&self) -> N { self.1 } } - impl X for XYWH { fn x (&self) -> N { self.0 } } - impl Y for XYWH { fn y (&self) -> N { self.1 } } - impl W for XYWH { fn w (&self) -> N { self.2 } } - impl H for XYWH { fn h (&self) -> N { self.3 } } - impl X for O { fn x (&self) -> O::Unit { self.area().x() } } - impl Y for O { fn y (&self) -> O::Unit { self.area().y() } } - impl W for O { fn w (&self) -> O::Unit { self.area().w() } } - impl H for O { fn h (&self) -> O::Unit { self.area().h() } } - + impl HasXY for XY { + fn x (&self) -> N { self.0 } + fn y (&self) -> N { self.1 } + } + impl HasXY for XYWH { + fn x (&self) -> N { self.0 } + fn y (&self) -> N { self.1 } + } + impl HasXY for O { + // X coordinate of output area + #[inline] fn x (&self) -> O::Unit { self.area().x() } + // Y coordinate of output area + #[inline] fn y (&self) -> O::Unit { self.area().y() } + } + impl HasWH for WH { + fn w (&self) -> N { self.0 } + fn h (&self) -> N { self.1 } + } + impl HasWH for XYWH { + fn w (&self) -> N { self.2 } + fn h (&self) -> N { self.3 } + } + impl HasWH for O { + // Width of output area + #[inline] fn w (&self) -> O::Unit { self.area().w() } + // Height of output area + #[inline] fn h (&self) -> O::Unit { self.area().h() } + } impl WH { pub fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w), self.h()] } pub fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h)] } @@ -547,48 +857,6 @@ mod xywh { } } -impl Thread { - /// Spawn a TUI thread that runs `callt least one, then repeats until `exit`. - pub fn new (exit: Arc, call: F) -> Result - where F: Fn(&PerfModel)->() + Send + Sync + 'static - { - let perf = Arc::new(PerfModel::default()); - Ok(Self { - exit: exit.clone(), - perf: perf.clone(), - join: std::thread::Builder::new().name("tengri tui output".into()).spawn(move || { - while !exit.fetch_and(true, Relaxed) { - let _ = perf.cycle(&call); - } - })?.into() - }) - } - - /// Spawn a TUI thread that runs `callt least one, then repeats - /// until `exit`, sleeping for `time` msec after every iteration. - pub fn new_sleep ( - exit: Arc, time: Duration, call: F - ) -> Result - where F: Fn(&PerfModel)->() + Send + Sync + 'static - { - Self::new(exit, move |perf| { let _ = call(perf); std::thread::sleep(time); }) - } - - /// Spawn a TUI thread that runs `callt least one, then repeats - /// until `exit`, using polling to run every `time` msec. - pub fn new_poll ( - exit: Arc, time: Duration, call: F - ) -> Result - where F: Fn(&PerfModel)->() + Send + Sync + 'static - { - Self::new(exit, move |perf| { if poll(time).is_ok() { let _ = call(perf); } }) - } - - pub fn join (self) -> Result<(), Box> { - self.join.join() - } -} - #[cfg(feature = "tui")] pub use self::tui_impls::*; #[cfg(feature = "tui")] mod tui_impls { use crate::*; @@ -601,12 +869,6 @@ impl Thread { impl_from!(ItemColor: |rgb: Color| Self { rgb, okhsl: rgb_to_okhsl(rgb) }); impl_from!(ItemColor: |okhsl: Okhsl| Self { okhsl, rgb: okhsl_to_rgb(okhsl) }); impl_debug!(BigBuffer |self, f| { write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len()) }); - impl Screen for Buffer { type Unit = u16; } - impl TuiOut for Buffer { fn tui_out (&mut self) -> &mut Buffer { self } } - impl TuiOut for Buffer { fn tui_out (&mut self) -> &mut Buffer { self } } - - //impl> TuiOut for T { fn tui_out (&mut self) -> &mut Buffer { self.as_mut() } } - impl Tui { /// True if done pub fn exited (&self) -> bool { self.exited.fetch_and(true, Relaxed) } @@ -615,7 +877,11 @@ impl Thread { /// Clean up after run pub fn teardown (&self) -> Usually<()> { tui_teardown(&mut*self.backend.write().unwrap()) } /// Apply changes to the display buffer. - pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { tui_flip(self, self.buffer, buffer, size) } + pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { + tui_resized(&mut*self.backend.write().unwrap(), &mut*self.buffer.write().unwrap(), size); + tui_redrawn(&mut*self.backend.write().unwrap(), &mut*self.buffer.write().unwrap(), &mut buffer); + buffer + } /// Create the engine. pub fn new (output: Box) -> Usually { let backend = CrosstermBackend::new(output); @@ -630,17 +896,15 @@ impl Thread { error: None, }) } - /// Run an app in the engine. + /// Run an amm in the engine. pub fn run (mut self, join: bool, state: &Arc>) -> Usually> where - T: Act + Draw + Send + Sync + 'static + T: Handle + Draw + Send + Sync + 'static { self.setup()?; - let tui = Arc::new(self); - let input_poll = Duration::from_millis(100); - let output_sleep = Duration::from_millis(10); // == 1/MAXFPS (?) - let _input_thread = tui_input(&tui, state, input_poll)?; - let render_thread = tui_output(&tui, state, output_sleep)?; - if join { // Run until render thread ends: + let tui = Arc::new(self); + let _input_thread = tui_input(&tui, state, Duration::from_millis(100))?; + let render_thread = tui_output(&tui, state, Duration::from_millis(10))?; + if join { let result = render_thread.join(); tui.teardown()?; match result { @@ -652,14 +916,77 @@ impl Thread { } } + /// Spawn the input thread. + pub fn tui_input + Send + Sync + 'static> ( + engine: &Arc, state: &Arc>, poll: Duration + ) -> Result { + let state = state.clone(); + let engine = engine.clone(); + TuiThread::new_poll(engine.exited.clone(), poll, move |_| { + let event = read().unwrap(); + match event { + Event::Key(KeyEvent { + modifiers: KeyModifiers::CONTROL, + code: KeyCode::Char('c'), + kind: KeyEventKind::Press, + state: KeyEventState::NONE + }) => { + engine.exited.store(true, Relaxed); + }, + _ => { + let event = TuiEvent::from_crossterm(event); + if let Err(e) = state.write().unwrap().handle(&engine) { + panic!("{e}") + } + } + } + }) + } - impl TuiEvent { - pub fn from_crossterm (event: Event) -> Self { Self(event) } - #[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Perhaps { - Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self)) + /// Spawn the output thread. + pub fn tui_output + Send + Sync + 'static> ( + engine: &Arc, state: &Arc>, sleep: Duration + ) -> Result { + let state = state.clone(); + let mut engine = engine.clone(); + let WH(width, height) = tui_wh(&engine); + let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height }); + TuiThread::new_sleep(engine.exited.clone(), sleep, move |perf| { + let WH(width, height) = tui_wh(&engine); + if let Ok(state) = state.try_read() { + let size = Rect { x: 0, y: 0, width, height }; + if buffer.area != size { + engine.backend.write().unwrap() + .clear_region(ClearType::All).expect("pre-frame clear region failed"); + buffer.resize(size); + buffer.reset(); + } + state.draw(&engine); + buffer = engine.flip(*engine.buffer.write().unwrap(), size); + } + let timer = format!("{:>3.3}ms", perf.used.load(Relaxed)); + buffer.set_string(0, 0, &timer, Style::default()); + }) + } + + fn tui_wh (engine: &Tui) -> WH { + let Size { width, height } = engine.backend.read().unwrap().size().expect("get size failed"); + WH(width, height) + } + + impl Input for Tui { + type Event = TuiEvent; + type Handled = bool; + fn event (&self) -> &TuiEvent { + self.event.as_ref().expect("input.event called outside of input handler") } } + impl Done for Tui { + fn done (&self) { self.exited.store(true, Relaxed); } + fn is_done (&self) -> bool { self.exited.fetch_and(true, Relaxed) } + } + impl Ord for TuiEvent { fn cmp (&self, other: &Self) -> std::cmp::Ordering { self.partial_cmp(other) @@ -667,6 +994,13 @@ impl Thread { } } + impl TuiEvent { + pub fn from_crossterm (event: Event) -> Self { Self(event) } + #[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Perhaps { + Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self)) + } + } + impl From for TuiEvent { fn from (e: Event) -> Self { Self(e) @@ -727,6 +1061,86 @@ impl Thread { } } + impl Out for Tui { + type Unit = u16; + #[inline] fn area (&self) -> XYWH { self.area } + #[inline] fn area_mut (&mut self) -> &mut XYWH { &mut self.area } + #[inline] fn place_at <'t, T: Draw + ?Sized> (&mut self, area: XYWH, content: &'t T) { + let last = self.area(); + *self.area_mut() = area; + content.draw(self); + *self.area_mut() = last; + } + } + + impl Tui { + #[inline] pub fn with_rect (&mut self, area: XYWH) -> &mut Self { self.area = area; self } + pub fn update (&mut self, area: XYWH, callback: &impl Fn(&mut Cell, u16, u16)) { + tui_update(&mut*self.buffer.write().unwrap(), area, callback); + } + pub fn fill_char (&mut self, area: XYWH, c: char) { self.update(area, &|cell,_,_|{cell.set_char(c);}) } + pub fn fill_bg (&mut self, area: XYWH, color: Color) { self.update(area, &|cell,_,_|{cell.set_bg(color);}) } + pub fn fill_fg (&mut self, area: XYWH, color: Color) { self.update(area, &|cell,_,_|{cell.set_fg(color);}) } + pub fn fill_mod (&mut self, area: XYWH, on: bool, modifier: Modifier) { + if on { + self.update(area, &|cell,_,_|cell.modifier.insert(modifier)) + } else { + self.update(area, &|cell,_,_|cell.modifier.remove(modifier)) + } + } + pub fn fill_bold (&mut self, area: XYWH, on: bool) { self.fill_mod(area, on, Modifier::BOLD) } + pub fn fill_reversed (&mut self, area: XYWH, on: bool) { self.fill_mod(area, on, Modifier::REVERSED) } + pub fn fill_crossed_out (&mut self, area: XYWH, on: bool) { self.fill_mod(area, on, Modifier::CROSSED_OUT) } + pub fn fill_ul (&mut self, area: XYWH, color: Option) { + if let Some(color) = color { + self.update(area, &|cell,_,_|{ + cell.modifier.insert(ratatui::prelude::Modifier::UNDERLINED); + cell.underline_color = color; + }) + } else { + self.update(area, &|cell,_,_|{ + cell.modifier.remove(ratatui::prelude::Modifier::UNDERLINED); + }) + } + } + pub fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) { + for cell in self.buffer.write().unwrap().content.iter_mut() { + cell.fg = fg; + cell.bg = bg; + cell.modifier = modifier; + } + } + pub fn blit (&mut self, text: &impl AsRef, x: u16, y: u16, style: Option