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"
18 namespace mozilla::dom
{
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
) {
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())) {
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();
56 aFrame
.GetUsedPadding().ApplySkipSides(aFrame
.GetSkipSides());
57 scrollPort
.Deflate(padding
);
58 // This can break in some edge cases like when layout overflows sizes or
61 !aFrame
.PresContext()->UseOverlayScrollbars() ||
62 scrollPort
.Size() == aFrame
.GetContentRectRelativeToSelf().Size(),
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();
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
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
) {
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
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
:
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
)));
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
)
179 mObserver(&aObserver
),
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()) {
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
)
223 NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner
, mDocument
, mCallback
, mActiveTargets
,
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
)
235 already_AddRefed
<ResizeObserver
> ResizeObserver::Constructor(
236 const GlobalObject
& aGlobal
, ResizeObserverCallback
& aCb
,
238 nsCOMPtr
<nsPIDOMWindowInner
> window
=
239 do_QueryInterface(aGlobal
.GetAsSupports());
241 aRv
.Throw(NS_ERROR_FAILURE
);
245 Document
* doc
= window
->GetExtantDoc();
247 aRv
.Throw(NS_ERROR_FAILURE
);
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?");
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
);
271 if (observation
->BoxOptions() == aOptions
.mBox
) {
272 // Already observed this target and the observed box is the same, so
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.
281 // Remove the pre-existing entry, but without unregistering ourselves from
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
293 mDocument
->ScheduleResizeObserversNotification();
296 void ResizeObserver::Unobserve(Element
& aTarget
) {
297 RefPtr
<ResizeObservation
> observation
;
298 if (!mObservationMap
.Remove(&aTarget
, getter_AddRefs(observation
))) {
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()) {
335 uint32_t targetDepth
= GetNodeDepth(observation
->Target());
337 if (targetDepth
> aDepth
) {
338 mActiveTargets
.AppendElement(observation
);
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
)) {
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
);
378 case ResizeObserverBoxOptions::Device_pixel_content_box
:
379 observation
->UpdateLastReportedSize(devicePixelContentBoxSize
);
381 case ResizeObserverBoxOptions::Content_box
:
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
,
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
)
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
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
)
501 } // namespace mozilla::dom