#[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::scoped_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::scoped_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; when ( $condition:expr ) { $($node:tt)* } $($rest:tt)*) => {{
let __flex = $b;
let __flex = if $condition {
__flex.auto($crate::view!(@one $($node)*))
} else {
__flex
};
$crate::view!(@kids __flex; $($rest)*)
}};
(@kids $b:expr; for ( $pattern:pat in $values:expr ) { $($node:tt)* } $($rest:tt)*) => {{
let mut __flex = $b;
for $pattern in $values {
__flex = __flex.auto($crate::view!(@one $($node)*));
}
$crate::view!(@kids __flex; $($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; row_gap = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@attrs $b.row_gap($e); $($($rest)*)?)
};
(@attrs $b:expr; column_gap = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@attrs $b.column_gap($e); $($($rest)*)?)
};
(@attrs $b:expr; wrap = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@attrs $b.wrap($e); $($($rest)*)?)
};
(@attrs $b:expr; align_content = $e:expr $(, $($rest:tt)*)?) => {
$crate::view!(@attrs $b.align_content($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, _ctx: &RenderCtx) -> 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:?}");
}
struct BorrowedLabel<'a>(&'a str);
impl View for BorrowedLabel<'_> {
fn measure(&self, available: Size, _ctx: &RenderCtx) -> Size {
Size::new(
(self.0.len() as u16).min(available.width),
available.height.min(1),
)
}
fn render(&self, area: Rect, surface: &mut Surface, _ctx: &RenderCtx) {
surface.set_string(area.x, area.y, self.0, Style::default());
}
}
#[test]
fn view_macro_preserves_borrows_through_nested_containers() {
let label = String::from("borrowed");
let tree = crate::view! {
boxed(title = " frame ") {
col {
node(BorrowedLabel(&label))
}
}
};
let rendered = crate::testing::render(&tree, 14, 3, &crate::Theme::default());
assert!(
crate::testing::grid(&rendered).contains("borrowed"),
"a borrow must survive node -> Flex -> Boxed composition"
);
}
#[test]
fn view_macro_composes_conditional_and_repeated_children() {
let labels = ["one", "two"];
let tree: Element = crate::view! {
col {
when(false) { text("hidden") }
for(label in labels) { node(Text::raw(label)) }
when(true) { text("shown") }
}
};
let output = crate::testing::grid(&crate::testing::render(
&tree,
8,
3,
&crate::Theme::default(),
));
assert_eq!(output, "one \ntwo \nshown ");
}
}