1
0
mirror of https://github.com/SerenityOS/serenity synced 2024-07-09 05:00:46 +00:00

LibWeb: Move clip rect calculation to happen before painting

With this change, clip rectangles for boxes with hidden overflow or the
clip property are no longer calculated during the recording of painting
commands. Instead, it has moved to the "pre-paint" phase, along with
the assignment of scrolling offsets, and works in the following way:

1. The paintable tree is traversed to collect all paintable boxes that
   have hidden overflow or use the CSS clip property. For each of these
   boxes, the "final" clip rectangle is calculated by intersecting clip
   rectangles in the containing block chain for a box.
2. The paintable tree is traversed another time, and a clip rectangle
   is assigned for each paintable box contained by a node with hidden
   overflow or the clip property.

This way, clipping becomes much easier during the painting commands
recording phase, as it only concerns the use of already assigned clip
rectangles. The same approach is applied to handle scrolling offsets.

Also, clip rectangle calculation is now implemented more correctly, as
we no longer stop at the stacking context boundary while intersecting
clip rectangles in the containing block chain.

Fixes:
https://github.com/SerenityOS/serenity/issues/22932
https://github.com/SerenityOS/serenity/issues/22883
https://github.com/SerenityOS/serenity/issues/22679
https://github.com/SerenityOS/serenity/issues/22534
This commit is contained in:
Aliaksandr Kalenik 2024-01-26 21:47:26 +01:00 committed by Andreas Kling
parent eaf9a56c10
commit 0bf82f748f
21 changed files with 365 additions and 166 deletions

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<link rel="match" href="reference/abspos-escapes-scroll-container-ref.html" />
<style>
#scrollable-box {
width: 300px;
height: 200px;
overflow: auto;
border: 1px solid black;
}
.content {
height: 600px;
}
.absolute-element {
position: absolute;
top: 10px;
right: 10px;
background-color: yellow;
}
</style>
<div id="scrollable-box">
<div class="content"><span><span>Content that makes the container scrollable...</span></span></div>
<div class="absolute-element">
Abspos Element
</div>
</div>
<script>
const scrollContainer = document.getElementById("scrollable-box");
scrollContainer.scrollTop = 500;
</script>

View File

@ -0,0 +1,23 @@
<!doctype html>
<link rel="match" href="reference/overflow-hidden-3-ref.html" />
<style>
body {
width: 100px;
height: 100px;
}
#outer {
width: 200px;
height: 100px;
overflow: hidden;
}
#inner {
position: relative;
z-index: 0;
overflow: hidden;
}
#green {
width: 200px;
height: 200px;
background: green;
}
</style><body><div id="outer"><div id="inner"><div id="green">

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<link rel="match" href="reference/overflow-hidden-4-ref.html" />
<style>
* {
outline: black solid 1px;
}
.outer {
background: pink;
width: 100px;
height: 100px;
overflow: hidden;
}
.middle {
transform: perspective(1200px);
background: orange;
}
.inner {
width: 300px;
height: 300px;
background: magenta;
overflow: hidden;
}
.red {
background: red;
width: 300px;
height: 300px;
}
</style><body><div class="outer"><div class="middle"><div class="inner"><div class="red">

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<head>
<link rel="match" href="reference/overflow-hidden-5-ref.html" />
<style type="text/css">
body {
margin: 0;
}
.wrapper {
margin-top: 100px;
margin-left: 100px;
width: 900px;
border: 5px solid magenta;
}
.container {
overflow: hidden;
display: block;
height: 400px;
}
.box {
background-color: darkblue;
width: 256px;
height: 256px;
transform: translate3d(-50px, -50px, 0px);
}
</style>
</head>
<div class="wrapper"><div class="container"><div class="box"></div></div></div>

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<style>
#scrollable-box {
width: 300px;
height: 200px;
border: 1px solid black;
}
.absolute-element {
position: absolute;
top: 10px;
right: 10px;
background-color: yellow;
}
</style>
<div id="scrollable-box"></div>
<div class="absolute-element">Abspos Element</div>

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<link rel="match" href="reference/overflow-hidden-3-ref.html" />
<style>
.green {
width: 200px;
height: 100px;
background: green;
}
</style><body><div class="green">

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<link rel="match" href="reference/overflow-hidden-4-ref.html" />
<style>
* {
outline: black solid 1px;
}
.red {
background: red;
width: 100px;
height: 100px;
}
</style><body><div class="red">

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<head>
<link rel="match" href="reference/overflow-hidden-5-ref.html" />
<style type="text/css">
body {
margin: 0;
}
.wrapper {
margin-top: 100px;
margin-left: 100px;
border: 5px solid magenta;
width: 900px;
height: 400px;
}
.box {
background-color: darkblue;
width: 206px;
height: 206px;
}
</style>
</head>
<div class="wrapper"><div class="box"></div></div>

View File

@ -2082,8 +2082,10 @@ void Navigable::paint(Painting::RecordingPainter& recording_painter, PaintConfig
context.set_should_paint_overlay(config.paint_overlay);
context.set_has_focus(config.has_focus);
HashMap<Painting::PaintableBox const*, Painting::ViewportPaintable::ScrollFrame> scroll_frames;
if (is_traversable()) {
document->paintable()->collect_scroll_frames(context);
document->paintable()->assign_scroll_frame_ids(scroll_frames);
document->paintable()->assign_clip_rectangles(context);
}
document->paintable()->paint_all_phases(context);
@ -2091,11 +2093,11 @@ void Navigable::paint(Painting::RecordingPainter& recording_painter, PaintConfig
// FIXME: Support scrollable frames inside iframes.
if (is_traversable()) {
Vector<Gfx::IntPoint> scroll_offsets_by_frame_id;
scroll_offsets_by_frame_id.resize(context.scroll_frames().size());
for (auto [_, scrollable_frame] : context.scroll_frames())
scroll_offsets_by_frame_id[scrollable_frame.id] = context.rounded_device_point(
scrollable_frame.offset)
.to_type<int>();
scroll_offsets_by_frame_id.resize(scroll_frames.size());
for (auto [_, scrollable_frame] : scroll_frames) {
auto scroll_offset = context.rounded_device_point(scrollable_frame.offset).to_type<int>();
scroll_offsets_by_frame_id[scrollable_frame.id] = scroll_offset;
}
recording_painter.apply_scroll_offsets(scroll_offsets_by_frame_id);
}
}

View File

@ -10,7 +10,7 @@
namespace Web::Painting {
Gfx::AntiAliasingPainter::CornerRadius BorderRadiusData::as_corner(PaintContext& context) const
Gfx::AntiAliasingPainter::CornerRadius BorderRadiusData::as_corner(PaintContext const& context) const
{
return Gfx::AntiAliasingPainter::CornerRadius {
context.floored_device_pixels(horizontal_radius).value(),

View File

@ -17,7 +17,7 @@ struct BorderRadiusData {
CSSPixels horizontal_radius { 0 };
CSSPixels vertical_radius { 0 };
Gfx::AntiAliasingPainter::CornerRadius as_corner(PaintContext& context) const;
Gfx::AntiAliasingPainter::CornerRadius as_corner(PaintContext const& context) const;
inline operator bool() const
{
@ -71,7 +71,7 @@ struct BorderRadiiData {
shrink(-top, -right, -bottom, -left);
}
inline CornerRadii as_corners(PaintContext& context) const
inline CornerRadii as_corners(PaintContext const& context) const
{
return CornerRadii {
top_left.as_corner(context),

View File

@ -27,6 +27,26 @@ Layout::InlineNode const& InlinePaintable::layout_node() const
return static_cast<Layout::InlineNode const&>(Paintable::layout_node());
}
void InlinePaintable::before_paint(PaintContext& context, PaintPhase) const
{
if (m_scroll_frame_id.has_value()) {
context.recording_painter().save();
context.recording_painter().set_scroll_frame_id(m_scroll_frame_id.value());
}
if (m_clip_rect.has_value()) {
context.recording_painter().save();
context.recording_painter().add_clip_rect(context.enclosing_device_rect(*m_clip_rect).to_type<int>());
}
}
void InlinePaintable::after_paint(PaintContext& context, PaintPhase) const
{
if (m_clip_rect.has_value())
context.recording_painter().restore();
if (m_scroll_frame_id.has_value())
context.recording_painter().restore();
}
void InlinePaintable::paint(PaintContext& context, PaintPhase phase) const
{
auto& painter = context.recording_painter();

View File

@ -20,6 +20,9 @@ public:
virtual void paint(PaintContext&, PaintPhase) const override;
virtual void before_paint(PaintContext& context, PaintPhase) const override;
virtual void after_paint(PaintContext& context, PaintPhase) const override;
Layout::InlineNode const& layout_node() const;
auto const& box_model() const { return layout_node().box_model(); }
@ -34,12 +37,18 @@ public:
void set_box_shadow_data(Vector<ShadowData>&& box_shadow_data) { m_box_shadow_data = move(box_shadow_data); }
Vector<ShadowData> const& box_shadow_data() const { return m_box_shadow_data; }
void set_scroll_frame_id(int id) { m_scroll_frame_id = id; }
void set_clip_rect(Optional<CSSPixelRect> rect) { m_clip_rect = rect; }
private:
InlinePaintable(Layout::InlineNode const&);
template<typename Callback>
void for_each_fragment(Callback) const;
Optional<int> m_scroll_frame_id;
Optional<CSSPixelRect> m_clip_rect;
Vector<ShadowData> m_box_shadow_data;
Vector<PaintableFragment> m_fragments;
};

View File

@ -74,12 +74,6 @@ public:
u32 allocate_corner_clipper_id() { return m_next_corner_clipper_id++; }
struct ScrollFrame {
i32 id { -1 };
CSSPixelPoint offset;
};
HashMap<Painting::PaintableBox const*, ScrollFrame>& scroll_frames() { return m_scroll_frames; }
private:
Painting::RecordingPainter& m_recording_painter;
Palette m_palette;
@ -90,7 +84,6 @@ private:
bool m_focus { false };
Gfx::AffineTransform m_svg_transform;
u32 m_next_corner_clipper_id { 0 };
HashMap<Painting::PaintableBox const*, ScrollFrame> m_scroll_frames;
};
}

View File

@ -148,19 +148,6 @@ void Paintable::set_needs_display() const
});
}
PaintableBox const* Paintable::nearest_scrollable_ancestor_within_stacking_context() const
{
auto* ancestor = parent();
while (ancestor) {
if (ancestor->stacking_context())
return nullptr;
if (ancestor->is_paintable_box() && static_cast<PaintableBox const*>(ancestor)->has_scrollable_overflow())
return static_cast<PaintableBox const*>(ancestor);
ancestor = ancestor->parent();
}
return nullptr;
}
CSSPixelPoint Paintable::box_type_agnostic_position() const
{
if (is_paintable_box())

View File

@ -186,8 +186,6 @@ public:
DOM::Document const& document() const { return layout_node().document(); }
DOM::Document& document() { return layout_node().document(); }
PaintableBox const* nearest_scrollable_ancestor_within_stacking_context() const;
CSSPixelPoint box_type_agnostic_position() const;
protected:

View File

@ -121,6 +121,26 @@ CSSPixelRect PaintableBox::compute_absolute_rect() const
return rect;
}
CSSPixelRect PaintableBox::compute_absolute_padding_rect_with_css_transform_applied() const
{
CSSPixelRect rect { offset(), content_size() };
for (auto const* block = containing_block(); block; block = block->containing_block()) {
auto offset = block->paintable_box()->offset();
auto affine_transform = Gfx::extract_2d_affine_transform(block->paintable_box()->transform());
offset.translate_by(affine_transform.translation().to_type<CSSPixels>());
rect.translate_by(offset);
}
auto affine_transform = Gfx::extract_2d_affine_transform(transform());
rect.translate_by(affine_transform.translation().to_type<CSSPixels>());
CSSPixelRect padding_rect;
padding_rect.set_x(rect.x() - box_model().padding.left);
padding_rect.set_width(content_width() + box_model().padding.left + box_model().padding.right);
padding_rect.set_y(rect.y() - box_model().padding.top);
padding_rect.set_height(content_height() + box_model().padding.top + box_model().padding.bottom);
return padding_rect;
}
CSSPixelRect PaintableBox::absolute_rect() const
{
if (!m_absolute_rect.has_value())
@ -171,11 +191,8 @@ void PaintableBox::before_paint(PaintContext& context, [[maybe_unused]] PaintPha
if (!is_visible())
return;
auto clip_rect = get_clip_rect();
if (clip_rect.has_value()) {
context.recording_painter().save();
context.recording_painter().add_clip_rect(clip_rect->to_type<int>());
}
apply_scroll_offset(context, phase);
apply_clip_overflow_rect(context, phase);
}
void PaintableBox::after_paint(PaintContext& context, [[maybe_unused]] PaintPhase phase) const
@ -183,8 +200,8 @@ void PaintableBox::after_paint(PaintContext& context, [[maybe_unused]] PaintPhas
if (!is_visible())
return;
if (get_clip_rect().has_value())
context.recording_painter().restore();
clear_clip_overflow_rect(context, phase);
reset_scroll_offset(context, phase);
}
void PaintableBox::paint(PaintContext& context, PaintPhase phase) const
@ -358,110 +375,63 @@ BorderRadiiData PaintableBox::normalized_border_radii_data(ShrinkRadiiForBorders
return border_radii_data;
}
Optional<CSSPixelRect> PaintableBox::calculate_overflow_clipped_rect() const
{
if (layout_node().is_viewport()) {
return {};
}
if (!m_clip_rect.has_value()) {
// NOTE: stacking context should not be crossed while aggregating rectangle to
// clip `overflow: hidden` because intersecting rectangles with different
// transforms doesn't make sense
// TODO: figure out if there are cases when stacking context should be
// crossed to calculate correct clip rect
if (!stacking_context() && containing_block() && containing_block()->paintable_box()) {
m_clip_rect = containing_block()->paintable_box()->calculate_overflow_clipped_rect();
}
auto overflow_x = computed_values().overflow_x();
auto overflow_y = computed_values().overflow_y();
if (overflow_x != CSS::Overflow::Visible && overflow_y != CSS::Overflow::Visible) {
if (m_clip_rect.has_value()) {
m_clip_rect->intersect(absolute_padding_box_rect());
} else {
m_clip_rect = absolute_padding_box_rect();
}
}
}
return m_clip_rect;
}
void PaintableBox::apply_scroll_offset(PaintContext& context, PaintPhase) const
{
if (context.scroll_frames().contains(this)) {
if (m_scroll_frame_id.has_value()) {
context.recording_painter().save();
context.recording_painter().set_scroll_frame_id(context.scroll_frames().get(this)->id);
context.recording_painter().set_scroll_frame_id(m_scroll_frame_id.value());
}
}
void PaintableBox::reset_scroll_offset(PaintContext& context, PaintPhase) const
{
if (context.scroll_frames().contains(this)) {
if (m_scroll_frame_id.has_value())
context.recording_painter().restore();
}
}
void PaintableBox::apply_clip_overflow_rect(PaintContext& context, PaintPhase phase) const
{
if (!AK::first_is_one_of(phase, PaintPhase::Background, PaintPhase::Border, PaintPhase::Foreground))
if (!AK::first_is_one_of(phase, PaintPhase::Background, PaintPhase::Border, PaintPhase::Foreground, PaintPhase::Outline))
return;
// FIXME: Support more overflow variations.
auto clip_rect = this->calculate_overflow_clipped_rect();
auto overflow_x = computed_values().overflow_x();
auto overflow_y = computed_values().overflow_y();
if (m_clip_rect.has_value()) {
auto overflow_clip_rect = m_clip_rect.value();
for (auto const* ancestor = &this->layout_box(); ancestor; ancestor = ancestor->containing_block()) {
auto affine_transform = Gfx::extract_2d_affine_transform(ancestor->paintable_box()->transform());
if (!affine_transform.is_identity()) {
// NOTE: Because the painting command executor applies CSS transform of the nearest stacking context
// and the m_clip_rect is determined considering CSS transforms, here transform of the nearest
// stacking context need to be compensated.
// This adjustment ensures the transform is accounted for just once.
overflow_clip_rect.translate_by(-affine_transform.translation().to_type<CSSPixels>());
break;
}
}
auto css_clip_property = get_clip_rect();
if (css_clip_property.has_value()) {
if (clip_rect.has_value())
clip_rect->intersect(css_clip_property.value());
else
clip_rect = css_clip_property.value();
}
if (!clip_rect.has_value())
return;
if (!m_clipping_overflow) {
context.recording_painter().save();
context.recording_painter().add_clip_rect(context.enclosing_device_rect(*clip_rect).to_type<int>());
m_clipping_overflow = true;
}
if (!clip_rect->is_empty() && overflow_y == CSS::Overflow::Hidden && overflow_x == CSS::Overflow::Hidden) {
auto border_radii_data = normalized_border_radii_data(ShrinkRadiiForBorders::Yes);
CornerRadii corner_radii {
.top_left = border_radii_data.top_left.as_corner(context),
.top_right = border_radii_data.top_right.as_corner(context),
.bottom_right = border_radii_data.bottom_right.as_corner(context),
.bottom_left = border_radii_data.bottom_left.as_corner(context)
};
if (corner_radii.has_any_radius()) {
context.recording_painter().save();
context.recording_painter().add_clip_rect(context.enclosing_device_rect(overflow_clip_rect).to_type<int>());
if (m_corner_clip_radii.has_value()) {
VERIFY(!m_corner_clipper_id.has_value());
m_corner_clipper_id = context.allocate_corner_clipper_id();
context.recording_painter().sample_under_corners(*m_corner_clipper_id, corner_radii, context.rounded_device_rect(*clip_rect).to_type<int>(), CornerClip::Outside);
context.recording_painter().sample_under_corners(*m_corner_clipper_id, *m_corner_clip_radii, context.rounded_device_rect(overflow_clip_rect).to_type<int>(), CornerClip::Outside);
}
}
}
void PaintableBox::clear_clip_overflow_rect(PaintContext& context, PaintPhase phase) const
{
if (!AK::first_is_one_of(phase, PaintPhase::Background, PaintPhase::Border, PaintPhase::Foreground))
if (!AK::first_is_one_of(phase, PaintPhase::Background, PaintPhase::Border, PaintPhase::Foreground, PaintPhase::Outline))
return;
// FIXME: Support more overflow variations.
if (m_clipping_overflow) {
context.recording_painter().restore();
m_clipping_overflow = false;
}
if (m_corner_clipper_id.has_value()) {
VERIFY(m_corner_clipper_id.has_value());
auto clip_rect = this->calculate_overflow_clipped_rect();
context.recording_painter().blit_corner_clipping(*m_corner_clipper_id, context.rounded_device_rect(*clip_rect).to_type<int>());
m_corner_clipper_id = {};
if (m_corner_clip_radii.has_value()) {
VERIFY(m_corner_clipper_id.has_value());
context.recording_painter().blit_corner_clipping(*m_corner_clipper_id, context.rounded_device_rect(*m_clip_rect).to_type<int>());
m_corner_clipper_id = {};
}
context.recording_painter().restore();
}
}

View File

@ -117,8 +117,6 @@ public:
return m_overflow_data->scrollable_overflow_rect;
}
Optional<CSSPixelRect> calculate_overflow_clipped_rect() const;
void set_overflow_data(OverflowData data) { m_overflow_data = move(data); }
DOM::Node const* dom_node() const { return layout_box().dom_node(); }
@ -191,11 +189,17 @@ public:
void set_transform_origin(CSSPixelPoint transform_origin) { m_transform_origin = transform_origin; }
CSSPixelPoint const& transform_origin() const { return m_transform_origin; }
protected:
explicit PaintableBox(Layout::Box const&);
CSSPixelRect compute_absolute_padding_rect_with_css_transform_applied() const;
Optional<CSSPixelRect> get_clip_rect() const;
void set_clip_rect(Optional<CSSPixelRect> rect) { m_clip_rect = rect; }
void set_scroll_frame_id(int id) { m_scroll_frame_id = id; }
void set_corner_clip_radii(CornerRadii const& corner_radii) { m_corner_clip_radii = corner_radii; }
protected:
explicit PaintableBox(Layout::Box const&);
virtual void paint_border(PaintContext&) const;
virtual void paint_backdrop_filter(PaintContext&) const;
virtual void paint_background(PaintContext&) const;
@ -215,11 +219,13 @@ private:
Optional<CSSPixelRect> mutable m_absolute_rect;
Optional<CSSPixelRect> mutable m_absolute_paint_rect;
Optional<CSSPixelRect> mutable m_clip_rect;
mutable bool m_clipping_overflow { false };
mutable Optional<u32> m_corner_clipper_id;
Optional<CSSPixelRect> m_clip_rect;
Optional<int> m_scroll_frame_id;
Optional<CornerRadii> m_corner_clip_radii;
Optional<BordersDataWithElementKind> m_override_borders_data;
Optional<TableCellCoordinates> m_table_cell_coordinates;

View File

@ -88,9 +88,7 @@ void StackingContext::paint_node_as_stacking_context(Paintable const& paintable,
void StackingContext::paint_descendants(PaintContext& context, Paintable const& paintable, StackingContextPaintPhase phase)
{
paintable.apply_scroll_offset(context, to_paint_phase(phase));
paintable.before_children_paint(context, to_paint_phase(phase));
paintable.apply_clip_overflow_rect(context, to_paint_phase(phase));
paintable.for_each_child([&context, phase](auto& child) {
auto* stacking_context = child.stacking_context();
@ -167,9 +165,7 @@ void StackingContext::paint_descendants(PaintContext& context, Paintable const&
}
});
paintable.clear_clip_overflow_rect(context, to_paint_phase(phase));
paintable.after_children_paint(context, to_paint_phase(phase));
paintable.reset_scroll_offset(context, to_paint_phase(phase));
}
void StackingContext::paint_child(PaintContext& context, StackingContext const& child)
@ -178,16 +174,8 @@ void StackingContext::paint_child(PaintContext& context, StackingContext const&
if (parent_paintable)
parent_paintable->before_children_paint(context, PaintPhase::Foreground);
PaintableBox const* nearest_scrollable_ancestor = child.paintable().nearest_scrollable_ancestor_within_stacking_context();
if (nearest_scrollable_ancestor)
nearest_scrollable_ancestor->apply_scroll_offset(context, PaintPhase::Foreground);
child.paint(context);
if (nearest_scrollable_ancestor)
nearest_scrollable_ancestor->reset_scroll_offset(context, PaintPhase::Foreground);
if (parent_paintable)
parent_paintable->after_children_paint(context, PaintPhase::Foreground);
}
@ -227,21 +215,12 @@ void StackingContext::paint_internal(PaintContext& context) const
: TraversalDecision::Continue;
}
// Apply scroll offset of nearest scrollable ancestor before painting the positioned descendant.
PaintableBox const* nearest_scrollable_ancestor = paintable.nearest_scrollable_ancestor_within_stacking_context();
if (nearest_scrollable_ancestor)
nearest_scrollable_ancestor->apply_scroll_offset(context, PaintPhase::Foreground);
// At this point, `paintable_box` is a positioned descendant with z-index: auto.
// FIXME: This is basically duplicating logic found elsewhere in this same function. Find a way to make this more elegant.
auto exit_decision = TraversalDecision::Continue;
auto* parent_paintable = paintable.parent();
if (parent_paintable)
parent_paintable->before_children_paint(context, PaintPhase::Foreground);
auto containing_block = paintable.containing_block();
auto* containing_block_paintable = containing_block ? containing_block->paintable() : nullptr;
if (containing_block_paintable)
containing_block_paintable->apply_clip_overflow_rect(context, PaintPhase::Foreground);
if (auto* child = paintable.stacking_context()) {
paint_child(context, *child);
exit_decision = TraversalDecision::SkipChildrenAndContinue;
@ -250,11 +229,6 @@ void StackingContext::paint_internal(PaintContext& context) const
}
if (parent_paintable)
parent_paintable->after_children_paint(context, PaintPhase::Foreground);
if (containing_block_paintable)
containing_block_paintable->clear_clip_overflow_rect(context, PaintPhase::Foreground);
if (nearest_scrollable_ancestor)
nearest_scrollable_ancestor->reset_scroll_offset(context, PaintPhase::Foreground);
return exit_decision;
});
@ -263,22 +237,8 @@ void StackingContext::paint_internal(PaintContext& context) const
// (smallest first) then tree order. (Step 9)
// NOTE: This doesn't check if a descendant is positioned as modern CSS allows for alternative methods to establish stacking contexts.
for (auto* child : m_children) {
PaintableBox const* nearest_scrollable_ancestor = child->paintable().nearest_scrollable_ancestor_within_stacking_context();
if (nearest_scrollable_ancestor)
nearest_scrollable_ancestor->apply_scroll_offset(context, PaintPhase::Foreground);
auto containing_block = child->paintable().containing_block();
auto const* containing_block_paintable = containing_block ? containing_block->paintable() : nullptr;
if (containing_block_paintable)
containing_block_paintable->apply_clip_overflow_rect(context, PaintPhase::Foreground);
if (child->paintable().computed_values().z_index().has_value() && child->paintable().computed_values().z_index().value() >= 1)
paint_child(context, *child);
if (containing_block_paintable)
containing_block_paintable->clear_clip_overflow_rect(context, PaintPhase::Foreground);
if (nearest_scrollable_ancestor)
nearest_scrollable_ancestor->reset_scroll_offset(context, PaintPhase::Foreground);
}
paint_node(paintable(), context, PaintPhase::Outline);

View File

@ -56,19 +56,97 @@ void ViewportPaintable::paint_all_phases(PaintContext& context)
stacking_context()->paint(context);
}
void ViewportPaintable::collect_scroll_frames(PaintContext& context) const
void ViewportPaintable::assign_scroll_frame_ids(HashMap<Painting::PaintableBox const*, ScrollFrame>& scroll_frames) const
{
i32 next_id = 0;
// Collect scroll frames with their offsets (accumulated offset for nested scroll frames).
for_each_in_subtree_of_type<PaintableBox>([&](auto const& paintable_box) {
if (paintable_box.has_scrollable_overflow()) {
auto offset = paintable_box.scroll_offset();
auto ancestor = paintable_box.parent();
auto ancestor = paintable_box.containing_block();
while (ancestor) {
if (ancestor->is_paintable_box() && static_cast<PaintableBox const*>(ancestor)->has_scrollable_overflow())
offset.translate_by(static_cast<PaintableBox const*>(ancestor)->scroll_offset());
ancestor = ancestor->parent();
if (ancestor->paintable()->is_paintable_box() && static_cast<PaintableBox const*>(ancestor->paintable())->has_scrollable_overflow())
offset.translate_by(static_cast<PaintableBox const*>(ancestor->paintable())->scroll_offset());
ancestor = ancestor->containing_block();
}
scroll_frames.set(&paintable_box, { .id = next_id++, .offset = -offset });
}
return TraversalDecision::Continue;
});
// Assign scroll frame id to all paintables contained in a scroll frame.
for_each_in_subtree([&](auto const& paintable) {
for (auto block = paintable.containing_block(); block; block = block->containing_block()) {
auto const& block_paintable_box = *block->paintable_box();
if (auto scroll_frame_id = scroll_frames.get(&block_paintable_box); scroll_frame_id.has_value()) {
if (paintable.is_paintable_box()) {
auto const& paintable_box = static_cast<PaintableBox const&>(paintable);
const_cast<PaintableBox&>(paintable_box).set_scroll_frame_id(scroll_frame_id->id);
} else if (paintable.is_inline_paintable()) {
auto const& inline_paintable = static_cast<InlinePaintable const&>(paintable);
const_cast<InlinePaintable&>(inline_paintable).set_scroll_frame_id(scroll_frame_id->id);
}
break;
}
}
return TraversalDecision::Continue;
});
}
void ViewportPaintable::assign_clip_rectangles(PaintContext const& context)
{
HashMap<Paintable const*, CSSPixelRect> clip_rects;
// Calculate clip rects for all boxes that either have hidden overflow or a CSS clip property.
for_each_in_subtree_of_type<PaintableBox>([&](auto const& paintable_box) {
auto overflow_x = paintable_box.computed_values().overflow_x();
auto overflow_y = paintable_box.computed_values().overflow_y();
// Start from CSS clip property if it exists.
Optional<CSSPixelRect> clip_rect = paintable_box.get_clip_rect();
// FIXME: Support overflow clip in one direction only.
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();
for (auto block = &paintable_box.layout_box(); !block->is_viewport(); block = block->containing_block()) {
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)
overflow_clip_rect.intersect(block_paintable_box.compute_absolute_padding_rect_with_css_transform_applied());
if (auto css_clip_property_rect = block->paintable_box()->get_clip_rect(); css_clip_property_rect.has_value())
overflow_clip_rect.intersect(css_clip_property_rect.value());
}
clip_rect = overflow_clip_rect;
}
if (clip_rect.has_value())
clip_rects.set(&paintable_box, *clip_rect);
return TraversalDecision::Continue;
});
// Assign clip rects to all paintable boxes contained by a box with a hidden overflow or a CSS clip property.
for_each_in_subtree_of_type<PaintableBox>([&](auto const& paintable_box) {
Optional<CSSPixelRect> clip_rect = paintable_box.get_clip_rect();
for (auto block = paintable_box.containing_block(); block; block = block->containing_block()) {
if (auto containing_block_clip_rect = clip_rects.get(block->paintable()); containing_block_clip_rect.has_value()) {
auto border_radii_data = block->paintable_box()->normalized_border_radii_data(ShrinkRadiiForBorders::Yes);
CornerRadii corner_radii = border_radii_data.as_corners(context);
if (corner_radii.has_any_radius()) {
// FIXME: Border radii of all boxes in containing block chain should be taken into account instead of just the closest one.
const_cast<PaintableBox&>(paintable_box).set_corner_clip_radii(corner_radii);
}
clip_rect = *containing_block_clip_rect;
break;
}
}
const_cast<PaintableBox&>(paintable_box).set_clip_rect(clip_rect);
return TraversalDecision::Continue;
});
// Assign clip rects to all inline paintables contained by a box with hidden overflow or a CSS clip property.
for_each_in_subtree_of_type<InlinePaintable>([&](auto const& paintable_box) {
for (auto block = paintable_box.containing_block(); block; block = block->containing_block()) {
if (auto clip_rect = clip_rects.get(block->paintable()); clip_rect.has_value()) {
const_cast<InlinePaintable&>(paintable_box).set_clip_rect(clip_rect);
break;
}
context.scroll_frames().set(&paintable_box, { .id = next_id++, .offset = -offset });
}
return TraversalDecision::Continue;
});

View File

@ -20,7 +20,12 @@ public:
void paint_all_phases(PaintContext&);
void build_stacking_context_tree_if_needed();
void collect_scroll_frames(PaintContext&) const;
struct ScrollFrame {
i32 id { -1 };
CSSPixelPoint offset;
};
void assign_scroll_frame_ids(HashMap<Painting::PaintableBox const*, ScrollFrame>&) const;
void assign_clip_rectangles(PaintContext const&);
private:
void build_stacking_context_tree();