blob: 2e4a02b05dad966e154bc3da57d2090a1d73f81f [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/paint/ng/ng_box_fragment_painter.h"
#include "base/containers/adapters.h"
#include "third_party/blink/renderer/core/css/style_engine.h"
#include "third_party/blink/renderer/core/editing/drag_caret.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/layout/background_bleed_avoidance.h"
#include "third_party/blink/renderer/core/layout/hit_test_location.h"
#include "third_party/blink/renderer/core/layout/hit_test_result.h"
#include "third_party/blink/renderer/core/layout/layout_inline.h"
#include "third_party/blink/renderer/core/layout/layout_table_cell.h"
#include "third_party/blink/renderer/core/layout/ng/geometry/ng_box_strut.h"
#include "third_party/blink/renderer/core/layout/ng/inline/layout_ng_text_combine.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h"
#include "third_party/blink/renderer/core/layout/ng/layout_ng_mixin.h"
#include "third_party/blink/renderer/core/layout/ng/ng_block_break_token.h"
#include "third_party/blink/renderer/core/layout/ng/ng_outline_utils.h"
#include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h"
#include "third_party/blink/renderer/core/layout/pointer_events_hit_rules.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/paint/background_image_geometry.h"
#include "third_party/blink/renderer/core/paint/box_border_painter.h"
#include "third_party/blink/renderer/core/paint/box_decoration_data.h"
#include "third_party/blink/renderer/core/paint/box_painter.h"
#include "third_party/blink/renderer/core/paint/compositing/composited_layer_mapping.h"
#include "third_party/blink/renderer/core/paint/ng/ng_fieldset_painter.h"
#include "third_party/blink/renderer/core/paint/ng/ng_fragment_painter.h"
#include "third_party/blink/renderer/core/paint/ng/ng_inline_box_fragment_painter.h"
#include "third_party/blink/renderer/core/paint/ng/ng_mathml_painter.h"
#include "third_party/blink/renderer/core/paint/ng/ng_table_painters.h"
#include "third_party/blink/renderer/core/paint/ng/ng_text_combine_painter.h"
#include "third_party/blink/renderer/core/paint/ng/ng_text_fragment_painter.h"
#include "third_party/blink/renderer/core/paint/object_painter.h"
#include "third_party/blink/renderer/core/paint/paint_info.h"
#include "third_party/blink/renderer/core/paint/paint_layer.h"
#include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
#include "third_party/blink/renderer/core/paint/paint_phase.h"
#include "third_party/blink/renderer/core/paint/paint_timing_detector.h"
#include "third_party/blink/renderer/core/paint/rounded_border_geometry.h"
#include "third_party/blink/renderer/core/paint/scoped_paint_state.h"
#include "third_party/blink/renderer/core/paint/scrollable_area_painter.h"
#include "third_party/blink/renderer/core/paint/theme_painter.h"
#include "third_party/blink/renderer/core/paint/url_metadata_utils.h"
#include "third_party/blink/renderer/core/scroll/scroll_types.h"
#include "third_party/blink/renderer/platform/geometry/layout_rect_outsets.h"
#include "third_party/blink/renderer/platform/graphics/graphics_context_state_saver.h"
#include "third_party/blink/renderer/platform/graphics/paint/display_item_cache_skipper.h"
#include "third_party/blink/renderer/platform/graphics/paint/drawing_recorder.h"
#include "third_party/blink/renderer/platform/graphics/paint/scoped_display_item_fragment.h"
namespace blink {
namespace {
LayoutRectOutsets BoxStrutToLayoutRectOutsets(
const NGPixelSnappedPhysicalBoxStrut& box_strut) {
return LayoutRectOutsets(
LayoutUnit(box_strut.top), LayoutUnit(box_strut.right),
LayoutUnit(box_strut.bottom), LayoutUnit(box_strut.left));
}
inline bool HasSelection(const LayoutObject* layout_object) {
return layout_object->GetSelectionState() != SelectionState::kNone;
}
inline bool IsVisibleToPaint(const NGPhysicalFragment& fragment,
const ComputedStyle& style) {
if (fragment.IsHiddenForPaint())
return false;
if (style.Visibility() != EVisibility::kVisible) {
auto display = style.Display();
// Hidden section/row backgrounds still paint into cells.
if (display != EDisplay::kTableRowGroup && display != EDisplay::kTableRow &&
display != EDisplay::kTableColumn &&
display != EDisplay::kTableColumnGroup) {
return false;
}
}
// When |NGLineTruncator| sets |IsHiddenForPaint|, it sets to the fragment in
// the line. However, when it has self-painting layer, the fragment stored in
// |LayoutBlockFlow| will be painted. Check |IsHiddenForPaint| of the fragment
// in the inline formatting context.
if (UNLIKELY(fragment.IsAtomicInline() && fragment.HasSelfPaintingLayer())) {
const LayoutObject* layout_object = fragment.GetLayoutObject();
if (layout_object->IsInLayoutNGInlineFormattingContext()) {
NGInlineCursor cursor;
cursor.MoveTo(*layout_object);
if (cursor && cursor.Current().IsHiddenForPaint())
return false;
}
}
return true;
}
inline bool IsVisibleToPaint(const NGFragmentItem& item,
const ComputedStyle& style) {
return !item.IsHiddenForPaint() &&
style.Visibility() == EVisibility::kVisible;
}
inline bool IsVisibleToHitTest(const ComputedStyle& style,
const HitTestRequest& request) {
return request.IgnorePointerEventsNone() ||
style.PointerEvents() != EPointerEvents::kNone;
}
inline bool IsVisibleToHitTest(const NGFragmentItem& item,
const HitTestRequest& request) {
const ComputedStyle& style = item.Style();
if (item.Type() != NGFragmentItem::kSvgText)
return IsVisibleToPaint(item, style) && IsVisibleToHitTest(style, request);
if (item.IsHiddenForPaint())
return false;
PointerEventsHitRules hit_rules(PointerEventsHitRules::SVG_TEXT_HITTESTING,
request, style.PointerEvents());
if (hit_rules.require_visible && style.Visibility() != EVisibility::kVisible)
return false;
if (hit_rules.can_hit_bounding_box ||
(hit_rules.can_hit_stroke &&
(style.HasStroke() || !hit_rules.require_stroke)) ||
(hit_rules.can_hit_fill && (style.HasFill() || !hit_rules.require_fill)))
return IsVisibleToHitTest(style, request);
return false;
}
inline bool IsVisibleToHitTest(const NGPhysicalFragment& fragment,
const HitTestRequest& request) {
const ComputedStyle& style = fragment.Style();
return IsVisibleToPaint(fragment, style) &&
IsVisibleToHitTest(style, request);
}
// Hit tests inline ancestor elements of |fragment| who do not have their own
// box fragments.
// @param physical_offset Physical offset of |fragment| in the paint layer.
bool HitTestCulledInlineAncestors(
HitTestResult& result,
const NGInlineCursor& parent_cursor,
const LayoutObject* current,
const LayoutObject* limit,
const NGInlineCursorPosition& previous_sibling,
const HitTestLocation& hit_test_location,
const PhysicalOffset fallback_accumulated_offset) {
DCHECK(current != limit && current->IsDescendantOf(limit));
// Check ancestors only when |current| is the first fragment in this line.
if (previous_sibling && current == previous_sibling.GetLayoutObject())
return false;
for (LayoutObject* parent = current->Parent(); parent && parent != limit;
current = parent, parent = parent->Parent()) {
// |culled_parent| is a culled inline element to be hit tested, since it's
// "between" |fragment| and |fragment->Parent()| but doesn't have its own
// box fragment.
// To ensure the correct hit test ordering, |culled_parent| must be hit
// tested only once after all of its descendants are hit tested:
// - Shortcut: when |current_layout_object| is the only child (of
// |culled_parent|), since it's just hit tested, we can safely hit test its
// parent;
// - General case: we hit test |culled_parent| only when it is not an
// ancestor of |previous_sibling|; otherwise, |previous_sibling| has to be
// hit tested first.
// TODO(crbug.com/849331): It's wrong for bidi inline fragmentation. Fix it.
const bool has_sibling =
current->PreviousSibling() || current->NextSibling();
if (has_sibling && previous_sibling &&
previous_sibling.GetLayoutObject()->IsDescendantOf(parent))
break;
if (auto* parent_layout_inline = DynamicTo<LayoutInline>(parent)) {
if (parent_layout_inline->HitTestCulledInline(result, hit_test_location,
fallback_accumulated_offset,
&parent_cursor))
return true;
}
}
return false;
}
bool HitTestCulledInlineAncestors(
HitTestResult& result,
const NGPhysicalBoxFragment& container,
const NGInlineCursor& parent_cursor,
const NGFragmentItem& item,
const NGInlineCursorPosition& previous_sibling,
const HitTestLocation& hit_test_location,
const PhysicalOffset& physical_offset) {
// Ellipsis can appear under a different parent from the ellipsized object
// that it can confuse culled inline logic.
if (UNLIKELY(item.IsEllipsis()))
return false;
// To be passed as |accumulated_offset| to LayoutInline::HitTestCulledInline,
// where it equals the physical offset of the containing block in paint layer.
const PhysicalOffset fallback_accumulated_offset =
physical_offset - item.OffsetInContainerFragment();
return HitTestCulledInlineAncestors(
result, parent_cursor, item.GetLayoutObject(),
// Limit the traversal up to the container fragment, or its container if
// the fragment is not a CSSBox.
container.GetSelfOrContainerLayoutObject(), previous_sibling,
hit_test_location, fallback_accumulated_offset);
}
// Returns if this fragment may not be laid out by LayoutNG.
//
// This function is for an optimization to skip a few virtual
// calls. When this is |false|, we know |LayoutObject::Paint()| calls
// |NGBoxFragmentPainter|, and that we can instantiate a child
// |NGBoxFragmentPainter| directly. All code should work without this.
//
// TODO(kojii): This may become more complicated when we use
// |NGBoxFragmentPainter| for all fragments, and we still want this
// oprimization.
bool FragmentRequiresLegacyFallback(const NGPhysicalFragment& fragment) {
// If |fragment| is |IsFormattingContextRoot|, it may be legacy.
// Avoid falling back to |LayoutObject| if |CanTraverse|, because it
// cannot handle block fragmented objects.
if (!fragment.IsFormattingContextRoot() || fragment.CanTraverse())
return false;
DCHECK(!To<NGPhysicalBoxFragment>(&fragment)->BreakToken());
return true;
}
// Returns a vector of backplates that surround the paragraphs of text within
// line_boxes.
//
// This function traverses descendants of an inline formatting context in
// pre-order DFS and build up backplates behind inline text boxes, each split at
// the paragraph level. Store the results in paragraph_backplates.
Vector<PhysicalRect> BuildBackplate(NGInlineCursor* descendants,
const PhysicalOffset& paint_offset) {
// The number of consecutive forced breaks that split the backplate by
// paragraph.
static constexpr int kMaxConsecutiveLineBreaks = 2;
struct Backplates {
STACK_ALLOCATED();
public:
void AddTextRect(const PhysicalRect& box_rect) {
if (consecutive_line_breaks >= kMaxConsecutiveLineBreaks) {
// This is a paragraph point.
paragraph_backplates.push_back(current_backplate);
current_backplate = PhysicalRect();
}
consecutive_line_breaks = 0;
current_backplate.Unite(box_rect);
}
void AddLineBreak() { consecutive_line_breaks++; }
Vector<PhysicalRect> paragraph_backplates;
PhysicalRect current_backplate;
int consecutive_line_breaks = 0;
} backplates;
// Build up and paint backplates of all child inline text boxes. We are not
// able to simply use the linebox rect to compute the backplate because the
// backplate should only be painted for inline text and not for atomic
// inlines.
for (; *descendants; descendants->MoveToNext()) {
if (const NGFragmentItem* child_item = descendants->CurrentItem()) {
if (child_item->IsHiddenForPaint())
continue;
if (child_item->IsText()) {
if (child_item->IsLineBreak()) {
backplates.AddLineBreak();
continue;
}
PhysicalRect box_rect(
child_item->OffsetInContainerFragment() + paint_offset,
child_item->Size());
backplates.AddTextRect(box_rect);
}
continue;
}
NOTREACHED();
}
if (!backplates.current_backplate.IsEmpty())
backplates.paragraph_backplates.push_back(backplates.current_backplate);
return backplates.paragraph_backplates;
}
bool HitTestAllPhasesInFragment(const NGPhysicalBoxFragment& fragment,
const HitTestLocation& hit_test_location,
PhysicalOffset accumulated_offset,
HitTestResult* result) {
// Hit test all phases of inline blocks, inline tables, replaced elements and
// non-positioned floats as if they created their own (pseudo- [1]) stacking
// context. https://www.w3.org/TR/CSS22/zindex.html#painting-order
//
// [1] As if it creates a new stacking context, but any positioned descendants
// and descendants which actually create a new stacking context should be
// considered part of the parent stacking context, not this new one.
if (!fragment.CanTraverse()) {
return fragment.GetMutableLayoutObject()->HitTestAllPhases(
*result, hit_test_location, accumulated_offset);
}
return NGBoxFragmentPainter(To<NGPhysicalBoxFragment>(fragment))
.HitTestAllPhases(*result, hit_test_location, accumulated_offset);
}
bool NodeAtPointInFragment(const NGPhysicalBoxFragment& fragment,
const HitTestLocation& hit_test_location,
PhysicalOffset accumulated_offset,
HitTestAction action,
HitTestResult* result) {
if (!fragment.CanTraverse()) {
return fragment.GetMutableLayoutObject()->NodeAtPoint(
*result, hit_test_location, accumulated_offset, action);
}
return NGBoxFragmentPainter(fragment).NodeAtPoint(*result, hit_test_location,
accumulated_offset, action);
}
void UpdateHitTestResult(HitTestResult& result,
const NGPhysicalBoxFragment& fragment,
PhysicalOffset offset) {
if (result.InnerNode())
return;
if (Node* node = fragment.NodeForHitTest())
result.SetNodeAndPosition(node, &fragment, offset);
}
// Return an ID for this fragmentainer, which is unique within the fragmentation
// context. We need to provide this ID when block-fragmenting, so that we can
// cache the painting of each individual fragment.
unsigned FragmentainerUniqueIdentifier(const NGPhysicalBoxFragment& fragment) {
if (const auto* break_token = To<NGBlockBreakToken>(fragment.BreakToken()))
return break_token->SequenceNumber() + 1;
return 0;
}
bool ShouldPaintCursorCaret(const NGPhysicalBoxFragment& fragment) {
return fragment.GetLayoutObject()->GetFrame()->Selection().ShouldPaintCaret(
fragment);
}
bool ShouldPaintDragCaret(const NGPhysicalBoxFragment& fragment) {
return fragment.GetLayoutObject()
->GetFrame()
->GetPage()
->GetDragCaret()
.ShouldPaintCaret(fragment);
}
bool ShouldPaintCarets(const NGPhysicalBoxFragment& fragment) {
return ShouldPaintCursorCaret(fragment) || ShouldPaintDragCaret(fragment);
}
} // anonymous namespace
PhysicalRect NGBoxFragmentPainter::InkOverflowIncludingFilters() const {
if (box_item_)
return box_item_->SelfInkOverflow();
const NGPhysicalFragment& fragment = PhysicalFragment();
DCHECK(!fragment.IsInlineBox());
return To<LayoutBox>(fragment.GetLayoutObject())
->PhysicalVisualOverflowRectIncludingFilters();
}
void NGBoxFragmentPainter::Paint(const PaintInfo& paint_info) {
if (PhysicalFragment().IsHiddenForPaint())
return;
if (PhysicalFragment().IsPaintedAtomically() &&
!box_fragment_.HasSelfPaintingLayer())
PaintAllPhasesAtomically(paint_info);
else
PaintInternal(paint_info);
}
void NGBoxFragmentPainter::PaintInternal(const PaintInfo& paint_info) {
// Avoid initialization of Optional ScopedPaintState::chunk_properties_
// and ScopedPaintState::adjusted_paint_info_.
STACK_UNINITIALIZED ScopedPaintState paint_state(box_fragment_, paint_info);
if (!ShouldPaint(paint_state))
return;
PaintInfo& info = paint_state.MutablePaintInfo();
const PhysicalOffset paint_offset = paint_state.PaintOffset();
const PaintPhase original_phase = info.phase;
// For text-combine-upright:all, we need to realize canvas here for scaling
// to fit text content in 1em and shear for "font-style: oblique -15deg".
absl::optional<DrawingRecorder> recorder;
absl::optional<GraphicsContextStateSaver> graphics_context_state_saver;
const auto* const text_combine =
DynamicTo<LayoutNGTextCombine>(box_fragment_.GetLayoutObject());
if (UNLIKELY(text_combine)) {
if (text_combine->NeedsAffineTransformInPaint()) {
if (original_phase == PaintPhase::kForeground)
PaintCaretsIfNeeded(paint_state, paint_info, paint_offset);
if (!paint_info.context.InDrawingRecorder()) {
if (DrawingRecorder::UseCachedDrawingIfPossible(
paint_info.context, GetDisplayItemClient(), paint_info.phase))
return;
recorder.emplace(paint_info.context, GetDisplayItemClient(),
paint_info.phase,
text_combine->VisualRectForPaint(paint_offset));
}
graphics_context_state_saver.emplace(paint_info.context);
paint_info.context.ConcatCTM(
text_combine->ComputeAffineTransformForPaint(paint_offset));
}
}
ScopedPaintTimingDetectorBlockPaintHook
scoped_paint_timing_detector_block_paint_hook;
if (original_phase == PaintPhase::kForeground &&
box_fragment_.GetLayoutObject()->IsBox()) {
scoped_paint_timing_detector_block_paint_hook.EmplaceIfNeeded(
To<LayoutBox>(*box_fragment_.GetLayoutObject()),
paint_info.context.GetPaintController().CurrentPaintChunkProperties());
}
if (original_phase == PaintPhase::kOutline) {
info.phase = PaintPhase::kDescendantOutlinesOnly;
} else if (ShouldPaintSelfBlockBackground(original_phase)) {
info.phase = PaintPhase::kSelfBlockBackgroundOnly;
// With CompositeAfterPaint we need to call PaintObject twice: once for the
// background painting that does not scroll, and a second time for the
// background painting that scrolls.
// Without CompositeAfterPaint, this happens as the main graphics layer
// paints the background, and then the scrolling contents graphics layer
// paints the background.
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) {
auto paint_location = To<LayoutBox>(*box_fragment_.GetLayoutObject())
.GetBackgroundPaintLocation();
if (!(paint_location & kBackgroundPaintInGraphicsLayer))
info.SetSkipsBackground(true);
PaintObject(info, paint_offset);
info.SetSkipsBackground(false);
if (paint_location & kBackgroundPaintInScrollingContents) {
info.SetIsPaintingScrollingBackground(true);
PaintObject(info, paint_offset);
info.SetIsPaintingScrollingBackground(false);
}
} else {
PaintObject(info, paint_offset);
}
if (ShouldPaintDescendantBlockBackgrounds(original_phase))
info.phase = PaintPhase::kDescendantBlockBackgroundsOnly;
}
if (original_phase != PaintPhase::kSelfBlockBackgroundOnly &&
original_phase != PaintPhase::kSelfOutlineOnly &&
// For now all scrollers with overlay overflow controls are
// self-painting layers, so we don't need to traverse descendants
// here.
original_phase != PaintPhase::kOverlayOverflowControls) {
if (original_phase == PaintPhase::kMask ||
!box_fragment_.GetLayoutObject()->IsBox()) {
PaintObject(info, paint_offset);
} else {
ScopedBoxContentsPaintState contents_paint_state(
paint_state, To<LayoutBox>(*box_fragment_.GetLayoutObject()));
PaintObject(contents_paint_state.GetPaintInfo(),
contents_paint_state.PaintOffset());
}
}
// If the caret's node's fragment's containing block is this block, and
// the paint action is PaintPhaseForeground, then paint the caret.
if (original_phase == PaintPhase::kForeground && LIKELY(!recorder)) {
DCHECK(!text_combine || !text_combine->NeedsAffineTransformInPaint());
PaintCaretsIfNeeded(paint_state, paint_info, paint_offset);
}
if (ShouldPaintSelfOutline(original_phase)) {
info.phase = PaintPhase::kSelfOutlineOnly;
PaintObject(info, paint_offset);
}
if (UNLIKELY(text_combine) &&
NGTextCombinePainter::ShouldPaint(*text_combine)) {
if (recorder) {
// Paint text decorations and emphasis marks without scaling and share.
DCHECK(text_combine->NeedsAffineTransformInPaint());
graphics_context_state_saver->Restore();
} else if (!paint_info.context.InDrawingRecorder()) {
if (DrawingRecorder::UseCachedDrawingIfPossible(
paint_info.context, GetDisplayItemClient(), paint_info.phase))
return;
recorder.emplace(paint_info.context, GetDisplayItemClient(),
paint_info.phase,
text_combine->VisualRectForPaint(paint_offset));
}
NGTextCombinePainter::Paint(info, paint_offset, *text_combine);
}
// We paint scrollbars after we painted other things, so that the scrollbars
// will sit above them.
info.phase = original_phase;
if (box_fragment_.IsScrollContainer()) {
DCHECK(!text_combine);
ScrollableAreaPainter(*PhysicalFragment().Layer()->GetScrollableArea())
.PaintOverflowControls(info, RoundedIntPoint(paint_offset));
}
}
void NGBoxFragmentPainter::RecordScrollHitTestData(
const PaintInfo& paint_info,
const DisplayItemClient& background_client) {
if (!box_fragment_.GetLayoutObject()->IsBox())
return;
BoxPainter(To<LayoutBox>(*box_fragment_.GetLayoutObject()))
.RecordScrollHitTestData(paint_info, background_client);
}
bool NGBoxFragmentPainter::ShouldRecordHitTestData(
const PaintInfo& paint_info) {
// Hit test data are only needed for compositing. This flag is used for for
// printing and drag images which do not need hit testing.
if (paint_info.GetGlobalPaintFlags() & kGlobalPaintFlattenCompositingLayers)
return false;
// If an object is not visible, it does not participate in hit testing.
if (PhysicalFragment().Style().Visibility() != EVisibility::kVisible)
return false;
// Table rows/sections do not participate in hit testing.
if (PhysicalFragment().IsTableNGRow() ||
PhysicalFragment().IsTableNGSection())
return false;
return true;
}
void NGBoxFragmentPainter::PaintObject(
const PaintInfo& paint_info,
const PhysicalOffset& paint_offset,
bool suppress_box_decoration_background) {
const PaintPhase paint_phase = paint_info.phase;
const NGPhysicalBoxFragment& fragment = PhysicalFragment();
const ComputedStyle& style = fragment.Style();
const bool is_visible = IsVisibleToPaint(fragment, style);
if (ShouldPaintSelfBlockBackground(paint_phase)) {
if (is_visible) {
PaintBoxDecorationBackground(paint_info, paint_offset,
suppress_box_decoration_background);
}
// We're done. We don't bother painting any children.
if (paint_phase == PaintPhase::kSelfBlockBackgroundOnly)
return;
}
if (paint_phase == PaintPhase::kMask && is_visible) {
PaintMask(paint_info, paint_offset);
return;
}
if (paint_phase == PaintPhase::kForeground) {
if (paint_info.ShouldAddUrlMetadata()) {
NGFragmentPainter(fragment, GetDisplayItemClient())
.AddURLRectIfNeeded(paint_info, paint_offset);
}
if (is_visible && fragment.HasExtraMathMLPainting())
NGMathMLPainter(fragment).Paint(paint_info, paint_offset);
}
// Paint children.
if (paint_phase != PaintPhase::kSelfOutlineOnly &&
(!fragment.Children().empty() || fragment.HasItems() ||
inline_box_cursor_) &&
!paint_info.DescendantPaintingBlocked()) {
if (is_visible && UNLIKELY(paint_phase == PaintPhase::kForeground &&
fragment.IsCSSBox() && style.HasColumnRule()))
PaintColumnRules(paint_info, paint_offset);
if (paint_phase != PaintPhase::kFloat) {
if (UNLIKELY(inline_box_cursor_)) {
// Use the descendants cursor for this painter if it is given.
// Self-painting inline box paints only parts of the container block.
// Adjust |paint_offset| because it is the offset of the inline box, but
// |descendants_| has offsets to the contaiing block.
DCHECK(box_item_);
NGInlineCursor descendants = inline_box_cursor_->CursorForDescendants();
const PhysicalOffset paint_offset_to_inline_formatting_context =
paint_offset - box_item_->OffsetInContainerFragment();
PaintInlineItems(paint_info.ForDescendants(),
paint_offset_to_inline_formatting_context,
box_item_->OffsetInContainerFragment(), &descendants);
} else if (items_) {
if (fragment.IsBlockFlow()) {
PaintBlockFlowContents(paint_info, paint_offset);
} else {
DCHECK(fragment.IsInlineBox());
NGInlineCursor cursor(fragment, *items_);
PaintInlineItems(paint_info.ForDescendants(), paint_offset,
PhysicalOffset(), &cursor);
}
} else if (!fragment.IsInlineFormattingContext()) {
PaintBlockChildren(paint_info, paint_offset);
}
}
if (paint_phase == PaintPhase::kFloat ||
paint_phase == PaintPhase::kSelectionDragImage ||
paint_phase == PaintPhase::kTextClip) {
if (fragment.HasFloatingDescendantsForPaint())
PaintFloats(paint_info);
}
}
if (!is_visible)
return;
// Collapsed borders paint *after* children have painted their backgrounds.
if (box_fragment_.IsTableNG() &&
paint_phase == PaintPhase::kDescendantBlockBackgroundsOnly) {
NGTablePainter(box_fragment_)
.PaintCollapsedBorders(paint_info, paint_offset,
VisualRect(paint_offset));
}
if (ShouldPaintSelfOutline(paint_phase)) {
if (NGOutlineUtils::HasPaintedOutline(style, fragment.GetNode())) {
NGFragmentPainter(fragment, GetDisplayItemClient())
.PaintOutline(paint_info, paint_offset, style);
}
} else if (ShouldPaintDescendantOutlines(paint_phase)) {
if (const ComputedStyle* outline_style =
fragment.StyleForContinuationOutline()) {
NGFragmentPainter(fragment, GetDisplayItemClient())
.PaintOutline(paint_info, paint_offset, *outline_style);
}
}
}
void NGBoxFragmentPainter::PaintCaretsIfNeeded(
const ScopedPaintState& paint_state,
const PaintInfo& paint_info,
const PhysicalOffset& paint_offset) {
if (!ShouldPaintCarets(box_fragment_))
return;
// Apply overflow clip if needed.
// reveal-caret-of-multiline-contenteditable.html needs this.
// TDOO(yoisn): We should share this code with |BlockPainter::Paint()|
absl::optional<ScopedPaintChunkProperties> paint_chunk_properties;
if (const auto* fragment = paint_state.FragmentToPaint()) {
if (const auto* properties = fragment->PaintProperties()) {
if (const auto* overflow_clip = properties->OverflowClip()) {
paint_chunk_properties.emplace(
paint_info.context.GetPaintController(), *overflow_clip,
*box_fragment_.GetLayoutObject(), DisplayItem::kCaret);
}
}
}
LocalFrame* frame = box_fragment_.GetLayoutObject()->GetFrame();
if (ShouldPaintCursorCaret(box_fragment_))
frame->Selection().PaintCaret(paint_info.context, paint_offset);
if (ShouldPaintDragCaret(box_fragment_)) {
frame->GetPage()->GetDragCaret().PaintDragCaret(frame, paint_info.context,
paint_offset);
}
}
void NGBoxFragmentPainter::PaintBlockFlowContents(
const PaintInfo& paint_info,
const PhysicalOffset& paint_offset) {
const NGPhysicalBoxFragment& fragment = PhysicalFragment();
const LayoutObject* layout_object = fragment.GetLayoutObject();
DCHECK(fragment.IsInlineFormattingContext());
// When the layout-tree gets into a bad state, we can end up trying to paint
// a fragment with inline children, without a paint fragment. See:
// http://crbug.com/1022545
if (!items_ || (layout_object && layout_object->NeedsLayout())) {
NOTREACHED();
return;
}
// MathML operators paint text (for example enlarged/stretched) content
// themselves using NGMathMLPainter.
if (UNLIKELY(fragment.IsMathMLOperator()))
return;
// Trying to rule out a null GraphicsContext, see: https://crbug.com/1040298
CHECK(&paint_info.context);
// Check if there were contents to be painted and return early if none.
// The union of |ContentsInkOverflow()| and |LocalRect()| covers the rect to
// check, in both cases of:
// 1. Painting non-scrolling contents.
// 2. Painting scrolling contents.
// For 1, check with |ContentsInkOverflow()|, except when there is no
// overflow, in which case check with |LocalRect()|. For 2, check with
// |LayoutOverflow()|, but this can be approximiated with
// |ContentsInkOverflow()|.
// TODO(crbug.com/829028): Column boxes do not have |ContentsInkOverflow| atm,
// hence skip the optimization. If we were to have it, this should be enabled.
// Otherwise, if we're ok with the perf, we can remove this TODO.
if (fragment.IsCSSBox()) {
PhysicalRect content_ink_rect = fragment.LocalRect();
content_ink_rect.Unite(fragment.ContentsInkOverflow());
if (!paint_info.IntersectsCullRect(content_ink_rect, paint_offset))
return;
}
DCHECK(items_);
NGInlineCursor children(fragment, *items_);
PaintLineBoxChildren(&children, paint_info.ForDescendants(), paint_offset);
}
void NGBoxFragmentPainter::PaintBlockChildren(const PaintInfo& paint_info,
PhysicalOffset paint_offset) {
DCHECK(!box_fragment_.IsInlineFormattingContext());
PaintInfo paint_info_for_descendants = paint_info.ForDescendants();
paint_info_for_descendants.SetIsInFragmentTraversal();
for (const NGLink& child : box_fragment_.Children()) {
const NGPhysicalFragment& child_fragment = *child;
DCHECK(child_fragment.IsBox());
if (child_fragment.HasSelfPaintingLayer() || child_fragment.IsFloating())
continue;
PaintBlockChild(child, paint_info, paint_info_for_descendants,
paint_offset);
}
}
void NGBoxFragmentPainter::PaintBlockChild(
const NGLink& child,
const PaintInfo& paint_info,
const PaintInfo& paint_info_for_descendants,
PhysicalOffset paint_offset) {
const NGPhysicalFragment& child_fragment = *child;
DCHECK(child_fragment.IsBox());
DCHECK(!child_fragment.HasSelfPaintingLayer());
DCHECK(!child_fragment.IsFloating());
const auto& box_child_fragment = To<NGPhysicalBoxFragment>(child_fragment);
if (box_child_fragment.CanTraverse()) {
if (!box_child_fragment.GetLayoutObject()) {
// It's normally FragmentData that provides us with the paint offset.
// FragmentData is (at least currently) associated with a LayoutObject.
// If we have no LayoutObject, we have no FragmentData, so we need to
// calculate the offset on our own (which is very simple, anyway).
// Bypass Paint() and jump directly to PaintObject(), to skip the code
// that assumes that we have a LayoutObject (and FragmentData).
PhysicalOffset child_offset = paint_offset + child.offset;
if (box_child_fragment.IsFragmentainerBox()) {
// This is a fragmentainer, and when node inside a fragmentation
// context paints multiple block fragments, we need to distinguish
// between them somehow, for paint caching to work. Therefore,
// establish a display item scope here.
unsigned identifier = FragmentainerUniqueIdentifier(box_child_fragment);
ScopedDisplayItemFragment scope(paint_info.context, identifier);
NGBoxFragmentPainter(box_child_fragment)
.PaintObject(paint_info, child_offset);
return;
}
NGBoxFragmentPainter(box_child_fragment)
.PaintObject(paint_info, child_offset);
return;
}
NGBoxFragmentPainter(box_child_fragment).Paint(paint_info_for_descendants);
return;
}
// Fall back to flow-thread painting when reaching a column (the flow thread
// is treated as a self-painting PaintLayer when fragment traversal is
// disabled, so nothing to do here).
if (box_child_fragment.IsColumnBox())
return;
auto* layout_object = child_fragment.GetLayoutObject();
DCHECK(layout_object);
if (child_fragment.IsPaintedAtomically() &&
child_fragment.IsLegacyLayoutRoot()) {
ObjectPainter(*layout_object)
.PaintAllPhasesAtomically(paint_info_for_descendants);
} else {
// TODO(ikilpatrick): Once FragmentItem ships we should call the
// NGBoxFragmentPainter directly for NG objects.
layout_object->Paint(paint_info_for_descendants);
}
}
void NGBoxFragmentPainter::PaintFloatingItems(const PaintInfo& paint_info,
NGInlineCursor* cursor) {
while (*cursor) {
const NGFragmentItem* item = cursor->Current().Item();
DCHECK(item);
const NGPhysicalBoxFragment* child_fragment = item->BoxFragment();
if (!child_fragment) {
cursor->MoveToNext();
continue;
}
if (child_fragment->HasSelfPaintingLayer()) {
cursor->MoveToNextSkippingChildren();
continue;
}
if (child_fragment->IsFloating()) {
if (child_fragment->CanTraverse()) {
NGBoxFragmentPainter(*child_fragment).Paint(paint_info);
} else {
ObjectPainter(*child_fragment->GetLayoutObject())
.PaintAllPhasesAtomically(paint_info);
}
}
DCHECK(child_fragment->IsInlineBox() || !cursor->Current().HasChildren());
cursor->MoveToNext();
}
}
void NGBoxFragmentPainter::PaintFloatingChildren(
const NGPhysicalFragment& container,
const PaintInfo& paint_info,
const PaintInfo& float_paint_info) {
DCHECK(container.HasFloatingDescendantsForPaint());
for (const NGLink& child : container.Children()) {
const NGPhysicalFragment& child_fragment = *child;
if (child_fragment.HasSelfPaintingLayer())
continue;
if (child_fragment.CanTraverse()) {
if (child_fragment.IsFloating()) {
NGBoxFragmentPainter(To<NGPhysicalBoxFragment>(child_fragment))
.Paint(float_paint_info);
continue;
}
// Any non-floated children which paint atomically shouldn't be traversed.
if (child_fragment.IsPaintedAtomically())
continue;
} else {
if (child_fragment.IsFloating()) {
// TODO(kojii): The float is outside of the inline formatting context
// and that it maybe another NG inline formatting context, NG block
// layout, or legacy. NGBoxFragmentPainter can handle only the first
// case. In order to cover more tests for other two cases, we always
// fallback to legacy, which will forward back to NGBoxFragmentPainter
// if the float is for NGBoxFragmentPainter. We can shortcut this for
// the first case when we're more stable.
ObjectPainter(*child_fragment.GetLayoutObject())
.PaintAllPhasesAtomically(float_paint_info);
continue;
}
// Any children which paint atomically shouldn't be traversed.
if (child_fragment.IsPaintedAtomically())
continue;
// Drawing in SelectionDragImage phase can result in an exponential
// paint time: crbug.com://1182106
if (paint_info.phase != PaintPhase::kSelectionDragImage &&
child_fragment.Type() == NGPhysicalFragment::kFragmentBox &&
FragmentRequiresLegacyFallback(child_fragment)) {
child_fragment.GetLayoutObject()->Paint(paint_info);
continue;
}
}
// The selection paint traversal is special. We will visit all fragments
// (including floats) in the normal paint traversal. There isn't any point
// performing the special float traversal here.
if (paint_info.phase == PaintPhase::kSelectionDragImage)
continue;
if (!child_fragment.HasFloatingDescendantsForPaint())
continue;
if (child_fragment.HasNonVisibleOverflow()) {
// We need to properly visit this fragment for painting, rather than
// jumping directly to its children (which is what we normally do when
// looking for floats), in order to set up the clip rectangle.
NGBoxFragmentPainter(To<NGPhysicalBoxFragment>(child_fragment))
.Paint(paint_info);
continue;
}
if (child_fragment.IsFragmentainerBox()) {
// This is a fragmentainer, and when node inside a fragmentation context
// paints multiple block fragments, we need to distinguish between them
// somehow, for paint caching to work. Therefore, establish a display item
// scope here.
unsigned identifier = FragmentainerUniqueIdentifier(
To<NGPhysicalBoxFragment>(child_fragment));
ScopedDisplayItemFragment scope(paint_info.context, identifier);
PaintFloatingChildren(child_fragment, paint_info, float_paint_info);
} else {
PaintFloatingChildren(child_fragment, paint_info, float_paint_info);
}
}
// Now process the inline formatting context, if any. Note that even if this
// is an inline formatting context, we still need to walk the box fragment
// children (like we did above). If a float is block-fragmented, it is resumed
// as a regular box fragment child, rather than becoming a fragment item.
if (const NGPhysicalBoxFragment* box =
DynamicTo<NGPhysicalBoxFragment>(&container)) {
if (const NGFragmentItems* items = box->Items()) {
NGInlineCursor cursor(*box, *items);
PaintFloatingItems(float_paint_info, &cursor);
return;
}
if (inline_box_cursor_) {
DCHECK(box->IsInlineBox());
NGInlineCursor descendants = inline_box_cursor_->CursorForDescendants();
PaintFloatingItems(float_paint_info, &descendants);
return;
}
DCHECK(!box->IsInlineBox());
}
}
void NGBoxFragmentPainter::PaintFloats(const PaintInfo& paint_info) {
DCHECK(PhysicalFragment().HasFloatingDescendantsForPaint() ||
!PhysicalFragment().IsInlineFormattingContext());
PaintInfo float_paint_info(paint_info);
if (paint_info.phase == PaintPhase::kFloat)
float_paint_info.phase = PaintPhase::kForeground;
PaintFloatingChildren(PhysicalFragment(), paint_info, float_paint_info);
}
void NGBoxFragmentPainter::PaintMask(const PaintInfo& paint_info,
const PhysicalOffset& paint_offset) {
DCHECK_EQ(PaintPhase::kMask, paint_info.phase);
const NGPhysicalBoxFragment& physical_box_fragment = PhysicalFragment();
const ComputedStyle& style = physical_box_fragment.Style();
if (!style.HasMask() || !IsVisibleToPaint(physical_box_fragment, style))
return;
if (DrawingRecorder::UseCachedDrawingIfPossible(
paint_info.context, GetDisplayItemClient(), paint_info.phase))
return;
if (physical_box_fragment.IsFieldsetContainer()) {
NGFieldsetPainter(box_fragment_).PaintMask(paint_info, paint_offset);
return;
}
// TODO(eae): Switch to LayoutNG version of BackgroundImageGeometry.
BackgroundImageGeometry geometry(*static_cast<const LayoutBoxModelObject*>(
box_fragment_.GetLayoutObject()));
DrawingRecorder recorder(paint_info.context, GetDisplayItemClient(),
paint_info.phase, VisualRect(paint_offset));
PhysicalRect paint_rect(paint_offset, box_fragment_.Size());
PaintMaskImages(paint_info, paint_rect, *box_fragment_.GetLayoutObject(),
geometry, box_fragment_.SidesToInclude());
}
// TODO(kojii): This logic is kept in sync with BoxPainter. Not much efforts to
// eliminate LayoutObject dependency were done yet.
void NGBoxFragmentPainter::PaintBoxDecorationBackground(
const PaintInfo& paint_info,
const PhysicalOffset& paint_offset,
bool suppress_box_decoration_background) {
// TODO(mstensho): Break dependency on LayoutObject functionality.
const LayoutObject& layout_object = *box_fragment_.GetLayoutObject();
PhysicalRect paint_rect;
const DisplayItemClient* background_client = nullptr;
absl::optional<ScopedBoxContentsPaintState> contents_paint_state;
bool painting_scrolling_background =
IsPaintingScrollingBackground(paint_info);
IntRect visual_rect;
if (painting_scrolling_background) {
// For the case where we are painting the background into the scrolling
// contents layer of a composited scroller we need to include the entire
// overflow rect.
const LayoutBox& layout_box = To<LayoutBox>(layout_object);
paint_rect = layout_box.PhysicalLayoutOverflowRect();
contents_paint_state.emplace(paint_info, paint_offset, layout_box);
paint_rect.Move(contents_paint_state->PaintOffset());
// The background painting code assumes that the borders are part of the
// paintRect so we expand the paintRect by the border size when painting the
// background into the scrolling contents layer.
paint_rect.Expand(layout_box.BorderBoxOutsets());
background_client = &layout_box.GetScrollableArea()
->GetScrollingBackgroundDisplayItemClient();
visual_rect = layout_box.GetScrollableArea()->ScrollingBackgroundVisualRect(
paint_offset);
} else {
paint_rect.offset = paint_offset;
paint_rect.size = box_fragment_.Size();
if (layout_object.IsTableCell()) {
paint_rect.size =
PhysicalSize(To<LayoutBox>(layout_object).PixelSnappedSize());
}
background_client = &GetDisplayItemClient();
visual_rect = VisualRect(paint_offset);
}
if (!suppress_box_decoration_background) {
// The fieldset painter is not skipped when there is no background because
// the legend needs to paint.
if (PhysicalFragment().IsFieldsetContainer()) {
NGFieldsetPainter(box_fragment_)
.PaintBoxDecorationBackground(paint_info, paint_offset);
} else if (PhysicalFragment().IsTableNGPart()) {
if (box_fragment_.IsTableNGCell()) {
NGTableCellPainter(box_fragment_)
.PaintBoxDecorationBackground(paint_info, paint_offset,
*background_client, visual_rect);
} else if (box_fragment_.IsTableNGRow()) {
NGTableRowPainter(box_fragment_)
.PaintBoxDecorationBackground(paint_info, paint_offset,
*background_client, visual_rect);
} else if (box_fragment_.IsTableNGSection()) {
NGTableSectionPainter(box_fragment_)
.PaintBoxDecorationBackground(paint_info, paint_offset,
*background_client, visual_rect);
} else {
DCHECK(box_fragment_.IsTableNG());
NGTablePainter(box_fragment_)
.PaintBoxDecorationBackground(paint_info, paint_offset,
*background_client, visual_rect);
}
} else if (box_fragment_.Style().HasBoxDecorationBackground()) {
PaintBoxDecorationBackgroundWithRect(
contents_paint_state ? contents_paint_state->GetPaintInfo()
: paint_info,
visual_rect, paint_rect, *background_client);
}
}
if (ShouldRecordHitTestData(paint_info)) {
paint_info.context.GetPaintController().RecordHitTestData(
*background_client, PixelSnappedIntRect(paint_rect),
PhysicalFragment().EffectiveAllowedTouchAction(),
PhysicalFragment().InsideBlockingWheelEventHandler());
}
bool needs_scroll_hit_test = true;
if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) {
// Pre-CompositeAfterPaint, there is no need to emit scroll hit test
// display items for composited scrollers because these display items are
// only used to create non-fast scrollable regions for non-composited
// scrollers. With CompositeAfterPaint, we always paint the scroll hit
// test display items but ignore the non-fast region if the scroll was
// composited in PaintArtifactCompositor::UpdateNonFastScrollableRegions.
const auto* layer = PhysicalFragment().Layer();
if (layer && layer->GetCompositedLayerMapping() &&
layer->GetCompositedLayerMapping()->ScrollingContentsLayer()) {
needs_scroll_hit_test = false;
}
}
// Record the scroll hit test after the non-scrolling background so
// background squashing is not affected. Hit test order would be equivalent
// if this were immediately before the non-scrolling background.
if (!painting_scrolling_background && needs_scroll_hit_test)
RecordScrollHitTestData(paint_info, *background_client);
}
void NGBoxFragmentPainter::PaintBoxDecorationBackgroundWithRect(
const PaintInfo& paint_info,
const IntRect& visual_rect,
const PhysicalRect& paint_rect,
const DisplayItemClient& background_client) {
const auto& layout_box = To<LayoutBox>(*box_fragment_.GetLayoutObject());
absl::optional<DisplayItemCacheSkipper> cache_skipper;
if (RuntimeEnabledFeatures::PaintUnderInvalidationCheckingEnabled() &&
ShouldSkipPaintUnderInvalidationChecking(layout_box))
cache_skipper.emplace(paint_info.context);
BoxDecorationData box_decoration_data(paint_info, PhysicalFragment());
if (!box_decoration_data.ShouldPaint())
return;
if (DrawingRecorder::UseCachedDrawingIfPossible(
paint_info.context, background_client,
DisplayItem::kBoxDecorationBackground))
return;
DrawingRecorder recorder(paint_info.context, background_client,
DisplayItem::kBoxDecorationBackground, visual_rect);
PaintBoxDecorationBackgroundWithRectImpl(paint_info, paint_rect,
box_decoration_data);
}
// TODO(kojii): This logic is kept in sync with BoxPainter. Not much efforts to
// eliminate LayoutObject dependency were done yet.
void NGBoxFragmentPainter::PaintBoxDecorationBackgroundWithRectImpl(
const PaintInfo& paint_info,
const PhysicalRect& paint_rect,
const BoxDecorationData& box_decoration_data) {
const LayoutObject& layout_object = *box_fragment_.GetLayoutObject();
const LayoutBox& layout_box = To<LayoutBox>(layout_object);
const ComputedStyle& style = box_fragment_.Style();
GraphicsContextStateSaver state_saver(paint_info.context, false);
if (box_decoration_data.ShouldPaintShadow()) {
PaintNormalBoxShadow(paint_info, paint_rect, style,
box_fragment_.SidesToInclude(),
!box_decoration_data.ShouldPaintBackground());
}
bool needs_end_layer = false;
if (!box_decoration_data.IsPaintingScrollingBackground()) {
if (box_fragment_.HasSelfPaintingLayer() && layout_box.IsTableCell() &&
ToInterface<LayoutNGTableCellInterface>(layout_box)
.TableInterface()
->ShouldCollapseBorders()) {
// TODO(crbug.com/1081425) This branch is only used by Legacy
// tables. Remove when Legacy tables are removed.
// We have to clip here because the background would paint on top of the
// collapsed table borders otherwise, since this is a self-painting layer.
PhysicalRect clip_rect = paint_rect;
clip_rect.Expand(layout_box.BorderInsets());
state_saver.Save();
paint_info.context.Clip(PixelSnappedIntRect(clip_rect));
} else if (BleedAvoidanceIsClipping(
box_decoration_data.GetBackgroundBleedAvoidance())) {
state_saver.Save();
FloatRoundedRect border =
RoundedBorderGeometry::PixelSnappedRoundedBorder(
style, paint_rect, box_fragment_.SidesToInclude());
paint_info.context.ClipRoundedRect(border);
if (box_decoration_data.GetBackgroundBleedAvoidance() ==
kBackgroundBleedClipLayer) {
paint_info.context.BeginLayer();
needs_end_layer = true;
}
}
}
IntRect snapped_paint_rect(PixelSnappedIntRect(paint_rect));
ThemePainter& theme_painter = LayoutTheme::GetTheme().Painter();
bool theme_painted =
box_decoration_data.HasAppearance() &&
!theme_painter.Paint(layout_box, paint_info, snapped_paint_rect);
if (!theme_painted) {
if (box_decoration_data.ShouldPaintBackground()) {
PaintBackground(paint_info, paint_rect,
box_decoration_data.BackgroundColor(),
box_decoration_data.GetBackgroundBleedAvoidance());
}
if (box_decoration_data.HasAppearance()) {
theme_painter.PaintDecorations(layout_box.GetNode(),
layout_box.GetDocument(), style,
paint_info, snapped_paint_rect);
}
}
if (box_decoration_data.ShouldPaintShadow()) {
if (layout_box.IsTableCell()) {
PhysicalRect inner_rect = paint_rect;
inner_rect.Contract(layout_box.BorderBoxOutsets());
// PaintInsetBoxShadowWithInnerRect doesn't subtract borders before
// painting. We have to use it here after subtracting collapsed borders
// above. PaintInsetBoxShadowWithBorderRect below subtracts the borders
// specified on the style object, which doesn't account for border
// collapsing.
BoxPainterBase::PaintInsetBoxShadowWithInnerRect(paint_info, inner_rect,
style);
} else {
PaintInsetBoxShadowWithBorderRect(paint_info, paint_rect, style,
box_fragment_.SidesToInclude());
}
}
// The theme will tell us whether or not we should also paint the CSS
// border.
if (box_decoration_data.ShouldPaintBorder()) {
if (!theme_painted) {
theme_painted =
box_decoration_data.HasAppearance() &&
!LayoutTheme::GetTheme().Painter().PaintBorderOnly(
layout_box.GetNode(), style, paint_info, snapped_paint_rect);
}
if (!theme_painted) {
Node* generating_node = layout_object.GeneratingNode();
const Document& document = layout_object.GetDocument();
PaintBorder(*box_fragment_.GetLayoutObject(), document, generating_node,
paint_info, paint_rect, style,
box_decoration_data.GetBackgroundBleedAvoidance(),
box_fragment_.SidesToInclude());
}
}
if (needs_end_layer)
paint_info.context.EndLayer();
}
void NGBoxFragmentPainter::PaintBoxDecorationBackgroundForBlockInInline(
NGInlineCursor* children,
const PaintInfo& paint_info,
const PhysicalOffset& paint_offset) {
for (; *children; children->MoveToNext()) {
const NGFragmentItem* item = children->Current().Item();
if (item->Type() != NGFragmentItem::kBox)
continue;
const NGPhysicalBoxFragment* fragment = item->BoxFragment();
if (fragment && fragment->IsBlockInInline())
PaintBoxItem(*item, *fragment, *children, paint_info, paint_offset);
}
}
void NGBoxFragmentPainter::PaintColumnRules(
const PaintInfo& paint_info,
const PhysicalOffset& paint_offset) {
const ComputedStyle& style = box_fragment_.Style();
DCHECK(box_fragment_.IsCSSBox());
DCHECK(style.HasColumnRule());
// TODO(crbug.com/792437): Certain rule styles should be converted.
EBorderStyle rule_style = style.ColumnRuleStyle();
if (DrawingRecorder::UseCachedDrawingIfPossible(paint_info.context,
GetDisplayItemClient(),
DisplayItem::kColumnRules))
return;
DrawingRecorder recorder(paint_info.context, GetDisplayItemClient(),
DisplayItem::kColumnRules, IntRect());
const Color& rule_color =
LayoutObject::ResolveColor(style, GetCSSPropertyColumnRuleColor());
LayoutUnit rule_thickness(style.ColumnRuleWidth());
PhysicalRect previous_column;
bool past_first_column_in_row = false;
for (const NGLink& child : box_fragment_.Children()) {
if (!child->IsColumnBox()) {
// Column spanner. Continue in the next row, if there are 2 columns or
// more there.
past_first_column_in_row = false;
previous_column = PhysicalRect();
continue;
}
PhysicalRect current_column(child.offset, child->Size());
if (!past_first_column_in_row) {
// Rules are painted *between* columns. Need to see if we have a second
// one before painting anything.
past_first_column_in_row = true;
previous_column = current_column;
continue;
}
PhysicalRect rule;
BoxSide box_side;
if (previous_column.Y() == current_column.Y() ||
previous_column.Bottom() == current_column.Bottom()) {
// Horizontal writing-mode.
DCHECK(style.IsHorizontalWritingMode());
LayoutUnit center;
if (previous_column.X() < current_column.X()) {
// Left to right.
center = (previous_column.X() + current_column.Right()) / 2;
box_side = BoxSide::kLeft;
} else {
// Right to left.
center = (current_column.X() + previous_column.Right()) / 2;
box_side = BoxSide::kRight;
}
LayoutUnit rule_length = previous_column.Height();
DCHECK_GE(rule_length, current_column.Height());
rule.offset.top = previous_column.offset.top;
rule.size.height = rule_length;
rule.offset.left = center - rule_thickness / 2;
rule.size.width = rule_thickness;
} else {
// Vertical writing-mode.
LayoutUnit center;
if (previous_column.Y() < current_column.Y()) {
// Top to bottom.
center = (previous_column.Y() + current_column.Bottom()) / 2;
box_side = BoxSide::kTop;
} else {
// Bottom to top.
center = (current_column.Y() + previous_column.Bottom()) / 2;
box_side = BoxSide::kBottom;
}
LayoutUnit rule_length = previous_column.Width();
DCHECK_GE(rule_length, current_column.Width());
rule.offset.left = previous_column.offset.left;
rule.size.width = rule_length;
rule.offset.top = center - rule_thickness / 2;
rule.size.height = rule_thickness;
}
// TODO(crbug.com/792435): The spec actually kind of says that the rules
// should be as tall as the entire multicol container, not just as tall as
// the column fragments (this difference matters when block-size is
// specified and columns are balanced).
rule.Move(paint_offset);
IntRect snapped_rule = PixelSnappedIntRect(rule);
BoxBorderPainter::DrawBoxSide(paint_info.context, snapped_rule, box_side,
rule_color, rule_style);
recorder.UniteVisualRect(snapped_rule);
previous_column = current_column;
}
}
// TODO(kojii): This logic is kept in sync with BoxPainter. Not much efforts to
// eliminate LayoutObject dependency were done yet.
void NGBoxFragmentPainter::PaintBackground(
const PaintInfo& paint_info,
const PhysicalRect& paint_rect,
const Color& background_color,
BackgroundBleedAvoidance bleed_avoidance) {
const auto& layout_box = To<LayoutBox>(*box_fragment_.GetLayoutObject());
if (layout_box.BackgroundTransfersToView())
return;
if (layout_box.BackgroundIsKnownToBeObscured())
return;
// TODO(eae): Switch to LayoutNG version of BackgroundImageGeometry.
BackgroundImageGeometry geometry(layout_box);
PaintFillLayers(paint_info, background_color,
box_fragment_.Style().BackgroundLayers(), paint_rect,
geometry, bleed_avoidance);
}
void NGBoxFragmentPainter::PaintInlineChildBoxUsingLegacyFallback(
const NGPhysicalFragment& fragment,
const PaintInfo& paint_info) {
const LayoutObject* child_layout_object = fragment.GetLayoutObject();
DCHECK(child_layout_object);
if (child_layout_object->IsAtomicInlineLevel()) {
// Pre-NG painters also expect callers to use |PaintAllPhasesAtomically()|
// for atomic inlines.
ObjectPainter(*child_layout_object).PaintAllPhasesAtomically(paint_info);
return;
}
child_layout_object->Paint(paint_info);
}
void NGBoxFragmentPainter::PaintAllPhasesAtomically(
const PaintInfo& paint_info) {
// Self-painting AtomicInlines should go to normal paint logic.
DCHECK(!(PhysicalFragment().IsPaintedAtomically() &&
box_fragment_.HasSelfPaintingLayer()));
// Pass PaintPhaseSelection and PaintPhaseTextClip is handled by the regular
// foreground paint implementation. We don't need complete painting for these
// phases.
PaintPhase phase = paint_info.phase;
if (phase == PaintPhase::kSelectionDragImage ||
phase == PaintPhase::kTextClip)
return PaintInternal(paint_info);
if (phase != PaintPhase::kForeground)
return;
PaintInfo local_paint_info(paint_info);
local_paint_info.phase = PaintPhase::kBlockBackground;
PaintInternal(local_paint_info);
local_paint_info.phase = PaintPhase::kForcedColorsModeBackplate;
PaintInternal(local_paint_info);
local_paint_info.phase = PaintPhase::kFloat;
PaintInternal(local_paint_info);
local_paint_info.phase = PaintPhase::kForeground;
PaintInternal(local_paint_info);
local_paint_info.phase = PaintPhase::kOutline;
PaintInternal(local_paint_info);
}
void NGBoxFragmentPainter::PaintInlineItems(const PaintInfo& paint_info,
const PhysicalOffset& paint_offset,
const PhysicalOffset& parent_offset,
NGInlineCursor* cursor) {
while (*cursor) {
const NGFragmentItem* item = cursor->CurrentItem();
DCHECK(item);
if (UNLIKELY(item->IsLayoutObjectDestroyedOrMoved())) {
// TODO(crbug.com/1099613): This should not happen, as long as it is
// really layout-clean.
NOTREACHED();
cursor->MoveToNextSkippingChildren();
continue;
}
switch (item->Type()) {
case NGFragmentItem::kText:
case NGFragmentItem::kSvgText:
case NGFragmentItem::kGeneratedText:
if (!item->IsHiddenForPaint())
PaintTextItem(*cursor, paint_info, paint_offset, parent_offset);
cursor->MoveToNext();
break;
case NGFragmentItem::kBox:
if (!item->IsHiddenForPaint())
PaintBoxItem(*item, *cursor, paint_info, paint_offset, parent_offset);
cursor->MoveToNextSkippingChildren();
break;
case NGFragmentItem::kLine:
NOTREACHED();
cursor->MoveToNext();
break;
}
}
}
// Paint a line box. This function paints hit tests and backgrounds of
// `::first-line`. In all other cases, the container box paints background.
inline void NGBoxFragmentPainter::PaintLineBox(
const NGPhysicalFragment& line_box_fragment,
const DisplayItemClient& display_item_client,
const NGFragmentItem& line_box_item,
wtf_size_t line_fragment_id,
const PaintInfo& paint_info,
const PhysicalOffset& child_offset) {
if (paint_info.phase != PaintPhase::kForeground)
return;
absl::optional<ScopedDisplayItemFragment> display_item_fragment;
if (ShouldRecordHitTestData(paint_info)) {
display_item_fragment.emplace(paint_info.context, line_fragment_id);
PhysicalRect border_box = line_box_fragment.LocalRect();
border_box.offset += child_offset;
paint_info.context.GetPaintController().RecordHitTestData(
display_item_client, PixelSnappedIntRect(border_box),
PhysicalFragment().EffectiveAllowedTouchAction(),
PhysicalFragment().InsideBlockingWheelEventHandler());
}
// Paint the background of the `::first-line` line box.
if (NGLineBoxFragmentPainter::NeedsPaint(line_box_fragment)) {
if (!display_item_fragment)
display_item_fragment.emplace(paint_info.context, line_fragment_id);
NGLineBoxFragmentPainter line_box_painter(line_box_fragment, line_box_item,
PhysicalFragment());
line_box_painter.PaintBackgroundBorderShadow(paint_info, child_offset);
}
}
void NGBoxFragmentPainter::PaintLineBoxChildren(
NGInlineCursor* children,
const PaintInfo& paint_info,
const PhysicalOffset& paint_offset) {
// Only paint during the foreground/selection phases.
if (paint_info.phase != PaintPhase::kForeground &&
paint_info.phase != PaintPhase::kForcedColorsModeBackplate &&
paint_info.phase != PaintPhase::kSelectionDragImage &&
paint_info.phase != PaintPhase::kTextClip &&
paint_info.phase != PaintPhase::kMask &&
paint_info.phase != PaintPhase::kDescendantOutlinesOnly &&
paint_info.phase != PaintPhase::kOutline) {
if (UNLIKELY(ShouldPaintDescendantBlockBackgrounds(paint_info.phase))) {
// When block-in-inline, block backgrounds need to be painted.
PaintBoxDecorationBackgroundForBlockInInline(children, paint_info,
paint_offset);
}
return;
}
// The only way an inline could paint like this is if it has a layer.
const auto* layout_object = box_fragment_.GetLayoutObject();
DCHECK(!layout_object || layout_object->IsLayoutBlock() ||
(layout_object->IsLayoutInline() && layout_object->HasLayer()));
if (paint_info.phase == PaintPhase::kForeground &&
paint_info.ShouldAddUrlMetadata()) {
AddURLRectsForInlineChildrenRecursively(*layout_object, paint_info,
paint_offset);
}
// If we have no lines then we have no work to do.
if (!*children)
return;
if (paint_info.phase == PaintPhase::kForcedColorsModeBackplate &&
box_fragment_.GetDocument().InForcedColorsMode()) {
PaintBackplate(children, paint_info, paint_offset);
return;
}
DCHECK(children->HasRoot());
PaintLineBoxChildItems(children, paint_info, paint_offset);
}
void NGBoxFragmentPainter::PaintLineBoxChildItems(
NGInlineCursor* children,
const PaintInfo& paint_info,
const PhysicalOffset& paint_offset) {
const bool is_horizontal = box_fragment_.Style().IsHorizontalWritingMode();
wtf_size_t line_fragment_id = NGFragmentItem::kInitialLineFragmentId;
for (; *children; children->MoveToNextSkippingChildren()) {
const NGFragmentItem* child_item = children->CurrentItem();
DCHECK(child_item);
if (child_item->IsFloating())
continue;
// Check if CullRect intersects with this child, only in block direction
// because soft-wrap and <br> needs to paint outside of InkOverflow() in
// inline direction.
const PhysicalOffset& child_offset =
paint_offset + child_item->OffsetInContainerFragment();
const PhysicalRect child_rect = child_item->InkOverflow();
if (is_horizontal) {
LayoutUnit y = child_rect.offset.top + child_offset.top;
if (!paint_info.GetCullRect().IntersectsVerticalRange(
y, y + child_rect.size.height))
continue;
} else {
LayoutUnit x = child_rect.offset.left + child_offset.left;
if (!paint_info.GetCullRect().IntersectsHorizontalRange(
x, x + child_rect.size.width))
continue;
}
if (child_item->Type() == NGFragmentItem::kLine) {
const NGPhysicalLineBoxFragment* line_box_fragment =
child_item->LineBoxFragment();
DCHECK(line_box_fragment);
PaintLineBox(*line_box_fragment, *child_item->GetDisplayItemClient(),
*child_item, line_fragment_id++, paint_info, child_offset);
NGInlineCursor line_box_cursor = children->CursorForDescendants();
PaintInlineItems(paint_info, paint_offset,
child_item->OffsetInContainerFragment(),
&line_box_cursor);
continue;
}
if (const NGPhysicalBoxFragment* child_fragment =
child_item->BoxFragment()) {
DCHECK(!child_fragment->IsOutOfFlowPositioned());
if (child_fragment->IsListMarker()) {
PaintBoxItem(*child_item, *child_fragment, *children, paint_info,
paint_offset);
continue;
}
}
NOTREACHED();
}
}
void NGBoxFragmentPainter::PaintBackplate(NGInlineCursor* line_boxes,
const PaintInfo& paint_info,
const PhysicalOffset& paint_offset) {
if (paint_info.phase != PaintPhase::kForcedColorsModeBackplate)
return;
// Only paint backplates behind text when forced-color-adjust is auto and the
// element is visible.
const ComputedStyle& style = PhysicalFragment().Style();
if (style.ForcedColorAdjust() == EForcedColorAdjust::kNone ||
style.Visibility() != EVisibility::kVisible)
return;
if (DrawingRecorder::UseCachedDrawingIfPossible(
paint_info.context, GetDisplayItemClient(),
DisplayItem::kForcedColorsModeBackplate))
return;
Color backplate_color = PhysicalFragment()
.GetLayoutObject()
->GetDocument()
.GetStyleEngine()
.ForcedBackgroundColor();
const auto& backplates = BuildBackplate(line_boxes, paint_offset);
DrawingRecorder recorder(paint_info.context, GetDisplayItemClient(),
DisplayItem::kForcedColorsModeBackplate,
EnclosingIntRect(UnionRect(backplates)));
for (const auto backplate : backplates)
paint_info.context.FillRect(FloatRect(backplate), backplate_color);
}
void NGBoxFragmentPainter::PaintTextItem(const NGInlineCursor& cursor,
const PaintInfo& paint_info,
const PhysicalOffset& paint_offset,
const PhysicalOffset& parent_offset) {
DCHECK(cursor.CurrentItem());
const NGFragmentItem& item = *cursor.CurrentItem();
DCHECK(item.IsText()) << item;
// Only paint during the foreground/selection phases.
if (paint_info.phase != PaintPhase::kForeground &&
paint_info.phase != PaintPhase::kSelectionDragImage &&
paint_info.phase != PaintPhase::kTextClip &&
paint_info.phase != PaintPhase::kMask)
return;
// Skip if this child does not intersect with CullRect.
if (!paint_info.IntersectsCullRect(
item.InkOverflow(),
paint_offset + item.OffsetInContainerFragment()) &&
// Don't skip <br>, it doesn't have ink but need to paint selection.
!(item.IsLineBreak() && HasSelection(item.GetLayoutObject())))
return;
ScopedDisplayItemFragment display_item_fragment(paint_info.context,
item.FragmentId());
NGTextFragmentPainter text_painter(cursor, parent_offset);
text_painter.Paint(paint_info, paint_offset);
}
// Paint non-culled box item.
void NGBoxFragmentPainter::PaintBoxItem(
const NGFragmentItem& item,
const NGPhysicalBoxFragment& child_fragment,
const NGInlineCursor& cursor,
const PaintInfo& paint_info,
const PhysicalOffset& paint_offset) {
DCHECK_EQ(item.Type(), NGFragmentItem::kBox);
DCHECK_EQ(&item, cursor.Current().Item());
DCHECK_EQ(item.PostLayoutBoxFragment(), &child_fragment);
DCHECK(!child_fragment.IsHiddenForPaint());
if (child_fragment.HasSelfPaintingLayer() || child_fragment.IsFloating())
return;
// Skip if this child does not intersect with CullRect.
if (!paint_info.IntersectsCullRect(
child_fragment.InkOverflow(),
paint_offset + item.OffsetInContainerFragment()))
return;
if (child_fragment.IsAtomicInline() || child_fragment.IsListMarker()) {
if (FragmentRequiresLegacyFallback(child_fragment)) {
// The legacy painter may be confused when painting fragmented descendant
// PaintLayers (which should not be fragmented but legacy layout does) and
// will produce duplicated PaintChunk ids for the fragments. Skip display
// item cache to tolerate that.
absl::optional<DisplayItemCacheSkipper> skipper;
if (paint_info.context.GetPaintController().CurrentFragment())
skipper.emplace(paint_info.context);
PaintInlineChildBoxUsingLegacyFallback(child_fragment, paint_info);
return;
}
NGBoxFragmentPainter(child_fragment).PaintAllPhasesAtomically(paint_info);
return;
}
if (child_fragment.IsInlineBox()) {
NGInlineBoxFragmentPainter(cursor, item, child_fragment)
.Paint(paint_info, paint_offset);
return;
}
// Block-in-inline
DCHECK(RuntimeEnabledFeatures::LayoutNGBlockInInlineEnabled());
DCHECK(!child_fragment.GetLayoutObject()->IsInline());
PaintInfo paint_info_for_descendants = paint_info.ForDescendants();
paint_info_for_descendants.SetIsInFragmentTraversal();
PaintBlockChild({&child_fragment, item.OffsetInContainerFragment()},
paint_info, paint_info_for_descendants, paint_offset);
}
void NGBoxFragmentPainter::PaintBoxItem(const NGFragmentItem& item,
const NGInlineCursor& cursor,
const PaintInfo& paint_info,
const PhysicalOffset& paint_offset,
const PhysicalOffset& parent_offset) {
DCHECK_EQ(item.Type(), NGFragmentItem::kBox);
DCHECK_EQ(&item, cursor.Current().Item());
if (const NGPhysicalBoxFragment* child_fragment = item.BoxFragment()) {
child_fragment = child_fragment->PostLayout();
if (child_fragment)
PaintBoxItem(item, *child_fragment, cursor, paint_info, paint_offset);
return;
}
// Skip if this child does not intersect with CullRect.
if (!paint_info.IntersectsCullRect(
item.InkOverflow(), paint_offset + item.OffsetInContainerFragment()))
return;
// This |item| is a culled inline box.
DCHECK(item.GetLayoutObject()->IsLayoutInline());
NGInlineCursor children = cursor.CursorForDescendants();
// Pass the given |parent_offset| because culled inline boxes do not affect
// the sub-pixel snapping behavior. TODO(kojii): This is for the
// compatibility, we may want to revisit in future.
PaintInlineItems(paint_info, paint_offset, parent_offset, &children);
}
bool NGBoxFragmentPainter::IsPaintingScrollingBackground(
const PaintInfo& paint_info) const {
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
return paint_info.IsPaintingScrollingBackground();
// TODO(layout-dev): Change paint_info.PaintContainer to accept fragments
// once LayoutNG supports scrolling containers.
return paint_info.PaintFlags() & kPaintLayerPaintingOverflowContents &&
!(paint_info.PaintFlags() &
kPaintLayerPaintingCompositingBackgroundPhase) &&
box_fragment_.GetLayoutObject() == paint_info.PaintContainer();
}
bool NGBoxFragmentPainter::ShouldPaint(
const ScopedPaintState& paint_state) const {
// TODO(layout-dev): Add support for scrolling, see BlockPainter::ShouldPaint.
const NGPhysicalBoxFragment& fragment = PhysicalFragment();
if (!fragment.IsInlineBox()) {
return paint_state.LocalRectIntersectsCullRect(
To<LayoutBox>(fragment.GetLayoutObject())
->PhysicalVisualOverflowRect());
}
NOTREACHED();
return false;
}
void NGBoxFragmentPainter::PaintTextClipMask(const PaintInfo& paint_info,
const IntRect& mask_rect,
const PhysicalOffset& paint_offset,
bool object_has_multiple_boxes) {
PaintInfo mask_paint_info(paint_info.context, CullRect(mask_rect),
PaintPhase::kTextClip, kGlobalPaintNormalPhase, 0);
mask_paint_info.SetFragmentID(paint_info.FragmentID());
if (!object_has_multiple_boxes) {
PaintObject(mask_paint_info, paint_offset);
return;
}
DCHECK(inline_box_cursor_);
DCHECK(box_item_);
NGInlineBoxFragmentPainter inline_box_painter(*inline_box_cursor_,
*box_item_);
PaintTextClipMask(mask_paint_info,
paint_offset - box_item_->OffsetInContainerFragment(),
&inline_box_painter);
}
void NGBoxFragmentPainter::PaintTextClipMask(
const PaintInfo& paint_info,
PhysicalOffset paint_offset,
NGInlineBoxFragmentPainter* inline_box_painter) {
const ComputedStyle& style = box_fragment_.Style();
if (style.BoxDecorationBreak() == EBoxDecorationBreak::kSlice) {
LayoutUnit offset_on_line;
LayoutUnit total_width;
inline_box_painter->ComputeFragmentOffsetOnLine(
style.Direction(), &offset_on_line, &total_width);
if (style.IsHorizontalWritingMode())
paint_offset.left += offset_on_line;
else
paint_offset.top += offset_on_line;
}
inline_box_painter->Paint(paint_info, paint_offset);
}
PhysicalRect NGBoxFragmentPainter::AdjustRectForScrolledContent(
const PaintInfo& paint_info,
const BoxPainterBase::FillLayerInfo& info,
const PhysicalRect& rect) {
PhysicalRect scrolled_paint_rect = rect;
GraphicsContext& context = paint_info.context;
const NGPhysicalBoxFragment& physical = PhysicalFragment();
// Clip to the overflow area.
if (info.is_clipped_with_local_scrolling &&
!IsPaintingScrollingBackground(paint_info)) {
context.Clip(FloatRect(physical.OverflowClipRect(rect.offset)));
// Adjust the paint rect to reflect a scrolled content box with borders at
// the ends.
scrolled_paint_rect.offset -=
PhysicalOffset(physical.PixelSnappedScrolledContentOffset());
LayoutRectOutsets borders = AdjustedBorderOutsets(info);
scrolled_paint_rect.size =
physical.ScrollSize() + PhysicalSize(borders.Size());
}
return scrolled_paint_rect;
}
LayoutRectOutsets NGBoxFragmentPainter::ComputeBorders() const {
if (box_fragment_.GetLayoutObject()->IsTableCellLegacy())
return To<LayoutBox>(box_fragment_.GetLayoutObject())->BorderBoxOutsets();
return BoxStrutToLayoutRectOutsets(PhysicalFragment().BorderWidths());
}
LayoutRectOutsets NGBoxFragmentPainter::ComputePadding() const {
return BoxStrutToLayoutRectOutsets(PhysicalFragment().PixelSnappedPadding());
}
BoxPainterBase::FillLayerInfo NGBoxFragmentPainter::GetFillLayerInfo(
const Color& color,
const FillLayer& bg_layer,
BackgroundBleedAvoidance bleed_avoidance,
bool is_painting_scrolling_background) const {
const NGPhysicalBoxFragment& fragment = PhysicalFragment();
RespectImageOrientationEnum respect_orientation =
LayoutObject::ShouldRespectImageOrientation(fragment.GetLayoutObject());
if (auto* style_image = bg_layer.GetImage()) {
respect_orientation =
style_image->ForceOrientationIfNecessary(respect_orientation);
}
return BoxPainterBase::FillLayerInfo(
fragment.GetLayoutObject()->GetDocument(), fragment.Style(),
fragment.IsScrollContainer(), color, bg_layer, bleed_avoidance,
respect_orientation, box_fragment_.SidesToInclude(),
fragment.GetLayoutObject()->IsLayoutInline(),
is_painting_scrolling_background);
}
template <typename T>
bool NGBoxFragmentPainter::HitTestContext::AddNodeToResult(
Node* node,
const NGPhysicalBoxFragment* box_fragment,
const T& bounds_rect,
const PhysicalOffset& offset) const {
if (node && !result->InnerNode())
result->SetNodeAndPosition(node, box_fragment, location.Point() - offset);
return result->AddNodeToListBasedTestResult(node, location, bounds_rect) ==
kStopHitTesting;
}
template <typename T>
bool NGBoxFragmentPainter::HitTestContext::AddNodeToResultWithContentOffset(
Node* node,
const NGPhysicalBoxFragment& container,
const T& bounds_rect,
PhysicalOffset offset) const {
if (container.IsScrollContainer())
offset += PhysicalOffset(container.PixelSnappedScrolledContentOffset());
return AddNodeToResult(node, &container, bounds_rect, offset);
}
bool NGBoxFragmentPainter::NodeAtPoint(HitTestResult& result,
const HitTestLocation& hit_test_location,
const PhysicalOffset& physical_offset,
HitTestAction action) {
HitTestContext hit_test(action, hit_test_location, physical_offset, &result);
const auto* const text_combine =
DynamicTo<LayoutNGTextCombine>(box_fragment_.GetLayoutObject());
if (UNLIKELY(text_combine) && text_combine->UsesScaleX())
hit_test.text_combine = text_combine;
return NodeAtPoint(hit_test, physical_offset);
}
bool NGBoxFragmentPainter::NodeAtPoint(HitTestResult& result,
const HitTestLocation& hit_test_location,
const PhysicalOffset& physical_offset,
const PhysicalOffset& inline_root_offset,
HitTestAction action) {
HitTestContext hit_test(action, hit_test_location, inline_root_offset,
&result);
const auto* const text_combine =
DynamicTo<LayoutNGTextCombine>(box_fragment_.GetLayoutObject());
if (UNLIKELY(text_combine) && text_combine->UsesScaleX())
hit_test.text_combine = text_combine;
return NodeAtPoint(hit_test, physical_offset);
}
bool NGBoxFragmentPainter::NodeAtPoint(const HitTestContext& hit_test,
const PhysicalOffset& physical_offset) {
const NGPhysicalBoxFragment& fragment = PhysicalFragment();
if (!fragment.MayIntersect(*hit_test.result, hit_test.location,
physical_offset))
return false;
bool pointer_events_bounding_box = false;
bool hit_test_self = fragment.IsInSelfHitTestingPhase(hit_test.action);
if (hit_test_self) {
// Table row and table section are never a hit target.
// SVG <text> is not a hit target except if 'pointer-events: bounding-box'.
if (PhysicalFragment().IsTableNGRow() ||
PhysicalFragment().IsTableNGSection()) {
hit_test_self = false;
} else if (fragment.IsSvgText()) {
pointer_events_bounding_box =
fragment.Style().PointerEvents() == EPointerEvents::kBoundingBox;
hit_test_self = pointer_events_bounding_box;
}
}
if (hit_test_self && box_fragment_.IsScrollContainer() &&
HitTestOverflowControl(hit_test, physical_offset))
return true;
const PhysicalSize& size = fragment.Size();
const ComputedStyle& style = fragment.Style();
const LayoutObject* layout_object = fragment.GetLayoutObject();
bool skip_children =
layout_object &&
layout_object == hit_test.result->GetHitTestRequest().GetStopNode();
if (!skip_children && box_fragment_.ShouldClipOverflowAlongEitherAxis()) {
// PaintLayer::HitTestContentsForFragments checked the fragments'
// foreground rect for intersection if a layer is self painting,
// so only do the overflow clip check here for non-self-painting layers.
if (!box_fragment_.HasSelfPaintingLayer() &&
!hit_test.location.Intersects(PhysicalFragment().OverflowClipRect(
physical_offset, kExcludeOverlayScrollbarSizeForHitTesting))) {
skip_children = true;
}
if (!skip_children && style.HasBorderRadius()) {
PhysicalRect bounds_rect(physical_offset, size);
skip_children = !hit_test.location.Intersects(
RoundedBorderGeometry::PixelSnappedRoundedInnerBorder(style,
bounds_rect));
}
}
if (!skip_children) {
if (!box_fragment_.IsScrollContainer()) {
if (HitTestChildren(hit_test, physical_offset))
return true;
} else {
const PhysicalOffset scrolled_offset =
physical_offset -
PhysicalOffset(
PhysicalFragment().PixelSnappedScrolledContentOffset());
HitTestContext adjusted_hit_test(hit_test.action, hit_test.location,
scrolled_offset, hit_test.result);
if (HitTestChildren(adjusted_hit_test, scrolled_offset))
return true;
}
}
if (style.HasBorderRadius() &&
HitTestClippedOutByBorder(hit_test.location, physical_offset))
return false;
// Now hit test ourselves.
if (hit_test_self &&
IsVisibleToHitTest(box_fragment_, hit_test.result->GetHitTestRequest())) {
PhysicalRect bounds_rect(physical_offset, size);
if (UNLIKELY(
hit_test.result->GetHitTestRequest().IsHitTestVisualOverflow())) {
// We'll include overflow from children here (in addition to self-overflow
// caused by filters), because we want to record a match if we hit the
// overflow of a child below the stop node. This matches legacy behavior
// in LayoutBox::NodeAtPoint(); see call to
// PhysicalVisualOverflowRectIncludingFilters().
bounds_rect = InkOverflowIncludingFilters();
bounds_rect.Move(physical_offset);
}
if (UNLIKELY(pointer_events_bounding_box)) {
bounds_rect = PhysicalRect::EnclosingRect(
PhysicalFragment().GetLayoutObject()->ObjectBoundingBox());
}
// TODO(kojii): Don't have good explanation why only inline box needs to
// snap, but matches to legacy and fixes crbug.com/976606.
if (fragment.IsInlineBox())
bounds_rect = PhysicalRect(PixelSnappedIntRect(bounds_rect));
if (hit_test.location.Intersects(bounds_rect)) {
// We set offset in container block instead of offset in |fragment| like
// |NGBoxFragmentPainter::HitTestTextFragment()|.
// See http://crbug.com/1043471
if (box_item_ && box_item_->IsInlineBox()) {
DCHECK(inline_box_cursor_);
if (hit_test.AddNodeToResultWithContentOffset(
fragment.NodeForHitTest(),
inline_box_cursor_->ContainerFragment(), bounds_rect,
physical_offset - box_item_->OffsetInContainerFragment()))
return true;
} else {
if (hit_test.AddNodeToResult(fragment.NodeForHitTest(), &box_fragment_,
bounds_rect, physical_offset))
return true;
}
}
}
return false;
}
bool NGBoxFragmentPainter::HitTestAllPhases(
HitTestResult& result,
const HitTestLocation& hit_test_location,
const PhysicalOffset& accumulated_offset,
HitTestFilter hit_test_filter) {
// Logic taken from LayoutObject::HitTestAllPhases().
HitTestContext hit_test(kHitTestForeground, hit_test_location,
accumulated_offset, &result);
bool inside = false;
if (hit_test_filter != kHitTestSelf) {
// First test the foreground layer (lines and inlines).
inside = NodeAtPoint(hit_test, accumulated_offset);
// Test floats next.
if (!inside) {
hit_test.action = kHitTestFloat;
inside = NodeAtPoint(hit_test, accumulated_offset);
}
// Finally test to see if the mouse is in the background (within a child
// block's background).
if (!inside) {
hit_test.action = kHitTestChildBlockBackgrounds;
inside = NodeAtPoint(hit_test, accumulated_offset);
}
}
// See if the pointer is inside us but not any of our descendants.
if (hit_test_filter != kHitTestDescendants && !inside) {
hit_test.action = kHitTestChildBlockBackground;
inside = NodeAtPoint(hit_test, accumulated_offset);
}
return inside;
}
bool NGBoxFragmentPainter::HitTestTextItem(
const HitTestContext& hit_test,
const NGFragmentItem& text_item,
const NGInlineBackwardCursor& cursor) {
DCHECK(text_item.IsText());
if (hit_test.action != kHitTestForeground)
return false;
if (!IsVisibleToHitTest(text_item, hit_test.result->GetHitTestRequest()))
return false;
if (text_item.Type() == NGFragmentItem::kSvgText &&
text_item.HasSvgTransformForBoundingBox()) {
const FloatQuad quad = text_item.SvgUnscaledQuad();
if (!hit_test.location.Intersects(quad))
return false;
return hit_test.AddNodeToResultWithContentOffset(
text_item.NodeForHitTest(), cursor.ContainerFragment(), quad,
hit_test.inline_root_offset);
}
// TODO(layout-dev): Clip to line-top/bottom.
const PhysicalRect rect =
UNLIKELY(hit_test.text_combine)
? hit_test.text_combine->ComputeTextBoundsRectForHitTest(
text_item, hit_test.inline_root_offset)
: text_item.ComputeTextBoundsRectForHitTest(
hit_test.inline_root_offset,
hit_test.result->GetHitTestRequest().IsHitTestVisualOverflow());
if (!hit_test.location.Intersects(rect))
return false;
return hit_test.AddNodeToResultWithContentOffset(
text_item.NodeForHitTest(), cursor.ContainerFragment(), rect,
hit_test.inline_root_offset);
}
// Replicates logic in legacy InlineFlowBox::NodeAtPoint().
bool NGBoxFragmentPainter::HitTestLineBoxFragment(
const HitTestContext& hit_test,
const NGPhysicalLineBoxFragment& fragment,
const NGInlineBackwardCursor& cursor,
const PhysicalOffset& physical_offset) {
PhysicalRect overflow_rect = cursor.Current().InkOverflow();
overflow_rect.Move(physical_offset);
if (!hit_test.location.Intersects(overflow_rect))
return false;
if (HitTestChildren(hit_test, PhysicalFragment(),
cursor.CursorForDescendants(), physical_offset))
return true;
if (hit_test.action != kHitTestForeground)
return false;
if (!IsVisibleToHitTest(box_fragment_, hit_test.result->GetHitTestRequest()))
return false;
const PhysicalOffset overflow_location =
cursor.Current().SelfInkOverflow().offset + physical_offset;
if (HitTestClippedOutByBorder(hit_test.location, overflow_location))
return false;
const PhysicalRect bounds_rect(physical_offset, fragment.Size());
const ComputedStyle& containing_box_style = box_fragment_.Style();
if (containing_box_style.HasBorderRadius() &&
!hit_test.location.Intersects(
RoundedBorderGeometry::PixelSnappedRoundedBorder(containing_box_style,
bounds_rect)))
return false;
if (cursor.ContainerFragment().IsSvgText())
return false;
// Now hit test ourselves.
if (!hit_test.location.Intersects(bounds_rect))
return false;
// Floats will be hit-tested in |kHitTestFloat| phase, but
// |LayoutObject::HitTestAllPhases| does not try it if |kHitTestForeground|
// succeeds. Pretend the location is not in this linebox if it hits floating
// descendants. TODO(kojii): Computing this is redundant, consider
// restructuring. Changing the caller logic isn't easy because currently
// floats are in the bounds of line boxes only in NG.
if (fragment.HasFloatingDescendantsForPaint()) {
DCHECK_NE(hit_test.action, kHitTestFloat);
HitTestContext hit_test_float = hit_test;
hit_test_float.action = kHitTestFloat;
if (HitTestChildren(hit_test_float, PhysicalFragment(),
cursor.CursorForDescendants(), physical_offset))
return false;
}
return hit_test.AddNodeToResultWithContentOffset(
fragment.NodeForHitTest(), box_fragment_, bounds_rect,
physical_offset - cursor.Current().OffsetInContainerFragment());
}
bool NGBoxFragmentPainter::HitTestChildBoxFragment(
const HitTestContext& hit_test,
const NGPhysicalBoxFragment& fragment,
const NGInlineBackwardCursor& backward_cursor,
const PhysicalOffset& physical_offset) {
// Note: Floats should only be hit tested in the |kHitTestFloat| phase, so we
// shouldn't enter a float when |action| doesn't match. However, as floats may
// scatter around in the entire inline formatting context, we should always
// enter non-floating inline child boxes to search for floats in the
// |kHitTestFloat| phase, unless the child box forms another context.
if (fragment.IsFloating() && hit_test.action != kHitTestFloat)
return false;
if (!FragmentRequiresLegacyFallback(fragment)) {
if (fragment.IsPaintedAtomically()) {
return HitTestAllPhasesInFragment(fragment, hit_test.location,
physical_offset, hit_test.result);
}
NGInlineCursor cursor(backward_cursor);
const NGFragmentItem* item = cursor.Current().Item();
DCHECK(item);
DCHECK_EQ(item->BoxFragment(), &fragment);
if (fragment.IsInlineBox()) {
return NGBoxFragmentPainter(cursor, *item, fragment)
.NodeAtPoint(hit_test, physical_offset);
}
// When traversing into a different inline formatting context,
// |inline_root_offset| needs to be updated.
return NGBoxFragmentPainter(cursor, *item, fragment)
.NodeAtPoint(*hit_test.result, hit_test.location, physical_offset,
hit_test.action);
}
if (fragment.IsInline() && hit_test.action != kHitTestForeground)
return false;
if (fragment.IsPaintedAtomically()) {
return HitTestAllPhasesInFragment(fragment, hit_test.location,
physical_offset, hit_test.result);
}
return fragment.GetMutableLayoutObject()->NodeAtPoint(
*hit_test.result, hit_test.location, physical_offset, hit_test.action);
}
bool NGBoxFragmentPainter::HitTestChildBoxItem(
const HitTestContext& hit_test,
const NGPhysicalBoxFragment& container,
const NGFragmentItem& item,
const NGInlineBackwardCursor& cursor) {
DCHECK_EQ(&item, cursor.Current().Item());
if (const NGPhysicalBoxFragment* child_fragment = item.BoxFragment()) {
const PhysicalOffset child_offset =
hit_test.inline_root_offset + item.OffsetInContainerFragment();
return HitTestChildBoxFragment(hit_test, *child_fragment, cursor,
child_offset);
}
DCHECK(item.GetLayoutObject()->IsLayoutInline());
DCHECK(!To<LayoutInline>(item.GetLayoutObject())->ShouldCreateBoxFragment());
if (NGInlineCursor descendants = cursor.CursorForDescendants()) {
if (HitTestItemsChildren(hit_test, container, descendants))
return true;
}
if (cursor.ContainerFragment().IsSvgText() &&
item.Style().PointerEvents() != EPointerEvents::kBoundingBox)
return false;
// Now hit test ourselves.
if (hit_test.action == kHitTestForeground &&
IsVisibleToHitTest(item, hit_test.result->GetHitTestRequest())) {
const PhysicalOffset child_offset =
hit_test.inline_root_offset + item.OffsetInContainerFragment();
PhysicalRect bounds_rect(child_offset, item.Size());
if (UNLIKELY(
hit_test.result->GetHitTestRequest().IsHitTestVisualOverflow())) {
bounds_rect = item.SelfInkOverflow();
bounds_rect.Move(child_offset);
}
// TODO(kojii): Don't have good explanation why only inline box needs to
// snap, but matches to legacy and fixes crbug.com/976606.
bounds_rect = PhysicalRect(PixelSnappedIntRect(bounds_rect));
if (hit_test.location.Intersects(bounds_rect)) {
if (hit_test.AddNodeToResultWithContentOffset(item.NodeForHitTest(),
cursor.ContainerFragment(),
bounds_rect, child_offset))
return true;
}
}
return false;
}
bool NGBoxFragmentPainter::HitTestChildren(
const HitTestContext& hit_test,
const PhysicalOffset& accumulated_offset) {
if (UNLIKELY(inline_box_cursor_)) {
NGInlineCursor descendants = inline_box_cursor_->CursorForDescendants();
if (descendants) {
return HitTestChildren(hit_test, PhysicalFragment(), descendants,
accumulated_offset);
}
return false;
}
if (items_) {
const NGPhysicalBoxFragment& fragment = PhysicalFragment();
NGInlineCursor cursor(fragment, *items_);
return HitTestChildren(hit_test, fragment, cursor, accumulated_offset);
}
// Check descendants of this fragment because floats may be in the
// |NGFragmentItems| of the descendants.
if (hit_test.action == kHitTestFloat &&
box_fragment_.HasFloatingDescendantsForPaint()) {
return HitTestFloatingChildren(hit_test, box_fragment_, accumulated_offset);
}
if (hit_test.action == kHitTestFloat) {
return box_fragment_.HasFloatingDescendantsForPaint() &&
HitTestFloatingChildren(hit_test, box_fragment_, accumulated_offset);
}
return HitTestBlockChildren(*hit_test.result, hit_test.location,
accumulated_offset, hit_test.action);
}
bool NGBoxFragmentPainter::HitTestChildren(
const HitTestContext& hit_test,
const NGPhysicalBoxFragment& container,
const NGInlineCursor& children,
const PhysicalOffset& accumulated_offset) {
if (children.HasRoot())
return HitTestItemsChildren(hit_test, container, children);
// Hits nothing if there were no children.
return false;
}
bool NGBoxFragmentPainter::HitTestBlockChildren(
HitTestResult& result,
const HitTestLocation& hit_test_location,
PhysicalOffset accumulated_offset,
HitTestAction action) {
if (action == kHitTestChildBlockBackgrounds)
action = kHitTestChildBlockBackground;
auto children = box_fragment_.Children();
for (const NGLink& child : base::Reversed(children)) {
const auto& block_child = To<NGPhysicalBoxFragment>(*child);
if (UNLIKELY(block_child.IsLayoutObjectDestroyedOrMoved()))
continue;
if (block_child.HasSelfPaintingLayer() || block_child.IsFloating())
continue;
const PhysicalOffset child_offset = accumulated_offset + child.offset;
bool hit_child = false;
if (block_child.IsPaintedAtomically()) {
if (action == kHitTestForeground) {
hit_child = HitTestAllPhasesInFragment(block_child, hit_test_location,
child_offset, &result);
}
} else {
hit_child = NodeAtPointInFragment(block_child, hit_test_location,
child_offset, action, &result);
}
if (hit_child) {
UpdateHitTestResult(result, block_child,
hit_test_location.Point() - accumulated_offset);
// Our child may have been an anonymous-block, update the hit-test node
// to include our node if needed.
UpdateHitTestResult(result, box_fragment_,
hit_test_location.Point() - accumulated_offset);
return true;
}
}
return false;
}
bool NGBoxFragmentPainter::HitTestItemsChildren(
const HitTestContext& hit_test,
const NGPhysicalBoxFragment& container,
const NGInlineCursor& children) {
DCHECK(children.HasRoot());
for (NGInlineBackwardCursor cursor(children); cursor;) {
const NGFragmentItem* item = cursor.Current().Item();
DCHECK(item);
if (UNLIKELY(item->IsLayoutObjectDestroyedOrMoved())) {
// TODO(crbug.com/1099613): This should not happen, as long as it is
// really layout-clean.
NOTREACHED();
cursor.MoveToPreviousSibling();
continue;
}
if (item->HasSelfPaintingLayer()) {
cursor.MoveToPreviousSibling();
continue;
}
if (item->IsText()) {
if (HitTestTextItem(hit_test, *item, cursor))
return true;
} else if (item->Type() == NGFragmentItem::kLine) {
const NGPhysicalLineBoxFragment* child_fragment = item->LineBoxFragment();
DCHECK(child_fragment);
const PhysicalOffset child_offset =
hit_test.inline_root_offset + item->OffsetInContainerFragment();
if (HitTestLineBoxFragment(hit_test, *child_fragment, cursor,
child_offset))
return true;
} else if (item->Type() == NGFragmentItem::kBox) {
if (HitTestChildBoxItem(hit_test, container, *item, cursor))
return true;
} else {
NOTREACHED();
}
cursor.MoveToPreviousSibling();
if (item->Type() != NGFragmentItem::kLine &&
hit_test.action == kHitTestForeground) {
// Hit test culled inline boxes between |fragment| and its parent
// fragment.
const PhysicalOffset child_offset =
hit_test.inline_root_offset + item->OffsetInContainerFragment();
if (HitTestCulledInlineAncestors(*hit_test.result, container, children,
*item, cursor.Current(),
hit_test.location, child_offset))
return true;
}
}
return false;
}
bool NGBoxFragmentPainter::HitTestFloatingChildren(
const HitTestContext& hit_test,
const NGPhysicalFragment& container,
const PhysicalOffset& accumulated_offset) {
DCHECK_EQ(hit_test.action, kHitTestFloat);
DCHECK(container.HasFloatingDescendantsForPaint());
if (const auto* box = DynamicTo<NGPhysicalBoxFragment>(&container)) {
if (const NGFragmentItems* items = box->Items()) {
NGInlineCursor children(*box, *items);
if (HitTestFloatingChildItems(hit_test, children, accumulated_offset))
return true;
// Even if this turned out to be an inline formatting context, we need to
// continue walking the box fragment children now. If a float is
// block-fragmented, it is resumed as a regular box fragment child, rather
// than becoming a fragment item.
}
}
auto children = container.Children();
for (const NGLink& child : base::Reversed(children)) {
const NGPhysicalFragment& child_fragment = *child.fragment;
if (UNLIKELY(child_fragment.IsLayoutObjectDestroyedOrMoved()))
continue;
if (child_fragment.HasSelfPaintingLayer())
continue;
const PhysicalOffset child_offset = accumulated_offset + child.offset;
if (child_fragment.IsFloating()) {
if (HitTestAllPhasesInFragment(To<NGPhysicalBoxFragment>(child_fragment),
hit_test.location, child_offset,
hit_test.result))
return true;
continue;
}
if (child_fragment.IsPaintedAtomically())
continue;
// If this is a legacy root, fallback to legacy. It does not have
// |HasFloatingDescendantsForPaint()| set, but it may have floating
// descendants.
if (child_fragment.IsLegacyLayoutRoot()) {
if (child_fragment.GetMutableLayoutObject()->NodeAtPoint(
*hit_test.result, hit_test.location, child_offset,
hit_test.action))
return true;
continue;
}
if (!child_fragment.HasFloatingDescendantsForPaint())
continue;
if (child_fragment.HasNonVisibleOverflow()) {
// We need to properly visit this fragment for hit-testing, rather than
// jumping directly to its children (which is what we normally do when
// looking for floats), in order to set up the clip rectangle.
if (child_fragment.CanTraverse()) {
if (NGBoxFragmentPainter(To<NGPhysicalBoxFragment>(child_fragment))
.NodeAtPoint(*hit_test.result, hit_test.location, child_offset,
kHitTestFloat))
return true;
} else if (child_fragment.GetMutableLayoutObject()->NodeAtPoint(
*hit_test.result, hit_test.location, child_offset,
kHitTestFloat)) {
return true;
}
continue;
}
if (HitTestFloatingChildren(hit_test, child_fragment, child_offset))
return true;
}
return false;
}
bool NGBoxFragmentPainter::HitTestFloatingChildItems(
const HitTestContext& hit_test,
const NGInlineCursor& children,
const PhysicalOffset& accumulated_offset) {
for (NGInlineBackwardCursor cursor(children); cursor;
cursor.MoveToPreviousSibling()) {
const NGFragmentItem* item = cursor.Current().Item();
DCHECK(item);
if (UNLIKELY(item->IsLayoutObjectDestroyedOrMoved()))
continue;
if (item->Type() == NGFragmentItem::kBox) {
if (const NGPhysicalBoxFragment* child_box = item->BoxFragment()) {
if (child_box->HasSelfPaintingLayer())
continue;
const PhysicalOffset child_offset =
accumulated_offset + item->OffsetInContainerFragment();
if (child_box->IsFloating()) {
if (HitTestAllPhasesInFragment(*child_box, hit_test.location,
child_offset, hit_test.result))
return true;
continue;
}
// Look into descendants of all inline boxes because inline boxes do not
// have |HasFloatingDescendantsForPaint()| flag.
if (!child_box->IsInlineBox())
continue;
}
DCHECK(item->GetLayoutObject()->IsLayoutInline());
} else if (item->Type() == NGFragmentItem::kLine) {
const NGPhysicalLineBoxFragment* child_line = item->LineBoxFragment();
DCHECK(child_line);
if (!child_line->HasFloatingDescendantsForPaint())
continue;
} else {
continue;
}
NGInlineCursor descendants = cursor.CursorForDescendants();
if (HitTestFloatingChildItems(hit_test, descendants, accumulated_offset))
return true;
}
return false;
}
bool NGBoxFragmentPainter::HitTestClippedOutByBorder(
const HitTestLocation& hit_test_location,
const PhysicalOffset& border_box_location) const {
const ComputedStyle& style = box_fragment_.Style();
PhysicalRect rect(PhysicalOffset(), PhysicalFragment().Size());
rect.Move(border_box_location);
return !hit_test_location.Intersects(
RoundedBorderGeometry::PixelSnappedRoundedBorder(
style, rect, box_fragment_.SidesToInclude()));
}
bool NGBoxFragmentPainter::HitTestOverflowControl(
const HitTestContext& hit_test,
PhysicalOffset accumulated_offset) {
const auto* layout_box =
DynamicTo<LayoutBox>(box_fragment_.GetLayoutObject());
return layout_box &&
layout_box->HitTestOverflowControl(*hit_test.result, hit_test.location,
accumulated_offset);
}
IntRect NGBoxFragmentPainter::VisualRect(const PhysicalOffset& paint_offset) {
if (const auto* layout_box =
DynamicTo<LayoutBox>(box_fragment_.GetLayoutObject()))
return BoxPainter(*layout_box).VisualRect(paint_offset);
DCHECK(box_item_);
PhysicalRect ink_overflow = box_item_->InkOverflow();
ink_overflow.Move(paint_offset);
return EnclosingIntRect(ink_overflow);
}
} // namespace blink