refactor(output): group ops/ and space/

This commit is contained in:
🪞👃🪞 2025-04-13 21:19:25 +03:00
parent 18a01b8355
commit ec7621eff9
17 changed files with 29 additions and 13 deletions

107
output/src/ops/align.rs Normal file
View file

@ -0,0 +1,107 @@
//! Aligns things to the container. Comes with caveats.
//! ```
//! use ::tengri::{output::*, tui::*};
//! let area: [u16;4] = [10, 10, 20, 20];
//! fn test (area: [u16;4], item: &impl Content<TuiOut>, expected: [u16;4]) {
//! assert_eq!(Content::layout(item, area), expected);
//! assert_eq!(Render::layout(item, area), expected);
//! };
//!
//! let four = ||Fixed::xy(4, 4, "");
//! 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 = ||Fixed::xy(4, 2, "");
//! 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]);
//! ```
use crate::*;
#[derive(Debug, Copy, Clone, Default)] pub enum Alignment { #[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W }
pub struct Align<A>(Alignment, A);
#[cfg(feature = "dsl")]
try_from_expr!(<'a, E>: Align<RenderBox<'a, E>>: |state, iter|{
if let Some(Token { value: Value::Key(key), .. }) = iter.peek() {
match key {
"align/c"|"align/x"|"align/y"|
"align/n"|"align/s"|"align/e"|"align/w"|
"align/nw"|"align/sw"|"align/ne"|"align/se" => {
let _ = iter.next().unwrap();
let c = iter.next().expect("no content specified");
let c = state.get_content(&c.value).expect("no content provided");
return Some(match key {
"align/c" => Self::c(c),
"align/x" => Self::x(c),
"align/y" => Self::y(c),
"align/n" => Self::n(c),
"align/s" => Self::s(c),
"align/e" => Self::e(c),
"align/w" => Self::w(c),
"align/nw" => Self::nw(c),
"align/ne" => Self::ne(c),
"align/sw" => Self::sw(c),
"align/se" => Self::se(c),
_ => unreachable!()
})
},
_ => return None
}
}
});
impl<A> Align<A> {
#[inline] pub const fn c (a: A) -> Self { Self(Alignment::Center, a) }
#[inline] pub const fn x (a: A) -> Self { Self(Alignment::X, a) }
#[inline] pub const fn y (a: A) -> Self { Self(Alignment::Y, a) }
#[inline] pub const fn n (a: A) -> Self { Self(Alignment::N, a) }
#[inline] pub const fn s (a: A) -> Self { Self(Alignment::S, a) }
#[inline] pub const fn e (a: A) -> Self { Self(Alignment::E, a) }
#[inline] pub const fn w (a: A) -> Self { Self(Alignment::W, a) }
#[inline] pub const fn nw (a: A) -> Self { Self(Alignment::NW, a) }
#[inline] pub const fn sw (a: A) -> Self { Self(Alignment::SW, a) }
#[inline] pub const fn ne (a: A) -> Self { Self(Alignment::NE, a) }
#[inline] pub const fn se (a: A) -> Self { Self(Alignment::SE, a) }
}
impl<E: Output, A: Content<E>> Content<E> for Align<A> {
fn content (&self) -> impl Render<E> {
&self.1
}
fn layout (&self, on: E::Area) -> E::Area {
use Alignment::*;
let it = Render::layout(&self.content(), on).xywh();
let cx = on.x()+(on.w().minus(it.w())/2.into());
let cy = on.y()+(on.h().minus(it.h())/2.into());
let fx = (on.x()+on.w()).minus(it.w());
let fy = (on.y()+on.h()).minus(it.h());
let [x, y] = match self.0 {
Center => [cx, cy],
X => [cx, it.y()],
Y => [it.x(), cy],
NW => [on.x(), on.y()],
N => [cx, on.y()],
NE => [fx, on.y()],
W => [on.x(), cy],
E => [fx, cy],
SW => [on.x(), fy],
S => [cx, fy],
SE => [fx, fy],
}.into();
[x, y, it.w(), it.h()].into()
}
fn render (&self, to: &mut E) {
to.place(Content::layout(self, to.area()), &self.content())
}
}

144
output/src/ops/bsp.rs Normal file
View file

@ -0,0 +1,144 @@
use crate::*;
pub use Direction::*;
/// A split or layer.
pub struct Bsp<X, Y>(Direction, X, Y);
impl<E: Output, A: Content<E>, B: Content<E>> Content<E> for Bsp<A, B> {
fn layout (&self, outer: E::Area) -> E::Area {
let [_, _, c] = self.areas(outer);
c
}
fn render (&self, to: &mut E) {
let [area_a, area_b, _] = self.areas(to.area());
let (a, b) = self.contents();
match self.0 {
Below => { to.place(area_a, a); to.place(area_b, b); },
_ => { to.place(area_b, b); to.place(area_a, a); }
}
}
}
#[cfg(feature = "dsl")]
try_from_expr!(<'a, E>: Bsp<RenderBox<'a, E>, RenderBox<'a, E>>: |state, iter| {
if let Some(Token { value: Value::Key(key), .. }) = iter.peek() {
match key {
"bsp/n"|"bsp/s"|"bsp/e"|"bsp/w"|"bsp/a"|"bsp/b" => {
let _ = iter.next().unwrap();
let c1 = iter.next().expect("no content1 specified");
let c2 = iter.next().expect("no content2 specified");
let c1 = state.get_content(&c1.value).expect("no content1 provided");
let c2 = state.get_content(&c2.value).expect("no content2 provided");
return Some(match key {
"bsp/n" => Self::n(c1, c2),
"bsp/s" => Self::s(c1, c2),
"bsp/e" => Self::e(c1, c2),
"bsp/w" => Self::w(c1, c2),
"bsp/a" => Self::a(c1, c2),
"bsp/b" => Self::b(c1, c2),
_ => unreachable!(),
})
},
_ => return None
}
}
});
impl<A, B> Bsp<A, B> {
#[inline] pub const fn n (a: A, b: B) -> Self { Self(North, a, b) }
#[inline] pub const fn s (a: A, b: B) -> Self { Self(South, a, b) }
#[inline] pub const fn e (a: A, b: B) -> Self { Self(East, a, b) }
#[inline] pub const fn w (a: A, b: B) -> Self { Self(West, a, b) }
#[inline] pub const fn a (a: A, b: B) -> Self { Self(Above, a, b) }
#[inline] pub const fn b (a: A, b: B) -> Self { Self(Below, a, b) }
}
pub trait BspAreas<E: Output, A: Content<E>, B: Content<E>> {
fn direction (&self) -> Direction;
fn contents (&self) -> (&A, &B);
fn areas (&self, outer: E::Area) -> [E::Area;3] {
let direction = self.direction();
let [x, y, w, h] = outer.xywh();
let (a, b) = self.contents();
let [aw, ah] = a.layout(outer).wh();
let [bw, bh] = b.layout(match direction {
Above | Below => outer,
South => [x, y + ah, w, h.minus(ah)].into(),
North => [x, y, w, h.minus(ah)].into(),
East => [x + aw, y, w.minus(aw), h].into(),
West => [x, y, w.minus(aw), h].into(),
}).wh();
match direction {
Above | Below => {
let [x, y, w, h] = outer.center_xy([aw.max(bw), ah.max(bh)]);
let a = [(x + w/2.into()).minus(aw/2.into()), (y + h/2.into()).minus(ah/2.into()), aw, ah];
let b = [(x + w/2.into()).minus(bw/2.into()), (y + h/2.into()).minus(bh/2.into()), bw, bh];
[a.into(), b.into(), [x, y, w, h].into()]
},
South => {
let [x, y, w, h] = outer.center_xy([aw.max(bw), ah + bh]);
let a = [(x + w/2.into()).minus(aw/2.into()), y, aw, ah];
let b = [(x + w/2.into()).minus(bw/2.into()), y + ah, bw, bh];
[a.into(), b.into(), [x, y, w, h].into()]
},
North => {
let [x, y, w, h] = outer.center_xy([aw.max(bw), ah + bh]);
let a = [(x + (w/2.into())).minus(aw/2.into()), y + bh, aw, ah];
let b = [(x + (w/2.into())).minus(bw/2.into()), y, bw, bh];
[a.into(), b.into(), [x, y, w, h].into()]
},
East => {
let [x, y, w, h] = outer.center_xy([aw + bw, ah.max(bh)]);
let a = [x, (y + h/2.into()).minus(ah/2.into()), aw, ah];
let b = [x + aw, (y + h/2.into()).minus(bh/2.into()), bw, bh];
[a.into(), b.into(), [x, y, w, h].into()]
},
West => {
let [x, y, w, h] = outer.center_xy([aw + bw, ah.max(bh)]);
let a = [x + bw, (y + h/2.into()).minus(ah/2.into()), aw, ah];
let b = [x, (y + h/2.into()).minus(bh/2.into()), bw, bh];
[a.into(), b.into(), [x, y, w, h].into()]
},
}
}
}
impl<E: Output, A: Content<E>, B: Content<E>> BspAreas<E, A, B> for Bsp<A, B> {
fn direction (&self) -> Direction { self.0 }
fn contents (&self) -> (&A, &B) { (&self.1, &self.2) }
}
/// Renders multiple 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 }};
}
#[cfg(test)] mod test {
use super::*;
use proptest::prelude::*;
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]),
Render::layout(&bsp, [x, y, w, h]),
);
}
}
}

23
output/src/ops/collect.rs Normal file
View file

@ -0,0 +1,23 @@
//! Groupings of elements.
use crate::*;
/// A function or closure that emits renderables.
pub trait Collector<E: Engine>: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)) {}
/// Any function or closure that emits renderables for the given engine matches [CollectCallback].
impl<E, F> Collector<E> for F
where E: Engine, F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)) {}
pub trait Render<E: Engine> {
fn area (&self, to: E::Area) -> E::Area;
fn render (&self, to: &mut E::Output);
}
impl<E: Engine, C: Content<E>> Render<E> for C {
fn area (&self, to: E::Area) -> E::Area {
Content::area(self, to)
}
fn render (&self, to: &mut E::Output) {
Content::render(self, to)
}
}

75
output/src/ops/cond.rs Normal file
View file

@ -0,0 +1,75 @@
use crate::*;
/// Show an item only when a condition is true.
pub struct When<A>(pub bool, pub A);
impl<A> When<A> {
/// Create a binary condition.
pub const fn new (c: bool, a: A) -> Self {
Self(c, a)
}
}
/// Show one item if a condition is true and another if the condition is false
pub struct Either<A, B>(pub bool, pub A, pub B);
impl<A, B> Either<A, B> {
/// Create a ternary condition.
pub const fn new (c: bool, a: A, b: B) -> Self {
Self(c, a, b)
}
}
#[cfg(feature = "dsl")]
try_from_expr!(<'a, E>: When<RenderBox<'a, E>>: |state, iter| {
if let Some(Token { value: Value::Key("when"), .. }) = iter.peek() {
let _ = iter.next().unwrap();
let condition = iter.next().expect("no condition specified");
let content = iter.next().expect("no content specified");
let condition = state.get(&condition.value).expect("no condition provided");
let content = state.get_content(&content.value).expect("no content provided");
return Some(Self(condition, content))
}
});
#[cfg(feature = "dsl")]
try_from_expr!(<'a, E>: Either<RenderBox<'a, E>, RenderBox<'a, E>>: |state, iter| {
if let Some(Token { value: Value::Key("either"), .. }) = iter.peek() {
let _ = iter.next().unwrap();
let condition = iter.next().expect("no condition specified");
let content = iter.next().expect("no content specified");
let alternate = iter.next().expect("no alternate specified");
let condition = state.get(&condition.value).expect("no condition provided");
let content = state.get_content(&content.value).expect("no content provided");
let alternate = state.get_content(&alternate.value).expect("no alternate provided");
return Some(Self(condition, content, alternate))
}
});
impl<E: Output, A: Render<E>> Content<E> for When<A> {
fn layout (&self, to: E::Area) -> E::Area {
let Self(cond, item) = self;
let mut area = E::Area::zero();
if *cond {
let item_area = item.layout(to);
area[0] = item_area.x();
area[1] = item_area.y();
area[2] = item_area.w();
area[3] = item_area.h();
}
area.into()
}
fn render (&self, to: &mut E) {
let Self(cond, item) = self;
if *cond { item.render(to) }
}
}
impl<E: Output, A: Render<E>, B: Render<E>> Content<E> for Either<A, B> {
fn layout (&self, to: E::Area) -> E::Area {
let Self(cond, a, b) = self;
if *cond { a.layout(to) } else { b.layout(to) }
}
fn render (&self, to: &mut E) {
let Self(cond, a, b) = self;
if *cond { a.render(to) } else { b.render(to) }
}
}

98
output/src/ops/map.rs Normal file
View file

@ -0,0 +1,98 @@
use crate::*;
#[inline] pub fn map_south<O: Output>(
item_offset: O::Unit,
item_height: O::Unit,
item: impl Content<O>
) -> impl Content<O> {
Push::y(item_offset, Fixed::y(item_height, Fill::x(item)))
}
#[inline] pub fn map_south_west<O: Output>(
item_offset: O::Unit,
item_height: O::Unit,
item: impl Content<O>
) -> impl Content<O> {
Push::y(item_offset, Align::nw(Fixed::y(item_height, Fill::x(item))))
}
#[inline] pub fn map_east<O: Output>(
item_offset: O::Unit,
item_width: O::Unit,
item: impl Content<O>
) -> impl Content<O> {
Push::x(item_offset, Align::w(Fixed::x(item_width, Fill::y(item))))
}
/// Renders items from an iterator.
pub struct Map<E, A, B, I, F, G>
where
I: Iterator<Item = A> + Send + Sync,
F: Fn() -> I + Send + Sync,
{
__: PhantomData<(E, B)>,
/// Function that returns iterator over stacked components
get_iterator: F,
/// Function that returns each stacked component
get_item: G,
}
impl<'a, E, A, B, I, F, G> Map<E, A, B, I, F, G> where
I: Iterator<Item = A> + Send + Sync + 'a,
F: Fn() -> I + Send + Sync + 'a,
{
pub const fn new (get_iterator: F, get_item: G) -> Self {
Self {
__: PhantomData,
get_iterator,
get_item
}
}
}
impl<'a, E, A, B, I, F, G> Content<E> for Map<E, A, B, I, F, G> where
E: Output,
B: Render<E>,
I: Iterator<Item = A> + Send + Sync + 'a,
F: Fn() -> I + Send + Sync + 'a,
G: Fn(A, usize)->B + Send + Sync
{
fn layout (&self, area: E::Area) -> E::Area {
let Self { get_iterator, get_item, .. } = self;
let mut index = 0;
let [mut min_x, mut min_y] = area.center();
let [mut max_x, mut max_y] = area.center();
for item in get_iterator() {
let [x,y,w,h] = get_item(item, index).layout(area).xywh();
min_x = min_x.min(x.into());
min_y = min_y.min(y.into());
max_x = max_x.max((x + w).into());
max_y = max_y.max((y + h).into());
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.center_xy([w.into(), h.into()].into()).into()
}
fn render (&self, to: &mut E) {
let Self { get_iterator, get_item, .. } = self;
let mut index = 0;
let area = Content::layout(self, to.area());
for item in get_iterator() {
let item = get_item(item, index);
//to.place(area.into(), &item);
to.place(item.layout(area), &item);
index += 1;
}
}
}
#[cfg(test)] #[test] fn test_iter_map () {
struct Foo;
impl<T: Output> Content<T> for Foo {}
fn make_map <T: Output, U: Content<T> + Send + Sync> (data: &Vec<U>) -> impl Content<T> {
Map::new(||data.iter(), |foo, index|{})
}
let data = vec![Foo, Foo, Foo];
//let map = make_map(&data);
}

93
output/src/ops/reduce.rs Normal file
View file

@ -0,0 +1,93 @@
use crate::*;
pub struct Reduce<A, B, I, F, G>(pub PhantomData<A>, pub F, pub G) where
A: Send + Sync, B: Send + Sync,
I: Iterator<Item = B> + Send + Sync,
F: Fn() -> I + Send + Sync,
G: Fn(A, B, usize)->A + Send + Sync;
impl<A, B, I, F, G> Reduce<A, B, I, F, G> where
A: Send + Sync, B: Send + Sync,
I: Iterator<Item = B> + Send + Sync,
F: Fn() -> I + Send + Sync,
G: Fn(A, B, usize)->A + Send + Sync
{
pub const fn new (f: F, g: G) -> Self { Self(Default::default(), f, g) }
}
impl<E: Output, A, B, I, F, G> Content<E> for Reduce<A, B, I, F, G> where
A: Send + Sync, B: Send + Sync,
I: Iterator<Item = B> + Send + Sync,
F: Fn() -> I + Send + Sync,
G: Fn(A, B, usize)->A + Send + Sync
{
fn content (&self) -> impl Render<E> {
}
}
/*
//pub fn reduce <E, T, I, R, F>(iterator: I, callback: F) -> Reduce<E, T, I, R, F> where
//E: Output,
//I: Iterator<Item = T> + Send + Sync,
//R: Render<E>,
//F: Fn(R, T, usize) -> R + Send + Sync
//{
//Reduce(Default::default(), iterator, callback)
//}
pub struct Reduce<E, T, I, R, F>(PhantomData<(E, R)>, I, F) where
E: Output,
I: Iterator<Item = T> + Send + Sync,
R: Render<E>,
F: Fn(R, T, usize) -> R + Send + Sync;
impl<E, T, I, R, F> Content<E> for Reduce<E, T, I, R, F> where
E: Output,
I: Iterator<Item = T> + Send + Sync,
R: Render<E>,
F: Fn(R, T, usize) -> R + Send + Sync
{
fn render (&self, to: &mut E) {
todo!()
}
}
*/
//macro_rules! define_ops {
//($Trait:ident<$E:ident:$Output:path> { $(
//$(#[$attr:meta $($attr_args:tt)*])*
//(
//$fn:ident
//$(<$($G:ident$(:$Gen:path)?, )+>)?
//$Op:ident
//($($arg:ident:$Arg:ty),*)
//)
//)* }) => {
//impl<$E: $Output> $Trait<E> for E {}
//pub trait $Trait<$E: $Output> {
//$(
//$(#[$attr $($attr_args)*])*
//fn $fn $(<$($G),+>)?
//($($arg:$Arg),*)-> $Op<$($(, $G)+)?>
//$(where $($G: $($Gen + Send + Sync)?),+)?
//{ $Op($($arg),*) }
//)*
//}
//}
//}
//define_ops! {
//Layout<E: Output> {
//(when <A: Render<E>,>
//When(cond: bool, item: A))
///// When `cond` is `true`, render `a`, otherwise render `b`.
//(either <A: Render<E>, B: Render<E>,>
//Either(cond: bool, a: A, b: B))
///// If `opt` is `Some(T)` renders `cb(t)`, otherwise nothing.
//(opt <A, F: Fn(A) -> B, B: Render<E>,>
//Opt(option: Option<A>, cb: F))
///// Maps items of iterator through callback.
//(map <A, B: Render<E>, I: Iterator<Item = A>, F: Fn() -> I, G: Fn(A, usize)->B,>
//Map(get_iterator: F, callback: G))
//}
//}

74
output/src/ops/thunk.rs Normal file
View file

@ -0,0 +1,74 @@
use crate::*;
use std::marker::PhantomData;
/// Lazily-evaluated [Render]able.
pub struct Thunk<E: Output, T: Render<E>, F: Fn()->T + Send + Sync>(
PhantomData<E>,
F
);
impl<E: Output, T: Render<E>, F: Fn()->T + Send + Sync> Thunk<E, T, F> {
pub const fn new (thunk: F) -> Self {
Self(PhantomData, thunk)
}
}
impl<E: Output, T: Render<E>, F: Fn()->T + Send + Sync> Content<E> for Thunk<E, T, F> {
fn content (&self) -> impl Render<E> { (self.1)() }
}
pub struct ThunkBox<'a, E: Output>(
PhantomData<E>,
Box<dyn Fn()->RenderBox<'a, E> + Send + Sync + 'a>
);
impl<'a, E: Output> ThunkBox<'a, E> {
pub const fn new (thunk: Box<dyn Fn()->RenderBox<'a, E> + Send + Sync + 'a>) -> Self {
Self(PhantomData, thunk)
}
}
impl<'a, E: Output> Content<E> for ThunkBox<'a, E> {
fn content (&self) -> impl Render<E> { (self.1)() }
}
impl<'a, E, F, T> From<F> for ThunkBox<'a, E>
where
E: Output,
F: Fn()->T + Send + Sync + 'a,
T: Render<E> + Send + Sync + 'a
{
fn from (f: F) -> Self {
Self(PhantomData, Box::new(move||f().boxed()))
}
}
//impl<'a, E: Output, F: Fn()->Box<dyn Render<E> + 'a> + Send + Sync + 'a> From<F> for ThunkBox<'a, E> {
//fn from (f: F) -> Self {
//Self(Default::default(), Box::new(f))
//}
//}
pub struct ThunkRender<E: Output, F: Fn(&mut E) + Send + Sync>(PhantomData<E>, F);
impl<E: Output, F: Fn(&mut E) + Send + Sync> ThunkRender<E, F> {
pub fn new (render: F) -> Self { Self(PhantomData, render) }
}
impl<E: Output, F: Fn(&mut E) + Send + Sync> Content<E> for ThunkRender<E, F> {
fn render (&self, to: &mut E) { (self.1)(to) }
}
pub struct ThunkLayout<
E: Output,
F1: Fn(E::Area)->E::Area + Send + Sync,
F2: Fn(&mut E) + Send + Sync
>(
PhantomData<E>,
F1,
F2
);
impl<E: Output, F1: Fn(E::Area)->E::Area + Send + Sync, F2: Fn(&mut E) + Send + Sync> ThunkLayout<E, F1, F2> {
pub fn new (layout: F1, render: F2) -> Self { Self(PhantomData, layout, render) }
}
impl<E, F1, F2> Content<E> for ThunkLayout<E, F1, F2>
where
E: Output,
F1: Fn(E::Area)->E::Area + Send + Sync,
F2: Fn(&mut E) + Send + Sync
{
fn layout (&self, to: E::Area) -> E::Area { (self.1)(to) }
fn render (&self, to: &mut E) { (self.2)(to) }
}

239
output/src/ops/transform.rs Normal file
View file

@ -0,0 +1,239 @@
//! [Content] items that modify the inherent
//! dimensions of their inner [Render]ables.
//!
//! Transform may also react to the [Area] provided.
//! ```
//! use ::tengri::{output::*, tui::*};
//! let area: [u16;4] = [10, 10, 20, 20];
//! fn test (area: [u16;4], item: &impl Content<TuiOut>, expected: [u16;4]) {
//! assert_eq!(Content::layout(item, area), expected);
//! assert_eq!(Render::layout(item, area), expected);
//! };
//! test(area, &(), [20, 20, 0, 0]);
//!
//! test(area, &Fill::xy(()), area);
//! test(area, &Fill::x(()), [10, 20, 20, 0]);
//! test(area, &Fill::y(()), [20, 10, 0, 20]);
//!
//! //FIXME:test(area, &Fixed::x(4, ()), [18, 20, 4, 0]);
//! //FIXME:test(area, &Fixed::y(4, ()), [20, 18, 0, 4]);
//! //FIXME:test(area, &Fixed::xy(4, 4, unit), [18, 18, 4, 4]);
//! ```
use crate::*;
/// Defines an enum that transforms its content
/// along either the X axis, the Y axis, or both.
macro_rules! transform_xy {
($x:literal $y:literal $xy:literal |$self:ident : $Enum:ident, $to:ident|$area:expr) => {
pub enum $Enum<T> { X(T), Y(T), XY(T) }
impl<T> $Enum<T> {
#[inline] pub const fn x (item: T) -> Self { Self::X(item) }
#[inline] pub const fn y (item: T) -> Self { Self::Y(item) }
#[inline] pub const fn xy (item: T) -> Self { Self::XY(item) }
}
#[cfg(feature = "dsl")]
impl<'a, E: Output + 'a, T: ViewContext<'a, E>> TryFromAtom<'a, T>
for $Enum<RenderBox<'a, E>> {
fn try_from_expr (state: &'a T, iter: TokenIter<'a>) -> Option<Self> {
let mut iter = iter.clone();
if let Some(Token { value: Value::Key(k), .. }) = iter.peek() {
if k == $x || k == $y || k == $xy {
let _ = iter.next().unwrap();
let token = iter.next()
.expect("no content specified");
let content = if let Some(content) = state.get_content(&token.value) {
content
} else {
panic!("no content corresponding to for {:?}", &token);
};
return Some(match k {
$x => Self::x(content),
$y => Self::y(content),
$xy => Self::xy(content),
_ => unreachable!()
})
}
}
None
}
}
impl<E: Output, T: Content<E>> Content<E> for $Enum<T> {
fn content (&self) -> impl Render<E> {
match self {
Self::X(item) => item,
Self::Y(item) => item,
Self::XY(item) => item,
}
}
fn layout (&$self, $to: <E as Output>::Area) -> <E as Output>::Area {
use $Enum::*;
$area
}
}
}
}
/// Defines an enum that parametrically transforms its content
/// along either the X axis, the Y axis, or both.
macro_rules! transform_xy_unit {
($x:literal $y:literal $xy:literal |$self:ident : $Enum:ident, $to:ident|$layout:expr) => {
pub enum $Enum<U, T> { X(U, T), Y(U, T), XY(U, U, T), }
impl<U, T> $Enum<U, T> {
#[inline] pub const fn x (x: U, item: T) -> Self { Self::X(x, item) }
#[inline] pub const fn y (y: U, item: T) -> Self { Self::Y(y, item) }
#[inline] pub const fn xy (x: U, y: U, item: T) -> Self { Self::XY(x, y, item) }
}
#[cfg(feature = "dsl")]
impl<'a, E: Output + 'a, T: ViewContext<'a, E>> TryFromAtom<'a, T>
for $Enum<E::Unit, RenderBox<'a, E>> {
fn try_from_expr (state: &'a T, iter: TokenIter<'a>) -> Option<Self> {
let mut iter = iter.clone();
if let Some(Token { value: Value::Key(k), .. }) = iter.peek() {
if k == $x || k == $y {
let _ = iter.next().unwrap();
let u = iter.next().expect("no unit specified");
let c = iter.next().expect("no content specified");
let u = state.get(&u.value).expect("no unit provided");
let c = state.get_content(&c.value).expect("no content provided");
return Some(match k {
$x => Self::x(u, c),
$y => Self::y(u, c),
_ => unreachable!(),
})
} else if k == $xy {
let _ = iter.next().unwrap();
let u = iter.next().expect("no unit specified");
let v = iter.next().expect("no unit specified");
let c = iter.next().expect("no content specified");
let u = state.get(&u.value).expect("no unit provided");
let v = state.get(&v.value).expect("no unit provided");
let c = state.get_content(&c.value).expect("no content provided");
return Some(Self::xy(u, v, c))
}
}
None
}
}
impl<E: Output, T: Content<E>> Content<E> for $Enum<E::Unit, T> {
fn content (&self) -> impl Render<E> {
Some(match self {
Self::X(_, content) => content,
Self::Y(_, content) => content,
Self::XY(_, _, content) => content,
})
}
fn layout (&$self, $to: E::Area) -> E::Area {
$layout.into()
}
}
impl<U: Copy + Coordinate, T> $Enum<U, T> {
#[inline] pub fn dx (&self) -> U {
match self {
Self::X(x, _) => *x, Self::Y(_, _) => 0.into(), Self::XY(x, _, _) => *x,
}
}
#[inline] pub fn dy (&self) -> U {
match self {
Self::X(_, _) => 0.into(), Self::Y(y, _) => *y, Self::XY(_, y, _) => *y,
}
}
}
}
}
transform_xy!("fill/x" "fill/y" "fill/xy" |self: Fill, to|{
let [x0, y0, wmax, hmax] = to.xywh();
let [x, y, w, h] = self.content().layout(to).xywh();
match self {
X(_) => [x0, y, wmax, h],
Y(_) => [x, y0, w, hmax],
XY(_) => [x0, y0, wmax, hmax],
}.into() });
transform_xy_unit!("fixed/x" "fixed/y" "fixed/xy"|self: Fixed, area|{
let [x, y, w, h] = area.xywh();
let fixed_area = match self {
Self::X(fw, _) => [x, y, *fw, h],
Self::Y(fh, _) => [x, y, w, *fh],
Self::XY(fw, fh, _) => [x, y, *fw, *fh],
};
let [x, y, w, h] = Render::layout(&self.content(), fixed_area.into()).xywh();
let fixed_area = match self {
Self::X(fw, _) => [x, y, *fw, h],
Self::Y(fh, _) => [x, y, w, *fh],
Self::XY(fw, fh, _) => [x, y, *fw, *fh],
};
fixed_area });
transform_xy_unit!("min/x" "min/y" "min/xy"|self: Min, area|{
let area = Render::layout(&self.content(), area);
match self {
Self::X(mw, _) => [area.x(), area.y(), area.w().max(*mw), area.h()],
Self::Y(mh, _) => [area.x(), area.y(), area.w(), area.h().max(*mh)],
Self::XY(mw, mh, _) => [area.x(), area.y(), area.w().max(*mw), area.h().max(*mh)],
}});
transform_xy_unit!("max/x" "max/y" "max/xy"|self: Max, area|{
let [x, y, w, h] = area.xywh();
Render::layout(&self.content(), match self {
Self::X(fw, _) => [x, y, *fw, h],
Self::Y(fh, _) => [x, y, w, *fh],
Self::XY(fw, fh, _) => [x, y, *fw, *fh],
}.into())});
transform_xy_unit!("shrink/x" "shrink/y" "shrink/xy"|self: Shrink, area|Render::layout(
&self.content(),
[area.x(), area.y(), area.w().minus(self.dx()), area.h().minus(self.dy())].into()));
transform_xy_unit!("expand/x" "expand/y" "expand/xy"|self: Expand, area|Render::layout(
&self.content(),
[area.x(), area.y(), area.w().plus(self.dx()), area.h().plus(self.dy())].into()));
transform_xy_unit!("push/x" "push/y" "push/xy"|self: Push, area|{
let area = Render::layout(&self.content(), area);
[area.x().plus(self.dx()), area.y().plus(self.dy()), area.w(), area.h()] });
transform_xy_unit!("pull/x" "pull/y" "pull/xy"|self: Pull, area|{
let area = Render::layout(&self.content(), area);
[area.x().minus(self.dx()), area.y().minus(self.dy()), area.w(), area.h()] });
transform_xy_unit!("margin/x" "margin/y" "margin/xy"|self: Margin, area|{
let area = Render::layout(&self.content(), area);
let dx = self.dx();
let dy = self.dy();
[area.x().minus(dx), area.y().minus(dy), area.w().plus(dy.plus(dy)), area.h().plus(dy.plus(dy))] });
transform_xy_unit!("padding/x" "padding/y" "padding/xy"|self: Padding, area|{
let area = Render::layout(&self.content(), area);
let dx = self.dx();
let dy = self.dy();
[area.x().plus(dx), area.y().plus(dy), area.w().minus(dy.plus(dy)), area.h().minus(dy.plus(dy)), ] });
#[cfg(test)] mod test_op_transform {
use super::*;
use proptest::prelude::*;
use proptest::option::of;
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)),
(Some(y), None) => Some($Op::y(y, content)),
_ => None
} {
assert_eq!(Content::layout(&op, [x, y, w, h]),
Render::layout(&op, [x, y, w, h]));
}
}
}
}
}
test_op_transform!(test_op_fixed, Fixed);
test_op_transform!(test_op_min, Min);
test_op_transform!(test_op_max, Max);
test_op_transform!(test_op_push, Push);
test_op_transform!(test_op_pull, Pull);
test_op_transform!(test_op_shrink, Shrink);
test_op_transform!(test_op_expand, Expand);
test_op_transform!(test_op_margin, Margin);
test_op_transform!(test_op_padding, Padding);
}