Skip to main content

css_ast/functions/
color_mix_function.rs

1use super::prelude::*;
2use crate::Percentage;
3
4/// <https://drafts.csswg.org/css-color-5/#color-mix>
5///
6/// ```text,ignore
7/// color-mix() = color-mix( <color-interpolation-method>? , [ <color> && <percentage [0,100]>? ]# )
8/// ```
9#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
11#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(all))]
12#[derive(csskit_derives::NodeWithMetadata)]
13pub struct ColorMixFunction<'a> {
14	#[atom(CssAtomSet::ColorMix)]
15	#[cfg_attr(feature = "visitable", visit(skip))]
16	pub name: T![Function],
17	pub interpolation: Option<ColorInterpolationMethod>,
18	#[cfg_attr(feature = "visitable", visit(skip))]
19	pub interpolation_comma: Option<T![,]>,
20	pub parts: CommaSeparated<'a, ColorMixPart<'a>, 1>,
21	#[cfg_attr(feature = "visitable", visit(skip))]
22	pub close: T![')'],
23}
24
25/// <https://drafts.csswg.org/css-color-4/#color-interpolation-method>
26///
27/// ```text,ignore
28/// <color-interpolation-method> = in [ <rectangular-color-space> | <polar-color-space> <hue-interpolation-method>? ]
29/// ```
30#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
32#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
33#[derive(csskit_derives::NodeWithMetadata)]
34pub struct ColorInterpolationMethod {
35	#[atom(CssAtomSet::In)]
36	pub in_keyword: T![Ident],
37	pub color_space: InterpolationColorSpace,
38}
39
40/// The color space for color interpolation, which can be rectangular or polar.
41///
42/// ```text,ignore
43/// <rectangular-color-space> | <polar-color-space> <hue-interpolation-method>?
44/// ```
45#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
47#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
48#[derive(csskit_derives::NodeWithMetadata)]
49pub enum InterpolationColorSpace {
50	Rectangular(RectangularColorSpace),
51	Polar(PolarColorSpace, Option<HueInterpolationMethod>),
52}
53
54/// <https://drafts.csswg.org/css-color-4/#typedef-rectangular-color-space>
55///
56/// ```text,ignore
57/// <rectangular-color-space> = srgb | srgb-linear | display-p3 | a98-rgb |
58///     prophoto-rgb | rec2020 | lab | oklab | xyz | xyz-d50 | xyz-d65
59/// ```
60#[derive(
61	Parse, Peek, IntoCursor, ToSpan, SemanticEq, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
62)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
64#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
65#[derive(csskit_derives::NodeWithMetadata)]
66pub enum RectangularColorSpace {
67	#[atom(CssAtomSet::Srgb)]
68	Srgb(T![Ident]),
69	#[atom(CssAtomSet::SrgbLinear)]
70	SrgbLinear(T![Ident]),
71	#[atom(CssAtomSet::DisplayP3)]
72	DisplayP3(T![Ident]),
73	#[atom(CssAtomSet::A98Rgb)]
74	A98Rgb(T![Ident]),
75	#[atom(CssAtomSet::ProphotoRgb)]
76	ProphotoRgb(T![Ident]),
77	#[atom(CssAtomSet::Rec2020)]
78	Rec2020(T![Ident]),
79	#[atom(CssAtomSet::Lab)]
80	Lab(T![Ident]),
81	#[atom(CssAtomSet::Oklab)]
82	Oklab(T![Ident]),
83	#[atom(CssAtomSet::Xyz)]
84	Xyz(T![Ident]),
85	#[atom(CssAtomSet::XyzD50)]
86	XyzD50(T![Ident]),
87	#[atom(CssAtomSet::XyzD65)]
88	XyzD65(T![Ident]),
89}
90
91/// <https://drafts.csswg.org/css-color-4/#typedef-polar-color-space>
92///
93/// ```text,ignore
94/// <polar-color-space> = hsl | hwb | lch | oklch
95/// ```
96#[derive(
97	Parse, Peek, IntoCursor, ToSpan, SemanticEq, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
98)]
99#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
100#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
101#[derive(csskit_derives::NodeWithMetadata)]
102pub enum PolarColorSpace {
103	#[atom(CssAtomSet::Hsl)]
104	Hsl(T![Ident]),
105	#[atom(CssAtomSet::Hwb)]
106	Hwb(T![Ident]),
107	#[atom(CssAtomSet::Lch)]
108	Lch(T![Ident]),
109	#[atom(CssAtomSet::Oklch)]
110	Oklch(T![Ident]),
111}
112
113/// <https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method>
114///
115/// ```text,ignore
116/// <hue-interpolation-method> = [ shorter | longer | increasing | decreasing ] hue
117/// ```
118#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
119#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
120#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
121#[derive(csskit_derives::NodeWithMetadata)]
122pub struct HueInterpolationMethod {
123	pub direction: HueInterpolationDirection,
124	#[atom(CssAtomSet::Hue)]
125	pub hue_keyword: T![Ident],
126}
127
128/// The direction keyword for hue interpolation.
129///
130/// ```text,ignore
131/// shorter | longer | increasing | decreasing
132/// ```
133#[derive(
134	Parse, Peek, IntoCursor, ToSpan, SemanticEq, ToCursors, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
135)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
137#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
138#[derive(csskit_derives::NodeWithMetadata)]
139pub enum HueInterpolationDirection {
140	#[atom(CssAtomSet::Shorter)]
141	Shorter(T![Ident]),
142	#[atom(CssAtomSet::Longer)]
143	Longer(T![Ident]),
144	#[atom(CssAtomSet::Increasing)]
145	Increasing(T![Ident]),
146	#[atom(CssAtomSet::Decreasing)]
147	Decreasing(T![Ident]),
148}
149
150/// A color with an optional percentage in a color-mix() function.
151///
152/// ```text,ignore
153/// [ <color> && <percentage [0,100]>? ]
154/// ```
155///
156/// The color and percentage can appear in either order.
157#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
158#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
159#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(children))]
160#[derive(csskit_derives::NodeWithMetadata)]
161pub struct ColorMixPart<'a> {
162	pub color: Color<'a>,
163	pub percentage: Option<Percentage>,
164}
165
166impl<'a> Peek<'a> for ColorMixPart<'a> {
167	const PEEK_KINDSET: KindSet = Color::PEEK_KINDSET.combine(Percentage::PEEK_KINDSET);
168
169	#[inline(always)]
170	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
171	where
172		I: Iterator<Item = Cursor> + Clone,
173	{
174		Color::peek(p, c) || Percentage::peek(p, c)
175	}
176}
177
178impl<'a> Parse<'a> for ColorMixPart<'a> {
179	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
180	where
181		I: Iterator<Item = Cursor> + Clone,
182	{
183		// Either order: <color> <percentage>? or <percentage> <color>
184		let mut color = p.parse_if_peek::<Color>()?;
185		let percentage = p.parse_if_peek::<Percentage>()?;
186		if color.is_none() {
187			color = Some(p.parse::<Color>()?);
188		}
189		Ok(Self { color: color.unwrap(), percentage })
190	}
191}
192
193#[cfg(feature = "chromashift")]
194impl HueInterpolationDirection {
195	/// Converts this AST node to the corresponding chromashift hue interpolation direction.
196	pub fn to_hue_interpolation(&self) -> chromashift::HueInterpolation {
197		match self {
198			Self::Shorter(_) => chromashift::HueInterpolation::Shorter,
199			Self::Longer(_) => chromashift::HueInterpolation::Longer,
200			Self::Increasing(_) => chromashift::HueInterpolation::Increasing,
201			Self::Decreasing(_) => chromashift::HueInterpolation::Decreasing,
202		}
203	}
204}
205
206#[cfg(feature = "chromashift")]
207impl crate::ToChromashift for ColorMixFunction<'_> {
208	fn to_chromashift(&self) -> Option<chromashift::Color> {
209		use chromashift::{
210			A98Rgb, Channel, DisplayP3, Hsl, Hwb, Lab, Lch, LinearRgb, Oklab, Oklch, PolarLayout, ProphotoRgb, Rec2020,
211			Srgb, XyzD50, XyzD65, mix_channels,
212		};
213
214		/// Two-color mix in space `C`, returning the result as `chromashift::Color`.
215		fn mix_in<C>(
216			a: &Color<'_>,
217			b: &Color<'_>,
218			percentage: f64,
219			hue: chromashift::HueInterpolation,
220		) -> Option<chromashift::Color>
221		where
222			C: From<chromashift::Color>
223				+ Into<[Channel; 4]>
224				+ From<[Channel; 4]>
225				+ Into<chromashift::Color>
226				+ PolarLayout,
227		{
228			let fa = a.to_mix_channels::<C>()?;
229			let fb = b.to_mix_channels::<C>()?;
230			Some(C::from(mix_channels(fa, fb, percentage, C::HUE_INDEX, hue)).into())
231		}
232
233		let color_space = self.interpolation.as_ref().map(|i| &i.color_space);
234
235		let hue = match color_space {
236			Some(InterpolationColorSpace::Polar(_, Some(him))) => him.direction.to_hue_interpolation(),
237			_ => chromashift::HueInterpolation::Shorter,
238		};
239
240		// Collect (color, percentage) pairs, defaulting missing percentages to None.
241		let parts: std::vec::Vec<(&Color<'_>, Option<f64>)> = (&self.parts)
242			.into_iter()
243			.map(|(p, _)| (&p.color, p.percentage.as_ref().map(|pct| pct.value() as f64)))
244			.collect();
245
246		// Normalise percentages: fill missing as equal shares summing to 100.
247		let n = parts.len() as f64;
248		let default_pct = 100.0 / n;
249		let mut stack: std::vec::Vec<(chromashift::Color, f64)> = std::vec::Vec::with_capacity(parts.len());
250		for (color, pct) in &parts {
251			let p = pct.unwrap_or(default_pct);
252			stack.push((color.to_chromashift()?, p));
253		}
254
255		// Per spec: if the sum of all percentages is 0, return transparent black.
256		if stack.iter().map(|(_, p)| p).sum::<f64>() == 0.0 {
257			return Some(chromashift::Color::Srgb(chromashift::Srgb::new(0, 0, 0, 0.0)));
258		}
259
260		// Pairwise left-to-right reduction per the spec stack algorithm.
261		while stack.len() >= 2 {
262			let (color_b, pct_b) = stack.remove(1);
263			let (color_a, pct_a) = stack.remove(0);
264			let combined = pct_a + pct_b;
265			let progress = pct_b / combined;
266
267			// Get the source Color AST nodes for none-channel preservation.
268			let idx = parts.len() - stack.len() - 2;
269			let ast_a = parts[idx].0;
270			let ast_b = parts[idx + 1].0;
271
272			// We need to_mix_channels for none-aware mixing, but we have a resolved
273			// chromashift::Color for intermediate results. For intermediates (idx > 0),
274			// none channels are already resolved so we use the chromashift color directly.
275			let mixed = if idx == 0 {
276				let dispatch = |space: &InterpolationColorSpace| match space {
277					InterpolationColorSpace::Rectangular(s) => match s {
278						RectangularColorSpace::Srgb(_) => mix_in::<Srgb>(ast_a, ast_b, progress * 100.0, hue),
279						RectangularColorSpace::SrgbLinear(_) => {
280							mix_in::<LinearRgb>(ast_a, ast_b, progress * 100.0, hue)
281						}
282						RectangularColorSpace::DisplayP3(_) => mix_in::<DisplayP3>(ast_a, ast_b, progress * 100.0, hue),
283						RectangularColorSpace::A98Rgb(_) => mix_in::<A98Rgb>(ast_a, ast_b, progress * 100.0, hue),
284						RectangularColorSpace::ProphotoRgb(_) => {
285							mix_in::<ProphotoRgb>(ast_a, ast_b, progress * 100.0, hue)
286						}
287						RectangularColorSpace::Rec2020(_) => mix_in::<Rec2020>(ast_a, ast_b, progress * 100.0, hue),
288						RectangularColorSpace::Lab(_) => mix_in::<Lab>(ast_a, ast_b, progress * 100.0, hue),
289						RectangularColorSpace::Oklab(_) => mix_in::<Oklab>(ast_a, ast_b, progress * 100.0, hue),
290						RectangularColorSpace::XyzD50(_) => mix_in::<XyzD50>(ast_a, ast_b, progress * 100.0, hue),
291						RectangularColorSpace::Xyz(_) | RectangularColorSpace::XyzD65(_) => {
292							mix_in::<XyzD65>(ast_a, ast_b, progress * 100.0, hue)
293						}
294					},
295					InterpolationColorSpace::Polar(s, _) => match s {
296						PolarColorSpace::Hsl(_) => mix_in::<Hsl>(ast_a, ast_b, progress * 100.0, hue),
297						PolarColorSpace::Hwb(_) => mix_in::<Hwb>(ast_a, ast_b, progress * 100.0, hue),
298						PolarColorSpace::Lch(_) => mix_in::<Lch>(ast_a, ast_b, progress * 100.0, hue),
299						PolarColorSpace::Oklch(_) => mix_in::<Oklch>(ast_a, ast_b, progress * 100.0, hue),
300					},
301				};
302				// Default to oklab when no interpolation method specified.
303				if let Some(space) = color_space {
304					dispatch(space)
305				} else {
306					mix_in::<Oklab>(ast_a, ast_b, progress * 100.0, hue)
307				}
308			} else {
309				// Intermediate results have no none channels; mix directly in target space.
310				let mix_direct = |space: &InterpolationColorSpace| {
311					let fa: [Channel; 4] = match space {
312						InterpolationColorSpace::Rectangular(s) => match s {
313							RectangularColorSpace::Srgb(_) => Srgb::from(color_a).into(),
314							RectangularColorSpace::SrgbLinear(_) => LinearRgb::from(color_a).into(),
315							RectangularColorSpace::DisplayP3(_) => DisplayP3::from(color_a).into(),
316							RectangularColorSpace::A98Rgb(_) => A98Rgb::from(color_a).into(),
317							RectangularColorSpace::ProphotoRgb(_) => ProphotoRgb::from(color_a).into(),
318							RectangularColorSpace::Rec2020(_) => Rec2020::from(color_a).into(),
319							RectangularColorSpace::Lab(_) => Lab::from(color_a).into(),
320							RectangularColorSpace::Oklab(_) => Oklab::from(color_a).into(),
321							RectangularColorSpace::XyzD50(_) => XyzD50::from(color_a).into(),
322							RectangularColorSpace::Xyz(_) | RectangularColorSpace::XyzD65(_) => {
323								XyzD65::from(color_a).into()
324							}
325						},
326						InterpolationColorSpace::Polar(s, _) => match s {
327							PolarColorSpace::Hsl(_) => Hsl::from(color_a).into(),
328							PolarColorSpace::Hwb(_) => Hwb::from(color_a).into(),
329							PolarColorSpace::Lch(_) => Lch::from(color_a).into(),
330							PolarColorSpace::Oklch(_) => Oklch::from(color_a).into(),
331						},
332					};
333					let fb: [Channel; 4] = match space {
334						InterpolationColorSpace::Rectangular(s) => match s {
335							RectangularColorSpace::Srgb(_) => Srgb::from(color_b).into(),
336							RectangularColorSpace::SrgbLinear(_) => LinearRgb::from(color_b).into(),
337							RectangularColorSpace::DisplayP3(_) => DisplayP3::from(color_b).into(),
338							RectangularColorSpace::A98Rgb(_) => A98Rgb::from(color_b).into(),
339							RectangularColorSpace::ProphotoRgb(_) => ProphotoRgb::from(color_b).into(),
340							RectangularColorSpace::Rec2020(_) => Rec2020::from(color_b).into(),
341							RectangularColorSpace::Lab(_) => Lab::from(color_b).into(),
342							RectangularColorSpace::Oklab(_) => Oklab::from(color_b).into(),
343							RectangularColorSpace::XyzD50(_) => XyzD50::from(color_b).into(),
344							RectangularColorSpace::Xyz(_) | RectangularColorSpace::XyzD65(_) => {
345								XyzD65::from(color_b).into()
346							}
347						},
348						InterpolationColorSpace::Polar(s, _) => match s {
349							PolarColorSpace::Hsl(_) => Hsl::from(color_b).into(),
350							PolarColorSpace::Hwb(_) => Hwb::from(color_b).into(),
351							PolarColorSpace::Lch(_) => Lch::from(color_b).into(),
352							PolarColorSpace::Oklch(_) => Oklch::from(color_b).into(),
353						},
354					};
355					Some(match space {
356						InterpolationColorSpace::Rectangular(s) => match s {
357							RectangularColorSpace::Srgb(_) => {
358								Srgb::from(mix_channels(fa, fb, progress * 100.0, Srgb::HUE_INDEX, hue)).into()
359							}
360							RectangularColorSpace::SrgbLinear(_) => {
361								LinearRgb::from(mix_channels(fa, fb, progress * 100.0, LinearRgb::HUE_INDEX, hue))
362									.into()
363							}
364							RectangularColorSpace::DisplayP3(_) => {
365								DisplayP3::from(mix_channels(fa, fb, progress * 100.0, DisplayP3::HUE_INDEX, hue))
366									.into()
367							}
368							RectangularColorSpace::A98Rgb(_) => {
369								A98Rgb::from(mix_channels(fa, fb, progress * 100.0, A98Rgb::HUE_INDEX, hue)).into()
370							}
371							RectangularColorSpace::ProphotoRgb(_) => {
372								ProphotoRgb::from(mix_channels(fa, fb, progress * 100.0, ProphotoRgb::HUE_INDEX, hue))
373									.into()
374							}
375							RectangularColorSpace::Rec2020(_) => {
376								Rec2020::from(mix_channels(fa, fb, progress * 100.0, Rec2020::HUE_INDEX, hue)).into()
377							}
378							RectangularColorSpace::Lab(_) => {
379								Lab::from(mix_channels(fa, fb, progress * 100.0, Lab::HUE_INDEX, hue)).into()
380							}
381							RectangularColorSpace::Oklab(_) => {
382								Oklab::from(mix_channels(fa, fb, progress * 100.0, Oklab::HUE_INDEX, hue)).into()
383							}
384							RectangularColorSpace::XyzD50(_) => {
385								XyzD50::from(mix_channels(fa, fb, progress * 100.0, XyzD50::HUE_INDEX, hue)).into()
386							}
387							RectangularColorSpace::Xyz(_) | RectangularColorSpace::XyzD65(_) => {
388								XyzD65::from(mix_channels(fa, fb, progress * 100.0, XyzD65::HUE_INDEX, hue)).into()
389							}
390						},
391						InterpolationColorSpace::Polar(s, _) => match s {
392							PolarColorSpace::Hsl(_) => {
393								Hsl::from(mix_channels(fa, fb, progress * 100.0, Hsl::HUE_INDEX, hue)).into()
394							}
395							PolarColorSpace::Hwb(_) => {
396								Hwb::from(mix_channels(fa, fb, progress * 100.0, Hwb::HUE_INDEX, hue)).into()
397							}
398							PolarColorSpace::Lch(_) => {
399								Lch::from(mix_channels(fa, fb, progress * 100.0, Lch::HUE_INDEX, hue)).into()
400							}
401							PolarColorSpace::Oklch(_) => {
402								Oklch::from(mix_channels(fa, fb, progress * 100.0, Oklch::HUE_INDEX, hue)).into()
403							}
404						},
405					})
406				};
407				if let Some(space) = color_space {
408					mix_direct(space)
409				} else {
410					let fa: [Channel; 4] = Oklab::from(color_a).into();
411					let fb: [Channel; 4] = Oklab::from(color_b).into();
412					Some(Oklab::from(mix_channels(fa, fb, progress * 100.0, Oklab::HUE_INDEX, hue)).into())
413				}
414			}?;
415
416			stack.insert(0, (mixed, combined));
417		}
418
419		stack.into_iter().next().map(|(c, _)| c)
420	}
421}
422
423#[cfg(test)]
424mod tests {
425	use super::*;
426	use crate::CssAtomSet;
427	use css_parse::{assert_parse, assert_parse_error};
428
429	#[test]
430	fn size_test() {
431		assert_eq!(std::mem::size_of::<ColorMixFunction>(), 128);
432		assert_eq!(std::mem::size_of::<ColorInterpolationMethod>(), 56);
433		assert_eq!(std::mem::size_of::<InterpolationColorSpace>(), 44);
434		assert_eq!(std::mem::size_of::<RectangularColorSpace>(), 16);
435		assert_eq!(std::mem::size_of::<PolarColorSpace>(), 16);
436		assert_eq!(std::mem::size_of::<HueInterpolationMethod>(), 28);
437		assert_eq!(std::mem::size_of::<HueInterpolationDirection>(), 16);
438		assert_eq!(std::mem::size_of::<ColorMixPart>(), 40);
439	}
440
441	#[test]
442	fn test_writes() {
443		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red,blue)");
444		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red 50%,blue 50%)");
445		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in oklch,red,blue)");
446		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in oklch longer hue,red,blue)");
447		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in hsl shorter hue,red,blue)");
448		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in hsl increasing hue,red,blue)");
449		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in hsl decreasing hue,red,blue)");
450		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in lab,rgb(255 0 0),rgb(0 0 255))");
451		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,50% red,blue)");
452		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red 50%,blue)");
453		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in oklab,#fff 30%,#000 70%)");
454		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in xyz-d50,red,green)");
455		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in xyz-d65,red,green)");
456		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb-linear,red,green)");
457		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(red,blue)");
458		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(red 50%,blue 50%)");
459		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in oklab,red,blue,green)");
460		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(in srgb,red 33%,blue 33%,green 34%)");
461		assert_parse!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(red,blue,green)");
462	}
463
464	#[test]
465	#[cfg(feature = "visitable")]
466	fn test_visits() {
467		use crate::assert_visits;
468		// Named colors
469		assert_visits!("color-mix(in srgb, red, blue)", ColorMixFunction, ColorInterpolationMethod, Color, Color,);
470		// Function colors recurse into ColorFunction and its variant
471		assert_visits!(
472			"color-mix(in srgb, rgb(255, 0, 0), blue)",
473			ColorMixFunction,
474			ColorInterpolationMethod,
475			Color,
476			ColorFunction,
477			RgbFunction,
478			Color,
479		);
480		// Percentages are visited
481		assert_visits!(
482			"color-mix(in srgb, red 50%, blue 50%)",
483			ColorMixFunction,
484			ColorInterpolationMethod,
485			Color,
486			Percentage,
487			Color,
488			Percentage,
489		);
490		// Polar color space with hue interpolation
491		assert_visits!(
492			"color-mix(in oklch shorter hue, red, blue)",
493			ColorMixFunction,
494			ColorInterpolationMethod,
495			Color,
496			Color,
497		);
498		assert_visits!("color-mix(red, blue)", ColorMixFunction, Color, Color,);
499		assert_visits!(
500			"color-mix(in oklab, red, blue, green)",
501			ColorMixFunction,
502			ColorInterpolationMethod,
503			Color,
504			Color,
505			Color,
506		);
507	}
508
509	#[test]
510	fn test_errors() {
511		assert_parse_error!(CssAtomSet::ATOMS, ColorMixFunction, "color-mix(srgb,red,blue)");
512	}
513}