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}