basic_shape.rs (38950B)
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 //! [`basic-shape`][basic-shape]s 7 //! 8 //! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape 9 10 use crate::derives::*; 11 use crate::parser::{Parse, ParserContext}; 12 use crate::values::computed::basic_shape::InsetRect as ComputedInsetRect; 13 use crate::values::computed::{ 14 Context, LengthPercentage as ComputedLengthPercentage, ToComputedValue, 15 }; 16 use crate::values::generics::basic_shape as generic; 17 use crate::values::generics::basic_shape::{Path, PolygonCoord}; 18 use crate::values::generics::position::GenericPositionOrAuto; 19 use crate::values::generics::rect::Rect; 20 use crate::values::specified::angle::Angle; 21 use crate::values::specified::border::BorderRadius; 22 use crate::values::specified::image::Image; 23 use crate::values::specified::length::LengthPercentageOrAuto; 24 use crate::values::specified::position::{Position, Side}; 25 use crate::values::specified::url::SpecifiedUrl; 26 use crate::values::specified::PositionComponent; 27 use crate::values::specified::{LengthPercentage, NonNegativeLengthPercentage, SVGPathData}; 28 use crate::values::CSSFloat; 29 use crate::Zero; 30 use cssparser::{match_ignore_ascii_case, Parser}; 31 use std::fmt::{self, Write}; 32 use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; 33 34 /// A specified alias for FillRule. 35 pub use crate::values::generics::basic_shape::FillRule; 36 37 /// A specified `clip-path` value. 38 pub type ClipPath = generic::GenericClipPath<BasicShape, SpecifiedUrl>; 39 40 /// A specified `shape-outside` value. 41 pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>; 42 43 /// A specified value for `at <position>` in circle() and ellipse(). 44 // Note: its computed value is the same as computed::position::Position. We just want to always use 45 // LengthPercentage as the type of its components, for basic shapes. 46 pub type RadialPosition = generic::ShapePosition<LengthPercentage>; 47 48 /// A specified basic shape. 49 pub type BasicShape = generic::GenericBasicShape<Angle, Position, LengthPercentage, BasicShapeRect>; 50 51 /// The specified value of `inset()`. 52 pub type InsetRect = generic::GenericInsetRect<LengthPercentage>; 53 54 /// A specified circle. 55 pub type Circle = generic::Circle<LengthPercentage>; 56 57 /// A specified ellipse. 58 pub type Ellipse = generic::Ellipse<LengthPercentage>; 59 60 /// The specified value of `ShapeRadius`. 61 pub type ShapeRadius = generic::ShapeRadius<LengthPercentage>; 62 63 /// The specified value of `Polygon`. 64 pub type Polygon = generic::GenericPolygon<LengthPercentage>; 65 66 /// The specified value of `PathOrShapeFunction`. 67 pub type PathOrShapeFunction = 68 generic::GenericPathOrShapeFunction<Angle, Position, LengthPercentage>; 69 70 /// The specified value of `ShapeCommand`. 71 pub type ShapeCommand = generic::GenericShapeCommand<Angle, Position, LengthPercentage>; 72 73 /// The specified value of `xywh()`. 74 /// Defines a rectangle via offsets from the top and left edge of the reference box, and a 75 /// specified width and height. 76 /// 77 /// The four <length-percentage>s define, respectively, the inset from the left edge of the 78 /// reference box, the inset from the top edge of the reference box, the width of the rectangle, 79 /// and the height of the rectangle. 80 /// 81 /// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-xywh 82 #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)] 83 pub struct Xywh { 84 /// The left edge of the reference box. 85 pub x: LengthPercentage, 86 /// The top edge of the reference box. 87 pub y: LengthPercentage, 88 /// The specified width. 89 pub width: NonNegativeLengthPercentage, 90 /// The specified height. 91 pub height: NonNegativeLengthPercentage, 92 /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle 93 /// using the border-radius shorthand syntax. 94 pub round: BorderRadius, 95 } 96 97 /// Defines a rectangle via insets from the top and left edges of the reference box. 98 /// 99 /// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect 100 #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)] 101 #[repr(C)] 102 pub struct ShapeRectFunction { 103 /// The four <length-percentage>s define the position of the top, right, bottom, and left edges 104 /// of a rectangle, respectively, as insets from the top edge of the reference box (for the 105 /// first and third values) or the left edge of the reference box (for the second and fourth 106 /// values). 107 /// 108 /// An auto value makes the edge of the box coincide with the corresponding edge of the 109 /// reference box: it’s equivalent to 0% as the first (top) or fourth (left) value, and 110 /// equivalent to 100% as the second (right) or third (bottom) value. 111 pub rect: Rect<LengthPercentageOrAuto>, 112 /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle 113 /// using the border-radius shorthand syntax. 114 pub round: BorderRadius, 115 } 116 117 /// The specified value of <basic-shape-rect>. 118 /// <basic-shape-rect> = <inset()> | <rect()> | <xywh()> 119 /// 120 /// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes 121 #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] 122 pub enum BasicShapeRect { 123 /// Defines an inset rectangle via insets from each edge of the reference box. 124 Inset(InsetRect), 125 /// Defines a xywh function. 126 #[css(function)] 127 Xywh(Xywh), 128 /// Defines a rect function. 129 #[css(function)] 130 Rect(ShapeRectFunction), 131 } 132 133 /// For filled shapes, we use fill-rule, and store it for path() and polygon(). 134 /// For outline shapes, we should ignore fill-rule. 135 /// 136 /// https://github.com/w3c/fxtf-drafts/issues/512 137 /// https://github.com/w3c/csswg-drafts/issues/7390 138 /// https://github.com/w3c/csswg-drafts/issues/3468 139 pub enum ShapeType { 140 /// The CSS property uses filled shapes. The default behavior. 141 Filled, 142 /// The CSS property uses outline shapes. This is especially useful for offset-path. 143 Outline, 144 } 145 146 bitflags! { 147 /// The flags to represent which basic shapes we would like to support. 148 /// 149 /// Different properties may use different subsets of <basic-shape>: 150 /// e.g. 151 /// clip-path: all basic shapes. 152 /// motion-path: all basic shapes (but ignore fill-rule). 153 /// shape-outside: inset(), circle(), ellipse(), polygon(). 154 /// 155 /// Also there are some properties we don't support for now: 156 /// shape-inside: inset(), circle(), ellipse(), polygon(). 157 /// SVG shape-inside and shape-subtract: circle(), ellipse(), polygon(). 158 /// 159 /// The spec issue proposes some better ways to clarify the usage of basic shapes, so for now 160 /// we use the bitflags to choose the supported basic shapes for each property at the parse 161 /// time. 162 /// https://github.com/w3c/csswg-drafts/issues/7390 163 #[derive(Clone, Copy)] 164 #[repr(C)] 165 pub struct AllowedBasicShapes: u8 { 166 /// inset(). 167 const INSET = 1 << 0; 168 /// xywh(). 169 const XYWH = 1 << 1; 170 /// rect(). 171 const RECT = 1 << 2; 172 /// circle(). 173 const CIRCLE = 1 << 3; 174 /// ellipse(). 175 const ELLIPSE = 1 << 4; 176 /// polygon(). 177 const POLYGON = 1 << 5; 178 /// path(). 179 const PATH = 1 << 6; 180 /// shape(). 181 const SHAPE = 1 << 7; 182 183 /// All flags. 184 const ALL = 185 Self::INSET.bits() | 186 Self::XYWH.bits() | 187 Self::RECT.bits() | 188 Self::CIRCLE.bits() | 189 Self::ELLIPSE.bits() | 190 Self::POLYGON.bits() | 191 Self::PATH.bits() | 192 Self::SHAPE.bits(); 193 194 /// For shape-outside. 195 const SHAPE_OUTSIDE = 196 Self::INSET.bits() | 197 Self::CIRCLE.bits() | 198 Self::ELLIPSE.bits() | 199 Self::POLYGON.bits(); 200 } 201 } 202 203 /// A helper for both clip-path and shape-outside parsing of shapes. 204 fn parse_shape_or_box<'i, 't, R, ReferenceBox>( 205 context: &ParserContext, 206 input: &mut Parser<'i, 't>, 207 to_shape: impl FnOnce(Box<BasicShape>, ReferenceBox) -> R, 208 to_reference_box: impl FnOnce(ReferenceBox) -> R, 209 flags: AllowedBasicShapes, 210 ) -> Result<R, ParseError<'i>> 211 where 212 ReferenceBox: Default + Parse, 213 { 214 let mut shape = None; 215 let mut ref_box = None; 216 loop { 217 if shape.is_none() { 218 shape = input 219 .try_parse(|i| BasicShape::parse(context, i, flags, ShapeType::Filled)) 220 .ok(); 221 } 222 223 if ref_box.is_none() { 224 ref_box = input.try_parse(|i| ReferenceBox::parse(context, i)).ok(); 225 if ref_box.is_some() { 226 continue; 227 } 228 } 229 break; 230 } 231 232 if let Some(shp) = shape { 233 return Ok(to_shape(Box::new(shp), ref_box.unwrap_or_default())); 234 } 235 236 match ref_box { 237 Some(r) => Ok(to_reference_box(r)), 238 None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), 239 } 240 } 241 242 impl Parse for ClipPath { 243 #[inline] 244 fn parse<'i, 't>( 245 context: &ParserContext, 246 input: &mut Parser<'i, 't>, 247 ) -> Result<Self, ParseError<'i>> { 248 if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { 249 return Ok(ClipPath::None); 250 } 251 252 if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) { 253 return Ok(ClipPath::Url(url)); 254 } 255 256 parse_shape_or_box( 257 context, 258 input, 259 ClipPath::Shape, 260 ClipPath::Box, 261 AllowedBasicShapes::ALL, 262 ) 263 } 264 } 265 266 impl Parse for ShapeOutside { 267 #[inline] 268 fn parse<'i, 't>( 269 context: &ParserContext, 270 input: &mut Parser<'i, 't>, 271 ) -> Result<Self, ParseError<'i>> { 272 // Need to parse this here so that `Image::parse_with_cors_anonymous` 273 // doesn't parse it. 274 if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { 275 return Ok(ShapeOutside::None); 276 } 277 278 if let Ok(image) = input.try_parse(|i| Image::parse_with_cors_anonymous(context, i)) { 279 debug_assert_ne!(image, Image::None); 280 return Ok(ShapeOutside::Image(image)); 281 } 282 283 parse_shape_or_box( 284 context, 285 input, 286 ShapeOutside::Shape, 287 ShapeOutside::Box, 288 AllowedBasicShapes::SHAPE_OUTSIDE, 289 ) 290 } 291 } 292 293 impl BasicShape { 294 /// Parse with some parameters. 295 /// 1. The supported <basic-shape>. 296 /// 2. The type of shapes. Should we ignore fill-rule? 297 /// 3. The default value of `at <position>`. 298 pub fn parse<'i, 't>( 299 context: &ParserContext, 300 input: &mut Parser<'i, 't>, 301 flags: AllowedBasicShapes, 302 shape_type: ShapeType, 303 ) -> Result<Self, ParseError<'i>> { 304 let location = input.current_source_location(); 305 let function = input.expect_function()?.clone(); 306 input.parse_nested_block(move |i| { 307 match_ignore_ascii_case! { &function, 308 "inset" if flags.contains(AllowedBasicShapes::INSET) => { 309 InsetRect::parse_function_arguments(context, i) 310 .map(BasicShapeRect::Inset) 311 .map(BasicShape::Rect) 312 }, 313 "xywh" if flags.contains(AllowedBasicShapes::XYWH) => { 314 Xywh::parse_function_arguments(context, i) 315 .map(BasicShapeRect::Xywh) 316 .map(BasicShape::Rect) 317 }, 318 "rect" if flags.contains(AllowedBasicShapes::RECT) => { 319 ShapeRectFunction::parse_function_arguments(context, i) 320 .map(BasicShapeRect::Rect) 321 .map(BasicShape::Rect) 322 }, 323 "circle" if flags.contains(AllowedBasicShapes::CIRCLE) => { 324 Circle::parse_function_arguments(context, i) 325 .map(BasicShape::Circle) 326 }, 327 "ellipse" if flags.contains(AllowedBasicShapes::ELLIPSE) => { 328 Ellipse::parse_function_arguments(context, i) 329 .map(BasicShape::Ellipse) 330 }, 331 "polygon" if flags.contains(AllowedBasicShapes::POLYGON) => { 332 Polygon::parse_function_arguments(context, i, shape_type) 333 .map(BasicShape::Polygon) 334 }, 335 "path" if flags.contains(AllowedBasicShapes::PATH) => { 336 Path::parse_function_arguments(i, shape_type) 337 .map(PathOrShapeFunction::Path) 338 .map(BasicShape::PathOrShape) 339 }, 340 "shape" 341 if flags.contains(AllowedBasicShapes::SHAPE) 342 && static_prefs::pref!("layout.css.basic-shape-shape.enabled") => 343 { 344 generic::Shape::parse_function_arguments(context, i, shape_type) 345 .map(PathOrShapeFunction::Shape) 346 .map(BasicShape::PathOrShape) 347 }, 348 _ => Err(location 349 .new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))), 350 } 351 }) 352 } 353 } 354 355 impl Parse for InsetRect { 356 fn parse<'i, 't>( 357 context: &ParserContext, 358 input: &mut Parser<'i, 't>, 359 ) -> Result<Self, ParseError<'i>> { 360 input.expect_function_matching("inset")?; 361 input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) 362 } 363 } 364 365 fn parse_round<'i, 't>( 366 context: &ParserContext, 367 input: &mut Parser<'i, 't>, 368 ) -> Result<BorderRadius, ParseError<'i>> { 369 if input 370 .try_parse(|i| i.expect_ident_matching("round")) 371 .is_ok() 372 { 373 return BorderRadius::parse(context, input); 374 } 375 376 Ok(BorderRadius::zero()) 377 } 378 379 impl InsetRect { 380 /// Parse the inner function arguments of `inset()` 381 fn parse_function_arguments<'i, 't>( 382 context: &ParserContext, 383 input: &mut Parser<'i, 't>, 384 ) -> Result<Self, ParseError<'i>> { 385 let rect = Rect::parse_with(context, input, LengthPercentage::parse)?; 386 let round = parse_round(context, input)?; 387 Ok(generic::InsetRect { rect, round }) 388 } 389 } 390 391 impl ToCss for RadialPosition { 392 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result 393 where 394 W: Write, 395 { 396 self.horizontal.to_css(dest)?; 397 dest.write_char(' ')?; 398 self.vertical.to_css(dest) 399 } 400 } 401 402 fn convert_to_length_percentage<S: Side>(c: PositionComponent<S>) -> LengthPercentage { 403 use crate::values::specified::{AllowedNumericType, Percentage}; 404 // Convert the value when parsing, to make sure we serialize it properly for both 405 // specified and computed values. 406 // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization 407 match c { 408 // Since <position> keywords stand in for percentages, keywords without an offset 409 // turn into percentages. 410 PositionComponent::Center => LengthPercentage::from(Percentage::new(0.5)), 411 PositionComponent::Side(keyword, None) => { 412 Percentage::new(if keyword.is_start() { 0. } else { 1. }).into() 413 }, 414 // Per spec issue, https://github.com/w3c/csswg-drafts/issues/8695, the part of 415 // "avoiding calc() expressions where possible" and "avoiding calc() 416 // transformations" will be removed from the spec, and we should follow the 417 // css-values-4 for position, i.e. we make it as length-percentage always. 418 // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization. 419 // https://drafts.csswg.org/css-values-4/#typedef-position 420 PositionComponent::Side(keyword, Some(length)) => { 421 if keyword.is_start() { 422 length 423 } else { 424 length.hundred_percent_minus(AllowedNumericType::All) 425 } 426 }, 427 PositionComponent::Length(length) => length, 428 } 429 } 430 431 fn parse_at_position<'i, 't>( 432 context: &ParserContext, 433 input: &mut Parser<'i, 't>, 434 ) -> Result<GenericPositionOrAuto<RadialPosition>, ParseError<'i>> { 435 use crate::values::specified::position::Position; 436 if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() { 437 Position::parse(context, input).map(|pos| { 438 GenericPositionOrAuto::Position(RadialPosition::new( 439 convert_to_length_percentage(pos.horizontal), 440 convert_to_length_percentage(pos.vertical), 441 )) 442 }) 443 } else { 444 // `at <position>` is omitted. 445 Ok(GenericPositionOrAuto::Auto) 446 } 447 } 448 449 impl Parse for Circle { 450 fn parse<'i, 't>( 451 context: &ParserContext, 452 input: &mut Parser<'i, 't>, 453 ) -> Result<Self, ParseError<'i>> { 454 input.expect_function_matching("circle")?; 455 input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) 456 } 457 } 458 459 impl Circle { 460 fn parse_function_arguments<'i, 't>( 461 context: &ParserContext, 462 input: &mut Parser<'i, 't>, 463 ) -> Result<Self, ParseError<'i>> { 464 let radius = input 465 .try_parse(|i| ShapeRadius::parse(context, i)) 466 .unwrap_or_default(); 467 let position = parse_at_position(context, input)?; 468 469 Ok(generic::Circle { radius, position }) 470 } 471 } 472 473 impl Parse for Ellipse { 474 fn parse<'i, 't>( 475 context: &ParserContext, 476 input: &mut Parser<'i, 't>, 477 ) -> Result<Self, ParseError<'i>> { 478 input.expect_function_matching("ellipse")?; 479 input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) 480 } 481 } 482 483 impl Ellipse { 484 fn parse_function_arguments<'i, 't>( 485 context: &ParserContext, 486 input: &mut Parser<'i, 't>, 487 ) -> Result<Self, ParseError<'i>> { 488 let (semiaxis_x, semiaxis_y) = input 489 .try_parse(|i| -> Result<_, ParseError> { 490 Ok(( 491 ShapeRadius::parse(context, i)?, 492 ShapeRadius::parse(context, i)?, 493 )) 494 }) 495 .unwrap_or_default(); 496 let position = parse_at_position(context, input)?; 497 498 Ok(generic::Ellipse { 499 semiaxis_x, 500 semiaxis_y, 501 position, 502 }) 503 } 504 } 505 506 fn parse_fill_rule<'i, 't>( 507 input: &mut Parser<'i, 't>, 508 shape_type: ShapeType, 509 expect_comma: bool, 510 ) -> FillRule { 511 match shape_type { 512 // Per [1] and [2], we ignore `<fill-rule>` for outline shapes, so always use a default 513 // value. 514 // [1] https://github.com/w3c/csswg-drafts/issues/3468 515 // [2] https://github.com/w3c/csswg-drafts/issues/7390 516 // 517 // Also, per [3] and [4], we would like the ignore `<file-rule>` from outline shapes, e.g. 518 // offset-path, which means we don't parse it when setting `ShapeType::Outline`. 519 // This should be web compatible because the shipped "offset-path:path()" doesn't have 520 // `<fill-rule>` and "offset-path:polygon()" is a new feature and still behind the 521 // preference. 522 // [3] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1545393321 523 // [4] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1555330929 524 ShapeType::Outline => Default::default(), 525 ShapeType::Filled => input 526 .try_parse(|i| -> Result<_, ParseError> { 527 let fill = FillRule::parse(i)?; 528 if expect_comma { 529 i.expect_comma()?; 530 } 531 Ok(fill) 532 }) 533 .unwrap_or_default(), 534 } 535 } 536 537 impl Parse for Polygon { 538 fn parse<'i, 't>( 539 context: &ParserContext, 540 input: &mut Parser<'i, 't>, 541 ) -> Result<Self, ParseError<'i>> { 542 input.expect_function_matching("polygon")?; 543 input.parse_nested_block(|i| Self::parse_function_arguments(context, i, ShapeType::Filled)) 544 } 545 } 546 547 impl Polygon { 548 /// Parse the inner arguments of a `polygon` function. 549 fn parse_function_arguments<'i, 't>( 550 context: &ParserContext, 551 input: &mut Parser<'i, 't>, 552 shape_type: ShapeType, 553 ) -> Result<Self, ParseError<'i>> { 554 let fill = parse_fill_rule(input, shape_type, true /* has comma */); 555 let coordinates = input 556 .parse_comma_separated(|i| { 557 Ok(PolygonCoord( 558 LengthPercentage::parse(context, i)?, 559 LengthPercentage::parse(context, i)?, 560 )) 561 })? 562 .into(); 563 564 Ok(Polygon { fill, coordinates }) 565 } 566 } 567 568 impl Path { 569 /// Parse the inner arguments of a `path` function. 570 fn parse_function_arguments<'i, 't>( 571 input: &mut Parser<'i, 't>, 572 shape_type: ShapeType, 573 ) -> Result<Self, ParseError<'i>> { 574 use crate::values::specified::svg_path::AllowEmpty; 575 576 let fill = parse_fill_rule(input, shape_type, true /* has comma */); 577 let path = SVGPathData::parse(input, AllowEmpty::No)?; 578 Ok(Path { fill, path }) 579 } 580 } 581 582 fn round_to_css<W>(round: &BorderRadius, dest: &mut CssWriter<W>) -> fmt::Result 583 where 584 W: Write, 585 { 586 if !round.is_zero() { 587 dest.write_str(" round ")?; 588 round.to_css(dest)?; 589 } 590 Ok(()) 591 } 592 593 impl ToCss for Xywh { 594 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result 595 where 596 W: Write, 597 { 598 self.x.to_css(dest)?; 599 dest.write_char(' ')?; 600 self.y.to_css(dest)?; 601 dest.write_char(' ')?; 602 self.width.to_css(dest)?; 603 dest.write_char(' ')?; 604 self.height.to_css(dest)?; 605 round_to_css(&self.round, dest) 606 } 607 } 608 609 impl Xywh { 610 /// Parse the inner function arguments of `xywh()`. 611 fn parse_function_arguments<'i, 't>( 612 context: &ParserContext, 613 input: &mut Parser<'i, 't>, 614 ) -> Result<Self, ParseError<'i>> { 615 let x = LengthPercentage::parse(context, input)?; 616 let y = LengthPercentage::parse(context, input)?; 617 let width = NonNegativeLengthPercentage::parse(context, input)?; 618 let height = NonNegativeLengthPercentage::parse(context, input)?; 619 let round = parse_round(context, input)?; 620 Ok(Xywh { 621 x, 622 y, 623 width, 624 height, 625 round, 626 }) 627 } 628 } 629 630 impl ToCss for ShapeRectFunction { 631 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result 632 where 633 W: Write, 634 { 635 self.rect.0.to_css(dest)?; 636 dest.write_char(' ')?; 637 self.rect.1.to_css(dest)?; 638 dest.write_char(' ')?; 639 self.rect.2.to_css(dest)?; 640 dest.write_char(' ')?; 641 self.rect.3.to_css(dest)?; 642 round_to_css(&self.round, dest) 643 } 644 } 645 646 impl ShapeRectFunction { 647 /// Parse the inner function arguments of `rect()`. 648 fn parse_function_arguments<'i, 't>( 649 context: &ParserContext, 650 input: &mut Parser<'i, 't>, 651 ) -> Result<Self, ParseError<'i>> { 652 let rect = Rect::parse_all_components_with(context, input, LengthPercentageOrAuto::parse)?; 653 let round = parse_round(context, input)?; 654 Ok(ShapeRectFunction { rect, round }) 655 } 656 } 657 658 impl ToComputedValue for BasicShapeRect { 659 type ComputedValue = ComputedInsetRect; 660 661 #[inline] 662 fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { 663 use crate::values::computed::LengthPercentage; 664 use crate::values::computed::LengthPercentageOrAuto; 665 use style_traits::values::specified::AllowedNumericType; 666 667 match self { 668 Self::Inset(ref inset) => inset.to_computed_value(context), 669 Self::Xywh(ref xywh) => { 670 // Given `xywh(x y w h)`, construct the equivalent inset() function, 671 // `inset(y calc(100% - x - w) calc(100% - y - h) x)`. 672 // 673 // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values 674 // https://github.com/w3c/csswg-drafts/issues/9053 675 let x = xywh.x.to_computed_value(context); 676 let y = xywh.y.to_computed_value(context); 677 let w = xywh.width.to_computed_value(context); 678 let h = xywh.height.to_computed_value(context); 679 // calc(100% - x - w). 680 let right = LengthPercentage::hundred_percent_minus_list( 681 &[&x, &w.0], 682 AllowedNumericType::All, 683 ); 684 // calc(100% - y - h). 685 let bottom = LengthPercentage::hundred_percent_minus_list( 686 &[&y, &h.0], 687 AllowedNumericType::All, 688 ); 689 690 ComputedInsetRect { 691 rect: Rect::new(y, right, bottom, x), 692 round: xywh.round.to_computed_value(context), 693 } 694 }, 695 Self::Rect(ref rect) => { 696 // Given `rect(t r b l)`, the equivalent function is 697 // `inset(t calc(100% - r) calc(100% - b) l)`. 698 // 699 // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values 700 fn compute_top_or_left(v: LengthPercentageOrAuto) -> LengthPercentage { 701 match v { 702 // it’s equivalent to 0% as the first (top) or fourth (left) value. 703 // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect 704 LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(), 705 LengthPercentageOrAuto::LengthPercentage(lp) => lp, 706 } 707 } 708 fn compute_bottom_or_right(v: LengthPercentageOrAuto) -> LengthPercentage { 709 match v { 710 // It's equivalent to 100% as the second (right) or third (bottom) value. 711 // So calc(100% - 100%) = 0%. 712 // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect 713 LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(), 714 LengthPercentageOrAuto::LengthPercentage(lp) => { 715 LengthPercentage::hundred_percent_minus(lp, AllowedNumericType::All) 716 }, 717 } 718 } 719 720 let round = rect.round.to_computed_value(context); 721 let rect = rect.rect.to_computed_value(context); 722 let rect = Rect::new( 723 compute_top_or_left(rect.0), 724 compute_bottom_or_right(rect.1), 725 compute_bottom_or_right(rect.2), 726 compute_top_or_left(rect.3), 727 ); 728 729 ComputedInsetRect { rect, round } 730 }, 731 } 732 } 733 734 #[inline] 735 fn from_computed_value(computed: &Self::ComputedValue) -> Self { 736 Self::Inset(ToComputedValue::from_computed_value(computed)) 737 } 738 } 739 740 impl generic::Shape<Angle, Position, LengthPercentage> { 741 /// Parse the inner arguments of a `shape` function. 742 /// shape() = shape(<fill-rule>? from <coordinate-pair>, <shape-command>#) 743 fn parse_function_arguments<'i, 't>( 744 context: &ParserContext, 745 input: &mut Parser<'i, 't>, 746 shape_type: ShapeType, 747 ) -> Result<Self, ParseError<'i>> { 748 let fill = parse_fill_rule(input, shape_type, false /* no following comma */); 749 750 let mut first = true; 751 let commands = input.parse_comma_separated(|i| { 752 if first { 753 first = false; 754 755 // The starting point for the first shape-command. It adds an initial absolute 756 // moveto to the list of path data commands, with the <coordinate-pair> measured 757 // from the top-left corner of the reference 758 i.expect_ident_matching("from")?; 759 Ok(ShapeCommand::Move { 760 point: generic::CommandEndPoint::parse_endpoint_as_abs(context, i)?, 761 }) 762 } else { 763 // The further path data commands. 764 ShapeCommand::parse(context, i) 765 } 766 })?; 767 768 // We must have one starting point and at least one following <shape-command>. 769 if commands.len() < 2 { 770 return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); 771 } 772 773 Ok(Self { 774 fill, 775 commands: commands.into(), 776 }) 777 } 778 } 779 780 impl Parse for ShapeCommand { 781 fn parse<'i, 't>( 782 context: &ParserContext, 783 input: &mut Parser<'i, 't>, 784 ) -> Result<Self, ParseError<'i>> { 785 use crate::values::generics::basic_shape::{ 786 ArcRadii, ArcSize, ArcSweep, AxisEndPoint, CommandEndPoint, ControlPoint, 787 }; 788 789 // <shape-command> = <move-command> | <line-command> | <hv-line-command> | 790 // <curve-command> | <smooth-command> | <arc-command> | close 791 Ok(try_match_ident_ignore_ascii_case! { input, 792 "close" => Self::Close, 793 "move" => { 794 let point = CommandEndPoint::parse(context, input)?; 795 Self::Move { point } 796 }, 797 "line" => { 798 let point = CommandEndPoint::parse(context, input)?; 799 Self::Line { point } 800 }, 801 "hline" => { 802 let x = AxisEndPoint::parse_hline(context, input)?; 803 Self::HLine { x } 804 }, 805 "vline" => { 806 let y = AxisEndPoint::parse_vline(context, input)?; 807 Self::VLine { y } 808 }, 809 "curve" => { 810 let point = CommandEndPoint::parse(context, input)?; 811 input.expect_ident_matching("with")?; 812 let control1 = ControlPoint::parse(context, input, point.is_abs())?; 813 if input.try_parse(|i| i.expect_delim('/')).is_ok() { 814 let control2 = ControlPoint::parse(context, input, point.is_abs())?; 815 Self::CubicCurve { 816 point, 817 control1, 818 control2, 819 } 820 } else { 821 Self::QuadCurve { 822 point, 823 control1, 824 } 825 } 826 }, 827 "smooth" => { 828 let point = CommandEndPoint::parse(context, input)?; 829 if input.try_parse(|i| i.expect_ident_matching("with")).is_ok() { 830 let control2 = ControlPoint::parse(context, input, point.is_abs())?; 831 Self::SmoothCubic { 832 point, 833 control2, 834 } 835 } else { 836 Self::SmoothQuad { point } 837 } 838 }, 839 "arc" => { 840 let point = CommandEndPoint::parse(context, input)?; 841 input.expect_ident_matching("of")?; 842 let rx = LengthPercentage::parse(context, input)?; 843 let ry = input.try_parse(|i| LengthPercentage::parse(context, i)).ok(); 844 let radii = ArcRadii { rx, ry: ry.into() }; 845 846 // [<arc-sweep> || <arc-size> || rotate <angle>]? 847 let mut arc_sweep = None; 848 let mut arc_size = None; 849 let mut rotate = None; 850 loop { 851 if arc_sweep.is_none() { 852 arc_sweep = input.try_parse(ArcSweep::parse).ok(); 853 } 854 855 if arc_size.is_none() { 856 arc_size = input.try_parse(ArcSize::parse).ok(); 857 if arc_size.is_some() { 858 continue; 859 } 860 } 861 862 if rotate.is_none() 863 && input 864 .try_parse(|i| i.expect_ident_matching("rotate")) 865 .is_ok() 866 { 867 rotate = Some(Angle::parse(context, input)?); 868 continue; 869 } 870 break; 871 } 872 Self::Arc { 873 point, 874 radii, 875 arc_sweep: arc_sweep.unwrap_or(ArcSweep::Ccw), 876 arc_size: arc_size.unwrap_or(ArcSize::Small), 877 rotate: rotate.unwrap_or(Angle::zero()), 878 } 879 }, 880 }) 881 } 882 } 883 884 impl Parse for generic::CoordinatePair<LengthPercentage> { 885 fn parse<'i, 't>( 886 context: &ParserContext, 887 input: &mut Parser<'i, 't>, 888 ) -> Result<Self, ParseError<'i>> { 889 let x = LengthPercentage::parse(context, input)?; 890 let y = LengthPercentage::parse(context, input)?; 891 Ok(Self::new(x, y)) 892 } 893 } 894 895 impl generic::ControlPoint<Position, LengthPercentage> { 896 /// Parse <control-point> = [ <position> | <relative-control-point> ] 897 fn parse<'i, 't>( 898 context: &ParserContext, 899 input: &mut Parser<'i, 't>, 900 is_end_point_abs: bool, 901 ) -> Result<Self, ParseError<'i>> { 902 use generic::ControlReference; 903 let coord = input.try_parse(|i| generic::CoordinatePair::parse(context, i)); 904 905 // Parse <position> 906 if is_end_point_abs && coord.is_err() { 907 let pos = Position::parse(context, input)?; 908 return Ok(Self::Absolute(pos)); 909 } 910 911 // Parse <relative-control-point> = <coordinate-pair> [from [ start | end | origin ]]? 912 let coord = coord?; 913 let mut reference = if is_end_point_abs { 914 ControlReference::Origin 915 } else { 916 ControlReference::Start 917 }; 918 if input.try_parse(|i| i.expect_ident_matching("from")).is_ok() { 919 reference = ControlReference::parse(input)?; 920 } 921 922 Ok(Self::Relative(generic::RelativeControlPoint { 923 coord, 924 reference, 925 })) 926 } 927 } 928 929 impl Parse for generic::CommandEndPoint<Position, LengthPercentage> { 930 /// Parse <command-end-point> = to <position> | by <coordinate-pair> 931 fn parse<'i, 't>( 932 context: &ParserContext, 933 input: &mut Parser<'i, 't>, 934 ) -> Result<Self, ParseError<'i>> { 935 if ByTo::parse(input)?.is_abs() { 936 Self::parse_endpoint_as_abs(context, input) 937 } else { 938 let point = generic::CoordinatePair::parse(context, input)?; 939 Ok(Self::ByCoordinate(point)) 940 } 941 } 942 } 943 944 impl generic::CommandEndPoint<Position, LengthPercentage> { 945 /// Parse <command-end-point> = to <position> 946 fn parse_endpoint_as_abs<'i, 't>( 947 context: &ParserContext, 948 input: &mut Parser<'i, 't>, 949 ) -> Result<Self, ParseError<'i>> { 950 let point = Position::parse(context, input)?; 951 Ok(generic::CommandEndPoint::ToPosition(point)) 952 } 953 } 954 955 impl generic::AxisEndPoint<LengthPercentage> { 956 /// Parse <horizontal-line-command> 957 pub fn parse_hline<'i, 't>( 958 context: &ParserContext, 959 input: &mut Parser<'i, 't>, 960 ) -> Result<Self, ParseError<'i>> { 961 use cssparser::Token; 962 use generic::{AxisPosition, AxisPositionKeyword}; 963 964 // If the command is relative, parse for <length-percentage> only. 965 if !ByTo::parse(input)?.is_abs() { 966 return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?)); 967 } 968 969 let x = AxisPosition::parse(context, input)?; 970 if let AxisPosition::Keyword( 971 _word @ (AxisPositionKeyword::Top 972 | AxisPositionKeyword::Bottom 973 | AxisPositionKeyword::YStart 974 | AxisPositionKeyword::YEnd), 975 ) = &x 976 { 977 let location = input.current_source_location(); 978 let token = Token::Ident(x.to_css_string().into()); 979 return Err(location.new_unexpected_token_error(token)); 980 } 981 Ok(Self::ToPosition(x)) 982 } 983 984 /// Parse <vertical-line-command> 985 pub fn parse_vline<'i, 't>( 986 context: &ParserContext, 987 input: &mut Parser<'i, 't>, 988 ) -> Result<Self, ParseError<'i>> { 989 use cssparser::Token; 990 use generic::{AxisPosition, AxisPositionKeyword}; 991 992 // If the command is relative, parse for <length-percentage> only. 993 if !ByTo::parse(input)?.is_abs() { 994 return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?)); 995 } 996 997 let y = AxisPosition::parse(context, input)?; 998 if let AxisPosition::Keyword( 999 _word @ (AxisPositionKeyword::Left 1000 | AxisPositionKeyword::Right 1001 | AxisPositionKeyword::XStart 1002 | AxisPositionKeyword::XEnd), 1003 ) = &y 1004 { 1005 // Return an error if we parsed a different keyword. 1006 let location = input.current_source_location(); 1007 let token = Token::Ident(y.to_css_string().into()); 1008 return Err(location.new_unexpected_token_error(token)); 1009 } 1010 Ok(Self::ToPosition(y)) 1011 } 1012 } 1013 1014 impl ToComputedValue for generic::AxisPosition<LengthPercentage> { 1015 type ComputedValue = generic::AxisPosition<ComputedLengthPercentage>; 1016 1017 fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { 1018 match self { 1019 Self::LengthPercent(lp) => { 1020 Self::ComputedValue::LengthPercent(lp.to_computed_value(context)) 1021 }, 1022 Self::Keyword(word) => { 1023 let lp = LengthPercentage::Percentage(word.as_percentage()); 1024 Self::ComputedValue::LengthPercent(lp.to_computed_value(context)) 1025 }, 1026 } 1027 } 1028 1029 fn from_computed_value(computed: &Self::ComputedValue) -> Self { 1030 match computed { 1031 Self::ComputedValue::LengthPercent(lp) => { 1032 Self::LengthPercent(LengthPercentage::from_computed_value(lp)) 1033 }, 1034 _ => unreachable!("Invalid state: computed value cannot be a keyword."), 1035 } 1036 } 1037 } 1038 1039 impl ToComputedValue for generic::AxisPosition<CSSFloat> { 1040 type ComputedValue = Self; 1041 1042 fn to_computed_value(&self, _context: &Context) -> Self { 1043 *self 1044 } 1045 1046 fn from_computed_value(computed: &Self) -> Self { 1047 *computed 1048 } 1049 } 1050 1051 /// This determines whether the command is absolutely or relatively positioned. 1052 /// https://drafts.csswg.org/css-shapes-1/#typedef-shape-command-end-point 1053 #[derive(Clone, Copy, Debug, Parse, PartialEq)] 1054 enum ByTo { 1055 /// Command is relative to the command’s starting point. 1056 By, 1057 /// Command is relative to the top-left corner of the reference box. 1058 To, 1059 } 1060 1061 impl ByTo { 1062 /// Return true if it is absolute, i.e. it is To. 1063 #[inline] 1064 pub fn is_abs(&self) -> bool { 1065 matches!(self, ByTo::To) 1066 } 1067 }