Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 67 additions & 16 deletions Libraries/LibWeb/DOM/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1807,7 +1807,7 @@ void Document::recompute_containing_block_and_derive_abspos_escape_flags(Layout:

// Refreshes every structure derived from committed layout results, shared by the partial and
// full layout paths so neither can forget one.
void Document::after_layout_commit(LayoutTreeChanged layout_tree_changed)
void Document::after_layout_commit(LayoutTreeChanged layout_tree_changed, LayoutCommitScope layout_commit_scope, ReadonlySpan<Layout::Box const*> boxes_needing_eager_overflow_measurement)
{
// NB: Called during layout update.
m_layout_root->invalidate_text_blocks_cache();
Expand All @@ -1821,7 +1821,10 @@ void Document::after_layout_commit(LayoutTreeChanged layout_tree_changed)
// recalculation rebuilds the map inside its own measurement traversal instead.
if (layout_tree_changed == LayoutTreeChanged::Yes && !m_needs_full_scrollable_overflow_recalculation)
m_scrollable_overflow_contained_boxes_from_last_layout = Layout::collect_scrollable_overflow_contained_boxes(*m_layout_root);
update_scrollable_overflow(ScrollableOverflowDerivedStructureUpdates::HandledByAfterLayoutCommit);
if (layout_commit_scope == LayoutCommitScope::Full)
update_scrollable_overflow(ScrollableOverflowDerivedStructureUpdates::HandledByFullLayoutCommit, boxes_needing_eager_overflow_measurement);
else
update_scrollable_overflow(ScrollableOverflowDerivedStructureUpdates::HandledByAfterLayoutCommit);

set_needs_accumulated_visual_contexts_update(true);

Expand Down Expand Up @@ -2108,7 +2111,7 @@ Document::PartialRelayoutResult Document::try_partial_relayout(HashTable<WeakPtr

++m_partial_layout_count;

after_layout_commit(layout_tree_was_built_in_partial_branch ? LayoutTreeChanged::Yes : LayoutTreeChanged::No);
after_layout_commit(layout_tree_was_built_in_partial_branch ? LayoutTreeChanged::Yes : LayoutTreeChanged::No, LayoutCommitScope::Subtree);
if (needs_style_update_after_layout() || !layout_is_up_to_date())
return PartialRelayoutResult::NeedsAnotherLayoutPass;
return PartialRelayoutResult::Done;
Expand Down Expand Up @@ -2211,19 +2214,26 @@ void Document::update_layout(UpdateLayoutReason reason)

layout_node_arena().sync_enrolled_content_for_layout();
Layout::LayoutRustBridge bridge;
Vector<Layout::Box const*> boxes_needing_eager_overflow_measurement;
bridge.run_root_layout(
*m_layout_root,
viewport_rect.width(),
viewport_rect.height(),
should_collect_devtools_layout_data);
m_scrollable_overflow_contained_boxes_from_last_layout = Layout::collect_scrollable_overflow_contained_boxes(
*m_layout_root, [&](Layout::Box const& box) {
auto paintable = box.paintable_box();
if (&box == m_layout_root.ptr() || box.is_scroll_container() || (paintable && !paintable->scroll_offset().is_zero()))
boxes_needing_eager_overflow_measurement.append(&box);
});

style_invalidation_counters().relayouts_performed++;

m_needs_full_scrollable_overflow_recalculation = true;

++m_full_layout_count;

after_layout_commit(LayoutTreeChanged::Yes);
after_layout_commit(LayoutTreeChanged::Yes, LayoutCommitScope::Full, boxes_needing_eager_overflow_measurement);

m_layout_root->for_each_in_inclusive_subtree([](auto& node) {
node.reset_needs_layout_update();
Expand Down Expand Up @@ -2481,7 +2491,7 @@ static void rebuild_sticky_insets(Layout::Node const& root)
});
}

void Document::update_scrollable_overflow(ScrollableOverflowDerivedStructureUpdates derived_structure_updates)
void Document::update_scrollable_overflow(ScrollableOverflowDerivedStructureUpdates derived_structure_updates, ReadonlySpan<Layout::Box const*> boxes_needing_eager_measurement)
{
// For every box that will be re-measured, the overflow data it had before, so the diff below
// can tell what actually changed; an empty value means the box's paintable was reset by a
Expand All @@ -2497,6 +2507,30 @@ void Document::update_scrollable_overflow(ScrollableOverflowDerivedStructureUpda

style_invalidation_counters().scrollable_overflow_recalculations++;

// The scroll offset can become invalid if the scrollable overflow rectangle has changed. For
// example, if the scroll container has been scrolled to the very end and then its scrollable
// overflow rect becomes smaller, the scroll offset would be out of bounds. Re-applying the
// current offset clamps it against the new rect.
auto clamp_scroll_offset = [](Painting::Paintable& paintable) {
if (!paintable.scroll_offset().is_zero())
paintable.set_scroll_offset(paintable.scroll_offset());
};

if (derived_structure_updates == ScrollableOverflowDerivedStructureUpdates::HandledByFullLayoutCommit) {
VERIFY(needs_full_recalculation);

// A full-root commit reset every surviving paintable, including its overflow data and
// paint cache. There is therefore no old overflow to preserve or diff. Ordinary boxes are
// measured recursively when their overflow contributes to one of these roots, so they do
// not need separate eager measurement.
for (auto const* box : boxes_needing_eager_measurement) {
Layout::measure_scrollable_overflow(*box, m_scrollable_overflow_contained_boxes_from_last_layout);
if (auto box_paintable = box->paintable_box())
clamp_scroll_offset(const_cast<Painting::Paintable&>(*box_paintable));
}
return;
}

auto record_and_clear_overflow_data = [&](Layout::Box const& box) {
auto box_paintable = box.paintable_box();
if (!box_paintable)
Expand Down Expand Up @@ -2528,6 +2562,8 @@ void Document::update_scrollable_overflow(ScrollableOverflowDerivedStructureUpda
});
}
for (auto const* containing_block = box->containing_block(); containing_block; containing_block = containing_block->containing_block()) {
if (auto containing_block_paintable = containing_block->paintable_box())
const_cast<Painting::Paintable&>(*containing_block_paintable).clear_cached_overflow_data();
if (!record_and_clear_overflow_data(*containing_block))
break;
}
Expand All @@ -2537,19 +2573,19 @@ void Document::update_scrollable_overflow(ScrollableOverflowDerivedStructureUpda
if (old_overflow_data_by_box.is_empty())
return;

// The scroll offset can become invalid if the scrollable overflow rectangle has changed. For
// example, if the scroll container has been scrolled to the very end and then its scrollable
// overflow rect becomes smaller, the scroll offset would be out of bounds. Re-applying the
// current offset clamps it against the new rect.
auto clamp_scroll_offset = [](Painting::Paintable& paintable) {
if (!paintable.scroll_offset().is_zero())
paintable.set_scroll_offset(paintable.scroll_offset());
};

for (auto const& it : old_overflow_data_by_box) {
auto box_paintable = it.key->paintable_box();
if (!box_paintable)
continue;

// Boxes reset by a subtree commit have no previous overflow data. They will be measured
// recursively if an ancestor reaches them. Measuring each one here would repeatedly walk
// the same containing-block chains after a small subtree update.
if (!it.value.has_value() && it.key != m_layout_root.ptr() && !it.key->is_scroll_container() && box_paintable->scroll_offset().is_zero())
continue;

Layout::measure_scrollable_overflow(*it.key, m_scrollable_overflow_contained_boxes_from_last_layout);
if (auto box_paintable = it.key->paintable_box())
clamp_scroll_offset(const_cast<Painting::Paintable&>(*box_paintable));
clamp_scroll_offset(const_cast<Painting::Paintable&>(*box_paintable));
}

bool any_overflow_changed = false;
Expand Down Expand Up @@ -2599,6 +2635,14 @@ void Document::update_scrollable_overflow(ScrollableOverflowDerivedStructureUpda
m_document->set_needs_repaint();
}

void Document::ensure_scrollable_overflow_is_measured(Layout::Box const& box) const
{
auto paintable = box.paintable_box();
if (!paintable || paintable->overflow_data().has_value() || paintable->cached_overflow_data().has_value())
return;
Layout::measure_scrollable_overflow(box, m_scrollable_overflow_contained_boxes_from_last_layout);
}

void Document::update_paint_and_hit_testing_properties_if_needed()
{
// NB: Called during paint property resolution.
Expand Down Expand Up @@ -8872,6 +8916,13 @@ void Document::schedule_scrollable_overflow_recalculation(Layout::Node const& la
return;
}

if (auto const* box = as_if<Layout::Box>(layout_node)) {
for (auto const* containing_box = box; containing_box; containing_box = containing_box->containing_block()) {
if (auto paintable = containing_box->paintable_box())
const_cast<Painting::Paintable&>(*paintable).clear_cached_overflow_data();
}
}

if (m_needs_full_scrollable_overflow_recalculation)
return;

Expand Down
13 changes: 9 additions & 4 deletions Libraries/LibWeb/DOM/Document.h
Original file line number Diff line number Diff line change
Expand Up @@ -475,8 +475,10 @@ class WEB_API Document
enum class ScrollableOverflowDerivedStructureUpdates : u8 {
UpdateAfterMeasure,
HandledByAfterLayoutCommit,
HandledByFullLayoutCommit,
};
void update_scrollable_overflow(ScrollableOverflowDerivedStructureUpdates);
void ensure_scrollable_overflow_is_measured(Layout::Box const&) const;
void update_scrollable_overflow(ScrollableOverflowDerivedStructureUpdates, ReadonlySpan<Layout::Box const*> boxes_needing_eager_measurement = {});
void update_paint_and_hit_testing_properties_if_needed();
void update_animated_style_if_needed();
void update_style_computer_viewport_rect();
Expand Down Expand Up @@ -1425,7 +1427,11 @@ class WEB_API Document
No,
Yes,
};
void after_layout_commit(LayoutTreeChanged);
enum class LayoutCommitScope : u8 {
Subtree,
Full,
};
void after_layout_commit(LayoutTreeChanged, LayoutCommitScope, ReadonlySpan<Layout::Box const*> boxes_needing_eager_overflow_measurement = {});

void run_unloading_cleanup_steps();

Expand Down Expand Up @@ -1799,8 +1805,7 @@ class WEB_API Document
Vector<WeakPtr<Painting::Paintable>> m_paintable_boxes_needing_scrollable_overflow_recalculation;
// NB: Holds raw layout node pointers that are only safe to read while m_layout_root still owns
// the tree they came from: every full layout rebuilds the map, layout tree teardown clears
// it, and its only reader, the scheduled scrollable overflow recalculation, runs only when
// layout is up to date.
// it, and overflow measurement only reads it while that root remains current.
Layout::ContainedBoxesMap m_scrollable_overflow_contained_boxes_from_last_layout;
CSS::SheetSetStyleCacheRegistry m_sheet_set_style_cache_registry;
RefPtr<Painting::HitTestDisplayList> m_hit_test_display_list;
Expand Down
1 change: 1 addition & 0 deletions Libraries/LibWeb/Layout/LayoutRustBridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,7 @@ RustFFI::FfiCommitSink LayoutRustBridge::commit_sink()
},
.set_box_metrics = [](void*, void* paintable_pointer, RustFFI::FfiCommittedBoxMetrics metrics) {
auto& paintable = *static_cast<Painting::Paintable*>(paintable_pointer);
paintable.set_layout_fragment_identity(metrics.fragment_identity);
auto& box_model = paintable.box_model();
box_model.inset = {
CSSPixels::from_raw(metrics.inset_top),
Expand Down
10 changes: 6 additions & 4 deletions Libraries/LibWeb/Layout/Node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,12 @@ void Node::bump_fragment_cache_epoch()
// into a fail-safe miss.
void Node::bump_fragment_cache_epoch_of_self_and_ancestors()
{
if (!fragment_cache_epochs_enabled())
return;
for (auto* node = this; node; node = node->parent_ptr())
++node->node_data().fragment_cache_epoch;
for (auto* node = this; node; node = node->parent_ptr()) {
if (fragment_cache_epochs_enabled())
++node->node_data().fragment_cache_epoch;
if (auto* box = as_if<Box>(*node); box && box->paintable_box())
const_cast<Painting::Paintable&>(*box->paintable_box()).clear_cached_overflow_data();
}
}

void* Node::arena_handle() const
Expand Down
75 changes: 62 additions & 13 deletions Libraries/LibWeb/Layout/ScrollableOverflow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,11 @@ PhysicalOverflowDirections physical_overflow_directions(Box const& box)
};
}

static CSSPixelRect apply_css_transform_to_overflow_rect(Box const& box, CSSPixelRect const& rect)
static CSSPixelRect apply_css_transform_to_overflow_rect(Box const& box, CSSPixelRect const& rect, bool has_css_transform)
{
auto const& paintable_box = *box.paintable_box();
if (!has_css_transform)
return rect;
auto transform_data = Painting::compute_transform(paintable_box, 1.0);
if (!transform_data.has_value())
return rect;
Expand Down Expand Up @@ -143,21 +145,43 @@ CSSPixelRect measure_scrollable_overflow(Box const& box, ContainedBoxesMap const

auto const& paintable_box = *box.paintable_box();

if (paintable_box.scrollable_overflow_rect().has_value())
return paintable_box.scrollable_overflow_rect().value();
if (paintable_box.overflow_data().has_value())
return paintable_box.overflow_data()->scrollable_overflow_rect;

auto const paintable_absolute_padding_box = paintable_box.absolute_padding_box_rect();
auto const paintable_absolute_content_box = paintable_box.absolute_rect();

auto store_overflow_data = [&](Painting::Paintable::OverflowData overflow_data) {
const_cast<Painting::Paintable&>(paintable_box).set_overflow_data(overflow_data);

auto rect_relative_to_padding_box = overflow_data.scrollable_overflow_rect;
rect_relative_to_padding_box.translate_by({ -paintable_absolute_padding_box.x(), -paintable_absolute_padding_box.y() });
const_cast<Painting::Paintable&>(paintable_box).set_cached_overflow_data({
.rect_relative_to_padding_box = rect_relative_to_padding_box,
.has_scrollable_overflow = overflow_data.has_scrollable_overflow,
});
};

if (auto const& cached_overflow = paintable_box.cached_overflow_data(); cached_overflow.has_value()) {
auto scrollable_overflow_rect = cached_overflow->rect_relative_to_padding_box;
scrollable_overflow_rect.translate_by(paintable_absolute_padding_box.location());
const_cast<Painting::Paintable&>(paintable_box).set_overflow_data({
.scrollable_overflow_rect = scrollable_overflow_rect,
.has_scrollable_overflow = cached_overflow->has_scrollable_overflow,
});
return scrollable_overflow_rect;
}

// https://drafts.csswg.org/css-overflow-3/#scrollable-overflow-calculation
// The scrollable overflow area of a box is the union of:

// - Its own padding box.
auto const paintable_absolute_padding_box = paintable_box.absolute_padding_box_rect();
auto const paintable_absolute_content_box = paintable_box.absolute_rect();
auto scrollable_overflow_rect = paintable_absolute_padding_box;
auto in_flow_and_floated_content_bounds = paintable_absolute_content_box;

// Replaced SVG viewports clip their content
if (is<SVGSVGBox>(box)) {
const_cast<Painting::Paintable&>(paintable_box).set_overflow_data({
store_overflow_data({
.scrollable_overflow_rect = scrollable_overflow_rect,
.has_scrollable_overflow = false,
});
Expand Down Expand Up @@ -206,8 +230,31 @@ CSSPixelRect measure_scrollable_overflow(Box const& box, ContainedBoxesMap const
if (child.is_fixed_position())
continue;

auto untransformed_child_border_box = child.paintable_box()->absolute_border_box_rect();
auto child_border_box = apply_css_transform_to_overflow_rect(child, untransformed_child_border_box);
auto const& child_paintable = *child.paintable_box();
auto const child_has_css_transform = child_paintable.has_css_transform();
if (child.position() == CSS::Positioning::Static && child.display().is_inline_outside() && !child.is_floating() && !child_has_css_transform) {
if (auto const& cached_overflow = child_paintable.cached_overflow_data(); cached_overflow.has_value()) {
auto const& border = child_paintable.box_model().border;
auto const has_border = border.top != 0 || border.right != 0 || border.bottom != 0 || border.left != 0;
if (!has_border) {
auto const& padding = child_paintable.box_model().padding;
CSSPixelRect content_box_relative_to_padding_box {
{ padding.left, padding.top },
{
child_paintable.content_width(),
child_paintable.content_height(),
},
};
// The committed line fragment already contributes this content box. A box with no border whose
// cached overflow fits inside the content box cannot expand its containing block's overflow.
if (content_box_relative_to_padding_box.contains(cached_overflow->rect_relative_to_padding_box))
continue;
}
}
}

auto untransformed_child_border_box = child_paintable.absolute_border_box_rect();
auto child_border_box = apply_css_transform_to_overflow_rect(child, untransformed_child_border_box, child_has_css_transform);

// NOTE: Only boxes that are not wholly in the unreachable scrollable overflow region contribute.
auto wholly_in_unreachable_horizontal_axis = overflow_directions.horizontal_axis_is_positive
Expand All @@ -234,13 +281,15 @@ CSSPixelRect measure_scrollable_overflow(Box const& box, ContainedBoxesMap const
if (child.has_layout_containment() || child.has_paint_containment())
continue;

if (child.overflow_x() == CSS::Overflow::Visible || child.overflow_y() == CSS::Overflow::Visible) {
auto child_scrollable_overflow = apply_css_transform_to_overflow_rect(child, measure_scrollable_overflow(child, contained_boxes_map));
auto const child_overflow_x = child.overflow_x();
auto const child_overflow_y = child.overflow_y();
if (child_overflow_x == CSS::Overflow::Visible || child_overflow_y == CSS::Overflow::Visible) {
auto child_scrollable_overflow = apply_css_transform_to_overflow_rect(child, measure_scrollable_overflow(child, contained_boxes_map), child_has_css_transform);
if (!child_scrollable_overflow.is_empty()) {
if (child.overflow_x() == CSS::Overflow::Visible) {
if (child_overflow_x == CSS::Overflow::Visible) {
scrollable_overflow_rect.unite_horizontally(child_scrollable_overflow);
}
if (child.overflow_y() == CSS::Overflow::Visible) {
if (child_overflow_y == CSS::Overflow::Visible) {
scrollable_overflow_rect.unite_vertically(child_scrollable_overflow);
}
}
Expand Down Expand Up @@ -287,7 +336,7 @@ CSSPixelRect measure_scrollable_overflow(Box const& box, ContainedBoxesMap const
has_scrollable_overflow = !paintable_absolute_padding_box.contains(scrollable_overflow_rect) && box.is_scroll_container();
}

const_cast<Painting::Paintable&>(paintable_box).set_overflow_data({
store_overflow_data({
.scrollable_overflow_rect = scrollable_overflow_rect,
.has_scrollable_overflow = has_scrollable_overflow,
});
Expand Down
Loading
Loading