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