weirdboi_bevy_colour_macros/
lib.rs1use proc_macro::TokenStream;
4use proc_macro2::Span;
5use quote::{format_ident, quote};
6use syn::braced;
7use syn::token::{Colon, Comma};
8use syn::{
9 Ident, LitStr, Result, parenthesized,
10 parse::{Parse, ParseStream},
11 parse_macro_input,
12};
13
14struct ColorDef {
16 name: String,
17 r: f32,
18 g: f32,
19 b: f32,
20}
21
22struct PaletteDef {
24 name: Ident,
25 colors: Vec<ColorDef>,
26}
27
28impl Parse for ColorDef {
30 fn parse(input: ParseStream) -> Result<Self> {
31 let name_lit = input.parse::<LitStr>()?;
33 let name = name_lit.value();
34
35 input.parse::<Colon>()?;
37
38 let content;
40 parenthesized!(content in input);
41
42 let r = content.parse::<syn::LitFloat>()?.base10_parse()?;
43 content.parse::<Comma>()?;
44 let g = content.parse::<syn::LitFloat>()?.base10_parse()?;
45 content.parse::<Comma>()?;
46 let b = content.parse::<syn::LitFloat>()?.base10_parse()?;
47
48 Ok(ColorDef { name, r, g, b })
49 }
50}
51
52impl Parse for PaletteDef {
54 fn parse(input: ParseStream) -> Result<Self> {
55 let name = input.parse::<Ident>()?;
57
58 let content;
60 braced!(content in input);
61
62 let mut colors = Vec::new();
64 while !content.is_empty() {
65 colors.push(content.parse::<ColorDef>()?);
66
67 if content.peek(Comma) {
69 content.parse::<Comma>()?;
70 } else if !content.is_empty() {
71 return Err(content.error("expected comma or end of block"));
72 }
73 }
74
75 Ok(PaletteDef { name, colors })
76 }
77}
78
79fn to_upper_snake_case(s: &str) -> String {
81 let mut result = String::new();
82 for (i, c) in s.chars().enumerate() {
83 if c.is_uppercase() && i > 0 && !s.chars().nth(i - 1).unwrap_or(' ').is_uppercase() {
84 result.push('_');
85 }
86 result.push(c.to_ascii_uppercase());
87 }
88 result
89}
90
91fn to_lower_snake_case(s: &str) -> String {
93 let mut result = String::new();
94 for (i, c) in s.chars().enumerate() {
95 if c.is_uppercase() {
96 if i > 0 && !s.chars().nth(i - 1).unwrap_or(' ').is_uppercase() {
97 result.push('_');
98 }
99 result.push(c.to_ascii_lowercase());
100 } else {
101 result.push(c);
102 }
103 }
104 result
105}
106
107#[proc_macro]
143pub fn palette(input: TokenStream) -> TokenStream {
144 let palette_def = parse_macro_input!(input as PaletteDef);
146
147 let palette_name = &palette_def.name;
149 let bevy_color = quote! { ::bevy::color::Color };
150
151 let mut const_defs = Vec::new();
153 let mut method_defs = Vec::new();
154 let mut get_color_match_arms = Vec::new();
155 let mut color_values = Vec::new();
156 let mut doc_grid_entry = Vec::new();
157 let mut color_rgb = Vec::new();
158
159 for color in &palette_def.colors {
160 let color_name = &color.name;
161 let normalised = normalize_color_name(color_name);
162 let const_name = Ident::new(&to_upper_snake_case(color_name), Span::call_site());
163 let method_name = format_ident!("{}", to_lower_snake_case(color_name));
164
165 let r = color.r;
166 let g = color.g;
167 let b = color.b;
168
169 let current_rgb = format!(
170 "rgb({:.0}%, {:.0}%, {:.0}%)",
171 r * 100.0,
172 g * 100.0,
173 b * 100.0
174 );
175
176 let rustdoc =
177 format!(r#"<div style="background-color: {current_rgb}; height: 20px"></div>"#,);
178
179 let funcdoc =
180 format!(r#"Returns the value of [{palette_name}::{const_name}]<br/>{rustdoc}"#,);
181 color_rgb.push(current_rgb);
182
183 const_defs.push(quote! {
185 #[doc = #rustdoc]
186 pub const #const_name: #bevy_color = #bevy_color::srgb(#r, #g, #b);
187 });
188
189 method_defs.push(quote! {
191 #[doc = #funcdoc]
192 pub const fn #method_name() -> #bevy_color {
193 Self::#const_name
194 }
195 });
196
197 get_color_match_arms.push(quote! {
199 #normalised => Some(Self::#const_name),
200 });
201
202 color_values.push(quote! {
204 Self::#const_name,
205 });
206
207 doc_grid_entry.push(format!(
208 r#"<div style="background-color: rgb({:.0}% {:.0}% {:.0}%); width: 20px; height: 20px;"></div>"#,
209 r * 100.0,
210 g * 100.0,
211 b * 100.0
212 ));
213 }
214
215 let num_colors = palette_def.colors.len();
217 let num_colors_lit = proc_macro2::Literal::usize_unsuffixed(num_colors);
218 let iter_type = quote! { ::core::array::IntoIter<#bevy_color, #num_colors_lit> };
219
220 let root_doc = format!(
221 r#"<span>The {palette_name} palette, containing {num_colors} colors.</span> <br />
222 <div style="display: grid; grid-template-columns: repeat(8, 20px); grid-auto-rows: 20px;">{}</div>"#,
223 doc_grid_entry.join("\n")
224 );
225
226 let expanded = quote! {
228 #[doc = #root_doc]
229 #[derive(::core::fmt::Debug, ::core::clone::Clone, ::core::marker::Copy)]
230 pub struct #palette_name;
231
232 impl #palette_name {
233 #(#const_defs)*
234
235 #(#method_defs)*
236
237 #[doc(hidden)]
239 fn normalize_color_name(s: &str) -> String {
240 s.chars()
241 .filter(|c| c.is_alphanumeric())
242 .map(|c| c.to_ascii_lowercase())
243 .collect()
244 }
245
246 pub const fn all() -> [#bevy_color; #num_colors_lit] {
248 [#(#color_values)*]
249 }
250
251 pub const fn len() -> usize {
253 #num_colors_lit
254 }
255
256 pub fn iter() -> impl Iterator<Item = #bevy_color> {
258 Self::all().into_iter()
259 }
260
261 pub fn get(name: &str) -> Option<#bevy_color> {
263 let name = Self::normalize_color_name(name);
264 match name.as_str() {
265 #(#get_color_match_arms)*
266 _ => None,
267 }
268 }
269 }
270
271 impl IntoIterator for #palette_name {
272 type Item = #bevy_color;
273 type IntoIter = #iter_type;
274
275 fn into_iter(self) -> Self::IntoIter {
276 Self::all().into_iter()
277 }
278 }
279
280 impl<'a> IntoIterator for &'a #palette_name {
281 type Item = #bevy_color;
282 type IntoIter = #iter_type;
283
284 fn into_iter(self) -> Self::IntoIter {
285 #palette_name::all().into_iter()
286 }
287 }
288 };
289
290 expanded.into()
292}
293
294fn normalize_color_name(s: &str) -> String {
295 s.chars()
296 .filter(|c| c.is_alphanumeric())
297 .map(|c| c.to_ascii_lowercase())
298 .collect()
299}