blob: 3e364f0af731acf5e776b627937c819836101fd0 [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// 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/intersection_observer/intersection_geometry.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/safe_conversions.h"
#include "third_party/blink/renderer/core/display_lock/display_lock_utilities.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/html/html_frame_owner_element.h"
#include "third_party/blink/renderer/core/intersection_observer/intersection_observer_entry.h"
#include "third_party/blink/renderer/core/layout/adjust_for_absolute_zoom.h"
#include "third_party/blink/renderer/core/layout/hit_test_result.h"
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/core/layout/layout_embedded_content.h"
#include "third_party/blink/renderer/core/layout/layout_inline.h"
#include "third_party/blink/renderer/core/layout/layout_text.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/paint/clip_path_clipper.h"
#include "third_party/blink/renderer/core/paint/paint_layer.h"
#include "third_party/blink/renderer/platform/graphics/paint/geometry_mapper.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
namespace blink {
namespace {
// Convert a Length value to physical pixels.
LayoutUnit ComputeMargin(const Length& length,
float reference_length,
float zoom) {
if (length.IsPercent()) {
return LayoutUnit(
static_cast<int>(reference_length * length.Percent() / 100.0));
}
DCHECK(length.IsFixed());
return LayoutUnit(length.Value() * zoom);
}
PhysicalBoxStrut ResolveMargin(const Vector<Length>& margin,
const gfx::SizeF& reference_size,
float zoom) {
DCHECK_EQ(margin.size(), 4u);
return PhysicalBoxStrut(
ComputeMargin(margin[0], reference_size.height(), zoom),
ComputeMargin(margin[1], reference_size.width(), zoom),
ComputeMargin(margin[2], reference_size.height(), zoom),
ComputeMargin(margin[3], reference_size.width(), zoom));
}
// Expand rect by the given margin values.
void ApplyMargin(PhysicalRect& expand_rect,
const Vector<Length>& margin,
float zoom,
const gfx::SizeF& reference_size) {
if (margin.empty())
return;
expand_rect.Expand(ResolveMargin(margin, reference_size, zoom));
}
void ApplyMargin(gfx::RectF& expand_rect,
const Vector<Length>& margin,
float zoom,
const gfx::SizeF& reference_size) {
if (margin.empty()) {
return;
}
expand_rect.Outset(
gfx::OutsetsF(ResolveMargin(margin, reference_size, zoom)));
}
// Returns the root intersect rect for the given root object, with the given
// margins applied, in the coordinate system of the root object.
//
// https://w3c.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle
gfx::RectF InitializeRootRect(const LayoutObject* root,
const Vector<Length>& margin) {
DCHECK(margin.empty() || margin.size() == 4);
PhysicalRect result;
auto* layout_view = DynamicTo<LayoutView>(root);
if (layout_view && root->GetDocument().GetFrame()->IsOutermostMainFrame()) {
// The main frame is a bit special as the scrolling viewport can differ in
// size from the LayoutView itself. There's two situations this occurs in:
// 1) The ForceZeroLayoutHeight quirk setting is used in Android WebView for
// compatibility and sets the initial-containing-block's (a.k.a.
// LayoutView) height to 0. Thus, we can't use its size for intersection
// testing. Use the FrameView geometry instead.
// 2) An element wider than the ICB can cause us to resize the FrameView so
// we can zoom out to fit the entire element width.
result = layout_view->OverflowClipRect(PhysicalOffset());
} else if (auto* layout_box = DynamicTo<LayoutBox>(root)) {
if (layout_box->ShouldClipOverflowAlongBothAxis()) {
// TODO(https://github.com/w3c/IntersectionObserver/issues/518):
// This doesn't strictly conform to the current spec (which says we
// should use the padding box rect) when there is overflow-clip-margin.
// We should also consider overflow-clip along only one axis.
result = layout_box->OverflowClipRect(PhysicalOffset());
} else {
result = layout_box->PhysicalBorderBoxRect();
}
} else {
result = To<LayoutInline>(root)->PhysicalLinesBoundingBox();
}
ApplyMargin(result, margin, root->StyleRef().EffectiveZoom(),
gfx::SizeF(result.size));
return gfx::RectF(result);
}
gfx::RectF GetBoxBounds(const LayoutBox* box, bool use_overflow_clip_edge) {
PhysicalRect bounds(box->PhysicalBorderBoxRect());
// Only use overflow clip rect if we need to use overflow clip edge and
// overflow clip margin may have an effect, meaning we clip to the overflow
// clip edge and not something else.
if (use_overflow_clip_edge && box->ShouldApplyOverflowClipMargin()) {
// OverflowClipRect() may be larger than PhysicalBorderBoxRect().
bounds.Unite(box->OverflowClipRect(PhysicalOffset()));
}
return gfx::RectF(bounds);
}
// Return the bounding box of target in target's own coordinate system.
gfx::RectF InitializeTargetRect(const LayoutObject* target, unsigned flags) {
if (flags & IntersectionGeometry::kForFrameViewportIntersection) {
return gfx::RectF(To<LayoutEmbeddedContent>(target)->ReplacedContentRect());
}
if (target->IsSVGChild()) {
return target->DecoratedBoundingBox();
}
if (auto* layout_box = DynamicTo<LayoutBox>(target)) {
return GetBoxBounds(layout_box,
flags & IntersectionGeometry::kUseOverflowClipEdge);
}
if (auto* layout_inline = DynamicTo<LayoutInline>(target)) {
return layout_inline->LocalBoundingBoxRectF();
}
return gfx::RectF(To<LayoutText>(target)->PhysicalLinesBoundingBox());
}
// Returns true if target has visual effects applied, or if rect, given in
// absolute coordinates, is overlapped by any content painted after target
//
// https://w3c.github.io/IntersectionObserver/v2/#calculate-visibility-algo
bool ComputeIsVisible(const LayoutObject* target, const PhysicalRect& rect) {
if (!target->GetDocument().GetFrame() ||
target->GetDocument().GetFrame()->LocalFrameRoot().GetOcclusionState() !=
mojom::blink::FrameOcclusionState::kGuaranteedNotOccluded) {
return false;
}
if (target->HasDistortingVisualEffects())
return false;
// TODO(layout-dev): This should hit-test the intersection rect, not the
// target rect; it's not helpful to know that the portion of the target that
// is clipped is also occluded.
HitTestResult result(target->HitTestForOcclusion(rect));
const Node* hit_node = result.InnerNode();
if (!hit_node || hit_node == target->GetNode())
return true;
// TODO(layout-dev): This IsDescendantOf tree walk could be optimized by
// stopping when hit_node's containing LayoutBlockFlow is reached.
if (target->IsLayoutInline())
return hit_node->IsDescendantOf(target->GetNode());
return false;
}
// Returns the transform that maps from object's local coordinates to the
// containing view's coordinates. Note that this doesn't work if `object` has
// multiple block fragments.
gfx::Transform ObjectToViewTransform(const LayoutObject& object) {
// Use faster GeometryMapper when possible.
PropertyTreeStateOrAlias container_properties =
PropertyTreeState::Uninitialized();
const LayoutObject* property_container =
IntersectionGeometry::CanUseGeometryMapper(object)
? object.GetPropertyContainer(nullptr, &container_properties)
: nullptr;
if (property_container) {
gfx::Transform transform = GeometryMapper::SourceToDestinationProjection(
container_properties.Transform(),
object.View()->FirstFragment().LocalBorderBoxProperties().Transform());
transform.Translate(gfx::Vector2dF(object.FirstFragment().PaintOffset()));
return transform;
}
// Fall back to MapLocalToAncestor.
TransformState transform_state(TransformState::kApplyTransformDirection);
object.MapLocalToAncestor(nullptr, transform_state, 0);
return transform_state.AccumulatedTransform();
}
void ScrollingContentsToBorderBoxSpace(const LayoutBox* box, gfx::RectF& rect) {
DCHECK(box->IsScrollContainer());
const PaintLayerScrollableArea* scrollable_area = box->GetScrollableArea();
CHECK(scrollable_area);
rect.Offset(-scrollable_area->ScrollPosition().OffsetFromOrigin());
}
bool ClipsSelf(const LayoutObject& object) {
return object.HasClip() || object.HasClipPath() || object.HasMask() ||
// For simplicity, assume all SVG children clip self (with e.g.
// SVG mask).
object.IsSVGChild();
}
bool ClipsContents(const LayoutObject& object) {
// An objects that clips itself also clips contents.
if (ClipsSelf(object)) {
return true;
}
// TODO(wangxianzhu): Ideally we should ignore clippers that don't have
// a scrollable overflow, but that caused crbug.com/41492283. Investigate.
return object.ShouldClipOverflowAlongEitherAxis();
}
static const unsigned kConstructorFlagsMask =
IntersectionGeometry::kShouldReportRootBounds |
IntersectionGeometry::kShouldComputeVisibility |
IntersectionGeometry::kShouldTrackFractionOfRoot |
IntersectionGeometry::kForFrameViewportIntersection |
IntersectionGeometry::kShouldConvertToCSSPixels |
IntersectionGeometry::kUseOverflowClipEdge |
IntersectionGeometry::kRespectFilters |
IntersectionGeometry::kScrollAndVisibilityOnly;
} // namespace
IntersectionGeometry::RootGeometry::RootGeometry(const LayoutObject* root,
const Vector<Length>& margin) {
if (!root || !root->GetNode() || !root->GetNode()->isConnected() ||
// TODO(crbug.com/1456208): Support inline root.
!root->IsBox()) {
return;
}
zoom = root->StyleRef().EffectiveZoom();
local_root_rect = InitializeRootRect(root, margin);
if (RuntimeEnabledFeatures::IntersectionOptimizationEnabled()) {
root_to_view_transform = ObjectToViewTransform(*root);
} else {
TransformState transform_state(TransformState::kApplyTransformDirection);
root->MapLocalToAncestor(nullptr, transform_state, 0);
root_to_view_transform = transform_state.AccumulatedTransform();
}
}
bool IntersectionGeometry::RootGeometry::operator==(
const RootGeometry& other) const {
return zoom == other.zoom && local_root_rect == other.local_root_rect &&
root_to_view_transform == other.root_to_view_transform;
}
#if CHECK_SKIPPED_UPDATE_ON_SCROLL()
String IntersectionGeometry::CachedRects::ToString() const {
auto transform_to_string = [](const gfx::Transform& t) {
return t.IsIdentityOr2dTranslation() ? t.To2dTranslation().ToString()
: t.ToString();
};
return String::Format(
"%d target_rect: %s %s root_rect: %s %s intersection: %s %s %s "
"min_to_update %s %s target_t: %s root_t: %s intersect: %d "
"rel: %d r_scrolls_t: %d",
valid, local_target_rect.ToString().c_str(),
target_rect.ToString().c_str(), local_root_rect.ToString().c_str(),
root_rect.ToString().c_str(),
unscrolled_unclipped_intersection_rect.ToString().c_str(),
unclipped_intersection_rect.ToString().c_str(),
intersection_rect.ToString().c_str(),
computed_min_scroll_delta_to_update.ToString().c_str(),
min_scroll_delta_to_update.ToString().c_str(),
transform_to_string(target_to_view_transform).c_str(),
transform_to_string(root_to_view_transform).c_str(), does_intersect,
relationship, root_scrolls_target);
}
#endif
const LayoutObject* IntersectionGeometry::GetExplicitRootLayoutObject(
const Node& root_node) {
if (!root_node.isConnected()) {
return nullptr;
}
if (root_node.IsDocumentNode()) {
return To<Document>(root_node).GetLayoutView();
}
return root_node.GetLayoutObject();
}
IntersectionGeometry::IntersectionGeometry(
const Node* root_node,
const Element& target_element,
const Vector<Length>& root_margin,
const Vector<float>& thresholds,
const Vector<Length>& target_margin,
const Vector<Length>& scroll_margin,
unsigned flags,
std::optional<RootGeometry>& root_geometry,
CachedRects* cached_rects)
: flags_(flags & kConstructorFlagsMask) {
// Only one of root_margin or target_margin can be specified.
DCHECK(root_margin.empty() || target_margin.empty());
if (!root_node) {
flags_ |= kRootIsImplicit;
}
RootAndTarget root_and_target(root_node, target_element,
!target_margin.empty(), !scroll_margin.empty());
UpdateShouldUseCachedRects(root_and_target, cached_rects);
if (root_and_target.relationship == RootAndTarget::kInvalid) {
return;
}
if (root_geometry) {
DCHECK(*root_geometry == RootGeometry(root_and_target.root, root_margin));
} else {
root_geometry.emplace(root_and_target.root, root_margin);
}
ComputeGeometry(*root_geometry, root_and_target, thresholds, target_margin,
scroll_margin, cached_rects);
}
IntersectionGeometry::RootAndTarget::RootAndTarget(
const Node* root_node,
const Element& target_element,
bool has_target_margin,
bool has_scroll_margin)
: target(GetTargetLayoutObject(target_element)),
root(target ? GetRootLayoutObject(root_node) : nullptr) {
ComputeRelationship(!root_node, has_target_margin, has_scroll_margin);
}
bool IsAllowedLayoutObjectType(const LayoutObject& target) {
return target.IsBoxModelObject() || target.IsText() || target.IsSVG();
}
// Validates the given target element and returns its LayoutObject
const LayoutObject* IntersectionGeometry::GetTargetLayoutObject(
const Element& target_element) {
if (!target_element.isConnected()) {
return nullptr;
}
LayoutObject* target = target_element.GetLayoutObject();
if (!target || !IsAllowedLayoutObjectType(*target)) {
return nullptr;
}
// If the target is inside a locked subtree, it isn't ever visible.
if (UNLIKELY(target->GetFrameView()->IsDisplayLocked() ||
DisplayLockUtilities::IsInLockedSubtreeCrossingFrames(
target_element))) {
return nullptr;
}
DCHECK(!target_element.GetDocument().View()->NeedsLayout());
return target;
}
// If root_node is non-null, it is treated as the explicit root of an
// IntersectionObserver; if it is valid, its LayoutObject is returned.
//
// If root_node is null, returns the object to be used to compute intersection
// for a given target with the implicit root. Note that if the target is in
// a remote frame, the returned object is the LayoutView of the local frame
// root instead of the topmost main frame.
//
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-root
const LayoutObject* IntersectionGeometry::RootAndTarget::GetRootLayoutObject(
const Node* root_node) const {
if (root_node) {
return GetExplicitRootLayoutObject(*root_node);
}
if (const LocalFrame* frame = target->GetDocument().GetFrame()) {
return frame->LocalFrameRoot().ContentLayoutObject();
}
return nullptr;
}
void IntersectionGeometry::RootAndTarget::ComputeRelationship(
bool root_is_implicit,
bool has_target_margin,
bool has_scroll_margin) {
if (!root || !target || root == target) {
relationship = kInvalid;
return;
}
if (root_is_implicit && !target->GetFrame()->IsOutermostMainFrame()) {
relationship = kTargetInSubFrame;
DCHECK(root->IsScrollContainer());
DCHECK(root->IsLayoutView());
if (RuntimeEnabledFeatures::IntersectionOptimizationEnabled()) {
root_scrolls_target = To<LayoutView>(root)->HasScrollableOverflow();
if (root_scrolls_target) {
// Check if target's ancestor container under root is fixed-position.
// If yes, reset root_scroll_target to false.
const LayoutObject* container = target;
while (container->GetFrame() != root->GetFrame()) {
container = container->GetFrame()->OwnerLayoutObject();
if (!container) {
relationship = kInvalid;
return;
}
}
while (true) {
const LayoutObject* next_container = container->Container();
if (next_container == root) {
root_scrolls_target = !container->IsFixedPositioned();
break;
}
container = next_container;
}
}
} else {
root_scrolls_target = true;
}
if (!has_scroll_margin) {
// When scroll margins are defined intermediate_scrollers still needs to
// get populated.
return;
}
}
if (target->GetFrame() != root->GetFrame() && !root_is_implicit) {
// The case of different frame with implicit root has been covered by the
// previous condition.
// The target and the explicit root are required to be in the same frame.
relationship = kInvalid;
return;
}
bool has_intermediate_clippers = false;
const LayoutObject* previous_container = nullptr;
const LayoutObject* container = target;
bool have_crossed_frame_boundary = false;
if (ClipsSelf(*target)) {
has_intermediate_clippers = true;
}
while (container != root) {
has_filter |=
!have_crossed_frame_boundary && container->HasFilterInducingProperty();
// Don't check for filters if we've already found one.
LayoutObject::AncestorSkipInfo skip_info(root, !has_filter);
previous_container = container;
container = container->Container(&skip_info);
if (!has_filter && !have_crossed_frame_boundary) {
has_filter = skip_info.FilterSkipped();
}
if (skip_info.AncestorSkipped()) {
DCHECK(!have_crossed_frame_boundary);
// The root is not in the containing block chain of the target.
relationship = kInvalid;
return;
}
if (!container) {
if (!root_is_implicit) {
relationship = kInvalid;
return;
}
// We need to jump up the frame tree
DCHECK(previous_container->IsLayoutView());
// previous_container is the layout view of the iframe.
// OwnerLayoutObject jumps the iframe boundary.
// owner is the iframe element node.
auto* owner =
previous_container->GetFrameView()->GetFrame().OwnerLayoutObject();
if (!owner) {
return;
}
container = owner;
have_crossed_frame_boundary = true;
// We can continue to top of loop since iframe element is not a scroller.
continue;
}
if (!has_intermediate_clippers && !have_crossed_frame_boundary &&
container != root && ClipsContents(*container)) {
has_intermediate_clippers = true;
}
if (container != root && has_scroll_margin &&
container->IsScrollContainer()) {
intermediate_scrollers.push_back(To<LayoutBox>(container));
}
}
DCHECK(previous_container);
if (RuntimeEnabledFeatures::IntersectionOptimizationEnabled()) {
root_scrolls_target =
root->IsScrollContainer() &&
To<LayoutBox>(root)->HasScrollableOverflow() &&
!(root->IsLayoutView() && previous_container->IsFixedPositioned());
} else {
root_scrolls_target = root->IsScrollContainer();
}
if (have_crossed_frame_boundary) {
DCHECK_EQ(relationship, kTargetInSubFrame);
} else if (has_intermediate_clippers) {
relationship = kHasIntermediateClippers;
} else if (root_scrolls_target) {
relationship = kScrollableByRootOnly;
} else {
relationship = kNotScrollable;
}
}
bool IntersectionGeometry::CanUseGeometryMapper(const LayoutObject& object) {
// This checks for cases where we didn't just complete a successful lifecycle
// update, e.g., if the frame is throttled.
LayoutView* layout_view = object.GetDocument().GetLayoutView();
return layout_view && !layout_view->NeedsPaintPropertyUpdate() &&
!layout_view->DescendantNeedsPaintPropertyUpdate();
}
void IntersectionGeometry::UpdateShouldUseCachedRects(
const RootAndTarget& root_and_target,
CachedRects* cached_rects) {
if (!cached_rects || !cached_rects->valid) {
return;
}
cached_rects->valid = false;
if (root_and_target.relationship == RootAndTarget::kInvalid) {
return;
}
if (!root_and_target.intermediate_scrollers.empty()) {
// This happens when there are scroll margins. We can't use cached rects
// because we need to call ApplyClip for each scroller to apply the
// scroll margins.
return;
}
if (RuntimeEnabledFeatures::IntersectionOptimizationEnabled()) {
if (!(flags_ & kScrollAndVisibilityOnly)) {
return;
}
// Cached rects can only be used if there are no scrollable objects in the
// hierarchy between target and root (a scrollable root is ok). The reason
// is that a scroll change in an intermediate scroller would change the
// intersection geometry, but we intentionally don't invalidate cached
// rects and schedule intersection update to enable the minimul-scroll-
// delta-to-update optimization.
if (root_and_target.relationship != RootAndTarget::kNotScrollable &&
root_and_target.relationship != RootAndTarget::kScrollableByRootOnly) {
return;
}
} else {
if (RootIsImplicit()) {
return;
}
// Cached rects can only be used if there are no scrollable objects in the
// hierarchy between target and root (a scrollable root is ok). The reason
// is that a scroll change in an intermediate scroller would change the
// intersection geometry, but it would not properly trigger an invalidation
// of the cached rects.
PaintLayer* root_layer = root_and_target.target->View()->Layer();
if (!root_layer) {
return;
}
if (root_and_target.target->DeprecatedEnclosingScrollableBox() !=
root_and_target.root) {
return;
}
}
flags_ |= kShouldUseCachedRects;
}
void IntersectionGeometry::ComputeGeometry(const RootGeometry& root_geometry,
const RootAndTarget& root_and_target,
const Vector<float>& thresholds,
const Vector<Length>& target_margin,
const Vector<Length>& scroll_margin,
CachedRects* cached_rects) {
CHECK_GE(thresholds.size(), 1u);
DCHECK(cached_rects || !ShouldUseCachedRects());
flags_ |= kDidComputeGeometry;
const LayoutObject* root = root_and_target.root;
const LayoutObject* target = root_and_target.target;
CHECK(root);
CHECK(target);
// Initially:
// target_rect_ is in target's coordinate system
// root_rect_ is in root's coordinate system
// The coordinate system for unclipped_intersection_rect_ depends on whether
// or not we can use previously cached geometry...
bool pre_margin_target_rect_is_empty;
if (ShouldUseCachedRects()) {
target_rect_ = cached_rects->local_target_rect;
pre_margin_target_rect_is_empty =
cached_rects->pre_margin_target_rect_is_empty;
// The cached intersection rect has already been mapped/clipped up to the
// root, except that the root's scroll offset and overflow clip have not
// been applied.
unclipped_intersection_rect_ =
cached_rects->unscrolled_unclipped_intersection_rect;
} else {
target_rect_ = InitializeTargetRect(target, flags_);
pre_margin_target_rect_is_empty = target_rect_.IsEmpty();
ApplyMargin(target_rect_, target_margin, root_geometry.zoom,
InitializeRootRect(root, {} /* margin */).size());
// We have to map/clip target_rect_ up to the root, so we begin with the
// intersection rect in target's coordinate system. After ClipToRoot, it
// will be in root's coordinate system.
unclipped_intersection_rect_ = target_rect_;
}
if (cached_rects) {
cached_rects->local_target_rect = target_rect_;
cached_rects->pre_margin_target_rect_is_empty =
pre_margin_target_rect_is_empty;
}
root_rect_ = root_geometry.local_root_rect;
bool does_intersect =
ClipToRoot(root_and_target, root_rect_, unclipped_intersection_rect_,
intersection_rect_, scroll_margin, cached_rects);
gfx::Transform target_to_view_transform = ObjectToViewTransform(*target);
target_rect_ = target_to_view_transform.MapRect(target_rect_);
if (does_intersect) {
gfx::RectF unclipped_intersection_rect;
if (RootIsImplicit()) {
// Generate matrix to transform from the space of the implicit root to
// the absolute coordinates of the target document.
TransformState implicit_root_to_target_document_transform(
TransformState::kUnapplyInverseTransformDirection);
target->View()->MapAncestorToLocal(
nullptr, implicit_root_to_target_document_transform,
kTraverseDocumentBoundaries | kApplyRemoteMainFrameTransform);
gfx::Transform matrix =
implicit_root_to_target_document_transform.AccumulatedTransform()
.InverseOrIdentity();
intersection_rect_ =
matrix.ProjectQuad(gfx::QuadF(intersection_rect_)).BoundingBox();
unclipped_intersection_rect =
matrix.ProjectQuad(gfx::QuadF(unclipped_intersection_rect_))
.BoundingBox();
} else {
// `intersection_rect` is in root's coordinate system; map it up to
// absolute coordinates for target's containing document (which is the
// same as root's document).
intersection_rect_ =
root_geometry.root_to_view_transform.MapRect(intersection_rect_);
unclipped_intersection_rect =
root_geometry.root_to_view_transform.MapRect(
unclipped_intersection_rect);
}
unclipped_intersection_rect_ = unclipped_intersection_rect;
} else {
intersection_rect_ = gfx::RectF();
}
// Map root_rect_ from root's coordinate system to absolute coordinates.
root_rect_ =
root_geometry.root_to_view_transform.MapRect(gfx::RectF(root_rect_));
// Some corner cases for threshold index:
// - If target rect is zero area, because it has zero width and/or zero
// height,
// only two states are recognized:
// - 0 means not intersecting.
// - 1 means intersecting.
// No other threshold crossings are possible.
// - Otherwise:
// - If root and target do not intersect, the threshold index is 0.
// - If root and target intersect but the intersection has zero-area
// (i.e., they have a coincident edge or corner), we consider the
// intersection to have "crossed" a zero threshold, but not crossed
// any non-zero threshold.
if (does_intersect) {
const gfx::RectF& comparison_rect =
ShouldTrackFractionOfRoot() ? root_rect_ : target_rect_;
// Note that if we are checking whether target is empty, we have to consider
// the fact that we might have padded the rect with a target margin. If we
// did, `pre_margin_target_rect_is_empty` would be true. Use this
// information to force the rect to be empty for the purposes of this
// computation. Note that it could also be the case that the rect started as
// non-empty and was transformed to be empty. In this case, we rely on
// target_rect_.IsEmpty() to be true, so we need to check the rect itself as
// well.
// In the fraction of root case, we can just check the comparison rect.
bool empty_override =
!ShouldTrackFractionOfRoot() && pre_margin_target_rect_is_empty;
if (comparison_rect.IsEmpty() || empty_override) {
intersection_ratio_ = 1;
} else {
const gfx::SizeF& intersection_size = intersection_rect_.size();
const float intersection_area = intersection_size.GetArea();
const gfx::SizeF& comparison_size = comparison_rect.size();
const float area_of_interest = comparison_size.GetArea();
intersection_ratio_ = std::min(intersection_area / area_of_interest, 1.f);
}
threshold_index_ =
FirstThresholdGreaterThan(intersection_ratio_, thresholds);
} else {
intersection_ratio_ = 0;
threshold_index_ = 0;
}
if (IsIntersecting() && ShouldComputeVisibility() &&
ComputeIsVisible(target,
PhysicalRect::FastAndLossyFromRectF(target_rect_))) {
flags_ |= kIsVisible;
}
if (cached_rects) {
cached_rects->min_scroll_delta_to_update = ComputeMinScrollDeltaToUpdate(
root_and_target, target_to_view_transform,
root_geometry.root_to_view_transform, thresholds, scroll_margin);
cached_rects->valid = true;
UMA_HISTOGRAM_COUNTS_1000(
"Blink.IntersectionObservation.MinScrollDeltaToUpdateX",
base::saturated_cast<int>(
cached_rects->min_scroll_delta_to_update.x()));
UMA_HISTOGRAM_COUNTS_1000(
"Blink.IntersectionObservation.MinScrollDeltaToUpdateY",
base::saturated_cast<int>(
cached_rects->min_scroll_delta_to_update.y()));
#if CHECK_SKIPPED_UPDATE_ON_SCROLL()
// TODO(wangxianzhu): Remove or clean up this code after fixing
// crbug.com/41492283.
PropertyTreeStateOrAlias target_properties =
PropertyTreeState::Uninitialized();
if (root_and_target.target->GetPropertyContainer(nullptr,
&target_properties)) {
cached_rects->clip_tree = target_properties.Clip().ToTreeString();
cached_rects->transform_tree =
target_properties.Transform().ToTreeString();
cached_rects->scroll_tree = target_properties.Transform()
.Unalias()
.NearestScrollTranslationNode()
.ScrollNode()
->ToTreeString();
} else {
cached_rects->clip_tree = cached_rects->transform_tree =
cached_rects->scroll_tree = "No properties";
}
cached_rects->computed_min_scroll_delta_to_update =
cached_rects->min_scroll_delta_to_update;
cached_rects->local_root_rect = root_geometry.local_root_rect;
cached_rects->root_rect = root_rect_;
cached_rects->target_rect = target_rect_;
cached_rects->intersection_rect = intersection_rect_;
cached_rects->unclipped_intersection_rect = unclipped_intersection_rect_;
cached_rects->target_to_view_transform = target_to_view_transform;
cached_rects->root_to_view_transform = root_geometry.root_to_view_transform;
cached_rects->relationship = static_cast<int>(root_and_target.relationship);
cached_rects->root_scrolls_target = root_and_target.root_scrolls_target;
#endif
}
// This must be the last step after all calculations in zoomed coordinates.
if (flags_ & kShouldConvertToCSSPixels) {
AdjustForAbsoluteZoom::AdjustRectMaybeExcludingCSSZoom(target_rect_,
*target);
AdjustForAbsoluteZoom::AdjustRectMaybeExcludingCSSZoom(intersection_rect_,
*target);
AdjustForAbsoluteZoom::AdjustRectMaybeExcludingCSSZoom(root_rect_, *root);
}
}
bool IntersectionGeometry::ClipToRoot(const RootAndTarget& root_and_target,
const gfx::RectF& root_rect,
gfx::RectF& unclipped_intersection_rect,
gfx::RectF& intersection_rect,
const Vector<Length>& scroll_margin,
CachedRects* cached_rects) {
const LayoutObject* root = root_and_target.root;
// TODO(crbug.com/1456208): Support inline root.
if (!root->IsBox()) {
return false;
}
const LayoutObject* target = root_and_target.target;
const LayoutBox* local_ancestor = nullptr;
bool ignore_local_clip_path = false;
if (!scroll_margin.empty()) {
// Apply clip and scroll margin for each intermediate scroller.
for (const LayoutBox* scroller : root_and_target.intermediate_scrollers) {
gfx::RectF scroller_rect =
gfx::RectF(scroller->OverflowClipRect(PhysicalOffset()));
if (std::optional<gfx::RectF> clip_path_box =
ClipPathClipper::LocalClipPathBoundingBox(*scroller)) {
scroller_rect.Intersect(*clip_path_box);
}
local_ancestor = To<LayoutBox>(scroller);
if (!ApplyClip(target, local_ancestor, scroller, scroller_rect,
unclipped_intersection_rect, intersection_rect,
scroll_margin, ignore_local_clip_path,
/*root_scrolls_target=*/true, cached_rects)) {
return false;
}
unclipped_intersection_rect = intersection_rect;
target = scroller;
// We have already applied clip-path on scroller (now target) above, so
// we don't need to apply clip-path on target in the next ApplyClip().
ignore_local_clip_path = true;
}
}
// Map and clip rect into root element coordinates.
if (!RootIsImplicit() ||
root->GetDocument().GetFrame()->IsOutermostMainFrame()) {
local_ancestor = To<LayoutBox>(root);
}
return ApplyClip(target, local_ancestor, root_and_target.root, root_rect,
unclipped_intersection_rect, intersection_rect,
scroll_margin, ignore_local_clip_path,
root_and_target.root_scrolls_target, cached_rects);
}
bool IntersectionGeometry::ApplyClip(const LayoutObject* target,
const LayoutBox* local_ancestor,
const LayoutObject* root,
const gfx::RectF& root_rect,
gfx::RectF& unclipped_intersection_rect,
gfx::RectF& intersection_rect,
const Vector<Length>& scroll_margin,
bool ignore_local_clip_path,
bool root_scrolls_target,
CachedRects* cached_rects) {
unsigned flags = kDefaultVisualRectFlags | kEdgeInclusive |
kDontApplyMainFrameOverflowClip;
if (!ShouldRespectFilters()) {
flags |= kIgnoreFilters;
}
if (CanUseGeometryMapper(*target)) {
flags |= kUseGeometryMapper;
}
if (ignore_local_clip_path) {
flags |= kIgnoreLocalClipPath;
}
bool does_intersect = false;
if (ShouldUseCachedRects()) {
does_intersect = cached_rects->does_intersect;
} else {
does_intersect = target->MapToVisualRectInAncestorSpace(
local_ancestor, unclipped_intersection_rect,
static_cast<VisualRectFlags>(flags));
if (RuntimeEnabledFeatures::IntersectionOptimizationEnabled() &&
local_ancestor && local_ancestor->IsScrollContainer() &&
!root_scrolls_target) {
// Convert the rect from the scrolling contents space to the border box
// space, so that we can use cached rects and avoid update on scroll of
// root.
ScrollingContentsToBorderBoxSpace(local_ancestor,
unclipped_intersection_rect);
}
}
if (cached_rects) {
cached_rects->unscrolled_unclipped_intersection_rect =
unclipped_intersection_rect;
cached_rects->does_intersect = does_intersect;
}
intersection_rect = gfx::RectF();
// If the target intersects with the unclipped root, calculate the clipped
// intersection.
if (does_intersect) {
if (local_ancestor) {
if (root_scrolls_target) {
ScrollingContentsToBorderBoxSpace(local_ancestor,
unclipped_intersection_rect);
} else {
// In case the ancestor in an SVG element with a viewbox property
// we need to convert the child's coordinates to the SVG coordinates
if (auto* properties =
local_ancestor->FirstFragment().PaintProperties()) {
if (auto* replaced_transform =
properties->ReplacedContentTransform()) {
gfx::Transform invert_replaced_transform =
GeometryMapper::SourceToDestinationProjection(
*replaced_transform, *replaced_transform->Parent());
unclipped_intersection_rect =
invert_replaced_transform.MapRect(unclipped_intersection_rect);
}
}
}
gfx::RectF root_clip_rect = root_rect;
if (!scroll_margin.empty() && root->IsScrollContainer()) {
// If the root is scrollable, apply the scroll margin to inflate the
// root_clip_rect.
ApplyMargin(root_clip_rect, scroll_margin,
root->StyleRef().EffectiveZoom(), root_clip_rect.size());
}
intersection_rect = unclipped_intersection_rect;
does_intersect &= intersection_rect.InclusiveIntersect(root_clip_rect);
} else {
// Note that we don't clip to root_rect here. That's ok because
// (!local_ancestor) implies that the root is implicit and the
// main frame is remote, in which case there can't be any root margin
// applied to root_rect (root margin is disallowed for implicit-root
// cross-origin observation). We still need to apply the remote main
// frame's overflow clip here, because the
// kDontApplyMainFrameOverflowClip flag above, means it hasn't been
// done yet.
LocalFrame* local_root_frame = root->GetDocument().GetFrame();
gfx::Rect clip_rect(local_root_frame->RemoteViewportIntersection());
if (clip_rect.IsEmpty()) {
intersection_rect = gfx::RectF();
does_intersect = false;
} else {
// Map clip_rect from the coordinate system of the local root frame to
// the coordinate system of the remote main frame.
clip_rect = ToPixelSnappedRect(
local_root_frame->ContentLayoutObject()->LocalToAncestorRect(
PhysicalRect(clip_rect), nullptr,
kTraverseDocumentBoundaries | kApplyRemoteMainFrameTransform));
intersection_rect = unclipped_intersection_rect;
does_intersect &=
intersection_rect.InclusiveIntersect(gfx::RectF(clip_rect));
}
}
}
return does_intersect;
}
wtf_size_t IntersectionGeometry::FirstThresholdGreaterThan(
float ratio,
const Vector<float>& thresholds) const {
wtf_size_t result = 0;
while (result < thresholds.size() && thresholds[result] <= ratio)
++result;
return result;
}
gfx::Vector2dF IntersectionGeometry::ComputeMinScrollDeltaToUpdate(
const RootAndTarget& root_and_target,
const gfx::Transform& target_to_view_transform,
const gfx::Transform& root_to_view_transform,
const Vector<float>& thresholds,
const Vector<Length>& scroll_margin) const {
if (!RuntimeEnabledFeatures::IntersectionOptimizationEnabled()) {
return gfx::Vector2dF();
}
if (!scroll_margin.empty()) {
return gfx::Vector2dF();
}
if (ShouldComputeVisibility()) {
// We don't have enough data (e.g. the occluded area of target and the
// occluding areas of the covering elements) to calculate the minimum
// scroll delta affecting visibility.
return gfx::Vector2dF();
}
if (root_and_target.relationship == RootAndTarget::kTargetInSubFrame) {
return gfx::Vector2dF();
}
if (root_and_target.relationship == RootAndTarget::kNotScrollable) {
// Intersection is not affected by scroll.
return kInfiniteScrollDelta;
}
if (root_and_target.has_filter && ShouldRespectFilters()) {
// With filters, the intersection rect can be non-empty even if root_rect_
// and target_rect_ don't intersect.
return gfx::Vector2dF();
}
if (!target_to_view_transform.IsIdentityOr2dTranslation() ||
!root_to_view_transform.IsIdentityOr2dTranslation()) {
return gfx::Vector2dF();
}
CHECK_GE(thresholds.size(), 1u);
if (thresholds[0] == 1) {
if (ShouldTrackFractionOfRoot()) {
if (root_rect_.width() > target_rect_.width() ||
root_rect_.height() > target_rect_.height()) {
// The intersection rect (which is contained by target_rect_) can never
// cover root_rect_ 100%.
return kInfiniteScrollDelta;
}
if (target_rect_.Contains(root_rect_) &&
root_and_target.relationship ==
RootAndTarget::kHasIntermediateClippers) {
// When target_rect_ fully contains root_rect_, whether the intersection
// rect fully covers root_rect_ depends on intermediate clips, so there
// is no minimum scroll delta.
return gfx::Vector2dF();
}
} else {
if (target_rect_.width() > root_rect_.width() ||
target_rect_.height() > root_rect_.height()) {
// The intersection rect (which is contained by root_rect_) can never
// cover target_rect_ 100%.
return kInfiniteScrollDelta;
}
if (root_rect_.Contains(target_rect_) &&
root_and_target.relationship ==
RootAndTarget::kHasIntermediateClippers) {
// When root_rect_ fully contains target_rect_, whether target_rect_
// is fully visible depends on intermediate clips, so there is no
// minimum scroll delta.
return gfx::Vector2dF();
}
}
// Otherwise, we can skip update until target_rect_/root_rect_ is or isn't
// fully contained by root_rect_/target_rect_.
return gfx::Vector2dF(
std::min(std::abs(root_rect_.x() - target_rect_.x()),
std::abs(root_rect_.right() - target_rect_.right())),
std::min(std::abs(root_rect_.y() - target_rect_.y()),
std::abs(root_rect_.bottom() - target_rect_.bottom())));
}
// Otherwise, if root_rect_ and target_rect_ intersect, the intersection
// status may change on any scroll in case of intermediate clips or non-zero
// thresholds. kMinimumThreshold equivalent to 0 for minimum scroll delta.
gfx::RectF root_target_intersection_rect = root_rect_;
bool inclusively_intersects =
root_target_intersection_rect.InclusiveIntersect(target_rect_);
if (inclusively_intersects &&
(thresholds.size() != 1 || thresholds[0] > kMinimumThreshold ||
root_and_target.relationship ==
RootAndTarget::kHasIntermediateClippers ||
IsForFrameViewportIntersection())) {
return gfx::Vector2dF();
}
// Otherwise we can skip update until root_rect_ and target_rect_ is about
// to change intersection status in either direction.
return gfx::Vector2dF(
std::min(std::abs(root_rect_.right() - target_rect_.x()),
std::abs(target_rect_.right() - root_rect_.x())),
std::min(std::abs(root_rect_.bottom() - target_rect_.y()),
std::abs(target_rect_.bottom() - root_rect_.y())));
}
} // namespace blink