Skip to main content

style/values/specified/
image.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! CSS handling for the specified value of
6//! [`image`][image]s
7//!
8//! [image]: https://drafts.csswg.org/css-images/#image-values
9
10use crate::color::mix::ColorInterpolationMethod;
11use crate::derives::*;
12use crate::parser::{Parse, ParserContext};
13use crate::stylesheets::CorsMode;
14use crate::typed_om::{ImageValue, KeywordValue, ToTyped, TypedValue};
15use crate::values::generics::color::{ColorMixFlags, GenericLightDark};
16use crate::values::generics::image::{
17    self as generic, Circle, Ellipse, GradientCompatMode, ShapeExtent,
18};
19use crate::values::generics::image::{GradientFlags, PaintWorklet};
20use crate::values::generics::position::Position as GenericPosition;
21use crate::values::generics::NonNegative;
22use crate::values::specified::position::{HorizontalPositionKeyword, VerticalPositionKeyword};
23use crate::values::specified::position::{Position, PositionComponent, Side};
24use crate::values::specified::url::SpecifiedUrl;
25use crate::values::specified::{
26    Angle, AngleOrPercentage, Color, Length, LengthPercentage, NonNegativeLength,
27    NonNegativeLengthPercentage, Resolution,
28};
29use crate::values::specified::{Number, NumberOrPercentage, Percentage};
30use crate::Atom;
31use cssparser::{match_ignore_ascii_case, Delimiter, Parser, Token};
32use selectors::parser::SelectorParseErrorKind;
33use std::cmp::Ordering;
34use std::fmt::{self, Write};
35use style_traits::{CssString, CssType, CssWriter, KeywordsCollectFn, ParseError};
36use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss};
37use thin_vec::ThinVec;
38
39/// Specified values for an image according to CSS-IMAGES.
40/// <https://drafts.csswg.org/css-images/#image-values>
41pub type Image = generic::Image<Gradient, SpecifiedUrl, Color, Percentage, Resolution>;
42
43impl ToTyped for Image {
44    fn to_typed(&self, dest: &mut ThinVec<TypedValue>) -> Result<(), ()> {
45        match *self {
46            Image::None => {
47                dest.push(TypedValue::Keyword(KeywordValue(CssString::from("none"))));
48                Ok(())
49            },
50            Image::Url(ref url) => {
51                dest.push(TypedValue::Image(ImageValue::Specified(url.clone())));
52                Ok(())
53            },
54            _ => Err(()),
55        }
56    }
57}
58
59// Images should remain small, see https://github.com/servo/servo/pull/18430
60size_of_test!(Image, 16);
61
62/// Specified values for a CSS gradient.
63/// <https://drafts.csswg.org/css-images/#gradients>
64pub type Gradient = generic::Gradient<
65    LineDirection,
66    Length,
67    LengthPercentage,
68    Position,
69    Angle,
70    AngleOrPercentage,
71    Color,
72>;
73
74/// Specified values for CSS cross-fade
75/// cross-fade( CrossFadeElement, ...)
76/// <https://drafts.csswg.org/css-images-4/#cross-fade-function>
77pub type CrossFade = generic::CrossFade<Image, Color, Percentage>;
78/// CrossFadeElement = percent? CrossFadeImage
79pub type CrossFadeElement = generic::CrossFadeElement<Image, Color, Percentage>;
80/// CrossFadeImage = image | color
81pub type CrossFadeImage = generic::CrossFadeImage<Image, Color>;
82
83/// `image-set()`
84pub type ImageSet = generic::ImageSet<Image, Resolution>;
85
86/// Each of the arguments to `image-set()`
87pub type ImageSetItem = generic::ImageSetItem<Image, Resolution>;
88
89type LengthPercentageItemList = crate::OwnedSlice<generic::GradientItem<Color, LengthPercentage>>;
90
91impl Color {
92    fn has_modern_syntax(&self) -> bool {
93        match self {
94            Self::Absolute(absolute) => !absolute.color.is_legacy_syntax(),
95            Self::ColorMix(mix) => {
96                if mix.flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) {
97                    true
98                } else {
99                    mix.items.iter().any(|item| item.color.has_modern_syntax())
100                }
101            },
102            Self::LightDark(ld) => ld.light.has_modern_syntax() || ld.dark.has_modern_syntax(),
103
104            // The default is that this color doesn't have any modern syntax.
105            _ => false,
106        }
107    }
108}
109
110fn default_color_interpolation_method<T>(
111    items: &[generic::GradientItem<Color, T>],
112) -> ColorInterpolationMethod {
113    let has_modern_syntax_item = items.iter().any(|item| match item {
114        generic::GenericGradientItem::SimpleColorStop(color) => color.has_modern_syntax(),
115        generic::GenericGradientItem::ComplexColorStop { color, .. } => color.has_modern_syntax(),
116        generic::GenericGradientItem::InterpolationHint(_) => false,
117    });
118
119    if has_modern_syntax_item {
120        ColorInterpolationMethod::default()
121    } else {
122        ColorInterpolationMethod::srgb()
123    }
124}
125
126fn cross_fade_enabled() -> bool {
127    crate::pref!("layout.css.cross-fade.enabled")
128}
129
130impl SpecifiedValueInfo for Gradient {
131    const SUPPORTED_TYPES: u8 = CssType::GRADIENT;
132
133    fn collect_completion_keywords(f: KeywordsCollectFn) {
134        // This list here should keep sync with that in Gradient::parse.
135        f(&[
136            "linear-gradient",
137            "-webkit-linear-gradient",
138            "-moz-linear-gradient",
139            "repeating-linear-gradient",
140            "-webkit-repeating-linear-gradient",
141            "-moz-repeating-linear-gradient",
142            "radial-gradient",
143            "-webkit-radial-gradient",
144            "-moz-radial-gradient",
145            "repeating-radial-gradient",
146            "-webkit-repeating-radial-gradient",
147            "-moz-repeating-radial-gradient",
148            "-webkit-gradient",
149            "conic-gradient",
150            "repeating-conic-gradient",
151        ]);
152    }
153}
154
155// Need to manually implement as whether or not cross-fade shows up in
156// completions & etc is dependent on it being enabled.
157impl<Image, Color, Percentage> SpecifiedValueInfo for generic::CrossFade<Image, Color, Percentage> {
158    const SUPPORTED_TYPES: u8 = 0;
159
160    fn collect_completion_keywords(f: KeywordsCollectFn) {
161        if cross_fade_enabled() {
162            f(&["cross-fade"]);
163        }
164    }
165}
166
167impl<Image, Resolution> SpecifiedValueInfo for generic::ImageSet<Image, Resolution> {
168    const SUPPORTED_TYPES: u8 = 0;
169
170    fn collect_completion_keywords(f: KeywordsCollectFn) {
171        f(&["image-set"]);
172    }
173}
174
175/// A specified gradient line direction.
176///
177/// FIXME(emilio): This should be generic over Angle.
178#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
179pub enum LineDirection {
180    /// An angular direction.
181    Angle(Angle),
182    /// A horizontal direction.
183    Horizontal(HorizontalPositionKeyword),
184    /// A vertical direction.
185    Vertical(VerticalPositionKeyword),
186    /// A direction towards a corner of a box.
187    Corner(HorizontalPositionKeyword, VerticalPositionKeyword),
188}
189
190/// A specified ending shape.
191pub type EndingShape = generic::EndingShape<NonNegativeLength, NonNegativeLengthPercentage>;
192
193bitflags! {
194    #[derive(Clone, Copy)]
195    struct ParseImageFlags: u8 {
196        const FORBID_NONE = 1 << 0;
197        const FORBID_IMAGE_SET = 1 << 1;
198        const FORBID_NON_URL = 1 << 2;
199    }
200}
201
202impl Parse for Image {
203    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Image, ParseError> {
204        Image::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::empty())
205    }
206}
207
208impl Image {
209    fn parse_with_cors_mode(
210        context: &ParserContext,
211        input: &mut Parser,
212        cors_mode: CorsMode,
213        flags: ParseImageFlags,
214    ) -> Result<Image, ParseError> {
215        if !flags.contains(ParseImageFlags::FORBID_NONE)
216            && input.try_parse(|i| i.expect_ident_matching("none")).is_ok()
217        {
218            return Ok(generic::Image::None);
219        }
220
221        if let Ok(url) =
222            input.try_parse(|input| SpecifiedUrl::parse_with_cors_mode(context, input, cors_mode))
223        {
224            return Ok(generic::Image::Url(url));
225        }
226
227        if !flags.contains(ParseImageFlags::FORBID_IMAGE_SET) {
228            if let Ok(is) =
229                input.try_parse(|input| ImageSet::parse(context, input, cors_mode, flags))
230            {
231                return Ok(generic::Image::ImageSet(Box::new(is)));
232            }
233        }
234
235        if flags.contains(ParseImageFlags::FORBID_NON_URL) {
236            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
237        }
238
239        if let Ok(gradient) = input.try_parse(|i| Gradient::parse(context, i)) {
240            return Ok(generic::Image::Gradient(Box::new(gradient)));
241        }
242
243        let function = input.expect_function()?.clone();
244        input.parse_nested_block(|input| Ok(match_ignore_ascii_case! { &function,
245            #[cfg(feature = "servo")]
246            "paint" => Self::PaintWorklet(Box::new(<PaintWorklet>::parse_args(context, input)?)),
247            "cross-fade" if cross_fade_enabled() => Self::CrossFade(Box::new(CrossFade::parse_args(context, input, cors_mode, flags)?)),
248            "image" => Self::Image(Box::new(Color::parse(context, input)?)),
249            "light-dark" if crate::pref!("layout.css.light-dark.images.enabled", gecko = true) => {
250                Self::LightDark(Box::new(GenericLightDark::parse_args_with(input, |input| {
251                    // `none` in `light-dark()` has a special meaning.
252                    Self::parse_with_cors_mode(context, input, cors_mode, flags & !ParseImageFlags::FORBID_NONE)
253                })?))
254            },
255            #[cfg(feature = "gecko")]
256            "-moz-element" => Self::Element(Self::parse_element(input)?),
257            #[cfg(feature = "gecko")]
258            "-moz-symbolic-icon" if context.chrome_rules_enabled() => Self::MozSymbolicIcon(input.expect_ident()?.as_ref().into()),
259            _ => return Err(ParseError::custom(StyleParseErrorKind::UnexpectedFunction)),
260        }))
261    }
262}
263
264impl Image {
265    /// Creates an already specified image value from an already resolved URL
266    /// for insertion in the cascade.
267    #[cfg(feature = "servo")]
268    pub fn for_cascade(url: ::servo_arc::Arc<::url::Url>) -> Self {
269        use crate::values::CssUrl;
270        generic::Image::Url(CssUrl::for_cascade(url))
271    }
272
273    /// Parses a `-moz-element(# <element-id>)`.
274    #[cfg(feature = "gecko")]
275    fn parse_element(input: &mut Parser) -> Result<Atom, ParseError> {
276        Ok(match *input.next()? {
277            Token::IDHash(ref id) => Atom::from(id.as_ref()),
278            _ => return Err(ParseError::unexpected_token()),
279        })
280    }
281
282    /// Provides an alternate method for parsing that associates the URL with
283    /// anonymous CORS headers.
284    pub fn parse_with_cors_anonymous(
285        context: &ParserContext,
286        input: &mut Parser,
287    ) -> Result<Image, ParseError> {
288        Self::parse_with_cors_mode(
289            context,
290            input,
291            CorsMode::Anonymous,
292            ParseImageFlags::empty(),
293        )
294    }
295
296    /// Provides an alternate method for parsing, but forbidding `none`
297    pub fn parse_forbid_none(
298        context: &ParserContext,
299        input: &mut Parser,
300    ) -> Result<Image, ParseError> {
301        Self::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::FORBID_NONE)
302    }
303
304    /// Provides an alternate method for parsing, but only for urls.
305    pub fn parse_only_url(
306        context: &ParserContext,
307        input: &mut Parser,
308    ) -> Result<Image, ParseError> {
309        Self::parse_with_cors_mode(
310            context,
311            input,
312            CorsMode::None,
313            ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_NON_URL,
314        )
315    }
316}
317
318impl CrossFade {
319    /// cross-fade() = cross-fade( <cf-image># )
320    fn parse_args(
321        context: &ParserContext,
322        input: &mut Parser,
323        cors_mode: CorsMode,
324        flags: ParseImageFlags,
325    ) -> Result<Self, ParseError> {
326        let elements = crate::OwnedSlice::from(input.parse_comma_separated(|input| {
327            CrossFadeElement::parse(context, input, cors_mode, flags)
328        })?);
329        Ok(Self { elements })
330    }
331}
332
333impl CrossFadeElement {
334    fn parse_percentage(context: &ParserContext, input: &mut Parser) -> Option<Percentage> {
335        // We clamp our values here as this is the way that Safari and Chrome's
336        // implementation handle out-of-bounds percentages but whether or not
337        // this behavior follows the specification is still being discussed.
338        // See: <https://github.com/w3c/csswg-drafts/issues/5333>
339        let mut p = input
340            .try_parse(|input| Percentage::parse_non_negative(context, input))
341            .ok()?;
342        p.clamp_to_hundred();
343        Some(p)
344    }
345
346    /// <cf-image> = <percentage>? && [ <image> | <color> ]
347    fn parse(
348        context: &ParserContext,
349        input: &mut Parser,
350        cors_mode: CorsMode,
351        flags: ParseImageFlags,
352    ) -> Result<Self, ParseError> {
353        // Try and parse a leading percent sign.
354        let mut percent = Self::parse_percentage(context, input);
355        // Parse the image
356        let image = CrossFadeImage::parse(context, input, cors_mode, flags)?;
357        // Try and parse a trailing percent sign.
358        if percent.is_none() {
359            percent = Self::parse_percentage(context, input);
360        }
361        Ok(Self {
362            percent: percent.into(),
363            image,
364        })
365    }
366}
367
368impl CrossFadeImage {
369    fn parse(
370        context: &ParserContext,
371        input: &mut Parser,
372        cors_mode: CorsMode,
373        flags: ParseImageFlags,
374    ) -> Result<Self, ParseError> {
375        if let Ok(image) = input.try_parse(|input| {
376            Image::parse_with_cors_mode(
377                context,
378                input,
379                cors_mode,
380                flags | ParseImageFlags::FORBID_NONE,
381            )
382        }) {
383            return Ok(Self::Image(image));
384        }
385        Ok(Self::Color(Color::parse(context, input)?))
386    }
387}
388
389impl ImageSet {
390    fn parse(
391        context: &ParserContext,
392        input: &mut Parser,
393        cors_mode: CorsMode,
394        flags: ParseImageFlags,
395    ) -> Result<Self, ParseError> {
396        let function = input.expect_function()?;
397        match_ignore_ascii_case! { &function,
398            "-webkit-image-set" | "image-set" => {},
399            _ => {
400                return Err(ParseError::custom(StyleParseErrorKind::UnexpectedFunction));
401            }
402        }
403        let items = input.parse_nested_block(|input| {
404            input.parse_comma_separated(|input| {
405                ImageSetItem::parse(context, input, cors_mode, flags)
406            })
407        })?;
408        Ok(Self {
409            selected_index: usize::MAX,
410            items: items.into(),
411        })
412    }
413}
414
415impl ImageSetItem {
416    fn parse_type(p: &mut Parser) -> Result<crate::OwnedStr, ParseError> {
417        p.expect_function_matching("type")?;
418        p.parse_nested_block(|input| Ok(input.expect_string()?.as_ref().to_owned().into()))
419    }
420
421    fn parse(
422        context: &ParserContext,
423        input: &mut Parser,
424        cors_mode: CorsMode,
425        flags: ParseImageFlags,
426    ) -> Result<Self, ParseError> {
427        let start = input.position().byte_index();
428        let image = match input.try_parse(|i| i.expect_url_or_string()) {
429            Ok(url) => {
430                let end = input.position().byte_index();
431                Image::Url(SpecifiedUrl::parse_from_string(
432                    url.as_ref().into(),
433                    start,
434                    end,
435                    context,
436                    cors_mode,
437                )?)
438            },
439            Err(..) => Image::parse_with_cors_mode(
440                context,
441                input,
442                cors_mode,
443                flags | ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_IMAGE_SET,
444            )?,
445        };
446
447        let mut resolution = input
448            .try_parse(|input| Resolution::parse(context, input))
449            .ok();
450        let mime_type = input.try_parse(Self::parse_type).ok();
451
452        // Try to parse resolution after type().
453        if mime_type.is_some() && resolution.is_none() {
454            resolution = input
455                .try_parse(|input| Resolution::parse(context, input))
456                .ok();
457        }
458
459        let resolution = resolution.unwrap_or_else(|| Resolution::from_x(1.0));
460        let has_mime_type = mime_type.is_some();
461        let mime_type = mime_type.unwrap_or_default();
462
463        Ok(Self {
464            image,
465            resolution,
466            has_mime_type,
467            mime_type,
468        })
469    }
470}
471
472impl Parse for Gradient {
473    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
474        enum Shape {
475            Linear,
476            Radial,
477            Conic,
478        }
479
480        let func = input.expect_function()?;
481        let (shape, repeating, compat_mode) = match_ignore_ascii_case! { &func,
482            "linear-gradient" => {
483                (Shape::Linear, false, GradientCompatMode::Modern)
484            },
485            "-webkit-linear-gradient" => {
486                (Shape::Linear, false, GradientCompatMode::WebKit)
487            },
488            #[cfg(feature = "gecko")]
489            "-moz-linear-gradient" => {
490                (Shape::Linear, false, GradientCompatMode::Moz)
491            },
492            "repeating-linear-gradient" => {
493                (Shape::Linear, true, GradientCompatMode::Modern)
494            },
495            "-webkit-repeating-linear-gradient" => {
496                (Shape::Linear, true, GradientCompatMode::WebKit)
497            },
498            #[cfg(feature = "gecko")]
499            "-moz-repeating-linear-gradient" => {
500                (Shape::Linear, true, GradientCompatMode::Moz)
501            },
502            "radial-gradient" => {
503                (Shape::Radial, false, GradientCompatMode::Modern)
504            },
505            "-webkit-radial-gradient" => {
506                (Shape::Radial, false, GradientCompatMode::WebKit)
507            },
508            #[cfg(feature = "gecko")]
509            "-moz-radial-gradient" => {
510                (Shape::Radial, false, GradientCompatMode::Moz)
511            },
512            "repeating-radial-gradient" => {
513                (Shape::Radial, true, GradientCompatMode::Modern)
514            },
515            "-webkit-repeating-radial-gradient" => {
516                (Shape::Radial, true, GradientCompatMode::WebKit)
517            },
518            #[cfg(feature = "gecko")]
519            "-moz-repeating-radial-gradient" => {
520                (Shape::Radial, true, GradientCompatMode::Moz)
521            },
522            "conic-gradient" => {
523                (Shape::Conic, false, GradientCompatMode::Modern)
524            },
525            "repeating-conic-gradient" => {
526                (Shape::Conic, true, GradientCompatMode::Modern)
527            },
528            "-webkit-gradient" => {
529                return input.parse_nested_block(|i| {
530                    Self::parse_webkit_gradient_argument(context, i)
531                });
532            },
533            _ => {
534                return Err(ParseError::custom(StyleParseErrorKind::UnexpectedFunction));
535            }
536        };
537
538        input.parse_nested_block(|i| {
539            Ok(match shape {
540                Shape::Linear => Self::parse_linear(context, i, repeating, compat_mode)?,
541                Shape::Radial => Self::parse_radial(context, i, repeating, compat_mode)?,
542                Shape::Conic => Self::parse_conic(context, i, repeating)?,
543            })
544        })
545    }
546}
547
548impl Gradient {
549    fn parse_webkit_gradient_argument(
550        context: &ParserContext,
551        input: &mut Parser,
552    ) -> Result<Self, ParseError> {
553        use crate::values::specified::position::{
554            HorizontalPositionKeyword as X, VerticalPositionKeyword as Y,
555        };
556        type Point = GenericPosition<Component<X>, Component<Y>>;
557
558        #[derive(Clone, Parse)]
559        enum Component<S> {
560            Center,
561            Number(NumberOrPercentage),
562            Side(S),
563        }
564
565        fn line_direction_from_points(first: Point, second: Point) -> LineDirection {
566            let h_ord = first.horizontal.partial_cmp(&second.horizontal);
567            let v_ord = first.vertical.partial_cmp(&second.vertical);
568            let (h, v) = match (h_ord, v_ord) {
569                (Some(h), Some(v)) => (h, v),
570                _ => return LineDirection::Vertical(Y::Bottom),
571            };
572            match (h, v) {
573                (Ordering::Less, Ordering::Less) => LineDirection::Corner(X::Right, Y::Bottom),
574                (Ordering::Less, Ordering::Equal) => LineDirection::Horizontal(X::Right),
575                (Ordering::Less, Ordering::Greater) => LineDirection::Corner(X::Right, Y::Top),
576                (Ordering::Equal, Ordering::Greater) => LineDirection::Vertical(Y::Top),
577                (Ordering::Equal, Ordering::Equal) | (Ordering::Equal, Ordering::Less) => {
578                    LineDirection::Vertical(Y::Bottom)
579                },
580                (Ordering::Greater, Ordering::Less) => LineDirection::Corner(X::Left, Y::Bottom),
581                (Ordering::Greater, Ordering::Equal) => LineDirection::Horizontal(X::Left),
582                (Ordering::Greater, Ordering::Greater) => LineDirection::Corner(X::Left, Y::Top),
583            }
584        }
585
586        impl Parse for Point {
587            fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
588                input.try_parse(|i| {
589                    let x = Component::parse(context, i)?;
590                    let y = Component::parse(context, i)?;
591
592                    // TODO(Bug 2037751) - Enable calc()-expressions that can only be resolved at
593                    // computed value time (due to relative lengths, sibling-index(), etc.).
594                    if matches!(&x, Component::Number(NumberOrPercentage::Number(n)) if n.resolve().is_none()) ||
595                        matches!(&y, Component::Number(NumberOrPercentage::Number(n)) if n.resolve().is_none())
596                    {
597                        return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
598                    }
599
600                    Ok(Self::new(x, y))
601                })
602            }
603        }
604
605        impl<S: Side> From<Component<S>> for NumberOrPercentage {
606            fn from(val: Component<S>) -> Self {
607                match val {
608                    Component::Center => NumberOrPercentage::Percentage(Percentage::new(0.5)),
609                    Component::Number(number) => number,
610                    Component::Side(side) => {
611                        let p = if side.is_start() {
612                            Percentage::zero()
613                        } else {
614                            Percentage::hundred()
615                        };
616                        NumberOrPercentage::Percentage(p)
617                    },
618                }
619            }
620        }
621
622        impl<S: Side> From<Component<S>> for PositionComponent<S> {
623            fn from(val: Component<S>) -> Self {
624                match val {
625                    Component::Center => PositionComponent::Center,
626                    Component::Number(NumberOrPercentage::Number(number)) => {
627                        // Unresolvable calc is rejected in Point::parse.
628                        PositionComponent::Length(Length::from_px(number.resolve().unwrap()).into())
629                    },
630                    Component::Number(NumberOrPercentage::Percentage(p)) => {
631                        PositionComponent::Length(p.to_length_percentage())
632                    },
633                    Component::Side(side) => PositionComponent::Side(side, None),
634                }
635            }
636        }
637
638        impl<S: Copy + Side> Component<S> {
639            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
640                match (self.clone().into(), other.clone().into()) {
641                    (
642                        NumberOrPercentage::Percentage(ref a),
643                        NumberOrPercentage::Percentage(ref b),
644                    ) => a.resolve().partial_cmp(&b.resolve()),
645                    (NumberOrPercentage::Number(a), NumberOrPercentage::Number(b)) => {
646                        a.resolve().partial_cmp(&b.resolve())
647                    },
648                    (_, _) => None,
649                }
650            }
651        }
652
653        let ident = input.expect_ident_cloned()?;
654        input.expect_comma()?;
655
656        Ok(match_ignore_ascii_case! { &ident,
657            "linear" => {
658                let first = Point::parse(context, input)?;
659                input.expect_comma()?;
660                let second = Point::parse(context, input)?;
661
662                let direction = line_direction_from_points(first, second);
663                let items = Gradient::parse_webkit_gradient_stops(context, input, false)?;
664
665                generic::Gradient::Linear {
666                    direction,
667                    color_interpolation_method: ColorInterpolationMethod::srgb(),
668                    items,
669                    // Legacy gradients always use srgb as a default.
670                    flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
671                    compat_mode: GradientCompatMode::Modern,
672                }
673            },
674            "radial" => {
675                let first_point = Point::parse(context, input)?;
676                input.expect_comma()?;
677                let first_radius = Number::parse_non_negative(context, input)?;
678                input.expect_comma()?;
679                let second_point = Point::parse(context, input)?;
680                input.expect_comma()?;
681                let second_radius = Number::parse_non_negative(context, input)?;
682
683                // TODO(Bug 2037751) - Enable calc()-expressions that can only be resolved at
684                // computed value time (due to relative lengths, sibling-index(), etc.).
685                if first_radius.resolve().is_none() || second_radius.resolve().is_none() {
686                    return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
687                }
688
689                let (reverse_stops, point, radius) = if second_radius.resolve() >= first_radius.resolve() {
690                    (false, second_point, second_radius)
691                } else {
692                    (true, first_point, first_radius)
693                };
694
695                // Unresolvable calc is rejected above.
696                let rad = Circle::Radius(NonNegative(Length::from_px(radius.resolve().unwrap())));
697                let shape = generic::EndingShape::Circle(rad);
698                let position = Position::new(point.horizontal.into(), point.vertical.into());
699                let items = Gradient::parse_webkit_gradient_stops(context, input, reverse_stops)?;
700
701                generic::Gradient::Radial {
702                    shape,
703                    position,
704                    color_interpolation_method: ColorInterpolationMethod::srgb(),
705                    items,
706                    // Legacy gradients always use srgb as a default.
707                    flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
708                    compat_mode: GradientCompatMode::Modern,
709                }
710            },
711            _ => {
712                let e = SelectorParseErrorKind::UnexpectedIdent;
713                return Err(ParseError::custom(e));
714            },
715        })
716    }
717
718    fn parse_webkit_gradient_stops(
719        context: &ParserContext,
720        input: &mut Parser,
721        reverse_stops: bool,
722    ) -> Result<LengthPercentageItemList, ParseError> {
723        let mut items = input
724            .try_parse(|i| {
725                i.expect_comma()?;
726                i.parse_comma_separated(|i| {
727                    let function = i.expect_function()?.clone();
728                    let (color, mut p) = i.parse_nested_block(|i| {
729                        let p = match_ignore_ascii_case! { &function,
730                            "color-stop" => {
731                                // TODO(Bug 2037751) - Enable calc()-expressions that can only be resolved at
732                                // computed value time (due to relative lengths, sibling-index(), etc.).
733                                let Some(p) = NumberOrPercentage::parse(context, i)?.to_percentage() else {
734                                    return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
735                                };
736                                i.expect_comma()?;
737                                p
738                            },
739                            "from" => Percentage::zero(),
740                            "to" => Percentage::hundred(),
741                            _ => {
742                                return Err(ParseError::custom(
743                                    StyleParseErrorKind::UnexpectedFunction
744                                ))
745                            },
746                        };
747                        let color = Color::parse(context, i)?;
748                        if color == Color::CurrentColor {
749                            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
750                        }
751                        Ok((color, p))
752                    })?;
753                    if reverse_stops {
754                        p.reverse();
755                    }
756                    Ok(generic::GradientItem::ComplexColorStop {
757                        color,
758                        position: p.to_length_percentage(),
759                    })
760                })
761            })
762            .unwrap_or_default();
763
764        if items.is_empty() {
765            items = vec![
766                generic::GradientItem::ComplexColorStop {
767                    color: Color::transparent(),
768                    position: LengthPercentage::zero_percent(),
769                },
770                generic::GradientItem::ComplexColorStop {
771                    color: Color::transparent(),
772                    position: LengthPercentage::hundred_percent(),
773                },
774            ];
775        } else if items.len() == 1 {
776            let first = items[0].clone();
777            items.push(first);
778        } else {
779            items.sort_by(|a, b| {
780                if let (
781                    generic::GradientItem::ComplexColorStop {
782                        position: a_position,
783                        ..
784                    },
785                    generic::GradientItem::ComplexColorStop {
786                        position: b_position,
787                        ..
788                    },
789                ) = (a, b)
790                {
791                    if let (&LengthPercentage::Percentage(a), &LengthPercentage::Percentage(b)) =
792                        (a_position, b_position)
793                    {
794                        return a.get().partial_cmp(&b.get()).unwrap_or(Ordering::Equal);
795                    }
796                }
797                if reverse_stops {
798                    Ordering::Greater
799                } else {
800                    Ordering::Less
801                }
802            })
803        }
804        Ok(items.into())
805    }
806
807    /// Not used for -webkit-gradient syntax and conic-gradient
808    fn parse_stops(
809        context: &ParserContext,
810        input: &mut Parser,
811    ) -> Result<LengthPercentageItemList, ParseError> {
812        let items =
813            generic::GradientItem::parse_comma_separated(context, input, LengthPercentage::parse)?;
814        if items.is_empty() {
815            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
816        }
817        Ok(items)
818    }
819
820    /// Parses a linear gradient.
821    /// GradientCompatMode can change during `-moz-` prefixed gradient parsing if it come across a `to` keyword.
822    fn parse_linear(
823        context: &ParserContext,
824        input: &mut Parser,
825        repeating: bool,
826        mut compat_mode: GradientCompatMode,
827    ) -> Result<Self, ParseError> {
828        let mut flags = GradientFlags::empty();
829        flags.set(GradientFlags::REPEATING, repeating);
830
831        let mut color_interpolation_method = input
832            .try_parse(|i| ColorInterpolationMethod::parse(context, i))
833            .ok();
834
835        let direction = input
836            .try_parse(|p| LineDirection::parse(context, p, &mut compat_mode))
837            .ok();
838
839        if direction.is_some() && color_interpolation_method.is_none() {
840            color_interpolation_method = input
841                .try_parse(|i| ColorInterpolationMethod::parse(context, i))
842                .ok();
843        }
844
845        // If either of the 2 options were specified, we require a comma.
846        if color_interpolation_method.is_some() || direction.is_some() {
847            input.expect_comma()?;
848        }
849
850        let items = Gradient::parse_stops(context, input)?;
851
852        let default = default_color_interpolation_method(&items);
853        let color_interpolation_method = color_interpolation_method.unwrap_or(default);
854        flags.set(
855            GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
856            default == color_interpolation_method,
857        );
858
859        let direction = direction.unwrap_or(match compat_mode {
860            GradientCompatMode::Modern => LineDirection::Vertical(VerticalPositionKeyword::Bottom),
861            _ => LineDirection::Vertical(VerticalPositionKeyword::Top),
862        });
863
864        Ok(Gradient::Linear {
865            direction,
866            color_interpolation_method,
867            items,
868            flags,
869            compat_mode,
870        })
871    }
872
873    /// Parses a radial gradient.
874    fn parse_radial(
875        context: &ParserContext,
876        input: &mut Parser,
877        repeating: bool,
878        compat_mode: GradientCompatMode,
879    ) -> Result<Self, ParseError> {
880        let mut flags = GradientFlags::empty();
881        flags.set(GradientFlags::REPEATING, repeating);
882
883        let mut color_interpolation_method = input
884            .try_parse(|i| ColorInterpolationMethod::parse(context, i))
885            .ok();
886
887        let (shape, position) = match compat_mode {
888            GradientCompatMode::Modern => {
889                let shape = input.try_parse(|i| EndingShape::parse(context, i, compat_mode));
890                let position = input.try_parse(|i| {
891                    i.expect_ident_matching("at")?;
892                    Position::parse(context, i)
893                });
894                (shape, position.ok())
895            },
896            _ => {
897                let position = input.try_parse(|i| Position::parse(context, i));
898                let shape = input.try_parse(|i| {
899                    if position.is_ok() {
900                        i.expect_comma()?;
901                    }
902                    EndingShape::parse(context, i, compat_mode)
903                });
904                (shape, position.ok())
905            },
906        };
907
908        let has_shape_or_position = shape.is_ok() || position.is_some();
909        if has_shape_or_position && color_interpolation_method.is_none() {
910            color_interpolation_method = input
911                .try_parse(|i| ColorInterpolationMethod::parse(context, i))
912                .ok();
913        }
914
915        if has_shape_or_position || color_interpolation_method.is_some() {
916            input.expect_comma()?;
917        }
918
919        let shape = shape.unwrap_or({
920            generic::EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner))
921        });
922
923        let position = position.unwrap_or(Position::center());
924
925        let items = Gradient::parse_stops(context, input)?;
926
927        let default = default_color_interpolation_method(&items);
928        let color_interpolation_method = color_interpolation_method.unwrap_or(default);
929        flags.set(
930            GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
931            default == color_interpolation_method,
932        );
933
934        Ok(Gradient::Radial {
935            shape,
936            position,
937            color_interpolation_method,
938            items,
939            flags,
940            compat_mode,
941        })
942    }
943
944    /// Parse a conic gradient.
945    fn parse_conic(
946        context: &ParserContext,
947        input: &mut Parser,
948        repeating: bool,
949    ) -> Result<Self, ParseError> {
950        let mut flags = GradientFlags::empty();
951        flags.set(GradientFlags::REPEATING, repeating);
952
953        let mut color_interpolation_method = input
954            .try_parse(|i| ColorInterpolationMethod::parse(context, i))
955            .ok();
956
957        let angle = input.try_parse(|i| {
958            i.expect_ident_matching("from")?;
959            // Spec allows unitless zero start angles
960            // https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle
961            Angle::parse_with_unitless(context, i)
962        });
963        let position = input.try_parse(|i| {
964            i.expect_ident_matching("at")?;
965            Position::parse(context, i)
966        });
967
968        let has_angle_or_position = angle.is_ok() || position.is_ok();
969        if has_angle_or_position && color_interpolation_method.is_none() {
970            color_interpolation_method = input
971                .try_parse(|i| ColorInterpolationMethod::parse(context, i))
972                .ok();
973        }
974
975        if has_angle_or_position || color_interpolation_method.is_some() {
976            input.expect_comma()?;
977        }
978
979        let angle = angle.unwrap_or(Angle::zero());
980
981        let position = position.unwrap_or(Position::center());
982
983        let items = generic::GradientItem::parse_comma_separated(
984            context,
985            input,
986            AngleOrPercentage::parse_with_unitless,
987        )?;
988
989        if items.is_empty() {
990            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
991        }
992
993        let default = default_color_interpolation_method(&items);
994        let color_interpolation_method = color_interpolation_method.unwrap_or(default);
995        flags.set(
996            GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
997            default == color_interpolation_method,
998        );
999
1000        Ok(Gradient::Conic {
1001            angle,
1002            position,
1003            color_interpolation_method,
1004            items,
1005            flags,
1006        })
1007    }
1008}
1009
1010impl generic::LineDirection for LineDirection {
1011    fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool {
1012        match *self {
1013            LineDirection::Angle(ref angle) => {
1014                angle.as_no_calc().is_some_and(|a| a.degrees() == 180.0)
1015            },
1016            LineDirection::Vertical(VerticalPositionKeyword::Bottom) => {
1017                compat_mode == GradientCompatMode::Modern
1018            },
1019            LineDirection::Vertical(VerticalPositionKeyword::Top) => {
1020                compat_mode != GradientCompatMode::Modern
1021            },
1022            _ => false,
1023        }
1024    }
1025
1026    fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result
1027    where
1028        W: Write,
1029    {
1030        match *self {
1031            LineDirection::Angle(ref angle) => angle.to_css(dest),
1032            LineDirection::Horizontal(x) => {
1033                if compat_mode == GradientCompatMode::Modern {
1034                    dest.write_str("to ")?;
1035                }
1036                x.to_css(dest)
1037            },
1038            LineDirection::Vertical(y) => {
1039                if compat_mode == GradientCompatMode::Modern {
1040                    dest.write_str("to ")?;
1041                }
1042                y.to_css(dest)
1043            },
1044            LineDirection::Corner(x, y) => {
1045                if compat_mode == GradientCompatMode::Modern {
1046                    dest.write_str("to ")?;
1047                }
1048                x.to_css(dest)?;
1049                dest.write_char(' ')?;
1050                y.to_css(dest)
1051            },
1052        }
1053    }
1054}
1055
1056impl LineDirection {
1057    fn parse(
1058        context: &ParserContext,
1059        input: &mut Parser,
1060        compat_mode: &mut GradientCompatMode,
1061    ) -> Result<Self, ParseError> {
1062        // Gradients allow unitless zero angles as an exception, see:
1063        // https://github.com/w3c/csswg-drafts/issues/1162
1064        if let Ok(angle) = input.try_parse(|i| Angle::parse_with_unitless(context, i)) {
1065            return Ok(LineDirection::Angle(angle));
1066        }
1067
1068        input.try_parse(|i| {
1069            let to_ident = i.try_parse(|i| i.expect_ident_matching("to"));
1070            match *compat_mode {
1071                // `to` keyword is mandatory in modern syntax.
1072                GradientCompatMode::Modern => to_ident?,
1073                // Fall back to Modern compatibility mode in case there is a `to` keyword.
1074                // According to Gecko, `-moz-linear-gradient(to ...)` should serialize like
1075                // `linear-gradient(to ...)`.
1076                GradientCompatMode::Moz if to_ident.is_ok() => {
1077                    *compat_mode = GradientCompatMode::Modern
1078                },
1079                // There is no `to` keyword in webkit prefixed syntax. If it's consumed,
1080                // parsing should throw an error.
1081                GradientCompatMode::WebKit if to_ident.is_ok() => {
1082                    return Err(ParseError::custom(SelectorParseErrorKind::UnexpectedIdent));
1083                },
1084                _ => {},
1085            }
1086
1087            if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) {
1088                if let Ok(y) = i.try_parse(VerticalPositionKeyword::parse) {
1089                    return Ok(LineDirection::Corner(x, y));
1090                }
1091                return Ok(LineDirection::Horizontal(x));
1092            }
1093            let y = VerticalPositionKeyword::parse(i)?;
1094            if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) {
1095                return Ok(LineDirection::Corner(x, y));
1096            }
1097            Ok(LineDirection::Vertical(y))
1098        })
1099    }
1100}
1101
1102impl EndingShape {
1103    fn parse(
1104        context: &ParserContext,
1105        input: &mut Parser,
1106        compat_mode: GradientCompatMode,
1107    ) -> Result<Self, ParseError> {
1108        if let Ok(extent) = input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode))
1109        {
1110            if input
1111                .try_parse(|i| i.expect_ident_matching("circle"))
1112                .is_ok()
1113            {
1114                return Ok(generic::EndingShape::Circle(Circle::Extent(extent)));
1115            }
1116            let _ = input.try_parse(|i| i.expect_ident_matching("ellipse"));
1117            return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent)));
1118        }
1119        if input
1120            .try_parse(|i| i.expect_ident_matching("circle"))
1121            .is_ok()
1122        {
1123            if let Ok(extent) =
1124                input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode))
1125            {
1126                return Ok(generic::EndingShape::Circle(Circle::Extent(extent)));
1127            }
1128            if compat_mode == GradientCompatMode::Modern {
1129                if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) {
1130                    return Ok(generic::EndingShape::Circle(Circle::Radius(length)));
1131                }
1132            }
1133            return Ok(generic::EndingShape::Circle(Circle::Extent(
1134                ShapeExtent::FarthestCorner,
1135            )));
1136        }
1137        if input
1138            .try_parse(|i| i.expect_ident_matching("ellipse"))
1139            .is_ok()
1140        {
1141            if let Ok(extent) =
1142                input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode))
1143            {
1144                return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent)));
1145            }
1146            if compat_mode == GradientCompatMode::Modern {
1147                let pair: Result<_, ParseError> = input.try_parse(|i| {
1148                    let x = NonNegativeLengthPercentage::parse(context, i)?;
1149                    let y = NonNegativeLengthPercentage::parse(context, i)?;
1150                    Ok((x, y))
1151                });
1152                if let Ok((x, y)) = pair {
1153                    return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(x, y)));
1154                }
1155            }
1156            return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(
1157                ShapeExtent::FarthestCorner,
1158            )));
1159        }
1160        if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) {
1161            if let Ok(y) = input.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) {
1162                if compat_mode == GradientCompatMode::Modern {
1163                    let _ = input.try_parse(|i| i.expect_ident_matching("ellipse"));
1164                }
1165                return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(
1166                    NonNegative(LengthPercentage::from(length.0)),
1167                    y,
1168                )));
1169            }
1170            if compat_mode == GradientCompatMode::Modern {
1171                let y = input.try_parse(|i| {
1172                    i.expect_ident_matching("ellipse")?;
1173                    NonNegativeLengthPercentage::parse(context, i)
1174                });
1175                if let Ok(y) = y {
1176                    return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(
1177                        NonNegative(LengthPercentage::from(length.0)),
1178                        y,
1179                    )));
1180                }
1181                let _ = input.try_parse(|i| i.expect_ident_matching("circle"));
1182            }
1183
1184            return Ok(generic::EndingShape::Circle(Circle::Radius(length)));
1185        }
1186        input.try_parse(|i| {
1187            let x = Percentage::parse_non_negative(context, i)?;
1188            let y = match i.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) {
1189                Ok(y) => {
1190                    if compat_mode == GradientCompatMode::Modern {
1191                        let _ = i.try_parse(|i| i.expect_ident_matching("ellipse"));
1192                    }
1193                    y
1194                },
1195                _ => {
1196                    if compat_mode == GradientCompatMode::Modern {
1197                        i.expect_ident_matching("ellipse")?;
1198                    }
1199                    NonNegativeLengthPercentage::parse(context, i)?
1200                },
1201            };
1202            Ok(generic::EndingShape::Ellipse(Ellipse::Radii(
1203                NonNegative(x.to_length_percentage()),
1204                y,
1205            )))
1206        })
1207    }
1208}
1209
1210impl ShapeExtent {
1211    fn parse_with_compat_mode(
1212        input: &mut Parser,
1213        compat_mode: GradientCompatMode,
1214    ) -> Result<Self, ParseError> {
1215        match Self::parse(input)? {
1216            ShapeExtent::Contain | ShapeExtent::Cover
1217                if compat_mode == GradientCompatMode::Modern =>
1218            {
1219                Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError))
1220            },
1221            ShapeExtent::Contain => Ok(ShapeExtent::ClosestSide),
1222            ShapeExtent::Cover => Ok(ShapeExtent::FarthestCorner),
1223            keyword => Ok(keyword),
1224        }
1225    }
1226}
1227
1228impl<T> generic::GradientItem<Color, T> {
1229    fn parse_comma_separated(
1230        context: &ParserContext,
1231        input: &mut Parser,
1232        parse_position: impl Fn(&ParserContext, &mut Parser) -> Result<T, ParseError> + Copy,
1233    ) -> Result<crate::OwnedSlice<Self>, ParseError> {
1234        let mut items = Vec::new();
1235        let mut seen_stop = false;
1236
1237        loop {
1238            input.parse_until_before(Delimiter::Comma, |input| {
1239                if seen_stop {
1240                    if let Ok(hint) = input.try_parse(|i| parse_position(context, i)) {
1241                        seen_stop = false;
1242                        items.push(generic::GradientItem::InterpolationHint(hint));
1243                        return Ok(());
1244                    }
1245                }
1246
1247                let stop = generic::ColorStop::parse(context, input, parse_position)?;
1248
1249                match input.try_parse(|i| parse_position(context, i)) {
1250                    Ok(multi_position) => {
1251                        let stop_color = stop.color.clone();
1252                        items.push(stop.into_item());
1253                        items.push(
1254                            generic::ColorStop {
1255                                color: stop_color,
1256                                position: Some(multi_position),
1257                            }
1258                            .into_item(),
1259                        );
1260                    },
1261                    _ => {
1262                        items.push(stop.into_item());
1263                    },
1264                }
1265
1266                seen_stop = true;
1267                Ok(())
1268            })?;
1269
1270            match input.next() {
1271                Err(_) => break,
1272                Ok(&Token::Comma) => continue,
1273                Ok(_) => unreachable!(),
1274            }
1275        }
1276
1277        if !seen_stop || items.is_empty() {
1278            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
1279        }
1280        Ok(items.into())
1281    }
1282}
1283
1284impl<T> generic::ColorStop<Color, T> {
1285    fn parse(
1286        context: &ParserContext,
1287        input: &mut Parser,
1288        parse_position: impl Fn(&ParserContext, &mut Parser) -> Result<T, ParseError>,
1289    ) -> Result<Self, ParseError> {
1290        Ok(generic::ColorStop {
1291            color: Color::parse(context, input)?,
1292            position: input.try_parse(|i| parse_position(context, i)).ok(),
1293        })
1294    }
1295}
1296
1297impl PaintWorklet {
1298    #[cfg(feature = "servo")]
1299    fn parse_args(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
1300        use crate::custom_properties::SpecifiedValue;
1301        use servo_arc::Arc;
1302        let name = Atom::from(&**input.expect_ident()?);
1303        let arguments = input
1304            .try_parse(|input| {
1305                input.expect_comma()?;
1306                input.parse_comma_separated(|input| {
1307                    SpecifiedValue::parse(
1308                        input,
1309                        Some(&context.namespaces.prefixes),
1310                        &context.url_data,
1311                    )
1312                    .map(Arc::new)
1313                })
1314            })
1315            .unwrap_or_default();
1316        Ok(Self { name, arguments })
1317    }
1318}
1319
1320/// https://drafts.csswg.org/css-images/#propdef-image-rendering
1321#[allow(missing_docs)]
1322#[derive(
1323    Clone,
1324    Copy,
1325    Debug,
1326    Eq,
1327    Hash,
1328    MallocSizeOf,
1329    Parse,
1330    PartialEq,
1331    SpecifiedValueInfo,
1332    ToCss,
1333    ToComputedValue,
1334    ToResolvedValue,
1335    ToShmem,
1336    ToTyped,
1337)]
1338#[repr(u8)]
1339pub enum ImageRendering {
1340    Auto,
1341    #[cfg(feature = "gecko")]
1342    Smooth,
1343    #[parse(aliases = "-moz-crisp-edges")]
1344    CrispEdges,
1345    Pixelated,
1346    // From the spec:
1347    //
1348    //     This property previously accepted the values optimizeSpeed and
1349    //     optimizeQuality. These are now deprecated; a user agent must accept
1350    //     them as valid values but must treat them as having the same behavior
1351    //     as crisp-edges and smooth respectively, and authors must not use
1352    //     them.
1353    //
1354    #[cfg(feature = "gecko")]
1355    Optimizespeed,
1356    #[cfg(feature = "gecko")]
1357    Optimizequality,
1358}
1359
1360/// Internal -moz-image-decoding property. This allows images to be forcefully sync-decoded.
1361///
1362/// We use different defaults for sync vs. async decoding, which sometimes cause site issues.
1363#[derive(
1364    Clone,
1365    Copy,
1366    Debug,
1367    Eq,
1368    FromPrimitive,
1369    MallocSizeOf,
1370    Parse,
1371    PartialEq,
1372    SpecifiedValueInfo,
1373    ToComputedValue,
1374    ToCss,
1375    ToResolvedValue,
1376    ToShmem,
1377    ToTyped,
1378)]
1379#[repr(u8)]
1380pub enum ImageDecoding {
1381    /// Use the default heuristics.
1382    Auto,
1383    /// Force sync-decoding.
1384    Sync,
1385}