diff --git a/Cargo.toml b/Cargo.toml index e442ec2..96c6bf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,35 +5,31 @@ version = "0.15.0" description = "UI metaframework." [features] -default = ["lang", "sing", "draw", "play", "tui", "text", "time", "rand", "okhsl"] -bumpalo = ["dep:bumpalo"] -draw = [] -dsl = ["dep:dizzle"] -gui = ["draw", "dep:winit"] +default = ["lang", "sing", "draw", "tui"] lang = ["dep:dizzle"] -okhsl = ["dep:palette"] -play = [] -rand = ["dep:rand"] sing = ["dep:jack"] -text = ["dep:unicode-width"] -time = ["dep:quanta"] +draw = [] tui = ["draw", "dep:ratatui", "dep:crossterm"] +gui = ["draw", "dep:winit"] + +bumpalo = ["dep:bumpalo"] +dsl = ["dep:dizzle"] [dependencies] anyhow = { version = "1.0" } atomic_float = { version = "1" } better-panic = { version = "0.3.0" } +palette = { version = "0.7.6", features = [ "random" ] } +quanta = { version = "0.12.3" } +rand = { version = "0.8.5" } +unicode-width = { version = "0.2" } -bumpalo = { optional = true, version = "3.19.0" } -crossterm = { optional = true, version = "0.29.0" } -dizzle = { optional = true, path = "./dizzle" } -jack = { optional = true, path = "./rust-jack" } -palette = { optional = true, version = "0.7.6", features = [ "random" ] } -quanta = { optional = true, version = "0.12.3" } -rand = { optional = true, version = "0.8.5" } -ratatui = { optional = true, version = "0.29.0", features = [ "unstable-widget-ref", "underline-color" ] } -unicode-width = { optional = true, version = "0.2" } -winit = { optional = true, version = "0.30.4", features = [ "x11" ]} +dizzle = { optional = true, path = "./dizzle" } +jack = { optional = true, path = "./rust-jack" } +winit = { optional = true, version = "0.30.4", features = [ "x11" ]} +bumpalo = { optional = true, version = "3.19.0" } +crossterm = { optional = true, version = "0.29.0" } +ratatui = { optional = true, version = "0.29.0", features = [ "unstable-widget-ref", "underline-color" ] } [dev-dependencies] proptest = { version = "^1" } @@ -41,6 +37,12 @@ proptest-derive = { version = "^0.5.1" } tengri = { path = ".", features = [ "dsl" ] } #tengri_proc = { path = "./proc" } +[lib] +path = "src/tengri.rs" + +[profile.release] +lto = true + [profile.coverage] inherits = "test" lto = false diff --git a/README.md b/README.md index 4cd2d57..0bf1e39 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,15 @@ connect to jack audio connection kit to process chunks of audio and midi. ### draw -backend-agnostic layout pattern. +abstract interface layout system for defining interface layouts abstractly. ### play the input handling system. -### term +### tui -uses `ratatui` to render in in a terminal. +uses `ratatui` to run in a terminal. ### gui (todo-todo-todo) @@ -31,14 +31,6 @@ opens windows, runs shaders in them and/or delegates them (to e.g. plugin guis). uses `dizzle` to let you livecode all of the above. -### time - -performance counter. - -### text - -backend-agnostic text layout. - ## license here and now, the blessings of `tengri` are invokable under the [**`AGPL3`**](./LICENSE). diff --git a/dizzle b/dizzle index 068e26d..6026aff 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit 068e26dd50699e51e9db01ac23fc0778074647bd +Subproject commit 6026aff29f9a0ca9ecafdfac13ea59ddd65d8f83 diff --git a/src/.scratch.rs b/src/.scratch.rs index beb03ea..1c7cebe 100644 --- a/src/.scratch.rs +++ b/src/.scratch.rs @@ -904,458 +904,3 @@ impl> MenuItem { ////test(":ctrl-alt-shift-x", ////KeyEvent::new(KeyCode::Char('x'), Mods::CONTROL | Mods::ALT | Mods::SHIFT )); //} - -//#[cfg(test)] pub(crate) use proptest_derive::Arbitrary; -//#[cfg(test)] pub(crate) use proptest::{prelude::*, option::of}; -//#[cfg(test)] use Direction::*; -//#[cfg(test)] use proptest_derive::Arbitrary; -//#[cfg(test)] proptest! { - //#[test] fn proptest_direction ( - //d in prop_oneof![ - //Just(North), Just(South), - //Just(East), Just(West), - //Just(Above), Just(Below) - //], - //x in u16::MIN..u16::MAX, - //y in u16::MIN..u16::MAX, - //w in u16::MIN..u16::MAX, - //h in u16::MIN..u16::MAX, - //a in u16::MIN..u16::MAX, - //) { - //let _ = d.split_fixed(XYWH(x, y, w, h), a); - //} -//} - -//proptest! { - //#[test] fn proptest_area ( - //x in u16::MIN..u16::MAX, - //y in u16::MIN..u16::MAX, - //w in u16::MIN..u16::MAX, - //h in u16::MIN..u16::MAX, - //a in u16::MIN..u16::MAX, - //b in u16::MIN..u16::MAX, - //) { - //let _: XYWH = XYWH::zero(); - ////let _: XYWH = XYWH::from_position([a, b]); - ////let _: XYWH = XYWH::from_size([a, b]); - //let area: XYWH = XYWH(x, y, w, h); - ////let _ = area.expect_min(a, b); - //let _ = area.xy(); - //let _ = area.wh(); - ////let _ = area.xywh(); - //let _ = area.clipped_h(a); - //let _ = area.clipped_w(b); - //let _ = area.clipped(WH(a, b)); - ////let _ = area.set_w(a); - ////let _ = area.set_h(b); - //let _ = area.x2(); - //let _ = area.y2(); - //let _ = area.lrtb(); - //let _ = area.center(); - //let _ = area.centered(); - //let _ = area.centered_x(a); - //let _ = area.centered_y(b); - //let _ = area.centered_xy([a, b]); - //} -//} - -//proptest! { - //#[test] fn proptest_size ( - //x in u16::MIN..u16::MAX, - //y in u16::MIN..u16::MAX, - //a in u16::MIN..u16::MAX, - //b in u16::MIN..u16::MAX, - //) { - //let size = WH(x, y); - //let _ = size.w(); - //let _ = size.h(); - //let _ = size.wh(); - //let _ = size.clip_w(a); - //let _ = size.clip_h(b); - ////let _ = size.expect_min(a, b); - ////let _ = size.to_area_pos(); - ////let _ = size.to_area_size(); - //} -//} - -////macro_rules! test_op_transform { - ////($fn:ident, $Op:ident) => { - ////proptest! { - ////#[test] fn $fn ( - ////op_x in of(u16::MIN..u16::MAX), - ////op_y in of(u16::MIN..u16::MAX), - ////content in "\\PC*", - ////x in u16::MIN..u16::MAX, - ////y in u16::MIN..u16::MAX, - ////w in u16::MIN..u16::MAX, - ////h in u16::MIN..u16::MAX, - ////) { - ////if let Some(op) = match (op_x, op_y) { - ////(Some(x), Some(y)) => Some($Op::XY(x, y, content)), - ////(Some(x), None) => Some($Op::X(x, content)), - ////(None, Some(y)) => Some($Op::Y(y, content)), - ////_ => None - ////} { - //////assert_eq!(Content::layout(&op, [x, y, w, h]), - //////Draw::layout(&op, [x, y, w, h])); - ////} - ////} - ////} - ////} -////} -////test_op_transform!(proptest_op_fixed, Fixed); -////test_op_transform!(proptest_op_min, Min); -////test_op_transform!(proptest_op_max, Max); -////test_op_transform!(proptest_op_push, Push); -////test_op_transform!(proptest_op_pull, Pull); -////test_op_transform!(proptest_op_shrink, Shrink); -////test_op_transform!(proptest_op_expand, Expand); -////test_op_transform!(proptest_op_padding, Pad); - -//proptest! { - //#[test] fn proptest_op_bsp ( - //d in prop_oneof![ - //Just(North), Just(South), - //Just(East), Just(West), - //Just(Above), Just(Below) - //], - //a in "\\PC*", - //b in "\\PC*", - //x in u16::MIN..u16::MAX, - //y in u16::MIN..u16::MAX, - //w in u16::MIN..u16::MAX, - //h in u16::MIN..u16::MAX, - //) { - //let bsp = Bsp(d, a, b); - ////assert_eq!( - ////Content::layout(&bsp, [x, y, w, h]), - ////Draw::layout(&bsp, [x, y, w, h]), - ////); - //} -//} - -////#[test] fn test_tui_engine () -> Usually<()> { - //////use std::sync::{Arc, RwLock}; - ////struct TestComponent(String); - ////impl Draw for TestComponent { - ////fn draw (&self, _to: &mut TuiOut) {} - ////} - ////impl Handle for TestComponent { - ////fn handle (&mut self, _from: &TuiIn) -> Perhaps { Ok(None) } - ////} - ////let engine = Tui::new(Box::<&mut Vec>::new(vec![0u8;0].as_mut()))?; - ////let state = engine.run(false, &Arc::new(RwLock::new(TestComponent("hello world".into()))))?; - ////state.read().unwrap().exited.store(true, std::sync::atomic::Ordering::Relaxed); - ////Ok(()) -////} - -////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) -> N { (self.x.load(Relaxed) as u16).into() } } -////impl H for Measure { fn h (&self) -> N { (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 , O: Screen> (&self, item: T) -> Bsp, T> { bsp_b(Fill::XY(self), item) } - ////pub fn new (x: N, y: N) -> Self { Self { x: Arc::new(x.atomic()), y: Arc::new(y.atomic()), __: PhantomData::default(), } } -////} -////impl From> for Measure { fn from (WH(x, y): WH) -> Self { Self::new(x, y) } } -////impl Clone for Measure { - ////fn clone (&self) -> Self { Self { __: Default::default(), x: self.x.clone(), y: self.y.clone(), } } -////} -////impl> Draw for Measure { - ////fn draw (&self, to: &mut O) { - ////// TODO: 🡘 🡙 ←🡙→ indicator to expand window when too small - ////self.x.store(to.area().w().into(), Relaxed); - ////self.y.store(to.area().h().into(), Relaxed); - ////} -////} - ////use Alignment::*; - ////let layout_x = match alignment { - ////NW | W | SW => to.x(), - ////N | C | 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!(), - ////}; - ////let layout_y = match alignment { - ////NW | N | NE => to.y(), - ////W | C | 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!(), - ////}; - ////draw.draw(to.clip(w, h)) - ////} - ////todo!() - ////impl> Layout for Align { - ////fn layout_x (&self, to: XYWH) -> O::Unit { - ////use Alignment::*; - ////match self.0 { - ////NW | W | SW => to.x(), - ////N | C | 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 | C | 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> 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> 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<'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; - ////} - ////} - ////} - ////fn split_cardinal ( - ////direction: Direction, area: XYWH, a: N - ////) -> (XYWH, XYWH) { - ////let XYWH(x, y, w, h) = area; - ////match direction { - ////North => (XYWH(x, y.plus(h).minus(a), w, a), XYWH(x, y, w, h.minus(a))), - ////South => (XYWH(x, y, w, a), XYWH(x, y.plus(a), w, h.minus(a))), - ////East => (XYWH(x, y, a, h), XYWH(x.plus(a), y, w.minus(a), h)), - ////West => (XYWH(x.plus(w).minus(a), y, a, h), XYWH(x, y, w.minus(a), h)), - ////Above | Below => (area, area) - ////} - ////} - ////fn bsp_areas , B: TwoD> ( - ////area: [N;4], - ////direction: Direction, - ////a: &A, - ////b: &B, - ////) -> [XYWH;3] { - ////let XYWH(x, y, w, h) = area; - ////let WH(aw, ah) = a.layout(area).wh(); - ////let WH(bw, bh) = b.layout(match direction { - ////South => XYWH(x, y + ah, w, h.minus(ah)), - ////North => XYWH(x, y, w, h.minus(ah)), - ////East => XYWH(x + aw, y, w.minus(aw), h), - ////West => XYWH(x, y, w.minus(aw), h), - ////Above => area, - ////Below => area, - ////}).wh(); - ////match direction { - ////Above | Below => { - ////let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah.max(bh)]); - ////let a = XYWH((x + w/2.into()).minus(aw/2.into()), (y + h/2.into()).minus(ah/2.into()), aw, ah); - ////let b = XYWH((x + w/2.into()).minus(bw/2.into()), (y + h/2.into()).minus(bh/2.into()), bw, bh); - ////[a.into(), b.into(), XYWH(x, y, w, h)] - ////}, - ////South => { - ////let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]); - ////let a = XYWH((x + w/2.into()).minus(aw/2.into()), y, aw, ah); - ////let b = XYWH((x + w/2.into()).minus(bw/2.into()), y + ah, bw, bh); - ////[a.into(), b.into(), XYWH(x, y, w, h)] - ////}, - ////North => { - ////let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]); - ////let a = XYWH((x + (w/2.into())).minus(aw/2.into()), y + bh, aw, ah); - ////let b = XYWH((x + (w/2.into())).minus(bw/2.into()), y, bw, bh); - ////[a.into(), b.into(), XYWH(x, y, w, h)] - ////}, - ////East => { - ////let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]); - ////let a = XYWH(x, (y + h/2.into()).minus(ah/2.into()), aw, ah); - ////let b = XYWH(x + aw, (y + h/2.into()).minus(bh/2.into()), bw, bh); - ////[a.into(), b.into(), XYWH(x, y, w, h)] - ////}, - ////West => { - ////let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]); - ////let a = XYWH(x + bw, (y + h/2.into()).minus(ah/2.into()), aw, ah); - ////let b = XYWH(x, (y + h/2.into()).minus(bh/2.into()), bw, bh); - ////[a.into(), b.into(), XYWH(x, y, w, h)] - ////}, - ////} - ////} - ////pub fn iter - - ////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, Tail: Draw> Draw for Bsp { - ////fn draw (&self, to: &mut O) { - ////let [a, b, _] = bsp_areas(to.area(), self.0, &self.1, &self.2); - //////panic!("{a:?} {b:?}"); - ////if self.0 == Below { - ////to.show(a, &self.1); - ////to.show(b, &self.2); - ////} else { - ////to.show(b, &self.2); - ////to.show(a, &self.1); - ////} - ////} - ////} diff --git a/src/color.rs b/src/color.rs deleted file mode 100644 index 3f0d48a..0000000 --- a/src/color.rs +++ /dev/null @@ -1,19 +0,0 @@ -pub(crate) use ::palette::Okhsl; - -pub trait HasColor { fn color (&self) -> ItemColor; } - -pub struct ItemColor {} - -pub struct ItemTheme {} - -#[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 } - } - } -} - -pub fn rgb (r: u8, g: u8, b: u8) -> ItemColor { - todo!(); -} diff --git a/src/draw.rs b/src/draw.rs index 323093e..8af6098 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -1,233 +1,8 @@ -use crate::{*, lang::*, color::*}; - -/// Drawable that supports dynamic dispatch. -/// -/// ``` -/// use tengri::draw::*; -/// struct TestScreen(bool); -/// struct TestWidget(bool); -/// impl Draw for TestWidget { -/// fn draw (&self, screen: &mut T) { -/// screen.0 |= self.0; -/// } -/// } -/// let mut screen = TestScreen(false); -/// TestWidget(false).draw(&mut screen); -/// TestWidget(true).draw(&mut screen); -/// TestWidget(false).draw(&mut screen); -/// ``` -pub trait Draw { - fn draw (&self, to: &mut T); -} -impl Draw for () { - fn draw (&self, __: &mut T) {} -} -impl Draw for F { - fn draw (&self, to: &mut T) { self(to) } -} -//impl> Draw for &D { - //fn draw (&self, to: &mut T) { (*self).draw(to) } -//} -impl> Draw for Arc { - fn draw (&self, to: &mut T) { (**self).draw(to) } -} -impl> Draw for RwLock { - fn draw (&self, to: &mut T) { self.read().unwrap().draw(to) } -} -impl> Draw for Option { - fn draw (&self, to: &mut T) { if let Some(draw) = self { draw.draw(to) } } -} - -/// Only render when condition is true. -/// -/// ``` -/// # fn test () -> impl tengri::Draw { -/// tengri::when(true, "Yes") -/// # } -/// ``` -pub fn when (condition: bool, draw: impl Draw) -> impl Draw { - move|to: &mut T|if condition { draw.draw(to); } -} - -/// Render one thing if a condition is true and another false. -/// -/// ``` -/// # fn test () -> impl tengri::Draw { -/// tengri::either(true, "Yes", "No") -/// # } -/// ``` -pub fn either (condition: bool, a: impl Draw, b: impl Draw) -> impl Draw { - move|to: &mut T|if condition { a.draw(to) } else { b.draw(to) } -} - -/// Output target. -/// -/// ``` -/// use tengri::*; -/// -/// struct TestOut(impl TwoD); -/// -/// impl Screen for TestOut { -/// type Unit = u16; -/// fn area (&self) -> impl TwoD { self.0 } -/// fn area_mut (&mut self) -> &mut impl TwoD { &mut self.0 } -/// fn show + ?Sized> (&mut self, area: impl TwoD, _: &T) { -/// println!("show: {area:?}"); -/// () -/// } -/// } -/// -/// impl Draw for String { -/// fn draw (&self, to: &mut TestOut) { -/// to.area_mut().set_w(self.len() as u16); -/// } -/// } -/// ``` -pub trait Screen: Area + Aligned + Send + Sync + Sized { - type Unit: Coord; - /// Render drawable in area specified by `area` - fn show <'t, T: Draw + ?Sized> (&mut self, area: impl Area, content: &'t T); - - fn clipped_mut (&mut self, w: Option, h: Option) - -> &mut Self; - fn padded_mut (&mut self, w: Self::Unit, h: Self::Unit) - -> &mut Self; -} -impl X for O { fn x (&self) -> O::Unit { self.x() } } -impl Y for O { fn y (&self) -> O::Unit { self.y() } } -impl W for O { fn w (&self) -> O::Unit { self.w() } } -impl H for O { fn h (&self) -> O::Unit { self.h() } } - -/// Something that has 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) - } - fn clip_wh (&self, w: Option, h: Option) -> [N;2] { - let w = w.map(|w|self.w().min(w)).unwrap_or(self.w()); - let h = h.map(|h|self.h().min(h)).unwrap_or(self.h()); - [w, h] - } -} - -/// Something that has length along horizontal axis. -pub trait W { - fn w (&self) -> N { N::zero() } - fn w_min (&self) -> N { self.w() } - fn w_max (&self) -> N { self.w() } -} -fn w_exact > (_w: N, _c: impl Draw) -> impl Draw { - move|_to: &mut T|{ todo!() } -} -fn w_fill > (_c: impl Draw) -> impl Draw { - move|_to: &mut T|{ todo!() } -} -fn w_min > (w: N, x: impl Draw) -> impl Draw { - min(Some(w), None, x) -} -fn w_max (w: N, x: impl Draw) -> impl Draw { - max(Some(w), None, x) -} - -/// Something that has length along vertical axis. -pub trait H { - fn h (&self) -> N { N::zero() } - fn h_min (&self) -> N { self.h() } - fn h_max (&self) -> N { self.h() } -} -fn h_exact > (_h: N, _c: impl Draw) -> impl Draw { - move|_to: &mut T|{ todo!() } -} -fn h_fill > (_c: impl Draw) -> impl Draw { - move|_to: &mut T|{ todo!() } -} -fn h_min > (h: N, x: impl Draw) -> impl Draw { - min(None, Some(h), x) -} -fn h_max (h: N, x: impl Draw) -> impl Draw { - max(None, Some(h), x) -} - -/// Something that has a [Measure] of its rendered size. -pub trait Measured {} - -/// Point along (X, Y). -pub trait Point: X + Y { - fn xy (&self) -> XY { XY(self.x(), self.y()) } -} - -/// Something that has `[0, 0]` at a particular point. -pub trait Anchored { fn anchor (&self) -> Align; } - -/// Something that has a bounding box -/// -/// ``` -/// use tengri::{Bounded, XYWH}; -/// let bounded: Bounded = Bounded(0, 0 ,0 ,0 ,""); -/// ``` -pub trait Bounding : Point + Area + Anchored { - /// Iterate over every covered X coordinate. - fn iter_x (&self) -> impl Iterator where N: std::iter::Step { - self.x_left()..self.x_right() - } - /// Iterate over every covered Y coordinate. - fn iter_y (&self) -> impl Iterator where N: std::iter::Step { - self.y_top()..self.y_bottom() - } - fn x_left (&self) -> N { - use Align::*; - let w = self.w(); - let a = self.anchor(); - 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_right (&self) -> N { - use Align::*; - let w = self.w(); - let a = self.anchor(); - 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 y_top (&self) -> N { - use Align::*; - let a = self.anchor(); - 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_bottom (&self) -> N { - use Align::*; - let a = self.anchor(); - 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) - } -} - -/// 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 -} +use crate::*; /// A numeric type that can be used as coordinate. /// -/// FIXME: Replace with `num` crate? -/// FIXME: Use AsRef/AsMut? +/// FIXME: Replace this ad-hoc trait with `num` crate. pub trait Coord: Send + Sync + Copy + Add + Sub @@ -241,45 +16,52 @@ 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()) } } /// Point along horizontal axis. -pub trait X { - fn x (&self) -> N { N::zero() } - fn push (a: impl Draw) -> impl Draw { a } - fn pull (a: impl Draw) -> impl Draw { a } -} +pub trait X { fn x (&self) -> N { N::zero() } } /// Point along vertical axis. -pub trait Y { - fn y (&self) -> N { N::zero() } - fn push (a: impl Draw) -> impl Draw { a } - fn pull (a: impl Draw) -> impl Draw { a } +pub trait Y { fn y (&self) -> N { N::zero() } } + +/// Length along horizontal axis. +pub trait W { fn w (&self) -> N { N::zero() } } + +/// Length along vertical axis. +pub trait H { fn h (&self) -> N { N::zero() } } + +/// 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()) } } + +/// 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> Draw for Bounded { - //fn draw (&self, to: &mut O) { - //let area = to.area(); - //*to.area_mut() = self.0; - //self.1.draw(to); - //*to.area_mut() = area; - //} -//} - /// A point (X, Y). /// /// ``` /// let xy = tengri::XY(0u16, 0); /// ``` #[cfg_attr(test, derive(Arbitrary))] -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub struct XY(pub C, pub C); -impl X for XY { fn x (&self) -> N { self.0 } } -impl Y for XY { fn y (&self) -> N { self.1 } } +#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XY(pub C, pub C); /// A size (Width, Height). /// @@ -287,21 +69,7 @@ impl Y for XY { fn y (&self) -> N { self.1 } } /// let wh = tengri::WH(0u16, 0); /// ``` #[cfg_attr(test, derive(Arbitrary))] -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub struct WH(pub C, pub C); -impl W for WH { fn w (&self) -> N { self.0 } } -impl H for WH { fn h (&self) -> N { self.1 } } -impl WH { - fn min > (&self, x: impl Draw) -> impl Draw { - min(Some(self.w()), Some(self.h()), x) - } - fn exact > (&self, c: impl Draw) -> impl Draw { - move|_to: &mut T|{ todo!() } - } - fn fill > (c: impl Draw) -> impl Draw { - move|_to: &mut T|{ todo!() } - } -} +#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct WH(pub C, pub C); /// Point with size. /// @@ -316,144 +84,139 @@ impl WH { #[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XYWH( pub C, pub C, pub C, pub C ); -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.0 } } -impl H for XYWH { fn h (&self) -> N { self.1 } } -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()) - } - pub fn center (&self) -> XY { - 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) - } -} - - -/// Set maximum width and/or height of drawing area. -pub fn clip , N: Coord> (w: Option, h: Option, draw: impl Draw) -> impl Draw { - move|to: &mut T|draw.draw(to.clipped_mut(w, h)) -} - -/// Shrink drawing area symmetrically. -pub fn pad , N: Coord> (w: N, h: N, draw: impl Draw) -> impl Draw { - move|to: &mut T|draw.draw(to.padded_mut(w, h)) -} -pub fn pad_x , N: Coord> (w: N, draw: impl Draw) -> impl Draw { - pad(w, N::zero(), draw) -} -pub fn pad_y , N: Coord> (h: N, draw: impl Draw) -> impl Draw { - pad(N::zero(), h, draw) -} -pub fn pad_xy , N: Coord> (p: N, draw: impl Draw) -> impl Draw { - pad(p, p, draw) +/// 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::Align::C; -/// use ::tengri::*; -/// let area = XYWH(10u16, 10, 20, 20); -/// fn test (area: XYWH, item: &impl Draw, expected: [u16;4]) { -/// //assert_eq!(Lay::layout(item, area), expected); -/// //assert_eq!(Draw::layout(item, area), expected); -/// }; -/// -/// let four = exact_xy(4, 4, "foo"); -/// test(area, &Align::nw(four()), [10, 10, 4, 4]); -/// test(area, &Align::n(four()), [18, 10, 4, 4]); -/// test(area, &Align::ne(four()), [26, 10, 4, 4]); -/// test(area, &Align::e(four()), [26, 18, 4, 4]); -/// test(area, &Align::se(four()), [26, 26, 4, 4]); -/// test(area, &Align::s(four()), [18, 26, 4, 4]); -/// test(area, &Align::sw(four()), [10, 26, 4, 4]); -/// test(area, &Align::w(four()), [10, 18, 4, 4]); -/// -/// let two_by_four = exact_xy(4, 2, "foo"); -/// test(area, &Align::nw(two_by_four()), [10, 10, 4, 2]); -/// test(area, &Align::n(two_by_four()), [18, 10, 4, 2]); -/// test(area, &Align::ne(two_by_four()), [26, 10, 4, 2]); -/// test(area, &Align::e(two_by_four()), [26, 19, 4, 2]); -/// test(area, &Align::se(two_by_four()), [26, 28, 4, 2]); -/// test(area, &Align::s(two_by_four()), [18, 28, 4, 2]); -/// test(area, &Align::sw(two_by_four()), [10, 28, 4, 2]); -/// test(area, &Align::w(two_by_four()), [10, 19, 4, 2]); -/// pub struct Align(pub Align, pub T); +/// let alignment = tengri::Alignment::Center; /// ``` #[cfg_attr(test, derive(Arbitrary))] -#[derive(Debug, Copy, Clone, Default)] pub enum Align { - #[default] C, X, Y, NW, N, NE, E, SE, S, SW, W +#[derive(Debug, Copy, Clone, Default)] pub enum Alignment { + #[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W } -pub trait Aligned { - fn aligned (&mut self, align: Align) -> &mut Self; + +/// Drawable with dynamic dispatch. +pub trait Draw: Fn(&mut T)->Usually<()> { fn draw (&self, to: &mut T) -> Usually<()>; } + +/// Drawable thunk. +impl()> Draw for F { fn draw (&self, to: &mut T) -> Usually<()> { self(to) } } + +impl Draw for () { fn draw (&self, _: &mut S) -> Usually<()> { Ok(()) } } +impl Draw for Box> { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } } + +impl> Draw for &D { fn draw (&self, to: &mut S) -> Usually<()> { (*self).draw(to) } } +impl> Draw for &mut D { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } } +impl> Draw for Arc { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } } +impl> Draw for RwLock { fn draw (&self, to: &mut S) -> Usually<()> { self.read().unwrap().draw(to) } } +impl> Draw for Option { fn draw (&self, to: &mut S) { if let Some(draw) = self { draw.draw(to) } } } + +/// Draw the content or its error message. +/// +/// ``` +/// let _ = tengri::Catcher::::new(Ok(Some("hello"))); +/// let _ = tengri::Catcher::::new(Ok(None)); +/// let _ = tengri::Catcher::::new(Err("draw fail".into())); +/// ``` +pub fn catcher (error: Perhaps, draw: impl Draw) -> impl Draw { + move|to|match self.0.as_ref() { + Ok(Some(content)) => draw(to), + Ok(None) => to.blit(&"", 0, 0, Some(Style::default().yellow())), + Err(e) => { + let err_fg = Color::Rgb(255,224,244); + let err_bg = Color::Rgb(96,24,24); + let title = Bsp::e(Tui::bold(true, "oopsie daisy. "), "rendering failed."); + let error = Bsp::e("\"why?\" ", Tui::bold(true, format!("{e}"))); + to.place(&Tui::fg_bg(err_fg, err_bg, Bsp::s(title, error))) + } + } } -/// Set 0, 0 of drawing subarea. -pub fn align (alignment: Align, draw: impl Draw) -> impl Draw { - move|to: &mut T|draw.draw(to.aligned(alignment)) +/// Only render when condition is true. +/// +/// ``` +/// fn test () -> impl tengri::Draw { +/// tengri::when(true, "Yes") +/// } +/// ``` +pub fn when (condition: bool, draw: impl Draw) -> impl Draw { + move|to|Ok(if condition { draw(to)? } }) } -impl Align { - pub fn n (x: impl Draw) -> impl Draw { align(Align::N, x) } - pub fn s (x: impl Draw) -> impl Draw { align(Align::S, x) } - pub fn e (x: impl Draw) -> impl Draw { align(Align::E, x) } - pub fn w (x: impl Draw) -> impl Draw { align(Align::W, x) } - pub fn nw (x: impl Draw) -> impl Draw { align(Align::NW, x) } - pub fn ne (x: impl Draw) -> impl Draw { align(Align::NE, x) } - pub fn sw (x: impl Draw) -> impl Draw { align(Align::SW, x) } - pub fn se (x: impl Draw) -> impl Draw { align(Align::SE, x) } - pub fn c (x: impl Draw) -> impl Draw { align(Align::C, x) } + +/// Render one thing if a condition is true and another false. +/// +/// ``` +/// fn test () -> impl tengri::Draw { +/// tengri::either(true, "Yes", "No") +/// } +/// ``` +pub fn either (condition: bool, a: impl Draw, b: impl Draw) -> impl Draw { + move|to|Ok(if condition { a(to)? } else { b(to)? }) +} + +/// Set maximum width and/or height of drawing area. +pub fn clip (w: Option, h: Option, draw: impl Draw) -> impl Draw { + move|to|draw(to.clip(w, h)) +} + +/// Shrink drawing area symmetrically. +pub fn pad (w: Option, h: Option, draw: impl Draw) -> impl Draw { + move|to|draw(to.pad(w, h)) } /// Only draw content if area is above a certain size. /// /// ``` -/// let minimum = tengri::min_wh(3, 5, "Hello"); // 5x5 +/// let minimum = tengri::Min::XY(3, 5, "Hello"); // 5x5 /// ``` -pub fn min (w: Option, h: Option, draw: impl Draw) -> impl Draw { - move|to: &mut T| { todo!() }//draw.draw(to.min(w, h)) +pub fn min (w: N, draw: impl Draw) -> impl Draw { + move|to|draw(to.min(w, h)) } /// Set the maximum width and/or height of the content. /// /// ``` -/// let maximum = tengri::max_wh(3, 5, "Hello"); // 3x1 +/// let maximum = tengri::Max::XY(3, 5, "Hello"); // 3x1 /// ``` -pub fn max (w: Option, h: Option, draw: impl Draw) -> impl Draw { - move|to: &mut T| { todo!() }// { todo!() }//draw.draw(to.max(w, h)) +pub fn max (x: N, draw: impl Draw) -> impl Draw { + move|to|draw(to.max(w, h)) } // pub fn displace ... -pub fn iter , F: Fn(U)->dyn Draw> ( - _items: impl Iterator, _cb: F +/// Shrink by amount on each axis. +/// +/// ``` +/// /// TODO +/// ``` +pub fn border (on: bool, style: S, draw: impl Draw) -> impl Draw { + fill_wh(above(when(on, |output|{/*TODO*/}), pad(Some(1), Some(1), draw))) +} + +/// Stackably padded. +/// +/// ``` +/// /// TODO +/// ``` +pub fn phat ( + w: N, h: N, [fg, bg, hi, lo]: [Color;4], draw: impl Draw ) -> impl Draw { - move|_to: &mut T|{ todo!() } + let top = exact_w(1, Self::lo(bg, hi)); + let low = exact_h(1, Self::hi(bg, lo)); + let draw = TuiOut::fg_bg(fg, bg, draw); + min_wh(w, h, bsp_s(top, bsp_n(low, fill_wh(draw)))) +} + +pub fn iter (items: impl Iterator>) -> impl Draw { + todo!() } /// Split screen between two items, or layer them atop each other. @@ -467,26 +230,129 @@ pub fn iter , F: Fn(U)->dyn Draw> ( /// let _ = tengri::draw::bsp(East, (), ()); /// let _ = tengri::draw::bsp(West, (), ()); /// ``` -pub fn bsp (_dir: Direction, a: impl Draw, b: impl Draw) -> impl Draw { - move|_to: &mut T|{ todo!() } +pub fn bsp (dir: Direction, a: impl Draw, b: impl Draw) -> impl Draw { + move|to|{ + //fn split_cardinal ( + //direction: Direction, area: XYWH, a: N + //) -> (XYWH, XYWH) { + //let XYWH(x, y, w, h) = area; + //match direction { + //North => (XYWH(x, y.plus(h).minus(a), w, a), XYWH(x, y, w, h.minus(a))), + //South => (XYWH(x, y, w, a), XYWH(x, y.plus(a), w, h.minus(a))), + //East => (XYWH(x, y, a, h), XYWH(x.plus(a), y, w.minus(a), h)), + //West => (XYWH(x.plus(w).minus(a), y, a, h), XYWH(x, y, w.minus(a), h)), + //Above | Below => (area, area) + //} + //} + //fn bsp_areas , B: TwoD> ( + //area: [N;4], + //direction: Direction, + //a: &A, + //b: &B, + //) -> [XYWH;3] { + //let XYWH(x, y, w, h) = area; + //let WH(aw, ah) = a.layout(area).wh(); + //let WH(bw, bh) = b.layout(match direction { + //South => XYWH(x, y + ah, w, h.minus(ah)), + //North => XYWH(x, y, w, h.minus(ah)), + //East => XYWH(x + aw, y, w.minus(aw), h), + //West => XYWH(x, y, w.minus(aw), h), + //Above => area, + //Below => area, + //}).wh(); + //match direction { + //Above | Below => { + //let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah.max(bh)]); + //let a = XYWH((x + w/2.into()).minus(aw/2.into()), (y + h/2.into()).minus(ah/2.into()), aw, ah); + //let b = XYWH((x + w/2.into()).minus(bw/2.into()), (y + h/2.into()).minus(bh/2.into()), bw, bh); + //[a.into(), b.into(), XYWH(x, y, w, h)] + //}, + //South => { + //let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]); + //let a = XYWH((x + w/2.into()).minus(aw/2.into()), y, aw, ah); + //let b = XYWH((x + w/2.into()).minus(bw/2.into()), y + ah, bw, bh); + //[a.into(), b.into(), XYWH(x, y, w, h)] + //}, + //North => { + //let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]); + //let a = XYWH((x + (w/2.into())).minus(aw/2.into()), y + bh, aw, ah); + //let b = XYWH((x + (w/2.into())).minus(bw/2.into()), y, bw, bh); + //[a.into(), b.into(), XYWH(x, y, w, h)] + //}, + //East => { + //let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]); + //let a = XYWH(x, (y + h/2.into()).minus(ah/2.into()), aw, ah); + //let b = XYWH(x + aw, (y + h/2.into()).minus(bh/2.into()), bw, bh); + //[a.into(), b.into(), XYWH(x, y, w, h)] + //}, + //West => { + //let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]); + //let a = XYWH(x + bw, (y + h/2.into()).minus(ah/2.into()), aw, ah); + //let b = XYWH(x, (y + h/2.into()).minus(bh/2.into()), bw, bh); + //[a.into(), b.into(), XYWH(x, y, w, h)] + //}, + //} + //} + //pub fn iter + } } -pub fn bsp_n (a: impl Draw, b: impl Draw) -> impl Draw { - bsp(Direction::North, a, b) + +/// 3-column layout with center priority. +/// +/// ``` +/// /// TODO +/// ``` +pub fn tryptich ( + top: boolean, [(w_a, ref a), (w_b, ref b), (w_c, ref c)]: [(N, impl Draw);3] +) -> impl Draw { + let Self { top, h, left: (w_a, ref a), middle: (w_b, ref b), right: (w_c, ref c) } = *self; + let a = exact_w(w_a, a); + let b = exact_w(w_b, align_w(b)); + let c = exact_w(w_c, c); + exact_h(h, if top { + bsp_above(fill_w(align_n(b)), bsp_above(fill_w(align_nw(a)), fill_w(align_ne(c)))) + } else { + bsp_above(fill_wh(align_c(b)), bsp_above(fill_wh(align_w(a)), fill_wh(align_e(c)))) + }) } -pub fn bsp_s (a: impl Draw, b: impl Draw) -> impl Draw { - bsp(Direction::South, a, b) + +impl> Draw for Measure { + fn draw (&self, to: &mut O) { + // TODO: 🡘 🡙 ←🡙→ indicator to expand window when too small + self.x.store(to.area().w().into(), Relaxed); + self.y.store(to.area().h().into(), Relaxed); + } } -pub fn bsp_e (a: impl Draw, b: impl Draw) -> impl Draw { - bsp(Direction::East, a, b) + +impl> Draw for Bounded { + fn draw (&self, to: &mut O) { + let area = to.area(); + *to.area_mut() = self.0; + self.1.draw(to); + *to.area_mut() = area; + } } -pub fn bsp_w (a: impl Draw, b: impl Draw) -> impl Draw { - bsp(Direction::West, a, b) + +impl> Draw for Align { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), &self.1).draw(to) } } -pub fn bsp_a (a: impl Draw, b: impl Draw) -> impl Draw { - bsp(Direction::Above, a, b) + +impl> Draw for Pad { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } } -pub fn bsp_b (a: impl Draw, b: impl Draw) -> impl Draw { - bsp(Direction::Below, a, b) + +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); + //panic!("{a:?} {b:?}"); + if self.0 == Below { + to.show(a, &self.1); + to.show(b, &self.2); + } else { + to.show(b, &self.2); + to.show(a, &self.1); + } + } } /// Clear a pre-allocated buffer, then write into it. @@ -522,44 +388,20 @@ pub fn bsp_b (a: impl Draw, b: impl Draw) -> impl Draw { /// Stack things on top of each other, #[macro_export] macro_rules! lay (($($expr:expr),* $(,)?) => {{ - let bsp = (); $(let bsp = bsp_b(bsp, $expr);)* bsp + let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)* bsp }}); /// Stack southward. #[macro_export] macro_rules! col (($($expr:expr),* $(,)?) => {{ - let bsp = (); $(let bsp = bsp_s(bsp, $expr);)* bsp + let bsp = (); $(let bsp = Bsp::s(bsp, $expr);)* bsp }}); /// Stack northward. #[macro_export] macro_rules! col_up (($($expr:expr),* $(,)?) => {{ - let bsp = (); $(let bsp = bsp_n(bsp, $expr);)* bsp + let bsp = (); $(let bsp = Bsp::n(bsp, $expr);)* bsp }}); /// Stack eastward. #[macro_export] macro_rules! row (($($expr:expr),* $(,)?) => {{ - let bsp = (); $(let bsp = bsp_e(bsp, $expr);)* bsp + let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)* bsp }}); - -/// Memoize a rendering. -/// -/// ``` -/// let _ = tengri::Memo::new((), ()); -/// ``` -#[derive(Debug, Default)] pub struct Memo { - pub value: T, - pub view: Arc> -} - -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 - } -} diff --git a/src/eval.rs b/src/eval.rs deleted file mode 100644 index d47aae6..0000000 --- a/src/eval.rs +++ /dev/null @@ -1,220 +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( - state.namespace(arg0?)?.unwrap(), - move|output: &mut O|state.understand(output, &arg1) - )), - - Some("either") => output.place(&either( - state.namespace(arg0?)?.unwrap(), - move|output: &mut O|state.understand(output, &arg1), - move|output: &mut O|state.understand(output, &arg2), - )), - - Some("bsp") => output.place(&{ - let a = move|output: &mut O|state.understand(output, &arg0); - let b = move|output: &mut O|state.understand(output, &arg1); - bsp(match frags.next() { - Some("n") => Alignment::N, - Some("s") => Alignment::S, - Some("e") => Alignment::E, - Some("w") => Alignment::W, - Some("a") => Alignment::A, - Some("b") => Alignment::B, - frag => unimplemented!("bsp/{frag:?}") - }, a, b) - }), - - Some("align") => output.place(&{ - let a = move|output: &mut O|state.understand(output, &arg0).unwrap(); - align(match frags.next() { - Some("c") => Alignment::Center, - Some("n") => Alignment::N, - Some("s") => Alignment::S, - Some("e") => Alignment::E, - Some("w") => Alignment::W, - Some("x") => Alignment::X, - Some("y") => Alignment::Y, - frag => unimplemented!("align/{frag:?}") - }, a) - }), - - Some("fill") => output.place(&{ - let a = move|output: &mut O|state.understand(output, &arg0).unwrap(); - match frags.next() { - Some("xy") | None => fill_wh(a), - Some("x") => fill_w(a), - Some("y") => fill_h(a), - frag => unimplemented!("fill/{frag:?}") - } - }), - - Some("exact") => output.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("exact: unsupported axis {axis:?}") }; - let cb = move|output: &mut O|state.understand(output, &arg).unwrap(); - match axis { - Some("xy") | None => exact_wh(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => exact_w(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => exact_h(state.namespace(arg0?)?.unwrap(), cb), - frag => unimplemented!("exact/{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!("min: unsupported axis {axis:?}") }; - let cb = move|output: &mut O|state.understand(output, &arg).unwrap(); - match axis { - Some("xy") | None => min_wh(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => min_w(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => min_h(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!("max: unsupported axis {axis:?}") }; - let cb = move|output: &mut O|state.understand(output, &arg).unwrap(); - match axis { - Some("xy") | None => max_wh(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => max_w(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => max_h(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!("push: unsupported axis {axis:?}") }; - let cb = move|output: &mut O|state.understand(output, &arg); - 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) -} - -/// Interpret TUI-specific layout operation. -/// -/// ``` -/// use tengri::{Namespace, Understand, Tui, ratatui::prelude::Color}; -/// -/// struct State; -/// impl<'b> Namespace<'b, bool> for State {} -/// impl<'b> Namespace<'b, u16> for State {} -/// impl<'b> Namespace<'b, Color> for State {} -/// impl Understand for State {} -/// # fn main () -> tengri::Usually<()> { -/// let state = State; -/// let mut out = TuiOut::default(); -/// tengri::eval_view_tui(&state, &mut out, "")?; -/// tengri::eval_view_tui(&state, &mut out, "text Hello world!")?; -/// tengri::eval_view_tui(&state, &mut out, "fg (g 0) (text Hello world!)")?; -/// tengri::eval_view_tui(&state, &mut out, "bg (g 2) (text Hello world!)")?; -/// tengri::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, output: &mut Buffer, expr: impl Expression + 'a -) -> Usually where - S: Understand - + for<'b>Namespace<'b, bool> - + for<'b>Namespace<'b, u16> - + for<'b>Namespace<'b, Color> -{ - // 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(); - let tail1 = tail0.tail(); - let arg2 = tail1.head(); - match frags.next() { - - Some("text") => { - if let Some(src) = args?.src()? { output.place(&src) } - }, - - Some("fg") => { - let arg0 = arg0?.expect("fg: expected arg 0 (color)"); - let color = Namespace::namespace(state, arg0)?.unwrap_or_else(||panic!("fg: {arg0:?}: not a color")); - let thunk = Thunk::new(move|output: &mut Buffer|state.understand(output, &arg1).unwrap()); - output.place(&TuiOut::fg(color, thunk)) - }, - - Some("bg") => { - //panic!("expr: {expr:?}\nhead: {head:?}\nfrags: {frags:?}\nargs: {args:?}\narg0: {arg0:?}\ntail0: {tail0:?}\narg1: {arg1:?}\ntail1: {tail1:?}\narg2: {arg2:?}"); - //panic!("head: {head:?}\narg0: {arg0:?}\narg1: {arg1:?}\narg2: {arg2:?}");; - //panic!("head: {head:?}\narg0: {arg0:?}\narg1: {arg1:?}\narg2: {arg2:?}"); - let arg0 = arg0?.expect("bg: expected arg 0 (color)"); - let color = Namespace::namespace(state, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color")); - let thunk = Thunk::new(move|output: &mut Buffer|state.understand(output, &arg1).unwrap()); - output.place(&TuiOut::bg(color, thunk)) - }, - - _ => return Ok(false) - - }; - Ok(true) -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index ecde0e6..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,71 +0,0 @@ -#![feature(anonymous_lifetime_in_impl_trait)] -#![feature(associated_type_defaults)] -#![feature(const_default)] -#![feature(const_option_ops)] -#![feature(const_precise_live_drops)] -#![feature(const_trait_impl)] -#![feature(impl_trait_in_assoc_type)] -#![feature(step_trait)] -#![feature(trait_alias)] -#![feature(type_alias_impl_trait)] -#![feature(type_changing_struct_update)] -pub extern crate atomic_float; -pub extern crate palette; -pub extern crate better_panic; -pub extern crate unicode_width; -#[cfg(test)] #[macro_use] pub extern crate proptest; - -pub(crate) use ::{ - atomic_float::AtomicF64, - std::fmt::{Debug, Display}, - std::ops::{Add, Sub, Mul, Div}, - std::sync::{Arc, RwLock}, - std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::*}, -}; - -#[cfg(feature = "lang")] pub extern crate dizzle as lang; -#[cfg(feature = "lang")] pub use ::dizzle::{self, Usually, Perhaps, impl_default}; - -#[cfg(feature = "time")] pub mod time; -#[cfg(feature = "play")] pub mod play; -#[cfg(feature = "sing")] pub extern crate jack; -#[cfg(feature = "sing")] pub mod sing; -#[cfg(feature = "sing")] pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; - -#[cfg(feature = "draw")] pub mod draw; -#[cfg(feature = "draw")] pub mod color; -#[cfg(feature = "text")] pub mod text; -#[cfg(feature = "tui")] pub mod tui; -#[cfg(feature = "tui")] pub extern crate ratatui; -#[cfg(feature = "tui")] pub extern crate crossterm; - -/// Define a trait an implement it for various mutation-enabled wrapper types. */ -#[macro_export] macro_rules! flex_trait_mut ( - ($Trait:ident $(<$($A:ident:$T:ident),+>)? { - $(fn $fn:ident (&mut $self:ident $(, $arg:ident:$ty:ty)*) -> $ret:ty $body:block)* - })=>{ - pub trait $Trait $(<$($A: $T),+>)? { - $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret $body)* - } - impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for &mut _T_ { - $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { (*$self).$fn($($arg),*) })* - } - impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for Option<_T_> { - $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { - if let Some(this) = $self { this.$fn($($arg),*) } else { Ok(None) } - })* - } - impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Mutex<_T_> { - $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.get_mut().unwrap().$fn($($arg),*) })* - } - impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::Mutex<_T_>> { - $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.lock().unwrap().$fn($($arg),*) })* - } - impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::RwLock<_T_> { - $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })* - } - impl<$($($A: $T,)+)? _T_: $Trait $(<$($A),+>)?> $Trait $(<$($A),+>)? for ::std::sync::Arc<::std::sync::RwLock<_T_>> { - $(fn $fn (&mut $self $(,$arg:$ty)*) -> $ret { $self.write().unwrap().$fn($($arg),*) })* - } - }; -); diff --git a/src/play.rs b/src/play.rs index b608106..686ab86 100644 --- a/src/play.rs +++ b/src/play.rs @@ -1,65 +1,4 @@ -use crate::{*, time::*, lang::*}; -use ::std::{thread::JoinHandle, time::Duration}; -#[cfg(feature = "tui")] use ::crossterm::event::poll; - -#[derive(Clone)] pub struct Exit(Arc); - -impl Exit { - pub fn run (run: impl Fn(Self)->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 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, call: F - ) -> Result - where F: Fn(&PerfModel)->() + Send + Sync + 'static - { - Self::new(exit, move |perf| { let _ = call(perf); std::thread::sleep(time); }) - } - - /// Spawn a thread that uses [crossterm::event::poll] - /// to run `call` every `time` msec. - #[cfg(feature = "tui")]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() - } -} +use crate::*; /// Define an enum containing commands, and implement [Command] trait for over given `State`. #[macro_export] macro_rules! def_command ( diff --git a/src/sing.rs b/src/sing.rs index 9775585..08c0bcb 100644 --- a/src/sing.rs +++ b/src/sing.rs @@ -1,27 +1,7 @@ -use crate::{*, time::PerfModel}; +use crate::*; use ::jack::*; use JackState::*; -/// Trait for thing that has a JACK process callback. -pub trait Audio { - /// Handle a JACK event. - fn handle (&mut self, _event: JackEvent) {} - /// Projecss a JACK chunk. - fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { - Control::Continue - } - /// The JACK process callback function passed to the server. - fn callback ( - state: &Arc>, client: &Client, scope: &ProcessScope - ) -> Control where Self: Sized { - if let Ok(mut state) = state.write() { - state.process(client, scope) - } else { - Control::Quit - } - } -} - /// Wraps [JackState], and through it [jack::Client] when connected. /// /// ``` @@ -219,87 +199,3 @@ impl JackPerfModel for PerfModel { } } } - -/// Things that can provide a [jack::Client] reference. -/// -/// ``` -/// use tengri::{Jack, HasJack}; -/// -/// let jack: &Jack = Jacked::default().jack(); -/// -/// #[derive(Default)] struct Jacked<'j>(Jack<'j>); -/// -/// impl<'j> HasJack<'j> for Jacked<'j> { -/// fn jack (&self) -> &Jack<'j> { &self.0 } -/// } -/// ``` -pub trait HasJack<'j>: Send + Sync { - /// Return the internal [jack::Client] handle - /// that lets you call the JACK API. - fn jack (&self) -> &Jack<'j>; - fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { - self.jack().with_client(op) - } - fn port_by_name (&self, name: &str) -> Option> { - self.with_client(|client|client.port_by_name(name)) - } - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - fn register_port (&self, name: impl AsRef) -> Usually> { - self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?)) - } - fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->jack::contrib::Position) - -> Usually<()> - { - if enable { - self.with_client(|client|match client.register_timebase_callback(false, callback) { - Ok(_) => Ok(()), - Err(e) => Err(e) - })? - } - Ok(()) - } - fn sync_follow (&self, _enable: bool) -> Usually<()> { - // TODO: sync follow - Ok(()) - } -} - -pub trait JackPerfModel { - fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope); -} - -/// Implement [Audio]: provide JACK callbacks. -#[macro_export] macro_rules! impl_audio { - - (| - $self1:ident: - $Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident - |$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => { - impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { - #[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb } - $(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })? - } - }; - - ($Struct:ident: $process:ident, $handle:ident) => { - impl Audio for $Struct { - #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { - $process(self, c, s) - } - #[inline] fn handle (&mut self, e: JackEvent) { - $handle(self, e) - } - } - }; - - ($Struct:ident: $process:ident) => { - impl Audio for $Struct { - #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { - $process(self, c, s) - } - } - }; - -} diff --git a/src/tengri.rs b/src/tengri.rs new file mode 100644 index 0000000..5e9e25e --- /dev/null +++ b/src/tengri.rs @@ -0,0 +1,64 @@ +#![feature(anonymous_lifetime_in_impl_trait)] +#![feature(associated_type_defaults)] +#![feature(const_default)] +#![feature(const_option_ops)] +#![feature(const_precise_live_drops)] +#![feature(const_trait_impl)] +#![feature(impl_trait_in_assoc_type)] +#![feature(step_trait)] +#![feature(trait_alias)] +#![feature(type_alias_impl_trait)] +#![feature(type_changing_struct_update)] +pub extern crate atomic_float; +pub extern crate palette; +pub extern crate better_panic; +pub extern crate unicode_width; +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; + +pub(crate) use ::{ + atomic_float::AtomicF64, + palette::{*, convert::*, okhsl::*}, + better_panic::{Settings, Verbosity}, + unicode_width::*, + std::{ + io::{stdout, Stdout, Write}, + sync::{Arc, Weak, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}}, + fmt::{Debug, Display}, + ops::{Add, Sub, Mul, Div}, + marker::PhantomData, + time::Duration, + thread::{spawn, JoinHandle} + } +}; + +#[cfg(feature = "lang")] extern crate dizzle; +#[cfg(feature = "lang")] pub use ::dizzle::self as lang; + +#[cfg(feature = "draw")] pub mod draw; + +#[cfg(feature = "tui")] pub extern crate ratatui; +#[cfg(feature = "tui")] pub extern crate crossterm; +#[cfg(feature = "tui")] pub(crate) use ::{ + ratatui::{ + prelude::{Color, Style, Buffer, Position}, + style::{Stylize, Modifier, Color::*}, + backend::{Backend, CrosstermBackend, ClearType}, + layout::{Size, Rect}, + buffer::Cell + }, + crossterm::{ + ExecutableCommand, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, + event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, + } +}; + +#[cfg(feature = "sing")] pub extern crate jack; +#[cfg(feature = "sing")] pub mod sing; +#[cfg(feature = "sing")] pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; diff --git a/src/tengri_fns.rs b/src/tengri_fns.rs new file mode 100644 index 0000000..7a4e6d4 --- /dev/null +++ b/src/tengri_fns.rs @@ -0,0 +1,182 @@ +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 new file mode 100644 index 0000000..476c657 --- /dev/null +++ b/src/tengri_impl.rs @@ -0,0 +1,1365 @@ +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