Skip to main content

guise/
macros.rs

1//! Declarative layout macros: terse builders for container components.
2//!
3//! ```ignore
4//! use guise::prelude::*;
5//!
6//! col![
7//!     row![avatar, name, Spacer::new(), actions],
8//!     divider,
9//!     body,
10//! ]
11//! ```
12//!
13//! Each macro forwards its comma-separated arguments as children and returns
14//! the underlying builder, so you can keep chaining configuration:
15//!
16//! ```ignore
17//! card![title, body].with_border(true).shadow(Size::Md)
18//! ```
19//!
20//! A trailing comma is allowed. The macros bring `.child()` into scope
21//! themselves, so no extra imports are needed.
22//!
23//! There is intentionally **no macro per component**: macros only help
24//! containers that take a variadic list of children. A leaf builder like
25//! `Button::new(id, label).variant(..)` is already the better API — a macro
26//! there would just lose the fluent setters.
27
28/// A Flutter-style horizontal [`Row`](crate::flex::Row).
29#[macro_export]
30macro_rules! row {
31    ($($child:expr),* $(,)?) => {{
32        #[allow(unused_imports)]
33        use $crate::__ParentElement as _;
34        $crate::flex::Row::new() $(.child($child))*
35    }};
36}
37
38/// A Flutter-style vertical [`Column`](crate::flex::Column).
39///
40/// Named `col!` (not `column!`) to avoid clashing with the std `column!` macro.
41#[macro_export]
42macro_rules! col {
43    ($($child:expr),* $(,)?) => {{
44        #[allow(unused_imports)]
45        use $crate::__ParentElement as _;
46        $crate::flex::Column::new() $(.child($child))*
47    }};
48}
49
50/// A layered [`Stack`](crate::flex::Stack) (z-axis overlap).
51#[macro_export]
52macro_rules! zstack {
53    ($($child:expr),* $(,)?) => {{
54        #[allow(unused_imports)]
55        use $crate::__ParentElement as _;
56        $crate::flex::Stack::new() $(.child($child))*
57    }};
58}
59
60/// A wrapping row, [`Wrap`](crate::flex::Wrap).
61#[macro_export]
62macro_rules! wrap {
63    ($($child:expr),* $(,)?) => {{
64        #[allow(unused_imports)]
65        use $crate::__ParentElement as _;
66        $crate::flex::Wrap::new() $(.child($child))*
67    }};
68}
69
70/// A themed vertical [`Stack`](crate::layout::Stack) (token-based spacing).
71#[macro_export]
72macro_rules! vstack {
73    ($($child:expr),* $(,)?) => {{
74        #[allow(unused_imports)]
75        use $crate::__ParentElement as _;
76        $crate::layout::Stack::new() $(.child($child))*
77    }};
78}
79
80/// A themed horizontal [`Group`](crate::layout::Group) (token-based spacing).
81#[macro_export]
82macro_rules! hstack {
83    ($($child:expr),* $(,)?) => {{
84        #[allow(unused_imports)]
85        use $crate::__ParentElement as _;
86        $crate::layout::Group::new() $(.child($child))*
87    }};
88}
89
90/// A [`Center`](crate::layout::Center) wrapping the given children.
91#[macro_export]
92macro_rules! center {
93    ($($child:expr),* $(,)?) => {{
94        #[allow(unused_imports)]
95        use $crate::__ParentElement as _;
96        $crate::layout::Center::new() $(.child($child))*
97    }};
98}
99
100/// A [`Paper`](crate::Paper) surface around the given children.
101#[macro_export]
102macro_rules! paper {
103    ($($child:expr),* $(,)?) => {{
104        #[allow(unused_imports)]
105        use $crate::__ParentElement as _;
106        $crate::Paper::new() $(.child($child))*
107    }};
108}
109
110/// A [`Card`](crate::Card) around the given children.
111#[macro_export]
112macro_rules! card {
113    ($($child:expr),* $(,)?) => {{
114        #[allow(unused_imports)]
115        use $crate::__ParentElement as _;
116        $crate::Card::new() $(.child($child))*
117    }};
118}
119
120/// A [`Modal`](crate::Modal) around the given children. Chain `.title(..)` and
121/// `.on_close(..)` after.
122#[macro_export]
123macro_rules! modal {
124    ($($child:expr),* $(,)?) => {{
125        #[allow(unused_imports)]
126        use $crate::__ParentElement as _;
127        $crate::Modal::new() $(.child($child))*
128    }};
129}
130
131// --- Component shorthands --------------------------------------------------
132//
133// Content components also accept `format!` args; e.g. `text!("Hi {}", name)`.
134// All of these return the underlying builder, so `.variant(..)`, `.color(..)`,
135// etc. still chain.
136
137/// [`Text`](crate::Text), with optional `format!` arguments.
138#[macro_export]
139macro_rules! text {
140    ($fmt:literal, $($arg:tt)*) => { $crate::Text::new(format!($fmt, $($arg)*)) };
141    ($content:expr) => { $crate::Text::new($content) };
142}
143
144/// [`Title`](crate::Title), with optional `format!` arguments.
145#[macro_export]
146macro_rules! title {
147    ($fmt:literal, $($arg:tt)*) => { $crate::Title::new(format!($fmt, $($arg)*)) };
148    ($content:expr) => { $crate::Title::new($content) };
149}
150
151/// Inline [`Code`](crate::Code), with optional `format!` arguments.
152#[macro_export]
153macro_rules! code {
154    ($fmt:literal, $($arg:tt)*) => { $crate::Code::new(format!($fmt, $($arg)*)) };
155    ($content:expr) => { $crate::Code::new($content) };
156}
157
158/// A [`Kbd`](crate::Kbd) key, with optional `format!` arguments.
159#[macro_export]
160macro_rules! kbd {
161    ($fmt:literal, $($arg:tt)*) => { $crate::Kbd::new(format!($fmt, $($arg)*)) };
162    ($content:expr) => { $crate::Kbd::new($content) };
163}
164
165/// A [`Button`](crate::Button): `button!(id, label)`. Chain setters after.
166#[macro_export]
167macro_rules! button {
168    ($($arg:expr),* $(,)?) => { $crate::Button::new($($arg),*) };
169}
170
171/// A [`Badge`](crate::Badge): `badge!(label)`. Chain setters after.
172#[macro_export]
173macro_rules! badge {
174    ($($arg:expr),* $(,)?) => { $crate::Badge::new($($arg),*) };
175}
176
177/// A CSS-style color literal, producing a gpui `Hsla`.
178///
179/// ```ignore
180/// color!(rgb(34, 139, 230))
181/// color!(rgba(34, 139, 230, 0.5))
182/// color!(hsl(210, 80, 52))          // s/l are percentages (no `%` token)
183/// color!(hsla(210, 80, 52, 0.5))
184/// color!(teal)                      // a CSS named color
185/// color!("#228be6")                 // any CSS string — hex (incl. #rgba/#rrggbbaa),
186/// color!("hsl(210, 80%, 52%)")      // functional with `%`, or a named color
187/// ```
188///
189/// Bare hex (`#228be6`) can't be a macro token in Rust, so pass hex as a string.
190/// The result is an `Hsla`, usable directly in `.bg(..)` / `.text_color(..)` and
191/// anywhere a `ColorValue` is accepted (component `.color(..)`, `Theme::with_*`).
192#[macro_export]
193macro_rules! color {
194    (rgb($r:expr, $g:expr, $b:expr $(,)?)) => {
195        $crate::theme::rgb($r, $g, $b)
196    };
197    (rgba($r:expr, $g:expr, $b:expr, $a:expr $(,)?)) => {
198        $crate::theme::rgba($r, $g, $b, $a)
199    };
200    (hsl($h:expr, $s:expr, $l:expr $(,)?)) => {
201        $crate::theme::hsl($h as f32, $s as f32, $l as f32)
202    };
203    (hsla($h:expr, $s:expr, $l:expr, $a:expr $(,)?)) => {
204        $crate::theme::hsla($h as f32, $s as f32, $l as f32, $a)
205    };
206    ($css:literal) => {
207        $crate::theme::css($css).expect("guise: invalid CSS color literal")
208    };
209    ($name:ident) => {
210        $crate::theme::css(stringify!($name)).expect("guise: unknown CSS color name")
211    };
212}
213
214/// A CSS-like style block. Expands to an element transform applied with
215/// [`StyleExt::apply`](crate::style::StyleExt):
216///
217/// ```ignore
218/// use guise::prelude::*;
219///
220/// gpui::div().apply(style! {
221///     display: flex;
222///     direction: column;
223///     align: center;
224///     gap: 8;
225///     padding: 16;
226///     width: full;
227///     radius: 12;
228///     background: "#11151c";              // string → css() shorthand
229///     color: color!(rgb(230, 230, 230));  // or any color! / Hsla expr
230///     border: color!("#2a2f3a");          // 1px border of this color
231///     weight: semibold;
232///     opacity: 0.95;
233/// })
234/// ```
235///
236/// Numbers are pixels; colors are a string literal (parsed via `css`) or any
237/// `Into<Hsla>` expression (e.g. `color!(..)`). Theme tokens (`Size::Md`) aren't
238/// available here — `style!` is pure and has no `cx`; use raw px or the builder
239/// methods for token-based values. Every declaration ends with `;`.
240///
241/// Supported: `background`, `color`, `border`; `display: flex`;
242/// `direction: row|column|col`; `align: start|center|end|stretch`;
243/// `justify: start|center|end|between|around|evenly`;
244/// `position: absolute|relative`; `weight: bold|semibold|medium|normal`;
245/// `width`/`height` (`full` or px), `size`, `min_width`, `min_height`,
246/// `padding`/`px`/`py`/`pt`/`pr`/`pb`/`pl`, `margin`/`mx`/`my`/`mt`/`mr`/`mb`/`ml`,
247/// `radius`, `gap`, `font_size`, `opacity`.
248#[macro_export]
249macro_rules! style {
250    ( $($decls:tt)* ) => {
251        |__guise_el| {
252            #[allow(unused_imports)]
253            use $crate::gpui::Styled as _;
254            $crate::__style!(@m __guise_el ; $($decls)*)
255        }
256    };
257}
258
259#[macro_export]
260#[doc(hidden)]
261macro_rules! __style {
262    (@m $el:expr ;) => { $el };
263
264    // --- color-valued (string literal → css; else any Into<Hsla> expr) ---
265    (@m $el:expr ; background : $v:literal ; $($r:tt)*) => {
266        $crate::__style!(@m $el.bg($crate::theme::css($v).expect("style!: invalid color")) ; $($r)*)
267    };
268    (@m $el:expr ; background : $v:expr ; $($r:tt)*) => {
269        $crate::__style!(@m $el.bg($v) ; $($r)*)
270    };
271    (@m $el:expr ; color : $v:literal ; $($r:tt)*) => {
272        $crate::__style!(@m $el.text_color($crate::theme::css($v).expect("style!: invalid color")) ; $($r)*)
273    };
274    (@m $el:expr ; color : $v:expr ; $($r:tt)*) => {
275        $crate::__style!(@m $el.text_color($v) ; $($r)*)
276    };
277    (@m $el:expr ; border : $v:literal ; $($r:tt)*) => {
278        $crate::__style!(@m $el.border_1().border_color($crate::theme::css($v).expect("style!: invalid color")) ; $($r)*)
279    };
280    (@m $el:expr ; border : $v:expr ; $($r:tt)*) => {
281        $crate::__style!(@m $el.border_1().border_color($v) ; $($r)*)
282    };
283
284    // --- keyword-valued (must precede the numeric arms for the same name) ---
285    (@m $el:expr ; display : flex ; $($r:tt)*) => { $crate::__style!(@m $el.flex() ; $($r)*) };
286
287    (@m $el:expr ; direction : row ; $($r:tt)*) => { $crate::__style!(@m $el.flex_row() ; $($r)*) };
288    (@m $el:expr ; direction : column ; $($r:tt)*) => { $crate::__style!(@m $el.flex_col() ; $($r)*) };
289    (@m $el:expr ; direction : col ; $($r:tt)*) => { $crate::__style!(@m $el.flex_col() ; $($r)*) };
290
291    (@m $el:expr ; align : start ; $($r:tt)*) => { $crate::__style!(@m $el.items_start() ; $($r)*) };
292    (@m $el:expr ; align : center ; $($r:tt)*) => { $crate::__style!(@m $el.items_center() ; $($r)*) };
293    (@m $el:expr ; align : end ; $($r:tt)*) => { $crate::__style!(@m $el.items_end() ; $($r)*) };
294    (@m $el:expr ; align : stretch ; $($r:tt)*) => { $crate::__style!(@m $el.items_stretch() ; $($r)*) };
295
296    (@m $el:expr ; justify : start ; $($r:tt)*) => { $crate::__style!(@m $el.justify_start() ; $($r)*) };
297    (@m $el:expr ; justify : center ; $($r:tt)*) => { $crate::__style!(@m $el.justify_center() ; $($r)*) };
298    (@m $el:expr ; justify : end ; $($r:tt)*) => { $crate::__style!(@m $el.justify_end() ; $($r)*) };
299    (@m $el:expr ; justify : between ; $($r:tt)*) => { $crate::__style!(@m $el.justify_between() ; $($r)*) };
300    (@m $el:expr ; justify : around ; $($r:tt)*) => { $crate::__style!(@m $el.justify_around() ; $($r)*) };
301    (@m $el:expr ; justify : evenly ; $($r:tt)*) => { $crate::__style!(@m $el.justify_evenly() ; $($r)*) };
302
303    (@m $el:expr ; position : absolute ; $($r:tt)*) => { $crate::__style!(@m $el.absolute() ; $($r)*) };
304    (@m $el:expr ; position : relative ; $($r:tt)*) => { $crate::__style!(@m $el.relative() ; $($r)*) };
305
306    (@m $el:expr ; weight : bold ; $($r:tt)*) => { $crate::__style!(@m $el.font_weight($crate::gpui::FontWeight::BOLD) ; $($r)*) };
307    (@m $el:expr ; weight : semibold ; $($r:tt)*) => { $crate::__style!(@m $el.font_weight($crate::gpui::FontWeight::SEMIBOLD) ; $($r)*) };
308    (@m $el:expr ; weight : medium ; $($r:tt)*) => { $crate::__style!(@m $el.font_weight($crate::gpui::FontWeight::MEDIUM) ; $($r)*) };
309    (@m $el:expr ; weight : normal ; $($r:tt)*) => { $crate::__style!(@m $el.font_weight($crate::gpui::FontWeight::NORMAL) ; $($r)*) };
310
311    (@m $el:expr ; width : full ; $($r:tt)*) => { $crate::__style!(@m $el.w_full() ; $($r)*) };
312    (@m $el:expr ; height : full ; $($r:tt)*) => { $crate::__style!(@m $el.h_full() ; $($r)*) };
313
314    // --- numeric (px) ---
315    (@m $el:expr ; width : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.w($crate::gpui::px($v as f32)) ; $($r)*) };
316    (@m $el:expr ; height : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.h($crate::gpui::px($v as f32)) ; $($r)*) };
317    (@m $el:expr ; size : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.w($crate::gpui::px($v as f32)).h($crate::gpui::px($v as f32)) ; $($r)*) };
318    (@m $el:expr ; min_width : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.min_w($crate::gpui::px($v as f32)) ; $($r)*) };
319    (@m $el:expr ; min_height : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.min_h($crate::gpui::px($v as f32)) ; $($r)*) };
320
321    (@m $el:expr ; padding : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.p($crate::gpui::px($v as f32)) ; $($r)*) };
322    (@m $el:expr ; px : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.px($crate::gpui::px($v as f32)) ; $($r)*) };
323    (@m $el:expr ; py : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.py($crate::gpui::px($v as f32)) ; $($r)*) };
324    (@m $el:expr ; pt : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.pt($crate::gpui::px($v as f32)) ; $($r)*) };
325    (@m $el:expr ; pr : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.pr($crate::gpui::px($v as f32)) ; $($r)*) };
326    (@m $el:expr ; pb : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.pb($crate::gpui::px($v as f32)) ; $($r)*) };
327    (@m $el:expr ; pl : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.pl($crate::gpui::px($v as f32)) ; $($r)*) };
328
329    (@m $el:expr ; margin : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.m($crate::gpui::px($v as f32)) ; $($r)*) };
330    (@m $el:expr ; mx : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.mx($crate::gpui::px($v as f32)) ; $($r)*) };
331    (@m $el:expr ; my : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.my($crate::gpui::px($v as f32)) ; $($r)*) };
332    (@m $el:expr ; mt : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.mt($crate::gpui::px($v as f32)) ; $($r)*) };
333    (@m $el:expr ; mr : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.mr($crate::gpui::px($v as f32)) ; $($r)*) };
334    (@m $el:expr ; mb : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.mb($crate::gpui::px($v as f32)) ; $($r)*) };
335    (@m $el:expr ; ml : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.ml($crate::gpui::px($v as f32)) ; $($r)*) };
336
337    (@m $el:expr ; radius : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.rounded($crate::gpui::px($v as f32)) ; $($r)*) };
338    (@m $el:expr ; gap : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.gap($crate::gpui::px($v as f32)) ; $($r)*) };
339    (@m $el:expr ; font_size : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.text_size($crate::gpui::px($v as f32)) ; $($r)*) };
340    (@m $el:expr ; opacity : $v:expr ; $($r:tt)*) => { $crate::__style!(@m $el.opacity($v as f32) ; $($r)*) };
341}
342
343#[cfg(test)]
344mod tests {
345    use crate::{Button, ColorName, Text, Variant};
346
347    /// Every macro must expand to a valid builder. Constructed (not rendered),
348    /// since `#[macro_export]` macros are only type-checked when invoked.
349    #[test]
350    fn macros_expand() {
351        let _ = row![Text::new("a"), Button::new("b", "B")];
352        let _ = col![Text::new("a")];
353        let _ = zstack![Text::new("a")];
354        let _ = wrap![Text::new("a")];
355        let _ = vstack![Text::new("a")];
356        let _ = hstack![Text::new("a")];
357        let _ = center![Text::new("a")];
358        let _ = paper![Text::new("a")];
359        let _ = card![Text::new("a")];
360        let _ = modal![Text::new("a")];
361        // Trailing comma and chaining still work.
362        let _ = card![Text::new("a"), Text::new("b"),].with_border(true);
363    }
364
365    #[test]
366    fn component_macros_expand() {
367        // Content macros: plain, and `format!`-style.
368        let name = "Ada";
369        let _ = text!("hello");
370        let _ = text!("hello {}", name);
371        let _ = title!("Heading").order(2);
372        let _ = code!("guise::Button");
373        let _ = kbd!("{}", "K");
374        // Forwarding macros, still chainable.
375        let _ = button!("save", "Save").variant(Variant::Filled);
376        let _ = badge!("New").color(ColorName::Blue);
377        let _ = Button::new("x", "y"); // sanity: still works directly
378    }
379
380    #[test]
381    fn color_macro_matches_constructors() {
382        use crate::theme::{css, hsl, rgb, rgba};
383        assert_eq!(color!(rgb(34, 139, 230)), rgb(34, 139, 230));
384        assert_eq!(color!(rgba(34, 139, 230, 0.5)), rgba(34, 139, 230, 0.5));
385        assert_eq!(color!(hsl(210, 80, 52)), hsl(210.0, 80.0, 52.0));
386        assert_eq!(color!(teal), css("teal").unwrap());
387        assert_eq!(color!("#228be6"), css("#228be6").unwrap());
388        // Usable anywhere a ColorValue is accepted.
389        let _ = Button::new("x", "y").color(color!(rgba(112, 72, 232, 1.0)));
390    }
391
392    #[test]
393    fn style_macro_builds() {
394        use crate::StyleExt;
395        let _ = gpui::div().apply(style! {
396            display: flex;
397            direction: column;
398            align: center;
399            justify: between;
400            gap: 8;
401            padding: 12;
402            width: full;
403            height: 200;
404            radius: 8;
405            background: "#11151c";
406            color: color!(rgb(230, 230, 230));
407            border: color!("#2a2f3a");
408            opacity: 0.95;
409            weight: semibold;
410            position: relative;
411        });
412    }
413}