// 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/scoped_svg_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(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(&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 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 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(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(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(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 recorder; absl::optional graphics_context_state_saver; const auto* const text_combine = DynamicTo(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(*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(*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(*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(*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 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_); if (fragment.IsSvgText()) { ScopedSVGPaintState paint_state(*fragment.GetLayoutObject(), paint_info); PaintLineBoxChildren(&children, paint_info.ForDescendants(), paint_offset); return; } 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(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(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(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(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(&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( 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 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(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(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(*box_fragment_.GetLayoutObject()); absl::optional 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(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(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(*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 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
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
, 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 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(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(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 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 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); 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); 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 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); } const auto* const text_combine = DynamicTo(box_fragment_.GetLayoutObject()); // TODO(layout-dev): Clip to line-top/bottom. const PhysicalRect rect = UNLIKELY(text_combine) ? 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(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(*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(&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(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(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(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(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