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