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
39pub 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
59size_of_test!(Image, 16);
61
62pub type Gradient = generic::Gradient<
65 LineDirection,
66 Length,
67 LengthPercentage,
68 Position,
69 Angle,
70 AngleOrPercentage,
71 Color,
72>;
73
74pub type CrossFade = generic::CrossFade<Image, Color, Percentage>;
78pub type CrossFadeElement = generic::CrossFadeElement<Image, Color, Percentage>;
80pub type CrossFadeImage = generic::CrossFadeImage<Image, Color>;
82
83pub type ImageSet = generic::ImageSet<Image, Resolution>;
85
86pub 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 _ => 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 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
155impl<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#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
179pub enum LineDirection {
180 Angle(Angle),
182 Horizontal(HorizontalPositionKeyword),
184 Vertical(VerticalPositionKeyword),
186 Corner(HorizontalPositionKeyword, VerticalPositionKeyword),
188}
189
190pub 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 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 #[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 #[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 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 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 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 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 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 fn parse(
348 context: &ParserContext,
349 input: &mut Parser,
350 cors_mode: CorsMode,
351 flags: ParseImageFlags,
352 ) -> Result<Self, ParseError> {
353 let mut percent = Self::parse_percentage(context, input);
355 let image = CrossFadeImage::parse(context, input, cors_mode, flags)?;
357 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 GradientCompatMode::Modern => to_ident?,
1073 GradientCompatMode::Moz if to_ident.is_ok() => {
1077 *compat_mode = GradientCompatMode::Modern
1078 },
1079 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#[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 #[cfg(feature = "gecko")]
1355 Optimizespeed,
1356 #[cfg(feature = "gecko")]
1357 Optimizequality,
1358}
1359
1360#[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 Auto,
1383 Sync,
1385}