Bug 1835710 - Cancel off-thread JIT compilation before changing nursery allocation...
[gecko.git] / dom / base / DOMIntersectionObserver.cpp
blob9b34e261c4637130299da65874617c719ea98a01
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 "Units.h"
25 namespace mozilla::dom {
27 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry)
28 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
29 NS_INTERFACE_MAP_ENTRY(nsISupports)
30 NS_INTERFACE_MAP_END
32 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserverEntry)
33 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserverEntry)
35 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DOMIntersectionObserverEntry, mOwner,
36 mRootBounds, mBoundingClientRect,
37 mIntersectionRect, mTarget)
39 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserver)
40 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
41 NS_INTERFACE_MAP_ENTRY(nsISupports)
42 NS_INTERFACE_MAP_ENTRY(DOMIntersectionObserver)
43 NS_INTERFACE_MAP_END
45 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserver)
46 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserver)
48 NS_IMPL_CYCLE_COLLECTION_CLASS(DOMIntersectionObserver)
50 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(DOMIntersectionObserver)
51 NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
52 NS_IMPL_CYCLE_COLLECTION_TRACE_END
54 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMIntersectionObserver)
55 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
56 tmp->Disconnect();
57 NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
58 NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
59 if (tmp->mCallback.is<RefPtr<dom::IntersectionCallback>>()) {
60 ImplCycleCollectionUnlink(
61 tmp->mCallback.as<RefPtr<dom::IntersectionCallback>>());
63 NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoot)
64 NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries)
65 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
67 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMIntersectionObserver)
68 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
69 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
70 if (tmp->mCallback.is<RefPtr<dom::IntersectionCallback>>()) {
71 ImplCycleCollectionTraverse(
72 cb, tmp->mCallback.as<RefPtr<dom::IntersectionCallback>>(), "mCallback",
73 0);
75 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
76 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries)
77 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
79 DOMIntersectionObserver::DOMIntersectionObserver(
80 already_AddRefed<nsPIDOMWindowInner>&& aOwner,
81 dom::IntersectionCallback& aCb)
82 : mOwner(aOwner),
83 mDocument(mOwner->GetExtantDoc()),
84 mCallback(RefPtr<dom::IntersectionCallback>(&aCb)),
85 mConnected(false) {}
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 } else {
132 double thresh = aOptions.mThreshold.GetAsDouble();
133 if (thresh < 0.0 || thresh > 1.0) {
134 aRv.ThrowRangeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
135 return nullptr;
137 observer->mThresholds.AppendElement(thresh);
140 return observer.forget();
143 static void LazyLoadCallback(
144 const Sequence<OwningNonNull<DOMIntersectionObserverEntry>>& aEntries) {
145 for (const auto& entry : aEntries) {
146 MOZ_ASSERT(entry->Target()->IsHTMLElement(nsGkAtoms::img));
147 if (entry->IsIntersecting()) {
148 static_cast<HTMLImageElement*>(entry->Target())
149 ->StopLazyLoading(HTMLImageElement::StartLoading::Yes);
154 static void ContentVisibilityCallback(
155 const Sequence<OwningNonNull<DOMIntersectionObserverEntry>>& aEntries) {
156 for (const auto& entry : aEntries) {
157 entry->Target()->SetVisibleForContentVisibility(entry->IsIntersecting());
159 if (RefPtr<Document> doc = entry->Target()->GetComposedDoc()) {
160 if (RefPtr<PresShell> presShell = doc->GetPresShell()) {
161 presShell->ScheduleContentRelevancyUpdate(
162 ContentRelevancyReason::Visible);
168 static LengthPercentage PrefMargin(float aValue, bool aIsPercentage) {
169 return aIsPercentage ? LengthPercentage::FromPercentage(aValue / 100.0f)
170 : LengthPercentage::FromPixels(aValue);
173 DOMIntersectionObserver::DOMIntersectionObserver(Document& aDocument,
174 NativeCallback aCallback)
175 : mOwner(aDocument.GetInnerWindow()),
176 mDocument(&aDocument),
177 mCallback(aCallback),
178 mConnected(false) {}
180 already_AddRefed<DOMIntersectionObserver>
181 DOMIntersectionObserver::CreateLazyLoadObserver(Document& aDocument) {
182 RefPtr<DOMIntersectionObserver> observer =
183 new DOMIntersectionObserver(aDocument, LazyLoadCallback);
184 observer->mThresholds.AppendElement(0.0f);
186 #define SET_MARGIN(side_, side_lower_) \
187 observer->mRootMargin.Get(eSide##side_) = PrefMargin( \
188 StaticPrefs::dom_image_lazy_loading_root_margin_##side_lower_(), \
189 StaticPrefs:: \
190 dom_image_lazy_loading_root_margin_##side_lower_##_percentage());
191 SET_MARGIN(Top, top);
192 SET_MARGIN(Right, right);
193 SET_MARGIN(Bottom, bottom);
194 SET_MARGIN(Left, left);
195 #undef SET_MARGIN
197 return observer.forget();
200 already_AddRefed<DOMIntersectionObserver>
201 DOMIntersectionObserver::CreateContentVisibilityObserver(Document& aDocument) {
202 RefPtr<DOMIntersectionObserver> observer =
203 new DOMIntersectionObserver(aDocument, ContentVisibilityCallback);
205 observer->mThresholds.AppendElement(0.0f);
207 auto margin = LengthPercentage::FromPercentage(
208 StaticPrefs::layout_css_content_visibility_relevant_content_margin() /
209 100.0f);
211 observer->mRootMargin.Get(eSideTop) = margin;
212 observer->mRootMargin.Get(eSideRight) = margin;
213 observer->mRootMargin.Get(eSideBottom) = margin;
214 observer->mRootMargin.Get(eSideLeft) = margin;
216 return observer.forget();
219 bool DOMIntersectionObserver::SetRootMargin(const nsACString& aString) {
220 return Servo_IntersectionObserverRootMargin_Parse(&aString, &mRootMargin);
223 nsISupports* DOMIntersectionObserver::GetParentObject() const { return mOwner; }
225 void DOMIntersectionObserver::GetRootMargin(nsACString& aRetVal) {
226 Servo_IntersectionObserverRootMargin_ToString(&mRootMargin, &aRetVal);
229 void DOMIntersectionObserver::GetThresholds(nsTArray<double>& aRetVal) {
230 aRetVal = mThresholds.Clone();
233 void DOMIntersectionObserver::Observe(Element& aTarget) {
234 if (!mObservationTargetSet.EnsureInserted(&aTarget)) {
235 return;
237 aTarget.RegisterIntersectionObserver(this);
238 mObservationTargets.AppendElement(&aTarget);
240 MOZ_ASSERT(mObservationTargets.Length() == mObservationTargetSet.Count());
242 Connect();
243 if (mDocument) {
244 if (nsPresContext* pc = mDocument->GetPresContext()) {
245 pc->RefreshDriver()->EnsureIntersectionObservationsUpdateHappens();
250 void DOMIntersectionObserver::Unobserve(Element& aTarget) {
251 if (!mObservationTargetSet.EnsureRemoved(&aTarget)) {
252 return;
255 mObservationTargets.RemoveElement(&aTarget);
256 aTarget.UnregisterIntersectionObserver(this);
258 MOZ_ASSERT(mObservationTargets.Length() == mObservationTargetSet.Count());
260 if (mObservationTargets.IsEmpty()) {
261 Disconnect();
265 void DOMIntersectionObserver::UnlinkTarget(Element& aTarget) {
266 mObservationTargets.RemoveElement(&aTarget);
267 mObservationTargetSet.Remove(&aTarget);
268 if (mObservationTargets.IsEmpty()) {
269 Disconnect();
273 void DOMIntersectionObserver::Connect() {
274 if (mConnected) {
275 return;
278 mConnected = true;
279 if (mDocument) {
280 mDocument->AddIntersectionObserver(this);
284 void DOMIntersectionObserver::Disconnect() {
285 if (!mConnected) {
286 return;
289 mConnected = false;
290 for (Element* target : mObservationTargets) {
291 target->UnregisterIntersectionObserver(this);
293 mObservationTargets.Clear();
294 mObservationTargetSet.Clear();
295 if (mDocument) {
296 mDocument->RemoveIntersectionObserver(this);
300 void DOMIntersectionObserver::TakeRecords(
301 nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal) {
302 aRetVal = std::move(mQueuedEntries);
305 static Maybe<nsRect> EdgeInclusiveIntersection(const nsRect& aRect,
306 const nsRect& aOtherRect) {
307 nscoord left = std::max(aRect.x, aOtherRect.x);
308 nscoord top = std::max(aRect.y, aOtherRect.y);
309 nscoord right = std::min(aRect.XMost(), aOtherRect.XMost());
310 nscoord bottom = std::min(aRect.YMost(), aOtherRect.YMost());
311 if (left > right || top > bottom) {
312 return Nothing();
314 return Some(nsRect(left, top, right - left, bottom - top));
317 enum class BrowsingContextOrigin { Similar, Different };
319 // NOTE(emilio): Checking docgroup as per discussion in:
320 // https://github.com/w3c/IntersectionObserver/issues/161
321 static BrowsingContextOrigin SimilarOrigin(const Element& aTarget,
322 const nsINode* aRoot) {
323 if (!aRoot) {
324 return BrowsingContextOrigin::Different;
326 return aTarget.OwnerDoc()->GetDocGroup() == aRoot->OwnerDoc()->GetDocGroup()
327 ? BrowsingContextOrigin::Similar
328 : BrowsingContextOrigin::Different;
331 // NOTE: This returns nullptr if |aDocument| is in another process from the top
332 // level content document.
333 static const Document* GetTopLevelContentDocumentInThisProcess(
334 const Document& aDocument) {
335 auto* wc = aDocument.GetTopLevelWindowContext();
336 return wc ? wc->GetExtantDoc() : nullptr;
339 // https://w3c.github.io/IntersectionObserver/#compute-the-intersection
341 // TODO(emilio): Proof of this being equivalent to the spec welcome, seems
342 // reasonably close.
344 // Also, it's unclear to me why the spec talks about browsing context while
345 // discarding observations of targets of different documents.
347 // Both aRootBounds and the return value are relative to
348 // nsLayoutUtils::GetContainingBlockForClientRect(aRoot).
350 // In case of out-of-process document, aRemoteDocumentVisibleRect is a rectangle
351 // in the out-of-process document's coordinate system.
352 static Maybe<nsRect> ComputeTheIntersection(
353 nsIFrame* aTarget, nsIFrame* aRoot, const nsRect& aRootBounds,
354 const Maybe<nsRect>& aRemoteDocumentVisibleRect) {
355 nsIFrame* target = aTarget;
356 // 1. Let intersectionRect be the result of running the
357 // getBoundingClientRect() algorithm on the target.
359 // `intersectionRect` is kept relative to `target` during the loop.
360 Maybe<nsRect> intersectionRect = Some(nsLayoutUtils::GetAllInFlowRectsUnion(
361 target, target, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS));
363 // 2. Let container be the containing block of the target.
364 // (We go through the parent chain and only look at scroll frames)
366 // FIXME(emilio): Spec uses containing blocks, we use scroll frames, but we
367 // only apply overflow-clipping, not clip-path, so it's ~fine. We do need to
368 // apply clip-path.
370 // 3. While container is not the intersection root:
371 nsIFrame* containerFrame =
372 nsLayoutUtils::GetCrossDocParentFrameInProcess(target);
373 while (containerFrame && containerFrame != aRoot) {
374 // FIXME(emilio): What about other scroll frames that inherit from
375 // nsHTMLScrollFrame but have a different type, like nsListControlFrame?
376 // This looks bogus in that case, but different bug.
377 if (nsIScrollableFrame* scrollFrame = do_QueryFrame(containerFrame)) {
378 if (containerFrame->GetParent() == aRoot && !aRoot->GetParent()) {
379 // This is subtle: if we're computing the intersection against the
380 // viewport (the root frame), and this is its scroll frame, we really
381 // want to skip this intersection (because we want to account for the
382 // root margin, which is already in aRootBounds).
383 break;
385 nsRect subFrameRect = scrollFrame->GetScrollPortRect();
387 // 3.1 Map intersectionRect to the coordinate space of container.
388 nsRect intersectionRectRelativeToContainer =
389 nsLayoutUtils::TransformFrameRectToAncestor(
390 target, intersectionRect.value(), containerFrame);
392 // 3.2 If container has overflow clipping or a css clip-path property,
393 // update intersectionRect by applying container's clip.
395 // 3.3 is handled, looks like, by this same clipping, given the root
396 // scroll-frame cannot escape the viewport, probably?
397 intersectionRect = EdgeInclusiveIntersection(
398 intersectionRectRelativeToContainer, subFrameRect);
399 if (!intersectionRect) {
400 return Nothing();
402 target = containerFrame;
403 } else {
404 const auto& disp = *containerFrame->StyleDisplay();
405 auto clipAxes = containerFrame->ShouldApplyOverflowClipping(&disp);
406 // 3.2 TODO: Apply clip-path.
407 if (clipAxes != PhysicalAxes::None) {
408 // 3.1 Map intersectionRect to the coordinate space of container.
409 const nsRect intersectionRectRelativeToContainer =
410 nsLayoutUtils::TransformFrameRectToAncestor(
411 target, intersectionRect.value(), containerFrame);
412 const nsRect clipRect = OverflowAreas::GetOverflowClipRect(
413 intersectionRectRelativeToContainer,
414 containerFrame->GetRectRelativeToSelf(), clipAxes,
415 containerFrame->OverflowClipMargin(clipAxes));
416 intersectionRect = EdgeInclusiveIntersection(
417 intersectionRectRelativeToContainer, clipRect);
418 if (!intersectionRect) {
419 return Nothing();
421 target = containerFrame;
424 containerFrame =
425 nsLayoutUtils::GetCrossDocParentFrameInProcess(containerFrame);
427 MOZ_ASSERT(intersectionRect);
429 // 4. Map intersectionRect to the coordinate space of the intersection root.
430 nsRect intersectionRectRelativeToRoot =
431 nsLayoutUtils::TransformFrameRectToAncestor(
432 target, intersectionRect.value(),
433 nsLayoutUtils::GetContainingBlockForClientRect(aRoot));
435 // 5.Update intersectionRect by intersecting it with the root intersection
436 // rectangle.
437 intersectionRect =
438 EdgeInclusiveIntersection(intersectionRectRelativeToRoot, aRootBounds);
439 if (intersectionRect.isNothing()) {
440 return Nothing();
442 // 6. Map intersectionRect to the coordinate space of the viewport of the
443 // Document containing the target.
445 // FIXME(emilio): I think this may not be correct if the root is explicit
446 // and in the same document, since then the rectangle may not be relative to
447 // the viewport already (but it's in the same document).
448 nsRect rect = intersectionRect.value();
449 if (aTarget->PresContext() != aRoot->PresContext()) {
450 if (nsIFrame* rootScrollFrame =
451 aTarget->PresShell()->GetRootScrollFrame()) {
452 nsLayoutUtils::TransformRect(aRoot, rootScrollFrame, rect);
456 // In out-of-process iframes we need to take an intersection with the remote
457 // document visible rect which was already clipped by ancestor document's
458 // viewports.
459 if (aRemoteDocumentVisibleRect) {
460 MOZ_ASSERT(aRoot->PresContext()->IsRootContentDocumentInProcess() &&
461 !aRoot->PresContext()->IsRootContentDocumentCrossProcess());
463 intersectionRect =
464 EdgeInclusiveIntersection(rect, *aRemoteDocumentVisibleRect);
465 if (intersectionRect.isNothing()) {
466 return Nothing();
468 rect = intersectionRect.value();
471 return Some(rect);
474 struct OopIframeMetrics {
475 nsIFrame* mInProcessRootFrame = nullptr;
476 nsRect mInProcessRootRect;
477 nsRect mRemoteDocumentVisibleRect;
480 static Maybe<OopIframeMetrics> GetOopIframeMetrics(
481 const Document& aDocument, const Document* aRootDocument) {
482 const Document* rootDoc =
483 nsContentUtils::GetInProcessSubtreeRootDocument(&aDocument);
484 MOZ_ASSERT(rootDoc);
486 if (rootDoc->IsTopLevelContentDocument()) {
487 return Nothing();
490 if (aRootDocument &&
491 rootDoc ==
492 nsContentUtils::GetInProcessSubtreeRootDocument(aRootDocument)) {
493 // aRootDoc, if non-null, is either the implicit root
494 // (top-level-content-document) or a same-origin document passed explicitly.
496 // In the former case, we should've returned above if there are no iframes
497 // in between. This condition handles the explicit, same-origin root
498 // document, when both are embedded in an OOP iframe.
499 return Nothing();
502 PresShell* rootPresShell = rootDoc->GetPresShell();
503 if (!rootPresShell || rootPresShell->IsDestroying()) {
504 return Some(OopIframeMetrics{});
507 nsIFrame* inProcessRootFrame = rootPresShell->GetRootFrame();
508 if (!inProcessRootFrame) {
509 return Some(OopIframeMetrics{});
512 BrowserChild* browserChild = BrowserChild::GetFrom(rootDoc->GetDocShell());
513 if (!browserChild) {
514 return Some(OopIframeMetrics{});
517 if (MOZ_UNLIKELY(browserChild->IsTopLevel())) {
518 // FIXME(bug 1772083): This can be hit, but it's unclear how... When can we
519 // have a top-level BrowserChild for a document that isn't a top-level
520 // content document?
521 MOZ_ASSERT_UNREACHABLE("Top level BrowserChild w/ non-top level Document?");
522 return Nothing();
525 nsRect inProcessRootRect;
526 if (nsIScrollableFrame* scrollFrame =
527 rootPresShell->GetRootScrollFrameAsScrollable()) {
528 inProcessRootRect = scrollFrame->GetScrollPortRect();
531 Maybe<LayoutDeviceRect> remoteDocumentVisibleRect =
532 browserChild->GetTopLevelViewportVisibleRectInSelfCoords();
533 if (!remoteDocumentVisibleRect) {
534 return Some(OopIframeMetrics{});
537 return Some(OopIframeMetrics{
538 inProcessRootFrame,
539 inProcessRootRect,
540 LayoutDeviceRect::ToAppUnits(
541 *remoteDocumentVisibleRect,
542 rootPresShell->GetPresContext()->AppUnitsPerDevPixel()),
546 // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
547 // step 2.1
548 IntersectionInput DOMIntersectionObserver::ComputeInput(
549 const Document& aDocument, const nsINode* aRoot,
550 const StyleRect<LengthPercentage>* aRootMargin) {
551 // 1 - Let rootBounds be observer's root intersection rectangle.
552 // ... but since the intersection rectangle depends on the target, we defer
553 // the inflation until later.
554 // NOTE: |rootRect| and |rootFrame| will be root in the same process. In
555 // out-of-process iframes, they are NOT root ones of the top level content
556 // document.
557 nsRect rootRect;
558 nsIFrame* rootFrame = nullptr;
559 const nsINode* root = aRoot;
560 const bool isImplicitRoot = !aRoot;
561 Maybe<nsRect> remoteDocumentVisibleRect;
562 if (aRoot && aRoot->IsElement()) {
563 if ((rootFrame = aRoot->AsElement()->GetPrimaryFrame())) {
564 nsRect rootRectRelativeToRootFrame;
565 if (nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame)) {
566 // rootRectRelativeToRootFrame should be the content rect of rootFrame,
567 // not including the scrollbars.
568 rootRectRelativeToRootFrame = scrollFrame->GetScrollPortRect();
569 } else {
570 // rootRectRelativeToRootFrame should be the border rect of rootFrame.
571 rootRectRelativeToRootFrame = rootFrame->GetRectRelativeToSelf();
573 nsIFrame* containingBlock =
574 nsLayoutUtils::GetContainingBlockForClientRect(rootFrame);
575 rootRect = nsLayoutUtils::TransformFrameRectToAncestor(
576 rootFrame, rootRectRelativeToRootFrame, containingBlock);
578 } else {
579 MOZ_ASSERT(!aRoot || aRoot->IsDocument());
580 const Document* rootDocument =
581 aRoot ? aRoot->AsDocument()
582 : GetTopLevelContentDocumentInThisProcess(aDocument);
583 root = rootDocument;
585 if (rootDocument) {
586 // We're in the same process as the root document, though note that there
587 // could be an out-of-process iframe in between us and the root. Grab the
588 // root frame and the root rect.
590 // Note that the root rect is always good (we assume no DPI changes in
591 // between the two documents, and we don't need to convert coordinates).
593 // The root frame however we may need to tweak in the block below, if
594 // there's any OOP iframe in between `rootDocument` and `aDocument`, to
595 // handle the OOP iframe positions.
596 if (PresShell* presShell = rootDocument->GetPresShell()) {
597 rootFrame = presShell->GetRootFrame();
598 // We use the root scrollable frame's scroll port to account the
599 // scrollbars in rootRect, if needed.
600 if (nsIScrollableFrame* scrollFrame =
601 presShell->GetRootScrollFrameAsScrollable()) {
602 rootRect = scrollFrame->GetScrollPortRect();
603 } else if (rootFrame) {
604 rootRect = rootFrame->GetRectRelativeToSelf();
609 if (Maybe<OopIframeMetrics> metrics =
610 GetOopIframeMetrics(aDocument, rootDocument)) {
611 rootFrame = metrics->mInProcessRootFrame;
612 if (!rootDocument) {
613 rootRect = metrics->mInProcessRootRect;
615 remoteDocumentVisibleRect = Some(metrics->mRemoteDocumentVisibleRect);
619 nsMargin rootMargin; // This root margin is NOT applied in `implicit root`
620 // case, e.g. in out-of-process iframes.
621 if (aRootMargin) {
622 for (const auto side : mozilla::AllPhysicalSides()) {
623 nscoord basis = side == eSideTop || side == eSideBottom
624 ? rootRect.Height()
625 : rootRect.Width();
626 rootMargin.Side(side) = aRootMargin->Get(side).Resolve(
627 basis, static_cast<nscoord (*)(float)>(NSToCoordRoundWithClamp));
630 return {isImplicitRoot, root, rootFrame,
631 rootRect, rootMargin, remoteDocumentVisibleRect};
634 // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
635 // (steps 2.1 - 2.5)
636 IntersectionOutput DOMIntersectionObserver::Intersect(
637 const IntersectionInput& aInput, Element& aTarget,
638 IgnoreContentVisibility aIgnoreContentVisibility) {
639 const bool isSimilarOrigin = SimilarOrigin(aTarget, aInput.mRootNode) ==
640 BrowsingContextOrigin::Similar;
641 nsIFrame* targetFrame = aTarget.GetPrimaryFrame();
642 if (!targetFrame || !aInput.mRootFrame) {
643 return {isSimilarOrigin};
646 // "From the perspective of an IntersectionObserver, the skipped contents
647 // of an element are never intersecting the intersection root. This is
648 // true even if both the root and the target elements are in the skipped
649 // contents."
650 // https://drafts.csswg.org/css-contain/#cv-notes
652 // Skip the intersection if the element is hidden, unless this is the
653 // DOMIntersectionObserver used specifically to track the visibility of
654 // `content-visibility: auto` elements.
655 if (aIgnoreContentVisibility == IgnoreContentVisibility::No &&
656 targetFrame->IsHiddenByContentVisibilityOnAnyAncestor()) {
657 return {isSimilarOrigin};
660 // 2.2. If the intersection root is not the implicit root, and target is
661 // not in the same Document as the intersection root, skip to step 11.
662 if (!aInput.mIsImplicitRoot &&
663 aInput.mRootNode->OwnerDoc() != aTarget.OwnerDoc()) {
664 return {isSimilarOrigin};
667 // 2.3. If the intersection root is an element and target is not a descendant
668 // of the intersection root in the containing block chain, skip to step 11.
670 // NOTE(emilio): We also do this if target is the implicit root, pending
671 // clarification in
672 // https://github.com/w3c/IntersectionObserver/issues/456.
673 if (aInput.mRootFrame == targetFrame ||
674 !nsLayoutUtils::IsAncestorFrameCrossDocInProcess(aInput.mRootFrame,
675 targetFrame)) {
676 return {isSimilarOrigin};
679 nsRect rootBounds = aInput.mRootRect;
680 if (isSimilarOrigin) {
681 rootBounds.Inflate(aInput.mRootMargin);
684 // 2.4. Set targetRect to the DOMRectReadOnly obtained by running the
685 // getBoundingClientRect() algorithm on target.
686 nsRect targetRect = targetFrame->GetBoundingClientRect();
688 // 2.5. Let intersectionRect be the result of running the compute the
689 // intersection algorithm on target and observer’s intersection root.
690 Maybe<nsRect> intersectionRect =
691 ComputeTheIntersection(targetFrame, aInput.mRootFrame, rootBounds,
692 aInput.mRemoteDocumentVisibleRect);
694 return {isSimilarOrigin, rootBounds, targetRect, intersectionRect};
697 IntersectionOutput DOMIntersectionObserver::Intersect(
698 const IntersectionInput& aInput, const nsRect& aTargetRect) {
699 nsRect rootBounds = aInput.mRootRect;
700 rootBounds.Inflate(aInput.mRootMargin);
701 auto intersectionRect =
702 EdgeInclusiveIntersection(aInput.mRootRect, aTargetRect);
703 if (intersectionRect && aInput.mRemoteDocumentVisibleRect) {
704 intersectionRect = EdgeInclusiveIntersection(
705 *intersectionRect, *aInput.mRemoteDocumentVisibleRect);
707 return {true, rootBounds, aTargetRect, intersectionRect};
710 // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
711 // (step 2)
712 void DOMIntersectionObserver::Update(Document& aDocument,
713 DOMHighResTimeStamp time) {
714 auto input = ComputeInput(aDocument, mRoot, &mRootMargin);
716 // If this observer is used to determine content relevancy for
717 // `content-visiblity: auto` content, then do not skip intersection
718 // for content that is hidden by `content-visibility: auto`.
719 IgnoreContentVisibility ignoreContentVisibility =
720 aDocument.GetContentVisibilityObserver() == this
721 ? IgnoreContentVisibility::Yes
722 : IgnoreContentVisibility::No;
724 // 2. For each target in observer’s internal [[ObservationTargets]] slot,
725 // processed in the same order that observe() was called on each target:
726 for (Element* target : mObservationTargets) {
727 // 2.1 - 2.4.
728 IntersectionOutput output =
729 Intersect(input, *target, ignoreContentVisibility);
731 // 2.5. Let targetArea be targetRect’s area.
732 int64_t targetArea = (int64_t)output.mTargetRect.Width() *
733 (int64_t)output.mTargetRect.Height();
735 // 2.6. Let intersectionArea be intersectionRect’s area.
736 int64_t intersectionArea =
737 !output.mIntersectionRect
739 : (int64_t)output.mIntersectionRect->Width() *
740 (int64_t)output.mIntersectionRect->Height();
742 // 2.7. Let isIntersecting be true if targetRect and rootBounds intersect or
743 // are edge-adjacent, even if the intersection has zero area (because
744 // rootBounds or targetRect have zero area); otherwise, let isIntersecting
745 // be false.
746 const bool isIntersecting = output.Intersects();
748 // 2.8. If targetArea is non-zero, let intersectionRatio be intersectionArea
749 // divided by targetArea. Otherwise, let intersectionRatio be 1 if
750 // isIntersecting is true, or 0 if isIntersecting is false.
751 double intersectionRatio;
752 if (targetArea > 0.0) {
753 intersectionRatio =
754 std::min((double)intersectionArea / (double)targetArea, 1.0);
755 } else {
756 intersectionRatio = isIntersecting ? 1.0 : 0.0;
759 // 2.9 Let thresholdIndex be the index of the first entry in
760 // observer.thresholds whose value is greater than intersectionRatio, or the
761 // length of observer.thresholds if intersectionRatio is greater than or
762 // equal to the last entry in observer.thresholds.
763 int32_t thresholdIndex = -1;
765 // If not intersecting, we can just shortcut, as we know that the thresholds
766 // are always between 0 and 1.
767 if (isIntersecting) {
768 thresholdIndex = mThresholds.IndexOfFirstElementGt(intersectionRatio);
769 if (thresholdIndex == 0) {
770 // Per the spec, we should leave threshold at 0 and distinguish between
771 // "less than all thresholds and intersecting" and "not intersecting"
772 // (queuing observer entries as both cases come to pass). However,
773 // neither Chrome nor the WPT tests expect this behavior, so treat these
774 // two cases as one.
776 // See https://github.com/w3c/IntersectionObserver/issues/432 about
777 // this.
778 thresholdIndex = -1;
782 // Steps 2.10 - 2.15.
783 if (target->UpdateIntersectionObservation(this, thresholdIndex)) {
784 // See https://github.com/w3c/IntersectionObserver/issues/432 about
785 // why we use thresholdIndex > 0 rather than isIntersecting for the
786 // entry's isIntersecting value.
787 QueueIntersectionObserverEntry(
788 target, time,
789 output.mIsSimilarOrigin ? Some(output.mRootBounds) : Nothing(),
790 output.mTargetRect, output.mIntersectionRect, thresholdIndex > 0,
791 intersectionRatio);
796 void DOMIntersectionObserver::QueueIntersectionObserverEntry(
797 Element* aTarget, DOMHighResTimeStamp time, const Maybe<nsRect>& aRootRect,
798 const nsRect& aTargetRect, const Maybe<nsRect>& aIntersectionRect,
799 bool aIsIntersecting, double aIntersectionRatio) {
800 RefPtr<DOMRect> rootBounds;
801 if (aRootRect.isSome()) {
802 rootBounds = new DOMRect(mOwner);
803 rootBounds->SetLayoutRect(aRootRect.value());
805 RefPtr<DOMRect> boundingClientRect = new DOMRect(mOwner);
806 boundingClientRect->SetLayoutRect(aTargetRect);
807 RefPtr<DOMRect> intersectionRect = new DOMRect(mOwner);
808 if (aIntersectionRect.isSome()) {
809 intersectionRect->SetLayoutRect(aIntersectionRect.value());
811 RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry(
812 mOwner, time, rootBounds.forget(), boundingClientRect.forget(),
813 intersectionRect.forget(), aIsIntersecting, aTarget, aIntersectionRatio);
814 mQueuedEntries.AppendElement(entry.forget());
817 void DOMIntersectionObserver::Notify() {
818 if (!mQueuedEntries.Length()) {
819 return;
821 Sequence<OwningNonNull<DOMIntersectionObserverEntry>> entries;
822 if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) {
823 for (size_t i = 0; i < mQueuedEntries.Length(); ++i) {
824 RefPtr<DOMIntersectionObserverEntry> next = mQueuedEntries[i];
825 *entries.AppendElement(mozilla::fallible) = next;
828 mQueuedEntries.Clear();
830 if (mCallback.is<RefPtr<dom::IntersectionCallback>>()) {
831 RefPtr<dom::IntersectionCallback> callback(
832 mCallback.as<RefPtr<dom::IntersectionCallback>>());
833 callback->Call(this, entries, *this);
834 } else {
835 mCallback.as<NativeCallback>()(entries);
839 } // namespace mozilla::dom