blob: 4783c5d595054d42af62fc3e8c840d06ee70c23f [file] [log] [blame]
// Copyright 2016 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/intersection_observer/intersection_geometry.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/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_view.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/paint/paint_layer.h"
namespace blink {
namespace {
bool IsContainingBlockChainDescendant(LayoutObject* descendant,
LayoutObject* ancestor) {
LocalFrame* ancestor_frame = ancestor->GetDocument().GetFrame();
LocalFrame* descendant_frame = descendant->GetDocument().GetFrame();
if (ancestor_frame != descendant_frame)
return false;
while (descendant && descendant != ancestor)
descendant = descendant->ContainingBlock();
return descendant;
}
void MapRectUpToDocument(LayoutRect& rect, const LayoutObject& descendant) {
FloatQuad mapped_quad =
descendant.LocalToAncestorQuad(FloatQuad(FloatRect(rect)), nullptr,
kUseTransforms | kApplyContainerFlip);
rect = LayoutRect(mapped_quad.BoundingBox());
}
void MapRectDownToDocument(LayoutRect& rect,
const Document& document) {
FloatQuad mapped_quad = document.GetLayoutView()->AncestorToLocalQuad(
nullptr, FloatQuad(FloatRect(rect)),
kUseTransforms | kApplyContainerFlip | kTraverseDocumentBoundaries);
rect = LayoutRect(mapped_quad.BoundingBox());
}
LayoutUnit ComputeMargin(const Length& length, LayoutUnit reference_length) {
if (length.IsPercent()) {
return LayoutUnit(static_cast<int>(reference_length.ToFloat() *
length.Percent() / 100.0));
}
DCHECK(length.IsFixed());
return LayoutUnit(length.IntValue());
}
LayoutView* LocalRootView(Element& element) {
LocalFrame* frame = element.GetDocument().GetFrame();
LocalFrame* frame_root = frame ? &frame->LocalFrameRoot() : nullptr;
return frame_root ? frame_root->ContentLayoutObject() : nullptr;
}
bool ComputeIsVisible(LayoutObject* target, const LayoutRect& rect) {
DCHECK(RuntimeEnabledFeatures::IntersectionObserverV2Enabled());
if (target->GetDocument().GetFrame()->LocalFrameRoot().GetOcclusionState() !=
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));
return (!result.InnerNode() || result.InnerNode() == target->GetNode());
}
static const unsigned kConstructorFlagsMask =
IntersectionGeometry::kShouldReportRootBounds |
IntersectionGeometry::kShouldComputeVisibility |
IntersectionGeometry::kShouldTrackFractionOfRoot |
IntersectionGeometry::kShouldUseReplacedContentRect |
IntersectionGeometry::kShouldConvertToCSSPixels;
} // namespace
IntersectionGeometry::IntersectionGeometry(Element* root_element,
Element& target_element,
const Vector<Length>& root_margin,
const Vector<float>& thresholds,
unsigned flags)
: flags_(flags & kConstructorFlagsMask),
intersection_ratio_(0),
threshold_index_(0) {
DCHECK(root_margin.IsEmpty() || root_margin.size() == 4);
ComputeGeometry(root_element, target_element, root_margin, thresholds);
}
IntersectionGeometry::~IntersectionGeometry() = default;
void IntersectionGeometry::ComputeGeometry(Element* root_element,
Element& target_element,
const Vector<Length>& root_margin,
const Vector<float>& thresholds) {
LayoutObject* target = target_element.GetLayoutObject();
LayoutObject* root;
if (root_element) {
root = root_element->GetLayoutObject();
flags_ &= ~kRootIsImplicit;
} else {
root = LocalRootView(target_element);
flags_ |= kRootIsImplicit;
}
if (!target_element.isConnected())
return;
if (root_element && !root_element->isConnected())
return;
if (!root || !root->IsBox())
return;
if (!target || (!target->IsBoxModelObject() && !target->IsText()))
return;
if (root_element && !IsContainingBlockChainDescendant(target, root))
return;
DCHECK(!target_element.GetDocument().View()->NeedsLayout());
target_rect_ = InitializeTargetRect(target);
intersection_rect_ = target_rect_;
root_rect_ = InitializeRootRect(root, root_margin);
bool does_intersect =
ClipToRoot(root, target, root_rect_, intersection_rect_);
MapRectUpToDocument(target_rect_, *target);
if (does_intersect) {
if (RootIsImplicit())
MapRectDownToDocument(intersection_rect_, target->GetDocument());
else
MapRectUpToDocument(intersection_rect_, *root);
} else {
intersection_rect_ = LayoutRect();
}
MapRectUpToDocument(root_rect_, *root);
// 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 LayoutRect comparison_rect =
ShouldTrackFractionOfRoot() ? root_rect_ : target_rect_;
if (comparison_rect.IsEmpty()) {
intersection_ratio_ = 1;
} else {
const LayoutSize& intersection_size = intersection_rect_.Size();
const float intersection_area = intersection_size.Width().ToFloat() *
intersection_size.Height().ToFloat();
const LayoutSize& comparison_size = comparison_rect.Size();
const float area_of_interest = comparison_size.Width().ToFloat() *
comparison_size.Height().ToFloat();
intersection_ratio_ = intersection_area / area_of_interest;
}
threshold_index_ =
FirstThresholdGreaterThan(intersection_ratio_, thresholds);
} else {
intersection_ratio_ = 0;
threshold_index_ = 0;
}
if (IsIntersecting() && ShouldComputeVisibility() &&
ComputeIsVisible(target, target_rect_))
flags_ |= kIsVisible;
if (flags_ & kShouldConvertToCSSPixels) {
FloatRect target_float_rect(target_rect_);
AdjustForAbsoluteZoom::AdjustFloatRect(target_float_rect, *target);
target_rect_ = LayoutRect(target_float_rect);
FloatRect intersection_float_rect(intersection_rect_);
AdjustForAbsoluteZoom::AdjustFloatRect(intersection_float_rect, *target);
intersection_rect_ = LayoutRect(intersection_float_rect);
FloatRect root_float_rect(root_rect_);
AdjustForAbsoluteZoom::AdjustFloatRect(root_float_rect, *root);
root_rect_ = LayoutRect(root_float_rect);
}
}
LayoutRect IntersectionGeometry::InitializeTargetRect(LayoutObject* target) {
if ((flags_ & kShouldUseReplacedContentRect) &&
target->IsLayoutEmbeddedContent()) {
return ToLayoutEmbeddedContent(target)->ReplacedContentRect();
}
if (target->IsBox())
return LayoutRect(ToLayoutBoxModelObject(target)->BorderBoundingBox());
if (target->IsLayoutInline())
return ToLayoutInline(target)->LinesBoundingBox();
return ToLayoutText(target)->LinesBoundingBox();
}
LayoutRect IntersectionGeometry::InitializeRootRect(
LayoutObject* root,
const Vector<Length>& margin) {
LayoutRect result;
if (root->IsLayoutView() && root->GetDocument().IsInMainFrame()) {
// 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 = ToLayoutView(root)->OverflowClipRect(LayoutPoint());
} else if (root->IsBox() && root->HasOverflowClip()) {
result = LayoutRect(ToLayoutBox(root)->PhysicalContentBoxRect());
} else {
result = LayoutRect(ToLayoutBoxModelObject(root)->BorderBoundingBox());
}
ApplyRootMargin(result, margin);
return result;
}
void IntersectionGeometry::ApplyRootMargin(LayoutRect& rect,
const Vector<Length>& margin) {
if (margin.IsEmpty())
return;
// TODO(szager): Make sure the spec is clear that left/right margins are
// resolved against width and not height.
LayoutUnit top_margin = ComputeMargin(margin[0], rect.Height());
LayoutUnit right_margin = ComputeMargin(margin[1], rect.Width());
LayoutUnit bottom_margin = ComputeMargin(margin[2], rect.Height());
LayoutUnit left_margin = ComputeMargin(margin[3], rect.Width());
rect.SetX(rect.X() - left_margin);
rect.SetWidth(rect.Width() + left_margin + right_margin);
rect.SetY(rect.Y() - top_margin);
rect.SetHeight(rect.Height() + top_margin + bottom_margin);
}
bool IntersectionGeometry::ClipToRoot(LayoutObject* root,
LayoutObject* target,
const LayoutRect& root_rect,
LayoutRect& intersection_rect) {
// Map and clip rect into root element coordinates.
// TODO(szager): the writing mode flipping needs a test.
LayoutBox* local_ancestor = nullptr;
if (!RootIsImplicit() || root->GetDocument().IsInMainFrame())
local_ancestor = ToLayoutBox(root);
LayoutView* layout_view = target->GetDocument().GetLayoutView();
unsigned flags = kDefaultVisualRectFlags | kEdgeInclusive;
if (!layout_view->NeedsPaintPropertyUpdate() &&
!layout_view->DescendantNeedsPaintPropertyUpdate()) {
flags |= kUseGeometryMapper;
}
bool does_intersect = target->MapToVisualRectInAncestorSpace(
local_ancestor, intersection_rect, static_cast<VisualRectFlags>(flags));
if (!does_intersect || !local_ancestor)
return does_intersect;
if (local_ancestor->HasOverflowClip())
intersection_rect.Move(-local_ancestor->ScrolledContentOffset());
LayoutRect root_clip_rect(root_rect);
local_ancestor->FlipForWritingMode(root_clip_rect);
return does_intersect & intersection_rect.InclusiveIntersect(root_clip_rect);
}
unsigned IntersectionGeometry::FirstThresholdGreaterThan(
float ratio,
const Vector<float>& thresholds) const {
unsigned result = 0;
while (result < thresholds.size() && thresholds[result] <= ratio)
++result;
return result;
}
} // namespace blink