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