From 4e8d58d793456475de6d535ec646cfeaece0e71c Mon Sep 17 00:00:00 2001 From: same mf who else Date: Mon, 9 Mar 2026 02:06:30 +0200 Subject: [PATCH] wip: refactor(tengri): routines! --- Cargo.toml | 3 +- src/draw.rs | 666 +++++++++++++++++++-- src/play.rs | 104 ++++ src/tengri.rs | 1 - src/tengri_fns.rs | 182 ------ src/tengri_impl.rs | 1358 ------------------------------------------ src/tengri_macros.rs | 51 -- src/tengri_struct.rs | 28 +- src/tui.rs | 771 ++++++++++++++++++++++++ 9 files changed, 1495 insertions(+), 1669 deletions(-) delete mode 100644 src/tengri_fns.rs diff --git a/Cargo.toml b/Cargo.toml index 96c6bf9..ab18d99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,11 @@ version = "0.15.0" description = "UI metaframework." [features] -default = ["lang", "sing", "draw", "tui"] +default = ["lang", "sing", "draw", "play", "tui"] lang = ["dep:dizzle"] sing = ["dep:jack"] draw = [] +play = [] tui = ["draw", "dep:ratatui", "dep:crossterm"] gui = ["draw", "dep:winit"] diff --git a/src/draw.rs b/src/draw.rs index 8af6098..a12f95b 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -1,8 +1,29 @@ use crate::*; +/// A cardinal direction. +/// +/// ``` +/// let direction = tengri::Direction::Above; +/// ``` +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Copy, Clone, PartialEq, Debug)] pub enum Direction { + North, South, East, West, Above, Below +} + +/// 9th of area to place. +/// +/// ``` +/// let alignment = tengri::Alignment::Center; +/// ``` +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Debug, Copy, Clone, Default)] pub enum Alignment { + #[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W +} + /// A numeric type that can be used as coordinate. /// -/// FIXME: Replace this ad-hoc trait with `num` crate. +/// FIXME: Replace with `num` crate? +/// FIXME: Use AsRef/AsMut? pub trait Coord: Send + Sync + Copy + Add + Sub @@ -16,6 +37,7 @@ pub trait Coord: Send + Sync + Copy { fn zero () -> Self { 0.into() } fn plus (self, other: Self) -> Self; + /// Saturating subtraction fn minus (self, other: Self) -> Self { if self >= other { self - other } else { 0.into() } } fn atomic (self) -> AtomicUsize { AtomicUsize::new(self.into()) } } @@ -26,32 +48,103 @@ pub trait X { fn x (&self) -> N { N::zero() } } /// Point along vertical axis. pub trait Y { fn y (&self) -> N { N::zero() } } +/// Point along (X, Y). +pub trait Point: X + Y { fn xy (&self) -> XY { XY(self.x(), self.y()) } } + /// Length along horizontal axis. -pub trait W { fn w (&self) -> N { N::zero() } } +pub trait W { + fn w (&self) -> N { N::zero() } + fn w_min (&self) -> N { self.w() } + fn w_max (&self) -> N { self.w() } +} /// Length along vertical axis. -pub trait H { fn h (&self) -> N { N::zero() } } +pub trait H { + fn h (&self) -> N { N::zero() } + fn h_min (&self) -> N { self.h() } + fn h_max (&self) -> N { self.h() } +} + +/// Area in 2D space. +pub trait Area: W + H { + fn wh (&self) -> WH { WH(self.w(), self.h()) } + fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w), self.h()] } + fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h)] } + fn at_least (&self, w: N, h: N) -> Usually<&Self> { + if self.w() < w || self.h() < h { return Err(format!("min {w}x{h}").into()) } + Ok(self) + } +} /// Which corner/side of a box is 0, 0 pub trait Anchor { fn anchor (&self) -> Alignment; } -/// Area along (X, Y). -pub trait Area: W + H { fn wh (&self) -> WH { WH(self.w(), self.h()) } } +/// Bounding box +pub trait Bounding: Point + Area + Anchor { + fn x_left (&self) -> N { + use Alignment::*; + let w = self.w(); + self.x().minus(match self.anchor() { NW|W|SW => 0, N|X|C|Y|S => w / 2, NE|E|SE => w } + } + fn x_right (&self) -> N { + use Alignment::*; + let w = self.w(); + self.x().plus(match self.anchor() { NW|W|SW => w, N|X|C|Y|S => w/2, NE|E|SE => 0 }) + } + fn y_top (&self) -> N { + use Alignment::*; + let h = self.h(); + self.y().minus(match self.anchor() { NW|N|NE => 0, W|X|C|Y|E => h/2, SW|E|SE => h }) + } + fn y_bottom (&self) -> N { + use Alignment::*; + let h = self.h(); + self.y().plus(match self.anchor() { NW|N|NE => h, W|X|C|Y|E => h/2, SW|E|SE => 0 }) + } +} -/// Point along (X, Y). -pub trait Point: X + Y { fn xy (&self) -> XY { XY(self.x(), self.y()) } } - -// Something that has a 2D bounding box (X, Y, W, H). -pub trait Bounded: Point + Area + Anchor { - fn x2 (&self) -> N { self.x().plus(self.w()) } - fn y2 (&self) -> N { self.y().plus(self.h()) } - fn xywh (&self) -> [N;4] { [..self.xy(), ..self.wh()] } - fn expect_min (&self, w: N, h: N) -> Usually<&Self> { - if self.w() < w || self.h() < h { - Err(format!("min {w}x{h}").into()) - } else { - Ok(self) - } +impl XYWH { + pub fn zero () -> Self { + Self(0.into(), 0.into(), 0.into(), 0.into()) + } + pub fn clipped_w (&self, w: N) -> XYWH { + Self(self.x(), self.y(), self.w().min(w), self.h()) + } + pub fn clipped_h (&self, h: N) -> XYWH { + Self(self.x(), self.y(), self.w(), self.h().min(h)) + } + pub fn clipped (&self, wh: WH) -> XYWH { + Self(self.x(), self.y(), wh.w(), wh.h()) + } + /// Iterate over every covered X coordinate. + pub fn iter_x (&self) -> impl Iterator where N: std::iter::Step { + let Self(x, _, w, _) = *self; + x..(x+w) + } + /// Iterate over every covered Y coordinate. + pub fn iter_y (&self) -> impl Iterator where N: std::iter::Step { + let Self(_, y, _, h) = *self; + y..(y+h) + } + pub fn center (&self) -> XY { + let Self(x, y, w, h) = self; + XY(self.x().plus(self.w()/2.into()), self.y().plus(self.h()/2.into())) + } + pub fn centered (&self) -> XY { + let Self(x, y, w, h) = *self; + XY(x.minus(w/2.into()), y.minus(h/2.into())) + } + pub fn centered_x (&self, n: N) -> XYWH { + let Self(x, y, w, h) = *self; + XYWH((x.plus(w / 2.into())).minus(n / 2.into()), y.plus(h / 2.into()), n, 1.into()) + } + pub fn centered_y (&self, n: N) -> XYWH { + let Self(x, y, w, h) = *self; + XYWH(x.plus(w / 2.into()), (y.plus(h / 2.into())).minus(n / 2.into()), 1.into(), n) + } + pub fn centered_xy (&self, [n, m]: [N;2]) -> XYWH { + let Self(x, y, w, h) = *self; + XYWH((x.plus(w / 2.into())).minus(n / 2.into()), (y.plus(h / 2.into())).minus(m / 2.into()), n, m) } } @@ -85,26 +178,6 @@ pub trait Bounded: Point + Area + Anchor { pub C, pub C, pub C, pub C ); -/// A cardinal direction. -/// -/// ``` -/// let direction = tengri::Direction::Above; -/// ``` -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Copy, Clone, PartialEq, Debug)] pub enum Direction { - North, South, East, West, Above, Below -} - -/// 9th of area to place. -/// -/// ``` -/// let alignment = tengri::Alignment::Center; -/// ``` -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Debug, Copy, Clone, Default)] pub enum Alignment { - #[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W -} - /// Drawable with dynamic dispatch. pub trait Draw: Fn(&mut T)->Usually<()> { fn draw (&self, to: &mut T) -> Usually<()>; } @@ -167,9 +240,38 @@ pub fn clip (w: Option, h: Option, draw: impl Draw) -> im move|to|draw(to.clip(w, h)) } +pub fn align () { + //impl> Layout for Align { + //fn layout_x (&self, to: XYWH) -> O::Unit { + //use Alignment::*; + //match self.0 { + //NW | W | SW => to.x(), + //N | Center | S => to.x().plus(to.w() / 2.into()).minus(self.1.layout_w(to) / 2.into()), + //NE | E | SE => to.x().plus(to.w()).minus(self.1.layout_w(to)), + //_ => todo!(), + //} + //} + //fn layout_y (&self, to: XYWH) -> O::Unit { + //use Alignment::*; + //match self.0 { + //NW | N | NE => to.y(), + //W | Center | E => to.y().plus(to.h() / 2.into()).minus(self.1.layout_h(to) / 2.into()), + //SW | S | SE => to.y().plus(to.h()).minus(self.1.layout_h(to)), + //_ => todo!(), + //} + //} + //} +} + /// Shrink drawing area symmetrically. pub fn pad (w: Option, h: Option, draw: impl Draw) -> impl Draw { move|to|draw(to.pad(w, h)) + //impl> Layout for Pad { + //fn layout_x (&self, area: XYWH) -> O::Unit { area.x().plus(self.dx()) } + //fn layout_y (&self, area: XYWH) -> O::Unit { area.x().plus(self.dx()) } + //fn layout_w (&self, area: XYWH) -> O::Unit { area.w().minus(self.dx() * 2.into()) } + //fn layout_h (&self, area: XYWH) -> O::Unit { area.h().minus(self.dy() * 2.into()) } + //} } /// Only draw content if area is above a certain size. @@ -215,8 +317,122 @@ pub fn phat ( min_wh(w, h, bsp_s(top, bsp_n(low, fill_wh(draw)))) } -pub fn iter (items: impl Iterator>) -> impl Draw { - todo!() +pub fn iter (items: impl Iterator>, callback: Fn(dyn Draw)->impl Draw) -> impl Draw { + //todo!() + + //impl<'a, O, A, B, I, F, G> Map where + //I: Iterator + Send + Sync + 'a, + //F: Fn() -> I + Send + Sync + 'a, + //{ + //pub const fn new (get_iter: F, get_item: G) -> Self { + //Self { + //__: PhantomData, + //get_iter, + //get_item + //} + //} + //const fn to_south ( + //item_offset: S::Unit, item_height: S::Unit, item: impl Draw + //) -> impl Draw { + //Push::Y(item_offset, Fixed::Y(item_height, Fill::X(item))) + //} + //const fn to_south_west ( + //item_offset: S::Unit, item_height: S::Unit, item: impl Draw + //) -> impl Draw { + //Push::Y(item_offset, Align::nw(Fixed::Y(item_height, Fill::X(item)))) + //} + //const fn to_east ( + //item_offset: S::Unit, item_width: S::Unit, item: impl Draw + //) -> impl Draw { + //Push::X(item_offset, Align::w(Fixed::X(item_width, Fill::Y(item)))) + //} + //} + + //macro_rules! impl_map_direction (($name:ident, $axis:ident, $align:ident)=>{ + //impl<'a, O, A, B, I, F> Map< + //O, A, Push>>>, I, F, fn(A, usize)->B + //> where + //O: Screen, + //B: Draw, + //I: Iterator + Send + Sync + 'a, + //F: Fn() -> I + Send + Sync + 'a + //{ + //pub const fn $name ( + //size: O::Unit, + //get_iter: F, + //get_item: impl Fn(A, usize)->B + Send + Sync + //) -> Map< + //O, A, + //Push>>, + //I, F, + //impl Fn(A, usize)->Push>> + Send + Sync + //> { + //Map { + //__: PhantomData, + //get_iter, + //get_item: move |item: A, index: usize|{ + //// FIXME: multiply + //let mut push: O::Unit = O::Unit::from(0u16); + //for _ in 0..index { + //push = push + size; + //} + //Push::$axis(push, Align::$align(Fixed::$axis(size, get_item(item, index)))) + //} + //} + //} + //} + //}); + //impl_map_direction!(east, X, w); + //impl_map_direction!(south, Y, n); + //impl_map_direction!(west, X, e); + //impl_map_direction!(north, Y, s); + + //impl<'a, O, A, B, I, F, G> Layout for Map where + //O: Screen, + //B: Layout, + //I: Iterator + Send + Sync + 'a, + //F: Fn() -> I + Send + Sync + 'a, + //G: Fn(A, usize)->B + Send + Sync + //{ + //fn layout (&self, area: XYWH) -> XYWH { + //let Self { get_iter, get_item, .. } = self; + //let mut index = 0; + //let XY(mut min_x, mut min_y) = area.centered(); + //let XY(mut max_x, mut max_y) = area.center(); + //for item in get_iter() { + //let XYWH(x, y, w, h) = get_item(item, index).layout(area); + //min_x = min_x.min(x); + //min_y = min_y.min(y); + //max_x = max_x.max(x + w); + //max_y = max_y.max(y + h); + //index += 1; + //} + //let w = max_x - min_x; + //let h = max_y - min_y; + ////[min_x.into(), min_y.into(), w.into(), h.into()].into() + //area.centered_xy([w.into(), h.into()]) + //} + //} + + //impl<'a, O, A, B, I, F, G> Draw for Map where + //O: Screen, + //B: Draw, + //I: Iterator + Send + Sync + 'a, + //F: Fn() -> I + Send + Sync + 'a, + //G: Fn(A, usize)->B + Send + Sync + //{ + //fn draw (&self, to: &mut O) { + //let Self { get_iter, get_item, .. } = self; + //let mut index = 0; + //let area = self.layout(to.area()); + //for item in get_iter() { + //let item = get_item(item, index); + ////to.show(area.into(), &item); + //to.show(item.layout(area), &item); + //index += 1; + //} + //} + //} } /// Split screen between two items, or layer them atop each other. @@ -294,6 +510,48 @@ pub fn bsp (dir: Direction, a: impl Draw, b: impl Draw) -> impl Draw, Tail: Layout> Layout for Bsp { + //fn layout_w (&self, area: XYWH) -> O::Unit { + //match self.0 { + //Above | Below | North | South => self.1.layout_w(area).max(self.2.layout_w(area)), + //East | West => self.1.layout_w_min(area).plus(self.2.layout_w(area)), + //} + //} + //fn layout_w_min (&self, area: XYWH) -> O::Unit { + //match self.0 { + //Above | Below | North | South => self.1.layout_w_min(area).max(self.2.layout_w_min(area)), + //East | West => self.1.layout_w_min(area).plus(self.2.layout_w_min(area)), + //} + //} + //fn layout_w_max (&self, area: XYWH) -> O::Unit { + //match self.0 { + //Above | Below | North | South => self.1.layout_w_max(area).max(self.2.layout_w_max(area)), + //East | West => self.1.layout_w_max(area).plus(self.2.layout_w_max(area)), + //} + //} + //fn layout_h (&self, area: XYWH) -> O::Unit { + //match self.0 { + //Above | Below | East | West => self.1.layout_h(area).max(self.2.layout_h(area)), + //North | South => self.1.layout_h(area).plus(self.2.layout_h(area)), + //} + //} + //fn layout_h_min (&self, area: XYWH) -> O::Unit { + //match self.0 { + //Above | Below | East | West => self.1.layout_h_min(area).max(self.2.layout_h_min(area)), + //North | South => self.1.layout_h_min(area).plus(self.2.layout_h_min(area)), + //} + //} + //fn layout_h_max (&self, area: XYWH) -> O::Unit { + //match self.0 { + //Above | Below | North | South => self.1.layout_h_max(area).max(self.2.layout_h_max(area)), + //East | West => self.1.layout_h_max(area).plus(self.2.layout_h_max(area)), + //} + //} + //fn layout (&self, area: XYWH) -> XYWH { + //bsp_areas(area, self.0, &self.1, &self.2)[2] + //} + //} } } @@ -333,14 +591,6 @@ impl> Draw for Bounded { } } -impl> Draw for Align { - fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), &self.1).draw(to) } -} - -impl> Draw for Pad { - fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } -} - impl, Tail: Draw> Draw for Bsp { fn draw (&self, to: &mut O) { let [a, b, _] = bsp_areas(to.area(), self.0, &self.1, &self.2); @@ -405,3 +655,321 @@ impl, Tail: Draw> Draw for Bsp { #[macro_export] macro_rules! row (($($expr:expr),* $(,)?) => {{ let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)* bsp }}); + +/// Interpret layout operation. +/// +/// ``` +/// # use tengri::*; +/// struct Target { xywh: XYWH /*platform-specific*/} +/// impl tengri::Out for Target { +/// type Unit = u16; +/// fn area (&self) -> XYWH { self.xywh } +/// fn area_mut (&mut self) -> &mut XYWH { &mut self.xywh } +/// fn show <'t, T: Draw + ?Sized> (&mut self, area: XYWH, content: &'t T) {} +/// } +/// +/// struct State {/*app-specific*/} +/// impl<'b> Namespace<'b, bool> for State {} +/// impl<'b> Namespace<'b, u16> for State {} +/// impl Understand for State {} +/// +/// # fn main () -> tengri::Usually<()> { +/// let state = State {}; +/// let mut target = Target { xywh: Default::default() }; +/// eval_view(&state, &mut target, &"")?; +/// eval_view(&state, &mut target, &"(when true (text hello))")?; +/// eval_view(&state, &mut target, &"(either true (text hello) (text world))")?; +/// // TODO test all +/// # Ok(()) } +/// ``` +#[cfg(feature = "dsl")] pub fn eval_view <'a, O: Screen + 'a, S> ( + state: &S, output: &mut O, expr: &'a impl Expression +) -> Usually where + S: Understand + + for<'b>Namespace<'b, bool> + + for<'b>Namespace<'b, O::Unit> +{ + // First element of expression is used for dispatch. + // Dispatch is proto-namespaced using 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 + let args = expr.tail(); + let arg0 = args.head(); + let tail0 = args.tail(); + let arg1 = tail0.head(); + let tail1 = tail0.tail(); + let arg2 = tail1.head(); + // And we also have to do the above binding dance + // so that the Perhapss remain in scope. + match frags.next() { + + Some("when") => output.place(&When::new( + state.namespace(arg0?)?.unwrap(), + Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()) + )), + + Some("either") => output.place(&Either::new( + state.namespace(arg0?)?.unwrap(), + Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()), + Thunk::new(move|output: &mut O|state.understand(output, &arg2).unwrap()) + )), + + Some("bsp") => output.place(&{ + let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); + let b = Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()); + match frags.next() { + Some("n") => Bsp::n(a, b), + Some("s") => Bsp::s(a, b), + Some("e") => Bsp::e(a, b), + Some("w") => Bsp::w(a, b), + Some("a") => Bsp::a(a, b), + Some("b") => Bsp::b(a, b), + frag => unimplemented!("bsp/{frag:?}") + } + }), + + Some("align") => output.place(&{ + let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); + match frags.next() { + Some("n") => Align::n(a), + Some("s") => Align::s(a), + Some("e") => Align::e(a), + Some("w") => Align::w(a), + Some("x") => Align::x(a), + Some("y") => Align::y(a), + Some("c") => Align::c(a), + frag => unimplemented!("align/{frag:?}") + } + }), + + Some("fill") => output.place(&{ + let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); + match frags.next() { + Some("xy") | None => Fill::XY(a), + Some("x") => Fill::X(a), + Some("y") => Fill::Y(a), + frag => unimplemented!("fill/{frag:?}") + } + }), + + Some("fixed") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); + match axis { + Some("xy") | None => Fixed::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => Fixed::X(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => Fixed::Y(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})", + head.src()?.unwrap_or_default().split("/").next()) + } + }), + + Some("min") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); + match axis { + Some("xy") | None => Min::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => Min::X(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => Min::Y(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("min/{frag:?}") + } + }), + + Some("max") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); + match axis { + Some("xy") | None => Max::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => Max::X(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => Max::Y(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("max/{frag:?}") + } + }), + + Some("push") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); + match axis { + Some("xy") | None => Push::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => Push::X(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => Push::Y(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("push/{frag:?}") + } + }), + + _ => return Ok(false) + + }; + Ok(true) +} + +/// 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() +} + +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 +} + +/// Memoize a rendering. +/// +/// ``` +/// let _ = tengri::Memo::new((), ()); +/// ``` +#[derive(Debug, Default)] pub struct Memo { + pub value: T, + pub view: Arc> +} + +impl<'a, T: AsRef> TrimString { + fn to_ref (&self) -> TrimStringRef<'_, T> { TrimStringRef(self.0, &self.1) } +} +impl Field { + pub fn new (direction: Direction) -> Field { + Field:: { + direction, + label: None, label_fg: None, label_bg: None, label_align: None, + value: None, value_fg: None, value_bg: None, value_align: None, + } + } + pub fn label ( + self, label: Option, align: Option, fg: Option, bg: Option + ) -> Field { + Field:: { label, label_fg: fg, label_bg: bg, label_align: align, ..self } + } + pub fn value ( + self, value: Option, align: Option, fg: Option, bg: Option + ) -> Field { + Field:: { value, value_fg: fg, value_bg: bg, value_align: align, ..self } + } +} +impl Clone for Measure { fn clone (&self) -> Self { Self { __: Default::default(), x: self.x.clone(), y: self.y.clone(), } } } +impl Memo { + pub fn new (value: T, view: U) -> Self { + Self { value, view: Arc::new(view.into()) } + } + pub fn update (&mut self, newval: T, draw: impl Fn(&mut U, &T, &T)->R) -> Option { + if newval != self.value { + let result = draw(&mut*self.view.write().unwrap(), &newval, &self.value); + self.value = newval; + return Some(result); + } + None + } +} +impl PartialEq for Measure { fn eq (&self, other: &Self) -> bool { self.w() == other.w() && self.h() == other.h() } } +impl W for Measure { fn w (&self) -> O::Unit { (self.x.load(Relaxed) as u16).into() } } +impl H for Measure { fn h (&self) -> O::Unit { (self.y.load(Relaxed) as u16).into() } } +impl Debug for Measure { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Measure").field("width", &self.x).field("height", &self.y).finish() } } +impl Measure { + pub fn set_w (&self, w: impl Into) -> &Self { self.x.store(w.into(), Relaxed); self } + pub fn set_h (&self, h: impl Into) -> &Self { self.y.store(h.into(), Relaxed); self } + pub fn set_wh (&self, w: impl Into, h: impl Into) -> &Self { self.set_w(w); self.set_h(h); self } + pub fn format (&self) -> Arc { format!("{}x{}", self.w(), self.h()).into() } + pub fn of > (&self, item: T) -> Bsp, T> { Bsp::b(Fill::XY(self), item) } + pub fn new (x: O::Unit, y: O::Unit) -> Self { + Self { __: PhantomData::default(), x: Arc::new(x.atomic()), y: Arc::new(y.atomic()), } + } +} +impl From> for Measure { fn from (WH(x, y): WH) -> Self { Self::new(x, y) } } +// Implement layout op that increments X and/or Y by fixed amount. +macro_rules! push_pull(($T:ident: $method: ident)=>{ + layout_op_xy!(1: $T); + impl> Layout for $T { + fn layout_x (&self, area: XYWH) -> O::Unit { area.x().$method(self.dx()) } + fn layout_y (&self, area: XYWH) -> O::Unit { area.y().$method(self.dy()) } + } +}); +push_pull!(Push: plus); +push_pull!(Pull: minus); + +layout_op_xy!(0: Fill); + +impl> Layout for Fill { + fn layout_x (&self, area: XYWH) -> O::Unit { if self.dx() { area.x() } else { self.inner().layout_x(area) } } + fn layout_y (&self, area: XYWH) -> O::Unit { if self.dy() { area.y() } else { self.inner().layout_y(area) } } + fn layout_w (&self, area: XYWH) -> O::Unit { if self.dx() { area.w() } else { self.inner().layout_w(area) } } + fn layout_w_min (&self, area: XYWH) -> O::Unit { if self.dx() { area.w() } else { self.inner().layout_w_min(area) } } + fn layout_w_max (&self, area: XYWH) -> O::Unit { if self.dx() { area.w() } else { self.inner().layout_w_max(area) } } + fn layout_h (&self, area: XYWH) -> O::Unit { if self.dy() { area.h() } else { self.inner().layout_h(area) } } + fn layout_h_min (&self, area: XYWH) -> O::Unit { if self.dy() { area.h() } else { self.inner().layout_h_min(area) } } + fn layout_h_max (&self, area: XYWH) -> O::Unit { if self.dy() { area.h() } else { self.inner().layout_h_max(area) } } +} + +impl Fill { + #[inline] pub const fn dx (&self) -> bool { matches!(self, Self::X(_) | Self::XY(_)) } + #[inline] pub const fn dy (&self) -> bool { matches!(self, Self::Y(_) | Self::XY(_)) } +} + + +impl> Layout for Fixed { + fn layout_w (&self, area: XYWH) -> O::Unit { self.dx().unwrap_or(self.inner().layout_w(area)) } + fn layout_w_min (&self, area: XYWH) -> O::Unit { self.dx().unwrap_or(self.inner().layout_w_min(area)) } + fn layout_w_max (&self, area: XYWH) -> O::Unit { self.dx().unwrap_or(self.inner().layout_w_max(area)) } + fn layout_h (&self, area: XYWH) -> O::Unit { self.dy().unwrap_or(self.inner().layout_h(area)) } + fn layout_h_min (&self, area: XYWH) -> O::Unit { self.dy().unwrap_or(self.inner().layout_h_min(area)) } + fn layout_h_max (&self, area: XYWH) -> O::Unit { self.dy().unwrap_or(self.inner().layout_h_max(area)) } +} + + +impl> Layout for Max { + fn layout (&self, area: XYWH) -> XYWH { + let XYWH(x, y, w, h) = self.inner().layout(area); + match self { + Self::X(mw, _) => XYWH(x, y, w.min(*mw), h ), + Self::Y(mh, _) => XYWH(x, y, w, h.min(*mh)), + Self::XY(mw, mh, _) => XYWH(x, y, w.min(*mw), h.min(*mh)), + } + } +} + + +impl> Layout for Min { + fn layout (&self, area: XYWH) -> XYWH { + let XYWH(x, y, w, h) = self.inner().layout(area); + match self { + Self::X(mw, _) => XYWH(x, y, w.max(*mw), h), + Self::Y(mh, _) => XYWH(x, y, w, h.max(*mh)), + Self::XY(mw, mh, _) => XYWH(x, y, w.max(*mw), h.max(*mh)), + } + } +} + +impl X for XY { fn x (&self) -> N { self.0 } } +impl Y for XY { fn y (&self) -> N { self.1 } } +impl W for WH { fn w (&self) -> N { self.0 } } +impl H for WH { fn h (&self) -> N { self.1 } } +impl X for XYWH { fn x (&self) -> N { self.0 } } +impl Y for XYWH { fn y (&self) -> N { self.1 } } +impl W for XYWH { fn w (&self) -> N { self.2 } } +impl H for XYWH { fn h (&self) -> N { self.3 } } +impl X for O { fn x (&self) -> O::Unit { self.area().x() } } +impl Y for O { fn y (&self) -> O::Unit { self.area().y() } } +impl W for O { fn w (&self) -> O::Unit { self.area().w() } } +impl H for O { fn h (&self) -> O::Unit { self.area().h() } } diff --git a/src/play.rs b/src/play.rs index 686ab86..f4b932c 100644 --- a/src/play.rs +++ b/src/play.rs @@ -1,5 +1,64 @@ use crate::*; +#[derive(Clone)] pub struct Exit(Arc); + +impl Exit { + pub fn run (run: impl Fn()->Usually) -> Usually { + run(Self(Arc::new(AtomicBool::new(false)))) + } +} + +#[derive(Debug)] pub struct Thread { + /// Exit flag. + pub exit: Arc, + /// Performance counter. + pub perf: Arc, + /// Use this to wait for the thread to finish. + pub join: JoinHandle<()>, +} + +impl Thread { + /// Spawn a TUI thread that runs `callt least one, then repeats until `exit`. + pub fn new (exit: Arc, call: F) -> Result + where F: Fn(&PerfModel)->() + Send + Sync + 'static + { + let perf = Arc::new(PerfModel::default()); + Ok(Self { + exit: exit.clone(), + perf: perf.clone(), + join: std::thread::Builder::new().name("tengri tui output".into()).spawn(move || { + while !exit.fetch_and(true, Relaxed) { + let _ = perf.cycle(&call); + } + })?.into() + }) + } + + /// Spawn a TUI thread that runs `callt least one, then repeats + /// until `exit`, sleeping for `time` msec after every iteration. + pub fn new_sleep ( + exit: Arc, time: Duration, call: F + ) -> Result + where F: Fn(&PerfModel)->() + Send + Sync + 'static + { + Self::new(exit, move |perf| { let _ = call(perf); std::thread::sleep(time); }) + } + + /// Spawn a TUI thread that runs `callt least one, then repeats + /// until `exit`, using polling to run every `time` msec. + pub fn new_poll ( + exit: Arc, time: Duration, call: F + ) -> Result + where F: Fn(&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() + } +} + /// 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| { @@ -39,3 +98,48 @@ use crate::*; } } } +impl> Action for Option { fn action (&self, _: &mut S) -> Perhaps { Ok(None) } } + +impl_default!(PerfModel: Self { + enabled: true, + clock: quanta::Clock::new(), + used: Default::default(), + window: Default::default(), +}); + +impl PerfModel { + pub fn get_t0 (&self) -> Option { + if self.enabled { + Some(self.clock.raw()) + } else { + None + } + } + pub fn get_t1 (&self, t0: Option) -> Option { + if let Some(t0) = t0 { + if self.enabled { + Some(self.clock.delta(t0, self.clock.raw())) + } else { + None + } + } else { + None + } + } + pub fn update (&self, t0: Option, microseconds: f64) { + if let Some(t0) = t0 { + let t1 = self.clock.raw(); + self.used.store(self.clock.delta_as_nanos(t0, t1) as f64, Relaxed); + self.window.store(microseconds, Relaxed,); + } + } + pub fn percentage (&self) -> Option { + let window = self.window.load(Relaxed) * 1000.0; + if window > 0.0 { + let used = self.used.load(Relaxed); + Some(100.0 * used / window) + } else { + None + } + } +} diff --git a/src/tengri.rs b/src/tengri.rs index 5e9e25e..997b9bd 100644 --- a/src/tengri.rs +++ b/src/tengri.rs @@ -17,7 +17,6 @@ mod tengri_macros; mod tengri_struct; pub use self::tengri_struct::*; mod tengri_trait; pub use self::tengri_trait::*; mod tengri_impl; pub use self::tengri_impl::*; -mod tengri_fns; pub use self::tengri_fns::*; #[cfg(test)] pub(crate) use proptest_derive::Arbitrary; diff --git a/src/tengri_fns.rs b/src/tengri_fns.rs deleted file mode 100644 index 7a4e6d4..0000000 --- a/src/tengri_fns.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::*; - -/// Interpret layout operation. -/// -/// ``` -/// # use tengri::*; -/// struct Target { xywh: XYWH /*platform-specific*/} -/// impl tengri::Out for Target { -/// type Unit = u16; -/// fn area (&self) -> XYWH { self.xywh } -/// fn area_mut (&mut self) -> &mut XYWH { &mut self.xywh } -/// fn show <'t, T: Draw + ?Sized> (&mut self, area: XYWH, content: &'t T) {} -/// } -/// -/// struct State {/*app-specific*/} -/// impl<'b> Namespace<'b, bool> for State {} -/// impl<'b> Namespace<'b, u16> for State {} -/// impl Understand for State {} -/// -/// # fn main () -> tengri::Usually<()> { -/// let state = State {}; -/// let mut target = Target { xywh: Default::default() }; -/// eval_view(&state, &mut target, &"")?; -/// eval_view(&state, &mut target, &"(when true (text hello))")?; -/// eval_view(&state, &mut target, &"(either true (text hello) (text world))")?; -/// // TODO test all -/// # Ok(()) } -/// ``` -#[cfg(feature = "dsl")] pub fn eval_view <'a, O: Screen + 'a, S> ( - state: &S, output: &mut O, expr: &'a impl Expression -) -> Usually where - S: Understand - + for<'b>Namespace<'b, bool> - + for<'b>Namespace<'b, O::Unit> -{ - // First element of expression is used for dispatch. - // Dispatch is proto-namespaced using 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 - let args = expr.tail(); - let arg0 = args.head(); - let tail0 = args.tail(); - let arg1 = tail0.head(); - let tail1 = tail0.tail(); - let arg2 = tail1.head(); - // And we also have to do the above binding dance - // so that the Perhapss remain in scope. - match frags.next() { - - Some("when") => output.place(&When::new( - state.namespace(arg0?)?.unwrap(), - Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()) - )), - - Some("either") => output.place(&Either::new( - state.namespace(arg0?)?.unwrap(), - Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()), - Thunk::new(move|output: &mut O|state.understand(output, &arg2).unwrap()) - )), - - Some("bsp") => output.place(&{ - let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); - let b = Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()); - match frags.next() { - Some("n") => Bsp::n(a, b), - Some("s") => Bsp::s(a, b), - Some("e") => Bsp::e(a, b), - Some("w") => Bsp::w(a, b), - Some("a") => Bsp::a(a, b), - Some("b") => Bsp::b(a, b), - frag => unimplemented!("bsp/{frag:?}") - } - }), - - Some("align") => output.place(&{ - let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); - match frags.next() { - Some("n") => Align::n(a), - Some("s") => Align::s(a), - Some("e") => Align::e(a), - Some("w") => Align::w(a), - Some("x") => Align::x(a), - Some("y") => Align::y(a), - Some("c") => Align::c(a), - frag => unimplemented!("align/{frag:?}") - } - }), - - Some("fill") => output.place(&{ - let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); - match frags.next() { - Some("xy") | None => Fill::XY(a), - Some("x") => Fill::X(a), - Some("y") => Fill::Y(a), - frag => unimplemented!("fill/{frag:?}") - } - }), - - Some("fixed") => output.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; - let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); - match axis { - Some("xy") | None => Fixed::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => Fixed::X(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => Fixed::Y(state.namespace(arg0?)?.unwrap(), cb), - frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})", - head.src()?.unwrap_or_default().split("/").next()) - } - }), - - Some("min") => output.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; - let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); - match axis { - Some("xy") | None => Min::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => Min::X(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => Min::Y(state.namespace(arg0?)?.unwrap(), cb), - frag => unimplemented!("min/{frag:?}") - } - }), - - Some("max") => output.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; - let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); - match axis { - Some("xy") | None => Max::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => Max::X(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => Max::Y(state.namespace(arg0?)?.unwrap(), cb), - frag => unimplemented!("max/{frag:?}") - } - }), - - Some("push") => output.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; - let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); - match axis { - Some("xy") | None => Push::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => Push::X(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => Push::Y(state.namespace(arg0?)?.unwrap(), cb), - frag => unimplemented!("push/{frag:?}") - } - }), - - _ => return Ok(false) - - }; - Ok(true) -} - -/// 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() -} - -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 -} diff --git a/src/tengri_impl.rs b/src/tengri_impl.rs index 476c657..3154266 100644 --- a/src/tengri_impl.rs +++ b/src/tengri_impl.rs @@ -1,1365 +1,7 @@ use crate::*; use Direction::*; use rand::{thread_rng, distributions::uniform::UniformSampler}; -impl<'a, T: AsRef> TrimString { - fn as_ref (&self) -> TrimStringRef<'_, T> { TrimStringRef(self.0, &self.1) } -} -impl Field { - pub fn new (direction: Direction) -> Field { - Field:: { - direction, - label: None, label_fg: None, label_bg: None, label_align: None, - value: None, value_fg: None, value_bg: None, value_align: None, - } - } - pub fn label ( - self, label: Option, align: Option, fg: Option, bg: Option - ) -> Field { - Field:: { label, label_fg: fg, label_bg: bg, label_align: align, ..self } - } - pub fn value ( - self, value: Option, align: Option, fg: Option, bg: Option - ) -> Field { - Field:: { value, value_fg: fg, value_bg: bg, value_align: align, ..self } - } -} -impl> Action for Option { fn action (&self, _: &mut S) -> Perhaps { Ok(None) } } -impl Clone for Measure { fn clone (&self) -> Self { Self { __: Default::default(), x: self.x.clone(), y: self.y.clone(), } } } -impl Thunk { pub const fn new (draw: F) -> Self { Self(draw, PhantomData) } } -impl Memo { - pub fn new (value: T, view: U) -> Self { - Self { value, view: Arc::new(view.into()) } - } - pub fn update (&mut self, newval: T, draw: impl Fn(&mut U, &T, &T)->R) -> Option { - if newval != self.value { - let result = draw(&mut*self.view.write().unwrap(), &newval, &self.value); - self.value = newval; - return Some(result); - } - None - } -} -impl PartialEq for Measure { fn eq (&self, other: &Self) -> bool { self.w() == other.w() && self.h() == other.h() } } -impl W for Measure { fn w (&self) -> O::Unit { (self.x.load(Relaxed) as u16).into() } } -impl H for Measure { fn h (&self) -> O::Unit { (self.y.load(Relaxed) as u16).into() } } -impl Debug for Measure { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Measure").field("width", &self.x).field("height", &self.y).finish() } } -impl Measure { - pub fn set_w (&self, w: impl Into) -> &Self { self.x.store(w.into(), Relaxed); self } - pub fn set_h (&self, h: impl Into) -> &Self { self.y.store(h.into(), Relaxed); self } - pub fn set_wh (&self, w: impl Into, h: impl Into) -> &Self { self.set_w(w); self.set_h(h); self } - pub fn format (&self) -> Arc { format!("{}x{}", self.w(), self.h()).into() } - pub fn of > (&self, item: T) -> Bsp, T> { Bsp::b(Fill::XY(self), item) } - pub fn new (x: O::Unit, y: O::Unit) -> Self { - Self { __: PhantomData::default(), x: Arc::new(x.atomic()), y: Arc::new(y.atomic()), } - } -} -impl From> for Measure { fn from (WH(x, y): WH) -> Self { Self::new(x, y) } } -macro_rules! layout_op_xy ( - // Variant for layout ops that take no coordinates - (0: $T: ident) => { - impl $T { - #[inline] pub const fn inner (&self) -> &A { - match self { Self::X(c) | Self::Y(c) | Self::XY(c) => c } - } - } - impl> Draw for $T { - fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } - } - }; - // Variant for layout ops that take one coordinate - (1: $T: ident) => { - impl $T { - #[inline] pub const fn inner (&self) -> &A { - match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, } - } - } - impl> Draw for $T { - fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } - } - impl $T { - #[inline] pub fn dx (&self) -> U { - match self { Self::X(x, _) | Self::XY(x, ..) => *x, _ => 0.into() } - } - #[inline] pub fn dy (&self) -> U { - match self { Self::Y(y, _) | Self::XY(y, ..) => *y, _ => 0.into() } - } - } - }; - (1 opt: $T: ident) => { - impl $T { - #[inline] pub const fn inner (&self) -> &A { - match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, } - } - } - impl> Draw for $T { - fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } - } - impl $T { - #[inline] pub const fn dx (&self) -> Option { - match self { Self::X(x, _) | Self::XY(x, ..) => Some(*x), _ => None } - } - #[inline] pub const fn dy (&self) -> Option { - match self { Self::Y(y, _) | Self::XY(y, ..) => Some(*y), _ => None } - } - } - }; -); -// Implement layout op that increments X and/or Y by fixed amount. -macro_rules! push_pull(($T:ident: $method: ident)=>{ - layout_op_xy!(1: $T); - impl> Layout for $T { - fn layout_x (&self, area: XYWH) -> O::Unit { area.x().$method(self.dx()) } - fn layout_y (&self, area: XYWH) -> O::Unit { area.y().$method(self.dy()) } - } -}); -push_pull!(Push: plus); -push_pull!(Pull: minus); - -layout_op_xy!(0: Fill); - -impl> Layout for Fill { - fn layout_x (&self, area: XYWH) -> O::Unit { if self.dx() { area.x() } else { self.inner().layout_x(area) } } - fn layout_y (&self, area: XYWH) -> O::Unit { if self.dy() { area.y() } else { self.inner().layout_y(area) } } - fn layout_w (&self, area: XYWH) -> O::Unit { if self.dx() { area.w() } else { self.inner().layout_w(area) } } - fn layout_w_min (&self, area: XYWH) -> O::Unit { if self.dx() { area.w() } else { self.inner().layout_w_min(area) } } - fn layout_w_max (&self, area: XYWH) -> O::Unit { if self.dx() { area.w() } else { self.inner().layout_w_max(area) } } - fn layout_h (&self, area: XYWH) -> O::Unit { if self.dy() { area.h() } else { self.inner().layout_h(area) } } - fn layout_h_min (&self, area: XYWH) -> O::Unit { if self.dy() { area.h() } else { self.inner().layout_h_min(area) } } - fn layout_h_max (&self, area: XYWH) -> O::Unit { if self.dy() { area.h() } else { self.inner().layout_h_max(area) } } -} - -impl Fill { - #[inline] pub const fn dx (&self) -> bool { matches!(self, Self::X(_) | Self::XY(_)) } - #[inline] pub const fn dy (&self) -> bool { matches!(self, Self::Y(_) | Self::XY(_)) } -} - -layout_op_xy!(1 opt: Fixed); - -impl> Layout for Fixed { - fn layout_w (&self, area: XYWH) -> O::Unit { self.dx().unwrap_or(self.inner().layout_w(area)) } - fn layout_w_min (&self, area: XYWH) -> O::Unit { self.dx().unwrap_or(self.inner().layout_w_min(area)) } - fn layout_w_max (&self, area: XYWH) -> O::Unit { self.dx().unwrap_or(self.inner().layout_w_max(area)) } - fn layout_h (&self, area: XYWH) -> O::Unit { self.dy().unwrap_or(self.inner().layout_h(area)) } - fn layout_h_min (&self, area: XYWH) -> O::Unit { self.dy().unwrap_or(self.inner().layout_h_min(area)) } - fn layout_h_max (&self, area: XYWH) -> O::Unit { self.dy().unwrap_or(self.inner().layout_h_max(area)) } -} - -layout_op_xy!(1 opt: Max); - -impl> Layout for Max { - fn layout (&self, area: XYWH) -> XYWH { - let XYWH(x, y, w, h) = self.inner().layout(area); - match self { - Self::X(mw, _) => XYWH(x, y, w.min(*mw), h ), - Self::Y(mh, _) => XYWH(x, y, w, h.min(*mh)), - Self::XY(mw, mh, _) => XYWH(x, y, w.min(*mw), h.min(*mh)), - } - } -} - -layout_op_xy!(1 opt: Min); - -impl> Layout for Min { - fn layout (&self, area: XYWH) -> XYWH { - let XYWH(x, y, w, h) = self.inner().layout(area); - match self { - Self::X(mw, _) => XYWH(x, y, w.max(*mw), h), - Self::Y(mh, _) => XYWH(x, y, w, h.max(*mh)), - Self::XY(mw, mh, _) => XYWH(x, y, w.max(*mw), h.max(*mh)), - } - } -} - -layout_op_xy!(1 opt: Expand); - -impl> Layout for Expand { - fn layout_w (&self, to: XYWH) -> O::Unit { - self.inner().layout_w(to).plus(self.dx().unwrap_or_default()) - } - fn layout_h (&self, to: XYWH) -> O::Unit { - self.inner().layout_w(to).plus(self.dy().unwrap_or_default()) - } -} - -// FIXME: why they differ? - -layout_op_xy!(1 opt: Shrink); - -impl> Layout for Shrink { - fn layout (&self, to: XYWH) -> XYWH { - let area = self.inner().layout(to); - let dx = self.dx().unwrap_or_default(); - let dy = self.dy().unwrap_or_default(); - XYWH(area.x(), area.y(), area.w().minus(dx), area.h().minus(dy)) - } -} - -impl Align { - #[inline] pub const fn c (a: T) -> Self { Self(Alignment::Center, a) } - #[inline] pub const fn x (a: T) -> Self { Self(Alignment::X, a) } - #[inline] pub const fn y (a: T) -> Self { Self(Alignment::Y, a) } - #[inline] pub const fn n (a: T) -> Self { Self(Alignment::N, a) } - #[inline] pub const fn s (a: T) -> Self { Self(Alignment::S, a) } - #[inline] pub const fn e (a: T) -> Self { Self(Alignment::E, a) } - #[inline] pub const fn w (a: T) -> Self { Self(Alignment::W, a) } - #[inline] pub const fn nw (a: T) -> Self { Self(Alignment::NW, a) } - #[inline] pub const fn sw (a: T) -> Self { Self(Alignment::SW, a) } - #[inline] pub const fn ne (a: T) -> Self { Self(Alignment::NE, a) } - #[inline] pub const fn se (a: T) -> Self { Self(Alignment::SE, a) } -} - -impl> Layout for Align { - fn layout_x (&self, to: XYWH) -> O::Unit { - use Alignment::*; - match self.0 { - NW | W | SW => to.x(), - N | Center | S => to.x().plus(to.w() / 2.into()).minus(self.1.layout_w(to) / 2.into()), - NE | E | SE => to.x().plus(to.w()).minus(self.1.layout_w(to)), - _ => todo!(), - } - } - fn layout_y (&self, to: XYWH) -> O::Unit { - use Alignment::*; - match self.0 { - NW | N | NE => to.y(), - W | Center | E => to.y().plus(to.h() / 2.into()).minus(self.1.layout_h(to) / 2.into()), - SW | S | SE => to.y().plus(to.h()).minus(self.1.layout_h(to)), - _ => todo!(), - } - } -} - -impl Pad { - #[inline] pub const fn inner (&self) -> &A { - use Pad::*; - match self { X(_, c) | Y(_, c) | XY(_, _, c) => c, } - } -} - -impl Pad { - #[inline] pub fn dx (&self) -> U { - use Pad::*; - match self { X(x, _) => *x, Y(_, _) => 0.into(), XY(x, _, _) => *x, } - } - #[inline] pub fn dy (&self) -> U { - use Pad::*; - match self { X(_, _) => 0.into(), Y(y, _) => *y, XY(_, y, _) => *y, } - } -} - -impl> Layout for Pad { - fn layout_x (&self, area: XYWH) -> O::Unit { area.x().plus(self.dx()) } - fn layout_y (&self, area: XYWH) -> O::Unit { area.x().plus(self.dx()) } - fn layout_w (&self, area: XYWH) -> O::Unit { area.w().minus(self.dx() * 2.into()) } - fn layout_h (&self, area: XYWH) -> O::Unit { area.h().minus(self.dy() * 2.into()) } -} - -impl Bsp { - #[inline] pub const fn n (a: Head, b: Tail) -> Self { Self(North, a, b) } - #[inline] pub const fn s (a: Head, b: Tail) -> Self { Self(South, a, b) } - #[inline] pub const fn e (a: Head, b: Tail) -> Self { Self(East, a, b) } - #[inline] pub const fn w (a: Head, b: Tail) -> Self { Self(West, a, b) } - #[inline] pub const fn a (a: Head, b: Tail) -> Self { Self(Above, a, b) } - #[inline] pub const fn b (a: Head, b: Tail) -> Self { Self(Below, a, b) } -} - -impl, Tail: Layout> Layout for Bsp { - fn layout_w (&self, area: XYWH) -> O::Unit { - match self.0 { - Above | Below | North | South => self.1.layout_w(area).max(self.2.layout_w(area)), - East | West => self.1.layout_w_min(area).plus(self.2.layout_w(area)), - } - } - fn layout_w_min (&self, area: XYWH) -> O::Unit { - match self.0 { - Above | Below | North | South => self.1.layout_w_min(area).max(self.2.layout_w_min(area)), - East | West => self.1.layout_w_min(area).plus(self.2.layout_w_min(area)), - } - } - fn layout_w_max (&self, area: XYWH) -> O::Unit { - match self.0 { - Above | Below | North | South => self.1.layout_w_max(area).max(self.2.layout_w_max(area)), - East | West => self.1.layout_w_max(area).plus(self.2.layout_w_max(area)), - } - } - fn layout_h (&self, area: XYWH) -> O::Unit { - match self.0 { - Above | Below | East | West => self.1.layout_h(area).max(self.2.layout_h(area)), - North | South => self.1.layout_h(area).plus(self.2.layout_h(area)), - } - } - fn layout_h_min (&self, area: XYWH) -> O::Unit { - match self.0 { - Above | Below | East | West => self.1.layout_h_min(area).max(self.2.layout_h_min(area)), - North | South => self.1.layout_h_min(area).plus(self.2.layout_h_min(area)), - } - } - fn layout_h_max (&self, area: XYWH) -> O::Unit { - match self.0 { - Above | Below | North | South => self.1.layout_h_max(area).max(self.2.layout_h_max(area)), - East | West => self.1.layout_h_max(area).plus(self.2.layout_h_max(area)), - } - } - fn layout (&self, area: XYWH) -> XYWH { - bsp_areas(area, self.0, &self.1, &self.2)[2] - } -} - -impl<'a, O, A, B, I, F, G> Map where - I: Iterator + Send + Sync + 'a, - F: Fn() -> I + Send + Sync + 'a, -{ - pub const fn new (get_iter: F, get_item: G) -> Self { - Self { - __: PhantomData, - get_iter, - get_item - } - } - const fn to_south ( - item_offset: S::Unit, item_height: S::Unit, item: impl Draw - ) -> impl Draw { - Push::Y(item_offset, Fixed::Y(item_height, Fill::X(item))) - } - const fn to_south_west ( - item_offset: S::Unit, item_height: S::Unit, item: impl Draw - ) -> impl Draw { - Push::Y(item_offset, Align::nw(Fixed::Y(item_height, Fill::X(item)))) - } - const fn to_east ( - item_offset: S::Unit, item_width: S::Unit, item: impl Draw - ) -> impl Draw { - Push::X(item_offset, Align::w(Fixed::X(item_width, Fill::Y(item)))) - } -} - -macro_rules! impl_map_direction (($name:ident, $axis:ident, $align:ident)=>{ - impl<'a, O, A, B, I, F> Map< - O, A, Push>>>, I, F, fn(A, usize)->B - > where - O: Screen, - B: Draw, - I: Iterator + Send + Sync + 'a, - F: Fn() -> I + Send + Sync + 'a - { - pub const fn $name ( - size: O::Unit, - get_iter: F, - get_item: impl Fn(A, usize)->B + Send + Sync - ) -> Map< - O, A, - Push>>, - I, F, - impl Fn(A, usize)->Push>> + Send + Sync - > { - Map { - __: PhantomData, - get_iter, - get_item: move |item: A, index: usize|{ - // FIXME: multiply - let mut push: O::Unit = O::Unit::from(0u16); - for _ in 0..index { - push = push + size; - } - Push::$axis(push, Align::$align(Fixed::$axis(size, get_item(item, index)))) - } - } - } - } -}); -impl_map_direction!(east, X, w); -impl_map_direction!(south, Y, n); -impl_map_direction!(west, X, e); -impl_map_direction!(north, Y, s); - -impl<'a, O, A, B, I, F, G> Layout for Map where - O: Screen, - B: Layout, - I: Iterator + Send + Sync + 'a, - F: Fn() -> I + Send + Sync + 'a, - G: Fn(A, usize)->B + Send + Sync -{ - fn layout (&self, area: XYWH) -> XYWH { - let Self { get_iter, get_item, .. } = self; - let mut index = 0; - let XY(mut min_x, mut min_y) = area.centered(); - let XY(mut max_x, mut max_y) = area.center(); - for item in get_iter() { - let XYWH(x, y, w, h) = get_item(item, index).layout(area); - min_x = min_x.min(x); - min_y = min_y.min(y); - max_x = max_x.max(x + w); - max_y = max_y.max(y + h); - index += 1; - } - let w = max_x - min_x; - let h = max_y - min_y; - //[min_x.into(), min_y.into(), w.into(), h.into()].into() - area.centered_xy([w.into(), h.into()]) - } -} - -impl<'a, O, A, B, I, F, G> Draw for Map where - O: Screen, - B: Draw, - I: Iterator + Send + Sync + 'a, - F: Fn() -> I + Send + Sync + 'a, - G: Fn(A, usize)->B + Send + Sync -{ - fn draw (&self, to: &mut O) { - let Self { get_iter, get_item, .. } = self; - let mut index = 0; - let area = self.layout(to.area()); - for item in get_iter() { - let item = get_item(item, index); - //to.show(area.into(), &item); - to.show(item.layout(area), &item); - index += 1; - } - } -} - -impl_default!(PerfModel: Self { - enabled: true, - clock: quanta::Clock::new(), - used: Default::default(), - window: Default::default(), -}); - -impl PerfModel { - pub fn get_t0 (&self) -> Option { - if self.enabled { - Some(self.clock.raw()) - } else { - None - } - } - pub fn get_t1 (&self, t0: Option) -> Option { - if let Some(t0) = t0 { - if self.enabled { - Some(self.clock.delta(t0, self.clock.raw())) - } else { - None - } - } else { - None - } - } - pub fn update (&self, t0: Option, microseconds: f64) { - if let Some(t0) = t0 { - let t1 = self.clock.raw(); - self.used.store(self.clock.delta_as_nanos(t0, t1) as f64, Relaxed); - self.window.store(microseconds, Relaxed,); - } - } - pub fn percentage (&self) -> Option { - let window = self.window.load(Relaxed) * 1000.0; - if window > 0.0 { - let used = self.used.load(Relaxed); - Some(100.0 * used / window) - } else { - None - } - } -} mod xywh { use crate::*; - impl X for XY { fn x (&self) -> N { self.0 } } - impl Y for XY { fn y (&self) -> N { self.1 } } - impl W for WH { fn w (&self) -> N { self.0 } } - impl H for WH { fn h (&self) -> N { self.1 } } - impl X for XYWH { fn x (&self) -> N { self.0 } } - impl Y for XYWH { fn y (&self) -> N { self.1 } } - impl W for XYWH { fn w (&self) -> N { self.2 } } - impl H for XYWH { fn h (&self) -> N { self.3 } } - impl X for O { fn x (&self) -> O::Unit { self.area().x() } } - impl Y for O { fn y (&self) -> O::Unit { self.area().y() } } - impl W for O { fn w (&self) -> O::Unit { self.area().w() } } - impl H for O { fn h (&self) -> O::Unit { self.area().h() } } - - impl WH { - pub fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w), self.h()] } - pub fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h)] } - pub fn expect_min (&self, w: N, h: N) -> Usually<&Self> { - if self.w() < w || self.h() < h { return Err(format!("min {w}x{h}").into()) } - Ok(self) - } - } - impl XYWH { - pub fn zero () -> Self { - Self(0.into(), 0.into(), 0.into(), 0.into()) - } - pub fn x2 (&self) -> N { - self.x().plus(self.w()) - } - pub fn y2 (&self) -> N { - self.y().plus(self.h()) - } - pub fn with_w (&self, w: N) -> XYWH { - Self(self.x(), self.y(), w, self.h()) - } - pub fn with_h (&self, h: N) -> XYWH { - Self(self.x(), self.y(), self.w(), h) - } - pub fn lrtb (&self) -> [N;4] { - [self.x(), self.x2(), self.y(), self.y2()] - } - pub fn clipped_w (&self, w: N) -> XYWH { - Self(self.x(), self.y(), self.w().min(w), self.h()) - } - pub fn clipped_h (&self, h: N) -> XYWH { - Self(self.x(), self.y(), self.w(), self.h().min(h)) - } - pub fn clipped (&self, wh: WH) -> XYWH { - Self(self.x(), self.y(), wh.w(), wh.h()) - } - /// Iterate over every covered X coordinate. - pub fn iter_x (&self) -> impl Iterator where N: std::iter::Step { - let Self(x, _, w, _) = *self; - x..(x+w) - } - /// Iterate over every covered Y coordinate. - pub fn iter_y (&self) -> impl Iterator where N: std::iter::Step { - let Self(_, y, _, h) = *self; - y..(y+h) - } - pub fn center (&self) -> XY { - let Self(x, y, w, h) = self; - XY(self.x().plus(self.w()/2.into()), self.y().plus(self.h()/2.into())) - } - pub fn centered (&self) -> XY { - let Self(x, y, w, h) = *self; - XY(x.minus(w/2.into()), y.minus(h/2.into())) - } - pub fn centered_x (&self, n: N) -> XYWH { - let Self(x, y, w, h) = *self; - XYWH((x.plus(w / 2.into())).minus(n / 2.into()), y.plus(h / 2.into()), n, 1.into()) - } - pub fn centered_y (&self, n: N) -> XYWH { - let Self(x, y, w, h) = *self; - XYWH(x.plus(w / 2.into()), (y.plus(h / 2.into())).minus(n / 2.into()), 1.into(), n) - } - pub fn centered_xy (&self, [n, m]: [N;2]) -> XYWH { - let Self(x, y, w, h) = *self; - XYWH((x.plus(w / 2.into())).minus(n / 2.into()), (y.plus(h / 2.into())).minus(m / 2.into()), n, m) - } - } -} - -impl Thread { - /// Spawn a TUI thread that runs `callt least one, then repeats until `exit`. - pub fn new (exit: Arc, call: F) -> Result - where F: Fn(&PerfModel)->() + Send + Sync + 'static - { - let perf = Arc::new(PerfModel::default()); - Ok(Self { - exit: exit.clone(), - perf: perf.clone(), - join: std::thread::Builder::new().name("tengri tui output".into()).spawn(move || { - while !exit.fetch_and(true, Relaxed) { - let _ = perf.cycle(&call); - } - })?.into() - }) - } - - /// Spawn a TUI thread that runs `callt least one, then repeats - /// until `exit`, sleeping for `time` msec after every iteration. - pub fn new_sleep ( - exit: Arc, time: Duration, call: F - ) -> Result - where F: Fn(&PerfModel)->() + Send + Sync + 'static - { - Self::new(exit, move |perf| { let _ = call(perf); std::thread::sleep(time); }) - } - - /// Spawn a TUI thread that runs `callt least one, then repeats - /// until `exit`, using polling to run every `time` msec. - pub fn new_poll ( - exit: Arc, time: Duration, call: F - ) -> Result - where F: Fn(&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 = "tui")] pub use self::tui_impls::*; -#[cfg(feature = "tui")] mod tui_impls { - use crate::*; - use unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; - use rand::distributions::uniform::UniformSampler; - impl Coord for u16 { fn plus (self, other: Self) -> Self { self.saturating_add(other) } } - impl_from!(BigBuffer: |size:(usize, usize)| Self::new(size.0, size.1)); - impl_from!(ItemTheme: |base: ItemColor| Self::from_item_color(base)); - impl_from!(ItemTheme: |base: Color| Self::from_tui_color(base)); - impl_from!(ItemColor: |rgb: Color| Self { rgb, okhsl: rgb_to_okhsl(rgb) }); - impl_from!(ItemColor: |okhsl: Okhsl| Self { okhsl, rgb: okhsl_to_rgb(okhsl) }); - impl_debug!(BigBuffer |self, f| { write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len()) }); - impl Screen for Buffer { type Unit = u16; } - impl TuiOut for Buffer { fn tui_out (&mut self) -> &mut Buffer { self } } - impl TuiOut for Buffer { fn tui_out (&mut self) -> &mut Buffer { self } } - - //impl> TuiOut for T { fn tui_out (&mut self) -> &mut Buffer { self.as_mut() } } - - impl Tui { - /// True if done - pub fn exited (&self) -> bool { self.exited.fetch_and(true, Relaxed) } - /// Prepare before run - pub fn setup (&self) -> Usually<()> { tui_setup(&mut*self.backend.write().unwrap()) } - /// Clean up after run - pub fn teardown (&self) -> Usually<()> { tui_teardown(&mut*self.backend.write().unwrap()) } - /// Apply changes to the display buffer. - pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { tui_flip(self, self.buffer, buffer, size) } - /// Create the engine. - pub fn new (output: Box) -> Usually { - let backend = CrosstermBackend::new(output); - let Size { width, height } = backend.size()?; - Ok(Self { - exited: Arc::new(AtomicBool::new(false)), - buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }).into(), - area: XYWH(0, 0, width, height), - perf: Default::default(), - backend: backend.into(), - event: None, - error: None, - }) - } - /// Run an app in the engine. - pub fn run (mut self, join: bool, state: &Arc>) -> Usually> where - T: Act + Draw + Send + Sync + 'static - { - self.setup()?; - let tui = Arc::new(self); - let input_poll = Duration::from_millis(100); - let output_sleep = Duration::from_millis(10); // == 1/MAXFPS (?) - let _input_thread = tui_input(&tui, state, input_poll)?; - let render_thread = tui_output(&tui, state, output_sleep)?; - if join { // Run until render thread ends: - let result = render_thread.join(); - tui.teardown()?; - match result { - Ok(result) => println!("\n\rRan successfully: {result:?}\n\r"), - Err(error) => panic!("\n\rDraw thread failed: error={error:?}.\n\r"), - } - } - Ok(tui) - } - } - - - impl TuiEvent { - pub fn from_crossterm (event: Event) -> Self { Self(event) } - #[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Perhaps { - Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self)) - } - } - - impl Ord for TuiEvent { - fn cmp (&self, other: &Self) -> std::cmp::Ordering { - self.partial_cmp(other) - .unwrap_or_else(||format!("{:?}", self).cmp(&format!("{other:?}"))) // FIXME perf - } - } - - impl From for TuiEvent { - fn from (e: Event) -> Self { - Self(e) - } - } - - impl From for TuiEvent { - fn from (c: char) -> Self { - Self(Event::Key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE))) - } - } - - impl TuiKey { - const SPLIT: char = '/'; - #[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Usually { - if let Some(word) = dsl.word()? { - let word = word.trim(); - Ok(if word == ":char" { - Self(None, KeyModifiers::NONE) - } else if word.chars().nth(0) == Some('@') { - let mut key = None; - let mut modifiers = KeyModifiers::NONE; - let mut tokens = word[1..].split(Self::SPLIT).peekable(); - while let Some(token) = tokens.next() { - if tokens.peek().is_some() { - match token { - "ctrl" | "Ctrl" | "c" | "C" => modifiers |= KeyModifiers::CONTROL, - "alt" | "Alt" | "m" | "M" => modifiers |= KeyModifiers::ALT, - "shift" | "Shift" | "s" | "S" => { - modifiers |= KeyModifiers::SHIFT; - // + TODO normalize character case, BackTab, etc. - }, - _ => panic!("unknown modifier {token}"), - } - } else { - key = if token.len() == 1 { - Some(KeyCode::Char(token.chars().next().unwrap())) - } else { - Some(named_key(token).unwrap_or_else(||panic!("unknown character {token}"))) - } - } - } - Self(key, modifiers) - } else { - return Err(format!("TuiKey: unexpected: {word}").into()) - }) - } else { - return Err(format!("TuiKey: unspecified").into()) - } - } - pub fn to_crossterm (&self) -> Option { - self.0.map(|code|Event::Key(KeyEvent { - code, - modifiers: self.1, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - })) - } - } - - impl BigBuffer { - pub fn new (width: usize, height: usize) -> Self { - Self { width, height, content: vec![Cell::default(); width*height] } - } - pub fn get (&self, x: usize, y: usize) -> Option<&Cell> { - let i = self.index_of(x, y); - self.content.get(i) - } - pub fn get_mut (&mut self, x: usize, y: usize) -> Option<&mut Cell> { - let i = self.index_of(x, y); - self.content.get_mut(i) - } - pub fn index_of (&self, x: usize, y: usize) -> usize { - y * self.width + x - } - } - - impl PerfModel { - fn cycle T, T> (&self, call: &F) -> T { - let t0 = self.get_t0(); - let result = call(self); - let _t1 = self.get_t1(t0).unwrap(); - result - } - } - - impl Phat { - pub const LO: &'static str = "▄"; - pub const HI: &'static str = "▀"; - /// A phat line - pub fn lo (fg: Color, bg: Color) -> impl Draw { - Fixed::Y(1, Tui::fg_bg(fg, bg, Repeat::X(Self::LO))) - } - /// A phat line - pub fn hi (fg: Color, bg: Color) -> impl Draw { - Fixed::Y(1, Tui::fg_bg(fg, bg, Repeat::X(Self::HI))) - } - } - impl Scrollbar { - const ICON_DEC_V: &[char] = &['▲']; - const ICON_INC_V: &[char] = &['▼']; - const ICON_DEC_H: &[char] = &[' ', '🞀', ' ']; - const ICON_INC_H: &[char] = &[' ', '🞂', ' ']; - } - impl Layout for &str { - fn layout (&self, to: XYWH) -> XYWH { - to.centered_xy([width_chars_max(to.w(), self), 1]) - } - } - impl Layout for String { - fn layout (&self, to: XYWH) -> XYWH { - self.as_str().layout(to) - } - } - impl Layout for Arc { - fn layout (&self, to: XYWH) -> XYWH { - self.as_ref().layout(to) - } - } - impl<'a, T: AsRef> Layout for TrimString { - fn layout (&self, to: XYWH) -> XYWH { - Layout::layout(&self.as_ref(), to) - } - } - impl<'a, T: AsRef> Layout for TrimStringRef<'a, T> { - fn layout (&self, to: XYWH) -> XYWH { - XYWH(to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h()) - } - } - - impl Draw for u64 { - fn draw (&self, _to: &mut T) { todo!() } - } - - impl Draw for f64 { - fn draw (&self, _to: &mut Buffer) { - todo!() - } - } - - impl Draw for Repeat<'_> { - fn draw (&self, to: &mut Buffer) { - let XYWH(x, y, w, h) = to.area(); - let mut buf = to.buffer.write().unwrap(); - match self { - Self::X(c) => { - for x in x..x+w { - if let Some(cell) = buf.cell_mut(Position::from((x, y))) { - cell.set_symbol(&c); - } - } - }, - Self::Y(c) => { - for y in y..y+h { - if let Some(cell) = buf.cell_mut(Position::from((x, y))) { - cell.set_symbol(&c); - } - } - }, - Self::XY(c) => { - let a = c.len(); - for (_v, y) in (y..y+h).enumerate() { - for (u, x) in (x..x+w).enumerate() { - if let Some(cell) = buf.cell_mut(Position::from((x, y))) { - let u = u % a; - cell.set_symbol(&c[u..u+1]); - } - } - } - }, - } - } - } - - impl Draw for Scrollbar { - fn draw (&self, to: &mut Buffer) { - let XYWH(x1, y1, w, h) = to.area(); - let mut buf = to.buffer.write().unwrap(); - match self { - Self::X { .. } => { - let x2 = x1 + w; - for (i, x) in (x1..=x2).enumerate() { - if let Some(cell) = buf.cell_mut(Position::from((x, y1))) { - if i < (Self::ICON_DEC_H.len()) { - cell.set_fg(Rgb(255, 255, 255)); - cell.set_bg(Rgb(0, 0, 0)); - cell.set_char(Self::ICON_DEC_H[i as usize]); - } else if i > (w as usize - Self::ICON_INC_H.len()) { - cell.set_fg(Rgb(255, 255, 255)); - cell.set_bg(Rgb(0, 0, 0)); - cell.set_char(Self::ICON_INC_H[w as usize - i]); - } else if false { - cell.set_fg(Rgb(255, 255, 255)); - cell.set_bg(Reset); - cell.set_char('━'); - } else { - cell.set_fg(Rgb(0, 0, 0)); - cell.set_bg(Reset); - cell.set_char('╌'); - } - } - } - }, - Self::Y { .. } => { - let y2 = y1 + h; - for (i, y) in (y1..=y2).enumerate() { - if let Some(cell) = buf.cell_mut(Position::from((x1, y))) { - if (i as usize) < (Self::ICON_DEC_V.len()) { - cell.set_fg(Rgb(255, 255, 255)); - cell.set_bg(Rgb(0, 0, 0)); - cell.set_char(Self::ICON_DEC_V[i as usize]); - } else if (i as usize) > (h as usize - Self::ICON_INC_V.len()) { - cell.set_fg(Rgb(255, 255, 255)); - cell.set_bg(Rgb(0, 0, 0)); - cell.set_char(Self::ICON_INC_V[h as usize - i]); - } else if false { - cell.set_fg(Rgb(255, 255, 255)); - cell.set_bg(Reset); - cell.set_char('‖'); // ━ - } else { - cell.set_fg(Rgb(0, 0, 0)); - cell.set_bg(Reset); - cell.set_char('╎'); // ━ - } - } - } - }, - } - } - } - - impl Draw for &str { - fn draw (&self, to: &mut Buffer) { - let XYWH(x, y, w, ..) = self.layout(to.area()); - to.text(&self, x, y, w) - } - } - - impl Draw for String { - fn draw (&self, to: &mut Buffer) { - self.as_str().draw(to) - } - } - - impl Draw for Arc { - fn draw (&self, to: &mut Buffer) { self.as_ref().draw(to) } - } - - impl> Draw for Foreground { - fn draw (&self, to: &mut Buffer) { - let area = self.layout(to.area()); - to.fill_fg(area, self.0); - to.show(area, &self.1); - } - } - - impl> Draw for Background { - fn draw (&self, to: &mut Buffer) { - let area = self.layout(to.area()); - to.fill_bg(area, self.0); - to.show(area, &self.1); - } - } - - impl> Draw for Modify { - fn draw (&self, to: &mut Buffer) { - to.fill_mod(to.area(), self.0, self.1); - self.2.draw(to) - } - } - - impl> Draw for Styled { - fn draw (&self, to: &mut Buffer) { - to.place(&self.1); - // TODO write style over area - } - } - - impl Draw for Border { - fn draw (&self, to: &mut Buffer) { - let Border(enabled, style) = self; - if *enabled { - let area = to.area(); - if area.w() > 0 && area.y() > 0 { - to.blit(&style.border_nw(), area.x(), area.y(), style.style()); - to.blit(&style.border_ne(), area.x() + area.w() - 1, area.y(), style.style()); - to.blit(&style.border_sw(), area.x(), area.y() + area.h() - 1, style.style()); - to.blit(&style.border_se(), area.x() + area.w() - 1, area.y() + area.h() - 1, style.style()); - for x in area.x()+1..area.x()+area.w()-1 { - to.blit(&style.border_n(), x, area.y(), style.style()); - to.blit(&style.border_s(), x, area.y() + area.h() - 1, style.style()); - } - for y in area.y()+1..area.y()+area.h()-1 { - to.blit(&style.border_w(), area.x(), y, style.style()); - to.blit(&style.border_e(), area.x() + area.w() - 1, y, style.style()); - } - } - } - } - } - - impl<'a, T: AsRef> Draw for TrimString { - fn draw (&self, to: &mut Buffer) { Draw::draw(&self.as_ref(), to) } - } - - impl> Draw for TrimStringRef<'_, T> { - fn draw (&self, target: &mut Buffer) { - let area = target.area(); - let mut buf = target.buffer.write().unwrap(); - let mut width: u16 = 1; - let mut chars = self.1.as_ref().chars(); - while let Some(c) = chars.next() { - if width > self.0 || width > area.w() { - break - } - if let Some(cell) = buf.cell_mut(Position { - x: area.x() + width - 1, - y: area.y() - }) { - cell.set_char(c); - } - width += c.width().unwrap_or(0) as u16; - } - } - } - - /// TUI helper defs. - impl Tui { - pub const fn fg (color: Color, w: T) -> Foreground { Foreground(color, w) } - pub const fn bg (color: Color, w: T) -> Background { Background(color, w) } - pub const fn fg_bg (fg: Color, bg: Color, w: T) -> Background> { Background(bg, Foreground(fg, w)) } - pub const fn modify (enable: bool, modifier: Modifier, w: T) -> Modify { Modify(enable, modifier, w) } - pub const fn bold (enable: bool, w: T) -> Modify { Self::modify(enable, Modifier::BOLD, w) } - pub const fn border (enable: bool, style: S, w: T) -> Bordered { Bordered(enable, style, w) } - - pub const fn null () -> Color { Color::Reset } - pub const fn red () -> Color { Color::Rgb(255,0, 0) } - pub const fn orange () -> Color { Color::Rgb(255,128,0) } - pub const fn yellow () -> Color { Color::Rgb(255,255,0) } - pub const fn brown () -> Color { Color::Rgb(128,255,0) } - pub const fn green () -> Color { Color::Rgb(0,255,0) } - pub const fn electric () -> Color { Color::Rgb(0,255,128) } - pub const fn g (g: u8) -> Color { Color::Rgb(g, g, g) } - //fn bg0 () -> Color { Color::Rgb(20, 20, 20) } - //fn bg () -> Color { Color::Rgb(28, 35, 25) } - //fn border_bg () -> Color { Color::Rgb(40, 50, 30) } - //fn border_fg (f: bool) -> Color { if f { Self::bo1() } else { Self::bo2() } } - //fn title_fg (f: bool) -> Color { if f { Self::ti1() } else { Self::ti2() } } - //fn separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) } - //fn mode_bg () -> Color { Color::Rgb(150, 160, 90) } - //fn mode_fg () -> Color { Color::Rgb(255, 255, 255) } - //fn status_bar_bg () -> Color { Color::Rgb(28, 35, 25) } - //fn bo1 () -> Color { Color::Rgb(100, 110, 40) } - //fn bo2 () -> Color { Color::Rgb(70, 80, 50) } - //fn ti1 () -> Color { Color::Rgb(150, 160, 90) } - //fn ti2 () -> Color { Color::Rgb(120, 130, 100) } - } - pub fn named_key (token: &str) -> Option { - use KeyCode::*; - Some(match token { - "up" => Up, - "down" => Down, - "left" => Left, - "right" => Right, - "esc" | "escape" => Esc, - "enter" | "return" => Enter, - "delete" | "del" => Delete, - "backspace" => Backspace, - "tab" => Tab, - "space" => Char(' '), - "comma" => Char(','), - "period" => Char('.'), - "plus" => Char('+'), - "minus" | "dash" => Char('-'), - "equal" | "equals" => Char('='), - "underscore" => Char('_'), - "backtick" => Char('`'), - "lt" => Char('<'), - "gt" => Char('>'), - "cbopen" | "openbrace" => Char('{'), - "cbclose" | "closebrace" => Char('}'), - "bropen" | "openbracket" => Char('['), - "brclose" | "closebracket" => Char(']'), - "pgup" | "pageup" => PageUp, - "pgdn" | "pagedown" => PageDown, - "f1" => F(1), - "f2" => F(2), - "f3" => F(3), - "f4" => F(4), - "f5" => F(5), - "f6" => F(6), - "f7" => F(7), - "f8" => F(8), - "f9" => F(9), - "f10" => F(10), - "f11" => F(11), - "f12" => F(12), - _ => return None, - }) - } - impl> Layout for Foreground { - fn layout (&self, to: XYWH) -> XYWH { self.1.layout(to) } - } - impl> Layout for Background { - fn layout (&self, to: XYWH) -> XYWH { self.1.layout(to) } - } - impl Tryptich<(), (), ()> { - pub fn center (h: u16) -> Self { - Self { h, top: false, left: (0, ()), middle: (0, ()), right: (0, ()) } - } - pub fn top (h: u16) -> Self { - Self { h, top: true, left: (0, ()), middle: (0, ()), right: (0, ()) } - } - } - impl Tryptich { - pub fn left (self, w: u16, content: D) -> Tryptich { - Tryptich { left: (w, content), ..self } - } - pub fn middle (self, w: u16, content: D) -> Tryptich { - Tryptich { middle: (w, content), ..self } - } - pub fn right (self, w: u16, content: D) -> Tryptich { - Tryptich { right: (w, content), ..self } - } - } - - /// ``` - /// let _ = tengri::button_2("", "", true); - /// let _ = tengri::button_2("", "", false); - /// ``` - pub fn button_2 <'a> (key: impl Draw, label: impl Draw, editing: bool) -> impl Draw { - Tui::bold(true, Bsp::e( - Tui::fg_bg(Tui::orange(), Tui::g(0), Bsp::e(Tui::fg(Tui::g(0), &"▐"), Bsp::e(key, Tui::fg(Tui::g(96), &"▐")))), - When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label)))) - } - - /// ``` - /// let _ = tengri::button_3("", "", "", true); - /// let _ = tengri::button_3("", "", "", false); - /// ``` - pub fn button_3 <'a> ( - key: impl Draw, label: impl Draw, value: impl Draw, editing: bool, - ) -> impl Draw { - Tui::bold(true, Bsp::e( - Tui::fg_bg(Tui::orange(), Tui::g(0), - Bsp::e(Tui::fg(Tui::g(0), &"▐"), Bsp::e(key, Tui::fg(if editing { Tui::g(128) } else { Tui::g(96) }, "▐")))), - Bsp::e( - When::new(!editing, Bsp::e(Tui::fg_bg(Tui::g(255), Tui::g(96), label), Tui::fg_bg(Tui::g(128), Tui::g(96), &"▐"),)), - Bsp::e(Tui::fg_bg(Tui::g(224), Tui::g(128), value), Tui::fg_bg(Tui::g(128), Reset, &"▌"), )))) - } - - macro_rules! border { - ($($T:ident { - $nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal - $($x:tt)* - }),+) => {$( - impl BorderStyle for $T { - const NW: &'static str = $nw; - const N: &'static str = $n; - const NE: &'static str = $ne; - const W: &'static str = $w; - const E: &'static str = $e; - const SW: &'static str = $sw; - const S: &'static str = $s; - const SE: &'static str = $se; - $($x)* - fn enabled (&self) -> bool { self.0 } - } - #[derive(Copy, Clone)] pub struct $T(pub bool, pub Style); - impl Layout for $T {} - impl Draw for $T { - fn draw (&self, to: &mut Buffer) { - if self.enabled() { let _ = BorderStyle::draw(self, to); } - } - } - )+} - } - - border! { - Square { - "┌" "─" "┐" - "│" "│" - "└" "─" "┘" fn style (&self) -> Option