From bbc89a383de5584fd6dccc06bdffad8d87119c13 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Wed, 31 Jul 2024 22:26:43 +0300 Subject: [PATCH] LibWeb: Fix overflow clip when "complicated" CSS transform is used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overflow clipping is currently implemented as: 1. Create clip frame for each box with hidden overflow 2. Calculate clip rect for each clip frame by intersecting padding boxes of all boxes with hidden overflow in containing block chain 3. Assign enclosing clip frame (closest clip frame in containing block chain) to each PaintableBox 4. Apply clip rect of enclosing clip frame in Paintable::before_paint() It breaks when any CSS transform other than simple translation is lying between box with hidden overflow and a clipped box, because clip rectangle will be applied when transform has already changed. The fix is implemented by relying on the following rule: "For elements whose layout is governed by the CSS box model, any value other than none for the transform also causes the element to establish a containing block for all descendants." It means everything nested into a stacking context with CSS transform can't escape its clip, so it's safe to apply its clip for all children. --- ...scrollable-contains-rotated-boxes-ref.html | 46 ++++++++++++++++++ .../scrollable-contains-rotated-boxes.html | 47 +++++++++++++++++++ Userland/Libraries/LibWeb/Layout/Node.h | 2 + .../Painting/ClippableAndScrollable.cpp | 12 +---- .../LibWeb/Painting/PaintableBox.cpp | 15 +++--- .../Libraries/LibWeb/Painting/PaintableBox.h | 4 +- .../LibWeb/Painting/StackingContext.cpp | 6 +++ .../LibWeb/Painting/ViewportPaintable.cpp | 10 +++- 8 files changed, 122 insertions(+), 20 deletions(-) create mode 100644 Tests/LibWeb/Ref/reference/scrollable-contains-rotated-boxes-ref.html create mode 100644 Tests/LibWeb/Ref/scrollable-contains-rotated-boxes.html diff --git a/Tests/LibWeb/Ref/reference/scrollable-contains-rotated-boxes-ref.html b/Tests/LibWeb/Ref/reference/scrollable-contains-rotated-boxes-ref.html new file mode 100644 index 00000000000..641a33c0d80 --- /dev/null +++ b/Tests/LibWeb/Ref/reference/scrollable-contains-rotated-boxes-ref.html @@ -0,0 +1,46 @@ + + + + + + +
+
+
+
+
+
+
+
+
+ + diff --git a/Tests/LibWeb/Ref/scrollable-contains-rotated-boxes.html b/Tests/LibWeb/Ref/scrollable-contains-rotated-boxes.html new file mode 100644 index 00000000000..1be3336f657 --- /dev/null +++ b/Tests/LibWeb/Ref/scrollable-contains-rotated-boxes.html @@ -0,0 +1,47 @@ + + + + + + +
+
+
+
+
+
+
+
+
+ + diff --git a/Userland/Libraries/LibWeb/Layout/Node.h b/Userland/Libraries/LibWeb/Layout/Node.h index 95899e59ba0..5598ab0722f 100644 --- a/Userland/Libraries/LibWeb/Layout/Node.h +++ b/Userland/Libraries/LibWeb/Layout/Node.h @@ -172,6 +172,8 @@ public: // https://www.w3.org/TR/CSS22/visuren.html#positioning-scheme bool is_in_flow() const { return !is_out_of_flow(); } + bool has_css_transform() const { return computed_values().transformations().size() > 0; } + protected: Node(DOM::Document&, DOM::Node*); diff --git a/Userland/Libraries/LibWeb/Painting/ClippableAndScrollable.cpp b/Userland/Libraries/LibWeb/Painting/ClippableAndScrollable.cpp index cb99bf4ea18..40ae66f8b96 100644 --- a/Userland/Libraries/LibWeb/Painting/ClippableAndScrollable.cpp +++ b/Userland/Libraries/LibWeb/Painting/ClippableAndScrollable.cpp @@ -24,16 +24,8 @@ Optional ClippableAndScrollable::enclosing_scroll_frame_offset() Optional ClippableAndScrollable::clip_rect() const { - if (m_enclosing_clip_frame) { - auto rect = m_enclosing_clip_frame->rect(); - // NOTE: Since the painting command executor applies a CSS transform and the clip rect is calculated - // with this transform taken into account, we need to remove the transform from the clip rect. - // Otherwise, the transform will be applied twice to the clip rect. - // Similarly, for hit-testing, the transform must be removed from the clip rectangle since the position - // includes the transform. - rect.translate_by(-m_combined_css_transform.translation().to_type()); - return rect; - } + if (m_enclosing_clip_frame) + return m_enclosing_clip_frame->rect(); return {}; } diff --git a/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp b/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp index f6fdf29a312..f38e828f6d7 100644 --- a/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -158,13 +158,12 @@ CSSPixelRect PaintableBox::compute_absolute_rect() const return rect; } -CSSPixelRect PaintableBox::compute_absolute_padding_rect_with_css_transform_applied() const +CSSPixelRect PaintableBox::compute_absolute_padding_rect_with_scroll_offset_applied() const { auto rect = absolute_rect(); auto scroll_offset = this->enclosing_scroll_frame_offset(); if (scroll_offset.has_value()) rect.translate_by(scroll_offset.value()); - rect.translate_by(combined_css_transform().translation().to_type()); CSSPixelRect padding_rect; padding_rect.set_x(rect.x() - box_model().padding.left); @@ -233,7 +232,9 @@ void PaintableBox::before_paint(PaintContext& context, [[maybe_unused]] PaintPha if (!is_visible()) return; - apply_clip_overflow_rect(context, phase); + if (!has_css_transform()) { + apply_clip_overflow_rect(context, phase); + } apply_scroll_offset(context, phase); } @@ -243,7 +244,9 @@ void PaintableBox::after_paint(PaintContext& context, [[maybe_unused]] PaintPhas return; reset_scroll_offset(context, phase); - clear_clip_overflow_rect(context, phase); + if (!has_css_transform()) { + clear_clip_overflow_rect(context, phase); + } } bool PaintableBox::is_scrollable(ScrollDirection direction) const @@ -533,14 +536,12 @@ void PaintableBox::apply_clip_overflow_rect(PaintContext& context, PaintPhase ph context.display_list_recorder().save(); context.display_list_recorder().add_clip_rect(context.enclosing_device_rect(overflow_clip_rect).to_type()); auto const& border_radii_clips = this->border_radii_clips(); - auto const& combined_transform = combined_css_transform(); for (size_t corner_clip_index = 0; corner_clip_index < border_radii_clips.size(); ++corner_clip_index) { auto const& corner_clip = border_radii_clips[corner_clip_index]; auto corners = corner_clip.radii.as_corners(context); if (!corners.has_any_radius()) continue; - auto rect = corner_clip.rect.translated(-combined_transform.translation().to_type()); - context.display_list_recorder().add_rounded_rect_clip(corner_clip.radii.as_corners(context), context.rounded_device_rect(rect).to_type(), CornerClip::Outside); + context.display_list_recorder().add_rounded_rect_clip(corner_clip.radii.as_corners(context), context.rounded_device_rect(corner_clip.rect).to_type(), CornerClip::Outside); } } } diff --git a/Userland/Libraries/LibWeb/Painting/PaintableBox.h b/Userland/Libraries/LibWeb/Painting/PaintableBox.h index b75f9d2c8d7..1ab4ef67187 100644 --- a/Userland/Libraries/LibWeb/Painting/PaintableBox.h +++ b/Userland/Libraries/LibWeb/Painting/PaintableBox.h @@ -113,6 +113,8 @@ public: [[nodiscard]] bool has_scrollable_overflow() const { return m_overflow_data->has_scrollable_overflow; } + bool has_css_transform() const { return computed_values().transformations().size() > 0; } + [[nodiscard]] Optional scrollable_overflow_rect() const { if (!m_overflow_data.has_value()) @@ -199,7 +201,7 @@ public: void set_outline_offset(CSSPixels outline_offset) { m_outline_offset = outline_offset; } CSSPixels outline_offset() const { return m_outline_offset; } - CSSPixelRect compute_absolute_padding_rect_with_css_transform_applied() const; + CSSPixelRect compute_absolute_padding_rect_with_scroll_offset_applied() const; Optional get_clip_rect() const; diff --git a/Userland/Libraries/LibWeb/Painting/StackingContext.cpp b/Userland/Libraries/LibWeb/Painting/StackingContext.cpp index 6aa591d1553..016722619ea 100644 --- a/Userland/Libraries/LibWeb/Painting/StackingContext.cpp +++ b/Userland/Libraries/LibWeb/Painting/StackingContext.cpp @@ -316,12 +316,18 @@ void StackingContext::paint(PaintContext& context) const } } + auto has_css_transform = paintable().is_paintable_box() && paintable_box().has_css_transform(); context.display_list_recorder().save(); + if (has_css_transform) { + paintable_box().apply_clip_overflow_rect(context, PaintPhase::Foreground); + } if (paintable().is_paintable_box() && paintable_box().scroll_frame_id().has_value()) context.display_list_recorder().set_scroll_frame_id(*paintable_box().scroll_frame_id()); context.display_list_recorder().push_stacking_context(push_stacking_context_params); paint_internal(context); context.display_list_recorder().pop_stacking_context(); + if (has_css_transform) + paintable_box().clear_clip_overflow_rect(context, PaintPhase::Foreground); context.display_list_recorder().restore(); } diff --git a/Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp b/Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp index 957c369abdc..cc0be41a4cd 100644 --- a/Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp +++ b/Userland/Libraries/LibWeb/Painting/ViewportPaintable.cpp @@ -119,6 +119,9 @@ void ViewportPaintable::assign_clip_frames() } break; } + if (block->has_css_transform()) { + break; + } } return TraversalDecision::Continue; }); @@ -164,14 +167,17 @@ void ViewportPaintable::refresh_clip_state() }; clip_frame.clear_border_radii_clips(); if (overflow_x != CSS::Overflow::Visible && overflow_y != CSS::Overflow::Visible) { - auto overflow_clip_rect = paintable_box.compute_absolute_padding_rect_with_css_transform_applied(); + auto overflow_clip_rect = paintable_box.compute_absolute_padding_rect_with_scroll_offset_applied(); add_border_radii_clip(overflow_clip_rect, paintable_box.normalized_border_radii_data(ShrinkRadiiForBorders::Yes)); for (auto const* block = &paintable_box.layout_box(); !block->is_viewport(); block = block->containing_block()) { + if (block->has_css_transform()) { + break; + } auto const& block_paintable_box = *block->paintable_box(); auto block_overflow_x = block_paintable_box.computed_values().overflow_x(); auto block_overflow_y = block_paintable_box.computed_values().overflow_y(); if (block_overflow_x != CSS::Overflow::Visible && block_overflow_y != CSS::Overflow::Visible) { - auto rect = block_paintable_box.compute_absolute_padding_rect_with_css_transform_applied(); + auto rect = block_paintable_box.compute_absolute_padding_rect_with_scroll_offset_applied(); overflow_clip_rect.intersect(rect); add_border_radii_clip(rect, block_paintable_box.normalized_border_radii_data(ShrinkRadiiForBorders::Yes)); }