diff --git a/Cargo.toml b/Cargo.toml index 4811fb1..588f2a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,12 +5,10 @@ version = "0.15.0" description = "UI metaframework." [features] -default = ["lang", "sing", "midi", "draw", "play", "term", "text", "time", "rand", "okhsl", "eval", "exit"] +default = ["lang", "sing", "midi", "draw", "play", "term", "text", "time", "rand", "okhsl"] bumpalo = ["dep:bumpalo"] draw = [] gui = ["draw", "dep:winit"] -eval = [] -exit = [] lang = ["dep:dizzle"] midi = ["dep:midly"] okhsl = ["dep:palette"] diff --git a/Justfile b/Justfile index c2d4501..83c5796 100644 --- a/Justfile +++ b/Justfile @@ -29,5 +29,9 @@ doc: CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' RUSTDOCFLAGS='-Cinstrument-coverage' \ cargo doc -mode MODE: - cargo run --example "mode_{{MODE}}" +mode-00: + cargo run --example mode_00 +mode-01: + cargo run --example mode_01 +mode-02: + cargo run --example mode_02 diff --git a/dizzle b/dizzle index e768064..0f06571 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit e768064b3002dbf1aeaa143a45d54030305b4beb +Subproject commit 0f06571f7fc7e5f87aadb82d531726d24bf769e8 diff --git a/examples/mode_00.rs b/examples/mode_00.rs index f53db74..31b148d 100644 --- a/examples/mode_00.rs +++ b/examples/mode_00.rs @@ -1,22 +1,21 @@ //! Mode 00: Direct draw, direct control -use ::tengri::{ - *, - dizzle::*, - crossterm::event::{KeyEvent, Event::*, KeyCode::*}, - ratatui::style::Color, -}; +use ::std::sync::{Arc, RwLock}; +use ::crossterm::event::{Event::*, KeyEvent, KeyCode::*}; +use ::ratatui::style::Color; +use ::tengri::{*, lang::*}; tui_app!(State { /** User-controllable value. */ - counter: usize, + cursor: usize, }); tui_view!(self: State { - draw(|to: &mut Tui|{ - let _ = "Demo [Mode 00]".align_sw().draw(to); + 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!("Counter:\n{}", self.counter).align_c().draw(to); + let _ = format!("Cursor: {}", self.cursor).align_c().draw(to); Ok(Some(to.area())) }) }); @@ -25,11 +24,11 @@ tui_keys!(self: State, input { Ok(if let Key(KeyEvent { code, .. }) = input.0 { match code { Up | Right => { - self.counter = (self.counter + 1) % 10; + self.cursor = (self.cursor + 1) % 10; () }, Down | Left => { - self.counter = if self.counter > 0 { self.counter - 1 } else { 10 - 1 }; + self.cursor = if self.cursor > 0 { self.cursor - 1 } else { 10 - 1 }; () }, _ => {} diff --git a/examples/mode_01.rs b/examples/mode_01.rs index 7392e15..d7ee626 100644 --- a/examples/mode_01.rs +++ b/examples/mode_01.rs @@ -1,11 +1,10 @@ //! Mode 01: Direct view, actions with history -use ::tengri::{ - *, - dizzle::{*, itertools::Itertools}, - crossterm::event::{Event::*, KeyEvent, KeyCode::*}, - ratatui::style::Color, -}; +use ::std::sync::{Arc, RwLock}; +use ::crossterm::event::{Event::*, KeyEvent, KeyCode::*}; +use ::ratatui::style::Color; +use ::tengri::{*, lang::*}; +use itertools::Itertools; tui_app!(State { /** Command history (undo/redo). */ @@ -27,10 +26,10 @@ tui_keys!(self: State, input { }); tui_view!(self: State { - let title = "Demo [Mode 01]"; + 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!("Counter: {}", self.cursor); + let cursor = format!("Cursor: {}", 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 a2106b3..70cec30 100644 --- a/examples/mode_02.rs +++ b/examples/mode_02.rs @@ -1,12 +1,9 @@ //! Mode 02: Inline Dizzle config -use ::tengri::{ - *, - dizzle::*, - crossterm::event::{Event::*, KeyEvent, KeyCode::*}, - ratatui::style::Color, -}; - +use ::std::sync::{Arc, RwLock}; +use ::crossterm::event::{Event::*, KeyEvent, KeyCode::*}; +use ::ratatui::style::Color; +use ::tengri::{*, lang::*}; tui_app!(State { /** Command history (undo/redo). */ history: Vec, @@ -15,55 +12,6 @@ tui_app!(State { /** Rendered window size. */ size: Sizer, }); - -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!("Demo [Mode 02]\nExample {}/{}\nSize {:?}", index, VIEWS.len(), &wh); - let title = bg(Color::Rgb(60, 10, 10), heading); - let code = bg(Color::Rgb(10, 60, 10), format!("Source:\n{}", src).max_h(10)); - let widget = draw(move|to: &mut Tui|self.interpret(to, &src)); // FIXME this forcefills - let sidebar = bg(Color::Rgb(20, 20, 20), north(code, title)).max_w(40); - let content = bg(Color::Rgb(64, 64, 64), widget); - east(sidebar, ShowSizeOf(content)) - //self.size.of(bg(Color::Rgb(10, 10, 10), south(east(sidebar, content), code)).full_w()) - //self.size.of(bg(Color::Rgb(10, 10, 10), east(sidebar, content)).full_w()) -}); - -impl_keywords!(Tui, XYWH, State [ - kw_when, - kw_either, - kw_split, - kw_align, - kw_exact, - kw_fixed, - kw_min, - kw_max, - kw_push, - kw_tui_text, - kw_tui_fg, - kw_tui_bg -]); - -impl Interpret>> for State { - fn interpret_word (&self, to: &mut Tui, sym: &impl Language) -> Perhaps> { - match sym.src()? { - Some(":foo") => "foo".draw(to), - Some(":bar") => "bar".draw(to), - Some(":foobar") => "FOOBAR".draw(to), - src => todo!("src: {src:?}") - } - } - fn interpret_expr (&self, to: &mut Tui, src: &impl Expression) -> Perhaps> { - if let Some(area) = self.keyword(to, src)? { - Ok(Some(area)) - } else { - Err(format!("App::interpret_expr: unexpected: {src:?}").into()) - } - } -} - tui_keys!(self: State, input { Ok(if let Key(KeyEvent { code, .. }) = input.0 { match code { @@ -73,7 +21,57 @@ tui_keys!(self: State, input { } }) }); - +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 widget = thunk(move|to: &mut Tui|self.interpret(to, &src)); + self.size.of(south(title, north(code, 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()? { + Some(":foo") => "foo".draw(to), + Some(":bar") => "bar".draw(to), + Some(":foobar") => "FOOBAR".draw(to), + _ => todo!() + } + } + 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)? { + area + } else { + return Err(format!("App::interpret_expr: unexpected: {src:?}").into()) + })) + } +} impl State { fn next (&mut self) -> Perhaps { self.cursor = (self.cursor + 1) % VIEWS.len(); @@ -84,30 +82,22 @@ 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! { (fill/xy :foobar) }, 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)) }, @@ -165,7 +155,6 @@ 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/draw.rs b/src/draw.rs new file mode 100644 index 0000000..9d27ad3 --- /dev/null +++ b/src/draw.rs @@ -0,0 +1,161 @@ +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 new file mode 100644 index 0000000..ccea5e6 --- /dev/null +++ b/src/draw/color.rs @@ -0,0 +1,163 @@ +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 new file mode 100644 index 0000000..6f0b611 --- /dev/null +++ b/src/draw/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/layout/iter.rs b/src/draw/iter.rs similarity index 87% rename from src/layout/iter.rs rename to src/draw/iter.rs index e1cb3d9..1bbdcfa 100644 --- a/src/layout/iter.rs +++ b/src/draw/iter.rs @@ -4,14 +4,14 @@ use super::*; pub fn iter <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, ) -> impl Draw { - draw(move|_to: &mut S|{ todo!() }) + thunk(move|_to: &mut S|{ todo!() }) } /// Iterate over a collection of the same kind of [Draw]able: pub fn iter_once <'a, S: Screen, D: 'a, U: Draw> ( _iter: impl Iterator, _draw: impl Fn(D, usize)->U, ) -> impl Draw { - draw(move|_to: &mut S|{ todo!() }) + thunk(move|_to: &mut S|{ todo!() }) } /// Iterate over a collection of various [Draw]ables: @@ -22,43 +22,43 @@ pub fn iter_dyn < I: Iterator, // Type of the iterator F: Fn(&D, usize)->dyn Draw, // Function that returns [Draw]able from iterator item > (_items: V, _cb: F) -> impl Draw { - draw(move|_to: &mut S|{ todo!() }) + thunk(move|_to: &mut S|{ todo!() }) } pub fn iter_north <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, ) -> impl Draw { - draw(move|_to: &mut S|{ todo!() }) + thunk(move|_to: &mut S|{ todo!() }) } pub fn iter_east <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, ) -> impl Draw { - draw(move|_to: &mut S|{ todo!() }) + thunk(move|_to: &mut S|{ todo!() }) } pub fn iter_east_fixed <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( _height: S::Unit, _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, ) -> impl Draw { - draw(move|_to: &mut S|{ todo!() }) + thunk(move|_to: &mut S|{ todo!() }) } pub fn iter_south <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, ) -> impl Draw { - draw(move|_to: &mut S|{ todo!() }) + thunk(move|_to: &mut S|{ todo!() }) } pub fn iter_south_fixed <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( _height: S::Unit, _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, ) -> impl Draw { - draw(move|_to: &mut S|{ todo!() }) + thunk(move|_to: &mut S|{ todo!() }) } pub fn iter_west <'a, S: Screen, D: 'a, I: Iterator, U: Draw> ( _iter: impl Fn()->I, _draw: impl Fn(D, usize)->U, ) -> impl Draw { - draw(move|_to: &mut S|{ todo!() }) + thunk(move|_to: &mut S|{ todo!() }) } pub fn row_south (south: S::Unit, height: S::Unit, content: impl Draw) diff --git a/src/draw/layout.rs b/src/draw/layout.rs new file mode 100644 index 0000000..fba4d9b --- /dev/null +++ b/src/draw/layout.rs @@ -0,0 +1,437 @@ +#![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 new file mode 100644 index 0000000..586f912 --- /dev/null +++ b/src/draw/lrtb.rs @@ -0,0 +1,47 @@ +use crate::*; +use Azimuth::*; + +impl> Lrtb for T {} + +pub trait Lrtb: Xywh { + fn lrtb (&self) -> [N;4] { + // FIXME: factor origin + [self.x(), self.y(), self.x()+self.w(), self.y()+self.h()] + } + fn iter_x (&self) -> std::ops::Range where Self: HasOrigin { + self.x_west()..self.x_east() + } + fn x_west (&self) -> N where Self: HasOrigin { + let w = self.w(); + let a = self.origin(); + let d = match a { NW|W|SW => 0.into(), N|X|C|Y|S => w/2.into(), NE|E|SE => w }; + self.x().minus(d) + } + fn x_east (&self) -> N where Self: HasOrigin { + let w = self.w(); + let a = self.origin(); + let d = match a { NW|W|SW => w, N|X|C|Y|S => w/2.into(), NE|E|SE => 0.into() }; + self.x().plus(d) + } + fn x_center (&self) -> N where Self: HasOrigin { + todo!() + } + fn iter_y (&self) -> std::ops::Range where Self: HasOrigin { + self.y_north()..self.y_south() + } + fn y_north (&self) -> N where Self: HasOrigin { + let a = self.origin(); + let h = self.h(); + let d = match a { NW|N|NE => 0.into(), W|X|C|Y|E => h/2.into(), SW|S|SE => h }; + self.y().minus(d) + } + fn y_south (&self) -> N where Self: HasOrigin { + let a = self.origin(); + let h = self.h(); + let d = match a { NW|N|NE => h, W|X|C|Y|E => h/2.into(), SW|S|SE => 0.into() }; + self.y().plus(d) + } + fn y_center (&self) -> N where Self: HasOrigin { + todo!() + } +} diff --git a/src/draw/sizer.rs b/src/draw/sizer.rs new file mode 100644 index 0000000..5e20693 --- /dev/null +++ b/src/draw/sizer.rs @@ -0,0 +1,45 @@ +use super::*; +use std::sync::atomic::Ordering; + +/// Uses [AtomicUsize] to measure size during\ +/// rendering (which is normally read-only). +#[derive(Default, Debug, Clone)] +pub struct Sizer( + /// Width + pub Arc, + /// Height + pub Arc, +); + +impl Xy for Sizer { + fn x (&self) -> u16 { self.0.load(Ordering::Relaxed) as u16 } + fn y (&self) -> u16 { self.1.load(Ordering::Relaxed) as u16 } +} +impl Wide for Sizer { fn w (&self) -> u16 { self.0.load(Relaxed) as u16 } } +impl Tall for Sizer { fn h (&self) -> u16 { self.1.load(Relaxed) as u16 } } +impl PartialEq for Sizer { fn eq (&self, _: &Self) -> bool { todo!() } } + +impl Sizer { + pub const fn of (&self, of: impl Draw) -> impl Draw { + thunk(move|to: &mut T|{ + let area = of.draw(to)?; + self.0.store(area.map(|a|a.w()).unwrap_or(T::Unit::zero()).into(), Relaxed); + self.1.store(area.map(|a|a.h()).unwrap_or(T::Unit::zero()).into(), Relaxed); + Ok(area) + }) + } +} + +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/layout/split.rs b/src/draw/split.rs similarity index 65% rename from src/layout/split.rs rename to src/draw/split.rs index 555c75a..8408f57 100644 --- a/src/layout/split.rs +++ b/src/draw/split.rs @@ -1,5 +1,29 @@ use super::*; +pub const fn east , B: Draw> (a: A, b: B) -> impl Draw { + Split::East.half(a, b) +} + +pub const fn north , B: Draw> (a: A, b: B) -> impl Draw { + Split::North.half(a, b) +} + +pub const fn west , B: Draw> (a: A, b: B) -> impl Draw { + Split::West.half(a, b) +} + +pub const fn south , B: Draw> (a: A, b: B) -> impl Draw { + Split::South.half(a, b) +} + +pub const fn above , B: Draw> (a: A, b: B) -> impl Draw { + Split::Above.half(a, b) +} + +pub const fn below , B: Draw> (a: A, b: B) -> impl Draw { + Split::Below.half(a, b) +} + /// Split along an axis. Direction determines order. #[cfg_attr(test, derive(Arbitrary))] #[derive(Copy, Clone, PartialEq, Debug, Default)] pub enum Split { @@ -11,28 +35,6 @@ use super::*; #[default] Below } -pub struct Pair, B: Draw>(Split, A, B, PhantomData); - -pub fn split , B: Draw> ( - split: Split, a: A, b: B -) -> Pair { - Pair(split, a, b, PhantomData) -} - -impl, B: Draw> Draw for Pair { - fn layout (&self, to: XYWH) -> Drawn { - let Self(split, a, b, ..) = self; - let (area_a, area_b) = stack_areas(split, to, a, b)?; - Ok(stack_drawn(split, area_a, area_b)) - } - fn draw (self, to: &mut S) -> Drawn { - let Self(ref split, a, b, ..) = self; - let (area_a, area_b) = stack_areas(split, to.area(), &a, &b)?; - let (drawn_a, drawn_b) = draw_stacks(split, to, a, area_a, None, b, area_b, None)?; - Ok(stack_drawn(split, drawn_a, drawn_b)) - } -} - impl Split { /// ``` @@ -45,7 +47,11 @@ impl Split { /// let _ = Split::West.stack("", ""); /// ``` pub const fn stack , B: Draw> (&self, a: A, b: B) -> impl Draw { - Pair(*self, a, b, PhantomData) + thunk(move|to: &mut S|{ + let (area_a, area_b) = stack_areas(self, to.area(), &a, &b)?; + let (drawn_a, drawn_b) = draw_stacks(self, to, a, area_a, None, b, area_b, None)?; + Ok(stack_drawn(self, drawn_a, drawn_b)) + }) } /// ``` @@ -58,7 +64,7 @@ impl Split { /// let _ = Split::West.half("", ""); /// ``` pub const fn half , B: Draw> (&self, a: A, b: B) -> impl Draw { - draw(move|to: &mut S|{ + thunk(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)?; @@ -121,26 +127,21 @@ fn draw_stacks ( area_b: impl Into>>, origin_b: impl Into>, ) -> Usually<(Option>, Option>)> { - let draw_a = |to: &mut S|Ok::<_, Box>(if let Some(origin_a) = origin_a.into() { - to.clip(area_a.into(), |to|a.align(origin_a).draw(to))? - } else { - to.clip(area_a.into(), |to|a.draw(to))? - }); - let draw_b = |to: &mut S|Ok::<_, Box>(if let Some(origin_b) = origin_b.into() { - to.clip(area_b.into(), |to|b.align(origin_b).draw(to))? - } else { - to.clip(area_b.into(), |to|b.draw(to))? - }); - Ok(if matches!(split, Split::Below) { - let drawn_b = draw_b(to)?; - let drawn_a = draw_a(to)?; - (drawn_a, drawn_b) - } else { - (draw_a(to)?, draw_b(to)?) + Ok(match split { + Split::Below => { + let drawn_b = to.clip(area_b.into(), |to|b.align(origin_b.into()).draw(to))?; + let drawn_a = to.clip(area_a.into(), |to|a.align(origin_a.into()).draw(to))?; + (drawn_a, drawn_b) + }, + _ => { + let drawn_a = to.clip(area_a.into(), |to|a.align(origin_a.into()).draw(to))?; + let drawn_b = to.clip(area_b.into(), |to|b.align(origin_b.into()).draw(to))?; + (drawn_a, drawn_b) + } }) } -pub fn stack_areas ( +fn stack_areas ( split: &Split, area: XYWH, a: &impl Draw, @@ -151,7 +152,9 @@ pub 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 } @@ -159,19 +162,23 @@ pub 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 } ), Split::North => ( if let Some(used) = area_a { - Some(XYWH(used.x(), (area.y() + area.h()).minus(used.h()), used.w(), used.h())) + Some(XYWH(used.x(), area.y() + used.h(), used.w(), used.h())) } else { None }, if let Some(used) = area_a { - b.layout(XYWH(area.x(), area.y(), area.w(), area.h().minus(used.h())))? + 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() @@ -180,12 +187,14 @@ pub fn stack_areas ( ), Split::West => ( if let Some(used) = area_a { - Some(XYWH((area.x() + area.w()).minus(used.w()), used.y(), used.w(), used.h())) + Some(XYWH(area.x() + used.w(), used.y(), used.w(), used.h())) } else { None }, if let Some(used) = area_a { - b.layout(XYWH(area.x(), area.y(), area.w().minus(used.w()), area.h()))? + 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() @@ -251,65 +260,3 @@ fn stack_drawn ( ($head:expr $(,)?) => { $head }; ($head:expr, $($tail:expr),* $(,)?) => { below($head, below!($($tail,)*)) }; } - -pub const fn east , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::East, a, b, PhantomData) -} - -pub const fn north , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::North, a, b, PhantomData) -} - -pub const fn west , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::West, a, b, PhantomData) -} - -pub const fn south , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::South, a, b, PhantomData) -} - -pub const fn above , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::Above, a, b, PhantomData) -} - -pub const fn below , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::Below, a, b, PhantomData) -} - -#[cfg(test)] mod test { - use crate::*; - - #[test] fn test_stack_areas () -> Usually<()> { - let area = XYWH(0u16, 0, 80, 25); - - assert_eq!(stack_areas(&Split::East, area, &"foo", &"bar")?, ( - Some(XYWH(0u16, 0, 3, 1)), - Some(XYWH(3u16, 0, 3, 1)), - )); - - assert_eq!(stack_areas(&Split::South, area, &"foo", &"bar")?, ( - Some(XYWH(0u16, 0, 3, 1)), - Some(XYWH(0u16, 1, 3, 1)), - )); - - Ok(()) - } - - #[test] fn test_split_stack () -> Usually<()> { - use tengri::{*, Split::*}; - - let stack = split(East, "foo", "bar"); - assert_eq!(stack.layout(XYWH(0, 0, 80, 25))?, Some(XYWH(0, 0, 6, 1))); - - let stack = split(South, "foo", "bar"); - assert_eq!(stack.layout(XYWH(0, 0, 80, 25))?, Some(XYWH(0, 0, 3, 2))); - - let stack = split(South, split(East, "foo", "bar"), "baz"); - assert_eq!(stack.layout(XYWH(0, 0, 80, 25))?, Some(XYWH(0, 0, 6, 2))); - - let stack = split(East, split(South, "foo", "bar"), "baz"); - assert_eq!(stack.layout(XYWH(0, 0, 80, 25))?, Some(XYWH(0, 0, 6, 2))); - - Ok(()) - } -} diff --git a/src/draw/thunk.rs b/src/draw/thunk.rs new file mode 100644 index 0000000..af27f5b --- /dev/null +++ b/src/draw/thunk.rs @@ -0,0 +1,48 @@ +use crate::*; + +/// Because we can't implement [Draw] for `F: FnOnce...` without conflicts. +pub struct Thunk(pub F, std::marker::PhantomData); + +implPerhaps>> Draw for Thunk { + fn draw (self, to: &mut T) -> Perhaps> { + (self.0)(to) + } +} + +/// Basic [Draw]able closure. +/// +/// ``` +/// # use tengri::*; +/// # fn test () -> impl Draw { +/// thunk(|to: &mut Tui|Ok(Some(to.1))) +/// # } +/// ``` +pub const fn thunk Perhaps>> ( + draw: F +) -> Thunk { + Thunk(draw, std::marker::PhantomData) +} + +/// Only render when condition is true. +/// +/// ``` +/// # use tengri::*; +/// # fn test () -> impl Draw { +/// when(true, "Yes") +/// # } +/// ``` +pub const fn when (condition: bool, draw: impl Draw) -> impl Draw { + thunk(move|to: &mut T|if condition { draw.draw(to) } else { Ok(Default::default()) }) +} + +/// Render one thing if a condition is true and another false. +/// +/// ``` +/// # use tengri::*; +/// # fn test () -> impl Draw { +/// either(true, "Yes", "No") +/// # } +/// ``` +pub const fn either (condition: bool, a: impl Draw, b: impl Draw) -> impl Draw { + thunk(move|to: &mut T|if condition { a.draw(to) } else { b.draw(to) }) +} diff --git a/src/draw/xywh.rs b/src/draw/xywh.rs new file mode 100644 index 0000000..5b308d1 --- /dev/null +++ b/src/draw/xywh.rs @@ -0,0 +1,115 @@ +use super::*; +use Split::*; + +pub trait Xy { + fn x (&self) -> N; + fn y (&self) -> N; +} + +pub trait Wh: Wide + Tall { + fn wh (&self) -> [N;2]; +} + +pub trait Xywh: Xy + Wh { + fn xywh (&self) -> XYWH { + XYWH(self.x(), self.y(), self.w(), self.h()) + } +} + +pub trait Wide: Xy { + fn w (&self) -> N { N::zero() } + fn w_min (&self) -> N { self.w() } + fn w_max (&self) -> N { self.w() } +} + +pub trait Tall { + fn h (&self) -> N { N::zero() } + fn h_min (&self) -> N { self.h() } + fn h_max (&self) -> N { self.h() } +} + +/// Point with size. +/// +/// ``` +/// # use tengri::*; +/// let xywh = XYWH(0u16, 0, 0, 0); +/// assert_eq!(XYWH(10u16, 10, 20, 20).center(), (20, 20)); +/// ``` +/// +/// * [ ] TODO: origin field (determines at which corner/side is X0 Y0) +/// +#[cfg_attr(test, derive(Arbitrary))] #[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct XYWH(pub N, pub N, pub N, pub N); + +impl Xy for XYWH { + fn x (&self) -> N { self.0 } + fn y (&self) -> N { self.1 } +} + +impl Wide for XYWH { fn w (&self) -> N { self.2 } } + +impl Tall for XYWH { fn h (&self) -> N { self.3 } } + +impl XYWH { + + pub fn zero () -> Self { + Self(0.into(), 0.into(), 0.into(), 0.into()) + } + + pub fn center (&self) -> (N, N) { + let Self(x, y, w, h) = *self; + (x.plus(w/2.into()), y.plus(h/2.into())) + } + + pub fn centered (&self) -> (N, N) { + let Self(x, y, w, h) = *self; + (x.minus(w/2.into()), y.minus(h/2.into())) + } + + pub fn centered_x (&self, n: N) -> Self { + let Self(x, y, w, h) = *self; + let x_center = (x.plus(w / 2.into())).minus(n / 2.into()); + let y_center = y.plus(h / 2.into()); + XYWH(x_center, y_center, n, 1.into()) + } + + pub fn centered_y (&self, n: N) -> Self { + let Self(x, y, w, h) = *self; + let x_center = x.plus(w / 2.into()); + let y_corner = (y.plus(h / 2.into())).minus(n / 2.into()); + XYWH(x_center, y_corner, 1.into(), n) + } + + pub fn centered_xy (&self, [n, m]: [N;2]) -> Self { + let Self(x, y, w, h) = *self; + let x_center = (x.plus(w / 2.into())).minus(n / 2.into()); + let y_corner = (y.plus(h / 2.into())).minus(m / 2.into()); + XYWH(x_center, y_corner, n, m) + } + + pub fn split_half (&self, direction: &Split) -> (Self, Self) { + let XYWH(x, y, w, h) = self.xywh(); + match direction { + South => (XYWH(x, y, w, h - h / 2.into()), XYWH(x, y + h / 2.into(), w, h / 2.into())), + East => (XYWH(x, y, w - w / 2.into(), h), XYWH(x + w / 2.into(), y, w / 2.into(), h)), + North => (XYWH(x, y + h / 2.into(), w, h - h / 2.into()), XYWH(x, y, w, h / 2.into())), + West => (XYWH(x + w / 2.into(), y, w - w / 2.into(), h), XYWH(x, y, w / 2.into(), h)), + Above | Below => (XYWH(x, y, w, h), XYWH(x, y, w, h)) + } + } + +} + +impl From<&ratatui::prelude::Rect> for XYWH { + fn from (rect: &ratatui::prelude::Rect) -> Self { + Self(rect.x, rect.y, rect.width, rect.height) + } +} + +impl + Tall> Wh for T { + fn wh (&self) -> [N;2] { + [self.w(), self.h()] + } +} + +impl + Wh> Xywh for T {} diff --git a/src/eval.rs b/src/eval.rs new file mode 100644 index 0000000..e0df0f1 --- /dev/null +++ b/src/eval.rs @@ -0,0 +1,298 @@ +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 new file mode 100644 index 0000000..8d96419 --- /dev/null +++ b/src/exit.rs @@ -0,0 +1,24 @@ +use crate::*; +use std::sync::{Arc, atomic::AtomicBool}; + +#[derive(Clone)] pub struct Exit(Arc); + +impl Exit { + pub fn run (run: impl FnOnce(Self)->Usually) -> Usually { + run(Self(Arc::new(AtomicBool::new(false)))) + } + pub fn is (event: &Event) -> bool { + matches!(event, Event::Key(KeyEvent { + modifiers: KeyModifiers::CONTROL, + code: KeyCode::Char('c'), + kind: KeyEventKind::Press, + state: KeyEventState::NONE + })) + } +} + +impl AsRef> for Exit { + fn as_ref (&self) -> &Arc { + &self.0 + } +} diff --git a/src/layout/align.rs b/src/layout/align.rs deleted file mode 100644 index 1e1c961..0000000 --- a/src/layout/align.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::*; -use Azimuth::*; - -pub struct Align( - pub(crate) Option, - pub(crate) T, -); - -impl> Draw for Align { - fn layout (&self, area: XYWH) -> Perhaps> { - Ok(align::(area, self.1.layout(area)?, self.0)) - } - fn draw (self, to: &mut S) -> Perhaps> { - Ok(self.layout(to.area())? - .map(|area|to.clip(area, |to|self.1.draw(to))) - .transpose()? - .flatten()) - } -} - -fn align ( - area0: XYWH, area: Option>, azimuth: Option -) -> Option> { - area.map(|XYWH(x, y, w, h)|{ - let XYWH(x0, y0, w0, h0) = area0; - match azimuth { - Some(NW) => XYWH(x0, y0, w, h), - Some(N) => XYWH(x0 + w0.minus(w) / 2.into(), y0, w, h), - Some(NE) => XYWH((x0 + w0).minus(w), y0, w, h), - Some(W) => XYWH(x0, y0 + h0.minus(h) / 2.into(), w, h), - Some(C) => XYWH(x0 + w0.minus(w) / 2.into(), y0 + h0.minus(h) / 2.into(), w, h), - Some(E) => XYWH((x0 + w0).minus(w), y0 + h0.minus(h) / 2.into(), w, h), - Some(SW) => XYWH(x0, (y0 + h0).minus(h), w, h), - Some(S) => XYWH(x0 + w0.minus(w) / 2.into(), (y0 + h0).minus(h), w, h), - Some(SE) => XYWH((x0 + w0).minus(w), (y0 + h0).minus(h), w, h), - Some(X) => XYWH(x0 + w0.minus(w) / 2.into(), y, w, h), - Some(Y) => XYWH(x, y0 + h0.minus(h) / 2.into(), w, h), - None => XYWH(x, y, w, h) - } - }) -} diff --git a/src/layout/area.rs b/src/layout/area.rs deleted file mode 100644 index b6d68da..0000000 --- a/src/layout/area.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::*; - -/// ``` -/// 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 deleted file mode 100644 index e94f51d..0000000 --- a/src/layout/exact.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::*; - -/// Set size of of drawing area. -/// -/// ``` -/// use tengri::Layout; -/// let _ = "".exact_w(1); -/// let _ = "".exact_h(1); -/// let _ = "".exact_wh(1, 1); -/// ``` -pub enum Exact, X: Into>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl , X: Into> + Copy> Draw for Exact { - fn layout (&self, area: XYWH) -> Perhaps> { - Ok(Some(layout_exact(self, area))) - } - fn draw (self, to: &mut S) -> Perhaps> { - let area = layout_exact(&self, to.area()); - let item = match self { Self::W(i, ..) => i, Self::H(i, ..) => i, Self::WH(i, ..) => i, _ => unreachable!() }; - to.clip(area, |to|item.draw(to)) - } -} - -fn layout_exact , X: Into> + Copy> ( - exact: &Exact, area: XYWH -) -> XYWH { - let (w, h): (S::Unit, S::Unit) = match exact { - Exact::W(_, w) => { - let w: Option = (*w).into(); - (w.unwrap_or(area.2), area.3) - }, - Exact::H(_, h) => { - let h: Option = (*h).into(); - (area.2, h.unwrap_or(area.3)) - }, - Exact::WH(_, w, h) => { - let w: Option = (*w).into(); - let h: Option = (*h).into(); - (w.unwrap_or(area.2), w.unwrap_or(area.3)) - }, - _ => unreachable!() - }; - XYWH(area.0, area.1, w, h) -} diff --git a/src/layout/full.rs b/src/layout/full.rs deleted file mode 100644 index 5dbd40f..0000000 --- a/src/layout/full.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::*; - -/// Use whole drawing area along one or both axes. -/// -/// ``` -/// # fn doctest_layout_full () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(0u16, 0, 80, 25); -/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); -/// assert_eq!("1".full_w().layout(area)?, Some(XYWH(0u16, 0, 80, 1))); -/// assert_eq!("1".full_h().layout(area)?, Some(XYWH(0u16, 0, 1, 25))); -/// assert_eq!("1".full_wh().layout(area)?, Some(XYWH(0u16, 0, 80, 25))); -/// # Ok(()) } -/// ``` -pub enum Full> { - __(PhantomData), - W(I), - H(I), - WH(I), -} -impl_draw!(,>|self: Full, to: T|{ - let XYWH(x0, y0, w0, h0) = to.area(); - match self { - Self::W(item) => if let Some(XYWH(_, y, _, h)) = item.layout(to.area())? { - to.clip(XYWH(x0, y, w0, h), |to|item.draw(to)) - } else { - Ok(None) - }, - Self::H(item) => if let Some(XYWH(x, _, w, _)) = item.layout(to.area())? { - to.clip(XYWH(x, y0, w, h0), |to|item.draw(to)) - } else { - Ok(None) - }, - Self::WH(item) => if let Some(XYWH(..)) = item.layout(to.area())? { - to.clip(XYWH(x0, y0, w0, h0), |to|item.draw(to)) - } else { - Ok(None) - }, - _ => unreachable!(), - } -}); diff --git a/src/layout/max.rs b/src/layout/max.rs deleted file mode 100644 index 9fc1c40..0000000 --- a/src/layout/max.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::*; - -/// Set maximum size of of drawing area. -/// -/// ``` -/// # fn doctest_layout_max () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(1u16, 1, 80, 25); -/// assert_eq!("12345".max_w(1).layout(area)?, Some(XYWH(1u16, 1, 1, 1))); -/// assert_eq!("12345".max_h(1).layout(area)?, Some(XYWH(1u16, 1, 1, 1))); -/// assert_eq!("12345".max_wh(1, 1).layout(area)?, Some(XYWH(1u16, 1, 5, 1))); -/// # Ok(()) } -/// ``` -pub enum Max, X: Into> + Copy> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl, X: Into> + Copy> Draw for Max { - fn layout (&self, area: XYWH) -> Perhaps> { - Ok(Some(match self { - Self::W(_, max_w) => XYWH( - area.0, area.1, (*max_w).into().map(|max|max.min(area.2)).unwrap_or(area.2), - area.3), - Self::H(_, max_h) => XYWH( - area.0, area.1, area.2, - (*max_h).into().map(|max|max.min(area.3)).unwrap_or(area.3)), - Self::WH(_, max_w, max_h) => XYWH( - area.0, area.1, (*max_w).into().map(|max|max.min(area.2)).unwrap_or(area.2), - (*max_h).into().map(|max|max.min(area.3)).unwrap_or(area.3)), - _ => return Ok(None) - })) - } - fn draw (self, to: &mut T) -> Perhaps> { - let area: XYWH = to.area(); - let (item, area) = match self { - Self::W(item, max_w) => (item, XYWH( - area.0, area.1, max_w.into().map(|max|max.min(area.2)).unwrap_or(area.2), - area.3 - )), - Self::H(item, max_h) => (item, XYWH( - area.0, area.1, area.2, - max_h.into().map(|max|max.min(area.3)).unwrap_or(area.3) - )), - Self::WH(item, max_w, max_h) => (item, XYWH( - area.0, area.1, max_w.into().map(|max|max.min(area.2)).unwrap_or(area.2), - max_h.into().map(|max|max.min(area.3)).unwrap_or(area.3), - )), - _ => return Ok(None) - }; - to.clip(area, |to|item.draw(to)) - } -} diff --git a/src/layout/min.rs b/src/layout/min.rs deleted file mode 100644 index d747253..0000000 --- a/src/layout/min.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::*; - -/// Only draw content if area is above a certain size. -/// -/// ``` -/// # fn doctest_layout_min () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(1u16, 1, 80, 25); -/// assert_eq!("1".min_w(5).layout(area)?, Some(XYWH(1u16, 1, 5, 1))); -/// assert_eq!("1".min_h(5).layout(area)?, Some(XYWH(1u16, 1, 1, 5))); -/// assert_eq!("1".min_wh(5, 5).layout(area)?, Some(XYWH(1u16, 1, 5, 5))); -/// assert_eq!("123456".min_w(5).layout(area)?, Some(XYWH(1u16, 1, 6, 1))); -/// # Ok(()) } -/// ``` -pub enum Min, X: Into>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Min, to: T|{ - match self { - Self::__(_) => unreachable!(), - Self::W(item, w1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - let w = w1.into().map(|w1|w.max(w1)).unwrap_or(w); - to.clip(XYWH(x, y, w, h), |to|item.draw(to)) - }, - Self::H(item, h1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - let h = h1.into().map(|h1|h.max(h1)).unwrap_or(h); - to.clip(XYWH(x, y, w, h), |to|item.draw(to)) - }, - Self::WH(item, w1, h1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - let w = w1.into().map(|w1|w.max(w1)).unwrap_or(w); - let h = h1.into().map(|h1|h.max(h1)).unwrap_or(h); - to.clip(XYWH(x, y, w, h), |to|item.draw(to)) - }, - _ => Ok(None) - } -}); diff --git a/src/layout/origin.rs b/src/layout/origin.rs deleted file mode 100644 index e5aaff9..0000000 --- a/src/layout/origin.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::*; - -pub struct Origin( - pub(crate) Option, - pub(crate) T -); - -impl_draw!(,>|self: Origin, _to: S|{ - todo!() -}); - -/// Something that has `[0, 0]` at a particular point. -pub trait HasOrigin { - fn origin (&self) -> Azimuth; -} - -impl> HasOrigin for T { - fn origin (&self) -> Azimuth { - *self.as_ref() - } -} diff --git a/src/layout/pad.rs b/src/layout/pad.rs deleted file mode 100644 index 6ac2b4a..0000000 --- a/src/layout/pad.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::*; - -/// Define inner drawing area. -/// -/// ``` -/// use tengri::Layout; -/// let _ = "".pad_w(1); -/// let _ = "".pad_h(1); -/// let _ = "".pad_wh(1, 1); -/// ``` -pub enum Pad, X: Into>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Pad, to: T|{ - let area = to.area(); - let (item, area) = match self { - Self::W(item, w1) => { - let w1 = w1.into().unwrap_or(T::Unit::zero()); - (item, XYWH(area.0 + w1, area.1, area.2.minus(w1 + w1), area.3)) - }, - Self::H(item, h1) => { - let h1 = h1.into().unwrap_or(T::Unit::zero()); - (item, XYWH(area.0, area.1 + h1, area.2, area.3.minus(h1 + h1))) - }, - Self::WH(item, w1, h1) => { - let w1 = w1.into().unwrap_or(T::Unit::zero()); - let h1 = h1.into().unwrap_or(T::Unit::zero()); - (item, XYWH(area.0 + w1, area.1 + h1, area.2.minus(w1 + w1), area.3.minus(h1 + h1))) - }, - _ => return Ok(None) - }; - item.draw(to) -}); diff --git a/src/layout/pull.rs b/src/layout/pull.rs deleted file mode 100644 index 287a998..0000000 --- a/src/layout/pull.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::*; - -/// Move content in the negative direction of one or both axes. -/// -/// ``` -/// # fn doctest_layout_pull () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(1u16, 1, 80, 25); -/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); -/// assert_eq!("1".pull_x(1).layout(area)?, Some(XYWH(0u16, 1, 1, 1))); -/// assert_eq!("1".pull_y(1).layout(area)?, Some(XYWH(1u16, 0, 1, 1))); -/// assert_eq!("1".pull_xy(1, 1).layout(area)?, Some(XYWH(0u16, 0, 1, 1))); -/// # Ok(()) } -/// ``` -pub enum Pull, X: Into>> { - __(PhantomData), - X(I, X), - Y(I, X), - XY(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Pull, _to: T|{ - todo!() -}); diff --git a/src/layout/push.rs b/src/layout/push.rs deleted file mode 100644 index 7d1114d..0000000 --- a/src/layout/push.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::*; - -/// Move content in the positive direction of one or both axes. -/// -/// ``` -/// # fn doctest_layout_push () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(0u16, 0, 80, 25); -/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); -/// assert_eq!("1".push_x(1).layout(area)?, Some(XYWH(1u16, 0, 1, 1))); -/// assert_eq!("1".push_y(1).layout(area)?, Some(XYWH(0u16, 1, 1, 1))); -/// assert_eq!("1".push_xy(1, 1).layout(area)?, Some(XYWH(1u16, 1, 1, 1))); -/// # Ok(()) } -/// ``` -pub enum Push, X: Into>> { - __(PhantomData), - X(I, X), - Y(I, X), - XY(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Push, to: T|{ - match self { - Self::__(_) => unreachable!(), - Self::X(item, x1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - to.clip(XYWH( - x + x1.into().unwrap_or_default(), y, w, h - ), |to|item.draw(to)) - }, - Self::Y(item, y1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - to.clip(XYWH( - x, y + y1.into().unwrap_or_default(), w, h - ), |to|item.draw(to)) - }, - Self::XY(item, x1, y1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - to.clip(XYWH( - x + x1.into().unwrap_or_default(), y + y1.into().unwrap_or_default(), w, h - ), |to|item.draw(to)) - }, - _ => Ok(None) - } -}); diff --git a/src/lib.rs b/src/lib.rs index 3a5342e..4b6fcfb 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; +#[cfg(feature = "lang")] pub extern crate dizzle as lang; #[cfg(test)] #[macro_use] pub extern crate proptest; #[cfg(test)] pub(crate) use proptest_derive::Arbitrary; @@ -41,39 +41,16 @@ macro_rules! features { } } -macro_rules! fn_kw_layout { - ($name:ident |$state:ident, $output: ident, $expr:ident| $body:block) => { - pub fn $name ( - $state: &S, $output: &mut O, $expr: &str - ) -> Perhaps> where - S: Interpret>> - + for<'b> Namespace<'b, bool> - + for<'b> Namespace<'b, O::Unit> - + for<'b> Namespace<'b, Option> - $body - } -} - -macro_rules! fn_kw_layout_tui { - ($name:ident |$state:ident, $output: ident, $expr:ident| $body:block) => { - pub fn $name ( - $state: &S, $output: &mut Tui, $expr: &str - ) -> Perhaps> where - S: Interpret>> - + for<'b> Namespace<'b, bool> - + for<'b> Namespace<'b, u16> - + for<'b> Namespace<'b, Option> - + for<'b> Namespace<'b, Color> - $body - } -} - -#[cfg(feature = "lang")] pub use ::dizzle::{Usually, Perhaps}; -#[cfg(feature = "lang")] use ::dizzle::*; +#[cfg(feature = "lang")] pub use ::dizzle::{Usually, Perhaps, impl_default}; features! { + "lang": [ eval ], "time": [ time ], - "sing": [ sing ] + "play": [ exit, task ], + "sing": [ sing ], + "text": [ text ], + "term": [ term ], + "draw": [ draw ] } /// Define a trait an implement it for various mutation-enabled wrapper types. */ @@ -107,6 +84,26 @@ 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) => { @@ -126,2053 +123,3 @@ 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:expr, $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:expr, - ) => {{ - // frags.next(): 2nd slash-delimited fragment: /x, /y, /xy - let variant = $variant; - let thunk = draw(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:expr, $arg1:expr, $arg2:expr, - ) => {{ - // frags.next(): 2nd slash-delimited fragment: /x, /y, /xy - let variant = $variant; - let thunk = draw(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 = "exit")] pub use self::exit::*; -#[cfg(feature = "exit")] mod exit { - use crate::*; - use std::sync::{Arc, atomic::AtomicBool}; - use crossterm::event::*; - - #[derive(Clone)] pub struct Exit(Arc); - - impl Exit { - pub fn run (run: impl FnOnce(Self)->Usually) -> Usually { - run(Self(Arc::new(AtomicBool::new(false)))) - } - pub fn is (event: &Event) -> bool { - matches!(event, Event::Key(KeyEvent { - modifiers: KeyModifiers::CONTROL, - code: KeyCode::Char('c'), - kind: KeyEventKind::Press, - state: KeyEventState::NONE - })) - } - } - - impl AsRef> for Exit { - fn as_ref (&self) -> &Arc { - &self.0 - } - } -} - -#[cfg(feature = "play")] pub use self::task::*; -#[cfg(feature = "play")] mod task { - use std::{ - time::Duration, - sync::{Arc, atomic::{AtomicBool, Ordering::*}}, - thread::{Builder, JoinHandle, sleep}, - }; - #[cfg(feature = "term")] use ::crossterm::event::poll; - use crate::time::PerfModel; - - #[derive(Debug)] pub struct Task { - /// Exit flag. - pub exit: Arc, - /// Performance counter. - pub perf: Arc, - /// Use this to wait for the thread to finish. - pub join: JoinHandle<()>, - } - - impl Task { - /// Spawn a TUI thread that runs `callt least one, then repeats until `exit`. - pub fn new (exit: Arc, mut call: F) -> Result - where F: FnMut(&PerfModel)->() + Send + Sync + 'static - { - let perf = Arc::new(PerfModel::default()); - Ok(Self { - exit: exit.clone(), - perf: perf.clone(), - join: Builder::new().name("tengri tui output".into()).spawn(move || { - while !exit.fetch_and(true, Relaxed) { - let _ = perf.cycle(&mut call); - } - })?.into() - }) - } - - /// Spawn a thread that runs `call` least one, then repeats - /// until `exit`, sleeping for `time` msec after every iteration. - pub fn new_sleep ( - exit: Arc, time: Duration, mut call: F - ) -> Result - where F: FnMut(&PerfModel)->() + Send + Sync + 'static - { - Self::new(exit, move |perf| { - let _ = call(perf); - sleep(time); - }) - } - - /// Spawn a thread that uses [crossterm::event::poll] - /// to run `call` every `time` msec. - #[cfg(feature = "term")] pub fn new_poll ( - exit: Arc, time: Duration, mut call: F - ) -> Result - where F: FnMut(&PerfModel)->() + Send + Sync + 'static - { - Self::new(exit, move |perf| { - if poll(time).is_ok() { - let _ = call(perf); - } - }) - } - - pub fn join (self) -> Result<(), Box> { - self.join.join() - } - } -} - -#[cfg(feature = "draw")] pub use self::draw::*; -#[cfg(feature = "draw")] mod draw { - use crate::*; - use Azimuth::*; - use Split::*; - - /// Output target. - /// - /// ``` - /// use tengri::*; - /// struct TestOut { w: u16, h: u16 }; - /// impl Wide for TestOut {} - /// impl Tall for TestOut {} - /// impl Xy for TestOut { - /// fn x (&self) -> u16 { 0 } - /// fn y (&self) -> u16 { 0 } - /// } - /// impl Screen for TestOut { - /// type Unit = u16; - /// fn show (&mut self, _: impl Draw) -> Perhaps> { - /// println!("placed"); - /// Ok(None) - /// } - /// fn area (&self) -> XYWH { - /// Default::default() - /// } - /// fn clip ( - /// &mut self, - /// area: impl Into>>, - /// draw: impl FnOnce(&mut Self)->T - /// ) -> T { - /// draw(self) - /// } - /// } - /// - /// impl_draw!(|self: String, to: TestOut|{ - /// to.w = self.len() as u16; - /// Ok(None) - /// }); - /// ``` - 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)) - } - } - - /// 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 { - () - } - } - - /// Return a [Draw]able. - /// - /// ``` - /// # use tengri::*; - /// let _ = view::(||"drawable"); - /// let _ = view::(||Some("drawable")); - /// ``` - pub const fn view , F: Fn()->T> (view: F) -> impl View { - ViewThunk(view, PhantomData) - } - - /// Because we can't implement [Draw] for `F: FnOnce...` without conflicts. - pub struct ViewThunk(pub F, std::marker::PhantomData); - - impl, F: Fn()->T> View for ViewThunk { - fn view (&self) -> impl Draw { - self.0() - } - } - - /// Because we can't implement [Draw] for `F: FnOnce...` without conflicts. - pub struct DrawThunk(pub F, std::marker::PhantomData); - - implPerhaps>> Draw for DrawThunk { - fn draw (self, to: &mut T) -> Perhaps> { - (self.0)(to) - } - } - - /// Basic [Draw]able closure. - /// - /// ``` - /// # use tengri::*; - /// let _ = draw(|to: &mut Tui|Ok(Some(to.area()))); // draws nothing - /// ``` - pub const fn draw Perhaps>> ( - item: F - ) -> DrawThunk { - DrawThunk(item, std::marker::PhantomData) - } - - /// Only render when condition is true. - /// - /// ``` - /// # use tengri::*; - /// # fn test () -> impl Draw { - /// when(true, "Yes") - /// # } - /// ``` - pub const fn when (condition: bool, item: impl Draw) -> impl Draw { - draw(move|to: &mut T|if condition { item.draw(to) } else { Ok(Default::default()) }) - } - - /// Render one thing if a condition is true and another false. - /// - /// ``` - /// # use tengri::*; - /// # fn test () -> impl Draw { - /// either(true, "Yes", "No") - /// # } - /// ``` - pub const fn either (condition: bool, a: impl Draw, b: impl Draw) -> impl Draw { - draw(move|to: &mut T|if condition { a.draw(to) } else { b.draw(to) }) - } - - pub type Drawn = Perhaps>; - - impl Draw for () { - fn draw (self, _: &mut S) -> Drawn { - 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!() - //} - //} - - impl> Draw for &V { - fn draw (self, to: &mut T) -> Perhaps> { - self.view().draw(to) - } - } - - pub trait Xy { - fn x (&self) -> N; - fn y (&self) -> N; - } - - pub trait Wh: Wide + Tall { - fn wh (&self) -> [N;2]; - } - - pub trait Xywh: Xy + Wh { - fn xywh (&self) -> XYWH { - XYWH(self.x(), self.y(), self.w(), self.h()) - } - } - - pub trait Wide: Xy { - fn w (&self) -> N { N::zero() } - fn w_min (&self) -> N { self.w() } - fn w_max (&self) -> N { self.w() } - } - - pub trait Tall { - fn h (&self) -> N { N::zero() } - fn h_min (&self) -> N { self.h() } - fn h_max (&self) -> N { self.h() } - } - - /// Point with size. - /// - /// ``` - /// # use tengri::*; - /// let xywh = XYWH(0u16, 0, 0, 0); - /// assert_eq!(XYWH(10u16, 10, 20, 20).center(), (20, 20)); - /// ``` - /// - /// * [ ] TODO: origin field (determines at which corner/side is X0 Y0) - /// - #[cfg_attr(test, derive(Arbitrary))] #[derive(Copy, Clone, Debug, Default, PartialEq)] - pub struct XYWH(pub N, pub N, pub N, pub N); - - impl Xy for XYWH { - fn x (&self) -> N { self.0 } - fn y (&self) -> N { self.1 } - } - - impl Wide for XYWH { fn w (&self) -> N { self.2 } } - - impl Tall for XYWH { fn h (&self) -> N { self.3 } } - - impl XYWH { - - pub fn zero () -> Self { - Self(0.into(), 0.into(), 0.into(), 0.into()) - } - - pub fn center (&self) -> (N, N) { - let Self(x, y, w, h) = *self; - (x.plus(w/2.into()), y.plus(h/2.into())) - } - - pub fn centered (&self) -> (N, N) { - let Self(x, y, w, h) = *self; - (x.minus(w/2.into()), y.minus(h/2.into())) - } - - pub fn centered_x (&self, n: N) -> Self { - let Self(x, y, w, h) = *self; - let x_center = (x.plus(w / 2.into())).minus(n / 2.into()); - let y_center = y.plus(h / 2.into()); - XYWH(x_center, y_center, n, 1.into()) - } - - pub fn centered_y (&self, n: N) -> Self { - let Self(x, y, w, h) = *self; - let x_center = x.plus(w / 2.into()); - let y_corner = (y.plus(h / 2.into())).minus(n / 2.into()); - XYWH(x_center, y_corner, 1.into(), n) - } - - pub fn centered_xy (&self, [n, m]: [N;2]) -> Self { - let Self(x, y, w, h) = *self; - let x_center = (x.plus(w / 2.into())).minus(n / 2.into()); - let y_corner = (y.plus(h / 2.into())).minus(m / 2.into()); - XYWH(x_center, y_corner, n, m) - } - - pub fn split_half (&self, direction: &Split) -> (Self, Self) { - let XYWH(x, y, w, h) = self.xywh(); - match direction { - South => (XYWH(x, y, w, h - h / 2.into()), XYWH(x, y + h / 2.into(), w, h / 2.into())), - East => (XYWH(x, y, w - w / 2.into(), h), XYWH(x + w / 2.into(), y, w / 2.into(), h)), - North => (XYWH(x, y + h / 2.into(), w, h - h / 2.into()), XYWH(x, y, w, h / 2.into())), - West => (XYWH(x + w / 2.into(), y, w - w / 2.into(), h), XYWH(x, y, w / 2.into(), h)), - Above | Below => (XYWH(x, y, w, h), XYWH(x, y, w, h)) - } - } - - } - - impl From<&ratatui::prelude::Rect> for XYWH { - fn from (rect: &ratatui::prelude::Rect) -> Self { - Self(rect.x, rect.y, rect.width, rect.height) - } - } - - impl + Tall> Wh for T { - fn wh (&self) -> [N;2] { - [self.w(), self.h()] - } - } - - impl + Wh> Xywh for T {} - - impl> Lrtb for T {} - - pub trait Lrtb: Xywh { - fn lrtb (&self) -> [N;4] { - // FIXME: factor origin - [self.x(), self.y(), self.x()+self.w(), self.y()+self.h()] - } - fn iter_x (&self) -> std::ops::Range where Self: HasOrigin { - self.x_west()..self.x_east() - } - fn x_west (&self) -> N where Self: HasOrigin { - let w = self.w(); - let a = self.origin(); - let d = match a { NW|W|SW => 0.into(), N|X|C|Y|S => w/2.into(), NE|E|SE => w }; - self.x().minus(d) - } - fn x_east (&self) -> N where Self: HasOrigin { - let w = self.w(); - let a = self.origin(); - let d = match a { NW|W|SW => w, N|X|C|Y|S => w/2.into(), NE|E|SE => 0.into() }; - self.x().plus(d) - } - fn x_center (&self) -> N where Self: HasOrigin { - todo!() - } - fn iter_y (&self) -> std::ops::Range where Self: HasOrigin { - self.y_north()..self.y_south() - } - fn y_north (&self) -> N where Self: HasOrigin { - let a = self.origin(); - let h = self.h(); - let d = match a { NW|N|NE => 0.into(), W|X|C|Y|E => h/2.into(), SW|S|SE => h }; - self.y().minus(d) - } - fn y_south (&self) -> N where Self: HasOrigin { - let a = self.origin(); - let h = self.h(); - let d = match a { NW|N|NE => h, W|X|C|Y|E => h/2.into(), SW|S|SE => 0.into() }; - self.y().plus(d) - } - fn y_center (&self) -> N where Self: HasOrigin { - todo!() - } - } -} - -#[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::*; - - impl> Layout for T {} - - pub trait Layout: Draw + Sized { - fn full_w (self) -> impl Draw { - Full::W(self) - } - fn full_h (self) -> impl Draw { - Full::H(self) - } - fn full_wh (self) -> impl Draw { - Full::WH(self) - } - - /// (bsp/e (exact/w 10 "Hello") "World") - fn exact_w >> (self, x: N) -> impl Draw { - Exact::W(self, x.into()) - } - /// (bsp/s (exact/h 10 "Hello") "World") - fn exact_h >> (self, y: N) -> impl Draw { - Exact::H(self, y.into()) - } - /// (exact/wh 10 2 "Hello World") - fn exact_wh >> (self, x: N, y: N) -> impl Draw { - Exact::WH(self, x.into(), y.into()) - } - - fn min_w >> (self, x: N) -> impl Draw { - Min::W(self, x.into()) - } - fn min_h >> (self, y: N) -> impl Draw { - Min::H(self, y.into()) - } - fn min_wh >> (self, x: N, y: N) -> impl Draw { - Min::WH(self, x.into(), y.into()) - } - - fn max_w >> (self, x: N) -> impl Draw { - Max::W(self, x.into()) - } - fn max_h >> (self, y: N) -> impl Draw { - Max::H(self, y.into()) - } - fn max_wh >> (self, x: N, y: N) -> impl Draw { - Max::WH(self, x.into(), y.into()) - } - - fn pad_w >> (self, x: N) -> impl Draw { - Pad::W(self, x.into()) - } - fn pad_h >> (self, y: N) -> impl Draw { - Pad::H(self, y.into()) - } - fn pad_wh >> (self, x: N, y: N) -> impl Draw { - Pad::WH(self, x.into(), y.into()) - } - - fn pull_x >> (self, x: N) -> impl Draw { - Pull::X(self, x.into()) - } - fn pull_y >> (self, y: N) -> impl Draw { - Pull::Y(self, y.into()) - } - fn pull_xy >> (self, x: N, y: N) -> impl Draw { - Pull::XY(self, x.into(), y.into()) - } - - fn push_x >> (self, x: N) -> impl Draw { - Push::X(self, x.into()) - } - fn push_y >> (self, y: N) -> impl Draw { - Push::Y(self, y.into()) - } - fn push_xy >> (self, x: N, y: N) -> impl Draw { - Push::XY(self, x.into(), y.into()) - } - - fn align (self, azimuth: impl Into>) -> Align { - Align(azimuth.into(), self) - } - fn align_c (self) -> Align { - Align(Some(Azimuth::C), self) - } - fn align_x (self) -> Align { - Align(Some(Azimuth::X), self) - } - fn align_y (self) -> Align { - Align(Some(Azimuth::Y), self) - } - - fn align_n (self) -> impl Draw { - Align(Some(Azimuth::N), self) - } - fn align_s (self) -> impl Draw { - Align(Some(Azimuth::S), self) - } - fn align_e (self) -> impl Draw { - Align(Some(Azimuth::E), self) - } - fn align_w (self) -> impl Draw { - Align(Some(Azimuth::W), self) - } - - fn align_ne (self) -> impl Draw { - Align(Some(Azimuth::NE), self) - } - fn align_se (self) -> impl Draw { - Align(Some(Azimuth::SE), self) - } - fn align_nw (self) -> impl Draw { - Align(Some(Azimuth::NW), self) - } - fn align_sw (self) -> impl Draw { - Align(Some(Azimuth::SW), self) - } - - fn origin (self, azimuth: impl Into>) -> impl Draw { - Origin(azimuth.into(), self) - } - fn origin_c (self) -> impl Draw { - Origin(Some(Azimuth::C), self) - } - fn origin_x (self) -> impl Draw { - Origin(Some(Azimuth::X), self) - } - fn origin_y (self) -> impl Draw { - Origin(Some(Azimuth::Y), self) - } - - fn origin_n (self) -> impl Draw { - Origin(Some(Azimuth::N), self) - } - fn origin_s (self) -> impl Draw { - Origin(Some(Azimuth::S), self) - } - fn origin_e (self) -> impl Draw { - Origin(Some(Azimuth::E), self) - } - fn origin_w (self) -> impl Draw { - Origin(Some(Azimuth::W), self) - } - - fn origin_ne (self) -> impl Draw { - Origin(Some(Azimuth::NE), self) - } - fn origin_se (self) -> impl Draw { - Origin(Some(Azimuth::SE), self) - } - fn origin_nw (self) -> impl Draw { - Origin(Some(Azimuth::NW), self) - } - fn origin_sw (self) -> impl Draw { - Origin(Some(Azimuth::SW), self) - } - } - - /// Where is [0, 0] located? - /// - /// ``` - /// use tengri::*; - /// use Azimuth::*; - /// let _ = "".align(NW); - /// ``` - #[cfg_attr(test, derive(Arbitrary))] - #[derive(Debug, Copy, Clone, Default)] pub enum Azimuth { - #[default] C, X, Y, NW, N, NE, E, SE, S, SW, W - } - - /// Uses [AtomicUsize] to measure size during\ - /// rendering (which is normally read-only). - #[derive(Default, Debug, Clone)] - pub struct Sizer( - /// Width - pub Arc, - /// Height - pub Arc, - ); - - impl Xy for Sizer { - fn x (&self) -> u16 { - self.0.load(Relaxed) as u16 - } - fn y (&self) -> u16 { - self.1.load(Relaxed) as u16 - } - } - impl Wide for Sizer { - fn w (&self) -> u16 { - self.0.load(Relaxed) as u16 - } - } - impl Tall for Sizer { - fn h (&self) -> u16 { - self.1.load(Relaxed) as u16 - } - } - impl PartialEq for Sizer { - fn eq (&self, _: &Self) -> bool { todo!() } - } - - impl Sizer { - pub const fn of (&self, of: impl Draw) -> impl Draw { - draw(move|to: &mut T|{ - let area = of.draw(to)?; - self.0.store(area.map(|a|a.w()).unwrap_or(T::Unit::zero()).into(), Relaxed); - self.1.store(area.map(|a|a.h()).unwrap_or(T::Unit::zero()).into(), Relaxed); - Ok(area) - }) - } - } - - mod align; pub use self::align::*; - mod area; pub use self::area::*; - mod exact; pub use self::exact::*; - mod full; pub use self::full::*; - mod iter; pub use self::iter::*; - mod max; pub use self::max::*; - mod min; pub use self::min::*; - mod origin; pub use self::origin::*; - mod pad; pub use self::pad::*; - mod pull; pub use self::pull::*; - mod push; pub use self::push::*; - mod split; pub use self::split::*; -} - -#[cfg(feature = "draw")] pub use self::color::*; -#[cfg(feature = "draw")] mod color { - use crate::*; - 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 = "draw")] pub use self::text::*; -#[cfg(feature = "text")] mod text { - #![allow(unused)] - - 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 Color::*; - - #[macro_export] macro_rules! tui_app { - ($Struct:ident { $($fields:tt)* }) => { - #[dizzle::namespace(bool)] - #[dizzle::namespace(u8)] - #[dizzle::namespace(u16)] - #[dizzle::namespace(Option)] - #[dizzle::namespace(Color Tui::eval_color_expr)] - #[derive(Debug, Default)] - pub struct $Struct { $($fields)* } - tui_main!($Struct { ..Default::default() }); - } - } - - /// Implement standard [main] entrypoint for TUI apps. - #[macro_export] macro_rules! tui_main { - ($state:expr) => { - pub fn main () -> Usually<()> { - tengri::Tui::setup_panic(); - tengri::Tui::run_main( - ::std::sync::Arc::new(::std::sync::RwLock::new($state)) - ) - } - } - } - - /// Enable TUI output for state struct. - #[macro_export] macro_rules! tui_view { - ($self:ident: $State:ty $body:block) => { - impl tengri::View for $State { - fn view (&$self) -> impl tengri::Draw $body - } - } - } - - #[macro_export] macro_rules! tui_interpret { - ($self:ident: $State:ident, $to:pat, $pat:ident -> $Result:ty { $($body:tt)+ }) => { - impl dizzle::Interpret for $State { - - fn interpret_word <'a> ( - &'a $self, $to: &mut Tui, $pat: &'a impl dizzle::Symbol - ) -> Usually<$Result> { - $($body)+ - } - - fn interpret_expr <'a> ( - &'a self, to: &mut Tui, src: &'a impl tengri::Expression - ) -> Usually<$Result> { - Ok(Some(if let Some(area) = tengri::eval_view(self, to, src)? { - area - } else if let Some(area) = tengri::Tui::eval_view(self, to, src)? { - area - } else { - return Err(format!("App::interpret_expr: unexpected: {src:?}").into()) - })) - } - - } - }; - } - - /// Enable TUI keyboard input for main state struct. - #[macro_export] macro_rules! tui_keys { - ($self:ident:$State:ty,$input:ident $($body:tt)+) => { - impl dizzle::Apply> for $State { - fn apply (&mut $self, $input: &tengri::TuiEvent) -> Usually<()> $($body)+ - } - }; - } - - //use unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; - //use rand::distributions::uniform::UniformSampler; - pub(crate) use ::{ - std::{ - io::{stdout, Write}, - time::Duration, - ops::{Deref, DerefMut}, - }, - ratatui::{ - prelude::{Style, Position, Backend, Color}, - style::{Modifier}, - backend::{CrosstermBackend, ClearType}, - layout::{Size, Rect}, - buffer::{Buffer, Cell}, - crossterm::{ - ExecutableCommand, - terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, - //event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, - } - }, - crossterm::event::read, - }; - - //impl Deref for Tui { type Target = Buffer; fn deref (&self) -> &Buffer { &self.0 } } - //impl DerefMut for Tui { fn deref_mut (&mut self) -> &mut Buffer { &mut self.0 } } - //impl AsMut for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } } - impl AsRef> for Tui { - fn as_ref (&self) -> &XYWH { - match self { - Self::Draw(_, area) => area, - Self::Layout(area) => area - } - } - } - - impl Wide for Tui { - fn w (&self) -> u16 { self.as_ref().2 } - } - - impl Tall for Tui { - fn h (&self) -> u16 { self.as_ref().3 } - } - - impl Xy for Tui { - fn x (&self) -> u16 { self.as_ref().0 } - fn y (&self) -> u16 { self.as_ref().1 } - } - - impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } } - - /// Terminal output. - pub enum Tui { - Draw ( - /// Ratatui buffer; area is screen size - Buffer, - /// Current draw area - XYWH - ), - Layout ( - XYWH - ) - } - - impl Tui { - pub fn setup_panic () { - use ::std::panic::{set_hook, PanicHookInfo}; - use ::better_panic::{Settings, Verbosity}; - let panic = Settings::auto() - .verbosity(Verbosity::Full) - .create_panic_handler(); - set_hook(Box::new(move |info: &PanicHookInfo|{ - let _ = Tui::teardown(&mut stdout()); - panic(info); - })); - } - pub fn run_main (state: Arc>) -> Usually<()> where - T: View + Apply> + Send + Sync + 'static - { - Exit::run(|exit|{ - let scan = Duration::from_millis(100); - let frame = Duration::from_millis(10); - let (_input, output) = Tui::io(exit.as_ref(), &state, scan, frame, std::io::stdout())?; - let _ = output.join(); - Tui::teardown(&mut stdout()) - }) - } - /// Spawn the TUI input and output threadsl. - pub fn io < - T: View + Apply> + Send + Sync + 'static, - W: Write + Send + Sync + 'static, - > ( - exited: &Arc, - state: &Arc>, - poll: Duration, - sleep: Duration, - output: W, - ) -> Result<(Task, Task), Box> { - Ok(( - Tui::input(exited, state, poll)?, - Tui::output(exited, state, sleep, output)?, - )) - } - /// Spawn the TUI input thread which reads keys from the terminal. - pub fn input > + Send + Sync + 'static> ( - exited: &Arc, state: &Arc>, poll: Duration - ) -> Result { - let exited = exited.clone(); - let state = state.clone(); - Task::new_poll(exited.clone(), poll, move |_| { - let event = read().unwrap(); - if Exit::is(&event) { - exited.store(true, Relaxed); - } else if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) { - panic!("{e}") - } - }) - } - pub fn teardown (backend: &mut W) -> Usually<()> { - use ::ratatui::backend::Backend; - stdout().execute(LeaveAlternateScreen)?; - CrosstermBackend::new(backend).show_cursor()?; - disable_raw_mode().map_err(Into::into) - } - pub fn new (width: u16, height: u16) -> Self { - Self::Draw(Buffer::empty(Rect { x: 0, y: 0, width, height }), XYWH(0, 0, width, height)) - } - pub fn resize (&mut self, back: &mut CrosstermBackend, width: u16, height: u16) { - let size = Rect { x: 0, y: 0, width, height }; - if let Self::Draw(buffer, ..) = self { - if buffer.area != size { - back.clear_region(ClearType::All).unwrap(); - buffer.resize(size); - buffer.reset(); - } - } - } - pub fn redraw <'b, W: Write> ( - &'b mut self, - back: &mut CrosstermBackend, - mut next: &'b mut Self - ) { - if let Self::Draw(prev, ..) = self && let Self::Draw(next, ..) = next { - let updates = prev.diff(&next); - back.draw(updates.into_iter()).expect("failed to render"); - Backend::flush(back).expect("failed to flush output new"); - std::mem::swap(prev, next); - next.reset(); - } - } - pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH { - let XYWH(x0, y0, w, h) = self.area(); - if let Self::Draw(buffer, ..) = self { - for row in 0..h { - let y = y0 + row; - for col in 0..w { - let x = x0 + col; - if x < buffer.area.width && y < buffer.area.height { - if let Some(cell) = buffer.cell_mut(Position { x, y }) { - callback(cell, col, row); - } - } - } - } - } - self.xywh() - } - pub fn blit (&mut self, text: &impl AsRef, x: u16, y: u16, style: Option