Macros
Terse builders for the common containers, a CSS-like block for styling, and a
matching pair for motion. They're all in the prelude, so
use guise::prelude::*; is all you need — the macros bring .child() into scope
themselves (no extra trait import).
Each container macro takes comma-separated children; a trailing comma is fine.
Containers
One macro per container component — every type that takes a variadic list of children.
| Macro | Builds | Spacing |
|---|---|---|
row![ … ] |
flex::Row |
none (use SizedBox/Spacer) |
col![ … ] |
flex::Column |
none |
zstack![ … ] |
flex::Stack (overlap) |
— |
wrap![ … ] |
flex::Wrap |
default spacing |
vstack![ … ] |
layout::Stack (themed) |
token gap |
hstack![ … ] |
layout::Group (themed) |
token gap |
center![ … ] |
layout::Center |
— |
paper![ … ] |
Paper |
— |
card![ … ] |
Card |
— |
modal![ … ] |
Modal |
— |
use guise::prelude::*;
col![
row![avatar, name, Spacer::new(), actions],
SizedBox::height(8.0),
body,
]
Because a macro returns the underlying builder, you can keep chaining:
row![left, right].main_axis_alignment(MainAxisAlignment::SpaceBetween)
Component shorthands
A few of the most common leaf components have shorthand macros too. They expand
to Type::new(...), so every builder method still chains.
| Macro | Builds | Notes |
|---|---|---|
text!(...) |
Text |
accepts format! args |
title!(...) |
Title |
accepts format! args |
code!(...) |
Code |
accepts format! args |
kbd!(...) |
Kbd |
accepts format! args |
button!(id, label) |
Button |
forwards args |
badge!(label) |
Badge |
forwards args |
There are two more that aren't components at all — motion! and sequence!,
below — plus color! and style!.
The content macros take format!-style arguments, which is the real win over
the plain constructor:
text!("Signed in as {name}") // = Text::new(format!("Signed in as {name}"))
title!("Page {}", n).order(2)
button!("save", "Save").variant(Variant::Filled).color(ColorName::Blue)
This is a deliberately small set. Most components don't get a macro: for a
builder with several setters, Type::new(...) chained with methods is already
the clearest form, and stateful entities (TextInput, Select, …) are created
with cx.new(...) where a macro doesn't fit. The shorthands exist only where
they genuinely read better.
color! — CSS color literals
color! produces a gpui Hsla from CSS notation. See
Theming → CSS-style colors.
color!(rgb(34, 139, 230)) color!(rgba(34, 139, 230, 0.5))
color!(hsl(210, 80, 52)) color!(teal) color!("#228be6")
style! — CSS-like style blocks
style! expands to an element transform you apply with .apply(...) (from the
StyleExt trait, in the prelude). It maps CSS-ish properties onto gpui's builder
methods, so a block of declarations reads like a stylesheet.
use guise::prelude::*;
gpui::div().apply(style! {
display: flex;
direction: column;
align: center;
justify: between;
gap: 8;
padding: 16;
width: full;
height: 200;
radius: 12;
background: "#11151c"; // string → css() shorthand
color: color!(rgb(230, 230, 230)); // or any color! / Hsla expr
border: color!("#2a2f3a"); // 1px border of this color
weight: semibold;
opacity: 0.95;
})
- Numbers are pixels.
padding: 16→.p(px(16.)). - Colors are a string literal (parsed by
css) or anyInto<Hsla>expression (e.g.color!(..)). - Every declaration ends with
;. - No theme tokens.
style!is pure and has nocx, soSize::Md-based spacing/radius/font aren't available — use raw px here, or the builder methods (which read the theme) for token values.
Supported properties: background, color, border; display: flex;
direction: row|column|col; align: start|center|end|stretch;
justify: start|center|end|between|around|evenly; position: absolute|relative;
weight: bold|semibold|medium|normal; width/height (full or px), size,
min_width, min_height, padding/px/py/pt/pr/pb/pl,
margin/mx/my/mt/mr/mb/ml, radius, gap, font_size, opacity.
Because it's just a transform, it composes with everything: keep chaining
interactive methods (.id(..), .on_click(..), .hover(..)) after .apply(..).
motion! — animation as a declaration block
What style! is to a box, motion! is to an animation: timing and tweens as
one block instead of a chain of setters. See
Motion & transitions for what the pieces mean, and the
motion tutorial for building one up.
use guise::prelude::*;
div().child(card).animate("card", motion! {
duration: 420;
ease: out back;
opacity: 0 => 1;
y: 12 => 0;
})
A track is prop: from => to. Numbers are px (or degrees for rotate, or a
multiplier for scale); colours are any Into<Hsla>, so color!(..) and a
theme read both work. A list on the right is a multi-leg path:
motion! {
duration: 900;
ease: in_out quad;
y: 0 => [-30, 0]; // two legs, splitting the duration
bg: soft => [
Keyframe::to(accent).duration(500.0), // or legs with their own timing
Keyframe::to(soft),
];
}
Timing and repetition
| Declaration | Means |
|---|---|
duration: 420; |
ms each track gets, before per-leg overrides |
delay: 80; |
ms of stillness first — tracks hold their starting value |
end_delay: 120; |
ms of stillness after, inside the loop |
ease: out back; |
see below |
repeat: forever; |
or once, or a count: repeat: 3; |
alternate; |
bare flag — every other pass runs backwards |
reversed; |
bare flag — the whole thing runs backwards |
margins; |
bare flag — x/y become margins, for an absolute() element |
Easing is a direction and a shape, plus three words of its own:
ease: linear; ease: spring; ease: steps(4);
ease: in quad; ease: out elastic; ease: in_out sine;
ease: Easing::CubicBezier(0.25, 0.1, 0.25, 1.0); // any Easing expression
Shapes: quad, cubic, quart, quint, sine, expo, circ, back,
elastic, bounce.
Presets pick the constructor, so they come first if you use one:
motion! { enter: slide_up; duration: 300; delay: 80; }
motion! { enter: slide_left 24; } // with the travel spelled out
motion! { exit: fade; }
fade, slide_up, slide_down, slide_left, slide_right.
Props: opacity, x, y, w/width, h/height, mt/mr/mb/ml,
pt/pr/pb/pl, radius, border_width, gap, font_size,
bg/background, border_color, color, rotate, scale.
The block returns the builder, so anything it doesn't cover still chains:
motion! { duration: 200; opacity: 0 => 1; }.repeat(3)
sequence! — motions on one clock
The variadic one. sequence! is to motions what col! is to children:
sequence![
fade_in,
rel(-120) => slide_up, // 120ms before the end so far
with(0) => tint, // alongside the previous entry
abs(600) => flash, // from the sequence's own start
label("settled", 50) => ripple, // 50ms after a placed label
]
A bare entry lands after everything before it. The position goes in front
because a Rust macro can't read anything but ,, ; or => after an
expression — and reading "at rel(-120), slide up" turns out to be the right way
round anyway.
Labels are placed with the builder (Sequence::label), so a sequence that
refers to one starts there and the macro fills in the rest.
Why col!, not column!
The standard library already exports a column! macro (it returns the current
source column number). Naming ours col! avoids the clash when both are in
scope via globs.
How they stay import-free
The macros expand to e.g. flex::Row::new().child(a).child(b). .child() comes
from gpui's ParentElement trait, which the macro brings into scope anonymously
through a hidden re-export (guise::__ParentElement). You never have to import
the trait yourself.