From 5cf1570f408ab9769c85b2f48722304ca8c49295 Mon Sep 17 00:00:00 2001 From: MacDue Date: Sat, 27 Jan 2024 16:23:22 +0000 Subject: [PATCH] LibWeb: Add initial support for nesting SVG viewports Previously, we were handling viewBoxes/viewports in a slightly hacky way, asking graphics elements to figure out what viewBox to use during layout. This does not work in all cases, and can't allow for more complex SVGs where it is possible to have nested viewports. This commit makes the SVGFormattingContext keep track of the viewport/boxes, and it now lays out each viewport recursively, where each nested `` or `` can establish a new viewport. This fixes some previous edge cases, and starts to allow nested viewports (there's still some issues to resolve there). Fixes #22931 --- .../Layout/expected/svg/nested-viewports.txt | 26 +++ .../expected/svg/svg-symbol-with-viewbox.txt | 4 +- .../expected/svg/use-honor-outer-viewBox.txt | 21 +++ .../Layout/input/svg/nested-viewports.html | 8 + .../input/svg/use-honor-outer-viewBox.html | 12 ++ .../LibWeb/Layout/SVGFormattingContext.cpp | 161 +++++++++++++----- .../LibWeb/Layout/SVGFormattingContext.h | 5 +- .../LibWeb/Painting/SVGPathPaintable.cpp | 2 +- .../LibWeb/SVG/SVGGraphicsElement.cpp | 15 -- .../Libraries/LibWeb/SVG/SVGGraphicsElement.h | 2 - Userland/Libraries/LibWeb/SVG/SVGSVGElement.h | 8 +- .../Libraries/LibWeb/SVG/SVGSymbolElement.h | 11 +- Userland/Libraries/LibWeb/SVG/SVGViewport.h | 21 +++ 13 files changed, 229 insertions(+), 67 deletions(-) create mode 100644 Tests/LibWeb/Layout/expected/svg/nested-viewports.txt create mode 100644 Tests/LibWeb/Layout/expected/svg/use-honor-outer-viewBox.txt create mode 100644 Tests/LibWeb/Layout/input/svg/nested-viewports.html create mode 100644 Tests/LibWeb/Layout/input/svg/use-honor-outer-viewBox.html create mode 100644 Userland/Libraries/LibWeb/SVG/SVGViewport.h diff --git a/Tests/LibWeb/Layout/expected/svg/nested-viewports.txt b/Tests/LibWeb/Layout/expected/svg/nested-viewports.txt new file mode 100644 index 0000000000..f398260af5 --- /dev/null +++ b/Tests/LibWeb/Layout/expected/svg/nested-viewports.txt @@ -0,0 +1,26 @@ +Viewport <#document> at (0,0) content-size 800x600 children: not-inline + BlockContainer at (0,0) content-size 800x600 [BFC] children: not-inline + BlockContainer at (8,8) content-size 784x200 children: inline + frag 0 from SVGSVGBox start: 0, length: 0, rect: [8,8 200x200] baseline: 200 + SVGSVGBox at (8,8) content-size 200x200 [SVG] children: inline + TextNode <#text> + SVGSVGBox at (8,8) content-size 200x200 [SVG] children: inline + TextNode <#text> + SVGSVGBox at (8,8) content-size 266.671875x266.671875 [SVG] children: inline + TextNode <#text> + SVGGeometryBox at (34.671875,34.671875) content-size 266.671875x266.671875 children: not-inline + TextNode <#text> + SVGGeometryBox at (34.671875,34.671875) content-size 133.328125x133.328125 children: not-inline + TextNode <#text> + TextNode <#text> + TextNode <#text> + TextNode <#text> + +ViewportPaintable (Viewport<#document>) [0,0 800x600] + PaintableWithLines (BlockContainer) [0,0 800x600] + PaintableWithLines (BlockContainer) [8,8 784x200] + SVGSVGPaintable (SVGSVGBox#a) [8,8 200x200] + SVGSVGPaintable (SVGSVGBox#b) [8,8 200x200] + SVGSVGPaintable (SVGSVGBox#c) [8,8 266.671875x266.671875] + SVGPathPaintable (SVGGeometryBox) [34.671875,34.671875 266.671875x266.671875] + SVGPathPaintable (SVGGeometryBox) [34.671875,34.671875 133.328125x133.328125] diff --git a/Tests/LibWeb/Layout/expected/svg/svg-symbol-with-viewbox.txt b/Tests/LibWeb/Layout/expected/svg/svg-symbol-with-viewbox.txt index 333942cc6b..b4ad41f15f 100644 --- a/Tests/LibWeb/Layout/expected/svg/svg-symbol-with-viewbox.txt +++ b/Tests/LibWeb/Layout/expected/svg/svg-symbol-with-viewbox.txt @@ -9,7 +9,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline TextNode <#text> SVGSVGBox at (8,8) content-size 300x150 [SVG] children: inline TextNode <#text> - Box at (8,8) content-size 215.625x130.90625 children: inline + Box at (8,8) content-size 300x150 children: inline Box at (92.375,26.75) content-size 131.25x112.15625 [BFC] children: inline TextNode <#text> SVGGeometryBox at (92.375,26.75) content-size 131.25x112.15625 children: inline @@ -24,6 +24,6 @@ ViewportPaintable (Viewport<#document>) [0,0 800x600] PaintableWithLines (BlockContainer(anonymous)) [8,8 784x0] PaintableWithLines (BlockContainer
) [8,8 784x150] SVGSVGPaintable (SVGSVGBox) [8,8 300x150] - PaintableBox (Box) [8,8 215.625x130.90625] + PaintableBox (Box) [8,8 300x150] PaintableBox (Box#braces) [92.375,26.75 131.25x112.15625] SVGPathPaintable (SVGGeometryBox) [92.375,26.75 131.25x112.15625] diff --git a/Tests/LibWeb/Layout/expected/svg/use-honor-outer-viewBox.txt b/Tests/LibWeb/Layout/expected/svg/use-honor-outer-viewBox.txt new file mode 100644 index 0000000000..b7d1fea940 --- /dev/null +++ b/Tests/LibWeb/Layout/expected/svg/use-honor-outer-viewBox.txt @@ -0,0 +1,21 @@ +Viewport <#document> at (0,0) content-size 800x600 children: not-inline + BlockContainer at (0,0) content-size 800x118 [BFC] children: not-inline + BlockContainer at (8,8) content-size 784x102 children: inline + frag 0 from SVGSVGBox start: 0, length: 0, rect: [9,9 100x100] baseline: 102 + SVGSVGBox at (9,9) content-size 100x100 [SVG] children: inline + TextNode <#text> + Box at (9,9) content-size 100x100 children: inline + SVGSVGBox at (9,9) content-size 100x100 [SVG] children: inline + TextNode <#text> + SVGGeometryBox at (9,9) content-size 50x50 children: inline + TextNode <#text> + TextNode <#text> + TextNode <#text> + +ViewportPaintable (Viewport<#document>) [0,0 800x600] + PaintableWithLines (BlockContainer) [0,0 800x118] + PaintableWithLines (BlockContainer) [8,8 784x102] + SVGSVGPaintable (SVGSVGBox#outer) [8,8 102x102] + PaintableBox (Box) [9,9 100x100] + SVGSVGPaintable (SVGSVGBox#whee) [9,9 100x100] + SVGPathPaintable (SVGGeometryBox) [9,9 50x50] diff --git a/Tests/LibWeb/Layout/input/svg/nested-viewports.html b/Tests/LibWeb/Layout/input/svg/nested-viewports.html new file mode 100644 index 0000000000..d77af229a5 --- /dev/null +++ b/Tests/LibWeb/Layout/input/svg/nested-viewports.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Tests/LibWeb/Layout/input/svg/use-honor-outer-viewBox.html b/Tests/LibWeb/Layout/input/svg/use-honor-outer-viewBox.html new file mode 100644 index 0000000000..0c17ab9c59 --- /dev/null +++ b/Tests/LibWeb/Layout/input/svg/use-honor-outer-viewBox.html @@ -0,0 +1,12 @@ + + + + + +
+ + + diff --git a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp index 74cd4ee329..ebce36ed4b 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp +++ b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp @@ -26,8 +26,9 @@ namespace Web::Layout { -SVGFormattingContext::SVGFormattingContext(LayoutState& state, Box const& box, FormattingContext* parent) +SVGFormattingContext::SVGFormattingContext(LayoutState& state, Box const& box, FormattingContext* parent, Gfx::AffineTransform parent_viewbox_transform) : FormattingContext(Type::SVG, state, box, parent) + , m_parent_viewbox_transform(parent_viewbox_transform) { } @@ -156,27 +157,56 @@ static bool is_container_element(Node const& node) return false; } +enum class TraversalDecision { + Continue, + SkipChildrenAndContinue, + Break, +}; + +// FIXME: Add TraversalDecision::SkipChildrenAndContinue to TreeNode's implementation. +template +static TraversalDecision for_each_in_inclusive_subtree(Layout::Node const& node, Callback callback) +{ + if (auto decision = callback(node); decision != TraversalDecision::Continue) + return decision; + for (auto* child = node.first_child(); child; child = child->next_sibling()) { + if (for_each_in_inclusive_subtree(*child, callback) == TraversalDecision::Break) + return TraversalDecision::Break; + } + return TraversalDecision::Continue; +} + +// FIXME: Add TraversalDecision::SkipChildrenAndContinue to TreeNode's implementation. +template +static TraversalDecision for_each_in_subtree(Layout::Node const& node, Callback callback) +{ + for (auto* child = node.first_child(); child; child = child->next_sibling()) { + if (for_each_in_inclusive_subtree(*child, callback) == TraversalDecision::Break) + return TraversalDecision::Break; + } + return TraversalDecision::Continue; +} + void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, AvailableSpace const& available_space) { - auto& svg_svg_element = verify_cast(*box.dom_node()); + auto& svg_viewport = dynamic_cast(*box.dom_node()); + auto& svg_box_state = m_state.get_mutable(box); - auto svg_box_state = m_state.get(box); - auto root_offset = svg_box_state.offset; - - box.for_each_child_of_type([&](BlockContainer const& child_box) { - if (is(child_box.dom_node())) { - Layout::BlockFormattingContext bfc(m_state, child_box, this); - bfc.run(child_box, LayoutMode::Normal, available_space); - - auto& child_state = m_state.get_mutable(child_box); - child_state.set_content_offset(child_state.offset.translated(root_offset)); + auto viewbox = svg_viewport.view_box(); + // https://svgwg.org/svg2-draft/coords.html#ViewBoxAttribute + if (viewbox.has_value()) { + if (viewbox->width < 0 || viewbox->height < 0) { + // A negative value for or is an error and invalidates the ‘viewBox’ attribute. + viewbox = {}; + } else if (viewbox->width == 0 || viewbox->height == 0) { + // A value of zero disables rendering of the element. + return; } - return IterationDecision::Continue; - }); + } - auto compute_viewbox_transform = [&](auto const& viewbox) -> Gfx::AffineTransform { + auto viewbox_transform = [&] { if (!viewbox.has_value()) - return {}; + return m_parent_viewbox_transform; // FIXME: This should allow just one of width or height to be specified. // E.g. We should be able to layout where height is unspecified/auto. @@ -188,34 +218,87 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available auto scale_height = svg_box_state.has_definite_height() ? svg_box_state.content_height() / viewbox->height : 1; // The initial value for preserveAspectRatio is xMidYMid meet. - auto preserve_aspect_ratio = svg_svg_element.preserve_aspect_ratio().value_or(SVG::PreserveAspectRatio {}); + auto preserve_aspect_ratio = svg_viewport.preserve_aspect_ratio().value_or(SVG::PreserveAspectRatio {}); auto viewbox_offset_and_scale = scale_and_align_viewbox_content(preserve_aspect_ratio, *viewbox, { scale_width, scale_height }, svg_box_state); CSSPixelPoint offset = viewbox_offset_and_scale.offset; - return Gfx::AffineTransform {}.translate(offset.to_type()).scale(viewbox_offset_and_scale.scale_factor, viewbox_offset_and_scale.scale_factor).translate({ -viewbox->min_x, -viewbox->min_y }); - }; + return Gfx::AffineTransform { m_parent_viewbox_transform }.multiply(Gfx::AffineTransform {} + .translate(offset.to_type()) + .scale(viewbox_offset_and_scale.scale_factor, viewbox_offset_and_scale.scale_factor) + .translate({ -viewbox->min_x, -viewbox->min_y })); + }(); - box.for_each_in_subtree([&](Node const& descendant) { + if (svg_box_state.has_definite_width() && svg_box_state.has_definite_height()) { + // Scale the box of the viewport based on the parent's viewBox transform. + // The viewBox transform is always just a simple scale + offset. + // FIXME: Avoid converting SVG box to floats. + Gfx::FloatRect svg_rect = { svg_box_state.offset.to_type(), + { float(svg_box_state.content_width()), float(svg_box_state.content_height()) } }; + svg_rect = m_parent_viewbox_transform.map(svg_rect); + svg_box_state.set_content_offset(svg_rect.location().to_type()); + svg_box_state.set_content_width(CSSPixels(svg_rect.width())); + svg_box_state.set_content_height(CSSPixels(svg_rect.height())); + } + + auto root_offset = svg_box_state.offset; + box.for_each_child_of_type([&](BlockContainer const& child_box) { + if (is(child_box.dom_node())) { + Layout::BlockFormattingContext bfc(m_state, child_box, this); + bfc.run(child_box, LayoutMode::Normal, available_space); + + auto& child_state = m_state.get_mutable(child_box); + child_state.set_content_offset(child_state.offset.translated(root_offset)); + } + return IterationDecision::Continue; + }); + + for_each_in_subtree(box, [&](Node const& descendant) { + if (is(descendant.dom_node())) { + // Layout for a nested SVG viewport. + // https://svgwg.org/svg2-draft/coords.html#EstablishingANewSVGViewport. + SVGFormattingContext nested_context(m_state, static_cast(descendant), this, viewbox_transform); + auto& nested_viewport_state = m_state.get_mutable(static_cast(descendant)); + + auto viewport_width = [&] { + if (viewbox.has_value()) + return CSSPixels::nearest_value_for(viewbox->width); + if (svg_box_state.has_definite_width()) + return svg_box_state.content_width(); + dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Failed to resolve width of SVG viewport!"); + return CSSPixels {}; + }(); + + auto viewport_height = [&] { + if (viewbox.has_value()) + return CSSPixels::nearest_value_for(viewbox->height); + if (svg_box_state.has_definite_height()) + return svg_box_state.content_height(); + dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Failed to resolve height of SVG viewport!"); + return CSSPixels {}; + }(); + + auto resolve_dimension = [](auto& node, auto size, auto reference_value) { + // The value auto for width and height on the ‘svg’ element is treated as 100%. + // https://svgwg.org/svg2-draft/geometry.html#Sizing + if (size.is_auto()) + return reference_value; + return size.to_px(node, reference_value); + }; + + // FIXME: Support the x/y attributes to calculate the offset. + auto nested_viewport_width = resolve_dimension(descendant, descendant.computed_values().width(), viewport_width); + auto nested_viewport_height = resolve_dimension(descendant, descendant.computed_values().height(), viewport_height); + nested_viewport_state.set_content_width(nested_viewport_width); + nested_viewport_state.set_content_height(nested_viewport_height); + nested_context.run(static_cast(descendant), layout_mode, available_space); + return TraversalDecision::SkipChildrenAndContinue; + } if (is(descendant)) { auto const& graphics_box = static_cast(descendant); auto& dom_node = const_cast(graphics_box).dom_node(); - auto viewbox = dom_node.view_box(); - - // https://svgwg.org/svg2-draft/coords.html#ViewBoxAttribute - if (viewbox.has_value()) { - if (viewbox->width < 0 || viewbox->height < 0) { - // A negative value for or is an error and invalidates the ‘viewBox’ attribute. - viewbox = {}; - } else if (viewbox->width == 0 || viewbox->height == 0) { - // A value of zero disables rendering of the element. - return IterationDecision::Continue; - } - } - auto& graphics_box_state = m_state.get_mutable(graphics_box); - auto svg_transform = dom_node.get_transform(); - Gfx::AffineTransform viewbox_transform = compute_viewbox_transform(viewbox); + auto svg_transform = dom_node.get_transform(); graphics_box_state.set_computed_svg_transforms(Painting::SVGGraphicsPaintable::ComputedTransforms(viewbox_transform, svg_transform)); auto to_css_pixels_transform = Gfx::AffineTransform {}.multiply(viewbox_transform).multiply(svg_transform); @@ -260,7 +343,7 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available auto& text_path_element = static_cast(dom_node); auto path_or_shape = text_path_element.path_or_shape(); if (!path_or_shape) - return IterationDecision::Continue; + return TraversalDecision::Continue; auto& font = graphics_box.first_available_font(); auto text_contents = text_path_element.text_contents(); @@ -278,12 +361,8 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available graphics_box_state.set_content_width(path_bounding_box.width()); graphics_box_state.set_content_height(path_bounding_box.height()); graphics_box_state.set_computed_svg_path(move(path)); - } else if (is(descendant)) { - SVGFormattingContext nested_context(m_state, static_cast(descendant), this); - nested_context.run(static_cast(descendant), layout_mode, available_space); } - - return IterationDecision::Continue; + return TraversalDecision::Continue; }); // https://svgwg.org/svg2-draft/struct.html#Groups diff --git a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.h b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.h index 654591c6d5..599b5d6d4c 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.h +++ b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.h @@ -13,12 +13,15 @@ namespace Web::Layout { class SVGFormattingContext : public FormattingContext { public: - explicit SVGFormattingContext(LayoutState&, Box const&, FormattingContext* parent); + explicit SVGFormattingContext(LayoutState&, Box const&, FormattingContext* parent, Gfx::AffineTransform parent_viewbox_transform = {}); ~SVGFormattingContext(); virtual void run(Box const&, LayoutMode, AvailableSpace const&) override; virtual CSSPixels automatic_content_width() const override; virtual CSSPixels automatic_content_height() const override; + +private: + Gfx::AffineTransform m_parent_viewbox_transform {}; }; } diff --git a/Userland/Libraries/LibWeb/Painting/SVGPathPaintable.cpp b/Userland/Libraries/LibWeb/Painting/SVGPathPaintable.cpp index 97559736f2..98daf7ff11 100644 --- a/Userland/Libraries/LibWeb/Painting/SVGPathPaintable.cpp +++ b/Userland/Libraries/LibWeb/Painting/SVGPathPaintable.cpp @@ -68,7 +68,7 @@ void SVGPathPaintable::paint(PaintContext& context, PaintPhase phase) const RecordingPainterStateSaver save_painter { context.recording_painter() }; auto offset = context.floored_device_point(svg_element_rect.location()).to_type().to_type(); - auto maybe_view_box = geometry_element.view_box(); + auto maybe_view_box = svg_element->view_box(); auto paint_transform = computed_transforms().svg_to_device_pixels_transform(context); Gfx::Path path = computed_path()->copy_transformed(paint_transform); diff --git a/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp b/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp index 28ce394b41..f2b278cf1a 100644 --- a/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp +++ b/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.cpp @@ -232,19 +232,4 @@ Optional SVGGraphicsElement::stroke_width() const return width.to_px(*layout_node(), scaled_viewport_size).to_double(); } -Optional SVGGraphicsElement::view_box() const -{ - if (auto* svg_svg_element = shadow_including_first_ancestor_of_type()) { - if (svg_svg_element->view_box().has_value()) - return svg_svg_element->view_box(); - } - - if (auto* svg_symbol_element = shadow_including_first_ancestor_of_type()) { - if (svg_symbol_element->view_box().has_value()) - return svg_symbol_element->view_box(); - } - - return {}; -} - } diff --git a/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.h b/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.h index 89ebe14b2d..441adeacad 100644 --- a/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.h +++ b/Userland/Libraries/LibWeb/SVG/SVGGraphicsElement.h @@ -47,8 +47,6 @@ public: JS::GCPtr mask() const; - Optional view_box() const; - protected: SVGGraphicsElement(DOM::Document&, DOM::QualifiedName); diff --git a/Userland/Libraries/LibWeb/SVG/SVGSVGElement.h b/Userland/Libraries/LibWeb/SVG/SVGSVGElement.h index f21fa1dd58..d8d68264da 100644 --- a/Userland/Libraries/LibWeb/SVG/SVGSVGElement.h +++ b/Userland/Libraries/LibWeb/SVG/SVGSVGElement.h @@ -9,11 +9,13 @@ #include #include #include +#include #include namespace Web::SVG { -class SVGSVGElement final : public SVGGraphicsElement { +class SVGSVGElement final : public SVGGraphicsElement + , public SVGViewport { WEB_PLATFORM_OBJECT(SVGSVGElement, SVGGraphicsElement); JS_DECLARE_ALLOCATOR(SVGSVGElement); @@ -25,8 +27,8 @@ public: virtual bool requires_svg_container() const override { return false; } virtual bool is_svg_container() const override { return true; } - [[nodiscard]] Optional view_box() const; - Optional const& preserve_aspect_ratio() const { return m_preserve_aspect_ratio; } + virtual Optional view_box() const override; + virtual Optional preserve_aspect_ratio() const override { return m_preserve_aspect_ratio; } void set_fallback_view_box_for_svg_as_image(Optional); diff --git a/Userland/Libraries/LibWeb/SVG/SVGSymbolElement.h b/Userland/Libraries/LibWeb/SVG/SVGSymbolElement.h index f42b2a00be..74b2de1e5a 100644 --- a/Userland/Libraries/LibWeb/SVG/SVGSymbolElement.h +++ b/Userland/Libraries/LibWeb/SVG/SVGSymbolElement.h @@ -7,10 +7,12 @@ #pragma once #include +#include namespace Web::SVG { -class SVGSymbolElement final : public SVGGraphicsElement { +class SVGSymbolElement final : public SVGGraphicsElement + , public SVGViewport { WEB_PLATFORM_OBJECT(SVGSymbolElement, SVGGraphicsElement); JS_DECLARE_ALLOCATOR(SVGSymbolElement); @@ -19,7 +21,12 @@ public: void apply_presentational_hints(CSS::StyleProperties& style) const override; - Optional view_box() const { return m_view_box; } + virtual Optional view_box() const override { return m_view_box; } + virtual Optional preserve_aspect_ratio() const override + { + // FIXME: Support the `preserveAspectRatio` attribute on . + return {}; + } private: SVGSymbolElement(DOM::Document&, DOM::QualifiedName); diff --git a/Userland/Libraries/LibWeb/SVG/SVGViewport.h b/Userland/Libraries/LibWeb/SVG/SVGViewport.h new file mode 100644 index 0000000000..5ae4aa5837 --- /dev/null +++ b/Userland/Libraries/LibWeb/SVG/SVGViewport.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024, MacDue + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::SVG { + +class SVGViewport { +public: + virtual Optional view_box() const = 0; + virtual Optional preserve_aspect_ratio() const = 0; + virtual ~SVGViewport() = default; +}; + +}