diff --git a/components/style/cbindgen.toml b/components/style/cbindgen.toml index 5a0e5e19ea7..c0eeee9b968 100644 --- a/components/style/cbindgen.toml +++ b/components/style/cbindgen.toml @@ -7,6 +7,7 @@ autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated usi * a. Alternatively, you can clone `https://github.com/eqrion/cbindgen` and use a tagged release * 2. Run `rustup run nightly cbindgen toolkit/library/rust/ --lockfile Cargo.lock --crate style -o layout/style/ServoStyleConsts.h` */""" +include_guard = "mozilla_ServoStyleConsts_h" include_version = true braces = "SameLine" line_length = 80 @@ -22,5 +23,5 @@ derive_helper_methods = true [export] prefix = "Style" -include = ["StyleDisplay", "StyleAppearance", "StyleDisplayMode"] -item_types = ["enums"] +include = ["StyleDisplay", "StyleAppearance", "StyleDisplayMode", "StylePathCommand"] +item_types = ["enums", "structs", "typedefs"] diff --git a/components/style/gecko/conversions.rs b/components/style/gecko/conversions.rs index 3b1f31a1b08..154ad95bc43 100644 --- a/components/style/gecko/conversions.rs +++ b/components/style/gecko/conversions.rs @@ -638,6 +638,7 @@ pub mod basic_shape { use values::computed::basic_shape::{BasicShape, ClippingShape, FloatAreaShape, ShapeRadius}; use values::computed::border::{BorderCornerRadius, BorderRadius}; use values::computed::length::LengthOrPercentage; + use values::computed::motion::OffsetPath; use values::computed::position; use values::computed::url::ComputedUrl; use values::generics::basic_shape::{BasicShape as GenericBasicShape, InsetRect, Polygon}; @@ -669,6 +670,7 @@ pub mod basic_shape { Some(ShapeSource::Shape(shape, reference_box)) }, StyleShapeSourceType::URL | StyleShapeSourceType::Image => None, + StyleShapeSourceType::Path => None, } } } @@ -710,6 +712,29 @@ pub mod basic_shape { } } + impl<'a> From<&'a StyleShapeSource> for OffsetPath { + fn from(other: &'a StyleShapeSource) -> Self { + use gecko_bindings::structs::StylePathCommand; + use values::specified::motion::{SVGPathData, PathCommand}; + match other.mType { + StyleShapeSourceType::Path => { + let gecko_path = unsafe { &*other.__bindgen_anon_1.mSVGPath.as_ref().mPtr }; + let result: Vec = + gecko_path.mPath.iter().map(|gecko: &StylePathCommand| { + // unsafe: cbindgen ensures the representation is the same. + unsafe{ ::std::mem::transmute(*gecko) } + }).collect(); + OffsetPath::Path(SVGPathData::new(result.into_boxed_slice())) + }, + StyleShapeSourceType::None => OffsetPath::none(), + StyleShapeSourceType::Shape | + StyleShapeSourceType::Box | + StyleShapeSourceType::URL | + StyleShapeSourceType::Image => unreachable!("Unsupported offset-path type"), + } + } + } + impl<'a> From<&'a StyleBasicShape> for BasicShape { fn from(other: &'a StyleBasicShape) -> Self { match other.mType { diff --git a/components/style/properties/gecko.mako.rs b/components/style/properties/gecko.mako.rs index 33a66498b2d..596dc29fb19 100644 --- a/components/style/properties/gecko.mako.rs +++ b/components/style/properties/gecko.mako.rs @@ -3053,7 +3053,7 @@ fn static_assert() { scroll-snap-points-x scroll-snap-points-y scroll-snap-type-x scroll-snap-type-y scroll-snap-coordinate perspective-origin -moz-binding will-change - overscroll-behavior-x overscroll-behavior-y + offset-path overscroll-behavior-x overscroll-behavior-y overflow-clip-box-inline overflow-clip-box-block perspective-origin -moz-binding will-change shape-outside contain touch-action translate @@ -3681,6 +3681,51 @@ fn static_assert() { ${impl_simple_copy("contain", "mContain")} ${impl_simple_type_with_conversion("touch_action")} + + pub fn set_offset_path(&mut self, v: longhands::offset_path::computed_value::T) { + use gecko_bindings::bindings::{Gecko_NewStyleMotion, Gecko_NewStyleSVGPath}; + use gecko_bindings::bindings::Gecko_SetStyleMotion; + use gecko_bindings::structs::StyleShapeSourceType; + use values::specified::OffsetPath; + + let motion = unsafe { Gecko_NewStyleMotion().as_mut().unwrap() }; + match v { + OffsetPath::None => motion.mOffsetPath.mType = StyleShapeSourceType::None, + OffsetPath::Path(servo_path) => { + motion.mOffsetPath.mType = StyleShapeSourceType::Path; + let gecko_path = unsafe { + let ref mut source = motion.mOffsetPath; + Gecko_NewStyleSVGPath(source); + &mut source.__bindgen_anon_1.mSVGPath.as_mut().mPtr.as_mut().unwrap().mPath + }; + unsafe { gecko_path.set_len(servo_path.commands().len() as u32) }; + debug_assert_eq!(gecko_path.len(), servo_path.commands().len()); + for (servo, gecko) in servo_path.commands().iter().zip(gecko_path.iter_mut()) { + // unsafe: cbindgen ensures the representation is the same. + *gecko = unsafe { transmute(*servo) }; + } + }, + } + unsafe { Gecko_SetStyleMotion(&mut self.gecko.mMotion, motion) }; + } + + pub fn clone_offset_path(&self) -> longhands::offset_path::computed_value::T { + use values::specified::OffsetPath; + match unsafe { self.gecko.mMotion.mPtr.as_ref() } { + None => OffsetPath::none(), + Some(v) => (&v.mOffsetPath).into() + } + } + + pub fn copy_offset_path_from(&mut self, other: &Self) { + use gecko_bindings::bindings::Gecko_CopyStyleMotions; + unsafe { Gecko_CopyStyleMotions(&mut self.gecko.mMotion, other.gecko.mMotion.mPtr) }; + } + + pub fn reset_offset_path(&mut self, other: &Self) { + self.copy_offset_path_from(other); + } + <%def name="simple_image_array_property(name, shorthand, field_name)"> diff --git a/components/style/properties/longhands/box.mako.rs b/components/style/properties/longhands/box.mako.rs index 6b6bcf9cbe3..4ef45a502bc 100644 --- a/components/style/properties/longhands/box.mako.rs +++ b/components/style/properties/longhands/box.mako.rs @@ -356,6 +356,17 @@ ${helpers.predefined_type( servo_restyle_damage="reflow_out_of_flow" )} +// Motion Path Module Level 1 +${helpers.predefined_type( + "offset-path", + "OffsetPath", + "computed::OffsetPath::none()", + animation_value_type="none", + gecko_pref="layout.css.motion-path.enabled", + flags="CREATES_STACKING_CONTEXT FIXPOS_CB", + spec="https://drafts.fxtf.org/motion-1/#offset-path-property" +)} + // CSSOM View Module // https://www.w3.org/TR/cssom-view-1/ ${helpers.single_keyword("scroll-behavior", diff --git a/components/style/values/computed/mod.rs b/components/style/values/computed/mod.rs index 9a6b5fd76b5..d8cc938c1f3 100644 --- a/components/style/values/computed/mod.rs +++ b/components/style/values/computed/mod.rs @@ -65,6 +65,7 @@ pub use self::length::{NonNegativeLengthOrPercentage, NonNegativeLengthOrPercent pub use self::list::Quotes; #[cfg(feature = "gecko")] pub use self::list::ListStyleType; +pub use self::motion::OffsetPath; pub use self::outline::OutlineStyle; pub use self::percentage::{Percentage, NonNegativePercentage}; pub use self::position::{GridAutoFlow, GridTemplateAreas, Position, ZIndex}; @@ -100,6 +101,7 @@ pub mod gecko; pub mod image; pub mod length; pub mod list; +pub mod motion; pub mod outline; pub mod percentage; pub mod position; diff --git a/components/style/values/computed/motion.rs b/components/style/values/computed/motion.rs new file mode 100644 index 00000000000..935ba57f845 --- /dev/null +++ b/components/style/values/computed/motion.rs @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS values that are related to motion path. + +/// A computed offset-path. The computed value is as specified value. +/// +/// https://drafts.fxtf.org/motion-1/#offset-path-property +pub use values::specified::motion::OffsetPath as OffsetPath; diff --git a/components/style/values/specified/mod.rs b/components/style/values/specified/mod.rs index 2da4c7e93d3..1bd52e916d0 100644 --- a/components/style/values/specified/mod.rs +++ b/components/style/values/specified/mod.rs @@ -58,6 +58,7 @@ pub use self::length::{NonNegativeLengthOrPercentage, NonNegativeLengthOrPercent pub use self::list::Quotes; #[cfg(feature = "gecko")] pub use self::list::ListStyleType; +pub use self::motion::OffsetPath; pub use self::outline::OutlineStyle; pub use self::rect::LengthOrNumberRect; pub use self::resolution::Resolution; @@ -101,6 +102,7 @@ pub mod image; pub mod length; pub mod list; pub mod outline; +pub mod motion; pub mod percentage; pub mod position; pub mod rect; diff --git a/components/style/values/specified/motion.rs b/components/style/values/specified/motion.rs new file mode 100644 index 00000000000..8b370369b85 --- /dev/null +++ b/components/style/values/specified/motion.rs @@ -0,0 +1,643 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values that are related to motion path. + +use cssparser::Parser; +use parser::{Parse, ParserContext}; +use std::fmt::{self, Write}; +use std::iter::Peekable; +use std::str::Chars; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; +use style_traits::values::SequenceWriter; +use values::CSSFloat; + +/// The offset-path value. +/// +/// https://drafts.fxtf.org/motion-1/#offset-path-property +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss)] +pub enum OffsetPath { + // We could merge SVGPathData into ShapeSource, so we could reuse them. However, + // we don't want to support other value for offset-path, so use SVGPathData only for now. + /// Path value for path(). + #[css(function)] + Path(SVGPathData), + /// None value. + None, + // Bug 1186329: Implement ray(), , , and . +} + +impl OffsetPath { + /// Return None. + #[inline] + pub fn none() -> Self { + OffsetPath::None + } +} + +impl Parse for OffsetPath { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't> + ) -> Result> { + // Parse none. + if input.try(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(OffsetPath::none()); + } + + // Parse possible functions. + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + input.parse_nested_block(move |i| { + match_ignore_ascii_case! { &function, + // Bug 1186329: Implement the parser for ray(), , , + // and . + "path" => SVGPathData::parse(context, i).map(OffsetPath::Path), + _ => { + Err(location.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(function.clone()) + )) + }, + } + }) + } +} + +/// SVG Path parser. +struct PathParser<'a> { + chars: Peekable>, + path: Vec, +} + +impl<'a> PathParser<'a> { + /// Parse a sub-path. + fn parse_subpath(&mut self) -> Result<(), ()> { + // Handle "moveto" Command first. If there is no "moveto", this is not a valid sub-path + // (i.e. not a valid moveto-drawto-command-group). + self.parse_moveto()?; + + // Handle other commands. + loop { + skip_wsp(&mut self.chars); + if self.chars.peek().map_or(true, |m| *m == 'M' || *m == 'm') { + break; + } + + match self.chars.next() { + Some(command) => { + let abs = command.is_uppercase(); + match command { + 'Z' | 'z' => { + // Note: A "closepath" coulbe be followed immediately by "moveto" or + // any other command, so we don't break this loop. + self.path.push(PathCommand::ClosePath); + }, + 'L' | 'l' => { + skip_wsp(&mut self.chars); + self.parse_lineto(abs)?; + }, + 'H' | 'h' => { + skip_wsp(&mut self.chars); + self.parse_h_lineto(abs)?; + }, + 'V' | 'v' => { + skip_wsp(&mut self.chars); + self.parse_v_lineto(abs)?; + }, + 'C' | 'c' => { + skip_wsp(&mut self.chars); + self.parse_curveto(abs)?; + }, + 'S' | 's' => { + skip_wsp(&mut self.chars); + self.parse_smooth_curveto(abs)?; + }, + 'Q' | 'q' => { + skip_wsp(&mut self.chars); + self.parse_quadratic_bezier_curveto(abs)?; + }, + 'T' | 't' => { + skip_wsp(&mut self.chars); + self.parse_smooth_quadratic_bezier_curveto(abs)?; + }, + 'A' | 'a' => { + skip_wsp(&mut self.chars); + self.parse_elliprical_arc(abs)?; + }, + _ => return Err(()), + } + }, + _ => break, // no more commands. + } + } + Ok(()) + } + + /// Parse "moveto" command. + fn parse_moveto(&mut self) -> Result<(), ()> { + let command = match self.chars.next() { + Some(c) if c == 'M' || c == 'm' => c, + _ => return Err(()), + }; + + skip_wsp(&mut self.chars); + let point = parse_coord(&mut self.chars)?; + let absolute = command == 'M'; + self.path.push(PathCommand::MoveTo { point, absolute } ); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut self.chars) || + self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) { + return Ok(()); + } + skip_comma_wsp(&mut self.chars); + + // If a moveto is followed by multiple pairs of coordinates, the subsequent + // pairs are treated as implicit lineto commands. + self.parse_lineto(absolute) + } + + /// Parse "lineto" command. + fn parse_lineto(&mut self, absolute: bool) -> Result<(), ()> { + loop { + let point = parse_coord(&mut self.chars)?; + self.path.push(PathCommand::LineTo { point, absolute }); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut self.chars) || + self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) { + break; + } + skip_comma_wsp(&mut self.chars); + } + Ok(()) + } + + /// Parse horizontal "lineto" command. + fn parse_h_lineto(&mut self, absolute: bool) -> Result<(), ()> { + loop { + let x = parse_number(&mut self.chars)?; + self.path.push(PathCommand::HorizontalLineTo { x, absolute }); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut self.chars) || + self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) { + break; + } + skip_comma_wsp(&mut self.chars); + } + Ok(()) + } + + /// Parse vertical "lineto" command. + fn parse_v_lineto(&mut self, absolute: bool) -> Result<(), ()> { + loop { + let y = parse_number(&mut self.chars)?; + self.path.push(PathCommand::VerticalLineTo { y, absolute }); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut self.chars) || + self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) { + break; + } + skip_comma_wsp(&mut self.chars); + } + Ok(()) + } + + /// Parse cubic Bézier curve command. + fn parse_curveto(&mut self, absolute: bool) -> Result<(), ()> { + loop { + let control1 = parse_coord(&mut self.chars)?; + skip_comma_wsp(&mut self.chars); + let control2 = parse_coord(&mut self.chars)?; + skip_comma_wsp(&mut self.chars); + let point = parse_coord(&mut self.chars)?; + + self.path.push(PathCommand::CurveTo { control1, control2, point, absolute }); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut self.chars) || + self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) { + break; + } + skip_comma_wsp(&mut self.chars); + } + Ok(()) + } + + /// Parse smooth "curveto" command. + fn parse_smooth_curveto(&mut self, absolute: bool) -> Result<(), ()> { + loop { + let control2 = parse_coord(&mut self.chars)?; + skip_comma_wsp(&mut self.chars); + let point = parse_coord(&mut self.chars)?; + + self.path.push(PathCommand::SmoothCurveTo { control2, point, absolute }); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut self.chars) || + self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) { + break; + } + skip_comma_wsp(&mut self.chars); + } + Ok(()) + } + + /// Parse quadratic Bézier curve command. + fn parse_quadratic_bezier_curveto(&mut self, absolute: bool) -> Result<(), ()> { + loop { + let control1 = parse_coord(&mut self.chars)?; + skip_comma_wsp(&mut self.chars); + let point = parse_coord(&mut self.chars)?; + + self.path.push(PathCommand::QuadBezierCurveTo { control1, point, absolute }); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut self.chars) || + self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) { + break; + } + skip_comma_wsp(&mut self.chars); + } + Ok(()) + } + + /// Parse smooth quadratic Bézier curveto command. + fn parse_smooth_quadratic_bezier_curveto(&mut self, absolute: bool) -> Result<(), ()> { + loop { + let point = parse_coord(&mut self.chars)?; + + self.path.push(PathCommand::SmoothQuadBezierCurveTo { point, absolute }); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut self.chars) || + self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) { + break; + } + skip_comma_wsp(&mut self.chars); + } + Ok(()) + } + + /// Parse elliptical arc curve command. + fn parse_elliprical_arc(&mut self, absolute: bool) -> Result<(), ()> { + // Parse a flag whose value is '0' or '1'; otherwise, return Err(()). + let parse_flag = |iter: &mut Peekable| -> Result { + let value = match iter.peek() { + Some(c) if *c == '0' || *c == '1' => *c == '1', + _ => return Err(()), + }; + iter.next(); + Ok(value) + }; + + loop { + let rx = parse_number(&mut self.chars)?; + skip_comma_wsp(&mut self.chars); + let ry = parse_number(&mut self.chars)?; + skip_comma_wsp(&mut self.chars); + let angle = parse_number(&mut self.chars)?; + skip_comma_wsp(&mut self.chars); + let large_arc_flag = parse_flag(&mut self.chars)?; + skip_comma_wsp(&mut self.chars); + let sweep_flag = parse_flag(&mut self.chars)?; + skip_comma_wsp(&mut self.chars); + let point = parse_coord(&mut self.chars)?; + + self.path.push( + PathCommand::EllipticalArc { + rx, ry, angle, large_arc_flag, sweep_flag, point, absolute + } + ); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut self.chars) || + self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) { + break; + } + skip_comma_wsp(&mut self.chars); + } + Ok(()) + } +} + +/// The SVG path data. +/// +/// https://www.w3.org/TR/SVG11/paths.html#PathData +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue)] +pub struct SVGPathData(Box<[PathCommand]>); + +impl SVGPathData { + /// Return SVGPathData by a slice of PathCommand. + #[inline] + pub fn new(cmd: Box<[PathCommand]>) -> Self { + debug_assert!(!cmd.is_empty()); + SVGPathData(cmd) + } + + /// Get the array of PathCommand. + #[inline] + pub fn commands(&self) -> &[PathCommand] { + debug_assert!(!self.0.is_empty()); + &self.0 + } +} + +impl ToCss for SVGPathData { + #[inline] + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write + { + dest.write_char('"')?; + { + let mut writer = SequenceWriter::new(dest, " "); + for command in self.0.iter() { + writer.item(command)?; + } + } + dest.write_char('"') + } +} + +impl Parse for SVGPathData { + // We cannot use cssparser::Parser to parse a SVG path string because the spec wants to make + // the SVG path string as compact as possible. (i.e. The whitespaces may be dropped.) + // e.g. "M100 200L100 200" is a valid SVG path string. If we use tokenizer, the first ident + // is "M100", instead of "M", and this is not correct. Therefore, we use a Peekable + // str::Char iterator to check each character. + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't> + ) -> Result> { + let location = input.current_source_location(); + let path_string = input.expect_string()?.as_ref(); + if path_string.is_empty() { + // Treat an empty string as invalid, so we will not set it. + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // Parse the svg path string as multiple sub-paths. + let mut path_parser = PathParser { + chars: path_string.chars().peekable(), + path: Vec::new(), + }; + while skip_wsp(&mut path_parser.chars) { + if path_parser.parse_subpath().is_err() { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + + Ok(SVGPathData::new(path_parser.path.into_boxed_slice())) + } +} + + +/// The SVG path command. +/// The fields of these commands are self-explanatory, so we skip the documents. +/// Note: the index of the control points, e.g. control1, control2, are mapping to the control +/// points of the Bézier curve in the spec. +/// +/// https://www.w3.org/TR/SVG11/paths.html#PathData +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo)] +#[allow(missing_docs)] +#[repr(C, u8)] +pub enum PathCommand { + /// The unknown type. + /// https://www.w3.org/TR/SVG/paths.html#__svg__SVGPathSeg__PATHSEG_UNKNOWN + Unknown, + /// The "moveto" command. + MoveTo { point: CoordPair, absolute: bool }, + /// The "lineto" command. + LineTo { point: CoordPair, absolute: bool }, + /// The horizontal "lineto" command. + HorizontalLineTo { x: CSSFloat, absolute: bool }, + /// The vertical "lineto" command. + VerticalLineTo { y: CSSFloat, absolute: bool }, + /// The cubic Bézier curve command. + CurveTo { control1: CoordPair, control2: CoordPair, point: CoordPair, absolute: bool }, + /// The smooth curve command. + SmoothCurveTo { control2: CoordPair, point: CoordPair, absolute: bool }, + /// The quadratic Bézier curve command. + QuadBezierCurveTo { control1: CoordPair, point: CoordPair, absolute: bool }, + /// The smooth quadratic Bézier curve command. + SmoothQuadBezierCurveTo { point: CoordPair, absolute: bool }, + /// The elliptical arc curve command. + EllipticalArc { + rx: CSSFloat, + ry: CSSFloat, + angle: CSSFloat, + large_arc_flag: bool, + sweep_flag: bool, + point: CoordPair, + absolute: bool + }, + /// The "closepath" command. + ClosePath, +} + +impl ToCss for PathCommand { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write + { + use self::PathCommand::*; + match *self { + Unknown => dest.write_str("X"), + ClosePath => dest.write_str("Z"), + MoveTo { point, absolute } => { + dest.write_char(if absolute { 'M' } else { 'm' })?; + dest.write_char(' ')?; + point.to_css(dest) + } + LineTo { point, absolute } => { + dest.write_char(if absolute { 'L' } else { 'l' })?; + dest.write_char(' ')?; + point.to_css(dest) + } + CurveTo { control1, control2, point, absolute } => { + dest.write_char(if absolute { 'C' } else { 'c' })?; + dest.write_char(' ')?; + control1.to_css(dest)?; + dest.write_char(' ')?; + control2.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + QuadBezierCurveTo { control1, point, absolute } => { + dest.write_char(if absolute { 'Q' } else { 'q' })?; + dest.write_char(' ')?; + control1.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + EllipticalArc { rx, ry, angle, large_arc_flag, sweep_flag, point, absolute } => { + dest.write_char(if absolute { 'A' } else { 'a' })?; + dest.write_char(' ')?; + rx.to_css(dest)?; + dest.write_char(' ')?; + ry.to_css(dest)?; + dest.write_char(' ')?; + angle.to_css(dest)?; + dest.write_char(' ')?; + (large_arc_flag as i32).to_css(dest)?; + dest.write_char(' ')?; + (sweep_flag as i32).to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + HorizontalLineTo { x, absolute } => { + dest.write_char(if absolute { 'H' } else { 'h' })?; + dest.write_char(' ')?; + x.to_css(dest) + }, + VerticalLineTo { y, absolute } => { + dest.write_char(if absolute { 'V' } else { 'v' })?; + dest.write_char(' ')?; + y.to_css(dest) + }, + SmoothCurveTo { control2, point, absolute } => { + dest.write_char(if absolute { 'S' } else { 's' })?; + dest.write_char(' ')?; + control2.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + SmoothQuadBezierCurveTo { point, absolute } => { + dest.write_char(if absolute { 'T' } else { 't' })?; + dest.write_char(' ')?; + point.to_css(dest) + }, + } + } +} + +/// The path coord type. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss)] +#[repr(C)] +pub struct CoordPair(CSSFloat, CSSFloat); + +impl CoordPair { + /// Create a CoordPair. + #[inline] + pub fn new(x: CSSFloat, y: CSSFloat) -> Self { + CoordPair(x, y) + } +} + +/// Parse a pair of numbers into CoordPair. +fn parse_coord(iter: &mut Peekable) -> Result { + let x = parse_number(iter)?; + skip_comma_wsp(iter); + let y = parse_number(iter)?; + Ok(CoordPair::new(x, y)) +} + +/// This is a special version which parses the number for SVG Path. e.g. "M 0.6.5" should be parsed +/// as MoveTo with a coordinate of ("0.6", ".5"), instead of treating 0.6.5 as a non-valid floating +/// point number. In other words, the logic here is similar with that of +/// tokenizer::consume_numeric, which also consumes the number as many as possible, but here the +/// input is a Peekable and we only accept an integer of a floating point number. +/// +/// The "number" syntax in https://www.w3.org/TR/SVG/paths.html#PathDataBNF +fn parse_number(iter: &mut Peekable) -> Result { + // 1. Check optional sign. + let sign = if iter.peek().map_or(false, |&sign: &char| sign == '+' || sign == '-') { + if iter.next().unwrap() == '-' { -1. } else { 1. } + } else { + 1. + }; + + // 2. Check integer part. + let mut integral_part: f64 = 0.; + let got_dot = if !iter.peek().map_or(false, |&n: &char| n == '.') { + // If the first digit in integer part is neither a dot nor a digit, this is not a number. + if iter.peek().map_or(true, |n: &char| !n.is_ascii_digit()) { + return Err(()); + } + + while iter.peek().map_or(false, |n: &char| n.is_ascii_digit()) { + integral_part = + integral_part * 10. + iter.next().unwrap().to_digit(10).unwrap() as f64; + } + + iter.peek().map_or(false, |&n: &char| n == '.') + } else { + true + }; + + // 3. Check fractional part. + let mut fractional_part: f64 = 0.; + if got_dot { + // Consume '.'. + iter.next(); + // If the first digit in fractional part is not a digit, this is not a number. + if iter.peek().map_or(true, |n: &char| !n.is_ascii_digit()) { + return Err(()); + } + + let mut factor = 0.1; + while iter.peek().map_or(false, |n: &char| n.is_ascii_digit()) { + fractional_part += iter.next().unwrap().to_digit(10).unwrap() as f64 * factor; + factor *= 0.1; + } + } + + let mut value = sign * (integral_part + fractional_part); + + // 4. Check exp part. The segment name of SVG Path doesn't include 'E' or 'e', so it's ok to + // treat the numbers after 'E' or 'e' are in the exponential part. + if iter.peek().map_or(false, |&exp: &char| exp == 'E' || exp == 'e') { + // Consume 'E' or 'e'. + iter.next(); + let exp_sign = if iter.peek().map_or(false, |&sign: &char| sign == '+' || sign == '-') { + if iter.next().unwrap() == '-' { -1. } else { 1. } + } else { + 1. + }; + + let mut exp: f64 = 0.; + while iter.peek().map_or(false, |n: &char| n.is_ascii_digit()) { + exp = exp * 10. + iter.next().unwrap().to_digit(10).unwrap() as f64; + } + + value *= f64::powf(10., exp * exp_sign); + } + + if value.is_finite() { + Ok(value.min(::std::f32::MAX as f64).max(::std::f32::MIN as f64) as CSSFloat) + } else { + Err(()) + } +} + +/// Skip all svg whitespaces, and return true if |iter| hasn't finished. +#[inline] +fn skip_wsp(iter: &mut Peekable) -> bool { + // Note: SVG 1.1 defines the whitespaces as \u{9}, \u{20}, \u{A}, \u{D}. + // However, SVG 2 has one extra whitespace: \u{C}. + // Therefore, we follow the newest spec for the definition of whitespace, + // i.e. \u{9}, \u{20}, \u{A}, \u{C}, \u{D}, by is_ascii_whitespace(). + while iter.peek().map_or(false, |c: &char| c.is_ascii_whitespace()) { + iter.next(); + } + iter.peek().is_some() +} + +/// Skip all svg whitespaces and one comma, and return true if |iter| hasn't finished. +#[inline] +fn skip_comma_wsp(iter: &mut Peekable) -> bool { + if !skip_wsp(iter) { + return false; + } + + if *iter.peek().unwrap() != ',' { + return true; + } + iter.next(); + + skip_wsp(iter) +}