English | 简体中文
A Rust procedural macro that provides JSX-like syntax for GPUI, making UI development more concise and intuitive.
- 🎨 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
Add to your Cargo.toml:
[dependencies]
gpui = "0.1"
gpui-rsx = "0.1"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 })
});
});
}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")
)
)
}See the Quick Start example above.
Code Reduction: ~50% ✨
rsx! {
<div>{"Hello GPUI"}</div>
}Expands to:
div().child("Hello GPUI")When you need to return multiple elements without a wrapper:
rsx! {
<>
<div>{"First"}</div>
<div>{"Second"}</div>
<div>{"Third"}</div>
</>
}Expands to:
vec![
div().child("First"),
div().child("Second"),
div().child("Third"),
]rsx! {
<div flex flex_col />
}Expands to:
div().flex().flex_col()rsx! {
<div gap={px(16.0)} bg={rgb(0xffffff)} />
}Expands to:
div().gap(px(16.0)).bg(rgb(0xffffff))The class attribute accepts a Tailwind-like string that expands into multiple GPUI method calls:
rsx! {
<div class="flex flex-col gap-4 p-4" />
}Expands to:
div().flex().flex_col().gap(px(4.0)).p(px(4.0))Note:
classonly accepts string literals. For dynamic styling, use individual attributes (e.g.,bg={color_var}).
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-3xl,text-4xl,text-5xlfont-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
rsx! {
<button onClick={cx.listener(|view, _, cx| {
println!("clicked");
})}>
{"Click me"}
</button>
}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) |
rsx! {
<div>
<h1>{"Title"}</h1>
<p>{"Description"}</p>
<div>
<button>{"Action 1"}</button>
<button>{"Action 2"}</button>
</div>
</div>
}rsx! {
<div>
{format!("Count: {}", self.count)}
{self.render_child_component()}
{if self.show {
rsx! { <span>{"Visible"}</span> }
} else {
rsx! { <span>{"Hidden"}</span> }
}}
</div>
}rsx! {
<div>
{self.items.iter().map(|item| {
rsx! {
<div key={item.id}>
{item.name.clone()}
</div>
}
}).collect::<Vec<_>>()}
</div>
}rsx! {
<ul>
{for item in &self.items {
<li>{item.name.clone()}</li>
}}
</ul>
}Expands to:
div().children((&self.items).into_iter().map(|item| {
div().child(item.name.clone())
}))For-loops also support ranges and method calls:
rsx! {
<div>
{for i in 0..5 {
<span>{i}</span>
}}
</div>
}rsx! {
<div>
{...items.iter().map(|item| rsx! { <span>{item}</span> })}
</div>
}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_hidden() / .overflow_y_hidden() |
inset |
.inset() |
Attributes not in this table are passed through as-is (e.g., bg={color} → .bg(color)).
rsx! {
<div
flex
when={(is_active, |this| {
this.bg(rgb(0x3b82f6))
.text_color(rgb(0xffffff))
})}
>
{"Button"}
</div>
}let custom_width: Option<f32> = Some(200.0);
rsx! {
<div
flex
whenSome={(custom_width, |this, w| this.w(px(w)))}
>
{"Content"}
</div>
}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>
}The styled flag injects sensible default styles based on the tag name:
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:
| 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 |
User attributes are applied after defaults and can override them.
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();
}
}
}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>
}
}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>
}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>
}
}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 | ✅ | ✅ |
cd gpui-rsx
cargo buildcargo test --test macro_tests# Counter example
cargo run --example counter
# Todo app example
cargo run --example todo_app# Install cargo-expand
cargo install cargo-expand
# View expanded code
cargo expand --libBreak complex UIs into small, reusable components:
// ✅ 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> }
}Extract repeated styles as constants:
const PRIMARY_BG: Rgb = rgb(0x3b82f6);
const PRIMARY_TEXT: Rgb = rgb(0xffffff);
rsx! {
<button bg={PRIMARY_BG} text_color={PRIMARY_TEXT}>
{"Button"}
</button>
}// ❌ Not recommended: Over-nested
rsx! {
<div>
<div>
<div>
<div>
{"Content"}
</div>
</div>
</div>
</div>
}
// ✅ Recommended: Flatten structure
rsx! {
<div class="container">
{"Content"}
</div>
}let title = "Hello";
rsx! {
<div>{title}</div>
}rsx! {
<div>
{if let Some(text) = &self.optional_text {
rsx! { <span>{text.clone()}</span> }
} else {
rsx! { <span>{"No text"}</span> }
}}
</div>
}Use cargo expand to view:
cargo expand --libAll GPUI-supported elements can be used, such as div, button, input, span, etc.
No. The class attribute only accepts string literals. For dynamic styling, use individual attributes:
// ❌ Won't work
rsx! { <div class={my_class} /> }
// ✅ Use individual attributes
rsx! {
<div bg={dynamic_color} flex />
}
// ✅ Or use `when` for conditional styles
rsx! {
<div when={(is_active, |this| this.bg(rgb(0x3b82f6)))} />
}Contributions are welcome! Feel free to submit Issues or Pull Requests.
- 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
- Use
rustfmtto format code - Use
clippyto check code quality - Add tests for new features
- Update documentation
MIT License
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! 🎉