weirdboi_bevy_colour_macros/
lib.rs

1//! Procedural macros for weirdboi_bevy_colour
2
3use 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
14/// A color definition with a name and RGB values
15struct ColorDef {
16	name: String,
17	r: f32,
18	g: f32,
19	b: f32,
20}
21
22/// A palette definition with a name and a list of color definitions
23struct PaletteDef {
24	name: Ident,
25	colors: Vec<ColorDef>,
26}
27
28/// Parse a color definition from a stream
29impl Parse for ColorDef {
30	fn parse(input: ParseStream) -> Result<Self> {
31		// Parse the color name as a string literal
32		let name_lit = input.parse::<LitStr>()?;
33		let name = name_lit.value();
34
35		// Parse the colon
36		input.parse::<Colon>()?;
37
38		// Parse the RGB tuple (r, g, b)
39		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
52/// Parse a palette definition from a stream
53impl Parse for PaletteDef {
54	fn parse(input: ParseStream) -> Result<Self> {
55		// Parse the palette name as an identifier
56		let name = input.parse::<Ident>()?;
57
58		// Parse the color definitions inside braces
59		let content;
60		braced!(content in input);
61
62		// Parse the color definitions
63		let mut colors = Vec::new();
64		while !content.is_empty() {
65			colors.push(content.parse::<ColorDef>()?);
66
67			// Parse the comma if there is one and we're not at the end
68			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
79/// Convert a string to UPPER_SNAKE_CASE
80fn 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
91/// Convert a string to lower_snake_case
92fn 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/// Generate a palette struct and implementation
108///
109/// # Example
110///
111/// ```
112/// use macros::palette;
113/// palette!(MyPalette {
114///     "red": (1.0, 0.0, 0.0),
115///     "green": (0.0, 1.0, 0.0),
116///     "blue": (0.0, 0.0, 1.0),
117/// });
118/// ```
119///
120/// This will generate:
121///
122/// ```no_run
123/// pub struct MyPalette;
124///
125/// impl MyPalette {
126///     /// RED; <div style="background-color: rgb(100% 0% 0%); height: 20px"></div>
127///     pub const RED: Color = Color::rgb(1.0, 0.0, 0.0);
128///     /// GREEN; <div style="background-color: rgb(0% 100% 0%); height: 20px"></div>
129///     pub const GREEN: Color = Color::rgb(0.0, 1.0, 0.0);
130///     /// BLUE; <div style="background-color: rgb(0% 0% 100%); height: 20px"></div>
131///     pub const BLUE: Color = Color::rgb(0.0, 0.0, 1.0);
132///     
133///     pub fn red(&self) -> Color { Self::RED }
134///     pub fn green(&self) -> Color { Self::GREEN }
135///     pub fn blue(&self) -> Color { Self::BLUE }
136/// }
137///
138/// impl Palette for MyPalette {
139///     // Implementation of Palette trait
140/// }
141/// ```
142#[proc_macro]
143pub fn palette(input: TokenStream) -> TokenStream {
144	// Parse the input
145	let palette_def = parse_macro_input!(input as PaletteDef);
146
147	// Generate the struct definition
148	let palette_name = &palette_def.name;
149	let bevy_color = quote! { ::bevy::color::Color };
150
151	// Generate the color constants and methods
152	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		// Add the constant definition
184		const_defs.push(quote! {
185			#[doc = #rustdoc]
186			pub const #const_name: #bevy_color = #bevy_color::srgb(#r, #g, #b);
187		});
188
189		// Add the method definition (static, no &self)
190		method_defs.push(quote! {
191			#[doc = #funcdoc]
192			pub const fn #method_name() -> #bevy_color {
193				Self::#const_name
194			}
195		});
196
197		// Add the match arm for get_color
198		get_color_match_arms.push(quote! {
199			#normalised => Some(Self::#const_name),
200		});
201
202		// Add the color value for the iterator
203		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	// Get the number of colors
216	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	// Generate the final code
227	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			// Helper function to normalize color names for case-insensitive and format-agnostic comparison
238			#[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			/// Returns all colors in the palette as a fixed-size array
247			pub const fn all() -> [#bevy_color; #num_colors_lit] {
248				[#(#color_values)*]
249			}
250
251			/// Returns the number of colours in the palette
252			pub const fn len() -> usize {
253				#num_colors_lit
254			}
255
256			/// Returns an iterator over all colors in the palette
257			pub fn iter() -> impl Iterator<Item = #bevy_color> {
258				Self::all().into_iter()
259			}
260
261			/// Returns a color by name, if it exists in the palette
262			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	// Return the generated code
291	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}