forked from mirrors/gecko-dev
		
	 e7e16e453b
			
		
	
	
		e7e16e453b
		
	
	
	
	
		
			
			Rewrite implementation of content-visibility: auto as defined in https://github.com/w3c/csswg-drafts/issues/8542 Differential Revision: https://phabricator.services.mozilla.com/D170394
		
			
				
	
	
		
			821 lines
		
	
	
	
		
			31 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			821 lines
		
	
	
	
		
			31 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 | ||
| /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | ||
|  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 | ||
|  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 | ||
| 
 | ||
| #include "DOMIntersectionObserver.h"
 | ||
| #include "nsCSSPropertyID.h"
 | ||
| #include "nsIFrame.h"
 | ||
| #include "nsContainerFrame.h"
 | ||
| #include "nsIScrollableFrame.h"
 | ||
| #include "nsContentUtils.h"
 | ||
| #include "nsLayoutUtils.h"
 | ||
| #include "nsRefreshDriver.h"
 | ||
| #include "mozilla/PresShell.h"
 | ||
| #include "mozilla/StaticPrefs_dom.h"
 | ||
| #include "mozilla/StaticPrefs_layout.h"
 | ||
| #include "mozilla/ServoBindings.h"
 | ||
| #include "mozilla/dom/BrowserChild.h"
 | ||
| #include "mozilla/dom/BrowsingContext.h"
 | ||
| #include "mozilla/dom/DocumentInlines.h"
 | ||
| #include "mozilla/dom/HTMLImageElement.h"
 | ||
| #include "mozilla/dom/HTMLIFrameElement.h"
 | ||
| #include "Units.h"
 | ||
| 
 | ||
| namespace mozilla::dom {
 | ||
| 
 | ||
| NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry)
 | ||
|   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
 | ||
|   NS_INTERFACE_MAP_ENTRY(nsISupports)
 | ||
| NS_INTERFACE_MAP_END
 | ||
| 
 | ||
| NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserverEntry)
 | ||
| NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserverEntry)
 | ||
| 
 | ||
| NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DOMIntersectionObserverEntry, mOwner,
 | ||
|                                       mRootBounds, mBoundingClientRect,
 | ||
|                                       mIntersectionRect, mTarget)
 | ||
| 
 | ||
| NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserver)
 | ||
|   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
 | ||
|   NS_INTERFACE_MAP_ENTRY(nsISupports)
 | ||
|   NS_INTERFACE_MAP_ENTRY(DOMIntersectionObserver)
 | ||
| NS_INTERFACE_MAP_END
 | ||
| 
 | ||
| NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserver)
 | ||
| NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserver)
 | ||
| 
 | ||
| NS_IMPL_CYCLE_COLLECTION_CLASS(DOMIntersectionObserver)
 | ||
| 
 | ||
| NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(DOMIntersectionObserver)
 | ||
|   NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
 | ||
| NS_IMPL_CYCLE_COLLECTION_TRACE_END
 | ||
| 
 | ||
| NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMIntersectionObserver)
 | ||
|   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
 | ||
|   tmp->Disconnect();
 | ||
|   NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
 | ||
|   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
 | ||
|   if (tmp->mCallback.is<RefPtr<dom::IntersectionCallback>>()) {
 | ||
|     ImplCycleCollectionUnlink(
 | ||
|         tmp->mCallback.as<RefPtr<dom::IntersectionCallback>>());
 | ||
|   }
 | ||
|   NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoot)
 | ||
|   NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries)
 | ||
| NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 | ||
| 
 | ||
| NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMIntersectionObserver)
 | ||
|   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
 | ||
|   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
 | ||
|   if (tmp->mCallback.is<RefPtr<dom::IntersectionCallback>>()) {
 | ||
|     ImplCycleCollectionTraverse(
 | ||
|         cb, tmp->mCallback.as<RefPtr<dom::IntersectionCallback>>(), "mCallback",
 | ||
|         0);
 | ||
|   }
 | ||
|   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
 | ||
|   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries)
 | ||
| NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 | ||
| 
 | ||
| DOMIntersectionObserver::DOMIntersectionObserver(
 | ||
|     already_AddRefed<nsPIDOMWindowInner>&& aOwner,
 | ||
|     dom::IntersectionCallback& aCb)
 | ||
|     : mOwner(aOwner),
 | ||
|       mDocument(mOwner->GetExtantDoc()),
 | ||
|       mCallback(RefPtr<dom::IntersectionCallback>(&aCb)) {}
 | ||
| 
 | ||
| already_AddRefed<DOMIntersectionObserver> DOMIntersectionObserver::Constructor(
 | ||
|     const GlobalObject& aGlobal, dom::IntersectionCallback& aCb,
 | ||
|     ErrorResult& aRv) {
 | ||
|   return Constructor(aGlobal, aCb, IntersectionObserverInit(), aRv);
 | ||
| }
 | ||
| 
 | ||
| already_AddRefed<DOMIntersectionObserver> DOMIntersectionObserver::Constructor(
 | ||
|     const GlobalObject& aGlobal, dom::IntersectionCallback& aCb,
 | ||
|     const IntersectionObserverInit& aOptions, ErrorResult& aRv) {
 | ||
|   nsCOMPtr<nsPIDOMWindowInner> window =
 | ||
|       do_QueryInterface(aGlobal.GetAsSupports());
 | ||
|   if (!window) {
 | ||
|     aRv.Throw(NS_ERROR_FAILURE);
 | ||
|     return nullptr;
 | ||
|   }
 | ||
|   RefPtr<DOMIntersectionObserver> observer =
 | ||
|       new DOMIntersectionObserver(window.forget(), aCb);
 | ||
| 
 | ||
|   if (!aOptions.mRoot.IsNull()) {
 | ||
|     if (aOptions.mRoot.Value().IsElement()) {
 | ||
|       observer->mRoot = aOptions.mRoot.Value().GetAsElement();
 | ||
|     } else {
 | ||
|       MOZ_ASSERT(aOptions.mRoot.Value().IsDocument());
 | ||
|       observer->mRoot = aOptions.mRoot.Value().GetAsDocument();
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   if (!observer->SetRootMargin(aOptions.mRootMargin)) {
 | ||
|     aRv.ThrowSyntaxError("rootMargin must be specified in pixels or percent.");
 | ||
|     return nullptr;
 | ||
|   }
 | ||
| 
 | ||
|   if (aOptions.mThreshold.IsDoubleSequence()) {
 | ||
|     const Sequence<double>& thresholds =
 | ||
|         aOptions.mThreshold.GetAsDoubleSequence();
 | ||
|     observer->mThresholds.SetCapacity(thresholds.Length());
 | ||
|     for (const auto& thresh : thresholds) {
 | ||
|       if (thresh < 0.0 || thresh > 1.0) {
 | ||
|         aRv.ThrowRangeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
 | ||
|         return nullptr;
 | ||
|       }
 | ||
|       observer->mThresholds.AppendElement(thresh);
 | ||
|     }
 | ||
|     observer->mThresholds.Sort();
 | ||
|     if (observer->mThresholds.IsEmpty()) {
 | ||
|       observer->mThresholds.AppendElement(0.0);
 | ||
|     }
 | ||
|   } else {
 | ||
|     double thresh = aOptions.mThreshold.GetAsDouble();
 | ||
|     if (thresh < 0.0 || thresh > 1.0) {
 | ||
|       aRv.ThrowRangeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
 | ||
|       return nullptr;
 | ||
|     }
 | ||
|     observer->mThresholds.AppendElement(thresh);
 | ||
|   }
 | ||
| 
 | ||
|   return observer.forget();
 | ||
| }
 | ||
| 
 | ||
| static void LazyLoadCallback(
 | ||
|     const Sequence<OwningNonNull<DOMIntersectionObserverEntry>>& aEntries) {
 | ||
|   for (const auto& entry : aEntries) {
 | ||
|     Element* target = entry->Target();
 | ||
|     if (entry->IsIntersecting()) {
 | ||
|       if (auto* image = HTMLImageElement::FromNode(target)) {
 | ||
|         image->StopLazyLoading(HTMLImageElement::StartLoading::Yes);
 | ||
|       } else if (auto* iframe = HTMLIFrameElement::FromNode(target)) {
 | ||
|         iframe->StopLazyLoading();
 | ||
|       } else {
 | ||
|         MOZ_ASSERT_UNREACHABLE(
 | ||
|             "Only <img> and <iframe> should be observed by lazy load observer");
 | ||
|       }
 | ||
|     }
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| static LengthPercentage PrefMargin(float aValue, bool aIsPercentage) {
 | ||
|   return aIsPercentage ? LengthPercentage::FromPercentage(aValue / 100.0f)
 | ||
|                        : LengthPercentage::FromPixels(aValue);
 | ||
| }
 | ||
| 
 | ||
| DOMIntersectionObserver::DOMIntersectionObserver(Document& aDocument,
 | ||
|                                                  NativeCallback aCallback)
 | ||
|     : mOwner(aDocument.GetInnerWindow()),
 | ||
|       mDocument(&aDocument),
 | ||
|       mCallback(aCallback) {}
 | ||
| 
 | ||
| already_AddRefed<DOMIntersectionObserver>
 | ||
| DOMIntersectionObserver::CreateLazyLoadObserver(Document& aDocument) {
 | ||
|   RefPtr<DOMIntersectionObserver> observer =
 | ||
|       new DOMIntersectionObserver(aDocument, LazyLoadCallback);
 | ||
|   observer->mThresholds.AppendElement(0.0f);
 | ||
| 
 | ||
| #define SET_MARGIN(side_, side_lower_)                                 \
 | ||
|   observer->mRootMargin.Get(eSide##side_) = PrefMargin(                \
 | ||
|       StaticPrefs::dom_image_lazy_loading_root_margin_##side_lower_(), \
 | ||
|       StaticPrefs::                                                    \
 | ||
|           dom_image_lazy_loading_root_margin_##side_lower_##_percentage());
 | ||
|   SET_MARGIN(Top, top);
 | ||
|   SET_MARGIN(Right, right);
 | ||
|   SET_MARGIN(Bottom, bottom);
 | ||
|   SET_MARGIN(Left, left);
 | ||
| #undef SET_MARGIN
 | ||
| 
 | ||
|   return observer.forget();
 | ||
| }
 | ||
| 
 | ||
| bool DOMIntersectionObserver::SetRootMargin(const nsACString& aString) {
 | ||
|   return Servo_IntersectionObserverRootMargin_Parse(&aString, &mRootMargin);
 | ||
| }
 | ||
| 
 | ||
| nsISupports* DOMIntersectionObserver::GetParentObject() const { return mOwner; }
 | ||
| 
 | ||
| void DOMIntersectionObserver::GetRootMargin(nsACString& aRetVal) {
 | ||
|   Servo_IntersectionObserverRootMargin_ToString(&mRootMargin, &aRetVal);
 | ||
| }
 | ||
| 
 | ||
| void DOMIntersectionObserver::GetThresholds(nsTArray<double>& aRetVal) {
 | ||
|   aRetVal = mThresholds.Clone();
 | ||
| }
 | ||
| 
 | ||
| void DOMIntersectionObserver::Observe(Element& aTarget) {
 | ||
|   if (!mObservationTargetSet.EnsureInserted(&aTarget)) {
 | ||
|     return;
 | ||
|   }
 | ||
|   aTarget.RegisterIntersectionObserver(this);
 | ||
|   mObservationTargets.AppendElement(&aTarget);
 | ||
| 
 | ||
|   MOZ_ASSERT(mObservationTargets.Length() == mObservationTargetSet.Count());
 | ||
| 
 | ||
|   Connect();
 | ||
|   if (mDocument) {
 | ||
|     if (nsPresContext* pc = mDocument->GetPresContext()) {
 | ||
|       pc->RefreshDriver()->EnsureIntersectionObservationsUpdateHappens();
 | ||
|     }
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| void DOMIntersectionObserver::Unobserve(Element& aTarget) {
 | ||
|   if (!mObservationTargetSet.EnsureRemoved(&aTarget)) {
 | ||
|     return;
 | ||
|   }
 | ||
| 
 | ||
|   mObservationTargets.RemoveElement(&aTarget);
 | ||
|   aTarget.UnregisterIntersectionObserver(this);
 | ||
| 
 | ||
|   MOZ_ASSERT(mObservationTargets.Length() == mObservationTargetSet.Count());
 | ||
| 
 | ||
|   if (mObservationTargets.IsEmpty()) {
 | ||
|     Disconnect();
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| void DOMIntersectionObserver::UnlinkTarget(Element& aTarget) {
 | ||
|   mObservationTargets.RemoveElement(&aTarget);
 | ||
|   mObservationTargetSet.Remove(&aTarget);
 | ||
|   if (mObservationTargets.IsEmpty()) {
 | ||
|     Disconnect();
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| void DOMIntersectionObserver::Connect() {
 | ||
|   if (mConnected) {
 | ||
|     return;
 | ||
|   }
 | ||
| 
 | ||
|   mConnected = true;
 | ||
|   if (mDocument) {
 | ||
|     mDocument->AddIntersectionObserver(this);
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| void DOMIntersectionObserver::Disconnect() {
 | ||
|   if (!mConnected) {
 | ||
|     return;
 | ||
|   }
 | ||
| 
 | ||
|   mConnected = false;
 | ||
|   for (Element* target : mObservationTargets) {
 | ||
|     target->UnregisterIntersectionObserver(this);
 | ||
|   }
 | ||
|   mObservationTargets.Clear();
 | ||
|   mObservationTargetSet.Clear();
 | ||
|   if (mDocument) {
 | ||
|     mDocument->RemoveIntersectionObserver(this);
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| void DOMIntersectionObserver::TakeRecords(
 | ||
|     nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal) {
 | ||
|   aRetVal = std::move(mQueuedEntries);
 | ||
| }
 | ||
| 
 | ||
| enum class BrowsingContextOrigin { Similar, Different };
 | ||
| 
 | ||
| // NOTE(emilio): Checking docgroup as per discussion in:
 | ||
| // https://github.com/w3c/IntersectionObserver/issues/161
 | ||
| static BrowsingContextOrigin SimilarOrigin(const Element& aTarget,
 | ||
|                                            const nsINode* aRoot) {
 | ||
|   if (!aRoot) {
 | ||
|     return BrowsingContextOrigin::Different;
 | ||
|   }
 | ||
|   return aTarget.OwnerDoc()->GetDocGroup() == aRoot->OwnerDoc()->GetDocGroup()
 | ||
|              ? BrowsingContextOrigin::Similar
 | ||
|              : BrowsingContextOrigin::Different;
 | ||
| }
 | ||
| 
 | ||
| // NOTE: This returns nullptr if |aDocument| is in another process from the top
 | ||
| // level content document.
 | ||
| static const Document* GetTopLevelContentDocumentInThisProcess(
 | ||
|     const Document& aDocument) {
 | ||
|   auto* wc = aDocument.GetTopLevelWindowContext();
 | ||
|   return wc ? wc->GetExtantDoc() : nullptr;
 | ||
| }
 | ||
| 
 | ||
| // https://w3c.github.io/IntersectionObserver/#compute-the-intersection
 | ||
| //
 | ||
| // TODO(emilio): Proof of this being equivalent to the spec welcome, seems
 | ||
| // reasonably close.
 | ||
| //
 | ||
| // Also, it's unclear to me why the spec talks about browsing context while
 | ||
| // discarding observations of targets of different documents.
 | ||
| //
 | ||
| // Both aRootBounds and the return value are relative to
 | ||
| // nsLayoutUtils::GetContainingBlockForClientRect(aRoot).
 | ||
| //
 | ||
| // In case of out-of-process document, aRemoteDocumentVisibleRect is a rectangle
 | ||
| // in the out-of-process document's coordinate system.
 | ||
| static Maybe<nsRect> ComputeTheIntersection(
 | ||
|     nsIFrame* aTarget, nsIFrame* aRoot, const nsRect& aRootBounds,
 | ||
|     const Maybe<nsRect>& aRemoteDocumentVisibleRect,
 | ||
|     DOMIntersectionObserver::IsForProximityToViewport
 | ||
|         aIsForProximityToViewport) {
 | ||
|   nsIFrame* target = aTarget;
 | ||
|   // 1. Let intersectionRect be the result of running the
 | ||
|   // getBoundingClientRect() algorithm on the target.
 | ||
|   //
 | ||
|   // `intersectionRect` is kept relative to `target` during the loop.
 | ||
|   auto inflowRect = nsLayoutUtils::GetAllInFlowRectsUnion(
 | ||
|       target, target, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
 | ||
|   // For content-visibility, we need to observe the overflow clip edge,
 | ||
|   // https://drafts.csswg.org/css-contain-2/#close-to-the-viewport
 | ||
|   if (aIsForProximityToViewport ==
 | ||
|       DOMIntersectionObserver::IsForProximityToViewport::Yes) {
 | ||
|     const auto& disp = *target->StyleDisplay();
 | ||
|     auto clipAxes = target->ShouldApplyOverflowClipping(&disp);
 | ||
|     if (clipAxes != PhysicalAxes::None) {
 | ||
|       inflowRect = OverflowAreas::GetOverflowClipRect(
 | ||
|           inflowRect, inflowRect, clipAxes,
 | ||
|           target->OverflowClipMargin(clipAxes));
 | ||
|     }
 | ||
|   }
 | ||
|   Maybe<nsRect> intersectionRect = Some(inflowRect);
 | ||
| 
 | ||
|   // 2. Let container be the containing block of the target.
 | ||
|   // (We go through the parent chain and only look at scroll frames)
 | ||
|   //
 | ||
|   // FIXME(emilio): Spec uses containing blocks, we use scroll frames, but we
 | ||
|   // only apply overflow-clipping, not clip-path, so it's ~fine. We do need to
 | ||
|   // apply clip-path.
 | ||
|   //
 | ||
|   // 3. While container is not the intersection root:
 | ||
|   nsIFrame* containerFrame =
 | ||
|       nsLayoutUtils::GetCrossDocParentFrameInProcess(target);
 | ||
|   while (containerFrame && containerFrame != aRoot) {
 | ||
|     // FIXME(emilio): What about other scroll frames that inherit from
 | ||
|     // nsHTMLScrollFrame but have a different type, like nsListControlFrame?
 | ||
|     // This looks bogus in that case, but different bug.
 | ||
|     if (nsIScrollableFrame* scrollFrame = do_QueryFrame(containerFrame)) {
 | ||
|       if (containerFrame->GetParent() == aRoot && !aRoot->GetParent()) {
 | ||
|         // This is subtle: if we're computing the intersection against the
 | ||
|         // viewport (the root frame), and this is its scroll frame, we really
 | ||
|         // want to skip this intersection (because we want to account for the
 | ||
|         // root margin, which is already in aRootBounds).
 | ||
|         break;
 | ||
|       }
 | ||
|       nsRect subFrameRect = scrollFrame->GetScrollPortRect();
 | ||
| 
 | ||
|       // 3.1 Map intersectionRect to the coordinate space of container.
 | ||
|       nsRect intersectionRectRelativeToContainer =
 | ||
|           nsLayoutUtils::TransformFrameRectToAncestor(
 | ||
|               target, intersectionRect.value(), containerFrame);
 | ||
| 
 | ||
|       // 3.2 If container has overflow clipping or a css clip-path property,
 | ||
|       // update intersectionRect by applying container's clip.
 | ||
|       //
 | ||
|       // 3.3 is handled, looks like, by this same clipping, given the root
 | ||
|       // scroll-frame cannot escape the viewport, probably?
 | ||
|       intersectionRect =
 | ||
|           intersectionRectRelativeToContainer.EdgeInclusiveIntersection(
 | ||
|               subFrameRect);
 | ||
|       if (!intersectionRect) {
 | ||
|         return Nothing();
 | ||
|       }
 | ||
|       target = containerFrame;
 | ||
|     } else {
 | ||
|       const auto& disp = *containerFrame->StyleDisplay();
 | ||
|       auto clipAxes = containerFrame->ShouldApplyOverflowClipping(&disp);
 | ||
|       // 3.2 TODO: Apply clip-path.
 | ||
|       if (clipAxes != PhysicalAxes::None) {
 | ||
|         // 3.1 Map intersectionRect to the coordinate space of container.
 | ||
|         const nsRect intersectionRectRelativeToContainer =
 | ||
|             nsLayoutUtils::TransformFrameRectToAncestor(
 | ||
|                 target, intersectionRect.value(), containerFrame);
 | ||
|         const nsRect clipRect = OverflowAreas::GetOverflowClipRect(
 | ||
|             intersectionRectRelativeToContainer,
 | ||
|             containerFrame->GetRectRelativeToSelf(), clipAxes,
 | ||
|             containerFrame->OverflowClipMargin(clipAxes));
 | ||
|         intersectionRect =
 | ||
|             intersectionRectRelativeToContainer.EdgeInclusiveIntersection(
 | ||
|                 clipRect);
 | ||
|         if (!intersectionRect) {
 | ||
|           return Nothing();
 | ||
|         }
 | ||
|         target = containerFrame;
 | ||
|       }
 | ||
|     }
 | ||
|     containerFrame =
 | ||
|         nsLayoutUtils::GetCrossDocParentFrameInProcess(containerFrame);
 | ||
|   }
 | ||
|   MOZ_ASSERT(intersectionRect);
 | ||
| 
 | ||
|   // 4. Map intersectionRect to the coordinate space of the intersection root.
 | ||
|   nsRect intersectionRectRelativeToRoot =
 | ||
|       nsLayoutUtils::TransformFrameRectToAncestor(
 | ||
|           target, intersectionRect.value(),
 | ||
|           nsLayoutUtils::GetContainingBlockForClientRect(aRoot));
 | ||
| 
 | ||
|   // 5.Update intersectionRect by intersecting it with the root intersection
 | ||
|   // rectangle.
 | ||
|   intersectionRect =
 | ||
|       intersectionRectRelativeToRoot.EdgeInclusiveIntersection(aRootBounds);
 | ||
|   if (intersectionRect.isNothing()) {
 | ||
|     return Nothing();
 | ||
|   }
 | ||
|   // 6. Map intersectionRect to the coordinate space of the viewport of the
 | ||
|   // Document containing the target.
 | ||
|   //
 | ||
|   // FIXME(emilio): I think this may not be correct if the root is explicit
 | ||
|   // and in the same document, since then the rectangle may not be relative to
 | ||
|   // the viewport already (but it's in the same document).
 | ||
|   nsRect rect = intersectionRect.value();
 | ||
|   if (aTarget->PresContext() != aRoot->PresContext()) {
 | ||
|     if (nsIFrame* rootScrollFrame =
 | ||
|             aTarget->PresShell()->GetRootScrollFrame()) {
 | ||
|       nsLayoutUtils::TransformRect(aRoot, rootScrollFrame, rect);
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   // In out-of-process iframes we need to take an intersection with the remote
 | ||
|   // document visible rect which was already clipped by ancestor document's
 | ||
|   // viewports.
 | ||
|   if (aRemoteDocumentVisibleRect) {
 | ||
|     MOZ_ASSERT(aRoot->PresContext()->IsRootContentDocumentInProcess() &&
 | ||
|                !aRoot->PresContext()->IsRootContentDocumentCrossProcess());
 | ||
| 
 | ||
|     intersectionRect =
 | ||
|         rect.EdgeInclusiveIntersection(*aRemoteDocumentVisibleRect);
 | ||
|     if (intersectionRect.isNothing()) {
 | ||
|       return Nothing();
 | ||
|     }
 | ||
|     rect = intersectionRect.value();
 | ||
|   }
 | ||
| 
 | ||
|   return Some(rect);
 | ||
| }
 | ||
| 
 | ||
| struct OopIframeMetrics {
 | ||
|   nsIFrame* mInProcessRootFrame = nullptr;
 | ||
|   nsRect mInProcessRootRect;
 | ||
|   nsRect mRemoteDocumentVisibleRect;
 | ||
| };
 | ||
| 
 | ||
| static Maybe<OopIframeMetrics> GetOopIframeMetrics(
 | ||
|     const Document& aDocument, const Document* aRootDocument) {
 | ||
|   const Document* rootDoc =
 | ||
|       nsContentUtils::GetInProcessSubtreeRootDocument(&aDocument);
 | ||
|   MOZ_ASSERT(rootDoc);
 | ||
| 
 | ||
|   if (rootDoc->IsTopLevelContentDocument()) {
 | ||
|     return Nothing();
 | ||
|   }
 | ||
| 
 | ||
|   if (aRootDocument &&
 | ||
|       rootDoc ==
 | ||
|           nsContentUtils::GetInProcessSubtreeRootDocument(aRootDocument)) {
 | ||
|     // aRootDoc, if non-null, is either the implicit root
 | ||
|     // (top-level-content-document) or a same-origin document passed explicitly.
 | ||
|     //
 | ||
|     // In the former case, we should've returned above if there are no iframes
 | ||
|     // in between. This condition handles the explicit, same-origin root
 | ||
|     // document, when both are embedded in an OOP iframe.
 | ||
|     return Nothing();
 | ||
|   }
 | ||
| 
 | ||
|   PresShell* rootPresShell = rootDoc->GetPresShell();
 | ||
|   if (!rootPresShell || rootPresShell->IsDestroying()) {
 | ||
|     return Some(OopIframeMetrics{});
 | ||
|   }
 | ||
| 
 | ||
|   nsIFrame* inProcessRootFrame = rootPresShell->GetRootFrame();
 | ||
|   if (!inProcessRootFrame) {
 | ||
|     return Some(OopIframeMetrics{});
 | ||
|   }
 | ||
| 
 | ||
|   BrowserChild* browserChild = BrowserChild::GetFrom(rootDoc->GetDocShell());
 | ||
|   if (!browserChild) {
 | ||
|     return Some(OopIframeMetrics{});
 | ||
|   }
 | ||
| 
 | ||
|   if (MOZ_UNLIKELY(browserChild->IsTopLevel())) {
 | ||
|     // FIXME(bug 1772083): This can be hit, but it's unclear how... When can we
 | ||
|     // have a top-level BrowserChild for a document that isn't a top-level
 | ||
|     // content document?
 | ||
|     MOZ_ASSERT_UNREACHABLE("Top level BrowserChild w/ non-top level Document?");
 | ||
|     return Nothing();
 | ||
|   }
 | ||
| 
 | ||
|   nsRect inProcessRootRect;
 | ||
|   if (nsIScrollableFrame* scrollFrame =
 | ||
|           rootPresShell->GetRootScrollFrameAsScrollable()) {
 | ||
|     inProcessRootRect = scrollFrame->GetScrollPortRect();
 | ||
|   }
 | ||
| 
 | ||
|   Maybe<LayoutDeviceRect> remoteDocumentVisibleRect =
 | ||
|       browserChild->GetTopLevelViewportVisibleRectInSelfCoords();
 | ||
|   if (!remoteDocumentVisibleRect) {
 | ||
|     return Some(OopIframeMetrics{});
 | ||
|   }
 | ||
| 
 | ||
|   return Some(OopIframeMetrics{
 | ||
|       inProcessRootFrame,
 | ||
|       inProcessRootRect,
 | ||
|       LayoutDeviceRect::ToAppUnits(
 | ||
|           *remoteDocumentVisibleRect,
 | ||
|           rootPresShell->GetPresContext()->AppUnitsPerDevPixel()),
 | ||
|   });
 | ||
| }
 | ||
| 
 | ||
| // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
 | ||
| // step 2.1
 | ||
| IntersectionInput DOMIntersectionObserver::ComputeInput(
 | ||
|     const Document& aDocument, const nsINode* aRoot,
 | ||
|     const StyleRect<LengthPercentage>* aRootMargin) {
 | ||
|   // 1 - Let rootBounds be observer's root intersection rectangle.
 | ||
|   //  ... but since the intersection rectangle depends on the target, we defer
 | ||
|   //      the inflation until later.
 | ||
|   // NOTE: |rootRect| and |rootFrame| will be root in the same process. In
 | ||
|   // out-of-process iframes, they are NOT root ones of the top level content
 | ||
|   // document.
 | ||
|   nsRect rootRect;
 | ||
|   nsIFrame* rootFrame = nullptr;
 | ||
|   const nsINode* root = aRoot;
 | ||
|   const bool isImplicitRoot = !aRoot;
 | ||
|   Maybe<nsRect> remoteDocumentVisibleRect;
 | ||
|   if (aRoot && aRoot->IsElement()) {
 | ||
|     if ((rootFrame = aRoot->AsElement()->GetPrimaryFrame())) {
 | ||
|       nsRect rootRectRelativeToRootFrame;
 | ||
|       if (nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame)) {
 | ||
|         // rootRectRelativeToRootFrame should be the content rect of rootFrame,
 | ||
|         // not including the scrollbars.
 | ||
|         rootRectRelativeToRootFrame = scrollFrame->GetScrollPortRect();
 | ||
|       } else {
 | ||
|         // rootRectRelativeToRootFrame should be the border rect of rootFrame.
 | ||
|         rootRectRelativeToRootFrame = rootFrame->GetRectRelativeToSelf();
 | ||
|       }
 | ||
|       nsIFrame* containingBlock =
 | ||
|           nsLayoutUtils::GetContainingBlockForClientRect(rootFrame);
 | ||
|       rootRect = nsLayoutUtils::TransformFrameRectToAncestor(
 | ||
|           rootFrame, rootRectRelativeToRootFrame, containingBlock);
 | ||
|     }
 | ||
|   } else {
 | ||
|     MOZ_ASSERT(!aRoot || aRoot->IsDocument());
 | ||
|     const Document* rootDocument =
 | ||
|         aRoot ? aRoot->AsDocument()
 | ||
|               : GetTopLevelContentDocumentInThisProcess(aDocument);
 | ||
|     root = rootDocument;
 | ||
| 
 | ||
|     if (rootDocument) {
 | ||
|       // We're in the same process as the root document, though note that there
 | ||
|       // could be an out-of-process iframe in between us and the root. Grab the
 | ||
|       // root frame and the root rect.
 | ||
|       //
 | ||
|       // Note that the root rect is always good (we assume no DPI changes in
 | ||
|       // between the two documents, and we don't need to convert coordinates).
 | ||
|       //
 | ||
|       // The root frame however we may need to tweak in the block below, if
 | ||
|       // there's any OOP iframe in between `rootDocument` and `aDocument`, to
 | ||
|       // handle the OOP iframe positions.
 | ||
|       if (PresShell* presShell = rootDocument->GetPresShell()) {
 | ||
|         rootFrame = presShell->GetRootFrame();
 | ||
|         // We use the root scrollable frame's scroll port to account the
 | ||
|         // scrollbars in rootRect, if needed.
 | ||
|         if (nsIScrollableFrame* scrollFrame =
 | ||
|                 presShell->GetRootScrollFrameAsScrollable()) {
 | ||
|           rootRect = scrollFrame->GetScrollPortRect();
 | ||
|         } else if (rootFrame) {
 | ||
|           rootRect = rootFrame->GetRectRelativeToSelf();
 | ||
|         }
 | ||
|       }
 | ||
|     }
 | ||
| 
 | ||
|     if (Maybe<OopIframeMetrics> metrics =
 | ||
|             GetOopIframeMetrics(aDocument, rootDocument)) {
 | ||
|       rootFrame = metrics->mInProcessRootFrame;
 | ||
|       if (!rootDocument) {
 | ||
|         rootRect = metrics->mInProcessRootRect;
 | ||
|       }
 | ||
|       remoteDocumentVisibleRect = Some(metrics->mRemoteDocumentVisibleRect);
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   nsMargin rootMargin;  // This root margin is NOT applied in `implicit root`
 | ||
|                         // case, e.g. in out-of-process iframes.
 | ||
|   if (aRootMargin) {
 | ||
|     for (const auto side : mozilla::AllPhysicalSides()) {
 | ||
|       nscoord basis = side == eSideTop || side == eSideBottom
 | ||
|                           ? rootRect.Height()
 | ||
|                           : rootRect.Width();
 | ||
|       rootMargin.Side(side) = aRootMargin->Get(side).Resolve(
 | ||
|           basis, static_cast<nscoord (*)(float)>(NSToCoordRoundWithClamp));
 | ||
|     }
 | ||
|   }
 | ||
|   return {isImplicitRoot, root,       rootFrame,
 | ||
|           rootRect,       rootMargin, remoteDocumentVisibleRect};
 | ||
| }
 | ||
| 
 | ||
| // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
 | ||
| // (steps 2.1 - 2.5)
 | ||
| IntersectionOutput DOMIntersectionObserver::Intersect(
 | ||
|     const IntersectionInput& aInput, const Element& aTarget,
 | ||
|     IsForProximityToViewport aIsForProximityToViewport) {
 | ||
|   const bool isSimilarOrigin = SimilarOrigin(aTarget, aInput.mRootNode) ==
 | ||
|                                BrowsingContextOrigin::Similar;
 | ||
|   nsIFrame* targetFrame = aTarget.GetPrimaryFrame();
 | ||
|   if (!targetFrame || !aInput.mRootFrame) {
 | ||
|     return {isSimilarOrigin};
 | ||
|   }
 | ||
| 
 | ||
|   // "From the perspective of an IntersectionObserver, the skipped contents
 | ||
|   // of an element are never intersecting the intersection root. This is
 | ||
|   // true even if both the root and the target elements are in the skipped
 | ||
|   // contents."
 | ||
|   // https://drafts.csswg.org/css-contain/#cv-notes
 | ||
|   //
 | ||
|   // Skip the intersection if the element is hidden, unless this is the
 | ||
|   // specifically to determine the proximity to the viewport for
 | ||
|   // `content-visibility: auto` elements.
 | ||
|   if (aIsForProximityToViewport == IsForProximityToViewport::No &&
 | ||
|       targetFrame->IsHiddenByContentVisibilityOnAnyAncestor()) {
 | ||
|     return {isSimilarOrigin};
 | ||
|   }
 | ||
| 
 | ||
|   // 2.2. If the intersection root is not the implicit root, and target is
 | ||
|   // not in the same Document as the intersection root, skip to step 11.
 | ||
|   if (!aInput.mIsImplicitRoot &&
 | ||
|       aInput.mRootNode->OwnerDoc() != aTarget.OwnerDoc()) {
 | ||
|     return {isSimilarOrigin};
 | ||
|   }
 | ||
| 
 | ||
|   // 2.3. If the intersection root is an element and target is not a descendant
 | ||
|   // of the intersection root in the containing block chain, skip to step 11.
 | ||
|   //
 | ||
|   // NOTE(emilio): We also do this if target is the implicit root, pending
 | ||
|   // clarification in
 | ||
|   // https://github.com/w3c/IntersectionObserver/issues/456.
 | ||
|   if (aInput.mRootFrame == targetFrame ||
 | ||
|       !nsLayoutUtils::IsAncestorFrameCrossDocInProcess(aInput.mRootFrame,
 | ||
|                                                        targetFrame)) {
 | ||
|     return {isSimilarOrigin};
 | ||
|   }
 | ||
| 
 | ||
|   nsRect rootBounds = aInput.mRootRect;
 | ||
|   if (isSimilarOrigin) {
 | ||
|     rootBounds.Inflate(aInput.mRootMargin);
 | ||
|   }
 | ||
| 
 | ||
|   // 2.4. Set targetRect to the DOMRectReadOnly obtained by running the
 | ||
|   // getBoundingClientRect() algorithm on target.
 | ||
|   nsRect targetRect = targetFrame->GetBoundingClientRect();
 | ||
|   // For content-visibility, we need to observe the overflow clip edge,
 | ||
|   // https://drafts.csswg.org/css-contain-2/#close-to-the-viewport
 | ||
|   if (aIsForProximityToViewport == IsForProximityToViewport::Yes) {
 | ||
|     const auto& disp = *targetFrame->StyleDisplay();
 | ||
|     auto clipAxes = targetFrame->ShouldApplyOverflowClipping(&disp);
 | ||
|     if (clipAxes != PhysicalAxes::None) {
 | ||
|       targetRect = OverflowAreas::GetOverflowClipRect(
 | ||
|           targetRect, targetRect, clipAxes,
 | ||
|           targetFrame->OverflowClipMargin(clipAxes));
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   // 2.5. Let intersectionRect be the result of running the compute the
 | ||
|   // intersection algorithm on target and observer’s intersection root.
 | ||
|   Maybe<nsRect> intersectionRect = ComputeTheIntersection(
 | ||
|       targetFrame, aInput.mRootFrame, rootBounds,
 | ||
|       aInput.mRemoteDocumentVisibleRect, aIsForProximityToViewport);
 | ||
| 
 | ||
|   return {isSimilarOrigin, rootBounds, targetRect, intersectionRect};
 | ||
| }
 | ||
| 
 | ||
| IntersectionOutput DOMIntersectionObserver::Intersect(
 | ||
|     const IntersectionInput& aInput, const nsRect& aTargetRect) {
 | ||
|   nsRect rootBounds = aInput.mRootRect;
 | ||
|   rootBounds.Inflate(aInput.mRootMargin);
 | ||
|   auto intersectionRect =
 | ||
|       aInput.mRootRect.EdgeInclusiveIntersection(aTargetRect);
 | ||
|   if (intersectionRect && aInput.mRemoteDocumentVisibleRect) {
 | ||
|     intersectionRect = intersectionRect->EdgeInclusiveIntersection(
 | ||
|         *aInput.mRemoteDocumentVisibleRect);
 | ||
|   }
 | ||
|   return {true, rootBounds, aTargetRect, intersectionRect};
 | ||
| }
 | ||
| 
 | ||
| // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
 | ||
| // (step 2)
 | ||
| void DOMIntersectionObserver::Update(Document& aDocument,
 | ||
|                                      DOMHighResTimeStamp time) {
 | ||
|   auto input = ComputeInput(aDocument, mRoot, &mRootMargin);
 | ||
| 
 | ||
|   // 2. For each target in observer’s internal [[ObservationTargets]] slot,
 | ||
|   // processed in the same order that observe() was called on each target:
 | ||
|   for (Element* target : mObservationTargets) {
 | ||
|     // 2.1 - 2.4.
 | ||
|     IntersectionOutput output = Intersect(input, *target);
 | ||
| 
 | ||
|     // 2.5. Let targetArea be targetRect’s area.
 | ||
|     int64_t targetArea = (int64_t)output.mTargetRect.Width() *
 | ||
|                          (int64_t)output.mTargetRect.Height();
 | ||
| 
 | ||
|     // 2.6. Let intersectionArea be intersectionRect’s area.
 | ||
|     int64_t intersectionArea =
 | ||
|         !output.mIntersectionRect
 | ||
|             ? 0
 | ||
|             : (int64_t)output.mIntersectionRect->Width() *
 | ||
|                   (int64_t)output.mIntersectionRect->Height();
 | ||
| 
 | ||
|     // 2.7. Let isIntersecting be true if targetRect and rootBounds intersect or
 | ||
|     // are edge-adjacent, even if the intersection has zero area (because
 | ||
|     // rootBounds or targetRect have zero area); otherwise, let isIntersecting
 | ||
|     // be false.
 | ||
|     const bool isIntersecting = output.Intersects();
 | ||
| 
 | ||
|     // 2.8. If targetArea is non-zero, let intersectionRatio be intersectionArea
 | ||
|     // divided by targetArea. Otherwise, let intersectionRatio be 1 if
 | ||
|     // isIntersecting is true, or 0 if isIntersecting is false.
 | ||
|     double intersectionRatio;
 | ||
|     if (targetArea > 0.0) {
 | ||
|       intersectionRatio =
 | ||
|           std::min((double)intersectionArea / (double)targetArea, 1.0);
 | ||
|     } else {
 | ||
|       intersectionRatio = isIntersecting ? 1.0 : 0.0;
 | ||
|     }
 | ||
| 
 | ||
|     // 2.9 Let thresholdIndex be the index of the first entry in
 | ||
|     // observer.thresholds whose value is greater than intersectionRatio, or the
 | ||
|     // length of observer.thresholds if intersectionRatio is greater than or
 | ||
|     // equal to the last entry in observer.thresholds.
 | ||
|     int32_t thresholdIndex = -1;
 | ||
| 
 | ||
|     // If not intersecting, we can just shortcut, as we know that the thresholds
 | ||
|     // are always between 0 and 1.
 | ||
|     if (isIntersecting) {
 | ||
|       thresholdIndex = mThresholds.IndexOfFirstElementGt(intersectionRatio);
 | ||
|       if (thresholdIndex == 0) {
 | ||
|         // Per the spec, we should leave threshold at 0 and distinguish between
 | ||
|         // "less than all thresholds and intersecting" and "not intersecting"
 | ||
|         // (queuing observer entries as both cases come to pass). However,
 | ||
|         // neither Chrome nor the WPT tests expect this behavior, so treat these
 | ||
|         // two cases as one.
 | ||
|         //
 | ||
|         // See https://github.com/w3c/IntersectionObserver/issues/432 about
 | ||
|         // this.
 | ||
|         thresholdIndex = -1;
 | ||
|       }
 | ||
|     }
 | ||
| 
 | ||
|     // Steps 2.10 - 2.15.
 | ||
|     if (target->UpdateIntersectionObservation(this, thresholdIndex)) {
 | ||
|       // See https://github.com/w3c/IntersectionObserver/issues/432 about
 | ||
|       // why we use thresholdIndex > 0 rather than isIntersecting for the
 | ||
|       // entry's isIntersecting value.
 | ||
|       QueueIntersectionObserverEntry(
 | ||
|           target, time,
 | ||
|           output.mIsSimilarOrigin ? Some(output.mRootBounds) : Nothing(),
 | ||
|           output.mTargetRect, output.mIntersectionRect, thresholdIndex > 0,
 | ||
|           intersectionRatio);
 | ||
|     }
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| void DOMIntersectionObserver::QueueIntersectionObserverEntry(
 | ||
|     Element* aTarget, DOMHighResTimeStamp time, const Maybe<nsRect>& aRootRect,
 | ||
|     const nsRect& aTargetRect, const Maybe<nsRect>& aIntersectionRect,
 | ||
|     bool aIsIntersecting, double aIntersectionRatio) {
 | ||
|   RefPtr<DOMRect> rootBounds;
 | ||
|   if (aRootRect.isSome()) {
 | ||
|     rootBounds = new DOMRect(mOwner);
 | ||
|     rootBounds->SetLayoutRect(aRootRect.value());
 | ||
|   }
 | ||
|   RefPtr<DOMRect> boundingClientRect = new DOMRect(mOwner);
 | ||
|   boundingClientRect->SetLayoutRect(aTargetRect);
 | ||
|   RefPtr<DOMRect> intersectionRect = new DOMRect(mOwner);
 | ||
|   if (aIntersectionRect.isSome()) {
 | ||
|     intersectionRect->SetLayoutRect(aIntersectionRect.value());
 | ||
|   }
 | ||
|   RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry(
 | ||
|       mOwner, time, rootBounds.forget(), boundingClientRect.forget(),
 | ||
|       intersectionRect.forget(), aIsIntersecting, aTarget, aIntersectionRatio);
 | ||
|   mQueuedEntries.AppendElement(entry.forget());
 | ||
| }
 | ||
| 
 | ||
| void DOMIntersectionObserver::Notify() {
 | ||
|   if (!mQueuedEntries.Length()) {
 | ||
|     return;
 | ||
|   }
 | ||
|   Sequence<OwningNonNull<DOMIntersectionObserverEntry>> entries;
 | ||
|   if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) {
 | ||
|     for (size_t i = 0; i < mQueuedEntries.Length(); ++i) {
 | ||
|       RefPtr<DOMIntersectionObserverEntry> next = mQueuedEntries[i];
 | ||
|       *entries.AppendElement(mozilla::fallible) = next;
 | ||
|     }
 | ||
|   }
 | ||
|   mQueuedEntries.Clear();
 | ||
| 
 | ||
|   if (mCallback.is<RefPtr<dom::IntersectionCallback>>()) {
 | ||
|     RefPtr<dom::IntersectionCallback> callback(
 | ||
|         mCallback.as<RefPtr<dom::IntersectionCallback>>());
 | ||
|     callback->Call(this, entries, *this);
 | ||
|   } else {
 | ||
|     mCallback.as<NativeCallback>()(entries);
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| }  // namespace mozilla::dom
 |