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/. */
6 
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/dom/BrowserChild.h"
19 #include "mozilla/dom/BrowsingContext.h"
20 #include "mozilla/dom/DocumentInlines.h"
21 #include "mozilla/dom/HTMLImageElement.h"
22 #include "Units.h"
23 
24 namespace mozilla::dom {
25 
26 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry)
27   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
28   NS_INTERFACE_MAP_ENTRY(nsISupports)
29 NS_INTERFACE_MAP_END
30 
31 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserverEntry)
32 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserverEntry)
33 
34 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DOMIntersectionObserverEntry, mOwner,
35                                       mRootBounds, mBoundingClientRect,
36                                       mIntersectionRect, mTarget)
37 
38 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserver)
39   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
40   NS_INTERFACE_MAP_ENTRY(nsISupports)
41   NS_INTERFACE_MAP_ENTRY(DOMIntersectionObserver)
42 NS_INTERFACE_MAP_END
43 
44 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserver)
45 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserver)
46 
47 NS_IMPL_CYCLE_COLLECTION_CLASS(DOMIntersectionObserver)
48 
49 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(DOMIntersectionObserver)
50   NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
51 NS_IMPL_CYCLE_COLLECTION_TRACE_END
52 
53 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMIntersectionObserver)
54   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
55   tmp->Disconnect();
56   NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
57   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
58   if (tmp->mCallback.is<RefPtr<dom::IntersectionCallback>>()) {
59     ImplCycleCollectionUnlink(
60         tmp->mCallback.as<RefPtr<dom::IntersectionCallback>>());
61   }
62   NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoot)
63   NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries)
64 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
65 
66 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMIntersectionObserver)
67   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
68   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
69   if (tmp->mCallback.is<RefPtr<dom::IntersectionCallback>>()) {
70     ImplCycleCollectionTraverse(
71         cb, tmp->mCallback.as<RefPtr<dom::IntersectionCallback>>(), "mCallback",
72         0);
73   }
74   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries)75   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries)
76 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
77 
78 DOMIntersectionObserver::DOMIntersectionObserver(
79     already_AddRefed<nsPIDOMWindowInner>&& aOwner,
80     dom::IntersectionCallback& aCb)
81     : mOwner(aOwner),
82       mDocument(mOwner->GetExtantDoc()),
83       mCallback(RefPtr<dom::IntersectionCallback>(&aCb)),
84       mConnected(false) {}
85 
Constructor(const GlobalObject & aGlobal,dom::IntersectionCallback & aCb,ErrorResult & aRv)86 already_AddRefed<DOMIntersectionObserver> DOMIntersectionObserver::Constructor(
87     const GlobalObject& aGlobal, dom::IntersectionCallback& aCb,
88     ErrorResult& aRv) {
89   return Constructor(aGlobal, aCb, IntersectionObserverInit(), aRv);
90 }
91 
Constructor(const GlobalObject & aGlobal,dom::IntersectionCallback & aCb,const IntersectionObserverInit & aOptions,ErrorResult & aRv)92 already_AddRefed<DOMIntersectionObserver> DOMIntersectionObserver::Constructor(
93     const GlobalObject& aGlobal, dom::IntersectionCallback& aCb,
94     const IntersectionObserverInit& aOptions, ErrorResult& aRv) {
95   nsCOMPtr<nsPIDOMWindowInner> window =
96       do_QueryInterface(aGlobal.GetAsSupports());
97   if (!window) {
98     aRv.Throw(NS_ERROR_FAILURE);
99     return nullptr;
100   }
101   RefPtr<DOMIntersectionObserver> observer =
102       new DOMIntersectionObserver(window.forget(), aCb);
103 
104   if (!aOptions.mRoot.IsNull()) {
105     if (aOptions.mRoot.Value().IsElement()) {
106       observer->mRoot = aOptions.mRoot.Value().GetAsElement();
107     } else {
108       MOZ_ASSERT(aOptions.mRoot.Value().IsDocument());
109       if (!StaticPrefs::
110               dom_IntersectionObserverExplicitDocumentRoot_enabled()) {
111         aRv.ThrowTypeError<dom::MSG_DOES_NOT_IMPLEMENT_INTERFACE>(
112             "'root' member of IntersectionObserverInit", "Element");
113         return nullptr;
114       }
115       observer->mRoot = aOptions.mRoot.Value().GetAsDocument();
116     }
117   }
118 
119   if (!observer->SetRootMargin(aOptions.mRootMargin)) {
120     aRv.ThrowSyntaxError("rootMargin must be specified in pixels or percent.");
121     return nullptr;
122   }
123 
124   if (aOptions.mThreshold.IsDoubleSequence()) {
125     const Sequence<double>& thresholds =
126         aOptions.mThreshold.GetAsDoubleSequence();
127     observer->mThresholds.SetCapacity(thresholds.Length());
128     for (const auto& thresh : thresholds) {
129       if (thresh < 0.0 || thresh > 1.0) {
130         aRv.ThrowRangeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
131         return nullptr;
132       }
133       observer->mThresholds.AppendElement(thresh);
134     }
135     observer->mThresholds.Sort();
136   } else {
137     double thresh = aOptions.mThreshold.GetAsDouble();
138     if (thresh < 0.0 || thresh > 1.0) {
139       aRv.ThrowRangeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
140       return nullptr;
141     }
142     observer->mThresholds.AppendElement(thresh);
143   }
144 
145   return observer.forget();
146 }
147 
LazyLoadCallback(const Sequence<OwningNonNull<DOMIntersectionObserverEntry>> & aEntries)148 static void LazyLoadCallback(
149     const Sequence<OwningNonNull<DOMIntersectionObserverEntry>>& aEntries) {
150   for (const auto& entry : aEntries) {
151     MOZ_ASSERT(entry->Target()->IsHTMLElement(nsGkAtoms::img));
152     if (entry->IsIntersecting()) {
153       static_cast<HTMLImageElement*>(entry->Target())
154           ->StopLazyLoading(HTMLImageElement::FromIntersectionObserver::Yes,
155                             HTMLImageElement::StartLoading::Yes);
156     }
157   }
158 }
159 
LazyLoadCallbackReachViewport(const Sequence<OwningNonNull<DOMIntersectionObserverEntry>> & aEntries)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();
167     }
168   }
169 }
170 
PrefMargin(float aValue,bool aIsPercentage)171 static LengthPercentage PrefMargin(float aValue, bool aIsPercentage) {
172   return aIsPercentage ? LengthPercentage::FromPercentage(aValue / 100.0f)
173                        : LengthPercentage::FromPixels(aValue);
174 }
175 
DOMIntersectionObserver(Document & aDocument,NativeCallback aCallback)176 DOMIntersectionObserver::DOMIntersectionObserver(Document& aDocument,
177                                                  NativeCallback aCallback)
178     : mOwner(aDocument.GetInnerWindow()),
179       mDocument(&aDocument),
180       mCallback(aCallback),
181       mConnected(false) {}
182 
183 already_AddRefed<DOMIntersectionObserver>
CreateLazyLoadObserver(Document & aDocument)184 DOMIntersectionObserver::CreateLazyLoadObserver(Document& aDocument) {
185   RefPtr<DOMIntersectionObserver> observer =
186       new DOMIntersectionObserver(aDocument, LazyLoadCallback);
187   observer->mThresholds.AppendElement(std::numeric_limits<double>::min());
188 
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
199 
200   return observer.forget();
201 }
202 
203 already_AddRefed<DOMIntersectionObserver>
CreateLazyLoadObserverViewport(Document & aDocument)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();
209 }
210 
SetRootMargin(const nsACString & aString)211 bool DOMIntersectionObserver::SetRootMargin(const nsACString& aString) {
212   return Servo_IntersectionObserverRootMargin_Parse(&aString, &mRootMargin);
213 }
214 
GetParentObject() const215 nsISupports* DOMIntersectionObserver::GetParentObject() const { return mOwner; }
216 
GetRootMargin(nsACString & aRetVal)217 void DOMIntersectionObserver::GetRootMargin(nsACString& aRetVal) {
218   Servo_IntersectionObserverRootMargin_ToString(&mRootMargin, &aRetVal);
219 }
220 
GetThresholds(nsTArray<double> & aRetVal)221 void DOMIntersectionObserver::GetThresholds(nsTArray<double>& aRetVal) {
222   aRetVal = mThresholds.Clone();
223 }
224 
Observe(Element & aTarget)225 void DOMIntersectionObserver::Observe(Element& aTarget) {
226   if (!mObservationTargetSet.EnsureInserted(&aTarget)) {
227     return;
228   }
229   aTarget.RegisterIntersectionObserver(this);
230   mObservationTargets.AppendElement(&aTarget);
231 
232   MOZ_ASSERT(mObservationTargets.Length() == mObservationTargetSet.Count());
233 
234   Connect();
235   if (mDocument) {
236     if (nsPresContext* pc = mDocument->GetPresContext()) {
237       pc->RefreshDriver()->EnsureIntersectionObservationsUpdateHappens();
238     }
239   }
240 }
241 
Unobserve(Element & aTarget)242 void DOMIntersectionObserver::Unobserve(Element& aTarget) {
243   if (!mObservationTargetSet.EnsureRemoved(&aTarget)) {
244     return;
245   }
246 
247   mObservationTargets.RemoveElement(&aTarget);
248   aTarget.UnregisterIntersectionObserver(this);
249 
250   MOZ_ASSERT(mObservationTargets.Length() == mObservationTargetSet.Count());
251 
252   if (mObservationTargets.IsEmpty()) {
253     Disconnect();
254   }
255 }
256 
UnlinkTarget(Element & aTarget)257 void DOMIntersectionObserver::UnlinkTarget(Element& aTarget) {
258   mObservationTargets.RemoveElement(&aTarget);
259   mObservationTargetSet.Remove(&aTarget);
260   if (mObservationTargets.IsEmpty()) {
261     Disconnect();
262   }
263 }
264 
Connect()265 void DOMIntersectionObserver::Connect() {
266   if (mConnected) {
267     return;
268   }
269 
270   mConnected = true;
271   if (mDocument) {
272     mDocument->AddIntersectionObserver(this);
273   }
274 }
275 
Disconnect()276 void DOMIntersectionObserver::Disconnect() {
277   if (!mConnected) {
278     return;
279   }
280 
281   mConnected = false;
282   for (Element* target : mObservationTargets) {
283     target->UnregisterIntersectionObserver(this);
284   }
285   mObservationTargets.Clear();
286   mObservationTargetSet.Clear();
287   if (mDocument) {
288     mDocument->RemoveIntersectionObserver(this);
289   }
290 }
291 
TakeRecords(nsTArray<RefPtr<DOMIntersectionObserverEntry>> & aRetVal)292 void DOMIntersectionObserver::TakeRecords(
293     nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal) {
294   aRetVal = std::move(mQueuedEntries);
295 }
296 
EdgeInclusiveIntersection(const nsRect & aRect,const nsRect & aOtherRect)297 static Maybe<nsRect> EdgeInclusiveIntersection(const nsRect& aRect,
298                                                const nsRect& aOtherRect) {
299   nscoord left = std::max(aRect.x, aOtherRect.x);
300   nscoord top = std::max(aRect.y, aOtherRect.y);
301   nscoord right = std::min(aRect.XMost(), aOtherRect.XMost());
302   nscoord bottom = std::min(aRect.YMost(), aOtherRect.YMost());
303   if (left > right || top > bottom) {
304     return Nothing();
305   }
306   return Some(nsRect(left, top, right - left, bottom - top));
307 }
308 
309 enum class BrowsingContextOrigin { Similar, Different };
310 
311 // NOTE(emilio): Checking docgroup as per discussion in:
312 // https://github.com/w3c/IntersectionObserver/issues/161
SimilarOrigin(const Element & aTarget,const nsINode * aRoot)313 static BrowsingContextOrigin SimilarOrigin(const Element& aTarget,
314                                            const nsINode* aRoot) {
315   if (!aRoot) {
316     return BrowsingContextOrigin::Different;
317   }
318   return aTarget.OwnerDoc()->GetDocGroup() == aRoot->OwnerDoc()->GetDocGroup()
319              ? BrowsingContextOrigin::Similar
320              : BrowsingContextOrigin::Different;
321 }
322 
323 // NOTE: This returns nullptr if |aDocument| is in another process from the top
324 // level content document.
GetTopLevelContentDocumentInThisProcess(Document & aDocument)325 static Document* GetTopLevelContentDocumentInThisProcess(Document& aDocument) {
326   auto* wc = aDocument.GetTopLevelWindowContext();
327   return wc ? wc->GetExtantDoc() : nullptr;
328 }
329 
330 // https://w3c.github.io/IntersectionObserver/#compute-the-intersection
331 //
332 // TODO(emilio): Proof of this being equivalent to the spec welcome, seems
333 // reasonably close.
334 //
335 // Also, it's unclear to me why the spec talks about browsing context while
336 // discarding observations of targets of different documents.
337 //
338 // Both aRootBounds and the return value are relative to
339 // nsLayoutUtils::GetContainingBlockForClientRect(aRoot).
340 //
341 // In case of out-of-process document, aRemoteDocumentVisibleRect is a rectangle
342 // in the out-of-process document's coordinate system.
ComputeTheIntersection(nsIFrame * aTarget,nsIFrame * aRoot,const nsRect & aRootBounds,const Maybe<nsRect> & aRemoteDocumentVisibleRect)343 static Maybe<nsRect> ComputeTheIntersection(
344     nsIFrame* aTarget, nsIFrame* aRoot, const nsRect& aRootBounds,
345     const Maybe<nsRect>& aRemoteDocumentVisibleRect) {
346   nsIFrame* target = aTarget;
347   // 1. Let intersectionRect be the result of running the
348   // getBoundingClientRect() algorithm on the target.
349   //
350   // `intersectionRect` is kept relative to `target` during the loop.
351   Maybe<nsRect> intersectionRect = Some(nsLayoutUtils::GetAllInFlowRectsUnion(
352       target, target, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS));
353 
354   // 2. Let container be the containing block of the target.
355   // (We go through the parent chain and only look at scroll frames)
356   //
357   // FIXME(emilio): Spec uses containing blocks, we use scroll frames, but we
358   // only apply overflow-clipping, not clip-path, so it's ~fine. We do need to
359   // apply clip-path.
360   //
361   // 3. While container is not the intersection root:
362   nsIFrame* containerFrame =
363       nsLayoutUtils::GetCrossDocParentFrameInProcess(target);
364   while (containerFrame && containerFrame != aRoot) {
365     // FIXME(emilio): What about other scroll frames that inherit from
366     // nsHTMLScrollFrame but have a different type, like nsListControlFrame?
367     // This looks bogus in that case, but different bug.
368     if (nsIScrollableFrame* scrollFrame = do_QueryFrame(containerFrame)) {
369       if (containerFrame->GetParent() == aRoot && !aRoot->GetParent()) {
370         // This is subtle: if we're computing the intersection against the
371         // viewport (the root frame), and this is its scroll frame, we really
372         // want to skip this intersection (because we want to account for the
373         // root margin, which is already in aRootBounds).
374         break;
375       }
376       nsRect subFrameRect = scrollFrame->GetScrollPortRect();
377 
378       // 3.1 Map intersectionRect to the coordinate space of container.
379       nsRect intersectionRectRelativeToContainer =
380           nsLayoutUtils::TransformFrameRectToAncestor(
381               target, intersectionRect.value(), containerFrame);
382 
383       // 3.2 If container has overflow clipping or a css clip-path property,
384       // update intersectionRect by applying container's clip.
385       //
386       // TODO: Apply clip-path.
387       //
388       // 3.3 is handled, looks like, by this same clipping, given the root
389       // scroll-frame cannot escape the viewport, probably?
390       //
391       intersectionRect = EdgeInclusiveIntersection(
392           intersectionRectRelativeToContainer, subFrameRect);
393       if (!intersectionRect) {
394         return Nothing();
395       }
396       target = containerFrame;
397     }
398 
399     containerFrame =
400         nsLayoutUtils::GetCrossDocParentFrameInProcess(containerFrame);
401   }
402   MOZ_ASSERT(intersectionRect);
403 
404   // 4. Map intersectionRect to the coordinate space of the intersection root.
405   nsRect intersectionRectRelativeToRoot =
406       nsLayoutUtils::TransformFrameRectToAncestor(
407           target, intersectionRect.value(),
408           nsLayoutUtils::GetContainingBlockForClientRect(aRoot));
409 
410   // 5.Update intersectionRect by intersecting it with the root intersection
411   // rectangle.
412   intersectionRect =
413       EdgeInclusiveIntersection(intersectionRectRelativeToRoot, aRootBounds);
414   if (intersectionRect.isNothing()) {
415     return Nothing();
416   }
417   // 6. Map intersectionRect to the coordinate space of the viewport of the
418   // Document containing the target.
419   //
420   // FIXME(emilio): I think this may not be correct if the root is explicit
421   // and in the same document, since then the rectangle may not be relative to
422   // the viewport already (but it's in the same document).
423   nsRect rect = intersectionRect.value();
424   if (aTarget->PresContext() != aRoot->PresContext()) {
425     if (nsIFrame* rootScrollFrame =
426             aTarget->PresShell()->GetRootScrollFrame()) {
427       nsLayoutUtils::TransformRect(aRoot, rootScrollFrame, rect);
428     }
429   }
430 
431   // In out-of-process iframes we need to take an intersection with the remote
432   // document visible rect which was already clipped by ancestor document's
433   // viewports.
434   if (aRemoteDocumentVisibleRect) {
435     MOZ_ASSERT(aRoot->PresContext()->IsRootContentDocumentInProcess() &&
436                !aRoot->PresContext()->IsRootContentDocumentCrossProcess());
437 
438     intersectionRect =
439         EdgeInclusiveIntersection(rect, *aRemoteDocumentVisibleRect);
440     if (intersectionRect.isNothing()) {
441       return Nothing();
442     }
443     rect = intersectionRect.value();
444   }
445 
446   return Some(rect);
447 }
448 
449 struct OopIframeMetrics {
450   nsIFrame* mInProcessRootFrame = nullptr;
451   nsRect mInProcessRootRect;
452   nsRect mRemoteDocumentVisibleRect;
453 };
454 
GetOopIframeMetrics(Document & aDocument,Document * aRootDocument)455 static Maybe<OopIframeMetrics> GetOopIframeMetrics(Document& aDocument,
456                                                    Document* aRootDocument) {
457   Document* rootDoc =
458       nsContentUtils::GetInProcessSubtreeRootDocument(&aDocument);
459   MOZ_ASSERT(rootDoc);
460 
461   if (rootDoc->IsTopLevelContentDocument()) {
462     return Nothing();
463   }
464 
465   if (aRootDocument &&
466       rootDoc ==
467           nsContentUtils::GetInProcessSubtreeRootDocument(aRootDocument)) {
468     // aRootDoc, if non-null, is either the implicit root
469     // (top-level-content-document) or a same-origin document passed explicitly.
470     //
471     // In the former case, we should've returned above if there are no iframes
472     // in between. This condition handles the explicit, same-origin root
473     // document, when both are embedded in an OOP iframe.
474     return Nothing();
475   }
476 
477   PresShell* rootPresShell = rootDoc->GetPresShell();
478   if (!rootPresShell || rootPresShell->IsDestroying()) {
479     return Some(OopIframeMetrics{});
480   }
481 
482   nsIFrame* inProcessRootFrame = rootPresShell->GetRootFrame();
483   if (!inProcessRootFrame) {
484     return Some(OopIframeMetrics{});
485   }
486 
487   BrowserChild* browserChild = BrowserChild::GetFrom(rootDoc->GetDocShell());
488   if (!browserChild) {
489     return Some(OopIframeMetrics{});
490   }
491   MOZ_DIAGNOSTIC_ASSERT(!browserChild->IsTopLevel());
492 
493   nsRect inProcessRootRect;
494   if (nsIScrollableFrame* scrollFrame =
495           rootPresShell->GetRootScrollFrameAsScrollable()) {
496     inProcessRootRect = scrollFrame->GetScrollPortRect();
497   }
498 
499   Maybe<LayoutDeviceRect> remoteDocumentVisibleRect =
500       browserChild->GetTopLevelViewportVisibleRectInSelfCoords();
501   if (!remoteDocumentVisibleRect) {
502     return Some(OopIframeMetrics{});
503   }
504 
505   return Some(OopIframeMetrics{
506       inProcessRootFrame,
507       inProcessRootRect,
508       LayoutDeviceRect::ToAppUnits(
509           *remoteDocumentVisibleRect,
510           rootPresShell->GetPresContext()->AppUnitsPerDevPixel()),
511   });
512 }
513 
514 // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo
515 // (step 2)
Update(Document * aDocument,DOMHighResTimeStamp time)516 void DOMIntersectionObserver::Update(Document* aDocument,
517                                      DOMHighResTimeStamp time) {
518   // 1 - Let rootBounds be observer's root intersection rectangle.
519   //  ... but since the intersection rectangle depends on the target, we defer
520   //      the inflation until later.
521   // NOTE: |rootRect| and |rootFrame| will be root in the same process. In
522   // out-of-process iframes, they are NOT root ones of the top level content
523   // document.
524   nsRect rootRect;
525   nsIFrame* rootFrame = nullptr;
526   nsINode* root = mRoot;
527   Maybe<nsRect> remoteDocumentVisibleRect;
528   if (mRoot && mRoot->IsElement()) {
529     if ((rootFrame = mRoot->AsElement()->GetPrimaryFrame())) {
530       nsRect rootRectRelativeToRootFrame;
531       if (nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame)) {
532         // rootRectRelativeToRootFrame should be the content rect of rootFrame,
533         // not including the scrollbars.
534         rootRectRelativeToRootFrame = scrollFrame->GetScrollPortRect();
535       } else {
536         // rootRectRelativeToRootFrame should be the border rect of rootFrame.
537         rootRectRelativeToRootFrame = rootFrame->GetRectRelativeToSelf();
538       }
539       nsIFrame* containingBlock =
540           nsLayoutUtils::GetContainingBlockForClientRect(rootFrame);
541       rootRect = nsLayoutUtils::TransformFrameRectToAncestor(
542           rootFrame, rootRectRelativeToRootFrame, containingBlock);
543     }
544   } else {
545     MOZ_ASSERT(!mRoot || mRoot->IsDocument());
546     Document* rootDocument =
547         mRoot ? mRoot->AsDocument()
548               : GetTopLevelContentDocumentInThisProcess(*aDocument);
549     root = rootDocument;
550 
551     if (rootDocument) {
552       // We're in the same process as the root document, though note that there
553       // could be an out-of-process iframe in between us and the root. Grab the
554       // root frame and the root rect.
555       //
556       // Note that the root rect is always good (we assume no DPI changes in
557       // between the two documents, and we don't need to convert coordinates).
558       //
559       // The root frame however we may need to tweak in the block below, if
560       // there's any OOP iframe in between `rootDocument` and `aDocument`, to
561       // handle the OOP iframe positions.
562       if (PresShell* presShell = rootDocument->GetPresShell()) {
563         rootFrame = presShell->GetRootFrame();
564         // We use the root scrollable frame's scroll port to account the
565         // scrollbars in rootRect, if needed.
566         if (nsIScrollableFrame* scrollFrame =
567                 presShell->GetRootScrollFrameAsScrollable()) {
568           rootRect = scrollFrame->GetScrollPortRect();
569         }
570       }
571     }
572 
573     if (Maybe<OopIframeMetrics> metrics =
574             GetOopIframeMetrics(*aDocument, rootDocument)) {
575       rootFrame = metrics->mInProcessRootFrame;
576       if (!rootDocument) {
577         rootRect = metrics->mInProcessRootRect;
578       }
579       remoteDocumentVisibleRect = Some(metrics->mRemoteDocumentVisibleRect);
580     }
581   }
582 
583   nsMargin rootMargin;  // This root margin is NOT applied in `implicit root`
584                         // case, e.g. in out-of-process iframes.
585   for (const auto side : mozilla::AllPhysicalSides()) {
586     nscoord basis = side == eSideTop || side == eSideBottom ? rootRect.Height()
587                                                             : rootRect.Width();
588     rootMargin.Side(side) =
589         mRootMargin.Get(side).Resolve(basis, NSToCoordRoundWithClamp);
590   }
591 
592   // 2. For each target in observer’s internal [[ObservationTargets]] slot,
593   // processed in the same order that observe() was called on each target:
594   for (Element* target : mObservationTargets) {
595     nsIFrame* targetFrame = target->GetPrimaryFrame();
596     BrowsingContextOrigin origin = SimilarOrigin(*target, root);
597 
598     Maybe<nsRect> intersectionRect;
599     nsRect targetRect;
600     nsRect rootBounds;
601 
602     const bool canComputeIntersection = [&] {
603       if (!targetFrame || !rootFrame) {
604         return false;
605       }
606 
607       // 2.1. If the intersection root is not the implicit root and target is
608       // not a descendant of the intersection root in the containing block
609       // chain, skip further processing for target.
610       //
611       // NOTE(emilio): We don't just "skip further processing" because that
612       // violates the invariant that there's at least one observation for a
613       // target (though that is also violated by 2.2), but it also causes
614       // different behavior when `target` is `display: none`, or not, which is
615       // really really odd, see:
616       // https://github.com/w3c/IntersectionObserver/issues/457
617       //
618       // NOTE(emilio): We also do this if target is the implicit root, pending
619       // clarification in
620       // https://github.com/w3c/IntersectionObserver/issues/456.
621       if (rootFrame == targetFrame ||
622           !nsLayoutUtils::IsAncestorFrameCrossDocInProcess(rootFrame,
623                                                            targetFrame)) {
624         return false;
625       }
626 
627       // 2.2. If the intersection root is not the implicit root, and target is
628       // not in the same Document as the intersection root, skip further
629       // processing for target.
630       //
631       // NOTE(emilio): We don't just "skip further processing", because that
632       // doesn't match reality and other browsers, see
633       // https://github.com/w3c/IntersectionObserver/issues/457.
634       if (mRoot && mRoot->OwnerDoc() != target->OwnerDoc()) {
635         return false;
636       }
637 
638       return true;
639     }();
640 
641     if (canComputeIntersection) {
642       rootBounds = rootRect;
643       if (origin == BrowsingContextOrigin::Similar) {
644         rootBounds.Inflate(rootMargin);
645       }
646 
647       // 2.3. Let targetRect be a DOMRectReadOnly obtained by running the
648       // getBoundingClientRect() algorithm on target.
649       targetRect = targetFrame->GetBoundingClientRect();
650 
651       // 2.4. Let intersectionRect be the result of running the compute the
652       // intersection algorithm on target.
653       intersectionRect = ComputeTheIntersection(
654           targetFrame, rootFrame, rootBounds, remoteDocumentVisibleRect);
655     }
656 
657     // 2.5. Let targetArea be targetRect’s area.
658     int64_t targetArea =
659         (int64_t)targetRect.Width() * (int64_t)targetRect.Height();
660     // 2.6. Let intersectionArea be intersectionRect’s area.
661     int64_t intersectionArea = !intersectionRect
662                                    ? 0
663                                    : (int64_t)intersectionRect->Width() *
664                                          (int64_t)intersectionRect->Height();
665 
666     // 2.7. Let isIntersecting be true if targetRect and rootBounds intersect or
667     // are edge-adjacent, even if the intersection has zero area (because
668     // rootBounds or targetRect have zero area); otherwise, let isIntersecting
669     // be false.
670     const bool isIntersecting = intersectionRect.isSome();
671 
672     // 2.8. If targetArea is non-zero, let intersectionRatio be intersectionArea
673     // divided by targetArea. Otherwise, let intersectionRatio be 1 if
674     // isIntersecting is true, or 0 if isIntersecting is false.
675     double intersectionRatio;
676     if (targetArea > 0.0) {
677       intersectionRatio =
678           std::min((double)intersectionArea / (double)targetArea, 1.0);
679     } else {
680       intersectionRatio = isIntersecting ? 1.0 : 0.0;
681     }
682 
683     // 2.9 Let thresholdIndex be the index of the first entry in
684     // observer.thresholds whose value is greater than intersectionRatio, or the
685     // length of observer.thresholds if intersectionRatio is greater than or
686     // equal to the last entry in observer.thresholds.
687     int32_t thresholdIndex = -1;
688 
689     // If not intersecting, we can just shortcut, as we know that the thresholds
690     // are always between 0 and 1.
691     if (isIntersecting) {
692       thresholdIndex = mThresholds.IndexOfFirstElementGt(intersectionRatio);
693       if (thresholdIndex == 0) {
694         // Per the spec, we should leave threshold at 0 and distinguish between
695         // "less than all thresholds and intersecting" and "not intersecting"
696         // (queuing observer entries as both cases come to pass). However,
697         // neither Chrome nor the WPT tests expect this behavior, so treat these
698         // two cases as one.
699         //
700         // See https://github.com/w3c/IntersectionObserver/issues/432 about
701         // this.
702         thresholdIndex = -1;
703       }
704     }
705 
706     // Steps 2.10 - 2.15.
707     if (target->UpdateIntersectionObservation(this, thresholdIndex)) {
708       // See https://github.com/w3c/IntersectionObserver/issues/432 about
709       // why we use thresholdIndex > 0 rather than isIntersecting for the
710       // entry's isIntersecting value.
711       QueueIntersectionObserverEntry(
712           target, time,
713           origin == BrowsingContextOrigin::Similar ? Some(rootBounds)
714                                                    : Nothing(),
715           targetRect, intersectionRect, thresholdIndex > 0, intersectionRatio);
716     }
717   }
718 }
719 
QueueIntersectionObserverEntry(Element * aTarget,DOMHighResTimeStamp time,const Maybe<nsRect> & aRootRect,const nsRect & aTargetRect,const Maybe<nsRect> & aIntersectionRect,bool aIsIntersecting,double aIntersectionRatio)720 void DOMIntersectionObserver::QueueIntersectionObserverEntry(
721     Element* aTarget, DOMHighResTimeStamp time, const Maybe<nsRect>& aRootRect,
722     const nsRect& aTargetRect, const Maybe<nsRect>& aIntersectionRect,
723     bool aIsIntersecting, double aIntersectionRatio) {
724   RefPtr<DOMRect> rootBounds;
725   if (aRootRect.isSome()) {
726     rootBounds = new DOMRect(mOwner);
727     rootBounds->SetLayoutRect(aRootRect.value());
728   }
729   RefPtr<DOMRect> boundingClientRect = new DOMRect(mOwner);
730   boundingClientRect->SetLayoutRect(aTargetRect);
731   RefPtr<DOMRect> intersectionRect = new DOMRect(mOwner);
732   if (aIntersectionRect.isSome()) {
733     intersectionRect->SetLayoutRect(aIntersectionRect.value());
734   }
735   RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry(
736       mOwner, time, rootBounds.forget(), boundingClientRect.forget(),
737       intersectionRect.forget(), aIsIntersecting, aTarget, aIntersectionRatio);
738   mQueuedEntries.AppendElement(entry.forget());
739 }
740 
Notify()741 void DOMIntersectionObserver::Notify() {
742   if (!mQueuedEntries.Length()) {
743     return;
744   }
745   Sequence<OwningNonNull<DOMIntersectionObserverEntry>> entries;
746   if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) {
747     for (size_t i = 0; i < mQueuedEntries.Length(); ++i) {
748       RefPtr<DOMIntersectionObserverEntry> next = mQueuedEntries[i];
749       *entries.AppendElement(mozilla::fallible) = next;
750     }
751   }
752   mQueuedEntries.Clear();
753 
754   if (mCallback.is<RefPtr<dom::IntersectionCallback>>()) {
755     RefPtr<dom::IntersectionCallback> callback(
756         mCallback.as<RefPtr<dom::IntersectionCallback>>());
757     callback->Call(this, entries, *this);
758   } else {
759     mCallback.as<NativeCallback>()(entries);
760   }
761 }
762 
763 }  // namespace mozilla::dom
764