Skip to main content

revue/widget/
macros.rs

1//! Declarative UI macros
2
3/// Create a vertical stack with children
4///
5/// # Examples
6///
7/// ```ignore
8/// use revue::prelude::*;
9///
10/// let view = vstack![
11///     Text::heading("Title"),
12///     Text::new("Content"),
13/// ];
14///
15/// // With gap
16/// let view = vstack![gap: 2;
17///     Text::new("Line 1"),
18///     Text::new("Line 2"),
19/// ];
20/// ```
21#[macro_export]
22macro_rules! vstack {
23    // With gap
24    (gap: $gap:expr; $($child:expr),* $(,)?) => {{
25        $crate::widget::vstack()
26            .gap($gap)
27            $(.child($child))*
28    }};
29    // Without gap
30    ($($child:expr),* $(,)?) => {{
31        $crate::widget::vstack()
32            $(.child($child))*
33    }};
34}
35
36/// Create a horizontal stack with children
37///
38/// # Examples
39///
40/// ```ignore
41/// use revue::prelude::*;
42///
43/// let view = hstack![
44///     Text::new("Left"),
45///     Text::new("Right"),
46/// ];
47///
48/// // With gap
49/// let view = hstack![gap: 1;
50///     Text::new("A"),
51///     Text::new("B"),
52/// ];
53/// ```
54#[macro_export]
55macro_rules! hstack {
56    // With gap
57    (gap: $gap:expr; $($child:expr),* $(,)?) => {{
58        $crate::widget::hstack()
59            .gap($gap)
60            $(.child($child))*
61    }};
62    // Without gap
63    ($($child:expr),* $(,)?) => {{
64        $crate::widget::hstack()
65            $(.child($child))*
66    }};
67}
68
69/// Create a border with a child
70///
71/// # Examples
72///
73/// ```ignore
74/// use revue::prelude::*;
75///
76/// let view = bordered![
77///     Text::new("Content")
78/// ];
79///
80/// // With title
81/// let view = bordered!["My Panel";
82///     Text::new("Content")
83/// ];
84///
85/// // With type and title
86/// let view = bordered![rounded, "Card Title";
87///     Text::new("Card content")
88/// ];
89/// ```
90#[macro_export]
91macro_rules! bordered {
92    // Border type + title + child
93    ($border_type:ident, $title:expr; $child:expr) => {{
94        $crate::widget::Border::$border_type()
95            .title($title)
96            .child($child)
97    }};
98    // Title + child
99    ($title:expr; $child:expr) => {{
100        $crate::widget::Border::single().title($title).child($child)
101    }};
102    // Just child
103    ($child:expr) => {{
104        $crate::widget::Border::single().child($child)
105    }};
106}
107
108/// Create text with common styling
109///
110/// # Examples
111///
112/// ```ignore
113/// use revue::prelude::*;
114///
115/// let t = text!("Hello");
116/// let t = text!("Error!", red);
117/// let t = text!("Success", green, bold);
118/// ```
119#[macro_export]
120macro_rules! text {
121    // Text with color and modifiers
122    ($content:expr, $color:ident, bold) => {{
123        $crate::widget::Text::new($content)
124            .fg($crate::style::Color::$color)
125            .bold()
126    }};
127    ($content:expr, $color:ident, italic) => {{
128        $crate::widget::Text::new($content)
129            .fg($crate::style::Color::$color)
130            .italic()
131    }};
132    // Text with color
133    ($content:expr, red) => {{
134        $crate::widget::Text::error($content)
135    }};
136    ($content:expr, green) => {{
137        $crate::widget::Text::success($content)
138    }};
139    ($content:expr, yellow) => {{
140        $crate::widget::Text::warning($content)
141    }};
142    ($content:expr, cyan) => {{
143        $crate::widget::Text::info($content)
144    }};
145    ($content:expr, $color:ident) => {{
146        $crate::widget::Text::new($content).fg($crate::style::Color::$color)
147    }};
148    // Plain text
149    ($content:expr) => {{
150        $crate::widget::Text::new($content)
151    }};
152}
153
154/// Build a complete UI layout declaratively
155///
156/// # Examples
157///
158/// ```ignore
159/// use revue::prelude::*;
160///
161/// let ui = ui! {
162///     vstack(gap: 1) {
163///         Text::heading("Dashboard")
164///         hstack {
165///             bordered!["Stats"; Text::new("100")]
166///             bordered!["Users"; Text::new("42")]
167///         }
168///         Text::muted("Press 'q' to quit")
169///     }
170/// };
171/// ```
172#[macro_export]
173macro_rules! ui {
174    // VStack with options
175    (vstack(gap: $gap:expr) { $($child:tt)* }) => {{
176        $crate::widget::vstack()
177            .gap($gap)
178            $(.child($crate::ui!(@child $child)))*
179    }};
180    // VStack without options
181    (vstack { $($child:tt)* }) => {{
182        $crate::widget::vstack()
183            $(.child($crate::ui!(@child $child)))*
184    }};
185    // HStack with options
186    (hstack(gap: $gap:expr) { $($child:tt)* }) => {{
187        $crate::widget::hstack()
188            .gap($gap)
189            $(.child($crate::ui!(@child $child)))*
190    }};
191    // HStack without options
192    (hstack { $($child:tt)* }) => {{
193        $crate::widget::hstack()
194            $(.child($crate::ui!(@child $child)))*
195    }};
196    // Child processing - recursive ui! call
197    (@child vstack $($rest:tt)*) => {{
198        $crate::ui!(vstack $($rest)*)
199    }};
200    (@child hstack $($rest:tt)*) => {{
201        $crate::ui!(hstack $($rest)*)
202    }};
203    // Child processing - direct expression
204    (@child $expr:expr) => {{
205        $expr
206    }};
207}
208
209#[cfg(test)]
210mod tests {
211    use crate::layout::Rect;
212    use crate::render::Buffer;
213    use crate::widget::{RenderContext, Text, View};
214
215    #[test]
216    fn test_vstack_macro() {
217        let stack = vstack![Text::new("Line 1"), Text::new("Line 2"),];
218        assert_eq!(stack.len(), 2);
219    }
220
221    #[test]
222    fn test_vstack_macro_with_gap() {
223        let stack = vstack![gap: 2;
224            Text::new("A"),
225            Text::new("B"),
226        ];
227        assert_eq!(stack.len(), 2);
228    }
229
230    #[test]
231    fn test_hstack_macro() {
232        let stack = hstack![Text::new("Left"), Text::new("Right"),];
233        assert_eq!(stack.len(), 2);
234    }
235
236    #[test]
237    fn test_text_macro() {
238        let t = text!("Hello");
239        assert_eq!(t.content(), "Hello");
240
241        let t = text!("Error", red);
242        assert_eq!(t.content(), "Error");
243    }
244
245    #[test]
246    fn test_bordered_macro() {
247        let b = bordered![Text::new("Content")];
248        // Just verify it compiles and creates a border
249        let mut buffer = Buffer::new(10, 5);
250        let area = Rect::new(0, 0, 10, 5);
251        let mut ctx = RenderContext::new(&mut buffer, area);
252        b.render(&mut ctx);
253    }
254
255    #[test]
256    fn test_bordered_macro_with_title() {
257        let b = bordered!["Title"; Text::new("Content")];
258        let mut buffer = Buffer::new(20, 5);
259        let area = Rect::new(0, 0, 20, 5);
260        let mut ctx = RenderContext::new(&mut buffer, area);
261        b.render(&mut ctx);
262    }
263
264    #[test]
265    fn test_nested_layout() {
266        let layout = vstack![
267            Text::heading("Title"),
268            hstack![Text::new("Left"), Text::new("Right"),],
269            Text::muted("Footer"),
270        ];
271        assert_eq!(layout.len(), 3);
272    }
273}