summaryrefslogtreecommitdiffstats
path: root/dom/base/ResizeObserver.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/base/ResizeObserver.cpp323
1 files changed, 323 insertions, 0 deletions
diff --git a/dom/base/ResizeObserver.cpp b/dom/base/ResizeObserver.cpp
new file mode 100644
index 0000000000..77fe99a371
--- /dev/null
+++ b/dom/base/ResizeObserver.cpp
@@ -0,0 +1,323 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/ResizeObserver.h"
+
+#include "mozilla/dom/DOMRect.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/SVGUtils.h"
+#include "nsIContent.h"
+#include "nsIContentInlines.h"
+#include <limits>
+
+namespace mozilla::dom {
+
+/**
+ * Returns the length of the parent-traversal path (in terms of the number of
+ * nodes) to an unparented/root node from aNode. An unparented/root node is
+ * considered to have a depth of 1, its children have a depth of 2, etc.
+ * aNode is expected to be non-null.
+ * Note: The shadow root is not part of the calculation because the caller,
+ * ResizeObserver, doesn't observe the shadow root, and only needs relative
+ * depths among all the observed targets. In other words, we calculate the
+ * depth of the flattened tree.
+ *
+ * However, these is a spec issue about how to handle shadow DOM case. We
+ * may need to update this function later:
+ * https://github.com/w3c/csswg-drafts/issues/3840
+ *
+ * https://drafts.csswg.org/resize-observer/#calculate-depth-for-node-h
+ */
+static uint32_t GetNodeDepth(nsINode* aNode) {
+ uint32_t depth = 1;
+
+ MOZ_ASSERT(aNode, "Node shouldn't be null");
+
+ // Use GetFlattenedTreeParentNode to bypass the shadow root and cross the
+ // shadow boundary to calculate the node depth without the shadow root.
+ while ((aNode = aNode->GetFlattenedTreeParentNode())) {
+ ++depth;
+ }
+
+ return depth;
+}
+
+/**
+ * Returns |aTarget|'s size in the form of nsSize.
+ * If the target is SVG, width and height are determined from bounding box.
+ */
+static nsSize GetTargetSize(Element* aTarget, ResizeObserverBoxOptions aBox) {
+ nsSize size;
+ nsIFrame* frame = aTarget->GetPrimaryFrame();
+
+ if (!frame) {
+ return size;
+ }
+
+ if (aTarget->IsSVGElement()) {
+ // Per the spec, SVG size is always its bounding box size no matter what
+ // box option you choose, because SVG elements do not use standard CSS box
+ // model.
+ gfxRect bbox = SVGUtils::GetBBox(frame);
+ size.width = NSFloatPixelsToAppUnits(bbox.width, AppUnitsPerCSSPixel());
+ size.height = NSFloatPixelsToAppUnits(bbox.height, AppUnitsPerCSSPixel());
+ } else {
+ // Per the spec, non-replaced inline Elements will always have an empty
+ // content rect. Therefore, we always use the same trivially-empty size
+ // for non-replaced inline elements here, and their IsActive() will
+ // always return false. (So its observation won't be fired.)
+ if (!frame->IsFrameOfType(nsIFrame::eReplaced) &&
+ frame->IsFrameOfType(nsIFrame::eLineParticipant)) {
+ return size;
+ }
+
+ switch (aBox) {
+ case ResizeObserverBoxOptions::Border_box:
+ // GetSize() includes the content area, borders, and padding.
+ size = frame->GetSize();
+ break;
+ case ResizeObserverBoxOptions::Content_box:
+ default:
+ size = frame->GetContentRectRelativeToSelf().Size();
+ }
+ }
+
+ return size;
+}
+
+NS_IMPL_CYCLE_COLLECTION(ResizeObservation, mTarget)
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(ResizeObservation, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(ResizeObservation, Release)
+
+bool ResizeObservation::IsActive() const {
+ nsIFrame* frame = mTarget->GetPrimaryFrame();
+ const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
+ const LogicalSize size(wm, GetTargetSize(mTarget, mObservedBox));
+ return mLastReportedSize.ISize(mLastReportedWM) != size.ISize(wm) ||
+ mLastReportedSize.BSize(mLastReportedWM) != size.BSize(wm);
+}
+
+void ResizeObservation::UpdateLastReportedSize(const nsSize& aSize) {
+ nsIFrame* frame = mTarget->GetPrimaryFrame();
+ mLastReportedWM = frame ? frame->GetWritingMode() : WritingMode();
+ mLastReportedSize = LogicalSize(mLastReportedWM, aSize);
+}
+
+// Only needed for refcounted objects.
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserver, mOwner, mDocument,
+ mCallback, mActiveTargets,
+ mObservationMap)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserver)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserver)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserver)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+already_AddRefed<ResizeObserver> ResizeObserver::Constructor(
+ const GlobalObject& aGlobal, ResizeObserverCallback& aCb,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> window =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!window) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ Document* doc = window->GetExtantDoc();
+ if (!doc) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ return do_AddRef(new ResizeObserver(std::move(window), doc, aCb));
+}
+
+void ResizeObserver::Observe(Element& aTarget,
+ const ResizeObserverOptions& aOptions,
+ ErrorResult& aRv) {
+ // NOTE(emilio): Per spec, this is supposed to happen on construction, but the
+ // spec isn't particularly sane here, see
+ // https://github.com/w3c/csswg-drafts/issues/4518
+ if (mObservationList.isEmpty()) {
+ MOZ_ASSERT(mObservationMap.IsEmpty());
+ if (MOZ_UNLIKELY(!mDocument)) {
+ return aRv.Throw(NS_ERROR_FAILURE);
+ }
+ mDocument->AddResizeObserver(*this);
+ }
+
+ RefPtr<ResizeObservation>& observation =
+ mObservationMap.LookupForAdd(&aTarget).OrInsert([] { return nullptr; });
+ if (observation) {
+ if (observation->BoxOptions() == aOptions.mBox) {
+ // Already observed this target and the observed box is the same, so
+ // return.
+ // Note: Based on the spec, we should unobserve it first. However,
+ // calling Unobserve() when we observe the same box will remove original
+ // ResizeObservation and then add a new one, this may cause an unexpected
+ // result because ResizeObservation stores the mLastReportedSize which
+ // should be kept to make sure IsActive() returns the correct result.
+ return;
+ }
+ // Remove the pre-existing entry, but without unregistering ourselves from
+ // the controller.
+ observation->remove();
+ observation = nullptr;
+ }
+
+ // FIXME(emilio): This should probably either flush or not look at the
+ // writing-mode or something.
+ nsIFrame* frame = aTarget.GetPrimaryFrame();
+ observation = new ResizeObservation(
+ aTarget, aOptions.mBox, frame ? frame->GetWritingMode() : WritingMode());
+ mObservationList.insertBack(observation);
+
+ // Per the spec, we need to trigger notification in event loop that
+ // contains ResizeObserver observe call even when resize/reflow does
+ // not happen.
+ aTarget.OwnerDoc()->ScheduleResizeObserversNotification();
+}
+
+void ResizeObserver::Unobserve(Element& aTarget, ErrorResult& aRv) {
+ RefPtr<ResizeObservation> observation;
+ if (!mObservationMap.Remove(&aTarget, getter_AddRefs(observation))) {
+ return;
+ }
+
+ MOZ_ASSERT(!mObservationList.isEmpty(),
+ "If ResizeObservation found for an element, observation list "
+ "must be not empty.");
+ observation->remove();
+ if (mObservationList.isEmpty()) {
+ if (MOZ_LIKELY(mDocument)) {
+ mDocument->RemoveResizeObserver(*this);
+ }
+ }
+}
+
+void ResizeObserver::Disconnect() {
+ const bool registered = !mObservationList.isEmpty();
+ mObservationList.clear();
+ mObservationMap.Clear();
+ mActiveTargets.Clear();
+ if (registered && MOZ_LIKELY(mDocument)) {
+ mDocument->RemoveResizeObserver(*this);
+ }
+}
+
+void ResizeObserver::GatherActiveObservations(uint32_t aDepth) {
+ mActiveTargets.Clear();
+ mHasSkippedTargets = false;
+
+ for (auto* observation : mObservationList) {
+ if (!observation->IsActive()) {
+ continue;
+ }
+
+ uint32_t targetDepth = GetNodeDepth(observation->Target());
+
+ if (targetDepth > aDepth) {
+ mActiveTargets.AppendElement(observation);
+ } else {
+ mHasSkippedTargets = true;
+ }
+ }
+}
+
+uint32_t ResizeObserver::BroadcastActiveObservations() {
+ uint32_t shallowestTargetDepth = std::numeric_limits<uint32_t>::max();
+
+ if (!HasActiveObservations()) {
+ return shallowestTargetDepth;
+ }
+
+ Sequence<OwningNonNull<ResizeObserverEntry>> entries;
+
+ for (auto& observation : mActiveTargets) {
+ Element* target = observation->Target();
+
+ nsSize borderBoxSize =
+ GetTargetSize(target, ResizeObserverBoxOptions::Border_box);
+ nsSize contentBoxSize =
+ GetTargetSize(target, ResizeObserverBoxOptions::Content_box);
+ RefPtr<ResizeObserverEntry> entry =
+ new ResizeObserverEntry(this, *target, borderBoxSize, contentBoxSize);
+
+ if (!entries.AppendElement(entry.forget(), fallible)) {
+ // Out of memory.
+ break;
+ }
+
+ // Sync the broadcast size of observation so the next size inspection
+ // will be based on the updated size from last delivered observations.
+ switch (observation->BoxOptions()) {
+ case ResizeObserverBoxOptions::Border_box:
+ observation->UpdateLastReportedSize(borderBoxSize);
+ break;
+ case ResizeObserverBoxOptions::Content_box:
+ default:
+ observation->UpdateLastReportedSize(contentBoxSize);
+ }
+
+ uint32_t targetDepth = GetNodeDepth(observation->Target());
+
+ if (targetDepth < shallowestTargetDepth) {
+ shallowestTargetDepth = targetDepth;
+ }
+ }
+
+ RefPtr<ResizeObserverCallback> callback(mCallback);
+ callback->Call(this, entries, *this);
+
+ mActiveTargets.Clear();
+ mHasSkippedTargets = false;
+
+ return shallowestTargetDepth;
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserverEntry, mOwner, mTarget,
+ mContentRect, mBorderBoxSize,
+ mContentBoxSize)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserverEntry)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserverEntry)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserverEntry)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+void ResizeObserverEntry::SetBorderBoxSize(const nsSize& aSize) {
+ nsIFrame* frame = mTarget->GetPrimaryFrame();
+ const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
+ mBorderBoxSize = new ResizeObserverSize(this, aSize, wm);
+}
+
+void ResizeObserverEntry::SetContentRectAndSize(const nsSize& aSize) {
+ nsIFrame* frame = mTarget->GetPrimaryFrame();
+
+ // 1. Update mContentRect.
+ nsMargin padding = frame ? frame->GetUsedPadding() : nsMargin();
+ // Per the spec, we need to use the top-left padding offset as the origin of
+ // our contentRect.
+ nsRect rect(nsPoint(padding.left, padding.top), aSize);
+ RefPtr<DOMRect> contentRect = new DOMRect(this);
+ contentRect->SetLayoutRect(rect);
+ mContentRect = std::move(contentRect);
+
+ // 2. Update mContentBoxSize.
+ const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
+ mContentBoxSize = new ResizeObserverSize(this, aSize, wm);
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserverSize, mOwner)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserverSize)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserverSize)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserverSize)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+} // namespace mozilla::dom