Bug 1550519 - Show a translucent parent highlight when a subgrid is highlighted....
[gecko.git] / dom / base / DOMIntersectionObserver.cpp
blob099933993ae20f8a125933b0dd27e087d20d81c8
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
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 file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "DOMIntersectionObserver.h"
8 #include "nsCSSPropertyID.h"
9 #include "nsIFrame.h"
10 #include "nsContentUtils.h"
11 #include "nsLayoutUtils.h"
12 #include "mozilla/PresShell.h"
13 #include "mozilla/ServoBindings.h"
15 namespace mozilla {
16 namespace dom {
18 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry)
19 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
20 NS_INTERFACE_MAP_ENTRY(nsISupports)
21 NS_INTERFACE_MAP_END
23 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserverEntry)
24 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserverEntry)
26 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DOMIntersectionObserverEntry, mOwner,
27 mRootBounds, mBoundingClientRect,
28 mIntersectionRect, mTarget)
30 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserver)
31 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
32 NS_INTERFACE_MAP_ENTRY(nsISupports)
33 NS_INTERFACE_MAP_ENTRY(DOMIntersectionObserver)
34 NS_INTERFACE_MAP_END
36 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserver)
37 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserver)
39 NS_IMPL_CYCLE_COLLECTION_CLASS(DOMIntersectionObserver)
41 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(DOMIntersectionObserver)
42 NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
43 NS_IMPL_CYCLE_COLLECTION_TRACE_END
45 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMIntersectionObserver)
46 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
47 tmp->Disconnect();
48 NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
49 NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
50 NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback)
51 NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoot)
52 NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries)
53 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
55 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMIntersectionObserver)
56 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
57 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
58 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback)
59 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
60 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries)
61 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
63 already_AddRefed<DOMIntersectionObserver> DOMIntersectionObserver::Constructor(
64 const mozilla::dom::GlobalObject& aGlobal,
65 mozilla::dom::IntersectionCallback& aCb, mozilla::ErrorResult& aRv) {
66 return Constructor(aGlobal, aCb, IntersectionObserverInit(), aRv);
69 already_AddRefed<DOMIntersectionObserver> DOMIntersectionObserver::Constructor(
70 const mozilla::dom::GlobalObject& aGlobal,
71 mozilla::dom::IntersectionCallback& aCb,
72 const mozilla::dom::IntersectionObserverInit& aOptions,
73 mozilla::ErrorResult& aRv) {
74 nsCOMPtr<nsPIDOMWindowInner> window =
75 do_QueryInterface(aGlobal.GetAsSupports());
76 if (!window) {
77 aRv.Throw(NS_ERROR_FAILURE);
78 return nullptr;
80 RefPtr<DOMIntersectionObserver> observer =
81 new DOMIntersectionObserver(window.forget(), aCb);
83 observer->mRoot = aOptions.mRoot;
85 if (!observer->SetRootMargin(aOptions.mRootMargin)) {
86 aRv.ThrowDOMException(
87 NS_ERROR_DOM_SYNTAX_ERR,
88 NS_LITERAL_CSTRING(
89 "rootMargin must be specified in pixels or percent."));
90 return nullptr;
93 if (aOptions.mThreshold.IsDoubleSequence()) {
94 const mozilla::dom::Sequence<double>& thresholds =
95 aOptions.mThreshold.GetAsDoubleSequence();
96 observer->mThresholds.SetCapacity(thresholds.Length());
97 for (const auto& thresh : thresholds) {
98 if (thresh < 0.0 || thresh > 1.0) {
99 aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
100 return nullptr;
102 observer->mThresholds.AppendElement(thresh);
104 observer->mThresholds.Sort();
105 } else {
106 double thresh = aOptions.mThreshold.GetAsDouble();
107 if (thresh < 0.0 || thresh > 1.0) {
108 aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
109 return nullptr;
111 observer->mThresholds.AppendElement(thresh);
114 return observer.forget();
117 bool DOMIntersectionObserver::SetRootMargin(const nsAString& aString) {
118 return Servo_IntersectionObserverRootMargin_Parse(&aString, &mRootMargin);
121 void DOMIntersectionObserver::GetRootMargin(mozilla::dom::DOMString& aRetVal) {
122 nsString& retVal = aRetVal;
123 Servo_IntersectionObserverRootMargin_ToString(&mRootMargin, &retVal);
126 void DOMIntersectionObserver::GetThresholds(nsTArray<double>& aRetVal) {
127 aRetVal = mThresholds;
130 void DOMIntersectionObserver::Observe(Element& aTarget) {
131 if (mObservationTargets.Contains(&aTarget)) {
132 return;
134 aTarget.RegisterIntersectionObserver(this);
135 mObservationTargets.AppendElement(&aTarget);
136 Connect();
139 void DOMIntersectionObserver::Unobserve(Element& aTarget) {
140 if (!mObservationTargets.Contains(&aTarget)) {
141 return;
144 if (mObservationTargets.Length() == 1) {
145 Disconnect();
146 return;
149 mObservationTargets.RemoveElement(&aTarget);
150 aTarget.UnregisterIntersectionObserver(this);
153 void DOMIntersectionObserver::UnlinkTarget(Element& aTarget) {
154 mObservationTargets.RemoveElement(&aTarget);
155 if (mObservationTargets.Length() == 0) {
156 Disconnect();
160 void DOMIntersectionObserver::Connect() {
161 if (mConnected) {
162 return;
165 mConnected = true;
166 if (mDocument) {
167 mDocument->AddIntersectionObserver(this);
171 void DOMIntersectionObserver::Disconnect() {
172 if (!mConnected) {
173 return;
176 mConnected = false;
177 for (size_t i = 0; i < mObservationTargets.Length(); ++i) {
178 Element* target = mObservationTargets.ElementAt(i);
179 target->UnregisterIntersectionObserver(this);
181 mObservationTargets.Clear();
182 if (mDocument) {
183 mDocument->RemoveIntersectionObserver(this);
187 void DOMIntersectionObserver::TakeRecords(
188 nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal) {
189 aRetVal.SwapElements(mQueuedEntries);
190 mQueuedEntries.Clear();
193 static Maybe<nsRect> EdgeInclusiveIntersection(const nsRect& aRect,
194 const nsRect& aOtherRect) {
195 nscoord left = std::max(aRect.x, aOtherRect.x);
196 nscoord top = std::max(aRect.y, aOtherRect.y);
197 nscoord right = std::min(aRect.XMost(), aOtherRect.XMost());
198 nscoord bottom = std::min(aRect.YMost(), aOtherRect.YMost());
199 if (left > right || top > bottom) {
200 return Nothing();
202 return Some(nsRect(left, top, right - left, bottom - top));
205 enum class BrowsingContextOrigin { Similar, Different, Unknown };
207 // FIXME(emilio): The whole concept of "units of related similar-origin browsing
208 // contexts" is gone, but this is still in the spec, see
209 // https://github.com/w3c/IntersectionObserver/issues/161
210 static BrowsingContextOrigin SimilarOrigin(const Element& aTarget,
211 const Element* aRoot) {
212 if (!aRoot) {
213 return BrowsingContextOrigin::Unknown;
215 nsIPrincipal* principal1 = aTarget.NodePrincipal();
216 nsIPrincipal* principal2 = aRoot->NodePrincipal();
218 if (principal1 == principal2) {
219 return BrowsingContextOrigin::Similar;
222 nsAutoCString baseDomain1;
223 nsAutoCString baseDomain2;
224 if (NS_FAILED(principal1->GetBaseDomain(baseDomain1)) ||
225 NS_FAILED(principal2->GetBaseDomain(baseDomain2))) {
226 return BrowsingContextOrigin::Different;
229 return baseDomain1 == baseDomain2 ? BrowsingContextOrigin::Similar
230 : BrowsingContextOrigin::Different;
233 void DOMIntersectionObserver::Update(Document* aDocument,
234 DOMHighResTimeStamp time) {
235 nsRect rootRect;
236 nsIFrame* rootFrame = nullptr;
237 Element* root = mRoot;
238 if (mRoot) {
239 if ((rootFrame = mRoot->GetPrimaryFrame())) {
240 nsRect rootRectRelativeToRootFrame;
241 if (rootFrame->IsScrollFrame()) {
242 // rootRectRelativeToRootFrame should be the content rect of rootFrame,
243 // not including the scrollbars.
244 nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
245 rootRectRelativeToRootFrame = scrollFrame->GetScrollPortRect();
246 } else {
247 // rootRectRelativeToRootFrame should be the border rect of rootFrame.
248 rootRectRelativeToRootFrame = rootFrame->GetRectRelativeToSelf();
250 nsIFrame* containingBlock =
251 nsLayoutUtils::GetContainingBlockForClientRect(rootFrame);
252 rootRect = nsLayoutUtils::TransformFrameRectToAncestor(
253 rootFrame, rootRectRelativeToRootFrame, containingBlock);
255 } else if (PresShell* presShell = aDocument->GetPresShell()) {
256 // FIXME(emilio): This shouldn't probably go through the presShell and just
257 // through the document tree.
258 rootFrame = presShell->GetRootScrollFrame();
259 if (rootFrame) {
260 nsPresContext* presContext = rootFrame->PresContext();
261 while (!presContext->IsRootContentDocument()) {
262 presContext = presContext->GetParentPresContext();
263 if (!presContext) {
264 break;
266 nsIFrame* rootScrollFrame =
267 presContext->PresShell()->GetRootScrollFrame();
268 if (rootScrollFrame) {
269 rootFrame = rootScrollFrame;
270 } else {
271 break;
274 root = rootFrame->GetContent()->AsElement();
275 nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
276 rootRect = scrollFrame->GetScrollPortRect();
280 nsMargin rootMargin;
281 NS_FOR_CSS_SIDES(side) {
282 nscoord basis = side == eSideTop || side == eSideBottom ? rootRect.Height()
283 : rootRect.Width();
284 rootMargin.Side(side) =
285 nsLayoutUtils::ComputeCBDependentValue(basis, mRootMargin.Get(side));
288 for (Element* target : mObservationTargets) {
289 nsIFrame* targetFrame = target->GetPrimaryFrame();
290 nsIFrame* originalTargetFrame = targetFrame;
291 nsRect targetRect;
292 Maybe<nsRect> intersectionRect;
293 bool isSameDoc = root && root->GetComposedDoc() == target->GetComposedDoc();
295 if (rootFrame && targetFrame) {
296 // If mRoot is set we are testing intersection with a container element
297 // instead of the implicit root.
298 if (mRoot) {
299 // Skip further processing of this target if it is not in the same
300 // Document as the intersection root, e.g. if root is an element of
301 // the main document and target an element from an embedded iframe.
302 if (!isSameDoc) {
303 continue;
305 // Skip further processing of this target if is not a descendant of the
306 // intersection root in the containing block chain. E.g. this would be
307 // the case if the target is in a position:absolute element whose
308 // containing block is an ancestor of root.
309 if (!nsLayoutUtils::IsAncestorFrameCrossDoc(rootFrame, targetFrame)) {
310 continue;
314 targetRect = nsLayoutUtils::GetAllInFlowRectsUnion(
315 targetFrame,
316 nsLayoutUtils::GetContainingBlockForClientRect(targetFrame),
317 nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
318 intersectionRect = Some(targetFrame->GetRectRelativeToSelf());
320 nsIFrame* containerFrame =
321 nsLayoutUtils::GetCrossDocParentFrame(targetFrame);
322 while (containerFrame && containerFrame != rootFrame) {
323 if (containerFrame->IsScrollFrame()) {
324 nsIScrollableFrame* scrollFrame = do_QueryFrame(containerFrame);
325 nsRect subFrameRect = scrollFrame->GetScrollPortRect();
326 nsRect intersectionRectRelativeToContainer =
327 nsLayoutUtils::TransformFrameRectToAncestor(
328 targetFrame, intersectionRect.value(), containerFrame);
329 intersectionRect = EdgeInclusiveIntersection(
330 intersectionRectRelativeToContainer, subFrameRect);
331 if (!intersectionRect) {
332 break;
334 targetFrame = containerFrame;
337 // TODO: Apply clip-path.
339 containerFrame = nsLayoutUtils::GetCrossDocParentFrame(containerFrame);
343 nsRect rootIntersectionRect;
344 if (rootFrame && targetFrame) {
345 // FIXME(emilio): Why only if there are frames?
346 rootIntersectionRect = rootRect;
349 BrowsingContextOrigin origin = SimilarOrigin(*target, root);
350 if (origin == BrowsingContextOrigin::Similar) {
351 rootIntersectionRect.Inflate(rootMargin);
354 if (intersectionRect.isSome()) {
355 nsRect intersectionRectRelativeToRoot =
356 nsLayoutUtils::TransformFrameRectToAncestor(
357 targetFrame, intersectionRect.value(),
358 nsLayoutUtils::GetContainingBlockForClientRect(rootFrame));
359 intersectionRect = EdgeInclusiveIntersection(
360 intersectionRectRelativeToRoot, rootIntersectionRect);
361 if (intersectionRect.isSome() && !isSameDoc) {
362 nsRect rect = intersectionRect.value();
363 nsPresContext* presContext = originalTargetFrame->PresContext();
364 nsIFrame* rootScrollFrame =
365 presContext->PresShell()->GetRootScrollFrame();
366 if (rootScrollFrame) {
367 nsLayoutUtils::TransformRect(rootFrame, rootScrollFrame, rect);
369 intersectionRect = Some(rect);
373 int64_t targetArea =
374 (int64_t)targetRect.Width() * (int64_t)targetRect.Height();
375 int64_t intersectionArea = !intersectionRect
377 : (int64_t)intersectionRect->Width() *
378 (int64_t)intersectionRect->Height();
380 double intersectionRatio;
381 if (targetArea > 0.0) {
382 intersectionRatio =
383 std::min((double)intersectionArea / (double)targetArea, 1.0);
384 } else {
385 intersectionRatio = intersectionRect.isSome() ? 1.0 : 0.0;
388 int32_t threshold = -1;
389 if (intersectionRect.isSome()) {
390 // Spec: "Let thresholdIndex be the index of the first entry in
391 // observer.thresholds whose value is greater than intersectionRatio."
392 threshold = mThresholds.IndexOfFirstElementGt(intersectionRatio);
393 if (threshold == 0) {
394 // Per the spec, we should leave threshold at 0 and distinguish between
395 // "less than all thresholds and intersecting" and "not intersecting"
396 // (queuing observer entries as both cases come to pass). However,
397 // neither Chrome nor the WPT tests expect this behavior, so treat these
398 // two cases as one.
399 threshold = -1;
403 if (target->UpdateIntersectionObservation(this, threshold)) {
404 QueueIntersectionObserverEntry(target, time,
405 origin == BrowsingContextOrigin::Different
406 ? Nothing()
407 : Some(rootIntersectionRect),
408 targetRect, intersectionRect,
409 intersectionRatio);
414 void DOMIntersectionObserver::QueueIntersectionObserverEntry(
415 Element* aTarget, DOMHighResTimeStamp time, const Maybe<nsRect>& aRootRect,
416 const nsRect& aTargetRect, const Maybe<nsRect>& aIntersectionRect,
417 double aIntersectionRatio) {
418 RefPtr<DOMRect> rootBounds;
419 if (aRootRect.isSome()) {
420 rootBounds = new DOMRect(this);
421 rootBounds->SetLayoutRect(aRootRect.value());
423 RefPtr<DOMRect> boundingClientRect = new DOMRect(this);
424 boundingClientRect->SetLayoutRect(aTargetRect);
425 RefPtr<DOMRect> intersectionRect = new DOMRect(this);
426 if (aIntersectionRect.isSome()) {
427 intersectionRect->SetLayoutRect(aIntersectionRect.value());
429 RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry(
430 this, time, rootBounds.forget(), boundingClientRect.forget(),
431 intersectionRect.forget(), aIntersectionRect.isSome(), aTarget,
432 aIntersectionRatio);
433 mQueuedEntries.AppendElement(entry.forget());
436 void DOMIntersectionObserver::Notify() {
437 if (!mQueuedEntries.Length()) {
438 return;
440 mozilla::dom::Sequence<mozilla::OwningNonNull<DOMIntersectionObserverEntry>>
441 entries;
442 if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) {
443 for (size_t i = 0; i < mQueuedEntries.Length(); ++i) {
444 RefPtr<DOMIntersectionObserverEntry> next = mQueuedEntries[i];
445 *entries.AppendElement(mozilla::fallible) = next;
448 mQueuedEntries.Clear();
449 RefPtr<dom::IntersectionCallback> callback(mCallback);
450 callback->Call(this, entries, *this);
453 } // namespace dom
454 } // namespace mozilla