Skip to main content

Crate gpui_rsx

Crate gpui_rsx 

Source
Expand description

§gpui-rsx

A procedural macro that brings JSX-like syntax to GPUI, making UI code more concise and readable while generating zero-overhead native GPUI method chains at compile time.

§At a Glance

use gpui::*;
use gpui_rsx::rsx;

// Before — verbose GPUI method chain
div()
    .flex()
    .flex_col()
    .gap(px(16.0))
    .p(px(16.0))
    .bg(rgb(0x3b82f6))
    .child(div().text_xl().font_bold().child("Hello GPUI"))
    .child(
        div()
            .cursor_pointer()
            .id("btn")
            .on_click(cx.listener(|_, _, cx| cx.notify()))
            .child("Click me"),
    )

// After — concise RSX (~50% less code)
rsx! {
    <div class="flex flex-col gap-4 p-4 bg-blue-500">
        <h1 class="text-xl font-bold">{"Hello GPUI"}</h1>
        <button
            cursor_pointer
            onClick={cx.listener(|_, _, cx| cx.notify())}
        >
            {"Click me"}
        </button>
    </div>
}

The two snippets produce identical compiled output.

§Features

FeatureDescription
HTML-like tags<div>, <span>, <button>, … → all map to div()
Boolean attributes<div flex flex_col />.flex().flex_col()
Value attributes<div gap={px(4.0)} />.gap(px(4.0))
class (static)Tailwind-like string, parsed at compile time
class (dynamic)Runtime expression, full Tailwind palette + common utilities
Full color palette242 Tailwind colors + arbitrary hex (bg-[#ff0000])
Fragments<>...</> — returns vec![...]
For-loop sugar{for item in iter { ... }}
Spread{...iterator}
Conditionalwhen / whenSome attributes
Styled defaults<h1 styled> injects sensible tag defaults
camelCase mappingonClick.on_click(), zIndex.z_index()
key (loop ID)Composite auto-ID for stateful elements in loops

§Syntax Reference

§Basic Elements

Any HTML tag maps to div(). Self-closing and pair tags are both valid:

rsx! { <div /> }                    // div()
rsx! { <span></span> }              // div()
rsx! { <button>{"OK"}</button> }    // div().child("OK")

§Fragment

Return multiple root elements without a wrapper. Expands to vec![...]:

rsx! {
    <>
        <div>{"First"}</div>
        <div>{"Second"}</div>
    </>
}
// → vec![div().child("First"), div().child("Second")]

§Boolean Attributes (Flags)

Bare attribute names become zero-argument method calls:

rsx! { <div flex flex_col items_center /> }
// → div().flex().flex_col().items_center()

§Value Attributes

name={expr} passes the expression as the method argument:

rsx! { <div gap={px(16.0)} bg={rgb(0x3b82f6)} opacity={0.8} /> }
// → div().gap(px(16.0)).bg(rgb(0x3b82f6)).opacity(0.8)

A Tailwind-inspired string that expands to method calls at compile time. All Tailwind utilities are supported; there is no runtime cost:

rsx! { <div class="flex flex-col gap-4 p-4 bg-blue-500 text-white rounded-md" /> }
// → div().flex().flex_col().gap(px(4.0)).p(px(4.0)).bg(rgb(0x3b82f6))
//         .text_color(rgb(0xffffff)).rounded_md()
§Supported class patterns

Layout: flex, flex-col, flex-row, flex-1, flex-wrap, flex-none, block, grid, hidden, absolute, relative

Alignment: items-center, items-start, items-end, justify-center, justify-between, justify-start, justify-end, justify-around, content-center, content-between, …

Spacing (numeric value → px(n.0)): gap-4.gap(px(4.0)), p-4, px-4, py-4, pt-4, pb-4, pl-4, pr-4, m-4, mx-4, my-4, mt-4, mb-4, fractional p-0.5.p(px(0.5))

Sizing: w-full, h-full, size-full, w-64, h-32

Text: text-xs, text-sm, text-base, text-lg, text-xl, text-2xl, text-3xl, font-bold, italic, underline, truncate, text-left, text-center

Border: border.border_1(), border-2, rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-full, rounded-none

Colors (full Tailwind palette): text-red-500.text_color(rgb(0xef4444)), bg-blue-600.bg(rgb(0x2563eb)), border-green-500.border_color(rgb(0x22c55e))

Supported families: slate, gray, zinc, neutral, stone, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose (shades 50–950) + black, white

Arbitrary hex (6-digit and 3-digit): bg-[#ff0000].bg(rgb(0xff0000)), text-[#f00].text_color(rgb(0xff0000))

§The class Attribute — Dynamic (Runtime)

When class receives an expression, a runtime match is generated. Supported classes: all Tailwind colors (22 families × 11 shades × 3 prefixes = 726+ entries), common layout/spacing/typography utilities, and arbitrary numeric values for spacing/sizing/opacity/z-index via prefix fallback. Truly unsupported classes (e.g. arbitrary hex colors, unknown utilities) are silently ignored in release and print a warning in debug builds.

let active = true;
rsx! { <div class={if active { "flex gap-4" } else { "block" }} /> }

Prefer static strings or the when attribute instead:

// ✅ static literal — compile-time, all classes work
rsx! { <div class="flex gap-4" /> }

// ✅ conditional literal — still static
let cls = if active { "flex gap-4" } else { "block" };
rsx! { <div class={cls} /> }

// ✅ when attribute — compile-time, fully flexible
rsx! { <div when={(active, |el| el.flex().gap(px(4.0)))} /> }

// ⚠️ dynamic expression — runtime, arbitrary hex colors not supported
rsx! { <div class={format!("gap-{}", spacing)} /> }   // works: numeric prefix fallback

§Event Handling

Event attributes (camelCase or snake_case) are mapped to GPUI listeners. Elements with event handlers automatically receive a deterministic .id().

rsx! {
    <button onClick={cx.listener(|view, _, cx| {
        view.count += 1;
        cx.notify();
    })}>
        {"Increment"}
    </button>
}
// → div().id("src/main.rs::__rsx_button_L42C8").on_click(cx.listener(...)).child("Increment")
§key — Unique IDs for stateful elements in loops

key={expr} is a macro-level attribute consumed at compile time; it never becomes a .key() method call on the GPUI element.

key only takes effect when the element already needs an .id() (i.e. it carries an event handler, tooltip, track_focus, or another stateful attribute). On elements without any stateful attributes, key is silently ignored and no .id() is injected — the element stays a plain Div.

SituationResult
stateful attrs + keycomposite ID: auto-prefix + key (runtime)
stateful attrs, no keypure source-location auto ID (compile-time)
no stateful attrs + keykey ignored, no .id() injected
explicit idalways used, key has no effect

Loop safety: Inside a {for ...} loop, every iteration shares the same source location. The macro emits a compile error when a stateful element is found inside a loop without an explicit id or key:

// ❌ compile error — all <li> would share the same auto ID
{for item in &self.items { <li onClick={handler}>{item}</li> }}

// ✅ key produces a unique ID per iteration
rsx! {
    <ul>
        {for item in &self.items {
            <li key={item.id} onClick={handler}>{item.name.clone()}</li>
        }}
    </ul>
}
// → div().id(format!("src/main.rs::__rsx_li_L42C8_{}", item.id)).on_click(handler)…
//   e.g. "src/main.rs::__rsx_li_L42C8_1", "src/main.rs::__rsx_li_L42C8_2", …

key accepts any type that implements Display (integers, &str, UUIDs, …). For a fully stable custom ID that survives refactors, use the id attribute instead.

AttributeGPUI method
onClick / on_click.on_click(h)
onMouseDown / on_mouse_down.on_mouse_down(h)
onMouseUp / on_mouse_up.on_mouse_up(h)
onMouseMove / on_mouse_move.on_mouse_move(h)
onKeyDown / on_key_down.on_key_down(h)
onKeyUp / on_key_up.on_key_up(h)
onFocus / on_focus.on_focus(h)
onBlur / on_blur.on_blur(h)
onHover / on_hover.on_hover(h)
onScrollWheel / on_scroll_wheel.on_scroll_wheel(h)
onDrag / on_drag.on_drag(h)
onDrop / on_drop.on_drop(h)
onAction / on_action.on_action(h)

§Expressions and Children

rsx! {
    <div>
        {format!("Count: {}", self.count)}   // any expression
        {self.render_child()}                 // method returning IntoElement
        {if self.show {
            rsx! { <span>{"Visible"}</span> }
        } else {
            rsx! { <span>{"Hidden"}</span> }
        }}
    </div>
}

Two or more consecutive expressions are batched into a single .children([...]) call (stack-allocated array, zero heap allocation):

rsx! { <div>{"a"}{"b"}{"c"}</div> }
// → div().children(["a", "b", "c"])

§For-loop Syntax Sugar

rsx! {
    <ul>
        {for item in &self.items {
            <li>{item.name.clone()}</li>
        }}
    </ul>
}
// → div().children((&self.items).into_iter().map(|item| {
//       div().child(item.name.clone())
//   }))

§Spread Syntax

rsx! {
    <div>
        {...self.items.iter().map(|i| rsx! { <span>{i}</span> })}
    </div>
}

§Conditional Attributes: when and whenSome

Apply style transformations based on a runtime condition without leaving compile-time safety:

rsx! {
    <button
        class="px-4 py-2 rounded-md"
        when={(is_selected, |el| el.bg(rgb(0x3b82f6)).text_color(rgb(0xffffff)))}
        when={(is_disabled, |el| el.opacity(0.5))}
        whenSome={(custom_color, |el, c| el.bg(rgb(c)))}
    >
        {"Button"}
    </button>
}

§The styled Flag — Semantic Tag Defaults

Adding styled injects sensible default classes for the tag name, applied before any user-provided attributes:

rsx! { <h1 styled>{"Title"}</h1> }
// → div().text_3xl().font_bold().child("Title")

rsx! { <button styled onClick={handler}>{"OK"}</button> }
// → div().cursor_pointer().id("…").on_click(handler).child("OK")
TagDefault classes
h1text-3xl font-bold
h2text-2xl font-bold
h3text-xl font-bold
h4text-lg font-bold
h5text-base font-bold
h6text-sm font-bold
button, acursor-pointer
input, textareapx-2 py-1
ul, olflex flex-col
liflex items-center
ptext-base
labeltext-sm
formflex flex-col gap-4

§Attribute Mapping Reference

camelCase names are automatically converted to snake_case GPUI methods. Attributes not in this table are passed through unchanged (e.g., bg={color}.bg(color)).

RSX attributeGPUI method
zIndex.z_index()
opacity.opacity()
invisible (flag).visible(false)
width / height.w() / .h()
minWidth / maxWidth.min_w() / .max_w()
minHeight / maxHeight.min_h() / .max_h()
gapX / gapY.gap_x() / .gap_y()
flexBasis.basis()
flexGrow / flexShrink.flex_grow() / .flex_shrink()
flexOrder.order()
fontSize.font_size()
lineHeight.line_height()
fontWeight.font_weight()
textAlign.text_align()
textDecoration.text_decoration()
borderRadius.border_radius()
borderTop / borderBottom.border_t() / .border_b()
borderLeft / borderRight.border_l() / .border_r()
roundedTop / roundedBottom.rounded_t() / .rounded_b()
roundedTopLeft / roundedTopRight.rounded_tl() / .rounded_tr()
roundedBottomLeft / roundedBottomRight.rounded_bl() / .rounded_br()
boxShadow.shadow()
overflowX / overflowY.overflow_x() / .overflow_y()
inset.inset()

§Auto ID Injection

Elements that require a stateful identity (event handlers, tooltip, track_focus) automatically receive a deterministic .id() derived from the element’s source location. The ID is chosen by the following priority:

  1. Explicit id — always wins, key is ignored.
  2. key present (and element is stateful) — composite ID at runtime: format!(concat!(file!(), "::{prefix}_{}"), key_expr)
  3. No key (and element is stateful) — pure compile-time source-location ID: concat!(file!(), "::", "__rsx_{tag}_L{line}C{col}")
  4. Not stateful — no .id() injected; key is silently ignored.
// Format (no key): concat!(file!(), "::", "__rsx_{tag}_L{line}C{col}")
// Example:         "src/views/counter.rs::__rsx_button_L42C8"
//
// Format (key):    format!(concat!(file!(), "::__rsx_{tag}_L{line}C{col}_{}"), key)
// Example:         "src/views/list.rs::__rsx_li_L55C12_42"

The source-location ID is stable across incremental rebuilds as long as the element’s position in the file does not change. For IDs that must survive refactors, use the explicit id attribute.

Note on style attributes: hover, active, focus, and group are Styled trait methods and do not trigger auto ID injection. Only StatefulInteractiveElement / FocusableElement attributes (event handlers, tooltip, track_focus) require an ID.

§Loop safety

Elements inside {for ...} loops share the same source location and would receive identical auto IDs across iterations. A compile error is emitted in this case. Add key={expr} (any Display type) to produce a unique ID per iteration:

// ❌ compile error — all <li> would share the same auto ID
{for item in &self.items { <li onClick={handler}>{item}</li> }}

// ✅ key produces a unique ID per iteration
{for item in &self.items { <li key={item.id} onClick={handler}>{item}</li> }}

§Performance

  • Compile-time — All class parsing, color lookup, and attribute mapping happen during macro expansion. The generated code is identical to hand-written GPUI.
  • O(1) lookups — Colors, attributes, and spacing prefixes use match statements that the compiler turns into jump tables.
  • Zero allocation — Static classes generate no heap allocation. Dynamic classes use AsRef<str> (zero-copy for &str inputs).
  • Binary size — Dynamic class helpers use #[inline(never)] + LLVM ICF to avoid code bloat when multiple class={expr} appear in the same component.

§Further Reading

Macros§

rsx
Transforms JSX-like markup into GPUI method chains at compile time.