diff options
Diffstat (limited to 'gfx/layers/apz/util')
23 files changed, 3953 insertions, 0 deletions
diff --git a/gfx/layers/apz/util/APZCCallbackHelper.cpp b/gfx/layers/apz/util/APZCCallbackHelper.cpp new file mode 100644 index 000000000..3f33a59e4 --- /dev/null +++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp @@ -0,0 +1,942 @@ +/* -*- 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/. */ + +#include "APZCCallbackHelper.h" + +#include "TouchActionHelper.h" +#include "gfxPlatform.h" // For gfxPlatform::UseTiling +#include "gfxPrefs.h" +#include "LayersLogging.h" // For Stringify +#include "mozilla/dom/Element.h" +#include "mozilla/dom/TabParent.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/layers/LayerTransactionChild.h" +#include "mozilla/layers/ShadowLayers.h" +#include "mozilla/TouchEvents.h" +#include "nsContentUtils.h" +#include "nsContainerFrame.h" +#include "nsIScrollableFrame.h" +#include "nsLayoutUtils.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIContent.h" +#include "nsIDocument.h" +#include "nsIDOMWindow.h" +#include "nsIDOMWindowUtils.h" +#include "nsRefreshDriver.h" +#include "nsString.h" +#include "nsView.h" +#include "Layers.h" + +#define APZCCH_LOG(...) +// #define APZCCH_LOG(...) printf_stderr("APZCCH: " __VA_ARGS__) + +namespace mozilla { +namespace layers { + +using dom::TabParent; + +uint64_t APZCCallbackHelper::sLastTargetAPZCNotificationInputBlock = uint64_t(-1); + +void +APZCCallbackHelper::AdjustDisplayPortForScrollDelta( + mozilla::layers::FrameMetrics& aFrameMetrics, + const CSSPoint& aActualScrollOffset) +{ + // Correct the display-port by the difference between the requested scroll + // offset and the resulting scroll offset after setting the requested value. + ScreenPoint shift = + (aFrameMetrics.GetScrollOffset() - aActualScrollOffset) * + aFrameMetrics.DisplayportPixelsPerCSSPixel(); + ScreenMargin margins = aFrameMetrics.GetDisplayPortMargins(); + margins.left -= shift.x; + margins.right += shift.x; + margins.top -= shift.y; + margins.bottom += shift.y; + aFrameMetrics.SetDisplayPortMargins(margins); +} + +static void +RecenterDisplayPort(mozilla::layers::FrameMetrics& aFrameMetrics) +{ + ScreenMargin margins = aFrameMetrics.GetDisplayPortMargins(); + margins.right = margins.left = margins.LeftRight() / 2; + margins.top = margins.bottom = margins.TopBottom() / 2; + aFrameMetrics.SetDisplayPortMargins(margins); +} + +static CSSPoint +ScrollFrameTo(nsIScrollableFrame* aFrame, const FrameMetrics& aMetrics, bool& aSuccessOut) +{ + aSuccessOut = false; + CSSPoint targetScrollPosition = aMetrics.GetScrollOffset(); + + 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 (!aMetrics.GetScrollOffsetUpdated()) { + return geckoScrollPosition; + } + + // If the frame is overflow:hidden on a particular axis, we don't want to allow + // user-driven scroll on that axis. Simply set the scroll position on that axis + // to whatever it already is. Note that this will leave the APZ's async scroll + // position out of sync with the gecko scroll position, but APZ can deal with that + // (by design). Note also that when we run into this case, even if both axes + // have overflow:hidden, we want to set aSuccessOut to true, so that the displayport + // follows the async scroll position rather than the gecko scroll position. + if (aFrame->GetScrollbarStyles().mVertical == NS_STYLE_OVERFLOW_HIDDEN) { + targetScrollPosition.y = geckoScrollPosition.y; + } + if (aFrame->GetScrollbarStyles().mHorizontal == NS_STYLE_OVERFLOW_HIDDEN) { + targetScrollPosition.x = geckoScrollPosition.x; + } + + // 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) { + aFrame->ScrollToCSSPixelsApproximate(targetScrollPosition, nsGkAtoms::apz); + 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 |aMetrics|. + * The scroll offset in |aMetrics| is updated to reflect the actual scroll + * position. + * The displayport stored in |aMetrics| and the callback-transform stored on + * the content are updated to reflect any difference between the requested + * and actual scroll positions. + */ +static void +ScrollFrame(nsIContent* aContent, + FrameMetrics& aMetrics) +{ + // Scroll the window to the desired spot + nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aMetrics.GetScrollId()); + if (sf) { + sf->ResetScrollInfoIfGeneration(aMetrics.GetScrollGeneration()); + sf->SetScrollableByAPZ(!aMetrics.IsScrollInfoLayer()); + } + bool scrollUpdated = false; + CSSPoint apzScrollOffset = aMetrics.GetScrollOffset(); + CSSPoint actualScrollOffset = ScrollFrameTo(sf, aMetrics, scrollUpdated); + + if (scrollUpdated) { + if (aMetrics.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 mScrollOffset and the + // actual scroll offset. + APZCCallbackHelper::AdjustDisplayPortForScrollDelta(aMetrics, 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. + RecenterDisplayPort(aMetrics); + } + + aMetrics.SetScrollOffset(actualScrollOffset); + + // 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() != aMetrics.GetScrollGeneration() && nsLayoutUtils::CanScrollOriginClobberApz(sf->LastScrollOrigin()); + if (aContent && !mainThreadScrollChanged) { + CSSPoint scrollDelta = apzScrollOffset - actualScrollOffset; + aContent->SetProperty(nsGkAtoms::apzCallbackTransform, new CSSPoint(scrollDelta), + nsINode::DeleteProperty<CSSPoint>); + } +} + +static void +SetDisplayPortMargins(nsIPresShell* aPresShell, + nsIContent* aContent, + const FrameMetrics& aMetrics) +{ + if (!aContent) { + return; + } + + bool hadDisplayPort = nsLayoutUtils::HasDisplayPort(aContent); + ScreenMargin margins = aMetrics.GetDisplayPortMargins(); + nsLayoutUtils::SetDisplayPortMargins(aContent, aPresShell, margins, 0); + if (!hadDisplayPort) { + nsLayoutUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors( + aContent->GetPrimaryFrame(), nsLayoutUtils::RepaintMode::Repaint); + } + + CSSRect baseCSS = aMetrics.CalculateCompositedRectInCssPixels(); + nsRect base(0, 0, + baseCSS.width * nsPresContext::AppUnitsPerCSSPixel(), + baseCSS.height * nsPresContext::AppUnitsPerCSSPixel()); + nsLayoutUtils::SetDisplayPortBaseIfNotSet(aContent, base); +} + +static already_AddRefed<nsIPresShell> +GetPresShell(const nsIContent* aContent) +{ + nsCOMPtr<nsIPresShell> result; + if (nsIDocument* doc = aContent->GetComposedDoc()) { + result = doc->GetShell(); + } + return result.forget(); +} + +static void +SetPaintRequestTime(nsIContent* aContent, const TimeStamp& aPaintRequestTime) +{ + aContent->SetProperty(nsGkAtoms::paintRequestTime, + new TimeStamp(aPaintRequestTime), + nsINode::DeleteProperty<TimeStamp>); +} + +void +APZCCallbackHelper::UpdateRootFrame(FrameMetrics& aMetrics) +{ + if (aMetrics.GetScrollId() == FrameMetrics::NULL_SCROLL_ID) { + return; + } + nsIContent* content = nsLayoutUtils::FindContentFor(aMetrics.GetScrollId()); + if (!content) { + return; + } + + nsCOMPtr<nsIPresShell> shell = GetPresShell(content); + if (!shell || aMetrics.GetPresShellId() != shell->GetPresShellId()) { + return; + } + + MOZ_ASSERT(aMetrics.GetUseDisplayPortMargins()); + + if (gfxPrefs::APZAllowZooming()) { + // 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 SPCSPS is bad because it can cause a stale value + // to be returned by window.innerWidth/innerHeight (see bug 1187792). + + float presShellResolution = shell->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. + if (!FuzzyEqualsMultiplicative(presShellResolution, aMetrics.GetPresShellResolution())) { + return; + } + + // The pres shell resolution is updated by the the async zoom since the + // last paint. + presShellResolution = aMetrics.GetPresShellResolution() + * aMetrics.GetAsyncZoom().scale; + shell->SetResolutionAndScaleTo(presShellResolution); + } + + // 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. + ScrollFrame(content, aMetrics); + + SetDisplayPortMargins(shell, content, aMetrics); + SetPaintRequestTime(content, aMetrics.GetPaintRequestTime()); +} + +void +APZCCallbackHelper::UpdateSubFrame(FrameMetrics& aMetrics) +{ + if (aMetrics.GetScrollId() == FrameMetrics::NULL_SCROLL_ID) { + return; + } + nsIContent* content = nsLayoutUtils::FindContentFor(aMetrics.GetScrollId()); + if (!content) { + return; + } + + MOZ_ASSERT(aMetrics.GetUseDisplayPortMargins()); + + // We don't currently support zooming for subframes, so nothing extra + // needs to be done beyond the tasks common to this and UpdateRootFrame. + ScrollFrame(content, aMetrics); + if (nsCOMPtr<nsIPresShell> shell = GetPresShell(content)) { + SetDisplayPortMargins(shell, content, aMetrics); + } + SetPaintRequestTime(content, aMetrics.GetPaintRequestTime()); +} + +bool +APZCCallbackHelper::GetOrCreateScrollIdentifiers(nsIContent* aContent, + uint32_t* aPresShellIdOut, + FrameMetrics::ViewID* aViewIdOut) +{ + if (!aContent) { + return false; + } + *aViewIdOut = nsLayoutUtils::FindOrCreateIDFor(aContent); + if (nsCOMPtr<nsIPresShell> shell = GetPresShell(aContent)) { + *aPresShellIdOut = shell->GetPresShellId(); + return true; + } + return false; +} + +void +APZCCallbackHelper::InitializeRootDisplayport(nsIPresShell* 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; + FrameMetrics::ViewID viewId; + if (APZCCallbackHelper::GetOrCreateScrollIdentifiers(content, &presShellId, &viewId)) { + // Note that the base rect that goes with these margins is set in + // nsRootBoxFrame::BuildDisplayList. + nsLayoutUtils::SetDisplayPortMargins(content, aPresShell, ScreenMargin(), 0, + nsLayoutUtils::RepaintMode::DoNotRepaint); + nsLayoutUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors( + content->GetPrimaryFrame(), nsLayoutUtils::RepaintMode::DoNotRepaint); + } +} + +nsPresContext* +APZCCallbackHelper::GetPresContextForContent(nsIContent* aContent) +{ + nsIDocument* doc = aContent->GetComposedDoc(); + if (!doc) { + return nullptr; + } + nsIPresShell* shell = doc->GetShell(); + if (!shell) { + return nullptr; + } + return shell->GetPresContext(); +} + +nsIPresShell* +APZCCallbackHelper::GetRootContentDocumentPresShellForContent(nsIContent* aContent) +{ + nsPresContext* context = GetPresContextForContent(aContent); + if (!context) { + return nullptr; + } + context = context->GetToplevelContentDocumentPresContext(); + if (!context) { + return nullptr; + } + return context->PresShell(); +} + +static nsIPresShell* +GetRootDocumentPresShell(nsIContent* aContent) +{ + nsIDocument* doc = aContent->GetComposedDoc(); + if (!doc) { + return nullptr; + } + nsIPresShell* shell = doc->GetShell(); + if (!shell) { + return nullptr; + } + nsPresContext* context = shell->GetPresContext(); + if (!context) { + return nullptr; + } + context = context->GetRootPresContext(); + if (!context) { + return nullptr; + } + return context->PresShell(); +} + +CSSPoint +APZCCallbackHelper::ApplyCallbackTransform(const CSSPoint& aInput, + const ScrollableLayerGuid& aGuid) +{ + CSSPoint input = aInput; + if (aGuid.mScrollId == FrameMetrics::NULL_SCROLL_ID) { + return input; + } + nsCOMPtr<nsIContent> content = nsLayoutUtils::FindContentFor(aGuid.mScrollId); + if (!content) { + return input; + } + + // First, scale inversely by the root content document's pres shell + // resolution to cancel the scale-to-resolution transform that the + // compositor adds to the layer with the pres shell resolution. The points + // sent to Gecko by APZ don't have this transform unapplied (unlike other + // compositor-side transforms) because APZ doesn't know about it. + if (nsIPresShell* shell = GetRootDocumentPresShell(content)) { + input = input / shell->GetResolution(); + } + + // This represents any resolution on the Root Content Document (RCD) + // that's not on the Root Document (RD). That is, on platforms where + // RCD == RD, it's 1, and on platforms where RCD != RD, it's the RCD + // resolution. 'input' has this resolution applied, but the scroll + // delta retrieved below do not, so we need to apply them to the + // delta before adding the delta to 'input'. (Technically, deltas + // from scroll frames outside the RCD would already have this + // resolution applied, but we don't have such scroll frames in + // practice.) + float nonRootResolution = 1.0f; + if (nsIPresShell* shell = GetRootContentDocumentPresShellForContent(content)) { + nonRootResolution = shell->GetCumulativeNonRootScaleResolution(); + } + // Now apply the callback-transform. This is only approximately correct, + // see the comment on GetCumulativeApzCallbackTransform for details. + CSSPoint transform = nsLayoutUtils::GetCumulativeApzCallbackTransform(content->GetPrimaryFrame()); + return input + transform * nonRootResolution; +} + +LayoutDeviceIntPoint +APZCCallbackHelper::ApplyCallbackTransform(const LayoutDeviceIntPoint& aPoint, + const ScrollableLayerGuid& aGuid, + const CSSToLayoutDeviceScale& aScale) +{ + LayoutDevicePoint point = LayoutDevicePoint(aPoint.x, aPoint.y); + point = ApplyCallbackTransform(point / aScale, aGuid) * aScale; + return LayoutDeviceIntPoint::Round(point); +} + +void +APZCCallbackHelper::ApplyCallbackTransform(WidgetEvent& aEvent, + const ScrollableLayerGuid& aGuid, + const CSSToLayoutDeviceScale& aScale) +{ + if (aEvent.AsTouchEvent()) { + WidgetTouchEvent& event = *(aEvent.AsTouchEvent()); + for (size_t i = 0; i < event.mTouches.Length(); i++) { + event.mTouches[i]->mRefPoint = ApplyCallbackTransform( + event.mTouches[i]->mRefPoint, aGuid, aScale); + } + } else { + aEvent.mRefPoint = ApplyCallbackTransform(aEvent.mRefPoint, aGuid, aScale); + } +} + +nsEventStatus +APZCCallbackHelper::DispatchWidgetEvent(WidgetGUIEvent& aEvent) +{ + nsEventStatus status = nsEventStatus_eConsumeNoDefault; + if (aEvent.mWidget) { + aEvent.mWidget->DispatchEvent(&aEvent, status); + } + return status; +} + +nsEventStatus +APZCCallbackHelper::DispatchSynthesizedMouseEvent(EventMessage aMsg, + uint64_t aTime, + 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.mTime = aTime; + event.button = WidgetMouseEvent::eLeftButton; + event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_TOUCH; + if (aMsg == eMouseLongTap) { + event.mFlags.mOnlyChromeDispatch = true; + } + event.mIgnoreRootScrollFrame = 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); +} + +bool +APZCCallbackHelper::DispatchMouseEvent(const nsCOMPtr<nsIPresShell>& aPresShell, + const nsString& aType, + const CSSPoint& aPoint, + int32_t aButton, + int32_t aClickCount, + int32_t aModifiers, + bool aIgnoreRootScrollFrame, + unsigned short aInputSourceArg) +{ + NS_ENSURE_TRUE(aPresShell, true); + + bool defaultPrevented = false; + nsContentUtils::SendMouseEvent(aPresShell, aType, aPoint.x, aPoint.y, + aButton, nsIDOMWindowUtils::MOUSE_BUTTONS_NOT_SPECIFIED, aClickCount, + aModifiers, aIgnoreRootScrollFrame, 0, aInputSourceArg, false, + &defaultPrevented, false, /* aIsWidgetEventSynthesized = */ false); + return defaultPrevented; +} + + +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", + Stringify(aPoint).c_str()); + int time = 0; + DispatchSynthesizedMouseEvent(eMouseMove, time, aPoint, aModifiers, aClickCount, aWidget); + DispatchSynthesizedMouseEvent(eMouseDown, time, aPoint, aModifiers, aClickCount, aWidget); + DispatchSynthesizedMouseEvent(eMouseUp, time, 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 (nsIPresShell* shell = view->GetPresShell()) { + MOZ_ASSERT(shell->GetDocument()); + return shell->GetDocument()->GetDocumentElement(); + } + } + return nullptr; +} + +static nsIFrame* +UpdateRootFrameForTouchTargetDocument(nsIFrame* aRootFrame) +{ +#if defined(MOZ_WIDGET_ANDROID) + // Re-target so that the hit test is performed relative to the frame for the + // Root Content Document instead of the Root Document which are different in + // Android. See bug 1229752 comment 16 for an explanation of why this is necessary. + if (nsIDocument* doc = aRootFrame->PresContext()->PresShell()->GetTouchEventTargetDocument()) { + if (nsIPresShell* shell = doc->GetShell()) { + if (nsIFrame* frame = shell->GetRootFrame()) { + return frame; + } + } + } +#endif + return aRootFrame; +} + +// 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 a displayport was set. +static bool +PrepareForSetTargetAPZCNotification(nsIWidget* aWidget, + const ScrollableLayerGuid& aGuid, + nsIFrame* aRootFrame, + const LayoutDeviceIntPoint& aRefPoint, + nsTArray<ScrollableLayerGuid>* aTargets) +{ + ScrollableLayerGuid guid(aGuid.mLayersId, 0, FrameMetrics::NULL_SCROLL_ID); + nsPoint point = + nsLayoutUtils::GetEventCoordinatesRelativeTo(aWidget, aRefPoint, aRootFrame); + nsIFrame* target = + nsLayoutUtils::GetFrameForPoint(aRootFrame, point, nsLayoutUtils::IGNORE_ROOT_SCROLL_FRAME); + nsIScrollableFrame* scrollAncestor = target + ? nsLayoutUtils::GetAsyncScrollableAncestorFrame(target) + : aRootFrame->PresContext()->PresShell()->GetRootScrollFrameAsScrollable(); + + // Assuming that if there's no scrollAncestor, there's already a displayPort. + nsCOMPtr<dom::Element> dpElement = scrollAncestor + ? GetDisplayportElementFor(scrollAncestor) + : GetRootDocumentElementFor(aWidget); + + nsAutoString dpElementDesc; + if (dpElement) { + dpElement->Describe(dpElementDesc); + } + APZCCH_LOG("For event at %s found scrollable element %p (%s)\n", + Stringify(aRefPoint).c_str(), dpElement.get(), + NS_LossyConvertUTF16toASCII(dpElementDesc).get()); + + bool guidIsValid = APZCCallbackHelper::GetOrCreateScrollIdentifiers( + dpElement, &(guid.mPresShellId), &(guid.mScrollId)); + aTargets->AppendElement(guid); + + if (!guidIsValid || nsLayoutUtils::HasDisplayPort(dpElement)) { + return false; + } + + if (!scrollAncestor) { + MOZ_ASSERT(false); // If you hit this, please file a bug with STR. + + // Attempt some sort of graceful handling based on a theory as to why we + // reach this point... + // If we get here, the document element is non-null, valid, but doesn't have + // a displayport. It's possible that the init code in ChromeProcessController + // failed for some reason, or the document element got swapped out at some + // later time. In this case let's try to set a displayport on the document + // element 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->PresContext()->PresShell()); + return false; + } + + APZCCH_LOG("%p didn't have a displayport, so setting one...\n", dpElement.get()); + bool activated = nsLayoutUtils::CalculateAndSetDisplayPortMargins( + scrollAncestor, nsLayoutUtils::RepaintMode::Repaint); + if (!activated) { + return false; + } + + nsIFrame* frame = do_QueryFrame(scrollAncestor); + nsLayoutUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(frame, + nsLayoutUtils::RepaintMode::Repaint); + + return true; +} + +static void +SendLayersDependentApzcTargetConfirmation(nsIPresShell* aShell, uint64_t aInputBlockId, + const nsTArray<ScrollableLayerGuid>& aTargets) +{ + LayerManager* lm = aShell->GetLayerManager(); + if (!lm) { + return; + } + + LayerTransactionChild* shadow = lm->AsShadowForwarder()->GetShadowManager(); + if (!shadow) { + return; + } + + shadow->SendSetConfirmedTargetAPZC(aInputBlockId, aTargets); +} + +class DisplayportSetListener : public nsAPostRefreshObserver { +public: + DisplayportSetListener(nsIPresShell* aPresShell, + const uint64_t& aInputBlockId, + const nsTArray<ScrollableLayerGuid>& aTargets) + : mPresShell(aPresShell) + , mInputBlockId(aInputBlockId) + , mTargets(aTargets) + { + } + + virtual ~DisplayportSetListener() + { + } + + void DidRefresh() override { + if (!mPresShell) { + MOZ_ASSERT_UNREACHABLE("Post-refresh observer fired again after failed attempt at unregistering it"); + return; + } + + APZCCH_LOG("Got refresh, sending target APZCs for input block %" PRIu64 "\n", mInputBlockId); + SendLayersDependentApzcTargetConfirmation(mPresShell, mInputBlockId, Move(mTargets)); + + if (!mPresShell->RemovePostRefreshObserver(this)) { + MOZ_ASSERT_UNREACHABLE("Unable to unregister post-refresh observer! Leaking it instead of leaving garbage registered"); + // Graceful handling, just in case... + mPresShell = nullptr; + return; + } + + delete this; + } + +private: + RefPtr<nsIPresShell> mPresShell; + uint64_t mInputBlockId; + nsTArray<ScrollableLayerGuid> mTargets; +}; + +// Sends a SetTarget notification for APZC, given one or more previous +// calls to PrepareForAPZCSetTargetNotification(). +static void +SendSetTargetAPZCNotificationHelper(nsIWidget* aWidget, + nsIPresShell* aShell, + const uint64_t& aInputBlockId, + const nsTArray<ScrollableLayerGuid>& aTargets, + bool aWaitForRefresh) +{ + bool waitForRefresh = aWaitForRefresh; + if (waitForRefresh) { + APZCCH_LOG("At least one target got a new displayport, need to wait for refresh\n"); + waitForRefresh = aShell->AddPostRefreshObserver( + new DisplayportSetListener(aShell, aInputBlockId, Move(aTargets))); + } + if (!waitForRefresh) { + APZCCH_LOG("Sending target APZCs for input block %" PRIu64 "\n", aInputBlockId); + aWidget->SetConfirmedTargetAPZC(aInputBlockId, aTargets); + } else { + APZCCH_LOG("Successfully registered post-refresh observer\n"); + } +} + +void +APZCCallbackHelper::SendSetTargetAPZCNotification(nsIWidget* aWidget, + nsIDocument* aDocument, + const WidgetGUIEvent& aEvent, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId) +{ + if (!aWidget || !aDocument) { + return; + } + 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; + } + sLastTargetAPZCNotificationInputBlock = aInputBlockId; + if (nsIPresShell* shell = aDocument->GetShell()) { + if (nsIFrame* rootFrame = shell->GetRootFrame()) { + rootFrame = UpdateRootFrameForTouchTargetDocument(rootFrame); + + 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, aGuid, + rootFrame, touchEvent->mTouches[i]->mRefPoint, &targets); + } + } else if (const WidgetWheelEvent* wheelEvent = aEvent.AsWheelEvent()) { + waitForRefresh = PrepareForSetTargetAPZCNotification(aWidget, aGuid, + rootFrame, wheelEvent->mRefPoint, &targets); + } else if (const WidgetMouseEvent* mouseEvent = aEvent.AsMouseEvent()) { + waitForRefresh = PrepareForSetTargetAPZCNotification(aWidget, aGuid, + rootFrame, mouseEvent->mRefPoint, &targets); + } + // TODO: Do other types of events need to be handled? + + if (!targets.IsEmpty()) { + SendSetTargetAPZCNotificationHelper( + aWidget, + shell, + aInputBlockId, + Move(targets), + waitForRefresh); + } + } + } +} + +void +APZCCallbackHelper::SendSetAllowedTouchBehaviorNotification( + nsIWidget* aWidget, + nsIDocument* aDocument, + const WidgetTouchEvent& aEvent, + uint64_t aInputBlockId, + const SetAllowedTouchBehaviorCallback& aCallback) +{ + if (nsIPresShell* shell = aDocument->GetShell()) { + if (nsIFrame* rootFrame = shell->GetRootFrame()) { + rootFrame = UpdateRootFrameForTouchTargetDocument(rootFrame); + + nsTArray<TouchBehaviorFlags> flags; + for (uint32_t i = 0; i < aEvent.mTouches.Length(); i++) { + flags.AppendElement( + TouchActionHelper::GetAllowedTouchBehavior(aWidget, + rootFrame, aEvent.mTouches[i]->mRefPoint)); + } + aCallback(aInputBlockId, Move(flags)); + } + } +} + +void +APZCCallbackHelper::NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId, const nsString& aEvent) +{ + nsCOMPtr<nsIContent> targetContent = nsLayoutUtils::FindContentFor(aScrollId); + if (!targetContent) { + return; + } + nsCOMPtr<nsIDocument> ownerDoc = targetContent->OwnerDoc(); + if (!ownerDoc) { + return; + } + + nsContentUtils::DispatchTrustedEvent( + ownerDoc, targetContent, + aEvent, + true, true); +} + +void +APZCCallbackHelper::NotifyFlushComplete(nsIPresShell* aShell) +{ + 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 (aShell && aShell->GetRootFrame()) { + aShell->GetRootFrame()->SchedulePaint(); + } + + nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService(); + MOZ_ASSERT(observerService); + observerService->NotifyObservers(nullptr, "apz-repaints-flushed", nullptr); +} + +static int32_t sActiveSuppressDisplayport = 0; +static bool sDisplayPortSuppressionRespected = true; + +void +APZCCallbackHelper::SuppressDisplayport(const bool& aEnabled, + const nsCOMPtr<nsIPresShell>& aShell) +{ + if (aEnabled) { + sActiveSuppressDisplayport++; + } else { + bool isSuppressed = IsDisplayportSuppressed(); + sActiveSuppressDisplayport--; + if (isSuppressed && !IsDisplayportSuppressed() && + aShell && aShell->GetRootFrame()) { + // We unsuppressed the displayport, trigger a paint + aShell->GetRootFrame()->SchedulePaint(); + } + } + + MOZ_ASSERT(sActiveSuppressDisplayport >= 0); +} + +void +APZCCallbackHelper::RespectDisplayPortSuppression(bool aEnabled, + const nsCOMPtr<nsIPresShell>& aShell) +{ + bool isSuppressed = IsDisplayportSuppressed(); + sDisplayPortSuppressionRespected = aEnabled; + if (isSuppressed && !IsDisplayportSuppressed() && + aShell && aShell->GetRootFrame()) { + // We unsuppressed the displayport, trigger a paint + aShell->GetRootFrame()->SchedulePaint(); + } +} + +bool +APZCCallbackHelper::IsDisplayportSuppressed() +{ + return sDisplayPortSuppressionRespected + && sActiveSuppressDisplayport > 0; +} + +/* static */ bool +APZCCallbackHelper::IsScrollInProgress(nsIScrollableFrame* aFrame) +{ + return aFrame->IsProcessingAsyncScroll() + || nsLayoutUtils::CanScrollOriginClobberApz(aFrame->LastScrollOrigin()) + || aFrame->LastSmoothScrollOrigin(); +} + +/* static */ void +APZCCallbackHelper::NotifyPinchGesture(PinchGestureInput::PinchGestureType aType, + LayoutDeviceCoord aSpanChange, + Modifiers aModifiers, + nsIWidget* aWidget) +{ + EventMessage msg; + switch (aType) { + case PinchGestureInput::PINCHGESTURE_START: + msg = eMagnifyGestureStart; + break; + case PinchGestureInput::PINCHGESTURE_SCALE: + msg = eMagnifyGestureUpdate; + break; + case PinchGestureInput::PINCHGESTURE_END: + msg = eMagnifyGesture; + break; + case PinchGestureInput::PINCHGESTURE_SENTINEL: + default: + MOZ_ASSERT_UNREACHABLE("Invalid gesture type"); + return; + } + + WidgetSimpleGestureEvent event(true, msg, aWidget); + event.mDelta = aSpanChange; + event.mModifiers = aModifiers; + 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 000000000..8e888805c --- /dev/null +++ b/gfx/layers/apz/util/APZCCallbackHelper.h @@ -0,0 +1,209 @@ +/* -*- 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 "FrameMetrics.h" +#include "InputData.h" +#include "mozilla/EventForwards.h" +#include "mozilla/Function.h" +#include "mozilla/layers/APZUtils.h" +#include "nsIDOMWindowUtils.h" + +class nsIContent; +class nsIDocument; +class nsIPresShell; +class nsIScrollableFrame; +class nsIWidget; +template<class T> struct already_AddRefed; +template<class T> class nsCOMPtr; + +namespace mozilla { +namespace layers { + +typedef function<void(uint64_t, const nsTArray<TouchBehaviorFlags>&)> + SetAllowedTouchBehaviorCallback; + +/* 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::FrameMetrics FrameMetrics; + typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid; + +public: + /* Applies the scroll and zoom parameters from the given FrameMetrics 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. aMetrics' display port and scroll position will + be updated with any modifications made. */ + static void UpdateRootFrame(FrameMetrics& aMetrics); + + /* Applies the scroll parameters from the given FrameMetrics 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. aMetrics' display port and scroll position + will be updated with any modifications made. */ + static void UpdateSubFrame(FrameMetrics& aMetrics); + + /* 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, + FrameMetrics::ViewID* aViewIdOut); + + /* Initialize a zero-margin displayport on the root document element of the + given presShell. */ + static void InitializeRootDisplayport(nsIPresShell* 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 nsIPresShell* GetRootContentDocumentPresShellForContent(nsIContent* aContent); + + /* Apply an "input transform" to the given |aInput| and return the transformed value. + The input transform applied is the one for the content element corresponding to + |aGuid|; this is populated in a previous call to UpdateCallbackTransform. See that + method's documentations for details. + This method additionally adjusts |aInput| by inversely scaling by the provided + pres shell resolution, to cancel out a compositor-side transform (added in + bug 1076241) that APZ doesn't unapply. */ + static CSSPoint ApplyCallbackTransform(const CSSPoint& aInput, + const ScrollableLayerGuid& aGuid); + + /* Same as above, but operates on LayoutDeviceIntPoint. + Requires an additonal |aScale| parameter to convert between CSS and + LayoutDevice space. */ + static mozilla::LayoutDeviceIntPoint + ApplyCallbackTransform(const LayoutDeviceIntPoint& aPoint, + const ScrollableLayerGuid& aGuid, + const CSSToLayoutDeviceScale& aScale); + + /* Convenience function for applying a callback transform to all refpoints + * in the input event. */ + static void ApplyCallbackTransform(WidgetEvent& aEvent, + const ScrollableLayerGuid& aGuid, + const CSSToLayoutDeviceScale& aScale); + + /* Dispatch a widget event via the widget stored in the event, if any. + * In a child process, allows the TabParent 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. */ + static nsEventStatus DispatchSynthesizedMouseEvent(EventMessage aMsg, + uint64_t aTime, + 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. */ + static bool DispatchMouseEvent(const nsCOMPtr<nsIPresShell>& aPresShell, + const nsString& aType, + const CSSPoint& aPoint, + int32_t aButton, + int32_t aClickCount, + int32_t aModifiers, + bool aIgnoreRootScrollFrame, + unsigned short aInputSourceArg); + + /* Fire a single-tap event at the given point. The event is dispatched + * via the given widget. */ + 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, the actual notification to APZ is + * sent to the compositor, which will then post a message back to APZ's + * controller thread. Otherwise, the provided widget's SetConfirmedTargetAPZC + * method is invoked immediately. + */ + static void SendSetTargetAPZCNotification(nsIWidget* aWidget, + nsIDocument* aDocument, + const WidgetGUIEvent& aEvent, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId); + + /* Figure out the allowed touch behaviors of each touch point in |aEvent| + * and send that information to the provided callback. */ + static void SendSetAllowedTouchBehaviorNotification(nsIWidget* aWidget, + nsIDocument* aDocument, + const WidgetTouchEvent& aEvent, + uint64_t aInputBlockId, + const SetAllowedTouchBehaviorCallback& aCallback); + + /* Notify content of a mouse scroll testing event. */ + static void NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId, const nsString& aEvent); + + /* Notify content that the repaint flush is complete. */ + static void NotifyFlushComplete(nsIPresShell* aShell); + + /* Temporarily ignore the Displayport for better paint performance. If at + * all possible, pass in a presShell if you have one at the call site, we + * use it to trigger a repaint once suppression is disabled. Without that + * the displayport may get left at the suppressed size for an extended + * period of time and result in unnecessary checkerboarding (see bug + * 1255054). */ + static void SuppressDisplayport(const bool& aEnabled, + const nsCOMPtr<nsIPresShell>& aShell); + + /* Whether or not displayport suppression should be turned on. Note that + * this only affects the return value of |IsDisplayportSuppressed()|, and + * doesn't change the value of the internal counter. As with + * SuppressDisplayport, this function should be passed a presShell to trigger + * a repaint if suppression is being turned off. + */ + static void RespectDisplayPortSuppression(bool aEnabled, + const nsCOMPtr<nsIPresShell>& aShell); + + /* Whether or not the displayport is currently suppressed. */ + static bool IsDisplayportSuppressed(); + + static void + AdjustDisplayPortForScrollDelta(mozilla::layers::FrameMetrics& aFrameMetrics, + const CSSPoint& aActualScrollOffset); + + /* + * Check if the scrollable frame is currently in the middle of an async + * or smooth scroll. We want to discard certain scroll input 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, + LayoutDeviceCoord aSpanChange, + Modifiers aModifiers, + 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 000000000..20a41eed5 --- /dev/null +++ b/gfx/layers/apz/util/APZEventState.cpp @@ -0,0 +1,512 @@ +/* -*- 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/. */ + +#include "APZEventState.h" + +#include "ActiveElementManager.h" +#include "APZCCallbackHelper.h" +#include "gfxPrefs.h" +#include "LayersLogging.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/Move.h" +#include "mozilla/Preferences.h" +#include "mozilla/TouchEvents.h" +#include "mozilla/layers/APZCCallbackHelper.h" +#include "nsCOMPtr.h" +#include "nsDocShell.h" +#include "nsIDOMMouseEvent.h" +#include "nsIDOMWindowUtils.h" +#include "nsIScrollableFrame.h" +#include "nsIScrollbarMediator.h" +#include "nsITimer.h" +#include "nsIWeakReferenceUtils.h" +#include "nsIWidget.h" +#include "nsLayoutUtils.h" +#include "nsQueryFrame.h" +#include "TouchManager.h" +#include "nsIDOMMouseEvent.h" +#include "nsLayoutUtils.h" +#include "nsIScrollableFrame.h" +#include "nsIScrollbarMediator.h" +#include "mozilla/TouchEvents.h" + +#define APZES_LOG(...) +// #define APZES_LOG(...) printf_stderr("APZES: " __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; + } + if (aModifiers & mozilla::MODIFIER_OS) { + result |= nsIDOMWindowUtils::MODIFIER_OS; + } + return result; +} + +} // namespace + +namespace mozilla { +namespace layers { + +static int32_t sActiveDurationMs = 10; +static bool sActiveDurationMsSet = false; + +APZEventState::APZEventState(nsIWidget* aWidget, + ContentReceivedInputBlockCallback&& aCallback) + : mWidget(nullptr) // initialized in constructor body + , mActiveElementManager(new ActiveElementManager()) + , mContentReceivedInputBlockCallback(Move(aCallback)) + , mPendingTouchPreventedResponse(false) + , mPendingTouchPreventedBlockId(0) + , mEndTouchIsClick(false) + , mTouchEndCancelled(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!"); + + if (!sActiveDurationMsSet) { + Preferences::AddIntVarCache(&sActiveDurationMs, + "ui.touch_activation.duration_ms", + sActiveDurationMs); + sActiveDurationMsSet = true; + } +} + +APZEventState::~APZEventState() +{} + +class DelayedFireSingleTapEvent final : public nsITimerCallback +{ +public: + NS_DECL_ISUPPORTS + + DelayedFireSingleTapEvent(nsWeakPtr aWidget, + LayoutDevicePoint& aPoint, + Modifiers aModifiers, + int32_t aClickCount, + nsITimer* aTimer) + : mWidget(aWidget) + , mPoint(aPoint) + , mModifiers(aModifiers) + , mClickCount(aClickCount) + // Hold the reference count until we are called back. + , mTimer(aTimer) + { + } + + NS_IMETHOD Notify(nsITimer*) override + { + if (nsCOMPtr<nsIWidget> widget = do_QueryReferent(mWidget)) { + APZCCallbackHelper::FireSingleTapEvent(mPoint, mModifiers, mClickCount, widget); + } + mTimer = nullptr; + return NS_OK; + } + + void ClearTimer() { + mTimer = nullptr; + } + +private: + ~DelayedFireSingleTapEvent() + { + } + + nsWeakPtr mWidget; + LayoutDevicePoint mPoint; + Modifiers mModifiers; + int32_t mClickCount; + nsCOMPtr<nsITimer> mTimer; +}; + +NS_IMPL_ISUPPORTS(DelayedFireSingleTapEvent, nsITimerCallback) + +void +APZEventState::ProcessSingleTap(const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers, + const ScrollableLayerGuid& aGuid, + int32_t aClickCount) +{ + APZES_LOG("Handling single tap at %s on %s with %d\n", + Stringify(aPoint).c_str(), Stringify(aGuid).c_str(), mTouchEndCancelled); + + nsCOMPtr<nsIWidget> widget = GetWidget(); + if (!widget) { + return; + } + + if (mTouchEndCancelled) { + return; + } + + LayoutDevicePoint ldPoint = aPoint * aScale; + if (!mActiveElementManager->ActiveElementUsesStyle()) { + // If the active element isn't visually affected by the :active style, we + // have no need to wait the extra sActiveDurationMs to make the activation + // visually obvious to the user. + APZCCallbackHelper::FireSingleTapEvent(ldPoint, aModifiers, aClickCount, widget); + return; + } + + APZES_LOG("Active element uses style, scheduling timer for click event\n"); + nsCOMPtr<nsITimer> timer = do_CreateInstance(NS_TIMER_CONTRACTID); + RefPtr<DelayedFireSingleTapEvent> callback = + new DelayedFireSingleTapEvent(mWidget, ldPoint, aModifiers, aClickCount, timer); + nsresult rv = timer->InitWithCallback(callback, + sActiveDurationMs, + nsITimer::TYPE_ONE_SHOT); + if (NS_FAILED(rv)) { + // Make |callback| not hold the timer, so they will both be destructed when + // we leave the scope of this function. + callback->ClearTimer(); + } +} + +bool +APZEventState::FireContextmenuEvents(const nsCOMPtr<nsIPresShell>& aPresShell, + const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers, + const nsCOMPtr<nsIWidget>& 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. + bool eventHandled = + APZCCallbackHelper::DispatchMouseEvent(aPresShell, NS_LITERAL_STRING("contextmenu"), + aPoint, 2, 1, WidgetModifiersToDOMModifiers(aModifiers), true, + nsIDOMMouseEvent::MOZ_SOURCE_TOUCH); + + APZES_LOG("Contextmenu event handled: %d\n", eventHandled); + if (eventHandled) { + // 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, /*time*/ 0, aPoint * aScale, aModifiers, + /*clickCount*/ 1, aWidget); + eventHandled = (status == nsEventStatus_eConsumeNoDefault); + APZES_LOG("eMouseLongTap event handled: %d\n", eventHandled); +#endif + } + + return eventHandled; +} + +void +APZEventState::ProcessLongTap(const nsCOMPtr<nsIPresShell>& aPresShell, + const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId) +{ + APZES_LOG("Handling long tap at %s\n", Stringify(aPoint).c_str()); + + nsCOMPtr<nsIWidget> widget = GetWidget(); + if (!widget) { + return; + } + + SendPendingTouchPreventedResponse(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. + nsEventStatus status = APZCCallbackHelper::DispatchSynthesizedMouseEvent( + eMouseLongTap, /*time*/ 0, aPoint * aScale, aModifiers, /*clickCount*/ 1, + widget); + + bool eventHandled = (status == nsEventStatus_eConsumeNoDefault); +#else + bool eventHandled = FireContextmenuEvents(aPresShell, aPoint, aScale, + aModifiers, widget); +#endif + mContentReceivedInputBlockCallback(aGuid, aInputBlockId, eventHandled); + + if (eventHandled) { + // Also send a touchcancel to content, 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(const nsCOMPtr<nsIPresShell>& 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) +{ + if (aEvent.mMessage == eTouchStart && aEvent.mTouches.Length() > 0) { + mActiveElementManager->SetTargetElement(aEvent.mTouches[0]->GetTarget()); + mLastTouchIdentifier = aEvent.mTouches[0]->Identifier(); + } + + bool isTouchPrevented = aContentResponse == nsEventStatus_eConsumeNoDefault; + bool sentContentResponse = false; + APZES_LOG("Handling event type %d\n", aEvent.mMessage); + switch (aEvent.mMessage) { + case eTouchStart: { + mTouchEndCancelled = false; + sentContentResponse = SendPendingTouchPreventedResponse(false); + // sentContentResponse can be true here 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 (isTouchPrevented) { + mContentReceivedInputBlockCallback(aGuid, aInputBlockId, isTouchPrevented); + sentContentResponse = true; + } else { + APZES_LOG("Event not prevented; pending response for %" PRIu64 " %s\n", + aInputBlockId, Stringify(aGuid).c_str()); + mPendingTouchPreventedResponse = true; + mPendingTouchPreventedGuid = aGuid; + mPendingTouchPreventedBlockId = aInputBlockId; + } + break; + } + + case eTouchEnd: + if (isTouchPrevented) { + mTouchEndCancelled = true; + mEndTouchIsClick = false; + } + MOZ_FALLTHROUGH; + case eTouchCancel: + mActiveElementManager->HandleTouchEndEvent(mEndTouchIsClick); + MOZ_FALLTHROUGH; + case eTouchMove: { + if (mPendingTouchPreventedResponse) { + MOZ_ASSERT(aGuid == mPendingTouchPreventedGuid); + } + sentContentResponse = SendPendingTouchPreventedResponse(isTouchPrevented); + break; + } + + default: + NS_WARNING("Unknown touch event type"); + } + + if (sentContentResponse && + aApzResponse == nsEventStatus_eConsumeDoDefault && + gfxPrefs::PointerEventsEnabled()) { + WidgetTouchEvent cancelEvent(aEvent); + cancelEvent.mMessage = eTouchCancel; + 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); + } +} + +void +APZEventState::ProcessWheelEvent(const WidgetWheelEvent& aEvent, + const ScrollableLayerGuid& aGuid, + 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(aGuid, aInputBlockId, defaultPrevented); +} + +void +APZEventState::ProcessMouseEvent(const WidgetMouseEvent& aEvent, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId) +{ + // If we get here and the drag block has not been confirmed by the code in + // nsSliderFrame, then no scrollbar reacted to the event thus APZC will + // ignore this drag block. We can send defaultPrevented as either true or + // false, it doesn't matter, because APZ won't have the scrollbar metrics + // anyway, and will know to drop the block. + bool defaultPrevented = false; + mContentReceivedInputBlockCallback(aGuid, aInputBlockId, defaultPrevented); +} + +void +APZEventState::ProcessAPZStateChange(ViewID aViewId, + APZStateChange aChange, + int aArg) +{ + switch (aChange) + { + case APZStateChange::eTransformBegin: + { + nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aViewId); + if (sf) { + sf->SetTransformingByAPZ(true); + } + nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(sf); + if (scrollbarMediator) { + scrollbarMediator->ScrollbarActivityStarted(); + } + + nsIContent* content = nsLayoutUtils::FindContentFor(aViewId); + nsIDocument* 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); + } + nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(sf); + if (scrollbarMediator) { + scrollbarMediator->ScrollbarActivityStopped(); + } + + nsIContent* content = nsLayoutUtils::FindContentFor(aViewId); + nsIDocument* 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: + { + mActiveElementManager->HandleTouchStart(aArg); + 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; + } + case APZStateChange::eSentinel: + // Should never happen, but we want this case branch to stop the compiler + // whining about unhandled values. + MOZ_ASSERT(false); + break; + } +} + +void +APZEventState::ProcessClusterHit() +{ + // If we hit a cluster of links then we shouldn't activate any of them, + // as we will be showing the zoomed view. (This is only called on Fennec). +#ifndef MOZ_WIDGET_ANDROID + MOZ_ASSERT(false); +#endif + mActiveElementManager->ClearActivation(); +} + +bool +APZEventState::SendPendingTouchPreventedResponse(bool aPreventDefault) +{ + if (mPendingTouchPreventedResponse) { + APZES_LOG("Sending response %d for pending guid: %s\n", aPreventDefault, + Stringify(mPendingTouchPreventedGuid).c_str()); + mContentReceivedInputBlockCallback(mPendingTouchPreventedGuid, + mPendingTouchPreventedBlockId, aPreventDefault); + mPendingTouchPreventedResponse = false; + return true; + } + return false; +} + +already_AddRefed<nsIWidget> +APZEventState::GetWidget() const +{ + nsCOMPtr<nsIWidget> result = do_QueryReferent(mWidget); + 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 000000000..44188eaa7 --- /dev/null +++ b/gfx/layers/apz/util/APZEventState.h @@ -0,0 +1,103 @@ +/* -*- 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_APZEventState_h +#define mozilla_layers_APZEventState_h + +#include <stdint.h> + +#include "FrameMetrics.h" // for ScrollableLayerGuid +#include "Units.h" +#include "mozilla/EventForwards.h" +#include "mozilla/Function.h" +#include "mozilla/layers/GeckoContentController.h" // for APZStateChange +#include "mozilla/RefPtr.h" +#include "nsCOMPtr.h" +#include "nsISupportsImpl.h" // for NS_INLINE_DECL_REFCOUNTING +#include "nsIWeakReferenceUtils.h" // for nsWeakPtr + +template <class> class nsCOMPtr; +class nsIDocument; +class nsIPresShell; +class nsIWidget; + +namespace mozilla { +namespace layers { + +class ActiveElementManager; + +typedef function<void(const ScrollableLayerGuid&, + 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 { + typedef GeckoContentController::APZStateChange APZStateChange; + typedef FrameMetrics::ViewID ViewID; +public: + APZEventState(nsIWidget* aWidget, + ContentReceivedInputBlockCallback&& aCallback); + + NS_INLINE_DECL_REFCOUNTING(APZEventState); + + void ProcessSingleTap(const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers, + const ScrollableLayerGuid& aGuid, + int32_t aClickCount); + void ProcessLongTap(const nsCOMPtr<nsIPresShell>& aUtils, + const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId); + void ProcessLongTapUp(const nsCOMPtr<nsIPresShell>& aPresShell, + const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers); + void ProcessTouchEvent(const WidgetTouchEvent& aEvent, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId, + nsEventStatus aApzResponse, + nsEventStatus aContentResponse); + void ProcessWheelEvent(const WidgetWheelEvent& aEvent, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId); + void ProcessMouseEvent(const WidgetMouseEvent& aEvent, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId); + void ProcessAPZStateChange(ViewID aViewId, + APZStateChange aChange, + int aArg); + void ProcessClusterHit(); +private: + ~APZEventState(); + bool SendPendingTouchPreventedResponse(bool aPreventDefault); + bool FireContextmenuEvents(const nsCOMPtr<nsIPresShell>& aPresShell, + const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers, + const nsCOMPtr<nsIWidget>& aWidget); + already_AddRefed<nsIWidget> GetWidget() const; +private: + nsWeakPtr mWidget; + RefPtr<ActiveElementManager> mActiveElementManager; + ContentReceivedInputBlockCallback mContentReceivedInputBlockCallback; + bool mPendingTouchPreventedResponse; + ScrollableLayerGuid mPendingTouchPreventedGuid; + uint64_t mPendingTouchPreventedBlockId; + bool mEndTouchIsClick; + bool mTouchEndCancelled; + int32_t mLastTouchIdentifier; +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_APZEventState_h */ diff --git a/gfx/layers/apz/util/APZThreadUtils.cpp b/gfx/layers/apz/util/APZThreadUtils.cpp new file mode 100644 index 000000000..46f67d010 --- /dev/null +++ b/gfx/layers/apz/util/APZThreadUtils.cpp @@ -0,0 +1,96 @@ +/* -*- 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/. */ + +#include "mozilla/layers/APZThreadUtils.h" + +#include "mozilla/layers/Compositor.h" +#ifdef MOZ_WIDGET_ANDROID +#include "AndroidBridge.h" +#endif + +namespace mozilla { +namespace layers { + +static bool sThreadAssertionsEnabled = true; +static MessageLoop* sControllerThread; + +/*static*/ void +APZThreadUtils::SetThreadAssertionsEnabled(bool aEnabled) { + sThreadAssertionsEnabled = aEnabled; +} + +/*static*/ bool +APZThreadUtils::GetThreadAssertionsEnabled() { + return sThreadAssertionsEnabled; +} + +/*static*/ void +APZThreadUtils::SetControllerThread(MessageLoop* aLoop) +{ + // We must either be setting the initial controller thread, or removing it, + // or re-using an existing controller thread. + MOZ_ASSERT(!sControllerThread || !aLoop || sControllerThread == aLoop); + sControllerThread = aLoop; +} + +/*static*/ void +APZThreadUtils::AssertOnControllerThread() { + if (!GetThreadAssertionsEnabled()) { + return; + } + + MOZ_ASSERT(sControllerThread == MessageLoop::current()); +} + +/*static*/ void +APZThreadUtils::AssertOnCompositorThread() +{ + if (GetThreadAssertionsEnabled()) { + Compositor::AssertOnCompositorThread(); + } +} + +/*static*/ void +APZThreadUtils::RunOnControllerThread(already_AddRefed<Runnable> aTask) +{ + RefPtr<Runnable> task = aTask; + +#ifdef MOZ_WIDGET_ANDROID + // This is needed while nsWindow::ConfigureAPZControllerThread is not propper + // implemented. + if (AndroidBridge::IsJavaUiThread()) { + task->Run(); + } else { + AndroidBridge::Bridge()->PostTaskToUiThread(task.forget(), 0); + } +#else + if (!sControllerThread) { + // Could happen on startup + NS_WARNING("Dropping task posted to controller thread"); + return; + } + + if (sControllerThread == MessageLoop::current()) { + task->Run(); + } else { + sControllerThread->PostTask(task.forget()); + } +#endif +} + +/*static*/ bool +APZThreadUtils::IsControllerThread() +{ +#ifdef MOZ_WIDGET_ANDROID + return AndroidBridge::IsJavaUiThread(); +#else + return sControllerThread == MessageLoop::current(); +#endif +} + +NS_IMPL_ISUPPORTS(GenericTimerCallbackBase, nsITimerCallback) + +} // 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 000000000..4b9b2c0d0 --- /dev/null +++ b/gfx/layers/apz/util/APZThreadUtils.h @@ -0,0 +1,104 @@ +/* -*- 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_APZThreadUtils_h +#define mozilla_layers_APZThreadUtils_h + +#include "base/message_loop.h" +#include "nsITimer.h" + +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(MessageLoop* aLoop); + + /** + * 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(); + + /** + * This can be used to assert that the current thread is the + * compositor thread (which applies the async transform). + * This does nothing if thread assertions are disabled. + */ + static void AssertOnCompositorThread(); + + /** + * 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(already_AddRefed<Runnable> aTask); + + /** + * Returns true if currently on APZ "controller thread". + */ + static bool IsControllerThread(); +}; + +// A base class for GenericTimerCallback<Function>. +// This is necessary because NS_IMPL_ISUPPORTS doesn't work for a class +// template. +class GenericTimerCallbackBase : public nsITimerCallback +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + +protected: + virtual ~GenericTimerCallbackBase() {} +}; + +// An nsITimerCallback implementation that can be used with any function +// object that's callable with no arguments. +template <typename Function> +class GenericTimerCallback final : public GenericTimerCallbackBase +{ +public: + explicit GenericTimerCallback(const Function& aFunction) : mFunction(aFunction) {} + + NS_IMETHOD Notify(nsITimer*) override + { + mFunction(); + return NS_OK; + } +private: + Function mFunction; +}; + +// Convenience function for constructing a GenericTimerCallback. +// Returns a raw pointer, suitable for passing directly as an argument to +// nsITimer::InitWithCallback(). The intention is to enable the following +// terse inline usage: +// timer->InitWithCallback(NewTimerCallback([](){ ... }), delay); +template <typename Function> +GenericTimerCallback<Function>* NewTimerCallback(const Function& aFunction) +{ + return new GenericTimerCallback<Function>(aFunction); +} + +} // 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 000000000..20d34aa2b --- /dev/null +++ b/gfx/layers/apz/util/ActiveElementManager.cpp @@ -0,0 +1,237 @@ +/* -*- 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/. */ + +#include "ActiveElementManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/EventStates.h" +#include "mozilla/StyleSetHandle.h" +#include "mozilla/StyleSetHandleInlines.h" +#include "mozilla/Preferences.h" +#include "base/message_loop.h" +#include "base/task.h" +#include "mozilla/dom/Element.h" +#include "nsIDocument.h" +#include "nsStyleSet.h" + +#define AEM_LOG(...) +// #define AEM_LOG(...) printf_stderr("AEM: " __VA_ARGS__) + +namespace mozilla { +namespace layers { + +static int32_t sActivationDelayMs = 100; +static bool sActivationDelayMsSet = false; + +ActiveElementManager::ActiveElementManager() + : mCanBePan(false), + mCanBePanSet(false), + mSetActiveTask(nullptr), + mActiveElementUsesStyle(false) +{ + if (!sActivationDelayMsSet) { + Preferences::AddIntVarCache(&sActivationDelayMs, + "ui.touch_activation.delay_ms", + sActivationDelayMs); + sActivationDelayMsSet = true; + } +} + +ActiveElementManager::~ActiveElementManager() {} + +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 = do_QueryInterface(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; + } + + // 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); + } 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>>(this, + &ActiveElementManager::SetActiveTask, + mTarget); + mSetActiveTask = task; + MessageLoop::current()->PostDelayedTask(task.forget(), sActivationDelayMs); + AEM_LOG("Scheduling mSetActiveTask %p\n", mSetActiveTask); + } +} + +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) { + 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; +} + +bool +ActiveElementManager::ActiveElementUsesStyle() const +{ + return mActiveElementUsesStyle; +} + +static nsPresContext* +GetPresContextFor(nsIContent* aContent) +{ + if (!aContent) { + return nullptr; + } + nsIPresShell* shell = aContent->OwnerDoc()->GetShell(); + if (!shell) { + return nullptr; + } + return shell->GetPresContext(); +} + +static bool +ElementHasActiveStyle(dom::Element* aElement) +{ + nsPresContext* pc = GetPresContextFor(aElement); + if (!pc) { + return false; + } + StyleSetHandle styleSet = pc->StyleSet(); + for (dom::Element* e = aElement; e; e = e->GetParentElement()) { + if (styleSet->HasStateDependentStyle(e, NS_EVENT_STATE_ACTIVE)) { + AEM_LOG("Element %p's style is dependent on the active state\n", e); + return true; + } + } + AEM_LOG("Element %p doesn't use active styles\n", aElement); + return false; +} + +void +ActiveElementManager::SetActive(dom::Element* aTarget) +{ + AEM_LOG("Setting active %p\n", aTarget); + + if (nsPresContext* pc = GetPresContextFor(aTarget)) { + pc->EventStateManager()->SetContentState(aTarget, NS_EVENT_STATE_ACTIVE); + mActiveElementUsesStyle = ElementHasActiveStyle(aTarget); + } +} + +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); + + // 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); + + 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 000000000..83d0cb29f --- /dev/null +++ b/gfx/layers/apz/util/ActiveElementManager.h @@ -0,0 +1,104 @@ +/* -*- 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_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 { + +/** + * Manages setting and clearing the ':active' CSS pseudostate in the presence + * of touch input. + */ +class ActiveElementManager { + ~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(); + /** + * @return true iff the currently active element (or one of its ancestors) + * actually had a style for the :active pseudo-class. The currently active + * element is the root element if no other elements are active. + */ + bool ActiveElementUsesStyle() const; +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; + /** + * See ActiveElementUsesStyle() documentation. + */ + bool mActiveElementUsesStyle; + + // 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 000000000..0924fe92d --- /dev/null +++ b/gfx/layers/apz/util/CheckerboardReportService.cpp @@ -0,0 +1,228 @@ +/* -*- 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/. */ + +#include "CheckerboardReportService.h" + +#include "gfxPrefs.h" // for gfxPrefs +#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/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 "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([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) +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CheckerboardReportService, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CheckerboardReportService, Release) + +/*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::IsCallerChrome() + || nsContentUtils::IsSpecificAboutPage(aGlobal, "about:checkerboard"); +} + +/*static*/ already_AddRefed<CheckerboardReportService> +CheckerboardReportService::Constructor(const dom::GlobalObject& aGlobal, ErrorResult& aRv) +{ + 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 CheckerboardReportServiceBinding::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 gfxPrefs::APZRecordCheckerboarding(); +} + +void +CheckerboardReportService::SetRecordingEnabled(bool aEnabled) +{ + gfxPrefs::SetAPZRecordCheckerboarding(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 000000000..743b29825 --- /dev/null +++ b/gfx/layers/apz/util/CheckerboardReportService.h @@ -0,0 +1,144 @@ +/* -*- 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_dom_CheckerboardReportService_h +#define mozilla_dom_CheckerboardReportService_h + +#include <string> + +#include "js/TypeDecls.h" // for JSContext, JSObject +#include "mozilla/ErrorResult.h" // for ErrorResult +#include "mozilla/StaticPtr.h" // for StaticRefPtr +#include "nsCOMPtr.h" // for nsCOMPtr +#include "nsISupports.h" // for NS_INLINE_DECL_REFCOUNTING +#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() {} + virtual ~CheckerboardEventStorage() {} + + 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, ErrorResult& aRv); + + 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_SCRIPT_HOLDER_NATIVE_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() {} + + 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 000000000..ac8b3824f --- /dev/null +++ b/gfx/layers/apz/util/ChromeProcessController.cpp @@ -0,0 +1,276 @@ +/* -*- 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/. */ + +#include "ChromeProcessController.h" + +#include "MainThreadUtils.h" // for NS_IsMainThread() +#include "base/message_loop.h" // for MessageLoop +#include "mozilla/dom/Element.h" +#include "mozilla/layers/CompositorBridgeParent.h" +#include "mozilla/layers/APZCCallbackHelper.h" +#include "mozilla/layers/APZEventState.h" +#include "mozilla/layers/IAPZCTreeManager.h" +#include "mozilla/layers/DoubleTapToZoom.h" +#include "nsIDocument.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIPresShell.h" +#include "nsLayoutUtils.h" +#include "nsView.h" + +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) + , mUILoop(MessageLoop::current()) +{ + // Otherwise we're initializing mUILoop incorrectly. + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aAPZEventState); + MOZ_ASSERT(aAPZCTreeManager); + + mUILoop->PostTask(NewRunnableMethod(this, &ChromeProcessController::InitializeRoot)); +} + +ChromeProcessController::~ChromeProcessController() {} + +void +ChromeProcessController::InitializeRoot() +{ + APZCCallbackHelper::InitializeRootDisplayport(GetPresShell()); +} + +void +ChromeProcessController::RequestContentRepaint(const FrameMetrics& aFrameMetrics) +{ + MOZ_ASSERT(IsRepaintThread()); + + FrameMetrics metrics = aFrameMetrics; + if (metrics.IsRootContent()) { + APZCCallbackHelper::UpdateRootFrame(metrics); + } else { + APZCCallbackHelper::UpdateSubFrame(metrics); + } +} + +void +ChromeProcessController::PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs) +{ + MessageLoop::current()->PostDelayedTask(Move(aTask), aDelayMs); +} + +bool +ChromeProcessController::IsRepaintThread() +{ + return NS_IsMainThread(); +} + +void +ChromeProcessController::DispatchToRepaintThread(already_AddRefed<Runnable> aTask) +{ + NS_DispatchToMainThread(Move(aTask)); +} + +void +ChromeProcessController::Destroy() +{ + if (MessageLoop::current() != mUILoop) { + mUILoop->PostTask(NewRunnableMethod(this, &ChromeProcessController::Destroy)); + return; + } + + MOZ_ASSERT(MessageLoop::current() == mUILoop); + mWidget = nullptr; + mAPZEventState = nullptr; +} + +nsIPresShell* +ChromeProcessController::GetPresShell() const +{ + if (!mWidget) { + return nullptr; + } + if (nsView* view = nsView::GetViewFor(mWidget)) { + return view->GetPresShell(); + } + return nullptr; +} + +nsIDocument* +ChromeProcessController::GetRootDocument() const +{ + if (nsIPresShell* presShell = GetPresShell()) { + return presShell->GetDocument(); + } + return nullptr; +} + +nsIDocument* +ChromeProcessController::GetRootContentDocument(const FrameMetrics::ViewID& aScrollId) const +{ + nsIContent* content = nsLayoutUtils::FindContentFor(aScrollId); + if (!content) { + return nullptr; + } + nsIPresShell* presShell = APZCCallbackHelper::GetRootContentDocumentPresShellForContent(content); + if (presShell) { + return presShell->GetDocument(); + } + return nullptr; +} + +void +ChromeProcessController::HandleDoubleTap(const mozilla::CSSPoint& aPoint, + Modifiers aModifiers, + const ScrollableLayerGuid& aGuid) +{ + MOZ_ASSERT(MessageLoop::current() == mUILoop); + + nsCOMPtr<nsIDocument> document = GetRootContentDocument(aGuid.mScrollId); + if (!document.get()) { + return; + } + + // CalculateRectToZoomTo performs a hit test on the frame associated with the + // Root Content Document. Unfortunately that frame does not know about the + // resolution of the document and so we must remove it before calculating + // the zoomToRect. + nsIPresShell* presShell = document->GetShell(); + const float resolution = presShell->ScaleToResolution() ? presShell->GetResolution () : 1.0f; + CSSPoint point(aPoint.x / resolution, aPoint.y / resolution); + CSSRect zoomToRect = CalculateRectToZoomTo(document, point); + + uint32_t presShellId; + FrameMetrics::ViewID viewId; + if (APZCCallbackHelper::GetOrCreateScrollIdentifiers( + document->GetDocumentElement(), &presShellId, &viewId)) { + mAPZCTreeManager->ZoomToRect( + ScrollableLayerGuid(aGuid.mLayersId, presShellId, viewId), zoomToRect); + } +} + +void +ChromeProcessController::HandleTap(TapType aType, + const mozilla::LayoutDevicePoint& aPoint, + Modifiers aModifiers, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId) +{ + if (MessageLoop::current() != mUILoop) { + mUILoop->PostTask(NewRunnableMethod<TapType, mozilla::LayoutDevicePoint, Modifiers, + ScrollableLayerGuid, uint64_t>(this, + &ChromeProcessController::HandleTap, + aType, aPoint, aModifiers, aGuid, aInputBlockId)); + return; + } + + if (!mAPZEventState) { + return; + } + + nsCOMPtr<nsIPresShell> presShell = GetPresShell(); + if (!presShell) { + return; + } + if (!presShell->GetPresContext()) { + return; + } + CSSToLayoutDeviceScale scale(presShell->GetPresContext()->CSSToDevPixelScale()); + CSSPoint point = APZCCallbackHelper::ApplyCallbackTransform(aPoint / scale, aGuid); + + switch (aType) { + case TapType::eSingleTap: + mAPZEventState->ProcessSingleTap(point, scale, aModifiers, aGuid, 1); + break; + case TapType::eDoubleTap: + HandleDoubleTap(point, aModifiers, aGuid); + break; + case TapType::eSecondTap: + mAPZEventState->ProcessSingleTap(point, scale, aModifiers, aGuid, 2); + break; + case TapType::eLongTap: + mAPZEventState->ProcessLongTap(presShell, point, scale, aModifiers, aGuid, + aInputBlockId); + break; + case TapType::eLongTapUp: + mAPZEventState->ProcessLongTapUp(presShell, point, scale, aModifiers); + break; + case TapType::eSentinel: + // Should never happen, but we need to handle this case branch for the + // compiler to be happy. + MOZ_ASSERT(false); + break; + } +} + +void +ChromeProcessController::NotifyPinchGesture(PinchGestureInput::PinchGestureType aType, + const ScrollableLayerGuid& aGuid, + LayoutDeviceCoord aSpanChange, + Modifiers aModifiers) +{ + if (MessageLoop::current() != mUILoop) { + mUILoop->PostTask(NewRunnableMethod + <PinchGestureInput::PinchGestureType, + ScrollableLayerGuid, + LayoutDeviceCoord, + Modifiers>(this, + &ChromeProcessController::NotifyPinchGesture, + aType, aGuid, aSpanChange, aModifiers)); + return; + } + + if (mWidget) { + APZCCallbackHelper::NotifyPinchGesture(aType, aSpanChange, aModifiers, mWidget.get()); + } +} + +void +ChromeProcessController::NotifyAPZStateChange(const ScrollableLayerGuid& aGuid, + APZStateChange aChange, + int aArg) +{ + if (MessageLoop::current() != mUILoop) { + mUILoop->PostTask(NewRunnableMethod + <ScrollableLayerGuid, + APZStateChange, + int>(this, &ChromeProcessController::NotifyAPZStateChange, + aGuid, aChange, aArg)); + return; + } + + if (!mAPZEventState) { + return; + } + + mAPZEventState->ProcessAPZStateChange(aGuid.mScrollId, aChange, aArg); +} + +void +ChromeProcessController::NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId, const nsString& aEvent) +{ + if (MessageLoop::current() != mUILoop) { + mUILoop->PostTask(NewRunnableMethod + <FrameMetrics::ViewID, + nsString>(this, &ChromeProcessController::NotifyMozMouseScrollEvent, + aScrollId, aEvent)); + return; + } + + APZCCallbackHelper::NotifyMozMouseScrollEvent(aScrollId, aEvent); +} + +void +ChromeProcessController::NotifyFlushComplete() +{ + MOZ_ASSERT(IsRepaintThread()); + + APZCCallbackHelper::NotifyFlushComplete(GetPresShell()); +} diff --git a/gfx/layers/apz/util/ChromeProcessController.h b/gfx/layers/apz/util/ChromeProcessController.h new file mode 100644 index 000000000..9a43297d4 --- /dev/null +++ b/gfx/layers/apz/util/ChromeProcessController.h @@ -0,0 +1,83 @@ +/* -*- 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_ChromeProcessController_h +#define mozilla_layers_ChromeProcessController_h + +#include "mozilla/layers/GeckoContentController.h" +#include "nsCOMPtr.h" +#include "mozilla/RefPtr.h" + +class nsIDOMWindowUtils; +class nsIDocument; +class nsIPresShell; +class nsIWidget; + +class MessageLoop; + +namespace mozilla { + +namespace layers { + +class IAPZCTreeManager; +class APZEventState; + +/** + * 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); + ~ChromeProcessController(); + virtual void Destroy() override; + + // GeckoContentController interface + virtual void RequestContentRepaint(const FrameMetrics& aFrameMetrics) override; + virtual void PostDelayedTask(already_AddRefed<Runnable> aTask, int aDelayMs) override; + virtual bool IsRepaintThread() override; + virtual void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) override; + virtual void HandleTap(TapType aType, + const mozilla::LayoutDevicePoint& aPoint, + Modifiers aModifiers, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId) override; + virtual void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType, + const ScrollableLayerGuid& aGuid, + LayoutDeviceCoord aSpanChange, + Modifiers aModifiers) override; + virtual void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid, + APZStateChange aChange, + int aArg) override; + virtual void NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId, + const nsString& aEvent) override; + virtual void NotifyFlushComplete() override; +private: + nsCOMPtr<nsIWidget> mWidget; + RefPtr<APZEventState> mAPZEventState; + RefPtr<IAPZCTreeManager> mAPZCTreeManager; + MessageLoop* mUILoop; + + void InitializeRoot(); + nsIPresShell* GetPresShell() const; + nsIDocument* GetRootDocument() const; + nsIDocument* GetRootContentDocument(const FrameMetrics::ViewID& aScrollId) const; + void HandleDoubleTap(const mozilla::CSSPoint& aPoint, Modifiers aModifiers, + const ScrollableLayerGuid& aGuid); +}; + +} // 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 000000000..eccd4179f --- /dev/null +++ b/gfx/layers/apz/util/ContentProcessController.cpp @@ -0,0 +1,207 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=4 ts=8 et 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/dom/TabChild.h" +#include "mozilla/layers/APZCCallbackHelper.h" +#include "mozilla/layers/APZChild.h" + +#include "InputData.h" // for InputData + +namespace mozilla { +namespace layers { + +/** + * There are cases where we try to create the APZChild before the corresponding + * TabChild has been created, we use an observer for the "tab-child-created" + * topic to set the TabChild in the APZChild when it has been created. + */ +class TabChildCreatedObserver : public nsIObserver +{ +public: + TabChildCreatedObserver(ContentProcessController* aController, const dom::TabId& aTabId) + : mController(aController), + mTabId(aTabId) + {} + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + +private: + virtual ~TabChildCreatedObserver() + {} + + // TabChildCreatedObserver is owned by mController, and mController outlives its + // TabChildCreatedObserver, so the raw pointer is fine. + ContentProcessController* mController; + dom::TabId mTabId; +}; + +NS_IMPL_ISUPPORTS(TabChildCreatedObserver, nsIObserver) + +NS_IMETHODIMP +TabChildCreatedObserver::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + MOZ_ASSERT(strcmp(aTopic, "tab-child-created") == 0); + + nsCOMPtr<nsITabChild> tabChild(do_QueryInterface(aSubject)); + NS_ENSURE_TRUE(tabChild, NS_ERROR_FAILURE); + + dom::TabChild* browser = static_cast<dom::TabChild*>(tabChild.get()); + + if (browser->GetTabId() == mTabId) { + mController->SetBrowser(browser); + } + return NS_OK; +} + +APZChild* +ContentProcessController::Create(const dom::TabId& aTabId) +{ + RefPtr<dom::TabChild> browser = dom::TabChild::FindTabChild(aTabId); + + ContentProcessController* controller = new ContentProcessController(); + + nsAutoPtr<APZChild> apz(new APZChild(controller)); + + if (browser) { + + controller->SetBrowser(browser); + + } else { + + RefPtr<TabChildCreatedObserver> observer = + new TabChildCreatedObserver(controller, aTabId); + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + if (!os || + NS_FAILED(os->AddObserver(observer, "tab-child-created", false))) { + return nullptr; + } + controller->SetObserver(observer); + + } + + return apz.forget(); +} + +ContentProcessController::ContentProcessController() + : mBrowser(nullptr) +{ +} +ContentProcessController::~ContentProcessController() +{ + if (mObserver) { + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + os->RemoveObserver(mObserver, "tab-child-created"); + } +} + +void +ContentProcessController::SetObserver(nsIObserver* aObserver) +{ + MOZ_ASSERT(!mBrowser); + mObserver = aObserver; +} + +void +ContentProcessController::SetBrowser(dom::TabChild* aBrowser) +{ + MOZ_ASSERT(!mBrowser); + mBrowser = aBrowser; + + if (mObserver) { + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + os->RemoveObserver(mObserver, "tab-child-created"); + mObserver = nullptr; + } +} +void +ContentProcessController::RequestContentRepaint(const FrameMetrics& aFrameMetrics) +{ + if (mBrowser) { + mBrowser->UpdateFrame(aFrameMetrics); + } +} + +void +ContentProcessController::HandleTap( + TapType aType, + const LayoutDevicePoint& aPoint, + Modifiers aModifiers, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId) +{ + // This should never get called + MOZ_ASSERT(false); +} + +void +ContentProcessController::NotifyPinchGesture( + PinchGestureInput::PinchGestureType aType, + const ScrollableLayerGuid& aGuid, + 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) +{ + if (mBrowser) { + mBrowser->NotifyAPZStateChange(aGuid.mScrollId, aChange, aArg); + } +} + +void +ContentProcessController::NotifyMozMouseScrollEvent( + const FrameMetrics::ViewID& aScrollId, + const nsString& aEvent) +{ + if (mBrowser) { + APZCCallbackHelper::NotifyMozMouseScrollEvent(aScrollId, aEvent); + } +} + +void +ContentProcessController::NotifyFlushComplete() +{ + if (mBrowser) { + nsCOMPtr<nsIPresShell> shell; + if (nsCOMPtr<nsIDocument> doc = mBrowser->GetDocument()) { + shell = doc->GetShell(); + } + APZCCallbackHelper::NotifyFlushComplete(shell.get()); + } +} + +void +ContentProcessController::PostDelayedTask(already_AddRefed<Runnable> aRunnable, int aDelayMs) +{ + MOZ_ASSERT_UNREACHABLE("ContentProcessController should only be used remotely."); +} + +bool +ContentProcessController::IsRepaintThread() +{ + return NS_IsMainThread(); +} + +void +ContentProcessController::DispatchToRepaintThread(already_AddRefed<Runnable> aTask) +{ + NS_DispatchToMainThread(Move(aTask)); +} + +} // 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 000000000..07d113c9e --- /dev/null +++ b/gfx/layers/apz/util/ContentProcessController.h @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=4 ts=8 et 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 TabChild; +} // namespace dom + +namespace layers { + +class APZChild; + +/** + * ContentProcessController is a GeckoContentController for a TabChild, 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: + ~ContentProcessController(); + + static APZChild* Create(const dom::TabId& aTabId); + + // ContentProcessController + + void SetBrowser(dom::TabChild* aBrowser); + + // GeckoContentController + + void RequestContentRepaint(const FrameMetrics& frame) override; + + void HandleTap(TapType aType, + const LayoutDevicePoint& aPoint, + Modifiers aModifiers, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId) override; + + void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType, + const ScrollableLayerGuid& aGuid, + LayoutDeviceCoord aSpanChange, + Modifiers aModifiers) override; + + void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid, + APZStateChange aChange, + int aArg) override; + + void NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId, + const nsString& aEvent) override; + + void NotifyFlushComplete() override; + + void PostDelayedTask(already_AddRefed<Runnable> aRunnable, int aDelayMs) override; + + bool IsRepaintThread() override; + + void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) override; + +private: + ContentProcessController(); + + void SetObserver(nsIObserver* aObserver); + + RefPtr<dom::TabChild> mBrowser; + RefPtr<nsIObserver> mObserver; +}; + +} // 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 000000000..62dd5feaa --- /dev/null +++ b/gfx/layers/apz/util/DoubleTapToZoom.cpp @@ -0,0 +1,178 @@ +/* -*- 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/. */ + +#include "DoubleTapToZoom.h" + +#include <algorithm> // for std::min, std::max + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/dom/Element.h" +#include "nsCOMPtr.h" +#include "nsIContent.h" +#include "nsIDocument.h" +#include "nsIDOMHTMLLIElement.h" +#include "nsIDOMHTMLQuoteElement.h" +#include "nsIDOMWindow.h" +#include "nsIFrame.h" +#include "nsIFrameInlines.h" +#include "nsIPresShell.h" +#include "nsLayoutUtils.h" +#include "nsStyleConsts.h" + +namespace mozilla { +namespace layers { + +// Returns the DOM element found at |aPoint|, interpreted as being relative to +// the root frame of |aShell|. 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 nsDocument::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 nsCOMPtr<nsIPresShell>& aShell, + const CSSPoint& aPoint) +{ + if (nsIFrame* rootFrame = aShell->GetRootFrame()) { + if (nsIFrame* frame = nsLayoutUtils::GetFrameForPoint(rootFrame, + CSSPoint::ToAppUnits(aPoint), + nsLayoutUtils::IGNORE_PAINT_SUPPRESSION | + nsLayoutUtils::IGNORE_ROOT_SCROLL_FRAME)) { + while (frame && (!frame->GetContent() || frame->GetContent()->IsInAnonymousSubtree())) { + frame = nsLayoutUtils::GetParentOrPlaceholderFor(frame); + } + nsIContent* content = frame->GetContent(); + if (content && !content->IsElement()) { + content = content->GetParent(); + } + if (content) { + nsCOMPtr<dom::Element> result = content->AsElement(); + return result.forget(); + } + } + } + return nullptr; +} + +static bool +ShouldZoomToElement(const nsCOMPtr<dom::Element>& aElement) { + if (nsIFrame* frame = aElement->GetPrimaryFrame()) { + if (frame->GetDisplay() == StyleDisplay::Inline) { + return false; + } + } + if (aElement->IsAnyOfHTMLElements(nsGkAtoms::li, nsGkAtoms::q)) { + return false; + } + return true; +} + +static bool +IsRectZoomedIn(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. + CSSRect overlap = aCompositedArea.Intersect(aRect); + float overlapArea = overlap.width * overlap.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); +} + +CSSRect +CalculateRectToZoomTo(const nsCOMPtr<nsIDocument>& aRootContentDocument, + const CSSPoint& aPoint) +{ + // Ensure the layout information we get is up-to-date. + aRootContentDocument->FlushPendingNotifications(Flush_Layout); + + // An empty rect as return value is interpreted as "zoom out". + const CSSRect zoomOut; + + nsCOMPtr<nsIPresShell> shell = aRootContentDocument->GetShell(); + if (!shell) { + return zoomOut; + } + + nsIScrollableFrame* rootScrollFrame = shell->GetRootScrollFrameAsScrollable(); + if (!rootScrollFrame) { + return zoomOut; + } + + nsCOMPtr<dom::Element> element = ElementFromPoint(shell, aPoint); + if (!element) { + return zoomOut; + } + + while (element && !ShouldZoomToElement(element)) { + element = element->GetParentElement(); + } + + if (!element) { + return zoomOut; + } + + FrameMetrics metrics = nsLayoutUtils::CalculateBasicFrameMetrics(rootScrollFrame); + CSSRect compositedArea(metrics.GetScrollOffset(), metrics.CalculateCompositedSizeInCssPixels()); + const CSSCoord margin = 15; + CSSRect rect = nsLayoutUtils::GetBoundingContentRect(element, rootScrollFrame); + + // 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() && compositedArea.width > 0.0f) { + const float widthRatio = rect.width / compositedArea.width; + float targetHeight = compositedArea.height * widthRatio; + if (widthRatio < 0.9 && targetHeight < rect.height) { + const CSSPoint scrollPoint = CSSPoint::FromAppUnits(rootScrollFrame->GetScrollPosition()); + float newY = aPoint.y + scrollPoint.y - (targetHeight * 0.5f); + if ((newY + targetHeight) > (rect.y + rect.height)) { + rect.y += rect.height - targetHeight; + } else if (newY > rect.y) { + rect.y = newY; + } + rect.height = targetHeight; + } + } + + rect = CSSRect(std::max(metrics.GetScrollableRect().x, rect.x - margin), + rect.y, + rect.width + 2 * margin, + rect.height); + // Constrict the rect to the screen's right edge + rect.width = std::min(rect.width, metrics.GetScrollableRect().XMost() - rect.x); + + // 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 (IsRectZoomedIn(rect, compositedArea)) { + return zoomOut; + } + + CSSRect rounded(rect); + rounded.Round(); + + // If the block we're zooming to is really tall, and the user double-tapped + // more than a screenful of height from the top of it, then adjust the + // y-coordinate so that we center the actual point the user double-tapped + // upon. This prevents flying to the top of the page when double-tapping + // to zoom in (bug 761721). The 1.2 multiplier is just a little fuzz to + // compensate for 'rect' including horizontal margins but not vertical ones. + CSSCoord cssTapY = metrics.GetScrollOffset().y + aPoint.y; + if ((rect.height > rounded.height) && (cssTapY > rounded.y + (rounded.height * 1.2))) { + rounded.y = cssTapY - (rounded.height / 2); + } + + return rounded; +} + +} +} diff --git a/gfx/layers/apz/util/DoubleTapToZoom.h b/gfx/layers/apz/util/DoubleTapToZoom.h new file mode 100644 index 000000000..7b8723865 --- /dev/null +++ b/gfx/layers/apz/util/DoubleTapToZoom.h @@ -0,0 +1,29 @@ +/* -*- 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_DoubleTapToZoom_h +#define mozilla_layers_DoubleTapToZoom_h + +#include "Units.h" + +class nsIDocument; +template<class T> class nsCOMPtr; + +namespace mozilla { +namespace layers { + +/** + * For a double tap at |aPoint|, return the rect to which the browser + * should zoom in response, or an empty rect if the browser should zoom out. + * |aDocument| should be the root content document for the content that was + * tapped. + */ +CSSRect CalculateRectToZoomTo(const nsCOMPtr<nsIDocument>& aRootContentDocument, + const CSSPoint& aPoint); + +} +} + +#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 000000000..af5bd9f0f --- /dev/null +++ b/gfx/layers/apz/util/InputAPZContext.cpp @@ -0,0 +1,69 @@ +/* -*- 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/. */ + +#include "InputAPZContext.h" + +namespace mozilla { +namespace layers { + +ScrollableLayerGuid InputAPZContext::sGuid; +uint64_t InputAPZContext::sBlockId = 0; +nsEventStatus InputAPZContext::sApzResponse = nsEventStatus_eIgnore; +bool InputAPZContext::sRoutedToChildProcess = false; + +/*static*/ ScrollableLayerGuid +InputAPZContext::GetTargetLayerGuid() +{ + return sGuid; +} + +/*static*/ uint64_t +InputAPZContext::GetInputBlockId() +{ + return sBlockId; +} + +/*static*/ nsEventStatus +InputAPZContext::GetApzResponse() +{ + return sApzResponse; +} + +/*static*/ void +InputAPZContext::SetRoutedToChildProcess() +{ + sRoutedToChildProcess = true; +} + +InputAPZContext::InputAPZContext(const ScrollableLayerGuid& aGuid, + const uint64_t& aBlockId, + const nsEventStatus& aApzResponse) + : mOldGuid(sGuid) + , mOldBlockId(sBlockId) + , mOldApzResponse(sApzResponse) + , mOldRoutedToChildProcess(sRoutedToChildProcess) +{ + sGuid = aGuid; + sBlockId = aBlockId; + sApzResponse = aApzResponse; + sRoutedToChildProcess = false; +} + +InputAPZContext::~InputAPZContext() +{ + sGuid = mOldGuid; + sBlockId = mOldBlockId; + sApzResponse = mOldApzResponse; + sRoutedToChildProcess = mOldRoutedToChildProcess; +} + +bool +InputAPZContext::WasRoutedToChildProcess() +{ + return sRoutedToChildProcess; +} + +} // 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 000000000..0f232e1cb --- /dev/null +++ b/gfx/layers/apz/util/InputAPZContext.h @@ -0,0 +1,50 @@ +/* -*- 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_InputAPZContext_h +#define mozilla_layers_InputAPZContext_h + +#include "FrameMetrics.h" +#include "mozilla/EventForwards.h" + +namespace mozilla { +namespace layers { + +// InputAPZContext is used to communicate the ScrollableLayerGuid, +// input block ID, APZ response from nsIWidget to RenderFrameParent. +// It is conceptually attached to any WidgetInputEvent +// that has been processed by APZ directly from a widget. +class MOZ_STACK_CLASS InputAPZContext +{ +private: + static ScrollableLayerGuid sGuid; + static uint64_t sBlockId; + static nsEventStatus sApzResponse; + static bool sRoutedToChildProcess; + +public: + static ScrollableLayerGuid GetTargetLayerGuid(); + static uint64_t GetInputBlockId(); + static nsEventStatus GetApzResponse(); + static void SetRoutedToChildProcess(); + + InputAPZContext(const ScrollableLayerGuid& aGuid, + const uint64_t& aBlockId, + const nsEventStatus& aApzResponse); + ~InputAPZContext(); + + bool WasRoutedToChildProcess(); + +private: + ScrollableLayerGuid mOldGuid; + uint64_t mOldBlockId; + nsEventStatus mOldApzResponse; + bool mOldRoutedToChildProcess; +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_InputAPZContext_h */ diff --git a/gfx/layers/apz/util/ScrollInputMethods.h b/gfx/layers/apz/util/ScrollInputMethods.h new file mode 100644 index 000000000..ba599cd8b --- /dev/null +++ b/gfx/layers/apz/util/ScrollInputMethods.h @@ -0,0 +1,62 @@ +/* -*- 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_ScrollInputMethods_h +#define mozilla_layers_ScrollInputMethods_h + +namespace mozilla { +namespace layers { + +/** + * An enumeration that lists various input methods used to trigger scrolling. + * Used as the values for the SCROLL_INPUT_METHODS telemetry histogram. + */ +enum class ScrollInputMethod { + + // === Driven by APZ === + + ApzTouch, // touch events + ApzWheelPixel, // wheel events, pixel scrolling mode + ApzWheelLine, // wheel events, line scrolling mode + ApzWheelPage, // wheel events, page scrolling mode + ApzPanGesture, // pan gesture events (generally triggered by trackpad) + ApzScrollbarDrag, // dragging the scrollbar + + // === Driven by the main thread === + + // Keyboard + MainThreadScrollLine, // line scrolling + // (generally triggered by up/down arrow keys) + MainThreadScrollCharacter, // character scrolling + // (generally triggered by left/right arrow keys) + MainThreadScrollPage, // page scrolling + // (generally triggered by PageUp/PageDown keys) + MainThreadCompleteScroll, // scrolling to the end of the scroll range + // (generally triggered by Home/End keys) + MainThreadScrollCaretIntoView, // scrolling to bring the caret into view + // after moving the caret via the keyboard + + // Touch + MainThreadTouch, // touch events + + // Scrollbar + MainThreadScrollbarDrag, // dragging the scrollbar + MainThreadScrollbarButtonClick, // clicking the buttons at the ends of the + // scrollback track + MainThreadScrollbarTrackClick, // clicking the scrollbar track above or + // below the thumb + + // Autoscrolling + MainThreadAutoscrolling, // autoscrolling + + // New input methods can be added at the end, up to a maximum of 64. + // They should only be added at the end, to preserve the numerical values + // of the existing enumerators. +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_ScrollInputMethods_h */ diff --git a/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp b/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp new file mode 100644 index 000000000..758b705a3 --- /dev/null +++ b/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp @@ -0,0 +1,49 @@ +/* -*- 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/. */ + +#include "ScrollLinkedEffectDetector.h" + +#include "nsIDocument.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(nsIDocument* aDoc) + : mDocument(aDoc) +{ + 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(); + 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 000000000..f792586cf --- /dev/null +++ b/gfx/layers/apz/util/ScrollLinkedEffectDetector.h @@ -0,0 +1,43 @@ +/* -*- 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_ScrollLinkedEffectDetector_h +#define mozilla_layers_ScrollLinkedEffectDetector_h + +#include "mozilla/RefPtr.h" + +class nsIDocument; + +namespace mozilla { +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 +{ +private: + static uint32_t sDepth; + static bool sFoundScrollLinkedEffect; + +public: + static void PositioningPropertyMutated(); + + explicit ScrollLinkedEffectDetector(nsIDocument* aDoc); + ~ScrollLinkedEffectDetector(); + +private: + RefPtr<nsIDocument> mDocument; +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_ScrollLinkedEffectDetector_h */ diff --git a/gfx/layers/apz/util/TouchActionHelper.cpp b/gfx/layers/apz/util/TouchActionHelper.cpp new file mode 100644 index 000000000..b35fd2ec7 --- /dev/null +++ b/gfx/layers/apz/util/TouchActionHelper.cpp @@ -0,0 +1,96 @@ +/* -*- 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/. */ + +#include "TouchActionHelper.h" + +#include "mozilla/layers/APZCTreeManager.h" +#include "nsContainerFrame.h" +#include "nsIScrollableFrame.h" +#include "nsLayoutUtils.h" + +namespace mozilla { +namespace layers { + +void +TouchActionHelper::UpdateAllowedBehavior(uint32_t aTouchActionValue, + bool aConsiderPanning, + TouchBehaviorFlags& aOutBehavior) +{ + if (aTouchActionValue != NS_STYLE_TOUCH_ACTION_AUTO) { + // Double-tap-zooming need property value AUTO + aOutBehavior &= ~AllowedTouchBehavior::DOUBLE_TAP_ZOOM; + if (aTouchActionValue != NS_STYLE_TOUCH_ACTION_MANIPULATION) { + // Pinch-zooming need value AUTO or MANIPULATION + aOutBehavior &= ~AllowedTouchBehavior::PINCH_ZOOM; + } + } + + if (aConsiderPanning) { + if (aTouchActionValue == NS_STYLE_TOUCH_ACTION_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 & NS_STYLE_TOUCH_ACTION_PAN_X) && !(aTouchActionValue & NS_STYLE_TOUCH_ACTION_PAN_Y)) { + aOutBehavior &= ~AllowedTouchBehavior::VERTICAL_PAN; + } else if ((aTouchActionValue & NS_STYLE_TOUCH_ACTION_PAN_Y) && !(aTouchActionValue & NS_STYLE_TOUCH_ACTION_PAN_X)) { + aOutBehavior &= ~AllowedTouchBehavior::HORIZONTAL_PAN; + } + } +} + +TouchBehaviorFlags +TouchActionHelper::GetAllowedTouchBehavior(nsIWidget* aWidget, + nsIFrame* aRootFrame, + const LayoutDeviceIntPoint& aPoint) +{ + TouchBehaviorFlags behavior = AllowedTouchBehavior::VERTICAL_PAN | AllowedTouchBehavior::HORIZONTAL_PAN | + AllowedTouchBehavior::PINCH_ZOOM | AllowedTouchBehavior::DOUBLE_TAP_ZOOM; + + nsPoint relativePoint = + nsLayoutUtils::GetEventCoordinatesRelativeTo(aWidget, aPoint, aRootFrame); + + nsIFrame *target = nsLayoutUtils::GetFrameForPoint(aRootFrame, relativePoint, nsLayoutUtils::IGNORE_ROOT_SCROLL_FRAME); + if (!target) { + return behavior; + } + nsIScrollableFrame *nearestScrollableParent = nsLayoutUtils::GetNearestScrollableFrame(target, 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 = target; frame && frame->GetContent() && behavior; frame = frame->GetParent()) { + UpdateAllowedBehavior(nsLayoutUtils::GetTouchActionFromFrame(frame), 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 layers +} // namespace mozilla diff --git a/gfx/layers/apz/util/TouchActionHelper.h b/gfx/layers/apz/util/TouchActionHelper.h new file mode 100644 index 000000000..1dacfd4c0 --- /dev/null +++ b/gfx/layers/apz/util/TouchActionHelper.h @@ -0,0 +1,42 @@ +/* -*- 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_TouchActionHelper_h__ +#define __mozilla_layers_TouchActionHelper_h__ + +#include "mozilla/layers/APZUtils.h" // for TouchBehaviorFlags + +class nsIFrame; +class nsIWidget; + +namespace mozilla { +namespace layers { + +/* + * Helper class to figure out the allowed touch behavior for frames, as per + * the touch-action spec. + */ +class TouchActionHelper +{ +private: + static void UpdateAllowedBehavior(uint32_t aTouchActionValue, + bool aConsiderPanning, + TouchBehaviorFlags& aOutBehavior); + +public: + /* + * Performs hit testing on content, finds frame that corresponds to the aPoint 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 TouchBehaviorFlags GetAllowedTouchBehavior(nsIWidget* aWidget, + nsIFrame* aRootFrame, + const LayoutDeviceIntPoint& aPoint); +}; + +} // namespace layers +} // namespace mozilla + +#endif /*__mozilla_layers_TouchActionHelper_h__ */ |