From c93d367d950816d324ed69a3a73f110e9354cc9f Mon Sep 17 00:00:00 2001 From: MacDue Date: Sun, 29 Oct 2023 19:11:46 +0000 Subject: [PATCH] LibWeb: Layout SVG elements during layout (not while painting) Previously, all SVG elements were zero-sized boxes, that were only actually positioned and sized during painting. This led to a number of problems, the most visible of which being that text could not be scaled based on the viewBox. Which this patch, elements get a correctly sized layout box, that can be hit-tested and respects the SVG viewBox. To share code with SVGGeometryElement's the PathData (from the prior commit) has been split into a computed path and computed transforms. The computed path is specific to geometry elements, but the computed transforms are shared between all SVG graphics elements. --- .../css-namespace-tag-name-selector.txt | 2 +- .../Layout/expected/svg-text-with-viewbox.txt | 30 +++++ .../Layout/expected/svg/text-fill-none.txt | 6 +- .../Layout/input/svg-text-with-viewbox.html | 20 ++++ .../Libraries/LibWeb/Layout/LayoutState.cpp | 10 +- .../Libraries/LibWeb/Layout/LayoutState.h | 12 +- .../LibWeb/Layout/SVGFormattingContext.cpp | 109 ++++++++++++------ .../Libraries/LibWeb/Layout/SVGTextBox.cpp | 21 ---- Userland/Libraries/LibWeb/Layout/SVGTextBox.h | 2 - .../LibWeb/Painting/SVGGeometryPaintable.cpp | 13 +-- .../LibWeb/Painting/SVGGeometryPaintable.h | 43 +------ .../LibWeb/Painting/SVGGraphicsPaintable.h | 42 +++++++ .../LibWeb/Painting/SVGTextPaintable.cpp | 61 +--------- .../LibWeb/Painting/SVGTextPaintable.h | 2 - .../LibWeb/SVG/SVGTextContentElement.cpp | 7 +- .../LibWeb/SVG/SVGTextContentElement.h | 2 + 16 files changed, 209 insertions(+), 173 deletions(-) create mode 100644 Tests/LibWeb/Layout/expected/svg-text-with-viewbox.txt create mode 100644 Tests/LibWeb/Layout/input/svg-text-with-viewbox.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 f6e14de298..5008ccc680 100644 --- a/Tests/LibWeb/Layout/expected/css-namespace-tag-name-selector.txt +++ b/Tests/LibWeb/Layout/expected/css-namespace-tag-name-selector.txt @@ -9,7 +9,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline frag 2 from Box start: 0, length: 0, rect: [319,51 0x108] SVGSVGBox at (9,9) content-size 300x150 [SVG] children: inline InlineNode - SVGTextBox at (9,9) content-size 0x0 children: inline + SVGTextBox at (29,9) content-size 198.90625x80 children: inline TextNode <#text> TextNode <#text> Box at (319,51) content-size 0x108 children: not-inline diff --git a/Tests/LibWeb/Layout/expected/svg-text-with-viewbox.txt b/Tests/LibWeb/Layout/expected/svg-text-with-viewbox.txt new file mode 100644 index 0000000000..b4bde816ff --- /dev/null +++ b/Tests/LibWeb/Layout/expected/svg-text-with-viewbox.txt @@ -0,0 +1,30 @@ +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 784x261.328125 children: inline + line 0 width: 784, height: 261.328125, bottom: 261.328125, baseline: 261.328125 + frag 0 from SVGSVGBox start: 0, length: 0, rect: [8,8 784x261.328125] + SVGSVGBox at (8,8) content-size 784x261.328125 [SVG] children: inline + TextNode <#text> + TextNode <#text> + SVGTextBox at (73.34375,79.859375) content-size 50.265625x42.46875 children: inline + TextNode <#text> + TextNode <#text> + SVGTextBox at (138.6875,24.328125) content-size 153.703125x98 children: inline + TextNode <#text> + TextNode <#text> + SVGTextBox at (187.671875,145.1875) content-size 36.90625x42.46875 children: inline + TextNode <#text> + TextNode <#text> + SVGTextBox at (220.34375,57) content-size 526.609375x130.65625 children: inline + TextNode <#text> + TextNode <#text> + TextNode <#text> + +ViewportPaintable (Viewport<#document>) [0,0 800x600] + PaintableWithLines (BlockContainer) [0,0 800x600] + PaintableWithLines (BlockContainer) [8,8 784x261.328125] + SVGSVGPaintable (SVGSVGBox) [8,8 784x261.328125] + SVGTextPaintable (SVGTextBox.small) [73.34375,79.859375 50.265625x42.46875] + SVGTextPaintable (SVGTextBox.heavy) [138.6875,24.328125 153.703125x98] + SVGTextPaintable (SVGTextBox.small) [187.671875,145.1875 36.90625x42.46875] + SVGTextPaintable (SVGTextBox.Rrrrr) [220.34375,57 526.609375x130.65625] diff --git a/Tests/LibWeb/Layout/expected/svg/text-fill-none.txt b/Tests/LibWeb/Layout/expected/svg/text-fill-none.txt index 7de6846f7d..bd127e4e9e 100644 --- a/Tests/LibWeb/Layout/expected/svg/text-fill-none.txt +++ b/Tests/LibWeb/Layout/expected/svg/text-fill-none.txt @@ -4,11 +4,11 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline line 0 width: 300, height: 150, bottom: 150, baseline: 150 frag 0 from SVGSVGBox start: 0, length: 0, rect: [8,8 300x150] SVGSVGBox at (8,8) content-size 300x150 [SVG] children: not-inline - SVGTextBox at (8,8) content-size 0x0 children: not-inline + SVGTextBox at (8,-8) content-size 0x16 children: not-inline TextNode <#text> ViewportPaintable (Viewport<#document>) [0,0 800x600] PaintableWithLines (BlockContainer) [0,0 800x600] PaintableWithLines (BlockContainer) [8,8 784x150] - SVGSVGPaintable (SVGSVGBox) [8,8 300x150] - SVGTextPaintable (SVGTextBox) [8,8 0x0] + SVGSVGPaintable (SVGSVGBox) [8,8 300x150] overflow: [8,-8 300x166] + SVGTextPaintable (SVGTextBox) [8,-8 0x16] diff --git a/Tests/LibWeb/Layout/input/svg-text-with-viewbox.html b/Tests/LibWeb/Layout/input/svg-text-with-viewbox.html new file mode 100644 index 0000000000..2501c87b34 --- /dev/null +++ b/Tests/LibWeb/Layout/input/svg-text-with-viewbox.html @@ -0,0 +1,20 @@ + + + + My + cat + is + Grumpy! + diff --git a/Userland/Libraries/LibWeb/Layout/LayoutState.cpp b/Userland/Libraries/LibWeb/Layout/LayoutState.cpp index bf439b4bb6..98ed65a2eb 100644 --- a/Userland/Libraries/LibWeb/Layout/LayoutState.cpp +++ b/Userland/Libraries/LibWeb/Layout/LayoutState.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace Web::Layout { @@ -266,9 +267,14 @@ void LayoutState::commit(Box& root) paintables_with_lines.append(paintable_with_lines); } - if (used_values.svg_path_data().has_value() && is(paintable_box)) { + if (used_values.computed_svg_transforms().has_value() && is(paintable_box)) { + auto& svg_graphics_paintable = static_cast(paintable_box); + svg_graphics_paintable.set_computed_transforms(*used_values.computed_svg_transforms()); + } + + if (used_values.computed_svg_path().has_value() && is(paintable_box)) { auto& svg_geometry_paintable = static_cast(paintable_box); - svg_geometry_paintable.set_path_data(move(*used_values.svg_path_data())); + svg_geometry_paintable.set_computed_path(move(*used_values.computed_svg_path())); } } } diff --git a/Userland/Libraries/LibWeb/Layout/LayoutState.h b/Userland/Libraries/LibWeb/Layout/LayoutState.h index 087cec7e60..0cb4c34b31 100644 --- a/Userland/Libraries/LibWeb/Layout/LayoutState.h +++ b/Userland/Libraries/LibWeb/Layout/LayoutState.h @@ -11,7 +11,7 @@ #include #include #include -#include +#include namespace Web::Layout { @@ -124,8 +124,11 @@ struct LayoutState { void set_table_cell_coordinates(Painting::PaintableBox::TableCellCoordinates const& table_cell_coordinates) { m_table_cell_coordinates = table_cell_coordinates; } auto const& table_cell_coordinates() const { return m_table_cell_coordinates; } - void set_svg_path_data(Painting::SVGGeometryPaintable::PathData const& svg_path_data) { m_svg_path_data = svg_path_data; } - auto& svg_path_data() const { return m_svg_path_data; } + void set_computed_svg_path(Gfx::Path const& svg_path) { m_computed_svg_path = svg_path; } + auto& computed_svg_path() { return m_computed_svg_path; } + + void set_computed_svg_transforms(Painting::SVGGraphicsPaintable::ComputedTransforms const& computed_transforms) { m_computed_svg_transforms = computed_transforms; } + auto const& computed_svg_transforms() const { return m_computed_svg_transforms; } private: AvailableSize available_width_inside() const; @@ -151,7 +154,8 @@ struct LayoutState { Optional m_override_borders_data; Optional m_table_cell_coordinates; - Optional m_svg_path_data; + Optional m_computed_svg_path; + Optional m_computed_svg_transforms; }; // Commits the used values produced by layout and builds a paintable tree. diff --git a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp index 9e5d43cba9..caf71c7c55 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp +++ b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp @@ -135,8 +135,6 @@ static ViewBoxTransform scale_and_align_viewbox_content(SVG::PreserveAspectRatio static bool should_ensure_creation_of_paintable(Node const& node) { - if (is(node)) - return true; if (is(node)) return true; if (node.dom_node()) { @@ -166,46 +164,87 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available return IterationDecision::Continue; }); + auto compute_viewbox_transform = [&](auto const& viewbox) -> Gfx::AffineTransform { + if (!viewbox.has_value()) + return {}; + + // 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()) { + dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Attempting to layout indefinitely sized SVG with a viewbox -- this likely won't work!"); + } + + auto scale_width = svg_box_state.has_definite_width() ? svg_box_state.content_width() / viewbox->width : 1; + 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 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 }); + }; + box.for_each_in_subtree([&](Node const& descendant) { - if (is(descendant)) { - auto const& geometry_box = static_cast(descendant); - auto& geometry_box_state = m_state.get_mutable(geometry_box); - auto& dom_node = const_cast(geometry_box).dom_node(); + if (is(descendant)) { + auto const& graphics_box = static_cast(descendant); + auto& graphics_box_state = m_state.get_mutable(graphics_box); + auto& dom_node = const_cast(graphics_box).dom_node(); - auto& path = dom_node.get_path(); auto svg_transform = dom_node.get_transform(); - Gfx::AffineTransform viewbox_transform; + Gfx::AffineTransform viewbox_transform = compute_viewbox_transform(dom_node.view_box()); + 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); - double viewbox_scale = 1; - auto maybe_view_box = dom_node.view_box(); - if (maybe_view_box.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()) { - dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Attempting to layout indefinitely sized SVG with a viewbox -- this likely won't work!"); + if (is(descendant)) { + auto path = static_cast(dom_node).get_path(); + 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_computed_svg_path(move(path)); + } else if (is(descendant)) { + auto& text_element = static_cast(dom_node); + + // FIXME: Support arbitrary path transforms for fonts. + // FIMXE: This assumes transform->x_scale() == transform->y_scale(). + auto& scaled_font = graphics_box.scaled_font(to_css_pixels_transform.x_scale()); + auto text_contents = text_element.text_contents(); + + auto text_offset = text_element.get_offset().transformed(to_css_pixels_transform).to_type(); + auto text_width = CSSPixels::nearest_value_for(scaled_font.width(Utf8View { text_contents })); + + // 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(); } - auto view_box = maybe_view_box.value(); - auto scale_width = svg_box_state.has_definite_width() ? svg_box_state.content_width() / view_box.width : 1; - auto scale_height = svg_box_state.has_definite_height() ? svg_box_state.content_height() / view_box.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 viewbox_offset_and_scale = scale_and_align_viewbox_content(preserve_aspect_ratio, view_box, { scale_width, scale_height }, svg_box_state); - viewbox_transform = Gfx::AffineTransform {}.translate(viewbox_offset_and_scale.offset.to_type()).scale(viewbox_offset_and_scale.scale_factor, viewbox_offset_and_scale.scale_factor).translate({ -view_box.min_x, -view_box.min_y }); - - viewbox_scale = viewbox_offset_and_scale.scale_factor; + auto text_height = CSSPixels::nearest_value_for(scaled_font.pixel_size()); + graphics_box_state.set_content_offset(text_offset.translated(0, -text_height)); + graphics_box_state.set_content_width(text_width); + graphics_box_state.set_content_height(text_height); } - - // Stroke increases the path's size by stroke_width/2 per side. - auto path_transform = Gfx::AffineTransform {}.multiply(viewbox_transform).multiply(svg_transform); - auto path_bounding_box = path_transform.map(path.bounding_box()).to_type(); - CSSPixels stroke_width = CSSPixels::nearest_value_for(static_cast(geometry_box.dom_node().visible_stroke_width()) * viewbox_scale); - path_bounding_box.inflate(stroke_width, stroke_width); - geometry_box_state.set_content_offset(path_bounding_box.top_left()); - geometry_box_state.set_content_width(path_bounding_box.width()); - geometry_box_state.set_content_height(path_bounding_box.height()); - geometry_box_state.set_svg_path_data(Painting::SVGGeometryPaintable::PathData(path, viewbox_transform, svg_transform)); } else if (is(descendant)) { SVGFormattingContext nested_context(m_state, static_cast(descendant), this); nested_context.run(static_cast(descendant), layout_mode, available_space); diff --git a/Userland/Libraries/LibWeb/Layout/SVGTextBox.cpp b/Userland/Libraries/LibWeb/Layout/SVGTextBox.cpp index 9258f51e37..a7257c89a5 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGTextBox.cpp +++ b/Userland/Libraries/LibWeb/Layout/SVGTextBox.cpp @@ -15,27 +15,6 @@ SVGTextBox::SVGTextBox(DOM::Document& document, SVG::SVGTextPositioningElement& { } -CSSPixelPoint SVGTextBox::viewbox_origin() const -{ - auto* svg_box = dom_node().first_ancestor_of_type(); - if (!svg_box || !svg_box->view_box().has_value()) - return { 0, 0 }; - return { svg_box->view_box().value().min_x, svg_box->view_box().value().min_y }; -} - -Optional SVGTextBox::layout_transform() const -{ - // FIXME: Since text layout boxes are currently 0x0 it is not possible handle viewBox scaling here. - auto& geometry_element = dom_node(); - auto transform = geometry_element.get_transform(); - auto* svg_box = geometry_element.first_ancestor_of_type(); - auto origin = viewbox_origin().to_type(); - Gfx::FloatPoint paint_offset = {}; - if (svg_box && svg_box->view_box().has_value()) - paint_offset = svg_box->paintable_box()->absolute_rect().location().to_type(); - return Gfx::AffineTransform {}.translate(paint_offset).translate(-origin).multiply(transform); -} - JS::GCPtr SVGTextBox::create_paintable() const { return Painting::SVGTextPaintable::create(*this); diff --git a/Userland/Libraries/LibWeb/Layout/SVGTextBox.h b/Userland/Libraries/LibWeb/Layout/SVGTextBox.h index dc9b58e0b8..893b96d1e4 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGTextBox.h +++ b/Userland/Libraries/LibWeb/Layout/SVGTextBox.h @@ -22,8 +22,6 @@ public: SVG::SVGTextPositioningElement& dom_node() { return static_cast(SVGGraphicsBox::dom_node()); } SVG::SVGTextPositioningElement const& dom_node() const { return static_cast(SVGGraphicsBox::dom_node()); } - Optional layout_transform() const; - virtual JS::GCPtr create_paintable() const override; private: diff --git a/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp b/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp index 2f6737cbda..979ef84df8 100644 --- a/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp +++ b/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp @@ -6,7 +6,6 @@ */ #include -#include #include #include @@ -30,9 +29,9 @@ Layout::SVGGeometryBox const& SVGGeometryPaintable::layout_box() const Optional SVGGeometryPaintable::hit_test(CSSPixelPoint position, HitTestType type) const { auto result = SVGGraphicsPaintable::hit_test(position, type); - if (!result.has_value() || !path_data().has_value()) + if (!result.has_value() || !computed_path().has_value()) return {}; - auto transformed_bounding_box = path_data()->svg_to_css_pixels_transform().map_to_quad(path_data()->computed_path().bounding_box()); + auto transformed_bounding_box = computed_transforms().svg_to_css_pixels_transform().map_to_quad(computed_path()->bounding_box()); if (!transformed_bounding_box.contains(position.to_type())) return {}; return result; @@ -52,7 +51,7 @@ static Gfx::Painter::WindingRule to_gfx_winding_rule(SVG::FillRule fill_rule) void SVGGeometryPaintable::paint(PaintContext& context, PaintPhase phase) const { - if (!is_visible() || !path_data().has_value()) + if (!is_visible() || !computed_path().has_value()) return; SVGGraphicsPaintable::paint(context, phase); @@ -71,8 +70,8 @@ void SVGGeometryPaintable::paint(PaintContext& context, PaintPhase phase) const auto offset = context.floored_device_point(svg_element_rect.location()).to_type().to_type(); auto maybe_view_box = geometry_element.view_box(); - auto paint_transform = path_data()->svg_to_device_pixels_transform(context, context.svg_transform()); - Gfx::Path path = path_data()->computed_path().copy_transformed(paint_transform); + auto paint_transform = computed_transforms().svg_to_device_pixels_transform(context); + Gfx::Path path = computed_path()->copy_transformed(paint_transform); // Fills are computed as though all subpaths are closed (https://svgwg.org/svg2-draft/painting.html#FillProperties) auto closed_path = [&] { @@ -95,7 +94,7 @@ void SVGGeometryPaintable::paint(PaintContext& context, PaintPhase phase) const SVG::SVGPaintContext paint_context { .viewport = svg_viewport, - .path_bounding_box = path_data()->computed_path().bounding_box(), + .path_bounding_box = computed_path()->bounding_box(), .transform = paint_transform }; diff --git a/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.h b/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.h index db3f5b9a04..97831534bf 100644 --- a/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.h +++ b/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.h @@ -15,41 +15,6 @@ class SVGGeometryPaintable final : public SVGGraphicsPaintable { JS_CELL(SVGGeometryPaintable, SVGGraphicsPaintable); public: - class PathData { - public: - PathData(Gfx::Path path, Gfx::AffineTransform svg_to_viewbox_transform, Gfx::AffineTransform svg_transform) - : m_computed_path(move(path)) - , m_svg_to_viewbox_transform(svg_to_viewbox_transform) - , m_svg_transform(svg_transform) - { - } - - Gfx::Path const& computed_path() const { return m_computed_path; } - - Gfx::AffineTransform const& svg_to_viewbox_transform() const { return m_svg_to_viewbox_transform; } - - Gfx::AffineTransform const& svg_transform() const { return m_svg_transform; } - - Gfx::AffineTransform svg_to_css_pixels_transform( - Optional additional_svg_transform = {}) const - { - return Gfx::AffineTransform {}.multiply(svg_to_viewbox_transform()).multiply(additional_svg_transform.value_or(Gfx::AffineTransform {})).multiply(svg_transform()); - } - - Gfx::AffineTransform svg_to_device_pixels_transform( - PaintContext const& context, - Gfx::AffineTransform const& additional_svg_transform) const - { - auto css_scale = context.device_pixels_per_css_pixel(); - return Gfx::AffineTransform {}.scale({ css_scale, css_scale }).multiply(svg_to_css_pixels_transform(additional_svg_transform)); - } - - private: - Gfx::Path m_computed_path; - Gfx::AffineTransform m_svg_to_viewbox_transform; - Gfx::AffineTransform m_svg_transform; - }; - static JS::NonnullGCPtr create(Layout::SVGGeometryBox const&); virtual Optional hit_test(CSSPixelPoint, HitTestType) const override; @@ -58,17 +23,17 @@ public: Layout::SVGGeometryBox const& layout_box() const; - void set_path_data(PathData path_data) + void set_computed_path(Gfx::Path path) { - m_path_data = move(path_data); + m_computed_path = move(path); } - Optional const& path_data() const { return m_path_data; } + Optional const& computed_path() const { return m_computed_path; } protected: SVGGeometryPaintable(Layout::SVGGeometryBox const&); - Optional m_path_data = {}; + Optional m_computed_path = {}; }; } diff --git a/Userland/Libraries/LibWeb/Painting/SVGGraphicsPaintable.h b/Userland/Libraries/LibWeb/Painting/SVGGraphicsPaintable.h index 08a92c17cc..d7c4e4e2b3 100644 --- a/Userland/Libraries/LibWeb/Painting/SVGGraphicsPaintable.h +++ b/Userland/Libraries/LibWeb/Painting/SVGGraphicsPaintable.h @@ -15,6 +15,36 @@ class SVGGraphicsPaintable : public SVGPaintable { JS_CELL(SVGGraphicsPaintable, SVGPaintable); public: + class ComputedTransforms { + public: + ComputedTransforms(Gfx::AffineTransform svg_to_viewbox_transform, Gfx::AffineTransform svg_transform) + : m_svg_to_viewbox_transform(svg_to_viewbox_transform) + , m_svg_transform(svg_transform) + { + } + + ComputedTransforms() = default; + + Gfx::AffineTransform const& svg_to_viewbox_transform() const { return m_svg_to_viewbox_transform; } + Gfx::AffineTransform const& svg_transform() const { return m_svg_transform; } + + Gfx::AffineTransform svg_to_css_pixels_transform( + Optional additional_svg_transform = {}) const + { + return Gfx::AffineTransform {}.multiply(svg_to_viewbox_transform()).multiply(additional_svg_transform.value_or(Gfx::AffineTransform {})).multiply(svg_transform()); + } + + Gfx::AffineTransform svg_to_device_pixels_transform(PaintContext const& context) const + { + auto css_scale = context.device_pixels_per_css_pixel(); + return Gfx::AffineTransform {}.scale({ css_scale, css_scale }).multiply(svg_to_css_pixels_transform(context.svg_transform())); + } + + private: + Gfx::AffineTransform m_svg_to_viewbox_transform {}; + Gfx::AffineTransform m_svg_transform {}; + }; + static JS::NonnullGCPtr create(Layout::SVGGraphicsBox const&); Layout::SVGGraphicsBox const& layout_box() const; @@ -25,8 +55,20 @@ public: virtual Optional get_mask_type() const override; virtual RefPtr calculate_mask(PaintContext&, CSSPixelRect const& masking_area) const override; + void set_computed_transforms(ComputedTransforms computed_transforms) + { + m_computed_transforms = computed_transforms; + } + + ComputedTransforms const& computed_transforms() const + { + return m_computed_transforms; + } + protected: SVGGraphicsPaintable(Layout::SVGGraphicsBox const&); + + ComputedTransforms m_computed_transforms; }; } diff --git a/Userland/Libraries/LibWeb/Painting/SVGTextPaintable.cpp b/Userland/Libraries/LibWeb/Painting/SVGTextPaintable.cpp index 870ad89ec6..ac1c2f0b83 100644 --- a/Userland/Libraries/LibWeb/Painting/SVGTextPaintable.cpp +++ b/Userland/Libraries/LibWeb/Painting/SVGTextPaintable.cpp @@ -19,13 +19,6 @@ SVGTextPaintable::SVGTextPaintable(Layout::SVGTextBox const& layout_box) { } -Optional SVGTextPaintable::hit_test(CSSPixelPoint position, HitTestType type) const -{ - (void)position; - (void)type; - return {}; -} - void SVGTextPaintable::paint(PaintContext& context, PaintPhase phase) const { if (!is_visible()) @@ -45,57 +38,13 @@ void SVGTextPaintable::paint(PaintContext& context, PaintPhase phase) const return; auto& painter = context.painter(); - - auto& text_element = layout_box().dom_node(); - auto const* svg_element = text_element.shadow_including_first_ancestor_of_type(); - auto svg_element_rect = svg_element->paintable_box()->absolute_rect(); - - RecordingPainterStateSaver save_painter { painter }; - auto svg_context_offset = context.floored_device_point(svg_element_rect.location()).to_type(); - painter.translate(svg_context_offset); - auto const& dom_node = layout_box().dom_node(); + auto paint_transform = computed_transforms().svg_to_device_pixels_transform(context); + auto& scaled_font = layout_box().scaled_font(paint_transform.x_scale()); + auto text_rect = context.enclosing_device_rect(absolute_rect()).to_type(); + auto text_contents = dom_node.text_contents(); - auto child_text_content = dom_node.child_text_content(); - - auto maybe_transform = layout_box().layout_transform(); - if (!maybe_transform.has_value()) - return; - - auto transform = Gfx::AffineTransform(context.svg_transform()).multiply(*maybe_transform); - - // FIXME: Support arbitrary path transforms for fonts. - // FIMXE: This assumes transform->x_scale() == transform->y_scale(). - auto& scaled_font = layout_node().scaled_font(static_cast(context.device_pixels_per_css_pixel()) * transform.x_scale()); - - Utf8View text_content { child_text_content }; - auto text_offset = context.floored_device_point(dom_node.get_offset().transformed(transform).to_type()); - - // FIXME: Once SVGFormattingContext does text layout this logic should move there. - // 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(-scaled_font.width(text_content) / 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(-scaled_font.width(text_content), 0); - break; - } - default: - VERIFY_NOT_REACHED(); - } - - painter.draw_text_run(text_offset.to_type(), text_content, scaled_font, layout_node().computed_values().fill()->as_color(), context.enclosing_device_rect(svg_element_rect).to_type()); + painter.draw_text_run(text_rect.bottom_left(), Utf8View { text_contents }, scaled_font, layout_node().computed_values().fill()->as_color(), text_rect); } } diff --git a/Userland/Libraries/LibWeb/Painting/SVGTextPaintable.h b/Userland/Libraries/LibWeb/Painting/SVGTextPaintable.h index c6910e48df..7476722964 100644 --- a/Userland/Libraries/LibWeb/Painting/SVGTextPaintable.h +++ b/Userland/Libraries/LibWeb/Painting/SVGTextPaintable.h @@ -17,8 +17,6 @@ class SVGTextPaintable final : public SVGGraphicsPaintable { public: static JS::NonnullGCPtr create(Layout::SVGTextBox const&); - virtual Optional hit_test(CSSPixelPoint, HitTestType) const override; - virtual void paint(PaintContext&, PaintPhase) const override; Layout::SVGTextBox const& layout_box() const diff --git a/Userland/Libraries/LibWeb/SVG/SVGTextContentElement.cpp b/Userland/Libraries/LibWeb/SVG/SVGTextContentElement.cpp index 2091a32ab9..4d957f5017 100644 --- a/Userland/Libraries/LibWeb/SVG/SVGTextContentElement.cpp +++ b/Userland/Libraries/LibWeb/SVG/SVGTextContentElement.cpp @@ -45,10 +45,15 @@ Optional SVGTextContentElement::text_anchor() const } } +DeprecatedString SVGTextContentElement::text_contents() const +{ + return child_text_content().trim_whitespace(); +} + // https://svgwg.org/svg2-draft/text.html#__svg__SVGTextContentElement__getNumberOfChars WebIDL::ExceptionOr SVGTextContentElement::get_number_of_chars() const { - auto chars = TRY_OR_THROW_OOM(vm(), utf8_to_utf16(child_text_content())); + auto chars = TRY_OR_THROW_OOM(vm(), utf8_to_utf16(text_contents())); return static_cast(chars.size()); } diff --git a/Userland/Libraries/LibWeb/SVG/SVGTextContentElement.h b/Userland/Libraries/LibWeb/SVG/SVGTextContentElement.h index 7babb5b8d9..c1a467c48b 100644 --- a/Userland/Libraries/LibWeb/SVG/SVGTextContentElement.h +++ b/Userland/Libraries/LibWeb/SVG/SVGTextContentElement.h @@ -21,6 +21,8 @@ public: Optional text_anchor() const; + DeprecatedString text_contents() const; + protected: SVGTextContentElement(DOM::Document&, DOM::QualifiedName);