Bug 1686668 [wpt PR 27185] - Update wpt metadata, a=testonly
[gecko.git] / dom / base / ResizeObserver.cpp
blob77fe99a3714510a3191345ce44af2245efeaa421
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 nsSize.
50 * If the target is SVG, width and height are determined from bounding box.
52 static nsSize GetTargetSize(Element* aTarget, ResizeObserverBoxOptions aBox) {
53 nsSize size;
54 nsIFrame* frame = aTarget->GetPrimaryFrame();
56 if (!frame) {
57 return size;
60 if (aTarget->IsSVGElement()) {
61 // Per the spec, SVG size is always its bounding box size no matter what
62 // box option you choose, because SVG elements do not use standard CSS box
63 // model.
64 gfxRect bbox = SVGUtils::GetBBox(frame);
65 size.width = NSFloatPixelsToAppUnits(bbox.width, AppUnitsPerCSSPixel());
66 size.height = NSFloatPixelsToAppUnits(bbox.height, AppUnitsPerCSSPixel());
67 } else {
68 // Per the spec, non-replaced inline Elements will always have an empty
69 // content rect. Therefore, we always use the same trivially-empty size
70 // for non-replaced inline elements here, and their IsActive() will
71 // always return false. (So its observation won't be fired.)
72 if (!frame->IsFrameOfType(nsIFrame::eReplaced) &&
73 frame->IsFrameOfType(nsIFrame::eLineParticipant)) {
74 return size;
77 switch (aBox) {
78 case ResizeObserverBoxOptions::Border_box:
79 // GetSize() includes the content area, borders, and padding.
80 size = frame->GetSize();
81 break;
82 case ResizeObserverBoxOptions::Content_box:
83 default:
84 size = frame->GetContentRectRelativeToSelf().Size();
88 return size;
91 NS_IMPL_CYCLE_COLLECTION(ResizeObservation, mTarget)
92 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(ResizeObservation, AddRef)
93 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(ResizeObservation, Release)
95 bool ResizeObservation::IsActive() const {
96 nsIFrame* frame = mTarget->GetPrimaryFrame();
97 const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
98 const LogicalSize size(wm, GetTargetSize(mTarget, mObservedBox));
99 return mLastReportedSize.ISize(mLastReportedWM) != size.ISize(wm) ||
100 mLastReportedSize.BSize(mLastReportedWM) != size.BSize(wm);
103 void ResizeObservation::UpdateLastReportedSize(const nsSize& aSize) {
104 nsIFrame* frame = mTarget->GetPrimaryFrame();
105 mLastReportedWM = frame ? frame->GetWritingMode() : WritingMode();
106 mLastReportedSize = LogicalSize(mLastReportedWM, aSize);
109 // Only needed for refcounted objects.
110 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserver, mOwner, mDocument,
111 mCallback, mActiveTargets,
112 mObservationMap)
113 NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserver)
114 NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserver)
115 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserver)
116 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
117 NS_INTERFACE_MAP_ENTRY(nsISupports)
118 NS_INTERFACE_MAP_END
120 already_AddRefed<ResizeObserver> ResizeObserver::Constructor(
121 const GlobalObject& aGlobal, ResizeObserverCallback& aCb,
122 ErrorResult& aRv) {
123 nsCOMPtr<nsPIDOMWindowInner> window =
124 do_QueryInterface(aGlobal.GetAsSupports());
125 if (!window) {
126 aRv.Throw(NS_ERROR_FAILURE);
127 return nullptr;
130 Document* doc = window->GetExtantDoc();
131 if (!doc) {
132 aRv.Throw(NS_ERROR_FAILURE);
133 return nullptr;
136 return do_AddRef(new ResizeObserver(std::move(window), doc, aCb));
139 void ResizeObserver::Observe(Element& aTarget,
140 const ResizeObserverOptions& aOptions,
141 ErrorResult& aRv) {
142 // NOTE(emilio): Per spec, this is supposed to happen on construction, but the
143 // spec isn't particularly sane here, see
144 // https://github.com/w3c/csswg-drafts/issues/4518
145 if (mObservationList.isEmpty()) {
146 MOZ_ASSERT(mObservationMap.IsEmpty());
147 if (MOZ_UNLIKELY(!mDocument)) {
148 return aRv.Throw(NS_ERROR_FAILURE);
150 mDocument->AddResizeObserver(*this);
153 RefPtr<ResizeObservation>& observation =
154 mObservationMap.LookupForAdd(&aTarget).OrInsert([] { return nullptr; });
155 if (observation) {
156 if (observation->BoxOptions() == aOptions.mBox) {
157 // Already observed this target and the observed box is the same, so
158 // return.
159 // Note: Based on the spec, we should unobserve it first. However,
160 // calling Unobserve() when we observe the same box will remove original
161 // ResizeObservation and then add a new one, this may cause an unexpected
162 // result because ResizeObservation stores the mLastReportedSize which
163 // should be kept to make sure IsActive() returns the correct result.
164 return;
166 // Remove the pre-existing entry, but without unregistering ourselves from
167 // the controller.
168 observation->remove();
169 observation = nullptr;
172 // FIXME(emilio): This should probably either flush or not look at the
173 // writing-mode or something.
174 nsIFrame* frame = aTarget.GetPrimaryFrame();
175 observation = new ResizeObservation(
176 aTarget, aOptions.mBox, frame ? frame->GetWritingMode() : WritingMode());
177 mObservationList.insertBack(observation);
179 // Per the spec, we need to trigger notification in event loop that
180 // contains ResizeObserver observe call even when resize/reflow does
181 // not happen.
182 aTarget.OwnerDoc()->ScheduleResizeObserversNotification();
185 void ResizeObserver::Unobserve(Element& aTarget, ErrorResult& aRv) {
186 RefPtr<ResizeObservation> observation;
187 if (!mObservationMap.Remove(&aTarget, getter_AddRefs(observation))) {
188 return;
191 MOZ_ASSERT(!mObservationList.isEmpty(),
192 "If ResizeObservation found for an element, observation list "
193 "must be not empty.");
194 observation->remove();
195 if (mObservationList.isEmpty()) {
196 if (MOZ_LIKELY(mDocument)) {
197 mDocument->RemoveResizeObserver(*this);
202 void ResizeObserver::Disconnect() {
203 const bool registered = !mObservationList.isEmpty();
204 mObservationList.clear();
205 mObservationMap.Clear();
206 mActiveTargets.Clear();
207 if (registered && MOZ_LIKELY(mDocument)) {
208 mDocument->RemoveResizeObserver(*this);
212 void ResizeObserver::GatherActiveObservations(uint32_t aDepth) {
213 mActiveTargets.Clear();
214 mHasSkippedTargets = false;
216 for (auto* observation : mObservationList) {
217 if (!observation->IsActive()) {
218 continue;
221 uint32_t targetDepth = GetNodeDepth(observation->Target());
223 if (targetDepth > aDepth) {
224 mActiveTargets.AppendElement(observation);
225 } else {
226 mHasSkippedTargets = true;
231 uint32_t ResizeObserver::BroadcastActiveObservations() {
232 uint32_t shallowestTargetDepth = std::numeric_limits<uint32_t>::max();
234 if (!HasActiveObservations()) {
235 return shallowestTargetDepth;
238 Sequence<OwningNonNull<ResizeObserverEntry>> entries;
240 for (auto& observation : mActiveTargets) {
241 Element* target = observation->Target();
243 nsSize borderBoxSize =
244 GetTargetSize(target, ResizeObserverBoxOptions::Border_box);
245 nsSize contentBoxSize =
246 GetTargetSize(target, ResizeObserverBoxOptions::Content_box);
247 RefPtr<ResizeObserverEntry> entry =
248 new ResizeObserverEntry(this, *target, borderBoxSize, contentBoxSize);
250 if (!entries.AppendElement(entry.forget(), fallible)) {
251 // Out of memory.
252 break;
255 // Sync the broadcast size of observation so the next size inspection
256 // will be based on the updated size from last delivered observations.
257 switch (observation->BoxOptions()) {
258 case ResizeObserverBoxOptions::Border_box:
259 observation->UpdateLastReportedSize(borderBoxSize);
260 break;
261 case ResizeObserverBoxOptions::Content_box:
262 default:
263 observation->UpdateLastReportedSize(contentBoxSize);
266 uint32_t targetDepth = GetNodeDepth(observation->Target());
268 if (targetDepth < shallowestTargetDepth) {
269 shallowestTargetDepth = targetDepth;
273 RefPtr<ResizeObserverCallback> callback(mCallback);
274 callback->Call(this, entries, *this);
276 mActiveTargets.Clear();
277 mHasSkippedTargets = false;
279 return shallowestTargetDepth;
282 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserverEntry, mOwner, mTarget,
283 mContentRect, mBorderBoxSize,
284 mContentBoxSize)
285 NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserverEntry)
286 NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserverEntry)
287 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserverEntry)
288 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
289 NS_INTERFACE_MAP_ENTRY(nsISupports)
290 NS_INTERFACE_MAP_END
292 void ResizeObserverEntry::SetBorderBoxSize(const nsSize& aSize) {
293 nsIFrame* frame = mTarget->GetPrimaryFrame();
294 const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
295 mBorderBoxSize = new ResizeObserverSize(this, aSize, wm);
298 void ResizeObserverEntry::SetContentRectAndSize(const nsSize& aSize) {
299 nsIFrame* frame = mTarget->GetPrimaryFrame();
301 // 1. Update mContentRect.
302 nsMargin padding = frame ? frame->GetUsedPadding() : nsMargin();
303 // Per the spec, we need to use the top-left padding offset as the origin of
304 // our contentRect.
305 nsRect rect(nsPoint(padding.left, padding.top), aSize);
306 RefPtr<DOMRect> contentRect = new DOMRect(this);
307 contentRect->SetLayoutRect(rect);
308 mContentRect = std::move(contentRect);
310 // 2. Update mContentBoxSize.
311 const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
312 mContentBoxSize = new ResizeObserverSize(this, aSize, wm);
315 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserverSize, mOwner)
316 NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserverSize)
317 NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserverSize)
318 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserverSize)
319 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
320 NS_INTERFACE_MAP_ENTRY(nsISupports)
321 NS_INTERFACE_MAP_END
323 } // namespace mozilla::dom