Skip to main content

css_ast/functions/
color_mix_function.rs

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