LibWeb: Resolve relative offsets *once* after layout
Instead of applying relative offsets (like position:relative insets) during painting and hit testing, we now do a pass at the end of layout and assign the final resolved offsets to paintables. This makes painting and hit testing easier since they don't have to think about relative offsets, and it also fixes a bug where offsets were not applied to text fragments inside inline-flow elements that were themselves position:relative.
This commit is contained in:
parent
481fdfee68
commit
25a3d0d643
Notes:
sideshowbarker
2024-07-17 05:23:40 +09:00
Author: https://github.com/awesomekling Commit: https://github.com/SerenityOS/serenity/commit/25a3d0d643 Pull-request: https://github.com/SerenityOS/serenity/pull/20587
6 changed files with 117 additions and 24 deletions
|
@ -0,0 +1,31 @@
|
|||
Viewport <#document> at (0,0) content-size 800x600 children: not-inline
|
||||
BlockContainer <html> at (0,0) content-size 800x600 [BFC] children: not-inline
|
||||
BlockContainer <body> at (8,8) content-size 784x17.46875 children: inline
|
||||
line 0 width: 98, height: 17.46875, bottom: 17.46875, baseline: 13.53125
|
||||
frag 0 from TextNode start: 0, length: 4, rect: [8,8 35.15625x17.46875]
|
||||
"foo "
|
||||
frag 1 from TextNode start: 0, length: 3, rect: [43,33 27.640625x17.46875]
|
||||
"bar"
|
||||
frag 2 from TextNode start: 0, length: 1, rect: [71,8 8x17.46875]
|
||||
" "
|
||||
frag 3 from TextNode start: 0, length: 3, rect: [54,58 27.203125x17.46875]
|
||||
"baz"
|
||||
TextNode <#text>
|
||||
InlineNode <b>
|
||||
TextNode <#text>
|
||||
TextNode <#text>
|
||||
InlineNode <b>
|
||||
InlineNode <i>
|
||||
TextNode <#text>
|
||||
TextNode <#text>
|
||||
|
||||
PaintableWithLines (Viewport<#document>) [0,0 800x600]
|
||||
PaintableWithLines (BlockContainer<HTML>) [0,0 800x600]
|
||||
PaintableWithLines (BlockContainer<BODY>) [8,8 784x17.46875] overflow: [8,8 784x67.46875]
|
||||
TextPaintable (TextNode<#text>)
|
||||
InlinePaintable (InlineNode<B>)
|
||||
TextPaintable (TextNode<#text>)
|
||||
TextPaintable (TextNode<#text>)
|
||||
InlinePaintable (InlineNode<B>)
|
||||
InlinePaintable (InlineNode<I>)
|
||||
TextPaintable (TextNode<#text>)
|
|
@ -0,0 +1,12 @@
|
|||
<style>
|
||||
b {
|
||||
position: relative;
|
||||
top: 25px;
|
||||
}
|
||||
i {
|
||||
position: relative;
|
||||
top: 25px;
|
||||
right: 25px;
|
||||
}
|
||||
</style>
|
||||
<body>foo <b>bar</b> <b><i>baz</i></b>
|
|
@ -131,6 +131,71 @@ static CSSPixelRect measure_scrollable_overflow(Box const& box)
|
|||
return scrollable_overflow_rect;
|
||||
}
|
||||
|
||||
void LayoutState::resolve_relative_positions(Vector<Painting::PaintableWithLines&> const& paintables_with_lines)
|
||||
{
|
||||
// This function resolves relative position offsets of all the boxes & fragments in the paint tree.
|
||||
// It runs *after* the paint tree has been constructed, so it modifies paintable node & fragment offsets directly.
|
||||
|
||||
// Regular boxes (not line box fragments):
|
||||
for (auto& it : used_values_per_layout_node) {
|
||||
auto& used_values = *it.value;
|
||||
auto& node = const_cast<NodeWithStyleAndBoxModelMetrics&>(used_values.node());
|
||||
|
||||
if (!node.is_box())
|
||||
continue;
|
||||
|
||||
auto& paintable = static_cast<Painting::PaintableBox&>(*node.paintable());
|
||||
CSSPixelPoint offset;
|
||||
|
||||
if (used_values.containing_line_box_fragment.has_value()) {
|
||||
// Atomic inline case:
|
||||
// We know that `node` is an atomic inline because `containing_line_box_fragments` refers to the
|
||||
// line box fragment in the parent block container that contains it.
|
||||
auto const& containing_line_box_fragment = used_values.containing_line_box_fragment.value();
|
||||
auto const& containing_block = *node.containing_block();
|
||||
auto const& containing_block_paintable = verify_cast<Painting::PaintableWithLines>(*containing_block.paintable_box());
|
||||
auto const& fragment = containing_block_paintable.line_boxes()[containing_line_box_fragment.line_box_index].fragments()[containing_line_box_fragment.fragment_index];
|
||||
|
||||
// The fragment has the final offset for the atomic inline, so we just need to copy it from there.
|
||||
offset = fragment.offset();
|
||||
} else {
|
||||
// Not an atomic inline, much simpler case.
|
||||
offset = used_values.offset;
|
||||
}
|
||||
// Apply relative position inset if appropriate.
|
||||
if (node.computed_values().position() == CSS::Position::Relative) {
|
||||
auto& inset = node.box_model().inset;
|
||||
offset.translate_by(inset.left, inset.top);
|
||||
}
|
||||
paintable.set_offset(offset);
|
||||
}
|
||||
|
||||
// Line box fragments:
|
||||
for (auto const& paintable_with_lines : paintables_with_lines) {
|
||||
for (auto const& line_box : paintable_with_lines.line_boxes()) {
|
||||
for (auto& fragment : line_box.fragments()) {
|
||||
auto const& fragment_node = fragment.layout_node();
|
||||
if (!is<Layout::NodeWithStyleAndBoxModelMetrics>(*fragment_node.parent()))
|
||||
continue;
|
||||
// Collect effective relative position offset from inline-flow parent chain.
|
||||
CSSPixelPoint offset;
|
||||
for (auto* ancestor = fragment_node.parent(); ancestor; ancestor = ancestor->parent()) {
|
||||
if (!is<Layout::NodeWithStyleAndBoxModelMetrics>(*ancestor))
|
||||
break;
|
||||
if (!ancestor->display().is_inline_outside() || !ancestor->display().is_flow_inside())
|
||||
break;
|
||||
if (ancestor->computed_values().position() == CSS::Position::Relative) {
|
||||
auto const& ancestor_node = static_cast<Layout::NodeWithStyleAndBoxModelMetrics const&>(*ancestor);
|
||||
auto const& inset = ancestor_node.box_model().inset;
|
||||
offset.translate_by(inset.left, inset.top);
|
||||
}
|
||||
}
|
||||
const_cast<LineBoxFragment&>(fragment).set_offset(fragment.offset().translated(offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LayoutState::commit()
|
||||
{
|
||||
// Only the top-level LayoutState should ever be committed.
|
||||
|
@ -158,7 +223,6 @@ void LayoutState::commit()
|
|||
auto& paintable_box = const_cast<Painting::PaintableBox&>(*box.paintable_box());
|
||||
paintable_box.set_offset(used_values.offset);
|
||||
paintable_box.set_content_size(used_values.content_width(), used_values.content_height());
|
||||
paintable_box.set_containing_line_box_fragment(used_values.containing_line_box_fragment);
|
||||
if (used_values.override_borders_data().has_value()) {
|
||||
paintable_box.set_override_borders_data(used_values.override_borders_data().value());
|
||||
}
|
||||
|
@ -174,7 +238,11 @@ void LayoutState::commit()
|
|||
}
|
||||
}
|
||||
|
||||
// Measure absolute rect of each line box. Also collect all text nodes.
|
||||
resolve_relative_positions(paintables_with_lines);
|
||||
|
||||
// Make a pass over all the line boxes to:
|
||||
// - Measure absolute rect of each line box.
|
||||
// - Collect all text nodes, so we can create paintables for them later.
|
||||
for (auto& paintable_with_lines : paintables_with_lines) {
|
||||
for (auto& line_box : paintable_with_lines.line_boxes()) {
|
||||
CSSPixelRect line_box_absolute_rect;
|
||||
|
|
|
@ -172,6 +172,9 @@ struct LayoutState {
|
|||
|
||||
LayoutState const* m_parent { nullptr };
|
||||
LayoutState const& m_root;
|
||||
|
||||
private:
|
||||
void resolve_relative_positions(Vector<Painting::PaintableWithLines&> const&);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -117,19 +117,7 @@ void PaintableBox::set_content_size(CSSPixelSize size)
|
|||
|
||||
CSSPixelPoint PaintableBox::effective_offset() const
|
||||
{
|
||||
CSSPixelPoint offset;
|
||||
if (containing_block() && m_containing_line_box_fragment.has_value()) {
|
||||
auto& paintable_with_lines = *verify_cast<PaintableWithLines>(containing_block()->paintable_box());
|
||||
auto const& fragment = paintable_with_lines.line_boxes()[m_containing_line_box_fragment->line_box_index].fragments()[m_containing_line_box_fragment->fragment_index];
|
||||
offset = fragment.offset();
|
||||
} else {
|
||||
offset = m_offset;
|
||||
}
|
||||
if (layout_box().computed_values().position() == CSS::Position::Relative) {
|
||||
auto const& inset = layout_box().box_model().inset;
|
||||
offset.translate_by(inset.left, inset.top);
|
||||
}
|
||||
return offset;
|
||||
return m_offset;
|
||||
}
|
||||
|
||||
CSSPixelRect PaintableBox::compute_absolute_rect() const
|
||||
|
@ -176,11 +164,6 @@ CSSPixelRect PaintableBox::absolute_paint_rect() const
|
|||
return *m_absolute_paint_rect;
|
||||
}
|
||||
|
||||
void PaintableBox::set_containing_line_box_fragment(Optional<Layout::LineBoxFragmentCoordinate> fragment_coordinate)
|
||||
{
|
||||
m_containing_line_box_fragment = move(fragment_coordinate);
|
||||
}
|
||||
|
||||
StackingContext* PaintableBox::enclosing_stacking_context()
|
||||
{
|
||||
for (auto* ancestor = layout_box().parent(); ancestor; ancestor = ancestor->parent()) {
|
||||
|
|
|
@ -112,7 +112,6 @@ public:
|
|||
Optional<CSSPixelRect> calculate_overflow_clipped_rect() const;
|
||||
|
||||
void set_overflow_data(OverflowData data) { m_overflow_data = move(data); }
|
||||
void set_containing_line_box_fragment(Optional<Layout::LineBoxFragmentCoordinate>);
|
||||
|
||||
StackingContext* stacking_context() { return m_stacking_context; }
|
||||
StackingContext const* stacking_context() const { return m_stacking_context; }
|
||||
|
@ -201,9 +200,6 @@ private:
|
|||
CSSPixelPoint m_offset;
|
||||
CSSPixelSize m_content_size;
|
||||
|
||||
// Some boxes hang off of line box fragments. (inline-block, inline-table, replaced, etc)
|
||||
Optional<Layout::LineBoxFragmentCoordinate> m_containing_line_box_fragment;
|
||||
|
||||
OwnPtr<StackingContext> m_stacking_context;
|
||||
|
||||
Optional<CSSPixelRect> mutable m_absolute_rect;
|
||||
|
|
Loading…
Add table
Reference in a new issue