diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index cc67aa2517b..f82485ee857 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -480,6 +480,7 @@ public: CSS::FillRule fill_rule() const { return m_inherited.fill_rule; } Optional const& stroke() const { return m_inherited.stroke; } float fill_opacity() const { return m_inherited.fill_opacity; } + Vector> const& stroke_dasharray() const { return m_inherited.stroke_dasharray; } LengthPercentage const& stroke_dashoffset() const { return m_inherited.stroke_dashoffset; } CSS::StrokeLinecap stroke_linecap() const { return m_inherited.stroke_linecap; } CSS::StrokeLinejoin stroke_linejoin() const { return m_inherited.stroke_linejoin; } @@ -581,6 +582,7 @@ protected: CSS::FillRule fill_rule { InitialValues::fill_rule() }; Optional stroke; float fill_opacity { InitialValues::fill_opacity() }; + Vector> stroke_dasharray; LengthPercentage stroke_dashoffset { InitialValues::stroke_dashoffset() }; CSS::StrokeLinecap stroke_linecap { InitialValues::stroke_linecap() }; CSS::StrokeLinejoin stroke_linejoin { InitialValues::stroke_linejoin() }; @@ -830,6 +832,7 @@ public: void set_stroke(SVGPaint value) { m_inherited.stroke = value; } void set_fill_rule(CSS::FillRule value) { m_inherited.fill_rule = value; } void set_fill_opacity(float value) { m_inherited.fill_opacity = value; } + void set_stroke_dasharray(Vector> value) { m_inherited.stroke_dasharray = move(value); } void set_stroke_dashoffset(LengthPercentage value) { m_inherited.stroke_dashoffset = value; } void set_stroke_linecap(CSS::StrokeLinecap value) { m_inherited.stroke_linecap = value; } void set_stroke_linejoin(CSS::StrokeLinejoin value) { m_inherited.stroke_linejoin = value; } diff --git a/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Libraries/LibWeb/CSS/Parser/Parser.cpp index 5d8ff1db63e..5edc59d3e92 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -5017,6 +5017,38 @@ RefPtr Parser::parse_rotate_value(TokenStream& to return nullptr; } +RefPtr Parser::parse_stroke_dasharray_value(TokenStream& tokens) +{ + // https://svgwg.org/svg2-draft/painting.html#StrokeDashing + // Value: none | + if (auto none = parse_all_as_single_keyword_value(tokens, Keyword::None)) + return none; + + // https://svgwg.org/svg2-draft/painting.html#DataTypeDasharray + // = [ [ | ]+ ]# + Vector> dashes; + while (tokens.has_next_token()) { + tokens.discard_whitespace(); + + // A is a list of comma and/or white space separated or values. A value represents a value in user units. + auto value = parse_number_value(tokens); + if (value) { + dashes.append(value.release_nonnull()); + } else { + auto value = parse_length_percentage_value(tokens); + if (!value) + return {}; + dashes.append(value.release_nonnull()); + } + + tokens.discard_whitespace(); + if (tokens.has_next_token() && tokens.next_token().is(Token::Type::Comma)) + tokens.discard_a_token(); + } + + return StyleValueList::create(move(dashes), StyleValueList::Separator::Comma); +} + RefPtr Parser::parse_content_value(TokenStream& tokens) { // FIXME: `content` accepts several kinds of function() type, which we don't handle in property_accepts_value() yet. @@ -7885,6 +7917,10 @@ Parser::ParseErrorOr> Parser::parse_css_value(Prope if (auto parsed_value = parse_scrollbar_gutter_value(tokens); parsed_value && !tokens.has_next_token()) return parsed_value.release_nonnull(); return ParseError::SyntaxError; + case PropertyID::StrokeDasharray: + if (auto parsed_value = parse_stroke_dasharray_value(tokens); parsed_value && !tokens.has_next_token()) + return parsed_value.release_nonnull(); + return ParseError::SyntaxError; case PropertyID::TextDecoration: if (auto parsed_value = parse_text_decoration_value(tokens); parsed_value && !tokens.has_next_token()) return parsed_value.release_nonnull(); diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index dd8476c68ad..94b4d117cba 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -339,6 +339,7 @@ private: RefPtr parse_text_decoration_value(TokenStream&); RefPtr parse_text_decoration_line_value(TokenStream&); RefPtr parse_rotate_value(TokenStream&); + RefPtr parse_stroke_dasharray_value(TokenStream&); RefPtr parse_easing_value(TokenStream&); RefPtr parse_transform_value(TokenStream&); RefPtr parse_transform_origin_value(TokenStream&); diff --git a/Libraries/LibWeb/CSS/Properties.json b/Libraries/LibWeb/CSS/Properties.json index 2212af2dfc2..756011a7c6b 100644 --- a/Libraries/LibWeb/CSS/Properties.json +++ b/Libraries/LibWeb/CSS/Properties.json @@ -2420,6 +2420,12 @@ "paint" ] }, + "stroke-dasharray": { + "animation-type": "custom", + "inherited": true, + "initial": "none", + "affects-layout": false + }, "stroke-dashoffset": { "affects-layout": false, "animation-type": "by-computed-value", diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index f61e8304951..3d35c79dc1c 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -855,6 +855,24 @@ void NodeWithStyle::apply_style(const CSS::StyleProperties& computed_style) computed_values.set_fill_opacity(computed_style.fill_opacity()); + if (auto const& stroke_dasharray_or_none = computed_style.property(CSS::PropertyID::StrokeDasharray); !stroke_dasharray_or_none.is_keyword()) { + auto const& stroke_dasharray = stroke_dasharray_or_none.as_value_list(); + Vector> dashes; + + for (auto const& value : stroke_dasharray.values()) { + if (value->is_length()) + dashes.append(CSS::LengthPercentage { value->as_length().length() }); + else if (value->is_percentage()) + dashes.append(CSS::LengthPercentage { value->as_percentage().percentage() }); + else if (value->is_math()) + dashes.append(CSS::LengthPercentage { value->as_math() }); + else if (value->is_number()) + dashes.append(CSS::NumberOrCalculated { value->as_number().number() }); + } + + computed_values.set_stroke_dasharray(move(dashes)); + } + auto const& stroke_dashoffset = computed_style.property(CSS::PropertyID::StrokeDashoffset); // FIXME: Converting to pixels isn't really correct - values should be in "user units" // https://svgwg.org/svg2-draft/coords.html#TermUserUnits diff --git a/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp b/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp index 769f3767b98..840a548d979 100644 --- a/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp +++ b/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp @@ -150,6 +150,7 @@ void SVGGraphicsElement::apply_presentational_hints(CSS::StyleProperties& style) NamedPropertyID(CSS::PropertyID::Fill), // FIXME: The `stroke` attribute and CSS `stroke` property are not the same! But our support is limited enough that they are equivalent for now. NamedPropertyID(CSS::PropertyID::Stroke), + NamedPropertyID(CSS::PropertyID::StrokeDasharray), NamedPropertyID(CSS::PropertyID::StrokeDashoffset), NamedPropertyID(CSS::PropertyID::StrokeLinecap), NamedPropertyID(CSS::PropertyID::StrokeLinejoin), @@ -285,6 +286,41 @@ float SVGGraphicsElement::resolve_relative_to_viewport_size(CSS::LengthPercentag return length_percentage.to_px(*layout_node(), scaled_viewport_size).to_double(); } +Vector SVGGraphicsElement::stroke_dasharray() const +{ + if (!layout_node()) + return {}; + + Vector dasharray; + for (auto const& value : layout_node()->computed_values().stroke_dasharray()) { + value.visit( + [&](CSS::LengthPercentage const& length_percentage) { + dasharray.append(resolve_relative_to_viewport_size(length_percentage)); + }, + [&](CSS::NumberOrCalculated const& number_or_calculated) { + dasharray.append(number_or_calculated.resolved(*layout_node())); + }); + } + + // https://svgwg.org/svg2-draft/painting.html#StrokeDashing + // If the list has an odd number of values, then it is repeated to yield an even number of values. + if (dasharray.size() % 2 == 1) + dasharray.extend(dasharray); + + // If any value in the list is negative, the value is invalid. If all of the values in the list are zero, then the stroke is rendered as a solid line without any dashing. + bool all_zero = true; + for (auto& value : dasharray) { + if (value < 0) + return {}; + if (value != 0) + all_zero = false; + } + if (all_zero) + return {}; + + return dasharray; +} + Optional SVGGraphicsElement::stroke_dashoffset() const { if (!layout_node()) diff --git a/Libraries/LibWeb/SVG/SVGGraphicsElement.h b/Libraries/LibWeb/SVG/SVGGraphicsElement.h index 9fbb1799ff8..83babc58465 100644 --- a/Libraries/LibWeb/SVG/SVGGraphicsElement.h +++ b/Libraries/LibWeb/SVG/SVGGraphicsElement.h @@ -36,6 +36,7 @@ public: Optional fill_color() const; Optional stroke_color() const; + Vector stroke_dasharray() const; Optional stroke_dashoffset() const; Optional stroke_width() const; Optional fill_opacity() const; diff --git a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt index 567376a2b1f..a64eb0e32e6 100644 --- a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt +++ b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt @@ -1,6 +1,6 @@ All supported properties and their default values exposed from CSSStyleDeclaration from getComputedStyle: 'cssText': '' -'length': '203' +'length': '204' 'parentRule': 'null' 'cssFloat': 'none' 'WebkitAlignContent': 'normal' @@ -495,6 +495,8 @@ All supported properties and their default values exposed from CSSStyleDeclarati 'stopOpacity': '1' 'stop-opacity': '1' 'stroke': 'none' +'strokeDasharray': 'none' +'stroke-dasharray': 'none' 'strokeDashoffset': '0' 'stroke-dashoffset': '0' 'strokeLinecap': 'butt' diff --git a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt index 37e322d0855..2a739619d4b 100644 --- a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt +++ b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt @@ -33,176 +33,177 @@ All properties associated with getComputedStyle(document.body): "30": "pointer-events", "31": "quotes", "32": "stroke", - "33": "stroke-dashoffset", - "34": "stroke-linecap", - "35": "stroke-linejoin", - "36": "stroke-miterlimit", - "37": "stroke-opacity", - "38": "stroke-width", - "39": "tab-size", - "40": "text-align", - "41": "text-anchor", - "42": "text-decoration-line", - "43": "text-indent", - "44": "text-justify", - "45": "text-shadow", - "46": "text-transform", - "47": "visibility", - "48": "white-space", - "49": "word-break", - "50": "word-spacing", - "51": "word-wrap", - "52": "writing-mode", - "53": "align-content", - "54": "align-items", - "55": "align-self", - "56": "animation-delay", - "57": "animation-direction", - "58": "animation-duration", - "59": "animation-fill-mode", - "60": "animation-iteration-count", - "61": "animation-name", - "62": "animation-play-state", - "63": "animation-timing-function", - "64": "appearance", - "65": "aspect-ratio", - "66": "backdrop-filter", - "67": "background-attachment", - "68": "background-clip", - "69": "background-color", - "70": "background-image", - "71": "background-origin", - "72": "background-position-x", - "73": "background-position-y", - "74": "background-repeat", - "75": "background-size", - "76": "border-bottom-color", - "77": "border-bottom-left-radius", - "78": "border-bottom-right-radius", - "79": "border-bottom-style", - "80": "border-bottom-width", - "81": "border-left-color", - "82": "border-left-style", - "83": "border-left-width", - "84": "border-right-color", - "85": "border-right-style", - "86": "border-right-width", - "87": "border-top-color", - "88": "border-top-left-radius", - "89": "border-top-right-radius", - "90": "border-top-style", - "91": "border-top-width", - "92": "bottom", - "93": "box-shadow", - "94": "box-sizing", - "95": "clear", - "96": "clip", - "97": "clip-path", - "98": "column-count", - "99": "column-gap", - "100": "column-span", - "101": "column-width", - "102": "content", - "103": "content-visibility", - "104": "counter-increment", - "105": "counter-reset", - "106": "counter-set", - "107": "cx", - "108": "cy", - "109": "display", - "110": "filter", - "111": "flex-basis", - "112": "flex-direction", - "113": "flex-grow", - "114": "flex-shrink", - "115": "flex-wrap", - "116": "float", - "117": "grid-auto-columns", - "118": "grid-auto-flow", - "119": "grid-auto-rows", - "120": "grid-column-end", - "121": "grid-column-start", - "122": "grid-row-end", - "123": "grid-row-start", - "124": "grid-template-areas", - "125": "grid-template-columns", - "126": "grid-template-rows", - "127": "height", - "128": "inline-size", - "129": "inset-block-end", - "130": "inset-block-start", - "131": "inset-inline-end", - "132": "inset-inline-start", - "133": "justify-content", - "134": "justify-items", - "135": "justify-self", - "136": "left", - "137": "margin-block-end", - "138": "margin-block-start", - "139": "margin-bottom", - "140": "margin-inline-end", - "141": "margin-inline-start", - "142": "margin-left", - "143": "margin-right", - "144": "margin-top", - "145": "mask", - "146": "mask-image", - "147": "mask-type", - "148": "max-height", - "149": "max-inline-size", - "150": "max-width", - "151": "min-height", - "152": "min-inline-size", - "153": "min-width", - "154": "object-fit", - "155": "object-position", - "156": "opacity", - "157": "order", - "158": "outline-color", - "159": "outline-offset", - "160": "outline-style", - "161": "outline-width", - "162": "overflow-x", - "163": "overflow-y", - "164": "padding-block-end", - "165": "padding-block-start", - "166": "padding-bottom", - "167": "padding-inline-end", - "168": "padding-inline-start", - "169": "padding-left", - "170": "padding-right", - "171": "padding-top", - "172": "position", - "173": "r", - "174": "right", - "175": "rotate", - "176": "row-gap", - "177": "rx", - "178": "ry", - "179": "scrollbar-gutter", - "180": "scrollbar-width", - "181": "stop-color", - "182": "stop-opacity", - "183": "table-layout", - "184": "text-decoration-color", - "185": "text-decoration-style", - "186": "text-decoration-thickness", - "187": "text-overflow", - "188": "top", - "189": "transform", - "190": "transform-box", - "191": "transform-origin", - "192": "transition-delay", - "193": "transition-duration", - "194": "transition-property", - "195": "transition-timing-function", - "196": "unicode-bidi", - "197": "user-select", - "198": "vertical-align", - "199": "width", - "200": "x", - "201": "y", - "202": "z-index" + "33": "stroke-dasharray", + "34": "stroke-dashoffset", + "35": "stroke-linecap", + "36": "stroke-linejoin", + "37": "stroke-miterlimit", + "38": "stroke-opacity", + "39": "stroke-width", + "40": "tab-size", + "41": "text-align", + "42": "text-anchor", + "43": "text-decoration-line", + "44": "text-indent", + "45": "text-justify", + "46": "text-shadow", + "47": "text-transform", + "48": "visibility", + "49": "white-space", + "50": "word-break", + "51": "word-spacing", + "52": "word-wrap", + "53": "writing-mode", + "54": "align-content", + "55": "align-items", + "56": "align-self", + "57": "animation-delay", + "58": "animation-direction", + "59": "animation-duration", + "60": "animation-fill-mode", + "61": "animation-iteration-count", + "62": "animation-name", + "63": "animation-play-state", + "64": "animation-timing-function", + "65": "appearance", + "66": "aspect-ratio", + "67": "backdrop-filter", + "68": "background-attachment", + "69": "background-clip", + "70": "background-color", + "71": "background-image", + "72": "background-origin", + "73": "background-position-x", + "74": "background-position-y", + "75": "background-repeat", + "76": "background-size", + "77": "border-bottom-color", + "78": "border-bottom-left-radius", + "79": "border-bottom-right-radius", + "80": "border-bottom-style", + "81": "border-bottom-width", + "82": "border-left-color", + "83": "border-left-style", + "84": "border-left-width", + "85": "border-right-color", + "86": "border-right-style", + "87": "border-right-width", + "88": "border-top-color", + "89": "border-top-left-radius", + "90": "border-top-right-radius", + "91": "border-top-style", + "92": "border-top-width", + "93": "bottom", + "94": "box-shadow", + "95": "box-sizing", + "96": "clear", + "97": "clip", + "98": "clip-path", + "99": "column-count", + "100": "column-gap", + "101": "column-span", + "102": "column-width", + "103": "content", + "104": "content-visibility", + "105": "counter-increment", + "106": "counter-reset", + "107": "counter-set", + "108": "cx", + "109": "cy", + "110": "display", + "111": "filter", + "112": "flex-basis", + "113": "flex-direction", + "114": "flex-grow", + "115": "flex-shrink", + "116": "flex-wrap", + "117": "float", + "118": "grid-auto-columns", + "119": "grid-auto-flow", + "120": "grid-auto-rows", + "121": "grid-column-end", + "122": "grid-column-start", + "123": "grid-row-end", + "124": "grid-row-start", + "125": "grid-template-areas", + "126": "grid-template-columns", + "127": "grid-template-rows", + "128": "height", + "129": "inline-size", + "130": "inset-block-end", + "131": "inset-block-start", + "132": "inset-inline-end", + "133": "inset-inline-start", + "134": "justify-content", + "135": "justify-items", + "136": "justify-self", + "137": "left", + "138": "margin-block-end", + "139": "margin-block-start", + "140": "margin-bottom", + "141": "margin-inline-end", + "142": "margin-inline-start", + "143": "margin-left", + "144": "margin-right", + "145": "margin-top", + "146": "mask", + "147": "mask-image", + "148": "mask-type", + "149": "max-height", + "150": "max-inline-size", + "151": "max-width", + "152": "min-height", + "153": "min-inline-size", + "154": "min-width", + "155": "object-fit", + "156": "object-position", + "157": "opacity", + "158": "order", + "159": "outline-color", + "160": "outline-offset", + "161": "outline-style", + "162": "outline-width", + "163": "overflow-x", + "164": "overflow-y", + "165": "padding-block-end", + "166": "padding-block-start", + "167": "padding-bottom", + "168": "padding-inline-end", + "169": "padding-inline-start", + "170": "padding-left", + "171": "padding-right", + "172": "padding-top", + "173": "position", + "174": "r", + "175": "right", + "176": "rotate", + "177": "row-gap", + "178": "rx", + "179": "ry", + "180": "scrollbar-gutter", + "181": "scrollbar-width", + "182": "stop-color", + "183": "stop-opacity", + "184": "table-layout", + "185": "text-decoration-color", + "186": "text-decoration-style", + "187": "text-decoration-thickness", + "188": "text-overflow", + "189": "top", + "190": "transform", + "191": "transform-box", + "192": "transform-origin", + "193": "transition-delay", + "194": "transition-duration", + "195": "transition-property", + "196": "transition-timing-function", + "197": "unicode-bidi", + "198": "user-select", + "199": "vertical-align", + "200": "width", + "201": "x", + "202": "y", + "203": "z-index" } All properties associated with document.body.style by default: {} diff --git a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt index 4bfd10393a5..6fe6d664a50 100644 --- a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt +++ b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt @@ -31,6 +31,7 @@ math-style: normal pointer-events: auto quotes: auto stroke: none +stroke-dasharray: none stroke-dashoffset: 0 stroke-linecap: butt stroke-linejoin: miter @@ -125,7 +126,7 @@ grid-row-start: auto grid-template-areas: none grid-template-columns: auto grid-template-rows: auto -height: 2159px +height: 2176px inline-size: auto inset-block-end: auto inset-block-start: auto