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