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 values
- ⚡ Dynamic Class - Runtime class switching: full Tailwind palette + numeric prefix fallback
📚 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
- API Reference - Complete API documentation
- Best Practices - Recommended patterns
- Migration Guide - Upgrade instructions
- Troubleshooting - Common issues and solutions
📦 Installation
Add to your Cargo.toml:
[]
= "0.1"
= "0.3"
🚀 Quick Start
Get Started in 5 Minutes
use *;
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). Static classes have zero runtime overhead. See Dynamic Class for details.
Supported class patterns
Layout:
flex,flex-col,flex-row,flex-wrap,flex-1,flex-none,flex-autoitems-center,items-start,items-endjustify-center,justify-between
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-4w-64,h-32- Fractional values:
p-0.5→.p(px(0.5))
Sizing:
w-full,h-full,size-full
Text:
text-xs,text-sm,text-base,text-lg,text-xltext-2xl,text-3xlfont-bold
Border:
border→.border_1()border-2→.border_2(),border-4→.border_4()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))- Arbitrary hex:
bg-[#ff0000],text-[#333],border-[#abc]
Effects:
shadow-sm,shadow-md,shadow-lgoverflow-hidden,overflow-scrollcursor-pointer,cursor-default,cursor-text
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(handler) |
onMouseUp / on_mouse_up |
.on_mouse_up(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(handler) |
onKeyDown / on_key_down |
.on_key_down(handler) |
onKeyUp / on_key_up |
.on_key_up(handler) |
onFocus / on_focus |
.on_focus(handler) |
onBlur / on_blur |
.on_blur(handler) |
onHover / on_hover |
.on_hover(handler) |
onScrollWheel / on_scroll_wheel |
.on_scroll_wheel(handler) |
onDrag / on_drag |
.on_drag(handler) |
onDrop / on_drop |
.on_drop(handler) |
onAction / on_action |
.on_action(handler) |
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
Stateful elements (with event handlers, tooltip, track_focus) 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 all classes
rsx!
Dynamic Class (Runtime)
let classes = if is_active else ;
rsx!
Supported at runtime: full Tailwind color palette (22 families × 11 shades × 3 prefixes = 726+ color entries), common layout/spacing/typography utilities, and arbitrary numeric values for spacing/sizing/opacity/z-index via prefix fallback (e.g.
gap-7,p-5,opacity-33,z-30). Truly unsupported classes (e.g. arbitrary hex colors) 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 all classes- Conditional literal:
class={if active { "flex gap-4" } else { "block" }}— still a literal- Individual attributes:
<div flex gap_4 />— compile-time, type-checkedwhenattribute:when={(cond, |el| el.flex())}— compile-time, fully flexible- Dynamic expression:
class={expr}— runtime, arbitrary hex colors not supported
Common Patterns:
// ✅ Conditional literal (compile-time, all classes work)
let button_class = if primary else ;
// ✅ when attribute (compile-time, fully flexible)
rsx!
// ✅ Dynamic string with numeric prefix (works: any gap-N, p-N, opacity-N, z-N)
let classes = format!; // gap-7, gap-32, etc. all work
11. Attribute Mapping Reference
camelCase attributes are automatically mapped to GPUI snake_case methods:
| RSX Attribute | GPUI Method |
|---|---|
zIndex |
.z_index() |
opacity |
.opacity() |
visible |
.visible() |
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() |
Attributes not in this table are passed through as-is (e.g., bg={color} → .bg(color)).
11. 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!
Multiple conditions
rsx!
12. 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 rsx;
🔧 Advanced Usage
Custom Components
Conditional Rendering
rsx!
Dynamic Styling
📊 Performance
GPUI-RSX is a compile-time macro that expands to the same code as hand-written GPUI, with zero runtime overhead.
| 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 8 classes to dynamic match table:
rounded-none,rounded-xl,cursor-default,cursor-text,overflow-visible,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:
panic = "abort"in[profile.release]removes 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 cargo expand to view:
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, supports ALL classes — recommended)
rsx!
// ✅ Individual attributes (compile-time, type-checked)
rsx!
// ✅ Conditional styling with `when` (compile-time, fully flexible)
rsx!
// ✅ Dynamic expression with numeric prefix (runtime — gap-N, p-N, opacity-N, z-N all work)
let classes = if active else ;
rsx!
// Arbitrary hex colors and other exotic utilities are silently ignored at runtime.
Tip: When you need dynamic styling, prefer when/whenSome or individual value
attributes (bg={color}) — they are compile-time and support everything GPUI offers.
🤝 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! 🎉