summaryrefslogtreecommitdiffstats
path: root/dom/base/ResizeObserverController.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/base/ResizeObserverController.cpp')
-rw-r--r--dom/base/ResizeObserverController.cpp226
1 files changed, 226 insertions, 0 deletions
diff --git a/dom/base/ResizeObserverController.cpp b/dom/base/ResizeObserverController.cpp
new file mode 100644
index 0000000000..54877081dc
--- /dev/null
+++ b/dom/base/ResizeObserverController.cpp
@@ -0,0 +1,226 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/ResizeObserverController.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/ErrorEvent.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/Unused.h"
+#include "nsPresContext.h"
+#include "nsRefreshDriver.h"
+#include <limits>
+
+namespace mozilla::dom {
+
+void ResizeObserverNotificationHelper::WillRefresh(TimeStamp aTime) {
+ MOZ_DIAGNOSTIC_ASSERT(mOwner, "Should've de-registered on-time!");
+ mOwner->Notify();
+ // Note that mOwner may be null / dead here.
+}
+
+nsRefreshDriver* ResizeObserverNotificationHelper::GetRefreshDriver() const {
+ PresShell* presShell = mOwner->GetPresShell();
+ if (MOZ_UNLIKELY(!presShell)) {
+ return nullptr;
+ }
+
+ nsPresContext* presContext = presShell->GetPresContext();
+ if (MOZ_UNLIKELY(!presContext)) {
+ return nullptr;
+ }
+
+ return presContext->RefreshDriver();
+}
+
+void ResizeObserverNotificationHelper::Register() {
+ if (mRegistered) {
+ return;
+ }
+
+ nsRefreshDriver* refreshDriver = GetRefreshDriver();
+ if (!refreshDriver) {
+ // We maybe navigating away from this page or currently in an iframe with
+ // display: none. Just abort the Register(), no need to do notification.
+ return;
+ }
+
+ refreshDriver->AddRefreshObserver(this, FlushType::Display, "ResizeObserver");
+ mRegistered = true;
+}
+
+void ResizeObserverNotificationHelper::Unregister() {
+ if (!mRegistered) {
+ return;
+ }
+
+ nsRefreshDriver* refreshDriver = GetRefreshDriver();
+ MOZ_RELEASE_ASSERT(
+ refreshDriver,
+ "We should not leave a dangling reference to the observer around");
+
+ bool rv = refreshDriver->RemoveRefreshObserver(this, FlushType::Display);
+ MOZ_DIAGNOSTIC_ASSERT(rv, "Should remove the observer successfully");
+ Unused << rv;
+
+ mRegistered = false;
+}
+
+ResizeObserverNotificationHelper::~ResizeObserverNotificationHelper() {
+ MOZ_RELEASE_ASSERT(!mRegistered, "How can we die when registered?");
+ MOZ_RELEASE_ASSERT(!mOwner, "Forgot to clear weak pointer?");
+}
+
+void ResizeObserverController::Traverse(
+ nsCycleCollectionTraversalCallback& aCb) {
+ ImplCycleCollectionTraverse(aCb, mResizeObservers, "mResizeObservers");
+}
+
+void ResizeObserverController::Unlink() { mResizeObservers.Clear(); }
+
+void ResizeObserverController::ShellDetachedFromDocument() {
+ mResizeObserverNotificationHelper->Unregister();
+}
+
+void ResizeObserverController::Notify() {
+ if (mResizeObservers.IsEmpty()) {
+ return;
+ }
+
+ // We may call BroadcastAllActiveObservations(), which might cause mDocument
+ // to be destroyed (and the ResizeObserverController with it).
+ // e.g. if mDocument is in an iframe, and the observer JS removes it from the
+ // parent document and we trigger an unlucky GC/CC (or maybe if the observer
+ // JS navigates to a different URL). Or the author uses elem.offsetTop API,
+ // which could flush style + layout and make the document destroyed if we're
+ // inside an iframe that has suddenly become |display:none| via the author
+ // doing something in their ResizeObserver callback.
+ //
+ // Therefore, we ref-count mDocument here to make sure it and its members
+ // (e.g. mResizeObserverController, which is `this` pointer) are still alive
+ // in the rest of this function because after it goes up, `this` is possible
+ // deleted.
+ RefPtr<Document> doc(mDocument);
+
+ uint32_t shallowestTargetDepth = 0;
+
+ GatherAllActiveObservations(shallowestTargetDepth);
+
+ while (HasAnyActiveObservations()) {
+ DebugOnly<uint32_t> oldShallowestTargetDepth = shallowestTargetDepth;
+ shallowestTargetDepth = BroadcastAllActiveObservations();
+ NS_ASSERTION(oldShallowestTargetDepth < shallowestTargetDepth,
+ "shallowestTargetDepth should be getting strictly deeper");
+
+ // Flush layout, so that any callback functions' style changes / resizes
+ // get a chance to take effect.
+ doc->FlushPendingNotifications(FlushType::Layout);
+
+ // To avoid infinite resize loop, we only gather all active observations
+ // that have the depth of observed target element more than current
+ // shallowestTargetDepth.
+ GatherAllActiveObservations(shallowestTargetDepth);
+ }
+
+ mResizeObserverNotificationHelper->Unregister();
+
+ if (HasAnySkippedObservations()) {
+ // Per spec, we deliver an error if the document has any skipped
+ // observations. Also, we re-register via ScheduleNotification().
+ RootedDictionary<ErrorEventInit> init(RootingCx());
+
+ init.mMessage.AssignLiteral(
+ "ResizeObserver loop completed with undelivered notifications.");
+ init.mBubbles = false;
+ init.mCancelable = false;
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+
+ if (nsCOMPtr<nsPIDOMWindowInner> window = doc->GetInnerWindow()) {
+ nsCOMPtr<nsIScriptGlobalObject> sgo = do_QueryInterface(window);
+ MOZ_ASSERT(sgo);
+
+ if (NS_WARN_IF(sgo->HandleScriptError(init, &status))) {
+ status = nsEventStatus_eIgnore;
+ }
+ } else {
+ // We don't fire error events at any global for non-window JS on the main
+ // thread.
+ }
+
+ // We need to deliver pending notifications in next cycle.
+ ScheduleNotification();
+ }
+}
+
+void ResizeObserverController::GatherAllActiveObservations(uint32_t aDepth) {
+ for (ResizeObserver* observer : mResizeObservers) {
+ observer->GatherActiveObservations(aDepth);
+ }
+}
+
+uint32_t ResizeObserverController::BroadcastAllActiveObservations() {
+ uint32_t shallowestTargetDepth = std::numeric_limits<uint32_t>::max();
+
+ // Copy the observers as this invokes the callbacks and could register and
+ // unregister observers at will.
+ for (auto& observer : mResizeObservers.Clone()) {
+ // MOZ_KnownLive because 'observers' is guaranteed to keep it
+ // alive.
+ //
+ // This can go away once
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1620312 is fixed.
+ uint32_t targetDepth =
+ MOZ_KnownLive(observer)->BroadcastActiveObservations();
+ if (targetDepth < shallowestTargetDepth) {
+ shallowestTargetDepth = targetDepth;
+ }
+ }
+
+ return shallowestTargetDepth;
+}
+
+bool ResizeObserverController::HasAnyActiveObservations() const {
+ for (auto& observer : mResizeObservers) {
+ if (observer->HasActiveObservations()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool ResizeObserverController::HasAnySkippedObservations() const {
+ for (auto& observer : mResizeObservers) {
+ if (observer->HasSkippedObservations()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void ResizeObserverController::ScheduleNotification() {
+ mResizeObserverNotificationHelper->Register();
+}
+
+ResizeObserverController::~ResizeObserverController() {
+ MOZ_RELEASE_ASSERT(
+ !mResizeObserverNotificationHelper->IsRegistered(),
+ "Nothing else should keep a reference to our helper when we go away");
+ mResizeObserverNotificationHelper->DetachFromOwner();
+}
+
+void ResizeObserverController::AddSizeOfIncludingThis(
+ nsWindowSizes& aSizes) const {
+ MallocSizeOf mallocSizeOf = aSizes.mState.mMallocSizeOf;
+ size_t size = mallocSizeOf(this);
+ size += mResizeObservers.ShallowSizeOfExcludingThis(mallocSizeOf);
+ // TODO(emilio): Measure the observers individually or something? They aren't
+ // really owned by us.
+ aSizes.mDOMResizeObserverControllerSize += size;
+}
+
+} // namespace mozilla::dom