mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2026-07-17 15:56:57 +02:00
This commit is contained in:
parent
ad070a4cbb
commit
8f0a2accce
13 changed files with 1819 additions and 2157 deletions
407
src/draw.rs
Normal file
407
src/draw.rs
Normal 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
|
||||
}});
|
||||
Loading…
Add table
Add a link
Reference in a new issue