breaking: refactor: cooking
Some checks failed
/ build (push) Has been cancelled

This commit is contained in:
same mf who else 2026-03-08 22:13:12 +02:00
parent ad070a4cbb
commit 8f0a2accce
13 changed files with 1819 additions and 2157 deletions

407
src/draw.rs Normal file
View file

@ -0,0 +1,407 @@
use crate::*;
/// A numeric type that can be used as coordinate.
///
/// FIXME: Replace this ad-hoc trait with `num` crate.
pub trait Coord: Send + Sync + Copy
+ Add<Self, Output=Self>
+ Sub<Self, Output=Self>
+ Mul<Self, Output=Self>
+ Div<Self, Output=Self>
+ Ord + PartialEq + Eq
+ Debug + Display + Default
+ From<u16> + Into<u16>
+ Into<usize>
+ Into<f64>
{
fn zero () -> Self { 0.into() }
fn plus (self, other: Self) -> Self;
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<N: Coord> { fn x (&self) -> N { N::zero() } }
/// Point along vertical axis.
pub trait Y<N: Coord> { fn y (&self) -> N { N::zero() } }
/// Length along horizontal axis.
pub trait W<N: Coord> { fn w (&self) -> N { N::zero() } }
/// Length along vertical axis.
pub trait H<N: Coord> { 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<N: Coord>: W<N> + H<N> { fn wh (&self) -> WH<N> { WH(self.w(), self.h()) } }
/// Point along (X, Y).
pub trait Point<N: Coord>: X<N> + Y<N> { fn xy (&self) -> XY<N> { XY(self.x(), self.y()) } }
// Something that has a 2D bounding box (X, Y, W, H).
pub trait Bounded<N: Coord>: Point<N> + Area<N> + Anchor<N> {
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)
}
}
}
/// A point (X, Y).
///
/// ```
/// let xy = tengri::XY(0u16, 0);
/// ```
#[cfg_attr(test, derive(Arbitrary))]
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XY<C: Coord>(pub C, pub C);
/// A size (Width, Height).
///
/// ```
/// let wh = tengri::WH(0u16, 0);
/// ```
#[cfg_attr(test, derive(Arbitrary))]
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct WH<C: Coord>(pub C, pub C);
/// Point with size.
///
/// ```
/// let xywh = tengri::XYWH(0u16, 0, 0, 0);
/// assert_eq!(tengri::XYWH(10u16, 10, 20, 20).center(), tengri::XY(20, 20));
/// ```
///
/// * [ ] TODO: anchor field (determines at which corner/side is X0 Y0)
///
#[cfg_attr(test, derive(Arbitrary))]
#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XYWH<C: Coord>(
pub C, pub C, pub C, pub C
);
/// A cardinal direction.
///
/// ```
/// let direction = tengri::Direction::Above;
/// ```
#[cfg_attr(test, derive(Arbitrary))]
#[derive(Copy, Clone, PartialEq, Debug)] pub enum Direction {
North, South, East, West, Above, Below
}
/// 9th of area to place.
///
/// ```
/// let alignment = tengri::Alignment::Center;
/// ```
#[cfg_attr(test, derive(Arbitrary))]
#[derive(Debug, Copy, Clone, Default)] pub enum Alignment {
#[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W
}
/// Drawable with dynamic dispatch.
pub trait Draw<T>: Fn(&mut T)->Usually<()> { fn draw (&self, to: &mut T) -> Usually<()>; }
/// Drawable thunk.
impl<T, F: Fn(&mut T)->()> Draw<T> for F { fn draw (&self, to: &mut T) -> Usually<()> { self(to) } }
impl<T> Draw<T> for () { fn draw (&self, _: &mut S) -> Usually<()> { Ok(()) } }
impl<T> Draw<T> for Box<dyn Draw<T>> { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } }
impl<T, D: Draw<T>> Draw<T> for &D { fn draw (&self, to: &mut S) -> Usually<()> { (*self).draw(to) } }
impl<T, D: Draw<T>> Draw<T> for &mut D { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } }
impl<T, D: Draw<T>> Draw<T> for Arc<D> { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } }
impl<T, D: Draw<T>> Draw<T> for RwLock<D> { fn draw (&self, to: &mut S) -> Usually<()> { self.read().unwrap().draw(to) } }
impl<T, D: Draw<T>> Draw<T> for Option<D> { fn draw (&self, to: &mut S) { if let Some(draw) = self { draw.draw(to) } } }
/// Draw the content or its error message.
///
/// ```
/// let _ = tengri::Catcher::<tengri::Tui, &'static str>::new(Ok(Some("hello")));
/// let _ = tengri::Catcher::<tengri::Tui, &'static str>::new(Ok(None));
/// let _ = tengri::Catcher::<tengri::Tui, &'static str>::new(Err("draw fail".into()));
/// ```
pub fn catcher <T, E> (error: Perhaps<E>, draw: impl Draw<T>) -> impl Draw<T> {
move|to|match self.0.as_ref() {
Ok(Some(content)) => draw(to),
Ok(None) => to.blit(&"<empty>", 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)))
}
}
}
/// Only render when condition is true.
///
/// ```
/// fn test () -> impl tengri::Draw<tengri::Tui> {
/// tengri::when(true, "Yes")
/// }
/// ```
pub fn when <T> (condition: bool, draw: impl Draw<T>) -> impl Draw<T> {
move|to|Ok(if condition { draw(to)? } })
}
/// Render one thing if a condition is true and another false.
///
/// ```
/// fn test () -> impl tengri::Draw<tengri::Tui> {
/// tengri::either(true, "Yes", "No")
/// }
/// ```
pub fn either <T> (condition: bool, a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
move|to|Ok(if condition { a(to)? } else { b(to)? })
}
/// Set maximum width and/or height of drawing area.
pub fn clip <T, N: Coord> (w: Option<N>, h: Option<N>, draw: impl Draw<T>) -> impl Draw<T> {
move|to|draw(to.clip(w, h))
}
/// Shrink drawing area symmetrically.
pub fn pad <T, N: Coord> (w: Option<N>, h: Option<N>, draw: impl Draw<T>) -> impl Draw<T> {
move|to|draw(to.pad(w, h))
}
/// Only draw content if area is above a certain size.
///
/// ```
/// let minimum = tengri::Min::XY(3, 5, "Hello"); // 5x5
/// ```
pub fn min <T, N: Coord> (w: N, draw: impl Draw<T>) -> impl Draw<T> {
move|to|draw(to.min(w, h))
}
/// Set the maximum width and/or height of the content.
///
/// ```
/// let maximum = tengri::Max::XY(3, 5, "Hello"); // 3x1
/// ```
pub fn max <T, N: Coord> (x: N, draw: impl Draw<T>) -> impl Draw<T> {
move|to|draw(to.max(w, h))
}
// pub fn displace ...
/// Shrink by amount on each axis.
///
/// ```
/// /// TODO
/// ```
pub fn border <T, S: BorderStyle> (on: bool, style: S, draw: impl Draw<T>) -> impl Draw<T> {
fill_wh(above(when(on, |output|{/*TODO*/}), pad(Some(1), Some(1), draw)))
}
/// Stackably padded.
///
/// ```
/// /// TODO
/// ```
pub fn phat <T, N: Coord> (
w: N, h: N, [fg, bg, hi, lo]: [Color;4], draw: impl Draw<T>
) -> impl Draw<T> {
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 <T> (items: impl Iterator<Item = dyn Draw<T>>) -> impl Draw<T> {
todo!()
}
/// Split screen between two items, or layer them atop each other.
///
/// ```
/// use tengri::Direction::*;
/// let _ = tengri::draw::bsp(Above, (), ());
/// let _ = tengri::draw::bsp(Below, (), ());
/// let _ = tengri::draw::bsp(North, (), ());
/// let _ = tengri::draw::bsp(South, (), ());
/// let _ = tengri::draw::bsp(East, (), ());
/// let _ = tengri::draw::bsp(West, (), ());
/// ```
pub fn bsp <T> (dir: Direction, a: impl Draw<T>, b: impl Draw<T>) -> impl Draw<T> {
move|to|{
//fn split_cardinal <N: Coord> (
//direction: Direction, area: XYWH<N>, a: N
//) -> (XYWH<N>, XYWH<N>) {
//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 <N: Coord, A: TwoD<N>, B: TwoD<N>> (
//area: [N;4],
//direction: Direction,
//a: &A,
//b: &B,
//) -> [XYWH<N>;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
}
}
/// 3-column layout with center priority.
///
/// ```
/// /// TODO
/// ```
pub fn tryptich <T, N: Coord> (
top: boolean, [(w_a, ref a), (w_b, ref b), (w_c, ref c)]: [(N, impl Draw<T>);3]
) -> impl Draw<T> {
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))))
})
}
impl<N: Unit, O: Screen<N>> Draw<O> for Measure<N> {
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);
}
}
impl<O: Screen, T: Draw<O>> Draw<O> for Bounded<O, T> {
fn draw (&self, to: &mut O) {
let area = to.area();
*to.area_mut() = self.0;
self.1.draw(to);
*to.area_mut() = area;
}
}
impl<O: Screen, T: Draw<O>> Draw<O> for Align<T> {
fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), &self.1).draw(to) }
}
impl<O: Screen, T: Draw<O>> Draw<O> for Pad<O::Unit, T> {
fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) }
}
impl<O: Screen, Head: Draw<O>, Tail: Draw<O>> Draw<O> for Bsp<Head, Tail> {
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.
#[macro_export] macro_rules! rewrite {
($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } }
}
/// FIXME: This macro should be some variant of `eval`, too.
/// But taking into account the different signatures (resolving them into 1?)
#[cfg(feature = "lang")] #[macro_export] macro_rules! draw {
($State:ident: $Output:ident: $layers:expr) => {
impl Draw<$Output> for $State {
fn draw (&self, to: &mut $Output) {
for layer in $layers { layer(self, to) }
}
}
}
}
/// FIXME: This is generic: should be called `eval` and be part of [dizzle].
#[cfg(feature = "lang")] #[macro_export] macro_rules! view {
($State:ident: $Output:ident: $namespaces:expr) => {
impl Understand<$Output, ()> for $State {
fn understand_expr <'a> (&'a self, to: &mut $Output, expr: &'a impl Expression) -> Usually<()> {
for namespace in $namespaces { if namespace(self, to, expr)? { return Ok(()) } }
Err(format!("{}::<{}, ()>::understand_expr: unexpected: {expr:?}",
stringify! { $State },
stringify! { $Output }).into())
}
}
}
}
/// Stack things on top of each other,
#[macro_export] macro_rules! lay (($($expr:expr),* $(,)?) => {{
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
}});
/// Stack northward.
#[macro_export] macro_rules! col_up (($($expr:expr),* $(,)?) => {{
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
}});