Skip to main content

takumi_css/style/properties/
color.rs

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/// Color interpolation configuration used by functions like `color-mix()` and gradients.
27#[derive(Debug, Clone, Copy, PartialEq)]
28#[non_exhaustive]
29pub struct ColorInterpolationMethod {
30  /// The color space used to interpolate between two colors.
31  pub color_space: ColorSpaceTag,
32  /// Optional hue interpolation strategy for cylindrical color spaces.
33  pub hue_direction: HueDirection,
34}
35
36impl Default for ColorInterpolationMethod {
37  fn default() -> Self {
38    Self {
39      // Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method
40      // When interpolating <color> values, the interpolation color space defaults to Oklab.
41      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/// Represents a color with 8-bit RGBA components.
149#[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/// Represents a color input value.
170#[derive(Debug, Clone, PartialEq, Copy)]
171#[non_exhaustive]
172pub enum ColorInput<const DEFAULT_CURRENT_COLOR: bool = true> {
173  /// Inherit from the `color` value.
174  CurrentColor,
175  /// A color value.
176  Value(Color),
177}
178
179/// A color value that defaults to transparent instead of currentColor.
180pub 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  /// Resolves the color input to a color.
218  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
238/// Tailwind v4 palette (OKLCH→sRGB via CSS Color 4 with gamut mapping).
239/// Shades: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950.
240const 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
370/// Shade values in ascending order for binary search
371const 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
402/// Returns the RGB value as a u32 (0xRRGGBB format)
403fn 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    // handle opacity text like `text-red-50/30`
415    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    // Handle basic colors first
423    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    // Parse color-shade format (e.g., "red-500")
431    let (color_name, shade_str) = token.rsplit_once('-')?;
432    let shade: u16 = shade_str.parse().ok()?;
433
434    // Lookup in color table
435    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  /// Creates a new transparent color.
503  pub const fn transparent() -> Self {
504    Color([0, 0, 0, 0])
505  }
506
507  /// Creates a new black color.
508  pub const fn black() -> Self {
509    Color([0, 0, 0, 255])
510  }
511
512  /// Creates a new white color.
513  pub const fn white() -> Self {
514    Color([255, 255, 255, 255])
515  }
516
517  /// Apply opacity to alpha channel
518  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  /// Creates a new color from a 32-bit integer containing RGB values.
525  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
667/// Reference: https://www.w3.org/TR/css-color-5/#relative-colors
668fn 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
692/// sRGB stores 0..1 but `r`/`g`/`b` keywords resolve to 0..255 per spec.
693fn 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
761/// `calc()` expressions in component slots are not yet supported.
762fn 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        // Have to clone to persist token, and allow input to be borrowed
826        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          // Slice from the function name till before the closing parenthesis
855          let body = input.slice_from(position);
856
857          let mut function = body.to_string();
858
859          // Add closing parenthesis
860          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}