Bug 1750871 - run mochitest-remote on fission everywhere. r=releng-reviewers,aki
[gecko.git] / dom / base / ResizeObserver.cpp
blob5448ac4cccbb5da932e8293fe7331b61cf7b480a
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 <limits>
16 namespace mozilla::dom {
18 /**
19 * Returns the length of the parent-traversal path (in terms of the number of
20 * nodes) to an unparented/root node from aNode. An unparented/root node is
21 * considered to have a depth of 1, its children have a depth of 2, etc.
22 * aNode is expected to be non-null.
23 * Note: The shadow root is not part of the calculation because the caller,
24 * ResizeObserver, doesn't observe the shadow root, and only needs relative
25 * depths among all the observed targets. In other words, we calculate the
26 * depth of the flattened tree.
28 * However, these is a spec issue about how to handle shadow DOM case. We
29 * may need to update this function later:
30 * https://github.com/w3c/csswg-drafts/issues/3840
32 * https://drafts.csswg.org/resize-observer/#calculate-depth-for-node-h
34 static uint32_t GetNodeDepth(nsINode* aNode) {
35 uint32_t depth = 1;
37 MOZ_ASSERT(aNode, "Node shouldn't be null");
39 // Use GetFlattenedTreeParentNode to bypass the shadow root and cross the
40 // shadow boundary to calculate the node depth without the shadow root.
41 while ((aNode = aNode->GetFlattenedTreeParentNode())) {
42 ++depth;
45 return depth;
48 /**
49 * Returns |aTarget|'s size in the form of gfx::Size (in pixels).
50 * If the target is SVG, width and height are determined from bounding box.
52 * https://www.w3.org/TR/resize-observer-1/#calculate-box-size
54 static gfx::Size CalculateBoxSize(Element* aTarget,
55 ResizeObserverBoxOptions aBox) {
56 gfx::Size size;
57 nsIFrame* frame = aTarget->GetPrimaryFrame();
59 if (!frame) {
60 return size;
63 if (aTarget->IsSVGElement()) {
64 // Per the spec, SVG size is always its bounding box size no matter what
65 // box option you choose, because SVG elements do not use standard CSS box
66 // model.
67 const gfxRect bbox = SVGUtils::GetBBox(frame);
68 size.width = static_cast<float>(bbox.width);
69 size.height = static_cast<float>(bbox.height);
70 if (aBox == ResizeObserverBoxOptions::Device_pixel_content_box) {
71 // Per spec, we calculate the inline/block sizes to target’s bounding box
72 // {inline|block} length, in integral device pixels, so we round the final
73 // result.
74 // https://drafts.csswg.org/resize-observer/#dom-resizeobserverboxoptions-device-pixel-content-box
75 const LayoutDeviceIntSize snappedSize =
76 RoundedToInt(CSSSize::FromUnknownSize(size) *
77 frame->PresContext()->CSSToDevPixelScale());
78 size = gfx::Size(snappedSize.ToUnknownSize());
80 } else {
81 // Per the spec, non-replaced inline Elements will always have an empty
82 // content rect. Therefore, we always use the same trivially-empty size
83 // for non-replaced inline elements here, and their IsActive() will
84 // always return false. (So its observation won't be fired.)
85 if (!frame->IsFrameOfType(nsIFrame::eReplaced) &&
86 frame->IsFrameOfType(nsIFrame::eLineParticipant)) {
87 return size;
90 switch (aBox) {
91 case ResizeObserverBoxOptions::Border_box:
92 // GetSize() includes the content area, borders, and padding.
93 size = CSSPixel::FromAppUnits(frame->GetSize()).ToUnknownSize();
94 break;
95 case ResizeObserverBoxOptions::Device_pixel_content_box: {
96 // This is a implementation-dependent for subpixel snapping algorithm.
97 // Gecko relys on LayoutDevicePixel to convert (and snap) the app units
98 // into device pixels in painting and gfx code, so here we simply
99 // convert it into dev pixels and round it.
101 // Note: This size must contain integer values.
102 // https://drafts.csswg.org/resize-observer/#dom-resizeobserverboxoptions-device-pixel-content-box
103 const LayoutDeviceIntSize snappedSize =
104 LayoutDevicePixel::FromAppUnitsRounded(
105 frame->GetContentRectRelativeToSelf().Size(),
106 frame->PresContext()->AppUnitsPerDevPixel());
107 size = gfx::Size(snappedSize.ToUnknownSize());
108 break;
110 case ResizeObserverBoxOptions::Content_box:
111 default:
112 size =
113 CSSPixel::FromAppUnits(frame->GetContentRectRelativeToSelf().Size())
114 .ToUnknownSize();
118 return size;
121 NS_IMPL_CYCLE_COLLECTION_CLASS(ResizeObservation)
123 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ResizeObservation)
124 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTarget);
125 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
127 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ResizeObservation)
128 tmp->Unlink(RemoveFromObserver::Yes);
129 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
131 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(ResizeObservation, AddRef)
132 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(ResizeObservation, Release)
134 ResizeObservation::ResizeObservation(Element& aTarget,
135 ResizeObserver& aObserver,
136 ResizeObserverBoxOptions aBox,
137 WritingMode aWm)
138 : mTarget(&aTarget), mObserver(&aObserver), mObservedBox(aBox) {
139 aTarget.BindObject(mObserver);
142 void ResizeObservation::Unlink(RemoveFromObserver aRemoveFromObserver) {
143 ResizeObserver* observer = std::exchange(mObserver, nullptr);
144 nsCOMPtr<Element> target = std::move(mTarget);
145 if (observer && target) {
146 if (aRemoveFromObserver == RemoveFromObserver::Yes) {
147 IgnoredErrorResult rv;
148 observer->Unobserve(*target, rv);
149 MOZ_DIAGNOSTIC_ASSERT(!rv.Failed(),
150 "How could we keep the observer and target around "
151 "without being in the observation map?");
153 target->UnbindObject(observer);
157 bool ResizeObservation::IsActive() const {
158 nsIFrame* frame = mTarget->GetPrimaryFrame();
159 const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
160 const LogicalPixelSize size(wm, CalculateBoxSize(mTarget, mObservedBox));
161 return mLastReportedSize != size;
164 void ResizeObservation::UpdateLastReportedSize(const gfx::Size& aSize) {
165 nsIFrame* frame = mTarget->GetPrimaryFrame();
166 WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
167 mLastReportedSize = {wm, aSize};
170 // Only needed for refcounted objects.
171 NS_IMPL_CYCLE_COLLECTION_CLASS(ResizeObserver)
173 NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(ResizeObserver)
175 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ResizeObserver)
176 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner, mDocument, mCallback,
177 mActiveTargets, mObservationMap);
178 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
180 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ResizeObserver)
181 tmp->Disconnect();
182 NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner, mDocument, mCallback, mActiveTargets,
183 mObservationMap);
184 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
185 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
187 NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserver)
188 NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserver)
189 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserver)
190 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
191 NS_INTERFACE_MAP_ENTRY(nsISupports)
192 NS_INTERFACE_MAP_END
194 already_AddRefed<ResizeObserver> ResizeObserver::Constructor(
195 const GlobalObject& aGlobal, ResizeObserverCallback& aCb,
196 ErrorResult& aRv) {
197 nsCOMPtr<nsPIDOMWindowInner> window =
198 do_QueryInterface(aGlobal.GetAsSupports());
199 if (!window) {
200 aRv.Throw(NS_ERROR_FAILURE);
201 return nullptr;
204 Document* doc = window->GetExtantDoc();
205 if (!doc) {
206 aRv.Throw(NS_ERROR_FAILURE);
207 return nullptr;
210 return do_AddRef(new ResizeObserver(std::move(window), doc, aCb));
213 void ResizeObserver::Observe(Element& aTarget,
214 const ResizeObserverOptions& aOptions,
215 ErrorResult& aRv) {
216 if (MOZ_UNLIKELY(!mDocument)) {
217 return aRv.Throw(NS_ERROR_FAILURE);
220 // NOTE(emilio): Per spec, this is supposed to happen on construction, but the
221 // spec isn't particularly sane here, see
222 // https://github.com/w3c/csswg-drafts/issues/4518
223 if (mObservationList.isEmpty()) {
224 MOZ_ASSERT(mObservationMap.IsEmpty());
225 mDocument->AddResizeObserver(*this);
228 auto& observation = mObservationMap.LookupOrInsert(&aTarget);
229 if (observation) {
230 if (observation->BoxOptions() == aOptions.mBox) {
231 // Already observed this target and the observed box is the same, so
232 // return.
233 // Note: Based on the spec, we should unobserve it first. However,
234 // calling Unobserve() when we observe the same box will remove original
235 // ResizeObservation and then add a new one, this may cause an unexpected
236 // result because ResizeObservation stores the mLastReportedSize which
237 // should be kept to make sure IsActive() returns the correct result.
238 return;
240 // Remove the pre-existing entry, but without unregistering ourselves from
241 // the controller.
242 observation->remove();
243 observation = nullptr;
246 // FIXME(emilio): This should probably either flush or not look at the
247 // writing-mode or something.
248 nsIFrame* frame = aTarget.GetPrimaryFrame();
249 observation =
250 new ResizeObservation(aTarget, *this, aOptions.mBox,
251 frame ? frame->GetWritingMode() : WritingMode());
252 mObservationList.insertBack(observation);
254 // Per the spec, we need to trigger notification in event loop that
255 // contains ResizeObserver observe call even when resize/reflow does
256 // not happen.
257 mDocument->ScheduleResizeObserversNotification();
260 void ResizeObserver::Unobserve(Element& aTarget, ErrorResult& aRv) {
261 RefPtr<ResizeObservation> observation;
262 if (!mObservationMap.Remove(&aTarget, getter_AddRefs(observation))) {
263 return;
266 MOZ_ASSERT(!mObservationList.isEmpty(),
267 "If ResizeObservation found for an element, observation list "
268 "must be not empty.");
269 observation->remove();
270 if (mObservationList.isEmpty()) {
271 if (MOZ_LIKELY(mDocument)) {
272 mDocument->RemoveResizeObserver(*this);
277 void ResizeObserver::Disconnect() {
278 const bool registered = !mObservationList.isEmpty();
279 while (auto* observation = mObservationList.popFirst()) {
280 observation->Unlink(ResizeObservation::RemoveFromObserver::No);
282 MOZ_ASSERT(mObservationList.isEmpty());
283 mObservationMap.Clear();
284 mActiveTargets.Clear();
285 if (registered && MOZ_LIKELY(mDocument)) {
286 mDocument->RemoveResizeObserver(*this);
290 void ResizeObserver::GatherActiveObservations(uint32_t aDepth) {
291 mActiveTargets.Clear();
292 mHasSkippedTargets = false;
294 for (auto* observation : mObservationList) {
295 if (!observation->IsActive()) {
296 continue;
299 uint32_t targetDepth = GetNodeDepth(observation->Target());
301 if (targetDepth > aDepth) {
302 mActiveTargets.AppendElement(observation);
303 } else {
304 mHasSkippedTargets = true;
309 uint32_t ResizeObserver::BroadcastActiveObservations() {
310 uint32_t shallowestTargetDepth = std::numeric_limits<uint32_t>::max();
312 if (!HasActiveObservations()) {
313 return shallowestTargetDepth;
316 Sequence<OwningNonNull<ResizeObserverEntry>> entries;
318 for (auto& observation : mActiveTargets) {
319 Element* target = observation->Target();
321 gfx::Size borderBoxSize =
322 CalculateBoxSize(target, ResizeObserverBoxOptions::Border_box);
323 gfx::Size contentBoxSize =
324 CalculateBoxSize(target, ResizeObserverBoxOptions::Content_box);
325 gfx::Size devicePixelContentBoxSize = CalculateBoxSize(
326 target, ResizeObserverBoxOptions::Device_pixel_content_box);
327 RefPtr<ResizeObserverEntry> entry =
328 new ResizeObserverEntry(this, *target, borderBoxSize, contentBoxSize,
329 devicePixelContentBoxSize);
331 if (!entries.AppendElement(entry.forget(), fallible)) {
332 // Out of memory.
333 break;
336 // Sync the broadcast size of observation so the next size inspection
337 // will be based on the updated size from last delivered observations.
338 switch (observation->BoxOptions()) {
339 case ResizeObserverBoxOptions::Border_box:
340 observation->UpdateLastReportedSize(borderBoxSize);
341 break;
342 case ResizeObserverBoxOptions::Device_pixel_content_box:
343 observation->UpdateLastReportedSize(devicePixelContentBoxSize);
344 break;
345 case ResizeObserverBoxOptions::Content_box:
346 default:
347 observation->UpdateLastReportedSize(contentBoxSize);
350 uint32_t targetDepth = GetNodeDepth(observation->Target());
352 if (targetDepth < shallowestTargetDepth) {
353 shallowestTargetDepth = targetDepth;
357 RefPtr<ResizeObserverCallback> callback(mCallback);
358 callback->Call(this, entries, *this);
360 mActiveTargets.Clear();
361 mHasSkippedTargets = false;
363 return shallowestTargetDepth;
366 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserverEntry, mOwner, mTarget,
367 mContentRect, mBorderBoxSize,
368 mContentBoxSize,
369 mDevicePixelContentBoxSize)
370 NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserverEntry)
371 NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserverEntry)
372 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserverEntry)
373 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
374 NS_INTERFACE_MAP_ENTRY(nsISupports)
375 NS_INTERFACE_MAP_END
377 void ResizeObserverEntry::GetBorderBoxSize(
378 nsTArray<RefPtr<ResizeObserverSize>>& aRetVal) const {
379 // In the resize-observer-1 spec, there will only be a single
380 // ResizeObserverSize returned in the FrozenArray for now.
382 // Note: the usage of FrozenArray is to support elements that have multiple
383 // fragments, which occur in multi-column scenarios.
384 // https://drafts.csswg.org/resize-observer/#resize-observer-entry-interface
385 aRetVal.Clear();
386 aRetVal.AppendElement(mBorderBoxSize);
389 void ResizeObserverEntry::GetContentBoxSize(
390 nsTArray<RefPtr<ResizeObserverSize>>& aRetVal) const {
391 // In the resize-observer-1 spec, there will only be a single
392 // ResizeObserverSize returned in the FrozenArray for now.
394 // Note: the usage of FrozenArray is to support elements that have multiple
395 // fragments, which occur in multi-column scenarios.
396 // https://drafts.csswg.org/resize-observer/#resize-observer-entry-interface
397 aRetVal.Clear();
398 aRetVal.AppendElement(mContentBoxSize);
401 void ResizeObserverEntry::GetDevicePixelContentBoxSize(
402 nsTArray<RefPtr<ResizeObserverSize>>& aRetVal) const {
403 // In the resize-observer-1 spec, there will only be a single
404 // ResizeObserverSize returned in the FrozenArray for now.
406 // Note: the usage of FrozenArray is to support elements that have multiple
407 // fragments, which occur in multi-column scenarios.
408 // https://drafts.csswg.org/resize-observer/#resize-observer-entry-interface
409 aRetVal.Clear();
410 aRetVal.AppendElement(mDevicePixelContentBoxSize);
413 void ResizeObserverEntry::SetBorderBoxSize(const gfx::Size& aSize) {
414 nsIFrame* frame = mTarget->GetPrimaryFrame();
415 const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
416 mBorderBoxSize = new ResizeObserverSize(this, aSize, wm);
419 void ResizeObserverEntry::SetContentRectAndSize(const gfx::Size& aSize) {
420 nsIFrame* frame = mTarget->GetPrimaryFrame();
422 // 1. Update mContentRect.
423 nsMargin padding = frame ? frame->GetUsedPadding() : nsMargin();
424 // Per the spec, we need to use the top-left padding offset as the origin of
425 // our contentRect.
426 nsRect rect(nsPoint(padding.left, padding.top),
427 CSSPixel::ToAppUnits(CSSSize::FromUnknownSize(aSize)));
428 RefPtr<DOMRect> contentRect = new DOMRect(this);
429 contentRect->SetLayoutRect(rect);
430 mContentRect = std::move(contentRect);
432 // 2. Update mContentBoxSize.
433 const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
434 mContentBoxSize = new ResizeObserverSize(this, aSize, wm);
437 void ResizeObserverEntry::SetDevicePixelContentSize(const gfx::Size& aSize) {
438 nsIFrame* frame = mTarget->GetPrimaryFrame();
439 const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
440 mDevicePixelContentBoxSize = new ResizeObserverSize(this, aSize, wm);
443 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserverSize, mOwner)
444 NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserverSize)
445 NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserverSize)
446 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserverSize)
447 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
448 NS_INTERFACE_MAP_ENTRY(nsISupports)
449 NS_INTERFACE_MAP_END
451 } // namespace mozilla::dom