Skip to content

wsafight/gpui-rsx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 { ... }}
  • 🎨 Full Tailwind Colors - 242 built-in colors + arbitrary hex values

📦 Installation

Add to your Cargo.toml:

[dependencies]
gpui = "0.1"
gpui-rsx = "0.1"

🚀 Quick Start

Get Started in 5 Minutes

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)

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

rsx! {
    <div>{"Hello GPUI"}</div>
}

Expands to:

div().child("Hello GPUI")

2. Fragment (Multiple Root Elements)

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"),
]

3. Attributes

Boolean Attributes (Flags)

rsx! {
    <div flex flex_col />
}

Expands to:

div().flex().flex_col()

Value Attributes

rsx! {
    <div gap={px(16.0)} bg={rgb(0xffffff)} />
}

Expands to:

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:

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: class only accepts string literals. For dynamic styling, use individual attributes (e.g., bg={color_var}).

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

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)

6. Nested Elements

rsx! {
    <div>
        <h1>{"Title"}</h1>
        <p>{"Description"}</p>
        <div>
            <button>{"Action 1"}</button>
            <button>{"Action 2"}</button>
        </div>
    </div>
}

7. Expressions

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)

rsx! {
    <div>
        {self.items.iter().map(|item| {
            rsx! {
                <div key={item.id}>
                    {item.name.clone()}
                </div>
            }
        }).collect::<Vec<_>>()}
    </div>
}

Using for-loop syntax sugar

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>
}

9. Spread Syntax

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

10. 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_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

rsx! {
    <div
        flex
        when={(is_active, |this| {
            this.bg(rgb(0x3b82f6))
                .text_color(rgb(0xffffff))
        })}
    >
        {"Button"}
    </div>
}

whenSome - Apply styles when Option has value

let custom_width: Option<f32> = Some(200.0);

rsx! {
    <div
        flex
        whenSome={(custom_width, |this, w| this.w(px(w)))}
    >
        {"Content"}
    </div>
}

Multiple conditions

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:

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.

🎯 Complete Example

Todo App

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

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

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

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.

Metric Traditional GPUI GPUI-RSX
Code Size 100 lines 50 lines (-50%)
Runtime Performance Baseline Same
Type Safety
Compile-time Checking

🛠️ Development

Build

cd gpui-rsx
cargo build

Test

cargo test --test macro_tests

Run Examples

# Counter example
cargo run --example counter

# Todo app example
cargo run --example todo_app

Expand Macros (Debugging)

# 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:

// ✅ 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:

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

// ❌ 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?

let title = "Hello";
rsx! {
    <div>{title}</div>
}

Q2: How to handle Option types?

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:

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?

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)))} />
}

🤝 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:


Make GPUI development more enjoyable! 🎉

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •