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 00000000000..f398260af50 --- /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 333942cc6b8..b4ad41f15f3 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 00000000000..b7d1fea9401 --- /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 00000000000..d77af229a59 --- /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 00000000000..0c17ab9c59f --- /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 74cd4ee329b..ebce36ed4bd 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 654591c6d52..599b5d6d4c6 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 97559736f26..98daf7ff112 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 28ce394b415..f2b278cf1a5 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 89ebe14b2da..441adeacad8 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 f21fa1dd582..d8d68264da7 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 f42b2a00beb..74b2de1e5a9 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 00000000000..5ae4aa5837f --- /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; +}; + +}