From dc9cb449b1fb2fdd99219f3b5b6c02588af5c1ef Mon Sep 17 00:00:00 2001 From: MacDue Date: Sun, 29 Oct 2023 17:08:35 +0000 Subject: [PATCH] LibWeb: Store computed SVG path data/transforms in LayoutState This removes the awkward hack to recompute the layout transform at paint time, and makes it possible for path sizes to be computed during layout. For example, it's possible to use relative units in SVG shapes (e.g. ), which can be resolved during layout, but would be hard to resolve again during painting. --- .../Ref/reference/simple-svg-mask-ref.html | 2 +- Tests/LibWeb/Ref/simple-svg-mask.html | 2 +- Tests/LibWeb/Ref/svg-alpha-mask.html | 2 +- Tests/LibWeb/Ref/svg-mask-in-defs.html | 2 +- .../svg-mask-maskUnits-userSpaceOnUse.html | 2 +- .../Libraries/LibWeb/Layout/LayoutState.cpp | 5 +++ .../Libraries/LibWeb/Layout/LayoutState.h | 6 +++ .../LibWeb/Layout/SVGFormattingContext.cpp | 12 +++-- .../LibWeb/Layout/SVGGeometryBox.cpp | 37 ---------------- .../Libraries/LibWeb/Layout/SVGGeometryBox.h | 4 -- .../LibWeb/Painting/SVGGeometryPaintable.cpp | 27 ++++-------- .../LibWeb/Painting/SVGGeometryPaintable.h | 44 +++++++++++++++++++ 12 files changed, 76 insertions(+), 69 deletions(-) diff --git a/Tests/LibWeb/Ref/reference/simple-svg-mask-ref.html b/Tests/LibWeb/Ref/reference/simple-svg-mask-ref.html index 1b879e142c6..dcdc1df6253 100644 --- a/Tests/LibWeb/Ref/reference/simple-svg-mask-ref.html +++ b/Tests/LibWeb/Ref/reference/simple-svg-mask-ref.html @@ -1,4 +1,4 @@ - + diff --git a/Tests/LibWeb/Ref/simple-svg-mask.html b/Tests/LibWeb/Ref/simple-svg-mask.html index 0aeecdf337d..4cf89f669ec 100644 --- a/Tests/LibWeb/Ref/simple-svg-mask.html +++ b/Tests/LibWeb/Ref/simple-svg-mask.html @@ -1,5 +1,5 @@ - + diff --git a/Tests/LibWeb/Ref/svg-alpha-mask.html b/Tests/LibWeb/Ref/svg-alpha-mask.html index c6172a2cd60..f3b8be4e05b 100644 --- a/Tests/LibWeb/Ref/svg-alpha-mask.html +++ b/Tests/LibWeb/Ref/svg-alpha-mask.html @@ -1,5 +1,5 @@ - + diff --git a/Tests/LibWeb/Ref/svg-mask-in-defs.html b/Tests/LibWeb/Ref/svg-mask-in-defs.html index c17d67b90ce..277c69fc3bb 100644 --- a/Tests/LibWeb/Ref/svg-mask-in-defs.html +++ b/Tests/LibWeb/Ref/svg-mask-in-defs.html @@ -1,5 +1,5 @@ - + diff --git a/Tests/LibWeb/Ref/svg-mask-maskUnits-userSpaceOnUse.html b/Tests/LibWeb/Ref/svg-mask-maskUnits-userSpaceOnUse.html index 1f0f521739e..7ba529188b9 100644 --- a/Tests/LibWeb/Ref/svg-mask-maskUnits-userSpaceOnUse.html +++ b/Tests/LibWeb/Ref/svg-mask-maskUnits-userSpaceOnUse.html @@ -1,5 +1,5 @@ - + diff --git a/Userland/Libraries/LibWeb/Layout/LayoutState.cpp b/Userland/Libraries/LibWeb/Layout/LayoutState.cpp index 75a7d67a5eb..bf439b4bb67 100644 --- a/Userland/Libraries/LibWeb/Layout/LayoutState.cpp +++ b/Userland/Libraries/LibWeb/Layout/LayoutState.cpp @@ -265,6 +265,11 @@ void LayoutState::commit(Box& root) paintable_with_lines.set_line_boxes(move(used_values.line_boxes)); paintables_with_lines.append(paintable_with_lines); } + + if (used_values.svg_path_data().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())); + } } } diff --git a/Userland/Libraries/LibWeb/Layout/LayoutState.h b/Userland/Libraries/LibWeb/Layout/LayoutState.h index ef09813f473..087cec7e60c 100644 --- a/Userland/Libraries/LibWeb/Layout/LayoutState.h +++ b/Userland/Libraries/LibWeb/Layout/LayoutState.h @@ -11,6 +11,7 @@ #include #include #include +#include namespace Web::Layout { @@ -123,6 +124,9 @@ 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; } + private: AvailableSize available_width_inside() const; AvailableSize available_height_inside() const; @@ -146,6 +150,8 @@ struct LayoutState { Optional m_override_borders_data; Optional m_table_cell_coordinates; + + Optional m_svg_path_data; }; // 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 212b6f58271..9e5d43cba9e 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp +++ b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp @@ -173,7 +173,8 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available auto& dom_node = const_cast(geometry_box).dom_node(); auto& path = dom_node.get_path(); - auto path_transform = dom_node.get_transform(); + auto svg_transform = dom_node.get_transform(); + Gfx::AffineTransform viewbox_transform; double viewbox_scale = 1; auto maybe_view_box = dom_node.view_box(); @@ -190,18 +191,21 @@ void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, Available // The initial value for preserveAspectRatio is xMidYMid meet. auto preserve_aspect_ratio = svg_svg_element.preserve_aspect_ratio().value_or(SVG::PreserveAspectRatio {}); - auto viewbox_transform = scale_and_align_viewbox_content(preserve_aspect_ratio, view_box, { scale_width, scale_height }, svg_box_state); - path_transform = Gfx::AffineTransform {}.translate(viewbox_transform.offset.to_type()).scale(viewbox_transform.scale_factor, viewbox_transform.scale_factor).translate({ -view_box.min_x, -view_box.min_y }).multiply(path_transform); - viewbox_scale = viewbox_transform.scale_factor; + 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; } // 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/SVGGeometryBox.cpp b/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.cpp index 0341ac86c34..88b0dc13aed 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.cpp +++ b/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.cpp @@ -17,43 +17,6 @@ SVGGeometryBox::SVGGeometryBox(DOM::Document& document, SVG::SVGGeometryElement& { } -CSSPixelPoint SVGGeometryBox::viewbox_origin() const -{ - auto* svg_box = dom_node().shadow_including_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 SVGGeometryBox::layout_transform(Gfx::AffineTransform additional_svg_transform) const -{ - auto& geometry_element = dom_node(); - auto transform = geometry_element.get_transform(); - auto* svg_box = geometry_element.shadow_including_first_ancestor_of_type(); - double scaling = 1; - auto origin = viewbox_origin().to_type(); - Gfx::FloatPoint paint_offset = {}; - if (svg_box && geometry_element.view_box().has_value()) { - // Note: SVGFormattingContext has already done the scaling based on the viewbox, - // we now have to derive what it was from the original bounding box size. - // FIXME: It would be nice if we could store the transform from layout somewhere, so we don't have to solve for it here. - auto original_bounding_box = Gfx::AffineTransform {}.translate(-origin).multiply(transform).map(const_cast(geometry_element).get_path().bounding_box()); - float stroke_width = geometry_element.visible_stroke_width(); - original_bounding_box.inflate(stroke_width, stroke_width); - // If the transform (or path) results in a empty box we can't display this. - if (original_bounding_box.is_empty()) - return {}; - auto scaled_width = paintable_box()->content_width().to_double(); - auto scaled_height = paintable_box()->content_height().to_double(); - scaling = min(scaled_width / static_cast(original_bounding_box.width()), scaled_height / static_cast(original_bounding_box.height())); - auto scaled_bounding_box = original_bounding_box.scaled(scaling, scaling); - paint_offset = (paintable_box()->absolute_rect().location() - svg_box->paintable_box()->absolute_rect().location()).to_type() - scaled_bounding_box.location(); - } - // Note: The "additional_svg_transform" is applied during mask painting to transform the mask element to match its target. - // It has to be applied while still in the SVG coordinate space. - return Gfx::AffineTransform {}.translate(paint_offset).scale(scaling, scaling).translate(-origin).multiply(additional_svg_transform).multiply(transform); -} - JS::GCPtr SVGGeometryBox::create_paintable() const { return Painting::SVGGeometryPaintable::create(*this); diff --git a/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.h b/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.h index ac615bbd5b5..9e198f319ec 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.h +++ b/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.h @@ -22,13 +22,9 @@ public: SVG::SVGGeometryElement& dom_node() { return static_cast(SVGGraphicsBox::dom_node()); } SVG::SVGGeometryElement const& dom_node() const { return static_cast(SVGGraphicsBox::dom_node()); } - Optional layout_transform(Gfx::AffineTransform additional_svg_transform) const; - virtual JS::GCPtr create_paintable() const override; private: - CSSPixelPoint viewbox_origin() const; - virtual bool is_svg_geometry_box() const final { return true; } }; diff --git a/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp b/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp index 0a3d8eac8bb..2f6737cbda1 100644 --- a/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp +++ b/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.cpp @@ -30,15 +30,11 @@ 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()) + if (!result.has_value() || !path_data().has_value()) + return {}; + auto transformed_bounding_box = path_data()->svg_to_css_pixels_transform().map_to_quad(path_data()->computed_path().bounding_box()); + if (!transformed_bounding_box.contains(position.to_type())) return {}; - auto& geometry_element = layout_box().dom_node(); - if (auto transform = layout_box().layout_transform({}); transform.has_value()) { - auto transformed_bounding_box = transform->map_to_quad( - const_cast(geometry_element).get_path().bounding_box()); - if (!transformed_bounding_box.contains(position.to_type())) - return {}; - } return result; } @@ -56,7 +52,7 @@ static Gfx::Painter::WindingRule to_gfx_winding_rule(SVG::FillRule fill_rule) void SVGGeometryPaintable::paint(PaintContext& context, PaintPhase phase) const { - if (!is_visible()) + if (!is_visible() || !path_data().has_value()) return; SVGGraphicsPaintable::paint(context, phase); @@ -73,17 +69,10 @@ void SVGGeometryPaintable::paint(PaintContext& context, PaintPhase phase) const RecordingPainterStateSaver save_painter { context.painter() }; auto offset = context.floored_device_point(svg_element_rect.location()).to_type().to_type(); - auto maybe_view_box = geometry_element.view_box(); - auto transform = layout_box().layout_transform(context.svg_transform()); - if (!transform.has_value()) - return; - - auto css_scale = context.device_pixels_per_css_pixel(); - auto paint_transform = Gfx::AffineTransform {}.scale(css_scale, css_scale).multiply(*transform); - auto const& original_path = const_cast(geometry_element).get_path(); - Gfx::Path path = original_path.copy_transformed(paint_transform); + 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); // Fills are computed as though all subpaths are closed (https://svgwg.org/svg2-draft/painting.html#FillProperties) auto closed_path = [&] { @@ -106,7 +95,7 @@ void SVGGeometryPaintable::paint(PaintContext& context, PaintPhase phase) const SVG::SVGPaintContext paint_context { .viewport = svg_viewport, - .path_bounding_box = original_path.bounding_box(), + .path_bounding_box = path_data()->computed_path().bounding_box(), .transform = paint_transform }; diff --git a/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.h b/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.h index 6da035091d9..db3f5b9a04d 100644 --- a/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.h +++ b/Userland/Libraries/LibWeb/Painting/SVGGeometryPaintable.h @@ -15,6 +15,41 @@ 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; @@ -23,8 +58,17 @@ public: Layout::SVGGeometryBox const& layout_box() const; + void set_path_data(PathData path_data) + { + m_path_data = move(path_data); + } + + Optional const& path_data() const { return m_path_data; } + protected: SVGGeometryPaintable(Layout::SVGGeometryBox const&); + + Optional m_path_data = {}; }; }