/* -*- 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 "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>()) { ImplCycleCollectionUnlink( tmp->mCallback.as>()); } 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>()) { ImplCycleCollectionTraverse( cb, tmp->mCallback.as>(), "mCallback", 0); } NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END DOMIntersectionObserver::DOMIntersectionObserver( already_AddRefed&& aOwner, dom::IntersectionCallback& aCb) : mOwner(aOwner), mDocument(mOwner->GetExtantDoc()), mCallback(RefPtr(&aCb)), mConnected(false) {} already_AddRefed DOMIntersectionObserver::Constructor( const GlobalObject& aGlobal, dom::IntersectionCallback& aCb, ErrorResult& aRv) { return Constructor(aGlobal, aCb, IntersectionObserverInit(), aRv); } already_AddRefed DOMIntersectionObserver::Constructor( const GlobalObject& aGlobal, dom::IntersectionCallback& aCb, const IntersectionObserverInit& aOptions, ErrorResult& aRv) { nsCOMPtr window = do_QueryInterface(aGlobal.GetAsSupports()); if (!window) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } RefPtr 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& thresholds = aOptions.mThreshold.GetAsDoubleSequence(); observer->mThresholds.SetCapacity(thresholds.Length()); for (const auto& thresh : thresholds) { if (thresh < 0.0 || thresh > 1.0) { aRv.ThrowRangeError(); return nullptr; } observer->mThresholds.AppendElement(thresh); } observer->mThresholds.Sort(); } else { double thresh = aOptions.mThreshold.GetAsDouble(); if (thresh < 0.0 || thresh > 1.0) { aRv.ThrowRangeError(); return nullptr; } observer->mThresholds.AppendElement(thresh); } return observer.forget(); } static void LazyLoadCallback( const Sequence>& aEntries) { for (const auto& entry : aEntries) { MOZ_ASSERT(entry->Target()->IsHTMLElement(nsGkAtoms::img)); if (entry->IsIntersecting()) { static_cast(entry->Target()) ->StopLazyLoading(HTMLImageElement::StartLoading::Yes); } } } static void ContentVisibilityCallback( const Sequence>& aEntries) { for (const auto& entry : aEntries) { entry->Target()->SetVisibleForContentVisibility(entry->IsIntersecting()); if (RefPtr doc = entry->Target()->GetComposedDoc()) { if (RefPtr presShell = doc->GetPresShell()) { presShell->ScheduleContentRelevancyUpdate( ContentRelevancyReason::Visible); } } } } 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), mConnected(false) {} already_AddRefed DOMIntersectionObserver::CreateLazyLoadObserver(Document& aDocument) { RefPtr 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(); } already_AddRefed DOMIntersectionObserver::CreateContentVisibilityObserver(Document& aDocument) { RefPtr observer = new DOMIntersectionObserver(aDocument, ContentVisibilityCallback); observer->mThresholds.AppendElement(0.0f); auto margin = LengthPercentage::FromPercentage( StaticPrefs::layout_css_content_visibility_relevant_content_margin() / 100.0f); observer->mRootMargin.Get(eSideTop) = margin; observer->mRootMargin.Get(eSideRight) = margin; observer->mRootMargin.Get(eSideBottom) = margin; observer->mRootMargin.Get(eSideLeft) = 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& 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>& aRetVal) { aRetVal = std::move(mQueuedEntries); } static Maybe EdgeInclusiveIntersection(const nsRect& aRect, const nsRect& aOtherRect) { nscoord left = std::max(aRect.x, aOtherRect.x); nscoord top = std::max(aRect.y, aOtherRect.y); nscoord right = std::min(aRect.XMost(), aOtherRect.XMost()); nscoord bottom = std::min(aRect.YMost(), aOtherRect.YMost()); if (left > right || top > bottom) { return Nothing(); } return Some(nsRect(left, top, right - left, bottom - top)); } 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 ComputeTheIntersection( nsIFrame* aTarget, nsIFrame* aRoot, const nsRect& aRootBounds, const Maybe& aRemoteDocumentVisibleRect) { 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. Maybe intersectionRect = Some(nsLayoutUtils::GetAllInFlowRectsUnion( target, target, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS)); // 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 = EdgeInclusiveIntersection( intersectionRectRelativeToContainer, 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 = EdgeInclusiveIntersection( intersectionRectRelativeToContainer, 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 = EdgeInclusiveIntersection(intersectionRectRelativeToRoot, 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 = EdgeInclusiveIntersection(rect, *aRemoteDocumentVisibleRect); if (intersectionRect.isNothing()) { return Nothing(); } rect = intersectionRect.value(); } return Some(rect); } struct OopIframeMetrics { nsIFrame* mInProcessRootFrame = nullptr; nsRect mInProcessRootRect; nsRect mRemoteDocumentVisibleRect; }; static Maybe 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 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* 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 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 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(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, Element& aTarget, IgnoreContentVisibility aIgnoreContentVisibility) { 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 // DOMIntersectionObserver used specifically to track the visibility of // `content-visibility: auto` elements. if (aIgnoreContentVisibility == IgnoreContentVisibility::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(); // 2.5. Let intersectionRect be the result of running the compute the // intersection algorithm on target and observer’s intersection root. Maybe intersectionRect = ComputeTheIntersection(targetFrame, aInput.mRootFrame, rootBounds, aInput.mRemoteDocumentVisibleRect); return {isSimilarOrigin, rootBounds, targetRect, intersectionRect}; } IntersectionOutput DOMIntersectionObserver::Intersect( const IntersectionInput& aInput, const nsRect& aTargetRect) { nsRect rootBounds = aInput.mRootRect; rootBounds.Inflate(aInput.mRootMargin); auto intersectionRect = EdgeInclusiveIntersection(aInput.mRootRect, aTargetRect); if (intersectionRect && aInput.mRemoteDocumentVisibleRect) { intersectionRect = EdgeInclusiveIntersection( *intersectionRect, *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); // If this observer is used to determine content relevancy for // `content-visiblity: auto` content, then do not skip intersection // for content that is hidden by `content-visibility: auto`. IgnoreContentVisibility ignoreContentVisibility = aDocument.GetContentVisibilityObserver() == this ? IgnoreContentVisibility::Yes : IgnoreContentVisibility::No; // 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, ignoreContentVisibility); // 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& aRootRect, const nsRect& aTargetRect, const Maybe& aIntersectionRect, bool aIsIntersecting, double aIntersectionRatio) { RefPtr rootBounds; if (aRootRect.isSome()) { rootBounds = new DOMRect(mOwner); rootBounds->SetLayoutRect(aRootRect.value()); } RefPtr boundingClientRect = new DOMRect(mOwner); boundingClientRect->SetLayoutRect(aTargetRect); RefPtr intersectionRect = new DOMRect(mOwner); if (aIntersectionRect.isSome()) { intersectionRect->SetLayoutRect(aIntersectionRect.value()); } RefPtr 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> entries; if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) { for (size_t i = 0; i < mQueuedEntries.Length(); ++i) { RefPtr next = mQueuedEntries[i]; *entries.AppendElement(mozilla::fallible) = next; } } mQueuedEntries.Clear(); if (mCallback.is>()) { RefPtr callback( mCallback.as>()); callback->Call(this, entries, *this); } else { mCallback.as()(entries); } } } // namespace mozilla::dom