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