diff --git a/examples/mode_02.rs b/examples/mode_02.rs index a2106b3..1ec2069 100644 --- a/examples/mode_02.rs +++ b/examples/mode_02.rs @@ -37,7 +37,6 @@ impl_keywords!(Tui, XYWH, State [ kw_split, kw_align, kw_exact, - kw_fixed, kw_min, kw_max, kw_push, diff --git a/src/layout.rs b/src/layout.rs new file mode 100644 index 0000000..9ef91cc --- /dev/null +++ b/src/layout.rs @@ -0,0 +1,103 @@ +use crate::*; + +impl> Layout for T {} + +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 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) + } +} + +/// 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(Relaxed) as u16 + } + fn y (&self) -> u16 { + self.1.load(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 { + draw(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) + }) + } +} + +mod area; pub use self::area::*; +mod axis; pub use self::axis::*; +mod azimuth; pub use self::azimuth::*; +mod cond; pub use self::cond::*; +mod iter; pub use self::iter::*; diff --git a/src/layout/align.rs b/src/layout/align.rs deleted file mode 100644 index 1e1c961..0000000 --- a/src/layout/align.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::*; -use Azimuth::*; - -pub struct Align( - pub(crate) Option, - pub(crate) T, -); - -impl> Draw for Align { - fn layout (&self, area: XYWH) -> Perhaps> { - Ok(align::(area, self.1.layout(area)?, self.0)) - } - fn draw (self, to: &mut S) -> Perhaps> { - Ok(self.layout(to.area())? - .map(|area|to.clip(area, |to|self.1.draw(to))) - .transpose()? - .flatten()) - } -} - -fn align ( - area0: XYWH, area: Option>, azimuth: Option -) -> Option> { - area.map(|XYWH(x, y, w, h)|{ - let XYWH(x0, y0, w0, h0) = area0; - match azimuth { - Some(NW) => XYWH(x0, y0, w, h), - Some(N) => XYWH(x0 + w0.minus(w) / 2.into(), y0, w, h), - Some(NE) => XYWH((x0 + w0).minus(w), y0, w, h), - Some(W) => XYWH(x0, y0 + h0.minus(h) / 2.into(), w, h), - Some(C) => XYWH(x0 + w0.minus(w) / 2.into(), y0 + h0.minus(h) / 2.into(), w, h), - Some(E) => XYWH((x0 + w0).minus(w), y0 + h0.minus(h) / 2.into(), w, h), - Some(SW) => XYWH(x0, (y0 + h0).minus(h), w, h), - Some(S) => XYWH(x0 + w0.minus(w) / 2.into(), (y0 + h0).minus(h), w, h), - Some(SE) => XYWH((x0 + w0).minus(w), (y0 + h0).minus(h), w, h), - Some(X) => XYWH(x0 + w0.minus(w) / 2.into(), y, w, h), - Some(Y) => XYWH(x, y0 + h0.minus(h) / 2.into(), w, h), - None => XYWH(x, y, w, h) - } - }) -} diff --git a/src/layout/area.rs b/src/layout/area.rs index b6d68da..a806c92 100644 --- a/src/layout/area.rs +++ b/src/layout/area.rs @@ -16,6 +16,7 @@ pub struct Area>( pub Option>, pub T ); + impl_draw!(,>|self: Area, to: S|{ - to.clip(self.0, |to|self.1.draw(to)) + to.draw(self.0, &self.1) }); diff --git a/src/layout/axis.rs b/src/layout/axis.rs new file mode 100644 index 0000000..7d6d6bb --- /dev/null +++ b/src/layout/axis.rs @@ -0,0 +1,248 @@ +use crate::*; + +macro_rules! def_layout_modifier { + ( + $Trait:ident ($fn_x:ident $fn_y:ident $fn_xy:ident), + $Struct:ident { $X:ident $Y:ident $XY:ident } $kw_name:ident $name:literal + |$self:ident, $to:ident| $body:block + ) => { + impl> $Trait for T {} + + pub trait $Trait: Draw + Sized { + fn $fn_x > + Copy> (self, x: N) + -> $Struct { $Struct::$X(self, x) } + fn $fn_y > + Copy> (self, y: N) + -> $Struct { $Struct::$Y(self, y) } + fn $fn_xy > + Copy> (self, x: N, y: N) + -> $Struct { $Struct::$XY(self, x, y) } + } + + pub enum $Struct, X: Into>> { + __(PhantomData), $X(I, X), $Y(I, X), $XY(I, X, X), + } + + impl , X: Into> + Copy> Draw for $Struct { + fn draw (&$self, $to: &mut S) -> Perhaps> { + $body + } + } + + fn_kw_layout!($kw_name |state, output, expr| { + let head = expr.head()?; + let mut frags = head.src()?.unwrap_or_default().split("/"); + Ok(matches!(expr.head()?, Some("exact")).then(||{ + let args = expr.tail(); + let arg0 = args.head(); + let tail0 = args.tail(); + let arg1 = tail0.head(); + let tail1 = tail0.tail(); + let arg2 = tail1.head(); + eval_xy!( + $name => expr, head, output, state, frags.next(), + $fn_xy, $fn_x, $fn_y, + arg0, arg1, arg2, + ) + }).transpose()?.flatten()) + }); + } +} + +def_layout_modifier!( + CanExact (exact_w exact_h exact_wh), + Exact { W H WH } kw_exact "exact" |self, to| { + let XYWH(x, y, w0, h0) = to.area(); + let (item, w, h) = match self { + Self::W(item, w) => (item, (*w).into().unwrap_or(w0), h0), + Self::H(item, h) => (item, w0, (*h).into().unwrap_or(h0)), + Self::WH(item, w, h) => (item, (*w).into().unwrap_or(h0), (*h).into().unwrap_or(h0)), + _ => unreachable!() + }; + to.draw(XYWH(x, y, w, h), item) + } +); + +def_layout_modifier!( + CanMin (min_w min_h min_wh), + Min { W H WH } kw_min "min" |self, to| { + match self { + Self::__(_) => unreachable!(), + Self::W(item, w1) if let Some(XYWH(x, y, w, h)) = to.size(None, item)? => { + let w: S::Unit = (*w1).into().map(|w1|w.max(w1)).unwrap_or(w); + to.draw(XYWH(x, y, w, h), item) + }, + Self::H(item, h1) if let Some(XYWH(x, y, w, h)) = to.size(None, item)? => { + let h: S::Unit = (*h1).into().map(|h1|h.max(h1)).unwrap_or(h); + to.draw(XYWH(x, y, w, h), item) + }, + Self::WH(item, w1, h1) if let Some(XYWH(x, y, w, h)) = to.size(None, item)? => { + let w: S::Unit = (*w1).into().map(|w1|w.max(w1)).unwrap_or(w); + let h: S::Unit = (*h1).into().map(|h1|h.max(h1)).unwrap_or(h); + to.draw(XYWH(x, y, w, h), item) + }, + _ => Ok(None) + } + } +); + +def_layout_modifier!( + CanMax (max_w max_h max_wh), + Max { W H WH } kw_max "max" |self, to| { + let area: XYWH = to.area(); + let (item, area) = match self { + Self::W(item, max_w) => (item, XYWH( + area.0, area.1, (*max_w).into().map(|max|max.min(area.2)).unwrap_or(area.2), + area.3 + )), + Self::H(item, max_h) => (item, XYWH( + area.0, area.1, area.2, + (*max_h).into().map(|max|max.min(area.3)).unwrap_or(area.3) + )), + Self::WH(item, max_w, max_h) => (item, XYWH( + area.0, area.1, (*max_w).into().map(|max|max.min(area.2)).unwrap_or(area.2), + (*max_h).into().map(|max|max.min(area.3)).unwrap_or(area.3), + )), + _ => return Ok(None) + }; + to.draw(area, item) + } +); + +def_layout_modifier!( + CanPad (pad_w pad_h pad_wh), + Pad { X Y XY } kw_pad "pad" |self, to| { + let area = to.area(); + let (item, area) = match self { + Self::X(item, w1) => { + let w1: S::Unit = (*w1).into().unwrap_or_default(); + (item, XYWH(area.0 + w1, area.1, area.2.minus(w1 + w1), area.3)) + }, + Self::Y(item, h1) => { + let h1: S::Unit = (*h1).into().unwrap_or_default(); + (item, XYWH(area.0, area.1 + h1, area.2, area.3.minus(h1 + h1))) + }, + Self::XY(item, w1, h1) => { + let w1: S::Unit = (*w1).into().unwrap_or_default(); + let h1: S::Unit = (*h1).into().unwrap_or_default(); + (item, XYWH(area.0 + w1, area.1 + h1, area.2.minus(w1 + w1), area.3.minus(h1 + h1))) + }, + _ => return Ok(None) + }; + item.draw(to) + } +); + +def_layout_modifier!( + CanPush (push_x push_y push_xy), + Push { X Y XY } kw_push "push" |self, to| { + match self { + Self::__(_) => unreachable!(), + Self::X(item, x1) if let Some(XYWH(x, y, w, h)) = to.size(None, item)? => { + to.draw(XYWH(x + (*x1).into().unwrap_or_default(), y, w, h), item) + }, + Self::Y(item, y1) if let Some(XYWH(x, y, w, h)) = to.size(None, item)? => { + to.draw(XYWH(x, y + (*y1).into().unwrap_or_default(), w, h), item) + }, + Self::XY(item, x1, y1) if let Some(XYWH(x, y, w, h)) = to.size(None, item)? => { + to.draw(XYWH(x + (*x1).into().unwrap_or_default(), y + (*y1).into().unwrap_or_default(), w, h), item) + }, + _ => Ok(None) + } + } +); + +def_layout_modifier!( + CanPull (pull_x pull_y pull_xy), + Pull { X Y XY } kw_pull "pull" |self, to| { + todo!() + } +); + +/// Use whole drawing area along one or both axes. +/// +/// ``` +/// # fn doctest_layout_full () -> Result<(), Box> { +/// use tengri::{Layout, Draw, XYWH, Tui, Perhaps, Screen}; +/// let area = XYWH(0u16, 0, 80, 25); +/// assert_eq!(layout("1")?, Some(XYWH(0u16, 0, 1, 1))); +/// assert_eq!(layout("1".full_w())?, Some(XYWH(0u16, 0, 80, 1))); +/// assert_eq!(layout("1".full_h())?, Some(XYWH(0u16, 0, 1, 25))); +/// assert_eq!(layout("1".full_wh())?, Some(XYWH(0u16, 0, 80, 25))); +/// fn layout (t: impl Draw) -> Perhaps> { +/// Tui::Layout(XYWH(0u16, 0, 80, 25)).size(None, t) +/// } +/// # Ok(()) } +/// ``` +pub enum Full> { + __(PhantomData), + W(I), + H(I), + WH(I), +} + +impl_draw!(,>|self: Full, to: T|{ + let XYWH(x0, y0, w0, h0) = to.area(); + match self { + Self::W(item) => if let Some(XYWH(_, y, _, h)) = to.size(None, item)? { + to.draw(XYWH(x0, y, w0, h), item) + } else { + Ok(None) + }, + Self::H(item) => if let Some(XYWH(x, _, w, _)) = to.size(None, item)? { + to.draw(XYWH(x, y0, w, h0), item) + } else { + Ok(None) + }, + Self::WH(item) => if let Some(XYWH(..)) = to.size(None, item)? { + to.draw(XYWH(x0, y0, w0, h0), item) + } else { + Ok(None) + }, + _ => unreachable!(), + } +}); + +/// Only draw content if area is above a certain size. +/// +/// ``` +/// # fn doctest_layout_min () -> Result<(), Box> { +/// use tengri::{Layout, Draw, XYWH}; +/// let area = XYWH(1u16, 1, 80, 25); +/// assert_eq!("1".min_w(5).layout(area)?, Some(XYWH(1u16, 1, 5, 1))); +/// assert_eq!("1".min_h(5).layout(area)?, Some(XYWH(1u16, 1, 1, 5))); +/// assert_eq!("1".min_wh(5, 5).layout(area)?, Some(XYWH(1u16, 1, 5, 5))); +/// assert_eq!("123456".min_w(5).layout(area)?, Some(XYWH(1u16, 1, 6, 1))); +/// # Ok(()) } +/// ``` +/// Move content in the negative direction of one or both axes. +/// +/// ``` +/// # fn doctest_layout_pull () -> Result<(), Box> { +/// use tengri::{Layout, Draw, XYWH}; +/// let area = XYWH(1u16, 1, 80, 25); +/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); +/// assert_eq!("1".pull_x(1).layout(area)?, Some(XYWH(0u16, 1, 1, 1))); +/// assert_eq!("1".pull_y(1).layout(area)?, Some(XYWH(1u16, 0, 1, 1))); +/// assert_eq!("1".pull_xy(1, 1).layout(area)?, Some(XYWH(0u16, 0, 1, 1))); +/// # Ok(()) } +/// ``` +/// Move content in the positive direction of one or both axes. +/// +/// ``` +/// # fn doctest_layout_push () -> Result<(), Box> { +/// use tengri::{Layout, Draw, XYWH}; +/// let area = XYWH(0u16, 0, 80, 25); +/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); +/// assert_eq!("1".push_x(1).layout(area)?, Some(XYWH(1u16, 0, 1, 1))); +/// assert_eq!("1".push_y(1).layout(area)?, Some(XYWH(0u16, 1, 1, 1))); +/// assert_eq!("1".push_xy(1, 1).layout(area)?, Some(XYWH(1u16, 1, 1, 1))); +/// # Ok(()) } +/// ``` +#[cfg(test)] #[test] fn test_exact () -> Usually<()> { + let mut screen = Tui::Layout(XYWH(0, 0, 80, 25)); + assert_eq!("FOOBAR\nKILROY".draw(&mut screen)?, Some(XYWH(0, 0, 6, 2))); + assert_eq!("FOOBAR\nKILROY".exact_w(3).draw(&mut screen)?, Some(XYWH(0, 0, 3, 2))); + assert_eq!("FOOBAR\nKILROY".exact_h(1).draw(&mut screen)?, Some(XYWH(0, 0, 6, 1))); + assert_eq!("FOOBAR\nKILROY".exact_wh(2, 1).draw(&mut screen)?, Some(XYWH(0, 0, 2, 1))); + Ok(()) +} + diff --git a/src/layout/azimuth.rs b/src/layout/azimuth.rs new file mode 100644 index 0000000..ac749b8 --- /dev/null +++ b/src/layout/azimuth.rs @@ -0,0 +1,455 @@ +use crate::*; +use Azimuth::*; + +/// 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 +} + +pub struct Origin( + pub(crate) Option, + pub(crate) 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() + } +} + +fn_kw_layout!(kw_align |state, output, expr| { + Ok(matches!(expr.head()?, Some("align")).then(||{ + draw(move|output: &mut O|{state.interpret(output, &expr.tail().head())}).align( + eval_enum!("align", output, state, + expr.head().src()?.unwrap_or_default().split("/").skip(1).next(), + expr.tail().head(), + Azimuth { + "c" => C, "x" => X, "y" => Y, + "n" => N, "s" => S, "e" => E, "w" => W, + "nw" => NW, "sw" => SW, "ne" => NE, "se" => SE, + }) + ).draw(output) + }).transpose()?.flatten()) +}); + +impl> CanAlign for T {} + +pub trait CanAlign: Draw + Sized { + fn align (self, azimuth: impl Into>) -> Align { + Align(azimuth.into(), self) + } + fn align_c (self) -> Align { + Align(Some(Azimuth::C), self) + } + fn align_x (self) -> Align { + Align(Some(Azimuth::X), self) + } + fn align_y (self) -> Align { + Align(Some(Azimuth::Y), self) + } + fn align_n (self) -> Align { + Align(Some(Azimuth::N), self) + } + fn align_s (self) -> Align { + Align(Some(Azimuth::S), self) + } + fn align_e (self) -> Align { + Align(Some(Azimuth::E), self) + } + fn align_w (self) -> Align { + Align(Some(Azimuth::W), self) + } + fn align_ne (self) -> Align { + Align(Some(Azimuth::NE), self) + } + fn align_se (self) -> Align { + Align(Some(Azimuth::SE), self) + } + fn align_nw (self) -> Align { + Align(Some(Azimuth::NW), self) + } + fn align_sw (self) -> Align { + Align(Some(Azimuth::SW), self) + } +} + +pub struct Align( + pub(crate) Option, + pub(crate) T, +); + +impl> Draw for Align { + fn draw (&self, to: &mut S) -> Perhaps> { + let area = to.area(); + Ok(if let Some(area) = align::(area, to.size(area, &self.1)?, self.0) { + to.draw(area, &self.1)? + } else { + None + }) + } +} + +fn align ( + area0: XYWH, area: Option>, azimuth: Option +) -> Option> { + area.map(|XYWH(x, y, w, h)|{ + let XYWH(x0, y0, w0, h0) = area0; + match azimuth { + Some(NW) => XYWH(x0, y0, w, h), + Some(N) => XYWH(x0 + w0.minus(w) / 2.into(), y0, w, h), + Some(NE) => XYWH((x0 + w0).minus(w), y0, w, h), + Some(W) => XYWH(x0, y0 + h0.minus(h) / 2.into(), w, h), + Some(C) => XYWH(x0 + w0.minus(w) / 2.into(), y0 + h0.minus(h) / 2.into(), w, h), + Some(E) => XYWH((x0 + w0).minus(w), y0 + h0.minus(h) / 2.into(), w, h), + Some(SW) => XYWH(x0, (y0 + h0).minus(h), w, h), + Some(S) => XYWH(x0 + w0.minus(w) / 2.into(), (y0 + h0).minus(h), w, h), + Some(SE) => XYWH((x0 + w0).minus(w), (y0 + h0).minus(h), w, h), + Some(X) => XYWH(x0 + w0.minus(w) / 2.into(), y, w, h), + Some(Y) => XYWH(x, y0 + h0.minus(h) / 2.into(), w, h), + None => XYWH(x, y, w, h) + } + }) +} + +fn_kw_layout!(kw_split |state, output, expr| { + let head = expr.head(); + let mut frags = head.src()?.unwrap_or_default().split("/"); + Ok(matches!(frags.next(), Some("bsp")).then(||{ + eval_enum!("bsp", output, state, frags.next(), expr.tail().head()?, Split { + "n" => North, + "s" => South, + "e" => East, + "w" => West, + "a" => Above, + "b" => Below + }).stack( + draw(move|output: &mut O|{ + state.interpret(output, &expr.tail().tail().head()?) + }), + draw(move|output: &mut O|{ + state.interpret(output, &expr.tail().tail().tail().head()?) + }), + ).draw(output) + }).transpose()?.flatten()) +}); + +/// 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 +} + +pub struct Pair, B: Draw>(Split, A, B, PhantomData); + +pub fn split , B: Draw> ( + split: Split, a: A, b: B +) -> Pair { + Pair(split, a, b, PhantomData) +} + +impl, B: Draw> Draw for Pair { + fn draw (&self, to: &mut S) -> Drawn { + let Self(split, a, b, ..) = self; + let (area_a, area_b) = stack_areas(split, to, a, b)?; + let (drawn_a, drawn_b) = draw_stacks(split, to, a, area_a, None, b, area_b, None)?; + Ok(stack_drawn(split, drawn_a, drawn_b)) + } +} + +impl Split { + + /// ``` + /// use tengri::*; + /// let _ = Split::Above.stack(&"", &""); + /// let _ = Split::Below.stack(&"", &""); + /// let _ = Split::North.stack(&"", &""); + /// let _ = Split::South.stack(&"", &""); + /// let _ = Split::East.stack(&"", &""); + /// let _ = Split::West.stack(&"", &""); + /// ``` + pub const fn stack , B: Draw> (&self, a: A, b: B) -> impl Draw { + Pair(*self, a, b, PhantomData) + } + + /// ``` + /// 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 , B: Draw> (&self, a: &A, b: &B) -> impl Draw { + draw(move|to: &mut S|{ + let (area_a, area_b) = to.xywh().split_half(self); + let (origin_a, origin_b) = self.origins(); + let (drawn_a, drawn_b) = draw_stacks(self, to, a, area_a, origin_a, b, area_b, origin_b)?; + Ok(stack_drawn(self, drawn_a, drawn_b)) + }) + } + + /// 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!() + //} + +} + +fn draw_stacks ( + split: &Split, + to: &mut S, + a: impl Draw, + area_a: impl Into>>, + origin_a: impl Into>, + b: impl Draw, + area_b: impl Into>>, + origin_b: impl Into>, +) -> Usually<(Option>, Option>)> { + let draw_a = |to: &mut S|Ok::<_, Box>(if let Some(origin_a) = origin_a.into() { + to.draw(area_a.into(), a.align(origin_a))? + } else { + to.draw(area_a.into(), a)? + }); + let draw_b = |to: &mut S|Ok::<_, Box>(if let Some(origin_b) = origin_b.into() { + to.draw(area_b.into(), b.align(origin_b))? + } else { + to.draw(area_b.into(), b)? + }); + Ok(if matches!(split, Split::Below) { + let drawn_b = draw_b(to)?; + let drawn_a = draw_a(to)?; + (drawn_a, drawn_b) + } else { + (draw_a(to)?, draw_b(to)?) + }) +} + +pub fn stack_areas ( + split: &Split, + to: &mut S, + a: impl Draw, + b: impl Draw, +) -> Usually<(Option>, Option>)> { + let area_a = to.draw(None, a)?; + Ok(match split { + Split::South => ( + area_a, + area_a + .map(|used|to.size(XYWH(to.x(), to.y() + used.h(), to.w(), to.h().minus(used.h())), b)) + .transpose()? + .flatten() + ), + Split::East => ( + area_a, + area_a.map(|used|to.size(XYWH(to.x() + used.w(), to.y(), to.w().minus(used.w()), to.h()), b)) + .transpose()? + .flatten() + ), + Split::North => ( + area_a.map(|used|XYWH(used.x(), (to.y() + to.h()).minus(used.h()), used.w(), used.h())), + if let Some(used) = area_a { + to.size(XYWH(to.x(), to.y(), to.w(), to.h().minus(used.h())), b)? + } else { + to.size(None, b)? + .map(|area_b|XYWH(area_b.x(), area_b.y() + area_b.h(), area_b.w(), area_b.h())) + } + ), + Split::West => ( + area_a.map(|used|XYWH((to.x() + to.w()).minus(used.w()), used.y(), used.w(), used.h())), + if let Some(used) = area_a { + to.size(XYWH(to.x(), to.y(), to.w().minus(used.w()), to.h()), b)? + } else { + to.size(None, b)? + .map(|area_b|XYWH(area_b.x() + area_b.w(), area_b.y(), area_b.w(), area_b.h())) + } + ), + Split::Above | Split::Below => ( + area_a, + to.size(None, b)?, + ), + }) +} + +fn stack_drawn ( + split: &Split, + drawn_a: Option>, + drawn_b: Option>, +) -> Option> { + if let (Some(XYWH(xa, ya, wa, ha)), Some(XYWH(xb, yb, wb, hb))) = (drawn_a, drawn_b) { + match split { + Split::South => Some(XYWH(xa.min(xb), ya, wa.max(wb), ha + hb)), + Split::East => Some(XYWH(xa, ya.min(yb), wa + wb, ha.max(hb))), + Split::North => Some(XYWH(xa.min(xb), yb, wa.max(wb), ha + hb)), + Split::West => Some(XYWH(xb, ya.min(yb), wa + wb, ha.max(hb))), + Split::Above | Split::Below => + Some(XYWH(xa.min(xb), ya.min(yb), wa.max(wb), ha.max(hb))), + } + } else if let Some(a) = drawn_a { + Some(a) + } else if let Some(b) = drawn_b { + Some(b) + } else { + None + } +} + +#[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,)*)) }; +} + +pub const fn east , B: Draw> (a: A, b: B) -> impl Draw { + Pair(Split::East, a, b, PhantomData) +} + +pub const fn north , B: Draw> (a: A, b: B) -> impl Draw { + Pair(Split::North, a, b, PhantomData) +} + +pub const fn west , B: Draw> (a: A, b: B) -> impl Draw { + Pair(Split::West, a, b, PhantomData) +} + +pub const fn south , B: Draw> (a: A, b: B) -> impl Draw { + Pair(Split::South, a, b, PhantomData) +} + +pub const fn above , B: Draw> (a: A, b: B) -> impl Draw { + Pair(Split::Above, a, b, PhantomData) +} + +pub const fn below , B: Draw> (a: A, b: B) -> impl Draw { + Pair(Split::Below, a, b, PhantomData) +} + +#[cfg(test)] #[test] fn test_stack_areas () -> Usually<()> { + let area = XYWH(0u16, 0, 80, 25); + + assert_eq!(stack_areas(&Split::East, &mut Tui::Layout(area), &"foo", &"bar")?, ( + Some(XYWH(0u16, 0, 3, 1)), + Some(XYWH(3u16, 0, 3, 1)), + )); + + assert_eq!(stack_areas(&Split::South, &mut Tui::Layout(area), &"foo", &"bar")?, ( + Some(XYWH(0u16, 0, 3, 1)), + Some(XYWH(0u16, 1, 3, 1)), + )); + + Ok(()) +} + +#[cfg(test)] #[test] fn test_split_stack () -> Usually<()> { + use Split::*; + fn size_of , B: Draw> (stack: &Pair) -> Perhaps> { + Tui::Layout(XYWH(0, 0, 80, 25)).size(None, stack) + } + assert_eq!(size_of(&split(East, "foo", "bar"))?, + Some(XYWH(0, 0, 6, 1))); + assert_eq!(size_of(&split(South, "foo", "bar"))?, + Some(XYWH(0, 0, 3, 2))); + assert_eq!(size_of(&split(South, split(East, "foo", "bar"), "baz"))?, + Some(XYWH(0, 0, 6, 2))); + assert_eq!(size_of(&split(East, split(South, "foo", "bar"), "baz"))?, + Some(XYWH(0, 0, 6, 2))); + return Ok(()); +} + +#[cfg(test)] #[test] fn test_align () -> Usually<()> { + let mut screen = Tui::Layout(XYWH(0, 0, 80, 25)); + assert_eq!("FOOBAR\nKILROY".draw(&mut screen)?, Some(XYWH(0, 0, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_nw().draw(&mut screen)?, Some(XYWH(0, 0, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_n().draw(&mut screen)?, Some(XYWH(37, 0, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_ne().draw(&mut screen)?, Some(XYWH(74, 0, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_w().draw(&mut screen)?, Some(XYWH(0, 11, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_c().draw(&mut screen)?, Some(XYWH(37, 11, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_e().draw(&mut screen)?, Some(XYWH(74, 11, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_sw().draw(&mut screen)?, Some(XYWH(0, 23, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_s().draw(&mut screen)?, Some(XYWH(37, 23, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_se().draw(&mut screen)?, Some(XYWH(74, 23, 6, 2))); + Ok(()) +} diff --git a/src/layout/cond.rs b/src/layout/cond.rs new file mode 100644 index 0000000..a95d8f1 --- /dev/null +++ b/src/layout/cond.rs @@ -0,0 +1,46 @@ +use crate::*; + +fn_kw_layout!(kw_when |state, output, expr| { + Ok(matches!(expr.head()?, Some("when")).then(||{ + when(state.namespace(expr.tail().head())?.unwrap(), + draw(move|output: &mut O|{state.interpret(output, &expr.tail().tail().head())}) + ).draw(output) + }).transpose()?.flatten()) +}); + +/// Only render when condition is true. +/// +/// ``` +/// # use tengri::*; +/// # fn test () -> impl Draw { +/// when(true, "Yes") +/// # } +/// ``` +pub const fn when (condition: bool, item: impl Draw) -> impl Draw { + draw(move|to: &mut T|if condition { item.draw(to) } else { Ok(Default::default()) }) +} + +fn_kw_layout!(kw_either |state, output, expr| { + Ok(matches!(expr.head()?, Some("either")).then(||{ + either(state.namespace(expr.tail().head()?)?.unwrap(), + draw(move|output: &mut O|{ + state.interpret(output, &expr.tail().tail().head()?) + }), + draw(move|output: &mut O|{ + state.interpret(output, &expr.tail().tail().tail().head()?) + }), + ).draw(output) + }).transpose()?.flatten()) +}); + +/// 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 { + draw(move|to: &mut T|if condition { a.draw(to) } else { b.draw(to) }) +} diff --git a/src/layout/exact.rs b/src/layout/exact.rs deleted file mode 100644 index e94f51d..0000000 --- a/src/layout/exact.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::*; - -/// 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 , X: Into> + Copy> Draw for Exact { - fn layout (&self, area: XYWH) -> Perhaps> { - Ok(Some(layout_exact(self, area))) - } - fn draw (self, to: &mut S) -> Perhaps> { - let area = layout_exact(&self, to.area()); - let item = match self { Self::W(i, ..) => i, Self::H(i, ..) => i, Self::WH(i, ..) => i, _ => unreachable!() }; - to.clip(area, |to|item.draw(to)) - } -} - -fn layout_exact , X: Into> + Copy> ( - exact: &Exact, area: XYWH -) -> XYWH { - let (w, h): (S::Unit, S::Unit) = match exact { - Exact::W(_, w) => { - let w: Option = (*w).into(); - (w.unwrap_or(area.2), area.3) - }, - Exact::H(_, h) => { - let h: Option = (*h).into(); - (area.2, h.unwrap_or(area.3)) - }, - Exact::WH(_, w, h) => { - let w: Option = (*w).into(); - let h: Option = (*h).into(); - (w.unwrap_or(area.2), w.unwrap_or(area.3)) - }, - _ => unreachable!() - }; - XYWH(area.0, area.1, w, h) -} diff --git a/src/layout/full.rs b/src/layout/full.rs deleted file mode 100644 index 5dbd40f..0000000 --- a/src/layout/full.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::*; - -/// Use whole drawing area along one or both axes. -/// -/// ``` -/// # fn doctest_layout_full () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(0u16, 0, 80, 25); -/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); -/// assert_eq!("1".full_w().layout(area)?, Some(XYWH(0u16, 0, 80, 1))); -/// assert_eq!("1".full_h().layout(area)?, Some(XYWH(0u16, 0, 1, 25))); -/// assert_eq!("1".full_wh().layout(area)?, Some(XYWH(0u16, 0, 80, 25))); -/// # Ok(()) } -/// ``` -pub enum Full> { - __(PhantomData), - W(I), - H(I), - WH(I), -} -impl_draw!(,>|self: Full, to: T|{ - let XYWH(x0, y0, w0, h0) = to.area(); - match self { - Self::W(item) => if let Some(XYWH(_, y, _, h)) = item.layout(to.area())? { - to.clip(XYWH(x0, y, w0, h), |to|item.draw(to)) - } else { - Ok(None) - }, - Self::H(item) => if let Some(XYWH(x, _, w, _)) = item.layout(to.area())? { - to.clip(XYWH(x, y0, w, h0), |to|item.draw(to)) - } else { - Ok(None) - }, - Self::WH(item) => if let Some(XYWH(..)) = item.layout(to.area())? { - to.clip(XYWH(x0, y0, w0, h0), |to|item.draw(to)) - } else { - Ok(None) - }, - _ => unreachable!(), - } -}); diff --git a/src/layout/max.rs b/src/layout/max.rs deleted file mode 100644 index 9fc1c40..0000000 --- a/src/layout/max.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::*; - -/// Set maximum size of of drawing area. -/// -/// ``` -/// # fn doctest_layout_max () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(1u16, 1, 80, 25); -/// assert_eq!("12345".max_w(1).layout(area)?, Some(XYWH(1u16, 1, 1, 1))); -/// assert_eq!("12345".max_h(1).layout(area)?, Some(XYWH(1u16, 1, 1, 1))); -/// assert_eq!("12345".max_wh(1, 1).layout(area)?, Some(XYWH(1u16, 1, 5, 1))); -/// # Ok(()) } -/// ``` -pub enum Max, X: Into> + Copy> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl, X: Into> + Copy> Draw for Max { - fn layout (&self, area: XYWH) -> Perhaps> { - Ok(Some(match self { - Self::W(_, max_w) => XYWH( - area.0, area.1, (*max_w).into().map(|max|max.min(area.2)).unwrap_or(area.2), - area.3), - Self::H(_, max_h) => XYWH( - area.0, area.1, area.2, - (*max_h).into().map(|max|max.min(area.3)).unwrap_or(area.3)), - Self::WH(_, max_w, max_h) => XYWH( - area.0, area.1, (*max_w).into().map(|max|max.min(area.2)).unwrap_or(area.2), - (*max_h).into().map(|max|max.min(area.3)).unwrap_or(area.3)), - _ => return Ok(None) - })) - } - fn draw (self, to: &mut T) -> Perhaps> { - let area: XYWH = to.area(); - let (item, area) = match self { - Self::W(item, max_w) => (item, XYWH( - area.0, area.1, max_w.into().map(|max|max.min(area.2)).unwrap_or(area.2), - area.3 - )), - Self::H(item, max_h) => (item, XYWH( - area.0, area.1, area.2, - max_h.into().map(|max|max.min(area.3)).unwrap_or(area.3) - )), - Self::WH(item, max_w, max_h) => (item, XYWH( - area.0, area.1, max_w.into().map(|max|max.min(area.2)).unwrap_or(area.2), - max_h.into().map(|max|max.min(area.3)).unwrap_or(area.3), - )), - _ => return Ok(None) - }; - to.clip(area, |to|item.draw(to)) - } -} diff --git a/src/layout/min.rs b/src/layout/min.rs deleted file mode 100644 index d747253..0000000 --- a/src/layout/min.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::*; - -/// Only draw content if area is above a certain size. -/// -/// ``` -/// # fn doctest_layout_min () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(1u16, 1, 80, 25); -/// assert_eq!("1".min_w(5).layout(area)?, Some(XYWH(1u16, 1, 5, 1))); -/// assert_eq!("1".min_h(5).layout(area)?, Some(XYWH(1u16, 1, 1, 5))); -/// assert_eq!("1".min_wh(5, 5).layout(area)?, Some(XYWH(1u16, 1, 5, 5))); -/// assert_eq!("123456".min_w(5).layout(area)?, Some(XYWH(1u16, 1, 6, 1))); -/// # Ok(()) } -/// ``` -pub enum Min, X: Into>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Min, to: T|{ - match self { - Self::__(_) => unreachable!(), - Self::W(item, w1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - let w = w1.into().map(|w1|w.max(w1)).unwrap_or(w); - to.clip(XYWH(x, y, w, h), |to|item.draw(to)) - }, - Self::H(item, h1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - let h = h1.into().map(|h1|h.max(h1)).unwrap_or(h); - to.clip(XYWH(x, y, w, h), |to|item.draw(to)) - }, - Self::WH(item, w1, h1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - let w = w1.into().map(|w1|w.max(w1)).unwrap_or(w); - let h = h1.into().map(|h1|h.max(h1)).unwrap_or(h); - to.clip(XYWH(x, y, w, h), |to|item.draw(to)) - }, - _ => Ok(None) - } -}); diff --git a/src/layout/origin.rs b/src/layout/origin.rs deleted file mode 100644 index e5aaff9..0000000 --- a/src/layout/origin.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::*; - -pub struct Origin( - pub(crate) Option, - pub(crate) 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/layout/pad.rs b/src/layout/pad.rs deleted file mode 100644 index 6ac2b4a..0000000 --- a/src/layout/pad.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::*; - -/// 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|{ - let area = to.area(); - let (item, area) = match self { - Self::W(item, w1) => { - let w1 = w1.into().unwrap_or(T::Unit::zero()); - (item, XYWH(area.0 + w1, area.1, area.2.minus(w1 + w1), area.3)) - }, - Self::H(item, h1) => { - let h1 = h1.into().unwrap_or(T::Unit::zero()); - (item, XYWH(area.0, area.1 + h1, area.2, area.3.minus(h1 + h1))) - }, - Self::WH(item, w1, h1) => { - let w1 = w1.into().unwrap_or(T::Unit::zero()); - let h1 = h1.into().unwrap_or(T::Unit::zero()); - (item, XYWH(area.0 + w1, area.1 + h1, area.2.minus(w1 + w1), area.3.minus(h1 + h1))) - }, - _ => return Ok(None) - }; - item.draw(to) -}); diff --git a/src/layout/pull.rs b/src/layout/pull.rs deleted file mode 100644 index 287a998..0000000 --- a/src/layout/pull.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::*; - -/// Move content in the negative direction of one or both axes. -/// -/// ``` -/// # fn doctest_layout_pull () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(1u16, 1, 80, 25); -/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); -/// assert_eq!("1".pull_x(1).layout(area)?, Some(XYWH(0u16, 1, 1, 1))); -/// assert_eq!("1".pull_y(1).layout(area)?, Some(XYWH(1u16, 0, 1, 1))); -/// assert_eq!("1".pull_xy(1, 1).layout(area)?, Some(XYWH(0u16, 0, 1, 1))); -/// # Ok(()) } -/// ``` -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!() -}); diff --git a/src/layout/push.rs b/src/layout/push.rs deleted file mode 100644 index 7d1114d..0000000 --- a/src/layout/push.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::*; - -/// Move content in the positive direction of one or both axes. -/// -/// ``` -/// # fn doctest_layout_push () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(0u16, 0, 80, 25); -/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); -/// assert_eq!("1".push_x(1).layout(area)?, Some(XYWH(1u16, 0, 1, 1))); -/// assert_eq!("1".push_y(1).layout(area)?, Some(XYWH(0u16, 1, 1, 1))); -/// assert_eq!("1".push_xy(1, 1).layout(area)?, Some(XYWH(1u16, 1, 1, 1))); -/// # Ok(()) } -/// ``` -pub enum Push, X: Into>> { - __(PhantomData), - X(I, X), - Y(I, X), - XY(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Push, to: T|{ - match self { - Self::__(_) => unreachable!(), - Self::X(item, x1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - to.clip(XYWH( - x + x1.into().unwrap_or_default(), y, w, h - ), |to|item.draw(to)) - }, - Self::Y(item, y1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - to.clip(XYWH( - x, y + y1.into().unwrap_or_default(), w, h - ), |to|item.draw(to)) - }, - Self::XY(item, x1, y1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - to.clip(XYWH( - x + x1.into().unwrap_or_default(), y + y1.into().unwrap_or_default(), w, h - ), |to|item.draw(to)) - }, - _ => Ok(None) - } -}); diff --git a/src/layout/split.rs b/src/layout/split.rs deleted file mode 100644 index 555c75a..0000000 --- a/src/layout/split.rs +++ /dev/null @@ -1,315 +0,0 @@ -use super::*; - -/// 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 -} - -pub struct Pair, B: Draw>(Split, A, B, PhantomData); - -pub fn split , B: Draw> ( - split: Split, a: A, b: B -) -> Pair { - Pair(split, a, b, PhantomData) -} - -impl, B: Draw> Draw for Pair { - fn layout (&self, to: XYWH) -> Drawn { - let Self(split, a, b, ..) = self; - let (area_a, area_b) = stack_areas(split, to, a, b)?; - Ok(stack_drawn(split, area_a, area_b)) - } - fn draw (self, to: &mut S) -> Drawn { - let Self(ref split, a, b, ..) = self; - let (area_a, area_b) = stack_areas(split, to.area(), &a, &b)?; - let (drawn_a, drawn_b) = draw_stacks(split, to, a, area_a, None, b, area_b, None)?; - Ok(stack_drawn(split, drawn_a, drawn_b)) - } -} - -impl Split { - - /// ``` - /// use tengri::*; - /// let _ = Split::Above.stack("", ""); - /// let _ = Split::Below.stack("", ""); - /// let _ = Split::North.stack("", ""); - /// let _ = Split::South.stack("", ""); - /// let _ = Split::East.stack("", ""); - /// let _ = Split::West.stack("", ""); - /// ``` - pub const fn stack , B: Draw> (&self, a: A, b: B) -> impl Draw { - Pair(*self, a, b, PhantomData) - } - - /// ``` - /// 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 , B: Draw> (&self, a: A, b: B) -> impl Draw { - draw(move|to: &mut S|{ - let (area_a, area_b) = to.xywh().split_half(self); - let (origin_a, origin_b) = self.origins(); - let (drawn_a, drawn_b) = draw_stacks(self, to, a, area_a, origin_a, b, area_b, origin_b)?; - Ok(stack_drawn(self, drawn_a, drawn_b)) - }) - } - - /// 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!() - //} - -} - -fn draw_stacks ( - split: &Split, - to: &mut S, - a: impl Draw, - area_a: impl Into>>, - origin_a: impl Into>, - b: impl Draw, - area_b: impl Into>>, - origin_b: impl Into>, -) -> Usually<(Option>, Option>)> { - let draw_a = |to: &mut S|Ok::<_, Box>(if let Some(origin_a) = origin_a.into() { - to.clip(area_a.into(), |to|a.align(origin_a).draw(to))? - } else { - to.clip(area_a.into(), |to|a.draw(to))? - }); - let draw_b = |to: &mut S|Ok::<_, Box>(if let Some(origin_b) = origin_b.into() { - to.clip(area_b.into(), |to|b.align(origin_b).draw(to))? - } else { - to.clip(area_b.into(), |to|b.draw(to))? - }); - Ok(if matches!(split, Split::Below) { - let drawn_b = draw_b(to)?; - let drawn_a = draw_a(to)?; - (drawn_a, drawn_b) - } else { - (draw_a(to)?, draw_b(to)?) - }) -} - -pub fn stack_areas ( - split: &Split, - area: XYWH, - a: &impl Draw, - b: &impl Draw, -) -> Usually<(Option>, Option>)> { - let area_a = a.layout(area)?; - Ok(match split { - Split::South => ( - area_a, - if let Some(used) = area_a { - b.layout(XYWH(area.x(), area.y() + used.h(), area.w(), area.h().minus(used.h())))? - } else { - None - } - ), - Split::East => ( - area_a, - if let Some(used) = area_a { - b.layout(XYWH(area.x() + used.w(), area.y(), area.w().minus(used.w()), area.h()))? - } else { - None - } - ), - Split::North => ( - if let Some(used) = area_a { - Some(XYWH(used.x(), (area.y() + area.h()).minus(used.h()), used.w(), used.h())) - } else { - None - }, - if let Some(used) = area_a { - b.layout(XYWH(area.x(), area.y(), area.w(), area.h().minus(used.h())))? - } else { - b.layout(area)?.map(|area_b|XYWH( - area_b.x(), area_b.y() + area_b.h(), area_b.w(), area_b.h() - )) - } - ), - Split::West => ( - if let Some(used) = area_a { - Some(XYWH((area.x() + area.w()).minus(used.w()), used.y(), used.w(), used.h())) - } else { - None - }, - if let Some(used) = area_a { - b.layout(XYWH(area.x(), area.y(), area.w().minus(used.w()), area.h()))? - } else { - b.layout(area)?.map(|area_b|XYWH( - area_b.x() + area_b.w(), area_b.y(), area_b.w(), area_b.h() - )) - } - ), - Split::Above | Split::Below => ( - area_a, - b.layout(area)?, - ), - }) -} - -fn stack_drawn ( - split: &Split, - drawn_a: Option>, - drawn_b: Option>, -) -> Option> { - if let (Some(XYWH(xa, ya, wa, ha)), Some(XYWH(xb, yb, wb, hb))) = (drawn_a, drawn_b) { - match split { - Split::South => Some(XYWH(xa.min(xb), ya, wa.max(wb), ha + hb)), - Split::East => Some(XYWH(xa, ya.min(yb), wa + wb, ha.max(hb))), - Split::North => Some(XYWH(xa.min(xb), yb, wa.max(wb), ha + hb)), - Split::West => Some(XYWH(xb, ya.min(yb), wa + wb, ha.max(hb))), - Split::Above | Split::Below => - Some(XYWH(xa.min(xb), ya.min(yb), wa.max(wb), ha.max(hb))), - } - } else if let Some(a) = drawn_a { - Some(a) - } else if let Some(b) = drawn_b { - Some(b) - } else { - None - } -} - -#[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,)*)) }; -} - -pub const fn east , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::East, a, b, PhantomData) -} - -pub const fn north , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::North, a, b, PhantomData) -} - -pub const fn west , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::West, a, b, PhantomData) -} - -pub const fn south , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::South, a, b, PhantomData) -} - -pub const fn above , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::Above, a, b, PhantomData) -} - -pub const fn below , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::Below, a, b, PhantomData) -} - -#[cfg(test)] mod test { - use crate::*; - - #[test] fn test_stack_areas () -> Usually<()> { - let area = XYWH(0u16, 0, 80, 25); - - assert_eq!(stack_areas(&Split::East, area, &"foo", &"bar")?, ( - Some(XYWH(0u16, 0, 3, 1)), - Some(XYWH(3u16, 0, 3, 1)), - )); - - assert_eq!(stack_areas(&Split::South, area, &"foo", &"bar")?, ( - Some(XYWH(0u16, 0, 3, 1)), - Some(XYWH(0u16, 1, 3, 1)), - )); - - Ok(()) - } - - #[test] fn test_split_stack () -> Usually<()> { - use tengri::{*, Split::*}; - - let stack = split(East, "foo", "bar"); - assert_eq!(stack.layout(XYWH(0, 0, 80, 25))?, Some(XYWH(0, 0, 6, 1))); - - let stack = split(South, "foo", "bar"); - assert_eq!(stack.layout(XYWH(0, 0, 80, 25))?, Some(XYWH(0, 0, 3, 2))); - - let stack = split(South, split(East, "foo", "bar"), "baz"); - assert_eq!(stack.layout(XYWH(0, 0, 80, 25))?, Some(XYWH(0, 0, 6, 2))); - - let stack = split(East, split(South, "foo", "bar"), "baz"); - assert_eq!(stack.layout(XYWH(0, 0, 80, 25))?, Some(XYWH(0, 0, 6, 2))); - - Ok(()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 3a5342e..119bd52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,6 @@ pub extern crate unicode_width; #[cfg(test)] pub(crate) use proptest_derive::Arbitrary; pub(crate) use ::{ - atomic_float::AtomicF64, std::fmt::{Debug, Display}, std::ops::{Add, Sub, Mul, Div}, std::sync::{Arc, RwLock}, @@ -30,19 +29,10 @@ pub(crate) use ::{ std::marker::PhantomData }; -macro_rules! features { - ($($feature:literal: [ $($module:ident),* ]),*) => { - $( - $( - #[cfg(feature = $feature)] pub mod $module; - #[cfg(feature = $feature)] pub use $module::*; - )* - )* - } -} - +#[cfg(all(feature = "draw", feature = "eval"))] macro_rules! fn_kw_layout { ($name:ident |$state:ident, $output: ident, $expr:ident| $body:block) => { + #[cfg(all(feature = "draw", feature = "eval"))] pub fn $name ( $state: &S, $output: &mut O, $expr: &str ) -> Perhaps> where @@ -54,8 +44,10 @@ macro_rules! fn_kw_layout { } } +#[cfg(all(feature = "draw", feature = "eval", feature = "term"))] macro_rules! fn_kw_layout_tui { ($name:ident |$state:ident, $output: ident, $expr:ident| $body:block) => { + #[cfg(all(feature = "draw", feature = "eval", feature = "term"))] pub fn $name ( $state: &S, $output: &mut Tui, $expr: &str ) -> Perhaps> where @@ -71,11 +63,6 @@ macro_rules! fn_kw_layout_tui { #[cfg(feature = "lang")] pub use ::dizzle::{Usually, Perhaps}; #[cfg(feature = "lang")] use ::dizzle::*; -features! { - "time": [ time ], - "sing": [ sing ] -} - /// Define a trait an implement it for various mutation-enabled wrapper types. */ #[macro_export] macro_rules! flex_trait_mut ( ($Trait:ident $(<$($A:ident:$T:ident),+>)? { @@ -291,6 +278,1002 @@ macro_rules! eval_xy ( } } +#[cfg(feature = "time")] pub use self::time::*; +#[cfg(feature = "time")] mod time { + use ::std::sync::atomic::Ordering::*; + use ::atomic_float::AtomicF64; + + /// Performance counter + #[derive(Debug)] + pub struct PerfModel { + pub clock: quanta::Clock, + /// Measurement has a small cost. Disable it here. + pub enabled: bool, + // In nanoseconds. Time used by last iteration. + pub used: AtomicF64, + // In microseconds. Max prescribed time for iteration (frame, chunk...). + pub window: AtomicF64, + } + + impl_default!(PerfModel: Self { + enabled: true, + clock: quanta::Clock::new(), + used: Default::default(), + window: Default::default(), + }); + + impl PerfModel { + pub fn get_t0 (&self) -> Option { + if self.enabled { + Some(self.clock.raw()) + } else { + None + } + } + pub fn get_t1 (&self, t0: Option) -> Option { + if let Some(t0) = t0 { + if self.enabled { + Some(self.clock.delta(t0, self.clock.raw())) + } else { + None + } + } else { + None + } + } + pub fn update (&self, t0: Option, microseconds: f64) { + 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(microseconds, Relaxed,); + } + } + pub fn percentage (&self) -> Option { + let window = self.window.load(Relaxed) * 1000.0; + if window > 0.0 { + let used = self.used.load(Relaxed); + Some(100.0 * used / window) + } else { + None + } + } + pub fn cycle T, T> (&self, call: &mut F) -> T { + let t0 = self.get_t0(); + let result = call(self); + let _t1 = self.get_t1(t0).unwrap(); + result + } + } +} + +#[cfg(feature = "sing")] pub use self::sing::*; +#[cfg(feature = "sing")] mod sing { + use crate::{*, time::PerfModel}; + pub use ::jack::{*, contrib::{TimebaseInfo, ClosureProcessHandler, PositionBBT, Position as JackPosition}}; + pub use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; + use ConnectName::*; + use ConnectScope::*; + use ConnectStatus::*; + 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))))) + } + /// 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()), + } + } + 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) + } + } + + 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() + } + } + + /// 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 + } + } + + 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, + ); + } + } + } + + /// Trait for thing that has a JACK process callback. + pub trait Audio { + /// Handle a JACK event. + fn handle (&mut self, _event: JackEvent) {} + /// Projecss a JACK chunk. + fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { + Control::Continue + } + /// The JACK process callback function passed to the server. + fn callback ( + state: &Arc>, client: &Client, scope: &ProcessScope + ) -> Control where Self: Sized { + if let Ok(mut state) = state.write() { + state.process(client, scope) + } else { + Control::Quit + } + } + } + + /// 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>; + + /// Things that can provide a [jack::Client] reference. + /// + /// ``` + /// use tengri::*; + /// + /// let jack: &Jack = Jacked::default().jack(); + /// + /// #[derive(Default)] struct Jacked<'j>(Jack<'j>); + /// + /// impl<'j> HasJack<'j> for Jacked<'j> { + /// fn jack (&self) -> &Jack<'j> { &self.0 } + /// } + /// ``` + pub trait HasJack<'j>: Send + Sync { + /// Return the internal [jack::Client] handle + /// that lets you call the JACK API. + fn jack (&self) -> &Jack<'j>; + fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { + self.jack().with_client(op) + } + fn port_by_name (&self, name: &str) -> Option> { + self.with_client(|client|client.port_by_name(name)) + } + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + fn register_port (&self, name: impl AsRef) -> Usually> { + self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?)) + } + fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->jack::contrib::Position) + -> Usually<()> + { + if enable { + self.with_client(|client|match client.register_timebase_callback(false, callback) { + Ok(_) => Ok(()), + Err(e) => Err(e) + })? + } + Ok(()) + } + fn sync_follow (&self, _enable: bool) -> Usually<()> { + // TODO: sync follow + Ok(()) + } + } + + /// Implement [Audio]: provide JACK callbacks. + #[macro_export] macro_rules! impl_audio { + (| + $self1:ident: + $Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident + |$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => { + impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { + #[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb } + $(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })? + } + }; + + ($Struct:ident: $process:ident, $handle:ident) => { + impl Audio for $Struct { + #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { + $process(self, c, s) + } + #[inline] fn handle (&mut self, e: JackEvent) { + $handle(self, e) + } + } + }; + + ($Struct:ident: $process:ident) => { + impl Audio for $Struct { + #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { + $process(self, c, s) + } + } + }; + } + + pub trait JackPorts: HasJack<'static> { + /// Register a MIDI input port. + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register a MIDI output port. + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio input port. + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio output port. + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + } + + impl> JackPorts for J { + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiInput::new(self.jack(), name, connect) + } + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiOutput::new(self.jack(), name, connect) + } + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioInput::new(self.jack(), name, connect) + } + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioOutput::new(self.jack(), name, connect) + } + } + + pub trait JackPort: HasJack<'static> { + const KIND: &'static str = "Port"; + type Port: PortSpec + Default; + type Pair: PortSpec + Default; + + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized; + + fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { + jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) + .map_err(|e|e.into()) + } + + fn close (self) -> Usually<()> where Self: Sized { + let jack = self.jack().clone(); + Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) + } + + fn into_port (self) -> Port where Self: Sized; + fn port_name (&self) -> &Arc; + fn port (&self) -> &Port; + fn port_mut (&mut self) -> &mut Port; + fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { + self.with_client(|c|c.ports(re_name, re_type, flags)) + } + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + fn port_by_name (&self, name: impl AsRef) -> Option> { + self.with_client(|c|c.port_by_name(name.as_ref())) + } + + fn connections (&self) -> &[Connect]; + fn connect_to_matching <'k> (&'k self) -> Usually<()> { + for connect in self.connections().iter() { + match &connect.name { + Some(Exact(name)) => { + *connect.status.write().unwrap() = self.connect_exact(name)?; + }, + Some(RegExp(re)) => { + *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; + }, + _ => {}, + }; + } + Ok(()) + } + fn connect_exact <'k> (&'k self, name: &str) -> + Usually, Arc, ConnectStatus)>> + { + self.with_client(move|c|{ + let mut status = vec![]; + for port in c.ports(None, None, PortFlags::empty()).iter() { + if port.as_str() == &*name { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected { + break + } + } + } + } + Ok(status) + }) + } + fn connect_regexp <'k> ( + &'k self, re: &str, scope: Option + ) -> Usually, Arc, ConnectStatus)>> { + self.with_client(move|c|{ + let mut status = vec![]; + let ports = c.ports(Some(&re), None, PortFlags::empty()); + for port in ports.iter() { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected && scope == Some(One) { + break + } + } + } + Ok(status) + }) + } + /** Connect to a matching port by name. */ + fn connect_to_name (&self, name: impl AsRef) -> Usually { + self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { + self.connect_to_unowned(port) + } else { + Ok(Missing) + }) + } + /** Connect to a matching port by reference. */ + fn connect_to_unowned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + /** Connect to an owned matching port by reference. */ + fn connect_to_owned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + } + + /// Audio input port. + #[derive(Debug)] pub struct AudioInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, + } + + /// Audio output port. + #[derive(Debug)] pub struct AudioOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, + } + + /// MIDI input port. + #[derive(Debug)] pub struct MidiInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of currently held notes. + pub held: Arc>, + /// List of ports to connect to. + pub connections: Vec, + } + + /// MIDI output port. + #[derive(Debug)] pub struct MidiOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of currently held notes. + pub held: Arc>, + /// List of ports to connect to. + pub connections: Vec, + /// Buffer + pub note_buffer: Vec, + /// Buffer + pub output_buffer: Vec>>, + } + + macro_rules! jack_port { + ($($Struct:ty = ($Port:ty => $Pair:ty) $({ $($tt:tt)* })?),*) => { + $( + impl HasJack<'static> for $Struct { + fn jack (&self) -> &Jack<'static> { &self.jack } + } + impl JackPort for $Struct { + type Port = $Port; + type Pair = $Pair; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + $($($tt)*)? + } + )* + }; + } + + jack_port!( + AudioInput = (AudioIn => AudioOut) { + const KIND: &'static str = "Audio In"; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + }; + port.connect_to_matching()?; + Ok(port) + } + }, + + AudioOutput = (AudioOut => AudioIn) { + const KIND: &'static str = "Audio Out"; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + }; + port.connect_to_matching()?; + Ok(port) + } + }, + + MidiInput = (MidiIn => MidiOut) { + const KIND: &'static str = "MIDI In"; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + held: Arc::new(RwLock::new([false;128])) + }; + port.connect_to_matching()?; + Ok(port) + } + }, + + MidiOutput = (MidiOut => MidiIn) { + const KIND: &'static str = "MIDI Out"; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self::register(jack, name)?; + let jack = jack.clone(); + let name = name.as_ref().into(); + let connections = connect.to_vec(); + let port = Self { + jack, + port, + name, + connections, + held: Arc::new([false;128].into()), + note_buffer: vec![0;8], + output_buffer: vec![vec![];65536], + }; + port.connect_to_matching()?; + Ok(port) + } + } + ); + + pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; + + /// Trait for thing that may receive MIDI. + pub trait HasMidiIns { + fn midi_ins (&self) -> &Vec; + fn midi_ins_mut (&mut self) -> &mut Vec; + /// Collect MIDI input from app ports (TODO preallocate large buffers) + fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { + self.midi_ins().iter() + .map(|port|port.port().iter(scope) + .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) + .collect::>()) + .collect::>() + } + fn midi_ins_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_ins().iter().enumerate().map(move|(i, input)|{ + let height = 1 + input.connections().len(); + let data = (i, input.port_name(), input.connections(), y, y + height); + y += height; + data + }) + } + } + /// Trait for thing that may output MIDI. + pub trait HasMidiOuts { + fn midi_outs (&self) -> &Vec; + fn midi_outs_mut (&mut self) -> &mut Vec; + fn midi_outs_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_outs().iter().enumerate().map(move|(i, output)|{ + let height = 1 + output.connections().len(); + let data = (i, output.port_name(), output.connections(), y, y + height); + y += height; + data + }) + } + fn midi_outs_emit (&mut self, scope: &ProcessScope) { + for port in self.midi_outs_mut().iter_mut() { + port.buffer_emit(scope) + } + } + } + + impl MidiOutput { + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); + for frame in &mut self.output_buffer[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(&mut self.output_buffer); + } + } + /// Write a note to the output buffer + pub fn buffer_write <'a> ( + &'a mut self, + sample: usize, + event: LiveEvent, + ) { + self.note_buffer.fill(0); + event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); + self.output_buffer[sample].push(self.note_buffer.clone()); + // Update the list of currently held notes. + if let LiveEvent::Midi { ref message, .. } = event { + update_keys(&mut*self.held.write().unwrap(), message); + } + } + /// Write a chunk of MIDI data from the output buffer to the output port. + pub fn buffer_emit (&mut self, scope: &ProcessScope) { + let samples = scope.n_frames() as usize; + let mut writer = self.port.writer(scope); + for (time, events) in self.output_buffer.iter().enumerate().take(samples) { + for bytes in events.iter() { + writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ + panic!("Failed to write MIDI data: {bytes:?}"); + }); + } + } + } + } + + impl MidiInput { + pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { + parse_midi_input(self.port().iter(scope)) + } + } + + /// Return boxed iterator of MIDI events + pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) + -> Box, &'a [u8])> + 'a> + { + Box::new(input.map(|::jack::RawMidi { time, bytes }|( + time as usize, + LiveEvent::parse(bytes).unwrap(), + bytes + ))) + } + + /// Add "all notes off" to the start of a buffer. + pub fn all_notes_off (output: &mut [Vec>]) { + let mut buf = vec![]; + let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; + let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; + evt.write(&mut buf).unwrap(); + output[0].push(buf); + } + + /// Update notes_in array + pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { + match message { + MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } + MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, + _ => {} + } + } + + impl> + AsMut>> HasMidiIns for T { + fn midi_ins (&self) -> &Vec { self.as_ref() } + fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } + } + + impl> + AsMut>> HasMidiOuts for T { + fn midi_outs (&self) -> &Vec { self.as_ref() } + fn midi_outs_mut (&mut self) -> &mut Vec { self.as_mut() } + } + + impl> AddMidiIn for T { + fn midi_in_add (&mut self) -> Usually<()> { + let index = self.midi_ins().len(); + let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; + self.midi_ins_mut().push(port); + Ok(()) + } + } + + /// Trail for thing that may gain new MIDI ports. + impl> AddMidiOut for T { + fn midi_out_add (&mut self) -> Usually<()> { + let index = self.midi_outs().len(); + let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; + self.midi_outs_mut().push(port); + Ok(()) + } + } + + /// May create new MIDI input ports. + pub trait AddMidiIn { + fn midi_in_add (&mut self) -> Usually<()>; + } + + /// May create new MIDI output ports. + pub trait AddMidiOut { + fn midi_out_add (&mut self) -> Usually<()>; + } + + #[derive(Clone, Debug, PartialEq)] pub enum ConnectName { + /** Exact match */ + Exact(Arc), + /** Match regular expression */ + RegExp(Arc), + } + + #[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { + One, + All + } + + #[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { + Missing, + Disconnected, + Connected, + Mismatch, + } + + /// Port connection manager. + /// + /// ``` + /// let connect = tengri::Connect::default(); + /// ``` + #[derive(Clone, Debug, Default)] + pub struct Connect { + pub name: Option, + pub scope: Option, + pub status: Arc, Arc, ConnectStatus)>>>, + pub info: Arc, + } + + impl Connect { + pub fn new > ( + exact: Option>, + re: Option>, + re_all: Option>, + ) -> Vec { + let mut connections = vec![]; + if let Some(exact ) = exact { for port in exact { connections.push(Self::exact(port)) } } + if let Some(regexp) = re { for port in regexp { connections.push(Self::regexp(port)) } } + if let Some(re_all) = re_all { for port in re_all { connections.push(Self::regexp_all(port)) } } + connections + } + + /// Connect to this exact port + pub fn exact (name: impl AsRef) -> Self { + let info = format!("=:{}", name.as_ref()).into(); + let name = Some(Exact(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + + pub fn regexp (name: impl AsRef) -> Self { + let info = format!("~:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + + pub fn regexp_all (name: impl AsRef) -> Self { + let info = format!("+:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } + } + + pub fn info (&self) -> Arc { + format!(" ({}) {} {}", { + let status = self.status.read().unwrap(); + let mut ok = 0; + for (_, _, state) in status.iter() { + if *state == Connected { + ok += 1 + } + } + format!("{ok}/{}", status.len()) + }, match self.scope { + None => "x", + Some(One) => " ", + Some(All) => "*", + }, match &self.name { + None => format!("x"), + Some(Exact(name)) => format!("= {name}"), + Some(RegExp(name)) => format!("~ {name}"), + }).into() + } + } + + pub fn connect_midi_ins > ( + jack: &Jack<'static>, + name: &T, + midi_from: &[T], + midi_from_re: Option<&[T]>, + ) -> Usually> { + Ok(Connect::new( + Some(midi_from.into_iter()), + Some([].into_iter()), + midi_from_re.map(|x|x.into_iter())).iter().enumerate() + .map(|(index, connect)|jack.midi_in(&format!("{}/{index}", name.as_ref()), &[connect.clone()])) + .collect::>()?) + } + + pub fn connect_midi_outs > ( + jack: &Jack<'static>, + name: &T, + midi_to: &[T], + midi_to_re: Option<&[T]>, + ) -> Usually> { + Ok(Connect::new( + Some(midi_to.into_iter()), + Some([].into_iter()), + midi_to_re.map(|x|x.into_iter())).iter().enumerate() + .map(|(index, connect)|jack.midi_out(&format!("{index}/{}", name.as_ref()), &[connect.clone()])) + .collect::>()?) + } + + pub fn connect_audio_ins > ( + jack: &Jack<'static>, + name: &T, + audio_from: &[T], + audio_from_re: Option<&[T]>, + ) -> Usually> { + Ok(Connect::new( + Some(audio_from.into_iter()), + Some([].into_iter()), + audio_from_re.map(|x|x.into_iter())).iter().enumerate() + .map(|(index, connect)|jack.audio_in(&format!("{}/{index}", name.as_ref()), &[connect.clone()])) + .collect::>()?) + } + + pub fn connect_audio_outs > ( + jack: &Jack<'static>, + name: &T, + audio_to: &[T], + audio_to_re: Option<&[T]>, + ) -> Usually> { + Ok(Connect::new( + Some(audio_to.into_iter()), + Some([].into_iter()), + audio_to_re.map(|x|x.into_iter())).iter().enumerate() + .map(|(index, connect)|jack.audio_out(&format!("{index}/{}", name.as_ref()), &[connect.clone()])) + .collect::>()?) + } +} + #[cfg(feature = "play")] pub use self::task::*; #[cfg(feature = "play")] mod task { use std::{ @@ -366,52 +1349,29 @@ macro_rules! eval_xy ( use Azimuth::*; use Split::*; - /// 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, _: impl Draw) -> Perhaps> { - /// println!("placed"); - /// Ok(None) - /// } - /// fn area (&self) -> XYWH { - /// Default::default() - /// } - /// fn clip ( - /// &mut self, - /// area: impl Into>>, - /// draw: impl FnOnce(&mut Self)->T - /// ) -> T { - /// draw(self) - /// } - /// } - /// - /// impl_draw!(|self: String, to: TestOut|{ - /// to.w = self.len() as u16; - /// Ok(None) - /// }); - /// ``` + /// Output target. See [Tui] for example implementation. pub trait Screen: Xy + Wh + Send + Sync + Sized { type Unit: Coord; - /// Render drawable in subarea specified by `area` - fn show (&mut self, content: impl Draw) -> Perhaps>; /// Get current clipping area fn area (&self) -> XYWH; - /// Set clipping area + /// Set current clipping area fn clip ( &mut self, area: impl Into>>, - draw: impl FnOnce(&mut Self)->T + draw: &impl Fn(&mut Self)->T ) -> T; + /// Determine used area without drawing + fn size ( + &mut self, + area: impl Into>>, + draw: impl Draw + ) -> Perhaps>; + /// Draw + fn draw ( + &mut self, + area: impl Into>>, + draw: impl Draw + ) -> Perhaps>; } /// Implement the [Draw] trait for a particular drawable and [Screen]. @@ -427,12 +1387,12 @@ macro_rules! eval_xy ( ($(<$($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 + 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 + fn draw (&$self, $to: &mut $To) -> Perhaps::Unit>> $draw } } ); @@ -452,16 +1412,13 @@ macro_rules! eval_xy ( /// use tengri::*; /// struct MyWidget(bool); /// impl Draw for MyWidget { - /// fn draw (self, to: &mut Tui) -> Perhaps> { + /// fn draw (&self, to: &mut Tui) -> Perhaps> { /// todo!("your draw logic") /// } /// } /// ``` pub trait Draw { - fn draw (self, to: &mut S) -> Drawn; - fn layout (&self, area: XYWH) -> Drawn { - Ok(Some(area)) - } + fn draw (&self, to: &mut S) -> Drawn; } /// Emit a [Draw]able. @@ -500,9 +1457,9 @@ macro_rules! eval_xy ( /// Because we can't implement [Draw] for `F: FnOnce...` without conflicts. pub struct DrawThunk(pub F, std::marker::PhantomData); - implPerhaps>> Draw for DrawThunk { - fn draw (self, to: &mut T) -> Perhaps> { - (self.0)(to) + implPerhaps>> Draw for DrawThunk { + fn draw (&self, to: &mut T) -> Perhaps> { + to.clip(None, &self.0) } } @@ -512,66 +1469,48 @@ macro_rules! eval_xy ( /// # use tengri::*; /// let _ = draw(|to: &mut Tui|Ok(Some(to.area()))); // draws nothing /// ``` - pub const fn draw Perhaps>> ( + pub const fn draw Perhaps>> ( item: F ) -> DrawThunk { DrawThunk(item, std::marker::PhantomData) } - /// Only render when condition is true. - /// - /// ``` - /// # use tengri::*; - /// # fn test () -> impl Draw { - /// when(true, "Yes") - /// # } - /// ``` - pub const fn when (condition: bool, item: impl Draw) -> impl Draw { - draw(move|to: &mut T|if condition { item.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 { - draw(move|to: &mut T|if condition { a.draw(to) } else { b.draw(to) }) - } - pub type Drawn = Perhaps>; impl Draw for () { - fn draw (self, _: &mut S) -> Drawn { + 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) + self.as_ref().map(|it|it.draw(to)).transpose().map(Option::unwrap_or_default) }); //impl> Draw for RwLock { - //fn draw (self, __: &mut S) -> Drawn { + //fn draw (&self, __: &mut S) -> Drawn { //todo!() //} //} //impl> Draw for Arc { - //fn draw (self, __: &mut T) -> Perhaps> { + //fn draw (&self, __: &mut T) -> Perhaps> { //todo!() //} //} - impl> Draw for &V { - fn draw (self, to: &mut T) -> Perhaps> { - self.view().draw(to) + impl> Draw for &V { + fn draw (&self, to: &mut T) -> Perhaps> { + (*self).draw(to) } } + //impl> Draw for &V { + //fn draw (&self, to: &mut T) -> Perhaps> { + //self.view().draw(to) + //} + //} + pub trait Xy { fn x (&self) -> N; fn y (&self) -> N; @@ -776,232 +1715,7 @@ macro_rules! eval_xy ( } #[cfg(feature = "draw")] pub use self::layout::*; -#[cfg(feature = "draw")] mod layout { - use crate::*; - - impl> Layout for T {} - - 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) - } - - /// (bsp/e (exact/w 10 "Hello") "World") - fn exact_w >> (self, x: N) -> impl Draw { - Exact::W(self, x.into()) - } - /// (bsp/s (exact/h 10 "Hello") "World") - fn exact_h >> (self, y: N) -> impl Draw { - Exact::H(self, y.into()) - } - /// (exact/wh 10 2 "Hello World") - 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::Y(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::Y(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>) -> Align { - Align(azimuth.into(), self) - } - fn align_c (self) -> Align { - Align(Some(Azimuth::C), self) - } - fn align_x (self) -> Align { - Align(Some(Azimuth::X), self) - } - fn align_y (self) -> Align { - 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) - } - } - - /// 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 - } - - /// 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(Relaxed) as u16 - } - fn y (&self) -> u16 { - self.1.load(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 { - draw(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) - }) - } - } - - mod align; pub use self::align::*; - mod area; pub use self::area::*; - mod exact; pub use self::exact::*; - mod full; pub use self::full::*; - mod iter; pub use self::iter::*; - mod max; pub use self::max::*; - mod min; pub use self::min::*; - mod origin; pub use self::origin::*; - mod pad; pub use self::pad::*; - mod pull; pub use self::pull::*; - mod push; pub use self::push::*; - mod split; pub use self::split::*; -} +#[cfg(feature = "draw")] mod layout; #[cfg(feature = "draw")] pub use self::color::*; #[cfg(feature = "draw")] mod color { @@ -1192,7 +1906,7 @@ macro_rules! eval_xy ( } } -#[cfg(feature = "draw")] pub use self::text::*; +#[cfg(feature = "text")] pub use self::text::*; #[cfg(feature = "text")] mod text { #![allow(unused)] @@ -1255,655 +1969,6 @@ macro_rules! eval_xy ( } } -#[cfg(feature = "term")] pub use self::term::*; -#[cfg(feature = "term")] mod term { - use crate::*; - use Color::*; - - #[macro_export] macro_rules! tui_app { - ($Struct:ident { $($fields:tt)* }) => { - #[dizzle::namespace(bool)] - #[dizzle::namespace(u8)] - #[dizzle::namespace(u16)] - #[dizzle::namespace(Option)] - #[dizzle::namespace(Color Tui::eval_color_expr)] - #[derive(Debug, Default)] - pub struct $Struct { $($fields)* } - tui_main!($Struct { ..Default::default() }); - } - } - - /// Implement standard [main] entrypoint for TUI apps. - #[macro_export] macro_rules! tui_main { - ($state:expr) => { - pub fn main () -> Usually<()> { - tengri::Tui::setup_panic(); - tengri::Tui::run_main( - ::std::sync::Arc::new(::std::sync::RwLock::new($state)) - ) - } - } - } - - /// Enable TUI output for state struct. - #[macro_export] macro_rules! tui_view { - ($self:ident: $State:ty $body:block) => { - impl tengri::View for $State { - fn view (&$self) -> impl tengri::Draw $body - } - } - } - - #[macro_export] macro_rules! tui_interpret { - ($self:ident: $State:ident, $to:pat, $pat:ident -> $Result:ty { $($body:tt)+ }) => { - impl dizzle::Interpret for $State { - - fn interpret_word <'a> ( - &'a $self, $to: &mut Tui, $pat: &'a impl dizzle::Symbol - ) -> Usually<$Result> { - $($body)+ - } - - fn interpret_expr <'a> ( - &'a self, to: &mut Tui, src: &'a impl tengri::Expression - ) -> Usually<$Result> { - Ok(Some(if let Some(area) = tengri::eval_view(self, to, src)? { - area - } else if let Some(area) = tengri::Tui::eval_view(self, to, src)? { - area - } else { - return Err(format!("App::interpret_expr: unexpected: {src:?}").into()) - })) - } - - } - }; - } - - /// Enable TUI keyboard input for main state struct. - #[macro_export] macro_rules! tui_keys { - ($self:ident:$State:ty,$input:ident $($body:tt)+) => { - impl dizzle::Apply> for $State { - fn apply (&mut $self, $input: &tengri::TuiEvent) -> Usually<()> $($body)+ - } - }; - } - - //use unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; - //use rand::distributions::uniform::UniformSampler; - pub(crate) use ::{ - std::{ - io::{stdout, Write}, - time::Duration, - ops::{Deref, DerefMut}, - }, - ratatui::{ - prelude::{Style, Position, Backend, Color}, - style::{Modifier}, - 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, - }; - - //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 AsRef> for Tui { - fn as_ref (&self) -> &XYWH { - match self { - Self::Draw(_, area) => area, - Self::Layout(area) => area - } - } - } - - impl Wide for Tui { - fn w (&self) -> u16 { self.as_ref().2 } - } - - impl Tall for Tui { - fn h (&self) -> u16 { self.as_ref().3 } - } - - impl Xy for Tui { - fn x (&self) -> u16 { self.as_ref().0 } - fn y (&self) -> u16 { self.as_ref().1 } - } - - impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } } - - /// Terminal output. - pub enum Tui { - Draw ( - /// Ratatui buffer; area is screen size - Buffer, - /// Current draw area - XYWH - ), - Layout ( - XYWH - ) - } - - impl Tui { - pub fn setup_panic () { - use ::std::panic::{set_hook, PanicHookInfo}; - use ::better_panic::{Settings, Verbosity}; - let panic = Settings::auto() - .verbosity(Verbosity::Full) - .create_panic_handler(); - set_hook(Box::new(move |info: &PanicHookInfo|{ - let _ = Tui::teardown(&mut stdout()); - panic(info); - })); - } - pub fn 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())?; - let _ = output.join(); - Tui::teardown(&mut stdout()) - }) - } - /// Spawn the TUI input and output threadsl. - pub fn io < - T: View + Apply> + Send + Sync + 'static, - W: Write + Send + Sync + 'static, - > ( - exited: &Arc, - state: &Arc>, - poll: Duration, - sleep: Duration, - output: W, - ) -> Result<(Task, Task), Box> { - Ok(( - Tui::input(exited, state, poll)?, - Tui::output(exited, state, sleep, output)?, - )) - } - /// Spawn the TUI input thread which reads keys from the terminal. - pub fn 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(); - if Exit::is(&event) { - exited.store(true, Relaxed); - } else if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) { - panic!("{e}") - } - }) - } - pub fn teardown (backend: &mut W) -> Usually<()> { - use ::ratatui::backend::Backend; - stdout().execute(LeaveAlternateScreen)?; - CrosstermBackend::new(backend).show_cursor()?; - disable_raw_mode().map_err(Into::into) - } - pub fn new (width: u16, height: u16) -> Self { - Self::Draw(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 let Self::Draw(buffer, ..) = self { - if buffer.area != size { - back.clear_region(ClearType::All).unwrap(); - buffer.resize(size); - buffer.reset(); - } - } - } - pub fn redraw <'b, W: Write> ( - &'b mut self, - back: &mut CrosstermBackend, - mut next: &'b mut Self - ) { - if let Self::Draw(prev, ..) = self && let Self::Draw(next, ..) = next { - let updates = prev.diff(&next); - back.draw(updates.into_iter()).expect("failed to render"); - Backend::flush(back).expect("failed to flush output new"); - std::mem::swap(prev, next); - next.reset(); - } - } - pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH { - let XYWH(x0, y0, w, h) = self.area(); - if let Self::Draw(buffer, ..) = self { - for row in 0..h { - let y = y0 + row; - for col in 0..w { - let x = x0 + col; - if x < buffer.area.width && y < buffer.area.height { - if let Some(cell) = buffer.cell_mut(Position { x, y }) { - callback(cell, col, row); - } - } - } - } - } - self.xywh() - } - pub fn blit (&mut self, text: &impl AsRef, x: u16, y: u16, style: Option