1use bevy::color::palettes::{basic, css};
11use bevy::color::{Color, Srgba};
12
13pub fn parse_css_color(input: &str) -> Option<Srgba> {
23 let s = input.trim();
24 if s.is_empty() {
25 return None;
26 }
27
28 if let Some(open) = s.find('(') {
30 if let Some(inner) = s.strip_suffix(')') {
31 let name = s[..open].trim().to_ascii_lowercase();
32 return parse_color_fn(&name, &inner[open + 1..]);
33 }
34 return None;
35 }
36
37 let lower = s.to_ascii_lowercase();
38 if lower == "transparent" {
39 return Some(Srgba::NONE);
40 }
41 if let Some(c) = named_color(&lower) {
42 return Some(c);
43 }
44
45 Srgba::hex(s).ok()
48}
49
50fn parse_color_fn(name: &str, inner: &str) -> Option<Srgba> {
53 let (comps, slash_alpha) = split_args(inner);
54 let alpha_tok = slash_alpha.or_else(|| comps.get(3).copied());
57 let a = match alpha_tok {
58 Some(t) => parse_alpha(t)?,
59 None => 1.0,
60 };
61 let c0 = comps.first().copied()?;
62 let c1 = comps.get(1).copied()?;
63 let c2 = comps.get(2).copied()?;
64
65 let color: Color = match name {
66 "rgb" | "rgba" => {
67 return Some(Srgba::new(
68 parse_rgb_channel(c0)?,
69 parse_rgb_channel(c1)?,
70 parse_rgb_channel(c2)?,
71 a,
72 ));
73 }
74 "hsl" | "hsla" => {
75 bevy::color::Hsla::new(parse_hue(c0)?, parse_fraction(c1)?, parse_fraction(c2)?, a)
76 .into()
77 }
78 "hwb" => {
79 bevy::color::Hwba::new(parse_hue(c0)?, parse_fraction(c1)?, parse_fraction(c2)?, a)
80 .into()
81 }
82 "oklab" => {
83 bevy::color::Oklaba::new(parse_fraction(c0)?, parse_num(c1)?, parse_num(c2)?, a).into()
84 }
85 "oklch" => {
86 bevy::color::Oklcha::new(parse_fraction(c0)?, parse_num(c1)?, parse_hue(c2)?, a).into()
87 }
88 _ => return None,
89 };
90 Some(color.to_srgba())
91}
92
93fn split_args(inner: &str) -> (Vec<&str>, Option<&str>) {
97 let (comp_part, slash_alpha) = match inner.split_once('/') {
98 Some((c, a)) => (c, Some(a.trim())),
99 None => (inner, None),
100 };
101 let comps = comp_part
102 .split(|c: char| c == ',' || c.is_whitespace())
103 .map(str::trim)
104 .filter(|t| !t.is_empty())
105 .collect();
106 (comps, slash_alpha)
107}
108
109fn parse_num(tok: &str) -> Option<f32> {
111 tok.parse().ok()
112}
113
114fn parse_fraction(tok: &str) -> Option<f32> {
117 match tok.strip_suffix('%') {
118 Some(p) => p.trim().parse::<f32>().ok().map(|v| v / 100.0),
119 None => tok.parse().ok(),
120 }
121}
122
123fn parse_rgb_channel(tok: &str) -> Option<f32> {
125 match tok.strip_suffix('%') {
126 Some(p) => p.trim().parse::<f32>().ok().map(|v| v / 100.0),
127 None => tok.parse::<f32>().ok().map(|v| v / 255.0),
128 }
129}
130
131fn parse_alpha(tok: &str) -> Option<f32> {
133 match tok.strip_suffix('%') {
134 Some(p) => p.trim().parse::<f32>().ok().map(|v| v / 100.0),
135 None => tok.parse().ok(),
136 }
137}
138
139fn parse_hue(tok: &str) -> Option<f32> {
141 tok.strip_suffix("deg").unwrap_or(tok).trim().parse().ok()
142}
143
144fn named_color(name: &str) -> Option<Srgba> {
148 let c = match name {
149 "aqua" => basic::AQUA,
151 "black" => basic::BLACK,
152 "blue" => basic::BLUE,
153 "fuchsia" => basic::FUCHSIA,
154 "gray" => basic::GRAY,
155 "green" => basic::GREEN,
156 "lime" => basic::LIME,
157 "maroon" => basic::MAROON,
158 "navy" => basic::NAVY,
159 "olive" => basic::OLIVE,
160 "purple" => basic::PURPLE,
161 "red" => basic::RED,
162 "silver" => basic::SILVER,
163 "teal" => basic::TEAL,
164 "white" => basic::WHITE,
165 "yellow" => basic::YELLOW,
166 "aliceblue" => css::ALICE_BLUE,
168 "antiquewhite" => css::ANTIQUE_WHITE,
169 "aquamarine" => css::AQUAMARINE,
170 "azure" => css::AZURE,
171 "beige" => css::BEIGE,
172 "bisque" => css::BISQUE,
173 "blanchedalmond" => css::BLANCHED_ALMOND,
174 "blueviolet" => css::BLUE_VIOLET,
175 "brown" => css::BROWN,
176 "burlywood" => css::BURLYWOOD,
177 "cadetblue" => css::CADET_BLUE,
178 "chartreuse" => css::CHARTREUSE,
179 "chocolate" => css::CHOCOLATE,
180 "coral" => css::CORAL,
181 "cornflowerblue" => css::CORNFLOWER_BLUE,
182 "cornsilk" => css::CORNSILK,
183 "crimson" => css::CRIMSON,
184 "darkblue" => css::DARK_BLUE,
185 "darkcyan" => css::DARK_CYAN,
186 "darkgoldenrod" => css::DARK_GOLDENROD,
187 "darkgray" => css::DARK_GRAY,
188 "darkgreen" => css::DARK_GREEN,
189 "darkgrey" => css::DARK_GREY,
190 "darkkhaki" => css::DARK_KHAKI,
191 "darkmagenta" => css::DARK_MAGENTA,
192 "darkolivegreen" => css::DARK_OLIVEGREEN,
193 "darkorange" => css::DARK_ORANGE,
194 "darkorchid" => css::DARK_ORCHID,
195 "darkred" => css::DARK_RED,
196 "darksalmon" => css::DARK_SALMON,
197 "darkseagreen" => css::DARK_SEA_GREEN,
198 "darkslateblue" => css::DARK_SLATE_BLUE,
199 "darkslategray" => css::DARK_SLATE_GRAY,
200 "darkslategrey" => css::DARK_SLATE_GREY,
201 "darkturquoise" => css::DARK_TURQUOISE,
202 "darkviolet" => css::DARK_VIOLET,
203 "deeppink" => css::DEEP_PINK,
204 "deepskyblue" => css::DEEP_SKY_BLUE,
205 "dimgray" => css::DIM_GRAY,
206 "dimgrey" => css::DIM_GREY,
207 "dodgerblue" => css::DODGER_BLUE,
208 "firebrick" => css::FIRE_BRICK,
209 "floralwhite" => css::FLORAL_WHITE,
210 "forestgreen" => css::FOREST_GREEN,
211 "gainsboro" => css::GAINSBORO,
212 "ghostwhite" => css::GHOST_WHITE,
213 "gold" => css::GOLD,
214 "goldenrod" => css::GOLDENROD,
215 "greenyellow" => css::GREEN_YELLOW,
216 "grey" => css::GREY,
217 "honeydew" => css::HONEYDEW,
218 "hotpink" => css::HOT_PINK,
219 "indianred" => css::INDIAN_RED,
220 "indigo" => css::INDIGO,
221 "ivory" => css::IVORY,
222 "khaki" => css::KHAKI,
223 "lavender" => css::LAVENDER,
224 "lavenderblush" => css::LAVENDER_BLUSH,
225 "lawngreen" => css::LAWN_GREEN,
226 "lemonchiffon" => css::LEMON_CHIFFON,
227 "lightblue" => css::LIGHT_BLUE,
228 "lightcoral" => css::LIGHT_CORAL,
229 "lightcyan" => css::LIGHT_CYAN,
230 "lightgoldenrodyellow" => css::LIGHT_GOLDENROD_YELLOW,
231 "lightgray" => css::LIGHT_GRAY,
232 "lightgreen" => css::LIGHT_GREEN,
233 "lightgrey" => css::LIGHT_GREY,
234 "lightpink" => css::LIGHT_PINK,
235 "lightsalmon" => css::LIGHT_SALMON,
236 "lightseagreen" => css::LIGHT_SEA_GREEN,
237 "lightskyblue" => css::LIGHT_SKY_BLUE,
238 "lightslategray" => css::LIGHT_SLATE_GRAY,
239 "lightslategrey" => css::LIGHT_SLATE_GREY,
240 "lightsteelblue" => css::LIGHT_STEEL_BLUE,
241 "lightyellow" => css::LIGHT_YELLOW,
242 "limegreen" => css::LIMEGREEN,
243 "linen" => css::LINEN,
244 "magenta" => css::MAGENTA,
245 "mediumaquamarine" => css::MEDIUM_AQUAMARINE,
246 "mediumblue" => css::MEDIUM_BLUE,
247 "mediumorchid" => css::MEDIUM_ORCHID,
248 "mediumpurple" => css::MEDIUM_PURPLE,
249 "mediumseagreen" => css::MEDIUM_SEA_GREEN,
250 "mediumslateblue" => css::MEDIUM_SLATE_BLUE,
251 "mediumspringgreen" => css::MEDIUM_SPRING_GREEN,
252 "mediumturquoise" => css::MEDIUM_TURQUOISE,
253 "mediumvioletred" => css::MEDIUM_VIOLET_RED,
254 "midnightblue" => css::MIDNIGHT_BLUE,
255 "mintcream" => css::MINT_CREAM,
256 "mistyrose" => css::MISTY_ROSE,
257 "moccasin" => css::MOCCASIN,
258 "navajowhite" => css::NAVAJO_WHITE,
259 "oldlace" => css::OLD_LACE,
260 "olivedrab" => css::OLIVE_DRAB,
261 "orange" => css::ORANGE,
262 "orangered" => css::ORANGE_RED,
263 "orchid" => css::ORCHID,
264 "palegoldenrod" => css::PALE_GOLDENROD,
265 "palegreen" => css::PALE_GREEN,
266 "paleturquoise" => css::PALE_TURQUOISE,
267 "palevioletred" => css::PALE_VIOLETRED,
268 "papayawhip" => css::PAPAYA_WHIP,
269 "peachpuff" => css::PEACHPUFF,
270 "peru" => css::PERU,
271 "pink" => css::PINK,
272 "plum" => css::PLUM,
273 "powderblue" => css::POWDER_BLUE,
274 "rebeccapurple" => css::REBECCA_PURPLE,
275 "rosybrown" => css::ROSY_BROWN,
276 "royalblue" => css::ROYAL_BLUE,
277 "saddlebrown" => css::SADDLE_BROWN,
278 "salmon" => css::SALMON,
279 "sandybrown" => css::SANDY_BROWN,
280 "seagreen" => css::SEA_GREEN,
281 "seashell" => css::SEASHELL,
282 "sienna" => css::SIENNA,
283 "skyblue" => css::SKY_BLUE,
284 "slateblue" => css::SLATE_BLUE,
285 "slategray" => css::SLATE_GRAY,
286 "slategrey" => css::SLATE_GREY,
287 "snow" => css::SNOW,
288 "springgreen" => css::SPRING_GREEN,
289 "steelblue" => css::STEEL_BLUE,
290 "tan" => css::TAN,
291 "thistle" => css::THISTLE,
292 "tomato" => css::TOMATO,
293 "turquoise" => css::TURQUOISE,
294 "violet" => css::VIOLET,
295 "wheat" => css::WHEAT,
296 "whitesmoke" => css::WHITE_SMOKE,
297 "yellowgreen" => css::YELLOW_GREEN,
298 "cyan" => basic::AQUA,
300 _ => return None,
301 };
302 Some(c)
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 fn rgba8(s: &str) -> [u8; 4] {
310 let c = parse_css_color(s).expect("parsed");
311 [
312 (c.red.clamp(0.0, 1.0) * 255.0).round() as u8,
313 (c.green.clamp(0.0, 1.0) * 255.0).round() as u8,
314 (c.blue.clamp(0.0, 1.0) * 255.0).round() as u8,
315 (c.alpha.clamp(0.0, 1.0) * 255.0).round() as u8,
316 ]
317 }
318
319 #[test]
320 fn hex_forms() {
321 assert_eq!(rgba8("#ff0000"), [255, 0, 0, 255]);
322 assert_eq!(rgba8("#f00"), [255, 0, 0, 255]);
323 assert_eq!(rgba8("#00ff0080"), [0, 255, 0, 128]);
324 assert_eq!(rgba8("ff0000"), [255, 0, 0, 255]); }
326
327 #[test]
328 fn named_and_transparent() {
329 assert_eq!(rgba8("red"), [255, 0, 0, 255]);
330 assert_eq!(rgba8("RED"), [255, 0, 0, 255]); assert_eq!(rgba8("rebeccapurple"), [102, 51, 153, 255]);
332 assert_eq!(rgba8("cyan"), rgba8("aqua"));
333 assert_eq!(parse_css_color("transparent"), Some(Srgba::NONE));
334 }
335
336 #[test]
337 fn rgb_functions() {
338 assert_eq!(rgba8("rgb(255, 0, 0)"), [255, 0, 0, 255]);
339 assert_eq!(rgba8("rgb(255 0 0)"), [255, 0, 0, 255]);
340 assert_eq!(rgba8("rgba(255, 0, 0, 0.5)"), [255, 0, 0, 128]);
341 assert_eq!(rgba8("rgb(255 0 0 / 50%)"), [255, 0, 0, 128]);
342 assert_eq!(rgba8("rgb(100%, 0%, 0%)"), [255, 0, 0, 255]);
343 }
344
345 #[test]
346 fn hsl_functions() {
347 assert_eq!(rgba8("hsl(0, 100%, 50%)"), [255, 0, 0, 255]);
348 assert_eq!(rgba8("hsl(120 100% 50%)"), [0, 255, 0, 255]);
349 assert_eq!(rgba8("hsla(0, 100%, 50%, 0.5)"), [255, 0, 0, 128]);
350 }
351
352 #[test]
353 fn invalid_is_none() {
354 assert_eq!(parse_css_color("notacolor"), None);
355 assert_eq!(parse_css_color("rgb(1 2)"), None);
356 assert_eq!(parse_css_color(""), None);
357 }
358}