From d7b77d7695b7288a628d342940529aab243e6746 Mon Sep 17 00:00:00 2001 From: MacDue Date: Sun, 7 Apr 2024 20:47:13 +0100 Subject: [PATCH] LibWeb: Split `SVGFormattingContext` up into functions Doing multiple `for_each_in_subtree()` passes was kind of a hack. We can resolve everything in a single pass with a little more control over the layout process. This also fixes a few minor issues like the sizing of nested `` elements. More work is needed here though as this is still fairly ad-hoc. Note: This does regress `css-namespace-tag-name-selector.html`, previously SVG text within `` elements would appear. However, this was only because `for_each_in_subtree()` would blindly look through the InlineNodes from the unimplemented `SVGAElement`s. --- .../css-namespace-tag-name-selector.txt | 2 +- .../Layout/expected/svg/svg-g-inside-g.txt | 24 + .../expected/svg/svg-symbol-with-viewbox.txt | 4 +- .../expected/svg/use-honor-outer-viewBox.txt | 4 +- .../Layout/input/svg/svg-g-inside-g.html | 8 + .../LibWeb/Layout/SVGFormattingContext.cpp | 431 +++++++++--------- .../LibWeb/Layout/SVGFormattingContext.h | 19 + 7 files changed, 274 insertions(+), 218 deletions(-) create mode 100644 Tests/LibWeb/Layout/expected/svg/svg-g-inside-g.txt create mode 100644 Tests/LibWeb/Layout/input/svg/svg-g-inside-g.html diff --git a/Tests/LibWeb/Layout/expected/css-namespace-tag-name-selector.txt b/Tests/LibWeb/Layout/expected/css-namespace-tag-name-selector.txt index 1665c2df26b..f8799eb24b4 100644 --- a/Tests/LibWeb/Layout/expected/css-namespace-tag-name-selector.txt +++ b/Tests/LibWeb/Layout/expected/css-namespace-tag-name-selector.txt @@ -8,7 +8,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline frag 2 from BlockContainer start: 0, length: 0, rect: [319,51 0x108] baseline: 110 SVGSVGBox at (9,9) content-size 300x150 [SVG] children: inline InlineNode - SVGTextBox at (29,25.015625) content-size 193.59375x67.578125 children: inline + SVGTextBox (not painted) children: inline TextNode <#text> TextNode <#text> BlockContainer at (319,51) content-size 0x108 children: not-inline diff --git a/Tests/LibWeb/Layout/expected/svg/svg-g-inside-g.txt b/Tests/LibWeb/Layout/expected/svg/svg-g-inside-g.txt new file mode 100644 index 00000000000..ef56b5c302d --- /dev/null +++ b/Tests/LibWeb/Layout/expected/svg/svg-g-inside-g.txt @@ -0,0 +1,24 @@ +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 784x100 children: inline + frag 0 from SVGSVGBox start: 0, length: 0, rect: [8,8 100x100] baseline: 100 + SVGSVGBox at (8,8) content-size 100x100 [SVG] children: inline + TextNode <#text> + TextNode <#text> + SVGGraphicsBox at (58,58) content-size 10x10 children: inline + TextNode <#text> + SVGGraphicsBox at (58,58) content-size 10x10 children: inline + TextNode <#text> + SVGGeometryBox at (58,58) content-size 10x10 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 784x100] + SVGSVGPaintable (SVGSVGBox) [8,8 100x100] + SVGGraphicsPaintable (SVGGraphicsBox) [58,58 10x10] + SVGGraphicsPaintable (SVGGraphicsBox) [58,58 10x10] + SVGPathPaintable (SVGGeometryBox) [58,58 10x10] 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 2f0eecf5333..6440536f8ff 100644 --- a/Tests/LibWeb/Layout/expected/svg/svg-symbol-with-viewbox.txt +++ b/Tests/LibWeb/Layout/expected/svg/svg-symbol-with-viewbox.txt @@ -10,7 +10,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline SVGSVGBox at (8,8) content-size 300x150 [SVG] children: inline TextNode <#text> SVGGraphicsBox at (8,8) content-size 300x150 children: not-inline - SVGGraphicsBox at (92.375,26.75) content-size 131.25x112.15625 [BFC] children: inline + SVGGraphicsBox at (8,8) content-size 300x150 [BFC] children: inline TextNode <#text> SVGGeometryBox at (92.375,26.75) content-size 131.25x112.15625 children: inline TextNode <#text> @@ -25,5 +25,5 @@ ViewportPaintable (Viewport<#document>) [0,0 800x600] PaintableWithLines (BlockContainer
) [8,8 784x150] SVGSVGPaintable (SVGSVGBox) [8,8 300x150] SVGGraphicsPaintable (SVGGraphicsBox) [8,8 300x150] - SVGGraphicsPaintable (SVGGraphicsBox#braces) [92.375,26.75 131.25x112.15625] + SVGGraphicsPaintable (SVGGraphicsBox#braces) [8,8 300x150] 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 index 7f3143fdd33..8aee3828e86 100644 --- a/Tests/LibWeb/Layout/expected/svg/use-honor-outer-viewBox.txt +++ b/Tests/LibWeb/Layout/expected/svg/use-honor-outer-viewBox.txt @@ -4,7 +4,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-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> - SVGGraphicsBox at (9,9) content-size 50x50 children: inline + SVGGraphicsBox 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 @@ -16,6 +16,6 @@ ViewportPaintable (Viewport<#document>) [0,0 800x600] PaintableWithLines (BlockContainer) [0,0 800x118] PaintableWithLines (BlockContainer) [8,8 784x102] SVGSVGPaintable (SVGSVGBox#outer) [8,8 102x102] - SVGGraphicsPaintable (SVGGraphicsBox) [9,9 50x50] + SVGGraphicsPaintable (SVGGraphicsBox) [9,9 100x100] SVGSVGPaintable (SVGSVGBox#whee) [9,9 100x100] SVGPathPaintable (SVGGeometryBox) [9,9 50x50] diff --git a/Tests/LibWeb/Layout/input/svg/svg-g-inside-g.html b/Tests/LibWeb/Layout/input/svg/svg-g-inside-g.html new file mode 100644 index 00000000000..a012d877251 --- /dev/null +++ b/Tests/LibWeb/Layout/input/svg/svg-g-inside-g.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp index 95574155356..9fdb80086ce 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp +++ b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp @@ -16,9 +16,6 @@ #include #include #include -#include -#include -#include #include #include #include @@ -168,37 +165,7 @@ 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) +void SVGFormattingContext::run(Box const& box, LayoutMode, AvailableSpace const& available_space) { // NOTE: SVG doesn't have a "formatting context" in the spec, but this is the most // obvious way to drive SVG layout in our engine at the moment. @@ -223,10 +190,8 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available } } - auto viewbox_transform = [&] { - if (!viewbox.has_value()) - return m_parent_viewbox_transform; - + m_current_viewbox_transform = m_parent_viewbox_transform; + if (viewbox.has_value()) { // 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. if (!svg_box_state.has_definite_width() || !svg_box_state.has_definite_height()) { @@ -241,11 +206,11 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available 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 { m_parent_viewbox_transform }.multiply(Gfx::AffineTransform {} - .translate(offset.to_type()) - .scale(viewbox_offset_and_scale.scale_factor_x, viewbox_offset_and_scale.scale_factor_y) - .translate({ -viewbox->min_x, -viewbox->min_y })); - }(); + m_current_viewbox_transform = Gfx::AffineTransform { m_current_viewbox_transform }.multiply(Gfx::AffineTransform {} + .translate(offset.to_type()) + .scale(viewbox_offset_and_scale.scale_factor_x, viewbox_offset_and_scale.scale_factor_y) + .translate({ -viewbox->min_x, -viewbox->min_y })); + } 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. @@ -261,18 +226,6 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available svg_box_state.set_has_definite_height(true); } - 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; - }); - auto viewport_width = [&] { if (viewbox.has_value()) return CSSPixels::nearest_value_for(viewbox->width); @@ -291,165 +244,217 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available return CSSPixels {}; }(); - for_each_in_subtree(box, [&](Node const& descendant) { - if (is(descendant) || is(descendant)) - return TraversalDecision::SkipChildrenAndContinue; - 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 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); - }; + m_available_space = available_space; + m_svg_offset = svg_box_state.offset; + m_viewport_size = { viewport_width, viewport_height }; - auto nested_viewport_x = descendant.computed_values().x().to_px(descendant, viewport_width); - auto nested_viewport_y = descendant.computed_values().y().to_px(descendant, viewport_height); - 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_offset({ nested_viewport_x, nested_viewport_y }); - nested_viewport_state.set_content_width(nested_viewport_width); - nested_viewport_state.set_content_height(nested_viewport_height); - nested_viewport_state.set_has_definite_width(true); - nested_viewport_state.set_has_definite_height(true); - 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& graphics_box_state = m_state.get_mutable(graphics_box); - - 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); - - Gfx::Path path; - if (is(descendant)) { - path = static_cast(dom_node).get_path({ viewport_width, viewport_height }); - } else if (is(descendant)) { - auto& text_element = static_cast(dom_node); - - auto& font = graphics_box.first_available_font(); - auto text_contents = text_element.text_contents(); - Utf8View text_utf8 { text_contents }; - auto text_width = font.width(text_utf8); - - auto text_offset = text_element.get_offset(); - // https://svgwg.org/svg2-draft/text.html#TextAnchoringProperties - switch (text_element.text_anchor().value_or(SVG::TextAnchor::Start)) { - case SVG::TextAnchor::Start: - // The rendered characters are aligned such that the start of the resulting rendered text is at the initial - // current text position. - break; - case SVG::TextAnchor::Middle: { - // The rendered characters are shifted such that the geometric middle of the resulting rendered text - // (determined from the initial and final current text position before applying the text-anchor property) - // is at the initial current text position. - text_offset.translate_by(-text_width / 2, 0); - break; - } - case SVG::TextAnchor::End: { - // The rendered characters are shifted such that the end of the resulting rendered text (final current text - // position before applying the text-anchor property) is at the initial current text position. - text_offset.translate_by(-text_width, 0); - break; - } - default: - VERIFY_NOT_REACHED(); - } - - path.move_to(text_offset); - path.text(text_utf8, font); - } else if (is(descendant)) { - auto& text_path_element = static_cast(dom_node); - auto path_or_shape = text_path_element.path_or_shape(); - if (!path_or_shape) - return TraversalDecision::Continue; - - auto& font = graphics_box.first_available_font(); - auto text_contents = text_path_element.text_contents(); - Utf8View text_utf8 { text_contents }; - - auto shape_path = const_cast(*path_or_shape).get_path({ viewport_width, viewport_height }); - path = shape_path.place_text_along(text_utf8, font); - } - - auto path_bounding_box = to_css_pixels_transform.map(path.bounding_box()).to_type(); - // Stroke increases the path's size by stroke_width/2 per side. - CSSPixels stroke_width = CSSPixels::nearest_value_for(graphics_box.dom_node().visible_stroke_width() * viewbox_transform.x_scale()); - path_bounding_box.inflate(stroke_width, stroke_width); - graphics_box_state.set_content_offset(path_bounding_box.top_left()); - graphics_box_state.set_content_width(path_bounding_box.width()); - graphics_box_state.set_content_height(path_bounding_box.height()); - graphics_box_state.set_has_definite_width(true); - graphics_box_state.set_has_definite_height(true); - graphics_box_state.set_computed_svg_path(move(path)); - } - return TraversalDecision::Continue; - }); - - // https://svgwg.org/svg2-draft/struct.html#Groups - // 5.2. Grouping: the ‘g’ element - // The ‘g’ element is a container element for grouping together related graphics elements. - box.for_each_in_subtree_of_type([&](SVGBox const& descendant) { - if (is_container_element(descendant)) { - Gfx::BoundingBox bounding_box; - for_each_in_subtree(descendant, [&](Node const& child_of_svg_container) { - if (!is(child_of_svg_container)) - return TraversalDecision::Continue; - // Masks/clips do not change the bounding box of their parents. - if (is(child_of_svg_container) || is(child_of_svg_container)) - return TraversalDecision::SkipChildrenAndContinue; - auto& box_state = m_state.get(static_cast(child_of_svg_container)); - bounding_box.add_point(box_state.offset); - bounding_box.add_point(box_state.offset.translated(box_state.content_width(), box_state.content_height())); - return TraversalDecision::Continue; - }); - - auto& box_state = m_state.get_mutable(descendant); - box_state.set_content_x(bounding_box.x()); - box_state.set_content_y(bounding_box.y()); - box_state.set_content_width(bounding_box.width()); - box_state.set_content_height(bounding_box.height()); - box_state.set_has_definite_width(true); - box_state.set_has_definite_height(true); - } - return IterationDecision::Continue; - }); - - // 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(descendant)) - content_units = static_cast(descendant).dom_node().mask_content_units(); - else if (is(descendant)) - content_units = static_cast(descendant).dom_node().clip_path_units(); - else - return TraversalDecision::Continue; - // FIXME: Somehow limit contents to: shape elements, , and . - auto& layout_state = m_state.get_mutable(static_cast(descendant)); - auto parent_viewbox_transform = viewbox_transform; - 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()); - } else { - layout_state.set_content_width(viewport_width); - layout_state.set_content_height(viewport_height); - } - // Pretend masks/clips are a viewport so we can scale the contents depending on the `contentUnits`. - SVGFormattingContext nested_context(m_state, static_cast(descendant), this, parent_viewbox_transform); - layout_state.set_has_definite_width(true); - layout_state.set_has_definite_height(true); - nested_context.run(static_cast(descendant), layout_mode, available_space); - return TraversalDecision::SkipChildrenAndContinue; + box.for_each_child_of_type([&](Box const& child) { + layout_svg_element(child); }); } + +void SVGFormattingContext::layout_svg_element(Box const& child) +{ + if (is(child.dom_node())) { + layout_nested_viewport(child); + } else if (is(child.dom_node()) && is(child)) { + Layout::BlockFormattingContext bfc(m_state, static_cast(child), this); + bfc.run(child, LayoutMode::Normal, *m_available_space); + auto& child_state = m_state.get_mutable(child); + child_state.set_content_offset(child_state.offset.translated(m_svg_offset)); + } else if (is(child)) { + layout_graphics_element(static_cast(child)); + } +} + +void SVGFormattingContext::layout_nested_viewport(Box const& viewport) +{ + // Layout for a nested SVG viewport. + // https://svgwg.org/svg2-draft/coords.html#EstablishingANewSVGViewport. + SVGFormattingContext nested_context(m_state, viewport, this, m_current_viewbox_transform); + auto& nested_viewport_state = m_state.get_mutable(viewport); + 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); + }; + + auto nested_viewport_x = viewport.computed_values().x().to_px(viewport, m_viewport_size.width()); + auto nested_viewport_y = viewport.computed_values().y().to_px(viewport, m_viewport_size.height()); + auto nested_viewport_width = resolve_dimension(viewport, viewport.computed_values().width(), m_viewport_size.width()); + auto nested_viewport_height = resolve_dimension(viewport, viewport.computed_values().height(), m_viewport_size.height()); + nested_viewport_state.set_content_offset({ nested_viewport_x, nested_viewport_y }); + nested_viewport_state.set_content_width(nested_viewport_width); + nested_viewport_state.set_content_height(nested_viewport_height); + nested_viewport_state.set_has_definite_width(true); + nested_viewport_state.set_has_definite_height(true); + nested_context.run(static_cast(viewport), LayoutMode::Normal, *m_available_space); +} + +Gfx::Path SVGFormattingContext::compute_path_for_text(SVGTextBox const& text_box) +{ + auto& text_element = static_cast(text_box.dom_node()); + auto& font = text_box.first_available_font(); + auto text_contents = text_element.text_contents(); + Utf8View text_utf8 { text_contents }; + auto text_width = font.width(text_utf8); + auto text_offset = text_element.get_offset(); + + // https://svgwg.org/svg2-draft/text.html#TextAnchoringProperties + switch (text_element.text_anchor().value_or(SVG::TextAnchor::Start)) { + case SVG::TextAnchor::Start: + // The rendered characters are aligned such that the start of the resulting rendered text is at the initial + // current text position. + break; + case SVG::TextAnchor::Middle: { + // The rendered characters are shifted such that the geometric middle of the resulting rendered text + // (determined from the initial and final current text position before applying the text-anchor property) + // is at the initial current text position. + text_offset.translate_by(-text_width / 2, 0); + break; + } + case SVG::TextAnchor::End: { + // The rendered characters are shifted such that the end of the resulting rendered text (final current text + // position before applying the text-anchor property) is at the initial current text position. + text_offset.translate_by(-text_width, 0); + break; + } + default: + VERIFY_NOT_REACHED(); + } + + Gfx::Path path; + path.move_to(text_offset); + path.text(text_utf8, font); + return path; +} + +Gfx::Path SVGFormattingContext::compute_path_for_text_path(SVGTextPathBox const& text_path_box) +{ + auto& text_path_element = static_cast(text_path_box.dom_node()); + auto path_or_shape = text_path_element.path_or_shape(); + if (!path_or_shape) + return {}; + + auto& font = text_path_box.first_available_font(); + auto text_contents = text_path_element.text_contents(); + Utf8View text_utf8 { text_contents }; + + auto shape_path = const_cast(*path_or_shape).get_path(m_viewport_size); + return shape_path.place_text_along(text_utf8, font); +} + +void SVGFormattingContext::layout_path_like_element(SVGGraphicsBox const& graphics_box) +{ + auto& graphics_box_state = m_state.get_mutable(graphics_box); + VERIFY(graphics_box_state.computed_svg_transforms().has_value()); + + auto to_css_pixels_transform = Gfx::AffineTransform {} + .multiply(m_current_viewbox_transform) + .multiply(graphics_box_state.computed_svg_transforms()->svg_transform()); + + Gfx::Path path; + if (is(graphics_box)) { + auto& geometry_box = static_cast(graphics_box); + path = const_cast(geometry_box).dom_node().get_path(m_viewport_size); + } else if (is(graphics_box)) { + auto& text_box = static_cast(graphics_box); + path = compute_path_for_text(text_box); + // and elements can contain more text elements. + text_box.for_each_child_of_type([&](auto& child) { + if (is(child) || is(child)) + layout_graphics_element(child); + }); + } else if (is(graphics_box)) { + // FIXME: Support in . + path = compute_path_for_text_path(static_cast(graphics_box)); + } + + auto path_bounding_box = to_css_pixels_transform.map(path.bounding_box()).to_type(); + // Stroke increases the path's size by stroke_width/2 per side. + CSSPixels stroke_width = CSSPixels::nearest_value_for(graphics_box.dom_node().visible_stroke_width() * m_current_viewbox_transform.x_scale()); + path_bounding_box.inflate(stroke_width, stroke_width); + graphics_box_state.set_content_offset(path_bounding_box.top_left()); + graphics_box_state.set_content_width(path_bounding_box.width()); + graphics_box_state.set_content_height(path_bounding_box.height()); + graphics_box_state.set_has_definite_width(true); + graphics_box_state.set_has_definite_height(true); + graphics_box_state.set_computed_svg_path(move(path)); +} + +void SVGFormattingContext::layout_graphics_element(SVGGraphicsBox const& graphics_box) +{ + auto& graphics_box_state = m_state.get_mutable(graphics_box); + auto svg_transform = const_cast(graphics_box).dom_node().get_transform(); + graphics_box_state.set_computed_svg_transforms(Painting::SVGGraphicsPaintable::ComputedTransforms(m_current_viewbox_transform, svg_transform)); + + if (is_container_element(graphics_box)) { + // https://svgwg.org/svg2-draft/struct.html#Groups + // 5.2. Grouping: the ‘g’ element + // The ‘g’ element is a container element for grouping together related graphics elements. + layout_container_element(graphics_box); + } else { + // Assume this is a path-like element. + layout_path_like_element(graphics_box); + } + + if (auto* mask_box = graphics_box.first_child_of_type()) + layout_mask_or_clip(*mask_box); + + if (auto* clip_box = graphics_box.first_child_of_type()) + layout_mask_or_clip(*clip_box); +} + +void SVGFormattingContext::layout_mask_or_clip(SVGBox const& mask_or_clip) +{ + SVG::SVGUnits content_units {}; + if (is(mask_or_clip)) + content_units = static_cast(mask_or_clip).dom_node().mask_content_units(); + else if (is(mask_or_clip)) + content_units = static_cast(mask_or_clip).dom_node().clip_path_units(); + else + VERIFY_NOT_REACHED(); + // FIXME: Somehow limit contents to: shape elements, , and . + auto& layout_state = m_state.get_mutable(mask_or_clip); + auto parent_viewbox_transform = m_current_viewbox_transform; + if (content_units == SVG::SVGUnits::ObjectBoundingBox) { + auto* parent_node = mask_or_clip.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()); + } else { + layout_state.set_content_width(m_viewport_size.width()); + layout_state.set_content_height(m_viewport_size.height()); + } + // Pretend masks/clips are a viewport so we can scale the contents depending on the `contentUnits`. + SVGFormattingContext nested_context(m_state, mask_or_clip, this, parent_viewbox_transform); + layout_state.set_has_definite_width(true); + layout_state.set_has_definite_height(true); + nested_context.run(static_cast(mask_or_clip), LayoutMode::Normal, *m_available_space); +} + +void SVGFormattingContext::layout_container_element(SVGBox const& container) +{ + auto& box_state = m_state.get_mutable(container); + Gfx::BoundingBox bounding_box; + container.for_each_child_of_type([&](Box const& child) { + // Masks/clips do not change the bounding box of their parents. + if (is(child) || is(child)) + return; + layout_svg_element(child); + auto& child_state = m_state.get(child); + bounding_box.add_point(child_state.offset); + bounding_box.add_point(child_state.offset.translated(child_state.content_width(), child_state.content_height())); + }); + box_state.set_content_x(bounding_box.x()); + box_state.set_content_y(bounding_box.y()); + box_state.set_content_width(bounding_box.width()); + box_state.set_content_height(bounding_box.height()); + box_state.set_has_definite_width(true); + box_state.set_has_definite_height(true); +} + } diff --git a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.h b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.h index 599b5d6d4c6..09d9e8c4e4e 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.h +++ b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.h @@ -6,8 +6,12 @@ #pragma once +#include #include #include +#include +#include +#include namespace Web::Layout { @@ -21,7 +25,22 @@ public: virtual CSSPixels automatic_content_height() const override; private: + void layout_svg_element(Box const&); + void layout_nested_viewport(Box const&); + void layout_container_element(SVGBox const&); + void layout_graphics_element(SVGGraphicsBox const&); + void layout_path_like_element(SVGGraphicsBox const&); + void layout_mask_or_clip(SVGBox const&); + + Gfx::Path compute_path_for_text(SVGTextBox const&); + Gfx::Path compute_path_for_text_path(SVGTextPathBox const&); + Gfx::AffineTransform m_parent_viewbox_transform {}; + + Optional m_available_space {}; + Gfx::AffineTransform m_current_viewbox_transform {}; + CSSPixelSize m_viewport_size {}; + CSSPixelPoint m_svg_offset {}; }; }