diff --git a/examples/mode_02.rs b/examples/mode_02.rs index 1ec2069..a2106b3 100644 --- a/examples/mode_02.rs +++ b/examples/mode_02.rs @@ -37,6 +37,7 @@ 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 deleted file mode 100644 index 9ef91cc..0000000 --- a/src/layout.rs +++ /dev/null @@ -1,103 +0,0 @@ -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 new file mode 100644 index 0000000..1e1c961 --- /dev/null +++ b/src/layout/align.rs @@ -0,0 +1,41 @@ +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 a806c92..b6d68da 100644 --- a/src/layout/area.rs +++ b/src/layout/area.rs @@ -16,7 +16,6 @@ pub struct Area>( pub Option>, pub T ); - impl_draw!(,>|self: Area, to: S|{ - to.draw(self.0, &self.1) + to.clip(self.0, |to|self.1.draw(to)) }); diff --git a/src/layout/axis.rs b/src/layout/axis.rs deleted file mode 100644 index 7d6d6bb..0000000 --- a/src/layout/axis.rs +++ /dev/null @@ -1,248 +0,0 @@ -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 deleted file mode 100644 index ac749b8..0000000 --- a/src/layout/azimuth.rs +++ /dev/null @@ -1,455 +0,0 @@ -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 deleted file mode 100644 index a95d8f1..0000000 --- a/src/layout/cond.rs +++ /dev/null @@ -1,46 +0,0 @@ -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 new file mode 100644 index 0000000..e94f51d --- /dev/null +++ b/src/layout/exact.rs @@ -0,0 +1,49 @@ +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 new file mode 100644 index 0000000..5dbd40f --- /dev/null +++ b/src/layout/full.rs @@ -0,0 +1,41 @@ +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 new file mode 100644 index 0000000..9fc1c40 --- /dev/null +++ b/src/layout/max.rs @@ -0,0 +1,55 @@ +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 new file mode 100644 index 0000000..d747253 --- /dev/null +++ b/src/layout/min.rs @@ -0,0 +1,40 @@ +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 new file mode 100644 index 0000000..e5aaff9 --- /dev/null +++ b/src/layout/origin.rs @@ -0,0 +1,21 @@ +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 new file mode 100644 index 0000000..6ac2b4a --- /dev/null +++ b/src/layout/pad.rs @@ -0,0 +1,37 @@ +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 new file mode 100644 index 0000000..287a998 --- /dev/null +++ b/src/layout/pull.rs @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000..7d1114d --- /dev/null +++ b/src/layout/push.rs @@ -0,0 +1,42 @@ +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 new file mode 100644 index 0000000..555c75a --- /dev/null +++ b/src/layout/split.rs @@ -0,0 +1,315 @@ +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 119bd52..3a5342e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ 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}, @@ -29,10 +30,19 @@ pub(crate) use ::{ std::marker::PhantomData }; -#[cfg(all(feature = "draw", feature = "eval"))] +macro_rules! features { + ($($feature:literal: [ $($module:ident),* ]),*) => { + $( + $( + #[cfg(feature = $feature)] pub mod $module; + #[cfg(feature = $feature)] pub use $module::*; + )* + )* + } +} + 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 @@ -44,10 +54,8 @@ 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 @@ -63,6 +71,11 @@ 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),+>)? { @@ -278,1002 +291,6 @@ 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::{ @@ -1349,29 +366,52 @@ macro_rules! eval_xy ( use Azimuth::*; use Split::*; - /// Output target. See [Tui] for example implementation. + /// 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) + /// }); + /// ``` 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 current clipping area + /// Set clipping area fn clip ( &mut self, area: impl Into>>, - draw: &impl Fn(&mut Self)->T + draw: impl FnOnce(&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]. @@ -1387,12 +427,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 } } ); @@ -1412,13 +452,16 @@ 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 draw (self, to: &mut S) -> Drawn; + fn layout (&self, area: XYWH) -> Drawn { + Ok(Some(area)) + } } /// Emit a [Draw]able. @@ -1457,9 +500,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> { - to.clip(None, &self.0) + implPerhaps>> Draw for DrawThunk { + fn draw (self, to: &mut T) -> Perhaps> { + (self.0)(to) } } @@ -1469,48 +512,66 @@ 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.as_ref().map(|it|it.draw(to)).transpose().map(Option::unwrap_or_default) + self.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).draw(to) + 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.view().draw(to) - //} - //} - pub trait Xy { fn x (&self) -> N; fn y (&self) -> N; @@ -1715,7 +776,232 @@ macro_rules! eval_xy ( } #[cfg(feature = "draw")] pub use self::layout::*; -#[cfg(feature = "draw")] mod 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")] pub use self::color::*; #[cfg(feature = "draw")] mod color { @@ -1906,7 +1192,7 @@ macro_rules! eval_xy ( } } -#[cfg(feature = "text")] pub use self::text::*; +#[cfg(feature = "draw")] pub use self::text::*; #[cfg(feature = "text")] mod text { #![allow(unused)] @@ -1969,6 +1255,655 @@ 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