#[macro_export]
macro_rules! view {
(col $($rest:tt)*) => { $crate::view!(@one col $($rest)*) };
(row $($rest:tt)*) => { $crate::view!(@one row $($rest)*) };
(boxed $($rest:tt)*) => { $crate::view!(@one boxed $($rest)*) };
(text $($rest:tt)*) => { $crate::view!(@one text $($rest)*) };
(spacer $($rest:tt)*) => { $crate::view!(@one spacer $($rest)*) };
(node $($rest:tt)*) => { $crate::view!(@one node $($rest)*) };
(@one col $(( $($attr:tt)* ))? { $($kids:tt)* }) => {{
let __flex = $crate::components::Flex::column();
let __flex = $crate::view!(@attrs __flex; $($($attr)*)?);
let __flex = $crate::view!(@kids __flex; $($kids)*);
$crate::element(__flex)
}};
(@one row $(( $($attr:tt)* ))? { $($kids:tt)* }) => {{
let __flex = $crate::components::Flex::row();
let __flex = $crate::view!(@attrs __flex; $($($attr)*)?);
let __flex = $crate::view!(@kids __flex; $($kids)*);
$crate::element(__flex)
}};
(@one boxed $(( $($attr:tt)* ))? { $($kids:tt)* }) => {{
let __boxed = $crate::components::Boxed::new($crate::view!(@one $($kids)*));
let __boxed = $crate::view!(@boxattrs __boxed; $($($attr)*)?);
$crate::element(__boxed)
}};
(@one text ( $e:expr )) => {
$crate::element($crate::components::Text::raw($e))
};
(@one spacer ()) => { $crate::element($crate::components::Spacer) };
(@one node ( $e:expr )) => { $crate::element($e) };
(@kids $b:expr; ) => { $b };
(@kids $b:expr; grow ( $n:expr ) { $($node:tt)* } $($rest:tt)*) => {
$crate::view!(@kids $b.grow($n, $crate::view!(@one $($node)*)); $($rest)*)
};
(@kids $b:expr; fixed ( $n:expr ) { $($node:tt)* } $($rest:tt)*) => {
$crate::view!(@kids $b.fixed($n, $crate::view!(@one $($node)*)); $($rest)*)
};
(@kids $b:expr; col $(( $($a:tt)* ))? { $($k:tt)* } $($rest:tt)*) => {
$crate::view!(@kids $b.auto($crate::view!(@one col $(($($a)*))? { $($k)* })); $($rest)*)
};
(@kids $b:expr; row $(( $($a:tt)* ))? { $($k:tt)* } $($rest:tt)*) => {
$crate::view!(@kids $b.auto($crate::view!(@one row $(($($a)*))? { $($k)* })); $($rest)*)
};
(@kids $b:expr; boxed $(( $($a:tt)* ))? { $($k:tt)* } $($rest:tt)*) => {
$crate::view!(@kids $b.auto($crate::view!(@one boxed $(($($a)*))? { $($k)* })); $($rest)*)
};
(@kids $b:expr; text ( $e:expr ) $($rest:tt)*) => {
$crate::view!(@kids $b.auto($crate::element($crate::components::Text::raw($e))); $($rest)*)
};
(@kids $b:expr; spacer () $($rest:tt)*) => {
$crate::view!(@kids $b.auto($crate::element($crate::components::Spacer)); $($rest)*)
};
(@kids $b:expr; node ( $e:expr ) $($rest:tt)*) => {
$crate::view!(@kids $b.auto($crate::element($e)); $($rest)*)
};
(@attrs $b:expr; ) => { $b };
(@attrs $b:expr; gap = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@attrs $b.gap($e); $($($rest)*)?)
};
(@attrs $b:expr; padding = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@attrs $b.padding($e); $($($rest)*)?)
};
(@attrs $b:expr; align = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@attrs $b.align($e); $($($rest)*)?)
};
(@attrs $b:expr; justify = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@attrs $b.justify($e); $($($rest)*)?)
};
(@attrs $b:expr; background = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@attrs $b.background($e); $($($rest)*)?)
};
(@boxattrs $b:expr; ) => { $b };
(@boxattrs $b:expr; title = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@boxattrs $b.title($e); $($($rest)*)?)
};
(@boxattrs $b:expr; title_bottom = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@boxattrs $b.title_bottom($e); $($($rest)*)?)
};
(@boxattrs $b:expr; border = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@boxattrs $b.border($e); $($($rest)*)?)
};
(@boxattrs $b:expr; border_color = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@boxattrs $b.border_color($e); $($($rest)*)?)
};
(@boxattrs $b:expr; padding = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@boxattrs $b.padding($e); $($($rest)*)?)
};
(@boxattrs $b:expr; background = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@boxattrs $b.background($e); $($($rest)*)?)
};
}
#[cfg(test)]
mod tests {
use crate::components::{Boxed, Flex, Spacer, Text};
use crate::tests::support::render_el;
use crate::view::{Element, RenderCtx, View, element};
use crate::{Size, Surface};
use ratatui_core::layout::Rect;
use ratatui_core::style::Style;
#[test]
fn view_macro_matches_builder() {
let built: Element = element(
Flex::column()
.gap(1)
.auto(element(Boxed::new(element(Text::raw("hi"))).title(" t ")))
.grow(1, element(Spacer)),
);
let macroed: Element = crate::view! {
col(gap = 1) {
boxed(title = " t ") { text("hi") }
grow(1) { spacer() }
}
};
let a = render_el(&built, 20, 5);
let b = render_el(¯oed, 20, 5);
assert_eq!(a, b, "view! must render identically to the builder form");
assert!(b.iter().any(|l| l.contains('t')), "{b:?}");
}
#[test]
fn view_macro_forwards_title_bottom() {
let built: Element = element(
Boxed::new(element(Text::raw("hi")))
.title(" top ")
.title_bottom(" 1/3 "),
);
let macroed: Element = crate::view! {
boxed(title = " top ", title_bottom = " 1/3 ") { text("hi") }
};
let a = render_el(&built, 12, 3);
let b = render_el(¯oed, 12, 3);
assert_eq!(a, b, "title_bottom must render identically to the builder");
assert!(
b.last().is_some_and(|l| l.contains("1/3")),
"bottom border carries the secondary title: {b:?}"
);
}
#[test]
fn view_macro_forwards_border_color() {
use ratatui_core::style::Color;
let built: Element =
element(Boxed::new(element(Text::raw("hi"))).border_color(Color::Indexed(201)));
let macroed: Element = crate::view! {
boxed(border_color = Color::Indexed(201)) { text("hi") }
};
assert_eq!(
render_el(&built, 10, 3),
render_el(¯oed, 10, 3),
"border_color must render identically to the builder"
);
}
struct Star;
impl View for Star {
fn measure(&self, _available: Size) -> Size {
Size::new(1, 1)
}
fn render(&self, area: Rect, surface: &mut Surface, _ctx: &RenderCtx) {
surface.set(area.x, area.y, '★', Style::default());
}
}
#[test]
fn view_macro_accepts_foreign_view_via_node() {
let tree: Element = crate::view! {
col {
node(Star)
}
};
let out = render_el(&tree, 5, 2);
assert!(out[0].contains('★'), "foreign view should render: {out:?}");
}
}