blob: 7b126fbe90ba6cd44a851bee15009594d9a1dfe8 [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 "core/dom/IntersectionObservation.h"
#include "core/dom/ElementRareData.h"
#include "core/dom/IntersectionObserver.h"
#include "core/frame/FrameView.h"
#include "core/layout/LayoutBox.h"
#include "core/layout/LayoutPart.h"
#include "core/layout/LayoutText.h"
#include "core/layout/LayoutView.h"
#include "core/paint/PaintLayer.h"
namespace blink {
IntersectionObservation::IntersectionObservation(IntersectionObserver& observer, Element& target, bool shouldReportRootBounds)
: m_observer(observer)
, m_target(target.ensureIntersectionObserverData().createWeakPtr(&target))
, m_shouldReportRootBounds(shouldReportRootBounds)
, m_lastThresholdIndex(0)
{
}
Element* IntersectionObservation::target() const
{
return toElement(m_target.get());
}
void IntersectionObservation::applyRootMargin(LayoutRect& rect) const
{
if (m_shouldReportRootBounds)
m_observer->applyRootMargin(rect);
}
void IntersectionObservation::initializeGeometry(IntersectionGeometry& geometry) const
{
initializeTargetRect(geometry.targetRect);
geometry.intersectionRect = geometry.targetRect;
initializeRootRect(geometry.rootRect);
geometry.doesIntersect = true;
}
void IntersectionObservation::initializeTargetRect(LayoutRect& rect) const
{
ASSERT(m_target);
LayoutObject* targetLayoutObject = target()->layoutObject();
ASSERT(targetLayoutObject && targetLayoutObject->isBoxModelObject());
rect = toLayoutBoxModelObject(targetLayoutObject)->visualOverflowRect();
}
void IntersectionObservation::initializeRootRect(LayoutRect& rect) const
{
LayoutObject* rootLayoutObject = m_observer->rootLayoutObject();
if (rootLayoutObject->isLayoutView())
rect = LayoutRect(toLayoutView(rootLayoutObject)->frameView()->visibleContentRect());
// TODO(szager): Obey the spec -- use content box for a scrolling element, border box otherwise.
else if (rootLayoutObject->isBox())
rect = LayoutRect(toLayoutBox(rootLayoutObject)->contentBoxRect());
else
rect = LayoutRect(toLayoutBoxModelObject(rootLayoutObject)->borderBoundingBox());
applyRootMargin(rect);
}
void IntersectionObservation::clipToRoot(IntersectionGeometry& geometry) const
{
// Map and clip rect into root element coordinates.
// TODO(szager): the writing mode flipping needs a test.
ASSERT(m_target);
LayoutObject* rootLayoutObject = m_observer->rootLayoutObject();
LayoutObject* targetLayoutObject = target()->layoutObject();
geometry.doesIntersect = targetLayoutObject->mapToVisibleRectInAncestorSpace(toLayoutBoxModelObject(rootLayoutObject), geometry.intersectionRect, nullptr, EdgeInclusive);
if (!geometry.doesIntersect)
return;
LayoutRect rootClipRect(geometry.rootRect);
toLayoutBox(rootLayoutObject)->flipForWritingMode(rootClipRect);
geometry.doesIntersect &= geometry.intersectionRect.inclusiveIntersect(rootClipRect);
}
static void mapRectUpToDocument(LayoutRect& rect, const LayoutObject& layoutObject, const Document& document)
{
FloatQuad mappedQuad = layoutObject.localToAbsoluteQuad(
FloatQuad(FloatRect(rect)),
UseTransforms | ApplyContainerFlip);
rect = LayoutRect(mappedQuad.boundingBox());
}
static void mapRectDownToDocument(LayoutRect& rect, LayoutBoxModelObject& layoutObject, const Document& document)
{
FloatQuad mappedQuad = document.layoutView()->ancestorToLocalQuad(
&layoutObject,
FloatQuad(FloatRect(rect)),
UseTransforms | ApplyContainerFlip | TraverseDocumentBoundaries);
rect = LayoutRect(mappedQuad.boundingBox());
}
void IntersectionObservation::mapTargetRectToTargetFrameCoordinates(LayoutRect& rect) const
{
LayoutObject& targetLayoutObject = *target()->layoutObject();
Document& targetDocument = target()->document();
LayoutSize scrollPosition = LayoutSize(toIntSize(targetDocument.view()->scrollPosition()));
mapRectUpToDocument(rect, targetLayoutObject, targetDocument);
rect.move(-scrollPosition);
}
void IntersectionObservation::mapRootRectToRootFrameCoordinates(LayoutRect& rect) const
{
LayoutObject& rootLayoutObject = *m_observer->rootLayoutObject();
Document& rootDocument = rootLayoutObject.document();
LayoutSize scrollPosition = LayoutSize(toIntSize(rootDocument.view()->scrollPosition()));
mapRectUpToDocument(rect, rootLayoutObject, rootLayoutObject.document());
rect.move(-scrollPosition);
}
void IntersectionObservation::mapRootRectToTargetFrameCoordinates(LayoutRect& rect) const
{
LayoutObject& rootLayoutObject = *m_observer->rootLayoutObject();
Document& targetDocument = target()->document();
LayoutSize scrollPosition = LayoutSize(toIntSize(targetDocument.view()->scrollPosition()));
if (&targetDocument == &rootLayoutObject.document())
mapRectUpToDocument(rect, rootLayoutObject, targetDocument);
else
mapRectDownToDocument(rect, toLayoutBoxModelObject(rootLayoutObject), targetDocument);
rect.move(-scrollPosition);
}
static bool isContainingBlockChainDescendant(LayoutObject* descendant, LayoutObject* ancestor)
{
LocalFrame* ancestorFrame = ancestor->document().frame();
LocalFrame* descendantFrame = descendant->document().frame();
if (ancestor->isLayoutView())
return descendantFrame && descendantFrame->tree().top() == ancestorFrame;
if (ancestorFrame != descendantFrame)
return false;
while (descendant && descendant != ancestor)
descendant = descendant->containingBlock();
return descendant;
}
bool IntersectionObservation::computeGeometry(IntersectionGeometry& geometry) const
{
// Pre-oilpan, there will be a delay between the time when the target Element gets deleted
// (because its ref count dropped to zero) and when this IntersectionObservation gets
// deleted (during the next gc run, because the target Element is the only thing keeping
// the IntersectionObservation alive). During that interval, we need to check that m_target
// hasn't been cleared.
Element* targetElement = target();
if (!targetElement || !targetElement->inDocument())
return false;
LayoutObject* targetLayoutObject = targetElement->layoutObject();
ASSERT(m_observer);
LayoutObject* rootLayoutObject = m_observer->rootLayoutObject();
// TODO(szager): Support SVG
if (!targetLayoutObject)
return false;
if (!targetLayoutObject->isBoxModelObject() && !targetLayoutObject->isText())
return false;
if (!rootLayoutObject || !rootLayoutObject->isBoxModelObject())
return false;
if (!isContainingBlockChainDescendant(targetLayoutObject, rootLayoutObject))
return false;
initializeGeometry(geometry);
clipToRoot(geometry);
mapTargetRectToTargetFrameCoordinates(geometry.targetRect);
if (geometry.doesIntersect)
mapRootRectToTargetFrameCoordinates(geometry.intersectionRect);
else
geometry.intersectionRect = LayoutRect();
// Small optimization: if we're not going to report root bounds, don't bother
// transforming them to the frame.
if (m_shouldReportRootBounds)
mapRootRectToRootFrameCoordinates(geometry.rootRect);
return true;
}
void IntersectionObservation::computeIntersectionObservations(DOMHighResTimeStamp timestamp)
{
IntersectionGeometry geometry;
if (!computeGeometry(geometry))
return;
// 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.
unsigned newThresholdIndex;
if (geometry.targetRect.isEmpty()) {
newThresholdIndex = geometry.doesIntersect ? 1 : 0;
} else if (!geometry.doesIntersect) {
newThresholdIndex = 0;
} else {
float intersectionArea = geometry.intersectionRect.size().width().toFloat() * geometry.intersectionRect.size().height().toFloat();
float targetArea = geometry.targetRect.size().width().toFloat() * geometry.targetRect.size().height().toFloat();
float newVisibleRatio = intersectionArea / targetArea;
newThresholdIndex = observer().firstThresholdGreaterThan(newVisibleRatio);
}
if (m_lastThresholdIndex != newThresholdIndex) {
IntRect snappedRootBounds = pixelSnappedIntRect(geometry.rootRect);
IntRect* rootBoundsPointer = m_shouldReportRootBounds ? &snappedRootBounds : nullptr;
IntersectionObserverEntry* newEntry = new IntersectionObserverEntry(
timestamp,
pixelSnappedIntRect(geometry.targetRect),
rootBoundsPointer,
pixelSnappedIntRect(geometry.intersectionRect),
target());
observer().enqueueIntersectionObserverEntry(*newEntry);
}
setLastThresholdIndex(newThresholdIndex);
}
void IntersectionObservation::disconnect()
{
IntersectionObserver* observer = m_observer;
clearRootAndRemoveFromTarget();
observer->removeObservation(*this);
}
void IntersectionObservation::clearRootAndRemoveFromTarget()
{
if (m_target)
target()->ensureIntersectionObserverData().removeObservation(observer());
m_observer.clear();
}
DEFINE_TRACE(IntersectionObservation)
{
visitor->trace(m_observer);
visitor->trace(m_target);
}
} // namespace blink