From fc01fd6ad3a6ed4ee17d65a65b3e59df522f895a Mon Sep 17 00:00:00 2001 From: facile pop culture reference Date: Fri, 31 Jul 2026 06:47:30 +0300 Subject: [PATCH] big flat --- Cargo.toml | 3 +- Justfile | 8 +- dizzle | 2 +- examples/mode_00.rs | 19 +- examples/mode_01.rs | 12 +- examples/mode_02.rs | 72 +- src/color.rs | 0 src/coord.rs | 40 ++ src/draw.rs | 161 ----- src/draw/color.rs | 163 ----- src/draw/coord.rs | 39 -- src/draw/iter.rs | 1 + src/draw/layout.rs | 437 ------------ src/draw/lrtb.rs | 2 +- src/draw/sizer.rs | 14 - src/draw/split.rs | 92 +-- src/draw/thunk.rs | 1 + src/eval.rs | 298 -------- src/exit.rs | 1 + src/layout/align.rs | 39 ++ src/layout/area.rs | 21 + src/layout/exact.rs | 29 + src/layout/full.rs | 41 ++ src/layout/max.rs | 55 ++ src/layout/min.rs | 40 ++ src/layout/origin.rs | 21 + src/layout/pad.rs | 37 + src/layout/pull.rs | 24 + src/layout/push.rs | 42 ++ src/lib.rs | 1594 +++++++++++++++++++++++++++++++++++++++++- src/term.rs | 349 --------- src/term/border.rs | 2 +- src/term/buffer.rs | 1 - src/term/button.rs | 3 +- src/term/colors.rs | 83 --- src/term/event.rs | 1 - src/term/phat.rs | 7 +- src/term/repeat.rs | 2 +- src/term/scroll.rs | 2 +- src/text.rs | 124 ---- 40 files changed, 2075 insertions(+), 1807 deletions(-) create mode 100644 src/color.rs create mode 100644 src/coord.rs delete mode 100644 src/draw.rs delete mode 100644 src/draw/layout.rs delete mode 100644 src/eval.rs create mode 100644 src/layout/align.rs create mode 100644 src/layout/area.rs create mode 100644 src/layout/exact.rs create mode 100644 src/layout/full.rs create mode 100644 src/layout/max.rs create mode 100644 src/layout/min.rs create mode 100644 src/layout/origin.rs create mode 100644 src/layout/pad.rs create mode 100644 src/layout/pull.rs create mode 100644 src/layout/push.rs delete mode 100644 src/term.rs diff --git a/Cargo.toml b/Cargo.toml index 588f2a3..2b7dc2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,11 @@ version = "0.15.0" description = "UI metaframework." [features] -default = ["lang", "sing", "midi", "draw", "play", "term", "text", "time", "rand", "okhsl"] +default = ["lang", "sing", "midi", "draw", "play", "term", "text", "time", "rand", "okhsl", "eval"] bumpalo = ["dep:bumpalo"] draw = [] gui = ["draw", "dep:winit"] +eval = [] lang = ["dep:dizzle"] midi = ["dep:midly"] okhsl = ["dep:palette"] diff --git a/Justfile b/Justfile index 83c5796..c2d4501 100644 --- a/Justfile +++ b/Justfile @@ -29,9 +29,5 @@ doc: CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' RUSTDOCFLAGS='-Cinstrument-coverage' \ cargo doc -mode-00: - cargo run --example mode_00 -mode-01: - cargo run --example mode_01 -mode-02: - cargo run --example mode_02 +mode MODE: + cargo run --example "mode_{{MODE}}" diff --git a/dizzle b/dizzle index 0f06571..e768064 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit 0f06571f7fc7e5f87aadb82d531726d24bf769e8 +Subproject commit e768064b3002dbf1aeaa143a45d54030305b4beb diff --git a/examples/mode_00.rs b/examples/mode_00.rs index 31b148d..443028e 100644 --- a/examples/mode_00.rs +++ b/examples/mode_00.rs @@ -1,21 +1,22 @@ //! Mode 00: Direct draw, direct control -use ::std::sync::{Arc, RwLock}; -use ::crossterm::event::{Event::*, KeyEvent, KeyCode::*}; -use ::ratatui::style::Color; -use ::tengri::{*, lang::*}; +use ::tengri::{ + *, + lang::*, + crossterm::event::{KeyEvent, Event::*, KeyCode::*}, + ratatui::style::Color, +}; tui_app!(State { /** User-controllable value. */ - cursor: usize, + counter: usize, }); tui_view!(self: State { thunk(|to: &mut Tui|{ - let cursor = format!("Cursor: {}", self.cursor); let _ = "DEMO [MODE 00]".align_sw().draw(to); let _ = ShowSize.align_se().draw(to); - let _ = format!("Cursor: {}", self.cursor).align_c().draw(to); + let _ = format!("Counter:\n{}", self.counter).align_c().draw(to); Ok(Some(to.area())) }) }); @@ -24,11 +25,11 @@ tui_keys!(self: State, input { Ok(if let Key(KeyEvent { code, .. }) = input.0 { match code { Up | Right => { - self.cursor = (self.cursor + 1) % 10; + self.counter = (self.counter + 1) % 10; () }, Down | Left => { - self.cursor = if self.cursor > 0 { self.cursor - 1 } else { 10 - 1 }; + self.counter = if self.counter > 0 { self.counter - 1 } else { 10 - 1 }; () }, _ => {} diff --git a/examples/mode_01.rs b/examples/mode_01.rs index d7ee626..2324245 100644 --- a/examples/mode_01.rs +++ b/examples/mode_01.rs @@ -1,10 +1,12 @@ //! Mode 01: Direct view, actions with history -use ::std::sync::{Arc, RwLock}; -use ::crossterm::event::{Event::*, KeyEvent, KeyCode::*}; -use ::ratatui::style::Color; -use ::tengri::{*, lang::*}; use itertools::Itertools; +use ::tengri::{ + *, + lang::*, + crossterm::event::{Event::*, KeyEvent, KeyCode::*}, + ratatui::style::Color, +}; tui_app!(State { /** Command history (undo/redo). */ @@ -29,7 +31,7 @@ tui_view!(self: State { let title = "Demo Mode 00"; let items = self.history.iter().take(10).map(|x|format!("{x:?}")).join("\n"); let history = format!("History: {}\n{items}", self.history.len()); - let cursor = format!("Cursor: {}", self.cursor); + let cursor = format!("Counter: {}", self.cursor); north( east(title.align_sw(), ShowSize.align_se()), east(history.align_c(), cursor.align_c()), diff --git a/examples/mode_02.rs b/examples/mode_02.rs index 70cec30..74c729c 100644 --- a/examples/mode_02.rs +++ b/examples/mode_02.rs @@ -1,9 +1,12 @@ //! Mode 02: Inline Dizzle config -use ::std::sync::{Arc, RwLock}; -use ::crossterm::event::{Event::*, KeyEvent, KeyCode::*}; -use ::ratatui::style::Color; -use ::tengri::{*, lang::*}; +use ::tengri::{ + *, + lang::*, + crossterm::event::{Event::*, KeyEvent, KeyCode::*}, + ratatui::style::Color, +}; + tui_app!(State { /** Command history (undo/redo). */ history: Vec, @@ -12,47 +15,18 @@ tui_app!(State { /** Rendered window size. */ size: Sizer, }); -tui_keys!(self: State, input { - Ok(if let Key(KeyEvent { code, .. }) = input.0 { - match code { - Up | Right => { self.next()?.map(|x|self.history.push(x)); }, - Down | Left => { self.prev()?.map(|x|self.history.push(x)); }, - _ => {} - } - }) -}); + tui_view!(self: State { let index = self.cursor + 1; let wh = (self.size.w(), self.size.h()); let src = VIEWS.get(self.cursor).unwrap_or(&""); let heading = format!("State {}/{} in {:?}", index, VIEWS.len(), &wh); - let title = bg(Color::Rgb(60, 10, 10), heading.align_n().push_y(1)); - let code = bg(Color::Rgb(10, 60, 10), format!("{}", src).align_n().push_y(2)); + let title = bg(Color::Rgb(60, 10, 10), heading.align_n().push_xy(2, 1)).exact_h(3); + let code = bg(Color::Rgb(10, 60, 10), format!("Source:\n{}", src).align_n().push_xy(2, 1)).min_h(4); let widget = thunk(move|to: &mut Tui|self.interpret(to, &src)); - self.size.of(south(title, north(code, widget))) + self.size.of(east(south(title, code).max_w(40), widget)) }); -impl Interpret for State { - fn interpret_expr (&self, to: &mut Tui, expr: &impl Language) -> Usually { - let expr = expr.expr()?; - match expr.head()? { - Some("g") if let Some(tail) = expr.tail()? => { - Color::new_g(tail.head()?, try_to_u8) - }, - Some("rgb") if let Some(tail) = expr.tail()? => { - Color::new_rgb(tail.head()?, try_to_u8) - }, - _ => Err(format!("not a color").into()) - } - } -} -fn try_to_u8 (src: Perhaps<&str>) -> Perhaps { - use std::str::FromStr; - if let Some(src) = src? { - Ok(Some(u8::from_str(src)?)) - } else { - Ok(None) - } -} + impl Interpret>> for State { fn interpret_word (&self, to: &mut Tui, sym: &impl Language) -> Perhaps> { match sym.src()? { @@ -65,13 +39,24 @@ impl Interpret>> for State { fn interpret_expr (&self, to: &mut Tui, src: &impl Expression) -> Perhaps> { Ok(Some(if let Some(area) = eval_view(self, to, src)? { area - } else if let Some(area) = eval_view_tui(self, to, src)? { + } else if let Some(area) = Tui::eval_view(self, to, src)? { area } else { return Err(format!("App::interpret_expr: unexpected: {src:?}").into()) })) } } + +tui_keys!(self: State, input { + Ok(if let Key(KeyEvent { code, .. }) = input.0 { + match code { + Up | Right => { self.next()?.map(|x|self.history.push(x)); }, + Down | Left => { self.prev()?.map(|x|self.history.push(x)); }, + _ => {} + } + }) +}); + impl State { fn next (&mut self) -> Perhaps { self.cursor = (self.cursor + 1) % VIEWS.len(); @@ -82,22 +67,30 @@ impl State { Ok(Some(Action::Next)) } } + #[derive(Debug)] enum Action { /** Increment cursor */ Next, /** Decrement cursor */ Prev, } + impl Action { fn eval (&self, state: &mut State) -> Perhaps { use Action::*; match self { Next => state.next(), Prev => state.prev(), } } } + const VIEWS: &'static [&'static str] = &[ stringify! { :foobar }, + stringify! { (text FOOBAR) }, stringify! { (bg (g 8) :foobar) }, stringify! { (fill/xy :foobar) }, + stringify! { (bsp/s (text foo) (text bar)) }, stringify! { (bsp/s :foo :bar) }, + stringify! { (bsp/s (bg (g 16) :foo) (bg (g 32) :bar)) }, + stringify! { (bsp/s (max/xy 20 10 (bg (g 16) :foo)) (max/y 5 (bg (g 32) :bar))) }, + stringify! { (bsp/e (bg (g 16) :foo) (bg (g 32) :bar)) }, stringify! { (fixed/xy 20 10 :foobar) }, stringify! { (bsp/s (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, stringify! { (bsp/e (fixed/xy 5 6 :foo) (fixed/xy 7 8 :bar)) }, @@ -155,6 +148,7 @@ const VIEWS: &'static [&'static str] = &[ (bg/behind :bg3 (border/around :border3 (margin/xy 6 3 :label3))))))) }, ]; + //handle!(TuiIn: |self: State, input|Action::from(input).eval(self).map(|_|None)); //view!(State: Tui: [ evaluate_output_expression, evaluate_output_expression_tui ]); //draw!(State: Tui: [ draw_example ]); diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/coord.rs b/src/coord.rs new file mode 100644 index 0000000..6f0b611 --- /dev/null +++ b/src/coord.rs @@ -0,0 +1,40 @@ +use super::*; + +/// A numeric type that can be used as coordinate. +/// +/// FIXME: Replace with `num` crate? +/// FIXME: Use AsRef/AsMut? +/// +/// ``` +/// use tengri::*; +/// let a: u16 = Coord::zero(); +/// let b: u16 = a.plus(1); +/// let c: u16 = a.minus(2); +/// let d = a.atomic(); +/// ``` +pub trait Coord: Send + Sync + Copy + + Add + + Sub + + Mul + + Div + + Ord + PartialEq + Eq + + Debug + Display + Default + + From + Into + + Into + + Into + //+ std::iter::Step +{ + /// Zero in own type. + fn zero () -> Self { 0.into() } + /// Addition. + fn plus (self, other: Self) -> Self; + /// Saturating subtraction. + fn minus (self, other: Self) -> Self { if self >= other { self - other } else { 0.into() } } + /// Convert to [AtomicUsize]. + fn atomic (self) -> AtomicUsize { AtomicUsize::new(self.into()) } +} + +/// TUI works in u16 coordinates. +impl Coord for u16 { + fn plus (self, other: Self) -> Self { self.saturating_add(other) } +} diff --git a/src/draw.rs b/src/draw.rs deleted file mode 100644 index 9d27ad3..0000000 --- a/src/draw.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::*; - -/// Output target. -/// -/// ``` -/// use tengri::*; -/// struct TestOut { w: u16, h: u16 }; -/// impl Wide for TestOut {} -/// impl Tall for TestOut {} -/// impl Xy for TestOut { -/// fn x (&self) -> u16 { 0 } -/// fn y (&self) -> u16 { 0 } -/// } -/// impl Screen for TestOut { -/// type Unit = u16; -/// fn show (&mut self, _: 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 clipping area - fn clip ( - &mut self, - area: impl Into>>, - draw: impl FnOnce(&mut Self)->T - ) -> T; -} - -/// Implement the [Draw] trait for a particular drawable and [Screen]. -/// -/// ``` -/// use tengri::*; -/// struct MyDrawable; -/// impl_draw!(|self: MyDrawable, to: Tui|{ -/// todo!("your draw logic") -/// }); -/// ``` -#[macro_export] macro_rules! impl_draw ( - ($(<$($T:ident: $Trait:path,)+>)?| - $self:ident:$Self:path, $to:ident:$To:ty - |$draw:block)=>{ impl$(<$($T:$Trait),+>)? Draw<$To> for $Self { - fn draw ($self, $to: &mut $To) -> Perhaps::Unit>> $draw - } }; - ($(<$($T:ident: $Trait:path,)+>)?| - $self:ident:$Self:ty, $to:ident:$To:ty - |$draw:block)=>{ impl$(<$($T:$Trait),+>)? Draw<$To> for $Self { - fn draw ($self, $to: &mut $To) -> Perhaps::Unit>> $draw - } } -); - -/// Drawable that supports dynamic dispatch. -/// -/// Drawables are composable, e.g. the [when] and [either] conditionals -/// or the layout constraints. -/// -/// Drawables are consumable, i.e. the [Draw::draw] method receives an -/// owned `self` and does not return it, consuming the drawable. -/// -/// To draw a thing multiple times, instead of explicitly constructing it -/// every time, implement the [View] trait instead, which will construct -/// a [Draw]able. -/// -/// ``` -/// use tengri::*; -/// struct MyWidget(bool); -/// impl Draw for MyWidget { -/// fn draw (self, to: &mut Tui) -> Perhaps> { -/// todo!("your draw logic") -/// } -/// } -/// ``` -pub trait Draw { - fn draw (self, to: &mut S) -> Drawn; - fn layout (&self, area: XYWH) -> Drawn { - Ok(Some(area)) - } -} - -/// The opposite of [thunk]? -pub fn draw > (item: T) -> impl FnOnce(&mut S) -> Perhaps> { - move|to: &mut S|item.draw(to) -} - -pub type Drawn = Perhaps>; - -impl Draw for () { - fn draw (self, _: &mut S) -> Drawn { - Ok(None) - } -} - -impl_draw!(,>|self: Option, to: S|{ - self.map(|it|it.draw(to)).transpose().map(Option::unwrap_or_default) -}); - -//impl> Draw for RwLock { - //fn draw (self, __: &mut S) -> Drawn { - //todo!() - //} -//} - -//impl> Draw for Arc { - //fn draw (self, __: &mut T) -> Perhaps> { - //todo!() - //} -//} - -/// Emit a [Draw]able. -/// -/// Speculative. How to avoid conflicts with [Draw] proper? -pub trait View { - fn view (&self) -> impl Draw; -} - -impl View for () { - fn view (&self) -> impl Draw { - () - } -} - -impl> Draw for &V { - fn draw (self, to: &mut T) -> Perhaps> { - self.view().draw(to) - } -} - -features! { - "draw": [ - color, - coord, - iter, - layout, - lrtb, - sizer, - split, - thunk, - xywh - ] -} diff --git a/src/draw/color.rs b/src/draw/color.rs index ccea5e6..e69de29 100644 --- a/src/draw/color.rs +++ b/src/draw/color.rs @@ -1,163 +0,0 @@ -use crate::lang::impl_from; -use ::ratatui::style::Color; -use ::rand::distributions::uniform::UniformSampler; -pub(crate) use ::palette::{ - Okhsl, Srgb, OklabHue, Mix, okhsl::UniformOkhsl, - convert::{FromColor, FromColorUnclamped} -}; - -pub fn rgb (r: u8, g: u8, b: u8) -> ItemColor { - let term = Color::Rgb(r, g, b); - ItemColor { okhsl: rgb_to_okhsl(term), term } -} - -pub fn g (g: u8) -> Color { - Color::Rgb(g, g, g) -} - -pub fn okhsl_to_rgb (color: Okhsl) -> Color { - let Srgb { red, green, blue, .. }: Srgb = Srgb::from_color_unclamped(color); - Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,) -} - -pub fn rgb_to_okhsl (color: Color) -> Okhsl { - if let Color::Rgb(r, g, b) = color { - Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)) - } else { - unreachable!("only Color::Rgb is supported") - } -} - -pub trait HasColor { fn color (&self) -> ItemColor; } - -#[macro_export] macro_rules! has_color { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? { - fn color (&$self) -> ItemColor { $cb } - } - } -} - -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub struct ItemColor { - pub term: Color, - pub okhsl: Okhsl -} - -impl_from!(ItemColor: |term: Color| Self { term, okhsl: rgb_to_okhsl(term) }); - -impl_from!(ItemColor: |okhsl: Okhsl| Self { okhsl, term: okhsl_to_rgb(okhsl) }); - -// A single color within item theme parameters, in OKHSL and RGB representations. -impl ItemColor { - #[cfg(feature = "term")] pub const fn from_tui (term: Color) -> Self { - Self { term, okhsl: Okhsl::new_const(OklabHue::new(0.0), 0.0, 0.0) } - } - pub fn random () -> Self { - let mut rng = ::rand::thread_rng(); - let lo = Okhsl::new(-180.0, 0.01, 0.25); - let hi = Okhsl::new( 180.0, 0.9, 0.5); - UniformOkhsl::new(lo, hi).sample(&mut rng).into() - } - pub fn random_dark () -> Self { - let mut rng = ::rand::thread_rng(); - let lo = Okhsl::new(-180.0, 0.025, 0.075); - let hi = Okhsl::new( 180.0, 0.5, 0.150); - UniformOkhsl::new(lo, hi).sample(&mut rng).into() - } - pub fn random_near (color: Self, distance: f32) -> Self { - color.mix(Self::random(), distance) - } - pub fn mix (&self, other: Self, distance: f32) -> Self { - if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); } - self.okhsl.mix(other.okhsl, distance).into() - } -} - -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub struct ItemTheme { - pub base: ItemColor, - pub light: ItemColor, - pub lighter: ItemColor, - pub lightest: ItemColor, - pub dark: ItemColor, - pub darker: ItemColor, - pub darkest: ItemColor, -} - -impl_from!(ItemTheme: |base: ItemColor| Self::from_item_color(base)); - -impl_from!(ItemTheme: |base: Color| Self::from_tui_color(base)); - -impl ItemTheme { - #[cfg(feature = "term")] pub const G: [Self;256] = { - let mut builder = dizzle::konst::array::ArrayBuilder::new(); - while !builder.is_full() { - let index = builder.len() as u8; - let light = (index as f64 * 1.15) as u8; - let lighter = (index as f64 * 1.7) as u8; - let lightest = (index as f64 * 1.85) as u8; - let dark = (index as f64 * 0.9) as u8; - let darker = (index as f64 * 0.6) as u8; - let darkest = (index as f64 * 0.3) as u8; - builder.push(ItemTheme { - base: ItemColor::from_tui(Color::Rgb(index, index, index )), - light: ItemColor::from_tui(Color::Rgb(light, light, light, )), - lighter: ItemColor::from_tui(Color::Rgb(lighter, lighter, lighter, )), - lightest: ItemColor::from_tui(Color::Rgb(lightest, lightest, lightest, )), - dark: ItemColor::from_tui(Color::Rgb(dark, dark, dark, )), - darker: ItemColor::from_tui(Color::Rgb(darker, darker, darker, )), - darkest: ItemColor::from_tui(Color::Rgb(darkest, darkest, darkest, )), - }); - } - builder.build() - }; - pub fn random () -> Self { ItemColor::random().into() } - pub fn random_near (color: Self, distance: f32) -> Self { - color.base.mix(ItemColor::random(), distance).into() - } - pub const G00: Self = { - let color: ItemColor = ItemColor { - okhsl: Okhsl { hue: OklabHue::new(0.0), lightness: 0.0, saturation: 0.0 }, - term: Color::Rgb(0, 0, 0) - }; - Self { - base: color, - light: color, - lighter: color, - lightest: color, - dark: color, - darker: color, - darkest: color, - } - }; - #[cfg(feature = "term")] pub fn from_tui_color (base: Color) -> Self { - Self::from_item_color(ItemColor::from_tui(base)) - } - pub fn from_item_color (base: ItemColor) -> Self { - let mut light = base.okhsl; - light.lightness = (light.lightness * 1.3).min(1.0); - let mut lighter = light; - lighter.lightness = (lighter.lightness * 1.3).min(1.0); - let mut lightest = base.okhsl; - lightest.lightness = 0.95; - let mut dark = base.okhsl; - dark.lightness = (dark.lightness * 0.75).max(0.0); - dark.saturation = (dark.saturation * 0.75).max(0.0); - let mut darker = dark; - darker.lightness = (darker.lightness * 0.66).max(0.0); - darker.saturation = (darker.saturation * 0.66).max(0.0); - let mut darkest = darker; - darkest.lightness = 0.1; - darkest.saturation = (darkest.saturation * 0.50).max(0.0); - Self { - base, - light: light.into(), - lighter: lighter.into(), - lightest: lightest.into(), - dark: dark.into(), - darker: darker.into(), - darkest: darkest.into(), - } - } -} diff --git a/src/draw/coord.rs b/src/draw/coord.rs index 6f0b611..4563e55 100644 --- a/src/draw/coord.rs +++ b/src/draw/coord.rs @@ -1,40 +1 @@ use super::*; - -/// A numeric type that can be used as coordinate. -/// -/// FIXME: Replace with `num` crate? -/// FIXME: Use AsRef/AsMut? -/// -/// ``` -/// use tengri::*; -/// let a: u16 = Coord::zero(); -/// let b: u16 = a.plus(1); -/// let c: u16 = a.minus(2); -/// let d = a.atomic(); -/// ``` -pub trait Coord: Send + Sync + Copy - + Add - + Sub - + Mul - + Div - + Ord + PartialEq + Eq - + Debug + Display + Default - + From + Into - + Into - + Into - //+ std::iter::Step -{ - /// Zero in own type. - fn zero () -> Self { 0.into() } - /// Addition. - fn plus (self, other: Self) -> Self; - /// Saturating subtraction. - fn minus (self, other: Self) -> Self { if self >= other { self - other } else { 0.into() } } - /// Convert to [AtomicUsize]. - fn atomic (self) -> AtomicUsize { AtomicUsize::new(self.into()) } -} - -/// TUI works in u16 coordinates. -impl Coord for u16 { - fn plus (self, other: Self) -> Self { self.saturating_add(other) } -} diff --git a/src/draw/iter.rs b/src/draw/iter.rs index 1bbdcfa..c6c0d13 100644 --- a/src/draw/iter.rs +++ b/src/draw/iter.rs @@ -1,4 +1,5 @@ use super::*; +use crate::layout::*; /// Iterate over a collection of the same kind of [Draw]able: pub fn iter <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( diff --git a/src/draw/layout.rs b/src/draw/layout.rs deleted file mode 100644 index fba4d9b..0000000 --- a/src/draw/layout.rs +++ /dev/null @@ -1,437 +0,0 @@ -#![allow(unused)] - -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) - } -} - -/// 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!(), - } -}); - -/// 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) - } -}); - -/// 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!() -}); - -/// 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|{ - todo!() -}); - -/// 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>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} -impl_draw!(, X: Into>,>|self: Max, to: T|{ - 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, draw(item)) -}); - -/// Set size of of drawing area. -/// -/// ``` -/// use tengri::Layout; -/// let _ = "".exact_w(1); -/// let _ = "".exact_h(1); -/// let _ = "".exact_wh(1, 1); -/// ``` -pub enum Exact, X: Into>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} -impl_draw!(, X: Into>,>|self: Exact, to: T|{ - let area: XYWH = to.area(); - let (item, area) = match self { - Self::W(item, w1) => - (item, XYWH(area.0, area.1, w1.into().unwrap_or(area.2), area.3)), - Self::H(item, h1) => - (item, XYWH(area.0, area.1, area.2, h1.into().unwrap_or(area.3))), - Self::WH(item, w1, h1) => - (item, XYWH(area.0, area.1, w1.into().unwrap_or(area.2), h1.into().unwrap_or(area.3))), - _ => return Ok(None) - }; - to.clip(area, |to|item.draw(to)) -}); - -/// Define inner drawing area. -/// -/// ``` -/// use tengri::Layout; -/// let _ = "".pad_w(1); -/// let _ = "".pad_h(1); -/// let _ = "".pad_wh(1, 1); -/// ``` -pub enum Pad, X: Into>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} -impl_draw!(, X: Into>,>|self: Pad, _to: T|{ - todo!() -}); - -pub struct Align(Option, T); -impl_draw!(,>|self: Align, to: S|{ - use Azimuth::*; - let XYWH(x0, y0, w0, h0) = to.area(); - if let Some(XYWH(x, y, w, h)) = self.1.layout(to.area())? { - to.clip(match self.0 { - 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 => to.area() - }, |to|self.1.draw(to)) - } else { - self.1.draw(to) - } -}); - -/// Where is [0, 0] located? -/// -/// ``` -/// use tengri::*; -/// use Azimuth::*; -/// let _ = "".align(NW); -/// ``` -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Debug, Copy, Clone, Default)] pub enum Azimuth { - #[default] C, X, Y, NW, N, NE, E, SE, S, SW, W -} - -/// ``` -/// use tengri::*; -/// let _ = area(None, "unareaed"); -/// let _ = area(XYWH(1, 2, 3, 4), "southeast"); -/// let _ = area(Some(XYWH(1, 2, 3, 4)), "southeast"); -/// ``` -pub fn area , U: Into>>> ( - origin: U, it: T -) -> Area { - Area(origin.into(), it) -} - -pub struct Area>( - pub Option>, - pub T -); -impl_draw!(,>|self: Area, to: S|{ - to.clip(self.0, |to|self.1.draw(to)) -}); - -pub struct Origin(Option, T); -impl_draw!(,>|self: Origin, _to: S|{ - todo!() -}); - -/// Something that has `[0, 0]` at a particular point. -pub trait HasOrigin { - fn origin (&self) -> Azimuth; -} - -impl> HasOrigin for T { - fn origin (&self) -> Azimuth { - *self.as_ref() - } -} diff --git a/src/draw/lrtb.rs b/src/draw/lrtb.rs index 586f912..7900a08 100644 --- a/src/draw/lrtb.rs +++ b/src/draw/lrtb.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{*, layout::*, draw::*}; use Azimuth::*; impl> Lrtb for T {} diff --git a/src/draw/sizer.rs b/src/draw/sizer.rs index 5e20693..7ff5ed5 100644 --- a/src/draw/sizer.rs +++ b/src/draw/sizer.rs @@ -29,17 +29,3 @@ impl Sizer { }) } } - -pub struct ShowSize; - -impl Draw for ShowSize { - fn layout (&self, area: XYWH) -> Perhaps> { - let info = format!("{area:?}"); - Ok(Some(XYWH(area.0, area.1, info.len() as u16, 1))) - } - fn draw (self, to: &mut Tui) -> Drawn { - let area = to.area(); - let info = format!("{area:?}"); - to.text(&info, area.0, area.1, info.len() as u16) - } -} diff --git a/src/draw/split.rs b/src/draw/split.rs index 8408f57..62ca578 100644 --- a/src/draw/split.rs +++ b/src/draw/split.rs @@ -1,4 +1,34 @@ -use super::*; +use super::{*, layout::*}; + +#[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 { Split::East.half(a, b) @@ -141,7 +171,7 @@ fn draw_stacks ( }) } -fn stack_areas ( +pub fn stack_areas ( split: &Split, area: XYWH, a: &impl Draw, @@ -152,9 +182,7 @@ fn stack_areas ( 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()) - ))? + b.layout(XYWH(area.x(), area.y() + used.h(), area.w(), area.h().minus(used.h())))? } else { None } @@ -162,9 +190,7 @@ fn stack_areas ( 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() - ))? + b.layout(XYWH(area.x() + used.w(), area.y(), area.w().minus(used.w()), area.h()))? } else { None } @@ -176,9 +202,7 @@ fn stack_areas ( None }, if let Some(used) = area_a { - b.layout(XYWH( - area.x(), area.y(), area.w(), area.h().minus(used.h()) - ))? + 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() @@ -192,9 +216,7 @@ fn stack_areas ( None }, if let Some(used) = area_a { - b.layout(XYWH( - area.x(), area.y(), area.w().minus(used.w()), area.h() - ))? + 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() @@ -231,32 +253,18 @@ fn stack_drawn ( } } -#[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,)*)) }; +#[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(()) + } } diff --git a/src/draw/thunk.rs b/src/draw/thunk.rs index af27f5b..3754fe8 100644 --- a/src/draw/thunk.rs +++ b/src/draw/thunk.rs @@ -1,4 +1,5 @@ use crate::*; +use super::*; /// Because we can't implement [Draw] for `F: FnOnce...` without conflicts. pub struct Thunk(pub F, std::marker::PhantomData); diff --git a/src/eval.rs b/src/eval.rs deleted file mode 100644 index e0df0f1..0000000 --- a/src/eval.rs +++ /dev/null @@ -1,298 +0,0 @@ -use crate::{*, lang::*}; -use ratatui::style::Color; - -/// Some layout operations exist in multiple variants that take a single argument. -/// Their handling in [eval_view] is uniform and goes like this: -macro_rules! eval_enum (( - $name:literal, $output:ident, $state:ident, $value:expr, $arg0: ident, $Enum:ident { - $($v:literal => $V:ident),* $(,)? - } -) => {{ - match $value { - $(Some($v) => $Enum::$V,)* - frag => unimplemented!("{}/{frag:?}", $name) - } -}}); - -/// Some layout operations exist in XY, X, and Y variants that take 3 or 2 arguments. -/// Their handling in [eval_view] is uniform and goes like this: -macro_rules! eval_xy ( - // Valueless variant: - // (fill/x ...) - // (fill/y ...) - // (fill/xy ...) - ( - $name:expr => $expr:expr, $head:expr, $output:ident, $state:ident, - $variant:expr, $xy:ident, $x:ident, $y:ident, $arg: ident, - ) => {{ - // frags.next(): 2nd slash-delimited fragment: /x, /y, /xy - let variant = $variant; - let thunk = thunk(move|screen|$state.interpret(screen, &$arg)); - match variant { - // X variant - Some("x") => thunk.$x().draw($output), - // Y variant - Some("y") => thunk.$y().draw($output), - // XY variant (can be omitted) - Some("xy") | None => thunk.$xy().draw($output), - // Other namespace members are invalid - frag => invalid_variant($name, frag, $expr, $head) - } - }}; - - // Variadic variant: - // (push/x n ...) - // (push/y n ...) - // (push/xy n m ...) - ( - $name:expr => $expr:expr, $head:expr, $output:ident, $state:ident, - $variant:expr, $xy:ident, $x:ident, $y:ident, $arg0: ident, $arg1: ident, $arg2: ident, - ) => {{ - // frags.next(): 2nd slash-delimited fragment: /x, /y, /xy - let variant = $variant; - let thunk = thunk(move|screen|$state.interpret(screen, &match variant { - Some("x") | Some("y") => $arg1, - Some("xy") | None => $arg2, - _ => panic!("{}: unsupported axis {variant:?}; try /x, /y, /xy", $name) - })); - match variant { - // X variant - Some("x") => thunk.$x($state.namespace($arg0?)?) - .draw($output), - // Y variant - Some("y") => thunk.$y($state.namespace($arg0?)?) - .draw($output), - // XY variant (can be omitted) - Some("xy") | None => thunk.$xy($state.namespace($arg0?)?, $state.namespace($arg1?)?) - .draw($output), - // Other namespace members are invalid - frag => invalid_variant($name, frag, $expr, $head) - } - }}; -); - -fn invalid_variant ( - name: &str, - frag: impl Language, - expr: impl Language, - head: impl Language, -) -> Usually { - unimplemented!( - "{name}/{frag:?} ({expr:?}) ({head:?}) ({:?})", - head.src()?.unwrap_or_default().split("/").next() - ) -} - -/// Interpret layout operation. -/// -/// ``` -/// # use tengri::{*, lang::*}; -/// -/// struct State {/*app-specific*/} -/// impl<'b> Namespace<'b, u16> for State {} -/// impl<'b> Namespace<'b, bool> for State {} -/// impl<'b> Namespace<'b, Option> for State {} -/// impl Interpret>> for State {} -/// -/// # fn main () -> tengri::Usually<()> { -/// let state = State {}; -/// let mut target = Tui::new(80, 25); -/// eval_view(&state, &mut target, &"")?; -/// eval_view(&state, &mut target, &"(whe true (text hello))")?; -/// eval_view(&state, &mut target, &"(either true (text hello) (text world))")?; -/// // TODO test all -/// # Ok(()) } -/// ``` -pub fn eval_view <'a, O: Screen + 'a, S> ( - state: &S, output: &mut O, expr: &'a impl Expression -) -> Perhaps> where - S: Interpret>> - + for<'b> Namespace<'b, bool> - + for<'b> Namespace<'b, O::Unit> - + for<'b> Namespace<'b, Option> -{ - // First element of expression is name of the operation. - // These are quasi-namespaced using the separator character, `/`. - let head = expr.head()?; - let mut frags = head.src()?.unwrap_or_default().split("/"); - - // The rest of the tokens in the expr are arguments. - // Their meanings depend on the dispatched operation - // Here we just reference them, so that they are in scope. - // Dereferencing them happens in the dispatch branch. - let args = expr.tail(); - let arg0 = args.head(); - let tail0 = args.tail(); - let arg1 = tail0.head(); - let tail1 = tail0.tail(); - let arg2 = tail1.head(); - // First `frags.next()` calls returns the namespace. - match frags.next() { - - Some("when") => when( - state.namespace(arg0?)?.unwrap(), - thunk(move|output: &mut O|{state.interpret(output, &arg1)}) - ).draw(output), - - Some("either") => either( - state.namespace(arg0?)?.unwrap(), - thunk(move|output: &mut O|{state.interpret(output, &arg1)}), - thunk(move|output: &mut O|{state.interpret(output, &arg2)}), - ).draw(output), - - Some("bsp") => eval_enum!("bsp", output, state, frags.next(), arg0, Split { - "n" => North, "s" => South, "e" => East, "w" => West, "a" => Above, "b" => Below - }).stack( - thunk(move|output: &mut O|{state.interpret(output, &arg0)}), - thunk(move|output: &mut O|{state.interpret(output, &arg1)}), - ).draw(output), - - Some("align") => thunk(move|output: &mut O|{ - state.interpret(output, &arg0) - }).align(eval_enum!("align", output, state, frags.next(), arg0, 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), - - Some("exact") => eval_xy!( - "exact" => expr, head, output, state, frags.next(), exact_wh, exact_w, exact_h, arg0, arg1, arg2, - ), - - Some("fixed") => eval_xy!( - "fixed" => expr, head, output, state, frags.next(), exact_wh, exact_w, exact_h, arg0, arg1, arg2, - ), - - Some("min") => eval_xy!( - "min" => expr, head, output, state, frags.next(), min_wh, min_w, min_h, arg0, arg1, arg2, - ), - - Some("max") => eval_xy!( - "max" => expr, head, output, state, frags.next(), max_wh, max_w, max_h, arg0, arg1, arg2, - ), - - Some("push") => eval_xy!( - "push" => expr, head, output, state, frags.next(), push_xy, push_x, push_y, arg0, arg1, arg2, - ), - - Some("fill") => eval_xy!( - "fill" => expr, head, output, state, frags.next(), full_wh, full_w, full_h, arg0, - ), - - _ => return Ok(None) - - } -} - -/// Interpret TUI-specific layout operation. -/// -/// ``` -/// use tengri::{*, lang::*, ratatui::prelude::Color}; -/// -/// #[namespace(bool)] -/// #[namespace(u8)] -/// #[namespace(u16)] -/// #[namespace(Color get_color)] -/// struct State; -/// -/// impl Interpret>> for State { -/// fn interpret_expr <'a> (&'a self, _: &mut Tui, lang: &'a impl Expression) -/// -> Usually>> -/// { -/// Ok(None) -/// } -/// } -/// -/// fn get_color (state: &State, src: impl Language) -> Perhaps { -/// if let Some(expr) = src.expr()? { -/// match (expr.head()?, expr.tail()?) { -/// (Some("g"), Some(tail)) => { -/// let n: u8 = state.namespace(tail.head().map_err(Into::into))?.ok_or(LanguageError::Domain("not gray"))?; -/// Ok(Some(Color::Rgb(n, n, n))) -/// }, -/// (Some("rgb"), Some(tail)) => { -/// let r: u8 = state.namespace(tail.head().map_err(Into::into))? -/// .ok_or(LanguageError::Domain("not red"))?; -/// let g: u8 = state.namespace(tail.tail().head().map_err(Into::into))? -/// .ok_or(LanguageError::Domain("not green"))?; -/// let b: u8 = state.namespace(tail.tail().tail().head().map_err(Into::into))? -/// .ok_or(LanguageError::Domain("not blue"))?; -/// Ok(Some(Color::Rgb(r, g, b))) -/// }, -/// (Some(_), _) => return Err(format!("not a color expression: {expr}").into()), -/// (None, _) => return Err(format!("not a color expression: {expr}").into()), -/// } -/// } else if let Ok(Some(sym)) = src.word() { -/// Ok(match sym { -/// ":color/bg" => Some(Color::Rgb(28, 32, 36)), -/// ":color/fg" => Some(Color::Rgb(98, 92, 96)), -/// _ => return Err(format!("not a color: {sym}").into()) -/// }) -/// } else { -/// return Err(format!("not a color: {:?}", src.src()?).into()) -/// } -/// } -/// -/// # fn main () -> tengri::Usually<()> { -/// let state = State; -/// let mut out = Tui::new(80, 25); -/// eval_view_tui(&state, &mut out, "")?; -/// eval_view_tui(&state, &mut out, "text Hello world!")?; -/// eval_view_tui(&state, &mut out, "fg (g 0) (text Hello world!)")?; -/// eval_view_tui(&state, &mut out, "bg (g 2) (text Hello world!)")?; -/// eval_view_tui(&state, &mut out, "(bg (g 3) (fg (g 4) (text Hello world!)))")?; -/// # Ok(()) } -/// ``` -pub fn eval_view_tui <'a, S> ( - state: &S, to: &mut Tui, expr: impl Expression + 'a -) -> Perhaps> where - S: Interpret>> - + for<'b>Namespace<'b, bool> - + for<'b>Namespace<'b, u16> - + for<'b>Namespace<'b, Color> -{ - use crate::term::*; - // See `tengri::eval_view` - let head = expr.head()?; - let mut frags = head.src()?.unwrap_or_default().split("/"); - let args = expr.tail(); - let arg0 = args.head(); - let tail0 = args.tail(); - let arg1 = tail0.head(); - match frags.next() { - Some("text") => { - if let Some(src) = args?.src()? { - to.show(src) - } else { - return Ok(None) - } - }, - - Some("fg") => { - let arg0 = arg0?.expect("fg: expected arg 0 (color)"); - if let Some(color) = Namespace::namespace(state, arg0)? { - fg(color, thunk(move|to: &mut Tui|{ - state.interpret(to, &arg1)?; - Ok(Some(to.area().into())) // FIXME?: don't max out the used area? - })).draw(to) - } else { - return Err(format!("fg: {arg0:?}: not a color").into()) - } - }, - - Some("bg") => { - let arg0 = arg0?.expect("bg: expected arg 0 (color)"); - if let Some(color) = Namespace::namespace(state, arg0)? { - bg(color, thunk(move|to: &mut Tui|{ - state.interpret(to, &arg1)?; - Ok(Some(to.area().into())) // FIXME?: don't max out the used area? - })).draw(to) - } else { - return Err(format!("bg: {arg0:?}: not a color").into()) - } - }, - - _ => return Ok(None) - - } -} diff --git a/src/exit.rs b/src/exit.rs index 8d96419..76078fa 100644 --- a/src/exit.rs +++ b/src/exit.rs @@ -1,5 +1,6 @@ use crate::*; use std::sync::{Arc, atomic::AtomicBool}; +use crossterm::event::*; #[derive(Clone)] pub struct Exit(Arc); diff --git a/src/layout/align.rs b/src/layout/align.rs new file mode 100644 index 0000000..faf6247 --- /dev/null +++ b/src/layout/align.rs @@ -0,0 +1,39 @@ +use crate::{*, draw::*, layout::*}; +use Azimuth::*; + +pub struct Align( + pub(crate) Option, + pub(crate) T, +); + +impl> Draw for Align { + fn layout (&self, area: XYWH) -> Perhaps> { + let XYWH(x0, y0, w0, h0) = area; + 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 new file mode 100644 index 0000000..16ee05d --- /dev/null +++ b/src/layout/area.rs @@ -0,0 +1,21 @@ +use crate::{*, draw::*}; + +/// ``` +/// use tengri::*; +/// let _ = area(None, "unareaed"); +/// let _ = area(XYWH(1, 2, 3, 4), "southeast"); +/// let _ = area(Some(XYWH(1, 2, 3, 4)), "southeast"); +/// ``` +pub fn area , U: Into>>> ( + origin: U, it: T +) -> Area { + Area(origin.into(), it) +} + +pub struct Area>( + pub Option>, + pub T +); +impl_draw!(,>|self: Area, to: S|{ + to.clip(self.0, |to|self.1.draw(to)) +}); diff --git a/src/layout/exact.rs b/src/layout/exact.rs new file mode 100644 index 0000000..c1d1299 --- /dev/null +++ b/src/layout/exact.rs @@ -0,0 +1,29 @@ +use crate::{*, draw::*}; + +/// Set size of of drawing area. +/// +/// ``` +/// use tengri::Layout; +/// let _ = "".exact_w(1); +/// let _ = "".exact_h(1); +/// let _ = "".exact_wh(1, 1); +/// ``` +pub enum Exact, X: Into>> { + __(PhantomData), + W(I, X), + H(I, X), + WH(I, X, X), +} +impl_draw!(, X: Into>,>|self: Exact, to: T|{ + let area: XYWH = to.area(); + let (item, area) = match self { + Self::W(item, w1) => + (item, XYWH(area.0, area.1, w1.into().unwrap_or(area.2), area.3)), + Self::H(item, h1) => + (item, XYWH(area.0, area.1, area.2, h1.into().unwrap_or(area.3))), + Self::WH(item, w1, h1) => + (item, XYWH(area.0, area.1, w1.into().unwrap_or(area.2), h1.into().unwrap_or(area.3))), + _ => return Ok(None) + }; + to.clip(area, |to|item.draw(to)) +}); diff --git a/src/layout/full.rs b/src/layout/full.rs new file mode 100644 index 0000000..28ce0c5 --- /dev/null +++ b/src/layout/full.rs @@ -0,0 +1,41 @@ +use crate::{*, draw::*}; + +/// 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..035eabf --- /dev/null +++ b/src/layout/max.rs @@ -0,0 +1,55 @@ +use crate::{*, draw::*}; + +/// 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, draw(item)) + } +} diff --git a/src/layout/min.rs b/src/layout/min.rs new file mode 100644 index 0000000..9f96684 --- /dev/null +++ b/src/layout/min.rs @@ -0,0 +1,40 @@ +use crate::{*, draw::*}; + +/// 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..ce25a54 --- /dev/null +++ b/src/layout/origin.rs @@ -0,0 +1,21 @@ +use crate::{*, draw::*, layout::*}; + +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..3e79a34 --- /dev/null +++ b/src/layout/pad.rs @@ -0,0 +1,37 @@ +use crate::{*, draw::*}; + +/// 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..5bd5522 --- /dev/null +++ b/src/layout/pull.rs @@ -0,0 +1,24 @@ +use crate::{*, draw::*}; + +/// 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..14fb3a9 --- /dev/null +++ b/src/layout/push.rs @@ -0,0 +1,42 @@ +use crate::{*, draw::*}; + +/// 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/lib.rs b/src/lib.rs index 4b6fcfb..fadccdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ pub extern crate unicode_width; #[cfg(feature = "midi")] pub extern crate midly; #[cfg(feature = "term")] pub extern crate ratatui; #[cfg(feature = "term")] pub extern crate crossterm; -#[cfg(feature = "lang")] pub extern crate dizzle as lang; +#[cfg(feature = "lang")] pub extern crate dizzle; #[cfg(test)] #[macro_use] pub extern crate proptest; #[cfg(test)] pub(crate) use proptest_derive::Arbitrary; @@ -41,16 +41,13 @@ macro_rules! features { } } -#[cfg(feature = "lang")] pub use ::dizzle::{Usually, Perhaps, impl_default}; +#[cfg(feature = "lang")] pub use ::dizzle::{Usually, Perhaps}; +#[cfg(feature = "lang")] use ::dizzle::*; features! { - "lang": [ eval ], "time": [ time ], "play": [ exit, task ], - "sing": [ sing ], - "text": [ text ], - "term": [ term ], - "draw": [ draw ] + "sing": [ sing ] } /// Define a trait an implement it for various mutation-enabled wrapper types. */ @@ -84,26 +81,6 @@ features! { }; ); -/// Define an enum containing commands, and implement [Command] trait for over given `State`. -#[macro_export] macro_rules! def_command ( - ($Command:ident: |$state:ident: $State:ty| { - // FIXME: support attrs (docstrings) - $($Variant:ident$({$($arg:ident:$Arg:ty),+ $(,)?})?=>$body:expr),* $(,)? - })=>{ - #[derive(Debug)] pub enum $Command { - // FIXME: support attrs (docstrings) - $($Variant $({ $($arg: $Arg),* })?),* - } - impl ::tengri::lang::Act<$State> for $Command { - fn act (&self, $state: &mut $State) -> Perhaps { - match self { - $(Self::$Variant $({ $($arg),* })? => $body,)* - _ => unimplemented!("Act<{}>: {self:?}", stringify!($State)), - } - } - } - }); - /// Implement [Handle] for given `State` and `handler`. #[macro_export] macro_rules! impl_handle { //(|$self:ident:$State:ty,$input:ident|$handler:expr) => { @@ -123,3 +100,1566 @@ features! { //} } } + +/// Implement [Default]. +#[macro_export] macro_rules! impl_default { + ($T:ty:$e:expr) => { impl Default for $T { fn default () -> Self { $e } } }; +} + +/// Implement [`Debug`] in bulk. +#[macro_export] macro_rules! impl_debug (($($S:ty|$self:ident,$w:ident|$body:block)*)=>{ + $(impl std::fmt::Debug for $S { fn fmt (&$self, $w: &mut std::fmt::Formatter) -> std::fmt::Result $body })* +}); + +/// Implement [`From`] in bulk. +#[macro_export] macro_rules! impl_from ( + ($(<$($lt:lifetime),+>)?$Target:ty:|$state:ident:$Source:ty|$cb:expr) => { + impl $(<$($lt),+>)? From<$Source> for $Target { fn from ($state:$Source) -> Self { $cb }} + }; + + ($($Struct:ty { $( $(<$($l:lifetime),* $($T:ident$(:$U:ident)?),*>)? ($source:ident: $From:ty) $expr:expr );+ $(;)? })*) => { $( + $(impl $(<$($l),* $($T$(:$U)?),*>)? From<$From> for $Struct { fn from ($source: $From) -> Self { $expr } })+ + )* }; +); + +/// Implement [AsRef]. +#[macro_export] macro_rules! impl_as_ref (($T:ty: |$self:ident:$S:ty|$x:expr)=>{ + impl AsRef<$T> for $S { fn as_ref (&$self) -> &$T { $x } } +}); + +/// Implement [AsMut]. +#[macro_export] macro_rules! impl_as_mut (($T:ty: |$self:ident:$S:ty|$x:expr)=>{ + impl AsMut<$T> for $S { fn as_mut (&mut $self) -> &mut $T { $x } } +}); + +/// Implement [AsRefOpt]. +#[macro_export] macro_rules! impl_as_ref_opt (($T:ty: |$self:ident:$S:ty|$x:expr)=>{ + impl AsRefOpt<$T> for $S { fn as_ref_opt (&$self) -> Option<&$T> { $x } } +}); + +/// Implement [AsMutOpt]. +#[macro_export] macro_rules! impl_as_mut_opt (($T:ty: |$self:ident:$S:ty|$x:expr)=>{ + impl AsMutOpt<$T> for $S { fn as_mut_opt (&mut $self) -> Option<&mut $T> { $x } } +}); + +pub trait AsRefOpt { fn as_ref_opt (&self) -> Option<&T>; } + +pub trait AsMutOpt { fn as_mut_opt (&mut self) -> Option<&mut T>; } + +/// Implement [AsRef] and [AsMut]. +#[macro_export] macro_rules! impl_has ( + ($T:ty: |$self:ident:$S:ty|$x:expr)=>{ + impl AsRef<$T> for $S { + fn as_ref (&$self) -> &$T { &$x } + } + impl AsMut<$T> for $S { + fn as_mut (&mut $self) -> &mut $T { &mut $x } + } + }; + ($T:ty: |$self:ident:$S:ty|$x:block;$y:block)=>{ + impl AsRef<$T> for $S { + fn as_ref (&$self) -> &$T $x + } + impl AsMut<$T> for $S { + fn as_mut (&mut $self) -> &mut $T $y + } + } +); + +/// Some layout operations exist in multiple variants that take a single argument. +/// Their handling in [eval_view] is uniform and goes like this: +macro_rules! eval_enum (( + $name:literal, $output:ident, $state:ident, $value:expr, $arg0: ident, $Enum:ident { + $($v:literal => $V:ident),* $(,)? + } +) => {{ + match $value { + $(Some($v) => $Enum::$V,)* + frag => unimplemented!("{}/{frag:?}", $name) + } +}}); + +/// Some layout operations exist in XY, X, and Y variants that take 3 or 2 arguments. +/// Their handling in [eval_view] is uniform and goes like this: +macro_rules! eval_xy ( + // Valueless variant: + // (fill/x ...) + // (fill/y ...) + // (fill/xy ...) + ( + $name:expr => $expr:expr, $head:expr, $output:ident, $state:ident, + $variant:expr, $xy:ident, $x:ident, $y:ident, $arg: ident, + ) => {{ + // frags.next(): 2nd slash-delimited fragment: /x, /y, /xy + let variant = $variant; + let thunk = thunk(move|screen|$state.interpret(screen, &$arg)); + match variant { + // X variant + Some("x") => thunk.$x().draw($output), + // Y variant + Some("y") => thunk.$y().draw($output), + // XY variant (can be omitted) + Some("xy") | None => thunk.$xy().draw($output), + // Other namespace members are invalid + frag => invalid_variant($name, frag, $expr, $head) + } + }}; + + // Variadic variant: + // (push/x n ...) + // (push/y n ...) + // (push/xy n m ...) + ( + $name:expr => $expr:expr, $head:expr, $output:ident, $state:ident, + $variant:expr, $xy:ident, $x:ident, $y:ident, $arg0: ident, $arg1: ident, $arg2: ident, + ) => {{ + // frags.next(): 2nd slash-delimited fragment: /x, /y, /xy + let variant = $variant; + let thunk = thunk(move|screen|$state.interpret(screen, &match variant { + Some("x") | Some("y") => $arg1, + Some("xy") | None => $arg2, + _ => panic!("{}: unsupported axis {variant:?}; try /x, /y, /xy", $name) + })); + match variant { + // X variant + Some("x") => thunk.$x($state.namespace($arg0?)?) + .draw($output), + // Y variant + Some("y") => thunk.$y($state.namespace($arg0?)?) + .draw($output), + // XY variant (can be omitted) + Some("xy") | None => thunk.$xy($state.namespace($arg0?)?, $state.namespace($arg1?)?) + .draw($output), + // Other namespace members are invalid + frag => invalid_variant($name, frag, $expr, $head) + } + }}; +); + +#[cfg(feature = "draw")] pub use self::draw::*; +#[cfg(feature = "draw")] mod draw { + use crate::*; + + /// Output target. + /// + /// ``` + /// use tengri::*; + /// struct TestOut { w: u16, h: u16 }; + /// impl Wide for TestOut {} + /// impl Tall for TestOut {} + /// impl Xy for TestOut { + /// fn x (&self) -> u16 { 0 } + /// fn y (&self) -> u16 { 0 } + /// } + /// impl Screen for TestOut { + /// type Unit = u16; + /// fn show (&mut self, _: 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 clipping area + fn clip ( + &mut self, + area: impl Into>>, + draw: impl FnOnce(&mut Self)->T + ) -> T; + } + + /// Implement the [Draw] trait for a particular drawable and [Screen]. + /// + /// ``` + /// use tengri::*; + /// struct MyDrawable; + /// impl_draw!(|self: MyDrawable, to: Tui|{ + /// todo!("your draw logic") + /// }); + /// ``` + #[macro_export] macro_rules! impl_draw ( + ($(<$($T:ident: $Trait:path,)+>)?| + $self:ident:$Self:path, $to:ident:$To:ty + |$draw:block)=>{ impl$(<$($T:$Trait),+>)? Draw<$To> for $Self { + fn draw ($self, $to: &mut $To) -> Perhaps::Unit>> $draw + } }; + ($(<$($T:ident: $Trait:path,)+>)?| + $self:ident:$Self:ty, $to:ident:$To:ty + |$draw:block)=>{ impl$(<$($T:$Trait),+>)? Draw<$To> for $Self { + fn draw ($self, $to: &mut $To) -> Perhaps::Unit>> $draw + } } + ); + + /// Drawable that supports dynamic dispatch. + /// + /// Drawables are composable, e.g. the [when] and [either] conditionals + /// or the layout constraints. + /// + /// Drawables are consumable, i.e. the [Draw::draw] method receives an + /// owned `self` and does not return it, consuming the drawable. + /// + /// To draw a thing multiple times, instead of explicitly constructing it + /// every time, implement the [View] trait instead, which will construct + /// a [Draw]able. + /// + /// ``` + /// use tengri::*; + /// struct MyWidget(bool); + /// impl Draw for MyWidget { + /// fn draw (self, to: &mut Tui) -> Perhaps> { + /// todo!("your draw logic") + /// } + /// } + /// ``` + pub trait Draw { + fn draw (self, to: &mut S) -> Drawn; + fn layout (&self, area: XYWH) -> Drawn { + Ok(Some(area)) + } + } + + /// The opposite of [thunk]? + pub fn draw > (item: T) -> impl FnOnce(&mut S) -> Perhaps> { + move|to: &mut S|item.draw(to) + } + + pub type Drawn = Perhaps>; + + impl Draw for () { + fn draw (self, _: &mut S) -> Drawn { + Ok(None) + } + } + + impl_draw!(,>|self: Option, to: S|{ + self.map(|it|it.draw(to)).transpose().map(Option::unwrap_or_default) + }); + + //impl> Draw for RwLock { + //fn draw (self, __: &mut S) -> Drawn { + //todo!() + //} + //} + + //impl> Draw for Arc { + //fn draw (self, __: &mut T) -> Perhaps> { + //todo!() + //} + //} + + /// Emit a [Draw]able. + /// + /// Speculative. How to avoid conflicts with [Draw] proper? + pub trait View { + fn view (&self) -> impl Draw; + } + + impl View for () { + fn view (&self) -> impl Draw { + () + } + } + + impl> Draw for &V { + fn draw (self, to: &mut T) -> Perhaps> { + self.view().draw(to) + } + } + + features! { + "draw": [ + iter, + lrtb, + sizer, + split, + thunk, + xywh + ] + } +} + +#[cfg(feature = "draw")] pub use self::coord::*; +#[cfg(feature = "draw")] mod coord { + use crate::*; + + /// A numeric type that can be used as coordinate. + /// + /// FIXME: Replace with `num` crate? + /// FIXME: Use AsRef/AsMut? + /// + /// ``` + /// use tengri::*; + /// let a: u16 = Coord::zero(); + /// let b: u16 = a.plus(1); + /// let c: u16 = a.minus(2); + /// let d = a.atomic(); + /// ``` + pub trait Coord: Send + Sync + Copy + + Add + + Sub + + Mul + + Div + + Ord + PartialEq + Eq + + Debug + Display + Default + + From + Into + + Into + + Into + //+ std::iter::Step + { + /// Zero in own type. + fn zero () -> Self { 0.into() } + /// Addition. + fn plus (self, other: Self) -> Self; + /// Saturating subtraction. + fn minus (self, other: Self) -> Self { if self >= other { self - other } else { 0.into() } } + /// Convert to [AtomicUsize]. + fn atomic (self) -> AtomicUsize { AtomicUsize::new(self.into()) } + } + + /// TUI works in u16 coordinates. + impl Coord for u16 { + fn plus (self, other: Self) -> Self { self.saturating_add(other) } + } +} + +#[cfg(feature = "draw")] pub use self::layout::*; +#[cfg(feature = "draw")] mod layout { + use crate::{*, draw::*}; + + 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 + } + + 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 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::*; +} + +#[cfg(feature = "draw")] pub use self::color::*; +#[cfg(feature = "draw")] mod color { + use crate::{*, draw::*}; + use dizzle::LanguageError::*; + use ::ratatui::style::Color; + use ::rand::distributions::uniform::UniformSampler; + pub(crate) use ::palette::{ + Okhsl, Srgb, OklabHue, Mix, okhsl::UniformOkhsl, + convert::{FromColor, FromColorUnclamped} + }; + + pub fn rgb (r: u8, g: u8, b: u8) -> ItemColor { + let term = Color::Rgb(r, g, b); + ItemColor { okhsl: rgb_to_okhsl(term), term } + } + + pub fn g (g: u8) -> Color { + Color::Rgb(g, g, g) + } + + pub fn okhsl_to_rgb (color: Okhsl) -> Color { + let Srgb { red, green, blue, .. }: Srgb = Srgb::from_color_unclamped(color); + Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,) + } + + pub fn rgb_to_okhsl (color: Color) -> Okhsl { + if let Color::Rgb(r, g, b) = color { + Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)) + } else { + unreachable!("only Color::Rgb is supported") + } + } + + pub trait HasColor { fn color (&self) -> ItemColor; } + + #[macro_export] macro_rules! has_color { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? { + fn color (&$self) -> ItemColor { $cb } + } + } + } + + #[derive(Copy, Clone, Debug, Default, PartialEq)] + pub struct ItemColor { + pub term: Color, + pub okhsl: Okhsl + } + + impl_from!(ItemColor: |term: Color| Self { term, okhsl: rgb_to_okhsl(term) }); + + impl_from!(ItemColor: |okhsl: Okhsl| Self { okhsl, term: okhsl_to_rgb(okhsl) }); + + // A single color within item theme parameters, in OKHSL and RGB representations. + impl ItemColor { + #[cfg(feature = "term")] pub const fn from_tui (term: Color) -> Self { + Self { term, okhsl: Okhsl::new_const(OklabHue::new(0.0), 0.0, 0.0) } + } + pub fn random () -> Self { + let mut rng = ::rand::thread_rng(); + let lo = Okhsl::new(-180.0, 0.01, 0.25); + let hi = Okhsl::new( 180.0, 0.9, 0.5); + UniformOkhsl::new(lo, hi).sample(&mut rng).into() + } + pub fn random_dark () -> Self { + let mut rng = ::rand::thread_rng(); + let lo = Okhsl::new(-180.0, 0.025, 0.075); + let hi = Okhsl::new( 180.0, 0.5, 0.150); + UniformOkhsl::new(lo, hi).sample(&mut rng).into() + } + pub fn random_near (color: Self, distance: f32) -> Self { + color.mix(Self::random(), distance) + } + pub fn mix (&self, other: Self, distance: f32) -> Self { + if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); } + self.okhsl.mix(other.okhsl, distance).into() + } + } + + #[derive(Copy, Clone, Debug, Default, PartialEq)] + pub struct ItemTheme { + pub base: ItemColor, + pub light: ItemColor, + pub lighter: ItemColor, + pub lightest: ItemColor, + pub dark: ItemColor, + pub darker: ItemColor, + pub darkest: ItemColor, + } + + impl_from!(ItemTheme: |base: ItemColor| Self::from_item_color(base)); + + impl_from!(ItemTheme: |base: Color| Self::from_tui_color(base)); + + impl ItemTheme { + #[cfg(feature = "term")] pub const G: [Self;256] = { + let mut builder = dizzle::konst::array::ArrayBuilder::new(); + while !builder.is_full() { + let index = builder.len() as u8; + let light = (index as f64 * 1.15) as u8; + let lighter = (index as f64 * 1.7) as u8; + let lightest = (index as f64 * 1.85) as u8; + let dark = (index as f64 * 0.9) as u8; + let darker = (index as f64 * 0.6) as u8; + let darkest = (index as f64 * 0.3) as u8; + builder.push(ItemTheme { + base: ItemColor::from_tui(Color::Rgb(index, index, index )), + light: ItemColor::from_tui(Color::Rgb(light, light, light, )), + lighter: ItemColor::from_tui(Color::Rgb(lighter, lighter, lighter, )), + lightest: ItemColor::from_tui(Color::Rgb(lightest, lightest, lightest, )), + dark: ItemColor::from_tui(Color::Rgb(dark, dark, dark, )), + darker: ItemColor::from_tui(Color::Rgb(darker, darker, darker, )), + darkest: ItemColor::from_tui(Color::Rgb(darkest, darkest, darkest, )), + }); + } + builder.build() + }; + pub fn random () -> Self { ItemColor::random().into() } + pub fn random_near (color: Self, distance: f32) -> Self { + color.base.mix(ItemColor::random(), distance).into() + } + pub const G00: Self = { + let color: ItemColor = ItemColor { + okhsl: Okhsl { hue: OklabHue::new(0.0), lightness: 0.0, saturation: 0.0 }, + term: Color::Rgb(0, 0, 0) + }; + Self { + base: color, + light: color, + lighter: color, + lightest: color, + dark: color, + darker: color, + darkest: color, + } + }; + #[cfg(feature = "term")] pub fn from_tui_color (base: Color) -> Self { + Self::from_item_color(ItemColor::from_tui(base)) + } + pub fn from_item_color (base: ItemColor) -> Self { + let mut light = base.okhsl; + light.lightness = (light.lightness * 1.3).min(1.0); + let mut lighter = light; + lighter.lightness = (lighter.lightness * 1.3).min(1.0); + let mut lightest = base.okhsl; + lightest.lightness = 0.95; + let mut dark = base.okhsl; + dark.lightness = (dark.lightness * 0.75).max(0.0); + dark.saturation = (dark.saturation * 0.75).max(0.0); + let mut darker = dark; + darker.lightness = (darker.lightness * 0.66).max(0.0); + darker.saturation = (darker.saturation * 0.66).max(0.0); + let mut darkest = darker; + darkest.lightness = 0.1; + darkest.saturation = (darkest.saturation * 0.50).max(0.0); + Self { + base, + light: light.into(), + lighter: lighter.into(), + lightest: lightest.into(), + dark: dark.into(), + darker: darker.into(), + darkest: darkest.into(), + } + } + } + + pub trait ColorDsl: Sized { + fn new_g (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps) -> Usually; + fn new_rgb (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps) -> Usually; + } + + impl ColorDsl for Color { + fn new_g (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps) -> Usually { + let n = try_to_u8(expr.tail().map_err(Into::into))?.ok_or(Domain("not gray"))?; + Ok(Self::Rgb(n, n, n)) + } + fn new_rgb (expr: T, try_to_u8: impl Fn(Perhaps<&str>)->Perhaps) -> Usually { + let r = try_to_u8(expr.tail().map_err(Into::into))? + .ok_or(Domain("not red"))?; + let g = try_to_u8(expr.tail().tail().head().map_err(Into::into))? + .ok_or(Domain("not green"))?; + let b = try_to_u8(expr.tail().tail().tail().head().map_err(Into::into))? + .ok_or(Domain("not blue"))?; + Ok(Color::Rgb(r, g, b)) + } + } +} + +#[cfg(feature = "text")] +mod text { + pub(crate) use ::unicode_width::*; + + /// Displays an owned [str]-like with fixed maximum width. + /// + /// Width is computed using [unicode_width]. + pub struct TrimString>(pub u16, pub T); + + impl> AsRef for TrimString { + fn as_ref (&self) -> &str { + self.1.as_ref() + } + } + + impl<'a, T: AsRef> TrimString { + fn to_ref (&self) -> TrimStr<'_, T> { + TrimStr(self.0, &self.1) + } + } + + /// Displays a borrowed [str]-like with fixed maximum width + /// + /// Width is computed using [unicode_width]. + pub struct TrimStr<'a, T: AsRef>(pub u16, pub &'a T); + + impl> AsRef for TrimStr<'_, T> { + fn as_ref (&self) -> &str { + self.1.as_ref() + } + } + + pub(crate) fn width_chars_max (max: u16, text: impl AsRef) -> u16 { + let mut width: u16 = 0; + let mut chars = text.as_ref().chars(); + while let Some(c) = chars.next() { + width += c.width().unwrap_or(0) as u16; + if width > max { + break + } + } + return width + } + + /// Trim string with [unicode_width]. + pub fn trim_string (max_width: usize, input: impl AsRef) -> String { + let input = input.as_ref(); + let mut output = Vec::with_capacity(input.len()); + let mut width: usize = 1; + let mut chars = input.chars(); + while let Some(c) = chars.next() { + if width > max_width { + break + } + output.push(c); + width += c.width().unwrap_or(0); + } + return output.into_iter().collect() + } +} + +#[cfg(feature = "term")] pub use self::term::*; +#[cfg(feature = "term")] mod term { + use crate::*; + use crate::draw::*; + use crate::layout::*; + use Color::*; + #[cfg(feature = "text")] use crate::text::*; + + #[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 Wide for Tui { fn w (&self) -> u16 { self.1.2 } } + impl Tall for Tui { fn h (&self) -> u16 { self.1.3 } } + impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } } + impl Xy for Tui { fn x (&self) -> u16 { self.1.0 } fn y (&self) -> u16 { self.1.1 } } + /// Terminal output. + pub struct Tui( + /// Ratatui buffer; area is screen size + pub Buffer, + /// Current draw area + pub 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(Buffer::empty(Rect { x: 0, y: 0, width, height }), XYWH(0, 0, width, height)) + } + pub fn resize (&mut self, back: &mut CrosstermBackend, width: u16, height: u16) { + let size = Rect { x: 0, y: 0, width, height }; + if self.0.area != size { + back.clear_region(ClearType::All).unwrap(); + self.0.resize(size); + self.0.reset(); + } + } + pub fn redraw <'b, W: Write> ( + &'b mut self, + back: &mut CrosstermBackend, + mut next: &'b mut Self + ) { + let updates = self.0.diff(&next.0); + back.draw(updates.into_iter()).expect("failed to render"); + Backend::flush(back).expect("failed to flush output new"); + std::mem::swap(self, &mut next); + next.0.reset(); + } + pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH { + for row in 0..self.h() { + let y = self.y() + row; + for col in 0..self.w() { + let x = self.x() + col; + if x < self.0.area.width && y < self.0.area.height { + if let Some(cell) = self.0.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