GPUI-RSX
English | 简体中文
A Rust procedural macro that provides JSX-like syntax for GPUI, making UI development more concise and intuitive.
✨ Features
- 🎨 HTML-like Syntax - React JSX-like development experience
- 🚀 Zero Runtime Overhead - Expands to native GPUI code at compile time
- 📦 Lightweight - Only depends on
syn,quote,proc-macro2 - 🔧 Flexible - Supports expressions, conditional rendering, component composition
- 💡 Type Safe - Full compile-time type checking
- 🧩 Fragment Support - Return multiple root elements with
<>...</> - 🔁 For-loop Sugar - Iterate with
{for item in iter { ... }} - 🔑 Loop-safe IDs -
key={expr}generates unique IDs per iteration; compile error on missing key - 🎨 Full Tailwind Colors - 242 built-in colors + arbitrary hex/RGB/RGBA values
- 📐 Desktop Layout Utilities - Arbitrary lengths, percentages, and fraction sizing for panels and split views
- ⚡ Dynamic Class - Runtime class switching with colors, sizing, spacing, and numeric prefix fallback
- 🔍 Diagnostics & Preview - Strict/permissive macros, readable errors, and
rsx_expand!
📚 Documentation
- Documentation Site - Bilingual Astro/Starlight documentation
- Architecture Guide - Detailed architecture documentation
- Module organization and data flow
- Code generation strategies
- Design patterns and testing approach
- Extension points and debugging guide
- Getting Started - Step-by-step tutorial
- Syntax Reference - Elements, attributes, children, and conditionals
- API Reference - Macro and mapping reference
- Best Practices - Recommended patterns
- Migration Guide - Upgrade instructions
- Troubleshooting - Common issues and solutions
Run the documentation site locally:
📦 Installation
Add to your Cargo.toml:
[]
= { = "https://github.com/zed-industries/zed" }
= { = "https://github.com/zed-industries/zed", = ["font-kit", "runtime_shaders", "wayland", "x11"] }
= "0.6"
GPUI Version Target
gpui-rsx 0.6 upgrades its GPUI target from the crates.io gpui = "0.2.2" package to GPUI from the Zed repository. The git dependency still reports gpui v0.2.2, but its API surface differs from the crates.io release and includes the helper methods used by this version.
If you are upgrading from earlier gpui-rsx versions, replace:
= "0.2.2"
with the Zed git gpui and gpui_platform dependencies shown above, and update application startup to use gpui_platform::application().
For applications, commit Cargo.lock to pin the exact resolved Zed revision. If you also use gpui-component, keep your direct gpui and gpui_platform source identical to gpui-component's GPUI source; mixing a bare git dependency with rev = "..." creates duplicate GPUI crate instances and incompatible component types.
🚀 Quick Start
Get Started in 5 Minutes
use *;
use *;
use application;
use rsx;
Before & After
❌ Traditional GPUI (Verbose)
✅ With GPUI-RSX (Concise)
See the Quick Start example above.
Code Reduction: ~50% ✨
📖 Syntax Guide
1. Basic Elements
rsx!
Expands to:
div.child
2. Fragment (Multiple Root Elements)
When you need to return multiple elements without a wrapper:
rsx!
Expands to:
vec!
3. Attributes
Boolean Attributes (Flags)
rsx!
Expands to:
div.flex.flex_col
Value Attributes
rsx!
Expands to:
div.gap.bg
4. Class Attribute
The class attribute accepts a Tailwind-like string that expands into multiple GPUI method calls:
rsx!
Expands to:
div.flex.flex_col.gap.p
Note:
classaccepts both static strings (compiled at build time) and dynamic expressions (parsed at runtime). GPUI-RSX implements a Tailwind-like subset, not the full Tailwind CSS engine. Static classes expand directly to GPUI builder calls; dynamic class expressions use a small runtime matcher. See Dynamic Class for details.
Supported class patterns
Layout:
flex,flex-col,flex-row,flex-wrap,flex-1,flex-none,flex-autoflex-grow,flex-grow-0,flex-grow-1,flex-shrink,flex-shrink-0,flex-shrink-1min-w-0,min-h-0,items-center,items-start,items-end,items-stretchjustify-center,justify-between,justify-around,justify-evenly
Spacing (numeric values become px(n)):
gap-4→.gap(px(4.0))p-4,px-4,py-4,pt-4,pb-4,pl-4,pr-4m-4,mx-4,my-4,mt-4,mb-4,ml-4,mr-4- Arbitrary spacing:
gap-[14px],gap-x-[0.75rem],p-[18px],mx-[1.25rem] - Percent spacing such as
gap-[10%]intentionally errors because GPUI spacing uses definite lengths
Sizing:
- Numeric values keep project semantics:
w-64→.w(px(64.0)),h-32→.h(px(32.0)) w-full,h-full,size-full,aspect-squarew-px,h-px,w-auto,h-auto,w-1/2,h-1/3,size-1/2- Arbitrary sizing:
w-[280px],w-[18rem],w-[37.5%],min-w-[280px],max-w-[32rem] - Fraction sizing with arbitrary denominators:
w-6/24,min-w-1/3,size-3/4
Text:
text-xs,text-sm,text-base,text-lg,text-xltext-2xl,text-3xlfont-thin,font-extralight,font-light,font-normal,font-medium,font-semibold,font-bold,font-extrabold,font-blackwhitespace-normal,whitespace-nowrap,line-clamp-*text-ellipsis,truncate,no-underlinetext-decoration-solid,text-decoration-wavy,text-decoration-0/1/2/4/8
Alignment:
content-normal,content-center,content-start,content-end,content-between,content-around,content-evenly,content-stretchself-start,self-end,self-flex-start,self-flex-end,self-center,self-baseline,self-stretch
Border:
border→.border_1()border-2→.border_2(),border-4→.border_4()rounded-sm,rounded-md,rounded-lg,rounded-xl,rounded-2xl,rounded-3xl,rounded-full,rounded-none- Directional radius classes such as
rounded-t-lg,rounded-b-lg,rounded-r-lg,rounded-l-lg
Colors (full Tailwind palette):
text-red-500→.text_color(rgb(0xef4444))bg-blue-600→.bg(rgb(0x2563eb))border-green-500→.border_color(rgb(0x22c55e))- Arbitrary colors:
bg-[#ff0000],text-[#333],border-[#11223344],bg-[rgb(15,23,42)],text-[rgba(15,23,42,0.8)]
Effects:
shadow-none,shadow-2xs,shadow-xs,shadow-sm,shadow-md,shadow-lg,shadow-xl,shadow-2xloverflow-hidden,overflow-x-hidden,overflow-y-hidden,overflow-scrollcursor-pointer,cursor-default,cursor-text,cursor-move,cursor-grab,cursor-not-allowed, resize cursor variantsdebug-outlineenables GPUI debug borders in debug builds and is a no-op in release builds
Grid placement:
col-span-*,col-start-*,col-end-*,row-span-*,row-start-*,row-end-*col-span-full,col-start-auto,col-end-auto,row-span-full,row-start-auto,row-end-auto
Supported colors: 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) + white, black
5. Event Handling
rsx!
Supported events (camelCase / snake_case):
| Event | Method |
|---|---|
onClick / on_click |
.on_click(handler) |
onMouseDown / on_mouse_down |
.on_mouse_down(button, handler) |
onMouseUp / on_mouse_up |
.on_mouse_up(button, handler) |
onMouseMove / on_mouse_move |
.on_mouse_move(handler) |
onMouseDownOut / on_mouse_down_out |
.on_mouse_down_out(handler) |
onMouseUpOut / on_mouse_up_out |
.on_mouse_up_out(button, handler) |
onAnyMouseDown / on_any_mouse_down |
.on_any_mouse_down(handler) |
onAnyMouseUp / on_any_mouse_up |
.on_any_mouse_up(handler) |
onKeyDown / on_key_down |
.on_key_down(handler) |
onKeyUp / on_key_up |
.on_key_up(handler) |
onModifiersChanged / on_modifiers_changed |
.on_modifiers_changed(handler) |
onHover / on_hover |
.on_hover(handler) |
onScrollWheel / on_scroll_wheel |
.on_scroll_wheel(handler) |
onDrag / on_drag |
.on_drag(value, constructor) |
onDragMove / on_drag_move |
.on_drag_move(handler) |
onDrop / on_drop |
.on_drop(handler) |
onAction / on_action |
.on_action(handler) |
onBoxedAction / on_boxed_action |
.on_boxed_action(action, handler) |
captureAnyMouseDown / capture_any_mouse_down |
.capture_any_mouse_down(handler) |
captureAnyMouseUp / capture_any_mouse_up |
.capture_any_mouse_up(handler) |
captureKeyDown / capture_key_down |
.capture_key_down(handler) |
captureKeyUp / capture_key_up |
.capture_key_up(handler) |
captureAction / capture_action |
.capture_action(handler) |
Methods with multiple GPUI parameters use tuple syntax in RSX:
onMouseDown={(MouseButton::Left, handler)} and onDrag={(value, constructor)}.
6. Nested Elements
rsx!
7. Expressions
rsx!
8. List Rendering
Using iterators (traditional)
rsx!
Using for-loop syntax sugar
rsx!
Expands to:
div.children
Loop safety — key attribute
Elements with stateful attributes (onClick, onHover, onDrag, onAuxClick,
onA11yAction, active, activeClass, groupActive, tooltip, tooltipShowDelay,
focusable, role, ariaLabel, overflowScroll, trackScroll, scrollbarWidth,
or overflow-scroll) inside a for-loop must
provide id or key, otherwise the macro emits a compile error:
// ❌ compile error — all <li> would share the same auto ID
// ✅ key makes every ID unique per iteration
// → div().id(format!("src/list.rs::__rsx_li_L42C8_{}", item.id)).on_click(handler)…
key is consumed by the macro and does not become a .key() method call.
It accepts any type implementing Display. On elements without stateful attributes,
key is silently ignored (no .id() is injected).
For-loops also support ranges and method calls:
rsx!
9. Spread Syntax
rsx!
10. Dynamic Class
GPUI-RSX supports both static and dynamic class attributes.
Static Class (Compile-time — Recommended)
// ✅ Best performance - parsed at compile time, supports the documented subset
rsx!
Dynamic Class (Runtime)
let classes = if is_active else ;
rsx!
Supported at runtime: common layout/spacing/typography utilities, the full Tailwind color palette, arbitrary colors (e.g.
bg-[#ff0000],text-[#f00],bg-[rgba(15,23,42,0.8)]), arbitrary spacing and sizing lengths (w-[280px],h-[50%],gap-[14px],mx-[1.25rem]), fraction sizing (w-6/24), and numeric prefix fallback (e.g.gap-7,p-5,opacity-33). Truly unsupported classes (e.g. Tailwind variants or unknown utilities) are silently ignored in release builds and print a warning in debug builds.Recommended alternatives (in priority order):
- String literal (best):
class="flex gap-4"— compile-time, supports the documented subset- Conditional/match literal:
class={if active { "flex gap-4" } else { "block" }}orclass={match state { State::Active => "flex", _ => "block" }}— still compile-time expanded- Individual attributes:
<div flex gap_4 />— compile-time, type-checkedwhenattribute:when={(cond, |el| el.flex())}— compile-time, fully flexible- Dynamic expression:
class={expr}— runtime parser, narrower coverage than static literals
Common Patterns:
// ✅ Conditional literal (compile-time, documented subset)
let button_class = if primary else ;
// ✅ when attribute (compile-time, fully flexible)
rsx!
// ✅ Dynamic string with numeric prefix and arbitrary hex colors
let classes = format!; // gap-7, gap-32, etc. all work
11. Macro Modes and Expansion Preview
rsx! is permissive by default: unsupported static class names are ignored when they cannot be parsed safely, while invalid arbitrary values emit compile errors. Use rsx_strict! to reject unsupported static classes:
use ;
rsx_strict!
rsx_permissive!
let preview = rsx_expand! ;
assert!;
Strict dynamic classes panic when an unsupported runtime token is evaluated. rsx_expand! returns a string preview for debugging and does not type-check the generated GPUI expression.
Dynamic class capability summary:
| Capability | Static class="..." |
Dynamic class={expr} |
|---|---|---|
| Layout, spacing, sizing | Supported | Supported subset |
| Colors and opacity | Supported | Supported |
| Arbitrary lengths/colors | Supported | Supported |
| Fraction sizing | Supported | Supported |
| Stateful scroll classes | Supported with auto ID | Not supported |
| Unknown Tailwind variants | Ignored in permissive, error in strict | Ignored in permissive, panic in strict |
12. Desktop Three-Column Layout
rsx!
min-w-0 is important in desktop split layouts because it allows the center pane to shrink instead of pushing fixed sidebars out of the window.
13. Attribute Mapping Reference
Most camelCase attributes map to GPUI snake_case methods; special flag behavior is listed below:
| RSX Attribute | Generated GPUI code |
|---|---|
opacity |
.opacity() |
visible / invisible |
.visible() / .invisible() |
width / height |
.w() / .h() |
minWidth / maxWidth |
.min_w() / .max_w() |
minHeight / maxHeight |
.min_h() / .max_h() |
gapX / gapY |
.gap_x() / .gap_y() |
flexBasis |
.flex_basis() |
flexGrow / flexShrink (flags) |
.flex_grow_1() / .flex_shrink_1() |
fontSize |
.text_size() |
lineHeight |
.line_height() |
fontWeight |
.font_weight() |
fontFamily |
.font_family() |
textAlign |
.text_align() |
textColor |
.text_color() |
backgroundColor |
.bg() |
borderColor |
.border_color() |
borderTop / borderBottom |
.border_t(value) / .border_b(value) |
borderLeft / borderRight |
.border_l(value) / .border_r(value) |
border_t / border_b / border_l / border_r (flags) |
.border_t_1() / .border_b_1() / .border_l_1() / .border_r_1() |
roundedTop / roundedBottom |
.rounded_t() / .rounded_b() |
roundedTopLeft / roundedTopRight |
.rounded_tl() / .rounded_tr() |
roundedBottomLeft / roundedBottomRight |
.rounded_bl() / .rounded_br() |
boxShadow |
.shadow() |
inset |
.inset() |
Attributes not in this table are passed through as-is (e.g., bg={color} → .bg(color)).
14. Conditional Styling with when and whenSome
when - Apply styles based on condition
rsx!
whenSome - Apply styles when Option has value
let custom_width: = Some;
rsx!
whenClass - Apply static classes based on condition
rsx!
whenClass only accepts string literals. Stateful classes such as overflow-scroll are rejected; use when={(cond, |el| el.overflow_scroll())} when ID-sensitive GPUI methods are needed.
State class attributes - hover/focus/active
Use hoverClass, focusClass, and activeClass when the state styling can be expressed as static classes:
rsx!
These attributes compile to GPUI StyleRefinement closures. They only accept string literals, and classes that need an element-level stateful method, such as overflow-scroll or debug-outline, are rejected. activeClass triggers auto ID injection; hoverClass and focusClass do not.
groupHover follows GPUI's non-stateful style-refinement path. groupActive requires a stateful
ID. groupDragOver is intentionally rejected as an attribute because GPUI requires an explicit drag
data type; use when or base and call group_drag_over::<YourType>(...) directly.
Multiple conditions
rsx!
15. Styled Flag (Default Tag Styles)
The styled flag injects sensible default styles based on the tag name:
rsx!
Default styles per tag:
| Tag | Default Styles |
|---|---|
h1 |
text-3xl font-bold |
h2 |
text-2xl font-bold |
h3 |
text-xl font-bold |
h4 |
text-lg font-bold |
h5 |
text-base font-bold |
h6 |
text-sm font-bold |
button, a |
cursor-pointer |
input, textarea |
px-2 py-1 |
ul, ol |
flex flex-col |
li |
flex items-center |
p |
text-base |
label |
text-sm |
form |
flex flex-col gap-4 |
User attributes are applied after defaults and can override them.
🎯 Complete Example
Todo App
use *;
use *;
use rsx;
🔧 Advanced Usage
Custom Components
Builder-backed Components
Use base={expr} when a component needs a custom constructor instead of the default <Tag /> to Tag() expansion:
rsx!
This expands to Button::new("continue").label("Continue").small().primary(). The base attribute is consumed by the macro and does not generate .base(...).
Path-qualified component tags are supported, which is useful when components live in modules:
rsx!
For gpui-component, keep imports explicit and use base for constructors that need IDs or custom arguments:
use ;
use Sizable as _;
rsx!
Conditional Rendering
rsx!
Dynamic Styling
📊 Performance
GPUI-RSX is a compile-time macro that expands static RSX into direct GPUI builder calls. Static markup has no parser at runtime; dynamic class={expr} intentionally uses a small runtime matcher.
| Metric | Traditional GPUI | GPUI-RSX |
|---|---|---|
| Code Size | 100 lines | 50 lines (-50%) |
| Runtime Performance | Baseline | Same |
| Type Safety | ✅ | ✅ |
| Compile-time Checking | ✅ | ✅ |
v0.3.2 Fixes & Improvements
- Fixed
parse_single_classpanic on Tailwind variant syntax (hover:bg-blue-500): invalid class names are now silently skipped instead of callingsyn::Ident::newwith illegal characters - Added 7 classes to dynamic match table:
rounded-none,rounded-xl,cursor-default,cursor-text,shadow-sm,shadow-md,shadow-lg - Docs styled defaults: added
li,p,label,formentries; fixedoverflowX/overflowYmethod names; removed unsupportedtext-4xl/text-5xl; updated dynamic class description
v0.3.1 Fixes & Features
- Fixed
is_stateful_attr:hover/active/focus/groupareStyledtrait methods and no longer trigger unnecessary.id()injection - Added
key={expr}attribute: composite auto ID for stateful elements in for-loops - Added compile error when a stateful element in a for-loop has no
idorkey keyon non-stateful elements is silently ignored (no unintended type change toStateful<Div>)
v0.3.0 Refactoring
- Eliminated ~60 duplicate method definitions in
tests/common/mod.rs(823 → 456 lines) - Simplified black/white color entry generation in
runtime.rs(method names encoded in data) - Extracted
is_directional_border()helper inclass.rsfor clearer border logic
v0.2.2 Optimizations
Compile-time Performance:
split_ascii_whitespacereplacessplit_whitespacein class parsing- Unified
text_prefix handling (singlestrip_prefixcall) - Early fast-path for empty elements
Vec::with_capacity(attrs * 2 + children)for class-heavy elements
Runtime Performance:
.children([...])batching threshold lowered 3 → 2
Binary Size:
- Applications may opt into
panic = "abort"in their own release profile to remove unwind tables
v0.2.1 Optimizations
Compile-time Performance:
- O(1) color / attribute / spacing lookups via
match(jump table, no linear scan) - Single-pass attribute scanning in
generate_element - Thread-local cache for dynamic class match arms (generated once per process)
Memory Allocation Reductions:
parse_class_stringreturns an iterator (no intermediateVec)generate_attr_methodspushes directly into caller's bufferCow<str>for class name transformations (zero-copy when no-present)Vec::with_capacitypre-allocation throughout
Runtime Performance:
- Zero-copy dynamic class strings via
AsRef<str>(&strneeds no allocation)
Binary Size:
- Dynamic class match table extracted to
#[inline(never)]+ LLVM ICF deduplication - Multiple
class={expr}in same component share one function body
🛠️ Development
Build
Test
Expand Macros (Debugging)
# Install cargo-expand
# View expanded code
💡 Best Practices
1. Component Splitting
Break complex UIs into small, reusable components:
// ✅ Recommended: Split into multiple methods
2. Use Constants
Extract repeated styles as constants:
const PRIMARY_BG: Rgb = rgb;
const PRIMARY_TEXT: Rgb = rgb;
rsx!
3. Avoid Over-nesting
// ❌ Not recommended: Over-nested
rsx!
// ✅ Recommended: Flatten structure
rsx!
🐛 FAQ
Q1: How to use variables in RSX?
let title = "Hello";
rsx!
Q2: How to handle Option types?
rsx!
Q3: What does the expanded macro code look like?
Use rsx_expand! for a local string preview, or cargo expand to inspect the full crate:
let preview = rsx_expand! ;
Q4: Which elements are supported?
All GPUI-supported elements can be used, such as div, button, input, span, etc.
Q5: Can I use dynamic class values?
Yes, but with an important limitation:
// ✅ Static literal (compile-time, documented subset — recommended)
rsx!
// ✅ Individual attributes (compile-time, type-checked)
rsx!
// ✅ Conditional styling with `when` (compile-time, fully flexible)
rsx!
// ✅ Dynamic expression with numeric prefix, arbitrary lengths, and arbitrary colors
let classes = if active else ;
rsx!
// Tailwind variants and unknown utilities are ignored in permissive mode.
Tip: When you need dynamic styling, prefer when/whenSome or individual value
attributes (bg={color}) — they are compile-time and support everything GPUI offers.
Q6: How do I mix different element types in a Fragment?
rsx! fragments return Vec<impl IntoElement>, so all root items need the same concrete type. Prefer wrapping mixed children in a parent element:
rsx!
If you really need a Fragment, erase the mixed items explicitly:
rsx!
🤝 Contributing
Contributions are welcome! Feel free to submit Issues or Pull Requests.
Development Workflow
- Fork the project
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit changes:
git commit -m 'Add amazing feature' - Push branch:
git push origin feature/amazing-feature - Submit a Pull Request
Code Standards
- Use
rustfmtto format code - Use
clippyto check code quality - Add tests for new features
- Update documentation
📝 License
MIT License
🙏 Acknowledgments
Inspired by:
- Dioxus RSX - RSX syntax design
- Yew html! macro - html! macro
- React JSX - JSX syntax
- GPUI - Underlying UI framework
Make GPUI development more enjoyable! 🎉