# GPUI-RSX
[](https://github.com/wsafight/gpui-rsx/actions/workflows/ci.yml)
[](https://codecov.io/gh/wsafight/gpui-rsx)
[](https://crates.io/crates/gpui-rsx)
[](https://docs.rs/gpui-rsx)
[](./LICENSE)
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 { ... }}`
- 🎨 **Full Tailwind Colors** - 242 built-in colors + arbitrary hex values
- ⚡ **Dynamic Class** - Runtime class switching with automatic fallback
## 📚 Documentation
- **[Architecture Guide](./ARCHITECTURE.md)** - Detailed architecture documentation
- Module organization and data flow
- Code generation strategies
- Design patterns and testing approach
- Extension points and debugging guide
- **[Getting Started](./docs/getting-started.md)** - Step-by-step tutorial
- **[API Reference](./docs/api-reference.md)** - Complete API documentation
- **[Best Practices](./docs/best-practices.md)** - Recommended patterns
- **[Migration Guide](./docs/migration-guide.md)** - Upgrade instructions
- **[Troubleshooting](./docs/troubleshooting.md)** - Common issues and solutions
## 📦 Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
gpui = "0.1"
gpui-rsx = "0.1"
```
## 🚀 Quick Start
### Get Started in 5 Minutes
```rust
use gpui::*;
use gpui_rsx::rsx;
struct CounterView {
count: i32,
}
impl Render for CounterView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
rsx! {
<div class="flex flex-col gap-4 p-4">
<h1>{format!("Count: {}", self.count)}</h1>
<div class="flex gap-2">
<button
bg={rgb(0x3b82f6)}
text_color={rgb(0xffffff)}
px_4
py_2
rounded_md
onClick={cx.listener(|view, _, cx| {
view.count += 1;
cx.notify();
})}
>
{"Increment"}
</button>
<button
bg={rgb(0xef4444)}
text_color={rgb(0xffffff)}
px_4
py_2
rounded_md
onClick={cx.listener(|view, _, cx| {
view.count -= 1;
cx.notify();
})}
>
{"Decrement"}
</button>
</div>
</div>
}
}
}
fn main() {
App::new().run(|cx: &mut AppContext| {
cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|_cx| CounterView { count: 0 })
});
});
}
```
### Before & After
#### ❌ Traditional GPUI (Verbose)
```rust
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_4()
.p_4()
.child(
div()
.text_xl()
.font_bold()
.child(format!("Count: {}", self.count))
)
.child(
div()
.flex()
.gap_2()
.child(
div()
.bg(rgb(0x3b82f6))
.text_color(rgb(0xffffff))
.px_4()
.py_2()
.rounded_md()
.on_click(cx.listener(|view, _, cx| {
view.count += 1;
cx.notify();
}))
.child("Increment")
)
.child(
div()
.bg(rgb(0xef4444))
.text_color(rgb(0xffffff))
.px_4()
.py_2()
.rounded_md()
.on_click(cx.listener(|view, _, cx| {
view.count -= 1;
cx.notify();
}))
.child("Decrement")
)
)
}
```
#### ✅ With GPUI-RSX (Concise)
See the Quick Start example above.
**Code Reduction: ~50%** ✨
## 📖 Syntax Guide
### 1. Basic Elements
```rust
rsx! {
<div>{"Hello GPUI"}</div>
}
```
Expands to:
```rust
div().child("Hello GPUI")
```
### 2. Fragment (Multiple Root Elements)
When you need to return multiple elements without a wrapper:
```rust
rsx! {
<>
<div>{"First"}</div>
<div>{"Second"}</div>
<div>{"Third"}</div>
</>
}
```
Expands to:
```rust
vec![
div().child("First"),
div().child("Second"),
div().child("Third"),
]
```
### 3. Attributes
#### Boolean Attributes (Flags)
```rust
rsx! {
<div flex flex_col />
}
```
Expands to:
```rust
div().flex().flex_col()
```
#### Value Attributes
```rust
rsx! {
<div gap={px(16.0)} bg={rgb(0xffffff)} />
}
```
Expands to:
```rust
div().gap(px(16.0)).bg(rgb(0xffffff))
```
### 4. Class Attribute
The `class` attribute accepts a Tailwind-like string that expands into multiple GPUI method calls:
```rust
rsx! {
<div class="flex flex-col gap-4 p-4" />
}
```
Expands to:
```rust
div().flex().flex_col().gap(px(4.0)).p(px(4.0))
```
> **Note:** `class` accepts both static strings (compiled at build time) and dynamic expressions (parsed at runtime). Static classes have zero runtime overhead. See [Dynamic Class](#10-dynamic-class-) for details.
#### Supported class patterns
**Layout:**
- `flex`, `flex-col`, `flex-row`, `flex-wrap`, `flex-1`, `flex-none`, `flex-auto`
- `items-center`, `items-start`, `items-end`
- `justify-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-4`
- `m-4`, `mx-4`, `my-4`, `mt-4`, `mb-4`, `ml-4`, `mr-4`
- `w-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-xl`
- `text-2xl`, `text-3xl`, `text-4xl`, `text-5xl`
- `font-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-lg`
- `overflow-hidden`, `overflow-scroll`
- `cursor-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
```rust
rsx! {
<button onClick={cx.listener(|view, _, cx| {
println!("clicked");
})}>
{"Click me"}
</button>
}
```
Supported events (camelCase / snake_case):
| `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
```rust
rsx! {
<div>
<h1>{"Title"}</h1>
<p>{"Description"}</p>
<div>
<button>{"Action 1"}</button>
<button>{"Action 2"}</button>
</div>
</div>
}
```
### 7. Expressions
```rust
rsx! {
<div>
{format!("Count: {}", self.count)}
{self.render_child_component()}
{if self.show {
rsx! { <span>{"Visible"}</span> }
} else {
rsx! { <span>{"Hidden"}</span> }
}}
</div>
}
```
### 8. List Rendering
#### Using iterators (traditional)
```rust
rsx! {
<div>
{self.items.iter().map(|item| {
rsx! {
<div key={item.id}>
{item.name.clone()}
</div>
}
}).collect::<Vec<_>>()}
</div>
}
```
#### Using for-loop syntax sugar
```rust
rsx! {
<ul>
{for item in &self.items {
<li>{item.name.clone()}</li>
}}
</ul>
}
```
Expands to:
```rust
div().children((&self.items).into_iter().map(|item| {
div().child(item.name.clone())
}))
```
For-loops also support ranges and method calls:
```rust
rsx! {
<div>
{for i in 0..5 {
<span>{i}</span>
}}
</div>
}
```
### 9. Spread Syntax
```rust
rsx! {
<div>
{...items.iter().map(|item| rsx! { <span>{item}</span> })}
</div>
}
```
### 10. Dynamic Class
GPUI-RSX supports both static and dynamic `class` attributes.
#### Static Class (Compile-time — Recommended)
```rust
// ✅ Best performance - parsed at compile time, supports all classes
rsx! {
<div class="flex gap-4 bg-blue-500">
{"Static styles"}
</div>
}
```
#### Dynamic Class (Runtime — Limited)
```rust
// ⚠️ Only ~58 pre-compiled common classes are supported at runtime.
// Unknown classes are silently ignored.
let classes = if is_active { "flex gap-4" } else { "block" };
rsx! {
<div class={classes}>
{"Dynamic styles"}
</div>
}
```
> **Important limitation:** When `class` is a runtime expression, only the ~58 pre-compiled
> common classes (flex, gap-1..gap-8, p-1..p-8, text-xl, rounded-md, etc.) are recognised.
> Any class not in this list is **silently ignored** at runtime.
>
> **Recommended alternatives** (in priority order):
> 1. **String literal** (best): `class="flex gap-4"` — compile-time, supports all classes
> 2. **Conditional literal**: `class={if active { "flex gap-4" } else { "block" }}` — still a literal
> 3. **Individual attributes**: `<div flex gap_4 />` — compile-time, type-checked
> 4. **`when` attribute**: `when={(cond, |el| el.flex())}` — compile-time, fully flexible
> 5. **Dynamic expression**: `class={expr}` — runtime only, ~58 classes supported
**Common Patterns:**
```rust
// ✅ Conditional literal (compile-time, all classes work)
let button_class = if primary { "bg-blue-500 text-white" } else { "bg-gray-200 text-black" };
// ✅ when attribute (compile-time, fully flexible)
// ⚠️ Dynamic string (only common classes work)
let classes = format!("flex gap-{}", spacing); // gap-4 works; gap-32 may not
```
### 11. Attribute Mapping Reference
camelCase attributes are automatically mapped to GPUI snake_case methods:
| `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_hidden()` / `.overflow_y_hidden()` |
| `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
```rust
rsx! {
<div
flex
when={(is_active, |this| {
this.bg(rgb(0x3b82f6))
.text_color(rgb(0xffffff))
})}
>
{"Button"}
</div>
}
```
#### whenSome - Apply styles when Option has value
```rust
let custom_width: Option<f32> = Some(200.0);
rsx! {
<div
flex
whenSome={(custom_width, |this, w| this.w(px(w)))}
>
{"Content"}
</div>
}
```
#### Multiple conditions
```rust
rsx! {
<button
class="px-4 py-2 rounded-md"
when={(is_selected, |this| this.bg(rgb(0x3b82f6)))}
when={(is_disabled, |this| this.bg(rgb(0xe5e7eb)))}
whenSome={(custom_color, |this, color| this.bg(rgb(color)))}
>
{"Button"}
</button>
}
```
### 12. Styled Flag (Default Tag Styles)
The `styled` flag injects sensible default styles based on the tag name:
```rust
rsx! {
<h1 styled>{"Title"}</h1>
// Expands to: div().text_3xl().font_bold().child("Title")
<button styled>{"Click"}</button>
// Expands to: div().cursor_pointer().child("Click")
}
```
Default styles per tag:
| `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` |
User attributes are applied after defaults and can override them.
## 🎯 Complete Example
### Todo App
```rust
use gpui::*;
use gpui_rsx::rsx;
struct TodoApp {
todos: Vec<Todo>,
input: String,
}
struct Todo {
id: usize,
text: String,
completed: bool,
}
impl Render for TodoApp {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
rsx! {
<div class="flex flex-col gap-4 p-4">
<h1 class="text-2xl font-bold">
{"Todo List"}
</h1>
<div class="flex gap-2">
<input
placeholder="Add a todo..."
value={self.input.clone()}
/>
<button
class="bg-blue-500 text-white px-4 py-2 rounded-md"
onClick={cx.listener(|view, _, cx| {
view.add_todo();
cx.notify();
})}
>
{"Add"}
</button>
</div>
<div class="flex flex-col gap-2">
{for todo in self.todos.iter() {
<div
class="flex gap-2 items-center p-2 rounded-md"
bg={if todo.completed {
rgb(0xf3f4f6)
} else {
rgb(0xffffff)
}}
>
<span>{todo.text.clone()}</span>
</div>
}}
</div>
</div>
}
}
}
impl TodoApp {
fn add_todo(&mut self) {
if !self.input.is_empty() {
self.todos.push(Todo {
id: self.todos.len(),
text: self.input.clone(),
completed: false,
});
self.input.clear();
}
}
}
```
## 🔧 Advanced Usage
### Custom Components
```rust
fn render_card(&self, title: &str, content: &str) -> impl IntoElement {
rsx! {
<div class="rounded-lg shadow-md p-6">
<h2 class="text-xl font-bold">
{title}
</h2>
<p class="text-gray-600">
{content}
</p>
</div>
}
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
rsx! {
<div>
{self.render_card("Title 1", "Content 1")}
{self.render_card("Title 2", "Content 2")}
</div>
}
}
```
### Conditional Rendering
```rust
rsx! {
<div>
{if self.loading {
rsx! { <div>{"Loading..."}</div> }
} else if let Some(error) = &self.error {
rsx! { <div class="text-red-500">{error.clone()}</div> }
} else {
rsx! { <div>{self.render_content()}</div> }
}}
</div>
}
```
### Dynamic Styling
```rust
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let bg_color = if self.is_active {
rgb(0x3b82f6)
} else {
rgb(0x6b7280)
};
rsx! {
<div bg={bg_color} class="px-4 py-2 rounded-md">
{"Button"}
</div>
}
}
```
## 📊 Performance
GPUI-RSX is a **compile-time macro** that expands to the same code as hand-written GPUI, with **zero runtime overhead**.
| Code Size | 100 lines | 50 lines (-50%) |
| Runtime Performance | Baseline | Same |
| Type Safety | ✅ | ✅ |
| Compile-time Checking | ✅ | ✅ |
### 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_string` returns an iterator (no intermediate `Vec`)
- `generate_attr_methods` pushes directly into caller's buffer
- `Cow<str>` for class name transformations (zero-copy when no `-` present)
- `Vec::with_capacity` pre-allocation throughout
**Runtime Performance:**
- Zero-copy dynamic class strings via `AsRef<str>` (`&str` needs 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
```bash
cd gpui-rsx
cargo build
```
### Test
```bash
cargo test --test macro_tests
```
### Expand Macros (Debugging)
```bash
# Install cargo-expand
cargo install cargo-expand
# View expanded code
cargo expand --lib
```
## 💡 Best Practices
### 1. Component Splitting
Break complex UIs into small, reusable components:
```rust
// ✅ Recommended: Split into multiple methods
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
rsx! {
<div>
{self.render_header()}
{self.render_content()}
{self.render_footer()}
</div>
}
}
fn render_header(&self) -> impl IntoElement {
rsx! { <header>{"Header"}</header> }
}
```
### 2. Use Constants
Extract repeated styles as constants:
```rust
const PRIMARY_BG: Rgb = rgb(0x3b82f6);
const PRIMARY_TEXT: Rgb = rgb(0xffffff);
rsx! {
<button bg={PRIMARY_BG} text_color={PRIMARY_TEXT}>
{"Button"}
</button>
}
```
### 3. Avoid Over-nesting
```rust
// ❌ Not recommended: Over-nested
rsx! {
<div>
<div>
<div>
<div>
{"Content"}
</div>
</div>
</div>
</div>
}
// ✅ Recommended: Flatten structure
rsx! {
<div class="container">
{"Content"}
</div>
}
```
## 🐛 FAQ
### Q1: How to use variables in RSX?
```rust
let title = "Hello";
rsx! {
<div>{title}</div>
}
```
### Q2: How to handle Option types?
```rust
rsx! {
<div>
{if let Some(text) = &self.optional_text {
rsx! { <span>{text.clone()}</span> }
} else {
rsx! { <span>{"No text"}</span> }
}}
</div>
}
```
### Q3: What does the expanded macro code look like?
Use `cargo expand` to view:
```bash
cargo expand --lib
```
### 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:
```rust
// ✅ Static literal (compile-time, supports ALL classes — recommended)
rsx! { <div class="flex gap-4" /> }
// ✅ Individual attributes (compile-time, type-checked)
rsx! { <div bg={dynamic_color} flex /> }
// ✅ Conditional styling with `when` (compile-time, fully flexible)
rsx! { <div when={(is_active, |this| this.bg(rgb(0x3b82f6)))} /> }
// ⚠️ Dynamic expression (runtime — only ~58 common classes supported)
let classes = if active { "flex gap-4" } else { "block" };
rsx! { <div class={classes} /> }
// Classes not in the pre-compiled list 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
1. Fork the project
2. Create a feature branch: `git checkout -b feature/amazing-feature`
3. Commit changes: `git commit -m 'Add amazing feature'`
4. Push branch: `git push origin feature/amazing-feature`
5. Submit a Pull Request
### Code Standards
- Use `rustfmt` to format code
- Use `clippy` to check code quality
- Add tests for new features
- Update documentation
## 📝 License
MIT License
## 🙏 Acknowledgments
Inspired by:
- [Dioxus RSX](https://dioxuslabs.com/) - RSX syntax design
- [Yew html! macro](https://yew.rs/) - html! macro
- [React JSX](https://react.dev/) - JSX syntax
- [GPUI](https://www.gpui.rs/) - Underlying UI framework
---
**Make GPUI development more enjoyable!** 🎉