From 799e49762250f48778dadd92874a29495114a6af Mon Sep 17 00:00:00 2001 From: facile pop culture reference Date: Thu, 6 Aug 2026 18:29:03 +0300 Subject: [PATCH] fix and coverage for layout modifiers --- Justfile | 2 +- examples/mode_02.rs | 4 +- shell.nix | 1 + src/layout.rs | 47 ++++++ src/layout/axis.rs | 339 +++++++++++++++++------------------------- src/layout/azimuth.rs | 218 ++++++++++++++------------- src/layout/iter.rs | 17 ++- src/lib.rs | 2 +- src/term.rs | 246 +++++++++++++++--------------- src/term/modify.rs | 40 ++--- 10 files changed, 456 insertions(+), 460 deletions(-) diff --git a/Justfile b/Justfile index c2d4501..bc55ceb 100644 --- a/Justfile +++ b/Justfile @@ -13,7 +13,7 @@ cov: CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' \ time cargo test -j4 --workspace --profile coverage rm -rf target/coverage/html || true - time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t html -o target/coverage/html + time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t html -o target/coverage/html && reset cov-md: CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' \ diff --git a/examples/mode_02.rs b/examples/mode_02.rs index a077f20..1ec2069 100644 --- a/examples/mode_02.rs +++ b/examples/mode_02.rs @@ -42,9 +42,7 @@ impl_keywords!(Tui, XYWH, State [ kw_push, kw_tui_text, kw_tui_fg, - kw_tui_bg, - kw_color_g, - kw_color_rgb, + kw_tui_bg ]); impl Interpret>> for State { diff --git a/shell.nix b/shell.nix index f3e43dc..6ee5c7b 100644 --- a/shell.nix +++ b/shell.nix @@ -14,6 +14,7 @@ in pkgs.mkShell.override { pkgs.jack2 ]; nativeBuildInputs = [ + pkgs.grcov pkgs.pkg-config pkgs.clang pkgs.libclang diff --git a/src/layout.rs b/src/layout.rs index 9ef91cc..1bd7906 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -96,6 +96,53 @@ impl Sizer { } } +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()) + }); + } +} + mod area; pub use self::area::*; mod axis; pub use self::axis::*; mod azimuth; pub use self::azimuth::*; diff --git a/src/layout/axis.rs b/src/layout/axis.rs index 7d6d6bb..2625744 100644 --- a/src/layout/axis.rs +++ b/src/layout/axis.rs @@ -1,138 +1,125 @@ 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 {} +/// Use whole drawing area along one or both axes. +pub enum Full> { + __(PhantomData), + W(I), + H(I), + WH(I), +} - 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) } - } +impl_draw!(,>|self: Full, to: T|{ + let XYWH(x0, y0, w0, h0) = to.area(); + let item = match self { + Self::W(i) => i, Self::H(i) => i, Self::WH(i) => i, _ => unreachable!() + }; + let (x1, y1, w1, h1) = match self { + Self::W(..) => (Some(x0), None, Some(w0), None), + Self::H(..) => (None, Some(y0), None, Some(h0)), + Self::WH(..) => (Some(x0), Some(y0), Some(w0), Some(h0)), + _ => unreachable!() + }; + Ok(to.draw(to.area(), item)?.map(|XYWH(x2, y2, w2, h2)|XYWH( + x1.unwrap_or(x2), y1.unwrap_or(y2), w1.unwrap_or(w2), h1.unwrap_or(h2), + ))) +}); - 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()) - }); - } +#[cfg(test)] #[test] fn test_layout_full () -> Usually<()> { + assert_eq!(check_layout("1")?, Some(XYWH(1u16, 1, 1, 1))); + assert_eq!(check_layout("1".full_w())?, Some(XYWH(1u16, 1, 80, 1))); + assert_eq!(check_layout("1".full_h())?, Some(XYWH(1u16, 1, 1, 25))); + assert_eq!(check_layout("1".full_wh())?, Some(XYWH(1u16, 1, 80, 25))); + Ok(()) } def_layout_modifier!( - CanExact (exact_w exact_h exact_wh), + LayoutExact (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(), None), + Self::H(item, h) => (item, None, (*h).into()), + Self::WH(item, w, h) => (item, (*w).into(), (*h).into()), + _ => return Ok(None) + }; + to.draw(XYWH(x, y, w.unwrap_or(w0), h.unwrap_or(h0)), item) + } +); + +#[cfg(test)] #[test] fn test_layout_exact () -> Usually<()> { + assert_eq!(check_layout("FOOBAR\nKILROY")?, Some(XYWH(1, 1, 6, 2))); + assert_eq!(check_layout("FOOBAR\nKILROY".exact_w(3))?, Some(XYWH(1, 1, 3, 2))); + assert_eq!(check_layout("FOOBAR\nKILROY".exact_h(1))?, Some(XYWH(1, 1, 6, 1))); + assert_eq!(check_layout("FOOBAR\nKILROY".exact_wh(2, 1))?, Some(XYWH(1, 1, 2, 1))); + Ok(()) +} + +def_layout_modifier!( + LayoutMin (min_w min_h min_wh), + Min { W H WH } kw_min "min" |self, to| { + let XYWH(x0, y0, w0, h0) = to.area(); + let (item, w1, h1) = match self { + Self::W(item, w1) => (item, (*w1).into(), None), + Self::H(item, h1) => (item, None, (*h1).into()), + Self::WH(item, w1, h1) => (item, (*w1).into(), (*h1).into()), + _ => return Ok(None) + }; + Ok(to.draw(XYWH( + x0, y0, + w1.map(|w1|w1.max(w0)).unwrap_or(w0), + h1.map(|h1|h1.max(h0)).unwrap_or(h0) + ), item)?.map(|XYWH(x2, y2, w2, h2)|XYWH( + x2, y2, + w1.map(|w1|w1.max(w2)).unwrap_or(w2), + h1.map(|h1|h1.max(h2)).unwrap_or(h2) + ))) + } +); + +#[cfg(test)] #[test] fn test_layout_min () -> Usually<()> { + assert_eq!(check_layout("1".min_w(5))?, Some(XYWH(1u16, 1, 5, 1))); + assert_eq!(check_layout("1".min_h(5))?, Some(XYWH(1u16, 1, 1, 5))); + assert_eq!(check_layout("1".min_wh(5, 5))?, Some(XYWH(1u16, 1, 5, 5))); + assert_eq!(check_layout("123456".min_w(5))?, Some(XYWH(1u16, 1, 6, 1))); + Ok(()) +} + +def_layout_modifier!( + LayoutMax (max_w max_h max_wh), + Max { W H WH } kw_max "max" |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) + to.draw(XYWH(x, y, if w0 > w { w } else { w0 }, if h0 > h { h } else { h0 }), item) } ); def_layout_modifier!( - CanPad (pad_w pad_h pad_wh), + LayoutPad (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))) - }, + let XYWH(x, y, w0, h0) = to.area(); + let (item, w, h) = match self { + Self::X(item, w) => (item, (*w).into().unwrap_or_default(), Default::default()), + Self::Y(item, h) => (item, Default::default(), (*h).into().unwrap_or_default()), + Self::XY(item, w, h) => (item, (*w).into().unwrap_or_default(), + (*h).into().unwrap_or_default()), _ => return Ok(None) }; - item.draw(to) + to.draw(XYWH( + x.plus(w), + y.plus(h), + w0.minus(w).minus(w), + h0.minus(h).minus(h), + ), item) } ); def_layout_modifier!( - CanPush (push_x push_y push_xy), + LayoutPush (push_x push_y push_xy), Push { X Y XY } kw_push "push" |self, to| { match self { Self::__(_) => unreachable!(), @@ -150,99 +137,47 @@ def_layout_modifier!( } ); -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))); +#[cfg(test)] #[test] fn test_layout_push () -> Usually<()> { + assert_eq!(check_layout("1")?, Some(XYWH(1u16, 1, 1, 1))); + assert_eq!(check_layout("1".push_x(1))?, Some(XYWH(2u16, 1, 1, 1))); + assert_eq!(check_layout("1".push_y(1))?, Some(XYWH(1u16, 2, 1, 1))); + assert_eq!(check_layout("1".push_xy(1, 1))?, Some(XYWH(2u16, 2, 1, 1))); Ok(()) } +def_layout_modifier!( + LayoutPull (pull_x pull_y pull_xy), + Pull { X Y XY } kw_pull "pull" |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.minus((*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.minus((*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.minus((*x1).into().unwrap_or_default()), + y.minus((*y1).into().unwrap_or_default()), + w, h), item) + }, + _ => Ok(None) + } + } +); + +#[cfg(test)] #[test] fn test_layout_pull () -> Usually<()> { + assert_eq!(check_layout("1")?, Some(XYWH(1u16, 1, 1, 1))); + assert_eq!(check_layout("1".pull_x(1))?, Some(XYWH(0u16, 1, 1, 1))); + assert_eq!(check_layout("1".pull_y(1))?, Some(XYWH(1u16, 0, 1, 1))); + assert_eq!(check_layout("1".pull_xy(1, 1))?, Some(XYWH(0u16, 0, 1, 1))); + Ok(()) +} + +#[cfg(test)] fn check_layout (t: impl Draw) -> Perhaps> { + Tui::Layout(XYWH(1u16, 1, 80, 25)).size(None, t) +} diff --git a/src/layout/azimuth.rs b/src/layout/azimuth.rs index 2eeb0af..eb8d038 100644 --- a/src/layout/azimuth.rs +++ b/src/layout/azimuth.rs @@ -34,7 +34,9 @@ impl> HasOrigin for T { } fn_kw_layout!(kw_align |state, output, expr| { - Ok(matches!(expr.head()?, Some("align")).then(||{ + let head = expr.head(); + let mut frags = head.src()?.unwrap_or_default().split("/"); + Ok(matches!(frags.next(), 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(), @@ -95,9 +97,13 @@ pub struct Align( 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)? + let Self(azimuth, item) = self; + let area0 = to.area(); + let size = to.size(area0, item)?; + let area1 = align::(area0, size, *azimuth); + //println!("\n\r{azimuth:?} {area0:?} {size:?}=>{area1:?}"); + Ok(if let Some(area) = area1 { + to.draw(area, &item)? } else { None }) @@ -176,84 +182,6 @@ impl, B: Draw> Draw for Pair { } } -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, @@ -352,6 +280,108 @@ fn stack_drawn ( } } +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!() + //} + +} + +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) +} + #[macro_export] macro_rules! north { ($head:expr $(,)?) => { $head }; ($head:expr, $($tail:expr),* $(,)?) => { north($head, north!($($tail,)*)) }; @@ -382,30 +412,6 @@ fn stack_drawn ( ($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); diff --git a/src/layout/iter.rs b/src/layout/iter.rs index bd80aae..b43a9d0 100644 --- a/src/layout/iter.rs +++ b/src/layout/iter.rs @@ -47,18 +47,21 @@ pub fn iter_south <'a, S: Screen, D: Draw, I: Iterator> ( iter: impl Fn()->I ) -> impl Draw { draw(move|to: &mut S|{ - let XYWH(x, mut y, w, mut h) = to.area(); - let h0 = h; + let XYWH(x, y, w, h) = to.area(); + let mut y_next = y; + let mut h_used = S::Unit::zero(); for item in iter() { - if let Some(used) = to.draw(Some(XYWH(x, y, w, h)), item)? { - y = y.add(used.y()); - h = h.minus(used.y()); - if h == S::Unit::zero() { + let area = XYWH(x, y_next, w, h.minus(h_used)); + if let Some(used) = to.draw(area, item)? { + let used_h = used.h(); + y_next = y_next.plus(used_h); + h_used = h_used.plus(used_h); + if h_used >= h { break; } } } - Ok(Some(XYWH(x, y, w, h))) + Ok(Some(XYWH(x, y, w, h_used))) }) } diff --git a/src/lib.rs b/src/lib.rs index fd39be3..892f0a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1949,7 +1949,7 @@ macro_rules! eval_xy ( let mut chars = text.as_ref().chars(); while let Some(c) = chars.next() { width += c.width().unwrap_or(0) as u16; - if width > max { + if width >= max { break } } diff --git a/src/term.rs b/src/term.rs index 298dacb..bfbe0e0 100644 --- a/src/term.rs +++ b/src/term.rs @@ -32,7 +32,6 @@ mod phat; pub use self::phat::*; mod repeat; pub use self::repeat::*; mod scroll; pub use self::scroll::*; -#[cfg(feature = "term")] #[macro_export] macro_rules! tui_app { ($Struct:ident { $($fields:tt)* }) => { #[dizzle::namespace(bool)] @@ -47,18 +46,19 @@ mod scroll; pub use self::scroll::*; } /// Implement standard [main] entrypoint for TUI apps. -#[cfg(feature = "term")] #[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))) + Exit::run(|exit|tengri::Tui::run_main( + exit, + ::std::sync::Arc::new(::std::sync::RwLock::new($state)) + )) } } } /// Enable TUI output for state struct. -#[cfg(feature = "term")] #[macro_export] macro_rules! tui_view { ($self:ident: $State:ty $body:block) => { impl tengri::View for $State { @@ -68,7 +68,6 @@ mod scroll; pub use self::scroll::*; } /// Enable TUI keyboard input for main state struct. -#[cfg(feature = "term")] #[macro_export] macro_rules! tui_keys { ($self:ident:$State:ty,$input:ident $($body:tt)+) => { impl dizzle::Apply> for $State { @@ -79,12 +78,14 @@ mod scroll; pub use self::scroll::*; /// Terminal output. pub enum Tui { + /// Draw mode: updates contained [Buffer]. Draw ( /// Ratatui buffer; area is screen size Buffer, /// Current draw area XYWH ), + /// Discard mode: only computes sizes Layout ( /// Draw area; no buffer XYWH @@ -96,6 +97,16 @@ impl Screen for Tui { fn area (&self) -> XYWH { self.into() } + fn size ( + &mut self, area: impl Into>>, draw: impl Draw, + ) -> Perhaps> { + Self::Layout(self.area()).clip(area, &|to|draw.draw(to)) + } + fn draw ( + &mut self, area: impl Into>>, draw: impl Draw + ) -> Perhaps> { + self.clip(area, &|to|draw.draw(to)) + } fn clip ( &mut self, area: impl Into>>, draw: &impl Fn(&mut Self)->T, ) -> T { @@ -107,30 +118,6 @@ impl Screen for Tui { *self.area_mut() = prev; result } - fn size ( - &mut self, area: impl Into>>, draw: impl Draw, - ) -> Perhaps> { - Self::Layout(self.area()).clip(area, &|to|draw.draw(to)) - } - fn draw ( - &mut self, area: impl Into>>, draw: impl Draw - ) -> Perhaps> { - self.clip(area, &|to|draw.draw(to)) - } - - //fn draw ( - //&mut self, - //area: impl Into>>, - //draw: impl Draw - //) -> Perhaps> { - //let prev = self.area(); - //if let Some(area) = area.into() { - //*self.area_mut() = area.into(); - //} - //let result = draw.draw(self); - //*self.area_mut() = prev; - //result - //} } impl Tui { @@ -316,33 +303,6 @@ impl Tui { }) } - #[cfg(feature = "text")] - /// Write a line of text - /// - /// TODO: do a paragraph (handle newlines) - pub fn text (&mut self, text: &impl AsRef, x0: u16, y: u16, max_width: u16) - -> Perhaps> - { - let text = text.as_ref(); - let mut string_width: u16 = 0; - for character in text.chars() { - let x = x0 + string_width; - let character_width = character.width().unwrap_or(0) as u16; - string_width += character_width; - if string_width > max_width { - break - } - if let Self::Draw(buffer, ..) = self { - if let Some(cell) = buffer.cell_mut(ratatui::prelude::Position { x, y }) { - cell.set_char(character); - } else { - break - } - } - } - Ok(Some(XYWH(x0, y, string_width, 1))) - } - pub fn buffer (&mut self) -> Option<&mut Buffer> { if let Tui::Draw(buffer, _) = self { Some(buffer) @@ -416,67 +376,7 @@ pub const fn fill_char (c: char) -> impl Draw { })))) } -#[cfg(feature = "text")] impl_draw!(|self: String, to: Tui|{ - self.as_str().draw(to) -}); - -#[cfg(feature = "text")] impl_draw!(|self: std::sync::Arc, to: Tui|{ - self.as_ref().draw(to) -}); - -#[cfg(feature = "text")] impl_draw!(,>|self: TrimString, to: Tui|{ - self.as_ref().draw(to) -}); - -#[cfg(feature = "text")] -impl Draw for &str { - fn draw (&self, to: &mut Tui) -> Drawn { - let XYWH(x, y, w, h) = to.area(); - let mut max_w = 0u16; - let mut max_h = 0u16; - for (index, line) in self.split("\n").enumerate() { - max_h += 1; - max_w = max_w.max(line.len() as u16); - let _ = to.text(&line, x, y + index as u16, width_chars_max(w, line) as u16)?; - } - Ok(Some(XYWH(x, y, w.min(max_w), h.min(max_h)))) - } -} - -#[cfg(feature = "text")] -impl<'t, T: AsRef> Draw for TrimStr<'_, T> { - fn draw (&self, to: &mut Tui) -> Drawn { - let text = self.1.as_ref(); - let area = layout_text_u16(text, to.area())?.unwrap(); - let XYWH(x, y, w, ..) = to.area(); - let mut width: u16 = 1; - let mut chars = text.chars(); - while let Some(c) = chars.next() { - if width > self.0 || width > w { - break - } - to.set_char(x + width - 1, y, c); - width += c.width().unwrap_or(0) as u16; - } - let XYWH(x, y, w, ..) = XYWH( - to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h() - ); - to.text(&self.as_ref(), x, y, w) - } -} - -#[cfg(feature = "text")] -fn layout_text_u16 (text: &str, area: XYWH) -> Perhaps> { - let XYWH(x, y, w, h) = area; - let mut max_w = 0u16; - let mut max_h = 0u16; - for line in text.split("\n") { - max_h += 1; - max_w = max_w.max(line.len() as u16); - } - Ok(Some(XYWH(x, y, w.min(max_w), h.min(max_h)))) -} - +/// FIXME: Don't use `format!` but implement number blitting pub struct ShowSize; impl Draw for ShowSize { @@ -500,16 +400,116 @@ impl> Draw for ShowSizeOf { #[cfg(feature = "eval")] fn_kw_layout_tui!(kw_tui_text |_state, output, expr| { Ok(matches!(expr.head()?, Some("text")).then(||{ - if let Some(src) = expr.tail().src()? { - src.draw(output) + if let Some(src) = expr.tail().src()? && src.len() > 0 { + (&src[1..]).draw(output) } else { return Ok(None) } }).transpose()?.flatten()) }); -#[cfg(feature = "text")] #[cfg(test)] #[test] fn test_layout_text_u16 () -> Usually<()> { - assert_eq!(layout_text_u16("foo", XYWH(5, 6, 10, 10))?, Some(XYWH(5, 6, 3, 1))); - assert_eq!(layout_text_u16("foo\nbarz", XYWH(5, 6, 10, 10))?, Some(XYWH(5, 6, 4, 2))); - Ok(()) +#[cfg(feature = "text")] pub use self::tui_text::*; +#[cfg(feature = "text")] mod tui_text { + use super::*; + + impl Tui { + /// Write a line of text + /// + /// TODO: do a paragraph (handle newlines) + pub fn text (&mut self, text: &impl AsRef, x0: u16, y: u16, max_width: u16) + -> Perhaps> + { + let text = text.as_ref(); + let mut string_width: u16 = 0; + for character in text.chars() { + let x = x0 + string_width; + let character_width = character.width().unwrap_or(0) as u16; + string_width += character_width; + if string_width > max_width { + break + } + if let Self::Draw(buffer, ..) = self { + if let Some(cell) = buffer.cell_mut(ratatui::prelude::Position { x, y }) { + cell.set_char(character); + } else { + break + } + } + } + Ok(Some(XYWH(x0, y, string_width, 1))) + } + } + + impl_draw!(|self: String, to: Tui|{ + self.as_str().draw(to) + }); + + impl_draw!(|self: std::sync::Arc, to: Tui|{ + self.as_ref().draw(to) + }); + + impl_draw!(,>|self: TrimString, to: Tui|{ + self.as_ref().draw(to) + }); + + impl Draw for &str { + fn draw (&self, to: &mut Tui) -> Drawn { + let XYWH(x, y, w, h) = to.area(); + let mut max_w = 0u16; + let mut max_h = 0u16; + for (index, line) in self.split("\n").enumerate() { + let length = width_chars_max(w, line) as u16; + max_w = max_w.max(length); + max_h += 1; + if let Tui::Draw(..) = to { + let _ = to.text(&line, x, y + index as u16, length)?; + } + if max_h >= h { + break; + } + } + Ok(Some(XYWH(x, y, max_w, max_h))) + } + } + + impl<'t, T: AsRef> Draw for TrimStr<'_, T> { + fn draw (&self, to: &mut Tui) -> Drawn { + let text = self.1.as_ref(); + let area = layout_text_u16(text, to.area())?.unwrap(); + let XYWH(x, y, w, ..) = to.area(); + let mut width: u16 = 1; + let mut chars = text.chars(); + while let Some(c) = chars.next() { + if width > self.0 || width > w { + break + } + if let Tui::Draw(..) = to { + to.set_char(x + width - 1, y, c); + } + width += c.width().unwrap_or(0) as u16; + } + let XYWH(x, y, w, ..) = XYWH( + to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h() + ); + to.text(&self.as_ref(), x, y, w) + } + } + + fn layout_text_u16 (text: &str, area: XYWH) -> Perhaps> { + let XYWH(x, y, w, h) = area; + let mut max_w = 0u16; + let mut max_h = 0u16; + for line in text.split("\n") { + max_h += 1; + max_w = max_w.max(line.len() as u16); + } + Ok(Some(XYWH(x, y, w.min(max_w), h.min(max_h)))) + } + + #[cfg(test)] #[test] fn test_layout_text_u16 () -> Usually<()> { + assert_eq!(layout_text_u16("foo", XYWH(5, 6, 10, 10))?, Some(XYWH(5, 6, 3, 1))); + assert_eq!(layout_text_u16("foo\nbarz", XYWH(5, 6, 10, 10))?, Some(XYWH(5, 6, 4, 2))); + Ok(()) + } + } diff --git a/src/term/modify.rs b/src/term/modify.rs index 5a7512c..118aeb7 100644 --- a/src/term/modify.rs +++ b/src/term/modify.rs @@ -65,35 +65,41 @@ impl Tui { /// Apply foreground color. pub const fn fg (fg: Color, item: impl Draw) -> impl Draw { draw(move|to: &mut Tui|{ - to.update(&|cell,_,_|{ cell.set_fg(fg); }); - item.draw(to) + if let Some(size) = to.size(None, &item)? { + to.draw(Some(size), draw(|to: &mut Tui|{ + to.update(&|cell,_,_|{ cell.set_fg(fg); }); + item.draw(to) + })) + } else { + Ok(None) + } }) } /// Apply background color. pub const fn bg (bg: Color, item: impl Draw) -> impl Draw { - Background(bg, item) -} - -pub struct Background(Color, T); - -impl> Draw for Background { - fn draw (&self, to: &mut Tui) -> Drawn { - if let Some(size) = to.size(None, &self.1)? { - to.draw(Some(size), draw(|to: &mut Tui|{ - to.update(&|cell,_,_|{ cell.set_bg(self.0); }); - self.1.draw(to) - })) + draw(move|to: &mut Tui|{ + if let Some(size) = to.size(None, &item)? { + let _ = to.draw(Some(size), draw(|to: &mut Tui|{ + Ok(Some(to.update(&|cell,_,_|{ cell.set_bg(bg); }))) + }))?; + to.draw(Some(size), &item) } else { Ok(None) } - } + }) } pub const fn fg_bg (fg: Color, bg: Color, item: impl Draw) -> impl Draw { draw(move|to: &mut Tui|{ - to.update(&|cell,_,_|{ cell.set_fg(fg); cell.set_bg(bg); }); - item.draw(to) + if let Some(size) = to.size(None, &item)? { + let _ = to.draw(Some(size), draw(|to: &mut Tui|{ + Ok(Some(to.update(&|cell,_,_|{ cell.set_fg(fg); cell.set_bg(bg); }))) + }))?; + to.draw(Some(size), &item) + } else { + Ok(None) + } }) }