1use crate::style::{ToCss, unexpected_token};
2use std::fmt::{self, Display};
3
4use color::{AlphaColor, ColorSpaceTag, DynamicColor, HueDirection, Srgb, parse_color};
5use cssparser::{
6 Parser, Token,
7 color::{parse_hash_color, parse_named_color},
8 match_ignore_ascii_case,
9};
10use image::Rgba;
11use tiny_skia::{ColorU8, PremultipliedColorU8};
12
13use crate::style::{
14 Animatable, Color as CurrentColor, CssDescriptorKind, CssSyntaxKind, CssToken, FromCss,
15 MakeComputed, ParseResult, PercentageNumber, SizingContext, fast_div_255,
16 properties::gradient_utils::interpolate_with_color_space, tw::TailwindPropertyParser,
17};
18
19fn is_cylindrical_color_space(color_space: ColorSpaceTag) -> bool {
20 matches!(
21 color_space,
22 ColorSpaceTag::Lch | ColorSpaceTag::Oklch | ColorSpaceTag::Hsl | ColorSpaceTag::Hwb
23 )
24}
25
26#[derive(Debug, Clone, Copy, PartialEq)]
28#[non_exhaustive]
29pub struct ColorInterpolationMethod {
30 pub color_space: ColorSpaceTag,
32 pub hue_direction: HueDirection,
34}
35
36impl Default for ColorInterpolationMethod {
37 fn default() -> Self {
38 Self {
39 color_space: ColorSpaceTag::Oklab,
42 hue_direction: HueDirection::Shorter,
43 }
44 }
45}
46
47impl<'i> FromCss<'i> for ColorInterpolationMethod {
48 fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
49 input.expect_ident_matching("in")?;
50
51 let location = input.current_source_location();
52 let token = input.next()?;
53 let Token::Ident(color_space_ident) = token else {
54 return Err(unexpected_token!(location, token));
55 };
56
57 let color_space = match_ignore_ascii_case! { &color_space_ident,
58 "srgb" => ColorSpaceTag::Srgb,
59 "srgb-linear" => ColorSpaceTag::LinearSrgb,
60 "lab" => ColorSpaceTag::Lab,
61 "oklab" => ColorSpaceTag::Oklab,
62 "lch" => ColorSpaceTag::Lch,
63 "oklch" => ColorSpaceTag::Oklch,
64 "hsl" => ColorSpaceTag::Hsl,
65 "hwb" => ColorSpaceTag::Hwb,
66 "display-p3" => ColorSpaceTag::DisplayP3,
67 "a98-rgb" => ColorSpaceTag::A98Rgb,
68 "prophoto-rgb" => ColorSpaceTag::ProphotoRgb,
69 "rec2020" => ColorSpaceTag::Rec2020,
70 "xyz" | "xyz-d65" => ColorSpaceTag::XyzD65,
71 "xyz-d50" => ColorSpaceTag::XyzD50,
72 _ => return Err(unexpected_token!(location, token)),
73 };
74
75 let mut hue_direction = HueDirection::Shorter;
76 let mut has_hue_direction = false;
77
78 if let Ok(direction) = input.try_parse(|input| -> ParseResult<'i, HueDirection> {
79 let location = input.current_source_location();
80 let token = input.next()?;
81 let Token::Ident(ident) = token else {
82 return Err(unexpected_token!(location, token));
83 };
84
85 let direction = match_ignore_ascii_case! { &ident,
86 "shorter" => HueDirection::Shorter,
87 "longer" => HueDirection::Longer,
88 "increasing" => HueDirection::Increasing,
89 "decreasing" => HueDirection::Decreasing,
90 _ => return Err(unexpected_token!(location, token)),
91 };
92
93 input.expect_ident_matching("hue")?;
94
95 Ok(direction)
96 }) {
97 hue_direction = direction;
98 has_hue_direction = true;
99 }
100
101 if has_hue_direction && !is_cylindrical_color_space(color_space) {
102 return Err(input.new_error_for_next_token());
103 }
104
105 Ok(Self {
106 color_space,
107 hue_direction,
108 })
109 }
110
111 const VALID_TOKENS: &'static [CssToken] =
112 &[CssToken::Descriptor(CssDescriptorKind::InColorSpace)];
113}
114
115impl ToCss for ColorInterpolationMethod {
116 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
117 if self.color_space == ColorSpaceTag::Oklab && self.hue_direction == HueDirection::Shorter {
118 return Ok(());
119 }
120 let space = match self.color_space {
121 ColorSpaceTag::Srgb => "srgb",
122 ColorSpaceTag::LinearSrgb => "srgb-linear",
123 ColorSpaceTag::Lab => "lab",
124 ColorSpaceTag::Oklab => "oklab",
125 ColorSpaceTag::Lch => "lch",
126 ColorSpaceTag::Oklch => "oklch",
127 ColorSpaceTag::Hsl => "hsl",
128 ColorSpaceTag::Hwb => "hwb",
129 ColorSpaceTag::DisplayP3 => "display-p3",
130 ColorSpaceTag::A98Rgb => "a98-rgb",
131 ColorSpaceTag::ProphotoRgb => "prophoto-rgb",
132 ColorSpaceTag::Rec2020 => "rec2020",
133 ColorSpaceTag::XyzD65 => "xyz-d65",
134 ColorSpaceTag::XyzD50 => "xyz-d50",
135 _ => "oklab",
136 };
137 let hue = match self.hue_direction {
138 HueDirection::Shorter => "",
139 HueDirection::Longer => " longer hue",
140 HueDirection::Increasing => " increasing hue",
141 HueDirection::Decreasing => " decreasing hue",
142 _ => "",
143 };
144 write!(dest, "in {}{}", space, hue)
145 }
146}
147
148#[derive(Debug, Default, Clone, PartialEq, Copy)]
150pub struct Color(pub [u8; 4]);
151
152impl From<[u8; 4]> for Color {
153 fn from(value: [u8; 4]) -> Self {
154 Self(value)
155 }
156}
157
158impl From<[f32; 4]> for Color {
159 fn from(value: [f32; 4]) -> Self {
160 Self([
161 value[0].clamp(0.0, 255.0).round() as u8,
162 value[1].clamp(0.0, 255.0).round() as u8,
163 value[2].clamp(0.0, 255.0).round() as u8,
164 value[3].clamp(0.0, 255.0).round() as u8,
165 ])
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Copy)]
171#[non_exhaustive]
172pub enum ColorInput<const DEFAULT_CURRENT_COLOR: bool = true> {
173 CurrentColor,
175 Value(Color),
177}
178
179pub type ColorDefaultsToTransparent = ColorInput<false>;
181
182impl<const DEFAULT_CURRENT_COLOR: bool> MakeComputed for ColorInput<DEFAULT_CURRENT_COLOR> {}
183
184impl<const DEFAULT_CURRENT_COLOR: bool> Animatable for ColorInput<DEFAULT_CURRENT_COLOR> {
185 fn interpolate(
186 &mut self,
187 from: &Self,
188 to: &Self,
189 progress: f32,
190 _sizing: &SizingContext,
191 current_color: CurrentColor,
192 ) {
193 *self = match (from, to) {
194 (ColorInput::CurrentColor, ColorInput::CurrentColor) => ColorInput::CurrentColor,
195 _ => ColorInput::Value(interpolate_with_color_space(
196 from.resolve(current_color),
197 to.resolve(current_color),
198 progress,
199 ColorSpaceTag::Oklab,
200 HueDirection::Shorter,
201 )),
202 };
203 }
204}
205
206impl<const DEFAULT_CURRENT_COLOR: bool> Default for ColorInput<DEFAULT_CURRENT_COLOR> {
207 fn default() -> Self {
208 if DEFAULT_CURRENT_COLOR {
209 ColorInput::CurrentColor
210 } else {
211 ColorInput::Value(Color::transparent())
212 }
213 }
214}
215
216impl<const DEFAULT_CURRENT_COLOR: bool> ColorInput<DEFAULT_CURRENT_COLOR> {
217 pub fn resolve(self, current_color: Color) -> Color {
219 match self {
220 ColorInput::Value(color) => color,
221 ColorInput::CurrentColor => current_color,
222 }
223 }
224}
225
226impl<const DEFAULT_CURRENT_COLOR: bool> TailwindPropertyParser
227 for ColorInput<DEFAULT_CURRENT_COLOR>
228{
229 fn parse_tw(token: &str) -> Option<Self> {
230 if token.eq_ignore_ascii_case("current") {
231 return Some(ColorInput::CurrentColor);
232 }
233
234 Color::parse_tw(token).map(ColorInput::Value)
235 }
236}
237
238const SLATE: [u32; 11] = [
241 0xf8fafc, 0xf1f5f9, 0xe2e8f0, 0xcad5e2, 0x90a1b9, 0x62748e, 0x45556c, 0x314158, 0x1d293d,
242 0x0f172b, 0x020618,
243];
244
245const GRAY: [u32; 11] = [
246 0xf9fafb, 0xf3f4f6, 0xe5e7eb, 0xd1d5dc, 0x99a1af, 0x6a7282, 0x4a5565, 0x364153, 0x1e2939,
247 0x101828, 0x030712,
248];
249
250const ZINC: [u32; 11] = [
251 0xfafafa, 0xf4f4f5, 0xe4e4e7, 0xd4d4d8, 0x9f9fa9, 0x71717b, 0x52525c, 0x3f3f46, 0x27272a,
252 0x18181b, 0x09090b,
253];
254
255const NEUTRAL: [u32; 11] = [
256 0xfafafa, 0xf5f5f5, 0xe5e5e5, 0xd4d4d4, 0xa1a1a1, 0x737373, 0x525252, 0x404040, 0x262626,
257 0x171717, 0x0a0a0a,
258];
259
260const STONE: [u32; 11] = [
261 0xfafaf9, 0xf5f5f4, 0xe7e5e4, 0xd6d3d1, 0xa6a09b, 0x79716b, 0x57534d, 0x44403b, 0x292524,
262 0x1c1917, 0x0c0a09,
263];
264
265const TAUPE: [u32; 11] = [
266 0xfbfaf9, 0xf3f1f1, 0xe8e4e3, 0xd8d2d0, 0xaba09c, 0x7c6d67, 0x5b4f4b, 0x473c39, 0x2b2422,
267 0x1d1816, 0x0c0a09,
268];
269
270const MAUVE: [u32; 11] = [
271 0xfafafa, 0xf3f1f3, 0xe7e4e7, 0xd7d0d7, 0xa89ea9, 0x79697b, 0x594c5b, 0x463947, 0x2a212c,
272 0x1d161e, 0x0c090c,
273];
274
275const MIST: [u32; 11] = [
276 0xf9fbfb, 0xf1f3f3, 0xe3e7e8, 0xd0d6d8, 0x9ca8ab, 0x67787c, 0x4b585b, 0x394447, 0x22292b,
277 0x161b1d, 0x090b0c,
278];
279
280const OLIVE: [u32; 11] = [
281 0xfbfbf9, 0xf4f4f0, 0xe8e8e3, 0xd8d8d0, 0xabab9c, 0x7c7c67, 0x5b5b4b, 0x474739, 0x2b2b22,
282 0x1d1d16, 0x0c0c09,
283];
284
285const RED: [u32; 11] = [
286 0xfef2f2, 0xffe2e2, 0xffc9c9, 0xffa2a2, 0xff6467, 0xfb2c36, 0xe7000b, 0xc10007, 0x9f0712,
287 0x82181a, 0x460809,
288];
289
290const ORANGE: [u32; 11] = [
291 0xfff7ed, 0xffedd4, 0xffd6a7, 0xffb86a, 0xff8904, 0xff6900, 0xf54900, 0xca3500, 0x9f2d00,
292 0x7e2a0c, 0x441306,
293];
294
295const AMBER: [u32; 11] = [
296 0xfffbeb, 0xfef3c6, 0xfee685, 0xffd230, 0xfcbb00, 0xf99c00, 0xe17100, 0xbb4d00, 0x973c00,
297 0x7b3306, 0x461901,
298];
299
300const YELLOW: [u32; 11] = [
301 0xfefce8, 0xfef9c2, 0xfff085, 0xffdf20, 0xfac800, 0xedb200, 0xd08700, 0xa65f00, 0x894b00,
302 0x733e0a, 0x432004,
303];
304
305const LIME: [u32; 11] = [
306 0xf7fee7, 0xecfcca, 0xd8f999, 0xbbf451, 0x9ae600, 0x7ccf00, 0x5ea500, 0x497d00, 0x3c6300,
307 0x35530e, 0x192e03,
308];
309
310const GREEN: [u32; 11] = [
311 0xf0fdf4, 0xdcfce7, 0xb9f8cf, 0x7bf1a8, 0x05df72, 0x00c950, 0x00a63e, 0x008236, 0x016630,
312 0x0d542b, 0x032e15,
313];
314
315const EMERALD: [u32; 11] = [
316 0xecfdf5, 0xd0fae5, 0xa4f4cf, 0x5ee9b5, 0x00d492, 0x00bc7d, 0x009966, 0x007a55, 0x006045,
317 0x004f3b, 0x002c22,
318];
319
320const TEAL: [u32; 11] = [
321 0xf0fdfa, 0xcbfbf1, 0x96f7e4, 0x46ecd5, 0x00d5be, 0x00bba7, 0x009689, 0x00786f, 0x005f5a,
322 0x0b4f4a, 0x022f2e,
323];
324
325const CYAN: [u32; 11] = [
326 0xecfeff, 0xcefafe, 0xa2f4fd, 0x53eafd, 0x00d3f2, 0x00b8db, 0x0092b8, 0x007595, 0x005f78,
327 0x104e64, 0x053345,
328];
329
330const SKY: [u32; 11] = [
331 0xf0f9ff, 0xdff2fe, 0xb8e6fe, 0x74d4ff, 0x00bcff, 0x00a6f4, 0x0084d1, 0x0069a8, 0x00598a,
332 0x024a70, 0x052f4a,
333];
334
335const BLUE: [u32; 11] = [
336 0xeff6ff, 0xdbeafe, 0xbedbff, 0x8ec5ff, 0x51a2ff, 0x2b7fff, 0x155dfc, 0x1447e6, 0x193cb8,
337 0x1c398e, 0x162456,
338];
339
340const INDIGO: [u32; 11] = [
341 0xeef2ff, 0xe0e7ff, 0xc6d2ff, 0xa3b3ff, 0x7c86ff, 0x615fff, 0x4f39f6, 0x432dd7, 0x372aac,
342 0x312c85, 0x1e1a4d,
343];
344
345const VIOLET: [u32; 11] = [
346 0xf5f3ff, 0xede9fe, 0xddd6ff, 0xc4b4ff, 0xa684ff, 0x8e51ff, 0x7f22fe, 0x7008e7, 0x5d0ec0,
347 0x4d179a, 0x2f0d68,
348];
349
350const PURPLE: [u32; 11] = [
351 0xfaf5ff, 0xf3e8ff, 0xe9d4ff, 0xdab2ff, 0xc27aff, 0xad46ff, 0x9810fa, 0x8200db, 0x6e11b0,
352 0x59168b, 0x3c0366,
353];
354
355const FUCHSIA: [u32; 11] = [
356 0xfdf4ff, 0xfae8ff, 0xf6cfff, 0xf4a8ff, 0xed6aff, 0xe12afb, 0xc800de, 0xa800b7, 0x8a0194,
357 0x721378, 0x4b004f,
358];
359
360const PINK: [u32; 11] = [
361 0xfdf2f8, 0xfce7f3, 0xfccee8, 0xfda5d5, 0xfb64b6, 0xf6339a, 0xe60076, 0xc6005c, 0xa3004c,
362 0x861043, 0x510424,
363];
364
365const ROSE: [u32; 11] = [
366 0xfff1f2, 0xffe4e6, 0xffccd3, 0xffa1ad, 0xff637e, 0xff2056, 0xec003f, 0xc70036, 0xa50036,
367 0x8b0836, 0x4d0218,
368];
369
370const SHADES: [u16; 11] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
372
373const PALETTE: [(&str, &[u32; 11]); 26] = [
374 ("slate", &SLATE),
375 ("gray", &GRAY),
376 ("zinc", &ZINC),
377 ("neutral", &NEUTRAL),
378 ("stone", &STONE),
379 ("taupe", &TAUPE),
380 ("mauve", &MAUVE),
381 ("mist", &MIST),
382 ("olive", &OLIVE),
383 ("red", &RED),
384 ("orange", &ORANGE),
385 ("amber", &AMBER),
386 ("yellow", &YELLOW),
387 ("lime", &LIME),
388 ("green", &GREEN),
389 ("emerald", &EMERALD),
390 ("teal", &TEAL),
391 ("cyan", &CYAN),
392 ("sky", &SKY),
393 ("blue", &BLUE),
394 ("indigo", &INDIGO),
395 ("violet", &VIOLET),
396 ("purple", &PURPLE),
397 ("fuchsia", &FUCHSIA),
398 ("pink", &PINK),
399 ("rose", &ROSE),
400];
401
402fn lookup_tailwind_color(color_name: &str, shade: u16) -> Option<u32> {
404 let index = SHADES.binary_search(&shade).ok()?;
405 let colors = PALETTE
406 .iter()
407 .find(|(name, _)| name.eq_ignore_ascii_case(color_name))
408 .map(|(_, c)| *c)?;
409 colors.get(index).copied()
410}
411
412impl TailwindPropertyParser for Color {
413 fn parse_tw(token: &str) -> Option<Self> {
414 if let Some((color, opacity)) = token.split_once('/') {
416 let color = Color::parse_tw(color)?;
417 let opacity = (opacity.parse::<f32>().ok()? * 2.55).round() as u8;
418
419 return Some(color.with_opacity(opacity));
420 }
421
422 match_ignore_ascii_case! {token,
424 "transparent" => return Some(Color::transparent()),
425 "black" => return Some(Color::black()),
426 "white" => return Some(Color::white()),
427 _ => {}
428 }
429
430 let (color_name, shade_str) = token.rsplit_once('-')?;
432 let shade: u16 = shade_str.parse().ok()?;
433
434 lookup_tailwind_color(color_name, shade).map(Color::from_rgb)
436 }
437}
438
439impl<const DEFAULT_CURRENT_COLOR: bool> From<Color> for ColorInput<DEFAULT_CURRENT_COLOR> {
440 fn from(color: Color) -> Self {
441 ColorInput::Value(color)
442 }
443}
444
445impl From<Color> for Rgba<u8> {
446 fn from(color: Color) -> Self {
447 Rgba(color.0)
448 }
449}
450
451impl From<Color> for PremultipliedColorU8 {
452 fn from(color: Color) -> Self {
453 let [r, g, b, a] = color.0;
454 let premul_r = fast_div_255(r as u32 * a as u32);
455 let premul_g = fast_div_255(g as u32 * a as u32);
456 let premul_b = fast_div_255(b as u32 * a as u32);
457
458 PremultipliedColorU8::from_rgba(premul_r, premul_g, premul_b, a)
459 .unwrap_or(PremultipliedColorU8::TRANSPARENT)
460 }
461}
462
463impl From<ColorU8> for Color {
464 fn from(color: ColorU8) -> Self {
465 Self([color.red(), color.green(), color.blue(), color.alpha()])
466 }
467}
468
469impl Display for Color {
470 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471 if self.0[3] == 255 {
472 return write!(f, "rgb({}, {}, {})", self.0[0], self.0[1], self.0[2]);
473 }
474
475 write!(
476 f,
477 "rgba({}, {}, {}, {:.6})",
478 self.0[0],
479 self.0[1],
480 self.0[2],
481 self.0[3] as f32 / 255.0
482 )
483 }
484}
485
486impl ToCss for Color {
487 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
488 write!(dest, "{}", self)
489 }
490}
491
492impl<const DEFAULT_CURRENT_COLOR: bool> ToCss for ColorInput<DEFAULT_CURRENT_COLOR> {
493 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
494 match self {
495 Self::CurrentColor => dest.write_str("currentColor"),
496 Self::Value(c) => c.to_css(dest),
497 }
498 }
499}
500
501impl Color {
502 pub const fn transparent() -> Self {
504 Color([0, 0, 0, 0])
505 }
506
507 pub const fn black() -> Self {
509 Color([0, 0, 0, 255])
510 }
511
512 pub const fn white() -> Self {
514 Color([255, 255, 255, 255])
515 }
516
517 pub fn with_opacity(mut self, opacity: u8) -> Self {
519 self.0[3] = fast_div_255(self.0[3] as u32 * opacity as u32);
520
521 self
522 }
523
524 pub const fn from_rgb(rgb: u32) -> Self {
526 Color([
527 ((rgb >> 16) & 0xFF) as u8,
528 ((rgb >> 8) & 0xFF) as u8,
529 (rgb & 0xFF) as u8,
530 255,
531 ])
532 }
533}
534
535#[derive(Debug, Clone, Copy)]
536struct ColorMixItem {
537 color: Color,
538 percentage: Option<PercentageNumber>,
539}
540
541impl<'i> FromCss<'i> for ColorMixItem {
542 fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
543 if let Ok(item) = input.try_parse(|input| -> ParseResult<'i, Self> {
544 let color = Color::from_css(input)?;
545 let percentage = input.try_parse(PercentageNumber::from_css).ok();
546
547 Ok(Self { color, percentage })
548 }) {
549 return Ok(item);
550 }
551
552 input.try_parse(|input| -> ParseResult<'i, Self> {
553 let percentage = PercentageNumber::from_css(input)?;
554 let color = Color::from_css(input)?;
555
556 Ok(Self {
557 color,
558 percentage: Some(percentage),
559 })
560 })
561 }
562
563 const VALID_TOKENS: &'static [CssToken] =
564 &[CssToken::Descriptor(CssDescriptorKind::ColorAndPercentage)];
565}
566
567#[derive(Debug, Clone, Copy)]
568struct ColorMix {
569 interpolation: ColorInterpolationMethod,
570 first: ColorMixItem,
571 second: ColorMixItem,
572}
573
574impl ColorMix {
575 fn evaluate(self) -> Option<Color> {
576 let mut p1 = self.first.percentage;
577 let mut p2 = self.second.percentage;
578
579 match (p1, p2) {
580 (None, None) => {
581 p1 = Some(PercentageNumber(0.5));
582 p2 = Some(PercentageNumber(0.5));
583 }
584 (Some(p1_value), None) => {
585 p2 = Some(PercentageNumber((1.0 - p1_value.0).max(0.0)));
586 }
587 (None, Some(p2_value)) => {
588 p1 = Some(PercentageNumber((1.0 - p2_value.0).max(0.0)));
589 }
590 _ => {}
591 }
592
593 let p1 = p1.unwrap_or(PercentageNumber(0.5)).0;
594 let p2 = p2.unwrap_or(PercentageNumber(0.5)).0;
595 let sum = p1 + p2;
596
597 if sum <= f32::EPSILON {
598 return None;
599 }
600
601 let weight_2 = p2 / sum;
602 let alpha_multiplier = sum.min(1.0);
603
604 let dynamic_1 = DynamicColor::from_alpha_color(AlphaColor::<Srgb>::from(
605 color::Rgba8::from_u8_array(self.first.color.0),
606 ));
607 let dynamic_2 = DynamicColor::from_alpha_color(AlphaColor::<Srgb>::from(
608 color::Rgba8::from_u8_array(self.second.color.0),
609 ));
610
611 let mixed = dynamic_1
612 .interpolate(
613 dynamic_2,
614 self.interpolation.color_space,
615 self.interpolation.hue_direction,
616 )
617 .eval(weight_2)
618 .multiply_alpha(alpha_multiplier);
619
620 Some(Color(
621 mixed.to_alpha_color::<Srgb>().to_rgba8().to_u8_array(),
622 ))
623 }
624}
625
626impl<'i> FromCss<'i> for ColorMix {
627 fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
628 let interpolation = ColorInterpolationMethod::from_css(input)?;
629
630 input.expect_comma()?;
631 let first = ColorMixItem::from_css(input)?;
632 input.expect_comma()?;
633 let second = ColorMixItem::from_css(input)?;
634
635 if !input.is_exhausted() {
636 return Err(input.new_error_for_next_token());
637 }
638
639 Ok(Self {
640 interpolation,
641 first,
642 second,
643 })
644 }
645
646 const VALID_TOKENS: &'static [CssToken] = &[CssToken::Descriptor(CssDescriptorKind::ColorMixFn)];
647}
648
649impl<'i, const DEFAULT_CURRENT_COLOR: bool> FromCss<'i> for ColorInput<DEFAULT_CURRENT_COLOR> {
650 fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
651 if input
652 .try_parse(|input| input.expect_ident_matching("currentcolor"))
653 .is_ok()
654 {
655 return Ok(ColorInput::CurrentColor);
656 }
657
658 Ok(ColorInput::Value(Color::from_css(input)?))
659 }
660
661 const VALID_TOKENS: &'static [CssToken] = &[
662 CssToken::Keyword("currentColor"),
663 CssToken::Syntax(CssSyntaxKind::Color),
664 ];
665}
666
667fn relative_target_cs(name: &str) -> Option<ColorSpaceTag> {
669 Some(match_ignore_ascii_case! { name,
670 "rgb" | "rgba" => ColorSpaceTag::Srgb,
671 "hsl" | "hsla" => ColorSpaceTag::Hsl,
672 "hwb" => ColorSpaceTag::Hwb,
673 "lab" => ColorSpaceTag::Lab,
674 "lch" => ColorSpaceTag::Lch,
675 "oklab" => ColorSpaceTag::Oklab,
676 "oklch" => ColorSpaceTag::Oklch,
677 _ => return None,
678 })
679}
680
681fn channel_names(cs: ColorSpaceTag) -> [&'static str; 3] {
682 match cs {
683 ColorSpaceTag::Srgb => ["r", "g", "b"],
684 ColorSpaceTag::Hsl => ["h", "s", "l"],
685 ColorSpaceTag::Hwb => ["h", "w", "b"],
686 ColorSpaceTag::Lab | ColorSpaceTag::Oklab => ["l", "a", "b"],
687 ColorSpaceTag::Lch | ColorSpaceTag::Oklch => ["l", "c", "h"],
688 _ => ["", "", ""],
689 }
690}
691
692fn channel_keyword_scale(cs: ColorSpaceTag) -> f32 {
694 if cs == ColorSpaceTag::Srgb {
695 255.0
696 } else {
697 1.0
698 }
699}
700
701fn channel_percentage_scale(cs: ColorSpaceTag, index: usize) -> f32 {
702 match (cs, index) {
703 (_, 3) => 1.0,
704 (ColorSpaceTag::Srgb, _) => 255.0,
705 (ColorSpaceTag::Hsl, _) | (ColorSpaceTag::Hwb, _) => 100.0,
706 (ColorSpaceTag::Lab, 0) | (ColorSpaceTag::Lch, 0) => 100.0,
707 (ColorSpaceTag::Lab, _) => 125.0,
708 (ColorSpaceTag::Lch, 1) => 150.0,
709 (ColorSpaceTag::Oklab, 0) | (ColorSpaceTag::Oklch, 0) => 1.0,
710 (ColorSpaceTag::Oklab, _) | (ColorSpaceTag::Oklch, 1) => 0.4,
711 _ => 1.0,
712 }
713}
714
715fn is_hue_slot(cs: ColorSpaceTag, index: usize) -> bool {
716 matches!(
717 (cs, index),
718 (ColorSpaceTag::Hsl | ColorSpaceTag::Hwb, 0) | (ColorSpaceTag::Lch | ColorSpaceTag::Oklch, 2)
719 )
720}
721
722fn parse_relative_slot<'i>(
723 input: &mut Parser<'i, '_>,
724 target_cs: ColorSpaceTag,
725 index: usize,
726 keyword_values: [f32; 4],
727) -> ParseResult<'i, f32> {
728 let location = input.current_source_location();
729 let token = input.next()?.clone();
730 let is_hue = is_hue_slot(target_cs, index);
731 match &token {
732 Token::Ident(ident) => {
733 if ident.eq_ignore_ascii_case("none") {
734 return Ok(0.0);
735 }
736 if ident.eq_ignore_ascii_case("alpha") {
737 return Ok(keyword_values[3]);
738 }
739 for (i, name) in channel_names(target_cs).iter().enumerate() {
740 if !name.is_empty() && ident.eq_ignore_ascii_case(name) {
741 return Ok(keyword_values[i]);
742 }
743 }
744 Err(unexpected_token!(Color, location, &token))
745 }
746 Token::Number { value, .. } => Ok(*value),
747 Token::Percentage { unit_value, .. } if !is_hue => {
748 Ok(*unit_value * channel_percentage_scale(target_cs, index))
749 }
750 Token::Dimension { value, unit, .. } if is_hue => Ok(match_ignore_ascii_case! { unit.as_ref(),
751 "deg" => *value,
752 "grad" => *value / 400.0 * 360.0,
753 "turn" => *value * 360.0,
754 "rad" => value.to_degrees(),
755 _ => return Err(unexpected_token!(Color, location, &token)),
756 }),
757 _ => Err(unexpected_token!(Color, location, &token)),
758 }
759}
760
761fn parse_relative_color<'i>(
763 input: &mut Parser<'i, '_>,
764 target_cs: ColorSpaceTag,
765) -> ParseResult<'i, Color> {
766 let origin_start = input.position();
767 let origin_location = input.current_source_location();
768 let origin_token = input.next()?.clone();
769 match &origin_token {
770 Token::Hash(_) | Token::IDHash(_) | Token::Ident(_) => {}
771 Token::Function(_) => {
772 input.parse_nested_block(|inner| -> ParseResult<'i, ()> {
773 while inner.next().is_ok() {}
774 Ok(())
775 })?;
776 }
777 _ => return Err(unexpected_token!(Color, origin_location, &origin_token)),
778 }
779
780 let scale = channel_keyword_scale(target_cs);
781 let origin_color = Color::from_str(input.slice_from(origin_start).trim())
782 .map_err(|_| unexpected_token!(Color, origin_location, &origin_token))?;
783 let [r, g, b, a] = origin_color.0;
784 let converted =
785 DynamicColor::from_alpha_color(AlphaColor::<Srgb>::from_rgba8(r, g, b, a)).convert(target_cs);
786 let [k0, k1, k2, k_alpha] = converted.components;
787 let keyword_values = [k0 * scale, k1 * scale, k2 * scale, k_alpha];
788
789 let s0 = parse_relative_slot(input, target_cs, 0, keyword_values)?;
790 let s1 = parse_relative_slot(input, target_cs, 1, keyword_values)?;
791 let s2 = parse_relative_slot(input, target_cs, 2, keyword_values)?;
792 let alpha = if input.try_parse(|i| i.expect_delim('/')).is_ok() {
793 parse_relative_slot(input, target_cs, 3, keyword_values)?
794 } else {
795 k_alpha
796 };
797
798 let new_components = [s0 / scale, s1 / scale, s2 / scale, alpha.clamp(0.0, 1.0)];
799 let result = converted.map(|_, _, _, _| new_components);
800 Ok(Color(
801 result.to_alpha_color::<Srgb>().to_rgba8().to_u8_array(),
802 ))
803}
804
805impl<'i> FromCss<'i> for Color {
806 fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
807 let location = input.current_source_location();
808 let position = input.position();
809 let token = input.next()?;
810
811 match *token {
812 Token::Hash(ref value) | Token::IDHash(ref value) => parse_hash_color(value.as_bytes())
813 .map(|(r, g, b, a)| Color([r, g, b, (a * 255.0) as u8]))
814 .map_err(|_| unexpected_token!(location, token)),
815 Token::Ident(ref ident) => {
816 if ident.eq_ignore_ascii_case("transparent") {
817 return Ok(Color::transparent());
818 }
819
820 parse_named_color(ident)
821 .map(|(r, g, b)| Color([r, g, b, 255]))
822 .map_err(|_| unexpected_token!(location, token))
823 }
824 Token::Function(_) => {
825 let token = token.clone();
827
828 if let Token::Function(function) = &token
829 && function.eq_ignore_ascii_case("color-mix")
830 {
831 return input.parse_nested_block(|input| {
832 let color_mix = ColorMix::from_css(input)?;
833 color_mix
834 .evaluate()
835 .ok_or_else(|| input.new_error_for_next_token())
836 });
837 }
838
839 let target_cs = if let Token::Function(name) = &token {
840 relative_target_cs(name)
841 } else {
842 None
843 };
844
845 input.parse_nested_block(|input| {
846 if let Some(cs) = target_cs
847 && input.try_parse(|i| i.expect_ident_matching("from")).is_ok()
848 {
849 return parse_relative_color(input, cs);
850 }
851
852 while input.next().is_ok() {}
853
854 let body = input.slice_from(position);
856
857 let mut function = body.to_string();
858
859 function.push(')');
861
862 parse_color(&function)
863 .map(|color| Color(color.to_alpha_color::<Srgb>().to_rgba8().to_u8_array()))
864 .map_err(|_| unexpected_token!(location, &token))
865 })
866 }
867 _ => Err(unexpected_token!(location, token)),
868 }
869 }
870 const VALID_TOKENS: &'static [CssToken] = &[CssToken::Syntax(CssSyntaxKind::Color)];
871}
872
873#[cfg(test)]
874mod tests {
875 use super::*;
876
877 #[test]
878 fn test_color_from_f32_array_clamps_before_rounding() {
879 assert_eq!(
880 Color::from([-1.0, 127.5, 255.4, 999.0]),
881 Color([0, 128, 255, 255])
882 );
883 }
884
885 #[test]
886 fn test_parse_color_opaque() {
887 for (css, expected) in [
888 ("#f09", Color([255, 0, 153, 255])),
889 ("#ff0099", Color([255, 0, 153, 255])),
890 ("transparent", Color([0, 0, 0, 0])),
891 ("rgb(255, 0, 153)", Color([255, 0, 153, 255])),
892 ("rgb(255 0 153)", Color([255, 0, 153, 255])),
893 ("grey", Color([128, 128, 128, 255])),
894 ("deepskyblue", Color([0, 191, 255, 255])),
895 ] {
896 assert_eq!(
897 ColorInput::from_str(css),
898 Ok(ColorInput::<true>::Value(expected)),
899 "failed for {css}",
900 );
901 }
902 }
903
904 #[test]
905 fn test_parse_color_with_alpha() {
906 for (css, expected) in [
907 ("rgba(255, 0, 153, 0.5)", Color([255, 0, 153, 128])),
908 ("rgb(255 0 153 / 0.5)", Color([255, 0, 153, 128])),
909 ] {
910 assert_eq!(
911 ColorInput::from_str(css),
912 Ok(ColorInput::<true>::Value(expected)),
913 "failed for {css}",
914 );
915 }
916 }
917
918 #[test]
919 fn test_parse_color_invalid_function() {
920 assert!(ColorInput::<true>::from_str("invalid(255, 0, 153)").is_err());
921 }
922
923 #[test]
924 fn test_parse_color_mix_srgb_default_percentages() {
925 assert_eq!(
926 ColorInput::from_str("color-mix(in srgb, red, blue)"),
927 Ok(ColorInput::<true>::Value(Color([128, 0, 128, 255])))
928 );
929 }
930
931 #[test]
932 fn test_parse_color_mix_equivalent_percentage_syntaxes() {
933 let canonical = ColorInput::<true>::from_str("color-mix(in srgb, red 25%, blue 75%)");
934
935 assert_eq!(
936 canonical,
937 ColorInput::<true>::from_str("color-mix(in srgb, 25% red, 75% blue)")
938 );
939 assert_eq!(
940 canonical,
941 ColorInput::<true>::from_str("color-mix(in srgb, red 25%, 75% blue)")
942 );
943 assert_eq!(
944 canonical,
945 ColorInput::<true>::from_str("color-mix(in srgb, red 25%, blue)")
946 );
947 assert_eq!(
948 canonical,
949 Ok(ColorInput::<true>::Value(Color([64, 0, 191, 255])))
950 );
951 }
952
953 #[test]
954 fn test_parse_color_mix_lch_missing_percentage_equivalence() {
955 let canonical = ColorInput::<true>::from_str("color-mix(in lch, purple 50%, plum 50%)");
956
957 assert_eq!(
958 canonical,
959 ColorInput::<true>::from_str("color-mix(in lch, purple 50%, plum)")
960 );
961 assert_eq!(
962 canonical,
963 ColorInput::<true>::from_str("color-mix(in lch, purple, plum 50%)")
964 );
965 assert_eq!(
966 canonical,
967 ColorInput::<true>::from_str("color-mix(in lch, purple, plum)")
968 );
969 assert_eq!(
970 canonical,
971 ColorInput::<true>::from_str("color-mix(in lch, plum, purple)")
972 );
973 }
974
975 #[test]
976 fn test_parse_color_mix_lch_normalizes_equal_opaque_percentages() {
977 let canonical = ColorInput::<true>::from_str("color-mix(in lch, purple 50%, plum 50%)");
978
979 assert_eq!(
980 canonical,
981 ColorInput::<true>::from_str("color-mix(in lch, purple 55%, plum 55%)")
982 );
983 assert_eq!(
984 canonical,
985 ColorInput::<true>::from_str("color-mix(in lch, purple 70%, plum 70%)")
986 );
987 assert_eq!(
988 canonical,
989 ColorInput::<true>::from_str("color-mix(in lch, purple 95%, plum 95%)")
990 );
991 assert_eq!(
992 canonical,
993 ColorInput::<true>::from_str("color-mix(in lch, purple 125%, plum 125%)")
994 );
995 assert_eq!(
996 canonical,
997 ColorInput::<true>::from_str("color-mix(in lch, purple 9999%, plum 9999%)")
998 );
999 }
1000
1001 #[test]
1002 fn test_parse_color_mix_endpoint_percentages_return_endpoint_colors() {
1003 assert_eq!(
1004 ColorInput::<true>::from_str("color-mix(in srgb, red 100%, blue 0%)"),
1005 ColorInput::<true>::from_str("red")
1006 );
1007 assert_eq!(
1008 ColorInput::<true>::from_str("color-mix(in srgb, red 0%, blue 100%)"),
1009 ColorInput::<true>::from_str("blue")
1010 );
1011 }
1012
1013 #[test]
1014 fn test_parse_color_mix_alpha_multiplier_under_100_percent() {
1015 assert_eq!(
1016 ColorInput::from_str("color-mix(in srgb, red 30%, blue 30%)"),
1017 Ok(ColorInput::<true>::Value(Color([128, 0, 128, 153])))
1018 );
1019 }
1020
1021 #[test]
1022 fn test_parse_color_mix_hue_directions_change_result() {
1023 assert_eq!(
1024 ColorInput::<true>::from_str(
1025 "color-mix(in hsl shorter hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%))",
1026 ),
1027 Ok(ColorInput::<true>::Value(Color([191, 86, 64, 255])))
1028 );
1029 assert_eq!(
1030 ColorInput::<true>::from_str(
1031 "color-mix(in hsl decreasing hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%))",
1032 ),
1033 Ok(ColorInput::<true>::Value(Color([191, 86, 64, 255])))
1034 );
1035 assert_eq!(
1036 ColorInput::<true>::from_str(
1037 "color-mix(in hsl longer hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%))",
1038 ),
1039 Ok(ColorInput::<true>::Value(Color([64, 169, 191, 255])))
1040 );
1041 assert_eq!(
1042 ColorInput::<true>::from_str(
1043 "color-mix(in hsl increasing hue, hsl(50deg 50% 50%), hsl(330deg 50% 50%))",
1044 ),
1045 Ok(ColorInput::<true>::Value(Color([64, 169, 191, 255])))
1046 );
1047 }
1048
1049 #[test]
1050 fn test_parse_color_mix_over_100_percent_normalizes_weights() {
1051 assert_eq!(
1052 ColorInput::from_str("color-mix(in srgb, red 120%, blue 80%)"),
1053 Ok(ColorInput::<true>::Value(Color([153, 0, 102, 255])))
1054 );
1055 }
1056
1057 #[test]
1058 fn test_parse_color_mix_unknown_color_space() {
1059 assert!(ColorInput::<true>::from_str("color-mix(in unknown, red, blue)").is_err());
1060 }
1061
1062 #[test]
1063 fn test_parse_color_mix_hue_method_with_non_cylindrical_space_errors() {
1064 assert!(ColorInput::<true>::from_str("color-mix(in srgb longer hue, red, blue)").is_err());
1065 }
1066
1067 #[test]
1068 fn test_parse_color_mix_malformed_missing_comma_errors() {
1069 assert!(ColorInput::<true>::from_str("color-mix(in srgb, red blue)").is_err());
1070 }
1071
1072 #[test]
1073 fn test_parse_color_mix_zero_sum_percentages_errors() {
1074 assert!(ColorInput::<true>::from_str("color-mix(in srgb, red 0%, blue 0%)").is_err());
1075 }
1076
1077 #[test]
1078 fn test_parse_color_mix_accepts_number_as_percentage() {
1079 assert_eq!(
1080 ColorInput::<true>::from_str("color-mix(in srgb, red 0.5, blue 0.5)"),
1081 Ok(ColorInput::<true>::Value(Color([128, 0, 128, 255])))
1082 );
1083 }
1084
1085 #[test]
1086 fn test_parse_color_mix_nested_color_mix() {
1087 assert!(
1088 ColorInput::<true>::from_str("color-mix(in srgb, color-mix(in srgb, red, blue), white)")
1089 .is_ok()
1090 );
1091 }
1092
1093 #[test]
1094 fn test_parse_relative_oklch_keywords_only() {
1095 let relative = ColorInput::<true>::from_str("oklch(from oklch(0.7 0.15 30) l c h / 0.5)");
1096 let direct = ColorInput::<true>::from_str("oklch(0.7 0.15 30 / 0.5)");
1097 assert_eq!(relative, direct);
1098 }
1099
1100 #[test]
1101 fn test_parse_relative_oklch_keywords_only_from_named_color() {
1102 assert!(ColorInput::<true>::from_str("oklch(from red l c h)").is_ok());
1103 }
1104
1105 #[test]
1106 fn test_parse_relative_rgb_keywords_only_scales_to_255() {
1107 assert_eq!(
1108 ColorInput::<true>::from_str("rgb(from #336699 r g b)"),
1109 ColorInput::<true>::from_str("rgb(51 102 153)"),
1110 );
1111 }
1112
1113 #[test]
1114 fn test_parse_relative_rgb_drops_origin_alpha_when_alpha_omitted() {
1115 assert_eq!(
1116 ColorInput::<true>::from_str("rgb(from rgb(10 20 30 / 0.5) r g b / 0.25)"),
1117 ColorInput::<true>::from_str("rgba(10, 20, 30, 0.25)"),
1118 );
1119 }
1120
1121 #[test]
1122 fn test_parse_relative_hsl_with_dimension_hue() {
1123 assert!(ColorInput::<true>::from_str("hsl(from red 120deg s l)").is_ok());
1124 }
1125
1126 #[test]
1127 fn test_parse_relative_oklch_substitutes_origin_alpha() {
1128 assert_eq!(
1129 ColorInput::<true>::from_str("oklch(from oklch(0.5 0.1 200 / 0.4) l c h / alpha)",),
1130 ColorInput::<true>::from_str("oklch(0.5 0.1 200 / 0.4)"),
1131 );
1132 }
1133
1134 #[test]
1135 fn test_parse_relative_color_with_color_mix_origin() {
1136 assert!(ColorInput::<true>::from_str("lch(from color-mix(in srgb, red, blue) l c h)").is_ok());
1137 }
1138
1139 #[test]
1140 fn test_parse_relative_color_in_linear_gradient() {
1141 use crate::style::properties::linear_gradient::LinearGradient;
1142
1143 assert!(
1144 LinearGradient::from_str(
1145 "linear-gradient(to right, oklch(from oklch(0.7 0.15 30) l c h / 0.5), white)",
1146 )
1147 .is_ok()
1148 );
1149 }
1150
1151 #[test]
1152 fn test_parse_color_mix_inside_linear_gradient() {
1153 use crate::style::properties::linear_gradient::LinearGradient;
1154
1155 assert!(
1156 LinearGradient::from_str("linear-gradient(to right, color-mix(in srgb, red, blue), white)")
1157 .is_ok()
1158 );
1159 }
1160}