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