summaryrefslogtreecommitdiffstats
path: root/gfx/layers/apz/util
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/layers/apz/util')
-rw-r--r--gfx/layers/apz/util/APZCCallbackHelper.cpp939
-rw-r--r--gfx/layers/apz/util/APZCCallbackHelper.h197
-rw-r--r--gfx/layers/apz/util/APZEventState.cpp567
-rw-r--r--gfx/layers/apz/util/APZEventState.h141
-rw-r--r--gfx/layers/apz/util/APZTaskRunnable.cpp142
-rw-r--r--gfx/layers/apz/util/APZTaskRunnable.h89
-rw-r--r--gfx/layers/apz/util/APZThreadUtils.cpp119
-rw-r--r--gfx/layers/apz/util/APZThreadUtils.h75
-rw-r--r--gfx/layers/apz/util/ActiveElementManager.cpp324
-rw-r--r--gfx/layers/apz/util/ActiveElementManager.h111
-rw-r--r--gfx/layers/apz/util/CheckerboardReportService.cpp217
-rw-r--r--gfx/layers/apz/util/CheckerboardReportService.h138
-rw-r--r--gfx/layers/apz/util/ChromeProcessController.cpp361
-rw-r--r--gfx/layers/apz/util/ChromeProcessController.h106
-rw-r--r--gfx/layers/apz/util/ContentProcessController.cpp123
-rw-r--r--gfx/layers/apz/util/ContentProcessController.h95
-rw-r--r--gfx/layers/apz/util/DoubleTapToZoom.cpp441
-rw-r--r--gfx/layers/apz/util/DoubleTapToZoom.h85
-rw-r--r--gfx/layers/apz/util/InputAPZContext.cpp75
-rw-r--r--gfx/layers/apz/util/InputAPZContext.h76
-rw-r--r--gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp48
-rw-r--r--gfx/layers/apz/util/ScrollLinkedEffectDetector.h48
-rw-r--r--gfx/layers/apz/util/ScrollingInteractionContext.cpp29
-rw-r--r--gfx/layers/apz/util/ScrollingInteractionContext.h40
-rw-r--r--gfx/layers/apz/util/TouchActionHelper.cpp131
-rw-r--r--gfx/layers/apz/util/TouchActionHelper.h46
-rw-r--r--gfx/layers/apz/util/TouchCounter.cpp74
-rw-r--r--gfx/layers/apz/util/TouchCounter.h36
28 files changed, 4873 insertions, 0 deletions
diff --git a/gfx/layers/apz/util/APZCCallbackHelper.cpp b/gfx/layers/apz/util/APZCCallbackHelper.cpp
new file mode 100644
index 0000000000..9ad65ce981
--- /dev/null
+++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp
@@ -0,0 +1,939 @@
+/* -*- 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 "APZCCallbackHelper.h"
+
+#include "gfxPlatform.h" // For gfxPlatform::UseTiling
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/dom/CustomEvent.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/layers/RepaintRequest.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/layers/WebRenderBridgeChild.h"
+#include "mozilla/DisplayPortUtils.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ToString.h"
+#include "mozilla/ViewportUtils.h"
+#include "nsContainerFrame.h"
+#include "nsContentUtils.h"
+#include "nsIContent.h"
+#include "nsIDOMWindowUtils.h"
+#include "mozilla/dom/Document.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIScrollableFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsPrintfCString.h"
+#include "nsPIDOMWindow.h"
+#include "nsRefreshDriver.h"
+#include "nsString.h"
+#include "nsView.h"
+
+static mozilla::LazyLogModule sApzHlpLog("apz.helper");
+#define APZCCH_LOG(...) MOZ_LOG(sApzHlpLog, LogLevel::Debug, (__VA_ARGS__))
+static mozilla::LazyLogModule sDisplayportLog("apz.displayport");
+
+namespace mozilla {
+namespace layers {
+
+using dom::BrowserParent;
+
+uint64_t APZCCallbackHelper::sLastTargetAPZCNotificationInputBlock =
+ uint64_t(-1);
+
+static ScreenMargin RecenterDisplayPort(const ScreenMargin& aDisplayPort) {
+ ScreenMargin margins = aDisplayPort;
+ margins.right = margins.left = margins.LeftRight() / 2;
+ margins.top = margins.bottom = margins.TopBottom() / 2;
+ return margins;
+}
+
+static PresShell* GetPresShell(const nsIContent* aContent) {
+ if (dom::Document* doc = aContent->GetComposedDoc()) {
+ return doc->GetPresShell();
+ }
+ return nullptr;
+}
+
+static CSSPoint ScrollFrameTo(nsIScrollableFrame* aFrame,
+ const RepaintRequest& aRequest,
+ bool& aSuccessOut) {
+ aSuccessOut = false;
+ CSSPoint targetScrollPosition = aRequest.GetLayoutScrollOffset();
+
+ if (!aFrame) {
+ return targetScrollPosition;
+ }
+
+ CSSPoint geckoScrollPosition =
+ CSSPoint::FromAppUnits(aFrame->GetScrollPosition());
+
+ // If the repaint request was triggered due to a previous main-thread scroll
+ // offset update sent to the APZ, then we don't need to do another scroll here
+ // and we can just return.
+ if (!aRequest.GetScrollOffsetUpdated()) {
+ return geckoScrollPosition;
+ }
+
+ // If this frame is overflow:hidden, then the expectation is that it was
+ // sized in a way that respects its scrollable boundaries. For the root
+ // frame, this means that it cannot be scrolled in such a way that it moves
+ // the layout viewport. For a non-root frame, this means that it cannot be
+ // scrolled at all.
+ //
+ // In either case, |targetScrollPosition| should be the same as
+ // |geckoScrollPosition| here.
+ //
+ // However, this is slightly racy. We query the overflow property of the
+ // scroll frame at the time the repaint request arrives at the main thread
+ // (i.e., right now), but APZ made the decision of whether or not to allow
+ // scrolling based on the information it had at the time it processed the
+ // scroll event. The overflow property could have changed at some time
+ // between the two events and so APZ may have computed a scrollable region
+ // that is larger than what is actually allowed.
+ //
+ // Currently, we allow the scroll position to change even though the frame is
+ // overflow:hidden (that is, we take |targetScrollPosition|). If this turns
+ // out to be problematic, an alternative solution would be to ignore the
+ // scroll position change (that is, use |geckoScrollPosition|).
+ if (aFrame->GetScrollStyles().mVertical == StyleOverflow::Hidden &&
+ targetScrollPosition.y != geckoScrollPosition.y) {
+ NS_WARNING(
+ nsPrintfCString(
+ "APZCCH: targetScrollPosition.y (%f) != geckoScrollPosition.y (%f)",
+ targetScrollPosition.y.value, geckoScrollPosition.y.value)
+ .get());
+ }
+ if (aFrame->GetScrollStyles().mHorizontal == StyleOverflow::Hidden &&
+ targetScrollPosition.x != geckoScrollPosition.x) {
+ NS_WARNING(
+ nsPrintfCString(
+ "APZCCH: targetScrollPosition.x (%f) != geckoScrollPosition.x (%f)",
+ targetScrollPosition.x.value, geckoScrollPosition.x.value)
+ .get());
+ }
+
+ // If the scrollable frame is currently in the middle of an async or smooth
+ // scroll then we don't want to interrupt it (see bug 961280).
+ // Also if the scrollable frame got a scroll request from a higher priority
+ // origin since the last layers update, then we don't want to push our scroll
+ // request because we'll clobber that one, which is bad.
+ bool scrollInProgress = APZCCallbackHelper::IsScrollInProgress(aFrame);
+ if (!scrollInProgress) {
+ ScrollSnapTargetIds snapTargetIds = aRequest.GetLastSnapTargetIds();
+ aFrame->ScrollToCSSPixelsForApz(targetScrollPosition,
+ std::move(snapTargetIds));
+ geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition());
+ aSuccessOut = true;
+ }
+ // Return the final scroll position after setting it so that anything that
+ // relies on it can have an accurate value. Note that even if we set it above
+ // re-querying it is a good idea because it may have gotten clamped or
+ // rounded.
+ return geckoScrollPosition;
+}
+
+/**
+ * Scroll the scroll frame associated with |aContent| to the scroll position
+ * requested in |aRequest|.
+ *
+ * Any difference between the requested and actual scroll positions is used to
+ * update the callback-transform stored on the content, and return a new
+ * display port.
+ */
+static DisplayPortMargins ScrollFrame(nsIContent* aContent,
+ const RepaintRequest& aRequest) {
+ // Scroll the window to the desired spot
+ nsIScrollableFrame* sf =
+ nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId());
+ if (sf) {
+ sf->ResetScrollInfoIfNeeded(aRequest.GetScrollGeneration(),
+ aRequest.GetScrollGenerationOnApz(),
+ aRequest.GetScrollAnimationType(),
+ nsIScrollableFrame::InScrollingGesture(
+ aRequest.IsInScrollingGesture()));
+ sf->SetScrollableByAPZ(!aRequest.IsScrollInfoLayer());
+ if (sf->IsRootScrollFrameOfDocument()) {
+ if (!APZCCallbackHelper::IsScrollInProgress(sf)) {
+ APZCCH_LOG("Setting VV offset to %s\n",
+ ToString(aRequest.GetVisualScrollOffset()).c_str());
+ if (sf->SetVisualViewportOffset(
+ CSSPoint::ToAppUnits(aRequest.GetVisualScrollOffset()),
+ /* aRepaint = */ false)) {
+ // sf can't be destroyed if SetVisualViewportOffset returned true.
+ sf->MarkEverScrolled();
+ }
+ }
+ }
+ }
+ // sf might have been destroyed by the call to SetVisualViewportOffset, so
+ // re-get it.
+ sf = nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId());
+ bool scrollUpdated = false;
+ auto displayPortMargins =
+ DisplayPortMargins::ForScrollFrame(sf, aRequest.GetDisplayPortMargins());
+ CSSPoint apzScrollOffset = aRequest.GetVisualScrollOffset();
+ CSSPoint actualScrollOffset = ScrollFrameTo(sf, aRequest, scrollUpdated);
+ CSSPoint scrollDelta = apzScrollOffset - actualScrollOffset;
+
+ if (scrollUpdated) {
+ if (aRequest.IsScrollInfoLayer()) {
+ // In cases where the APZ scroll offset is different from the content
+ // scroll offset, we want to interpret the margins as relative to the APZ
+ // scroll offset except when the frame is not scrollable by APZ.
+ // Therefore, if the layer is a scroll info layer, we leave the margins
+ // as-is and they will be interpreted as relative to the content scroll
+ // offset.
+ if (nsIFrame* frame = aContent->GetPrimaryFrame()) {
+ frame->SchedulePaint();
+ }
+ } else {
+ // Correct the display port due to the difference between the requested
+ // and actual scroll offsets.
+ displayPortMargins =
+ DisplayPortMargins::FromAPZ(aRequest.GetDisplayPortMargins(),
+ apzScrollOffset, actualScrollOffset);
+ }
+ } else if (aRequest.IsRootContent() &&
+ apzScrollOffset != aRequest.GetLayoutScrollOffset()) {
+ // APZ uses the visual viewport's offset to calculate where to place the
+ // display port, so the display port is misplaced when a pinch zoom occurs.
+ //
+ // We need to force a display port adjustment in the following paint to
+ // account for a difference between the requested and actual scroll
+ // offsets in repaints requested by
+ // AsyncPanZoomController::NotifyLayersUpdated.
+ displayPortMargins = DisplayPortMargins::FromAPZ(
+ aRequest.GetDisplayPortMargins(), apzScrollOffset, actualScrollOffset);
+ } else {
+ // For whatever reason we couldn't update the scroll offset on the scroll
+ // frame, which means the data APZ used for its displayport calculation is
+ // stale. Fall back to a sane default behaviour. Note that we don't
+ // tile-align the recentered displayport because tile-alignment depends on
+ // the scroll position, and the scroll position here is out of our control.
+ // See bug 966507 comment 21 for a more detailed explanation.
+ displayPortMargins = DisplayPortMargins::ForScrollFrame(
+ sf, RecenterDisplayPort(aRequest.GetDisplayPortMargins()));
+ }
+
+ // APZ transforms inputs assuming we applied the exact scroll offset it
+ // requested (|apzScrollOffset|). Since we may not have, record the difference
+ // between what APZ asked for and what we actually applied, and apply it to
+ // input events to compensate.
+ // Note that if the main-thread had a change in its scroll position, we don't
+ // want to record that difference here, because it can be large and throw off
+ // input events by a large amount. It is also going to be transient, because
+ // any main-thread scroll position change will be synced to APZ and we will
+ // get another repaint request when APZ confirms. In the interval while this
+ // is happening we can just leave the callback transform as it was.
+ bool mainThreadScrollChanged =
+ sf && sf->CurrentScrollGeneration() != aRequest.GetScrollGeneration() &&
+ nsLayoutUtils::CanScrollOriginClobberApz(sf->LastScrollOrigin());
+ if (aContent && !mainThreadScrollChanged) {
+ aContent->SetProperty(nsGkAtoms::apzCallbackTransform,
+ new CSSPoint(scrollDelta),
+ nsINode::DeleteProperty<CSSPoint>);
+ }
+
+ return displayPortMargins;
+}
+
+static void SetDisplayPortMargins(PresShell* aPresShell, nsIContent* aContent,
+ const DisplayPortMargins& aDisplayPortMargins,
+ CSSSize aDisplayPortBase) {
+ if (!aContent) {
+ return;
+ }
+
+ bool hadDisplayPort = DisplayPortUtils::HasDisplayPort(aContent);
+ if (MOZ_LOG_TEST(sDisplayportLog, LogLevel::Debug)) {
+ if (!hadDisplayPort) {
+ mozilla::layers::ScrollableLayerGuid::ViewID viewID =
+ mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID;
+ nsLayoutUtils::FindIDFor(aContent, &viewID);
+ MOZ_LOG(
+ sDisplayportLog, LogLevel::Debug,
+ ("APZCCH installing displayport margins %s on scrollId=%" PRIu64 "\n",
+ ToString(aDisplayPortMargins).c_str(), viewID));
+ }
+ }
+ DisplayPortUtils::SetDisplayPortMargins(
+ aContent, aPresShell, aDisplayPortMargins,
+ hadDisplayPort ? DisplayPortUtils::ClearMinimalDisplayPortProperty::No
+ : DisplayPortUtils::ClearMinimalDisplayPortProperty::Yes,
+ 0);
+ if (!hadDisplayPort) {
+ DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(
+ aContent->GetPrimaryFrame());
+ }
+
+ nsRect base(0, 0, aDisplayPortBase.width * AppUnitsPerCSSPixel(),
+ aDisplayPortBase.height * AppUnitsPerCSSPixel());
+ DisplayPortUtils::SetDisplayPortBaseIfNotSet(aContent, base);
+}
+
+static void SetPaintRequestTime(nsIContent* aContent,
+ const TimeStamp& aPaintRequestTime) {
+ aContent->SetProperty(nsGkAtoms::paintRequestTime,
+ new TimeStamp(aPaintRequestTime),
+ nsINode::DeleteProperty<TimeStamp>);
+}
+
+void APZCCallbackHelper::NotifyLayerTransforms(
+ const nsTArray<MatrixMessage>& aTransforms) {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (const MatrixMessage& msg : aTransforms) {
+ BrowserParent* parent =
+ BrowserParent::GetBrowserParentFromLayersId(msg.GetLayersId());
+ if (parent) {
+ parent->SetChildToParentConversionMatrix(
+ ViewAs<LayoutDeviceToLayoutDeviceMatrix4x4>(
+ msg.GetMatrix(),
+ PixelCastJustification::ContentProcessIsLayerInUiProcess),
+ msg.GetTopLevelViewportVisibleRectInBrowserCoords());
+ }
+ }
+}
+
+void APZCCallbackHelper::UpdateRootFrame(const RepaintRequest& aRequest) {
+ if (aRequest.GetScrollId() == ScrollableLayerGuid::NULL_SCROLL_ID) {
+ return;
+ }
+ RefPtr<nsIContent> content =
+ nsLayoutUtils::FindContentFor(aRequest.GetScrollId());
+ if (!content) {
+ return;
+ }
+
+ RefPtr<PresShell> presShell = GetPresShell(content);
+ if (!presShell || aRequest.GetPresShellId() != presShell->GetPresShellId()) {
+ return;
+ }
+
+ APZCCH_LOG("Handling request %s\n", ToString(aRequest).c_str());
+ if (nsLayoutUtils::AllowZoomingForDocument(presShell->GetDocument()) &&
+ aRequest.GetAsyncZoom().scale != 1.0) {
+ // If zooming is disabled then we don't really want to let APZ fiddle
+ // with these things. In theory setting the resolution here should be a
+ // no-op, but setting the visual viewport size is bad because it can cause a
+ // stale value to be returned by window.innerWidth/innerHeight (see bug
+ // 1187792).
+
+ float presShellResolution = presShell->GetResolution();
+
+ // If the pres shell resolution has changed on the content side side
+ // the time this repaint request was fired, consider this request out of
+ // date and drop it; setting a zoom based on the out-of-date resolution can
+ // have the effect of getting us stuck with the stale resolution.
+ // One might think that if the last ResolutionChangeOrigin was apz then the
+ // pres shell resolutions should match but
+ // that is not the case. We can get multiple repaint requests that has the
+ // same pres shell resolution (because apz didn't receive a content layers
+ // update inbetween) if the first has async zoom we apply that and chance
+ // the content pres shell resolution and thus when handling the second
+ // repaint request the pres shell resolution won't match. So that's why we
+ // also check if the last resolution change origin was apz (aka 'us').
+ if (!FuzzyEqualsMultiplicative(presShellResolution,
+ aRequest.GetPresShellResolution()) &&
+ presShell->GetLastResolutionChangeOrigin() !=
+ ResolutionChangeOrigin::Apz) {
+ return;
+ }
+
+ // The pres shell resolution is updated by the the async zoom since the
+ // last paint.
+ // We want to calculate the new presshell resolution as
+ // |aRequest.GetPresShellResolution() * aRequest.GetAsyncZoom()| but that
+ // calculation can lead to small inaccuracies due to limited floating point
+ // precision. Specifically,
+ // clang-format off
+ // asyncZoom = zoom / layerPixelsPerCSSPixel
+ // = zoom / (devPixelsPerCSSPixel * cumulativeResolution)
+ // clang-format on
+ // Since this is a root frame we generally do not allow css transforms to
+ // scale it, so it is very likely that cumulativeResolution ==
+ // presShellResoluion. So
+ // clang-format off
+ // newPresShellResoluion = presShellResoluion * asyncZoom
+ // = presShellResoluion * zoom / (devPixelsPerCSSPixel * presShellResoluion)
+ // = zoom / devPixelsPerCSSPixel
+ // clang-format on
+ // However, we want to keep the calculation general and so we do not assume
+ // presShellResoluion == cumulativeResolution, but rather factor those
+ // values out so they cancel and the floating point division has a very high
+ // probability of being exactly 1.
+ presShellResolution =
+ (aRequest.GetPresShellResolution() /
+ aRequest.GetCumulativeResolution().scale) *
+ (aRequest.GetZoom() / aRequest.GetDevPixelsPerCSSPixel()).scale;
+ presShell->SetResolutionAndScaleTo(presShellResolution,
+ ResolutionChangeOrigin::Apz);
+
+ // Changing the resolution will trigger a reflow which will cause the
+ // main-thread scroll position to be realigned in layer pixels. This
+ // (subpixel) scroll mutation can trigger a scroll update to APZ which
+ // is undesirable. Instead of having that happen as part of the post-reflow
+ // code, we force it to happen here with ScrollOrigin::Apz so that it
+ // doesn't trigger a scroll update to APZ.
+ nsIScrollableFrame* sf =
+ nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId());
+ CSSPoint currentScrollPosition =
+ CSSPoint::FromAppUnits(sf->GetScrollPosition());
+ ScrollSnapTargetIds snapTargetIds = aRequest.GetLastSnapTargetIds();
+ sf->ScrollToCSSPixelsForApz(currentScrollPosition,
+ std::move(snapTargetIds));
+ }
+
+ // Do this as late as possible since scrolling can flush layout. It also
+ // adjusts the display port margins, so do it before we set those.
+ DisplayPortMargins displayPortMargins = ScrollFrame(content, aRequest);
+
+ SetDisplayPortMargins(presShell, content, displayPortMargins,
+ aRequest.CalculateCompositedSizeInCssPixels());
+ SetPaintRequestTime(content, aRequest.GetPaintRequestTime());
+}
+
+void APZCCallbackHelper::UpdateSubFrame(const RepaintRequest& aRequest) {
+ if (aRequest.GetScrollId() == ScrollableLayerGuid::NULL_SCROLL_ID) {
+ return;
+ }
+ RefPtr<nsIContent> content =
+ nsLayoutUtils::FindContentFor(aRequest.GetScrollId());
+ if (!content) {
+ return;
+ }
+
+ // We don't currently support zooming for subframes, so nothing extra
+ // needs to be done beyond the tasks common to this and UpdateRootFrame.
+ DisplayPortMargins displayPortMargins = ScrollFrame(content, aRequest);
+ if (RefPtr<PresShell> presShell = GetPresShell(content)) {
+ SetDisplayPortMargins(presShell, content, displayPortMargins,
+ aRequest.CalculateCompositedSizeInCssPixels());
+ }
+ SetPaintRequestTime(content, aRequest.GetPaintRequestTime());
+}
+
+bool APZCCallbackHelper::GetOrCreateScrollIdentifiers(
+ nsIContent* aContent, uint32_t* aPresShellIdOut,
+ ScrollableLayerGuid::ViewID* aViewIdOut) {
+ if (!aContent) {
+ return false;
+ }
+ *aViewIdOut = nsLayoutUtils::FindOrCreateIDFor(aContent);
+ if (PresShell* presShell = GetPresShell(aContent)) {
+ *aPresShellIdOut = presShell->GetPresShellId();
+ return true;
+ }
+ return false;
+}
+
+void APZCCallbackHelper::InitializeRootDisplayport(PresShell* aPresShell) {
+ // Create a view-id and set a zero-margin displayport for the root element
+ // of the root document in the chrome process. This ensures that the scroll
+ // frame for this element gets an APZC, which in turn ensures that all content
+ // in the chrome processes is covered by an APZC.
+ // The displayport is zero-margin because this element is generally not
+ // actually scrollable (if it is, APZC will set proper margins when it's
+ // scrolled).
+ if (!aPresShell) {
+ return;
+ }
+
+ MOZ_ASSERT(aPresShell->GetDocument());
+ nsIContent* content = aPresShell->GetDocument()->GetDocumentElement();
+ if (!content) {
+ return;
+ }
+
+ uint32_t presShellId;
+ ScrollableLayerGuid::ViewID viewId;
+ if (APZCCallbackHelper::GetOrCreateScrollIdentifiers(content, &presShellId,
+ &viewId)) {
+ MOZ_LOG(
+ sDisplayportLog, LogLevel::Debug,
+ ("Initializing root displayport on scrollId=%" PRIu64 "\n", viewId));
+ Maybe<nsRect> baseRect =
+ DisplayPortUtils::GetRootDisplayportBase(aPresShell);
+ if (baseRect) {
+ DisplayPortUtils::SetDisplayPortBaseIfNotSet(content, *baseRect);
+ }
+
+ DisplayPortUtils::SetDisplayPortMargins(
+ content, aPresShell, DisplayPortMargins::Empty(content),
+ DisplayPortUtils::ClearMinimalDisplayPortProperty::Yes, 0);
+ DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(
+ content->GetPrimaryFrame());
+ }
+}
+
+nsPresContext* APZCCallbackHelper::GetPresContextForContent(
+ nsIContent* aContent) {
+ dom::Document* doc = aContent->GetComposedDoc();
+ if (!doc) {
+ return nullptr;
+ }
+ PresShell* presShell = doc->GetPresShell();
+ if (!presShell) {
+ return nullptr;
+ }
+ return presShell->GetPresContext();
+}
+
+PresShell* APZCCallbackHelper::GetRootContentDocumentPresShellForContent(
+ nsIContent* aContent) {
+ nsPresContext* context = GetPresContextForContent(aContent);
+ if (!context) {
+ return nullptr;
+ }
+ context = context->GetInProcessRootContentDocumentPresContext();
+ if (!context) {
+ return nullptr;
+ }
+ return context->PresShell();
+}
+
+nsEventStatus APZCCallbackHelper::DispatchWidgetEvent(WidgetGUIEvent& aEvent) {
+ nsEventStatus status = nsEventStatus_eConsumeNoDefault;
+ if (aEvent.mWidget) {
+ aEvent.mWidget->DispatchEvent(&aEvent, status);
+ }
+ return status;
+}
+
+nsEventStatus APZCCallbackHelper::DispatchSynthesizedMouseEvent(
+ EventMessage aMsg, const LayoutDevicePoint& aRefPoint, Modifiers aModifiers,
+ int32_t aClickCount, nsIWidget* aWidget) {
+ MOZ_ASSERT(aMsg == eMouseMove || aMsg == eMouseDown || aMsg == eMouseUp ||
+ aMsg == eMouseLongTap);
+
+ WidgetMouseEvent event(true, aMsg, aWidget, WidgetMouseEvent::eReal,
+ WidgetMouseEvent::eNormal);
+ event.mRefPoint = LayoutDeviceIntPoint::Truncate(aRefPoint.x, aRefPoint.y);
+ event.mButton = MouseButton::ePrimary;
+ event.mButtons |= MouseButtonsFlag::ePrimaryFlag;
+ event.mInputSource = dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH;
+ if (aMsg == eMouseLongTap) {
+ event.mFlags.mOnlyChromeDispatch = true;
+ }
+ if (aMsg != eMouseMove) {
+ event.mClickCount = aClickCount;
+ }
+ event.mModifiers = aModifiers;
+ // Real touch events will generate corresponding pointer events. We set
+ // convertToPointer to false to prevent the synthesized mouse events generate
+ // pointer events again.
+ event.convertToPointer = false;
+ return DispatchWidgetEvent(event);
+}
+
+PreventDefaultResult APZCCallbackHelper::DispatchMouseEvent(
+ PresShell* aPresShell, const nsString& aType, const CSSPoint& aPoint,
+ int32_t aButton, int32_t aClickCount, int32_t aModifiers,
+ unsigned short aInputSourceArg, uint32_t aPointerId) {
+ NS_ENSURE_TRUE(aPresShell, PreventDefaultResult::ByContent);
+
+ PreventDefaultResult preventDefaultResult;
+ nsContentUtils::SendMouseEvent(
+ aPresShell, aType, aPoint.x, aPoint.y, aButton,
+ nsIDOMWindowUtils::MOUSE_BUTTONS_NOT_SPECIFIED, aClickCount, aModifiers,
+ /* aIgnoreRootScrollFrame = */ false, 0, aInputSourceArg, aPointerId,
+ false, &preventDefaultResult, false,
+ /* aIsWidgetEventSynthesized = */ false);
+ return preventDefaultResult;
+}
+
+void APZCCallbackHelper::FireSingleTapEvent(const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers,
+ int32_t aClickCount,
+ nsIWidget* aWidget) {
+ if (aWidget->Destroyed()) {
+ return;
+ }
+ APZCCH_LOG("Dispatching single-tap component events to %s\n",
+ ToString(aPoint).c_str());
+ DispatchSynthesizedMouseEvent(eMouseMove, aPoint, aModifiers, aClickCount,
+ aWidget);
+ DispatchSynthesizedMouseEvent(eMouseDown, aPoint, aModifiers, aClickCount,
+ aWidget);
+ DispatchSynthesizedMouseEvent(eMouseUp, aPoint, aModifiers, aClickCount,
+ aWidget);
+}
+
+static dom::Element* GetDisplayportElementFor(
+ nsIScrollableFrame* aScrollableFrame) {
+ if (!aScrollableFrame) {
+ return nullptr;
+ }
+ nsIFrame* scrolledFrame = aScrollableFrame->GetScrolledFrame();
+ if (!scrolledFrame) {
+ return nullptr;
+ }
+ // |scrolledFrame| should at this point be the root content frame of the
+ // nearest ancestor scrollable frame. The element corresponding to this
+ // frame should be the one with the displayport set on it, so find that
+ // element and return it.
+ nsIContent* content = scrolledFrame->GetContent();
+ MOZ_ASSERT(content->IsElement()); // roc says this must be true
+ return content->AsElement();
+}
+
+static dom::Element* GetRootDocumentElementFor(nsIWidget* aWidget) {
+ // This returns the root element that ChromeProcessController sets the
+ // displayport on during initialization.
+ if (nsView* view = nsView::GetViewFor(aWidget)) {
+ if (PresShell* presShell = view->GetPresShell()) {
+ MOZ_ASSERT(presShell->GetDocument());
+ return presShell->GetDocument()->GetDocumentElement();
+ }
+ }
+ return nullptr;
+}
+
+namespace {
+
+using FrameForPointOption = nsLayoutUtils::FrameForPointOption;
+
+// Determine the scrollable target frame for the given point and add it to
+// the target list. If the frame doesn't have a displayport, set one.
+// Return whether or not the frame had a displayport that has already been
+// painted (in this case, the caller can send the SetTargetAPZC notification
+// right away, rather than waiting for a transaction to propagate the
+// displayport to APZ first).
+static bool PrepareForSetTargetAPZCNotification(
+ nsIWidget* aWidget, const LayersId& aLayersId, nsIFrame* aRootFrame,
+ const LayoutDeviceIntPoint& aRefPoint,
+ nsTArray<ScrollableLayerGuid>* aTargets) {
+ ScrollableLayerGuid guid(aLayersId, 0, ScrollableLayerGuid::NULL_SCROLL_ID);
+ RelativeTo relativeTo{aRootFrame, ViewportType::Visual};
+ nsPoint point = nsLayoutUtils::GetEventCoordinatesRelativeTo(
+ aWidget, aRefPoint, relativeTo);
+ nsIFrame* target = nsLayoutUtils::GetFrameForPoint(relativeTo, point);
+ nsIScrollableFrame* scrollAncestor =
+ target ? nsLayoutUtils::GetAsyncScrollableAncestorFrame(target)
+ : aRootFrame->PresShell()->GetRootScrollFrameAsScrollable();
+
+ // Assuming that if there's no scrollAncestor, there's already a displayPort.
+ nsCOMPtr<dom::Element> dpElement =
+ scrollAncestor ? GetDisplayportElementFor(scrollAncestor)
+ : GetRootDocumentElementFor(aWidget);
+
+ if (MOZ_LOG_TEST(sApzHlpLog, LogLevel::Debug)) {
+ nsAutoString dpElementDesc;
+ if (dpElement) {
+ dpElement->Describe(dpElementDesc);
+ }
+ APZCCH_LOG("For event at %s found scrollable element %p (%s)\n",
+ ToString(aRefPoint).c_str(), dpElement.get(),
+ NS_LossyConvertUTF16toASCII(dpElementDesc).get());
+ }
+
+ bool guidIsValid = APZCCallbackHelper::GetOrCreateScrollIdentifiers(
+ dpElement, &(guid.mPresShellId), &(guid.mScrollId));
+ aTargets->AppendElement(guid);
+
+ if (!guidIsValid) {
+ return false;
+ }
+ if (DisplayPortUtils::HasNonMinimalNonZeroDisplayPort(dpElement)) {
+ // If the element has a displayport but it hasn't been painted yet,
+ // we want the caller to wait for the paint to happen, but we don't
+ // need to set the displayport here since it's already been set.
+ return !DisplayPortUtils::HasPaintedDisplayPort(dpElement);
+ }
+
+ if (!scrollAncestor) {
+ // This can happen if the document element gets swapped out after
+ // ChromeProcessController runs InitializeRootDisplayport. In this case
+ // let's try to set a displayport again and bail out on this operation.
+ APZCCH_LOG("Widget %p's document element %p didn't have a displayport\n",
+ aWidget, dpElement.get());
+ APZCCallbackHelper::InitializeRootDisplayport(aRootFrame->PresShell());
+ return false;
+ }
+
+ APZCCH_LOG("%p didn't have a displayport, so setting one...\n",
+ dpElement.get());
+ MOZ_LOG(sDisplayportLog, LogLevel::Debug,
+ ("Activating displayport on scrollId=%" PRIu64 " for SetTargetAPZC\n",
+ guid.mScrollId));
+ bool activated = DisplayPortUtils::CalculateAndSetDisplayPortMargins(
+ scrollAncestor, DisplayPortUtils::RepaintMode::Repaint);
+ if (!activated) {
+ return false;
+ }
+
+ nsIFrame* frame = do_QueryFrame(scrollAncestor);
+ DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(frame);
+
+ return !DisplayPortUtils::HasPaintedDisplayPort(dpElement);
+}
+
+static void SendLayersDependentApzcTargetConfirmation(
+ nsIWidget* aWidget, uint64_t aInputBlockId,
+ nsTArray<ScrollableLayerGuid>&& aTargets) {
+ WindowRenderer* renderer = aWidget->GetWindowRenderer();
+ if (!renderer) {
+ return;
+ }
+
+ if (WebRenderLayerManager* wrlm = renderer->AsWebRender()) {
+ if (WebRenderBridgeChild* wrbc = wrlm->WrBridge()) {
+ wrbc->SendSetConfirmedTargetAPZC(aInputBlockId, aTargets);
+ }
+ return;
+ }
+}
+
+} // namespace
+
+DisplayportSetListener::DisplayportSetListener(
+ nsIWidget* aWidget, nsPresContext* aPresContext,
+ const uint64_t& aInputBlockId, nsTArray<ScrollableLayerGuid>&& aTargets)
+ : ManagedPostRefreshObserver(aPresContext),
+ mWidget(aWidget),
+ mInputBlockId(aInputBlockId),
+ mTargets(std::move(aTargets)) {
+ MOZ_ASSERT(!mAction, "Setting Action twice");
+ mAction = [instance = MOZ_KnownLive(this)](bool aWasCanceled) {
+ instance->OnPostRefresh();
+ return Unregister::Yes;
+ };
+}
+
+DisplayportSetListener::~DisplayportSetListener() = default;
+
+void DisplayportSetListener::Register() {
+ APZCCH_LOG("DisplayportSetListener::Register\n");
+ mPresContext->RegisterManagedPostRefreshObserver(this);
+}
+
+void DisplayportSetListener::OnPostRefresh() {
+ APZCCH_LOG("Got refresh, sending target APZCs for input block %" PRIu64 "\n",
+ mInputBlockId);
+ SendLayersDependentApzcTargetConfirmation(mWidget, mInputBlockId,
+ std::move(mTargets));
+}
+
+already_AddRefed<DisplayportSetListener>
+APZCCallbackHelper::SendSetTargetAPZCNotification(nsIWidget* aWidget,
+ dom::Document* aDocument,
+ const WidgetGUIEvent& aEvent,
+ const LayersId& aLayersId,
+ uint64_t aInputBlockId) {
+ if (!aWidget || !aDocument) {
+ return nullptr;
+ }
+ if (aInputBlockId == sLastTargetAPZCNotificationInputBlock) {
+ // We have already confirmed the target APZC for a previous event of this
+ // input block. If we activated a scroll frame for this input block,
+ // sending another target APZC confirmation would be harmful, as it might
+ // race the original confirmation (which needs to go through a layers
+ // transaction).
+ APZCCH_LOG("Not resending target APZC confirmation for input block %" PRIu64
+ "\n",
+ aInputBlockId);
+ return nullptr;
+ }
+ sLastTargetAPZCNotificationInputBlock = aInputBlockId;
+ if (PresShell* presShell = aDocument->GetPresShell()) {
+ if (nsIFrame* rootFrame = presShell->GetRootFrame()) {
+ bool waitForRefresh = false;
+ nsTArray<ScrollableLayerGuid> targets;
+
+ if (const WidgetTouchEvent* touchEvent = aEvent.AsTouchEvent()) {
+ for (size_t i = 0; i < touchEvent->mTouches.Length(); i++) {
+ waitForRefresh |= PrepareForSetTargetAPZCNotification(
+ aWidget, aLayersId, rootFrame, touchEvent->mTouches[i]->mRefPoint,
+ &targets);
+ }
+ } else if (const WidgetWheelEvent* wheelEvent = aEvent.AsWheelEvent()) {
+ waitForRefresh = PrepareForSetTargetAPZCNotification(
+ aWidget, aLayersId, rootFrame, wheelEvent->mRefPoint, &targets);
+ } else if (const WidgetMouseEvent* mouseEvent = aEvent.AsMouseEvent()) {
+ waitForRefresh = PrepareForSetTargetAPZCNotification(
+ aWidget, aLayersId, rootFrame, mouseEvent->mRefPoint, &targets);
+ }
+ // TODO: Do other types of events need to be handled?
+
+ if (!targets.IsEmpty()) {
+ if (waitForRefresh) {
+ APZCCH_LOG(
+ "At least one target got a new displayport, need to wait for "
+ "refresh\n");
+ return MakeAndAddRef<DisplayportSetListener>(
+ aWidget, presShell->GetPresContext(), aInputBlockId,
+ std::move(targets));
+ }
+ APZCCH_LOG("Sending target APZCs for input block %" PRIu64 "\n",
+ aInputBlockId);
+ aWidget->SetConfirmedTargetAPZC(aInputBlockId, targets);
+ }
+ }
+ }
+ return nullptr;
+}
+
+void APZCCallbackHelper::NotifyMozMouseScrollEvent(
+ const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent) {
+ nsCOMPtr<nsIContent> targetContent = nsLayoutUtils::FindContentFor(aScrollId);
+ if (!targetContent) {
+ return;
+ }
+ RefPtr<dom::Document> ownerDoc = targetContent->OwnerDoc();
+ if (!ownerDoc) {
+ return;
+ }
+
+ nsContentUtils::DispatchEventOnlyToChrome(ownerDoc, targetContent, aEvent,
+ CanBubble::eYes, Cancelable::eYes);
+}
+
+void APZCCallbackHelper::NotifyFlushComplete(PresShell* aPresShell) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // In some cases, flushing the APZ state to the main thread doesn't actually
+ // trigger a flush and repaint (this is an intentional optimization - the
+ // stuff visible to the user is still correct). However, reftests update their
+ // snapshot based on invalidation events that are emitted during paints,
+ // so we ensure that we kick off a paint when an APZ flush is done. Note that
+ // only chrome/testing code can trigger this behaviour.
+ if (aPresShell && aPresShell->GetRootFrame()) {
+ aPresShell->GetRootFrame()->SchedulePaint(nsIFrame::PAINT_DEFAULT, false);
+ }
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(observerService);
+ observerService->NotifyObservers(nullptr, "apz-repaints-flushed", nullptr);
+}
+
+/* static */
+bool APZCCallbackHelper::IsScrollInProgress(nsIScrollableFrame* aFrame) {
+ using AnimationState = nsIScrollableFrame::AnimationState;
+
+ return aFrame->ScrollAnimationState().contains(AnimationState::MainThread) ||
+ nsLayoutUtils::CanScrollOriginClobberApz(aFrame->LastScrollOrigin());
+}
+
+/* static */
+void APZCCallbackHelper::NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (nsIScrollableFrame* scrollFrame =
+ nsLayoutUtils::FindScrollableFrameFor(aScrollId)) {
+ scrollFrame->AsyncScrollbarDragInitiated(aDragBlockId, aDirection);
+ }
+}
+
+/* static */
+void APZCCallbackHelper::NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (nsIScrollableFrame* scrollFrame =
+ nsLayoutUtils::FindScrollableFrameFor(aScrollId)) {
+ scrollFrame->AsyncScrollbarDragRejected();
+ }
+}
+
+/* static */
+void APZCCallbackHelper::NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(observerService);
+
+ nsAutoString data;
+ data.AppendInt(aScrollId);
+ observerService->NotifyObservers(nullptr, "autoscroll-rejected-by-apz",
+ data.get());
+}
+
+/* static */
+void APZCCallbackHelper::CancelAutoscroll(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(observerService);
+
+ nsAutoString data;
+ data.AppendInt(aScrollId);
+ observerService->NotifyObservers(nullptr, "apz:cancel-autoscroll",
+ data.get());
+}
+
+/* static */
+void APZCCallbackHelper::NotifyScaleGestureComplete(
+ const nsCOMPtr<nsIWidget>& aWidget, float aScale) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (nsView* view = nsView::GetViewFor(aWidget)) {
+ if (PresShell* presShell = view->GetPresShell()) {
+ dom::Document* doc = presShell->GetDocument();
+ MOZ_ASSERT(doc);
+ if (nsPIDOMWindowInner* win = doc->GetInnerWindow()) {
+ dom::AutoJSAPI jsapi;
+ if (!jsapi.Init(win)) {
+ return;
+ }
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JS::Value> detail(cx, JS::Float32Value(aScale));
+ RefPtr<dom::CustomEvent> event =
+ NS_NewDOMCustomEvent(doc, nullptr, nullptr);
+ event->InitCustomEvent(cx, u"MozScaleGestureComplete"_ns,
+ /* CanBubble */ true,
+ /* Cancelable */ false, detail);
+ event->SetTrusted(true);
+ auto* dispatcher = new AsyncEventDispatcher(doc, event.forget(),
+ ChromeOnlyDispatch::eYes);
+ dispatcher->PostDOMEvent();
+ }
+ }
+ }
+}
+
+/* static */
+void APZCCallbackHelper::NotifyPinchGesture(
+ PinchGestureInput::PinchGestureType aType,
+ const LayoutDevicePoint& aFocusPoint, LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers, const nsCOMPtr<nsIWidget>& aWidget) {
+ APZCCH_LOG("APZCCallbackHelper dispatching pinch gesture\n");
+ EventMessage msg;
+ switch (aType) {
+ case PinchGestureInput::PINCHGESTURE_START:
+ msg = eMagnifyGestureStart;
+ break;
+ case PinchGestureInput::PINCHGESTURE_SCALE:
+ msg = eMagnifyGestureUpdate;
+ break;
+ case PinchGestureInput::PINCHGESTURE_FINGERLIFTED:
+ case PinchGestureInput::PINCHGESTURE_END:
+ msg = eMagnifyGesture;
+ break;
+ }
+
+ WidgetSimpleGestureEvent event(true, msg, aWidget.get());
+ // XXX mDelta for the eMagnifyGesture event is supposed to be the
+ // cumulative magnification over the entire gesture (per docs in
+ // SimpleGestureEvent.webidl) but currently APZ just sends us a zero
+ // aSpanChange for that event, so the mDelta is wrong. Nothing relies
+ // on that currently, but we might want to fix it at some point.
+ event.mDelta = aSpanChange;
+ event.mModifiers = aModifiers;
+ event.mRefPoint = RoundedToInt(aFocusPoint);
+
+ DispatchWidgetEvent(event);
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/APZCCallbackHelper.h b/gfx/layers/apz/util/APZCCallbackHelper.h
new file mode 100644
index 0000000000..c04bae88d2
--- /dev/null
+++ b/gfx/layers/apz/util/APZCCallbackHelper.h
@@ -0,0 +1,197 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_layers_APZCCallbackHelper_h
+#define mozilla_layers_APZCCallbackHelper_h
+
+#include "InputData.h"
+#include "LayersTypes.h"
+#include "Units.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/MatrixMessage.h"
+#include "nsRefreshObservers.h"
+
+#include <functional>
+
+class nsIContent;
+class nsIScrollableFrame;
+class nsIWidget;
+class nsPresContext;
+template <class T>
+struct already_AddRefed;
+template <class T>
+class nsCOMPtr;
+
+namespace mozilla {
+
+class PresShell;
+enum class PreventDefaultResult : uint8_t;
+
+namespace layers {
+
+struct RepaintRequest;
+
+/* Refer to documentation on SendSetTargetAPZCNotification for this class */
+class DisplayportSetListener : public ManagedPostRefreshObserver {
+ public:
+ DisplayportSetListener(nsIWidget* aWidget, nsPresContext*,
+ const uint64_t& aInputBlockId,
+ nsTArray<ScrollableLayerGuid>&& aTargets);
+ virtual ~DisplayportSetListener();
+ void Register();
+
+ private:
+ RefPtr<nsIWidget> mWidget;
+ uint64_t mInputBlockId;
+ nsTArray<ScrollableLayerGuid> mTargets;
+
+ void OnPostRefresh();
+};
+
+/* This class contains some helper methods that facilitate implementing the
+ GeckoContentController callback interface required by the
+ AsyncPanZoomController. Since different platforms need to implement this
+ interface in similar-but- not-quite-the-same ways, this utility class
+ provides some helpful methods to hold code that can be shared across the
+ different platform implementations.
+ */
+class APZCCallbackHelper {
+ typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid;
+
+ public:
+ static void NotifyLayerTransforms(const nsTArray<MatrixMessage>& aTransforms);
+
+ /* Applies the scroll and zoom parameters from the given RepaintRequest object
+ to the root frame for the given metrics' scrollId. If tiled thebes layers
+ are enabled, this will align the displayport to tile boundaries. Setting
+ the scroll position can cause some small adjustments to be made to the
+ actual scroll position. */
+ static void UpdateRootFrame(const RepaintRequest& aRequest);
+
+ /* Applies the scroll parameters from the given RepaintRequest object to the
+ subframe corresponding to given metrics' scrollId. If tiled thebes
+ layers are enabled, this will align the displayport to tile boundaries.
+ Setting the scroll position can cause some small adjustments to be made
+ to the actual scroll position. */
+ static void UpdateSubFrame(const RepaintRequest& aRequest);
+
+ /* Get the presShellId and view ID for the given content element.
+ * If the view ID does not exist, one is created.
+ * The pres shell ID should generally already exist; if it doesn't for some
+ * reason, false is returned. */
+ static bool GetOrCreateScrollIdentifiers(
+ nsIContent* aContent, uint32_t* aPresShellIdOut,
+ ScrollableLayerGuid::ViewID* aViewIdOut);
+
+ /* Initialize a zero-margin displayport on the root document element of the
+ given presShell. */
+ static void InitializeRootDisplayport(PresShell* aPresShell);
+
+ /* Get the pres context associated with the document enclosing |aContent|. */
+ static nsPresContext* GetPresContextForContent(nsIContent* aContent);
+
+ /* Get the pres shell associated with the root content document enclosing
+ * |aContent|. */
+ static PresShell* GetRootContentDocumentPresShellForContent(
+ nsIContent* aContent);
+
+ /* Dispatch a widget event via the widget stored in the event, if any.
+ * In a child process, allows the BrowserParent event-capture mechanism to
+ * intercept the event. */
+ static nsEventStatus DispatchWidgetEvent(WidgetGUIEvent& aEvent);
+
+ /* Synthesize a mouse event with the given parameters, and dispatch it
+ * via the given widget. */
+ MOZ_CAN_RUN_SCRIPT
+ static nsEventStatus DispatchSynthesizedMouseEvent(
+ EventMessage aMsg, const LayoutDevicePoint& aRefPoint,
+ Modifiers aModifiers, int32_t aClickCount, nsIWidget* aWidget);
+
+ /* Dispatch a mouse event with the given parameters.
+ * Return whether or not any listeners have called preventDefault on the
+ * event.
+ * This is a lightweight wrapper around nsContentUtils::SendMouseEvent()
+ * and as such expects |aPoint| to be in layout coordinates. */
+ MOZ_CAN_RUN_SCRIPT
+ static PreventDefaultResult DispatchMouseEvent(
+ PresShell* aPresShell, const nsString& aType, const CSSPoint& aPoint,
+ int32_t aButton, int32_t aClickCount, int32_t aModifiers,
+ unsigned short aInputSourceArg, uint32_t aPointerId);
+
+ /* Fire a single-tap event at the given point. The event is dispatched
+ * via the given widget. */
+ MOZ_CAN_RUN_SCRIPT
+ static void FireSingleTapEvent(const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers, int32_t aClickCount,
+ nsIWidget* aWidget);
+
+ /* Perform hit-testing on the touch points of |aEvent| to determine
+ * which scrollable frames they target. If any of these frames don't have
+ * a displayport, set one.
+ *
+ * If any displayports need to be set, this function returns a heap-allocated
+ * object. The caller is responsible for calling Register() on that object.
+ *
+ * The object registers itself as a post-refresh observer on the presShell
+ * and ensures that notifications get sent to APZ correctly after the
+ * refresh.
+ *
+ * Having the caller manage this object is desirable in case they want to
+ * (a) know about the fact that a displayport needs to be set, and
+ * (b) register a post-refresh observer of their own that will run in
+ * a defined ordering relative to the APZ messages.
+ */
+ static already_AddRefed<DisplayportSetListener> SendSetTargetAPZCNotification(
+ nsIWidget* aWidget, mozilla::dom::Document* aDocument,
+ const WidgetGUIEvent& aEvent, const LayersId& aLayersId,
+ uint64_t aInputBlockId);
+
+ /* Notify content of a mouse scroll testing event. */
+ static void NotifyMozMouseScrollEvent(
+ const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent);
+
+ /* Notify content that the repaint flush is complete. */
+ static void NotifyFlushComplete(PresShell* aPresShell);
+
+ static void NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection);
+ static void NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId);
+ static void NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId);
+
+ static void CancelAutoscroll(const ScrollableLayerGuid::ViewID& aScrollId);
+ static void NotifyScaleGestureComplete(const nsCOMPtr<nsIWidget>& aWidget,
+ float aScale);
+
+ /*
+ * Check if the scrollable frame is currently in the middle of a main thread
+ * async or smooth scroll, or has already requested some other apz scroll that
+ * hasn't been acknowledged by apz.
+ *
+ * We want to discard apz updates to the main-thread scroll offset if this is
+ * true to prevent clobbering higher priority origins.
+ */
+ static bool IsScrollInProgress(nsIScrollableFrame* aFrame);
+
+ /* Notify content of the progress of a pinch gesture that APZ won't do
+ * zooming for (because the apz.allow_zooming pref is false). This function
+ * will dispatch appropriate WidgetSimpleGestureEvent events to gecko.
+ */
+ static void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ const LayoutDevicePoint& aFocusPoint,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers,
+ const nsCOMPtr<nsIWidget>& aWidget);
+
+ private:
+ static uint64_t sLastTargetAPZCNotificationInputBlock;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_APZCCallbackHelper_h */
diff --git a/gfx/layers/apz/util/APZEventState.cpp b/gfx/layers/apz/util/APZEventState.cpp
new file mode 100644
index 0000000000..7384296f45
--- /dev/null
+++ b/gfx/layers/apz/util/APZEventState.cpp
@@ -0,0 +1,567 @@
+/* -*- 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 "APZEventState.h"
+
+#include <utility>
+
+#include "APZCCallbackHelper.h"
+#include "ActiveElementManager.h"
+#include "TouchManager.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/PositionedEventTargeting.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/ToString.h"
+#include "mozilla/TouchEvents.h"
+#include "mozilla/ViewportUtils.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/widget/nsAutoRollup.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsDocShell.h"
+#include "nsIDOMWindowUtils.h"
+#include "nsINamed.h"
+#include "nsIScrollableFrame.h"
+#include "nsIScrollbarMediator.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsIWidget.h"
+#include "nsLayoutUtils.h"
+#include "nsQueryFrame.h"
+
+static mozilla::LazyLogModule sApzEvtLog("apz.eventstate");
+#define APZES_LOG(...) MOZ_LOG(sApzEvtLog, LogLevel::Debug, (__VA_ARGS__))
+
+// Static helper functions
+namespace {
+
+int32_t WidgetModifiersToDOMModifiers(mozilla::Modifiers aModifiers) {
+ int32_t result = 0;
+ if (aModifiers & mozilla::MODIFIER_SHIFT) {
+ result |= nsIDOMWindowUtils::MODIFIER_SHIFT;
+ }
+ if (aModifiers & mozilla::MODIFIER_CONTROL) {
+ result |= nsIDOMWindowUtils::MODIFIER_CONTROL;
+ }
+ if (aModifiers & mozilla::MODIFIER_ALT) {
+ result |= nsIDOMWindowUtils::MODIFIER_ALT;
+ }
+ if (aModifiers & mozilla::MODIFIER_META) {
+ result |= nsIDOMWindowUtils::MODIFIER_META;
+ }
+ if (aModifiers & mozilla::MODIFIER_ALTGRAPH) {
+ result |= nsIDOMWindowUtils::MODIFIER_ALTGRAPH;
+ }
+ if (aModifiers & mozilla::MODIFIER_CAPSLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_CAPSLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_FN) {
+ result |= nsIDOMWindowUtils::MODIFIER_FN;
+ }
+ if (aModifiers & mozilla::MODIFIER_FNLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_FNLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_NUMLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_NUMLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_SCROLLLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_SCROLLLOCK;
+ }
+ if (aModifiers & mozilla::MODIFIER_SYMBOL) {
+ result |= nsIDOMWindowUtils::MODIFIER_SYMBOL;
+ }
+ if (aModifiers & mozilla::MODIFIER_SYMBOLLOCK) {
+ result |= nsIDOMWindowUtils::MODIFIER_SYMBOLLOCK;
+ }
+ return result;
+}
+
+} // namespace
+
+namespace mozilla {
+namespace layers {
+
+APZEventState::APZEventState(nsIWidget* aWidget,
+ ContentReceivedInputBlockCallback&& aCallback)
+ : mWidget(nullptr) // initialized in constructor body
+ ,
+ mActiveElementManager(new ActiveElementManager()),
+ mContentReceivedInputBlockCallback(std::move(aCallback)),
+ mPendingTouchPreventedResponse(false),
+ mPendingTouchPreventedBlockId(0),
+ mEndTouchIsClick(false),
+ mFirstTouchCancelled(false),
+ mTouchEndCancelled(false),
+ mReceivedNonTouchStart(false),
+ mTouchStartPrevented(false),
+ mLastTouchIdentifier(0) {
+ nsresult rv;
+ mWidget = do_GetWeakReference(aWidget, &rv);
+ MOZ_ASSERT(NS_SUCCEEDED(rv),
+ "APZEventState constructed with a widget that"
+ " does not support weak references. APZ will NOT work!");
+}
+
+APZEventState::~APZEventState() = default;
+
+void APZEventState::ProcessSingleTap(const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers, int32_t aClickCount,
+ uint64_t aInputBlockId) {
+ APZES_LOG("Handling single tap at %s with %d\n", ToString(aPoint).c_str(),
+ mTouchEndCancelled);
+
+ RefPtr<nsIContent> touchRollup = GetTouchRollup();
+ mTouchRollup = nullptr;
+
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ if (!widget) {
+ return;
+ }
+
+ if (mTouchEndCancelled) {
+ return;
+ }
+
+ nsCOMPtr<nsIWidget> localWidget = do_QueryReferent(mWidget);
+ if (localWidget) {
+ widget::nsAutoRollup rollup(touchRollup);
+ APZCCallbackHelper::FireSingleTapEvent(aPoint * aScale, aModifiers,
+ aClickCount, localWidget);
+ }
+
+ mActiveElementManager->ProcessSingleTap();
+}
+
+PreventDefaultResult APZEventState::FireContextmenuEvents(
+ PresShell* aPresShell, const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale, Modifiers aModifiers,
+ const nsCOMPtr<nsIWidget>& aWidget) {
+ // Suppress retargeting for mouse events generated by a long-press
+ EventRetargetSuppression suppression;
+
+ // Synthesize mousemove event for allowing users to emulate to move mouse
+ // cursor over the element. As a result, users can open submenu UI which
+ // is opened when mouse cursor is moved over a link (i.e., it's a case that
+ // users cannot stay in the page after tapping it). So, this improves
+ // accessibility in websites which are designed for desktop.
+ // Note that we don't need to check whether mousemove event is consumed or
+ // not because Chrome also ignores the result.
+ APZCCallbackHelper::DispatchSynthesizedMouseEvent(
+ eMouseMove, aPoint * aScale, aModifiers, 0 /* clickCount */, aWidget);
+
+ // Converting the modifiers to DOM format for the DispatchMouseEvent call
+ // is the most useless thing ever because nsDOMWindowUtils::SendMouseEvent
+ // just converts them back to widget format, but that API has many callers,
+ // including in JS code, so it's not trivial to change.
+ CSSPoint point = CSSPoint::FromAppUnits(
+ ViewportUtils::VisualToLayout(CSSPoint::ToAppUnits(aPoint), aPresShell));
+ PreventDefaultResult preventDefaultResult =
+ APZCCallbackHelper::DispatchMouseEvent(
+ aPresShell, u"contextmenu"_ns, point, 2, 1,
+ WidgetModifiersToDOMModifiers(aModifiers),
+ dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH,
+ 0 /* Use the default value here. */);
+
+ APZES_LOG("Contextmenu event %s\n", ToString(preventDefaultResult).c_str());
+ if (preventDefaultResult != PreventDefaultResult::No) {
+ // If the contextmenu event was handled then we're showing a contextmenu,
+ // and so we should remove any activation
+ mActiveElementManager->ClearActivation();
+#ifndef XP_WIN
+ } else {
+ // If the contextmenu wasn't consumed, fire the eMouseLongTap event.
+ nsEventStatus status = APZCCallbackHelper::DispatchSynthesizedMouseEvent(
+ eMouseLongTap, aPoint * aScale, aModifiers,
+ /*clickCount*/ 1, aWidget);
+ APZES_LOG("eMouseLongTap event %s\n", ToString(status).c_str());
+#endif
+ }
+
+ return preventDefaultResult;
+}
+
+void APZEventState::ProcessLongTap(PresShell* aPresShell,
+ const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers,
+ uint64_t aInputBlockId) {
+ APZES_LOG("Handling long tap at %s block id %" PRIu64 "\n",
+ ToString(aPoint).c_str(), aInputBlockId);
+
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ if (!widget) {
+ return;
+ }
+
+ // If the touch block is waiting for a content response, send one now.
+ // Bug 1848736: Why is a content response needed here? Can it be removed?
+ // However, do not clear |mPendingTouchPreventedResponse|, because APZ will
+ // wait for an additional content response before processing touch-move
+ // events (since the first touch-move could still be prevented, and that
+ // should prevent the touch block from being processed).
+ if (mPendingTouchPreventedResponse) {
+ APZES_LOG("Sending response %d for pending guid: %s block id: %" PRIu64
+ " due to long tap\n",
+ false, ToString(mPendingTouchPreventedGuid).c_str(),
+ mPendingTouchPreventedBlockId);
+ mContentReceivedInputBlockCallback(mPendingTouchPreventedBlockId, false);
+ }
+
+#ifdef XP_WIN
+ // On Windows, we fire the contextmenu events when the user lifts their
+ // finger, in keeping with the platform convention. This happens in the
+ // ProcessLongTapUp function. However, we still fire the eMouseLongTap event
+ // at this time, because things like text selection or dragging may want
+ // to know about it.
+ APZCCallbackHelper::DispatchSynthesizedMouseEvent(
+ eMouseLongTap, aPoint * aScale, aModifiers, /*clickCount*/ 1, widget);
+#else
+ PreventDefaultResult preventDefaultResult =
+ FireContextmenuEvents(aPresShell, aPoint, aScale, aModifiers, widget);
+#endif
+
+ const bool contextmenuOpen =
+#ifdef XP_WIN
+ // On Windows context menu will never be opened by long tap events, the
+ // menu will open after the user lifts their finger.
+ false;
+#elif defined(MOZ_WIDGET_ANDROID)
+ // On Android, GeckoView calls preventDefault() in a JSActor
+ // (ContentDelegateChild.jsm) when opening context menu so that we can
+ // tell whether contextmenu opens in response to the contextmenu event by
+ // checking where preventDefault() got called.
+ preventDefaultResult == PreventDefaultResult::ByChrome;
+#else
+ // On desktop platforms (other than Windows) unlike Android, context menu
+ // can be opened anywhere even if, for example, there's no link under the
+ // touch point. So we can assume that "not preventDefault" means a context
+ // menu is open.
+ preventDefaultResult == PreventDefaultResult::No;
+#endif
+ // Assuming that contextmenuOpen=true here means a context menu was opened, it
+ // will be treated as "preventDefaulted" in APZ.
+ mContentReceivedInputBlockCallback(aInputBlockId, contextmenuOpen);
+
+ if (contextmenuOpen) {
+ // Also send a touchcancel to content
+ // a) on Android if browser's contextmenu is open
+ // b) on desktop platforms other than Windows if browser's contextmenu is
+ // open
+ // so that listeners that might be waiting for a touchend don't trigger.
+ WidgetTouchEvent cancelTouchEvent(true, eTouchCancel, widget.get());
+ cancelTouchEvent.mModifiers = aModifiers;
+ auto ldPoint = LayoutDeviceIntPoint::Round(aPoint * aScale);
+ cancelTouchEvent.mTouches.AppendElement(new mozilla::dom::Touch(
+ mLastTouchIdentifier, ldPoint, LayoutDeviceIntPoint(), 0, 0));
+ APZCCallbackHelper::DispatchWidgetEvent(cancelTouchEvent);
+ }
+}
+
+void APZEventState::ProcessLongTapUp(PresShell* aPresShell,
+ const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers) {
+#ifdef XP_WIN
+ nsCOMPtr<nsIWidget> widget = GetWidget();
+ if (widget) {
+ FireContextmenuEvents(aPresShell, aPoint, aScale, aModifiers, widget);
+ }
+#endif
+}
+
+void APZEventState::ProcessTouchEvent(
+ const WidgetTouchEvent& aEvent, const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId, nsEventStatus aApzResponse,
+ nsEventStatus aContentResponse,
+ nsTArray<TouchBehaviorFlags>&& aAllowedTouchBehaviors) {
+ if (aEvent.mMessage == eTouchStart && aEvent.mTouches.Length() > 0) {
+ mActiveElementManager->SetTargetElement(
+ aEvent.mTouches[0]->GetOriginalTarget());
+ mLastTouchIdentifier = aEvent.mTouches[0]->Identifier();
+ }
+ if (aEvent.mMessage == eTouchStart) {
+ // We get the allowed touch behaviors on a touchstart, but may not actually
+ // use them until the first touchmove, so we stash them in a member
+ // variable.
+ mTouchBlockAllowedBehaviors = std::move(aAllowedTouchBehaviors);
+ }
+
+ bool isTouchPrevented = aContentResponse == nsEventStatus_eConsumeNoDefault;
+ bool mayNeedPointerCancelEvent = false;
+ APZES_LOG("Handling event type %d isPrevented=%d\n", aEvent.mMessage,
+ isTouchPrevented);
+ switch (aEvent.mMessage) {
+ case eTouchStart: {
+ mTouchEndCancelled = false;
+ mReceivedNonTouchStart = false;
+ mTouchRollup = do_GetWeakReference(widget::nsAutoRollup::GetLastRollup());
+
+ SendPendingTouchPreventedResponse(false);
+ // The above call may have sent a message to APZ if we get two
+ // TOUCH_STARTs in a row and just responded to the first one.
+
+ // We're about to send a response back to APZ, but we should only do it
+ // for events that went through APZ (which should be all of them).
+ MOZ_ASSERT(aEvent.mFlags.mHandledByAPZ);
+
+ // If the first touchstart event was preventDefaulted, ensure that any
+ // subsequent additional touchstart events also get preventDefaulted. This
+ // ensures that e.g. pinch zooming is prevented even if just the first
+ // touchstart was prevented by content.
+ if (mTouchCounter.GetActiveTouchCount() == 0) {
+ mFirstTouchCancelled = isTouchPrevented;
+ } else {
+ if (mFirstTouchCancelled && !isTouchPrevented) {
+ APZES_LOG(
+ "Propagating prevent-default from first-touch for block %" PRIu64
+ "\n",
+ aInputBlockId);
+ }
+ isTouchPrevented |= mFirstTouchCancelled;
+ }
+
+ mTouchStartPrevented = isTouchPrevented;
+ if (isTouchPrevented) {
+ mContentReceivedInputBlockCallback(aInputBlockId, isTouchPrevented);
+ } else {
+ APZES_LOG("Event not prevented; pending response for %" PRIu64 " %s\n",
+ aInputBlockId, ToString(aGuid).c_str());
+ mPendingTouchPreventedResponse = true;
+ mPendingTouchPreventedGuid = aGuid;
+ mPendingTouchPreventedBlockId = aInputBlockId;
+ }
+ break;
+ }
+
+ case eTouchEnd:
+ if (isTouchPrevented) {
+ mTouchEndCancelled = true;
+ mEndTouchIsClick = false;
+ }
+ [[fallthrough]];
+ case eTouchCancel:
+ mActiveElementManager->HandleTouchEndEvent(mEndTouchIsClick);
+ [[fallthrough]];
+ case eTouchMove: {
+ if (!mReceivedNonTouchStart) {
+ // In the case where `touchstart` was preventDefaulted,
+ // pointercancel event should NOT be fired.
+ mayNeedPointerCancelEvent = !isTouchPrevented && !mTouchStartPrevented;
+ mReceivedNonTouchStart = true;
+ }
+
+ if (mPendingTouchPreventedResponse) {
+ MOZ_ASSERT(aGuid == mPendingTouchPreventedGuid);
+ if (aEvent.mMessage == eTouchCancel) {
+ // If we received a touch-cancel and we were waiting for the
+ // first touch-move to send a content response, make the content
+ // response be preventDefault=true. This is the safer choice
+ // because content might have prevented the first touch-move,
+ // and even though the touch-cancel means any subsequent touch-moves
+ // will not be processed, the content response still influences
+ // the InputResult sent to GeckoView.
+ isTouchPrevented = true;
+ }
+ mContentReceivedInputBlockCallback(aInputBlockId, isTouchPrevented);
+ mPendingTouchPreventedResponse = false;
+ }
+ break;
+ }
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown touch event type");
+ break;
+ }
+
+ mTouchCounter.Update(aEvent);
+ if (mTouchCounter.GetActiveTouchCount() == 0) {
+ mFirstTouchCancelled = false;
+ }
+
+ APZES_LOG("Pointercancel if %d %d %d %d\n", mayNeedPointerCancelEvent,
+ !isTouchPrevented, aApzResponse == nsEventStatus_eConsumeDoDefault,
+ MainThreadAgreesEventsAreConsumableByAPZ());
+ // From https://w3c.github.io/pointerevents/#the-pointercancel-event;
+ // The user agent MUST fire a pointer event named pointercancel when it
+ // detects a scenario to suppress a pointer event stream.
+ //
+ // And "suppress a pointer event steam" is defined in
+ // https://w3c.github.io/pointerevents/#suppressing-a-pointer-event-stream .
+ //
+ // There are four scenarios when the user agent fires a pointercancel event in
+ // the spec. Below code corresponds to one of the scenarios (the third bullet
+ // point);
+ // The pointer is subsequently used by the user agent to manipulate the page
+ // viewport (e.g. panning or zooming).
+ if (mayNeedPointerCancelEvent &&
+ aApzResponse == nsEventStatus_eConsumeDoDefault &&
+ MainThreadAgreesEventsAreConsumableByAPZ()) {
+ WidgetTouchEvent cancelEvent(aEvent);
+ cancelEvent.mMessage = eTouchPointerCancel;
+ cancelEvent.mFlags.mCancelable = false; // mMessage != eTouchCancel;
+ for (uint32_t i = 0; i < cancelEvent.mTouches.Length(); ++i) {
+ if (mozilla::dom::Touch* touch = cancelEvent.mTouches[i]) {
+ touch->convertToPointer = true;
+ }
+ }
+ nsEventStatus status;
+ cancelEvent.mWidget->DispatchEvent(&cancelEvent, status);
+ }
+}
+
+bool APZEventState::MainThreadAgreesEventsAreConsumableByAPZ() const {
+ // APZ errs on the side of saying it can consume touch events to perform
+ // default user-agent behaviours. In particular it may say this if it hasn't
+ // received accurate touch-action information. Here we double-check using
+ // accurate touch-action information. This code is kinda-sorta the main
+ // thread equivalent of AsyncPanZoomController::ArePointerEventsConsumable().
+
+ switch (mTouchBlockAllowedBehaviors.Length()) {
+ case 0:
+ // If we don't have any touch-action (e.g. because it is disabled) then
+ // APZ has no restrictions.
+ return true;
+
+ case 1: {
+ // If there's one touch point in this touch block, then check the pan-x
+ // and pan-y flags. If neither is allowed, then we disagree with APZ and
+ // say that it can't do anything with this touch block. Note that it would
+ // be even better if we could check the allowed scroll directions of the
+ // scrollframe at this point and refine this further.
+ TouchBehaviorFlags flags = mTouchBlockAllowedBehaviors[0];
+ return (flags & AllowedTouchBehavior::HORIZONTAL_PAN) ||
+ (flags & AllowedTouchBehavior::VERTICAL_PAN);
+ }
+
+ case 2: {
+ // If there's two touch points in this touch block, check that they both
+ // allow zooming.
+ for (const auto& allowed : mTouchBlockAllowedBehaviors) {
+ if (!(allowed & AllowedTouchBehavior::PINCH_ZOOM)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ default:
+ // More than two touch points? APZ shouldn't be doing anything with this,
+ // so APZ shouldn't be consuming them.
+ return false;
+ }
+}
+
+void APZEventState::ProcessWheelEvent(const WidgetWheelEvent& aEvent,
+ uint64_t aInputBlockId) {
+ // If this event starts a swipe, indicate that it shouldn't result in a
+ // scroll by setting defaultPrevented to true.
+ bool defaultPrevented = aEvent.DefaultPrevented() || aEvent.TriggersSwipe();
+ mContentReceivedInputBlockCallback(aInputBlockId, defaultPrevented);
+}
+
+void APZEventState::ProcessMouseEvent(const WidgetMouseEvent& aEvent,
+ uint64_t aInputBlockId) {
+ bool defaultPrevented = false;
+ mContentReceivedInputBlockCallback(aInputBlockId, defaultPrevented);
+}
+
+void APZEventState::ProcessAPZStateChange(ViewID aViewId,
+ APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) {
+ switch (aChange) {
+ case APZStateChange::eTransformBegin: {
+ nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aViewId);
+ if (sf) {
+ sf->SetTransformingByAPZ(true);
+ sf->ScrollbarActivityStarted();
+ }
+
+ nsIContent* content = nsLayoutUtils::FindContentFor(aViewId);
+ dom::Document* doc = content ? content->GetComposedDoc() : nullptr;
+ nsCOMPtr<nsIDocShell> docshell(doc ? doc->GetDocShell() : nullptr);
+ if (docshell && sf) {
+ nsDocShell* nsdocshell = static_cast<nsDocShell*>(docshell.get());
+ nsdocshell->NotifyAsyncPanZoomStarted();
+ }
+ break;
+ }
+ case APZStateChange::eTransformEnd: {
+ nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aViewId);
+ if (sf) {
+ sf->SetTransformingByAPZ(false);
+ sf->ScrollbarActivityStopped();
+ }
+
+ nsIContent* content = nsLayoutUtils::FindContentFor(aViewId);
+ dom::Document* doc = content ? content->GetComposedDoc() : nullptr;
+ nsCOMPtr<nsIDocShell> docshell(doc ? doc->GetDocShell() : nullptr);
+ if (docshell && sf) {
+ nsDocShell* nsdocshell = static_cast<nsDocShell*>(docshell.get());
+ nsdocshell->NotifyAsyncPanZoomStopped();
+ }
+ break;
+ }
+ case APZStateChange::eStartTouch: {
+ bool canBePan = aArg;
+ mActiveElementManager->HandleTouchStart(canBePan);
+ // If this is a non-scrollable content, set a timer for the amount of
+ // time specified by ui.touch_activation.duration_ms to clear the
+ // active element state.
+ APZES_LOG("%s: can-be-pan=%d", __FUNCTION__, aArg);
+ if (!canBePan) {
+ MOZ_ASSERT(aInputBlockId.isSome());
+ }
+ break;
+ }
+ case APZStateChange::eStartPanning: {
+ // The user started to pan, so we don't want anything to be :active.
+ mActiveElementManager->ClearActivation();
+ break;
+ }
+ case APZStateChange::eEndTouch: {
+ mEndTouchIsClick = aArg;
+ mActiveElementManager->HandleTouchEnd();
+ break;
+ }
+ }
+}
+
+void APZEventState::Destroy() { mActiveElementManager->Destroy(); }
+
+void APZEventState::SendPendingTouchPreventedResponse(bool aPreventDefault) {
+ if (mPendingTouchPreventedResponse) {
+ APZES_LOG("Sending response %d for pending guid: %s block id: %" PRIu64
+ "\n",
+ aPreventDefault, ToString(mPendingTouchPreventedGuid).c_str(),
+ mPendingTouchPreventedBlockId);
+ mContentReceivedInputBlockCallback(mPendingTouchPreventedBlockId,
+ aPreventDefault);
+ mPendingTouchPreventedResponse = false;
+ }
+}
+
+already_AddRefed<nsIWidget> APZEventState::GetWidget() const {
+ nsCOMPtr<nsIWidget> result = do_QueryReferent(mWidget);
+ return result.forget();
+}
+
+already_AddRefed<nsIContent> APZEventState::GetTouchRollup() const {
+ nsCOMPtr<nsIContent> result = do_QueryReferent(mTouchRollup);
+ return result.forget();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/APZEventState.h b/gfx/layers/apz/util/APZEventState.h
new file mode 100644
index 0000000000..52febc0424
--- /dev/null
+++ b/gfx/layers/apz/util/APZEventState.h
@@ -0,0 +1,141 @@
+/* -*- 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/. */
+
+#ifndef mozilla_layers_APZEventState_h
+#define mozilla_layers_APZEventState_h
+
+#include <stdint.h>
+
+#include "ActiveElementManager.h"
+#include "Units.h"
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/GeckoContentControllerTypes.h" // for APZStateChange
+#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid
+#include "mozilla/layers/TouchCounter.h" // for TouchCounter
+#include "mozilla/RefPtr.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "nsCOMPtr.h"
+#include "nsISupportsImpl.h" // for NS_INLINE_DECL_REFCOUNTING
+#include "nsITimer.h"
+#include "nsIWeakReferenceUtils.h" // for nsWeakPtr
+
+#include <functional>
+#include <unordered_map>
+
+template <class>
+class nsCOMPtr;
+class nsIContent;
+class nsIWidget;
+
+namespace mozilla {
+
+class PresShell;
+enum class PreventDefaultResult : uint8_t;
+
+namespace layers {
+
+class ActiveElementManager;
+
+typedef std::function<void(uint64_t /* input block id */,
+ bool /* prevent default */)>
+ ContentReceivedInputBlockCallback;
+
+/**
+ * A content-side component that keeps track of state for handling APZ
+ * gestures and sending APZ notifications.
+ */
+class APZEventState final {
+ typedef GeckoContentController_APZStateChange APZStateChange;
+ typedef ScrollableLayerGuid::ViewID ViewID;
+
+ public:
+ APZEventState(nsIWidget* aWidget,
+ ContentReceivedInputBlockCallback&& aCallback);
+
+ NS_INLINE_DECL_REFCOUNTING(APZEventState);
+
+ MOZ_CAN_RUN_SCRIPT
+ void ProcessSingleTap(const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers, int32_t aClickCount,
+ uint64_t aInputBlockId);
+ MOZ_CAN_RUN_SCRIPT
+ void ProcessLongTap(PresShell* aPresShell, const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers, uint64_t aInputBlockId);
+ MOZ_CAN_RUN_SCRIPT
+ void ProcessLongTapUp(PresShell* aPresShell, const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale,
+ Modifiers aModifiers);
+ void ProcessTouchEvent(const WidgetTouchEvent& aEvent,
+ const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId, nsEventStatus aApzResponse,
+ nsEventStatus aContentResponse,
+ nsTArray<TouchBehaviorFlags>&& aAllowedTouchBehaviors);
+ void ProcessWheelEvent(const WidgetWheelEvent& aEvent,
+ uint64_t aInputBlockId);
+ void ProcessMouseEvent(const WidgetMouseEvent& aEvent,
+ uint64_t aInputBlockId);
+ void ProcessAPZStateChange(ViewID aViewId, APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId);
+ /**
+ * Cleanup on destroy window.
+ */
+ void Destroy();
+
+ private:
+ ~APZEventState();
+ void SendPendingTouchPreventedResponse(bool aPreventDefault);
+ MOZ_CAN_RUN_SCRIPT
+ PreventDefaultResult FireContextmenuEvents(
+ PresShell* aPresShell, const CSSPoint& aPoint,
+ const CSSToLayoutDeviceScale& aScale, Modifiers aModifiers,
+ const nsCOMPtr<nsIWidget>& aWidget);
+ already_AddRefed<nsIWidget> GetWidget() const;
+ already_AddRefed<nsIContent> GetTouchRollup() const;
+ bool MainThreadAgreesEventsAreConsumableByAPZ() const;
+
+ private:
+ nsWeakPtr mWidget;
+ RefPtr<ActiveElementManager> mActiveElementManager;
+ ContentReceivedInputBlockCallback mContentReceivedInputBlockCallback;
+ TouchCounter mTouchCounter;
+ bool mPendingTouchPreventedResponse;
+ ScrollableLayerGuid mPendingTouchPreventedGuid;
+ uint64_t mPendingTouchPreventedBlockId;
+ bool mEndTouchIsClick;
+ bool mFirstTouchCancelled;
+ bool mTouchEndCancelled;
+ // Set to true when we have received any one of
+ // touch-move/touch-end/touch-cancel events in the touch block being
+ // processed.
+ bool mReceivedNonTouchStart;
+ bool mTouchStartPrevented;
+
+ int32_t mLastTouchIdentifier;
+ nsTArray<TouchBehaviorFlags> mTouchBlockAllowedBehaviors;
+
+ // Because touch-triggered mouse events (e.g. mouse events from a tap
+ // gesture) happen asynchronously from the touch events themselves, we
+ // need to stash and replicate some of the state from the touch events
+ // to the mouse events. One piece of state is the rollup content, which
+ // is the content for which a popup window was recently closed. If we
+ // don't replicate this state properly during the mouse events, the
+ // synthetic click might reopen a popup window that was just closed by
+ // the touch event, which is undesirable. See also documentation in
+ // nsAutoRollup.h
+ // Note that in cases where we get multiple touch blocks interleaved with
+ // their single-tap event notifications, mTouchRollup may hold an incorrect
+ // value. This is kind of an edge case, and falls in the same category of
+ // problems as bug 1227241. I intend that fixing that bug will also take
+ // care of this potential problem.
+ nsWeakPtr mTouchRollup;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_APZEventState_h */
diff --git a/gfx/layers/apz/util/APZTaskRunnable.cpp b/gfx/layers/apz/util/APZTaskRunnable.cpp
new file mode 100644
index 0000000000..6192f385a7
--- /dev/null
+++ b/gfx/layers/apz/util/APZTaskRunnable.cpp
@@ -0,0 +1,142 @@
+/* -*- 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 "APZTaskRunnable.h"
+
+#include "mozilla/PresShell.h"
+#include "nsRefreshDriver.h"
+
+namespace mozilla::layers {
+
+NS_IMETHODIMP
+APZTaskRunnable::Run() {
+ if (!mController) {
+ mRegisteredPresShellId = 0;
+ return NS_OK;
+ }
+
+ // Move these variables first since below RequestContentPaint and
+ // NotifyFlushComplete might spin event loop so that any new incoming requests
+ // will be properly queued and run in the next refresh driver's tick.
+ const bool needsFlushCompleteNotification = mNeedsFlushCompleteNotification;
+ auto requests = std::move(mPendingRepaintRequestQueue);
+ mPendingRepaintRequestMap.clear();
+ mNeedsFlushCompleteNotification = false;
+ mRegisteredPresShellId = 0;
+ RefPtr<GeckoContentController> controller = mController;
+
+ // We need to process pending RepaintRequests first.
+ while (!requests.empty()) {
+ controller->RequestContentRepaint(requests.front());
+ requests.pop_front();
+ }
+
+ if (needsFlushCompleteNotification) {
+ // Then notify "apz-repaints-flushed" so that we can ensure that all pending
+ // scroll position updates have finished when the "apz-repaints-flushed"
+ // arrives.
+ controller->NotifyFlushComplete();
+ }
+
+ return NS_OK;
+}
+
+void APZTaskRunnable::QueueRequest(const RepaintRequest& aRequest) {
+ // If we are in test-controlled refreshes mode, process this |aRequest|
+ // synchronously.
+ if (IsTestControllingRefreshesEnabled()) {
+ // Flush all pending requests and notification just in case the refresh
+ // driver mode was changed before flushing them.
+ RefPtr<GeckoContentController> controller = mController;
+ Run();
+ controller->RequestContentRepaint(aRequest);
+ return;
+ }
+ EnsureRegisterAsEarlyRunner();
+
+ RepaintRequestKey key{aRequest.GetScrollId(), aRequest.GetScrollUpdateType()};
+
+ auto lastDiscardableRequest = mPendingRepaintRequestMap.find(key);
+ // If there's an existing request with the same key, we can discard it and we
+ // push the incoming one into the queue's tail so that we can ensure the order
+ // of processing requests.
+ if (lastDiscardableRequest != mPendingRepaintRequestMap.end()) {
+ for (auto it = mPendingRepaintRequestQueue.begin();
+ it != mPendingRepaintRequestQueue.end(); it++) {
+ if (RepaintRequestKey{it->GetScrollId(), it->GetScrollUpdateType()} ==
+ key) {
+ mPendingRepaintRequestQueue.erase(it);
+ break;
+ }
+ }
+ }
+ mPendingRepaintRequestMap.insert(key);
+ mPendingRepaintRequestQueue.push_back(aRequest);
+}
+
+void APZTaskRunnable::QueueFlushCompleteNotification() {
+ // If we are in test-controlled refreshes mode, notify apz-repaints-flushed
+ // synchronously.
+ if (IsTestControllingRefreshesEnabled()) {
+ // Flush all pending requests and notification just in case the refresh
+ // driver mode was changed before flushing them.
+ RefPtr<GeckoContentController> controller = mController;
+ Run();
+ controller->NotifyFlushComplete();
+ return;
+ }
+
+ EnsureRegisterAsEarlyRunner();
+
+ mNeedsFlushCompleteNotification = true;
+}
+
+bool APZTaskRunnable::IsRegisteredWithCurrentPresShell() const {
+ MOZ_ASSERT(mController);
+
+ uint32_t current = 0;
+ if (PresShell* presShell = mController->GetTopLevelPresShell()) {
+ current = presShell->GetPresShellId();
+ }
+ return mRegisteredPresShellId == current;
+}
+
+void APZTaskRunnable::EnsureRegisterAsEarlyRunner() {
+ if (IsRegisteredWithCurrentPresShell()) {
+ return;
+ }
+
+ // If the registered presshell id has been changed, we need to discard pending
+ // requests and notification since all of them are for documents which
+ // have been torn down.
+ if (mRegisteredPresShellId) {
+ mPendingRepaintRequestMap.clear();
+ mPendingRepaintRequestQueue.clear();
+ mNeedsFlushCompleteNotification = false;
+ }
+
+ if (PresShell* presShell = mController->GetTopLevelPresShell()) {
+ if (nsRefreshDriver* driver = presShell->GetRefreshDriver()) {
+ driver->AddEarlyRunner(this);
+ mRegisteredPresShellId = presShell->GetPresShellId();
+ }
+ }
+}
+
+bool APZTaskRunnable::IsTestControllingRefreshesEnabled() const {
+ if (!mController) {
+ return false;
+ }
+
+ if (PresShell* presShell = mController->GetTopLevelPresShell()) {
+ if (nsRefreshDriver* driver = presShell->GetRefreshDriver()) {
+ return driver->IsTestControllingRefreshesEnabled();
+ }
+ }
+ return false;
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/apz/util/APZTaskRunnable.h b/gfx/layers/apz/util/APZTaskRunnable.h
new file mode 100644
index 0000000000..f5de21abd4
--- /dev/null
+++ b/gfx/layers/apz/util/APZTaskRunnable.h
@@ -0,0 +1,89 @@
+/* -*- 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/. */
+
+#ifndef mozilla_layers_RepaintRequestRunnable_h
+#define mozilla_layers_RepaintRequestRunnable_h
+
+#include <deque>
+#include <unordered_set>
+
+#include "mozilla/layers/GeckoContentController.h"
+#include "mozilla/layers/RepaintRequest.h"
+#include "mozilla/layers/ScrollableLayerGuid.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+class GeckoContentController;
+
+// A runnable invoked in nsRefreshDriver::Tick as an early runnable.
+class APZTaskRunnable final : public Runnable {
+ public:
+ explicit APZTaskRunnable(GeckoContentController* aController)
+ : Runnable("RepaintRequestRunnable"),
+ mController(aController),
+ mRegisteredPresShellId(0),
+ mNeedsFlushCompleteNotification(false) {}
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_DECL_NSIRUNNABLE
+
+ // Queue a RepaintRequest.
+ // If there's already a RepaintRequest having the same scroll id, the old
+ // one will be discarded.
+ void
+ QueueRequest(const RepaintRequest& aRequest);
+ void QueueFlushCompleteNotification();
+ void Revoke() {
+ mController = nullptr;
+ mRegisteredPresShellId = 0;
+ }
+
+ private:
+ void EnsureRegisterAsEarlyRunner();
+ bool IsRegisteredWithCurrentPresShell() const;
+ bool IsTestControllingRefreshesEnabled() const;
+
+ // Use a GeckoContentController raw pointer here since the owner of the
+ // GeckoContentController instance (an APZChild instance) holds a strong
+ // reference of this APZTaskRunnable instance and will call Revoke() before
+ // the GeckoContentController gets destroyed in the dtor of the APZChild
+ // instance.
+ GeckoContentController* mController;
+
+ struct RepaintRequestKey {
+ ScrollableLayerGuid::ViewID mScrollId;
+ RepaintRequest::ScrollOffsetUpdateType mScrollUpdateType;
+ bool operator==(const RepaintRequestKey& aOther) const {
+ return mScrollId == aOther.mScrollId &&
+ mScrollUpdateType == aOther.mScrollUpdateType;
+ }
+ struct HashFn {
+ std::size_t operator()(const RepaintRequestKey& aKey) const {
+ return HashGeneric(aKey.mScrollId, aKey.mScrollUpdateType);
+ }
+ };
+ };
+ using RepaintRequests =
+ std::unordered_set<RepaintRequestKey, RepaintRequestKey::HashFn>;
+ // We have an unordered_map and a deque for pending RepaintRequests. The
+ // unordered_map is for quick lookup and the deque is for processing the
+ // pending RepaintRequests in the order we queued.
+ RepaintRequests mPendingRepaintRequestMap;
+ std::deque<RepaintRequest> mPendingRepaintRequestQueue;
+ // This APZTaskRunnable instance is per APZChild instance, which means its
+ // lifetime is tied to the APZChild instance, thus this APZTaskRunnable
+ // instance will be (re-)used for different pres shells so we'd need to
+ // have to remember the pres shell which is currently tied to the APZChild
+ // to deliver queued requests and notifications to the proper pres shell.
+ uint32_t mRegisteredPresShellId;
+ bool mNeedsFlushCompleteNotification;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_RepaintRequestRunnable_h
diff --git a/gfx/layers/apz/util/APZThreadUtils.cpp b/gfx/layers/apz/util/APZThreadUtils.cpp
new file mode 100644
index 0000000000..d3bf43e61d
--- /dev/null
+++ b/gfx/layers/apz/util/APZThreadUtils.cpp
@@ -0,0 +1,119 @@
+/* -*- 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 "APZThreadUtils.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ProfilerRunnable.h"
+#include "mozilla/StaticMutex.h"
+
+#include "nsISerialEventTarget.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+namespace layers {
+
+static bool sThreadAssertionsEnabled = true;
+static StaticRefPtr<nsISerialEventTarget> sControllerThread;
+static StaticMutex sControllerThreadMutex MOZ_UNANNOTATED;
+
+/*static*/
+void APZThreadUtils::SetThreadAssertionsEnabled(bool aEnabled) {
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ sThreadAssertionsEnabled = aEnabled;
+}
+
+/*static*/
+bool APZThreadUtils::GetThreadAssertionsEnabled() {
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ return sThreadAssertionsEnabled;
+}
+
+/*static*/
+void APZThreadUtils::SetControllerThread(nsISerialEventTarget* aThread) {
+ // We must either be setting the initial controller thread, or removing it,
+ // or re-using an existing controller thread.
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ MOZ_ASSERT(!sControllerThread || !aThread || sControllerThread == aThread);
+ if (aThread != sControllerThread) {
+ // This can only happen once, on startup.
+ sControllerThread = aThread;
+ ClearOnShutdown(&sControllerThread);
+ }
+}
+
+/*static*/
+void APZThreadUtils::AssertOnControllerThread() {
+#if DEBUG
+ if (!GetThreadAssertionsEnabled()) {
+ return;
+ }
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ MOZ_ASSERT(sControllerThread && sControllerThread->IsOnCurrentThread());
+#endif
+}
+
+/*static*/
+void APZThreadUtils::RunOnControllerThread(RefPtr<Runnable>&& aTask,
+ uint32_t flags) {
+ RefPtr<nsISerialEventTarget> thread;
+ {
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ thread = sControllerThread;
+ }
+ RefPtr<Runnable> task = std::move(aTask);
+
+ if (!thread) {
+ // Could happen on startup or if Shutdown() got called.
+ NS_WARNING("Dropping task posted to controller thread");
+ return;
+ }
+
+ if (thread->IsOnCurrentThread()) {
+ AUTO_PROFILE_FOLLOWING_RUNNABLE(task);
+ task->Run();
+ } else {
+ thread->Dispatch(task.forget(), flags);
+ }
+}
+
+/*static*/
+bool APZThreadUtils::IsControllerThread() {
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ return sControllerThread && sControllerThread->IsOnCurrentThread();
+}
+
+/*static*/
+bool APZThreadUtils::IsControllerThreadAlive() {
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ return !!sControllerThread;
+}
+
+/*static*/
+void APZThreadUtils::DelayedDispatch(already_AddRefed<Runnable> aRunnable,
+ int aDelayMs) {
+ MOZ_ASSERT(!XRE_IsContentProcess(),
+ "ContentProcessController should only be used remotely.");
+ RefPtr<nsISerialEventTarget> thread;
+ {
+ StaticMutexAutoLock lock(sControllerThreadMutex);
+ thread = sControllerThread;
+ }
+ if (!thread) {
+ // Could happen on startup
+ NS_WARNING("Dropping task posted to controller thread");
+ return;
+ }
+ if (aDelayMs) {
+ thread->DelayedDispatch(std::move(aRunnable), aDelayMs);
+ } else {
+ thread->Dispatch(std::move(aRunnable));
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/APZThreadUtils.h b/gfx/layers/apz/util/APZThreadUtils.h
new file mode 100644
index 0000000000..f1560eee8c
--- /dev/null
+++ b/gfx/layers/apz/util/APZThreadUtils.h
@@ -0,0 +1,75 @@
+/* -*- 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/. */
+
+#ifndef mozilla_layers_APZThreadUtils_h
+#define mozilla_layers_APZThreadUtils_h
+
+#include "nsIEventTarget.h"
+#include "nsINamed.h"
+#include "nsITimer.h"
+#include "nsString.h"
+
+class nsISerialEventTarget;
+
+namespace mozilla {
+
+class Runnable;
+
+namespace layers {
+
+class APZThreadUtils {
+ public:
+ /**
+ * In the gtest environment everything runs on one thread, so we
+ * shouldn't assert that we're on a particular thread. This enables
+ * that behaviour.
+ */
+ static void SetThreadAssertionsEnabled(bool aEnabled);
+ static bool GetThreadAssertionsEnabled();
+
+ /**
+ * Set the controller thread.
+ */
+ static void SetControllerThread(nsISerialEventTarget* aThread);
+
+ /**
+ * This can be used to assert that the current thread is the
+ * controller/UI thread (on which input events are received).
+ * This does nothing if thread assertions are disabled.
+ */
+ static void AssertOnControllerThread();
+
+ /**
+ * Run the given task on the APZ "controller thread" for this platform. If
+ * this function is called from the controller thread itself then the task is
+ * run immediately without getting queued.
+ */
+ static void RunOnControllerThread(
+ RefPtr<Runnable>&& aTask,
+ uint32_t flags = nsIEventTarget::DISPATCH_NORMAL);
+
+ /**
+ * Returns true if currently on APZ "controller thread".
+ */
+ static bool IsControllerThread();
+
+ /**
+ * Returns true if the controller thread is still alive.
+ */
+ static bool IsControllerThreadAlive();
+
+ /**
+ * Schedules a runnable to run on the controller thread at some time
+ * in the future.
+ */
+ static void DelayedDispatch(already_AddRefed<Runnable> aRunnable,
+ int aDelayMs);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_APZThreadUtils_h */
diff --git a/gfx/layers/apz/util/ActiveElementManager.cpp b/gfx/layers/apz/util/ActiveElementManager.cpp
new file mode 100644
index 0000000000..f2d981e9f0
--- /dev/null
+++ b/gfx/layers/apz/util/ActiveElementManager.cpp
@@ -0,0 +1,324 @@
+/* -*- 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 "ActiveElementManager.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Document.h"
+#include "nsITimer.h"
+
+static mozilla::LazyLogModule sApzAemLog("apz.activeelement");
+#define AEM_LOG(...) MOZ_LOG(sApzAemLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+namespace layers {
+
+class DelayedClearElementActivation final : public nsITimerCallback,
+ public nsINamed {
+ private:
+ explicit DelayedClearElementActivation(nsCOMPtr<dom::Element>& aTarget,
+ const nsCOMPtr<nsITimer>& aTimer)
+ : mTarget(aTarget)
+ // Hold the reference count until we are called back.
+ ,
+ mTimer(aTimer),
+ mProcessedSingleTap(false) {}
+
+ public:
+ NS_DECL_ISUPPORTS
+
+ static RefPtr<DelayedClearElementActivation> Create(
+ nsCOMPtr<dom::Element>& aTarget);
+
+ NS_IMETHOD Notify(nsITimer*) override;
+
+ NS_IMETHOD GetName(nsACString& aName) override;
+
+ void MarkSingleTapProcessed();
+
+ bool ProcessedSingleTap() const { return mProcessedSingleTap; }
+
+ void StartTimer();
+
+ /**
+ * Clear the Event State Manager's global active content.
+ */
+ void ClearGlobalActiveContent();
+
+ void ClearTimer() {
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ }
+
+ private:
+ ~DelayedClearElementActivation() = default;
+
+ nsCOMPtr<dom::Element> mTarget;
+ nsCOMPtr<nsITimer> mTimer;
+ bool mProcessedSingleTap;
+};
+
+static nsPresContext* GetPresContextFor(nsIContent* aContent) {
+ if (!aContent) {
+ return nullptr;
+ }
+ PresShell* presShell = aContent->OwnerDoc()->GetPresShell();
+ if (!presShell) {
+ return nullptr;
+ }
+ return presShell->GetPresContext();
+}
+
+RefPtr<DelayedClearElementActivation> DelayedClearElementActivation::Create(
+ nsCOMPtr<dom::Element>& aTarget) {
+ nsCOMPtr<nsITimer> timer = NS_NewTimer();
+ if (!timer) {
+ return nullptr;
+ }
+ RefPtr<DelayedClearElementActivation> event =
+ new DelayedClearElementActivation(aTarget, timer);
+ return event;
+}
+
+NS_IMETHODIMP DelayedClearElementActivation::Notify(nsITimer*) {
+ AEM_LOG("DelayedClearElementActivation notification ready=%d",
+ mProcessedSingleTap);
+ // If the single tap has been processed and the timer has expired,
+ // clear the active element state.
+ if (mProcessedSingleTap) {
+ AEM_LOG("DelayedClearElementActivation clearing active content\n");
+ ClearGlobalActiveContent();
+ }
+ mTimer = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP DelayedClearElementActivation::GetName(nsACString& aName) {
+ aName.AssignLiteral("DelayedClearElementActivation");
+ return NS_OK;
+}
+
+void DelayedClearElementActivation::StartTimer() {
+ MOZ_ASSERT(mTimer);
+ // If the timer is null, active content state has already been cleared.
+ if (!mTimer) {
+ return;
+ }
+ nsresult rv = mTimer->InitWithCallback(
+ this, StaticPrefs::ui_touch_activation_duration_ms(),
+ nsITimer::TYPE_ONE_SHOT);
+ if (NS_FAILED(rv)) {
+ ClearTimer();
+ }
+}
+
+void DelayedClearElementActivation::MarkSingleTapProcessed() {
+ mProcessedSingleTap = true;
+ if (!mTimer) {
+ AEM_LOG("Clear activation immediate!");
+ ClearGlobalActiveContent();
+ }
+}
+
+void DelayedClearElementActivation::ClearGlobalActiveContent() {
+ if (nsPresContext* pc = GetPresContextFor(mTarget)) {
+ EventStateManager::ClearGlobalActiveContent(pc->EventStateManager());
+ }
+ mTarget = nullptr;
+}
+
+NS_IMPL_ISUPPORTS(DelayedClearElementActivation, nsITimerCallback, nsINamed)
+
+ActiveElementManager::ActiveElementManager()
+ : mCanBePan(false), mCanBePanSet(false), mSetActiveTask(nullptr) {}
+
+ActiveElementManager::~ActiveElementManager() = default;
+
+void ActiveElementManager::SetTargetElement(dom::EventTarget* aTarget) {
+ if (mTarget) {
+ // Multiple fingers on screen (since HandleTouchEnd clears mTarget).
+ AEM_LOG("Multiple fingers on-screen, clearing target element\n");
+ CancelTask();
+ ResetActive();
+ ResetTouchBlockState();
+ return;
+ }
+
+ mTarget = dom::Element::FromEventTargetOrNull(aTarget);
+ AEM_LOG("Setting target element to %p\n", mTarget.get());
+ TriggerElementActivation();
+}
+
+void ActiveElementManager::HandleTouchStart(bool aCanBePan) {
+ AEM_LOG("Touch start, aCanBePan: %d\n", aCanBePan);
+ if (mCanBePanSet) {
+ // Multiple fingers on screen (since HandleTouchEnd clears mCanBePanSet).
+ AEM_LOG("Multiple fingers on-screen, clearing touch block state\n");
+ CancelTask();
+ ResetActive();
+ ResetTouchBlockState();
+ return;
+ }
+
+ mCanBePan = aCanBePan;
+ mCanBePanSet = true;
+ TriggerElementActivation();
+}
+
+void ActiveElementManager::TriggerElementActivation() {
+ // Both HandleTouchStart() and SetTargetElement() call this. They can be
+ // called in either order. One will set mCanBePanSet, and the other, mTarget.
+ // We want to actually trigger the activation once both are set.
+ if (!(mTarget && mCanBePanSet)) {
+ return;
+ }
+
+ RefPtr<DelayedClearElementActivation> delayedEvent =
+ DelayedClearElementActivation::Create(mTarget);
+ if (mDelayedClearElementActivation) {
+ mDelayedClearElementActivation->ClearTimer();
+ mDelayedClearElementActivation->ClearGlobalActiveContent();
+ }
+ mDelayedClearElementActivation = delayedEvent;
+
+ // If the touch cannot be a pan, make mTarget :active right away.
+ // Otherwise, wait a bit to see if the user will pan or not.
+ if (!mCanBePan) {
+ SetActive(mTarget);
+
+ if (mDelayedClearElementActivation) {
+ mDelayedClearElementActivation->StartTimer();
+ }
+ } else {
+ CancelTask(); // this is only needed because of bug 1169802. Fixing that
+ // bug properly should make this unnecessary.
+ MOZ_ASSERT(mSetActiveTask == nullptr);
+
+ RefPtr<CancelableRunnable> task =
+ NewCancelableRunnableMethod<nsCOMPtr<dom::Element>>(
+ "layers::ActiveElementManager::SetActiveTask", this,
+ &ActiveElementManager::SetActiveTask, mTarget);
+ mSetActiveTask = task;
+ NS_GetCurrentThread()->DelayedDispatch(
+ task.forget(), StaticPrefs::ui_touch_activation_delay_ms());
+ AEM_LOG("Scheduling mSetActiveTask %p\n", mSetActiveTask.get());
+ }
+}
+
+void ActiveElementManager::ClearActivation() {
+ AEM_LOG("Clearing element activation\n");
+ CancelTask();
+ ResetActive();
+}
+
+void ActiveElementManager::HandleTouchEndEvent(bool aWasClick) {
+ AEM_LOG("Touch end event, aWasClick: %d\n", aWasClick);
+
+ // If the touch was a click, make mTarget :active right away.
+ // nsEventStateManager will reset the active element when processing
+ // the mouse-down event generated by the click.
+ CancelTask();
+ if (aWasClick) {
+ // Scrollbar thumbs use a different mechanism for their active
+ // highlight (the "active" attribute), so don't set the active state
+ // on them because nothing will clear it.
+ if (!(mTarget && mTarget->IsXULElement(nsGkAtoms::thumb))) {
+ SetActive(mTarget);
+ }
+ } else {
+ // We might reach here if mCanBePan was false on touch-start and
+ // so we set the element active right away. Now it turns out the
+ // action was not a click so we need to reset the active element.
+ ResetActive();
+ }
+
+ ResetTouchBlockState();
+}
+
+void ActiveElementManager::HandleTouchEnd() {
+ AEM_LOG("Touch end, clearing pan state\n");
+ mCanBePanSet = false;
+}
+
+void ActiveElementManager::ProcessSingleTap() {
+ if (!mDelayedClearElementActivation) {
+ return;
+ }
+
+ mDelayedClearElementActivation->MarkSingleTapProcessed();
+
+ if (mCanBePan) {
+ // In the case that we have not started the delayed reset of the element
+ // activation state, start the timer now.
+ mDelayedClearElementActivation->StartTimer();
+ }
+
+ // We don't need to keep a reference to the element activation
+ // clearing, because the event and its timer keep each other alive
+ // until the timer expires
+ mDelayedClearElementActivation = nullptr;
+}
+
+void ActiveElementManager::Destroy() {
+ if (mDelayedClearElementActivation) {
+ mDelayedClearElementActivation->ClearTimer();
+ mDelayedClearElementActivation = nullptr;
+ }
+}
+
+void ActiveElementManager::SetActive(dom::Element* aTarget) {
+ AEM_LOG("Setting active %p\n", aTarget);
+
+ if (nsPresContext* pc = GetPresContextFor(aTarget)) {
+ pc->EventStateManager()->SetContentState(aTarget,
+ dom::ElementState::ACTIVE);
+ }
+}
+
+void ActiveElementManager::ResetActive() {
+ AEM_LOG("Resetting active from %p\n", mTarget.get());
+
+ // Clear the :active flag from mTarget by setting it on the document root.
+ if (mTarget) {
+ dom::Element* root = mTarget->OwnerDoc()->GetDocumentElement();
+ if (root) {
+ AEM_LOG("Found root %p, making active\n", root);
+ SetActive(root);
+ }
+ }
+}
+
+void ActiveElementManager::ResetTouchBlockState() {
+ mTarget = nullptr;
+ mCanBePanSet = false;
+}
+
+void ActiveElementManager::SetActiveTask(
+ const nsCOMPtr<dom::Element>& aTarget) {
+ AEM_LOG("mSetActiveTask %p running\n", mSetActiveTask.get());
+
+ // This gets called from mSetActiveTask's Run() method. The message loop
+ // deletes the task right after running it, so we need to null out
+ // mSetActiveTask to make sure we're not left with a dangling pointer.
+ mSetActiveTask = nullptr;
+ SetActive(aTarget);
+}
+
+void ActiveElementManager::CancelTask() {
+ AEM_LOG("Cancelling task %p\n", mSetActiveTask.get());
+
+ if (mSetActiveTask) {
+ mSetActiveTask->Cancel();
+ mSetActiveTask = nullptr;
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/ActiveElementManager.h b/gfx/layers/apz/util/ActiveElementManager.h
new file mode 100644
index 0000000000..f8a6f07261
--- /dev/null
+++ b/gfx/layers/apz/util/ActiveElementManager.h
@@ -0,0 +1,111 @@
+/* -*- 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/. */
+
+#ifndef mozilla_layers_ActiveElementManager_h
+#define mozilla_layers_ActiveElementManager_h
+
+#include "nsCOMPtr.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+class CancelableRunnable;
+
+namespace dom {
+class Element;
+class EventTarget;
+} // namespace dom
+
+namespace layers {
+
+class DelayedClearElementActivation;
+
+/**
+ * Manages setting and clearing the ':active' CSS pseudostate in the presence
+ * of touch input.
+ */
+class ActiveElementManager final {
+ ~ActiveElementManager();
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ActiveElementManager)
+
+ ActiveElementManager();
+
+ /**
+ * Specify the target of a touch. Typically this should be called right
+ * after HandleTouchStart(), but in cases where the APZ needs to wait for
+ * a content response the HandleTouchStart() may be delayed, in which case
+ * this function can be called first.
+ * |aTarget| may be nullptr.
+ */
+ void SetTargetElement(dom::EventTarget* aTarget);
+ /**
+ * Handle a touch-start state notification from APZ. This notification
+ * may be delayed until after touch listeners have responded to the APZ.
+ * @param aCanBePan whether the touch can be a pan
+ */
+ void HandleTouchStart(bool aCanBePan);
+ /**
+ * Clear the active element.
+ */
+ void ClearActivation();
+ /**
+ * Handle a touch-end or touch-cancel event.
+ * @param aWasClick whether the touch was a click
+ */
+ void HandleTouchEndEvent(bool aWasClick);
+ /**
+ * Handle a touch-end state notification from APZ. This notification may be
+ * delayed until after touch listeners have responded to the APZ.
+ */
+ void HandleTouchEnd();
+ /**
+ * Possibly clear active element sate in response to a single tap.
+ */
+ void ProcessSingleTap();
+ /**
+ * Cleanup on window destroy.
+ */
+ void Destroy();
+
+ private:
+ /**
+ * The target of the first touch point in the current touch block.
+ */
+ nsCOMPtr<dom::Element> mTarget;
+ /**
+ * Whether the current touch block can be a pan. Set in HandleTouchStart().
+ */
+ bool mCanBePan;
+ /**
+ * Whether mCanBePan has been set for the current touch block.
+ * We need to keep track of this to allow HandleTouchStart() and
+ * SetTargetElement() to be called in either order.
+ */
+ bool mCanBePanSet;
+ /**
+ * A task for calling SetActive() after a timeout.
+ */
+ RefPtr<CancelableRunnable> mSetActiveTask;
+
+ // Store the pending single tap event element activation clearing
+ // task.
+ RefPtr<DelayedClearElementActivation> mDelayedClearElementActivation;
+
+ // Helpers
+ void TriggerElementActivation();
+ void SetActive(dom::Element* aTarget);
+ void ResetActive();
+ void ResetTouchBlockState();
+ void SetActiveTask(const nsCOMPtr<dom::Element>& aTarget);
+ void CancelTask();
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_ActiveElementManager_h */
diff --git a/gfx/layers/apz/util/CheckerboardReportService.cpp b/gfx/layers/apz/util/CheckerboardReportService.cpp
new file mode 100644
index 0000000000..b6ae712b3f
--- /dev/null
+++ b/gfx/layers/apz/util/CheckerboardReportService.cpp
@@ -0,0 +1,217 @@
+/* -*- 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 "CheckerboardReportService.h"
+
+#include "jsapi.h" // for JS_Now
+#include "MainThreadUtils.h" // for NS_IsMainThread
+#include "mozilla/Assertions.h" // for MOZ_ASSERT
+#include "mozilla/ClearOnShutdown.h" // for ClearOnShutdown
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_apz.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/CheckerboardReportServiceBinding.h" // for dom::CheckerboardReports
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "nsContentUtils.h" // for nsContentUtils
+#include "nsIObserverService.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+namespace layers {
+
+/*static*/
+StaticRefPtr<CheckerboardEventStorage> CheckerboardEventStorage::sInstance;
+
+/*static*/
+already_AddRefed<CheckerboardEventStorage>
+CheckerboardEventStorage::GetInstance() {
+ // The instance in the parent process does all the work, so if this is getting
+ // called in the child process something is likely wrong.
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!sInstance) {
+ sInstance = new CheckerboardEventStorage();
+ ClearOnShutdown(&sInstance);
+ }
+ RefPtr<CheckerboardEventStorage> instance = sInstance.get();
+ return instance.forget();
+}
+
+void CheckerboardEventStorage::Report(uint32_t aSeverity,
+ const std::string& aLog) {
+ if (!NS_IsMainThread()) {
+ RefPtr<Runnable> task = NS_NewRunnableFunction(
+ "layers::CheckerboardEventStorage::Report",
+ [aSeverity, aLog]() -> void {
+ CheckerboardEventStorage::Report(aSeverity, aLog);
+ });
+ NS_DispatchToMainThread(task.forget());
+ return;
+ }
+
+ if (XRE_IsGPUProcess()) {
+ if (gfx::GPUParent* gpu = gfx::GPUParent::GetSingleton()) {
+ nsCString log(aLog.c_str());
+ Unused << gpu->SendReportCheckerboard(aSeverity, log);
+ }
+ return;
+ }
+
+ RefPtr<CheckerboardEventStorage> storage = GetInstance();
+ storage->ReportCheckerboard(aSeverity, aLog);
+}
+
+void CheckerboardEventStorage::ReportCheckerboard(uint32_t aSeverity,
+ const std::string& aLog) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aSeverity == 0) {
+ // This code assumes all checkerboard reports have a nonzero severity.
+ return;
+ }
+
+ CheckerboardReport severe(aSeverity, JS_Now(), aLog);
+ CheckerboardReport recent;
+
+ // First look in the "severe" reports to see if the new one belongs in that
+ // list.
+ for (int i = 0; i < SEVERITY_MAX_INDEX; i++) {
+ if (mCheckerboardReports[i].mSeverity >= severe.mSeverity) {
+ continue;
+ }
+ // The new one deserves to be in the "severe" list. Take the one getting
+ // bumped off the list, and put it in |recent| for possible insertion into
+ // the recents list.
+ recent = mCheckerboardReports[SEVERITY_MAX_INDEX - 1];
+
+ // Shuffle the severe list down, insert the new one.
+ for (int j = SEVERITY_MAX_INDEX - 1; j > i; j--) {
+ mCheckerboardReports[j] = mCheckerboardReports[j - 1];
+ }
+ mCheckerboardReports[i] = severe;
+ severe.mSeverity = 0; // mark |severe| as inserted
+ break;
+ }
+
+ // If |severe.mSeverity| is nonzero, the incoming report didn't get inserted
+ // into the severe list; put it into |recent| for insertion into the recent
+ // list.
+ if (severe.mSeverity) {
+ MOZ_ASSERT(recent.mSeverity == 0, "recent should be empty here");
+ recent = severe;
+ } // else |recent| may hold a report that got knocked out of the severe list.
+
+ if (recent.mSeverity == 0) {
+ // Nothing to be inserted into the recent list.
+ return;
+ }
+
+ // If it wasn't in the "severe" list, add it to the "recent" list.
+ for (int i = SEVERITY_MAX_INDEX; i < RECENT_MAX_INDEX; i++) {
+ if (mCheckerboardReports[i].mTimestamp >= recent.mTimestamp) {
+ continue;
+ }
+ // |recent| needs to be inserted at |i|. Shuffle the remaining ones down
+ // and insert it.
+ for (int j = RECENT_MAX_INDEX - 1; j > i; j--) {
+ mCheckerboardReports[j] = mCheckerboardReports[j - 1];
+ }
+ mCheckerboardReports[i] = recent;
+ break;
+ }
+}
+
+void CheckerboardEventStorage::GetReports(
+ nsTArray<dom::CheckerboardReport>& aOutReports) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ for (int i = 0; i < RECENT_MAX_INDEX; i++) {
+ CheckerboardReport& r = mCheckerboardReports[i];
+ if (r.mSeverity == 0) {
+ continue;
+ }
+ dom::CheckerboardReport report;
+ report.mSeverity.Construct() = r.mSeverity;
+ report.mTimestamp.Construct() = r.mTimestamp / 1000; // micros to millis
+ report.mLog.Construct() =
+ NS_ConvertUTF8toUTF16(r.mLog.c_str(), r.mLog.size());
+ report.mReason.Construct() = (i < SEVERITY_MAX_INDEX)
+ ? dom::CheckerboardReason::Severe
+ : dom::CheckerboardReason::Recent;
+ aOutReports.AppendElement(report);
+ }
+}
+
+} // namespace layers
+
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CheckerboardReportService, mParent)
+
+/*static*/
+bool CheckerboardReportService::IsEnabled(JSContext* aCtx, JSObject* aGlobal) {
+ // Only allow this in the parent process
+ if (!XRE_IsParentProcess()) {
+ return false;
+ }
+ // Allow privileged code or about:checkerboard (unprivileged) to access this.
+ return nsContentUtils::IsSystemCaller(aCtx) ||
+ nsContentUtils::IsSpecificAboutPage(aGlobal, "about:checkerboard");
+}
+
+/*static*/
+already_AddRefed<CheckerboardReportService>
+CheckerboardReportService::Constructor(const dom::GlobalObject& aGlobal) {
+ RefPtr<CheckerboardReportService> ces =
+ new CheckerboardReportService(aGlobal.GetAsSupports());
+ return ces.forget();
+}
+
+CheckerboardReportService::CheckerboardReportService(nsISupports* aParent)
+ : mParent(aParent) {}
+
+JSObject* CheckerboardReportService::WrapObject(
+ JSContext* aCtx, JS::Handle<JSObject*> aGivenProto) {
+ return CheckerboardReportService_Binding::Wrap(aCtx, this, aGivenProto);
+}
+
+nsISupports* CheckerboardReportService::GetParentObject() { return mParent; }
+
+void CheckerboardReportService::GetReports(
+ nsTArray<dom::CheckerboardReport>& aOutReports) {
+ RefPtr<mozilla::layers::CheckerboardEventStorage> instance =
+ mozilla::layers::CheckerboardEventStorage::GetInstance();
+ MOZ_ASSERT(instance);
+ instance->GetReports(aOutReports);
+}
+
+bool CheckerboardReportService::IsRecordingEnabled() const {
+ return StaticPrefs::apz_record_checkerboarding();
+}
+
+void CheckerboardReportService::SetRecordingEnabled(bool aEnabled) {
+ Preferences::SetBool("apz.record_checkerboarding", aEnabled);
+}
+
+void CheckerboardReportService::FlushActiveReports() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ gfx::GPUProcessManager* gpu = gfx::GPUProcessManager::Get();
+ if (gpu && gpu->NotifyGpuObservers("APZ:FlushActiveCheckerboard")) {
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsSvc);
+ if (obsSvc) {
+ obsSvc->NotifyObservers(nullptr, "APZ:FlushActiveCheckerboard", nullptr);
+ }
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/CheckerboardReportService.h b/gfx/layers/apz/util/CheckerboardReportService.h
new file mode 100644
index 0000000000..d9b37509c5
--- /dev/null
+++ b/gfx/layers/apz/util/CheckerboardReportService.h
@@ -0,0 +1,138 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_CheckerboardReportService_h
+#define mozilla_dom_CheckerboardReportService_h
+
+#include <string>
+
+#include "js/TypeDecls.h" // for JSContext, JSObject
+#include "mozilla/StaticPtr.h" // for StaticRefPtr
+#include "nsCOMPtr.h" // for nsCOMPtr
+#include "nsISupports.h" // for NS_INLINE_DECL_REFCOUNTING
+#include "nsTArrayForwardDeclare.h" // for nsTArray
+#include "nsWrapperCache.h" // for nsWrapperCache
+
+namespace mozilla {
+
+namespace dom {
+struct CheckerboardReport;
+}
+
+namespace layers {
+
+// CheckerboardEventStorage is a singleton that stores info on checkerboard
+// events, so that they can be accessed from about:checkerboard and visualized.
+// Note that this class is NOT threadsafe, and all methods must be called on
+// the main thread.
+class CheckerboardEventStorage {
+ NS_INLINE_DECL_REFCOUNTING(CheckerboardEventStorage)
+
+ public:
+ /**
+ * Get the singleton instance.
+ */
+ static already_AddRefed<CheckerboardEventStorage> GetInstance();
+
+ /**
+ * Get the stored checkerboard reports.
+ */
+ void GetReports(nsTArray<dom::CheckerboardReport>& aOutReports);
+
+ /**
+ * Save a checkerboard event log, optionally dropping older ones that were
+ * less severe or less recent. Zero-severity reports may be ignored entirely.
+ */
+ static void Report(uint32_t aSeverity, const std::string& aLog);
+
+ private:
+ /* Stuff for refcounted singleton */
+ CheckerboardEventStorage() = default;
+ virtual ~CheckerboardEventStorage() = default;
+
+ static StaticRefPtr<CheckerboardEventStorage> sInstance;
+
+ void ReportCheckerboard(uint32_t aSeverity, const std::string& aLog);
+
+ private:
+ /**
+ * Struct that this class uses internally to store a checkerboard report.
+ */
+ struct CheckerboardReport {
+ uint32_t mSeverity; // if 0, this report is empty
+ int64_t mTimestamp; // microseconds since epoch, as from JS_Now()
+ std::string mLog;
+
+ CheckerboardReport() : mSeverity(0), mTimestamp(0) {}
+
+ CheckerboardReport(uint32_t aSeverity, int64_t aTimestamp,
+ const std::string& aLog)
+ : mSeverity(aSeverity), mTimestamp(aTimestamp), mLog(aLog) {}
+ };
+
+ // The first 5 (indices 0-4) are the most severe ones in decreasing order
+ // of severity; the next 5 (indices 5-9) are the most recent ones that are
+ // not already in the "severe" list.
+ static const int SEVERITY_MAX_INDEX = 5;
+ static const int RECENT_MAX_INDEX = 10;
+ CheckerboardReport mCheckerboardReports[RECENT_MAX_INDEX];
+};
+
+} // namespace layers
+
+namespace dom {
+
+class GlobalObject;
+
+/**
+ * CheckerboardReportService is a wrapper object that allows access to the
+ * stuff in CheckerboardEventStorage (above). We need this wrapper for proper
+ * garbage/cycle collection, since this can be accessed from JS.
+ */
+class CheckerboardReportService : public nsWrapperCache {
+ public:
+ /**
+ * Check if the given page is allowed to access this object via the WebIDL
+ * bindings. It only returns true if the page is about:checkerboard.
+ */
+ static bool IsEnabled(JSContext* aCtx, JSObject* aGlobal);
+
+ /*
+ * Other standard WebIDL binding glue.
+ */
+
+ static already_AddRefed<CheckerboardReportService> Constructor(
+ const dom::GlobalObject& aGlobal);
+
+ explicit CheckerboardReportService(nsISupports* aSupports);
+
+ JSObject* WrapObject(JSContext* aCtx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsISupports* GetParentObject();
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(CheckerboardReportService)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(CheckerboardReportService)
+
+ public:
+ /*
+ * The methods exposed via the webidl.
+ */
+ void GetReports(nsTArray<dom::CheckerboardReport>& aOutReports);
+ bool IsRecordingEnabled() const;
+ void SetRecordingEnabled(bool aEnabled);
+ void FlushActiveReports();
+
+ private:
+ virtual ~CheckerboardReportService() = default;
+
+ nsCOMPtr<nsISupports> mParent;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_layers_CheckerboardReportService_h */
diff --git a/gfx/layers/apz/util/ChromeProcessController.cpp b/gfx/layers/apz/util/ChromeProcessController.cpp
new file mode 100644
index 0000000000..a836f0ecdc
--- /dev/null
+++ b/gfx/layers/apz/util/ChromeProcessController.cpp
@@ -0,0 +1,361 @@
+/* -*- 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 "ChromeProcessController.h"
+
+#include "MainThreadUtils.h" // for NS_IsMainThread()
+#include "base/task.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/layers/CompositorBridgeParent.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/APZEventState.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/layers/InputAPZContext.h"
+#include "mozilla/layers/DoubleTapToZoom.h"
+#include "mozilla/layers/RepaintRequest.h"
+#include "mozilla/dom/Document.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsLayoutUtils.h"
+#include "nsView.h"
+
+static mozilla::LazyLogModule sApzChromeLog("apz.cc.chrome");
+
+using namespace mozilla;
+using namespace mozilla::layers;
+using namespace mozilla::widget;
+
+ChromeProcessController::ChromeProcessController(
+ nsIWidget* aWidget, APZEventState* aAPZEventState,
+ IAPZCTreeManager* aAPZCTreeManager)
+ : mWidget(aWidget),
+ mAPZEventState(aAPZEventState),
+ mAPZCTreeManager(aAPZCTreeManager),
+ mUIThread(NS_GetCurrentThread()) {
+ // Otherwise we're initializing mUIThread incorrectly.
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aAPZEventState);
+ MOZ_ASSERT(aAPZCTreeManager);
+
+ mUIThread->Dispatch(
+ NewRunnableMethod("layers::ChromeProcessController::InitializeRoot", this,
+ &ChromeProcessController::InitializeRoot));
+}
+
+ChromeProcessController::~ChromeProcessController() = default;
+
+void ChromeProcessController::InitializeRoot() {
+ APZCCallbackHelper::InitializeRootDisplayport(GetPresShell());
+}
+
+void ChromeProcessController::NotifyLayerTransforms(
+ nsTArray<MatrixMessage>&& aTransforms) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(
+ NewRunnableMethod<StoreCopyPassByRRef<nsTArray<MatrixMessage>>>(
+ "layers::ChromeProcessController::NotifyLayerTransforms", this,
+ &ChromeProcessController::NotifyLayerTransforms,
+ std::move(aTransforms)));
+ return;
+ }
+
+ APZCCallbackHelper::NotifyLayerTransforms(aTransforms);
+}
+
+void ChromeProcessController::RequestContentRepaint(
+ const RepaintRequest& aRequest) {
+ MOZ_ASSERT(IsRepaintThread());
+
+ if (aRequest.IsRootContent()) {
+ APZCCallbackHelper::UpdateRootFrame(aRequest);
+ } else {
+ APZCCallbackHelper::UpdateSubFrame(aRequest);
+ }
+}
+
+bool ChromeProcessController::IsRepaintThread() { return NS_IsMainThread(); }
+
+void ChromeProcessController::DispatchToRepaintThread(
+ already_AddRefed<Runnable> aTask) {
+ NS_DispatchToMainThread(std::move(aTask));
+}
+
+void ChromeProcessController::Destroy() {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(
+ NewRunnableMethod("layers::ChromeProcessController::Destroy", this,
+ &ChromeProcessController::Destroy));
+ return;
+ }
+
+ MOZ_ASSERT(mUIThread->IsOnCurrentThread());
+ mWidget = nullptr;
+ if (mAPZEventState) {
+ mAPZEventState->Destroy();
+ }
+ mAPZEventState = nullptr;
+}
+
+PresShell* ChromeProcessController::GetPresShell() const {
+ if (!mWidget) {
+ return nullptr;
+ }
+ if (nsView* view = nsView::GetViewFor(mWidget)) {
+ return view->GetPresShell();
+ }
+ return nullptr;
+}
+
+dom::Document* ChromeProcessController::GetRootDocument() const {
+ if (PresShell* presShell = GetPresShell()) {
+ return presShell->GetDocument();
+ }
+ return nullptr;
+}
+
+dom::Document* ChromeProcessController::GetRootContentDocument(
+ const ScrollableLayerGuid::ViewID& aScrollId) const {
+ nsIContent* content = nsLayoutUtils::FindContentFor(aScrollId);
+ if (!content) {
+ return nullptr;
+ }
+ if (PresShell* presShell =
+ APZCCallbackHelper::GetRootContentDocumentPresShellForContent(
+ content)) {
+ return presShell->GetDocument();
+ }
+ return nullptr;
+}
+
+void ChromeProcessController::HandleDoubleTap(
+ const mozilla::CSSPoint& aPoint, Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ const DoubleTapToZoomMetrics& aDoubleTapToZoomMetrics) {
+ MOZ_LOG(sApzChromeLog, LogLevel::Debug, ("HandleDoubleTap\n"));
+ MOZ_ASSERT(mUIThread->IsOnCurrentThread());
+
+ RefPtr<dom::Document> document = GetRootContentDocument(aGuid.mScrollId);
+ if (!document.get()) {
+ return;
+ }
+
+ ZoomTarget zoomTarget =
+ CalculateRectToZoomTo(document, aPoint, aDoubleTapToZoomMetrics);
+
+ mAPZCTreeManager->ZoomToRect(aGuid, zoomTarget,
+ ZoomToRectBehavior::DEFAULT_BEHAVIOR);
+}
+
+void ChromeProcessController::HandleTap(
+ TapType aType, const mozilla::LayoutDevicePoint& aPoint,
+ Modifiers aModifiers, const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId,
+ const Maybe<DoubleTapToZoomMetrics>& aDoubleTapToZoomMetrics) {
+ MOZ_LOG(sApzChromeLog, LogLevel::Debug,
+ ("HandleTap called with %d\n", (int)aType));
+ if (!mUIThread->IsOnCurrentThread()) {
+ MOZ_LOG(sApzChromeLog, LogLevel::Debug, ("HandleTap redispatching\n"));
+ mUIThread->Dispatch(
+ NewRunnableMethod<TapType, mozilla::LayoutDevicePoint, Modifiers,
+ ScrollableLayerGuid, uint64_t,
+ Maybe<DoubleTapToZoomMetrics>>(
+ "layers::ChromeProcessController::HandleTap", this,
+ &ChromeProcessController::HandleTap, aType, aPoint, aModifiers,
+ aGuid, aInputBlockId, aDoubleTapToZoomMetrics));
+ return;
+ }
+
+ if (!mAPZEventState) {
+ return;
+ }
+
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (!presShell) {
+ return;
+ }
+ if (!presShell->GetPresContext()) {
+ return;
+ }
+ CSSToLayoutDeviceScale scale(
+ presShell->GetPresContext()->CSSToDevPixelScale());
+
+ CSSPoint point = aPoint / scale;
+
+ // Stash the guid in InputAPZContext so that when the visual-to-layout
+ // transform is applied to the event's coordinates, we use the right transform
+ // based on the scroll frame being targeted.
+ // The other values don't really matter.
+ InputAPZContext context(aGuid, aInputBlockId, nsEventStatus_eSentinel);
+
+ switch (aType) {
+ case TapType::eSingleTap: {
+ RefPtr<APZEventState> eventState(mAPZEventState);
+ eventState->ProcessSingleTap(point, scale, aModifiers, 1, aInputBlockId);
+ break;
+ }
+ case TapType::eDoubleTap:
+ MOZ_ASSERT(aDoubleTapToZoomMetrics);
+ HandleDoubleTap(point, aModifiers, aGuid, *aDoubleTapToZoomMetrics);
+ break;
+ case TapType::eSecondTap: {
+ RefPtr<APZEventState> eventState(mAPZEventState);
+ eventState->ProcessSingleTap(point, scale, aModifiers, 2, aInputBlockId);
+ break;
+ }
+ case TapType::eLongTap: {
+ RefPtr<APZEventState> eventState(mAPZEventState);
+ eventState->ProcessLongTap(presShell, point, scale, aModifiers,
+ aInputBlockId);
+ break;
+ }
+ case TapType::eLongTapUp: {
+ RefPtr<APZEventState> eventState(mAPZEventState);
+ eventState->ProcessLongTapUp(presShell, point, scale, aModifiers);
+ break;
+ }
+ }
+}
+
+void ChromeProcessController::NotifyPinchGesture(
+ PinchGestureInput::PinchGestureType aType, const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint, LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(
+ NewRunnableMethod<PinchGestureInput::PinchGestureType,
+ ScrollableLayerGuid, LayoutDevicePoint,
+ LayoutDeviceCoord, Modifiers>(
+ "layers::ChromeProcessController::NotifyPinchGesture", this,
+ &ChromeProcessController::NotifyPinchGesture, aType, aGuid,
+ aFocusPoint, aSpanChange, aModifiers));
+ return;
+ }
+
+ if (mWidget) {
+ // Dispatch the call to APZCCallbackHelper::NotifyPinchGesture to the main
+ // thread so that it runs asynchronously from the current call. This is
+ // because the call can run arbitrary JS code, which can also spin the event
+ // loop and cause undesirable re-entrancy in APZ.
+ mUIThread->Dispatch(NewRunnableFunction(
+ "layers::ChromeProcessController::NotifyPinchGestureAsync",
+ &APZCCallbackHelper::NotifyPinchGesture, aType, aFocusPoint,
+ aSpanChange, aModifiers, mWidget));
+ }
+}
+
+void ChromeProcessController::NotifyAPZStateChange(
+ const ScrollableLayerGuid& aGuid, APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid, APZStateChange,
+ int, Maybe<uint64_t>>(
+ "layers::ChromeProcessController::NotifyAPZStateChange", this,
+ &ChromeProcessController::NotifyAPZStateChange, aGuid, aChange, aArg,
+ aInputBlockId));
+ return;
+ }
+
+ if (!mAPZEventState) {
+ return;
+ }
+
+ mAPZEventState->ProcessAPZStateChange(aGuid.mScrollId, aChange, aArg,
+ aInputBlockId);
+}
+
+void ChromeProcessController::NotifyMozMouseScrollEvent(
+ const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(
+ NewRunnableMethod<ScrollableLayerGuid::ViewID, nsString>(
+ "layers::ChromeProcessController::NotifyMozMouseScrollEvent", this,
+ &ChromeProcessController::NotifyMozMouseScrollEvent, aScrollId,
+ aEvent));
+ return;
+ }
+
+ APZCCallbackHelper::NotifyMozMouseScrollEvent(aScrollId, aEvent);
+}
+
+void ChromeProcessController::NotifyFlushComplete() {
+ MOZ_ASSERT(IsRepaintThread());
+
+ APZCCallbackHelper::NotifyFlushComplete(GetPresShell());
+}
+
+void ChromeProcessController::NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(NewRunnableMethod<uint64_t, ScrollableLayerGuid::ViewID,
+ ScrollDirection>(
+ "layers::ChromeProcessController::NotifyAsyncScrollbarDragInitiated",
+ this, &ChromeProcessController::NotifyAsyncScrollbarDragInitiated,
+ aDragBlockId, aScrollId, aDirection));
+ return;
+ }
+
+ APZCCallbackHelper::NotifyAsyncScrollbarDragInitiated(aDragBlockId, aScrollId,
+ aDirection);
+}
+
+void ChromeProcessController::NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid::ViewID>(
+ "layers::ChromeProcessController::NotifyAsyncScrollbarDragRejected",
+ this, &ChromeProcessController::NotifyAsyncScrollbarDragRejected,
+ aScrollId));
+ return;
+ }
+
+ APZCCallbackHelper::NotifyAsyncScrollbarDragRejected(aScrollId);
+}
+
+void ChromeProcessController::NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid::ViewID>(
+ "layers::ChromeProcessController::NotifyAsyncAutoscrollRejected", this,
+ &ChromeProcessController::NotifyAsyncAutoscrollRejected, aScrollId));
+ return;
+ }
+
+ APZCCallbackHelper::NotifyAsyncAutoscrollRejected(aScrollId);
+}
+
+void ChromeProcessController::CancelAutoscroll(
+ const ScrollableLayerGuid& aGuid) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid>(
+ "layers::ChromeProcessController::CancelAutoscroll", this,
+ &ChromeProcessController::CancelAutoscroll, aGuid));
+ return;
+ }
+
+ APZCCallbackHelper::CancelAutoscroll(aGuid.mScrollId);
+}
+
+void ChromeProcessController::NotifyScaleGestureComplete(
+ const ScrollableLayerGuid& aGuid, float aScale) {
+ if (!mUIThread->IsOnCurrentThread()) {
+ mUIThread->Dispatch(NewRunnableMethod<ScrollableLayerGuid, float>(
+ "layers::ChromeProcessController::NotifyScaleGestureComplete", this,
+ &ChromeProcessController::NotifyScaleGestureComplete, aGuid, aScale));
+ return;
+ }
+
+ if (mWidget) {
+ // Dispatch the call to APZCCallbackHelper::NotifyScaleGestureComplete
+ // to the main thread so that it runs asynchronously from the current call.
+ // This is because the call can run arbitrary JS code, which can also spin
+ // the event loop and cause undesirable re-entrancy in APZ.
+ mUIThread->Dispatch(NewRunnableFunction(
+ "layers::ChromeProcessController::NotifyScaleGestureComplete",
+ &APZCCallbackHelper::NotifyScaleGestureComplete, mWidget, aScale));
+ }
+}
diff --git a/gfx/layers/apz/util/ChromeProcessController.h b/gfx/layers/apz/util/ChromeProcessController.h
new file mode 100644
index 0000000000..47315ef193
--- /dev/null
+++ b/gfx/layers/apz/util/ChromeProcessController.h
@@ -0,0 +1,106 @@
+/* -*- 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/. */
+
+#ifndef mozilla_layers_ChromeProcessController_h
+#define mozilla_layers_ChromeProcessController_h
+
+#include "mozilla/layers/GeckoContentController.h"
+#include "nsCOMPtr.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/layers/MatrixMessage.h"
+
+class nsIDOMWindowUtils;
+class nsISerialEventTarget;
+class nsIWidget;
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class Document;
+}
+
+namespace layers {
+
+class IAPZCTreeManager;
+class APZEventState;
+struct DoubleTapToZoomMetrics;
+
+/**
+ * ChromeProcessController is a GeckoContentController attached to the root of
+ * a compositor's layer tree. It's used directly by APZ by default, and remoted
+ * using PAPZ if there is a gpu process.
+ *
+ * If ChromeProcessController needs to implement a new method on
+ * GeckoContentController PAPZ, APZChild, and RemoteContentController must be
+ * updated to handle it.
+ */
+class ChromeProcessController : public mozilla::layers::GeckoContentController {
+ protected:
+ typedef mozilla::layers::FrameMetrics FrameMetrics;
+ typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid;
+
+ public:
+ explicit ChromeProcessController(nsIWidget* aWidget,
+ APZEventState* aAPZEventState,
+ IAPZCTreeManager* aAPZCTreeManager);
+ virtual ~ChromeProcessController();
+ void Destroy() override;
+
+ // GeckoContentController interface
+ void NotifyLayerTransforms(nsTArray<MatrixMessage>&& aTransforms) override;
+ void RequestContentRepaint(const RepaintRequest& aRequest) override;
+ bool IsRepaintThread() override;
+ void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) override;
+ MOZ_CAN_RUN_SCRIPT
+ void HandleTap(
+ TapType aType, const mozilla::LayoutDevicePoint& aPoint,
+ Modifiers aModifiers, const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId,
+ const Maybe<DoubleTapToZoomMetrics>& aDoubleTapToZoomMetrics) override;
+ void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) override;
+ void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid,
+ APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) override;
+ void NotifyMozMouseScrollEvent(const ScrollableLayerGuid::ViewID& aScrollId,
+ const nsString& aEvent) override;
+ void NotifyFlushComplete() override;
+ void NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection) override;
+ void NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) override;
+ void NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) override;
+ void CancelAutoscroll(const ScrollableLayerGuid& aGuid) override;
+ void NotifyScaleGestureComplete(const ScrollableLayerGuid& aGuid,
+ float aScale) override;
+
+ PresShell* GetTopLevelPresShell() const override { return GetPresShell(); }
+
+ private:
+ nsCOMPtr<nsIWidget> mWidget;
+ RefPtr<APZEventState> mAPZEventState;
+ RefPtr<IAPZCTreeManager> mAPZCTreeManager;
+ nsCOMPtr<nsISerialEventTarget> mUIThread;
+
+ void InitializeRoot();
+ PresShell* GetPresShell() const;
+ dom::Document* GetRootDocument() const;
+ dom::Document* GetRootContentDocument(
+ const ScrollableLayerGuid::ViewID& aScrollId) const;
+ void HandleDoubleTap(const mozilla::CSSPoint& aPoint, Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid,
+ const DoubleTapToZoomMetrics& aDoubleTapToZoomMetrics);
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_ChromeProcessController_h */
diff --git a/gfx/layers/apz/util/ContentProcessController.cpp b/gfx/layers/apz/util/ContentProcessController.cpp
new file mode 100644
index 0000000000..16a6c301c3
--- /dev/null
+++ b/gfx/layers/apz/util/ContentProcessController.cpp
@@ -0,0 +1,123 @@
+/* -*- 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 "ContentProcessController.h"
+
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/APZChild.h"
+#include "mozilla/layers/DoubleTapToZoom.h"
+#include "nsIContentInlines.h"
+
+#include "InputData.h" // for InputData
+
+namespace mozilla {
+namespace layers {
+
+ContentProcessController::ContentProcessController(
+ const RefPtr<dom::BrowserChild>& aBrowser)
+ : mBrowser(aBrowser) {
+ MOZ_ASSERT(mBrowser);
+}
+
+void ContentProcessController::NotifyLayerTransforms(
+ nsTArray<MatrixMessage>&& aTransforms) {
+ // This should never get called
+ MOZ_ASSERT(false);
+}
+
+void ContentProcessController::RequestContentRepaint(
+ const RepaintRequest& aRequest) {
+ if (mBrowser) {
+ mBrowser->UpdateFrame(aRequest);
+ }
+}
+
+void ContentProcessController::HandleTap(
+ TapType aType, const LayoutDevicePoint& aPoint, Modifiers aModifiers,
+ const ScrollableLayerGuid& aGuid, uint64_t aInputBlockId,
+ const Maybe<DoubleTapToZoomMetrics>& aMetrics) {
+ // This should never get called
+ MOZ_ASSERT(false);
+}
+
+void ContentProcessController::NotifyPinchGesture(
+ PinchGestureInput::PinchGestureType aType, const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint, LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) {
+ // This should never get called
+ MOZ_ASSERT_UNREACHABLE("Unexpected message to content process");
+}
+
+void ContentProcessController::NotifyAPZStateChange(
+ const ScrollableLayerGuid& aGuid, APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) {
+ if (mBrowser) {
+ mBrowser->NotifyAPZStateChange(aGuid.mScrollId, aChange, aArg,
+ aInputBlockId);
+ }
+}
+
+void ContentProcessController::NotifyMozMouseScrollEvent(
+ const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent) {
+ if (mBrowser) {
+ APZCCallbackHelper::NotifyMozMouseScrollEvent(aScrollId, aEvent);
+ }
+}
+
+void ContentProcessController::NotifyFlushComplete() {
+ if (mBrowser) {
+ RefPtr<PresShell> presShell = mBrowser->GetTopLevelPresShell();
+ APZCCallbackHelper::NotifyFlushComplete(presShell);
+ }
+}
+
+void ContentProcessController::NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection) {
+ APZCCallbackHelper::NotifyAsyncScrollbarDragInitiated(aDragBlockId, aScrollId,
+ aDirection);
+}
+
+void ContentProcessController::NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ APZCCallbackHelper::NotifyAsyncScrollbarDragRejected(aScrollId);
+}
+
+void ContentProcessController::NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) {
+ APZCCallbackHelper::NotifyAsyncAutoscrollRejected(aScrollId);
+}
+
+void ContentProcessController::CancelAutoscroll(
+ const ScrollableLayerGuid& aGuid) {
+ // This should never get called
+ MOZ_ASSERT_UNREACHABLE("Unexpected message to content process");
+}
+
+void ContentProcessController::NotifyScaleGestureComplete(
+ const ScrollableLayerGuid& aGuid, float aScale) {
+ // This should never get called
+ MOZ_ASSERT_UNREACHABLE("Unexpected message to content process");
+}
+
+bool ContentProcessController::IsRepaintThread() { return NS_IsMainThread(); }
+
+void ContentProcessController::DispatchToRepaintThread(
+ already_AddRefed<Runnable> aTask) {
+ NS_DispatchToMainThread(std::move(aTask));
+}
+
+PresShell* ContentProcessController::GetTopLevelPresShell() const {
+ if (!mBrowser) {
+ return nullptr;
+ }
+ return mBrowser->GetTopLevelPresShell();
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/ContentProcessController.h b/gfx/layers/apz/util/ContentProcessController.h
new file mode 100644
index 0000000000..af4745eed1
--- /dev/null
+++ b/gfx/layers/apz/util/ContentProcessController.h
@@ -0,0 +1,95 @@
+/* -*- 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/. */
+
+#ifndef mozilla_layers_ContentProcessController_h
+#define mozilla_layers_ContentProcessController_h
+
+#include "mozilla/layers/GeckoContentController.h"
+
+class nsIObserver;
+
+namespace mozilla {
+
+namespace dom {
+class BrowserChild;
+} // namespace dom
+
+namespace layers {
+
+class APZChild;
+struct DoubleTapToZoomMetrics;
+
+/**
+ * ContentProcessController is a GeckoContentController for a BrowserChild, and
+ * is always remoted using PAPZ/APZChild.
+ *
+ * ContentProcessController is created in ContentChild when a layer tree id has
+ * been allocated for a PBrowser that lives in that content process, and is
+ * destroyed when the Destroy message is received, or when the tab dies.
+ *
+ * If ContentProcessController needs to implement a new method on
+ * GeckoContentController PAPZ, APZChild, and RemoteContentController must be
+ * updated to handle it.
+ */
+class ContentProcessController final : public GeckoContentController {
+ public:
+ explicit ContentProcessController(const RefPtr<dom::BrowserChild>& aBrowser);
+
+ // GeckoContentController
+
+ void NotifyLayerTransforms(nsTArray<MatrixMessage>&& aTransforms) override;
+
+ void RequestContentRepaint(const RepaintRequest& aRequest) override;
+
+ void HandleTap(TapType aType, const LayoutDevicePoint& aPoint,
+ Modifiers aModifiers, const ScrollableLayerGuid& aGuid,
+ uint64_t aInputBlockId,
+ const Maybe<DoubleTapToZoomMetrics>& aMetrics) override;
+
+ void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
+ const ScrollableLayerGuid& aGuid,
+ const LayoutDevicePoint& aFocusPoint,
+ LayoutDeviceCoord aSpanChange,
+ Modifiers aModifiers) override;
+
+ void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid,
+ APZStateChange aChange, int aArg,
+ Maybe<uint64_t> aInputBlockId) override;
+
+ void NotifyMozMouseScrollEvent(const ScrollableLayerGuid::ViewID& aScrollId,
+ const nsString& aEvent) override;
+
+ void NotifyFlushComplete() override;
+
+ void NotifyAsyncScrollbarDragInitiated(
+ uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
+ ScrollDirection aDirection) override;
+ void NotifyAsyncScrollbarDragRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) override;
+
+ void NotifyAsyncAutoscrollRejected(
+ const ScrollableLayerGuid::ViewID& aScrollId) override;
+
+ void CancelAutoscroll(const ScrollableLayerGuid& aGuid) override;
+
+ void NotifyScaleGestureComplete(const ScrollableLayerGuid& aGuid,
+ float aScale) override;
+
+ bool IsRepaintThread() override;
+
+ void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) override;
+
+ PresShell* GetTopLevelPresShell() const override;
+
+ private:
+ RefPtr<dom::BrowserChild> mBrowser;
+};
+
+} // namespace layers
+
+} // namespace mozilla
+
+#endif // mozilla_layers_ContentProcessController_h
diff --git a/gfx/layers/apz/util/DoubleTapToZoom.cpp b/gfx/layers/apz/util/DoubleTapToZoom.cpp
new file mode 100644
index 0000000000..cf64a310b7
--- /dev/null
+++ b/gfx/layers/apz/util/DoubleTapToZoom.cpp
@@ -0,0 +1,441 @@
+/* -*- 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 "DoubleTapToZoom.h"
+
+#include <algorithm> // for std::min, std::max
+
+#include "mozilla/PresShell.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/EffectsInfo.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "nsCOMPtr.h"
+#include "nsIContent.h"
+#include "mozilla/dom/Document.h"
+#include "nsIFrame.h"
+#include "nsIFrameInlines.h"
+#include "nsIScrollableFrame.h"
+#include "nsTableCellFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsStyleConsts.h"
+#include "mozilla/ViewportUtils.h"
+#include "mozilla/EventListenerManager.h"
+#include "mozilla/layers/APZUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+namespace {
+
+using FrameForPointOption = nsLayoutUtils::FrameForPointOption;
+
+static bool IsGeneratedContent(nsIContent* aContent) {
+ // We exclude marks because making them double tap targets does not seem
+ // desirable.
+ return aContent->IsGeneratedContentContainerForBefore() ||
+ aContent->IsGeneratedContentContainerForAfter();
+}
+
+// Returns the DOM element found at |aPoint|, interpreted as being relative to
+// the root frame of |aPresShell| in visual coordinates. If the point is inside
+// a subdocument, returns an element inside the subdocument, rather than the
+// subdocument element (and does so recursively). The implementation was adapted
+// from DocumentOrShadowRoot::ElementFromPoint(), with the notable exception
+// that we don't pass nsLayoutUtils::IGNORE_CROSS_DOC to GetFrameForPoint(), so
+// as to get the behaviour described above in the presence of subdocuments.
+static already_AddRefed<dom::Element> ElementFromPoint(
+ const RefPtr<PresShell>& aPresShell, const CSSPoint& aPoint) {
+ nsIFrame* rootFrame = aPresShell->GetRootFrame();
+ if (!rootFrame) {
+ return nullptr;
+ }
+ nsIFrame* frame = nsLayoutUtils::GetFrameForPoint(
+ RelativeTo{rootFrame, ViewportType::Visual}, CSSPoint::ToAppUnits(aPoint),
+ {{FrameForPointOption::IgnorePaintSuppression}});
+ while (frame && (!frame->GetContent() ||
+ (frame->GetContent()->IsInNativeAnonymousSubtree() &&
+ !IsGeneratedContent(frame->GetContent())))) {
+ frame = nsLayoutUtils::GetParentOrPlaceholderFor(frame);
+ }
+ if (!frame) {
+ return nullptr;
+ }
+ // FIXME(emilio): This should probably use the flattened tree, GetParent() is
+ // not guaranteed to be an element in presence of shadow DOM.
+ nsIContent* content = frame->GetContent();
+ if (!content) {
+ return nullptr;
+ }
+ if (dom::Element* element = content->GetAsElementOrParentElement()) {
+ return do_AddRef(element);
+ }
+ return nullptr;
+}
+
+// Get table cell from element, parent or grand parent.
+static dom::Element* GetNearbyTableCell(
+ const nsCOMPtr<dom::Element>& aElement) {
+ nsTableCellFrame* tableCell = do_QueryFrame(aElement->GetPrimaryFrame());
+ if (tableCell) {
+ return aElement.get();
+ }
+ if (dom::Element* parent = aElement->GetFlattenedTreeParentElement()) {
+ nsTableCellFrame* tableCell = do_QueryFrame(parent->GetPrimaryFrame());
+ if (tableCell) {
+ return parent;
+ }
+ if (dom::Element* grandParent = parent->GetFlattenedTreeParentElement()) {
+ tableCell = do_QueryFrame(grandParent->GetPrimaryFrame());
+ if (tableCell) {
+ return grandParent;
+ }
+ }
+ }
+ return nullptr;
+}
+
+// A utility function returns the given |aElement| rectangle relative to the top
+// level content document coordinates.
+static CSSRect GetBoundingContentRect(
+ const dom::Element* aElement,
+ const RefPtr<dom::Document>& aInProcessRootContentDocument,
+ const nsIScrollableFrame* aRootScrollFrame,
+ const DoubleTapToZoomMetrics& aMetrics,
+ mozilla::Maybe<CSSRect>* aOutNearestScrollClip = nullptr) {
+ CSSRect result = nsLayoutUtils::GetBoundingContentRect(
+ aElement, aRootScrollFrame, aOutNearestScrollClip);
+ if (aInProcessRootContentDocument->IsTopLevelContentDocument()) {
+ return result;
+ }
+
+ nsIFrame* frame = aElement->GetPrimaryFrame();
+ if (!frame) {
+ return CSSRect();
+ }
+
+ // If the nearest scroll frame is |aRootScrollFrame|,
+ // nsLayoutUtils::GetBoundingContentRect doesn't set |aOutNearestScrollClip|,
+ // thus in the cases of OOP iframs, we need to use the visible rect of the
+ // iframe as the nearest scroll clip.
+ if (aOutNearestScrollClip && aOutNearestScrollClip->isNothing()) {
+ if (dom::BrowserChild* browserChild =
+ dom::BrowserChild::GetFrom(frame->PresShell())) {
+ const dom::EffectsInfo& effectsInfo = browserChild->GetEffectsInfo();
+ if (effectsInfo.IsVisible()) {
+ *aOutNearestScrollClip =
+ effectsInfo.mVisibleRect.map([&aMetrics](const nsRect& aRect) {
+ return aMetrics.mTransformMatrix.TransformBounds(
+ CSSRect::FromAppUnits(aRect));
+ });
+ }
+ }
+ }
+
+ // In the case of an element inside an OOP iframe, |aMetrics.mTransformMatrix|
+ // includes the translation information about the root layout scroll offset,
+ // thus we use nsIFrame::GetBoundingClientRect rather than
+ // nsLayoutUtils::GetBoundingContent.
+ return aMetrics.mTransformMatrix.TransformBounds(
+ CSSRect::FromAppUnits(frame->GetBoundingClientRect()));
+}
+
+static bool ShouldZoomToElement(
+ const nsCOMPtr<dom::Element>& aElement,
+ const RefPtr<dom::Document>& aInProcessRootContentDocument,
+ nsIScrollableFrame* aRootScrollFrame,
+ const DoubleTapToZoomMetrics& aMetrics) {
+ if (nsIFrame* frame = aElement->GetPrimaryFrame()) {
+ if (frame->StyleDisplay()->IsInlineFlow() &&
+ // Replaced elements are suitable zoom targets because they act like
+ // inline-blocks instead of inline. (textarea's are the specific reason
+ // we do this)
+ !frame->IsReplaced()) {
+ return false;
+ }
+ }
+ // Trying to zoom to the html element will just end up scrolling to the start
+ // of the document, return false and we'll run out of elements and just
+ // zoomout (without scrolling to the start).
+ if (aElement->OwnerDoc() == aInProcessRootContentDocument &&
+ aElement->IsHTMLElement(nsGkAtoms::html)) {
+ return false;
+ }
+ if (aElement->IsAnyOfHTMLElements(nsGkAtoms::li, nsGkAtoms::q)) {
+ return false;
+ }
+
+ // Ignore elements who are table cells or their parents are table cells, and
+ // they take up less than 30% of page rect width because they are likely cells
+ // in data tables (as opposed to tables used for layout purposes), and we
+ // don't want to zoom to them. This heuristic is quite naive and leaves a lot
+ // to be desired.
+ if (dom::Element* tableCell = GetNearbyTableCell(aElement)) {
+ CSSRect rect = GetBoundingContentRect(
+ tableCell, aInProcessRootContentDocument, aRootScrollFrame, aMetrics);
+ if (rect.width < 0.3 * aMetrics.mRootScrollableRect.width) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// Calculates if zooming to aRect would have almost the same zoom level as
+// aCompositedArea currently has. If so we would want to zoom out instead.
+static bool RectHasAlmostSameZoomLevel(const CSSRect& aRect,
+ const CSSRect& aCompositedArea) {
+ // This functions checks to see if the area of the rect visible in the
+ // composition bounds (i.e. the overlapArea variable below) is approximately
+ // the max area of the rect we can show.
+
+ // AsyncPanZoomController::ZoomToRect will adjust the zoom and scroll offset
+ // so that the zoom to rect fills the composited area. If after adjusting the
+ // scroll offset _only_ the rect would fill the composited area we want to
+ // zoom out (we don't want to _just_ scroll, we want to do some amount of
+ // zooming, either in or out it doesn't matter which). So translate both rects
+ // to the same origin and then compute their overlap, which is what the
+ // following calculation does.
+
+ float overlapArea = std::min(aRect.width, aCompositedArea.width) *
+ std::min(aRect.height, aCompositedArea.height);
+ float availHeight = std::min(
+ aRect.Width() * aCompositedArea.Height() / aCompositedArea.Width(),
+ aRect.Height());
+ float showing = overlapArea / (aRect.Width() * availHeight);
+ float ratioW = aRect.Width() / aCompositedArea.Width();
+ float ratioH = aRect.Height() / aCompositedArea.Height();
+
+ return showing > 0.9 && (ratioW > 0.9 || ratioH > 0.9);
+}
+
+} // namespace
+
+static CSSRect AddHMargin(const CSSRect& aRect, const CSSCoord& aMargin,
+ const CSSRect& aRootScrollableRect) {
+ CSSRect rect =
+ CSSRect(std::max(aRootScrollableRect.X(), aRect.X() - aMargin), aRect.Y(),
+ aRect.Width() + 2 * aMargin, aRect.Height());
+ // Constrict the rect to the screen's right edge
+ rect.SetWidth(std::min(rect.Width(), aRootScrollableRect.XMost() - rect.X()));
+ return rect;
+}
+
+static CSSRect AddVMargin(const CSSRect& aRect, const CSSCoord& aMargin,
+ const CSSRect& aRootScrollableRect) {
+ CSSRect rect =
+ CSSRect(aRect.X(), std::max(aRootScrollableRect.Y(), aRect.Y() - aMargin),
+ aRect.Width(), aRect.Height() + 2 * aMargin);
+ // Constrict the rect to the screen's bottom edge
+ rect.SetHeight(
+ std::min(rect.Height(), aRootScrollableRect.YMost() - rect.Y()));
+ return rect;
+}
+
+static bool IsReplacedElement(const nsCOMPtr<dom::Element>& aElement) {
+ if (nsIFrame* frame = aElement->GetPrimaryFrame()) {
+ if (frame->IsReplaced()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool HasNonPassiveWheelListenerOnAncestor(nsIContent* aContent) {
+ for (nsIContent* content = aContent; content;
+ content = content->GetFlattenedTreeParent()) {
+ EventListenerManager* elm = content->GetExistingListenerManager();
+ if (elm && elm->HasNonPassiveWheelListener()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+ZoomTarget CalculateRectToZoomTo(
+ const RefPtr<dom::Document>& aInProcessRootContentDocument,
+ const CSSPoint& aPoint, const DoubleTapToZoomMetrics& aMetrics) {
+ // Ensure the layout information we get is up-to-date.
+ aInProcessRootContentDocument->FlushPendingNotifications(FlushType::Layout);
+
+ // An empty rect as return value is interpreted as "zoom out".
+ const CSSRect zoomOut;
+
+ RefPtr<PresShell> presShell = aInProcessRootContentDocument->GetPresShell();
+ if (!presShell) {
+ return ZoomTarget{zoomOut, CantZoomOutBehavior::ZoomIn};
+ }
+
+ nsIScrollableFrame* rootScrollFrame =
+ presShell->GetRootScrollFrameAsScrollable();
+ if (!rootScrollFrame) {
+ return ZoomTarget{zoomOut, CantZoomOutBehavior::ZoomIn};
+ }
+
+ CSSPoint documentRelativePoint =
+ aInProcessRootContentDocument->IsTopLevelContentDocument()
+ ? CSSPoint::FromAppUnits(ViewportUtils::VisualToLayout(
+ CSSPoint::ToAppUnits(aPoint), presShell)) +
+ CSSPoint::FromAppUnits(rootScrollFrame->GetScrollPosition())
+ : aMetrics.mTransformMatrix.TransformPoint(aPoint);
+
+ nsCOMPtr<dom::Element> element = ElementFromPoint(presShell, aPoint);
+ if (!element) {
+ return ZoomTarget{zoomOut, CantZoomOutBehavior::ZoomIn, Nothing(),
+ Some(documentRelativePoint)};
+ }
+
+ CantZoomOutBehavior cantZoomOutBehavior =
+ HasNonPassiveWheelListenerOnAncestor(element)
+ ? CantZoomOutBehavior::Nothing
+ : CantZoomOutBehavior::ZoomIn;
+
+ while (element && !ShouldZoomToElement(element, aInProcessRootContentDocument,
+ rootScrollFrame, aMetrics)) {
+ element = element->GetFlattenedTreeParentElement();
+ }
+
+ if (!element) {
+ return ZoomTarget{zoomOut, cantZoomOutBehavior, Nothing(),
+ Some(documentRelativePoint)};
+ }
+
+ Maybe<CSSRect> nearestScrollClip;
+ CSSRect rect =
+ GetBoundingContentRect(element, aInProcessRootContentDocument,
+ rootScrollFrame, aMetrics, &nearestScrollClip);
+
+ // In some cases, like overflow: visible and overflowing content, the bounding
+ // client rect of the targeted element won't contain the point the user double
+ // tapped on. In that case we use the scrollable overflow rect if it contains
+ // the user point.
+ if (!rect.Contains(documentRelativePoint)) {
+ if (nsIFrame* scrolledFrame = rootScrollFrame->GetScrolledFrame()) {
+ if (nsIFrame* f = element->GetPrimaryFrame()) {
+ nsRect overflowRect = f->ScrollableOverflowRect();
+ nsLayoutUtils::TransformResult res =
+ nsLayoutUtils::TransformRect(f, scrolledFrame, overflowRect);
+ MOZ_ASSERT(res == nsLayoutUtils::TRANSFORM_SUCCEEDED ||
+ res == nsLayoutUtils::NONINVERTIBLE_TRANSFORM);
+ if (res == nsLayoutUtils::TRANSFORM_SUCCEEDED) {
+ CSSRect overflowRectCSS = CSSRect::FromAppUnits(overflowRect);
+
+ // In the case of OOP iframes, above |overflowRectCSS| in the iframe
+ // documents coords, we need to convert it into the top level coords.
+ if (!aInProcessRootContentDocument->IsTopLevelContentDocument()) {
+ overflowRectCSS.MoveBy(
+ CSSPoint::FromAppUnits(-rootScrollFrame->GetScrollPosition()));
+ overflowRectCSS =
+ aMetrics.mTransformMatrix.TransformBounds(overflowRectCSS);
+ }
+ if (nearestScrollClip.isSome()) {
+ overflowRectCSS = nearestScrollClip->Intersect(overflowRectCSS);
+ }
+ if (overflowRectCSS.Contains(documentRelativePoint)) {
+ rect = overflowRectCSS;
+ }
+ }
+ }
+ }
+ }
+
+ CSSRect elementBoundingRect = rect;
+
+ // Generally we zoom to the width of some element, but sometimes we zoom to
+ // the height. We set this to true when that happens so that we can add a
+ // vertical margin to the rect, otherwise it looks weird.
+ bool heightConstrained = false;
+
+ // If the element is taller than the visible area of the page scale
+ // the height of the |rect| so that it has the same aspect ratio as
+ // the root frame. The clipped |rect| is centered on the y value of
+ // the touch point. This allows tall narrow elements to be zoomed.
+ if (!rect.IsEmpty() && aMetrics.mVisualViewport.Width() > 0.0f &&
+ aMetrics.mVisualViewport.Height() > 0.0f) {
+ // Calculate the height of the rect if it had the same aspect ratio as
+ // aMetrics.mVisualViewport.
+ const float widthRatio = rect.Width() / aMetrics.mVisualViewport.Width();
+ float targetHeight = aMetrics.mVisualViewport.Height() * widthRatio;
+
+ // We don't want to cut off the top or bottoms of replaced elements that are
+ // square or wider in aspect ratio.
+
+ // If it's a replaced element and we would otherwise trim it's height below
+ if (IsReplacedElement(element) && targetHeight < rect.Height() &&
+ // If the target rect is at most 1.1x away from being square or wider
+ // aspect ratio
+ rect.Height() < 1.1 * rect.Width() &&
+ // and our aMetrics.mVisualViewport is wider than it is tall
+ aMetrics.mVisualViewport.Width() >= aMetrics.mVisualViewport.Height()) {
+ heightConstrained = true;
+ // Expand the width of the rect so that it fills aMetrics.mVisualViewport
+ // so that if we are already zoomed to this element then the
+ // IsRectZoomedIn call below returns true so that we zoom out. This won't
+ // change what we actually zoom to as we are just making the rect the same
+ // aspect ratio as aMetrics.mVisualViewport.
+ float targetWidth = rect.Height() * aMetrics.mVisualViewport.Width() /
+ aMetrics.mVisualViewport.Height();
+ MOZ_ASSERT(targetWidth > rect.Width());
+ if (targetWidth > rect.Width()) {
+ rect.x -= (targetWidth - rect.Width()) / 2;
+ rect.SetWidth(targetWidth);
+ // keep elementBoundingRect containing rect
+ elementBoundingRect = rect;
+ }
+
+ } else if (targetHeight < rect.Height()) {
+ // Trim the height so that the target rect has the same aspect ratio as
+ // aMetrics.mVisualViewport, centering it around the user tap point.
+ float newY = documentRelativePoint.y - (targetHeight * 0.5f);
+ if ((newY + targetHeight) > rect.YMost()) {
+ rect.MoveByY(rect.Height() - targetHeight);
+ } else if (newY > rect.Y()) {
+ rect.MoveToY(newY);
+ }
+ rect.SetHeight(targetHeight);
+ }
+ }
+
+ const CSSCoord margin = 15;
+ rect = AddHMargin(rect, margin, aMetrics.mRootScrollableRect);
+
+ if (heightConstrained) {
+ rect = AddVMargin(rect, margin, aMetrics.mRootScrollableRect);
+ }
+
+ // If the rect is already taking up most of the visible area and is
+ // stretching the width of the page, then we want to zoom out instead.
+ if (RectHasAlmostSameZoomLevel(rect, aMetrics.mVisualViewport)) {
+ return ZoomTarget{zoomOut, cantZoomOutBehavior, Nothing(),
+ Some(documentRelativePoint)};
+ }
+
+ elementBoundingRect =
+ AddHMargin(elementBoundingRect, margin, aMetrics.mRootScrollableRect);
+
+ // Unlike rect, elementBoundingRect is the full height of the element we are
+ // zooming to. If we zoom to it without a margin it can look a weird, so give
+ // it a vertical margin.
+ elementBoundingRect =
+ AddVMargin(elementBoundingRect, margin, aMetrics.mRootScrollableRect);
+
+ rect.Round();
+ elementBoundingRect.Round();
+
+ return ZoomTarget{rect, cantZoomOutBehavior, Some(elementBoundingRect),
+ Some(documentRelativePoint)};
+}
+
+std::ostream& operator<<(std::ostream& aStream,
+ const DoubleTapToZoomMetrics& aMetrics) {
+ aStream << "{ vv=" << aMetrics.mVisualViewport
+ << ", rscr=" << aMetrics.mRootScrollableRect
+ << ", transform=" << aMetrics.mTransformMatrix << " }";
+ return aStream;
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/DoubleTapToZoom.h b/gfx/layers/apz/util/DoubleTapToZoom.h
new file mode 100644
index 0000000000..3c4503405e
--- /dev/null
+++ b/gfx/layers/apz/util/DoubleTapToZoom.h
@@ -0,0 +1,85 @@
+/* -*- 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/. */
+
+#ifndef mozilla_layers_DoubleTapToZoom_h
+#define mozilla_layers_DoubleTapToZoom_h
+
+#include "Units.h"
+#include "mozilla/gfx/Matrix.h"
+
+template <class T>
+class RefPtr;
+
+namespace mozilla {
+namespace dom {
+class Document;
+}
+
+namespace layers {
+
+enum class CantZoomOutBehavior : int8_t { Nothing = 0, ZoomIn };
+
+struct ZoomTarget {
+ // The preferred target rect that we'd like to zoom in on, if possible. An
+ // empty rect means the browser should zoom out.
+ CSSRect targetRect;
+
+ // If we are asked to zoom out but cannot (due to zoom constraints, etc), then
+ // zoom in some small amount to provide feedback to the user.
+ CantZoomOutBehavior cantZoomOutBehavior = CantZoomOutBehavior::Nothing;
+
+ // If zooming all the way in on |targetRect| is not possible (for example, due
+ // to a max zoom constraint), |elementBoundingRect| may be used to inform a
+ // more optimal target scroll position (for example, we may try to maximize
+ // the area of |elementBoundingRect| that's showing, while keeping
+ // |targetRect| in view and keeping the zoom as close to the desired zoom as
+ // possible).
+ Maybe<CSSRect> elementBoundingRect;
+
+ // The document relative (ie if the content inside the root scroll frame
+ // existed without that scroll frame) pointer position at the time of the
+ // double tap or location of the double tap if we can compute it. Only used if
+ // the rest of this ZoomTarget is asking to zoom out but we are already at the
+ // minimum zoom. In which case we zoom in a small amount on this point.
+ Maybe<CSSPoint> documentRelativePointerPosition;
+};
+
+struct DoubleTapToZoomMetrics {
+ // The visual viewport rect of the top-level content document.
+ CSSRect mVisualViewport;
+ // The scrollable rect of the root scroll container of the top-level content
+ // document.
+ CSSRect mRootScrollableRect;
+ // If double-tap-to-zoom happens inside an OOP iframe, this transform matrix
+ // is the matrix converting the coordinates relative to layout viewport origin
+ // of the OOP iframe to the document origin of the top level content document.
+ // If not, this is the identity matrix.
+ CSSToCSSMatrix4x4 mTransformMatrix;
+
+ bool operator==(const DoubleTapToZoomMetrics& aOther) const {
+ return mVisualViewport == aOther.mVisualViewport &&
+ mRootScrollableRect == aOther.mRootScrollableRect &&
+ mTransformMatrix == aOther.mTransformMatrix;
+ }
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const DoubleTapToZoomMetrics& aUpdate);
+};
+
+/**
+ * For a double tap at |aPoint|, return a ZoomTarget struct with contains a rect
+ * to which the browser should zoom in response (see ZoomTarget definition for
+ * more details). An empty rect means the browser should zoom out. |aDocument|
+ * should be the in-process root content document for the content that was
+ * tapped.
+ */
+ZoomTarget CalculateRectToZoomTo(
+ const RefPtr<mozilla::dom::Document>& aInProcessRootContentDocument,
+ const CSSPoint& aPoint, const DoubleTapToZoomMetrics& aMetrics);
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_DoubleTapToZoom_h */
diff --git a/gfx/layers/apz/util/InputAPZContext.cpp b/gfx/layers/apz/util/InputAPZContext.cpp
new file mode 100644
index 0000000000..b1af5c3118
--- /dev/null
+++ b/gfx/layers/apz/util/InputAPZContext.cpp
@@ -0,0 +1,75 @@
+/* -*- 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 "InputAPZContext.h"
+
+namespace mozilla {
+namespace layers {
+
+ScrollableLayerGuid InputAPZContext::sGuid;
+uint64_t InputAPZContext::sBlockId = 0;
+nsEventStatus InputAPZContext::sApzResponse = nsEventStatus_eSentinel;
+bool InputAPZContext::sPendingLayerization = false;
+bool InputAPZContext::sRoutedToChildProcess = false;
+bool InputAPZContext::sDropped = false;
+
+/*static*/
+ScrollableLayerGuid InputAPZContext::GetTargetLayerGuid() { return sGuid; }
+
+/*static*/
+uint64_t InputAPZContext::GetInputBlockId() { return sBlockId; }
+
+/*static*/
+nsEventStatus InputAPZContext::GetApzResponse() { return sApzResponse; }
+
+/*static*/
+bool InputAPZContext::HavePendingLayerization() { return sPendingLayerization; }
+
+/*static*/
+bool InputAPZContext::WasRoutedToChildProcess() {
+ return sRoutedToChildProcess;
+}
+
+/*static*/
+bool InputAPZContext::WasDropped() { return sDropped; }
+
+InputAPZContext::InputAPZContext(const ScrollableLayerGuid& aGuid,
+ const uint64_t& aBlockId,
+ const nsEventStatus& aApzResponse,
+ bool aPendingLayerization)
+ : mOldGuid(sGuid),
+ mOldBlockId(sBlockId),
+ mOldApzResponse(sApzResponse),
+ mOldPendingLayerization(sPendingLayerization),
+ mOldRoutedToChildProcess(sRoutedToChildProcess),
+ mOldDropped(sDropped) {
+ sGuid = aGuid;
+ sBlockId = aBlockId;
+ sApzResponse = aApzResponse;
+ sPendingLayerization = aPendingLayerization;
+ sRoutedToChildProcess = false;
+ sDropped = false;
+}
+
+InputAPZContext::~InputAPZContext() {
+ sGuid = mOldGuid;
+ sBlockId = mOldBlockId;
+ sApzResponse = mOldApzResponse;
+ sPendingLayerization = mOldPendingLayerization;
+ sRoutedToChildProcess = mOldRoutedToChildProcess;
+ sDropped = mOldDropped;
+}
+
+/*static*/
+void InputAPZContext::SetRoutedToChildProcess() {
+ sRoutedToChildProcess = true;
+}
+
+/*static*/
+void InputAPZContext::SetDropped() { sDropped = true; }
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/InputAPZContext.h b/gfx/layers/apz/util/InputAPZContext.h
new file mode 100644
index 0000000000..2793607f04
--- /dev/null
+++ b/gfx/layers/apz/util/InputAPZContext.h
@@ -0,0 +1,76 @@
+/* -*- 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/. */
+
+#ifndef mozilla_layers_InputAPZContext_h
+#define mozilla_layers_InputAPZContext_h
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/ScrollableLayerGuid.h"
+
+namespace mozilla {
+namespace layers {
+
+// InputAPZContext is used to communicate various pieces of information
+// around the codebase without having to plumb it through lots of functions
+// and codepaths. Conceptually it is attached to a WidgetInputEvent that is
+// relevant to APZ.
+//
+// There are two types of information bits propagated using this class. One
+// type is propagated "downwards" (from a process entry point like nsBaseWidget
+// or BrowserChild) into deeper code that is run during complicated operations
+// like event dispatch. The other type is information that is propagated
+// "upwards", from the deeper code back to the entry point.
+class MOZ_STACK_CLASS InputAPZContext {
+ private:
+ // State that is propagated downwards from InputAPZContext creation into
+ // "deeper" code.
+ static ScrollableLayerGuid sGuid;
+ static uint64_t sBlockId;
+ static nsEventStatus sApzResponse;
+ static bool sPendingLayerization;
+
+ // State that is set in deeper code and propagated upwards.
+ static bool sRoutedToChildProcess;
+ // True if the WidgetInputEvent was dropped (which means the event wasn't
+ // dispatched) on the main-thread.
+ static bool sDropped;
+
+ public:
+ // Functions to access downwards-propagated data
+ static ScrollableLayerGuid GetTargetLayerGuid();
+ static uint64_t GetInputBlockId();
+ static nsEventStatus GetApzResponse();
+ static bool HavePendingLayerization();
+
+ // Functions to access upwards-propagated data
+ static bool WasRoutedToChildProcess();
+
+ static bool WasDropped();
+
+ // Constructor sets the data to be propagated downwards
+ InputAPZContext(const ScrollableLayerGuid& aGuid, const uint64_t& aBlockId,
+ const nsEventStatus& aApzResponse,
+ bool aPendingLayerization = false);
+ ~InputAPZContext();
+
+ // Functions to set data to be propagated upwards
+ static void SetRoutedToChildProcess();
+ static void SetDropped();
+
+ private:
+ ScrollableLayerGuid mOldGuid;
+ uint64_t mOldBlockId;
+ nsEventStatus mOldApzResponse;
+ bool mOldPendingLayerization;
+
+ bool mOldRoutedToChildProcess;
+ bool mOldDropped;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_InputAPZContext_h */
diff --git a/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp b/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp
new file mode 100644
index 0000000000..eb456fa243
--- /dev/null
+++ b/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp
@@ -0,0 +1,48 @@
+/* -*- 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 "ScrollLinkedEffectDetector.h"
+
+#include "mozilla/dom/Document.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace layers {
+
+uint32_t ScrollLinkedEffectDetector::sDepth = 0;
+bool ScrollLinkedEffectDetector::sFoundScrollLinkedEffect = false;
+
+/* static */
+void ScrollLinkedEffectDetector::PositioningPropertyMutated() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (sDepth > 0) {
+ // We are inside a scroll event dispatch
+ sFoundScrollLinkedEffect = true;
+ }
+}
+
+ScrollLinkedEffectDetector::ScrollLinkedEffectDetector(
+ dom::Document* aDoc, const TimeStamp& aTimeStamp)
+ : mDocument(aDoc), mTimeStamp(aTimeStamp) {
+ MOZ_ASSERT(NS_IsMainThread());
+ sDepth++;
+}
+
+ScrollLinkedEffectDetector::~ScrollLinkedEffectDetector() {
+ sDepth--;
+ if (sDepth == 0) {
+ // We have exited all (possibly-nested) scroll event dispatches,
+ // record whether or not we found an effect, and reset state
+ if (sFoundScrollLinkedEffect) {
+ mDocument->ReportHasScrollLinkedEffect(mTimeStamp);
+ sFoundScrollLinkedEffect = false;
+ }
+ }
+}
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/ScrollLinkedEffectDetector.h b/gfx/layers/apz/util/ScrollLinkedEffectDetector.h
new file mode 100644
index 0000000000..4568fe649b
--- /dev/null
+++ b/gfx/layers/apz/util/ScrollLinkedEffectDetector.h
@@ -0,0 +1,48 @@
+/* -*- 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/. */
+
+#ifndef mozilla_layers_ScrollLinkedEffectDetector_h
+#define mozilla_layers_ScrollLinkedEffectDetector_h
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+
+namespace dom {
+class Document;
+}
+
+namespace layers {
+
+// ScrollLinkedEffectDetector is used to detect the existence of a scroll-linked
+// effect on a webpage. Generally speaking, a scroll-linked effect is something
+// on the page that animates or changes with respect to the scroll position.
+// Content authors usually rely on running some JS in response to the scroll
+// event in order to implement such effects, and therefore it tends to be laggy
+// or work improperly with APZ enabled. This class helps us detect such an
+// effect so that we can warn the author and/or take other preventative
+// measures.
+class MOZ_STACK_CLASS ScrollLinkedEffectDetector final {
+ private:
+ static uint32_t sDepth;
+ static bool sFoundScrollLinkedEffect;
+
+ public:
+ static void PositioningPropertyMutated();
+
+ ScrollLinkedEffectDetector(dom::Document*, const TimeStamp& aTimeStamp);
+ ~ScrollLinkedEffectDetector();
+
+ private:
+ RefPtr<dom::Document> mDocument;
+ TimeStamp mTimeStamp;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_ScrollLinkedEffectDetector_h */
diff --git a/gfx/layers/apz/util/ScrollingInteractionContext.cpp b/gfx/layers/apz/util/ScrollingInteractionContext.cpp
new file mode 100644
index 0000000000..1a92a9eb07
--- /dev/null
+++ b/gfx/layers/apz/util/ScrollingInteractionContext.cpp
@@ -0,0 +1,29 @@
+/* -*- 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 "ScrollingInteractionContext.h"
+
+namespace mozilla::layers {
+
+/*static*/
+bool ScrollingInteractionContext::sScrollingToAnchor = false;
+
+/*static*/
+bool ScrollingInteractionContext::IsScrollingToAnchor() {
+ return sScrollingToAnchor;
+}
+
+ScrollingInteractionContext::ScrollingInteractionContext(
+ bool aScrollingToAnchor)
+ : mOldScrollingToAnchor(sScrollingToAnchor) {
+ sScrollingToAnchor = aScrollingToAnchor;
+}
+
+ScrollingInteractionContext::~ScrollingInteractionContext() {
+ sScrollingToAnchor = mOldScrollingToAnchor;
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/apz/util/ScrollingInteractionContext.h b/gfx/layers/apz/util/ScrollingInteractionContext.h
new file mode 100644
index 0000000000..cae953008b
--- /dev/null
+++ b/gfx/layers/apz/util/ScrollingInteractionContext.h
@@ -0,0 +1,40 @@
+/* -*- 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/. */
+
+#ifndef mozilla_layers_ScrollingInteractionContext_h
+#define mozilla_layers_ScrollingInteractionContext_h
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/layers/ScrollableLayerGuid.h"
+
+namespace mozilla {
+namespace layers {
+
+// The ScrollingInteractionContext is used to store minor details of the
+// current scrolling interaction on the stack to avoid having to pass them
+// though the callstack
+class MOZ_STACK_CLASS ScrollingInteractionContext {
+ private:
+ static bool sScrollingToAnchor;
+
+ public:
+ // Functions to access downwards-propagated data
+ static bool IsScrollingToAnchor();
+
+ // Constructor sets the data to be propagated downwards
+ explicit ScrollingInteractionContext(bool aScrollingToAnchor);
+
+ // Destructor restores the previous state
+ ~ScrollingInteractionContext();
+
+ private:
+ bool mOldScrollingToAnchor;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_ScrollingInteractionContext_h */
diff --git a/gfx/layers/apz/util/TouchActionHelper.cpp b/gfx/layers/apz/util/TouchActionHelper.cpp
new file mode 100644
index 0000000000..4598e30a6a
--- /dev/null
+++ b/gfx/layers/apz/util/TouchActionHelper.cpp
@@ -0,0 +1,131 @@
+/* -*- 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 "TouchActionHelper.h"
+
+#include "mozilla/layers/IAPZCTreeManager.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/TouchEvents.h"
+#include "nsContainerFrame.h"
+#include "nsIFrameInlines.h"
+#include "nsIScrollableFrame.h"
+#include "nsLayoutUtils.h"
+
+namespace mozilla::layers {
+
+static void UpdateAllowedBehavior(StyleTouchAction aTouchActionValue,
+ bool aConsiderPanning,
+ TouchBehaviorFlags& aOutBehavior) {
+ if (aTouchActionValue != StyleTouchAction::AUTO) {
+ // Double-tap-zooming need property value AUTO
+ aOutBehavior &= ~AllowedTouchBehavior::ANIMATING_ZOOM;
+ if (aTouchActionValue != StyleTouchAction::MANIPULATION &&
+ !(aTouchActionValue & StyleTouchAction::PINCH_ZOOM)) {
+ // Pinch-zooming needs value AUTO or MANIPULATION, or the PINCH_ZOOM bit
+ // set
+ aOutBehavior &= ~AllowedTouchBehavior::PINCH_ZOOM;
+ }
+ }
+
+ if (aConsiderPanning) {
+ if (aTouchActionValue == StyleTouchAction::NONE) {
+ aOutBehavior &= ~AllowedTouchBehavior::VERTICAL_PAN;
+ aOutBehavior &= ~AllowedTouchBehavior::HORIZONTAL_PAN;
+ }
+
+ // Values pan-x and pan-y set at the same time to the same element do not
+ // affect panning constraints. Therefore we need to check whether pan-x is
+ // set without pan-y and the same for pan-y.
+ if ((aTouchActionValue & StyleTouchAction::PAN_X) &&
+ !(aTouchActionValue & StyleTouchAction::PAN_Y)) {
+ aOutBehavior &= ~AllowedTouchBehavior::VERTICAL_PAN;
+ } else if ((aTouchActionValue & StyleTouchAction::PAN_Y) &&
+ !(aTouchActionValue & StyleTouchAction::PAN_X)) {
+ aOutBehavior &= ~AllowedTouchBehavior::HORIZONTAL_PAN;
+ }
+ }
+}
+
+static TouchBehaviorFlags GetAllowedTouchBehaviorForPoint(
+ nsIWidget* aWidget, RelativeTo aRootFrame,
+ const LayoutDeviceIntPoint& aPoint) {
+ nsPoint relativePoint =
+ nsLayoutUtils::GetEventCoordinatesRelativeTo(aWidget, aPoint, aRootFrame);
+
+ nsIFrame* target = nsLayoutUtils::GetFrameForPoint(aRootFrame, relativePoint);
+
+ return TouchActionHelper::GetAllowedTouchBehaviorForFrame(target);
+}
+
+nsTArray<TouchBehaviorFlags> TouchActionHelper::GetAllowedTouchBehavior(
+ nsIWidget* aWidget, dom::Document* aDocument,
+ const WidgetTouchEvent& aEvent) {
+ nsTArray<TouchBehaviorFlags> flags;
+ if (!aWidget || !aDocument) {
+ return flags;
+ }
+ if (PresShell* presShell = aDocument->GetPresShell()) {
+ if (nsIFrame* rootFrame = presShell->GetRootFrame()) {
+ for (const auto& touch : aEvent.mTouches) {
+ flags.AppendElement(GetAllowedTouchBehaviorForPoint(
+ aWidget, RelativeTo{rootFrame, ViewportType::Visual},
+ touch->mRefPoint));
+ }
+ }
+ }
+ return flags;
+}
+
+TouchBehaviorFlags TouchActionHelper::GetAllowedTouchBehaviorForFrame(
+ nsIFrame* aFrame) {
+ TouchBehaviorFlags behavior = AllowedTouchBehavior::VERTICAL_PAN |
+ AllowedTouchBehavior::HORIZONTAL_PAN |
+ AllowedTouchBehavior::PINCH_ZOOM |
+ AllowedTouchBehavior::ANIMATING_ZOOM;
+
+ if (!aFrame) {
+ return behavior;
+ }
+
+ nsIScrollableFrame* nearestScrollableParent =
+ nsLayoutUtils::GetNearestScrollableFrame(aFrame, 0);
+ nsIFrame* nearestScrollableFrame = do_QueryFrame(nearestScrollableParent);
+
+ // We're walking up the DOM tree until we meet the element with touch behavior
+ // and accumulating touch-action restrictions of all elements in this chain.
+ // The exact quote from the spec, that clarifies more:
+ // To determine the effect of a touch, find the nearest ancestor (starting
+ // from the element itself) that has a default touch behavior. Then examine
+ // the touch-action property of each element between the hit tested element
+ // and the element with the default touch behavior (including both the hit
+ // tested element and the element with the default touch behavior). If the
+ // touch-action property of any of those elements disallows the default touch
+ // behavior, do nothing. Otherwise allow the element to start considering the
+ // touch for the purposes of executing a default touch behavior.
+
+ // Currently we support only two touch behaviors: panning and zooming.
+ // For panning we walk up until we meet the first scrollable element (the
+ // element that supports panning) or root element. For zooming we walk up
+ // until the root element since Firefox currently supports only zooming of the
+ // root frame but not the subframes.
+
+ bool considerPanning = true;
+
+ for (nsIFrame* frame = aFrame; frame && frame->GetContent() && behavior;
+ frame = frame->GetInFlowParent()) {
+ UpdateAllowedBehavior(frame->UsedTouchAction(), considerPanning, behavior);
+
+ if (frame == nearestScrollableFrame) {
+ // We met the scrollable element, after it we shouldn't consider
+ // touch-action values for the purpose of panning but only for zooming.
+ considerPanning = false;
+ }
+ }
+
+ return behavior;
+}
+
+} // namespace mozilla::layers
diff --git a/gfx/layers/apz/util/TouchActionHelper.h b/gfx/layers/apz/util/TouchActionHelper.h
new file mode 100644
index 0000000000..b83d0d9ecb
--- /dev/null
+++ b/gfx/layers/apz/util/TouchActionHelper.h
@@ -0,0 +1,46 @@
+/* -*- 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/. */
+
+#ifndef __mozilla_layers_TouchActionHelper_h__
+#define __mozilla_layers_TouchActionHelper_h__
+
+#include "mozilla/layers/LayersTypes.h" // for TouchBehaviorFlags
+#include "RelativeTo.h" // for RelativeTo
+
+class nsIWidget;
+namespace mozilla {
+
+namespace dom {
+class Document;
+} // namespace dom
+
+class WidgetTouchEvent;
+} // namespace mozilla
+
+namespace mozilla::layers {
+
+/*
+ * Helper class to figure out the allowed touch behavior for frames, as per
+ * the touch-action spec.
+ */
+class TouchActionHelper {
+ public:
+ /*
+ * Performs hit testing on content, finds frame that corresponds to the touch
+ * points of aEvent and retrieves touch-action CSS property value from it
+ * according the rules specified in the spec:
+ * http://www.w3.org/TR/pointerevents/#the-touch-action-css-property.
+ */
+ static nsTArray<TouchBehaviorFlags> GetAllowedTouchBehavior(
+ nsIWidget* aWidget, dom::Document* aDocument,
+ const WidgetTouchEvent& aPoint);
+
+ static TouchBehaviorFlags GetAllowedTouchBehaviorForFrame(nsIFrame* aFrame);
+};
+
+} // namespace mozilla::layers
+
+#endif /*__mozilla_layers_TouchActionHelper_h__ */
diff --git a/gfx/layers/apz/util/TouchCounter.cpp b/gfx/layers/apz/util/TouchCounter.cpp
new file mode 100644
index 0000000000..9e4d6f1ba6
--- /dev/null
+++ b/gfx/layers/apz/util/TouchCounter.cpp
@@ -0,0 +1,74 @@
+/* -*- 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 "TouchCounter.h"
+
+#include "InputData.h"
+#include "mozilla/TouchEvents.h"
+
+namespace mozilla {
+namespace layers {
+
+TouchCounter::TouchCounter() : mActiveTouchCount(0) {}
+
+void TouchCounter::Update(const MultiTouchInput& aInput) {
+ switch (aInput.mType) {
+ case MultiTouchInput::MULTITOUCH_START:
+ // touch-start event contains all active touches of the current session
+ mActiveTouchCount = aInput.mTouches.Length();
+ break;
+ case MultiTouchInput::MULTITOUCH_END:
+ if (mActiveTouchCount >= aInput.mTouches.Length()) {
+ // touch-end event contains only released touches
+ mActiveTouchCount -= aInput.mTouches.Length();
+ } else {
+ NS_WARNING("Got an unexpected touchend");
+ mActiveTouchCount = 0;
+ }
+ break;
+ case MultiTouchInput::MULTITOUCH_CANCEL:
+ mActiveTouchCount = 0;
+ break;
+ case MultiTouchInput::MULTITOUCH_MOVE:
+ break;
+ }
+}
+
+void TouchCounter::Update(const WidgetTouchEvent& aEvent) {
+ switch (aEvent.mMessage) {
+ case eTouchStart:
+ // touch-start event contains all active touches of the current session
+ mActiveTouchCount = aEvent.mTouches.Length();
+ break;
+ case eTouchEnd: {
+ // touch-end contains all touches, but ones being lifted are marked as
+ // changed
+ uint32_t liftedTouches = 0;
+ for (const auto& touch : aEvent.mTouches) {
+ if (touch->mChanged) {
+ liftedTouches++;
+ }
+ }
+ if (mActiveTouchCount >= liftedTouches) {
+ mActiveTouchCount -= liftedTouches;
+ } else {
+ NS_WARNING("Got an unexpected touchend");
+ mActiveTouchCount = 0;
+ }
+ break;
+ }
+ case eTouchCancel:
+ mActiveTouchCount = 0;
+ break;
+ default:
+ break;
+ }
+}
+
+uint32_t TouchCounter::GetActiveTouchCount() const { return mActiveTouchCount; }
+
+} // namespace layers
+} // namespace mozilla
diff --git a/gfx/layers/apz/util/TouchCounter.h b/gfx/layers/apz/util/TouchCounter.h
new file mode 100644
index 0000000000..c13f475355
--- /dev/null
+++ b/gfx/layers/apz/util/TouchCounter.h
@@ -0,0 +1,36 @@
+/* -*- 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/. */
+
+#ifndef mozilla_layers_TouchCounter_h
+#define mozilla_layers_TouchCounter_h
+
+#include "mozilla/EventForwards.h"
+
+namespace mozilla {
+
+class MultiTouchInput;
+
+namespace layers {
+
+// TouchCounter simply tracks the number of active touch points. Feed it
+// your input events to update the internal state. Generally you should
+// only be calling one of the Update functions, depending on which type
+// of touch inputs you have access to.
+class TouchCounter {
+ public:
+ TouchCounter();
+ void Update(const MultiTouchInput& aInput);
+ void Update(const WidgetTouchEvent& aEvent);
+ uint32_t GetActiveTouchCount() const;
+
+ private:
+ uint32_t mActiveTouchCount;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif /* mozilla_layers_TouchCounter_h */