style: Define offset-path and implement it in style system.

Define OffsetPath & SVGPathData on the servo-side, and StyleMotion &
StyleSVGPath on the gecko-side. We parse the SVG Path string into a
vector of PathCommand. To build the gfx::Path, we will convert it into
gfx::Path later in a different patch.

The basic flow is:
  - Parse SVG Path String into SVGPathData (in Rust).
  - Use cbindgen to make sure the layout of PathCommand and StylePathCommand, and then set the Box[PathCommand] into nsTArray<StylePathCommand>.
  - Try to convert nsTArray<StylePathCommand> into gfx::Path. (This part will be implemented in a different patch.)

Finally, we use the gfx::Path to create a motion path transform.
The layout implementation is in the later patch.

Depends on D2962

Differential Revision: https://phabricator.services.mozilla.com/D2963
This commit is contained in:
Boris Chiou 2018-08-22 01:24:13 +00:00 committed by Emilio Cobos Álvarez
parent 249b865eb8
commit dce2e2927f
No known key found for this signature in database
GPG key ID: 056B727BB9C1027C
8 changed files with 742 additions and 3 deletions

View file

@ -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"]

View file

@ -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<PathCommand> =
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 {

View file

@ -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);
}
</%self:impl_trait>
<%def name="simple_image_array_property(name, shorthand, field_name)">

View file

@ -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",

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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(<string>).
#[css(function)]
Path(SVGPathData),
/// None value.
None,
// Bug 1186329: Implement ray(), <basic-shape>, <geometry-box>, and <url>.
}
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<Self, ParseError<'i>> {
// 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(), <basic-shape>, <geometry-box>,
// and <url>.
"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<Chars<'a>>,
path: Vec<PathCommand>,
}
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<Chars>| -> Result<bool, ()> {
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<W>(&self, dest: &mut CssWriter<W>) -> 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<Self, ParseError<'i>> {
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<W>(&self, dest: &mut CssWriter<W>) -> 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<Chars>) -> Result<CoordPair, ()> {
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<Chars>) -> Result<CSSFloat, ()> {
// 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<Chars>) -> 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<Chars>) -> bool {
if !skip_wsp(iter) {
return false;
}
if *iter.peek().unwrap() != ',' {
return true;
}
iter.next();
skip_wsp(iter)
}