ResizeObserver.cpp (19691B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim:set ts=2 sw=2 sts=2 et cindent: */ 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 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "mozilla/dom/ResizeObserver.h" 8 9 #include <limits> 10 11 #include "mozilla/SVGUtils.h" 12 #include "mozilla/ScrollContainerFrame.h" 13 #include "mozilla/dom/DOMRect.h" 14 #include "mozilla/dom/Document.h" 15 #include "nsIContent.h" 16 #include "nsIContentInlines.h" 17 #include "nsLayoutUtils.h" 18 19 namespace mozilla::dom { 20 21 /** 22 * Returns the length of the parent-traversal path (in terms of the number of 23 * nodes) to an unparented/root node from aNode. An unparented/root node is 24 * considered to have a depth of 1, its children have a depth of 2, etc. 25 * aNode is expected to be non-null. 26 * Note: The shadow root is not part of the calculation because the caller, 27 * ResizeObserver, doesn't observe the shadow root, and only needs relative 28 * depths among all the observed targets. In other words, we calculate the 29 * depth of the flattened tree. 30 * 31 * However, these is a spec issue about how to handle shadow DOM case. We 32 * may need to update this function later: 33 * https://github.com/w3c/csswg-drafts/issues/3840 34 * 35 * https://drafts.csswg.org/resize-observer/#calculate-depth-for-node-h 36 */ 37 static uint32_t GetNodeDepth(nsINode* aNode) { 38 uint32_t depth = 1; 39 40 MOZ_ASSERT(aNode, "Node shouldn't be null"); 41 42 // Use GetFlattenedTreeParentNode to bypass the shadow root and cross the 43 // shadow boundary to calculate the node depth without the shadow root. 44 while ((aNode = aNode->GetFlattenedTreeParentNode())) { 45 ++depth; 46 } 47 48 return depth; 49 } 50 51 static nsSize GetContentRectSize(const nsIFrame& aFrame) { 52 if (const ScrollContainerFrame* f = do_QueryFrame(&aFrame)) { 53 // We return the scrollport rect for compat with other UAs, see bug 1733042. 54 // But the scrollPort includes padding (but not border!), so remove it. 55 nsRect scrollPort = f->GetScrollPortRect(); 56 nsMargin padding = 57 aFrame.GetUsedPadding().ApplySkipSides(aFrame.GetSkipSides()); 58 scrollPort.Deflate(padding); 59 // This can break in some edge cases like when layout overflows sizes or 60 // what not. 61 NS_ASSERTION( 62 !aFrame.PresContext()->UseOverlayScrollbars() || 63 scrollPort.Size() == aFrame.GetContentRectRelativeToSelf().Size(), 64 "Wrong scrollport?"); 65 return scrollPort.Size(); 66 } 67 return aFrame.GetContentRectRelativeToSelf().Size(); 68 } 69 70 AutoTArray<LogicalPixelSize, 1> ResizeObserver::CalculateBoxSize( 71 Element* aTarget, ResizeObserverBoxOptions aBox, 72 bool aForceFragmentHandling) { 73 nsIFrame* frame = aTarget->GetPrimaryFrame(); 74 75 if (!frame) { 76 // TODO: Should this return an empty array instead? 77 // https://github.com/w3c/csswg-drafts/issues/7734 78 return {LogicalPixelSize()}; 79 } 80 81 const auto zoom = frame->Style()->EffectiveZoom(); 82 if (frame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) { 83 // Per the spec, this target's SVG size is always its bounding box size no 84 // matter what box option you choose, because SVG elements do not use 85 // standard CSS box model. 86 // TODO: what if the SVG is fragmented? 87 // https://github.com/w3c/csswg-drafts/issues/7736 88 const gfxRect bbox = SVGUtils::GetBBox(frame); 89 gfx::Size size(static_cast<float>(bbox.width), 90 static_cast<float>(bbox.height)); 91 const WritingMode wm = frame->GetWritingMode(); 92 if (aBox == ResizeObserverBoxOptions::Device_pixel_content_box) { 93 // Per spec, we calculate the inline/block sizes to target’s bounding box 94 // {inline|block} length, in integral device pixels, so we round the final 95 // result. 96 // https://drafts.csswg.org/resize-observer/#dom-resizeobserverboxoptions-device-pixel-content-box 97 const LayoutDeviceIntSize snappedSize = 98 RoundedToInt(CSSSize::FromUnknownSize(size) * 99 frame->PresContext()->CSSToDevPixelScale()); 100 return {LogicalPixelSize(wm, gfx::Size(snappedSize.ToUnknownSize()))}; 101 } 102 size.width = zoom.Unzoom(size.width); 103 size.height = zoom.Unzoom(size.height); 104 return {LogicalPixelSize(wm, size)}; 105 } 106 107 // Per the spec, non-replaced inline Elements will always have an empty 108 // content rect. Therefore, we always use the same trivially-empty size 109 // for non-replaced inline elements here, and their IsActive() will 110 // always return false. (So its observation won't be fired.) 111 // TODO: Should we use an empty array instead? 112 // https://github.com/w3c/csswg-drafts/issues/7734 113 if (!frame->IsReplaced() && frame->IsLineParticipant()) { 114 return {LogicalPixelSize()}; 115 } 116 117 auto GetFrameSize = [aBox, zoom](nsIFrame* aFrame) { 118 switch (aBox) { 119 case ResizeObserverBoxOptions::Border_box: 120 return CSSPixel::FromAppUnits(zoom.Unzoom(aFrame->GetSize())) 121 .ToUnknownSize(); 122 case ResizeObserverBoxOptions::Device_pixel_content_box: { 123 // Simply converting from app units to device units is insufficient - we 124 // need to take subpixel snapping into account. Subpixel snapping 125 // happens with respect to the reference frame, so do the dev pixel 126 // conversion with our rectangle positioned relative to the reference 127 // frame, then get the size from there. 128 const auto* referenceFrame = nsLayoutUtils::GetReferenceFrame(aFrame); 129 // GetOffsetToCrossDoc version handles <iframe>s in addition to normal 130 // cases. We don't expect this to tight loop for additional checks to 131 // matter. 132 const auto offset = aFrame->GetOffsetToCrossDoc(referenceFrame); 133 const auto contentSize = GetContentRectSize(*aFrame); 134 // Casting to double here is deliberate to minimize rounding error in 135 // upcoming operations. 136 const auto appUnitsPerDevPixel = 137 static_cast<double>(aFrame->PresContext()->AppUnitsPerDevPixel()); 138 // Calculation here is a greatly simplified version of 139 // `NSRectToSnappedRect` as 1) we're not actually drawing (i.e. no draw 140 // target), and 2) transform does not need to be taken into account. 141 gfx::Rect rect{gfx::Float(offset.X() / appUnitsPerDevPixel), 142 gfx::Float(offset.Y() / appUnitsPerDevPixel), 143 gfx::Float(contentSize.Width() / appUnitsPerDevPixel), 144 gfx::Float(contentSize.Height() / appUnitsPerDevPixel)}; 145 gfx::Point tl = rect.TopLeft().Round(); 146 gfx::Point br = rect.BottomRight().Round(); 147 148 rect.SizeTo(gfx::Size(br.x - tl.x, br.y - tl.y)); 149 rect.NudgeToIntegers(); 150 return rect.Size().ToUnknownSize(); 151 } 152 case ResizeObserverBoxOptions::Content_box: 153 default: 154 break; 155 } 156 return CSSPixel::FromAppUnits(zoom.Unzoom(GetContentRectSize(*aFrame))) 157 .ToUnknownSize(); 158 }; 159 if (!StaticPrefs::dom_resize_observer_support_fragments() && 160 !aForceFragmentHandling) { 161 return {LogicalPixelSize(frame->GetWritingMode(), GetFrameSize(frame))}; 162 } 163 AutoTArray<LogicalPixelSize, 1> size; 164 for (nsIFrame* cur = frame; cur; cur = cur->GetNextContinuation()) { 165 const WritingMode wm = cur->GetWritingMode(); 166 size.AppendElement(LogicalPixelSize(wm, GetFrameSize(cur))); 167 } 168 return size; 169 } 170 171 NS_IMPL_CYCLE_COLLECTION_CLASS(ResizeObservation) 172 173 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ResizeObservation) 174 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTarget); 175 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 176 177 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ResizeObservation) 178 tmp->Unlink(RemoveFromObserver::Yes); 179 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 180 181 ResizeObservation::ResizeObservation(Element& aTarget, 182 ResizeObserver& aObserver, 183 ResizeObserverBoxOptions aBox) 184 : mTarget(&aTarget), 185 mObserver(&aObserver), 186 mObservedBox(aBox), 187 mLastReportedSize({LogicalPixelSize(WritingMode(), gfx::Size(-1, -1))}) { 188 aTarget.BindObject(mObserver); 189 } 190 191 void ResizeObservation::Unlink(RemoveFromObserver aRemoveFromObserver) { 192 ResizeObserver* observer = std::exchange(mObserver, nullptr); 193 nsCOMPtr<Element> target = std::move(mTarget); 194 if (observer && target) { 195 if (aRemoveFromObserver == RemoveFromObserver::Yes) { 196 observer->Unobserve(*target); 197 } 198 target->UnbindObject(observer); 199 } 200 } 201 202 bool ResizeObservation::IsActive() const { 203 // As detailed in the css-contain specification, if the target is hidden by 204 // `content-visibility` it should not call its ResizeObservation callbacks. 205 nsIFrame* frame = mTarget->GetPrimaryFrame(); 206 if (frame && frame->IsHiddenByContentVisibilityOnAnyAncestor()) { 207 return false; 208 } 209 210 return mLastReportedSize != 211 ResizeObserver::CalculateBoxSize(mTarget, mObservedBox); 212 } 213 214 void ResizeObservation::UpdateLastReportedSize( 215 const nsTArray<LogicalPixelSize>& aSize) { 216 mLastReportedSize.Assign(aSize); 217 } 218 219 // Only needed for refcounted objects. 220 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ResizeObserver) 221 222 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ResizeObserver) 223 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner, mDocument, mCallback, 224 mActiveTargets, mObservationMap); 225 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 226 227 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ResizeObserver) 228 tmp->Disconnect(); 229 NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner, mDocument, mCallback, mActiveTargets, 230 mObservationMap); 231 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER 232 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 233 234 NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserver) 235 NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserver) 236 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserver) 237 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY 238 NS_INTERFACE_MAP_ENTRY(nsISupports) 239 NS_INTERFACE_MAP_END 240 241 already_AddRefed<ResizeObserver> ResizeObserver::Constructor( 242 const GlobalObject& aGlobal, ResizeObserverCallback& aCb, 243 ErrorResult& aRv) { 244 nsCOMPtr<nsPIDOMWindowInner> window = 245 do_QueryInterface(aGlobal.GetAsSupports()); 246 if (!window) { 247 aRv.Throw(NS_ERROR_FAILURE); 248 return nullptr; 249 } 250 251 Document* doc = window->GetExtantDoc(); 252 if (!doc) { 253 aRv.Throw(NS_ERROR_FAILURE); 254 return nullptr; 255 } 256 257 return do_AddRef(new ResizeObserver(std::move(window), doc, aCb)); 258 } 259 260 void ResizeObserver::Observe(Element& aTarget, 261 const ResizeObserverOptions& aOptions) { 262 if (MOZ_UNLIKELY(!mDocument)) { 263 MOZ_ASSERT_UNREACHABLE("How did we call observe() after unlink?"); 264 return; 265 } 266 267 // NOTE(emilio): Per spec, this is supposed to happen on construction, but the 268 // spec isn't particularly sane here, see 269 // https://github.com/w3c/csswg-drafts/issues/4518 270 if (mObservationList.isEmpty()) { 271 MOZ_ASSERT(mObservationMap.IsEmpty()); 272 mDocument->AddResizeObserver(*this); 273 } 274 275 auto& observation = mObservationMap.LookupOrInsert(&aTarget); 276 if (observation) { 277 if (observation->BoxOptions() == aOptions.mBox) { 278 // Already observed this target and the observed box is the same, so 279 // return. 280 // Note: Based on the spec, we should unobserve it first. However, 281 // calling Unobserve() when we observe the same box will remove original 282 // ResizeObservation and then add a new one, this may cause an unexpected 283 // result because ResizeObservation stores the mLastReportedSize which 284 // should be kept to make sure IsActive() returns the correct result. 285 return; 286 } 287 // Remove the pre-existing entry, but without unregistering ourselves from 288 // the controller. 289 observation->remove(); 290 observation = nullptr; 291 } 292 293 observation = new ResizeObservation(aTarget, *this, aOptions.mBox); 294 mObservationList.insertBack(observation); 295 296 // Per the spec, we need to trigger notification in event loop that 297 // contains ResizeObserver observe call even when resize/reflow does 298 // not happen. 299 mDocument->ScheduleResizeObserversNotification(); 300 } 301 302 void ResizeObserver::Unobserve(Element& aTarget) { 303 RefPtr<ResizeObservation> observation; 304 if (!mObservationMap.Remove(&aTarget, getter_AddRefs(observation))) { 305 return; 306 } 307 308 MOZ_ASSERT(!mObservationList.isEmpty(), 309 "If ResizeObservation found for an element, observation list " 310 "must be not empty."); 311 observation->remove(); 312 if (mObservationList.isEmpty()) { 313 if (MOZ_LIKELY(mDocument)) { 314 mDocument->RemoveResizeObserver(*this); 315 } 316 } 317 } 318 319 void ResizeObserver::Disconnect() { 320 const bool registered = !mObservationList.isEmpty(); 321 while (auto* observation = mObservationList.popFirst()) { 322 observation->Unlink(ResizeObservation::RemoveFromObserver::No); 323 } 324 MOZ_ASSERT(mObservationList.isEmpty()); 325 mObservationMap.Clear(); 326 mActiveTargets.Clear(); 327 if (registered && MOZ_LIKELY(mDocument)) { 328 mDocument->RemoveResizeObserver(*this); 329 } 330 } 331 332 void ResizeObserver::GatherActiveObservations(uint32_t aDepth) { 333 mActiveTargets.Clear(); 334 mHasSkippedTargets = false; 335 336 for (auto* observation : mObservationList) { 337 if (!observation->IsActive()) { 338 continue; 339 } 340 341 uint32_t targetDepth = GetNodeDepth(observation->Target()); 342 343 if (targetDepth > aDepth) { 344 mActiveTargets.AppendElement(observation); 345 } else { 346 mHasSkippedTargets = true; 347 } 348 } 349 } 350 351 uint32_t ResizeObserver::BroadcastActiveObservations() { 352 uint32_t shallowestTargetDepth = std::numeric_limits<uint32_t>::max(); 353 354 if (!HasActiveObservations()) { 355 return shallowestTargetDepth; 356 } 357 358 Sequence<OwningNonNull<ResizeObserverEntry>> entries; 359 360 for (auto& observation : mActiveTargets) { 361 Element* target = observation->Target(); 362 363 auto borderBoxSize = ResizeObserver::CalculateBoxSize( 364 target, ResizeObserverBoxOptions::Border_box); 365 auto contentBoxSize = ResizeObserver::CalculateBoxSize( 366 target, ResizeObserverBoxOptions::Content_box); 367 auto devicePixelContentBoxSize = ResizeObserver::CalculateBoxSize( 368 target, ResizeObserverBoxOptions::Device_pixel_content_box); 369 RefPtr<ResizeObserverEntry> entry = 370 new ResizeObserverEntry(mOwner, *target, borderBoxSize, contentBoxSize, 371 devicePixelContentBoxSize); 372 373 if (!entries.AppendElement(entry.forget(), fallible)) { 374 // Out of memory. 375 break; 376 } 377 378 // Sync the broadcast size of observation so the next size inspection 379 // will be based on the updated size from last delivered observations. 380 switch (observation->BoxOptions()) { 381 case ResizeObserverBoxOptions::Border_box: 382 observation->UpdateLastReportedSize(borderBoxSize); 383 break; 384 case ResizeObserverBoxOptions::Device_pixel_content_box: 385 observation->UpdateLastReportedSize(devicePixelContentBoxSize); 386 break; 387 case ResizeObserverBoxOptions::Content_box: 388 default: 389 observation->UpdateLastReportedSize(contentBoxSize); 390 } 391 392 uint32_t targetDepth = GetNodeDepth(observation->Target()); 393 394 if (targetDepth < shallowestTargetDepth) { 395 shallowestTargetDepth = targetDepth; 396 } 397 } 398 399 RefPtr<ResizeObserverCallback> callback(mCallback); 400 callback->Call(this, entries, *this); 401 402 mActiveTargets.Clear(); 403 mHasSkippedTargets = false; 404 405 return shallowestTargetDepth; 406 } 407 408 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserverEntry, mOwner, mTarget, 409 mContentRect, mBorderBoxSize, 410 mContentBoxSize, 411 mDevicePixelContentBoxSize) 412 NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserverEntry) 413 NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserverEntry) 414 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserverEntry) 415 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY 416 NS_INTERFACE_MAP_ENTRY(nsISupports) 417 NS_INTERFACE_MAP_END 418 419 void ResizeObserverEntry::GetBorderBoxSize( 420 nsTArray<RefPtr<ResizeObserverSize>>& aRetVal) const { 421 // In the resize-observer-1 spec, there will only be a single 422 // ResizeObserverSize returned in the FrozenArray for now. 423 // 424 // Note: the usage of FrozenArray is to support elements that have multiple 425 // fragments, which occur in multi-column scenarios. 426 // https://drafts.csswg.org/resize-observer/#resize-observer-entry-interface 427 aRetVal.Assign(mBorderBoxSize); 428 } 429 430 void ResizeObserverEntry::GetContentBoxSize( 431 nsTArray<RefPtr<ResizeObserverSize>>& aRetVal) const { 432 // In the resize-observer-1 spec, there will only be a single 433 // ResizeObserverSize returned in the FrozenArray for now. 434 // 435 // Note: the usage of FrozenArray is to support elements that have multiple 436 // fragments, which occur in multi-column scenarios. 437 // https://drafts.csswg.org/resize-observer/#resize-observer-entry-interface 438 aRetVal.Assign(mContentBoxSize); 439 } 440 441 void ResizeObserverEntry::GetDevicePixelContentBoxSize( 442 nsTArray<RefPtr<ResizeObserverSize>>& aRetVal) const { 443 // In the resize-observer-1 spec, there will only be a single 444 // ResizeObserverSize returned in the FrozenArray for now. 445 // 446 // Note: the usage of FrozenArray is to support elements that have multiple 447 // fragments, which occur in multi-column scenarios. 448 // https://drafts.csswg.org/resize-observer/#resize-observer-entry-interface 449 aRetVal.Assign(mDevicePixelContentBoxSize); 450 } 451 452 void ResizeObserverEntry::SetBorderBoxSize( 453 const nsTArray<LogicalPixelSize>& aSize) { 454 mBorderBoxSize.Clear(); 455 mBorderBoxSize.SetCapacity(aSize.Length()); 456 for (const LogicalPixelSize& size : aSize) { 457 mBorderBoxSize.AppendElement(new ResizeObserverSize(mOwner, size)); 458 } 459 } 460 461 void ResizeObserverEntry::SetContentRectAndSize( 462 const nsTArray<LogicalPixelSize>& aSize) { 463 nsIFrame* frame = mTarget->GetPrimaryFrame(); 464 465 // 1. Update mContentRect. 466 mContentRect = [&] { 467 nsMargin padding = frame ? frame->GetUsedPadding() : nsMargin(); 468 const auto zoom = frame ? frame->Style()->EffectiveZoom() : StyleZoom::ONE; 469 // Per the spec, we need to use the top-left padding offset as the origin of 470 // our contentRect. 471 // NOTE(emilio): aSize already has been unzoomed if needed. 472 const nsPoint origin = zoom.Unzoom(nsPoint(padding.left, padding.top)); 473 474 gfx::Size sizeForRect; 475 MOZ_DIAGNOSTIC_ASSERT(!aSize.IsEmpty()); 476 if (!aSize.IsEmpty()) { 477 const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode(); 478 sizeForRect = aSize[0].PhysicalSize(wm); 479 } 480 nsRect rect(origin, 481 CSSPixel::ToAppUnits(CSSSize::FromUnknownSize(sizeForRect))); 482 RefPtr<DOMRect> contentRect = new DOMRect(mOwner); 483 contentRect->SetLayoutRect(rect); 484 return contentRect.forget(); 485 }(); 486 487 // 2. Update mContentBoxSize. 488 mContentBoxSize.Clear(); 489 mContentBoxSize.SetCapacity(aSize.Length()); 490 for (const LogicalPixelSize& size : aSize) { 491 mContentBoxSize.AppendElement(new ResizeObserverSize(mOwner, size)); 492 } 493 } 494 495 void ResizeObserverEntry::SetDevicePixelContentSize( 496 const nsTArray<LogicalPixelSize>& aSize) { 497 mDevicePixelContentBoxSize.Clear(); 498 mDevicePixelContentBoxSize.SetCapacity(aSize.Length()); 499 for (const LogicalPixelSize& size : aSize) { 500 mDevicePixelContentBoxSize.AppendElement( 501 new ResizeObserverSize(mOwner, size)); 502 } 503 } 504 505 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserverSize, mOwner) 506 NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserverSize) 507 NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserverSize) 508 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserverSize) 509 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY 510 NS_INTERFACE_MAP_ENTRY(nsISupports) 511 NS_INTERFACE_MAP_END 512 513 } // namespace mozilla::dom