1use proc_macro::TokenStream;
4use proc_macro_crate::{FoundCrate, crate_name};
5use proc_macro2::Span;
6use quote::{format_ident, quote};
7use syn::braced;
8use syn::token::{Colon, Comma};
9use syn::{
10 Ident, LitStr, Result, parenthesized,
11 parse::{Parse, ParseStream},
12 parse_macro_input,
13};
14
15fn crate_root() -> proc_macro2::TokenStream {
17 match crate_name("bevy-color-palettes") {
18 Ok(FoundCrate::Itself) => quote!(crate),
19 Ok(FoundCrate::Name(name)) => {
20 let ident = Ident::new(&name, Span::call_site());
21 quote!(::#ident)
22 }
23 Err(_) => quote!(::bevy_color_palettes),
24 }
25}
26
27struct ColorDef {
29 name: String,
30 r8: u8,
31 g8: u8,
32 b8: u8,
33 a8: u8,
34}
35
36struct PaletteDef {
38 name: Ident,
39 colors: Vec<ColorDef>,
40}
41
42impl Parse for ColorDef {
44 fn parse(input: ParseStream) -> Result<Self> {
45 let name_lit = input.parse::<LitStr>()?;
47 let name = name_lit.value();
48
49 input.parse::<Colon>()?;
51
52 if input.peek(syn::LitStr) {
54 let lit = input.parse::<LitStr>()?;
55 let (r8, g8, b8, a8) = parse_hex_color(&lit.value(), lit.span())?;
56 Ok(ColorDef {
57 name,
58 r8,
59 g8,
60 b8,
61 a8,
62 })
63 } else {
64 let content;
66 parenthesized!(content in input);
67
68 let r: f32 = content.parse::<syn::LitFloat>()?.base10_parse()?;
69 content.parse::<Comma>()?;
70 let g: f32 = content.parse::<syn::LitFloat>()?.base10_parse()?;
71 content.parse::<Comma>()?;
72 let b: f32 = content.parse::<syn::LitFloat>()?.base10_parse()?;
73
74 #[allow(clippy::cast_possible_truncation)]
75 let r32 = (r * 255.0) as i32;
76 #[allow(clippy::cast_possible_truncation)]
77 let g32 = (g * 255.0) as i32;
78 #[allow(clippy::cast_possible_truncation)]
79 let b32 = (b * 255.0) as i32;
80
81 let r8 = if r32 <= 255 {
82 #[allow(clippy::cast_possible_truncation)]
83 #[allow(clippy::cast_sign_loss)]
84 if r32 >= 0 { r32 as u8 } else { 0 }
85 } else {
86 255
87 };
88 let g8 = if g32 <= 255 {
89 #[allow(clippy::cast_possible_truncation)]
90 #[allow(clippy::cast_sign_loss)]
91 if g32 >= 0 { g32 as u8 } else { 0 }
92 } else {
93 255
94 };
95 let b8 = if b32 <= 255 {
96 #[allow(clippy::cast_possible_truncation)]
97 #[allow(clippy::cast_sign_loss)]
98 if b32 >= 0 { b32 as u8 } else { 0 }
99 } else {
100 255
101 };
102
103 let a8 = 255;
104
105 Ok(ColorDef {
106 name,
107 r8,
108 g8,
109 b8,
110 a8,
111 })
112 }
113 }
114}
115
116fn parse_hex_color(html_hex_color_string: &str, span: Span) -> Result<(u8, u8, u8, u8)> {
117 let hex = html_hex_color_string
118 .strip_prefix('#')
119 .ok_or_else(|| syn::Error::new(span, "HTML hex color string must start with '#'."))?; let (r, g, b, a) = match hex.len() {
122 8 => {
123 let r = u8::from_str_radix(&hex[0..2], 16)
124 .map_err(|_| syn::Error::new(span, "#RRggbbaa RR was invalid."))?;
125 let g = u8::from_str_radix(&hex[2..4], 16)
126 .map_err(|_| syn::Error::new(span, "#rrGGbbaa GG was invalid."))?;
127 let b = u8::from_str_radix(&hex[4..6], 16)
128 .map_err(|_| syn::Error::new(span, "#rrggBBaa BB was invalid."))?;
129 let a = u8::from_str_radix(&hex[6..8], 16)
130 .map_err(|_| syn::Error::new(span, "#rrggbbAA AA was invalid."))?;
131 (r, g, b, a)
132 }
133 6 => {
134 let r = u8::from_str_radix(&hex[0..2], 16)
135 .map_err(|_| syn::Error::new(span, "#RRggbb RR was invalid."))?;
136 let g = u8::from_str_radix(&hex[2..4], 16)
137 .map_err(|_| syn::Error::new(span, "#rrGGbb GG was invalid."))?;
138 let b = u8::from_str_radix(&hex[4..6], 16)
139 .map_err(|_| syn::Error::new(span, "#rrggBB BB was invalid."))?;
140 (r, g, b, 255_u8)
141 }
142 4 => {
143 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16)
144 .map_err(|_| syn::Error::new(span, "#Rgba R was invalid."))?;
145 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16)
146 .map_err(|_| syn::Error::new(span, "#rGba G was invalid."))?;
147 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16)
148 .map_err(|_| syn::Error::new(span, "#rgBa B was invalid."))?;
149 let a = u8::from_str_radix(&hex[3..4].repeat(2), 16)
150 .map_err(|_| syn::Error::new(span, "#rgbA A was invalid."))?;
151 (r, g, b, a)
152 }
153 3 => {
154 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16)
155 .map_err(|_| syn::Error::new(span, "#Rgb R was invalid."))?;
156 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16)
157 .map_err(|_| syn::Error::new(span, "#rGb G was invalid."))?;
158 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16)
159 .map_err(|_| syn::Error::new(span, "#rgB B was invalid."))?;
160 (r, g, b, 255_u8)
161 }
162 _ => {
163 return Err(syn::Error::new(
164 span,
165 "Hex color must be in #rrggbb, #rrggbbaa, #rgb, or #rgba format.",
166 ));
167 }
168 };
169
170 Ok((r, g, b, a))
171}
172
173impl Parse for PaletteDef {
175 fn parse(input: ParseStream) -> Result<Self> {
176 let name = input.parse::<Ident>()?;
178
179 let content;
181 braced!(content in input);
182
183 let mut colors = Vec::new();
185 while !content.is_empty() {
186 colors.push(content.parse::<ColorDef>()?);
187
188 if content.peek(Comma) {
190 content.parse::<Comma>()?;
191 } else if !content.is_empty() {
192 return Err(content.error("Expected comma or end of block."));
193 }
194 }
195
196 Ok(PaletteDef { name, colors })
197 }
198}
199
200fn to_upper_snake_case(s: &str) -> String {
202 let mut result = String::new();
203 for (i, c) in s.chars().enumerate() {
204 if c.is_uppercase() && i > 0 && !s.chars().nth(i - 1).unwrap_or(' ').is_uppercase() {
205 result.push('_');
206 }
207 result.push(c.to_ascii_uppercase());
208 }
209 result
210}
211
212fn to_lower_snake_case(s: &str) -> String {
214 let mut result = String::new();
215 for (i, c) in s.chars().enumerate() {
216 if c.is_uppercase() {
217 if i > 0 && !s.chars().nth(i - 1).unwrap_or(' ').is_uppercase() {
218 result.push('_');
219 }
220 result.push(c.to_ascii_lowercase());
221 } else {
222 result.push(c);
223 }
224 }
225 result
226}
227
228#[allow(clippy::too_many_lines)]
264#[proc_macro]
265pub fn palette(input: TokenStream) -> TokenStream {
266 let palette_def = parse_macro_input!(input as PaletteDef);
268
269 let palette_name = &palette_def.name;
271 let crate_root = crate_root();
272 let crate_color = quote! { #crate_root::color::Color };
273
274 let mut const_defs = Vec::new();
276 let mut method_defs = Vec::new();
277 let mut get_color_match_arms = Vec::new();
278 let mut color_values = Vec::new();
279 let mut doc_grid_entry = Vec::new();
280 let mut color_rgba = Vec::new();
281
282 for color in &palette_def.colors {
283 let color_name = &color.name;
284 let normalised = normalize_color_name(color_name);
285 let const_name = Ident::new(&to_upper_snake_case(color_name), Span::call_site());
286 let method_name = format_ident!("{}", to_lower_snake_case(color_name));
287
288 let r8 = color.r8;
289 let g8 = color.g8;
290 let b8 = color.b8;
291 let a8 = color.a8;
292
293 let current_rgba = format!(
294 "rgba({:.0}%, {:.0}%, {:.0}%, {:.2})",
295 f32::from(r8) * (100.0 / 255.0),
296 f32::from(g8) * (100.0 / 255.0),
297 f32::from(b8) * (100.0 / 255.0),
298 f32::from(a8) / 255.0,
299 );
300
301 let rustdoc =
302 format!(r#"<div style="background-color: {current_rgba}; height: 20px"></div>"#,);
303
304 let funcdoc =
305 format!(r"Returns the value of [{palette_name}::{const_name}]<br/>{rustdoc}",);
306 color_rgba.push(current_rgba);
307
308 const_defs.push(quote! {
310 #[doc = #rustdoc]
311 pub const #const_name: #crate_color = #crate_color::new(#r8, #g8, #b8, #a8);
312 });
313
314 method_defs.push(quote! {
316 #[doc = #funcdoc]
317 pub const fn #method_name() -> #crate_color {
318 Self::#const_name
319 }
320 });
321
322 get_color_match_arms.push(quote! {
324 #normalised => Some(Self::#const_name),
325 });
326
327 color_values.push(quote! {
329 Self::#const_name,
330 });
331
332 doc_grid_entry.push(format!(
333 r#"<div style="background-color: rgba({:.0}% {:.0}% {:.0}% {:.2}); width: 20px; height: 20px;"></div>"#,
334 f32::from(r8) * (100.0 / 255.0),
335 f32::from(g8) * (100.0 / 255.0),
336 f32::from(b8) * (100.0 / 255.0),
337 f32::from(a8) / 255.0,
338 ));
339 }
340
341 let num_colors = palette_def.colors.len();
343 let num_colors_lit = proc_macro2::Literal::usize_unsuffixed(num_colors);
344 let iter_type = quote! { ::core::array::IntoIter<#crate_color, #num_colors_lit> };
345
346 let root_doc = format!(
347 r#"<span>The {palette_name} palette, containing {num_colors} colors.</span> <br />
348 <div style="display: grid; grid-template-columns: repeat(8, 20px); grid-auto-rows: 20px;">{}</div>"#,
349 doc_grid_entry.join("\n")
350 );
351
352 let expanded = quote! {
354 #[doc = #root_doc]
355 #[derive(::core::fmt::Debug, ::core::clone::Clone, ::core::marker::Copy)]
356 pub struct #palette_name;
357
358 impl #palette_name {
359 #(#const_defs)*
360
361 #(#method_defs)*
362
363 #[doc(hidden)]
365 fn normalize_color_name(s: &str) -> String {
366 s.chars()
367 .filter(|c| c.is_alphanumeric())
368 .map(|c| c.to_ascii_lowercase())
369 .collect()
370 }
371
372 pub const fn all() -> [#crate_color; #num_colors_lit] {
374 [#(#color_values)*]
375 }
376
377 pub const fn len() -> usize {
379 #num_colors_lit
380 }
381
382 pub fn iter() -> impl Iterator<Item = #crate_color> {
384 Self::all().into_iter()
385 }
386
387 pub fn get(name: &str) -> Option<#crate_color> {
389 let name = Self::normalize_color_name(name);
390 match name.as_str() {
391 #(#get_color_match_arms)*
392 _ => None,
393 }
394 }
395 }
396
397 impl IntoIterator for #palette_name {
398 type Item = #crate_color;
399 type IntoIter = #iter_type;
400
401 fn into_iter(self) -> Self::IntoIter {
402 Self::all().into_iter()
403 }
404 }
405
406 impl<'a> IntoIterator for &'a #palette_name {
407 type Item = #crate_color;
408 type IntoIter = #iter_type;
409
410 fn into_iter(self) -> Self::IntoIter {
411 #palette_name::all().into_iter()
412 }
413 }
414 };
415
416 expanded.into()
418}
419
420fn normalize_color_name(s: &str) -> String {
421 s.chars()
422 .filter(|c| c.is_alphanumeric())
423 .map(|c| c.to_ascii_lowercase())
424 .collect()
425}