gpui_ui_kit_macros/lib.rs
1//! Proc macros for gpui-ui-kit
2//!
3//! Provides derive macros to reduce boilerplate in component theme definitions.
4//! The primary macro is [`ComponentTheme`] which generates `Default` and `From<&Theme>`
5//! implementations for theme structs, reducing repetitive boilerplate code.
6//!
7//! # Quick Start
8//!
9//! ```ignore
10//! use gpui_ui_kit_macros::ComponentTheme;
11//!
12//! #[derive(Debug, Clone, ComponentTheme)]
13//! pub struct MyComponentTheme {
14//! #[theme(default = 0x007acc, from = accent)]
15//! pub primary_color: Rgba,
16//!
17//! #[theme(default = 0xffffff, from = text_primary)]
18//! pub text_color: Rgba,
19//! }
20//! ```
21//!
22//! This generates:
23//! - `impl Default for MyComponentTheme` using the hex `default` values
24//! - `impl From<&Theme> for MyComponentTheme` mapping from global theme fields
25//!
26//! # Crate Features
27//!
28//! This is a proc-macro crate. It must be used alongside the main `gpui-ui-kit`
29//! crate which re-exports the macro as `ComponentTheme`.
30
31use proc_macro::TokenStream;
32use quote::quote;
33use syn::punctuated::Punctuated;
34use syn::{Data, DeriveInput, Expr, Fields, Lit, Meta, Token, parse_macro_input};
35
36/// Derive macro for component themes.
37///
38/// Generates `Default` and `From<&Theme>` implementations for theme structs,
39/// allowing components to have fallback colors while also automatically adapting
40/// to the global theme.
41///
42/// # Requirements
43///
44/// - Only works on structs with named fields
45/// - Every field must have a `#[theme(...)]` attribute
46/// - Each field needs both a default value and a mapping from Theme
47///
48/// # Attribute Reference
49///
50/// ## For Color Fields (Rgba)
51///
52/// | Attribute | Description | Example |
53/// |-----------|-------------|---------|
54/// | `default = 0xRRGGBB` | RGB hex color for Default impl | `default = 0x007acc` |
55/// | `default = 0xRRGGBBAA` | RGBA hex color (with alpha) | `default = 0x007acc80` |
56/// | `from = field_name` | Direct mapping from Theme field | `from = accent` |
57/// | `from_expr = "expr"` | Custom expression (uses `theme` variable) | `from_expr = "with_alpha(theme.accent, 0.2)"` |
58///
59/// ## For Numeric Fields (f32, etc.)
60///
61/// | Attribute | Description | Example |
62/// |-----------|-------------|---------|
63/// | `default_f32 = value` | f32 literal for Default impl | `default_f32 = 0.5` |
64/// | `from_expr = "value"` | Expression for From impl | `from_expr = "0.5"` |
65///
66/// ## For Other Types (Option, nested themes, etc.)
67///
68/// | Attribute | Description | Example |
69/// |-----------|-------------|---------|
70/// | `default_expr = "expr"` | Arbitrary expression for Default | `default_expr = "None"` |
71/// | `from_expr = "expr"` | Arbitrary expression for From | `from_expr = "Some(theme.accent)"` |
72///
73/// # Available Theme Fields
74///
75/// The global `Theme` struct provides these fields for mapping:
76///
77/// **Backgrounds:** `background`, `surface`, `surface_hover`, `muted`, `transparent`, `overlay_bg`
78///
79/// **Text:** `text_primary`, `text_secondary`, `text_muted`, `text_on_accent`, `icon_on_accent`
80///
81/// **Accent:** `accent`, `accent_hover`, `accent_muted`
82///
83/// **Semantic:** `success`, `warning`, `error`, `info`
84///
85/// **Border:** `border`, `border_hover`
86///
87/// # Examples
88///
89/// ## Basic Color Theme
90///
91/// ```ignore
92/// #[derive(Debug, Clone, ComponentTheme)]
93/// pub struct ButtonTheme {
94/// #[theme(default = 0x007acc, from = accent)]
95/// pub background: Rgba,
96///
97/// #[theme(default = 0xffffff, from = text_primary)]
98/// pub text: Rgba,
99///
100/// #[theme(default = 0x3a3a3a, from = border)]
101/// pub border: Rgba,
102/// }
103/// ```
104///
105/// ## With Custom Expressions
106///
107/// ```ignore
108/// use crate::color_tokens::with_alpha;
109///
110/// #[derive(Debug, Clone, ComponentTheme)]
111/// pub struct TooltipTheme {
112/// #[theme(default = 0x2a2a2aff, from = surface)]
113/// pub background: Rgba,
114///
115/// // Use with_alpha helper for transparency
116/// #[theme(default = 0x007acc33, from_expr = "with_alpha(theme.accent, 0.2)")]
117/// pub highlight: Rgba,
118///
119/// // Derived from another theme field
120/// #[theme(default = 0x888888, from_expr = "darken(theme.text_secondary, 0.1)")]
121/// pub shadow: Rgba,
122/// }
123/// ```
124///
125/// ## With Non-Color Fields
126///
127/// ```ignore
128/// #[derive(Debug, Clone, ComponentTheme)]
129/// pub struct FadeTheme {
130/// #[theme(default = 0xffffff, from = text_primary)]
131/// pub color: Rgba,
132///
133/// #[theme(default_f32 = 0.5, from_expr = "0.5")]
134/// pub disabled_opacity: f32,
135///
136/// #[theme(default_expr = "None", from_expr = "None")]
137/// pub optional_accent: Option<Rgba>,
138/// }
139/// ```
140///
141/// # Generated Code
142///
143/// For a theme struct `MyTheme`, this macro generates:
144///
145/// ```ignore
146/// impl Default for MyTheme {
147/// fn default() -> Self {
148/// Self {
149/// // Fields initialized with default values
150/// }
151/// }
152/// }
153///
154/// impl From<&crate::theme::Theme> for MyTheme {
155/// fn from(theme: &crate::theme::Theme) -> Self {
156/// Self {
157/// // Fields mapped from global theme
158/// }
159/// }
160/// }
161/// ```
162///
163/// # Common Patterns
164///
165/// ## Creating a theme from global state
166///
167/// ```ignore
168/// fn render(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
169/// let global_theme = cx.theme();
170/// let button_theme = ButtonTheme::from(&global_theme);
171/// // or use the default
172/// let default_theme = ButtonTheme::default();
173/// }
174/// ```
175///
176/// ## Customizing specific fields
177///
178/// ```ignore
179/// let mut theme = ButtonTheme::from(&cx.theme());
180/// theme.background = rgb(0xff0000); // Override just the background
181/// ```
182///
183/// # Compile Errors
184///
185/// The macro will panic at compile time if:
186/// - A field is missing the `#[theme(...)]` attribute
187/// - A field is missing `default`, `default_f32`, or `default_expr`
188/// - A field is missing `from` or `from_expr`
189/// - An expression in `from_expr` or `default_expr` fails to parse
190#[proc_macro_derive(ComponentTheme, attributes(theme))]
191pub fn derive_component_theme(input: TokenStream) -> TokenStream {
192 let input = parse_macro_input!(input as DeriveInput);
193 let name = &input.ident;
194
195 let fields = match &input.data {
196 Data::Struct(data) => match &data.fields {
197 Fields::Named(fields) => &fields.named,
198 _ => panic!("ComponentTheme only supports structs with named fields"),
199 },
200 _ => panic!("ComponentTheme only supports structs"),
201 };
202
203 let mut default_fields = Vec::new();
204 let mut from_fields = Vec::new();
205
206 for field in fields {
207 let field_name = field.ident.as_ref().unwrap();
208
209 // Find the #[theme(...)] attribute
210 let theme_attr = field
211 .attrs
212 .iter()
213 .find(|attr| attr.path().is_ident("theme"));
214
215 let Some(attr) = theme_attr else {
216 panic!("Field `{}` is missing #[theme(...)] attribute", field_name);
217 };
218
219 let mut default_value: Option<u32> = None;
220 let mut default_f32: Option<f64> = None;
221 let mut default_expr_str: Option<String> = None;
222 let mut from_field: Option<syn::Ident> = None;
223 let mut from_expr: Option<String> = None;
224
225 // Parse the attribute arguments
226 let nested = attr
227 .parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
228 .expect("Failed to parse theme attribute");
229
230 for meta in nested {
231 match meta {
232 Meta::NameValue(nv) => {
233 let ident = nv.path.get_ident().expect("Expected identifier");
234 match ident.to_string().as_str() {
235 "default" => {
236 if let Expr::Lit(lit) = &nv.value
237 && let Lit::Int(int_lit) = &lit.lit
238 {
239 default_value = Some(int_lit.base10_parse().unwrap());
240 }
241 }
242 "default_f32" => {
243 if let Expr::Lit(lit) = &nv.value {
244 match &lit.lit {
245 Lit::Float(f) => {
246 default_f32 = Some(f.base10_parse().unwrap());
247 }
248 Lit::Int(i) => {
249 // Allow integers like 0 or 1
250 default_f32 = Some(i.base10_parse::<i64>().unwrap() as f64);
251 }
252 _ => {}
253 }
254 }
255 }
256 "default_expr" => {
257 if let Expr::Lit(lit) = &nv.value
258 && let Lit::Str(s) = &lit.lit
259 {
260 default_expr_str = Some(s.value());
261 }
262 }
263 "from" => {
264 if let Expr::Path(path) = &nv.value {
265 from_field = path.path.get_ident().cloned();
266 }
267 }
268 "from_expr" => {
269 if let Expr::Lit(lit) = &nv.value
270 && let Lit::Str(s) = &lit.lit
271 {
272 from_expr = Some(s.value());
273 }
274 }
275 _ => panic!("Unknown theme attribute: {}", ident),
276 }
277 }
278 _ => panic!("Expected name = value in theme attribute"),
279 }
280 }
281
282 // Generate Default field based on type
283 if let Some(expr_str) = default_expr_str {
284 // Arbitrary expression (for Option types, nested themes, etc.)
285 let expr: syn::Expr = syn::parse_str(&expr_str).unwrap_or_else(|_| {
286 panic!("Failed to parse default_expr for field `{}`", field_name)
287 });
288 default_fields.push(quote! {
289 #field_name: #expr
290 });
291 } else if let Some(f32_val) = default_f32 {
292 // f32 field
293 default_fields.push(quote! {
294 #field_name: #f32_val as f32
295 });
296 } else if let Some(default_val) = default_value {
297 // Check if it's RGB (6 hex digits) or RGBA (8 hex digits)
298 let default_expr = if default_val > 0xFFFFFF {
299 // RGBA - use rgba()
300 quote! { gpui::rgba(#default_val) }
301 } else {
302 // RGB - use rgb()
303 quote! { gpui::rgb(#default_val) }
304 };
305
306 default_fields.push(quote! {
307 #field_name: #default_expr
308 });
309 } else {
310 panic!(
311 "Field `{}` is missing `default`, `default_f32`, or `default_expr` in #[theme(...)]",
312 field_name
313 );
314 }
315
316 // Generate From<&Theme> field
317 if let Some(expr_str) = from_expr {
318 let expr: syn::Expr = syn::parse_str(&expr_str)
319 .unwrap_or_else(|_| panic!("Failed to parse from_expr for field `{}`", field_name));
320 from_fields.push(quote! {
321 #field_name: #expr
322 });
323 } else if let Some(from) = from_field {
324 from_fields.push(quote! {
325 #field_name: theme.#from
326 });
327 } else {
328 panic!(
329 "Field `{}` needs either `from` or `from_expr` in #[theme(...)]",
330 field_name
331 );
332 }
333 }
334
335 let expanded = quote! {
336 impl Default for #name {
337 fn default() -> Self {
338 Self {
339 #(#default_fields),*
340 }
341 }
342 }
343
344 impl From<&crate::theme::Theme> for #name {
345 fn from(theme: &crate::theme::Theme) -> Self {
346 Self {
347 #(#from_fields),*
348 }
349 }
350 }
351 };
352
353 TokenStream::from(expanded)
354}