| // Copyright 2015 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/paint_property_tree_builder.h" |
| |
| #include <memory> |
| |
| #include "cc/input/main_thread_scrolling_reason.h" |
| #include "cc/input/overscroll_behavior.h" |
| #include "third_party/blink/renderer/core/animation/element_animations.h" |
| #include "third_party/blink/renderer/core/dom/dom_node_ids.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_view.h" |
| #include "third_party/blink/renderer/core/frame/settings.h" |
| #include "third_party/blink/renderer/core/frame/visual_viewport.h" |
| #include "third_party/blink/renderer/core/layout/fragmentainer_iterator.h" |
| #include "third_party/blink/renderer/core/layout/geometry/transform_state.h" |
| #include "third_party/blink/renderer/core/layout/layout_image.h" |
| #include "third_party/blink/renderer/core/layout/layout_inline.h" |
| #include "third_party/blink/renderer/core/layout/layout_table_row.h" |
| #include "third_party/blink/renderer/core/layout/layout_table_section.h" |
| #include "third_party/blink/renderer/core/layout/layout_video.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_fragment_child_iterator.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" |
| #include "third_party/blink/renderer/core/layout/svg/layout_svg_resource_masker.h" |
| #include "third_party/blink/renderer/core/layout/svg/layout_svg_root.h" |
| #include "third_party/blink/renderer/core/layout/svg/layout_svg_viewport_container.h" |
| #include "third_party/blink/renderer/core/layout/svg/svg_layout_support.h" |
| #include "third_party/blink/renderer/core/layout/svg/svg_resources.h" |
| #include "third_party/blink/renderer/core/layout/svg/transform_helper.h" |
| #include "third_party/blink/renderer/core/page/link_highlight.h" |
| #include "third_party/blink/renderer/core/page/page.h" |
| #include "third_party/blink/renderer/core/page/scrolling/snap_coordinator.h" |
| #include "third_party/blink/renderer/core/page/scrolling/top_document_root_scroller_controller.h" |
| #include "third_party/blink/renderer/core/paint/clip_path_clipper.h" |
| #include "third_party/blink/renderer/core/paint/compositing/composited_layer_mapping.h" |
| #include "third_party/blink/renderer/core/paint/compositing/compositing_reason_finder.h" |
| #include "third_party/blink/renderer/core/paint/css_mask_painter.h" |
| #include "third_party/blink/renderer/core/paint/find_paint_offset_needing_update.h" |
| #include "third_party/blink/renderer/core/paint/find_properties_needing_update.h" |
| #include "third_party/blink/renderer/core/paint/object_paint_properties.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_property_tree_printer.h" |
| #include "third_party/blink/renderer/core/paint/rounded_border_geometry.h" |
| #include "third_party/blink/renderer/core/paint/svg_root_painter.h" |
| #include "third_party/blink/renderer/platform/graphics/bitmap_image.h" |
| #include "third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.h" |
| #include "third_party/blink/renderer/platform/transforms/transformation_matrix.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| bool AreSubtreeUpdateReasonsIsolationPiercing(unsigned reasons) { |
| // This is written to mean that if we have any reason other than the specified |
| // ones then the reasons are isolation piercing. This means that if new |
| // reasons are added, they will be isolation piercing by default. |
| // - Isolation establishes a containing block for all descendants, so it is |
| // not piercing. |
| // TODO(vmpstr): Investigate if transform style is also isolated. |
| return reasons & |
| ~(static_cast<unsigned>( |
| SubtreePaintPropertyUpdateReason::kContainerChainMayChange)); |
| } |
| |
| } // namespace |
| |
| PaintPropertyTreeBuilderFragmentContext:: |
| PaintPropertyTreeBuilderFragmentContext() |
| : current_effect(&EffectPaintPropertyNode::Root()) { |
| current.clip = absolute_position.clip = fixed_position.clip = |
| &ClipPaintPropertyNode::Root(); |
| current.transform = absolute_position.transform = fixed_position.transform = |
| &TransformPaintPropertyNode::Root(); |
| current.scroll = absolute_position.scroll = fixed_position.scroll = |
| &ScrollPaintPropertyNode::Root(); |
| } |
| |
| PaintPropertyTreeBuilderContext::PaintPropertyTreeBuilderContext() |
| : force_subtree_update_reasons(0u), |
| clip_changed(false), |
| is_repeating_fixed_position(false), |
| has_svg_hidden_container_ancestor(false), |
| supports_composited_raster_invalidation(true), |
| was_layout_shift_root(false), |
| was_main_thread_scrolling(false) {} |
| |
| PaintPropertyChangeType VisualViewportPaintPropertyTreeBuilder::Update( |
| VisualViewport& visual_viewport, |
| PaintPropertyTreeBuilderContext& full_context) { |
| if (full_context.fragments.IsEmpty()) |
| full_context.fragments.push_back(PaintPropertyTreeBuilderFragmentContext()); |
| |
| PaintPropertyTreeBuilderFragmentContext& context = full_context.fragments[0]; |
| |
| auto property_changed = |
| visual_viewport.UpdatePaintPropertyNodesIfNeeded(context); |
| |
| context.current.transform = visual_viewport.GetScrollTranslationNode(); |
| context.absolute_position.transform = |
| visual_viewport.GetScrollTranslationNode(); |
| context.fixed_position.transform = visual_viewport.GetScrollTranslationNode(); |
| |
| context.current.scroll = visual_viewport.GetScrollNode(); |
| context.absolute_position.scroll = visual_viewport.GetScrollNode(); |
| context.fixed_position.scroll = visual_viewport.GetScrollNode(); |
| |
| if (property_changed >= PaintPropertyChangeType::kNodeAddedOrRemoved) { |
| // Force piercing subtree update for the worst case (scroll node added/ |
| // removed). Not a big deal for performance because this is rare. |
| full_context.force_subtree_update_reasons |= |
| PaintPropertyTreeBuilderContext::kSubtreeUpdateIsolationPiercing; |
| } |
| |
| #if DCHECK_IS_ON() |
| paint_property_tree_printer::UpdateDebugNames(visual_viewport); |
| #endif |
| return property_changed; |
| } |
| |
| void PaintPropertyTreeBuilder::SetupContextForFrame( |
| LocalFrameView& frame_view, |
| PaintPropertyTreeBuilderContext& full_context) { |
| if (full_context.fragments.IsEmpty()) |
| full_context.fragments.push_back(PaintPropertyTreeBuilderFragmentContext()); |
| |
| PaintPropertyTreeBuilderFragmentContext& context = full_context.fragments[0]; |
| context.current.paint_offset += PhysicalOffset(frame_view.Location()); |
| context.current.rendering_context_id = 0; |
| context.current.should_flatten_inherited_transform = true; |
| context.absolute_position = context.current; |
| full_context.container_for_absolute_position = nullptr; |
| full_context.container_for_fixed_position = nullptr; |
| context.fixed_position = context.current; |
| context.fixed_position.fixed_position_children_fixed_to_root = true; |
| } |
| |
| namespace { |
| |
| class FragmentPaintPropertyTreeBuilder { |
| STACK_ALLOCATED(); |
| |
| public: |
| FragmentPaintPropertyTreeBuilder( |
| const LayoutObject& object, |
| NGPrePaintInfo* pre_paint_info, |
| PaintPropertyTreeBuilderContext& full_context, |
| PaintPropertyTreeBuilderFragmentContext& context, |
| FragmentData& fragment_data) |
| : object_(object), |
| pre_paint_info_(pre_paint_info), |
| full_context_(full_context), |
| context_(context), |
| fragment_data_(fragment_data), |
| properties_(fragment_data.PaintProperties()) {} |
| |
| ~FragmentPaintPropertyTreeBuilder() { |
| if (property_changed_ >= PaintPropertyChangeType::kNodeAddedOrRemoved) { |
| // Tree topology changes are blocked by isolation. |
| full_context_.force_subtree_update_reasons |= |
| PaintPropertyTreeBuilderContext::kSubtreeUpdateIsolationBlocked; |
| } |
| #if DCHECK_IS_ON() |
| if (properties_) |
| paint_property_tree_printer::UpdateDebugNames(object_, *properties_); |
| #endif |
| } |
| |
| ALWAYS_INLINE void UpdateForSelf(); |
| ALWAYS_INLINE void UpdateForChildren(); |
| |
| PaintPropertyChangeType PropertyChanged() const { return property_changed_; } |
| bool HasIsolationNodes() const { |
| // All or nothing check on the isolation nodes. |
| DCHECK(!properties_ || |
| (properties_->TransformIsolationNode() && |
| properties_->ClipIsolationNode() && |
| properties_->EffectIsolationNode()) || |
| (!properties_->TransformIsolationNode() && |
| !properties_->ClipIsolationNode() && |
| !properties_->EffectIsolationNode())); |
| return properties_ && properties_->TransformIsolationNode(); |
| } |
| |
| private: |
| ALWAYS_INLINE bool CanPropagateSubpixelAccumulation() const; |
| ALWAYS_INLINE void UpdatePaintOffset(); |
| ALWAYS_INLINE void UpdateForPaintOffsetTranslation(base::Optional<IntPoint>&); |
| ALWAYS_INLINE void UpdatePaintOffsetTranslation( |
| const base::Optional<IntPoint>&); |
| ALWAYS_INLINE void SetNeedsPaintPropertyUpdateIfNeeded(); |
| ALWAYS_INLINE void UpdateForObjectLocationAndSize( |
| base::Optional<IntPoint>& paint_offset_translation); |
| ALWAYS_INLINE void UpdateClipPathCache(); |
| ALWAYS_INLINE void UpdateStickyTranslation(); |
| ALWAYS_INLINE void UpdateTransform(); |
| ALWAYS_INLINE void UpdateTransformForSVGChild(CompositingReasons); |
| ALWAYS_INLINE bool EffectCanUseCurrentClipAsOutputClip() const; |
| ALWAYS_INLINE void UpdateEffect(); |
| ALWAYS_INLINE void UpdateFilter(); |
| ALWAYS_INLINE void UpdateFragmentClip(); |
| ALWAYS_INLINE void UpdateCssClip(); |
| ALWAYS_INLINE void UpdateClipPathClip(); |
| ALWAYS_INLINE void UpdateLocalBorderBoxContext(); |
| ALWAYS_INLINE bool NeedsOverflowControlsClip() const; |
| ALWAYS_INLINE void UpdateOverflowControlsClip(); |
| ALWAYS_INLINE void UpdateInnerBorderRadiusClip(); |
| ALWAYS_INLINE void UpdateOverflowClip(); |
| ALWAYS_INLINE void UpdatePerspective(); |
| ALWAYS_INLINE void UpdateReplacedContentTransform(); |
| ALWAYS_INLINE void UpdateScrollAndScrollTranslation(); |
| ALWAYS_INLINE void UpdateOutOfFlowContext(); |
| // See core/paint/README.md for the description of isolation nodes. |
| ALWAYS_INLINE void UpdateTransformIsolationNode(); |
| ALWAYS_INLINE void UpdateEffectIsolationNode(); |
| ALWAYS_INLINE void UpdateClipIsolationNode(); |
| ALWAYS_INLINE void SetTransformNodeStateForSVGChild( |
| TransformPaintPropertyNode::State&); |
| ALWAYS_INLINE void UpdateLayoutShiftRootChanged(bool is_layout_shift_root); |
| |
| bool NeedsPaintPropertyUpdate() const { |
| return object_.NeedsPaintPropertyUpdate() || |
| full_context_.force_subtree_update_reasons; |
| } |
| |
| bool IsInNGFragmentTraversal() const { return pre_paint_info_; } |
| |
| void OnUpdate(PaintPropertyChangeType change) { |
| property_changed_ = std::max(property_changed_, change); |
| } |
| // Like |OnUpdate| but sets |clip_changed| if the clip values change. |
| void OnUpdateClip(PaintPropertyChangeType change) { |
| OnUpdate(change); |
| full_context_.clip_changed |= |
| change >= PaintPropertyChangeType::kChangedOnlySimpleValues; |
| } |
| // Like |OnUpdate| but forces a piercing subtree update if the scroll tree |
| // hierarchy changes because the scroll tree does not have isolation nodes |
| // and non-piercing updates can fail to update scroll descendants. |
| void OnUpdateScroll(PaintPropertyChangeType change) { |
| OnUpdate(change); |
| if (change >= PaintPropertyChangeType::kNodeAddedOrRemoved) { |
| full_context_.force_subtree_update_reasons |= |
| PaintPropertyTreeBuilderContext::kSubtreeUpdateIsolationPiercing; |
| } |
| } |
| void OnClear(bool cleared) { |
| if (cleared) { |
| property_changed_ = PaintPropertyChangeType::kNodeAddedOrRemoved; |
| } |
| } |
| // See: |OnUpdateScroll|. |
| void OnClearScroll(bool cleared) { |
| OnClear(cleared); |
| if (cleared) { |
| full_context_.force_subtree_update_reasons |= |
| PaintPropertyTreeBuilderContext::kSubtreeUpdateIsolationPiercing; |
| } |
| } |
| void OnClearClip(bool cleared) { |
| OnClear(cleared); |
| full_context_.clip_changed |= cleared; |
| } |
| |
| CompositorElementId GetCompositorElementId( |
| CompositorElementIdNamespace namespace_id) const { |
| return CompositorElementIdFromUniqueObjectId(fragment_data_.UniqueId(), |
| namespace_id); |
| } |
| |
| const LayoutObject& object_; |
| NGPrePaintInfo* pre_paint_info_; |
| // The tree builder context for the whole object. |
| PaintPropertyTreeBuilderContext& full_context_; |
| // The tree builder context for the current fragment, which is one of the |
| // entries in |full_context.fragments|. |
| PaintPropertyTreeBuilderFragmentContext& context_; |
| FragmentData& fragment_data_; |
| ObjectPaintProperties* properties_; |
| PaintPropertyChangeType property_changed_ = |
| PaintPropertyChangeType::kUnchanged; |
| }; |
| |
| // True if a scroll translation is needed for static scroll offset (e.g., |
| // overflow hidden with scroll), or if a scroll node is needed for composited |
| // scrolling. |
| static bool NeedsScrollOrScrollTranslation( |
| const LayoutObject& object, |
| CompositingReasons direct_compositing_reasons) { |
| if (!object.IsScrollContainer()) |
| return false; |
| |
| const LayoutBox& box = To<LayoutBox>(object); |
| if (!box.GetScrollableArea()) |
| return false; |
| |
| ScrollOffset scroll_offset = box.GetScrollableArea()->GetScrollOffset(); |
| return !scroll_offset.IsZero() || |
| box.NeedsScrollNode(direct_compositing_reasons); |
| } |
| |
| static bool NeedsReplacedContentTransform(const LayoutObject& object) { |
| // Quick reject. |
| if (!object.IsLayoutReplaced()) |
| return false; |
| |
| if (object.IsSVGRoot()) |
| return true; |
| |
| return false; |
| } |
| |
| static bool NeedsPaintOffsetTranslationForOverflowControls( |
| const LayoutBoxModelObject& object) { |
| if (auto* area = object.GetScrollableArea()) { |
| if (area->HorizontalScrollbar() || area->VerticalScrollbar() || |
| area->Resizer()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| static bool NeedsIsolationNodes(const LayoutObject& object) { |
| if (!object.HasLayer()) |
| return false; |
| |
| // Non-SVG replaced content should not have isolation nodes. Specifically if |
| // these nodes generate a replaced content transform, they don't update the |
| // current transform (See UpdateReplacedContentTransform()). This means that |
| // if we put isolation nodes, which isolate the current transform, then while |
| // getting FragmentData::PostScrollTranslation(), we return a wrong transform |
| // chain (isolation -> "current" transform, instead of replaced transform -> |
| // "current" transform). Note that using ReplacedContentTransform() and |
| // isolating that would violate the condition that the replaced content |
| // transform should not update the current transform (in the non-svg case). |
| if (NeedsReplacedContentTransform(object) && !object.IsSVGRoot()) |
| return false; |
| |
| // Paint containment establishes isolation. |
| // Style & Layout containment also establish isolation. |
| if (object.ShouldApplyPaintContainment() || |
| (object.ShouldApplyStyleContainment() && |
| object.ShouldApplyLayoutContainment())) { |
| return true; |
| } |
| |
| // Layout view establishes isolation with the exception of local roots (since |
| // they are already essentially isolated). |
| if (IsA<LayoutView>(object)) { |
| const auto* parent_frame = object.GetFrame()->Tree().Parent(); |
| return IsA<LocalFrame>(parent_frame); |
| } |
| return false; |
| } |
| |
| static bool NeedsStickyTranslation(const LayoutObject& object) { |
| if (!object.IsBoxModelObject()) |
| return false; |
| |
| return object.StyleRef().HasStickyConstrainedPosition(); |
| } |
| |
| static bool NeedsPaintOffsetTranslation( |
| const LayoutObject& object, |
| CompositingReasons direct_compositing_reasons) { |
| if (!object.IsBoxModelObject()) |
| return false; |
| |
| // <foreignObject> inherits no paint offset, because there is no such |
| // concept within SVG. However, the foreign object can have its own paint |
| // offset due to the x and y parameters of the element. This affects the |
| // offset of painting of the <foreignObject> element and its children. |
| // However, <foreignObject> otherwise behaves like other SVG elements, in |
| // that the x and y offset is applied *after* any transform, instead of |
| // before. Therefore there is no paint offset translation needed. |
| if (object.IsSVGForeignObject()) |
| return false; |
| |
| const auto& box_model = To<LayoutBoxModelObject>(object); |
| |
| if (IsA<LayoutView>(box_model)) { |
| // A translation node for LayoutView is always created to ensure fixed and |
| // absolute contexts use the correct transform space. |
| return true; |
| } |
| |
| if (NeedsIsolationNodes(box_model)) { |
| DCHECK(box_model.HasLayer()); |
| return true; |
| } |
| |
| if (box_model.HasLayer() && box_model.Layer()->PaintsWithTransform( |
| kGlobalPaintFlattenCompositingLayers)) { |
| return true; |
| } |
| if (NeedsScrollOrScrollTranslation(object, direct_compositing_reasons)) |
| return true; |
| if (NeedsStickyTranslation(object)) |
| return true; |
| if (NeedsPaintOffsetTranslationForOverflowControls(box_model)) |
| return true; |
| if (NeedsReplacedContentTransform(object)) |
| return true; |
| |
| // Reference filter and reflection (which creates a reference filter) requires |
| // zero paint offset. |
| if (box_model.HasLayer() && |
| (object.StyleRef().Filter().HasReferenceFilter() || |
| object.HasReflection())) |
| return true; |
| |
| // Don't let paint offset cross composited layer boundaries when possible, to |
| // avoid unnecessary full layer paint/raster invalidation when paint offset in |
| // ancestor transform node changes which should not affect the descendants |
| // of the composited layer. For now because of crbug.com/780242, this is |
| // limited to LayoutBlocks and LayoutReplaceds that won't be escaped by |
| // floating objects and column spans when finding their containing blocks. |
| // TODO(crbug.com/780242): This can be avoided if we have fully correct |
| // paint property tree states for floating objects and column spans. |
| if ((box_model.IsLayoutBlock() || object.IsLayoutReplaced()) && |
| // TODO(wangxianzhu): Don't depend on PaintLayer for CompositeAfterPaint. |
| object.HasLayer()) { |
| PaintLayer* layer = To<LayoutBoxModelObject>(object).Layer(); |
| if (!layer->EnclosingPaginationLayer()) { |
| if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) { |
| if (direct_compositing_reasons != CompositingReason::kNone) |
| return true; |
| // In CompositeAfterPaint though we don't treat hidden backface as |
| // a direct compositing reason, it's very likely that the object will |
| // be composited, so a paint offset translation will be beneficial. |
| if (box_model.StyleRef().BackfaceVisibility() == |
| EBackfaceVisibility::kHidden) |
| return true; |
| } else { |
| if (layer->NeedsPaintOffsetTranslationForCompositing()) |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| bool FragmentPaintPropertyTreeBuilder::CanPropagateSubpixelAccumulation() |
| const { |
| if (!object_.HasLayer()) |
| return true; |
| |
| if (full_context_.direct_compositing_reasons & |
| CompositingReason::kPreventingSubpixelAccumulationReasons) { |
| return false; |
| } |
| if (full_context_.direct_compositing_reasons & |
| CompositingReason::kActiveTransformAnimation) { |
| if (const auto* element = DynamicTo<Element>(object_.GetNode())) { |
| DCHECK(element->GetElementAnimations()); |
| return element->GetElementAnimations()->IsIdentityOrTranslation(); |
| } |
| return false; |
| } |
| |
| const PaintLayer* layer = To<LayoutBoxModelObject>(object_).Layer(); |
| return !layer->Transform() || layer->Transform()->IsIdentityOrTranslation(); |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateForPaintOffsetTranslation( |
| base::Optional<IntPoint>& paint_offset_translation) { |
| if (!NeedsPaintOffsetTranslation( |
| object_, full_context_.direct_compositing_reasons)) |
| return; |
| |
| // We should use the same subpixel paint offset values for snapping regardless |
| // of paint offset translation. If we create a paint offset translation we |
| // round the paint offset but keep around the residual fractional component |
| // (i.e. subpixel accumulation) for the transformed content to paint with. |
| // In pre-CompositeAfterPaint, if the object has layer, this corresponds to |
| // PaintLayer::SubpixelAccumulation(). |
| paint_offset_translation = RoundedIntPoint(context_.current.paint_offset); |
| // Don't propagate subpixel accumulation through paint isolation. In |
| // pre-CompositeAfterPaint we still need to keep consistence with the legacy |
| // compositing code. |
| if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled() && |
| NeedsIsolationNodes(object_)) { |
| context_.current.paint_offset = PhysicalOffset(); |
| context_.current.directly_composited_container_paint_offset_subpixel_delta = |
| PhysicalOffset(); |
| return; |
| } |
| |
| PhysicalOffset subpixel_accumulation = |
| context_.current.paint_offset - PhysicalOffset(*paint_offset_translation); |
| if (!subpixel_accumulation.IsZero() || |
| !context_.current |
| .directly_composited_container_paint_offset_subpixel_delta |
| .IsZero()) { |
| // If the object has a non-translation transform, discard the fractional |
| // paint offset which can't be transformed by the transform. |
| if (!CanPropagateSubpixelAccumulation()) { |
| context_.current.paint_offset = PhysicalOffset(); |
| context_.current |
| .directly_composited_container_paint_offset_subpixel_delta = |
| PhysicalOffset(); |
| return; |
| } |
| } |
| |
| context_.current.paint_offset = subpixel_accumulation; |
| |
| bool can_be_directly_composited = |
| RuntimeEnabledFeatures::CompositeAfterPaintEnabled() |
| ? full_context_.direct_compositing_reasons != CompositingReason::kNone |
| : object_.CanBeCompositedForDirectReasons(); |
| if (!can_be_directly_composited) |
| return; |
| |
| if (paint_offset_translation && properties_ && |
| properties_->PaintOffsetTranslation()) { |
| // The composited subpixel movement optimization applies only if the |
| // composited layer has and had PaintOffsetTranslation, so that both the |
| // the old and new paint offsets are just subpixel accumulations. |
| DCHECK_EQ(IntPoint(), RoundedIntPoint(fragment_data_.PaintOffset())); |
| context_.current.directly_composited_container_paint_offset_subpixel_delta = |
| context_.current.paint_offset - fragment_data_.PaintOffset(); |
| } else { |
| // Otherwise disable the optimization. |
| context_.current.directly_composited_container_paint_offset_subpixel_delta = |
| PhysicalOffset(); |
| } |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdatePaintOffsetTranslation( |
| const base::Optional<IntPoint>& paint_offset_translation) { |
| DCHECK(properties_); |
| |
| if (paint_offset_translation) { |
| FloatSize new_translation(ToIntSize(*paint_offset_translation)); |
| TransformPaintPropertyNode::State state{new_translation}; |
| state.flags.flattens_inherited_transform = |
| context_.current.should_flatten_inherited_transform; |
| state.direct_compositing_reasons = |
| full_context_.direct_compositing_reasons & |
| CompositingReason::kDirectReasonsForPaintOffsetTranslationProperty; |
| state.rendering_context_id = context_.current.rendering_context_id; |
| if (IsA<LayoutView>(object_)) { |
| DCHECK(object_.GetFrame()); |
| state.flags.is_frame_paint_offset_translation = true; |
| state.visible_frame_element_id = |
| object_.GetFrame()->GetVisibleToHitTesting() |
| ? CompositorElementIdFromUniqueObjectId( |
| DOMNodeIds::IdForNode(&object_.GetDocument()), |
| CompositorElementIdNamespace::kDOMNodeId) |
| : cc::ElementId(); |
| } |
| OnUpdate(properties_->UpdatePaintOffsetTranslation( |
| *context_.current.transform, std::move(state))); |
| context_.current.transform = properties_->PaintOffsetTranslation(); |
| if (IsA<LayoutView>(object_)) { |
| context_.absolute_position.transform = |
| properties_->PaintOffsetTranslation(); |
| context_.fixed_position.transform = properties_->PaintOffsetTranslation(); |
| } |
| |
| if (!object_.ShouldAssumePaintOffsetTranslationForLayoutShiftTracking()) { |
| context_.current.additional_offset_to_layout_shift_root_delta += |
| PhysicalOffset(*paint_offset_translation); |
| } |
| } else { |
| OnClear(properties_->ClearPaintOffsetTranslation()); |
| } |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateStickyTranslation() { |
| DCHECK(properties_); |
| |
| if (NeedsPaintPropertyUpdate()) { |
| if (NeedsStickyTranslation(object_)) { |
| const auto& box_model = To<LayoutBoxModelObject>(object_); |
| TransformPaintPropertyNode::State state{ |
| FloatSize(box_model.StickyPositionOffset())}; |
| // TODO(wangxianzhu): Not using GetCompositorElementId() here because |
| // sticky elements don't work properly under multicol for now, to keep |
| // consistency with CompositorElementIdFromUniqueObjectId() below. |
| // This will be fixed by LayoutNG block fragments. |
| state.compositor_element_id = CompositorElementIdFromUniqueObjectId( |
| box_model.UniqueId(), |
| CompositorElementIdNamespace::kStickyTranslation); |
| |
| auto* layer = box_model.Layer(); |
| const auto* scroller_properties = layer->AncestorScrollContainerLayer() |
| ->GetLayoutObject() |
| .FirstFragment() |
| .PaintProperties(); |
| // A scroll node is only created if an object can be scrolled manually, |
| // while sticky position attaches to anything that clips overflow. |
| // No need to (actually can't) setup composited sticky constraint if |
| // the clipping ancestor we attach to doesn't have a scroll node. |
| // TODO(crbug.com/881555): If the clipping ancestor does have a scroll |
| // node, this really should be a DCHECK the current scrolll node |
| // matches it. i.e. |
| // if (scroller_properties && scroller_properties->Scroll()) { |
| // DCHECK_EQ(scroller_properties->Scroll(), context_.current.scroll); |
| // However there is a bug that AncestorScrollContainerLayer() may be |
| // computed incorrectly with clip escaping involved. |
| bool nearest_scroller_is_clip = |
| scroller_properties && |
| scroller_properties->Scroll() == context_.current.scroll; |
| |
| // Additionally, we also want to make sure that the nearest scroller |
| // actually translates this node. If it doesn't (e.g. a position fixed |
| // node in a scrolling document), there's no point in adding a constraint |
| // since scrolling won't affect it. Indeed, if we do add it, the |
| // compositor assumes scrolling does affect it and produces incorrect |
| // results. |
| bool translates_with_nearest_scroller = |
| context_.current.transform->Unalias() |
| .NearestScrollTranslationNode() |
| .ScrollNode() == context_.current.scroll; |
| if (nearest_scroller_is_clip && translates_with_nearest_scroller) { |
| const StickyPositionScrollingConstraints& layout_constraint = |
| layer->AncestorScrollContainerLayer() |
| ->GetScrollableArea() |
| ->GetStickyConstraintsMap() |
| .at(layer); |
| auto constraint = std::make_unique<CompositorStickyConstraint>(); |
| constraint->is_anchored_left = layout_constraint.is_anchored_left; |
| constraint->is_anchored_right = layout_constraint.is_anchored_right; |
| constraint->is_anchored_top = layout_constraint.is_anchored_top; |
| constraint->is_anchored_bottom = layout_constraint.is_anchored_bottom; |
| |
| constraint->left_offset = layout_constraint.left_offset.ToFloat(); |
| constraint->right_offset = layout_constraint.right_offset.ToFloat(); |
| constraint->top_offset = layout_constraint.top_offset.ToFloat(); |
| constraint->bottom_offset = layout_constraint.bottom_offset.ToFloat(); |
| constraint->constraint_box_rect = |
| FloatRect(box_model.ComputeStickyConstrainingRect()); |
| constraint->scroll_container_relative_sticky_box_rect = FloatRect( |
| layout_constraint.scroll_container_relative_sticky_box_rect); |
| constraint->scroll_container_relative_containing_block_rect = FloatRect( |
| layout_constraint.scroll_container_relative_containing_block_rect); |
| if (PaintLayer* sticky_box_shifting_ancestor = |
| layout_constraint.nearest_sticky_layer_shifting_sticky_box) { |
| constraint->nearest_element_shifting_sticky_box = |
| CompositorElementIdFromUniqueObjectId( |
| sticky_box_shifting_ancestor->GetLayoutObject().UniqueId(), |
| CompositorElementIdNamespace::kStickyTranslation); |
| } |
| if (PaintLayer* containing_block_shifting_ancestor = |
| layout_constraint |
| .nearest_sticky_layer_shifting_containing_block) { |
| constraint->nearest_element_shifting_containing_block = |
| CompositorElementIdFromUniqueObjectId( |
| containing_block_shifting_ancestor->GetLayoutObject() |
| .UniqueId(), |
| CompositorElementIdNamespace::kStickyTranslation); |
| } |
| state.sticky_constraint = std::move(constraint); |
| } |
| |
| OnUpdate(properties_->UpdateStickyTranslation(*context_.current.transform, |
| std::move(state))); |
| } else { |
| OnClear(properties_->ClearStickyTranslation()); |
| } |
| } |
| |
| if (properties_->StickyTranslation()) |
| context_.current.transform = properties_->StickyTranslation(); |
| } |
| |
| // TODO(crbug.com/900241): Remove this function and let the caller use |
| // CompositingReason::kDirectReasonForTransformProperty directly. |
| static CompositingReasons CompositingReasonsForTransformProperty() { |
| CompositingReasons reasons = |
| CompositingReason::kDirectReasonsForTransformProperty; |
| // TODO(crbug.com/900241): Check for nodes for each KeyframeModel target |
| // property instead of creating all nodes and only create a transform/ |
| // effect/filter node if needed. |
| reasons |= CompositingReason::kComboActiveAnimation; |
| // We also need to create a transform node if will-change creates other nodes, |
| // to avoid raster invalidation caused by creating/deleting those nodes when |
| // starting/stopping an animation. See: https://crbug.com/942681. |
| reasons |= CompositingReason::kWillChangeOpacity; |
| reasons |= CompositingReason::kWillChangeFilter; |
| reasons |= CompositingReason::kWillChangeBackdropFilter; |
| |
| if (RuntimeEnabledFeatures::TransformInteropEnabled()) |
| reasons |= CompositingReason::kBackfaceInvisibility3DAncestor; |
| |
| return reasons; |
| } |
| |
| static bool NeedsTransformForSVGChild( |
| const LayoutObject& object, |
| CompositingReasons direct_compositing_reasons) { |
| if (!object.IsSVGChild() || object.IsText()) |
| return false; |
| if (direct_compositing_reasons & CompositingReasonsForTransformProperty()) |
| return true; |
| // TODO(pdr): Check for the presence of a transform instead of the value. |
| // Checking for an identity matrix will cause the property tree structure |
| // to change during animations if the animation passes through the |
| // identity matrix. |
| return !object.LocalToSVGParentTransform().IsIdentity(); |
| } |
| |
| static void SetTransformNodeStateFromAffineTransform( |
| TransformPaintPropertyNode::State& state, |
| const AffineTransform& transform) { |
| if (transform.IsIdentityOrTranslation()) |
| state.transform_and_origin = {FloatSize(transform.E(), transform.F())}; |
| else |
| state.transform_and_origin = {TransformationMatrix(transform)}; |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::SetTransformNodeStateForSVGChild( |
| TransformPaintPropertyNode::State& state) { |
| if (full_context_.direct_compositing_reasons & |
| CompositingReason::kActiveTransformAnimation) { |
| if (CompositorAnimations::CanStartTransformAnimationOnCompositorForSVG( |
| *To<SVGElement>(object_.GetNode()))) { |
| // For composited transform animation to work, we need to store transform |
| // origin separately. It's baked in object_.LocalToSVGParentTransform(). |
| state.transform_and_origin = { |
| TransformationMatrix(TransformHelper::ComputeTransform( |
| object_, ComputedStyle::kExcludeTransformOrigin)), |
| FloatPoint3D(TransformHelper::ComputeTransformOrigin(object_))}; |
| // Composited transform animation works only if |
| // LocalToSVGParentTransform() reflects the CSS transform properties. |
| // If this fails, we need to exclude the case in |
| // CompositorAnimations::CanStartTransformAnimationOnCompositorForSVG(). |
| DCHECK_EQ(TransformHelper::ComputeTransform( |
| object_, ComputedStyle::kIncludeTransformOrigin), |
| object_.LocalToSVGParentTransform()); |
| } else { |
| // We composite the object but can't start composited animation. Still |
| // keep the compositing reason because it still improves performance of |
| // main thread animation, but avoid the 2d translation optimization to |
| // meet the requirement of TransformPaintPropertyNode. |
| state.transform_and_origin = { |
| TransformationMatrix(object_.LocalToSVGParentTransform())}; |
| } |
| } else { |
| SetTransformNodeStateFromAffineTransform( |
| state, object_.LocalToSVGParentTransform()); |
| } |
| } |
| |
| // SVG does not use the general transform update of |UpdateTransform|, instead |
| // creating a transform node for SVG-specific transforms without 3D. |
| void FragmentPaintPropertyTreeBuilder::UpdateTransformForSVGChild( |
| CompositingReasons direct_compositing_reasons) { |
| DCHECK(properties_); |
| DCHECK(object_.IsSVGChild()); |
| // SVG does not use paint offset internally, except for SVGForeignObject which |
| // has different SVG and HTML coordinate spaces. |
| DCHECK(object_.IsSVGForeignObject() || |
| context_.current.paint_offset.IsZero()); |
| |
| if (NeedsPaintPropertyUpdate()) { |
| if (NeedsTransformForSVGChild(object_, direct_compositing_reasons)) { |
| // The origin is included in the local transform, so leave origin empty. |
| TransformPaintPropertyNode::State state; |
| SetTransformNodeStateForSVGChild(state); |
| |
| // TODO(pdr): There is additional logic in |
| // FragmentPaintPropertyTreeBuilder::UpdateTransform that likely needs to |
| // be included here, such as setting rendering context ids, etc. |
| if (RuntimeEnabledFeatures::CompositeSVGEnabled()) { |
| state.direct_compositing_reasons = |
| direct_compositing_reasons & |
| CompositingReasonsForTransformProperty(); |
| state.flags.flattens_inherited_transform = |
| context_.current.should_flatten_inherited_transform; |
| state.flags.is_for_svg_child = true; |
| state.compositor_element_id = GetCompositorElementId( |
| CompositorElementIdNamespace::kPrimaryTransform); |
| } |
| |
| TransformPaintPropertyNode::AnimationState animation_state; |
| animation_state.is_running_animation_on_compositor = |
| object_.StyleRef().IsRunningTransformAnimationOnCompositor(); |
| auto effective_change_type = properties_->UpdateTransform( |
| *context_.current.transform, std::move(state), animation_state); |
| if (effective_change_type == |
| PaintPropertyChangeType::kChangedOnlySimpleValues && |
| properties_->Transform()->HasDirectCompositingReasons()) { |
| if (auto* paint_artifact_compositor = |
| object_.GetFrameView()->GetPaintArtifactCompositor()) { |
| bool updated = paint_artifact_compositor->DirectlyUpdateTransform( |
| *properties_->Transform()); |
| if (updated) { |
| effective_change_type = |
| PaintPropertyChangeType::kChangedOnlyCompositedValues; |
| properties_->Transform()->CompositorSimpleValuesUpdated(); |
| } |
| } |
| } |
| OnUpdate(effective_change_type); |
| } else { |
| OnClear(properties_->ClearTransform()); |
| } |
| } |
| |
| if (properties_->Transform()) { |
| context_.current.transform = properties_->Transform(); |
| context_.current.should_flatten_inherited_transform = false; |
| context_.current.rendering_context_id = 0; |
| } |
| } |
| |
| static FloatPoint3D TransformOrigin(const LayoutBox& box) { |
| const ComputedStyle& style = box.StyleRef(); |
| // Transform origin has no effect without a transform or motion path. |
| if (!style.HasTransform()) |
| return FloatPoint3D(); |
| FloatSize border_box_size(box.Size()); |
| return FloatPoint3D( |
| FloatValueForLength(style.TransformOriginX(), border_box_size.Width()), |
| FloatValueForLength(style.TransformOriginY(), border_box_size.Height()), |
| style.TransformOriginZ()); |
| } |
| |
| static bool NeedsTransform(const LayoutObject& object, |
| CompositingReasons direct_compositing_reasons) { |
| if (object.IsText()) |
| return false; |
| |
| if (object.StyleRef().BackfaceVisibility() == EBackfaceVisibility::kHidden) |
| return true; |
| |
| if (direct_compositing_reasons & CompositingReasonsForTransformProperty()) |
| return true; |
| |
| if (!object.IsBox()) |
| return false; |
| |
| if (object.StyleRef().HasTransform() || object.StyleRef().Preserves3D()) |
| return true; |
| |
| return false; |
| } |
| |
| static bool UpdateBoxSizeAndCheckActiveAnimationAxisAlignment( |
| const LayoutBox& object, |
| CompositingReasons compositing_reasons) { |
| if (!(compositing_reasons & CompositingReason::kActiveTransformAnimation)) |
| return false; |
| |
| if (!object.GetNode() || !object.GetNode()->IsElementNode()) |
| return false; |
| const Element* element = To<Element>(object.GetNode()); |
| auto* animations = element->GetElementAnimations(); |
| DCHECK(animations); |
| return animations->UpdateBoxSizeAndCheckTransformAxisAlignment( |
| FloatSize(object.Size())); |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateTransform() { |
| if (object_.IsSVGChild()) { |
| UpdateTransformForSVGChild(full_context_.direct_compositing_reasons); |
| return; |
| } |
| |
| DCHECK(properties_); |
| |
| if (NeedsPaintPropertyUpdate()) { |
| const ComputedStyle& style = object_.StyleRef(); |
| // A transform node is allocated for transforms, preserves-3d and any |
| // direct compositing reason. The latter is required because this is the |
| // only way to represent compositing both an element and its stacking |
| // descendants. |
| if (NeedsTransform(object_, full_context_.direct_compositing_reasons)) { |
| TransformPaintPropertyNode::State state; |
| |
| if (object_.IsBox()) { |
| auto& box = To<LayoutBox>(object_); |
| TransformationMatrix matrix; |
| style.ApplyTransform( |
| matrix, box.Size(), ComputedStyle::kExcludeTransformOrigin, |
| ComputedStyle::kIncludeMotionPath, |
| ComputedStyle::kIncludeIndependentTransformProperties); |
| // If we are running transform animation on compositor, we should |
| // disable 2d translation optimization to ensure that the compositor |
| // gets the correct origin (which might be omitted by the optimization) |
| // to the compositor, in case later animated values will use the origin. |
| // See http://crbug.com/937929 for why we are not using |
| // style.IsRunningTransformAnimationOnCompositor() here. |
| bool disable_2d_translation_optimization = |
| full_context_.direct_compositing_reasons & |
| CompositingReason::kActiveTransformAnimation; |
| if (!disable_2d_translation_optimization && |
| matrix.IsIdentityOr2DTranslation()) { |
| state.transform_and_origin = {matrix.To2DTranslation()}; |
| } else { |
| state.transform_and_origin = {matrix, TransformOrigin(box)}; |
| } |
| |
| // TODO(trchen): transform-style should only be respected if a |
| // PaintLayer is created. If a node with transform-style: preserve-3d |
| // does not exist in an existing rendering context, it establishes a |
| // new one. |
| state.rendering_context_id = context_.current.rendering_context_id; |
| if (style.Preserves3D() && !state.rendering_context_id) { |
| state.rendering_context_id = |
| PtrHash<const LayoutObject>::GetHash(&object_); |
| } |
| |
| state.flags.animation_is_axis_aligned = |
| UpdateBoxSizeAndCheckActiveAnimationAxisAlignment( |
| box, full_context_.direct_compositing_reasons); |
| } |
| |
| state.direct_compositing_reasons = |
| full_context_.direct_compositing_reasons & |
| CompositingReasonsForTransformProperty(); |
| state.flags.flattens_inherited_transform = |
| context_.current.should_flatten_inherited_transform; |
| state.backface_visibility = |
| object_.HasHiddenBackface() |
| ? TransformPaintPropertyNode::BackfaceVisibility::kHidden |
| : TransformPaintPropertyNode::BackfaceVisibility::kVisible; |
| state.compositor_element_id = GetCompositorElementId( |
| CompositorElementIdNamespace::kPrimaryTransform); |
| |
| TransformPaintPropertyNode::AnimationState animation_state; |
| animation_state.is_running_animation_on_compositor = |
| style.IsRunningTransformAnimationOnCompositor(); |
| auto effective_change_type = properties_->UpdateTransform( |
| *context_.current.transform, std::move(state), animation_state); |
| if (effective_change_type == |
| PaintPropertyChangeType::kChangedOnlySimpleValues && |
| properties_->Transform()->HasDirectCompositingReasons()) { |
| if (auto* paint_artifact_compositor = |
| object_.GetFrameView()->GetPaintArtifactCompositor()) { |
| bool updated = paint_artifact_compositor->DirectlyUpdateTransform( |
| *properties_->Transform()); |
| if (updated) { |
| effective_change_type = |
| PaintPropertyChangeType::kChangedOnlyCompositedValues; |
| properties_->Transform()->CompositorSimpleValuesUpdated(); |
| } |
| } |
| } |
| OnUpdate(effective_change_type); |
| } else { |
| OnClear(properties_->ClearTransform()); |
| } |
| } |
| |
| // properties_->Transform() is present if a CSS transform is present, |
| // and is also present if transform-style: preserve-3d is set. |
| // See NeedsTransform. |
| if (const auto* transform = properties_->Transform()) { |
| context_.current.transform = transform; |
| if (object_.StyleRef().Preserves3D()) { |
| context_.current.rendering_context_id = transform->RenderingContextId(); |
| context_.current.should_flatten_inherited_transform = false; |
| } else { |
| context_.current.rendering_context_id = 0; |
| context_.current.should_flatten_inherited_transform = true; |
| } |
| if (transform->IsIdentityOr2DTranslation()) { |
| context_.translation_2d_to_layout_shift_root_delta += |
| transform->Translation2D(); |
| } |
| } else if (RuntimeEnabledFeatures::TransformInteropEnabled() && |
| !object_.IsAnonymous()) { |
| // With kTransformInterop enabled, 3D rendering contexts follow the |
| // DOM ancestor chain, so flattening should apply regardless of |
| // presence of transform. |
| context_.current.rendering_context_id = 0; |
| context_.current.should_flatten_inherited_transform = true; |
| } |
| } |
| |
| static bool MayNeedClipPathClip(const LayoutObject& object) { |
| // We only apply clip-path if the LayoutObject has a layer or is an SVG |
| // child. See NeedsEffect() for additional information on the former. |
| return !object.IsText() && object.StyleRef().HasClipPath() && |
| (object.HasLayer() || object.IsSVGChild()); |
| } |
| |
| static bool NeedsClipPathClip(const LayoutObject& object) { |
| // We should have already updated the clip path cache when this is called. |
| if (object.FirstFragment().ClipPathPath()) { |
| DCHECK(MayNeedClipPathClip(object)); |
| return true; |
| } |
| return false; |
| } |
| |
| // TODO(crbug.com/900241): When this bug is fixed, we should let NeedsEffect() |
| // use CompositingReason::kDirectReasonForEffectProperty directly instead of |
| // calling this function. We should still call this function in UpdateEffect(). |
| static CompositingReasons CompositingReasonsForEffectProperty() { |
| CompositingReasons reasons = |
| CompositingReason::kDirectReasonsForEffectProperty; |
| // TODO(crbug.com/900241): Check for nodes for each KeyframeModel target |
| // property instead of creating all nodes and only create a transform/ |
| // effect/filter node if needed. |
| reasons |= CompositingReason::kComboActiveAnimation; |
| // We also need to create an effect node if will-change creates other nodes, |
| // to avoid raster invalidation caused by creating/deleting those nodes when |
| // starting/stopping an animation. See: https://crbug.com/942681. |
| // In CompositeAfterPaint, this also avoids decomposition of the effect when |
| // the object is forced compositing with will-change:transform. |
| reasons |= CompositingReason::kWillChangeTransform; |
| reasons |= CompositingReason::kWillChangeFilter; |
| return reasons; |
| } |
| |
| static bool NeedsEffect(const LayoutObject& object, |
| CompositingReasons direct_compositing_reasons) { |
| if (object.IsText()) |
| return false; |
| |
| const ComputedStyle& style = object.StyleRef(); |
| |
| // For now some objects (e.g. LayoutTableCol) with stacking context style |
| // don't create layer thus are not actual stacking contexts, so the HasLayer() |
| // condition. TODO(crbug.com/892734): Support effects for LayoutTableCol. |
| const bool is_css_isolated_group = |
| object.HasLayer() && object.IsStackingContext(); |
| |
| if (!is_css_isolated_group && !object.IsSVG()) |
| return false; |
| |
| if (object.IsSVG() && SVGLayoutSupport::IsIsolationRequired(&object)) |
| return true; |
| |
| if (is_css_isolated_group) { |
| const auto* layer = To<LayoutBoxModelObject>(object).Layer(); |
| DCHECK(layer); |
| |
| if (layer->HasNonIsolatedDescendantWithBlendMode()) |
| return true; |
| |
| // An effect node is required by cc if the layer flattens its subtree but it |
| // is treated as a 3D object by its parent. |
| if (!layer->Preserves3D() && layer->HasSelfPaintingLayerDescendant() && |
| layer->Parent() && layer->Parent()->Preserves3D()) |
| return true; |
| } |
| |
| SkBlendMode blend_mode = object.IsBlendingAllowed() |
| ? WebCoreCompositeToSkiaComposite( |
| kCompositeSourceOver, style.GetBlendMode()) |
| : SkBlendMode::kSrcOver; |
| if (blend_mode != SkBlendMode::kSrcOver) |
| return true; |
| |
| if (!style.BackdropFilter().IsEmpty()) |
| return true; |
| |
| if (style.Opacity() != 1.0f) |
| return true; |
| |
| if (direct_compositing_reasons & CompositingReasonsForEffectProperty()) |
| return true; |
| |
| if (object.StyleRef().HasMask()) |
| return true; |
| |
| if (object.StyleRef().HasClipPath() && |
| object.FirstFragment().ClipPathBoundingBox() && |
| !object.FirstFragment().ClipPathPath()) { |
| // If the object has a valid clip-path but can't use path-based clip-path, |
| // a clip-path effect node must be created. |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // An effect node can use the current clip as its output clip if the clip won't |
| // end before the effect ends. Having explicit output clip can let the later |
| // stages use more optimized code path. |
| bool FragmentPaintPropertyTreeBuilder::EffectCanUseCurrentClipAsOutputClip() |
| const { |
| DCHECK(NeedsEffect(object_, full_context_.direct_compositing_reasons)); |
| |
| if (!object_.HasLayer()) { |
| // An SVG object's effect never interleaves with clips. |
| DCHECK(object_.IsSVG()); |
| return true; |
| } |
| |
| const auto* layer = To<LayoutBoxModelObject>(object_).Layer(); |
| // Out-of-flow descendants not contained by this object may escape clips. |
| if (layer->HasNonContainedAbsolutePositionDescendant() && |
| &object_.ContainerForAbsolutePosition() |
| ->FirstFragment() |
| .PostOverflowClip() != context_.current.clip) |
| return false; |
| if (layer->HasFixedPositionDescendant() && |
| !object_.CanContainFixedPositionObjects() && |
| &object_.ContainerForFixedPosition() |
| ->FirstFragment() |
| .PostOverflowClip() != context_.current.clip) |
| return false; |
| |
| // Some descendants under a pagination container (e.g. composited objects |
| // in SPv1 and column spanners) may escape fragment clips. |
| // TODO(crbug.com/803649): Remove this when we fix fragment clip hierarchy |
| // issues. |
| if (layer->EnclosingPaginationLayer()) |
| return false; |
| |
| return true; |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateEffect() { |
| DCHECK(properties_); |
| const ComputedStyle& style = object_.StyleRef(); |
| |
| if (NeedsPaintPropertyUpdate()) { |
| if (NeedsEffect(object_, full_context_.direct_compositing_reasons)) { |
| base::Optional<IntRect> mask_clip = CSSMaskPainter::MaskBoundingBox( |
| object_, context_.current.paint_offset); |
| bool has_clip_path = |
| style.HasClipPath() && fragment_data_.ClipPathBoundingBox(); |
| bool has_mask_based_clip_path = |
| has_clip_path && !fragment_data_.ClipPathPath(); |
| base::Optional<IntRect> clip_path_clip; |
| if (has_mask_based_clip_path) |
| clip_path_clip = fragment_data_.ClipPathBoundingBox(); |
| |
| const auto* output_clip = EffectCanUseCurrentClipAsOutputClip() |
| ? context_.current.clip |
| : nullptr; |
| |
| if (mask_clip || clip_path_clip) { |
| IntRect combined_clip = mask_clip ? *mask_clip : *clip_path_clip; |
| if (mask_clip && clip_path_clip) |
| combined_clip.Intersect(*clip_path_clip); |
| |
| OnUpdateClip(properties_->UpdateMaskClip( |
| *context_.current.clip, |
| ClipPaintPropertyNode::State(context_.current.transform, |
| FloatRoundedRect(combined_clip)))); |
| output_clip = properties_->MaskClip(); |
| } else { |
| OnClearClip(properties_->ClearMaskClip()); |
| } |
| |
| CompositorElementId mask_compositor_element_id; |
| if (mask_clip) { |
| mask_compositor_element_id = |
| GetCompositorElementId(CompositorElementIdNamespace::kEffectMask); |
| } |
| |
| EffectPaintPropertyNode::State state; |
| state.local_transform_space = context_.current.transform; |
| state.output_clip = output_clip; |
| state.opacity = style.Opacity(); |
| if (object_.IsBlendingAllowed()) { |
| state.blend_mode = WebCoreCompositeToSkiaComposite( |
| kCompositeSourceOver, style.GetBlendMode()); |
| } |
| if (object_.IsBoxModelObject()) { |
| if (auto* layer = To<LayoutBoxModelObject>(object_).Layer()) { |
| // Try to use the cached effect for backdrop-filter. |
| if (properties_->Effect()) { |
| state.backdrop_filter = properties_->Effect()->BackdropFilter(); |
| state.backdrop_filter_bounds = |
| properties_->Effect()->BackdropFilterBounds(); |
| } |
| layer->UpdateCompositorFilterOperationsForBackdropFilter( |
| state.backdrop_filter, &state.backdrop_filter_bounds); |
| layer->ClearBackdropFilterOnEffectNodeDirty(); |
| if (!state.backdrop_filter.IsEmpty()) { |
| state.backdrop_mask_element_id = mask_compositor_element_id; |
| } |
| } |
| } |
| |
| state.direct_compositing_reasons = |
| full_context_.direct_compositing_reasons & |
| CompositingReasonsForEffectProperty(); |
| |
| if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) { |
| state.direct_compositing_reasons |= |
| full_context_.direct_compositing_reasons & |
| CompositingReason::kSVGRoot; |
| } |
| |
| // We may begin to composite our subtree prior to an animation starts, but |
| // a compositor element ID is only needed when an animation is current. |
| // Currently, we use the existence of this id to check if effect nodes |
| // have been created for animations on this element. |
| if (state.direct_compositing_reasons) { |
| state.compositor_element_id = GetCompositorElementId( |
| CompositorElementIdNamespace::kPrimaryEffect); |
| } else { |
| // The effect node CompositorElementId is used to uniquely identify |
| // renderpasses so even if we don't need one for animations we still |
| // need to set an id. Using kPrimary avoids confusing cc::Animation |
| // into thinking the element has been composited for animations. |
| state.compositor_element_id = |
| GetCompositorElementId(CompositorElementIdNamespace::kPrimary); |
| } |
| |
| // TODO(crbug.com/900241): Remove these setters when we can use |
| // state.direct_compositing_reasons to check for active animations. |
| state.has_active_opacity_animation = style.HasCurrentOpacityAnimation(); |
| state.has_active_backdrop_filter_animation = |
| style.HasCurrentBackdropFilterAnimation(); |
| |
| EffectPaintPropertyNode::AnimationState animation_state; |
| animation_state.is_running_opacity_animation_on_compositor = |
| style.IsRunningOpacityAnimationOnCompositor(); |
| animation_state.is_running_backdrop_filter_animation_on_compositor = |
| style.IsRunningBackdropFilterAnimationOnCompositor(); |
| auto effective_change_type = properties_->UpdateEffect( |
| *context_.current_effect, std::move(state), animation_state); |
| // If we have simple value change, which means opacity, we should try to |
| // directly update it on the PaintArtifactCompositor in order to avoid |
| // doing a full rebuild. |
| if (effective_change_type == |
| PaintPropertyChangeType::kChangedOnlySimpleValues && |
| properties_->Effect()->HasDirectCompositingReasons()) { |
| if (auto* paint_artifact_compositor = |
| object_.GetFrameView()->GetPaintArtifactCompositor()) { |
| bool updated = |
| paint_artifact_compositor->DirectlyUpdateCompositedOpacityValue( |
| *properties_->Effect()); |
| if (updated) { |
| effective_change_type = |
| PaintPropertyChangeType::kChangedOnlyCompositedValues; |
| properties_->Effect()->CompositorSimpleValuesUpdated(); |
| } |
| } |
| } |
| OnUpdate(effective_change_type); |
| |
| auto mask_direct_compositing_reasons = |
| full_context_.direct_compositing_reasons & |
| CompositingReason::kDirectReasonsForBackdropFilter |
| ? CompositingReason::kBackdropFilterMask |
| : CompositingReason::kNone; |
| |
| if (mask_clip) { |
| EffectPaintPropertyNode::State mask_state; |
| mask_state.local_transform_space = context_.current.transform; |
| mask_state.output_clip = output_clip; |
| mask_state.color_filter = CSSMaskPainter::MaskColorFilter(object_); |
| mask_state.blend_mode = SkBlendMode::kDstIn; |
| mask_state.compositor_element_id = mask_compositor_element_id; |
| mask_state.direct_compositing_reasons = mask_direct_compositing_reasons; |
| OnUpdate(properties_->UpdateMask(*properties_->Effect(), |
| std::move(mask_state))); |
| } else { |
| OnClear(properties_->ClearMask()); |
| } |
| |
| if (has_mask_based_clip_path) { |
| EffectPaintPropertyNode::State clip_path_state; |
| clip_path_state.local_transform_space = context_.current.transform; |
| clip_path_state.output_clip = output_clip; |
| clip_path_state.blend_mode = SkBlendMode::kDstIn; |
| clip_path_state.compositor_element_id = GetCompositorElementId( |
| CompositorElementIdNamespace::kEffectClipPath); |
| if (!mask_clip) { |
| clip_path_state.direct_compositing_reasons = |
| mask_direct_compositing_reasons; |
| } |
| OnUpdate(properties_->UpdateClipPathMask( |
| properties_->Mask() ? *properties_->Mask() : *properties_->Effect(), |
| std::move(clip_path_state))); |
| } else { |
| OnClear(properties_->ClearClipPathMask()); |
| } |
| } else { |
| OnClear(properties_->ClearEffect()); |
| OnClear(properties_->ClearMask()); |
| OnClear(properties_->ClearClipPathMask()); |
| OnClearClip(properties_->ClearMaskClip()); |
| } |
| } |
| |
| if (const auto* effect = properties_->Effect()) { |
| context_.current_effect = effect; |
| context_.this_or_ancestor_opacity_is_zero |= effect->Opacity() == 0; |
| if (properties_->MaskClip()) { |
| context_.current.clip = context_.absolute_position.clip = |
| context_.fixed_position.clip = properties_->MaskClip(); |
| } |
| } |
| } |
| |
| static bool NeedsLinkHighlightEffect(const LayoutObject& object) { |
| auto* page = object.GetFrame()->GetPage(); |
| return page->GetLinkHighlight().NeedsHighlightEffect(object); |
| } |
| |
| // TODO(crbug.com/900241): When this bug is fixed, we should let NeedsFilter() |
| // use CompositingReason::kDirectReasonForFilterProperty directly instead of |
| // calling this function. We should still call this function in UpdateFilter(). |
| static CompositingReasons CompositingReasonsForFilterProperty() { |
| CompositingReasons reasons = |
| CompositingReason::kDirectReasonsForFilterProperty; |
| // TODO(crbug.com/900241): Check for nodes for each KeyframeModel target |
| // property instead of creating all nodes and only create a transform/ |
| // effect/filter node if needed. |
| reasons |= CompositingReason::kComboActiveAnimation; |
| |
| // We also need to create a filter node if will-change creates other nodes, |
| // to avoid raster invalidation caused by creating/deleting those nodes when |
| // starting/stopping an animation. See: https://crbug.com/942681. |
| // In CompositeAfterPaint, this also avoids decomposition of the filter when |
| // the object is forced compositing with will-change. |
| reasons |= CompositingReason::kWillChangeTransform | |
| CompositingReason::kWillChangeOpacity | |
| CompositingReason::kWillChangeBackdropFilter; |
| return reasons; |
| } |
| |
| static bool IsClipPathDescendant(const LayoutObject& object) { |
| // If the object itself is a resource container (root of a resource subtree) |
| // it is not considered a clipPath descendant since it is independent of its |
| // ancestors. |
| if (object.IsSVGResourceContainer()) |
| return false; |
| const LayoutObject* parent = object.Parent(); |
| while (parent) { |
| if (parent->IsSVGResourceContainer()) { |
| auto* container = To<LayoutSVGResourceContainer>(parent); |
| return container->ResourceType() == kClipperResourceType; |
| } |
| parent = parent->Parent(); |
| } |
| return false; |
| } |
| |
| static bool NeedsFilter(const LayoutObject& object, |
| const PaintPropertyTreeBuilderContext& full_context) { |
| if (full_context.direct_compositing_reasons & |
| CompositingReasonsForFilterProperty()) |
| return true; |
| |
| if (object.IsBoxModelObject() && |
| To<LayoutBoxModelObject>(object).HasLayer()) { |
| if (object.StyleRef().HasFilter() || object.HasReflection()) |
| return true; |
| } else if (object.IsSVGChild() && !object.IsText() && |
| SVGResources::GetClient(object)) { |
| if (object.StyleRef().HasFilter()) { |
| // Filters don't apply to elements that are descendants of a <clipPath>. |
| if (!full_context.has_svg_hidden_container_ancestor || |
| !IsClipPathDescendant(object)) |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| static void UpdateFilterEffect(const LayoutObject& object, |
| const EffectPaintPropertyNode* effect_node, |
| CompositorFilterOperations& filter) { |
| if (object.IsBoxModelObject() && |
| To<LayoutBoxModelObject>(object).HasLayer()) { |
| // Try to use the cached filter. |
| if (effect_node) |
| filter = effect_node->Filter(); |
| PaintLayer* layer = To<LayoutBoxModelObject>(object).Layer(); |
| layer->UpdateFilterReferenceBox(); |
| layer->UpdateCompositorFilterOperationsForFilter(filter); |
| layer->ClearFilterOnEffectNodeDirty(); |
| return; |
| } |
| if (object.IsSVGChild() && !object.IsText()) { |
| SVGElementResourceClient* client = SVGResources::GetClient(object); |
| if (!client) |
| return; |
| if (!object.StyleRef().HasFilter()) |
| return; |
| // Try to use the cached filter. |
| if (effect_node) |
| filter = effect_node->Filter(); |
| client->UpdateFilterData(filter); |
| } |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateFilter() { |
| DCHECK(properties_); |
| if (NeedsPaintPropertyUpdate()) { |
| if (NeedsFilter(object_, full_context_)) { |
| EffectPaintPropertyNode::State state; |
| state.local_transform_space = context_.current.transform; |
| |
| UpdateFilterEffect(object_, properties_->Filter(), state.filter); |
| |
| // The CSS filter spec didn't specify how filters interact with overflow |
| // clips. The implementation here mimics the old Blink/WebKit behavior for |
| // backward compatibility. |
| // Basically the output of the filter will be affected by clips that |
| // applies to the current element. The descendants that paints into the |
| // input of the filter ignores any clips collected so far. For example: |
| // <div style="overflow:scroll"> |
| // <div style="filter:blur(1px);"> |
| // <div>A</div> |
| // <div style="position:absolute;">B</div> |
| // </div> |
| // </div> |
| // In this example "A" should be clipped if the filter was not present. |
| // With the filter, "A" will be rastered without clipping, but instead |
| // the blurred result will be clipped. |
| // On the other hand, "B" should not be clipped because the overflow clip |
| // is not in its containing block chain, but as the filter output will be |
| // clipped, so a blurred "B" may still be invisible. |
| if (!state.filter.IsEmpty() || |
| (full_context_.direct_compositing_reasons & |
| CompositingReason::kActiveFilterAnimation)) |
| state.output_clip = context_.current.clip; |
| |
| // TODO(trchen): A filter may contain spatial operations such that an |
| // output pixel may depend on an input pixel outside of the output clip. |
| // We should generate a special clip node to represent this expansion. |
| |
| // We may begin to composite our subtree prior to an animation starts, |
| // but a compositor element ID is only needed when an animation is |
| // current. |
| state.direct_compositing_reasons = |
| full_context_.direct_compositing_reasons & |
| CompositingReasonsForFilterProperty(); |
| state.compositor_element_id = |
| GetCompositorElementId(CompositorElementIdNamespace::kEffectFilter); |
| |
| // TODO(crbug.com/900241): Remove the setter when we can use |
| // state.direct_compositing_reasons to check for active animations. |
| state.has_active_filter_animation = |
| object_.StyleRef().HasCurrentFilterAnimation(); |
| |
| EffectPaintPropertyNode::AnimationState animation_state; |
| animation_state.is_running_filter_animation_on_compositor = |
| object_.StyleRef().IsRunningFilterAnimationOnCompositor(); |
| OnUpdate(properties_->UpdateFilter(*context_.current_effect, |
| std::move(state), animation_state)); |
| } else { |
| OnClear(properties_->ClearFilter()); |
| } |
| } |
| |
| if (properties_->Filter()) { |
| context_.current_effect = properties_->Filter(); |
| // TODO(trchen): Change input clip to expansion hint once implemented. |
| if (const auto* input_clip = properties_->Filter()->OutputClip()) { |
| DCHECK_EQ(input_clip, context_.current.clip); |
| context_.absolute_position.clip = context_.fixed_position.clip = |
| input_clip; |
| } |
| } |
| } |
| |
| static FloatRoundedRect ToSnappedClipRect(const PhysicalRect& rect) { |
| return FloatRoundedRect(PixelSnappedIntRect(rect)); |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateFragmentClip() { |
| DCHECK(properties_); |
| |
| if (NeedsPaintPropertyUpdate()) { |
| if (context_.fragment_clip) { |
| const auto& clip_rect = *context_.fragment_clip; |
| OnUpdateClip(properties_->UpdateFragmentClip( |
| *context_.current.clip, |
| ClipPaintPropertyNode::State(context_.current.transform, |
| FloatRoundedRect(FloatRect(clip_rect)), |
| ToSnappedClipRect(clip_rect)))); |
| } else { |
| OnClearClip(properties_->ClearFragmentClip()); |
| } |
| } |
| |
| if (properties_->FragmentClip()) |
| context_.current.clip = properties_->FragmentClip(); |
| } |
| |
| static bool NeedsCssClip(const LayoutObject& object) { |
| if (object.HasClip()) { |
| DCHECK(!object.IsText()); |
| return true; |
| } |
| return false; |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateCssClip() { |
| DCHECK(properties_); |
| |
| if (NeedsPaintPropertyUpdate()) { |
| if (NeedsCssClip(object_)) { |
| // Create clip node for descendants that are not fixed position. |
| // We don't have to setup context.absolutePosition.clip here because this |
| // object must be a container for absolute position descendants, and will |
| // copy from in-flow context later at updateOutOfFlowContext() step. |
| DCHECK(object_.CanContainAbsolutePositionObjects()); |
| const auto& clip_rect = |
| To<LayoutBox>(object_).ClipRect(context_.current.paint_offset); |
| OnUpdateClip(properties_->UpdateCssClip( |
| *context_.current.clip, |
| ClipPaintPropertyNode::State(context_.current.transform, |
| FloatRoundedRect(FloatRect(clip_rect)), |
| ToSnappedClipRect(clip_rect)))); |
| } else { |
| OnClearClip(properties_->ClearCssClip()); |
| } |
| } |
| |
| if (properties_->CssClip()) |
| context_.current.clip = properties_->CssClip(); |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateClipPathClip() { |
| if (NeedsPaintPropertyUpdate()) { |
| if (!NeedsClipPathClip(object_)) { |
| OnClearClip(properties_->ClearClipPathClip()); |
| } else { |
| ClipPaintPropertyNode::State state( |
| context_.current.transform, |
| FloatRoundedRect(*fragment_data_.ClipPathBoundingBox())); |
| state.clip_path = fragment_data_.ClipPathPath(); |
| OnUpdateClip(properties_->UpdateClipPathClip(*context_.current.clip, |
| std::move(state))); |
| } |
| } |
| |
| if (properties_->ClipPathClip()) { |
| context_.current.clip = context_.absolute_position.clip = |
| context_.fixed_position.clip = properties_->ClipPathClip(); |
| } |
| } |
| |
| // TODO(wangxianzhu): Combine the logic by overriding LayoutBox:: |
| // ComputeOverflowClipAxes() in LayoutReplaced and subclasses and remove |
| // this function. |
| static bool NeedsOverflowClipForReplacedContents( |
| const LayoutReplaced& replaced) { |
| // <svg> may optionally allow overflow. If an overflow clip is required, |
| // always create it without checking whether the actual content overflows. |
| if (replaced.IsSVGRoot()) |
| return To<LayoutSVGRoot>(replaced).ShouldApplyViewportClip(); |
| |
| // A replaced element with border-radius always clips the content. |
| if (replaced.StyleRef().HasBorderRadius()) |
| return true; |
| |
| // ImagePainter (but not painters for LayoutMedia whose IsImage is also true) |
| // won't paint outside of the content box. |
| if (replaced.IsImage() && !replaced.IsMedia()) |
| return false; |
| |
| // Non-plugin embedded contents are always sized to fit the content box. |
| if (replaced.IsLayoutEmbeddedContent() && !replaced.IsEmbeddedObject()) |
| return false; |
| |
| return true; |
| } |
| |
| static bool NeedsOverflowClip(const LayoutObject& object) { |
| if (const auto* replaced = DynamicTo<LayoutReplaced>(object)) |
| return NeedsOverflowClipForReplacedContents(*replaced); |
| |
| if (object.IsSVGViewportContainer() && |
| SVGLayoutSupport::IsOverflowHidden(object)) |
| return true; |
| |
| if (!object.IsBox()) |
| return false; |
| |
| if (!To<LayoutBox>(object).ShouldClipOverflowAlongEitherAxis()) |
| return false; |
| |
| if (IsA<LayoutView>(object) && !object.GetFrame()->ClipsContent()) |
| return false; |
| |
| return true; |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateLocalBorderBoxContext() { |
| if (!NeedsPaintPropertyUpdate()) |
| return; |
| |
| if (object_.HasLayer() || properties_) { |
| DCHECK(context_.current.transform); |
| DCHECK(context_.current.clip); |
| DCHECK(context_.current_effect); |
| PropertyTreeStateOrAlias local_border_box(*context_.current.transform, |
| *context_.current.clip, |
| *context_.current_effect); |
| |
| if (!fragment_data_.HasLocalBorderBoxProperties() || |
| local_border_box != fragment_data_.LocalBorderBoxProperties()) |
| property_changed_ = PaintPropertyChangeType::kNodeAddedOrRemoved; |
| |
| fragment_data_.SetLocalBorderBoxProperties(std::move(local_border_box)); |
| } else { |
| fragment_data_.ClearLocalBorderBoxProperties(); |
| } |
| } |
| |
| bool FragmentPaintPropertyTreeBuilder::NeedsOverflowControlsClip() const { |
| if (!object_.IsScrollContainer()) |
| return false; |
| |
| const auto& box = To<LayoutBox>(object_); |
| const auto* scrollable_area = box.GetScrollableArea(); |
| IntRect scroll_controls_bounds = |
| scrollable_area->ScrollCornerAndResizerRect(); |
| if (const auto* scrollbar = scrollable_area->HorizontalScrollbar()) |
| scroll_controls_bounds.Unite(scrollbar->FrameRect()); |
| if (const auto* scrollbar = scrollable_area->VerticalScrollbar()) |
| scroll_controls_bounds.Unite(scrollbar->FrameRect()); |
| IntRect pixel_snapped_border_box_rect( |
| IntPoint(), box.PixelSnappedBorderBoxSize(context_.current.paint_offset)); |
| return !pixel_snapped_border_box_rect.Contains(scroll_controls_bounds); |
| } |
| |
| static bool NeedsInnerBorderRadiusClip(const LayoutObject& object) { |
| // Replaced elements don't have scrollbars thus needs no separate clip |
| // for the padding box (InnerBorderRadiusClip) and the client box (padding |
| // box minus scrollbar, OverflowClip). |
| // Furthermore, replaced elements clip to the content box instead, |
| if (object.IsLayoutReplaced()) |
| return false; |
| |
| return object.StyleRef().HasBorderRadius() && object.IsBox() && |
| NeedsOverflowClip(object); |
| } |
| |
| static PhysicalOffset VisualOffsetFromPaintOffsetRoot( |
| const PaintPropertyTreeBuilderFragmentContext& context, |
| const PaintLayer* child) { |
| const LayoutObject* paint_offset_root = context.current.paint_offset_root; |
| PaintLayer* painting_layer = paint_offset_root->PaintingLayer(); |
| PhysicalOffset result = child->VisualOffsetFromAncestor(painting_layer); |
| if (!paint_offset_root->HasLayer() || |
| To<LayoutBoxModelObject>(paint_offset_root)->Layer() != painting_layer) { |
| result -= paint_offset_root->OffsetFromAncestor( |
| &painting_layer->GetLayoutObject()); |
| } |
| |
| // Convert the result into the space of the scrolling contents space. |
| if (const auto* properties = |
| paint_offset_root->FirstFragment().PaintProperties()) { |
| if (const auto* scroll_translation = properties->ScrollTranslation()) { |
| result -= PhysicalOffset::FromFloatSizeRound( |
| scroll_translation->Translation2D()); |
| } |
| } |
| return result; |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateOverflowControlsClip() { |
| DCHECK(properties_); |
| |
| if (!NeedsPaintPropertyUpdate()) |
| return; |
| |
| if (NeedsOverflowControlsClip()) { |
| // Clip overflow controls to the border box rect. Not wrapped with |
| // OnUpdateClip() because this clip doesn't affect descendants. Wrap with |
| // OnUpdate() to let PrePaintTreeWalk see the change. This may cause |
| // unnecessary subtree update, but is not a big deal because it is rare. |
| const auto& clip_rect = PhysicalRect(context_.current.paint_offset, |
| To<LayoutBox>(object_).Size()); |
| OnUpdate(properties_->UpdateOverflowControlsClip( |
| *context_.current.clip, |
| ClipPaintPropertyNode::State(context_.current.transform, |
| FloatRoundedRect(FloatRect(clip_rect)), |
| ToSnappedClipRect(clip_rect)))); |
| } else { |
| OnClear(properties_->ClearOverflowControlsClip()); |
| } |
| |
| // We don't walk into custom scrollbars in PrePaintTreeWalk because |
| // LayoutObjects under custom scrollbars don't support paint properties. |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateInnerBorderRadiusClip() { |
| DCHECK(properties_); |
| |
| if (NeedsPaintPropertyUpdate()) { |
| if (NeedsInnerBorderRadiusClip(object_)) { |
| const auto& box = To<LayoutBox>(object_); |
| PhysicalRect box_rect(context_.current.paint_offset, box.Size()); |
| ClipPaintPropertyNode::State state( |
| context_.current.transform, |
| RoundedBorderGeometry::RoundedInnerBorder(box.StyleRef(), box_rect), |
| RoundedBorderGeometry::PixelSnappedRoundedInnerBorder(box.StyleRef(), |
| box_rect)); |
| OnUpdateClip(properties_->UpdateInnerBorderRadiusClip( |
| *context_.current.clip, std::move(state))); |
| } else { |
| OnClearClip(properties_->ClearInnerBorderRadiusClip()); |
| } |
| } |
| |
| if (auto* border_radius_clip = properties_->InnerBorderRadiusClip()) |
| context_.current.clip = border_radius_clip; |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateOverflowClip() { |
| DCHECK(properties_); |
| |
| if (NeedsPaintPropertyUpdate()) { |
| if (NeedsOverflowClip(object_)) { |
| ClipPaintPropertyNode::State state(context_.current.transform, |
| FloatRoundedRect()); |
| |
| if (object_.IsLayoutReplaced()) { |
| const auto& replaced = To<LayoutReplaced>(object_); |
| |
| // Videos need to be pre-snapped so that they line up with the |
| // display_rect and can enable hardware overlays. Adjust the base rect |
| // here, before applying padding and corner rounding. |
| PhysicalRect content_rect(context_.current.paint_offset, |
| replaced.Size()); |
| if (IsA<LayoutVideo>(replaced)) { |
| content_rect = |
| LayoutReplaced::PreSnappedRectForPersistentSizing(content_rect); |
| } |
| // LayoutReplaced clips the foreground by rounded content box. |
| auto clip_rect = RoundedBorderGeometry::PixelSnappedRoundedInnerBorder( |
| replaced.StyleRef(), content_rect, |
| LayoutRectOutsets( |
| -(replaced.PaddingTop() + replaced.BorderTop()), |
| -(replaced.PaddingRight() + replaced.BorderRight()), |
| -(replaced.PaddingBottom() + replaced.BorderBottom()), |
| -(replaced.PaddingLeft() + replaced.BorderLeft()))); |
| state.SetClipRect(clip_rect, clip_rect); |
| if (replaced.IsLayoutEmbeddedContent()) { |
| // Embedded objects are always sized to fit the content rect, but they |
| // could overflow by 1px due to pre-snapping. Adjust clip rect to |
| // match pre-snapped box as a special case. |
| FloatRect adjusted_rect = clip_rect.Rect(); |
| adjusted_rect.SetSize(FloatSize(replaced.ReplacedContentRect().size)); |
| FloatRoundedRect adjusted_clip_rect(adjusted_rect, |
| clip_rect.GetRadii()); |
| state.SetClipRect(adjusted_clip_rect, adjusted_clip_rect); |
| } |
| } else if (object_.IsBox()) { |
| const auto& clip_rect = To<LayoutBox>(object_).OverflowClipRect( |
| context_.current.paint_offset); |
| state.SetClipRect(FloatRoundedRect(FloatRect(clip_rect)), |
| ToSnappedClipRect(clip_rect)); |
| |
| state.clip_rect_excluding_overlay_scrollbars = |
| FloatClipRect(FloatRect(To<LayoutBox>(object_).OverflowClipRect( |
| context_.current.paint_offset, |
| kExcludeOverlayScrollbarSizeForHitTesting))); |
| } else { |
| DCHECK(object_.IsSVGViewportContainer()); |
| const auto& viewport_container = |
| To<LayoutSVGViewportContainer>(object_); |
| const auto clip_rect = FloatRoundedRect( |
| viewport_container.LocalToSVGParentTransform().Inverse().MapRect( |
| viewport_container.Viewport())); |
| state.SetClipRect(clip_rect, clip_rect); |
| } |
| OnUpdateClip(properties_->UpdateOverflowClip(*context_.current.clip, |
| std::move(state))); |
| } else { |
| OnClearClip(properties_->ClearOverflowClip()); |
| } |
| } |
| |
| if (auto* overflow_clip = properties_->OverflowClip()) |
| context_.current.clip = overflow_clip; |
| } |
| |
| static FloatPoint PerspectiveOrigin(const LayoutBox& box) { |
| const ComputedStyle& style = box.StyleRef(); |
| // Perspective origin has no effect without perspective. |
| DCHECK(style.HasPerspective()); |
| FloatSize border_box_size(box.Size()); |
| return FloatPointForLengthPoint(style.PerspectiveOrigin(), border_box_size); |
| } |
| |
| static bool NeedsPerspective(const LayoutObject& object) { |
| return object.IsBox() && object.StyleRef().HasPerspective(); |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdatePerspective() { |
| DCHECK(properties_); |
| |
| if (NeedsPaintPropertyUpdate()) { |
| if (NeedsPerspective(object_)) { |
| const ComputedStyle& style = object_.StyleRef(); |
| // The perspective node must not flatten (else nothing will get |
| // perspective), but it should still extend the rendering context as |
| // most transform nodes do. |
| TransformPaintPropertyNode::State state{ |
| TransformPaintPropertyNode::TransformAndOrigin( |
| TransformationMatrix().ApplyPerspective(style.Perspective()), |
| PerspectiveOrigin(To<LayoutBox>(object_)) + |
| FloatSize(context_.current.paint_offset))}; |
| state.flags.flattens_inherited_transform = |
| context_.current.should_flatten_inherited_transform; |
| state.rendering_context_id = context_.current.rendering_context_id; |
| OnUpdate(properties_->UpdatePerspective(*context_.current.transform, |
| std::move(state))); |
| } else { |
| OnClear(properties_->ClearPerspective()); |
| } |
| } |
| |
| if (properties_->Perspective()) { |
| context_.current.transform = properties_->Perspective(); |
| context_.current.should_flatten_inherited_transform = false; |
| } |
| } |
| |
| static AffineTransform RectToRect(const FloatRect& src_rect, |
| const FloatRect& dst_rect) { |
| float x_scale = dst_rect.Width() / src_rect.Width(); |
| float y_scale = dst_rect.Height() / src_rect.Height(); |
| float x_offset = dst_rect.X() - src_rect.X() * x_scale; |
| float y_offset = dst_rect.Y() - src_rect.Y() * y_scale; |
| return AffineTransform(x_scale, 0.f, 0.f, y_scale, x_offset, y_offset); |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateReplacedContentTransform() { |
| DCHECK(properties_); |
| |
| if (NeedsPaintPropertyUpdate() && !NeedsReplacedContentTransform(object_)) { |
| OnClear(properties_->ClearReplacedContentTransform()); |
| } else if (NeedsPaintPropertyUpdate()) { |
| AffineTransform content_to_parent_space; |
| if (object_.IsSVGRoot()) { |
| content_to_parent_space = |
| SVGRootPainter(To<LayoutSVGRoot>(object_)) |
| .TransformToPixelSnappedBorderBox(context_.current.paint_offset); |
| } else if (const auto* layout_image = DynamicTo<LayoutImage>(object_)) { |
| PhysicalRect layout_replaced_rect = layout_image->ReplacedContentRect(); |
| layout_replaced_rect.Move(context_.current.paint_offset); |
| IntRect replaced_rect = PixelSnappedIntRect(layout_replaced_rect); |
| scoped_refptr<Image> image = |
| layout_image->ImageResource()->GetImage(replaced_rect.Size()); |
| if (image && !image->IsNull()) { |
| IntRect src_rect( |
| IntPoint(), image->Size(LayoutObject::ShouldRespectImageOrientation( |
| layout_image))); |
| content_to_parent_space = |
| RectToRect(FloatRect(src_rect), FloatRect(replaced_rect)); |
| } |
| } else { |
| NOTREACHED(); |
| } |
| if (!content_to_parent_space.IsIdentity()) { |
| TransformPaintPropertyNode::State state; |
| SetTransformNodeStateFromAffineTransform(state, content_to_parent_space); |
| state.flags.flattens_inherited_transform = |
| context_.current.should_flatten_inherited_transform; |
| OnUpdate(properties_->UpdateReplacedContentTransform( |
| *context_.current.transform, std::move(state))); |
| } else { |
| OnClear(properties_->ClearReplacedContentTransform()); |
| } |
| } |
| |
| if (object_.IsSVGRoot()) { |
| // SVG painters don't use paint offset. The paint offset is baked into |
| // the transform node instead. |
| context_.current.paint_offset = PhysicalOffset(); |
| context_.current.directly_composited_container_paint_offset_subpixel_delta = |
| PhysicalOffset(); |
| |
| // Only <svg> paints its subtree as replaced contents. Other replaced |
| // element type may have shadow DOM that should not be affected by the |
| // replaced object fit. |
| if (properties_->ReplacedContentTransform()) { |
| context_.current.transform = properties_->ReplacedContentTransform(); |
| // TODO(pdr): SVG does not support 3D transforms so this should be |
| // should_flatten_inherited_transform = true. |
| context_.current.should_flatten_inherited_transform = false; |
| context_.current.rendering_context_id = 0; |
| } |
| } |
| } |
| |
| static MainThreadScrollingReasons GetMainThreadScrollingReasons( |
| const LayoutObject& object, |
| MainThreadScrollingReasons ancestor_reasons) { |
| auto reasons = ancestor_reasons; |
| if (!object.IsBox()) |
| return reasons; |
| |
| if (IsA<LayoutView>(object)) { |
| if (object.GetFrameView() |
| ->RequiresMainThreadScrollingForBackgroundAttachmentFixed()) { |
| reasons |= |
| cc::MainThreadScrollingReason::kHasBackgroundAttachmentFixedObjects; |
| } |
| |
| // TODO(pdr): This should apply to all scrollable areas, not just the |
| // viewport. This is not a user-visible bug because the threaded scrolling |
| // setting is only for testing. |
| if (!object.GetFrame()->GetSettings()->GetThreadedScrollingEnabled()) |
| reasons |= cc::MainThreadScrollingReason::kThreadedScrollingDisabled; |
| } |
| return reasons; |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateScrollAndScrollTranslation() { |
| DCHECK(properties_); |
| |
| FloatSize old_scroll_offset; |
| if (const auto* old_scroll_translation = properties_->ScrollTranslation()) { |
| DCHECK(full_context_.was_layout_shift_root); |
| old_scroll_offset = old_scroll_translation->Translation2D(); |
| } |
| |
| if (NeedsPaintPropertyUpdate()) { |
| if (object_.IsBox() && To<LayoutBox>(object_).NeedsScrollNode( |
| full_context_.direct_compositing_reasons)) { |
| const auto& box = To<LayoutBox>(object_); |
| PaintLayerScrollableArea* scrollable_area = box.GetScrollableArea(); |
| ScrollPaintPropertyNode::State state; |
| |
| // The container bounds are snapped to integers to match the equivalent |
| // bounds on cc::ScrollNode. The offset is snapped to match the current |
| // integer offsets used in CompositedLayerMapping. |
| state.container_rect = PixelSnappedIntRect( |
| box.OverflowClipRect(context_.current.paint_offset)); |
| state.contents_size = scrollable_area->PixelSnappedContentsSize( |
| context_.current.paint_offset); |
| |
| state.user_scrollable_horizontal = |
| scrollable_area->UserInputScrollable(kHorizontalScrollbar); |
| state.user_scrollable_vertical = |
| scrollable_area->UserInputScrollable(kVerticalScrollbar); |
| |
| // TODO(bokan): We probably don't need to pass ancestor reasons down the |
| // scroll tree. On the compositor, in |
| // LayerTreeHostImpl::FindScrollNodeForDeviceViewportPoint, we walk up |
| // the scroll tree looking at all the ancestor MainThreadScrollingReasons. |
| // https://crbug.com/985127. |
| auto ancestor_reasons = |
| context_.current.scroll->GetMainThreadScrollingReasons(); |
| state.main_thread_scrolling_reasons = |
| GetMainThreadScrollingReasons(object_, ancestor_reasons); |
| |
| // Main thread scrolling reasons depend on their ancestor's reasons |
| // so ensure the entire subtree is updated when reasons change. |
| if (auto* existing_scroll = properties_->Scroll()) { |
| if (existing_scroll->GetMainThreadScrollingReasons() != |
| state.main_thread_scrolling_reasons) { |
| // Main thread scrolling reasons cross into isolation. |
| full_context_.force_subtree_update_reasons |= |
| PaintPropertyTreeBuilderContext::kSubtreeUpdateIsolationPiercing; |
| } |
| } |
| |
| state.compositor_element_id = scrollable_area->GetScrollElementId(); |
| |
| state.overscroll_behavior = |
| cc::OverscrollBehavior(static_cast<cc::OverscrollBehavior::Type>( |
| box.StyleRef().OverscrollBehaviorX()), |
| static_cast<cc::OverscrollBehavior::Type>( |
| box.StyleRef().OverscrollBehaviorY())); |
| |
| state.snap_container_data = |
| box.GetScrollableArea() && |
| box.GetScrollableArea()->GetSnapContainerData() |
| ? base::Optional<cc::SnapContainerData>( |
| *box.GetScrollableArea()->GetSnapContainerData()) |
| : base::nullopt; |
| |
| OnUpdateScroll(properties_->UpdateScroll(*context_.current.scroll, |
| std::move(state))); |
| |
| // Create opacity effect nodes for overlay scrollbars for their fade |
| // animation in the compositor. |
| if (scrollable_area->VerticalScrollbar() && |
| scrollable_area->VerticalScrollbar()->IsOverlayScrollbar()) { |
| EffectPaintPropertyNode::State effect_state; |
| effect_state.local_transform_space = context_.current.transform; |
| effect_state.direct_compositing_reasons = |
| CompositingReason::kActiveOpacityAnimation; |
| effect_state.has_active_opacity_animation = true; |
| effect_state.compositor_element_id = |
| scrollable_area->GetScrollbarElementId( |
| ScrollbarOrientation::kVerticalScrollbar); |
| OnUpdate(properties_->UpdateVerticalScrollbarEffect( |
| *context_.current_effect, std::move(effect_state))); |
| } else { |
| OnClear(properties_->ClearVerticalScrollbarEffect()); |
| } |
| |
| if (scrollable_area->HorizontalScrollbar() && |
| scrollable_area->HorizontalScrollbar()->IsOverlayScrollbar()) { |
| EffectPaintPropertyNode::State effect_state; |
| effect_state.local_transform_space = context_.current.transform; |
| effect_state.direct_compositing_reasons = |
| CompositingReason::kActiveOpacityAnimation; |
| effect_state.has_active_opacity_animation = true; |
| effect_state.compositor_element_id = |
| scrollable_area->GetScrollbarElementId( |
| ScrollbarOrientation::kHorizontalScrollbar); |
| OnUpdate(properties_->UpdateHorizontalScrollbarEffect( |
| *context_.current_effect, std::move(effect_state))); |
| } else { |
| OnClear(properties_->ClearHorizontalScrollbarEffect()); |
| } |
| } else { |
| OnClearScroll(properties_->ClearScroll()); |
| OnClear(properties_->ClearVerticalScrollbarEffect()); |
| OnClear(properties_->ClearHorizontalScrollbarEffect()); |
| } |
| |
| // A scroll translation node is created for static offset (e.g., overflow |
| // hidden with scroll offset) or cases that scroll and have a scroll node. |
| if (NeedsScrollOrScrollTranslation( |
| object_, full_context_.direct_compositing_reasons)) { |
| const auto& box = To<LayoutBox>(object_); |
| DCHECK(box.GetScrollableArea()); |
| |
| // Bake ScrollOrigin into ScrollTranslation. See comments for |
| // ScrollTranslation in object_paint_properties.h for details. |
| FloatPoint scroll_position = FloatPoint(box.ScrollOrigin()) + |
| box.GetScrollableArea()->GetScrollOffset(); |
| TransformPaintPropertyNode::State state{-ToFloatSize(scroll_position)}; |
| state.flags.flattens_inherited_transform = |
| context_.current.should_flatten_inherited_transform; |
| state.direct_compositing_reasons = |
| full_context_.direct_compositing_reasons & |
| CompositingReason::kDirectReasonsForScrollTranslationProperty; |
| state.rendering_context_id = context_.current.rendering_context_id; |
| state.scroll = properties_->Scroll(); |
| // If scroll and transform are both present, we should use the |
| // transform property tree node to determine visibility of the |
| // scrolling contents. |
| if (object_.StyleRef().HasTransform() && |
| object_.StyleRef().BackfaceVisibility() == |
| EBackfaceVisibility::kHidden) |
| state.flags.delegates_to_parent_for_backface = true; |
| auto effective_change_type = properties_->UpdateScrollTranslation( |
| *context_.current.transform, std::move(state)); |
| if (effective_change_type == |
| PaintPropertyChangeType::kChangedOnlySimpleValues && |
| properties_->ScrollTranslation()->HasDirectCompositingReasons()) { |
| if (auto* paint_artifact_compositor = |
| object_.GetFrameView()->GetPaintArtifactCompositor()) { |
| bool updated = |
| paint_artifact_compositor->DirectlyUpdateScrollOffsetTransform( |
| *properties_->ScrollTranslation()); |
| if (updated) { |
| effective_change_type = |
| PaintPropertyChangeType::kChangedOnlyCompositedValues; |
| properties_->ScrollTranslation()->CompositorSimpleValuesUpdated(); |
| } |
| } |
| } |
| OnUpdate(effective_change_type); |
| } else { |
| OnClear(properties_->ClearScrollTranslation()); |
| } |
| } |
| |
| if (properties_->Scroll()) |
| context_.current.scroll = properties_->Scroll(); |
| |
| if (const auto* scroll_translation = properties_->ScrollTranslation()) { |
| context_.current.transform = properties_->ScrollTranslation(); |
| // See comments for ScrollTranslation in object_paint_properties.h for the |
| // reason of adding ScrollOrigin(). |
| context_.current.paint_offset += |
| PhysicalOffset(To<LayoutBox>(object_).ScrollOrigin()); |
| |
| // A scroller creates a layout shift root, so we just calculate one scroll |
| // offset delta without accumulation. |
| context_.current.scroll_offset_to_layout_shift_root_delta = |
| scroll_translation->Translation2D() - old_scroll_offset; |
| } |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateOutOfFlowContext() { |
| if (!object_.IsBoxModelObject() && !properties_) |
| return; |
| |
| if (object_.IsLayoutBlock()) |
| context_.paint_offset_for_float = context_.current.paint_offset; |
| |
| if (object_.CanContainAbsolutePositionObjects()) |
| context_.absolute_position = context_.current; |
| |
| if (IsA<LayoutView>(object_)) { |
| const auto* initial_fixed_transform = context_.fixed_position.transform; |
| |
| context_.fixed_position = context_.current; |
| context_.fixed_position.fixed_position_children_fixed_to_root = true; |
| |
| // Fixed position transform should not be affected. |
| context_.fixed_position.transform = initial_fixed_transform; |
| |
| // Scrolling in a fixed position element should chain up through the |
| // LayoutView. |
| if (properties_->Scroll()) |
| context_.fixed_position.scroll = properties_->Scroll(); |
| if (properties_->ScrollTranslation()) { |
| // Also undo the ScrollOrigin part in paint offset that was added when |
| // ScrollTranslation was updated. |
| context_.fixed_position.paint_offset -= |
| PhysicalOffset(To<LayoutBox>(object_).ScrollOrigin()); |
| } |
| } else if (object_.CanContainFixedPositionObjects()) { |
| context_.fixed_position = context_.current; |
| context_.fixed_position.fixed_position_children_fixed_to_root = false; |
| } else if (properties_ && properties_->CssClip()) { |
| // CSS clip applies to all descendants, even if this object is not a |
| // containing block ancestor of the descendant. It is okay for |
| // absolute-position descendants because having CSS clip implies being |
| // absolute position container. However for fixed-position descendants we |
| // need to insert the clip here if we are not a containing block ancestor of |
| // them. |
| auto* css_clip = properties_->CssClip(); |
| |
| // Before we actually create anything, check whether in-flow context and |
| // fixed-position context has exactly the same clip. Reuse if possible. |
| if (context_.fixed_position.clip == css_clip->Parent()) { |
| context_.fixed_position.clip = css_clip; |
| } else { |
| if (NeedsPaintPropertyUpdate()) { |
| OnUpdate(properties_->UpdateCssClipFixedPosition( |
| *context_.fixed_position.clip, |
| ClipPaintPropertyNode::State(&css_clip->LocalTransformSpace(), |
| css_clip->PixelSnappedClipRect()))); |
| } |
| if (properties_->CssClipFixedPosition()) |
| context_.fixed_position.clip = properties_->CssClipFixedPosition(); |
| return; |
| } |
| } |
| |
| if (NeedsPaintPropertyUpdate() && properties_) |
| OnClear(properties_->ClearCssClipFixedPosition()); |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateTransformIsolationNode() { |
| if (NeedsPaintPropertyUpdate()) { |
| if (NeedsIsolationNodes(object_)) { |
| OnUpdate(properties_->UpdateTransformIsolationNode( |
| *context_.current.transform)); |
| } else { |
| OnClear(properties_->ClearTransformIsolationNode()); |
| } |
| } |
| if (properties_->TransformIsolationNode()) |
| context_.current.transform = properties_->TransformIsolationNode(); |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateEffectIsolationNode() { |
| if (NeedsPaintPropertyUpdate()) { |
| if (NeedsIsolationNodes(object_)) { |
| OnUpdate( |
| properties_->UpdateEffectIsolationNode(*context_.current_effect)); |
| } else { |
| OnClear(properties_->ClearEffectIsolationNode()); |
| } |
| } |
| if (properties_->EffectIsolationNode()) |
| context_.current_effect = properties_->EffectIsolationNode(); |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateClipIsolationNode() { |
| if (NeedsPaintPropertyUpdate()) { |
| if (NeedsIsolationNodes(object_)) { |
| OnUpdate(properties_->UpdateClipIsolationNode(*context_.current.clip)); |
| } else { |
| OnClear(properties_->ClearClipIsolationNode()); |
| } |
| } |
| if (properties_->ClipIsolationNode()) |
| context_.current.clip = properties_->ClipIsolationNode(); |
| } |
| |
| static PhysicalRect MapLocalRectToAncestorLayer( |
| const LayoutBox& box, |
| const PhysicalRect& local_rect, |
| const PaintLayer& ancestor_layer) { |
| return box.LocalToAncestorRect(local_rect, &ancestor_layer.GetLayoutObject(), |
| kIgnoreTransforms); |
| } |
| |
| static bool IsRepeatingTableSection(const LayoutObject& object) { |
| if (!object.IsTableSection()) |
| return false; |
| const auto& section = ToInterface<LayoutNGTableSectionInterface>(object); |
| return section.IsRepeatingHeaderGroup() || section.IsRepeatingFooterGroup(); |
| } |
| |
| static PhysicalRect BoundingBoxInPaginationContainer( |
| const LayoutObject& object, |
| const PaintLayer& enclosing_pagination_layer) { |
| // The special path for fragmented layers ensures that the bounding box also |
| // covers contents visual overflow, so that the fragments will cover all |
| // fragments of contents except for self-painting layers, because we initiate |
| // fragment painting of contents from the layer. |
| if (object.HasLayer() && |
| // Table section may repeat, and doesn't need the special layer path |
| // because it doesn't have contents visual overflow. |
| !object.IsTableSection()) { |
| const auto* layer = To<LayoutBoxModelObject>(object).Layer(); |
| if (layer->ShouldFragmentCompositedBounds()) { |
| ClipRect clip; |
| layer->Clipper(PaintLayer::GeometryMapperOption::kDoNotUseGeometryMapper) |
| .CalculateBackgroundClipRect( |
| ClipRectsContext(&enclosing_pagination_layer, nullptr, |
| kUncachedClipRects), |
| clip); |
| return Intersection( |
| clip.Rect(), layer->PhysicalBoundingBox(&enclosing_pagination_layer)); |
| } |
| } |
| |
| PhysicalRect local_bounds; |
| const LayoutBox* local_space_object = nullptr; |
| if (object.IsBox()) { |
| local_space_object = To<LayoutBox>(&object); |
| local_bounds = local_space_object->PhysicalBorderBoxRect(); |
| } else { |
| // Non-boxes paint in the space of their containing block. |
| local_space_object = object.ContainingBlock(); |
| // For non-SVG we can get a more accurate result with LocalVisualRect, |
| // instead of falling back to the bounds of the enclosing block. |
| if (!object.IsSVG()) { |
| local_bounds = object.LocalVisualRect(); |
| } else { |
| local_bounds = PhysicalRect::EnclosingRect( |
| SVGLayoutSupport::LocalVisualRect(object)); |
| } |
| } |
| |
| // The link highlight covers block visual overflows, continuations, etc. which |
| // may intersect with more fragments than the object itself. |
| if (NeedsLinkHighlightEffect(object)) { |
| local_bounds.Unite(UnionRect(object.OutlineRects( |
| PhysicalOffset(), NGOutlineType::kIncludeBlockVisualOverflow))); |
| } |
| |
| // Compute the bounding box without transforms. |
| auto bounding_box = MapLocalRectToAncestorLayer( |
| *local_space_object, local_bounds, enclosing_pagination_layer); |
| |
| if (!IsRepeatingTableSection(object)) |
| return bounding_box; |
| |
| const auto& section = ToInterface<LayoutNGTableSectionInterface>(object); |
| const auto& table = *section.TableInterface(); |
| |
| if (section.IsRepeatingHeaderGroup()) { |
| // Now bounding_box covers the original header. Expand it to intersect |
| // with all fragments containing the original and repeatings, i.e. to |
| // intersect any fragment containing any row. |
| if (const auto* bottom_section = table.BottomNonEmptySectionInterface()) { |
| const auto* bottom_section_box = |
| To<LayoutBox>(bottom_section->ToLayoutObject()); |
| bounding_box.Unite(MapLocalRectToAncestorLayer( |
| *bottom_section_box, bottom_section_box->PhysicalBorderBoxRect(), |
| enclosing_pagination_layer)); |
| } |
| return bounding_box; |
| } |
| |
| DCHECK(section.IsRepeatingFooterGroup()); |
| // Similar to repeating header, expand bounding_box to intersect any |
| // fragment containing any row first. |
| if (const auto* top_section = table.TopNonEmptySectionInterface()) { |
| const auto* top_section_box = To<LayoutBox>(top_section->ToLayoutObject()); |
| bounding_box.Unite(MapLocalRectToAncestorLayer( |
| *top_section_box, top_section_box->PhysicalBorderBoxRect(), |
| enclosing_pagination_layer)); |
| // However, the first fragment intersecting the expanded bounding_box may |
| // not have enough space to contain the repeating footer. Exclude the |
| // total height of the first row and repeating footers from the top of |
| // bounding_box to exclude the first fragment without enough space. |
| LayoutUnit top_exclusion = table.RowOffsetFromRepeatingFooter(); |
| if (top_section != §ion) { |
| top_exclusion += |
| To<LayoutBox>(top_section->FirstRowInterface()->ToLayoutObject()) |
| ->LogicalHeight() + |
| table.VBorderSpacing(); |
| } |
| // Subtract 1 to ensure overlap of 1 px for a fragment that has exactly |
| // one row plus space for the footer. |
| if (top_exclusion) |
| top_exclusion -= 1; |
| bounding_box.ShiftTopEdgeTo(bounding_box.Y() + top_exclusion); |
| } |
| return bounding_box; |
| } |
| |
| static PhysicalOffset PaintOffsetInPaginationContainer( |
| const LayoutObject& object, |
| const PaintLayer& enclosing_pagination_layer) { |
| // Non-boxes use their containing blocks' paint offset. |
| if (!object.IsBox() && !object.HasLayer()) { |
| return PaintOffsetInPaginationContainer(*object.ContainingBlock(), |
| enclosing_pagination_layer); |
| } |
| return object.LocalToAncestorPoint( |
| PhysicalOffset(), &enclosing_pagination_layer.GetLayoutObject(), |
| kIgnoreTransforms); |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdatePaintOffset() { |
| if (IsInNGFragmentTraversal()) { |
| // Text and non-atomic inlines are special, in that they share one |
| // FragmentData per fragmentainer, so their paint offset is kept at their |
| // container. For all other objects, include the offset now. |
| if (object_.IsBox()) |
| context_.current.paint_offset += pre_paint_info_->iterator->Link().offset; |
| return; |
| } |
| |
| // Paint offsets for fragmented content are computed from scratch. |
| const auto* enclosing_pagination_layer = |
| full_context_.painting_layer->EnclosingPaginationLayer(); |
| if (enclosing_pagination_layer && |
| // Except if the paint_offset_root is below the pagination container, in |
| // which case fragmentation offsets are already baked into the paint |
| // offset transform for paint_offset_root. |
| !context_.current.paint_offset_root->PaintingLayer() |
| ->EnclosingPaginationLayer()) { |
| if (object_.StyleRef().GetPosition() == EPosition::kAbsolute) |
| context_.current = context_.absolute_position; |
| else if (object_.StyleRef().GetPosition() == EPosition::kFixed) |
| context_.current = context_.fixed_position; |
| |
| // Set fragment visual paint offset. |
| PhysicalOffset paint_offset = |
| PaintOffsetInPaginationContainer(object_, *enclosing_pagination_layer); |
| |
| paint_offset += fragment_data_.LegacyPaginationOffset(); |
| paint_offset += context_.repeating_paint_offset_adjustment; |
| paint_offset += |
| VisualOffsetFromPaintOffsetRoot(context_, enclosing_pagination_layer); |
| |
| // The paint offset root can have a subpixel paint offset adjustment. The |
| // paint offset root always has one fragment. |
| const auto& paint_offset_root_fragment = |
| context_.current.paint_offset_root->FirstFragment(); |
| paint_offset += paint_offset_root_fragment.PaintOffset(); |
| |
| context_.current.paint_offset = paint_offset; |
| return; |
| } |
| |
| if (object_.IsFloating() && !object_.IsInLayoutNGInlineFormattingContext()) |
| context_.current.paint_offset = context_.paint_offset_for_float; |
| |
| // Multicolumn spanners are painted starting at the multicolumn container (but |
| // still inherit properties in layout-tree order) so reset the paint offset. |
| if (object_.IsColumnSpanAll()) { |
| context_.current.paint_offset = |
| object_.Container()->FirstFragment().PaintOffset(); |
| } |
| |
| if (object_.IsBoxModelObject()) { |
| const auto& box_model_object = To<LayoutBoxModelObject>(object_); |
| switch (box_model_object.StyleRef().GetPosition()) { |
| case EPosition::kStatic: |
| break; |
| case EPosition::kRelative: |
| context_.current.paint_offset += |
| box_model_object.OffsetForInFlowPosition(); |
| break; |
| case EPosition::kAbsolute: { |
| DCHECK_EQ(full_context_.container_for_absolute_position, |
| box_model_object.Container()); |
| context_.current = context_.absolute_position; |
| |
| // Absolutely positioned content in an inline should be positioned |
| // relative to the inline. |
| const auto* container = full_context_.container_for_absolute_position; |
| if (container && container->IsLayoutInline()) { |
| DCHECK(container->CanContainAbsolutePositionObjects()); |
| DCHECK(box_model_object.IsBox()); |
| context_.current.paint_offset += |
| To<LayoutInline>(container)->OffsetForInFlowPositionedInline( |
| To<LayoutBox>(box_model_object)); |
| } |
| break; |
| } |
| case EPosition::kSticky: |
| break; |
| case EPosition::kFixed: { |
| DCHECK_EQ(full_context_.container_for_fixed_position, |
| box_model_object.Container()); |
| context_.current = context_.fixed_position; |
| // Fixed-position elements that are fixed to the viewport have a |
| // transform above the scroll of the LayoutView. Child content is |
| // relative to that transform, and hence the fixed-position element. |
| if (context_.fixed_position.fixed_position_children_fixed_to_root) |
| context_.current.paint_offset_root = &box_model_object; |
| |
| const auto* container = full_context_.container_for_fixed_position; |
| if (container && container->IsLayoutInline()) { |
| DCHECK(container->CanContainFixedPositionObjects()); |
| DCHECK(box_model_object.IsBox()); |
| context_.current.paint_offset += |
| To<LayoutInline>(container)->OffsetForInFlowPositionedInline( |
| To<LayoutBox>(box_model_object)); |
| } |
| break; |
| } |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| if (object_.IsBox()) { |
| // TODO(pdr): Several calls in this function walk back up the tree to |
| // calculate containers (e.g., physicalLocation, offsetForInFlowPosition*). |
| // The containing block and other containers can be stored on |
| // PaintPropertyTreeBuilderFragmentContext instead of recomputing them. |
| context_.current.paint_offset += To<LayoutBox>(object_).PhysicalLocation(); |
| |
| // This is a weird quirk that table cells paint as children of table rows, |
| // but their location have the row's location baked-in. |
| // Similar adjustment is done in LayoutTableCell::offsetFromContainer(). |
| if (object_.IsTableCellLegacy()) { |
| LayoutObject* parent_row = object_.Parent(); |
| DCHECK(parent_row && parent_row->IsTableRow()); |
| context_.current.paint_offset -= |
| To<LayoutBox>(parent_row)->PhysicalLocation(); |
| } |
| } |
| |
| context_.current.paint_offset += context_.repeating_paint_offset_adjustment; |
| |
| context_.current.additional_offset_to_layout_shift_root_delta += |
| context_.pending_additional_offset_to_layout_shift_root_delta; |
| context_.pending_additional_offset_to_layout_shift_root_delta = |
| PhysicalOffset(); |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::SetNeedsPaintPropertyUpdateIfNeeded() { |
| if (object_.HasLayer()) { |
| PaintLayer* layer = To<LayoutBoxModelObject>(object_).Layer(); |
| layer->UpdateFilterReferenceBox(); |
| } |
| |
| if (!object_.IsBox()) |
| return; |
| |
| const LayoutBox& box = To<LayoutBox>(object_); |
| |
| if (box.IsLayoutReplaced() && |
| box.PreviousPhysicalContentBoxRect() != box.PhysicalContentBoxRect()) |
| box.GetMutableForPainting().SetNeedsPaintPropertyUpdate(); |
| |
| if (box.Size() == box.PreviousSize()) |
| return; |
| |
| // CSS mask and clip-path comes with an implicit clip to the border box. |
| // Currently only CAP generate and take advantage of those. |
| const bool box_generates_property_nodes_for_mask_and_clip_path = |
| box.HasMask() || box.HasClipPath(); |
| // The overflow clip paint property depends on the border box rect through |
| // overflowClipRect(). The border box rect's size equals the frame rect's |
| // size so we trigger a paint property update when the frame rect changes. |
| if (NeedsOverflowClip(box) || NeedsInnerBorderRadiusClip(box) || |
| // The used value of CSS clip may depend on size of the box, e.g. for |
| // clip: rect(auto auto auto -5px). |
| NeedsCssClip(box) || |
| // Relative lengths (e.g., percentage values) in transform, perspective, |
| // transform-origin, and perspective-origin can depend on the size of the |
| // frame rect, so force a property update if it changes. TODO(pdr): We |
| // only need to update properties if there are relative lengths. |
| box.StyleRef().HasTransform() || NeedsPerspective(box) || |
| box_generates_property_nodes_for_mask_and_clip_path) { |
| box.GetMutableForPainting().SetNeedsPaintPropertyUpdate(); |
| } |
| |
| if (MayNeedClipPathClip(box)) |
| box.GetMutableForPainting().InvalidateClipPathCache(); |
| |
| // The filter generated for reflection depends on box size. |
| if (box.HasReflection()) { |
| DCHECK(box.HasLayer()); |
| box.Layer()->SetFilterOnEffectNodeDirty(); |
| box.GetMutableForPainting().SetNeedsPaintPropertyUpdate(); |
| } |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateForObjectLocationAndSize( |
| base::Optional<IntPoint>& paint_offset_translation) { |
| context_.old_paint_offset = fragment_data_.PaintOffset(); |
| UpdatePaintOffset(); |
| UpdateForPaintOffsetTranslation(paint_offset_translation); |
| |
| PhysicalOffset paint_offset_delta = |
| fragment_data_.PaintOffset() - context_.current.paint_offset; |
| if (!paint_offset_delta.IsZero()) { |
| // Many paint properties depend on paint offset so we force an update of |
| // the entire subtree on paint offset changes. However, they are blocked by |
| // isolation if subpixel accumulation doesn't change or CompositeAfterPaint |
| // is enabled. |
| if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled() || |
| !paint_offset_delta.HasFraction()) { |
| full_context_.force_subtree_update_reasons |= |
| PaintPropertyTreeBuilderContext::kSubtreeUpdateIsolationBlocked; |
| } else { |
| full_context_.force_subtree_update_reasons |= |
| PaintPropertyTreeBuilderContext::kSubtreeUpdateIsolationPiercing; |
| } |
| |
| object_.GetMutableForPainting().SetShouldCheckForPaintInvalidation(); |
| fragment_data_.SetPaintOffset(context_.current.paint_offset); |
| fragment_data_.InvalidateClipPathCache(); |
| |
| if (object_.IsBox()) { |
| // See PaintLayerScrollableArea::PixelSnappedBorderBoxSize() for the |
| // reason of this. |
| if (auto* scrollable_area = To<LayoutBox>(object_).GetScrollableArea()) |
| scrollable_area->PositionOverflowControls(); |
| } |
| |
| object_.GetMutableForPainting().InvalidateIntersectionObserverCachedRects(); |
| object_.GetFrameView()->SetIntersectionObservationState( |
| LocalFrameView::kDesired); |
| } |
| |
| if (paint_offset_translation) |
| context_.current.paint_offset_root = &To<LayoutBoxModelObject>(object_); |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateClipPathCache() { |
| if (!MayNeedClipPathClip(object_)) { |
| fragment_data_.ClearClipPathCache(); |
| return; |
| } |
| |
| if (fragment_data_.IsClipPathCacheValid()) |
| return; |
| |
| base::Optional<FloatRect> bounding_box = |
| ClipPathClipper::LocalClipPathBoundingBox(object_); |
| if (!bounding_box) { |
| fragment_data_.ClearClipPathCache(); |
| return; |
| } |
| bounding_box->MoveBy(FloatPoint(fragment_data_.PaintOffset())); |
| |
| base::Optional<Path> path = ClipPathClipper::PathBasedClip(object_); |
| if (path) |
| path->Translate(ToFloatSize(FloatPoint(fragment_data_.PaintOffset()))); |
| fragment_data_.SetClipPathCache( |
| EnclosingIntRect(*bounding_box), |
| path ? AdoptRef(new RefCountedPath(std::move(*path))) : nullptr); |
| } |
| |
| static bool IsLayoutShiftRoot(const LayoutObject& object, |
| const FragmentData& fragment) { |
| const auto* properties = fragment.PaintProperties(); |
| if (!properties) |
| return false; |
| if (IsA<LayoutView>(object)) |
| return true; |
| if (auto* transform = properties->Transform()) { |
| if (!transform->IsIdentityOr2DTranslation()) |
| return true; |
| } |
| if (properties->ReplacedContentTransform()) |
| return true; |
| if (properties->TransformIsolationNode()) |
| return true; |
| if (auto* offset_translation = properties->PaintOffsetTranslation()) { |
| if (offset_translation->RequiresCompositingForScrollDependentPosition()) |
| return true; |
| } |
| if (properties->OverflowClip()) |
| return true; |
| return false; |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateForSelf() { |
| #if DCHECK_IS_ON() |
| FindPaintOffsetNeedingUpdateScope check_paint_offset( |
| object_, fragment_data_, full_context_.is_actually_needed); |
| #endif |
| |
| // This is not in FindObjectPropertiesNeedingUpdateScope because paint offset |
| // can change without NeedsPaintPropertyUpdate. |
| base::Optional<IntPoint> paint_offset_translation; |
| UpdateForObjectLocationAndSize(paint_offset_translation); |
| if (&fragment_data_ == &object_.FirstFragment()) |
| SetNeedsPaintPropertyUpdateIfNeeded(); |
| UpdateClipPathCache(); |
| |
| if (properties_) { |
| { |
| #if DCHECK_IS_ON() |
| FindPropertiesNeedingUpdateScope check_fragment_clip( |
| object_, fragment_data_, full_context_.force_subtree_update_reasons); |
| #endif |
| UpdateFragmentClip(); |
| } |
| // Update of PaintOffsetTranslation is checked by |
| // FindPaintOffsetNeedingUpdateScope. |
| UpdatePaintOffsetTranslation(paint_offset_translation); |
| } |
| |
| #if DCHECK_IS_ON() |
| FindPropertiesNeedingUpdateScope check_paint_properties( |
| object_, fragment_data_, full_context_.force_subtree_update_reasons); |
| #endif |
| |
| if (properties_) { |
| UpdateStickyTranslation(); |
| UpdateTransform(); |
| UpdateClipPathClip(); |
| UpdateEffect(); |
| UpdateCssClip(); |
| UpdateFilter(); |
| UpdateOverflowControlsClip(); |
| } else if (RuntimeEnabledFeatures::TransformInteropEnabled() && |
| !object_.IsAnonymous()) { |
| // With kTransformInterop enabled, 3D rendering contexts follow the |
| // DOM ancestor chain, so flattening should apply regardless of |
| // presence of transform. |
| context_.current.rendering_context_id = 0; |
| context_.current.should_flatten_inherited_transform = true; |
| } |
| UpdateLocalBorderBoxContext(); |
| UpdateLayoutShiftRootChanged(IsLayoutShiftRoot(object_, fragment_data_)); |
| |
| // For LayoutView, additional_offset_to_layout_shift_root_delta applies to |
| // neither itself nor descendants. For other layout shift roots, we clear the |
| // delta at the end of UpdateForChildren() because the delta still applies to |
| // the object itself. Same for translation_2d_to_layout_shift_delta and |
| // scroll_offset_to_layout_shift_root_delta. |
| if (IsA<LayoutView>(object_)) { |
| context_.current.additional_offset_to_layout_shift_root_delta = |
| PhysicalOffset(); |
| context_.translation_2d_to_layout_shift_root_delta = FloatSize(); |
| context_.current.scroll_offset_to_layout_shift_root_delta = FloatSize(); |
| } |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateForChildren() { |
| #if DCHECK_IS_ON() |
| // Paint offset should not change during this function. |
| const bool needs_paint_offset_update = false; |
| FindPaintOffsetNeedingUpdateScope check_paint_offset( |
| object_, fragment_data_, needs_paint_offset_update); |
| |
| FindPropertiesNeedingUpdateScope check_paint_properties( |
| object_, fragment_data_, full_context_.force_subtree_update_reasons); |
| #endif |
| |
| if (properties_) { |
| UpdateInnerBorderRadiusClip(); |
| UpdateOverflowClip(); |
| UpdatePerspective(); |
| UpdateReplacedContentTransform(); |
| UpdateScrollAndScrollTranslation(); |
| UpdateTransformIsolationNode(); |
| UpdateEffectIsolationNode(); |
| UpdateClipIsolationNode(); |
| } |
| UpdateOutOfFlowContext(); |
| |
| bool is_layout_shift_root = IsLayoutShiftRoot(object_, fragment_data_); |
| UpdateLayoutShiftRootChanged(is_layout_shift_root); |
| if (full_context_.was_layout_shift_root || is_layout_shift_root) { |
| // A layout shift root (e.g. with mere OverflowClip) may have non-zero |
| // paint offset. Exclude the layout shift root's paint offset delta from |
| // additional_offset_to_layout_shift_root_delta. |
| context_.current.additional_offset_to_layout_shift_root_delta = |
| context_.old_paint_offset - fragment_data_.PaintOffset(); |
| context_.translation_2d_to_layout_shift_root_delta = FloatSize(); |
| // Don't reset scroll_offset_to_layout_shift_root_delta if this object has |
| // scroll translation because we need to propagate the delta to descendants. |
| if (!properties_ || !properties_->ScrollTranslation()) |
| context_.current.scroll_offset_to_layout_shift_root_delta = FloatSize(); |
| } |
| |
| #if DCHECK_IS_ON() |
| if (properties_) |
| properties_->Validate(); |
| #endif |
| } |
| |
| void FragmentPaintPropertyTreeBuilder::UpdateLayoutShiftRootChanged( |
| bool is_layout_shift_root) { |
| if (is_layout_shift_root != full_context_.was_layout_shift_root) { |
| context_.current.layout_shift_root_changed = true; |
| } else if (is_layout_shift_root && full_context_.was_layout_shift_root) { |
| context_.current.layout_shift_root_changed = false; |
| } |
| } |
| |
| } // namespace |
| |
| void PaintPropertyTreeBuilder::InitFragmentPaintProperties( |
| FragmentData& fragment, |
| bool needs_paint_properties, |
| PaintPropertyTreeBuilderFragmentContext& context) { |
| if (const auto* properties = fragment.PaintProperties()) { |
| if (const auto* translation = properties->PaintOffsetTranslation()) { |
| // If there is a paint offset translation, it only causes a net change |
| // in additional_offset_to_layout_shift_root_delta by the amount the |
| // paint offset translation changed from the prior frame. To implement |
| // this, we record a negative offset here, and then re-add it in |
| // UpdatePaintOffsetTranslation. The net effect is that the value |
| // of additional_offset_to_layout_shift_root_delta is the difference |
| // between the old and new paint offset translation. |
| context.pending_additional_offset_to_layout_shift_root_delta = |
| -PhysicalOffset::FromFloatSizeRound(translation->Translation2D()); |
| } |
| if (const auto* transform = properties->Transform()) { |
| if (transform->IsIdentityOr2DTranslation()) { |
| context.translation_2d_to_layout_shift_root_delta -= |
| transform->Translation2D(); |
| } |
| } |
| } |
| |
| if (needs_paint_properties) { |
| fragment.EnsurePaintProperties(); |
| } else if (fragment.PaintProperties()) { |
| // Tree topology changes are blocked by isolation. |
| context_.force_subtree_update_reasons |= |
| PaintPropertyTreeBuilderContext::kSubtreeUpdateIsolationBlocked; |
| fragment.ClearPaintProperties(); |
| } |
| } |
| |
| void PaintPropertyTreeBuilder::InitFragmentPaintPropertiesForLegacy( |
| FragmentData& fragment, |
| bool needs_paint_properties, |
| const PhysicalOffset& pagination_offset, |
| PaintPropertyTreeBuilderFragmentContext& context) { |
| DCHECK(!IsInNGFragmentTraversal()); |
| InitFragmentPaintProperties(fragment, needs_paint_properties, context); |
| fragment.SetLegacyPaginationOffset(pagination_offset); |
| fragment.SetLogicalTopInFlowThread(context.logical_top_in_flow_thread); |
| } |
| |
| void PaintPropertyTreeBuilder::InitFragmentPaintPropertiesForNG( |
| bool needs_paint_properties) { |
| if (context_.fragments.IsEmpty()) |
| context_.fragments.push_back(PaintPropertyTreeBuilderFragmentContext()); |
| else |
| context_.fragments.resize(1); |
| InitFragmentPaintProperties(pre_paint_info_->fragment_data, |
| needs_paint_properties, context_.fragments[0]); |
| } |
| |
| void PaintPropertyTreeBuilder::InitSingleFragmentFromParent( |
| bool needs_paint_properties) { |
| FragmentData& first_fragment = |
| object_.GetMutableForPainting().FirstFragment(); |
| first_fragment.ClearNextFragment(); |
| if (context_.fragments.IsEmpty()) { |
| context_.fragments.push_back(PaintPropertyTreeBuilderFragmentContext()); |
| } else { |
| context_.fragments.resize(1); |
| context_.fragments[0].fragment_clip.reset(); |
| context_.fragments[0].logical_top_in_flow_thread = LayoutUnit(); |
| } |
| InitFragmentPaintPropertiesForLegacy(first_fragment, needs_paint_properties, |
| PhysicalOffset(), context_.fragments[0]); |
| |
| // Column-span:all skips pagination container in the tree hierarchy, so it |
| // should also skip any fragment clip created by the skipped pagination |
| // container. We also need to skip fragment clip if the object is a paint |
| // invalidation container which doesn't allow fragmentation. |
| bool skip_fragment_clip_for_composited_layer = |
| !RuntimeEnabledFeatures::CompositeAfterPaintEnabled() && |
| object_.CanBeCompositedForDirectReasons() && |
| To<LayoutBoxModelObject>(object_).Layer()->EnclosingPaginationLayer(); |
| if (!skip_fragment_clip_for_composited_layer && !object_.IsColumnSpanAll()) |
| return; |
| |
| const auto* pagination_layer_in_tree_hierarchy = |
| object_.Parent()->EnclosingLayer()->EnclosingPaginationLayer(); |
| if (!pagination_layer_in_tree_hierarchy) |
| return; |
| |
| const auto& clip_container = |
| pagination_layer_in_tree_hierarchy->GetLayoutObject(); |
| const auto* properties = clip_container.FirstFragment().PaintProperties(); |
| if (!properties || !properties->FragmentClip()) |
| return; |
| |
| // Skip fragment clip for composited layer only when there are no other clips. |
| // TODO(crbug.com/803649): This is still incorrect if this object first |
| // appear in the second or later fragment of its parent. |
| if (skip_fragment_clip_for_composited_layer && |
| properties->FragmentClip() != context_.fragments[0].current.clip) |
| return; |
| |
| // However, because we don't allow an object's clip to escape the |
| // output clip of the object's effect, we can't skip fragment clip if |
| // between this object and the container there is any effect that has |
| // an output clip. TODO(crbug.com/803649): Fix this workaround. |
| const auto& clip_container_effect = clip_container.FirstFragment() |
| .LocalBorderBoxProperties() |
| .Effect() |
| .Unalias(); |
| for (const auto* effect = &context_.fragments[0].current_effect->Unalias(); |
| effect && effect != &clip_container_effect; |
| effect = effect->UnaliasedParent()) { |
| if (effect->OutputClip()) |
| return; |
| } |
| |
| // Skip the fragment clip. |
| context_.fragments[0].current.clip = properties->FragmentClip()->Parent(); |
| } |
| |
| void PaintPropertyTreeBuilder::UpdateCompositedLayerPaginationOffset() { |
| DCHECK(!IsInNGFragmentTraversal()); |
| if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) |
| return; |
| |
| const auto* enclosing_pagination_layer = |
| context_.painting_layer->EnclosingPaginationLayer(); |
| if (!enclosing_pagination_layer) |
| return; |
| |
| // We reach here because context_.painting_layer is in a composited layer |
| // under the pagination layer. SPv1* doesn't fragment composited layers, |
| // but we still need to set correct pagination offset for correct paint |
| // offset calculation. |
| DCHECK(!context_.painting_layer->ShouldFragmentCompositedBounds()); |
| FragmentData& first_fragment = |
| object_.GetMutableForPainting().FirstFragment(); |
| bool can_be_directly_composited = object_.CanBeCompositedForDirectReasons(); |
| const auto* parent_directly_composited_layer = |
| context_.painting_layer->EnclosingDirectlyCompositableLayer( |
| can_be_directly_composited ? kExcludeSelf : kIncludeSelf); |
| if (can_be_directly_composited && |
| (!parent_directly_composited_layer || |
| !parent_directly_composited_layer->EnclosingPaginationLayer())) { |
| // |object_| establishes the top level composited layer under the |
| // pagination layer. |
| FragmentainerIterator iterator( |
| To<LayoutFlowThread>(enclosing_pagination_layer->GetLayoutObject()), |
| BoundingBoxInPaginationContainer(object_, *enclosing_pagination_layer) |
| .ToLayoutRect()); |
| if (!iterator.AtEnd()) { |
| first_fragment.SetLegacyPaginationOffset( |
| PhysicalOffsetToBeNoop(iterator.PaginationOffset())); |
| first_fragment.SetLogicalTopInFlowThread( |
| iterator.FragmentainerLogicalTopInFlowThread()); |
| } |
| } else if (parent_directly_composited_layer) { |
| // All objects under the composited layer use the same pagination offset. |
| const auto& fragment = |
| parent_directly_composited_layer->GetLayoutObject().FirstFragment(); |
| first_fragment.SetLegacyPaginationOffset(fragment.LegacyPaginationOffset()); |
| first_fragment.SetLogicalTopInFlowThread(fragment.LogicalTopInFlowThread()); |
| } |
| } |
| |
| void PaintPropertyTreeBuilder:: |
| UpdateRepeatingTableSectionPaintOffsetAdjustment() { |
| if (!context_.repeating_table_section) |
| return; |
| |
| if (object_ == context_.repeating_table_section->ToLayoutObject()) { |
| if (ToInterface<LayoutNGTableSectionInterface>(object_) |
| .IsRepeatingHeaderGroup()) |
| UpdateRepeatingTableHeaderPaintOffsetAdjustment(); |
| else if (ToInterface<LayoutNGTableSectionInterface>(object_) |
| .IsRepeatingFooterGroup()) |
| UpdateRepeatingTableFooterPaintOffsetAdjustment(); |
| } else if (!context_.painting_layer->EnclosingPaginationLayer()) { |
| // When repeating a table section in paged media, paint_offset is inherited |
| // by descendants, so we only need to adjust point offset for the table |
| // section. |
| for (auto& fragment_context : context_.fragments) { |
| fragment_context.repeating_paint_offset_adjustment = PhysicalOffset(); |
| } |
| } |
| |
| // Otherwise the object is a descendant of the object which initiated the |
| // repeating. It just uses repeating_paint_offset_adjustment in its fragment |
| // contexts inherited from the initiating object. |
| } |
| |
| // TODO(wangxianzhu): For now this works for horizontal-bt writing mode only. |
| // Need to support vertical writing modes. |
| void PaintPropertyTreeBuilder:: |
| UpdateRepeatingTableHeaderPaintOffsetAdjustment() { |
| const auto& section = ToInterface<LayoutNGTableSectionInterface>(object_); |
| DCHECK(section.IsRepeatingHeaderGroup()); |
| |
| LayoutUnit fragment_height; |
| LayoutUnit original_offset_in_flow_thread = |
| context_.repeating_table_section_bounding_box.Y(); |
| LayoutUnit original_offset_in_fragment; |
| const LayoutFlowThread* flow_thread = nullptr; |
| if (const auto* pagination_layer = |
| context_.painting_layer->EnclosingPaginationLayer()) { |
| flow_thread = &To<LayoutFlowThread>(pagination_layer->GetLayoutObject()); |
| // TODO(crbug.com/757947): This shouldn't be possible but happens to |
| // column-spanners in nested multi-col contexts. |
| if (!flow_thread->IsPageLogicalHeightKnown()) |
| return; |
| |
| fragment_height = |
| flow_thread->PageLogicalHeightForOffset(original_offset_in_flow_thread); |
| original_offset_in_fragment = |
| fragment_height - flow_thread->PageRemainingLogicalHeightForOffset( |
| original_offset_in_flow_thread, |
| LayoutBox::kAssociateWithLatterPage); |
| } else { |
| // The containing LayoutView serves as the virtual pagination container |
| // for repeating table section in paged media. |
| fragment_height = object_.View()->PageLogicalHeight(); |
| original_offset_in_fragment = |
| IntMod(original_offset_in_flow_thread, fragment_height); |
| } |
| |
| const LayoutNGTableInterface& table = *section.TableInterface(); |
| |
| // This is total height of repeating headers seen by the table - height of |
| // this header (which is the lowest repeating header seen by this table. |
| auto repeating_offset_in_fragment = |
| table.RowOffsetFromRepeatingHeader() - |
| To<LayoutBox>(section.ToLayoutObject())->LogicalHeight(); |
| |
| // For a repeating table header, the original location (which may be in the |
| // middle of the fragment) and repeated locations (which should be always, |
| // together with repeating headers of outer tables, aligned to the top of |
| // the fragments) may be different. Therefore, for fragments other than the |
| // first, adjust by |alignment_offset|. |
| auto adjustment = repeating_offset_in_fragment - original_offset_in_fragment; |
| |
| auto fragment_offset_in_flow_thread = |
| original_offset_in_flow_thread - original_offset_in_fragment; |
| |
| // It's the table sections that make room for repeating headers. Stop |
| // repeating when we're past the last section. There may be trailing |
| // border-spacing, and also bottom captions. No room has been made for a |
| // repeated header there. |
| auto sections_logical_height = |
| To<LayoutBox>(table.BottomSectionInterface()->ToLayoutObject()) |
| ->LogicalBottom() - |
| To<LayoutBox>(table.TopSectionInterface()->ToLayoutObject()) |
| ->LogicalTop(); |
| auto content_remaining = sections_logical_height - table.VBorderSpacing(); |
| |
| for (wtf_size_t i = 0; i < context_.fragments.size(); ++i) { |
| auto& fragment_context = context_.fragments[i]; |
| fragment_context.repeating_paint_offset_adjustment = PhysicalOffset(); |
| // Adjust paint offsets of repeatings (not including the original). |
| if (i) |
| fragment_context.repeating_paint_offset_adjustment.top = adjustment; |
| |
| // Calculate the adjustment for the repeating which will appear in the next |
| // fragment. |
| adjustment += fragment_height; |
| |
| if (adjustment >= content_remaining) |
| break; |
| |
| // Calculate the offset of the next fragment in flow thread. It's used to |
| // get the height of that fragment. |
| fragment_offset_in_flow_thread += fragment_height; |
| |
| if (flow_thread) { |
| fragment_height = flow_thread->PageLogicalHeightForOffset( |
| fragment_offset_in_flow_thread); |
| } |
| } |
| } |
| |
| void PaintPropertyTreeBuilder:: |
| UpdateRepeatingTableFooterPaintOffsetAdjustment() { |
| const auto& section = ToInterface<LayoutNGTableSectionInterface>(object_); |
| DCHECK(section.IsRepeatingFooterGroup()); |
| |
| LayoutUnit fragment_height; |
| LayoutUnit original_offset_in_flow_thread = |
| context_.repeating_table_section_bounding_box.Bottom() - |
| To<LayoutBox>(section.ToLayoutObject())->LogicalHeight(); |
| LayoutUnit original_offset_in_fragment; |
| const LayoutFlowThread* flow_thread = nullptr; |
| if (const auto* pagination_layer = |
| context_.painting_layer->EnclosingPaginationLayer()) { |
| flow_thread = &To<LayoutFlowThread>(pagination_layer->GetLayoutObject()); |
| // TODO(crbug.com/757947): This shouldn't be possible but happens to |
| // column-spanners in nested multi-col contexts. |
| if (!flow_thread->IsPageLogicalHeightKnown()) |
| return; |
| |
| fragment_height = |
| flow_thread->PageLogicalHeightForOffset(original_offset_in_flow_thread); |
| original_offset_in_fragment = |
| fragment_height - flow_thread->PageRemainingLogicalHeightForOffset( |
| original_offset_in_flow_thread, |
| LayoutBox::kAssociateWithLatterPage); |
| } else { |
| // The containing LayoutView serves as the virtual pagination container |
| // for repeating table section in paged media. |
| fragment_height = object_.View()->PageLogicalHeight(); |
| original_offset_in_fragment = |
| IntMod(original_offset_in_flow_thread, fragment_height); |
| } |
| |
| const auto& table = *section.TableInterface(); |
| // TODO(crbug.com/798153): This keeps the existing behavior of repeating |
| // footer painting in TableSectionPainter. Should change both places when |
| // tweaking border-spacing for repeating footers. |
| auto repeating_offset_in_fragment = fragment_height - |
| table.RowOffsetFromRepeatingFooter() - |
| table.VBorderSpacing(); |
| // We should show the whole bottom border instead of half if the table |
| // collapses borders. |
| if (table.ShouldCollapseBorders()) { |
| repeating_offset_in_fragment -= |
| To<LayoutBox>(table.ToLayoutObject())->BorderBottom(); |
| } |
| |
| // Similar to repeating header, this is to adjust the repeating footer from |
| // its original location to the repeating location. |
| auto adjustment = repeating_offset_in_fragment - original_offset_in_fragment; |
| |
| auto fragment_offset_in_flow_thread = |
| original_offset_in_flow_thread - original_offset_in_fragment; |
| for (auto i = context_.fragments.size(); i > 0; --i) { |
| auto& fragment_context = context_.fragments[i - 1]; |
| fragment_context.repeating_paint_offset_adjustment = PhysicalOffset(); |
| // Adjust paint offsets of repeatings. |
| if (i != context_.fragments.size()) |
| fragment_context.repeating_paint_offset_adjustment.top = adjustment; |
| |
| // Calculate the adjustment for the repeating which will appear in the |
| // previous fragment. |
| adjustment -= fragment_height; |
| // Calculate the offset of the previous fragment in flow thread. It's used |
| // to get the height of that fragment. |
| fragment_offset_in_flow_thread -= fragment_height; |
| |
| if (flow_thread) { |
| fragment_height = flow_thread->PageLogicalHeightForOffset( |
| fragment_offset_in_flow_thread); |
| } |
| } |
| } |
| |
| static LayoutUnit FragmentLogicalTopInParentFlowThread( |
| const LayoutFlowThread& flow_thread, |
| LayoutUnit logical_top_in_current_flow_thread) { |
| const auto* parent_pagination_layer = |
| flow_thread.Layer()->Parent()->EnclosingPaginationLayer(); |
| if (!parent_pagination_layer) |
| return LayoutUnit(); |
| |
| const auto* parent_flow_thread = |
| &To<LayoutFlowThread>(parent_pagination_layer->GetLayoutObject()); |
| |
| LayoutPoint location(LayoutUnit(), logical_top_in_current_flow_thread); |
| // TODO(crbug.com/467477): Should we flip for writing-mode? For now regardless |
| // of flipping, fast/multicol/vertical-rl/nested-columns.html fails. |
| if (!flow_thread.IsHorizontalWritingMode()) |
| location = location.TransposedPoint(); |
| |
| // Convert into parent_flow_thread's coordinates. |
| location = flow_thread |
| .LocalToAncestorPoint(PhysicalOffsetToBeNoop(location), |
| parent_flow_thread) |
| .ToLayoutPoint(); |
| if (!parent_flow_thread->IsHorizontalWritingMode()) |
| location = location.TransposedPoint(); |
| |
| if (location.X() >= parent_flow_thread->LogicalWidth()) { |
| // TODO(crbug.com/803649): We hit this condition for |
| // external/wpt/css/css-multicol/multicol-height-block-child-001.xht. |
| // The normal path would cause wrong FragmentClip hierarchy. |
| // Return -1 to force standalone FragmentClip in the case. |
| return LayoutUnit(-1); |
| } |
| |
| // Return the logical top of the containing fragment in parent_flow_thread. |
| return location.Y() + |
| parent_flow_thread->PageRemainingLogicalHeightForOffset( |
| location.Y(), LayoutBox::kAssociateWithLatterPage) - |
| parent_flow_thread->PageLogicalHeightForOffset(location.Y()); |
| } |
| |
| // Find from parent contexts with matching |logical_top_in_flow_thread|, if any, |
| // to allow for correct property tree parenting of fragments. |
| PaintPropertyTreeBuilderFragmentContext |
| PaintPropertyTreeBuilder::ContextForFragment( |
| const base::Optional<PhysicalRect>& fragment_clip, |
| LayoutUnit logical_top_in_flow_thread) const { |
| DCHECK(!IsInNGFragmentTraversal()); |
| const auto& parent_fragments = context_.fragments; |
| if (parent_fragments.IsEmpty()) |
| return PaintPropertyTreeBuilderFragmentContext(); |
| |
| // This will be used in the loop finding matching fragment from ancestor flow |
| // threads after no matching from parent_fragments. |
| LayoutUnit logical_top_in_containing_flow_thread; |
| bool crossed_flow_thread = false; |
| |
| if (object_.IsLayoutFlowThread()) { |
| const auto& flow_thread = To<LayoutFlowThread>(object_); |
| // If this flow thread is under another flow thread, find the fragment in |
| // the parent flow thread containing this fragment. Otherwise, the following |
| // code will just match parent_contexts[0]. |
| logical_top_in_containing_flow_thread = |
| FragmentLogicalTopInParentFlowThread(flow_thread, |
| logical_top_in_flow_thread); |
| for (const auto& parent_context : parent_fragments) { |
| if (logical_top_in_containing_flow_thread == |
| parent_context.logical_top_in_flow_thread) { |
| auto context = parent_context; |
| context.fragment_clip = fragment_clip; |
| context.logical_top_in_flow_thread = logical_top_in_flow_thread; |
| return context; |
| } |
| } |
| crossed_flow_thread = true; |
| } else { |
| bool parent_is_under_same_flow_thread; |
| auto* pagination_layer = |
| context_.painting_layer->EnclosingPaginationLayer(); |
| if (object_.IsColumnSpanAll()) { |
| parent_is_under_same_flow_thread = false; |
| } else if (object_.IsOutOfFlowPositioned()) { |
| parent_is_under_same_flow_thread = |
| (object_.Parent()->PaintingLayer()->EnclosingPaginationLayer() == |
| pagination_layer); |
| } else { |
| parent_is_under_same_flow_thread = true; |
| } |
| |
| // Match against parent_fragments if the fragment and parent_fragments are |
| // under the same flow thread. |
| if (parent_is_under_same_flow_thread) { |
| DCHECK(object_.Parent()->PaintingLayer()->EnclosingPaginationLayer() == |
| pagination_layer); |
| for (const auto& parent_context : parent_fragments) { |
| if (logical_top_in_flow_thread == |
| parent_context.logical_top_in_flow_thread) { |
| auto context = parent_context; |
| // The context inherits fragment clip from parent so we don't need |
| // to issue fragment clip again. |
| context.fragment_clip = base::nullopt; |
| return context; |
| } |
| } |
| } |
| |
| logical_top_in_containing_flow_thread = logical_top_in_flow_thread; |
| crossed_flow_thread = !parent_is_under_same_flow_thread; |
| } |
| |
| // Found no matching parent fragment. Use parent_fragments[0] to inherit |
| // transforms and effects from ancestors, and adjust the clip state. |
| // TODO(crbug.com/803649): parent_fragments[0] is not always correct because |
| // some ancestor transform/effect may be missing in the fragment if the |
| // ancestor doesn't intersect with the first fragment of the flow thread. |
| auto context = parent_fragments[0]; |
| context.logical_top_in_flow_thread = logical_top_in_flow_thread; |
| context.fragment_clip = fragment_clip; |
| |
| // We reach here because of the following reasons: |
| // 1. the parent doesn't have the corresponding fragment because the fragment |
| // overflows the parent; |
| // 2. the fragment and parent_fragments are not under the same flow thread |
| // (e.g. column-span:all or some out-of-flow positioned). |
| // For each case, we need to adjust context.current.clip. For now it's the |
| // first parent fragment's FragmentClip which is not the correct clip for |
| // object_. |
| const ClipPaintPropertyNodeOrAlias* found_clip = nullptr; |
| for (const auto* container = object_.Container(); container; |
| container = container->Container()) { |
| if (!container->FirstFragment().HasLocalBorderBoxProperties()) |
| continue; |
| |
| const FragmentData* container_fragment = &container->FirstFragment(); |
| while (container_fragment->LogicalTopInFlowThread() < |
| logical_top_in_containing_flow_thread && |
| container_fragment->NextFragment()) |
| container_fragment = container_fragment->NextFragment(); |
| |
| if (container_fragment->LogicalTopInFlowThread() == |
| logical_top_in_containing_flow_thread) { |
| // Found a matching fragment in an ancestor container. Use the |
| // container's content clip as the clip state. |
| found_clip = &container_fragment->PostOverflowClip(); |
| break; |
| } |
| |
| // We didn't find corresponding fragment in the container because the |
| // fragment fully overflows the container. If the container has overflow |
| // clip, then this fragment should be under |container_fragment|. |
| // This works only when the current fragment and the overflow clip are under |
| // the same flow thread. In other cases, we just leave it broken, which will |
| // be fixed by LayoutNG block fragments hopefully. |
| if (!crossed_flow_thread) { |
| if (const auto* container_properties = |
| container_fragment->PaintProperties()) { |
| if (const auto* overflow_clip = container_properties->OverflowClip()) { |
| context.logical_top_in_flow_thread = |
| container_fragment->LogicalTopInFlowThread(); |
| found_clip = overflow_clip; |
| break; |
| } |
| } |
| } |
| |
| if (container->IsLayoutFlowThread()) { |
| logical_top_in_containing_flow_thread = |
| FragmentLogicalTopInParentFlowThread( |
| To<LayoutFlowThread>(*container), |
| logical_top_in_containing_flow_thread); |
| crossed_flow_thread = true; |
| } |
| } |
| |
| // We should always find a matching ancestor fragment in the above loop |
| // because logical_top_in_containing_flow_thread will be zero when we traverse |
| // across the top-level flow thread and it should match the first fragment of |
| // a non-fragmented ancestor container. |
| DCHECK(found_clip); |
| |
| if (!crossed_flow_thread) |
| context.fragment_clip = base::nullopt; |
| context.current.clip = found_clip; |
| if (object_.StyleRef().GetPosition() == EPosition::kAbsolute) |
| context.absolute_position.clip = found_clip; |
| else if (object_.StyleRef().GetPosition() == EPosition::kFixed) |
| context.fixed_position.clip = found_clip; |
| return context; |
| } |
| |
| void PaintPropertyTreeBuilder::CreateFragmentContextsInFlowThread( |
| bool needs_paint_properties) { |
| DCHECK(!IsInNGFragmentTraversal()); |
| // We need at least the fragments for all fragmented objects, which store |
| // their local border box properties and paint invalidation data (such |
| // as paint offset and visual rect) on each fragment. |
| PaintLayer* paint_layer = context_.painting_layer; |
| PaintLayer* enclosing_pagination_layer = |
| paint_layer->EnclosingPaginationLayer(); |
| |
| const auto& flow_thread = |
| To<LayoutFlowThread>(enclosing_pagination_layer->GetLayoutObject()); |
| PhysicalRect object_bounding_box_in_flow_thread; |
| if (context_.repeating_table_section) { |
| // The object is a descendant of a repeating object. It should use the |
| // repeating bounding box to repeat in the same fragments as its |
| // repeating ancestor. |
| object_bounding_box_in_flow_thread = |
| context_.repeating_table_section_bounding_box; |
| } else { |
| object_bounding_box_in_flow_thread = |
| BoundingBoxInPaginationContainer(object_, *enclosing_pagination_layer); |
| if (IsRepeatingTableSection(object_)) { |
| context_.repeating_table_section = |
| &ToInterface<LayoutNGTableSectionInterface>(object_); |
| context_.repeating_table_section_bounding_box = |
| object_bounding_box_in_flow_thread; |
| } |
| } |
| |
| FragmentData* current_fragment_data = nullptr; |
| FragmentainerIterator iterator( |
| flow_thread, object_bounding_box_in_flow_thread.ToLayoutRect()); |
| bool fragments_changed = false; |
| Vector<PaintPropertyTreeBuilderFragmentContext, 1> new_fragment_contexts; |
| for (; !iterator.AtEnd(); iterator.Advance()) { |
| auto pagination_offset = |
| PhysicalOffsetToBeNoop(iterator.PaginationOffset()); |
| auto logical_top_in_flow_thread = |
| iterator.FragmentainerLogicalTopInFlowThread(); |
| base::Optional<PhysicalRect> fragment_clip; |
| |
| if (object_.HasLayer()) { |
| // 1. Compute clip in flow thread space. |
| fragment_clip = PhysicalRectToBeNoop(iterator.ClipRectInFlowThread()); |
| |
| // We skip empty clip fragments, since they can share the same logical top |
| // with the subsequent fragments. Since we skip drawing empty fragments |
| // anyway, it doesn't affect the paint output, but it allows us to use |
| // logical top to uniquely identify fragments in an object. |
| if (fragment_clip->IsEmpty()) |
| continue; |
| |
| // 2. Convert #1 to visual coordinates in the space of the flow thread. |
| fragment_clip->Move(pagination_offset); |
| // 3. Adjust #2 to visual coordinates in the containing "paint offset" |
| // space. |
| { |
| DCHECK(context_.fragments[0].current.paint_offset_root); |
| PhysicalOffset pagination_visual_offset = |
| VisualOffsetFromPaintOffsetRoot(context_.fragments[0], |
| enclosing_pagination_layer); |
| // Adjust for paint offset of the root, which may have a subpixel |
| // component. The paint offset root never has more than one fragment. |
| pagination_visual_offset += |
| context_.fragments[0] |
| .current.paint_offset_root->FirstFragment() |
| .PaintOffset(); |
| fragment_clip->Move(pagination_visual_offset); |
| } |
| } |
| |
| // Match to parent fragments from the same containing flow thread. |
| auto fragment_context = |
| ContextForFragment(fragment_clip, logical_top_in_flow_thread); |
| // ContextForFragment may override logical_top_in_flow_thread. |
| logical_top_in_flow_thread = fragment_context.logical_top_in_flow_thread; |
| // Avoid fragment with duplicated overridden logical_top_in_flow_thread. |
| if (new_fragment_contexts.size() && |
| new_fragment_contexts.back().logical_top_in_flow_thread == |
| logical_top_in_flow_thread) |
| break; |
| new_fragment_contexts.push_back(fragment_context); |
| |
| if (current_fragment_data) { |
| if (!current_fragment_data->NextFragment()) |
| fragments_changed = true; |
| current_fragment_data = ¤t_fragment_data->EnsureNextFragment(); |
| } else { |
| current_fragment_data = &object_.GetMutableForPainting().FirstFragment(); |
| } |
| |
| fragments_changed |= logical_top_in_flow_thread != |
| current_fragment_data->LogicalTopInFlowThread(); |
| if (!fragments_changed) { |
| const ClipPaintPropertyNode* old_fragment_clip = nullptr; |
| if (const auto* properties = current_fragment_data->PaintProperties()) |
| old_fragment_clip = properties->FragmentClip(); |
| const base::Optional<PhysicalRect>& new_fragment_clip = |
| new_fragment_contexts.back().fragment_clip; |
| fragments_changed = !!old_fragment_clip != !!new_fragment_clip || |
| (old_fragment_clip && new_fragment_clip && |
| old_fragment_clip->PixelSnappedClipRect() != |
| ToSnappedClipRect(*new_fragment_clip)); |
| } |
| |
| InitFragmentPaintPropertiesForLegacy( |
| *current_fragment_data, |
| needs_paint_properties || new_fragment_contexts.back().fragment_clip, |
| pagination_offset, new_fragment_contexts.back()); |
| } |
| |
| if (!current_fragment_data) { |
| // This will be an empty fragment - get rid of it? |
| InitSingleFragmentFromParent(needs_paint_properties); |
| } else { |
| if (current_fragment_data->NextFragment()) |
| fragments_changed = true; |
| current_fragment_data->ClearNextFragment(); |
| context_.fragments = std::move(new_fragment_contexts); |
| } |
| |
| // Need to update subtree paint properties for the changed fragments. |
| if (fragments_changed) { |
| object_.GetMutableForPainting().AddSubtreePaintPropertyUpdateReason( |
| SubtreePaintPropertyUpdateReason::kFragmentsChanged); |
| } |
| } |
| |
| bool PaintPropertyTreeBuilder::ObjectIsRepeatingTableSectionInPagedMedia() |
| const { |
| if (!IsRepeatingTableSection(object_)) |
| return false; |
| |
| // The table section repeats in the pagination layer instead of paged media. |
| if (context_.painting_layer->EnclosingPaginationLayer()) |
| return false; |
| |
| if (!object_.View()->PageLogicalHeight()) |
| return false; |
| |
| // TODO(crbug.com/619094): Figure out the correct behavior for repeating |
| // objects in paged media with vertical writing modes. |
| if (!object_.View()->IsHorizontalWritingMode()) |
| return false; |
| |
| return true; |
| } |
| |
| void PaintPropertyTreeBuilder:: |
| CreateFragmentContextsForRepeatingFixedPosition() { |
| DCHECK(object_.IsFixedPositionObjectInPagedMedia()); |
| |
| LayoutView* view = object_.View(); |
| auto page_height = view->PageLogicalHeight(); |
| int page_count = ceilf(view->DocumentRect().Height() / page_height); |
| context_.fragments.resize(page_count); |
| |
| if (auto* scrollable_area = view->GetScrollableArea()) { |
| context_.fragments[0].fixed_position.paint_offset.top -= |
| LayoutUnit(scrollable_area->ScrollPosition().Y()); |
| } |
| |
| for (int page = 1; page < page_count; page++) { |
| context_.fragments[page] = context_.fragments[page - 1]; |
| context_.fragments[page].fixed_position.paint_offset.top += page_height; |
| context_.fragments[page].logical_top_in_flow_thread += page_height; |
| } |
| } |
| |
| void PaintPropertyTreeBuilder:: |
| CreateFragmentContextsForRepeatingTableSectionInPagedMedia() { |
| DCHECK(ObjectIsRepeatingTableSectionInPagedMedia()); |
| |
| // The containing LayoutView serves as the virtual pagination container |
| // for repeating table section in paged media. |
| LayoutView* view = object_.View(); |
| context_.repeating_table_section_bounding_box = |
| BoundingBoxInPaginationContainer(object_, *view->Layer()); |
| // Convert the bounding box into the scrolling contents space. |
| if (auto* scrollable_area = view->GetScrollableArea()) { |
| context_.repeating_table_section_bounding_box.offset.top += |
| LayoutUnit(scrollable_area->ScrollPosition().Y()); |
| } |
| |
| auto page_height = view->PageLogicalHeight(); |
| const auto& bounding_box = context_.repeating_table_section_bounding_box; |
| int first_page = floorf(bounding_box.Y() / page_height); |
| int last_page = ceilf(bounding_box.Bottom() / page_height) - 1; |
| if (first_page >= last_page) |
| return; |
| |
| context_.fragments.resize(last_page - first_page + 1); |
| for (int page = first_page; page <= last_page; page++) { |
| if (page > first_page) |
| context_.fragments[page - first_page] = context_.fragments[0]; |
| context_.fragments[page - first_page].logical_top_in_flow_thread = |
| page * page_height; |
| } |
| } |
| |
| bool PaintPropertyTreeBuilder::IsRepeatingInPagedMedia() const { |
| return context_.is_repeating_fixed_position || |
| (context_.repeating_table_section && |
| !context_.painting_layer->EnclosingPaginationLayer()); |
| } |
| |
| void PaintPropertyTreeBuilder::CreateFragmentDataForRepeatingInPagedMedia( |
| bool needs_paint_properties) { |
| DCHECK(IsRepeatingInPagedMedia()); |
| |
| FragmentData* fragment_data = nullptr; |
| for (auto& fragment_context : context_.fragments) { |
| fragment_data = fragment_data |
| ? &fragment_data->EnsureNextFragment() |
| : &object_.GetMutableForPainting().FirstFragment(); |
| InitFragmentPaintPropertiesForLegacy(*fragment_data, needs_paint_properties, |
| PhysicalOffset(), fragment_context); |
| } |
| DCHECK(fragment_data); |
| fragment_data->ClearNextFragment(); |
| } |
| |
| bool PaintPropertyTreeBuilder::UpdateFragments() { |
| bool had_paint_properties = object_.FirstFragment().PaintProperties(); |
| bool needs_paint_properties = |
| #if !DCHECK_IS_ON() |
| // If DCHECK is not on, use fast path for text. |
| !object_.IsText() && |
| #endif |
| (NeedsPaintOffsetTranslation( |
| object_, context_.direct_compositing_reasons) || |
| NeedsStickyTranslation(object_) || |
| NeedsTransform(object_, context_.direct_compositing_reasons) || |
| // Note: It is important to use MayNeedClipPathClip() instead of |
| // NeedsClipPathClip() which requires the clip path cache to be |
| // resolved, but the clip path cache invalidation must delayed until |
| // the paint offset and border box has been computed. |
| MayNeedClipPathClip(object_) || |
| NeedsEffect(object_, context_.direct_compositing_reasons) || |
| NeedsTransformForSVGChild(object_, |
| context_.direct_compositing_reasons) || |
| NeedsFilter(object_, context_) || NeedsCssClip(object_) || |
| NeedsInnerBorderRadiusClip(object_) || NeedsOverflowClip(object_) || |
| NeedsPerspective(object_) || NeedsReplacedContentTransform(object_) || |
| NeedsLinkHighlightEffect(object_) || |
| NeedsScrollOrScrollTranslation(object_, |
| context_.direct_compositing_reasons)); |
| |
| // If the object is a text, none of the above function should return true. |
| DCHECK(!needs_paint_properties || !object_.IsText()); |
| |
| // Need of fragmentation clip will be determined in CreateFragmentContexts(). |
| |
| if (IsInNGFragmentTraversal()) { |
| InitFragmentPaintPropertiesForNG(needs_paint_properties); |
| } else { |
| if (object_.IsFixedPositionObjectInPagedMedia()) { |
| // This flag applies to the object itself and descendants. |
| context_.is_repeating_fixed_position = true; |
| CreateFragmentContextsForRepeatingFixedPosition(); |
| } else if (ObjectIsRepeatingTableSectionInPagedMedia()) { |
| context_.repeating_table_section = |
| &ToInterface<LayoutNGTableSectionInterface>(object_); |
| CreateFragmentContextsForRepeatingTableSectionInPagedMedia(); |
| } |
| |
| if (IsRepeatingInPagedMedia()) { |
| CreateFragmentDataForRepeatingInPagedMedia(needs_paint_properties); |
| } else if (context_.painting_layer->ShouldFragmentCompositedBounds()) { |
| CreateFragmentContextsInFlowThread(needs_paint_properties); |
| } else { |
| InitSingleFragmentFromParent(needs_paint_properties); |
| UpdateCompositedLayerPaginationOffset(); |
| context_.is_repeating_fixed_position = false; |
| context_.repeating_table_section = nullptr; |
| } |
| } |
| |
| if (object_.IsSVGHiddenContainer()) { |
| // SVG resources are painted within one or more other locations in the |
| // SVG during paint, and hence have their own independent paint property |
| // trees, paint offset, etc. |
| context_.fragments.clear(); |
| context_.fragments.Grow(1); |
| context_.has_svg_hidden_container_ancestor = true; |
| PaintPropertyTreeBuilderFragmentContext& fragment_context = |
| context_.fragments[0]; |
| |
| fragment_context.current.paint_offset_root = |
| fragment_context.absolute_position.paint_offset_root = |
| fragment_context.fixed_position.paint_offset_root = &object_; |
| |
| object_.GetMutableForPainting().FirstFragment().ClearNextFragment(); |
| } |
| |
| if (object_.HasLayer()) { |
| To<LayoutBoxModelObject>(object_).Layer()->SetIsUnderSVGHiddenContainer( |
| context_.has_svg_hidden_container_ancestor); |
| } |
| |
| if (!IsInNGFragmentTraversal()) |
| UpdateRepeatingTableSectionPaintOffsetAdjustment(); |
| |
| return needs_paint_properties != had_paint_properties; |
| } |
| |
| bool PaintPropertyTreeBuilder::ObjectTypeMightNeedPaintProperties() const { |
| return !object_.IsText() && (object_.IsBoxModelObject() || object_.IsSVG()); |
| } |
| |
| bool PaintPropertyTreeBuilder::ObjectTypeMightNeedMultipleFragmentData() const { |
| return context_.painting_layer->EnclosingPaginationLayer() || |
| context_.repeating_table_section || |
| context_.is_repeating_fixed_position; |
| } |
| |
| void PaintPropertyTreeBuilder::UpdatePaintingLayer() { |
| if (object_.HasLayer() && |
| To<LayoutBoxModelObject>(object_).HasSelfPaintingLayer()) { |
| context_.painting_layer = To<LayoutBoxModelObject>(object_).Layer(); |
| } else if (!IsInNGFragmentTraversal() && |
| (object_.IsColumnSpanAll() || |
| object_.IsFloatingWithNonContainingBlockParent())) { |
| // See LayoutObject::paintingLayer() for the special-cases of floating under |
| // inline and multicolumn. |
| context_.painting_layer = object_.PaintingLayer(); |
| } |
| DCHECK(context_.painting_layer == object_.PaintingLayer()); |
| } |
| |
| PaintPropertyChangeType PaintPropertyTreeBuilder::UpdateForSelf() { |
| // This is not inherited from the parent context and we always recalculate it. |
| context_.direct_compositing_reasons = |
| CompositingReasonFinder::DirectReasonsForPaintProperties(object_); |
| context_.was_layout_shift_root = |
| IsLayoutShiftRoot(object_, object_.FirstFragment()); |
| context_.was_main_thread_scrolling = false; |
| if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled() && |
| IsA<LayoutBox>(object_)) { |
| if (auto* scrollable_area = To<LayoutBox>(object_).GetScrollableArea()) { |
| context_.was_main_thread_scrolling = |
| scrollable_area->ShouldScrollOnMainThread(); |
| } |
| } |
| |
| UpdatePaintingLayer(); |
| |
| PaintPropertyChangeType property_changed = |
| PaintPropertyChangeType::kUnchanged; |
| if (ObjectTypeMightNeedPaintProperties() || |
| ObjectTypeMightNeedMultipleFragmentData()) { |
| if (UpdateFragments()) |
| property_changed = PaintPropertyChangeType::kNodeAddedOrRemoved; |
| } else { |
| DCHECK_EQ(context_.direct_compositing_reasons, CompositingReason::kNone); |
| if (!IsInNGFragmentTraversal()) |
| object_.GetMutableForPainting().FirstFragment().ClearNextFragment(); |
| } |
| |
| if (pre_paint_info_) { |
| DCHECK_EQ(context_.fragments.size(), 1u); |
| FragmentPaintPropertyTreeBuilder builder(object_, pre_paint_info_, context_, |
| context_.fragments[0], |
| pre_paint_info_->fragment_data); |
| builder.UpdateForSelf(); |
| property_changed = std::max(property_changed, builder.PropertyChanged()); |
| } else { |
| auto* fragment_data = &object_.GetMutableForPainting().FirstFragment(); |
| for (auto& fragment_context : context_.fragments) { |
| FragmentPaintPropertyTreeBuilder builder( |
| object_, /* pre_paint_info */ nullptr, context_, fragment_context, |
| *fragment_data); |
| builder.UpdateForSelf(); |
| property_changed = std::max(property_changed, builder.PropertyChanged()); |
| fragment_data = fragment_data->NextFragment(); |
| } |
| DCHECK(!fragment_data); |
| } |
| |
| // We need to update property tree states of paint chunks. |
| if (property_changed >= PaintPropertyChangeType::kNodeAddedOrRemoved) { |
| context_.painting_layer->SetNeedsRepaint(); |
| if (object_.IsDocumentElement()) { |
| // View background painting depends on existence of the document element's |
| // paint properties (see callsite of ViewPainter::PaintRootGroup()). |
| // Invalidate view background display item clients. |
| // SetBackgroundNeedsFullPaintInvalidation() won't work here because we |
| // have already walked the LayoutView in PrePaintTreeWalk. |
| LayoutView* layout_view = object_.View(); |
| layout_view->Layer()->SetNeedsRepaint(); |
| auto reason = PaintInvalidationReason::kBackground; |
| static_cast<const DisplayItemClient*>(layout_view)->Invalidate(reason); |
| if (auto* scrollable_area = layout_view->GetScrollableArea()) { |
| scrollable_area->GetScrollingBackgroundDisplayItemClient().Invalidate( |
| reason); |
| } |
| } |
| } |
| |
| object_.GetMutableForPainting() |
| .SetShouldAssumePaintOffsetTranslationForLayoutShiftTracking(false); |
| |
| return property_changed; |
| } |
| |
| PaintPropertyChangeType PaintPropertyTreeBuilder::UpdateForChildren() { |
| PaintPropertyChangeType property_changed = |
| PaintPropertyChangeType::kUnchanged; |
| if (!ObjectTypeMightNeedPaintProperties()) |
| return property_changed; |
| |
| FragmentData* fragment_data; |
| if (pre_paint_info_) { |
| DCHECK_EQ(context_.fragments.size(), 1u); |
| fragment_data = &pre_paint_info_->fragment_data; |
| DCHECK(fragment_data); |
| } else { |
| fragment_data = &object_.GetMutableForPainting().FirstFragment(); |
| } |
| |
| // For now, only consider single fragment elements as possible isolation |
| // boundaries. |
| // TODO(crbug.com/890932): See if this is needed. |
| bool is_isolated = context_.fragments.size() == 1u; |
| for (auto& fragment_context : context_.fragments) { |
| FragmentPaintPropertyTreeBuilder builder(object_, pre_paint_info_, context_, |
| fragment_context, *fragment_data); |
| // The element establishes an isolation boundary if it has isolation nodes |
| // before and after updating the children. In other words, if it didn't have |
| // isolation nodes previously then we still want to do a subtree walk. If it |
| // now doesn't have isolation nodes, then of course it is also not isolated. |
| is_isolated &= builder.HasIsolationNodes(); |
| builder.UpdateForChildren(); |
| is_isolated &= builder.HasIsolationNodes(); |
| |
| property_changed = std::max(property_changed, builder.PropertyChanged()); |
| fragment_data = fragment_data->NextFragment(); |
| } |
| |
| // With NG fragment traversal we were supplied with the right FragmentData by |
| // the caller, and we only ran one lap in the loop above. Whether or not there |
| // are more FragmentData objects following is irrelevant then. |
| DCHECK(pre_paint_info_ || !fragment_data); |
| |
| if (object_.SubtreePaintPropertyUpdateReasons() != |
| static_cast<unsigned>(SubtreePaintPropertyUpdateReason::kNone)) { |
| if (AreSubtreeUpdateReasonsIsolationPiercing( |
| object_.SubtreePaintPropertyUpdateReasons())) { |
| context_.force_subtree_update_reasons |= |
| PaintPropertyTreeBuilderContext::kSubtreeUpdateIsolationPiercing; |
| } else { |
| context_.force_subtree_update_reasons |= |
| PaintPropertyTreeBuilderContext::kSubtreeUpdateIsolationBlocked; |
| } |
| } |
| if (object_.CanContainAbsolutePositionObjects()) |
| context_.container_for_absolute_position = &object_; |
| if (object_.CanContainFixedPositionObjects()) |
| context_.container_for_fixed_position = &object_; |
| |
| if (is_isolated) { |
| context_.force_subtree_update_reasons &= |
| ~PaintPropertyTreeBuilderContext::kSubtreeUpdateIsolationBlocked; |
| } |
| |
| // We need to update property tree states of paint chunks. |
| if (property_changed >= PaintPropertyChangeType::kNodeAddedOrRemoved) |
| context_.painting_layer->SetNeedsRepaint(); |
| |
| if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled() && |
| IsA<LayoutBox>(object_)) { |
| if (auto* scrollable_area = To<LayoutBox>(object_).GetScrollableArea()) { |
| if (context_.was_main_thread_scrolling != |
| scrollable_area->ShouldScrollOnMainThread()) |
| scrollable_area->MainThreadScrollingDidChange(); |
| } |
| } |
| |
| return property_changed; |
| } |
| |
| } // namespace blink |