Bug 1700051: part 35) Reduce accessibility of `mSoftText.mDOMMapping` to `private...
[gecko.git] / dom / base / DOMIntersectionObserver.cpp
blobf31b7c860ebf77f5e94f863a8ad1bd4cac1e3c70
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/ServoBindings.h"
18 #include "mozilla/StaticPrefs_dom.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 if (!StaticPrefs::
111 dom_IntersectionObserverExplicitDocumentRoot_enabled()) {
112 aRv.ThrowTypeError<dom::MSG_DOES_NOT_IMPLEMENT_INTERFACE>(
113 "'root' member of IntersectionObserverInit", "Element");
114 return nullptr;
116 observer->mRoot = aOptions.mRoot.Value().GetAsDocument();
120 if (!observer->SetRootMargin(aOptions.mRootMargin)) {
121 aRv.ThrowSyntaxError("rootMargin must be specified in pixels or percent.");
122 return nullptr;
125 if (aOptions.mThreshold.IsDoubleSequence()) {
126 const Sequence<double>& thresholds =
127 aOptions.mThreshold.GetAsDoubleSequence();
128 observer->mThresholds.SetCapacity(thresholds.Length());
129 for (const auto& thresh : thresholds) {
130 if (thresh < 0.0 || thresh > 1.0) {
131 aRv.ThrowRangeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
132 return nullptr;
134 observer->mThresholds.AppendElement(thresh);
136 observer->mThresholds.Sort();
137 } else {
138 double thresh = aOptions.mThreshold.GetAsDouble();
139 if (thresh < 0.0 || thresh > 1.0) {
140 aRv.ThrowRangeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
141 return nullptr;
143 observer->mThresholds.AppendElement(thresh);
146 return observer.forget();
149 static void LazyLoadCallback(
150 const Sequence<OwningNonNull<DOMIntersectionObserverEntry>>& aEntries) {
151 for (const auto& entry : aEntries) {
152 MOZ_ASSERT(entry->Target()->IsHTMLElement(nsGkAtoms::img));
153 if (entry->IsIntersecting()) {
154 static_cast<HTMLImageElement*>(entry->Target())
155 ->StopLazyLoadingAndStartLoadIfNeeded(true);
160 static void LazyLoadCallbackReachViewport(
161 const Sequence<OwningNonNull<DOMIntersectionObserverEntry>>& aEntries) {
162 for (const auto& entry : aEntries) {
163 MOZ_ASSERT(entry->Target()->IsHTMLElement(nsGkAtoms::img));
164 if (entry->IsIntersecting()) {
165 static_cast<HTMLImageElement*>(entry->Target())
166 ->LazyLoadImageReachedViewport();
171 static LengthPercentage PrefMargin(float aValue, bool aIsPercentage) {
172 return aIsPercentage ? LengthPercentage::FromPercentage(aValue / 100.0f)
173 : LengthPercentage::FromPixels(aValue);
176 DOMIntersectionObserver::DOMIntersectionObserver(Document& aDocument,
177 NativeCallback aCallback)
178 : mOwner(aDocument.GetInnerWindow()),
179 mDocument(&aDocument),
180 mCallback(aCallback),
181 mConnected(false) {}
183 already_AddRefed<DOMIntersectionObserver>
184 DOMIntersectionObserver::CreateLazyLoadObserver(Document& aDocument) {
185 RefPtr<DOMIntersectionObserver> observer =
186 new DOMIntersectionObserver(aDocument, LazyLoadCallback);
187 observer->mThresholds.AppendElement(std::numeric_limits<double>::min());
189 #define SET_MARGIN(side_, side_lower_) \
190 observer->mRootMargin.Get(eSide##side_) = PrefMargin( \
191 StaticPrefs::dom_image_lazy_loading_root_margin_##side_lower_(), \
192 StaticPrefs:: \
193 dom_image_lazy_loading_root_margin_##side_lower_##_percentage());
194 SET_MARGIN(Top, top);
195 SET_MARGIN(Right, right);
196 SET_MARGIN(Bottom, bottom);
197 SET_MARGIN(Left, left);
198 #undef SET_MARGIN
200 return observer.forget();
203 already_AddRefed<DOMIntersectionObserver>
204 DOMIntersectionObserver::CreateLazyLoadObserverViewport(Document& aDocument) {
205 RefPtr<DOMIntersectionObserver> observer =
206 new DOMIntersectionObserver(aDocument, LazyLoadCallbackReachViewport);
207 observer->mThresholds.AppendElement(std::numeric_limits<double>::min());
208 return observer.forget();
211 bool DOMIntersectionObserver::SetRootMargin(const nsACString& aString) {
212 return Servo_IntersectionObserverRootMargin_Parse(&aString, &mRootMargin);
215 nsISupports* DOMIntersectionObserver::GetParentObject() const { return mOwner; }
217 void DOMIntersectionObserver::GetRootMargin(nsACString& aRetVal) {
218 Servo_IntersectionObserverRootMargin_ToString(&mRootMargin, &aRetVal);
221 void DOMIntersectionObserver::GetThresholds(nsTArray<double>& aRetVal) {
222 aRetVal = mThresholds.Clone();
225 void DOMIntersectionObserver::Observe(Element& aTarget) {
226 if (mObservationTargets.Contains(&aTarget)) {
227 return;
229 aTarget.RegisterIntersectionObserver(this);
230 mObservationTargets.AppendElement(&aTarget);
231 Connect();
232 if (mDocument) {
233 if (nsPresContext* pc = mDocument->GetPresContext()) {
234 pc->RefreshDriver()->EnsureIntersectionObservationsUpdateHappens();
239 void DOMIntersectionObserver::Unobserve(Element& aTarget) {
240 if (!mObservationTargets.Contains(&aTarget)) {
241 return;
244 if (mObservationTargets.Length() == 1) {
245 Disconnect();
246 return;
249 mObservationTargets.RemoveElement(&aTarget);
250 aTarget.UnregisterIntersectionObserver(this);
253 void DOMIntersectionObserver::UnlinkTarget(Element& aTarget) {
254 mObservationTargets.RemoveElement(&aTarget);
255 if (mObservationTargets.Length() == 0) {
256 Disconnect();
260 void DOMIntersectionObserver::Connect() {
261 if (mConnected) {
262 return;
265 mConnected = true;
266 if (mDocument) {
267 mDocument->AddIntersectionObserver(this);
271 void DOMIntersectionObserver::Disconnect() {
272 if (!mConnected) {
273 return;
276 mConnected = false;
277 for (size_t i = 0; i < mObservationTargets.Length(); ++i) {
278 Element* target = mObservationTargets.ElementAt(i);
279 target->UnregisterIntersectionObserver(this);
281 mObservationTargets.Clear();
282 if (mDocument) {
283 mDocument->RemoveIntersectionObserver(this);
287 void DOMIntersectionObserver::TakeRecords(
288 nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal) {
289 aRetVal = std::move(mQueuedEntries);
292 static Maybe<nsRect> EdgeInclusiveIntersection(const nsRect& aRect,
293 const nsRect& aOtherRect) {
294 nscoord left = std::max(aRect.x, aOtherRect.x);
295 nscoord top = std::max(aRect.y, aOtherRect.y);
296 nscoord right = std::min(aRect.XMost(), aOtherRect.XMost());
297 nscoord bottom = std::min(aRect.YMost(), aOtherRect.YMost());
298 if (left > right || top > bottom) {
299 return Nothing();
301 return Some(nsRect(left, top, right - left, bottom - top));
304 enum class BrowsingContextOrigin { Similar, Different };
306 // NOTE(emilio): Checking docgroup as per discussion in:
307 // https://github.com/w3c/IntersectionObserver/issues/161
308 static BrowsingContextOrigin SimilarOrigin(const Element& aTarget,
309 const nsINode* aRoot) {
310 if (!aRoot) {
311 return BrowsingContextOrigin::Different;
313 return aTarget.OwnerDoc()->GetDocGroup() == aRoot->OwnerDoc()->GetDocGroup()
314 ? BrowsingContextOrigin::Similar
315 : BrowsingContextOrigin::Different;
318 // NOTE: This returns nullptr if |aDocument| is in another process from the top
319 // level content document.
320 static Document* GetTopLevelContentDocumentInThisProcess(Document& aDocument) {
321 auto* wc = aDocument.GetTopLevelWindowContext();
322 return wc ? wc->GetExtantDoc() : nullptr;
325 // https://w3c.github.io/IntersectionObserver/#compute-the-intersection
327 // TODO(emilio): Proof of this being equivalent to the spec welcome, seems
328 // reasonably close.
330 // Also, it's unclear to me why the spec talks about browsing context while
331 // discarding observations of targets of different documents.
333 // Both aRootBounds and the return value are relative to
334 // nsLayoutUtils::GetContainingBlockForClientRect(aRoot).
336 // In case of out-of-process document, aRemoteDocumentVisibleRect is a rectangle
337 // in the out-of-process document's coordinate system.
338 static Maybe<nsRect> ComputeTheIntersection(
339 nsIFrame* aTarget, nsIFrame* aRoot, const nsRect& aRootBounds,
340 const Maybe<nsRect>& aRemoteDocumentVisibleRect) {
341 nsIFrame* target = aTarget;
342 // 1. Let intersectionRect be the result of running the
343 // getBoundingClientRect() algorithm on the target.
345 // `intersectionRect` is kept relative to `target` during the loop.
346 Maybe<nsRect> intersectionRect = Some(nsLayoutUtils::GetAllInFlowRectsUnion(
347 target, target, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS));
349 // 2. Let container be the containing block of the target.
350 // (We go through the parent chain and only look at scroll frames)
352 // FIXME(emilio): Spec uses containing blocks, we use scroll frames, but we
353 // only apply overflow-clipping, not clip-path, so it's ~fine. We do need to
354 // apply clip-path.
356 // 3. While container is not the intersection root:
357 nsIFrame* containerFrame =
358 nsLayoutUtils::GetCrossDocParentFrameInProcess(target);
359 while (containerFrame && containerFrame != aRoot) {
360 // FIXME(emilio): What about other scroll frames that inherit from
361 // nsHTMLScrollFrame but have a different type, like nsListControlFrame?
362 // This looks bogus in that case, but different bug.
363 if (nsIScrollableFrame* scrollFrame = do_QueryFrame(containerFrame)) {
364 if (containerFrame->GetParent() == aRoot && !aRoot->GetParent()) {
365 // This is subtle: if we're computing the intersection against the
366 // viewport (the root frame), and this is its scroll frame, we really
367 // want to skip this intersection (because we want to account for the
368 // root margin, which is already in aRootBounds).
369 break;
371 nsRect subFrameRect = scrollFrame->GetScrollPortRect();
373 // 3.1 Map intersectionRect to the coordinate space of container.
374 nsRect intersectionRectRelativeToContainer =
375 nsLayoutUtils::TransformFrameRectToAncestor(
376 target, intersectionRect.value(), containerFrame);
378 // 3.2 If container has overflow clipping or a css clip-path property,
379 // update intersectionRect by applying container's clip.
381 // TODO: Apply clip-path.
383 // 3.3 is handled, looks like, by this same clipping, given the root
384 // scroll-frame cannot escape the viewport, probably?
386 intersectionRect = EdgeInclusiveIntersection(
387 intersectionRectRelativeToContainer, subFrameRect);
388 if (!intersectionRect) {
389 return Nothing();
391 target = containerFrame;
394 containerFrame =
395 nsLayoutUtils::GetCrossDocParentFrameInProcess(containerFrame);
397 MOZ_ASSERT(intersectionRect);
399 // 4. Map intersectionRect to the coordinate space of the intersection root.
400 nsRect intersectionRectRelativeToRoot =
401 nsLayoutUtils::TransformFrameRectToAncestor(
402 target, intersectionRect.value(),
403 nsLayoutUtils::GetContainingBlockForClientRect(aRoot));
405 // 5.Update intersectionRect by intersecting it with the root intersection
406 // rectangle.
407 intersectionRect =
408 EdgeInclusiveIntersection(intersectionRectRelativeToRoot, aRootBounds);
409 if (intersectionRect.isNothing()) {
410 return Nothing();
412 // 6. Map intersectionRect to the coordinate space of the viewport of the
413 // Document containing the target.
415 // FIXME(emilio): I think this may not be correct if the root is explicit
416 // and in the same document, since then the rectangle may not be relative to
417 // the viewport already (but it's in the same document).
418 nsRect rect = intersectionRect.value();
419 if (aTarget->PresContext() != aRoot->PresContext()) {
420 if (nsIFrame* rootScrollFrame =
421 aTarget->PresShell()->GetRootScrollFrame()) {
422 nsLayoutUtils::TransformRect(aRoot, rootScrollFrame, rect);
426 // In out-of-process iframes we need to take an intersection with the remote
427 // document visible rect which was already clipped by ancestor document's
428 // viewports.
429 if (aRemoteDocumentVisibleRect) {
430 MOZ_ASSERT(aRoot->PresContext()->IsRootContentDocumentInProcess() &&
431 !aRoot->PresContext()->IsRootContentDocumentCrossProcess());
433 intersectionRect =
434 EdgeInclusiveIntersection(rect, *aRemoteDocumentVisibleRect);
435 if (intersectionRect.isNothing()) {
436 return Nothing();
438 rect = intersectionRect.value();
441 return Some(rect);
444 struct OopIframeMetrics {
445 nsIFrame* mInProcessRootFrame = nullptr;
446 nsRect mInProcessRootRect;
447 nsRect mRemoteDocumentVisibleRect;
450 static Maybe<OopIframeMetrics> GetOopIframeMetrics(Document& aDocument,
451 Document* aRootDocument) {
452 Document* rootDoc = nsContentUtils::GetRootDocument(&aDocument);
453 MOZ_ASSERT(rootDoc);
455 if (rootDoc->IsTopLevelContentDocument()) {
456 return Nothing();
459 if (aRootDocument &&
460 rootDoc == nsContentUtils::GetRootDocument(aRootDocument)) {
461 // aRootDoc, if non-null, is either the implicit root
462 // (top-level-content-document) or a same-origin document passed explicitly.
464 // In the former case, we should've returned above if there are no iframes
465 // in between. This condition handles the explicit, same-origin root
466 // document, when both are embedded in an OOP iframe.
467 return Nothing();
470 PresShell* rootPresShell = rootDoc->GetPresShell();
471 if (!rootPresShell || rootPresShell->IsDestroying()) {
472 return Some(OopIframeMetrics{});
475 nsIFrame* inProcessRootFrame = rootPresShell->GetRootFrame();
476 if (!inProcessRootFrame) {
477 return Some(OopIframeMetrics{});
480 BrowserChild* browserChild = BrowserChild::GetFrom(rootDoc->GetDocShell());
481 if (!browserChild) {
482 return Some(OopIframeMetrics{});
484 MOZ_DIAGNOSTIC_ASSERT(!browserChild->IsTopLevel());
486 nsRect inProcessRootRect;
487 if (nsIScrollableFrame* scrollFrame =
488 rootPresShell->GetRootScrollFrameAsScrollable()) {
489 inProcessRootRect = scrollFrame->GetScrollPortRect();
492 Maybe<LayoutDeviceRect> remoteDocumentVisibleRect =
493 browserChild->GetTopLevelViewportVisibleRectInSelfCoords();
494 if (!remoteDocumentVisibleRect) {
495 return Some(OopIframeMetrics{});
498 return Some(OopIframeMetrics{
499 inProcessRootFrame,
500 inProcessRootRect,
501 LayoutDeviceRect::ToAppUnits(
502 *remoteDocumentVisibleRect,
503 rootPresShell->GetPresContext()->AppUnitsPerDevPixel()),
507 // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
508 // (step 2)
509 void DOMIntersectionObserver::Update(Document* aDocument,
510 DOMHighResTimeStamp time) {
511 // 1 - Let rootBounds be observer's root intersection rectangle.
512 // ... but since the intersection rectangle depends on the target, we defer
513 // the inflation until later.
514 // NOTE: |rootRect| and |rootFrame| will be root in the same process. In
515 // out-of-process iframes, they are NOT root ones of the top level content
516 // document.
517 nsRect rootRect;
518 nsIFrame* rootFrame = nullptr;
519 nsINode* root = mRoot;
520 Maybe<nsRect> remoteDocumentVisibleRect;
521 if (mRoot && mRoot->IsElement()) {
522 if ((rootFrame = mRoot->AsElement()->GetPrimaryFrame())) {
523 nsRect rootRectRelativeToRootFrame;
524 if (nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame)) {
525 // rootRectRelativeToRootFrame should be the content rect of rootFrame,
526 // not including the scrollbars.
527 rootRectRelativeToRootFrame = scrollFrame->GetScrollPortRect();
528 } else {
529 // rootRectRelativeToRootFrame should be the border rect of rootFrame.
530 rootRectRelativeToRootFrame = rootFrame->GetRectRelativeToSelf();
532 nsIFrame* containingBlock =
533 nsLayoutUtils::GetContainingBlockForClientRect(rootFrame);
534 rootRect = nsLayoutUtils::TransformFrameRectToAncestor(
535 rootFrame, rootRectRelativeToRootFrame, containingBlock);
537 } else {
538 MOZ_ASSERT(!mRoot || mRoot->IsDocument());
539 Document* rootDocument =
540 mRoot ? mRoot->AsDocument()
541 : GetTopLevelContentDocumentInThisProcess(*aDocument);
542 root = rootDocument;
544 if (rootDocument) {
545 // We're in the same process as the root document, though note that there
546 // could be an out-of-process iframe in between us and the root. Grab the
547 // root frame and the root rect.
549 // Note that the root rect is always good (we assume no DPI changes in
550 // between the two documents, and we don't need to convert coordinates).
552 // The root frame however we may need to tweak in the block below, if
553 // there's any OOP iframe in between `rootDocument` and `aDocument`, to
554 // handle the OOP iframe positions.
555 if (PresShell* presShell = rootDocument->GetPresShell()) {
556 rootFrame = presShell->GetRootFrame();
557 // We use the root scrollable frame's scroll port to account the
558 // scrollbars in rootRect, if needed.
559 if (nsIScrollableFrame* scrollFrame =
560 presShell->GetRootScrollFrameAsScrollable()) {
561 rootRect = scrollFrame->GetScrollPortRect();
566 if (Maybe<OopIframeMetrics> metrics =
567 GetOopIframeMetrics(*aDocument, rootDocument)) {
568 rootFrame = metrics->mInProcessRootFrame;
569 if (!rootDocument) {
570 rootRect = metrics->mInProcessRootRect;
572 remoteDocumentVisibleRect = Some(metrics->mRemoteDocumentVisibleRect);
576 nsMargin rootMargin; // This root margin is NOT applied in `implicit root`
577 // case, e.g. in out-of-process iframes.
578 for (const auto side : mozilla::AllPhysicalSides()) {
579 nscoord basis = side == eSideTop || side == eSideBottom ? rootRect.Height()
580 : rootRect.Width();
581 rootMargin.Side(side) =
582 mRootMargin.Get(side).Resolve(basis, NSToCoordRoundWithClamp);
585 // 2. For each target in observer’s internal [[ObservationTargets]] slot,
586 // processed in the same order that observe() was called on each target:
587 for (Element* target : mObservationTargets) {
588 nsIFrame* targetFrame = target->GetPrimaryFrame();
589 BrowsingContextOrigin origin = SimilarOrigin(*target, root);
591 Maybe<nsRect> intersectionRect;
592 nsRect targetRect;
593 nsRect rootBounds;
595 const bool canComputeIntersection = [&] {
596 if (!targetFrame || !rootFrame) {
597 return false;
600 // 2.1. If the intersection root is not the implicit root and target is
601 // not a descendant of the intersection root in the containing block
602 // chain, skip further processing for target.
604 // NOTE(emilio): We don't just "skip further processing" because that
605 // violates the invariant that there's at least one observation for a
606 // target (though that is also violated by 2.2), but it also causes
607 // different behavior when `target` is `display: none`, or not, which is
608 // really really odd, see:
609 // https://github.com/w3c/IntersectionObserver/issues/457
611 // NOTE(emilio): We also do this if target is the implicit root, pending
612 // clarification in
613 // https://github.com/w3c/IntersectionObserver/issues/456.
614 if (rootFrame == targetFrame ||
615 !nsLayoutUtils::IsAncestorFrameCrossDoc(rootFrame, targetFrame)) {
616 return false;
619 // 2.2. If the intersection root is not the implicit root, and target is
620 // not in the same Document as the intersection root, skip further
621 // processing for target.
623 // NOTE(emilio): We don't just "skip further processing", because that
624 // doesn't match reality and other browsers, see
625 // https://github.com/w3c/IntersectionObserver/issues/457.
626 if (mRoot && mRoot->OwnerDoc() != target->OwnerDoc()) {
627 return false;
630 return true;
631 }();
633 if (canComputeIntersection) {
634 rootBounds = rootRect;
635 if (origin == BrowsingContextOrigin::Similar) {
636 rootBounds.Inflate(rootMargin);
639 // 2.3. Let targetRect be a DOMRectReadOnly obtained by running the
640 // getBoundingClientRect() algorithm on target.
641 targetRect = targetFrame->GetBoundingClientRect();
643 // 2.4. Let intersectionRect be the result of running the compute the
644 // intersection algorithm on target.
645 intersectionRect = ComputeTheIntersection(
646 targetFrame, rootFrame, rootBounds, remoteDocumentVisibleRect);
649 // 2.5. Let targetArea be targetRect’s area.
650 int64_t targetArea =
651 (int64_t)targetRect.Width() * (int64_t)targetRect.Height();
652 // 2.6. Let intersectionArea be intersectionRect’s area.
653 int64_t intersectionArea = !intersectionRect
655 : (int64_t)intersectionRect->Width() *
656 (int64_t)intersectionRect->Height();
658 // 2.7. Let isIntersecting be true if targetRect and rootBounds intersect or
659 // are edge-adjacent, even if the intersection has zero area (because
660 // rootBounds or targetRect have zero area); otherwise, let isIntersecting
661 // be false.
662 const bool isIntersecting = intersectionRect.isSome();
664 // 2.8. If targetArea is non-zero, let intersectionRatio be intersectionArea
665 // divided by targetArea. Otherwise, let intersectionRatio be 1 if
666 // isIntersecting is true, or 0 if isIntersecting is false.
667 double intersectionRatio;
668 if (targetArea > 0.0) {
669 intersectionRatio =
670 std::min((double)intersectionArea / (double)targetArea, 1.0);
671 } else {
672 intersectionRatio = isIntersecting ? 1.0 : 0.0;
675 // 2.9 Let thresholdIndex be the index of the first entry in
676 // observer.thresholds whose value is greater than intersectionRatio, or the
677 // length of observer.thresholds if intersectionRatio is greater than or
678 // equal to the last entry in observer.thresholds.
679 int32_t thresholdIndex = -1;
681 // If not intersecting, we can just shortcut, as we know that the thresholds
682 // are always between 0 and 1.
683 if (isIntersecting) {
684 thresholdIndex = mThresholds.IndexOfFirstElementGt(intersectionRatio);
685 if (thresholdIndex == 0) {
686 // Per the spec, we should leave threshold at 0 and distinguish between
687 // "less than all thresholds and intersecting" and "not intersecting"
688 // (queuing observer entries as both cases come to pass). However,
689 // neither Chrome nor the WPT tests expect this behavior, so treat these
690 // two cases as one.
692 // See https://github.com/w3c/IntersectionObserver/issues/432 about
693 // this.
694 thresholdIndex = -1;
698 // Steps 2.10 - 2.15.
699 if (target->UpdateIntersectionObservation(this, thresholdIndex)) {
700 // See https://github.com/w3c/IntersectionObserver/issues/432 about
701 // why we use thresholdIndex > 0 rather than isIntersecting for the
702 // entry's isIntersecting value.
703 QueueIntersectionObserverEntry(
704 target, time,
705 origin == BrowsingContextOrigin::Similar ? Some(rootBounds)
706 : Nothing(),
707 targetRect, intersectionRect, thresholdIndex > 0, intersectionRatio);
712 void DOMIntersectionObserver::QueueIntersectionObserverEntry(
713 Element* aTarget, DOMHighResTimeStamp time, const Maybe<nsRect>& aRootRect,
714 const nsRect& aTargetRect, const Maybe<nsRect>& aIntersectionRect,
715 bool aIsIntersecting, double aIntersectionRatio) {
716 RefPtr<DOMRect> rootBounds;
717 if (aRootRect.isSome()) {
718 rootBounds = new DOMRect(this);
719 rootBounds->SetLayoutRect(aRootRect.value());
721 RefPtr<DOMRect> boundingClientRect = new DOMRect(this);
722 boundingClientRect->SetLayoutRect(aTargetRect);
723 RefPtr<DOMRect> intersectionRect = new DOMRect(this);
724 if (aIntersectionRect.isSome()) {
725 intersectionRect->SetLayoutRect(aIntersectionRect.value());
727 RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry(
728 this, time, rootBounds.forget(), boundingClientRect.forget(),
729 intersectionRect.forget(), aIsIntersecting, aTarget, aIntersectionRatio);
730 mQueuedEntries.AppendElement(entry.forget());
733 void DOMIntersectionObserver::Notify() {
734 if (!mQueuedEntries.Length()) {
735 return;
737 Sequence<OwningNonNull<DOMIntersectionObserverEntry>> entries;
738 if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) {
739 for (size_t i = 0; i < mQueuedEntries.Length(); ++i) {
740 RefPtr<DOMIntersectionObserverEntry> next = mQueuedEntries[i];
741 *entries.AppendElement(mozilla::fallible) = next;
744 mQueuedEntries.Clear();
746 if (mCallback.is<RefPtr<dom::IntersectionCallback>>()) {
747 RefPtr<dom::IntersectionCallback> callback(
748 mCallback.as<RefPtr<dom::IntersectionCallback>>());
749 callback->Call(this, entries, *this);
750 } else {
751 mCallback.as<NativeCallback>()(entries);
755 } // namespace mozilla::dom