Backed out changeset 2450366cf7ca (bug 1891629) for causing win msix mochitest failures
[gecko.git] / dom / base / DOMIntersectionObserver.cpp
blob12f7ee3029010a2b68ea4e4d42a1c28d2590eb6e
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "DOMIntersectionObserver.h"
8 #include "nsCSSPropertyID.h"
9 #include "nsIFrame.h"
10 #include "nsContainerFrame.h"
11 #include "nsIScrollableFrame.h"
12 #include "nsContentUtils.h"
13 #include "nsLayoutUtils.h"
14 #include "nsRefreshDriver.h"
15 #include "mozilla/PresShell.h"
16 #include "mozilla/StaticPrefs_dom.h"
17 #include "mozilla/StaticPrefs_layout.h"
18 #include "mozilla/ServoBindings.h"
19 #include "mozilla/dom/BrowserChild.h"
20 #include "mozilla/dom/BrowsingContext.h"
21 #include "mozilla/dom/DocumentInlines.h"
22 #include "mozilla/dom/HTMLImageElement.h"
23 #include "mozilla/dom/HTMLIFrameElement.h"
24 #include "Units.h"
26 namespace mozilla::dom {
28 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry)
29 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
30 NS_INTERFACE_MAP_ENTRY(nsISupports)
31 NS_INTERFACE_MAP_END
33 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserverEntry)
34 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserverEntry)
36 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DOMIntersectionObserverEntry, mOwner,
37 mRootBounds, mBoundingClientRect,
38 mIntersectionRect, mTarget)
40 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserver)
41 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
42 NS_INTERFACE_MAP_ENTRY(nsISupports)
43 NS_INTERFACE_MAP_ENTRY(DOMIntersectionObserver)
44 NS_INTERFACE_MAP_END
46 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserver)
47 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserver)
49 NS_IMPL_CYCLE_COLLECTION_CLASS(DOMIntersectionObserver)
51 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(DOMIntersectionObserver)
52 NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
53 NS_IMPL_CYCLE_COLLECTION_TRACE_END
55 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMIntersectionObserver)
56 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
57 tmp->Disconnect();
58 NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
59 NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
60 if (tmp->mCallback.is<RefPtr<dom::IntersectionCallback>>()) {
61 ImplCycleCollectionUnlink(
62 tmp->mCallback.as<RefPtr<dom::IntersectionCallback>>());
64 NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoot)
65 NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries)
66 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
68 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMIntersectionObserver)
69 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
70 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
71 if (tmp->mCallback.is<RefPtr<dom::IntersectionCallback>>()) {
72 ImplCycleCollectionTraverse(
73 cb, tmp->mCallback.as<RefPtr<dom::IntersectionCallback>>(), "mCallback",
74 0);
76 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
77 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries)
78 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
80 DOMIntersectionObserver::DOMIntersectionObserver(
81 already_AddRefed<nsPIDOMWindowInner>&& aOwner,
82 dom::IntersectionCallback& aCb)
83 : mOwner(aOwner),
84 mDocument(mOwner->GetExtantDoc()),
85 mCallback(RefPtr<dom::IntersectionCallback>(&aCb)) {}
87 already_AddRefed<DOMIntersectionObserver> DOMIntersectionObserver::Constructor(
88 const GlobalObject& aGlobal, dom::IntersectionCallback& aCb,
89 ErrorResult& aRv) {
90 return Constructor(aGlobal, aCb, IntersectionObserverInit(), aRv);
93 already_AddRefed<DOMIntersectionObserver> DOMIntersectionObserver::Constructor(
94 const GlobalObject& aGlobal, dom::IntersectionCallback& aCb,
95 const IntersectionObserverInit& aOptions, ErrorResult& aRv) {
96 nsCOMPtr<nsPIDOMWindowInner> window =
97 do_QueryInterface(aGlobal.GetAsSupports());
98 if (!window) {
99 aRv.Throw(NS_ERROR_FAILURE);
100 return nullptr;
102 RefPtr<DOMIntersectionObserver> observer =
103 new DOMIntersectionObserver(window.forget(), aCb);
105 if (!aOptions.mRoot.IsNull()) {
106 if (aOptions.mRoot.Value().IsElement()) {
107 observer->mRoot = aOptions.mRoot.Value().GetAsElement();
108 } else {
109 MOZ_ASSERT(aOptions.mRoot.Value().IsDocument());
110 observer->mRoot = aOptions.mRoot.Value().GetAsDocument();
114 if (!observer->SetRootMargin(aOptions.mRootMargin)) {
115 aRv.ThrowSyntaxError("rootMargin must be specified in pixels or percent.");
116 return nullptr;
119 if (aOptions.mThreshold.IsDoubleSequence()) {
120 const Sequence<double>& thresholds =
121 aOptions.mThreshold.GetAsDoubleSequence();
122 observer->mThresholds.SetCapacity(thresholds.Length());
123 for (const auto& thresh : thresholds) {
124 if (thresh < 0.0 || thresh > 1.0) {
125 aRv.ThrowRangeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
126 return nullptr;
128 observer->mThresholds.AppendElement(thresh);
130 observer->mThresholds.Sort();
131 if (observer->mThresholds.IsEmpty()) {
132 observer->mThresholds.AppendElement(0.0);
134 } else {
135 double thresh = aOptions.mThreshold.GetAsDouble();
136 if (thresh < 0.0 || thresh > 1.0) {
137 aRv.ThrowRangeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
138 return nullptr;
140 observer->mThresholds.AppendElement(thresh);
143 return observer.forget();
146 static void LazyLoadCallback(
147 const Sequence<OwningNonNull<DOMIntersectionObserverEntry>>& aEntries) {
148 for (const auto& entry : aEntries) {
149 Element* target = entry->Target();
150 if (entry->IsIntersecting()) {
151 if (auto* image = HTMLImageElement::FromNode(target)) {
152 image->StopLazyLoading(HTMLImageElement::StartLoading::Yes);
153 } else if (auto* iframe = HTMLIFrameElement::FromNode(target)) {
154 iframe->StopLazyLoading();
155 } else {
156 MOZ_ASSERT_UNREACHABLE(
157 "Only <img> and <iframe> should be observed by lazy load observer");
163 static LengthPercentage PrefMargin(float aValue, bool aIsPercentage) {
164 return aIsPercentage ? LengthPercentage::FromPercentage(aValue / 100.0f)
165 : LengthPercentage::FromPixels(aValue);
168 DOMIntersectionObserver::DOMIntersectionObserver(Document& aDocument,
169 NativeCallback aCallback)
170 : mOwner(aDocument.GetInnerWindow()),
171 mDocument(&aDocument),
172 mCallback(aCallback) {}
174 already_AddRefed<DOMIntersectionObserver>
175 DOMIntersectionObserver::CreateLazyLoadObserver(Document& aDocument) {
176 RefPtr<DOMIntersectionObserver> observer =
177 new DOMIntersectionObserver(aDocument, LazyLoadCallback);
178 observer->mThresholds.AppendElement(0.0f);
180 #define SET_MARGIN(side_, side_lower_) \
181 observer->mRootMargin.Get(eSide##side_) = PrefMargin( \
182 StaticPrefs::dom_image_lazy_loading_root_margin_##side_lower_(), \
183 StaticPrefs:: \
184 dom_image_lazy_loading_root_margin_##side_lower_##_percentage());
185 SET_MARGIN(Top, top);
186 SET_MARGIN(Right, right);
187 SET_MARGIN(Bottom, bottom);
188 SET_MARGIN(Left, left);
189 #undef SET_MARGIN
191 return observer.forget();
194 bool DOMIntersectionObserver::SetRootMargin(const nsACString& aString) {
195 return Servo_IntersectionObserverRootMargin_Parse(&aString, &mRootMargin);
198 nsISupports* DOMIntersectionObserver::GetParentObject() const { return mOwner; }
200 void DOMIntersectionObserver::GetRootMargin(nsACString& aRetVal) {
201 Servo_IntersectionObserverRootMargin_ToString(&mRootMargin, &aRetVal);
204 void DOMIntersectionObserver::GetThresholds(nsTArray<double>& aRetVal) {
205 aRetVal = mThresholds.Clone();
208 void DOMIntersectionObserver::Observe(Element& aTarget) {
209 if (!mObservationTargetSet.EnsureInserted(&aTarget)) {
210 return;
212 aTarget.RegisterIntersectionObserver(this);
213 mObservationTargets.AppendElement(&aTarget);
215 MOZ_ASSERT(mObservationTargets.Length() == mObservationTargetSet.Count());
217 Connect();
218 if (mDocument) {
219 if (nsPresContext* pc = mDocument->GetPresContext()) {
220 pc->RefreshDriver()->EnsureIntersectionObservationsUpdateHappens();
225 void DOMIntersectionObserver::Unobserve(Element& aTarget) {
226 if (!mObservationTargetSet.EnsureRemoved(&aTarget)) {
227 return;
230 mObservationTargets.RemoveElement(&aTarget);
231 aTarget.UnregisterIntersectionObserver(this);
233 MOZ_ASSERT(mObservationTargets.Length() == mObservationTargetSet.Count());
235 if (mObservationTargets.IsEmpty()) {
236 Disconnect();
240 void DOMIntersectionObserver::UnlinkTarget(Element& aTarget) {
241 mObservationTargets.RemoveElement(&aTarget);
242 mObservationTargetSet.Remove(&aTarget);
243 if (mObservationTargets.IsEmpty()) {
244 Disconnect();
248 void DOMIntersectionObserver::Connect() {
249 if (mConnected) {
250 return;
253 mConnected = true;
254 if (mDocument) {
255 mDocument->AddIntersectionObserver(this);
259 void DOMIntersectionObserver::Disconnect() {
260 if (!mConnected) {
261 return;
264 mConnected = false;
265 for (Element* target : mObservationTargets) {
266 target->UnregisterIntersectionObserver(this);
268 mObservationTargets.Clear();
269 mObservationTargetSet.Clear();
270 if (mDocument) {
271 mDocument->RemoveIntersectionObserver(this);
275 void DOMIntersectionObserver::TakeRecords(
276 nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal) {
277 aRetVal = std::move(mQueuedEntries);
280 enum class BrowsingContextOrigin { Similar, Different };
282 // NOTE(emilio): Checking docgroup as per discussion in:
283 // https://github.com/w3c/IntersectionObserver/issues/161
284 static BrowsingContextOrigin SimilarOrigin(const Element& aTarget,
285 const nsINode* aRoot) {
286 if (!aRoot) {
287 return BrowsingContextOrigin::Different;
289 return aTarget.OwnerDoc()->GetDocGroup() == aRoot->OwnerDoc()->GetDocGroup()
290 ? BrowsingContextOrigin::Similar
291 : BrowsingContextOrigin::Different;
294 // NOTE: This returns nullptr if |aDocument| is in another process from the top
295 // level content document.
296 static const Document* GetTopLevelContentDocumentInThisProcess(
297 const Document& aDocument) {
298 auto* wc = aDocument.GetTopLevelWindowContext();
299 return wc ? wc->GetExtantDoc() : nullptr;
302 // https://w3c.github.io/IntersectionObserver/#compute-the-intersection
304 // TODO(emilio): Proof of this being equivalent to the spec welcome, seems
305 // reasonably close.
307 // Also, it's unclear to me why the spec talks about browsing context while
308 // discarding observations of targets of different documents.
310 // Both aRootBounds and the return value are relative to
311 // nsLayoutUtils::GetContainingBlockForClientRect(aRoot).
313 // In case of out-of-process document, aRemoteDocumentVisibleRect is a rectangle
314 // in the out-of-process document's coordinate system.
315 static Maybe<nsRect> ComputeTheIntersection(
316 nsIFrame* aTarget, nsIFrame* aRoot, const nsRect& aRootBounds,
317 const Maybe<nsRect>& aRemoteDocumentVisibleRect,
318 DOMIntersectionObserver::IsForProximityToViewport
319 aIsForProximityToViewport) {
320 nsIFrame* target = aTarget;
321 // 1. Let intersectionRect be the result of running the
322 // getBoundingClientRect() algorithm on the target.
324 // `intersectionRect` is kept relative to `target` during the loop.
325 auto inflowRect = nsLayoutUtils::GetAllInFlowRectsUnion(
326 target, target, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
327 // For content-visibility, we need to observe the overflow clip edge,
328 // https://drafts.csswg.org/css-contain-2/#close-to-the-viewport
329 if (aIsForProximityToViewport ==
330 DOMIntersectionObserver::IsForProximityToViewport::Yes) {
331 const auto& disp = *target->StyleDisplay();
332 auto clipAxes = target->ShouldApplyOverflowClipping(&disp);
333 if (clipAxes != PhysicalAxes::None) {
334 inflowRect = OverflowAreas::GetOverflowClipRect(
335 inflowRect, inflowRect, clipAxes,
336 target->OverflowClipMargin(clipAxes));
339 Maybe<nsRect> intersectionRect = Some(inflowRect);
341 // 2. Let container be the containing block of the target.
342 // (We go through the parent chain and only look at scroll frames)
344 // FIXME(emilio): Spec uses containing blocks, we use scroll frames, but we
345 // only apply overflow-clipping, not clip-path, so it's ~fine. We do need to
346 // apply clip-path.
348 // 3. While container is not the intersection root:
349 nsIFrame* containerFrame =
350 nsLayoutUtils::GetCrossDocParentFrameInProcess(target);
351 while (containerFrame && containerFrame != aRoot) {
352 // FIXME(emilio): What about other scroll frames that inherit from
353 // nsHTMLScrollFrame but have a different type, like nsListControlFrame?
354 // This looks bogus in that case, but different bug.
355 if (nsIScrollableFrame* scrollFrame = do_QueryFrame(containerFrame)) {
356 if (containerFrame->GetParent() == aRoot && !aRoot->GetParent()) {
357 // This is subtle: if we're computing the intersection against the
358 // viewport (the root frame), and this is its scroll frame, we really
359 // want to skip this intersection (because we want to account for the
360 // root margin, which is already in aRootBounds).
361 break;
363 nsRect subFrameRect = scrollFrame->GetScrollPortRect();
365 // 3.1 Map intersectionRect to the coordinate space of container.
366 nsRect intersectionRectRelativeToContainer =
367 nsLayoutUtils::TransformFrameRectToAncestor(
368 target, intersectionRect.value(), containerFrame);
370 // 3.2 If container has overflow clipping or a css clip-path property,
371 // update intersectionRect by applying container's clip.
373 // 3.3 is handled, looks like, by this same clipping, given the root
374 // scroll-frame cannot escape the viewport, probably?
375 intersectionRect =
376 intersectionRectRelativeToContainer.EdgeInclusiveIntersection(
377 subFrameRect);
378 if (!intersectionRect) {
379 return Nothing();
381 target = containerFrame;
382 } else {
383 const auto& disp = *containerFrame->StyleDisplay();
384 auto clipAxes = containerFrame->ShouldApplyOverflowClipping(&disp);
385 // 3.2 TODO: Apply clip-path.
386 if (clipAxes != PhysicalAxes::None) {
387 // 3.1 Map intersectionRect to the coordinate space of container.
388 const nsRect intersectionRectRelativeToContainer =
389 nsLayoutUtils::TransformFrameRectToAncestor(
390 target, intersectionRect.value(), containerFrame);
391 const nsRect clipRect = OverflowAreas::GetOverflowClipRect(
392 intersectionRectRelativeToContainer,
393 containerFrame->GetRectRelativeToSelf(), clipAxes,
394 containerFrame->OverflowClipMargin(clipAxes));
395 intersectionRect =
396 intersectionRectRelativeToContainer.EdgeInclusiveIntersection(
397 clipRect);
398 if (!intersectionRect) {
399 return Nothing();
401 target = containerFrame;
404 containerFrame =
405 nsLayoutUtils::GetCrossDocParentFrameInProcess(containerFrame);
407 MOZ_ASSERT(intersectionRect);
409 // 4. Map intersectionRect to the coordinate space of the intersection root.
410 nsRect intersectionRectRelativeToRoot =
411 nsLayoutUtils::TransformFrameRectToAncestor(
412 target, intersectionRect.value(),
413 nsLayoutUtils::GetContainingBlockForClientRect(aRoot));
415 // 5.Update intersectionRect by intersecting it with the root intersection
416 // rectangle.
417 intersectionRect =
418 intersectionRectRelativeToRoot.EdgeInclusiveIntersection(aRootBounds);
419 if (intersectionRect.isNothing()) {
420 return Nothing();
422 // 6. Map intersectionRect to the coordinate space of the viewport of the
423 // Document containing the target.
425 // FIXME(emilio): I think this may not be correct if the root is explicit
426 // and in the same document, since then the rectangle may not be relative to
427 // the viewport already (but it's in the same document).
428 nsRect rect = intersectionRect.value();
429 if (aTarget->PresContext() != aRoot->PresContext()) {
430 if (nsIFrame* rootScrollFrame =
431 aTarget->PresShell()->GetRootScrollFrame()) {
432 nsLayoutUtils::TransformRect(aRoot, rootScrollFrame, rect);
436 // In out-of-process iframes we need to take an intersection with the remote
437 // document visible rect which was already clipped by ancestor document's
438 // viewports.
439 if (aRemoteDocumentVisibleRect) {
440 MOZ_ASSERT(aRoot->PresContext()->IsRootContentDocumentInProcess() &&
441 !aRoot->PresContext()->IsRootContentDocumentCrossProcess());
443 intersectionRect =
444 rect.EdgeInclusiveIntersection(*aRemoteDocumentVisibleRect);
445 if (intersectionRect.isNothing()) {
446 return Nothing();
448 rect = intersectionRect.value();
451 return Some(rect);
454 struct OopIframeMetrics {
455 nsIFrame* mInProcessRootFrame = nullptr;
456 nsRect mInProcessRootRect;
457 nsRect mRemoteDocumentVisibleRect;
460 static Maybe<OopIframeMetrics> GetOopIframeMetrics(
461 const Document& aDocument, const Document* aRootDocument) {
462 const Document* rootDoc =
463 nsContentUtils::GetInProcessSubtreeRootDocument(&aDocument);
464 MOZ_ASSERT(rootDoc);
466 if (rootDoc->IsTopLevelContentDocument()) {
467 return Nothing();
470 if (aRootDocument &&
471 rootDoc ==
472 nsContentUtils::GetInProcessSubtreeRootDocument(aRootDocument)) {
473 // aRootDoc, if non-null, is either the implicit root
474 // (top-level-content-document) or a same-origin document passed explicitly.
476 // In the former case, we should've returned above if there are no iframes
477 // in between. This condition handles the explicit, same-origin root
478 // document, when both are embedded in an OOP iframe.
479 return Nothing();
482 PresShell* rootPresShell = rootDoc->GetPresShell();
483 if (!rootPresShell || rootPresShell->IsDestroying()) {
484 return Some(OopIframeMetrics{});
487 nsIFrame* inProcessRootFrame = rootPresShell->GetRootFrame();
488 if (!inProcessRootFrame) {
489 return Some(OopIframeMetrics{});
492 BrowserChild* browserChild = BrowserChild::GetFrom(rootDoc->GetDocShell());
493 if (!browserChild) {
494 return Some(OopIframeMetrics{});
497 if (MOZ_UNLIKELY(browserChild->IsTopLevel())) {
498 // FIXME(bug 1772083): This can be hit, but it's unclear how... When can we
499 // have a top-level BrowserChild for a document that isn't a top-level
500 // content document?
501 MOZ_ASSERT_UNREACHABLE("Top level BrowserChild w/ non-top level Document?");
502 return Nothing();
505 nsRect inProcessRootRect;
506 if (nsIScrollableFrame* scrollFrame =
507 rootPresShell->GetRootScrollFrameAsScrollable()) {
508 inProcessRootRect = scrollFrame->GetScrollPortRect();
511 Maybe<LayoutDeviceRect> remoteDocumentVisibleRect =
512 browserChild->GetTopLevelViewportVisibleRectInSelfCoords();
513 if (!remoteDocumentVisibleRect) {
514 return Some(OopIframeMetrics{});
517 return Some(OopIframeMetrics{
518 inProcessRootFrame,
519 inProcessRootRect,
520 LayoutDeviceRect::ToAppUnits(
521 *remoteDocumentVisibleRect,
522 rootPresShell->GetPresContext()->AppUnitsPerDevPixel()),
526 // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
527 // step 2.1
528 IntersectionInput DOMIntersectionObserver::ComputeInput(
529 const Document& aDocument, const nsINode* aRoot,
530 const StyleRect<LengthPercentage>* aRootMargin) {
531 // 1 - Let rootBounds be observer's root intersection rectangle.
532 // ... but since the intersection rectangle depends on the target, we defer
533 // the inflation until later.
534 // NOTE: |rootRect| and |rootFrame| will be root in the same process. In
535 // out-of-process iframes, they are NOT root ones of the top level content
536 // document.
537 nsRect rootRect;
538 nsIFrame* rootFrame = nullptr;
539 const nsINode* root = aRoot;
540 const bool isImplicitRoot = !aRoot;
541 Maybe<nsRect> remoteDocumentVisibleRect;
542 if (aRoot && aRoot->IsElement()) {
543 if ((rootFrame = aRoot->AsElement()->GetPrimaryFrame())) {
544 nsRect rootRectRelativeToRootFrame;
545 if (nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame)) {
546 // rootRectRelativeToRootFrame should be the content rect of rootFrame,
547 // not including the scrollbars.
548 rootRectRelativeToRootFrame = scrollFrame->GetScrollPortRect();
549 } else {
550 // rootRectRelativeToRootFrame should be the border rect of rootFrame.
551 rootRectRelativeToRootFrame = rootFrame->GetRectRelativeToSelf();
553 nsIFrame* containingBlock =
554 nsLayoutUtils::GetContainingBlockForClientRect(rootFrame);
555 rootRect = nsLayoutUtils::TransformFrameRectToAncestor(
556 rootFrame, rootRectRelativeToRootFrame, containingBlock);
558 } else {
559 MOZ_ASSERT(!aRoot || aRoot->IsDocument());
560 const Document* rootDocument =
561 aRoot ? aRoot->AsDocument()
562 : GetTopLevelContentDocumentInThisProcess(aDocument);
563 root = rootDocument;
565 if (rootDocument) {
566 // We're in the same process as the root document, though note that there
567 // could be an out-of-process iframe in between us and the root. Grab the
568 // root frame and the root rect.
570 // Note that the root rect is always good (we assume no DPI changes in
571 // between the two documents, and we don't need to convert coordinates).
573 // The root frame however we may need to tweak in the block below, if
574 // there's any OOP iframe in between `rootDocument` and `aDocument`, to
575 // handle the OOP iframe positions.
576 if (PresShell* presShell = rootDocument->GetPresShell()) {
577 rootFrame = presShell->GetRootFrame();
578 // We use the root scrollable frame's scroll port to account the
579 // scrollbars in rootRect, if needed.
580 if (nsIScrollableFrame* scrollFrame =
581 presShell->GetRootScrollFrameAsScrollable()) {
582 rootRect = scrollFrame->GetScrollPortRect();
583 } else if (rootFrame) {
584 rootRect = rootFrame->GetRectRelativeToSelf();
589 if (Maybe<OopIframeMetrics> metrics =
590 GetOopIframeMetrics(aDocument, rootDocument)) {
591 rootFrame = metrics->mInProcessRootFrame;
592 if (!rootDocument) {
593 rootRect = metrics->mInProcessRootRect;
595 remoteDocumentVisibleRect = Some(metrics->mRemoteDocumentVisibleRect);
599 nsMargin rootMargin; // This root margin is NOT applied in `implicit root`
600 // case, e.g. in out-of-process iframes.
601 if (aRootMargin) {
602 for (const auto side : mozilla::AllPhysicalSides()) {
603 nscoord basis = side == eSideTop || side == eSideBottom
604 ? rootRect.Height()
605 : rootRect.Width();
606 rootMargin.Side(side) = aRootMargin->Get(side).Resolve(
607 basis, static_cast<nscoord (*)(float)>(NSToCoordRoundWithClamp));
610 return {isImplicitRoot, root, rootFrame,
611 rootRect, rootMargin, remoteDocumentVisibleRect};
614 // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
615 // (steps 2.1 - 2.5)
616 IntersectionOutput DOMIntersectionObserver::Intersect(
617 const IntersectionInput& aInput, const Element& aTarget,
618 IsForProximityToViewport aIsForProximityToViewport) {
619 const bool isSimilarOrigin = SimilarOrigin(aTarget, aInput.mRootNode) ==
620 BrowsingContextOrigin::Similar;
621 nsIFrame* targetFrame = aTarget.GetPrimaryFrame();
622 if (!targetFrame || !aInput.mRootFrame) {
623 return {isSimilarOrigin};
626 // "From the perspective of an IntersectionObserver, the skipped contents
627 // of an element are never intersecting the intersection root. This is
628 // true even if both the root and the target elements are in the skipped
629 // contents."
630 // https://drafts.csswg.org/css-contain/#cv-notes
632 // Skip the intersection if the element is hidden, unless this is the
633 // specifically to determine the proximity to the viewport for
634 // `content-visibility: auto` elements.
635 if (aIsForProximityToViewport == IsForProximityToViewport::No &&
636 targetFrame->IsHiddenByContentVisibilityOnAnyAncestor()) {
637 return {isSimilarOrigin};
640 // 2.2. If the intersection root is not the implicit root, and target is
641 // not in the same Document as the intersection root, skip to step 11.
642 if (!aInput.mIsImplicitRoot &&
643 aInput.mRootNode->OwnerDoc() != aTarget.OwnerDoc()) {
644 return {isSimilarOrigin};
647 // 2.3. If the intersection root is an element and target is not a descendant
648 // of the intersection root in the containing block chain, skip to step 11.
650 // NOTE(emilio): We also do this if target is the implicit root, pending
651 // clarification in
652 // https://github.com/w3c/IntersectionObserver/issues/456.
653 if (aInput.mRootFrame == targetFrame ||
654 !nsLayoutUtils::IsAncestorFrameCrossDocInProcess(aInput.mRootFrame,
655 targetFrame)) {
656 return {isSimilarOrigin};
659 nsRect rootBounds = aInput.mRootRect;
660 if (isSimilarOrigin) {
661 rootBounds.Inflate(aInput.mRootMargin);
664 // 2.4. Set targetRect to the DOMRectReadOnly obtained by running the
665 // getBoundingClientRect() algorithm on target.
666 nsRect targetRect = targetFrame->GetBoundingClientRect();
667 // For content-visibility, we need to observe the overflow clip edge,
668 // https://drafts.csswg.org/css-contain-2/#close-to-the-viewport
669 if (aIsForProximityToViewport == IsForProximityToViewport::Yes) {
670 const auto& disp = *targetFrame->StyleDisplay();
671 auto clipAxes = targetFrame->ShouldApplyOverflowClipping(&disp);
672 if (clipAxes != PhysicalAxes::None) {
673 targetRect = OverflowAreas::GetOverflowClipRect(
674 targetRect, targetRect, clipAxes,
675 targetFrame->OverflowClipMargin(clipAxes));
679 // 2.5. Let intersectionRect be the result of running the compute the
680 // intersection algorithm on target and observer’s intersection root.
681 Maybe<nsRect> intersectionRect = ComputeTheIntersection(
682 targetFrame, aInput.mRootFrame, rootBounds,
683 aInput.mRemoteDocumentVisibleRect, aIsForProximityToViewport);
685 return {isSimilarOrigin, rootBounds, targetRect, intersectionRect};
688 IntersectionOutput DOMIntersectionObserver::Intersect(
689 const IntersectionInput& aInput, const nsRect& aTargetRect) {
690 nsRect rootBounds = aInput.mRootRect;
691 rootBounds.Inflate(aInput.mRootMargin);
692 auto intersectionRect =
693 aInput.mRootRect.EdgeInclusiveIntersection(aTargetRect);
694 if (intersectionRect && aInput.mRemoteDocumentVisibleRect) {
695 intersectionRect = intersectionRect->EdgeInclusiveIntersection(
696 *aInput.mRemoteDocumentVisibleRect);
698 return {true, rootBounds, aTargetRect, intersectionRect};
701 // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
702 // (step 2)
703 void DOMIntersectionObserver::Update(Document& aDocument,
704 DOMHighResTimeStamp time) {
705 auto input = ComputeInput(aDocument, mRoot, &mRootMargin);
707 // 2. For each target in observer’s internal [[ObservationTargets]] slot,
708 // processed in the same order that observe() was called on each target:
709 for (Element* target : mObservationTargets) {
710 // 2.1 - 2.4.
711 IntersectionOutput output = Intersect(input, *target);
713 // 2.5. Let targetArea be targetRect’s area.
714 int64_t targetArea = (int64_t)output.mTargetRect.Width() *
715 (int64_t)output.mTargetRect.Height();
717 // 2.6. Let intersectionArea be intersectionRect’s area.
718 int64_t intersectionArea =
719 !output.mIntersectionRect
721 : (int64_t)output.mIntersectionRect->Width() *
722 (int64_t)output.mIntersectionRect->Height();
724 // 2.7. Let isIntersecting be true if targetRect and rootBounds intersect or
725 // are edge-adjacent, even if the intersection has zero area (because
726 // rootBounds or targetRect have zero area); otherwise, let isIntersecting
727 // be false.
728 const bool isIntersecting = output.Intersects();
730 // 2.8. If targetArea is non-zero, let intersectionRatio be intersectionArea
731 // divided by targetArea. Otherwise, let intersectionRatio be 1 if
732 // isIntersecting is true, or 0 if isIntersecting is false.
733 double intersectionRatio;
734 if (targetArea > 0.0) {
735 intersectionRatio =
736 std::min((double)intersectionArea / (double)targetArea, 1.0);
737 } else {
738 intersectionRatio = isIntersecting ? 1.0 : 0.0;
741 // 2.9 Let thresholdIndex be the index of the first entry in
742 // observer.thresholds whose value is greater than intersectionRatio, or the
743 // length of observer.thresholds if intersectionRatio is greater than or
744 // equal to the last entry in observer.thresholds.
745 int32_t thresholdIndex = -1;
747 // If not intersecting, we can just shortcut, as we know that the thresholds
748 // are always between 0 and 1.
749 if (isIntersecting) {
750 thresholdIndex = mThresholds.IndexOfFirstElementGt(intersectionRatio);
751 if (thresholdIndex == 0) {
752 // Per the spec, we should leave threshold at 0 and distinguish between
753 // "less than all thresholds and intersecting" and "not intersecting"
754 // (queuing observer entries as both cases come to pass). However,
755 // neither Chrome nor the WPT tests expect this behavior, so treat these
756 // two cases as one.
758 // See https://github.com/w3c/IntersectionObserver/issues/432 about
759 // this.
760 thresholdIndex = -1;
764 // Steps 2.10 - 2.15.
765 if (target->UpdateIntersectionObservation(this, thresholdIndex)) {
766 // See https://github.com/w3c/IntersectionObserver/issues/432 about
767 // why we use thresholdIndex > 0 rather than isIntersecting for the
768 // entry's isIntersecting value.
769 QueueIntersectionObserverEntry(
770 target, time,
771 output.mIsSimilarOrigin ? Some(output.mRootBounds) : Nothing(),
772 output.mTargetRect, output.mIntersectionRect, thresholdIndex > 0,
773 intersectionRatio);
778 void DOMIntersectionObserver::QueueIntersectionObserverEntry(
779 Element* aTarget, DOMHighResTimeStamp time, const Maybe<nsRect>& aRootRect,
780 const nsRect& aTargetRect, const Maybe<nsRect>& aIntersectionRect,
781 bool aIsIntersecting, double aIntersectionRatio) {
782 RefPtr<DOMRect> rootBounds;
783 if (aRootRect.isSome()) {
784 rootBounds = new DOMRect(mOwner);
785 rootBounds->SetLayoutRect(aRootRect.value());
787 RefPtr<DOMRect> boundingClientRect = new DOMRect(mOwner);
788 boundingClientRect->SetLayoutRect(aTargetRect);
789 RefPtr<DOMRect> intersectionRect = new DOMRect(mOwner);
790 if (aIntersectionRect.isSome()) {
791 intersectionRect->SetLayoutRect(aIntersectionRect.value());
793 RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry(
794 mOwner, time, rootBounds.forget(), boundingClientRect.forget(),
795 intersectionRect.forget(), aIsIntersecting, aTarget, aIntersectionRatio);
796 mQueuedEntries.AppendElement(entry.forget());
799 void DOMIntersectionObserver::Notify() {
800 if (!mQueuedEntries.Length()) {
801 return;
803 Sequence<OwningNonNull<DOMIntersectionObserverEntry>> entries;
804 if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) {
805 for (size_t i = 0; i < mQueuedEntries.Length(); ++i) {
806 RefPtr<DOMIntersectionObserverEntry> next = mQueuedEntries[i];
807 *entries.AppendElement(mozilla::fallible) = next;
810 mQueuedEntries.Clear();
812 if (mCallback.is<RefPtr<dom::IntersectionCallback>>()) {
813 RefPtr<dom::IntersectionCallback> callback(
814 mCallback.as<RefPtr<dom::IntersectionCallback>>());
815 callback->Call(this, entries, *this);
816 } else {
817 mCallback.as<NativeCallback>()(entries);
821 } // namespace mozilla::dom