diff options
Diffstat (limited to 'dom/base/ResizeObserverController.cpp')
-rw-r--r-- | dom/base/ResizeObserverController.cpp | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/dom/base/ResizeObserverController.cpp b/dom/base/ResizeObserverController.cpp new file mode 100644 index 0000000000..3a315cb026 --- /dev/null +++ b/dom/base/ResizeObserverController.cpp @@ -0,0 +1,238 @@ +/* -*- 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/BrowsingContext.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::ShellDetachedFromDocument() { + mResizeObserverNotificationHelper->Unregister(); +} + +static void FlushLayoutForWholeBrowsingContextTree(Document& aDoc) { + if (BrowsingContext* bc = aDoc.GetBrowsingContext()) { + RefPtr<BrowsingContext> top = bc->Top(); + top->PreOrderWalk([](BrowsingContext* aCur) { + if (Document* doc = aCur->GetExtantDocument()) { + doc->FlushPendingNotifications(FlushType::Layout); + } + }); + } else { + // If there is no browsing context, we just flush this document itself. + aDoc.FlushPendingNotifications(FlushType::Layout); + } +} + +void ResizeObserverController::Notify() { + mResizeObserverNotificationHelper->Unregister(); + 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; + while (true) { + // Flush layout, so that any callback functions' style changes / resizes + // get a chance to take effect. The callback functions may do changes in its + // sub-documents or ancestors, so flushing layout for the whole browsing + // context tree makes sure we don't miss anyone. + FlushLayoutForWholeBrowsingContextTree(*doc); + + // 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); + + if (!HasAnyActiveObservations()) { + break; + } + + DebugOnly<uint32_t> oldShallowestTargetDepth = shallowestTargetDepth; + shallowestTargetDepth = BroadcastAllActiveObservations(); + NS_ASSERTION(oldShallowestTargetDepth < shallowestTargetDepth, + "shallowestTargetDepth should be getting strictly deeper"); + } + + 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. + const auto observers = + ToTArray<nsTArray<RefPtr<ResizeObserver>>>(mResizeObservers); + for (auto& observer : observers) { + // 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.mDOMSizes.mDOMResizeObserverControllerSize += size; +} + +} // namespace mozilla::dom |