Skip to main content

gpui_rsx/
lib.rs

1//! # gpui-rsx
2//!
3//! A procedural macro that brings JSX-like syntax to [GPUI], making UI code more concise
4//! and readable while generating **zero-overhead** native GPUI method chains at compile time.
5//!
6//! [GPUI]: https://www.gpui.rs/
7//!
8//! ## At a Glance
9//!
10//! ```ignore
11//! use gpui::*;
12//! use gpui::prelude::*;
13//! use gpui_rsx::rsx;
14//!
15//! // Before — verbose GPUI method chain
16//! div()
17//!     .flex()
18//!     .flex_col()
19//!     .gap(px(16.0))
20//!     .p(px(16.0))
21//!     .bg(rgb(0x3b82f6))
22//!     .child(div().text_xl().font_weight(FontWeight::BOLD).child("Hello GPUI"))
23//!     .child(
24//!         div()
25//!             .cursor_pointer()
26//!             .id("btn")
27//!             .on_click(cx.listener(|_, _, _window, cx| cx.notify()))
28//!             .child("Click me"),
29//!     )
30//!
31//! // After — concise RSX (~50% less code)
32//! rsx! {
33//!     <div class="flex flex-col gap-4 p-4 bg-blue-500">
34//!         <h1 class="text-xl font-bold">{"Hello GPUI"}</h1>
35//!         <button
36//!             cursor_pointer
37//!             onClick={cx.listener(|_, _, _window, cx| cx.notify())}
38//!         >
39//!             {"Click me"}
40//!         </button>
41//!     </div>
42//! }
43//! ```
44//!
45//! The two snippets produce **identical** compiled output.
46//!
47//! ## Features
48//!
49//! | Feature | Description |
50//! |---------|-------------|
51//! | HTML-like tags | `<div>`, `<span>`, `<button>`, … → all map to `div()` |
52//! | Boolean attributes | `<div flex flex_col />` → `.flex().flex_col()` |
53//! | Value attributes | `<div gap={px(4.0)} />` → `.gap(px(4.0))` |
54//! | `class` (static) | Tailwind-like subset, parsed at compile time |
55//! | `class` (dynamic) | Runtime expression with colors, sizing, spacing, and common utilities |
56//! | Full color palette | 242 Tailwind colors + arbitrary hex/RGB/RGBA (`bg-[#ff0000]`) |
57//! | Desktop sizing | `w-[280px]`, `w-[37.5%]`, `w-6/24`, `min-w-0` |
58//! | Fragments | `<>...</>` — returns `vec![...]` |
59//! | For-loop sugar | `{for item in iter { ... }}` |
60//! | Spread | `{...iterator}` |
61//! | Conditional | `when` / `whenSome` attributes |
62//! | State classes | `hoverClass`, `focusClass`, `activeClass` |
63//! | Styled defaults | `<h1 styled>` injects sensible tag defaults |
64//! | camelCase mapping | `onClick` → `.on_click()`, `fontSize` → `.text_size()` |
65//! | Custom constructors | `base={Button::new("id")}` starts a component method chain |
66//! | `key` (loop ID) | Composite auto-ID for stateful elements in loops |
67//!
68//! ## Syntax Reference
69//!
70//! ### Basic Elements
71//!
72//! Any HTML tag maps to `div()`. Self-closing and pair tags are both valid:
73//!
74//! ```ignore
75//! rsx! { <div /> }                    // div()
76//! rsx! { <span></span> }              // div()
77//! rsx! { <button>{"OK"}</button> }    // div().child("OK")
78//! ```
79//!
80//! ### Fragment
81//!
82//! Return multiple root elements without a wrapper. Expands to `vec![...]`:
83//!
84//! ```ignore
85//! rsx! {
86//!     <>
87//!         <div>{"First"}</div>
88//!         <div>{"Second"}</div>
89//!     </>
90//! }
91//! // → vec![div().child("First"), div().child("Second")]
92//! ```
93//!
94//! ### Boolean Attributes (Flags)
95//!
96//! Bare attribute names become zero-argument method calls:
97//!
98//! ```ignore
99//! rsx! { <div flex flex_col items_center /> }
100//! // → div().flex().flex_col().items_center()
101//! ```
102//!
103//! ### Value Attributes
104//!
105//! `name={expr}` passes the expression as the method argument:
106//!
107//! ```ignore
108//! rsx! { <div gap={px(16.0)} bg={rgb(0x3b82f6)} opacity={0.8} /> }
109//! // → div().gap(px(16.0)).bg(rgb(0x3b82f6)).opacity(0.8)
110//! ```
111//!
112//! ### Custom Constructor with `base`
113//!
114//! `base={expr}` replaces the constructor inferred from the tag name, then the remaining
115//! attributes continue as a normal method chain:
116//!
117//! ```ignore
118//! rsx! { <Button base={Button::new("save")} label={"Save"} small /> }
119//! // → Button::new("save").label("Save").small()
120//! ```
121//!
122//! Path-qualified tags are supported for module-scoped components:
123//!
124//! ```ignore
125//! rsx! { <ui::TaskCard base={ui::TaskCard::new(task.id)} title={task.title.clone()} /> }
126//! ```
127//!
128//! ### The `class` Attribute — Static (Compile-time, Recommended)
129//!
130//! A Tailwind-inspired subset that expands to method calls at compile time.
131//! It maps directly to GPUI APIs and is not a full Tailwind CSS engine.
132//!
133//! ```ignore
134//! rsx! { <div class="flex flex-col gap-4 p-4 bg-blue-500 text-white rounded-md" /> }
135//! // → div().flex().flex_col().gap(px(4.0)).p(px(4.0)).bg(rgb(0x3b82f6))
136//! //         .text_color(rgb(0xffffff)).rounded_md()
137//! ```
138//!
139//! #### Supported class patterns
140//!
141//! **Layout:** `flex`, `flex-col`, `flex-row`, `flex-1`, `flex-wrap`, `flex-none`,
142//! `min-w-0`, `min-h-0`, `block`, `grid`, `hidden`, `absolute`, `relative`
143//!
144//! **Alignment:** `items-center`, `items-start`, `items-end`, `justify-center`,
145//! `justify-between`, `justify-start`, `justify-end`, `justify-around`,
146//! `content-center`, `content-between`, …
147//!
148//! **Spacing** (numeric value → `px(n.0)`):
149//! `gap-4` → `.gap(px(4.0))`, `p-4`, `px-4`, `py-4`, `pt-4`, `pb-4`, `pl-4`, `pr-4`,
150//! `m-4`, `mx-4`, `my-4`, `mt-4`, `mb-4`, arbitrary lengths such as `gap-[14px]`
151//! and `mx-[1.25rem]`
152//!
153//! **Sizing:** `w-full`, `h-full`, `size-full`, `w-64`, `h-32`, `w-[280px]`,
154//! `w-[18rem]`, `w-[37.5%]`, `w-6/24`
155//!
156//! **Text:** `text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`,
157//! `text-3xl`, `font-thin` through `font-black`, `italic`, `underline`, `truncate`,
158//! `text-left`, `text-center`
159//!
160//! **Border:** `border` → `.border_1()`, `border-2`, `rounded-sm`, `rounded-md`,
161//! `rounded-lg`, `rounded-xl`, `rounded-full`, `rounded-none`
162//!
163//! **Colors (full Tailwind palette):**
164//! `text-red-500` → `.text_color(rgb(0xef4444))`,
165//! `bg-blue-600` → `.bg(rgb(0x2563eb))`,
166//! `border-green-500` → `.border_color(rgb(0x22c55e))`
167//!
168//! Supported families: `slate`, `gray`, `zinc`, `neutral`, `stone`, `red`, `orange`,
169//! `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`,
170//! `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose` (shades 50–950) +
171//! `black`, `white`
172//!
173//! **Arbitrary colors:**
174//! `bg-[#ff0000]` → `.bg(rgb(0xff0000))`,
175//! `text-[#f00]` → `.text_color(rgb(0xff0000))`,
176//! `border-[#11223344]` → `.border_color(rgba(0x11223344))`,
177//! `bg-[rgb(15,23,42)]`, `text-[rgba(15,23,42,0.8)]`
178//!
179//! ### The `class` Attribute — Dynamic (Runtime)
180//!
181//! When `class` receives an expression, a runtime matcher is generated.
182//! Supported classes: common layout/spacing/typography utilities, the full Tailwind
183//! color palette, arbitrary colors (`bg-[#ff0000]`, `text-[#f00]`,
184//! `bg-[rgba(15,23,42,0.8)]`), arbitrary lengths (`w-[280px]`, `gap-[14px]`),
185//! fraction sizing (`w-6/24`), and numeric values for spacing/sizing/opacity via prefix fallback. Truly
186//! unsupported classes (e.g. Tailwind variants, unknown utilities) are silently ignored
187//! in release and print a warning in debug builds.
188//!
189//! ```ignore
190//! let active = true;
191//! rsx! { <div class={if active { "flex gap-4" } else { "block" }} /> }
192//! ```
193//!
194//! Prefer static strings or the `when` attribute instead:
195//!
196//! ```ignore
197//! // static literal — compile-time, documented subset
198//! rsx! { <div class="flex gap-4" /> }
199//!
200//! // ✅ conditional literal — still static
201//! let cls = if active { "flex gap-4" } else { "block" };
202//! rsx! { <div class={cls} /> }
203//!
204//! // ✅ when attribute — compile-time, fully flexible
205//! rsx! { <div when={(active, |el| el.flex().gap(px(4.0)))} /> }
206//!
207//! // Dynamic expression — runtime parser, narrower coverage than static literals
208//! rsx! { <div class={format!("gap-{} bg-[#f00]", spacing)} /> }
209//! ```
210//!
211//! ### Event Handling
212//!
213//! Event attributes (camelCase or snake_case) are mapped to GPUI listeners.
214//! Elements with event handlers automatically receive a deterministic `.id()`.
215//!
216//! ```ignore
217//! rsx! {
218//!     <button onClick={cx.listener(|view, _, _window, cx| {
219//!         view.count += 1;
220//!         cx.notify();
221//!     })}>
222//!         {"Increment"}
223//!     </button>
224//! }
225//! // → div().id("src/main.rs::__rsx_button_L42C8").on_click(cx.listener(...)).child("Increment")
226//! ```
227//!
228//! #### `key` — Unique IDs for stateful elements in loops
229//!
230//! `key={expr}` is a **macro-level** attribute consumed at compile time; it never
231//! becomes a `.key()` method call on the GPUI element.
232//!
233//! **`key` only takes effect when the element already needs an `.id()`** (i.e. it
234//! carries `onClick`, `onHover`, `onDrag`, `tooltip`, `focusable`, `overflowScroll`,
235//! `trackScroll`, or another attribute that requires a stateful element).
236//! On elements without any stateful attributes, `key` is silently ignored and no
237//! `.id()` is injected — the element stays a plain `Div`.
238//!
239//! | Situation | Result |
240//! |-----------|--------|
241//! | stateful attrs + `key` | composite ID: auto-prefix + key (runtime) |
242//! | stateful attrs, no `key` | pure source-location auto ID (compile-time) |
243//! | no stateful attrs + `key` | **`key` ignored**, no `.id()` injected |
244//! | explicit `id` | always used, `key` has no effect |
245//!
246//! **Loop safety:** Inside a `{for ...}` loop, every iteration shares the same source
247//! location. The macro **emits a compile error** when a stateful element is found inside
248//! a loop without an explicit `id` or `key`:
249//!
250//! ```ignore
251//! // ❌ compile error — all <li> would share the same auto ID
252//! {for item in &self.items { <li onClick={handler}>{item}</li> }}
253//!
254//! // ✅ key produces a unique ID per iteration
255//! rsx! {
256//!     <ul>
257//!         {for item in &self.items {
258//!             <li key={item.id} onClick={handler}>{item.name.clone()}</li>
259//!         }}
260//!     </ul>
261//! }
262//! // → div().id(format!("src/main.rs::__rsx_li_L42C8_{}", item.id)).on_click(handler)…
263//! //   e.g. "src/main.rs::__rsx_li_L42C8_1", "src/main.rs::__rsx_li_L42C8_2", …
264//! ```
265//!
266//! `key` accepts any type that implements `Display` (integers, `&str`, UUIDs, …).
267//! For a fully stable custom ID that survives refactors, use the `id` attribute instead.
268//!
269//! | Attribute | GPUI method |
270//! |-----------|-------------|
271//! | `onClick` / `on_click` | `.on_click(h)` |
272//! | `onMouseDown` / `on_mouse_down` | `.on_mouse_down(button, h)` |
273//! | `onMouseUp` / `on_mouse_up` | `.on_mouse_up(button, h)` |
274//! | `onMouseMove` / `on_mouse_move` | `.on_mouse_move(h)` |
275//! | `onMouseDownOut` / `on_mouse_down_out` | `.on_mouse_down_out(h)` |
276//! | `onMouseUpOut` / `on_mouse_up_out` | `.on_mouse_up_out(button, h)` |
277//! | `onAnyMouseDown` / `on_any_mouse_down` | `.on_any_mouse_down(h)` |
278//! | `onAnyMouseUp` / `on_any_mouse_up` | `.on_any_mouse_up(h)` |
279//! | `onKeyDown` / `on_key_down` | `.on_key_down(h)` |
280//! | `onKeyUp` / `on_key_up` | `.on_key_up(h)` |
281//! | `onModifiersChanged` / `on_modifiers_changed` | `.on_modifiers_changed(h)` |
282//! | `onHover` / `on_hover` | `.on_hover(h)` |
283//! | `onScrollWheel` / `on_scroll_wheel` | `.on_scroll_wheel(h)` |
284//! | `onDrag` / `on_drag` | `.on_drag(value, constructor)` |
285//! | `onDragMove` / `on_drag_move` | `.on_drag_move(h)` |
286//! | `onDrop` / `on_drop` | `.on_drop(h)` |
287//! | `onAction` / `on_action` | `.on_action(h)` |
288//! | `onBoxedAction` / `on_boxed_action` | `.on_boxed_action(action, h)` |
289//! | `captureAnyMouseDown` / `capture_any_mouse_down` | `.capture_any_mouse_down(h)` |
290//! | `captureAnyMouseUp` / `capture_any_mouse_up` | `.capture_any_mouse_up(h)` |
291//! | `captureKeyDown` / `capture_key_down` | `.capture_key_down(h)` |
292//! | `captureKeyUp` / `capture_key_up` | `.capture_key_up(h)` |
293//! | `captureAction` / `capture_action` | `.capture_action(h)` |
294//!
295//! Multi-argument GPUI methods use tuple syntax in RSX, e.g.
296//! `onMouseDown={(MouseButton::Left, handler)}`.
297//!
298//! ### Expressions and Children
299//!
300//! ```ignore
301//! rsx! {
302//!     <div>
303//!         {format!("Count: {}", self.count)}   // any expression
304//!         {self.render_child()}                 // method returning IntoElement
305//!         {if self.show {
306//!             rsx! { <span>{"Visible"}</span> }
307//!         } else {
308//!             rsx! { <span>{"Hidden"}</span> }
309//!         }}
310//!     </div>
311//! }
312//! ```
313//!
314//! Two or more consecutive expressions are batched into a single `.children([...])`
315//! call (stack-allocated array, zero heap allocation):
316//!
317//! ```ignore
318//! rsx! { <div>{"a"}{"b"}{"c"}</div> }
319//! // → div().children(["a", "b", "c"])
320//! ```
321//!
322//! ### For-loop Syntax Sugar
323//!
324//! ```ignore
325//! rsx! {
326//!     <ul>
327//!         {for item in &self.items {
328//!             <li>{item.name.clone()}</li>
329//!         }}
330//!     </ul>
331//! }
332//! // → div().children((&self.items).into_iter().map(|item| {
333//! //       div().child(item.name.clone())
334//! //   }))
335//! ```
336//!
337//! ### Spread Syntax
338//!
339//! ```ignore
340//! rsx! {
341//!     <div>
342//!         {...self.items.iter().map(|i| rsx! { <span>{i}</span> })}
343//!     </div>
344//! }
345//! ```
346//!
347//! ### Conditional Attributes: `when` and `whenSome`
348//!
349//! Apply style transformations based on a runtime condition without leaving compile-time
350//! safety:
351//!
352//! ```ignore
353//! rsx! {
354//!     <button
355//!         class="px-4 py-2 rounded-md"
356//!         when={(is_selected, |el| el.bg(rgb(0x3b82f6)).text_color(rgb(0xffffff)))}
357//!         when={(is_disabled, |el| el.opacity(0.5))}
358//!         whenSome={(custom_color, |el, c| el.bg(rgb(c)))}
359//!         whenClass={(is_focused, "border-blue-500 bg-blue-50")}
360//!     >
361//!         {"Button"}
362//!     </button>
363//! }
364//! ```
365//!
366//! `whenClass` only accepts string literal classes and rejects stateful classes such as
367//! `overflow-scroll`; use `when` for those explicit GPUI method calls.
368//!
369//! State-style classes can be attached to GPUI's style-refinement hooks:
370//!
371//! ```ignore
372//! rsx! {
373//!     <button
374//!         class="px-4 py-2 rounded-md bg-blue-500 text-white"
375//!         hoverClass="bg-blue-600"
376//!         focusClass="border-blue-500"
377//!         activeClass="opacity-75"
378//!     />
379//! }
380//! ```
381//!
382//! These attributes accept only string literal classes. Element-only or stateful classes such as
383//! `overflow-scroll` are rejected because GPUI passes a `StyleRefinement` into the closure.
384//!
385//! ### The `styled` Flag — Semantic Tag Defaults
386//!
387//! Adding `styled` injects sensible default classes for the tag name, applied before
388//! any user-provided attributes:
389//!
390//! ```ignore
391//! rsx! { <h1 styled>{"Title"}</h1> }
392//! // → div().text_3xl().font_weight(FontWeight::BOLD).child("Title")
393//!
394//! rsx! { <button styled onClick={handler}>{"OK"}</button> }
395//! // → div().cursor_pointer().id("…").on_click(handler).child("OK")
396//! ```
397//!
398//! | Tag | Default classes |
399//! |-----|----------------|
400//! | `h1` | `text-3xl font-bold` |
401//! | `h2` | `text-2xl font-bold` |
402//! | `h3` | `text-xl font-bold` |
403//! | `h4` | `text-lg font-bold` |
404//! | `h5` | `text-base font-bold` |
405//! | `h6` | `text-sm font-bold` |
406//! | `button`, `a` | `cursor-pointer` |
407//! | `input`, `textarea` | `px-2 py-1` |
408//! | `ul`, `ol` | `flex flex-col` |
409//! | `li` | `flex items-center` |
410//! | `p` | `text-base` |
411//! | `label` | `text-sm` |
412//! | `form` | `flex flex-col gap-4` |
413//!
414//! ### Attribute Mapping Reference
415//!
416//! Most camelCase names are converted to snake_case GPUI methods. Attributes not in this
417//! table are passed through unchanged (e.g., `bg={color}` → `.bg(color)`).
418//!
419//! | RSX attribute | Generated GPUI code |
420//! |---------------|---------------------|
421//! | `opacity` | `.opacity()` |
422//! | `visible` / `invisible` | `.visible()` / `.invisible()` |
423//! | `width` / `height` | `.w()` / `.h()` |
424//! | `minWidth` / `maxWidth` | `.min_w()` / `.max_w()` |
425//! | `minHeight` / `maxHeight` | `.min_h()` / `.max_h()` |
426//! | `gapX` / `gapY` | `.gap_x()` / `.gap_y()` |
427//! | `flexBasis` | `.flex_basis()` |
428//! | `flexGrow` / `flexShrink` (flags) | `.flex_grow_1()` / `.flex_shrink_1()` |
429//! | `fontSize` | `.text_size()` |
430//! | `lineHeight` | `.line_height()` |
431//! | `fontWeight` | `.font_weight()` |
432//! | `fontFamily` | `.font_family()` |
433//! | `textAlign` | `.text_align()` |
434//! | `textColor` | `.text_color()` |
435//! | `backgroundColor` | `.bg()` |
436//! | `borderColor` | `.border_color()` |
437//! | `borderTop` / `borderBottom` | `.border_t(value)` / `.border_b(value)` |
438//! | `borderLeft` / `borderRight` | `.border_l(value)` / `.border_r(value)` |
439//! | `border_t` / `border_b` / `border_l` / `border_r` (flags) | `.border_t_1()` / `.border_b_1()` / `.border_l_1()` / `.border_r_1()` |
440//! | `roundedTop` / `roundedBottom` | `.rounded_t()` / `.rounded_b()` |
441//! | `roundedTopLeft` / `roundedTopRight` | `.rounded_tl()` / `.rounded_tr()` |
442//! | `roundedBottomLeft` / `roundedBottomRight` | `.rounded_bl()` / `.rounded_br()` |
443//! | `boxShadow` | `.shadow()` |
444//! | `inset` | `.inset()` |
445//!
446//! ## Auto ID Injection
447//!
448//! Elements that require a stateful identity (`onClick`, `onHover`, `onDrag`, `tooltip`,
449//! `focusable`, `overflowScroll`, `trackScroll`, and static `overflow-scroll` classes)
450//! automatically receive a deterministic `.id()` derived from the element's source
451//! location. The ID is chosen by the following priority:
452//!
453//! 1. **Explicit `id`** — always wins, `key` is ignored.
454//! 2. **`key` present** (and element is stateful) — composite ID. Literal keys use
455//!    `concat!(...)`; dynamic keys use `format!(concat!(file!(), "::{prefix}_{}"), key_expr)`.
456//! 3. **No `key`** (and element is stateful) — pure compile-time source-location ID:
457//!    `concat!(file!(), "::", "__rsx_{tag}_L{line}C{col}")`
458//! 4. **Not stateful** — no `.id()` injected; `key` is silently ignored.
459//!
460//! ```text
461//! // Format (no key): concat!(file!(), "::", "__rsx_{tag}_L{line}C{col}")
462//! // Example:         "src/views/counter.rs::__rsx_button_L42C8"
463//! //
464//! // Format (literal key): concat!(file!(), "::__rsx_{tag}_L{line}C{col}_", "42")
465//! // Format (dynamic key): format!(concat!(file!(), "::__rsx_{tag}_L{line}C{col}_{}"), key)
466//! // Example:              "src/views/list.rs::__rsx_li_L55C12_42"
467//! ```
468//!
469//! The source-location ID is stable across incremental rebuilds as long as the
470//! element's position in the file does not change. For IDs that must survive
471//! refactors, use the explicit `id` attribute.
472//!
473//! > **Note on style attributes:** `hover`, `focus`, `group`, and `groupHover` are
474//! > *`Styled` trait* methods and do **not** trigger auto ID injection. `active`,
475//! > `activeClass`, and `groupActive` map to stateful GPUI hooks in the current target
476//! > and do require an ID.
477//!
478//! ### Loop safety
479//!
480//! Elements inside `{for ...}` loops share the same source location and would receive
481//! identical auto IDs across iterations. **A compile error is emitted** in this case.
482//! Add `key={expr}` (any `Display` type) to produce a unique ID per iteration:
483//!
484//! ```ignore
485//! // ❌ compile error — all <li> would share the same auto ID
486//! {for item in &self.items { <li onClick={handler}>{item}</li> }}
487//!
488//! // ✅ key produces a unique ID per iteration
489//! {for item in &self.items { <li key={item.id} onClick={handler}>{item}</li> }}
490//! ```
491//!
492//! ## Performance
493//!
494//! - **Compile-time** — All class parsing, color lookup, and attribute mapping happen
495//!   during macro expansion. The generated code is identical to hand-written GPUI.
496//! - **O(1) lookups** — Colors, attributes, and spacing prefixes use `match` statements
497//!   that the compiler turns into jump tables.
498//! - **Zero allocation** — Static classes generate no heap allocation. Dynamic classes
499//!   use `AsRef<str>` (zero-copy for `&str` inputs).
500//! - **Binary size** — Dynamic class helpers use `#[inline(never)]` + LLVM ICF to avoid
501//!   code bloat when multiple `class={expr}` appear in the same component.
502//!
503//! ## Further Reading
504//!
505//! - [Architecture guide (ARCHITECTURE.md)](https://github.com/wsafight/gpui-rsx/blob/main/ARCHITECTURE.md) —
506//!   module design, data flow, extension points
507//! - [Getting started](https://wsafight.github.io/gpui-rsx/getting-started/)
508//! - [API reference](https://wsafight.github.io/gpui-rsx/reference/api/)
509//! - [Best practices](https://wsafight.github.io/gpui-rsx/guides/best-practices/)
510//! - [Changelog](https://github.com/wsafight/gpui-rsx/blob/main/CHANGELOG.md)
511
512use proc_macro::TokenStream;
513use syn::parse_macro_input;
514
515mod codegen;
516mod diagnostics;
517mod parser;
518
519use codegen::class::ClassMode;
520use codegen::element::{generate_body_expansion_preview, generate_body_with_mode};
521use parser::RsxBody;
522
523/// Transforms JSX-like markup into GPUI method chains at compile time.
524///
525/// # Syntax
526///
527/// ```text
528/// rsx! { <tag attr1 attr2={expr} class="…"> {children} </tag> }
529/// rsx! { <tag /> }          — self-closing
530/// rsx! { <> … </> }         — fragment (returns vec![…])
531/// ```
532///
533/// # Examples
534///
535/// ## Basic element with static classes
536///
537/// ```ignore
538/// rsx! {
539///     <div class="flex flex-col gap-4 p-4">
540///         <h1 class="text-2xl font-bold">{"Hello"}</h1>
541///     </div>
542/// }
543/// ```
544///
545/// ## Event handling (auto ID injected)
546///
547/// ```ignore
548/// rsx! {
549///     <button
550///         class="bg-blue-500 text-white px-4 py-2 rounded-md"
551///         onClick={cx.listener(|view, _, _window, cx| {
552///             view.count += 1;
553///             cx.notify();
554///         })}
555///     >
556///         {"Click me"}
557///     </button>
558/// }
559/// ```
560///
561/// ## For-loop rendering
562///
563/// ```ignore
564/// rsx! {
565///     <ul>
566///         {for item in &self.items {
567///             <li>{item.name.clone()}</li>
568///         }}
569///     </ul>
570/// }
571/// ```
572///
573/// ## Fragment (multiple root elements)
574///
575/// ```ignore
576/// rsx! {
577///     <>
578///         <div>{"First"}</div>
579///         <div>{"Second"}</div>
580///     </>
581/// }
582/// ```
583///
584/// ## Conditional styling
585///
586/// ```ignore
587/// rsx! {
588///     <div
589///         class="px-4 py-2 rounded-md"
590///         when={(is_active, |el| el.bg(rgb(0x3b82f6)))}
591///         whenSome={(custom_width, |el, w| el.w(px(w)))}
592///     >
593///         {"Content"}
594///     </div>
595/// }
596/// ```
597///
598/// ## Styled defaults
599///
600/// ```ignore
601/// rsx! { <h1 styled>{"Title"}</h1> }
602/// // → div().text_3xl().font_weight(FontWeight::BOLD).child("Title")
603/// ```
604///
605/// # Notes
606///
607/// - **Static `class`**: parsed entirely at compile time, supports the documented Tailwind-like subset.
608/// - **Dynamic `class={expr}`**: runtime matcher; full Tailwind color palette,
609///   arbitrary colors, common layout/spacing/typography utilities, arbitrary
610///   lengths, fraction sizing, and numeric values via prefix fallback. Truly unsupported classes are silently ignored
611///   in release builds. Prefer `when` or static strings for full coverage.
612/// - **Auto ID**: elements with stateful event handlers receive a source-location-based
613///   ID automatically. Provide an explicit `id` attribute for state-sensitive elements.
614#[proc_macro]
615pub fn rsx(input: TokenStream) -> TokenStream {
616    let body = parse_macro_input!(input as RsxBody);
617    let code = generate_body_with_mode(&body, ClassMode::Permissive);
618    TokenStream::from(code)
619}
620
621/// Transforms RSX into GPUI method chains and rejects unsupported static classes.
622///
623/// Static unknown classes emit a compile error instead of being ignored or falling through
624/// to a GPUI method name. Dynamic unknown classes panic when evaluated.
625#[proc_macro]
626pub fn rsx_strict(input: TokenStream) -> TokenStream {
627    let body = parse_macro_input!(input as RsxBody);
628    let code = generate_body_with_mode(&body, ClassMode::Strict);
629    TokenStream::from(code)
630}
631
632/// Transforms RSX using the default permissive class handling.
633///
634/// This is equivalent to [`rsx!`], and exists as an explicit opt-in when codebases
635/// also use [`rsx_strict!`].
636#[proc_macro]
637pub fn rsx_permissive(input: TokenStream) -> TokenStream {
638    let body = parse_macro_input!(input as RsxBody);
639    let code = generate_body_with_mode(&body, ClassMode::Permissive);
640    TokenStream::from(code)
641}
642
643/// Returns a string preview of the generated GPUI method chain.
644///
645/// This macro is intended for debugging generated code. It does not type-check the
646/// generated GPUI expression because it returns a `&'static str`.
647#[proc_macro]
648pub fn rsx_expand(input: TokenStream) -> TokenStream {
649    let body = parse_macro_input!(input as RsxBody);
650    let preview = generate_body_expansion_preview(&body, ClassMode::Permissive);
651    let code = quote::quote! { #preview };
652    TokenStream::from(code)
653}