From 6fc06f45c255adfec9c8a6861496ee7b6ee7db6f Mon Sep 17 00:00:00 2001 From: Nico Weber Date: Mon, 18 Nov 2024 21:21:22 -0500 Subject: [PATCH] LibWeb: Plumbing for svg stroke-dashoffset --- Libraries/LibWeb/CSS/ComputedValues.h | 7 +- Libraries/LibWeb/CSS/Properties.json | 12 + Libraries/LibWeb/Layout/Node.cpp | 11 + Libraries/LibWeb/SVG/SVGGraphicsElement.cpp | 22 +- Libraries/LibWeb/SVG/SVGGraphicsElement.h | 2 + ...upported-properties-and-default-values.txt | 4 +- ...eclaration-has-indexed-property-getter.txt | 339 +++++++++--------- .../css/getComputedStyle-print-all.txt | 3 +- 8 files changed, 223 insertions(+), 177 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 3c87c141603..cc67aa2517b 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -150,10 +150,12 @@ public: static float fill_opacity() { return 1.0f; } static CSS::FillRule fill_rule() { return CSS::FillRule::Nonzero; } static CSS::ClipRule clip_rule() { return CSS::ClipRule::Nonzero; } + static CSS::LengthPercentage stroke_dashoffset() { return CSS::Length::make_px(0); } static CSS::StrokeLinecap stroke_linecap() { return CSS::StrokeLinecap::Butt; } static CSS::StrokeLinejoin stroke_linejoin() { return CSS::StrokeLinejoin::Miter; } static float stroke_miterlimit() { return 4.0f; } static float stroke_opacity() { return 1.0f; } + static CSS::LengthPercentage stroke_width() { return CSS::Length::make_px(1); } static float stop_opacity() { return 1.0f; } static CSS::TextAnchor text_anchor() { return CSS::TextAnchor::Start; } static CSS::Length border_radius() { return Length::make_px(0); } @@ -478,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; } + 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; } NumberOrCalculated stroke_miterlimit() const { return m_inherited.stroke_miterlimit; } @@ -578,11 +581,12 @@ protected: CSS::FillRule fill_rule { InitialValues::fill_rule() }; Optional stroke; float fill_opacity { InitialValues::fill_opacity() }; + LengthPercentage stroke_dashoffset { InitialValues::stroke_dashoffset() }; CSS::StrokeLinecap stroke_linecap { InitialValues::stroke_linecap() }; CSS::StrokeLinejoin stroke_linejoin { InitialValues::stroke_linejoin() }; NumberOrCalculated stroke_miterlimit { InitialValues::stroke_miterlimit() }; float stroke_opacity { InitialValues::stroke_opacity() }; - LengthPercentage stroke_width { Length::make_px(1) }; + LengthPercentage stroke_width { InitialValues::stroke_width() }; CSS::TextAnchor text_anchor { InitialValues::text_anchor() }; CSS::ClipRule clip_rule { InitialValues::clip_rule() }; @@ -826,6 +830,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_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; } void set_stroke_miterlimit(NumberOrCalculated value) { m_inherited.stroke_miterlimit = value; } diff --git a/Libraries/LibWeb/CSS/Properties.json b/Libraries/LibWeb/CSS/Properties.json index 80b76be9503..2212af2dfc2 100644 --- a/Libraries/LibWeb/CSS/Properties.json +++ b/Libraries/LibWeb/CSS/Properties.json @@ -2420,6 +2420,18 @@ "paint" ] }, + "stroke-dashoffset": { + "affects-layout": false, + "animation-type": "by-computed-value", + "inherited": true, + "initial": "0", + "valid-types": [ + "length [0,∞]", + "number [0,∞]", + "percentage [0,∞]" + ], + "percentages-resolve-to": "length" + }, "stroke-linecap": { "affects-layout": false, "animation-type": "discrete", diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index c2b99f1ea92..f61e8304951 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -854,6 +854,17 @@ void NodeWithStyle::apply_style(const CSS::StyleProperties& computed_style) computed_values.set_fill_rule(*fill_rule); computed_values.set_fill_opacity(computed_style.fill_opacity()); + + 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 + if (stroke_dashoffset.is_number()) + computed_values.set_stroke_dashoffset(CSS::Length::make_px(CSSPixels::nearest_value_for(stroke_dashoffset.as_number().number()))); + else if (stroke_dashoffset.is_length()) + computed_values.set_stroke_dashoffset(stroke_dashoffset.as_length().length()); + else if (stroke_dashoffset.is_percentage()) + computed_values.set_stroke_dashoffset(CSS::LengthPercentage { stroke_dashoffset.as_percentage().percentage() }); + if (auto stroke_linecap = computed_style.stroke_linecap(); stroke_linecap.has_value()) computed_values.set_stroke_linecap(stroke_linecap.value()); if (auto stroke_linejoin = computed_style.stroke_linejoin(); stroke_linejoin.has_value()) diff --git a/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp b/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp index a643fa535bb..769f3767b98 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::StrokeDashoffset), NamedPropertyID(CSS::PropertyID::StrokeLinecap), NamedPropertyID(CSS::PropertyID::StrokeLinejoin), NamedPropertyID(CSS::PropertyID::StrokeMiterlimit), @@ -266,13 +267,10 @@ Optional SVGGraphicsElement::stroke_opacity() const return layout_node()->computed_values().stroke_opacity(); } -Optional SVGGraphicsElement::stroke_width() const +float SVGGraphicsElement::resolve_relative_to_viewport_size(CSS::LengthPercentage const& length_percentage) const { - if (!layout_node()) - return {}; // FIXME: Converting to pixels isn't really correct - values should be in "user units" // https://svgwg.org/svg2-draft/coords.html#TermUserUnits - auto width = layout_node()->computed_values().stroke_width(); // Resolved relative to the "Scaled viewport size": https://www.w3.org/TR/2017/WD-fill-stroke-3-20170413/#scaled-viewport-size // FIXME: This isn't right, but it's something. CSSPixels viewport_width = 0; @@ -284,7 +282,21 @@ Optional SVGGraphicsElement::stroke_width() const } } auto scaled_viewport_size = (viewport_width + viewport_height) * CSSPixels(0.5); - return width.to_px(*layout_node(), scaled_viewport_size).to_double(); + return length_percentage.to_px(*layout_node(), scaled_viewport_size).to_double(); +} + +Optional SVGGraphicsElement::stroke_dashoffset() const +{ + if (!layout_node()) + return {}; + return resolve_relative_to_viewport_size(layout_node()->computed_values().stroke_dashoffset()); +} + +Optional SVGGraphicsElement::stroke_width() const +{ + if (!layout_node()) + return {}; + return resolve_relative_to_viewport_size(layout_node()->computed_values().stroke_width()); } // https://svgwg.org/svg2-draft/types.html#__svg__SVGGraphicsElement__getBBox diff --git a/Libraries/LibWeb/SVG/SVGGraphicsElement.h b/Libraries/LibWeb/SVG/SVGGraphicsElement.h index f0ee21261d6..9fbb1799ff8 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; + Optional stroke_dashoffset() const; Optional stroke_width() const; Optional fill_opacity() const; Optional stroke_linecap() const; @@ -94,6 +95,7 @@ protected: private: virtual bool is_svg_graphics_element() const final { return true; } + float resolve_relative_to_viewport_size(CSS::LengthPercentage const& length_percentage) const; }; Gfx::AffineTransform transform_from_transform_list(ReadonlySpan transform_list); 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 e61ef9be224..567376a2b1f 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': '202' +'length': '203' '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' +'strokeDashoffset': '0' +'stroke-dashoffset': '0' 'strokeLinecap': 'butt' 'stroke-linecap': 'butt' 'strokeLinejoin': 'miter' 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 b080dde96f0..37e322d0855 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,175 +33,176 @@ All properties associated with getComputedStyle(document.body): "30": "pointer-events", "31": "quotes", "32": "stroke", - "33": "stroke-linecap", - "34": "stroke-linejoin", - "35": "stroke-miterlimit", - "36": "stroke-opacity", - "37": "stroke-width", - "38": "tab-size", - "39": "text-align", - "40": "text-anchor", - "41": "text-decoration-line", - "42": "text-indent", - "43": "text-justify", - "44": "text-shadow", - "45": "text-transform", - "46": "visibility", - "47": "white-space", - "48": "word-break", - "49": "word-spacing", - "50": "word-wrap", - "51": "writing-mode", - "52": "align-content", - "53": "align-items", - "54": "align-self", - "55": "animation-delay", - "56": "animation-direction", - "57": "animation-duration", - "58": "animation-fill-mode", - "59": "animation-iteration-count", - "60": "animation-name", - "61": "animation-play-state", - "62": "animation-timing-function", - "63": "appearance", - "64": "aspect-ratio", - "65": "backdrop-filter", - "66": "background-attachment", - "67": "background-clip", - "68": "background-color", - "69": "background-image", - "70": "background-origin", - "71": "background-position-x", - "72": "background-position-y", - "73": "background-repeat", - "74": "background-size", - "75": "border-bottom-color", - "76": "border-bottom-left-radius", - "77": "border-bottom-right-radius", - "78": "border-bottom-style", - "79": "border-bottom-width", - "80": "border-left-color", - "81": "border-left-style", - "82": "border-left-width", - "83": "border-right-color", - "84": "border-right-style", - "85": "border-right-width", - "86": "border-top-color", - "87": "border-top-left-radius", - "88": "border-top-right-radius", - "89": "border-top-style", - "90": "border-top-width", - "91": "bottom", - "92": "box-shadow", - "93": "box-sizing", - "94": "clear", - "95": "clip", - "96": "clip-path", - "97": "column-count", - "98": "column-gap", - "99": "column-span", - "100": "column-width", - "101": "content", - "102": "content-visibility", - "103": "counter-increment", - "104": "counter-reset", - "105": "counter-set", - "106": "cx", - "107": "cy", - "108": "display", - "109": "filter", - "110": "flex-basis", - "111": "flex-direction", - "112": "flex-grow", - "113": "flex-shrink", - "114": "flex-wrap", - "115": "float", - "116": "grid-auto-columns", - "117": "grid-auto-flow", - "118": "grid-auto-rows", - "119": "grid-column-end", - "120": "grid-column-start", - "121": "grid-row-end", - "122": "grid-row-start", - "123": "grid-template-areas", - "124": "grid-template-columns", - "125": "grid-template-rows", - "126": "height", - "127": "inline-size", - "128": "inset-block-end", - "129": "inset-block-start", - "130": "inset-inline-end", - "131": "inset-inline-start", - "132": "justify-content", - "133": "justify-items", - "134": "justify-self", - "135": "left", - "136": "margin-block-end", - "137": "margin-block-start", - "138": "margin-bottom", - "139": "margin-inline-end", - "140": "margin-inline-start", - "141": "margin-left", - "142": "margin-right", - "143": "margin-top", - "144": "mask", - "145": "mask-image", - "146": "mask-type", - "147": "max-height", - "148": "max-inline-size", - "149": "max-width", - "150": "min-height", - "151": "min-inline-size", - "152": "min-width", - "153": "object-fit", - "154": "object-position", - "155": "opacity", - "156": "order", - "157": "outline-color", - "158": "outline-offset", - "159": "outline-style", - "160": "outline-width", - "161": "overflow-x", - "162": "overflow-y", - "163": "padding-block-end", - "164": "padding-block-start", - "165": "padding-bottom", - "166": "padding-inline-end", - "167": "padding-inline-start", - "168": "padding-left", - "169": "padding-right", - "170": "padding-top", - "171": "position", - "172": "r", - "173": "right", - "174": "rotate", - "175": "row-gap", - "176": "rx", - "177": "ry", - "178": "scrollbar-gutter", - "179": "scrollbar-width", - "180": "stop-color", - "181": "stop-opacity", - "182": "table-layout", - "183": "text-decoration-color", - "184": "text-decoration-style", - "185": "text-decoration-thickness", - "186": "text-overflow", - "187": "top", - "188": "transform", - "189": "transform-box", - "190": "transform-origin", - "191": "transition-delay", - "192": "transition-duration", - "193": "transition-property", - "194": "transition-timing-function", - "195": "unicode-bidi", - "196": "user-select", - "197": "vertical-align", - "198": "width", - "199": "x", - "200": "y", - "201": "z-index" + "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" } 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 124a50719a9..4bfd10393a5 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-dashoffset: 0 stroke-linecap: butt stroke-linejoin: miter stroke-miterlimit: 4 @@ -124,7 +125,7 @@ grid-row-start: auto grid-template-areas: none grid-template-columns: auto grid-template-rows: auto -height: 2142px +height: 2159px inline-size: auto inset-block-end: auto inset-block-start: auto