Ver código fonte

LibWeb: Lay out SVG `<clipPath>` uses

This uses the same trick as done for masks in #23554. Each use of an
SVG `<clipPath>` becomes it's own layout subtree rooted at it's user.
This allows each use have it's own layout (which allows supporting
features such as `clipPathUnits`).
MacDue 1 ano atrás
pai
commit
c1b5fe61d1

+ 2 - 0
Userland/Libraries/LibWeb/CMakeLists.txt

@@ -478,6 +478,7 @@ set(SOURCES
     Layout/SVGGraphicsBox.cpp
     Layout/SVGSVGBox.cpp
     Layout/SVGMaskBox.cpp
+    Layout/SVGClipBox.cpp
     Layout/SVGTextBox.cpp
     Layout/SVGTextPathBox.cpp
     Layout/TableFormattingContext.cpp
@@ -533,6 +534,7 @@ set(SOURCES
     Painting/SVGPathPaintable.cpp
     Painting/SVGGraphicsPaintable.cpp
     Painting/SVGMaskPaintable.cpp
+    Painting/SVGClipPaintable.cpp
     Painting/SVGPaintable.cpp
     Painting/SVGSVGPaintable.cpp
     Painting/ShadowPainting.cpp

+ 23 - 0
Userland/Libraries/LibWeb/Layout/SVGClipBox.cpp

@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2024, MacDue <macdue@dueutil.tech>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibWeb/Layout/SVGClipBox.h>
+#include <LibWeb/Painting/SVGClipPaintable.h>
+#include <LibWeb/Painting/StackingContext.h>
+
+namespace Web::Layout {
+
+SVGClipBox::SVGClipBox(DOM::Document& document, SVG::SVGClipPathElement& element, NonnullRefPtr<CSS::StyleProperties> properties)
+    : SVGBox(document, element, properties)
+{
+}
+
+JS::GCPtr<Painting::Paintable> SVGClipBox::create_paintable() const
+{
+    return Painting::SVGClipPaintable::create(*this);
+}
+
+}

+ 28 - 0
Userland/Libraries/LibWeb/Layout/SVGClipBox.h

@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2024, MacDue <macdue@dueutil.tech>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibWeb/Layout/SVGBox.h>
+#include <LibWeb/SVG/SVGClipPathElement.h>
+#include <LibWeb/SVG/SVGElement.h>
+
+namespace Web::Layout {
+
+class SVGClipBox : public SVGBox {
+    JS_CELL(SVGClipBox, SVGBox);
+
+public:
+    SVGClipBox(DOM::Document&, SVG::SVGClipPathElement&, NonnullRefPtr<CSS::StyleProperties>);
+    virtual ~SVGClipBox() override = default;
+
+    SVG::SVGClipPathElement& dom_node() { return verify_cast<SVG::SVGClipPathElement>(SVGBox::dom_node()); }
+    SVG::SVGClipPathElement const& dom_node() const { return verify_cast<SVG::SVGClipPathElement>(SVGBox::dom_node()); }
+
+    virtual JS::GCPtr<Painting::Paintable> create_paintable() const override;
+};
+
+}

+ 30 - 21
Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp

@@ -12,12 +12,14 @@
 #include <LibGfx/Font/ScaledFont.h>
 #include <LibGfx/TextLayout.h>
 #include <LibWeb/Layout/BlockFormattingContext.h>
+#include <LibWeb/Layout/SVGClipBox.h>
 #include <LibWeb/Layout/SVGFormattingContext.h>
 #include <LibWeb/Layout/SVGGeometryBox.h>
 #include <LibWeb/Layout/SVGMaskBox.h>
 #include <LibWeb/Layout/SVGSVGBox.h>
 #include <LibWeb/Layout/SVGTextBox.h>
 #include <LibWeb/Layout/SVGTextPathBox.h>
+#include <LibWeb/SVG/SVGClipPathElement.h>
 #include <LibWeb/SVG/SVGForeignObjectElement.h>
 #include <LibWeb/SVG/SVGGElement.h>
 #include <LibWeb/SVG/SVGMaskElement.h>
@@ -290,7 +292,7 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available
     }();
 
     for_each_in_subtree(box, [&](Node const& descendant) {
-        if (is<SVGMaskBox>(descendant))
+        if (is<SVGMaskBox>(descendant) || is<SVGClipBox>(descendant))
             return TraversalDecision::SkipChildrenAndContinue;
         if (is<SVG::SVGViewport>(descendant.dom_node())) {
             // Layout for a nested SVG viewport.
@@ -400,8 +402,8 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available
             for_each_in_subtree(descendant, [&](Node const& child_of_svg_container) {
                 if (!is<SVGBox>(child_of_svg_container))
                     return TraversalDecision::Continue;
-                // Masks do not change the bounding box of their parents.
-                if (is<SVGMaskBox>(child_of_svg_container))
+                // Masks/clips do not change the bounding box of their parents.
+                if (is<SVGMaskBox>(child_of_svg_container) || is<SVGClipBox>(child_of_svg_container))
                     return TraversalDecision::SkipChildrenAndContinue;
                 auto& box_state = m_state.get(static_cast<SVGBox const&>(child_of_svg_container));
                 bounding_box.add_point(box_state.offset);
@@ -420,27 +422,34 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available
         return IterationDecision::Continue;
     });
 
-    // Lay out masks last (as their parent needs to be sized first).
-    box.for_each_in_subtree_of_type<SVGMaskBox>([&](SVGMaskBox const& mask_box) {
-        auto& mask_state = m_state.get_mutable(static_cast<Box const&>(mask_box));
+    // Lay out masks/clip paths last (as their parent needs to be sized first).
+    for_each_in_subtree(box, [&](Node const& descendant) {
+        SVG::SVGUnits content_units {};
+        if (is<SVGMaskBox>(descendant))
+            content_units = static_cast<SVGMaskBox const&>(descendant).dom_node().mask_content_units();
+        else if (is<SVGClipBox>(descendant))
+            content_units = static_cast<SVGClipBox const&>(descendant).dom_node().clip_path_units();
+        else
+            return TraversalDecision::Continue;
+        // FIXME: Somehow limit <clipPath> contents to: shape elements, <text>, and <use>.
+        auto& layout_state = m_state.get_mutable(static_cast<Box const&>(descendant));
         auto parent_viewbox_transform = viewbox_transform;
-        if (mask_box.dom_node().mask_content_units() == SVG::MaskContentUnits::ObjectBoundingBox) {
-            auto* masked_node = mask_box.parent();
-            auto& masked_node_state = m_state.get(*masked_node);
-            mask_state.set_content_width(masked_node_state.content_width());
-            mask_state.set_content_height(masked_node_state.content_height());
-            parent_viewbox_transform = Gfx::AffineTransform {}.translate(masked_node_state.offset.to_type<float>());
+        if (content_units == SVG::SVGUnits::ObjectBoundingBox) {
+            auto* parent_node = descendant.parent();
+            auto& parent_node_state = m_state.get(*parent_node);
+            layout_state.set_content_width(parent_node_state.content_width());
+            layout_state.set_content_height(parent_node_state.content_height());
+            parent_viewbox_transform = Gfx::AffineTransform {}.translate(parent_node_state.offset.to_type<float>());
         } else {
-            mask_state.set_content_width(viewport_width);
-            mask_state.set_content_height(viewport_height);
+            layout_state.set_content_width(viewport_width);
+            layout_state.set_content_height(viewport_height);
         }
-        // Pretend masks are a viewport so we can scale the contents depending on the `maskContentUnits`.
-        SVGFormattingContext nested_context(m_state, static_cast<Box const&>(mask_box), this, parent_viewbox_transform);
-        mask_state.set_has_definite_width(true);
-        mask_state.set_has_definite_height(true);
-        nested_context.run(static_cast<Box const&>(mask_box), layout_mode, available_space);
-        return IterationDecision::Continue;
+        // Pretend masks/clips are a viewport so we can scale the contents depending on the `contentUnits`.
+        SVGFormattingContext nested_context(m_state, static_cast<Box const&>(descendant), this, parent_viewbox_transform);
+        layout_state.set_has_definite_width(true);
+        layout_state.set_has_definite_height(true);
+        nested_context.run(static_cast<Box const&>(descendant), layout_mode, available_space);
+        return TraversalDecision::SkipChildrenAndContinue;
     });
 }
-
 }

+ 22 - 11
Userland/Libraries/LibWeb/Layout/TreeBuilder.cpp

@@ -26,6 +26,7 @@
 #include <LibWeb/Layout/ListItemBox.h>
 #include <LibWeb/Layout/ListItemMarkerBox.h>
 #include <LibWeb/Layout/Node.h>
+#include <LibWeb/Layout/SVGClipBox.h>
 #include <LibWeb/Layout/SVGMaskBox.h>
 #include <LibWeb/Layout/TableGrid.h>
 #include <LibWeb/Layout/TableWrapper.h>
@@ -349,10 +350,15 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
         display = CSS::Display(CSS::DisplayOutside::Inline, CSS::DisplayInside::Flow);
     }
 
-    if (context.layout_svg_mask && is<SVG::SVGMaskElement>(dom_node)) {
-        layout_node = document.heap().allocate_without_realm<Layout::SVGMaskBox>(document, static_cast<SVG::SVGMaskElement&>(dom_node), *style);
-        // We're here if our parent is a use of an SVG mask, but we don't want to lay out any <mask> elements that could be a child of this mask.
-        context.layout_svg_mask = false;
+    if (context.layout_svg_mask_or_clip_path) {
+        if (is<SVG::SVGMaskElement>(dom_node))
+            layout_node = document.heap().allocate_without_realm<Layout::SVGMaskBox>(document, static_cast<SVG::SVGMaskElement&>(dom_node), *style);
+        else if (is<SVG::SVGClipPathElement>(dom_node))
+            layout_node = document.heap().allocate_without_realm<Layout::SVGClipBox>(document, static_cast<SVG::SVGClipPathElement&>(dom_node), *style);
+        else
+            VERIFY_NOT_REACHED();
+        // Only layout direct uses of SVG masks/clipPaths.
+        context.layout_svg_mask_or_clip_path = false;
     }
 
     if (!layout_node)
@@ -419,15 +425,20 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
 
     if (is<SVG::SVGGraphicsElement>(dom_node)) {
         auto& graphics_element = static_cast<SVG::SVGGraphicsElement&>(dom_node);
-        // Create the layout tree for the SVG mask as a child of the masked element. Note: This will create
-        // a new subtree for each use of the mask (so there's  not a 1-to-1 mapping from DOM node to mask
-        // layout node). Each use of a mask may be laid out differently so this duplication is necessary.
-        if (auto mask = graphics_element.mask()) {
-            TemporaryChange<bool> layout_mask(context.layout_svg_mask, true);
+        // Create the layout tree for the SVG mask/clip paths as a child of the masked element.
+        // Note: This will create a new subtree for each use of the mask (so there's  not a 1-to-1 mapping
+        // from DOM node to mask layout node). Each use of a mask may be laid out differently so this
+        // duplication is necessary.
+        auto layout_mask_or_clip_path = [&](JS::GCPtr<SVG::SVGElement const> mask_or_clip_path) {
+            TemporaryChange<bool> layout_mask(context.layout_svg_mask_or_clip_path, true);
             push_parent(verify_cast<NodeWithStyle>(*layout_node));
-            create_layout_tree(const_cast<SVG::SVGMaskElement&>(*mask), context);
+            create_layout_tree(const_cast<SVG::SVGElement&>(*mask_or_clip_path), context);
             pop_parent();
-        }
+        };
+        if (auto mask = graphics_element.mask())
+            layout_mask_or_clip_path(mask);
+        if (auto clip_path = graphics_element.clip_path())
+            layout_mask_or_clip_path(clip_path);
     }
 
     // https://html.spec.whatwg.org/multipage/rendering.html#button-layout

+ 1 - 1
Userland/Libraries/LibWeb/Layout/TreeBuilder.h

@@ -23,8 +23,8 @@ public:
 private:
     struct Context {
         bool has_svg_root = false;
-        bool layout_svg_mask = false;
         bool layout_top_layer = false;
+        bool layout_svg_mask_or_clip_path = false;
     };
 
     i32 calculate_list_item_index(DOM::Node&);

+ 21 - 0
Userland/Libraries/LibWeb/Painting/SVGClipPaintable.cpp

@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2024, MacDue <macdue@dueutil.tech>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibWeb/Painting/SVGClipPaintable.h>
+
+namespace Web::Painting {
+
+JS::NonnullGCPtr<SVGClipPaintable> SVGClipPaintable::create(Layout::SVGClipBox const& layout_box)
+{
+    return layout_box.heap().allocate_without_realm<SVGClipPaintable>(layout_box);
+}
+
+SVGClipPaintable::SVGClipPaintable(Layout::SVGClipBox const& layout_box)
+    : SVGPaintable(layout_box)
+{
+}
+
+}

+ 29 - 0
Userland/Libraries/LibWeb/Painting/SVGClipPaintable.h

@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2024, MacDue <macdue@dueutil.tech>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibWeb/Layout/SVGClipBox.h>
+#include <LibWeb/Painting/SVGPaintable.h>
+
+namespace Web::Painting {
+
+class SVGClipPaintable : public SVGPaintable {
+    JS_CELL(SVGClipPaintable, SVGPaintable);
+
+public:
+    static JS::NonnullGCPtr<SVGClipPaintable> create(Layout::SVGClipBox const&);
+
+    bool forms_unconnected_subtree() const override
+    {
+        return true;
+    }
+
+protected:
+    SVGClipPaintable(Layout::SVGClipBox const&);
+};
+
+}

+ 1 - 0
Userland/Libraries/LibWeb/SVG/AttributeParser.h

@@ -97,6 +97,7 @@ enum class SVGUnits {
 using GradientUnits = SVGUnits;
 using MaskUnits = SVGUnits;
 using MaskContentUnits = SVGUnits;
+using ClipPathUnits = SVGUnits;
 
 enum class SpreadMethod {
     Pad,

+ 9 - 0
Userland/Libraries/LibWeb/SVG/SVGClipPathElement.cpp

@@ -5,6 +5,7 @@
  */
 
 #include <LibWeb/Bindings/Intrinsics.h>
+#include <LibWeb/SVG/AttributeNames.h>
 #include <LibWeb/SVG/SVGClipPathElement.h>
 
 namespace Web::SVG {
@@ -26,8 +27,16 @@ void SVGClipPathElement::initialize(JS::Realm& realm)
     WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGClipPathElement);
 }
 
+void SVGClipPathElement::attribute_changed(FlyString const& name, Optional<String> const& value)
+{
+    SVGElement::attribute_changed(name, value);
+    if (name == AttributeNames::clipPathUnits)
+        m_clip_path_units = AttributeParser::parse_units(value.value_or(String {}));
+}
+
 JS::GCPtr<Layout::Node> SVGClipPathElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties>)
 {
+    // Clip paths are handled as a special case in the TreeBuilder.
     return nullptr;
 }
 

+ 26 - 2
Userland/Libraries/LibWeb/SVG/SVGClipPathElement.h

@@ -6,23 +6,47 @@
 
 #pragma once
 
+#include <LibWeb/SVG/AttributeParser.h>
 #include <LibWeb/SVG/SVGElement.h>
+#include <LibWeb/SVG/SVGViewport.h>
 
 namespace Web::SVG {
 
-class SVGClipPathElement final : public SVGElement {
+class SVGClipPathElement final : public SVGElement
+    , public SVGViewport {
     WEB_PLATFORM_OBJECT(SVGClipPathElement, SVGElement);
     JS_DECLARE_ALLOCATOR(SVGClipPathElement);
 
 public:
     virtual ~SVGClipPathElement();
 
+    virtual Optional<ViewBox> view_box() const override
+    {
+        // Same trick as SVGMaskElement.
+        if (clip_path_units() == MaskContentUnits::ObjectBoundingBox)
+            return ViewBox { 0, 0, 1, 1 };
+        return {};
+    }
+
+    virtual Optional<PreserveAspectRatio> preserve_aspect_ratio() const override
+    {
+        return PreserveAspectRatio { PreserveAspectRatio::Align::None, {} };
+    }
+
+    virtual void attribute_changed(FlyString const& name, Optional<String> const& value) override;
+
+    ClipPathUnits clip_path_units() const
+    {
+        return m_clip_path_units.value_or(ClipPathUnits::UserSpaceOnUse);
+    }
+
     virtual JS::GCPtr<Layout::Node> create_layout_node(NonnullRefPtr<CSS::StyleProperties>) override;
 
 private:
     SVGClipPathElement(DOM::Document&, DOM::QualifiedName);
-
     virtual void initialize(JS::Realm&) override;
+
+    Optional<ClipPathUnits> m_clip_path_units = {};
 };
 
 }