| // 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 |