DOMIntersectionObserver.cpp (40586B)
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 9 #include "NonCustomCSSPropertyId.h" 10 #include "Units.h" 11 #include "mozilla/PresShell.h" 12 #include "mozilla/ScrollContainerFrame.h" 13 #include "mozilla/ServoBindings.h" 14 #include "mozilla/StaticPrefs_dom.h" 15 #include "mozilla/StaticPrefs_layout.h" 16 #include "mozilla/dom/BrowserChild.h" 17 #include "mozilla/dom/BrowsingContext.h" 18 #include "mozilla/dom/DocumentInlines.h" 19 #include "mozilla/dom/Element.h" 20 #include "mozilla/dom/ElementInlines.h" 21 #include "mozilla/dom/HTMLIFrameElement.h" 22 #include "mozilla/dom/HTMLImageElement.h" 23 #include "nsContainerFrame.h" 24 #include "nsContentUtils.h" 25 #include "nsIFrame.h" 26 #include "nsLayoutUtils.h" 27 #include "nsRefreshDriver.h" 28 29 namespace mozilla::dom { 30 31 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry) 32 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY 33 NS_INTERFACE_MAP_ENTRY(nsISupports) 34 NS_INTERFACE_MAP_END 35 36 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserverEntry) 37 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserverEntry) 38 39 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DOMIntersectionObserverEntry, mOwner, 40 mRootBounds, mBoundingClientRect, 41 mIntersectionRect, mTarget) 42 43 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserver) 44 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY 45 NS_INTERFACE_MAP_ENTRY(nsISupports) 46 NS_INTERFACE_MAP_ENTRY(DOMIntersectionObserver) 47 NS_INTERFACE_MAP_END 48 49 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserver) 50 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserver) 51 52 NS_IMPL_CYCLE_COLLECTION_CLASS(DOMIntersectionObserver) 53 54 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(DOMIntersectionObserver) 55 NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER 56 NS_IMPL_CYCLE_COLLECTION_TRACE_END 57 58 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMIntersectionObserver) 59 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER 60 tmp->Disconnect(); 61 NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner) 62 NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument) 63 if (tmp->mCallback.is<RefPtr<dom::IntersectionCallback>>()) { 64 ImplCycleCollectionUnlink( 65 tmp->mCallback.as<RefPtr<dom::IntersectionCallback>>()); 66 } 67 NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoot) 68 NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries) 69 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 70 71 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMIntersectionObserver) 72 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner) 73 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) 74 if (tmp->mCallback.is<RefPtr<dom::IntersectionCallback>>()) { 75 ImplCycleCollectionTraverse( 76 cb, tmp->mCallback.as<RefPtr<dom::IntersectionCallback>>(), "mCallback", 77 0); 78 } 79 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot) 80 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries) 81 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 82 83 DOMIntersectionObserver::DOMIntersectionObserver( 84 already_AddRefed<nsPIDOMWindowInner>&& aOwner, 85 dom::IntersectionCallback& aCb) 86 : mOwner(aOwner), 87 mDocument(mOwner->GetExtantDoc()), 88 mCallback(RefPtr<dom::IntersectionCallback>(&aCb)) {} 89 90 already_AddRefed<DOMIntersectionObserver> DOMIntersectionObserver::Constructor( 91 const GlobalObject& aGlobal, dom::IntersectionCallback& aCb, 92 ErrorResult& aRv) { 93 return Constructor(aGlobal, aCb, IntersectionObserverInit(), aRv); 94 } 95 96 // https://w3c.github.io/IntersectionObserver/#initialize-new-intersection-observer 97 already_AddRefed<DOMIntersectionObserver> DOMIntersectionObserver::Constructor( 98 const GlobalObject& aGlobal, dom::IntersectionCallback& aCb, 99 const IntersectionObserverInit& aOptions, ErrorResult& aRv) { 100 nsCOMPtr<nsPIDOMWindowInner> window = 101 do_QueryInterface(aGlobal.GetAsSupports()); 102 if (!window) { 103 aRv.Throw(NS_ERROR_FAILURE); 104 return nullptr; 105 } 106 107 // 1. Let this be a new IntersectionObserver object 108 // 2. Set this’s internal [[callback]] slot to callback. 109 RefPtr<DOMIntersectionObserver> observer = 110 new DOMIntersectionObserver(window.forget(), aCb); 111 112 if (!aOptions.mRoot.IsNull()) { 113 if (aOptions.mRoot.Value().IsElement()) { 114 observer->mRoot = aOptions.mRoot.Value().GetAsElement(); 115 } else { 116 MOZ_ASSERT(aOptions.mRoot.Value().IsDocument()); 117 observer->mRoot = aOptions.mRoot.Value().GetAsDocument(); 118 } 119 } 120 121 // 3. Attempt to parse a margin from options.rootMargin. If a list is 122 // returned, set this’s internal [[rootMargin]] slot to that. Otherwise, throw 123 // a SyntaxError exception. 124 if (!observer->SetRootMargin(aOptions.mRootMargin)) { 125 aRv.ThrowSyntaxError("rootMargin must be specified in pixels or percent."); 126 return nullptr; 127 } 128 129 // 4. Attempt to parse a margin from options.scrollMargin. If a list is 130 // returned, set this’s internal [[scrollMargin]] slot to that. Otherwise, 131 // throw a SyntaxError exception. 132 if (!observer->SetScrollMargin(aOptions.mScrollMargin)) { 133 aRv.ThrowSyntaxError( 134 "scrollMargin must be specified in pixels or percent."); 135 return nullptr; 136 } 137 138 // 5. Let thresholds be a list equal to options.threshold. 139 if (aOptions.mThreshold.IsDoubleSequence()) { 140 const Sequence<double>& thresholds = 141 aOptions.mThreshold.GetAsDoubleSequence(); 142 observer->mThresholds.SetCapacity(thresholds.Length()); 143 144 // 6. If any value in thresholds is less than 0.0 or greater than 1.0, throw 145 // a RangeError exception. 146 for (const auto& thresh : thresholds) { 147 if (thresh < 0.0 || thresh > 1.0) { 148 aRv.ThrowRangeError<dom::MSG_THRESHOLD_RANGE_ERROR>(); 149 return nullptr; 150 } 151 observer->mThresholds.AppendElement(thresh); 152 } 153 154 // 7. Sort thresholds in ascending order. 155 observer->mThresholds.Sort(); 156 157 // 8. If thresholds is empty, append 0 to thresholds. 158 if (observer->mThresholds.IsEmpty()) { 159 observer->mThresholds.AppendElement(0.0); 160 } 161 } else { 162 double thresh = aOptions.mThreshold.GetAsDouble(); 163 if (thresh < 0.0 || thresh > 1.0) { 164 aRv.ThrowRangeError<dom::MSG_THRESHOLD_RANGE_ERROR>(); 165 return nullptr; 166 } 167 observer->mThresholds.AppendElement(thresh); 168 } 169 170 // 9. The thresholds attribute getter will return this sorted thresholds list. 171 // (This is implicit given `observer->mThresholds`) 172 173 // 10. Let delay be the value of options.delay. 174 // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1896900 175 176 // 11. If options.trackVisibility is true and delay is less than 100, set 177 // delay to 100. 178 // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1896900 179 180 // 12. Set this’s internal [[delay]] slot to options.delay to delay. 181 // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1896900 182 183 // 13. Set this’s internal [[trackVisibility]] slot to 184 // options.trackVisibility. 185 // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1896900 186 187 // 14. Return this. 188 return observer.forget(); 189 } 190 191 static void LazyLoadCallback( 192 const Sequence<OwningNonNull<DOMIntersectionObserverEntry>>& aEntries) { 193 for (const auto& entry : aEntries) { 194 Element* target = entry->Target(); 195 if (entry->IsIntersecting()) { 196 if (auto* image = HTMLImageElement::FromNode(target)) { 197 image->StopLazyLoading(); 198 } else if (auto* iframe = HTMLIFrameElement::FromNode(target)) { 199 iframe->StopLazyLoading(); 200 } else { 201 MOZ_ASSERT_UNREACHABLE( 202 "Only <img> and <iframe> should be observed by lazy load observer"); 203 } 204 } 205 } 206 } 207 208 static LengthPercentage PrefMargin(float aValue, bool aIsPercentage) { 209 return aIsPercentage ? LengthPercentage::FromPercentage(aValue / 100.0f) 210 : LengthPercentage::FromPixels(aValue); 211 } 212 213 static IntersectionObserverMargin LazyLoadingMargin() { 214 IntersectionObserverMargin margin; 215 #define SET_MARGIN(side_, side_lower_) \ 216 margin.Get(eSide##side_) = PrefMargin( \ 217 StaticPrefs::dom_lazy_loading_margin_##side_lower_(), \ 218 StaticPrefs::dom_lazy_loading_margin_##side_lower_##_percentage()); 219 SET_MARGIN(Top, top); 220 SET_MARGIN(Right, right); 221 SET_MARGIN(Bottom, bottom); 222 SET_MARGIN(Left, left); 223 #undef SET_MARGIN 224 return margin; 225 } 226 227 DOMIntersectionObserver::DOMIntersectionObserver(Document& aDocument, 228 NativeCallback aCallback) 229 : mOwner(aDocument.GetInnerWindow()), 230 mDocument(&aDocument), 231 mCallback(aCallback) {} 232 233 already_AddRefed<DOMIntersectionObserver> 234 DOMIntersectionObserver::CreateLazyLoadObserver(Document& aDocument) { 235 RefPtr<DOMIntersectionObserver> observer = 236 new DOMIntersectionObserver(aDocument, LazyLoadCallback); 237 observer->mThresholds.AppendElement(0.0f); 238 auto* margin = StaticPrefs::dom_lazy_loading_margin_is_scroll() 239 ? &observer->mScrollMargin 240 : &observer->mRootMargin; 241 *margin = LazyLoadingMargin(); 242 return observer.forget(); 243 } 244 245 bool DOMIntersectionObserver::SetRootMargin(const nsACString& aString) { 246 return Servo_IntersectionObserverMargin_Parse(&aString, &mRootMargin); 247 } 248 249 bool DOMIntersectionObserver::SetScrollMargin(const nsACString& aString) { 250 return Servo_IntersectionObserverMargin_Parse(&aString, &mScrollMargin); 251 } 252 253 nsISupports* DOMIntersectionObserver::GetParentObject() const { return mOwner; } 254 255 void DOMIntersectionObserver::GetRootMargin(nsACString& aRetVal) { 256 Servo_IntersectionObserverMargin_ToString(&mRootMargin, &aRetVal); 257 } 258 259 void DOMIntersectionObserver::GetScrollMargin(nsACString& aRetVal) { 260 Servo_IntersectionObserverMargin_ToString(&mScrollMargin, &aRetVal); 261 } 262 263 void DOMIntersectionObserver::GetThresholds(nsTArray<double>& aRetVal) { 264 aRetVal = mThresholds.Clone(); 265 } 266 267 // https://w3c.github.io/IntersectionObserver/#observe-target-element 268 void DOMIntersectionObserver::Observe(Element& aTarget) { 269 // 1. If target is in observer’s internal [[ObservationTargets]] slot, return. 270 bool wasPresent = 271 mObservationTargetMap.WithEntryHandle(&aTarget, [](auto handle) { 272 if (handle.HasEntry()) { 273 return true; 274 } 275 handle.Insert(Uninitialized); 276 return false; 277 }); 278 if (wasPresent) { 279 return; 280 } 281 282 // 2. Let intersectionObserverRegistration be an 283 // IntersectionObserverRegistration record with an observer property set to 284 // observer, a previousThresholdIndex property set to -1, a 285 // previousIsIntersecting property set to false, and a previousIsVisible 286 // property set to false. 287 // 3. Append intersectionObserverRegistration to target’s internal 288 // [[RegisteredIntersectionObservers]] slot. 289 // 4. Add target to observer’s internal [[ObservationTargets]] slot. 290 aTarget.BindObject(this, [](nsISupports* aObserver, nsINode* aTarget) { 291 static_cast<DOMIntersectionObserver*>(aObserver)->UnlinkTarget( 292 *aTarget->AsElement()); 293 }); 294 mObservationTargets.AppendElement(&aTarget); 295 296 MOZ_ASSERT(mObservationTargets.Length() == mObservationTargetMap.Count()); 297 298 Connect(); 299 if (mDocument) { 300 if (nsPresContext* pc = mDocument->GetPresContext()) { 301 pc->RefreshDriver()->EnsureIntersectionObservationsUpdateHappens(); 302 } 303 } 304 } 305 306 // https://w3c.github.io/IntersectionObserver/#unobserve-target-element 307 void DOMIntersectionObserver::Unobserve(Element& aTarget) { 308 // 1. Remove the IntersectionObserverRegistration record whose observer 309 // property is equal to this from target’s internal 310 // [[RegisteredIntersectionObservers]] slot, if present. 311 if (!mObservationTargetMap.Remove(&aTarget)) { 312 return; 313 } 314 315 // 2. Remove target from this’s internal [[ObservationTargets]] slot, if 316 // present 317 mObservationTargets.RemoveElement(&aTarget); 318 319 aTarget.UnbindObject(this); 320 321 MOZ_ASSERT(mObservationTargets.Length() == mObservationTargetMap.Count()); 322 323 if (mObservationTargets.IsEmpty()) { 324 Disconnect(); 325 } 326 } 327 328 // Inner step of 329 // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-disconnect 330 void DOMIntersectionObserver::UnlinkTarget(Element& aTarget) { 331 // 1. Remove the IntersectionObserverRegistration record whose observer 332 // property is equal to this from target’s internal 333 // [[RegisteredIntersectionObservers]] slot. 334 // 2. Remove target from this’s internal [[ObservationTargets]] slot. 335 mObservationTargets.RemoveElement(&aTarget); 336 mObservationTargetMap.Remove(&aTarget); 337 338 if (mObservationTargets.IsEmpty()) { 339 Disconnect(); 340 } 341 } 342 343 void DOMIntersectionObserver::Connect() { 344 if (mConnected) { 345 return; 346 } 347 348 mConnected = true; 349 if (mDocument) { 350 mDocument->AddIntersectionObserver(*this); 351 } 352 } 353 354 // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-disconnect 355 void DOMIntersectionObserver::Disconnect() { 356 if (!mConnected) { 357 return; 358 } 359 360 mConnected = false; 361 // 1. For each target in this’s internal [[ObservationTargets]] slot: 362 for (Element* target : mObservationTargets) { 363 // 2. Remove the IntersectionObserverRegistration record whose observer 364 // property is equal to this from target’s internal 365 // [[RegisteredIntersectionObservers]] slot. 366 // 3. Remove target from this’s internal [[ObservationTargets]] slot. 367 target->UnbindObject(this); 368 } 369 370 mObservationTargets.Clear(); 371 mObservationTargetMap.Clear(); 372 if (mDocument) { 373 mDocument->RemoveIntersectionObserver(*this); 374 } 375 } 376 377 // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-takerecords 378 void DOMIntersectionObserver::TakeRecords( 379 nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal) { 380 aRetVal = std::move(mQueuedEntries); 381 } 382 383 enum class BrowsingContextOrigin { Similar, Different }; 384 385 // NOTE(emilio): Checking docgroup as per discussion in: 386 // https://github.com/w3c/IntersectionObserver/issues/161 387 static BrowsingContextOrigin SimilarOrigin(const nsIContent& aTarget, 388 const nsINode* aRoot) { 389 if (!aRoot) { 390 return BrowsingContextOrigin::Different; 391 } 392 return aTarget.OwnerDoc()->GetDocGroup() == aRoot->OwnerDoc()->GetDocGroup() 393 ? BrowsingContextOrigin::Similar 394 : BrowsingContextOrigin::Different; 395 } 396 397 // NOTE: This returns nullptr if |aDocument| is in another process from the top 398 // level content document. 399 static const Document* GetTopLevelContentDocumentInThisProcess( 400 const Document& aDocument) { 401 auto* wc = aDocument.GetTopLevelWindowContext(); 402 return wc ? wc->GetExtantDoc() : nullptr; 403 } 404 405 static nsMargin ResolveMargin(const IntersectionObserverMargin& aMargin, 406 const nsSize& aPercentBasis) { 407 nsMargin margin; 408 for (const auto side : mozilla::AllPhysicalSides()) { 409 nscoord basis = side == eSideTop || side == eSideBottom 410 ? aPercentBasis.Height() 411 : aPercentBasis.Width(); 412 margin.Side(side) = aMargin.Get(side).Resolve( 413 basis, static_cast<nscoord (*)(float)>(NSToCoordRoundWithClamp)); 414 } 415 return margin; 416 } 417 418 // https://w3c.github.io/IntersectionObserver/#compute-the-intersection 419 // 420 // TODO(emilio): Proof of this being equivalent to the spec welcome, seems 421 // reasonably close. 422 // 423 // Also, it's unclear to me why the spec talks about browsing context while 424 // discarding observations of targets of different documents. 425 // 426 // Both aRootBounds and the return value are relative to 427 // nsLayoutUtils::GetContainingBlockForClientRect(aRoot). 428 // 429 // In case of out-of-process document, aRemoteDocumentVisibleRect is a rectangle 430 // in the out-of-process document's coordinate system. 431 static Maybe<nsRect> ComputeTheIntersection( 432 nsIFrame* aTarget, const nsRect& aTargetRectRelativeToTarget, 433 nsIFrame* aRoot, const nsRect& aRootBounds, 434 const IntersectionObserverMargin& aScrollMargin, 435 const Maybe<nsRect>& aRemoteDocumentVisibleRect, 436 DOMIntersectionObserver::IsForProximityToViewport aIsForProximityToViewport, 437 bool* aPreservesAxisAlignedRectangles) { 438 nsIFrame* target = aTarget; 439 // 1. Let intersectionRect be the result of running the 440 // getBoundingClientRect() algorithm on the target. 441 // 442 // `intersectionRect` is kept relative to `target` during the loop. 443 auto inflowRect = aTargetRectRelativeToTarget; 444 Maybe<nsRect> intersectionRect = Some(inflowRect); 445 446 // 2. Let container be the containing block of the target. 447 // (We go through the parent chain and only look at scroll frames) 448 // 449 // FIXME(emilio): Spec uses containing blocks, we use scroll frames, but we 450 // only apply overflow-clipping, not clip-path, so it's ~fine. We do need to 451 // apply clip-path. 452 // 453 // 3. While container is not the intersection root: 454 nsIFrame* containerFrame = 455 nsLayoutUtils::GetCrossDocParentFrameInProcess(target); 456 while (containerFrame && containerFrame != aRoot) { 457 // FIXME(emilio): What about other scroll frames that inherit from 458 // ScrollContainerFrame but have a different type, like nsListControlFrame? 459 // This looks bogus in that case, but different bug. 460 if (ScrollContainerFrame* scrollContainerFrame = 461 do_QueryFrame(containerFrame)) { 462 if (containerFrame->GetParent() == aRoot && !aRoot->GetParent()) { 463 // This is subtle: if we're computing the intersection against the 464 // viewport (the root frame), and this is its scroll frame, we really 465 // want to skip this intersection (because we want to account for the 466 // root margin, which is already in aRootBounds). 467 break; 468 } 469 nsRect subFrameRect = 470 scrollContainerFrame->GetScrollPortRectAccountingForDynamicToolbar(); 471 472 // 3.1 If container is the document of a nested browsing context, update 473 // intersectionRect by clipping to the viewport of the document, and 474 // update container to be the browsing context container of container. 475 // XXX: Handled below by aRemoteDocumentVisibleRect & walking 476 // CrossDocParentFrame. 477 478 // 3.2 Map intersectionRect to the coordinate space of container. 479 bool preservesAxisAlignedRectangles = false; 480 nsRect intersectionRectRelativeToContainer = 481 nsLayoutUtils::TransformFrameRectToAncestor( 482 target, intersectionRect.value(), containerFrame, 483 &preservesAxisAlignedRectangles); 484 if (aPreservesAxisAlignedRectangles) { 485 *aPreservesAxisAlignedRectangles |= preservesAxisAlignedRectangles; 486 } 487 488 // 3.3 If container is a scroll container, apply the 489 // IntersectionObserver’s [[scrollMargin]] to the container’s clip rect as 490 // described in apply scroll margin to a scrollport. 491 subFrameRect.Inflate(ResolveMargin(aScrollMargin, subFrameRect.Size())); 492 493 intersectionRect = 494 intersectionRectRelativeToContainer.EdgeInclusiveIntersection( 495 subFrameRect); 496 if (!intersectionRect) { 497 return Nothing(); 498 } 499 target = containerFrame; 500 } else { 501 const auto& disp = *containerFrame->StyleDisplay(); 502 auto clipAxes = containerFrame->ShouldApplyOverflowClipping(&disp); 503 // 3.4 If container has a content clip or a css clip-path property, update 504 // intersectionRect by applying container’s clip. 505 506 // 3.4 TODO: Apply clip-path. 507 if (!clipAxes.isEmpty()) { 508 // 3.2 Map intersectionRect to the coordinate space of container. 509 bool preservesAxisAlignedRectangles = false; 510 const nsRect intersectionRectRelativeToContainer = 511 nsLayoutUtils::TransformFrameRectToAncestor( 512 target, intersectionRect.value(), containerFrame, 513 &preservesAxisAlignedRectangles); 514 if (aPreservesAxisAlignedRectangles) { 515 *aPreservesAxisAlignedRectangles |= preservesAxisAlignedRectangles; 516 } 517 const nsRect clipRect = OverflowAreas::GetOverflowClipRect( 518 intersectionRectRelativeToContainer, 519 containerFrame->GetRectRelativeToSelf(), clipAxes, 520 containerFrame->OverflowClipMargin(clipAxes)); 521 intersectionRect = 522 intersectionRectRelativeToContainer.EdgeInclusiveIntersection( 523 clipRect); 524 if (!intersectionRect) { 525 return Nothing(); 526 } 527 target = containerFrame; 528 } 529 } 530 // 3.5 If container is the root element of a browsing context, update 531 // container to be the browsing context’s document; otherwise, update 532 // container to be the containing block of container. 533 containerFrame = 534 nsLayoutUtils::GetCrossDocParentFrameInProcess(containerFrame); 535 } 536 MOZ_ASSERT(intersectionRect); 537 538 // 4. Map intersectionRect to the coordinate space of the intersection root. 539 bool preservesAxisAlignedRectangles = false; 540 nsRect intersectionRectRelativeToRoot = 541 nsLayoutUtils::TransformFrameRectToAncestor( 542 target, intersectionRect.value(), 543 nsLayoutUtils::GetContainingBlockForClientRect(aRoot), 544 &preservesAxisAlignedRectangles); 545 if (aPreservesAxisAlignedRectangles) { 546 *aPreservesAxisAlignedRectangles |= preservesAxisAlignedRectangles; 547 } 548 549 // 5.Update intersectionRect by intersecting it with the root intersection 550 // rectangle. 551 // 552 // In out-of-process iframes we need to take an intersection with the remote 553 // document visible rect which was already clipped by ancestor document's 554 // viewports. 555 if (aRemoteDocumentVisibleRect) { 556 MOZ_ASSERT(aRoot->PresContext()->IsRootContentDocumentInProcess() && 557 !aRoot->PresContext()->IsRootContentDocumentCrossProcess()); 558 559 intersectionRect = intersectionRectRelativeToRoot.EdgeInclusiveIntersection( 560 *aRemoteDocumentVisibleRect); 561 } else if (aTarget->HasAnyStateBits(NS_FRAME_IN_POPUP)) { 562 // Popups don't get clipped to the viewport, so avoid applying the root 563 // intersection rect, see bug 1991410. 564 intersectionRect = Some(intersectionRectRelativeToRoot); 565 } else { 566 intersectionRect = 567 intersectionRectRelativeToRoot.EdgeInclusiveIntersection(aRootBounds); 568 } 569 570 if (intersectionRect.isNothing()) { 571 return Nothing(); 572 } 573 // 6. Map intersectionRect to the coordinate space of the viewport of the 574 // Document containing the target. 575 // 576 // FIXME(emilio): I think this may not be correct if the root is explicit 577 // and in the same document, since then the rectangle may not be relative to 578 // the viewport already (but it's in the same document). 579 nsRect rect = intersectionRect.value(); 580 if (aTarget->PresContext() != aRoot->PresContext()) { 581 if (nsIFrame* rootScrollContainerFrame = 582 aTarget->PresShell()->GetRootScrollContainerFrame()) { 583 nsLayoutUtils::TransformRect(aRoot, rootScrollContainerFrame, rect); 584 } 585 } 586 587 // 7. Return intersectionRect. 588 return Some(rect); 589 } 590 591 struct OopIframeMetrics { 592 nsIFrame* mInProcessRootFrame = nullptr; 593 nsRect mInProcessRootRect; 594 nsRect mRemoteDocumentVisibleRect; 595 }; 596 597 static Maybe<OopIframeMetrics> GetOopIframeMetrics( 598 const Document& aDocument, const Document* aRootDocument) { 599 const Document* rootDoc = 600 nsContentUtils::GetInProcessSubtreeRootDocument(&aDocument); 601 MOZ_ASSERT(rootDoc); 602 603 if (rootDoc->IsTopLevelContentDocument()) { 604 return Nothing(); 605 } 606 607 if (aRootDocument && 608 rootDoc == 609 nsContentUtils::GetInProcessSubtreeRootDocument(aRootDocument)) { 610 // aRootDoc, if non-null, is either the implicit root 611 // (top-level-content-document) or a same-origin document passed explicitly. 612 // 613 // In the former case, we should've returned above if there are no iframes 614 // in between. This condition handles the explicit, same-origin root 615 // document, when both are embedded in an OOP iframe. 616 return Nothing(); 617 } 618 619 PresShell* rootPresShell = rootDoc->GetPresShell(); 620 if (!rootPresShell || rootPresShell->IsDestroying()) { 621 return Some(OopIframeMetrics{}); 622 } 623 624 nsIFrame* inProcessRootFrame = rootPresShell->GetRootFrame(); 625 if (!inProcessRootFrame) { 626 return Some(OopIframeMetrics{}); 627 } 628 629 BrowserChild* browserChild = BrowserChild::GetFrom(rootDoc->GetDocShell()); 630 if (!browserChild) { 631 return Some(OopIframeMetrics{}); 632 } 633 634 if (MOZ_UNLIKELY(NS_WARN_IF(browserChild->IsTopLevel()))) { 635 // FIXME(bug 1772083): This can be hit with popups, e.g. in 636 // html/browsers/the-window-object/open-close/no_window_open_when_term_nesting_level_nonzero.window.html 637 // temporarily while opening a new popup (on the about:blank doc). 638 // MOZ_ASSERT_UNREACHABLE("Top level BrowserChild but non-top level doc?"); 639 return Nothing(); 640 } 641 642 nsRect inProcessRootRect; 643 if (ScrollContainerFrame* rootScrollContainerFrame = 644 rootPresShell->GetRootScrollContainerFrame()) { 645 inProcessRootRect = rootScrollContainerFrame 646 ->GetScrollPortRectAccountingForDynamicToolbar(); 647 } 648 649 Maybe<LayoutDeviceRect> remoteDocumentVisibleRect = 650 browserChild->GetTopLevelViewportVisibleRectInSelfCoords(); 651 if (!remoteDocumentVisibleRect) { 652 return Some(OopIframeMetrics{}); 653 } 654 655 return Some(OopIframeMetrics{ 656 inProcessRootFrame, 657 inProcessRootRect, 658 LayoutDeviceRect::ToAppUnits( 659 *remoteDocumentVisibleRect, 660 rootPresShell->GetPresContext()->AppUnitsPerDevPixel()), 661 }); 662 } 663 664 IntersectionInput DOMIntersectionObserver::ComputeInputForIframeThrottling( 665 const Document& aEmbedderDocument) { 666 auto margin = LazyLoadingMargin(); 667 // TODO: Consider not using exactly the same parameters as lazy-load? 668 const bool useScroll = StaticPrefs::dom_lazy_loading_margin_is_scroll(); 669 return ComputeInput(aEmbedderDocument, /* aRoot = */ nullptr, 670 /* aRootMargin = */ useScroll ? nullptr : &margin, 671 /* aScrollMargin = */ useScroll ? &margin : nullptr); 672 } 673 674 // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo 675 // step 2.1 676 IntersectionInput DOMIntersectionObserver::ComputeInput( 677 const Document& aDocument, const nsINode* aRoot, 678 const IntersectionObserverMargin* aRootMargin, 679 const IntersectionObserverMargin* aScrollMargin) { 680 // 1 - Let rootBounds be observer's root intersection rectangle. 681 // ... but since the intersection rectangle depends on the target, we defer 682 // the inflation until later. 683 // NOTE: |rootRect| and |rootFrame| will be root in the same process. In 684 // out-of-process iframes, they are NOT root ones of the top level content 685 // document. 686 nsRect rootRect; 687 nsIFrame* rootFrame = nullptr; 688 const nsINode* root = aRoot; 689 const bool isImplicitRoot = !aRoot; 690 Maybe<nsRect> remoteDocumentVisibleRect; 691 if (aRoot && aRoot->IsElement()) { 692 if ((rootFrame = aRoot->AsElement()->GetPrimaryFrame())) { 693 nsRect rootRectRelativeToRootFrame; 694 if (ScrollContainerFrame* scrollContainerFrame = 695 do_QueryFrame(rootFrame)) { 696 // rootRectRelativeToRootFrame should be the content rect of rootFrame, 697 // not including the scrollbars. 698 rootRectRelativeToRootFrame = 699 scrollContainerFrame 700 ->GetScrollPortRectAccountingForDynamicToolbar(); 701 } else { 702 // rootRectRelativeToRootFrame should be the border rect of rootFrame. 703 rootRectRelativeToRootFrame = rootFrame->GetRectRelativeToSelf(); 704 } 705 nsIFrame* containingBlock = 706 nsLayoutUtils::GetContainingBlockForClientRect(rootFrame); 707 rootRect = nsLayoutUtils::TransformFrameRectToAncestor( 708 rootFrame, rootRectRelativeToRootFrame, containingBlock); 709 } 710 } else { 711 MOZ_ASSERT(!aRoot || aRoot->IsDocument()); 712 const Document* rootDocument = 713 aRoot ? aRoot->AsDocument() 714 : GetTopLevelContentDocumentInThisProcess(aDocument); 715 root = rootDocument; 716 717 if (rootDocument) { 718 // We're in the same process as the root document, though note that there 719 // could be an out-of-process iframe in between us and the root. Grab the 720 // root frame and the root rect. 721 // 722 // Note that the root rect is always good (we assume no DPI changes in 723 // between the two documents, and we don't need to convert coordinates). 724 // 725 // The root frame however we may need to tweak in the block below, if 726 // there's any OOP iframe in between `rootDocument` and `aDocument`, to 727 // handle the OOP iframe positions. 728 if (PresShell* presShell = rootDocument->GetPresShell()) { 729 rootFrame = presShell->GetRootFrame(); 730 // We use the root scroll container frame's scroll port to account the 731 // scrollbars in rootRect, if needed. 732 if (ScrollContainerFrame* rootScrollContainerFrame = 733 presShell->GetRootScrollContainerFrame()) { 734 rootRect = rootScrollContainerFrame 735 ->GetScrollPortRectAccountingForDynamicToolbar(); 736 } else if (rootFrame) { 737 rootRect = rootFrame->GetRectRelativeToSelf(); 738 } 739 } 740 } 741 742 if (Maybe<OopIframeMetrics> metrics = 743 GetOopIframeMetrics(aDocument, rootDocument)) { 744 rootFrame = metrics->mInProcessRootFrame; 745 if (!rootDocument) { 746 rootRect = metrics->mInProcessRootRect; 747 } 748 remoteDocumentVisibleRect = Some(metrics->mRemoteDocumentVisibleRect); 749 } 750 } 751 752 nsMargin rootMargin; // This root margin is NOT applied in `implicit root` 753 // case, e.g. in out-of-process iframes. 754 if (aRootMargin) { 755 rootMargin = ResolveMargin(*aRootMargin, rootRect.Size()); 756 } 757 758 return {isImplicitRoot, 759 root, 760 rootFrame, 761 rootRect, 762 rootMargin, 763 aScrollMargin ? *aScrollMargin : IntersectionObserverMargin(), 764 remoteDocumentVisibleRect}; 765 } 766 767 IntersectionOutput DOMIntersectionObserver::Intersect( 768 const IntersectionInput& aInput, const Element& aTarget, BoxToUse aBoxToUse, 769 IsForProximityToViewport aIsForProximityToViewport) { 770 nsIFrame* targetFrame = aTarget.GetPrimaryFrame(); 771 if (!targetFrame) { 772 return {SimilarOrigin(aTarget, aInput.mRootNode) == 773 BrowsingContextOrigin::Similar}; 774 } 775 return Intersect(aInput, targetFrame, aBoxToUse, aIsForProximityToViewport); 776 } 777 778 // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo 779 // (steps 2.1 - 2.5) 780 IntersectionOutput DOMIntersectionObserver::Intersect( 781 const IntersectionInput& aInput, nsIFrame* aTargetFrame, BoxToUse aBoxToUse, 782 IsForProximityToViewport aIsForProximityToViewport) { 783 MOZ_ASSERT(aTargetFrame); 784 785 const nsIContent* target = aTargetFrame->GetContent(); 786 const bool isSimilarOrigin = 787 target && SimilarOrigin(*target, aInput.mRootNode) == 788 BrowsingContextOrigin::Similar; 789 if (!aInput.mRootFrame) { 790 return {isSimilarOrigin}; 791 } 792 793 // "From the perspective of an IntersectionObserver, the skipped contents 794 // of an element are never intersecting the intersection root. This is 795 // true even if both the root and the target elements are in the skipped 796 // contents." 797 // https://drafts.csswg.org/css-contain/#cv-notes 798 // 799 // Skip the intersection if the element is hidden, unless this is the 800 // specifically to determine the proximity to the viewport for 801 // `content-visibility: auto` elements. 802 if (aIsForProximityToViewport == IsForProximityToViewport::No && 803 aTargetFrame->IsHiddenByContentVisibilityOnAnyAncestor()) { 804 return {isSimilarOrigin}; 805 } 806 807 // 2.2. If the intersection root is not the implicit root, and target is 808 // not in the same Document as the intersection root, skip to step 11. 809 if (!aInput.mIsImplicitRoot && 810 aInput.mRootNode->OwnerDoc() != target->OwnerDoc()) { 811 return {isSimilarOrigin}; 812 } 813 814 // 2.3. If the intersection root is an element and target is not a descendant 815 // of the intersection root in the containing block chain, skip to step 11. 816 // 817 // NOTE(emilio): We also do this if target is the implicit root, pending 818 // clarification in 819 // https://github.com/w3c/IntersectionObserver/issues/456. 820 if (aInput.mRootFrame == aTargetFrame || 821 !nsLayoutUtils::IsAncestorFrameCrossDocInProcess(aInput.mRootFrame, 822 aTargetFrame)) { 823 return {isSimilarOrigin}; 824 } 825 826 nsRect rootBounds = aInput.mRootRect; 827 if (isSimilarOrigin) { 828 rootBounds.Inflate(aInput.mRootMargin); 829 830 // Implicit roots should apply the scrollMargin as well: 831 if (aInput.mIsImplicitRoot) { 832 rootBounds.Inflate( 833 ResolveMargin(aInput.mScrollMargin, aInput.mRootRect.Size())); 834 } 835 } 836 837 // 2.4. Set targetRect to the DOMRectReadOnly obtained by running the 838 // getBoundingClientRect() algorithm on target. We compute the box relative to 839 // self first, then transform. 840 nsLayoutUtils::GetAllInFlowRectsFlags flags{ 841 nsLayoutUtils::GetAllInFlowRectsFlag::AccountForTransforms}; 842 if (aBoxToUse == BoxToUse::Content) { 843 flags += nsLayoutUtils::GetAllInFlowRectsFlag::UseContentBox; 844 } 845 nsRect targetRectRelativeToTarget = 846 nsLayoutUtils::GetAllInFlowRectsUnion(aTargetFrame, aTargetFrame, flags); 847 848 if (aBoxToUse == BoxToUse::OverflowClip) { 849 const auto& disp = *aTargetFrame->StyleDisplay(); 850 auto clipAxes = aTargetFrame->ShouldApplyOverflowClipping(&disp); 851 if (!clipAxes.isEmpty()) { 852 targetRectRelativeToTarget = OverflowAreas::GetOverflowClipRect( 853 targetRectRelativeToTarget, targetRectRelativeToTarget, clipAxes, 854 aTargetFrame->OverflowClipMargin(clipAxes, 855 /* aAllowNegative = */ false)); 856 } 857 } 858 859 auto targetRect = nsLayoutUtils::TransformFrameRectToAncestor( 860 aTargetFrame, targetRectRelativeToTarget, 861 nsLayoutUtils::GetContainingBlockForClientRect(aTargetFrame)); 862 863 // For content-visibility, we need to observe the overflow clip edge, 864 // https://drafts.csswg.org/css-contain-2/#close-to-the-viewport 865 MOZ_ASSERT_IF(aIsForProximityToViewport == IsForProximityToViewport::Yes, 866 aBoxToUse == BoxToUse::OverflowClip); 867 868 // 2.5. Let intersectionRect be the result of running the compute the 869 // intersection algorithm on target and observer’s intersection root. 870 bool preservesAxisAlignedRectangles = false; 871 Maybe<nsRect> intersectionRect = ComputeTheIntersection( 872 aTargetFrame, targetRectRelativeToTarget, aInput.mRootFrame, rootBounds, 873 aInput.mScrollMargin, aInput.mRemoteDocumentVisibleRect, 874 aIsForProximityToViewport, &preservesAxisAlignedRectangles); 875 876 return {isSimilarOrigin, rootBounds, targetRect, intersectionRect, 877 preservesAxisAlignedRectangles}; 878 } 879 880 IntersectionOutput DOMIntersectionObserver::Intersect( 881 const IntersectionInput& aInput, const nsRect& aTargetRect) { 882 nsRect rootBounds = aInput.mRootRect; 883 rootBounds.Inflate(aInput.mRootMargin); 884 auto intersectionRect = 885 aInput.mRootRect.EdgeInclusiveIntersection(aTargetRect); 886 if (intersectionRect && aInput.mRemoteDocumentVisibleRect) { 887 intersectionRect = intersectionRect->EdgeInclusiveIntersection( 888 *aInput.mRemoteDocumentVisibleRect); 889 } 890 return {true, rootBounds, aTargetRect, intersectionRect, 891 /* mPreserverAxisAlignedRectangles= */ false}; 892 } 893 894 // https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo 895 // (step 2) 896 void DOMIntersectionObserver::Update(Document& aDocument, 897 DOMHighResTimeStamp time) { 898 auto input = ComputeInput(aDocument, mRoot, &mRootMargin, &mScrollMargin); 899 900 // 2. For each target in observer’s internal [[ObservationTargets]] slot, 901 // processed in the same order that observe() was called on each target: 902 for (Element* target : mObservationTargets) { 903 // 2.1 - 2.4. 904 IntersectionOutput output = Intersect(input, *target); 905 906 // 2.5. Let targetArea be targetRect’s area. 907 int64_t targetArea = (int64_t)output.mTargetRect.Width() * 908 (int64_t)output.mTargetRect.Height(); 909 910 // 2.6. Let intersectionArea be intersectionRect’s area. 911 int64_t intersectionArea = 912 !output.mIntersectionRect 913 ? 0 914 : (int64_t)output.mIntersectionRect->Width() * 915 (int64_t)output.mIntersectionRect->Height(); 916 917 // 2.7. Let isIntersecting be true if targetRect and rootBounds intersect or 918 // are edge-adjacent, even if the intersection has zero area (because 919 // rootBounds or targetRect have zero area); otherwise, let isIntersecting 920 // be false. 921 const bool isIntersecting = output.Intersects(); 922 923 // 2.8. If targetArea is non-zero, let intersectionRatio be intersectionArea 924 // divided by targetArea. Otherwise, let intersectionRatio be 1 if 925 // isIntersecting is true, or 0 if isIntersecting is false. 926 double intersectionRatio; 927 if (targetArea > 0.0) { 928 intersectionRatio = 929 std::min((double)intersectionArea / (double)targetArea, 1.0); 930 } else { 931 intersectionRatio = isIntersecting ? 1.0 : 0.0; 932 } 933 934 // 2.9 Let thresholdIndex be the index of the first entry in 935 // observer.thresholds whose value is greater than intersectionRatio, or the 936 // length of observer.thresholds if intersectionRatio is greater than or 937 // equal to the last entry in observer.thresholds. 938 int32_t thresholdIndex = -1; 939 940 // If not intersecting, we can just shortcut, as we know that the thresholds 941 // are always between 0 and 1. 942 if (isIntersecting) { 943 thresholdIndex = mThresholds.IndexOfFirstElementGt(intersectionRatio); 944 if (thresholdIndex == 0) { 945 // Per the spec, we should leave threshold at 0 and distinguish between 946 // "less than all thresholds and intersecting" and "not intersecting" 947 // (queuing observer entries as both cases come to pass). However, 948 // neither Chrome nor the WPT tests expect this behavior, so treat these 949 // two cases as one. 950 // 951 // See https://github.com/w3c/IntersectionObserver/issues/432 about 952 // this. 953 thresholdIndex = -1; 954 } 955 } 956 957 // Steps 2.10 - 2.15. 958 bool updated = false; 959 if (auto entry = mObservationTargetMap.Lookup(target)) { 960 updated = entry.Data() != thresholdIndex; 961 entry.Data() = thresholdIndex; 962 } else { 963 MOZ_ASSERT_UNREACHABLE("Target not properly registered?"); 964 } 965 966 if (updated) { 967 // See https://github.com/w3c/IntersectionObserver/issues/432 about 968 // why we use thresholdIndex > 0 rather than isIntersecting for the 969 // entry's isIntersecting value. 970 QueueIntersectionObserverEntry( 971 target, time, 972 output.mIsSimilarOrigin ? Some(output.mRootBounds) : Nothing(), 973 output.mTargetRect, output.mIntersectionRect, thresholdIndex > 0, 974 intersectionRatio); 975 } 976 } 977 } 978 979 void DOMIntersectionObserver::QueueIntersectionObserverEntry( 980 Element* aTarget, DOMHighResTimeStamp time, const Maybe<nsRect>& aRootRect, 981 const nsRect& aTargetRect, const Maybe<nsRect>& aIntersectionRect, 982 bool aIsIntersecting, double aIntersectionRatio) { 983 RefPtr<DOMRect> rootBounds; 984 if (aRootRect.isSome()) { 985 rootBounds = new DOMRect(mOwner); 986 rootBounds->SetLayoutRect(aRootRect.value()); 987 } 988 RefPtr<DOMRect> boundingClientRect = new DOMRect(mOwner); 989 boundingClientRect->SetLayoutRect(aTargetRect); 990 RefPtr<DOMRect> intersectionRect = new DOMRect(mOwner); 991 if (aIntersectionRect.isSome()) { 992 intersectionRect->SetLayoutRect(aIntersectionRect.value()); 993 } 994 RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry( 995 mOwner, time, rootBounds.forget(), boundingClientRect.forget(), 996 intersectionRect.forget(), aIsIntersecting, aTarget, aIntersectionRatio); 997 mQueuedEntries.AppendElement(entry.forget()); 998 } 999 1000 void DOMIntersectionObserver::Notify() { 1001 if (!mQueuedEntries.Length()) { 1002 return; 1003 } 1004 Sequence<OwningNonNull<DOMIntersectionObserverEntry>> entries; 1005 if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) { 1006 for (size_t i = 0; i < mQueuedEntries.Length(); ++i) { 1007 RefPtr<DOMIntersectionObserverEntry> next = mQueuedEntries[i]; 1008 *entries.AppendElement(mozilla::fallible) = next; 1009 } 1010 } 1011 mQueuedEntries.Clear(); 1012 1013 if (mCallback.is<RefPtr<dom::IntersectionCallback>>()) { 1014 RefPtr<dom::IntersectionCallback> callback( 1015 mCallback.as<RefPtr<dom::IntersectionCallback>>()); 1016 callback->Call(this, entries, *this); 1017 } else { 1018 mCallback.as<NativeCallback>()(entries); 1019 } 1020 } 1021 1022 } // namespace mozilla::dom