diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /gfx/layers/apz/util | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gfx/layers/apz/util')
25 files changed, 4083 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..8d5d38f0cd --- /dev/null +++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp @@ -0,0 +1,925 @@ +/* -*- 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 "TouchActionHelper.h" +#include "gfxPlatform.h" // For gfxPlatform::UseTiling + +#include "mozilla/dom/Element.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/layers/LayerTransactionChild.h" +#include "mozilla/layers/RepaintRequest.h" +#include "mozilla/layers/ShadowLayers.h" +#include "mozilla/layers/WebRenderLayerManager.h" +#include "mozilla/layers/WebRenderBridgeChild.h" +#include "mozilla/DisplayPortUtils.h" +#include "mozilla/PresShell.h" +#include "mozilla/TouchEvents.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 "nsRefreshDriver.h" +#include "nsString.h" +#include "nsView.h" +#include "Layers.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, geckoScrollPosition.y) + .get()); + } + if (aFrame->GetScrollStyles().mHorizontal == StyleOverflow::Hidden && + targetScrollPosition.x != geckoScrollPosition.x) { + NS_WARNING( + nsPrintfCString( + "APZCCH: targetScrollPosition.x (%f) != geckoScrollPosition.x (%f)", + targetScrollPosition.x, geckoScrollPosition.x) + .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) { + aFrame->ScrollToCSSPixelsApproximate(targetScrollPosition, + ScrollOrigin::Apz); + geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition()); + aSuccessOut = true; + } + // Return the final scroll position after setting it so that anything that + // relies on it can have an accurate value. Note that even if we set it above + // re-querying it is a good idea because it may have gotten clamped or + // rounded. + return geckoScrollPosition; +} + +/** + * Scroll the scroll frame associated with |aContent| to the scroll position + * requested in |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.IsAnimationInProgress()); + 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(), + Some(aRequest.DisplayportPixelsPerCSSPixel())); + 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, + aRequest.DisplayportPixelsPerCSSPixel()); + } + } 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, + aRequest.DisplayportPixelsPerCSSPixel()); + } 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()), + Some(aRequest.DisplayportPixelsPerCSSPixel())); + } + + // 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, 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. + if (!FuzzyEqualsMultiplicative(presShellResolution, + aRequest.GetPresShellResolution())) { + return; + } + + // The pres shell resolution is updated by the the async zoom since the + // last paint. + presShellResolution = + aRequest.GetPresShellResolution() * aRequest.GetAsyncZoom().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()); + sf->ScrollToCSSPixelsApproximate(currentScrollPosition, ScrollOrigin::Apz); + } + + // 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)) { + nsPresContext* pc = aPresShell->GetPresContext(); + // This code is only correct for root content or toplevel documents. + MOZ_ASSERT(!pc || pc->IsRootContentDocumentCrossProcess() || + !pc->GetParentPresContext()); + nsIFrame* frame = aPresShell->GetRootScrollFrame(); + if (!frame) { + frame = aPresShell->GetRootFrame(); + } + nsRect baseRect; + if (frame) { + baseRect = nsRect(nsPoint(0, 0), + nsLayoutUtils::CalculateCompositionSizeForFrame(frame)); + } else if (pc) { + baseRect = nsRect(nsPoint(0, 0), pc->GetVisibleArea().Size()); + } + MOZ_LOG( + sDisplayportLog, LogLevel::Debug, + ("Initializing root displayport on scrollId=%" PRIu64 "\n", viewId)); + DisplayPortUtils::SetDisplayPortBaseIfNotSet(content, baseRect); + // Note that we also set the base rect that goes with these margins in + // nsRootBoxFrame::BuildDisplayList. + DisplayPortUtils::SetDisplayPortMargins( + content, aPresShell, DisplayPortMargins::Empty(content), 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, uint64_t aTime, const LayoutDevicePoint& aRefPoint, + Modifiers aModifiers, int32_t aClickCount, nsIWidget* aWidget) { + MOZ_ASSERT(aMsg == eMouseMove || aMsg == eMouseDown || aMsg == eMouseUp || + aMsg == eMouseLongTap); + + WidgetMouseEvent event(true, aMsg, aWidget, WidgetMouseEvent::eReal, + WidgetMouseEvent::eNormal); + event.mRefPoint = LayoutDeviceIntPoint::Truncate(aRefPoint.x, aRefPoint.y); + event.mTime = aTime; + event.mButton = MouseButton::ePrimary; + 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); +} + +bool 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, true); + + bool defaultPrevented = false; + nsContentUtils::SendMouseEvent( + aPresShell, aType, aPoint.x, aPoint.y, aButton, + nsIDOMWindowUtils::MOUSE_BUTTONS_NOT_SPECIFIED, aClickCount, aModifiers, + /* aIgnoreRootScrollFrame = */ false, 0, aInputSourceArg, aPointerId, + false, &defaultPrevented, false, /* aIsWidgetEventSynthesized = */ false); + return defaultPrevented; +} + +void APZCCallbackHelper::FireSingleTapEvent(const LayoutDevicePoint& aPoint, + Modifiers aModifiers, + int32_t aClickCount, + nsIWidget* aWidget) { + if (aWidget->Destroyed()) { + return; + } + APZCCH_LOG("Dispatching single-tap component events to %s\n", + ToString(aPoint).c_str()); + int time = 0; + DispatchSynthesizedMouseEvent(eMouseMove, time, aPoint, aModifiers, + aClickCount, aWidget); + DispatchSynthesizedMouseEvent(eMouseDown, time, aPoint, aModifiers, + aClickCount, aWidget); + DispatchSynthesizedMouseEvent(eMouseUp, time, aPoint, aModifiers, aClickCount, + aWidget); +} + +static dom::Element* GetDisplayportElementFor( + nsIScrollableFrame* aScrollableFrame) { + if (!aScrollableFrame) { + return nullptr; + } + nsIFrame* scrolledFrame = aScrollableFrame->GetScrolledFrame(); + if (!scrolledFrame) { + return nullptr; + } + // |scrolledFrame| should at this point be the root content frame of the + // nearest ancestor scrollable frame. The element corresponding to this + // frame should be the one with the displayport set on it, so find that + // element and return it. + nsIContent* content = scrolledFrame->GetContent(); + MOZ_ASSERT(content->IsElement()); // roc says this must be true + return content->AsElement(); +} + +static dom::Element* GetRootDocumentElementFor(nsIWidget* aWidget) { + // This returns the root element that ChromeProcessController sets the + // displayport on during initialization. + if (nsView* view = nsView::GetViewFor(aWidget)) { + if (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::HasDisplayPort(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 true; +} + +static void SendLayersDependentApzcTargetConfirmation( + PresShell* aPresShell, uint64_t aInputBlockId, + nsTArray<ScrollableLayerGuid>&& aTargets) { + LayerManager* lm = aPresShell->GetLayerManager(); + if (!lm) { + return; + } + + if (WebRenderLayerManager* wrlm = lm->AsWebRenderLayerManager()) { + if (WebRenderBridgeChild* wrbc = wrlm->WrBridge()) { + wrbc->SendSetConfirmedTargetAPZC(aInputBlockId, aTargets); + } + return; + } + + ShadowLayerForwarder* lf = lm->AsShadowForwarder(); + if (!lf) { + return; + } + + LayerTransactionChild* shadow = lf->GetShadowManager(); + if (!shadow) { + return; + } + + shadow->SendSetConfirmedTargetAPZC(aInputBlockId, aTargets); +} + +} // namespace + +DisplayportSetListener::DisplayportSetListener( + nsIWidget* aWidget, PresShell* aPresShell, const uint64_t& aInputBlockId, + nsTArray<ScrollableLayerGuid>&& aTargets) + : OneShotPostRefreshObserver(aPresShell), + mWidget(aWidget), + mInputBlockId(aInputBlockId), + mTargets(std::move(aTargets)) { + MOZ_ASSERT(!mAction, "Setting Action twice"); + mAction = [](PresShell* aPresShell, + OneShotPostRefreshObserver* aThisObserver) { + OnPostRefresh(static_cast<DisplayportSetListener*>(aThisObserver), + aPresShell); + }; +} + +DisplayportSetListener::~DisplayportSetListener() = default; + +bool DisplayportSetListener::Register() { + if (nsPresContext* presContext = mPresShell->GetPresContext()) { + presContext->RegisterOneShotPostRefreshObserver(this); + APZCCH_LOG("Successfully registered post-refresh observer\n"); + return true; + } + // In case of failure just send the notification right away + APZCCH_LOG("Sending target APZCs for input block %" PRIu64 "\n", + mInputBlockId); + mWidget->SetConfirmedTargetAPZC(mInputBlockId, mTargets); + return false; +} + +/* static */ +void DisplayportSetListener::OnPostRefresh(DisplayportSetListener* aListener, + PresShell* aPresShell) { + APZCCH_LOG("Got refresh, sending target APZCs for input block %" PRIu64 "\n", + aListener->mInputBlockId); + SendLayersDependentApzcTargetConfirmation( + aPresShell, aListener->mInputBlockId, std::move(aListener->mTargets)); +} + +UniquePtr<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 MakeUnique<DisplayportSetListener>( + aWidget, presShell, aInputBlockId, std::move(targets)); + } + APZCCH_LOG("Sending target APZCs for input block %" PRIu64 "\n", + aInputBlockId); + aWidget->SetConfirmedTargetAPZC(aInputBlockId, targets); + } + } + } + return nullptr; +} + +nsTArray<TouchBehaviorFlags> +APZCCallbackHelper::SendSetAllowedTouchBehaviorNotification( + nsIWidget* aWidget, dom::Document* aDocument, + const WidgetTouchEvent& aEvent, uint64_t aInputBlockId, + const SetAllowedTouchBehaviorCallback& aCallback) { + nsTArray<TouchBehaviorFlags> flags; + if (!aWidget || !aDocument) { + return flags; + } + if (PresShell* presShell = aDocument->GetPresShell()) { + if (nsIFrame* rootFrame = presShell->GetRootFrame()) { + for (uint32_t i = 0; i < aEvent.mTouches.Length(); i++) { + flags.AppendElement(TouchActionHelper::GetAllowedTouchBehavior( + aWidget, RelativeTo{rootFrame, ViewportType::Visual}, + aEvent.mTouches[i]->mRefPoint)); + } + aCallback(aInputBlockId, flags); + } + } + return flags; +} + +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 IncludeApzAnimation = nsIScrollableFrame::IncludeApzAnimation; + + return aFrame->IsScrollAnimating(IncludeApzAnimation::No) || + 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::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..4bf6806c76 --- /dev/null +++ b/gfx/layers/apz/util/APZCCallbackHelper.h @@ -0,0 +1,204 @@ +/* -*- 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; +template <class T> +struct already_AddRefed; +template <class T> +class nsCOMPtr; + +namespace mozilla { + +class PresShell; + +namespace layers { + +struct RepaintRequest; + +typedef std::function<void(uint64_t, const nsTArray<TouchBehaviorFlags>&)> + SetAllowedTouchBehaviorCallback; + +/* Refer to documentation on SendSetTargetAPZCNotification for this class */ +class DisplayportSetListener : public OneShotPostRefreshObserver { + public: + DisplayportSetListener(nsIWidget* aWidget, PresShell* aPresShell, + const uint64_t& aInputBlockId, + nsTArray<ScrollableLayerGuid>&& aTargets); + virtual ~DisplayportSetListener(); + bool Register(); + + private: + RefPtr<nsIWidget> mWidget; + uint64_t mInputBlockId; + nsTArray<ScrollableLayerGuid> mTargets; + + static void OnPostRefresh(DisplayportSetListener* aListener, + PresShell* aPresShell); +}; + +/* 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, uint64_t aTime, const LayoutDevicePoint& aRefPoint, + Modifiers aModifiers, int32_t aClickCount, nsIWidget* aWidget); + + /* Dispatch a mouse event with the given parameters. + * Return whether or not any listeners have called preventDefault on the + * event. + * This is a lightweight wrapper around nsContentUtils::SendMouseEvent() + * and as such expects |aPoint| to be in layout coordinates. */ + MOZ_CAN_RUN_SCRIPT + static bool 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, + * and release()'ing the UniquePtr if that Register() call returns true. + * 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 UniquePtr<DisplayportSetListener> SendSetTargetAPZCNotification( + nsIWidget* aWidget, mozilla::dom::Document* aDocument, + const WidgetGUIEvent& aEvent, const LayersId& aLayersId, + uint64_t aInputBlockId); + + /* Figure out the allowed touch behaviors of each touch point in |aEvent| + * and send that information to the provided callback. Also returns the + * allowed touch behaviors. */ + static nsTArray<TouchBehaviorFlags> SendSetAllowedTouchBehaviorNotification( + nsIWidget* aWidget, mozilla::dom::Document* aDocument, + const WidgetTouchEvent& aEvent, uint64_t aInputBlockId, + const SetAllowedTouchBehaviorCallback& aCallback); + + /* 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); + + /* + * 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..159952f772 --- /dev/null +++ b/gfx/layers/apz/util/APZEventState.cpp @@ -0,0 +1,553 @@ +/* -*- 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 "nsITimer.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), + 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; + +class DelayedFireSingleTapEvent final : public nsITimerCallback, + public nsINamed { + public: + NS_DECL_ISUPPORTS + + DelayedFireSingleTapEvent(nsWeakPtr aWidget, LayoutDevicePoint& aPoint, + Modifiers aModifiers, int32_t aClickCount, + nsITimer* aTimer, RefPtr<nsIContent>& aTouchRollup) + : mWidget(aWidget), + mPoint(aPoint), + mModifiers(aModifiers), + mClickCount(aClickCount) + // Hold the reference count until we are called back. + , + mTimer(aTimer), + mTouchRollup(aTouchRollup) {} + + NS_IMETHOD Notify(nsITimer*) override { + if (nsCOMPtr<nsIWidget> widget = do_QueryReferent(mWidget)) { + widget::nsAutoRollup rollup(mTouchRollup.get()); + APZCCallbackHelper::FireSingleTapEvent(mPoint, mModifiers, mClickCount, + widget); + } + mTimer = nullptr; + return NS_OK; + } + + NS_IMETHOD + GetName(nsACString& aName) override { + aName.AssignLiteral("DelayedFireSingleTapEvent"); + return NS_OK; + } + + void ClearTimer() { mTimer = nullptr; } + + private: + ~DelayedFireSingleTapEvent() = default; + + nsWeakPtr mWidget; + LayoutDevicePoint mPoint; + Modifiers mModifiers; + int32_t mClickCount; + nsCOMPtr<nsITimer> mTimer; + RefPtr<nsIContent> mTouchRollup; +}; + +NS_IMPL_ISUPPORTS(DelayedFireSingleTapEvent, nsITimerCallback, nsINamed) + +void APZEventState::ProcessSingleTap(const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers, + int32_t aClickCount) { + 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; + } + + LayoutDevicePoint ldPoint = aPoint * aScale; + + APZES_LOG("Scheduling timer for click event\n"); + nsCOMPtr<nsITimer> timer = NS_NewTimer(); + RefPtr<DelayedFireSingleTapEvent> callback = new DelayedFireSingleTapEvent( + mWidget, ldPoint, aModifiers, aClickCount, timer, touchRollup); + nsresult rv = timer->InitWithCallback( + callback, StaticPrefs::ui_touch_activation_duration_ms(), + nsITimer::TYPE_ONE_SHOT); + if (NS_FAILED(rv)) { + // Make |callback| not hold the timer, so they will both be destructed when + // we leave the scope of this function. + callback->ClearTimer(); + } +} + +bool APZEventState::FireContextmenuEvents(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, 0 /* time */, 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)); + bool eventHandled = 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 handled: %d\n", eventHandled); + if (eventHandled) { + // If the contextmenu event was handled then we're showing a contextmenu, + // and so we should remove any activation + mActiveElementManager->ClearActivation(); +#ifndef XP_WIN + } else { + // If the contextmenu wasn't consumed, fire the eMouseLongTap event. + nsEventStatus status = APZCCallbackHelper::DispatchSynthesizedMouseEvent( + eMouseLongTap, /*time*/ 0, aPoint * aScale, aModifiers, + /*clickCount*/ 1, aWidget); + eventHandled = (status == nsEventStatus_eConsumeNoDefault); + APZES_LOG("eMouseLongTap event handled: %d\n", eventHandled); +#endif + } + + return eventHandled; +} + +void APZEventState::ProcessLongTap(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, /*time*/ 0, aPoint * aScale, aModifiers, /*clickCount*/ 1, + widget); + + bool eventHandled = (status == nsEventStatus_eConsumeNoDefault); +#else + bool eventHandled = + FireContextmenuEvents(aPresShell, aPoint, aScale, aModifiers, widget); +#endif + mContentReceivedInputBlockCallback(aInputBlockId, eventHandled); + + if (eventHandled) { + // Also send a touchcancel to content, so that listeners that might be + // waiting for a touchend don't trigger. + WidgetTouchEvent cancelTouchEvent(true, eTouchCancel, widget.get()); + cancelTouchEvent.mModifiers = aModifiers; + auto ldPoint = LayoutDeviceIntPoint::Round(aPoint * aScale); + cancelTouchEvent.mTouches.AppendElement(new mozilla::dom::Touch( + mLastTouchIdentifier, ldPoint, LayoutDeviceIntPoint(), 0, 0)); + APZCCallbackHelper::DispatchWidgetEvent(cancelTouchEvent); + } +} + +void APZEventState::ProcessLongTapUp(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]->GetTarget()); + 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 %d\n", sentContentResponse, + !isTouchPrevented, aApzResponse == nsEventStatus_eConsumeDoDefault, + StaticPrefs::dom_w3c_pointer_events_enabled(), + MainThreadAgreesEventsAreConsumableByAPZ()); + if (sentContentResponse && !isTouchPrevented && + aApzResponse == nsEventStatus_eConsumeDoDefault && + StaticPrefs::dom_w3c_pointer_events_enabled() && + 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) { + switch (aChange) { + case APZStateChange::eTransformBegin: { + nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aViewId); + if (sf) { + sf->SetTransformingByAPZ(true); + } + nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(sf); + if (scrollbarMediator) { + scrollbarMediator->ScrollbarActivityStarted(); + } + + nsIContent* content = nsLayoutUtils::FindContentFor(aViewId); + 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); + } + nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(sf); + if (scrollbarMediator) { + scrollbarMediator->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: { + mActiveElementManager->HandleTouchStart(aArg); + break; + } + case APZStateChange::eStartPanning: { + // The user started to pan, so we don't want anything to be :active. + mActiveElementManager->ClearActivation(); + break; + } + case APZStateChange::eEndTouch: { + mEndTouchIsClick = aArg; + mActiveElementManager->HandleTouchEnd(); + break; + } + } +} + +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..60020f0f9a --- /dev/null +++ b/gfx/layers/apz/util/APZEventState.h @@ -0,0 +1,124 @@ +/* -*- 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 "nsCOMPtr.h" +#include "nsISupportsImpl.h" // for NS_INLINE_DECL_REFCOUNTING +#include "nsIWeakReferenceUtils.h" // for nsWeakPtr + +#include <functional> + +template <class> +class nsCOMPtr; +class nsIContent; +class nsIWidget; + +namespace mozilla { + +class PresShell; + +namespace layers { + +class ActiveElementManager; + +typedef std::function<void(uint64_t /* input block id */, + bool /* prevent default */)> + ContentReceivedInputBlockCallback; + +/** + * A content-side component that keeps track of state for handling APZ + * gestures and sending APZ notifications. + */ +class APZEventState final { + typedef GeckoContentController_APZStateChange APZStateChange; + typedef ScrollableLayerGuid::ViewID ViewID; + + public: + APZEventState(nsIWidget* aWidget, + ContentReceivedInputBlockCallback&& aCallback); + + NS_INLINE_DECL_REFCOUNTING(APZEventState); + + void ProcessSingleTap(const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers, int32_t aClickCount); + 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); + void ProcessClusterHit(); + + private: + ~APZEventState(); + bool SendPendingTouchPreventedResponse(bool aPreventDefault); + MOZ_CAN_RUN_SCRIPT + bool 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; + 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/APZThreadUtils.cpp b/gfx/layers/apz/util/APZThreadUtils.cpp new file mode 100644 index 0000000000..63db976834 --- /dev/null +++ b/gfx/layers/apz/util/APZThreadUtils.cpp @@ -0,0 +1,118 @@ +/* -*- 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/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; + +/*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) { + 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()) { + task->Run(); + } else { + thread->Dispatch(task.forget()); + } +} + +/*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)); + } +} + +NS_IMPL_ISUPPORTS(GenericNamedTimerCallbackBase, nsITimerCallback, nsINamed) + +} // 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..fb03ac10da --- /dev/null +++ b/gfx/layers/apz/util/APZThreadUtils.h @@ -0,0 +1,117 @@ +/* -*- 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 "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); + + /** + * 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); +}; + +// A base class for GenericNamedTimerCallback<Function>. +// This is necessary because NS_IMPL_ISUPPORTS doesn't work for a class +// template. +class GenericNamedTimerCallbackBase : public nsITimerCallback, public nsINamed { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + protected: + virtual ~GenericNamedTimerCallbackBase() = default; +}; + +// An nsITimerCallback implementation with nsINamed that can be used with any +// function object that's callable with no arguments. +template <typename Function> +class GenericNamedTimerCallback final : public GenericNamedTimerCallbackBase { + public: + GenericNamedTimerCallback(const Function& aFunction, const char* aName) + : mFunction(aFunction), mName(aName) {} + + NS_IMETHOD Notify(nsITimer*) override { + mFunction(); + return NS_OK; + } + + NS_IMETHOD GetName(nsACString& aName) override { + aName = mName; + return NS_OK; + } + + private: + Function mFunction; + nsCString mName; +}; + +// Convenience function for constructing a GenericNamedTimerCallback. +// Returns a raw pointer, suitable for passing directly as an argument to +// nsITimer::InitWithCallback(). The intention is to enable the following +// terse inline usage: +// timer->InitWithCallback(NewNamedTimerCallback([](){ ... }, name), delay); +template <typename Function> +GenericNamedTimerCallback<Function>* NewNamedTimerCallback( + const Function& aFunction, const char* aName) { + return new GenericNamedTimerCallback<Function>(aFunction, aName); +} + +} // 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..ed485c3a1d --- /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/EventStates.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 = do_QueryInterface(aTarget); + AEM_LOG("Setting target element to %p\n", mTarget.get()); + TriggerElementActivation(); +} + +void ActiveElementManager::HandleTouchStart(bool aCanBePan) { + AEM_LOG("Touch start, aCanBePan: %d\n", aCanBePan); + if (mCanBePanSet) { + // Multiple fingers on screen (since HandleTouchEnd clears mCanBePanSet). + AEM_LOG("Multiple fingers on-screen, clearing touch block state\n"); + CancelTask(); + ResetActive(); + ResetTouchBlockState(); + return; + } + + mCanBePan = aCanBePan; + mCanBePanSet = true; + TriggerElementActivation(); +} + +void ActiveElementManager::TriggerElementActivation() { + // Both HandleTouchStart() and SetTargetElement() call this. They can be + // called in either order. One will set mCanBePanSet, and the other, mTarget. + // We want to actually trigger the activation once both are set. + if (!(mTarget && mCanBePanSet)) { + return; + } + + // If the touch cannot be a pan, make mTarget :active right away. + // Otherwise, wait a bit to see if the user will pan or not. + if (!mCanBePan) { + SetActive(mTarget); + } else { + CancelTask(); // this is only needed because of bug 1169802. Fixing that + // bug properly should make this unnecessary. + MOZ_ASSERT(mSetActiveTask == nullptr); + + RefPtr<CancelableRunnable> task = + NewCancelableRunnableMethod<nsCOMPtr<dom::Element>>( + "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, NS_EVENT_STATE_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..24668deecf --- /dev/null +++ b/gfx/layers/apz/util/CheckerboardReportService.cpp @@ -0,0 +1,219 @@ +/* -*- 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) +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CheckerboardReportService, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CheckerboardReportService, Release) + +/*static*/ +bool CheckerboardReportService::IsEnabled(JSContext* aCtx, JSObject* aGlobal) { + // Only allow this in the parent process + if (!XRE_IsParentProcess()) { + return false; + } + // Allow privileged code or about:checkerboard (unprivileged) to access this. + return nsContentUtils::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..3062255f95 --- /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_SCRIPT_HOLDER_NATIVE_CLASS(CheckerboardReportService) + + public: + /* + * The methods exposed via the webidl. + */ + void GetReports(nsTArray<dom::CheckerboardReport>& aOutReports); + bool IsRecordingEnabled() const; + void SetRecordingEnabled(bool aEnabled); + void FlushActiveReports(); + + private: + virtual ~CheckerboardReportService() = 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..0f907f8049 --- /dev/null +++ b/gfx/layers/apz/util/ChromeProcessController.cpp @@ -0,0 +1,335 @@ +/* -*- 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; + } + + CSSRect zoomToRect = CalculateRectToZoomTo(document, aPoint); + + uint32_t presShellId; + ScrollableLayerGuid::ViewID viewId; + if (APZCCallbackHelper::GetOrCreateScrollIdentifiers( + document->GetDocumentElement(), &presShellId, &viewId)) { + APZThreadUtils::RunOnControllerThread( + NewRunnableMethod<ScrollableLayerGuid, CSSRect, uint32_t>( + "IAPZCTreeManager::ZoomToRect", mAPZCTreeManager, + &IAPZCTreeManager::ZoomToRect, + ScrollableLayerGuid(aGuid.mLayersId, presShellId, viewId), + zoomToRect, 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); + break; + case TapType::eDoubleTap: + HandleDoubleTap(point, aModifiers, aGuid); + break; + case TapType::eSecondTap: + mAPZEventState->ProcessSingleTap(point, scale, aModifiers, 2); + 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) { + if (!mUIThread->IsOnCurrentThread()) { + mUIThread->Dispatch( + NewRunnableMethod<ScrollableLayerGuid, APZStateChange, int>( + "layers::ChromeProcessController::NotifyAPZStateChange", this, + &ChromeProcessController::NotifyAPZStateChange, aGuid, aChange, + aArg)); + return; + } + + if (!mAPZEventState) { + return; + } + + mAPZEventState->ProcessAPZStateChange(aGuid.mScrollId, aChange, aArg); +} + +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); +} diff --git a/gfx/layers/apz/util/ChromeProcessController.h b/gfx/layers/apz/util/ChromeProcessController.h new file mode 100644 index 0000000000..158a0d6eba --- /dev/null +++ b/gfx/layers/apz/util/ChromeProcessController.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_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) 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; + + 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..0c52890631 --- /dev/null +++ b/gfx/layers/apz/util/ContentProcessController.cpp @@ -0,0 +1,108 @@ +/* -*- 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) { + if (mBrowser) { + mBrowser->NotifyAPZStateChange(aGuid.mScrollId, aChange, aArg); + } +} + +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"); +} + +bool ContentProcessController::IsRepaintThread() { return NS_IsMainThread(); } + +void ContentProcessController::DispatchToRepaintThread( + already_AddRefed<Runnable> aTask) { + NS_DispatchToMainThread(std::move(aTask)); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/util/ContentProcessController.h b/gfx/layers/apz/util/ContentProcessController.h new file mode 100644 index 0000000000..ab5522cd44 --- /dev/null +++ b/gfx/layers/apz/util/ContentProcessController.h @@ -0,0 +1,87 @@ +/* -*- 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) 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; + + bool IsRepaintThread() override; + + void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) 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..6d4d3d7f0c --- /dev/null +++ b/gfx/layers/apz/util/DoubleTapToZoom.cpp @@ -0,0 +1,188 @@ +/* -*- 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 "nsLayoutUtils.h" +#include "nsStyleConsts.h" + +namespace mozilla { +namespace layers { + +namespace { + +using FrameForPointOption = nsLayoutUtils::FrameForPointOption; + +// 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())) { + 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; +} + +static bool ShouldZoomToElement(const nsCOMPtr<dom::Element>& aElement) { + if (nsIFrame* frame = aElement->GetPrimaryFrame()) { + if (frame->StyleDisplay()->IsInlineFlow()) { + return false; + } + } + if (aElement->IsAnyOfHTMLElements(nsGkAtoms::li, nsGkAtoms::q)) { + return false; + } + return true; +} + +static bool IsRectZoomedIn(const CSSRect& aRect, + const CSSRect& aCompositedArea) { + // This functions checks to see if the area of the rect visible in the + // composition bounds (i.e. the overlapArea variable below) is approximately + // the max area of the rect we can show. + CSSRect overlap = aCompositedArea.Intersect(aRect); + float overlapArea = overlap.Width() * overlap.Height(); + float availHeight = std::min( + aRect.Width() * aCompositedArea.Height() / aCompositedArea.Width(), + aRect.Height()); + float showing = overlapArea / (aRect.Width() * availHeight); + float ratioW = aRect.Width() / aCompositedArea.Width(); + float ratioH = aRect.Height() / aCompositedArea.Height(); + + return showing > 0.9 && (ratioW > 0.9 || ratioH > 0.9); +} + +} // namespace + +CSSRect 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 zoomOut; + } + + nsIScrollableFrame* rootScrollFrame = + presShell->GetRootScrollFrameAsScrollable(); + if (!rootScrollFrame) { + return zoomOut; + } + + nsCOMPtr<dom::Element> element = ElementFromPoint(presShell, aPoint); + if (!element) { + return zoomOut; + } + + while (element && !ShouldZoomToElement(element)) { + element = element->GetParentElement(); + } + + if (!element) { + return zoomOut; + } + + FrameMetrics metrics = + nsLayoutUtils::CalculateBasicFrameMetrics(rootScrollFrame); + CSSPoint visualScrollOffset = metrics.GetVisualScrollOffset(); + CSSRect compositedArea(visualScrollOffset, + metrics.CalculateCompositedSizeInCssPixels()); + const CSSCoord margin = 15; + CSSRect rect = + nsLayoutUtils::GetBoundingContentRect(element, rootScrollFrame); + + // If the element is taller than the visible area of the page scale + // the height of the |rect| so that it has the same aspect ratio as + // the root frame. The clipped |rect| is centered on the y value of + // the touch point. This allows tall narrow elements to be zoomed. + if (!rect.IsEmpty() && compositedArea.Width() > 0.0f) { + const float widthRatio = rect.Width() / compositedArea.Width(); + float targetHeight = compositedArea.Height() * widthRatio; + if (widthRatio < 0.9 && targetHeight < rect.Height()) { + const CSSPoint scrollPoint = + CSSPoint::FromAppUnits(rootScrollFrame->GetScrollPosition()); + float newY = aPoint.y + scrollPoint.y - (targetHeight * 0.5f); + if ((newY + targetHeight) > rect.YMost()) { + rect.MoveByY(rect.Height() - targetHeight); + } else if (newY > rect.Y()) { + rect.MoveToY(newY); + } + rect.SetHeight(targetHeight); + } + } + + rect = CSSRect(std::max(metrics.GetScrollableRect().X(), rect.X() - margin), + rect.Y(), rect.Width() + 2 * margin, rect.Height()); + // Constrict the rect to the screen's right edge + rect.SetWidth( + std::min(rect.Width(), metrics.GetScrollableRect().XMost() - rect.X())); + + // If the rect is already taking up most of the visible area and is + // stretching the width of the page, then we want to zoom out instead. + if (IsRectZoomedIn(rect, compositedArea)) { + return zoomOut; + } + + CSSRect rounded(rect); + rounded.Round(); + + // If the block we're zooming to is really tall, and the user double-tapped + // more than a screenful of height from the top of it, then adjust the + // y-coordinate so that we center the actual point the user double-tapped + // upon. This prevents flying to the top of the page when double-tapping + // to zoom in (bug 761721). The 1.2 multiplier is just a little fuzz to + // compensate for 'rect' including horizontal margins but not vertical ones. + CSSCoord cssTapY = visualScrollOffset.y + aPoint.y; + if ((rect.Height() > rounded.Height()) && + (cssTapY > rounded.Y() + (rounded.Height() * 1.2))) { + rounded.MoveToY(cssTapY - (rounded.Height() / 2)); + } + + return rounded; +} + +} // 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..163481fe03 --- /dev/null +++ b/gfx/layers/apz/util/DoubleTapToZoom.h @@ -0,0 +1,35 @@ +/* -*- 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 { + +/** + * For a double tap at |aPoint|, return the rect to which the browser + * should zoom in response, or an empty rect if the browser should zoom out. + * |aDocument| should be the root content document for the content that was + * tapped. + */ +CSSRect CalculateRectToZoomTo( + const 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/ScrollInputMethods.h b/gfx/layers/apz/util/ScrollInputMethods.h new file mode 100644 index 0000000000..999f2c6abf --- /dev/null +++ b/gfx/layers/apz/util/ScrollInputMethods.h @@ -0,0 +1,78 @@ +/* -*- 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_ScrollInputMethods_h +#define mozilla_layers_ScrollInputMethods_h + +namespace mozilla { +namespace layers { + +/** + * An enumeration that lists various input methods used to trigger scrolling. + * Used as the values for the SCROLL_INPUT_METHODS telemetry histogram. + */ +enum class ScrollInputMethod { + + // === Driven by APZ === + + ApzTouch, // touch events + ApzWheelPixel, // wheel events, pixel scrolling mode + ApzWheelLine, // wheel events, line scrolling mode + ApzWheelPage, // wheel events, page scrolling mode + ApzPanGesture, // pan gesture events (generally triggered by trackpad) + ApzScrollbarDrag, // dragging the scrollbar + + // === Driven by the main thread === + + // Keyboard + MainThreadScrollLine, // line scrolling + // (generally triggered by up/down arrow keys) + MainThreadScrollCharacter, // character scrolling + // (generally triggered by left/right arrow keys) + MainThreadScrollPage, // page scrolling + // (generally triggered by PageUp/PageDown keys) + MainThreadCompleteScroll, // scrolling to the end of the scroll range + // (generally triggered by Home/End keys) + MainThreadScrollCaretIntoView, // scrolling to bring the caret into view + // after moving the caret via the keyboard + + // Touch + MainThreadTouch, // touch events + + // Scrollbar + MainThreadScrollbarDrag, // dragging the scrollbar + MainThreadScrollbarButtonClick, // clicking the buttons at the ends of the + // scrollback track + MainThreadScrollbarTrackClick, // clicking the scrollbar track above or + // below the thumb + + // Autoscrolling + MainThreadAutoscrolling, // autoscrolling + + // === Additional input methods implemented in APZ === + + // Async Keyboard + ApzScrollLine, // line scrolling + // (generally triggered by up/down arrow keys) + ApzScrollCharacter, // character scrolling + // (generally triggered by left/right arrow keys) + ApzScrollPage, // page scrolling + // (generally triggered by PageUp/PageDown keys) + ApzCompleteScroll, // scrolling to the end of the scroll range + // (generally triggered by Home/End keys) + + // Autoscrolling + ApzAutoscrolling, + + // New input methods can be added at the end, up to a maximum of 64. + // They should only be added at the end, to preserve the numerical values + // of the existing enumerators. +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_ScrollInputMethods_h */ diff --git a/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp b/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp new file mode 100644 index 0000000000..6ef8749a9c --- /dev/null +++ b/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp @@ -0,0 +1,47 @@ +/* -*- 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) + : mDocument(aDoc) { + MOZ_ASSERT(NS_IsMainThread()); + sDepth++; +} + +ScrollLinkedEffectDetector::~ScrollLinkedEffectDetector() { + sDepth--; + if (sDepth == 0) { + // We have exited all (possibly-nested) scroll event dispatches, + // record whether or not we found an effect, and reset state + if (sFoundScrollLinkedEffect) { + mDocument->ReportHasScrollLinkedEffect(); + sFoundScrollLinkedEffect = false; + } + } +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/util/ScrollLinkedEffectDetector.h b/gfx/layers/apz/util/ScrollLinkedEffectDetector.h new file mode 100644 index 0000000000..81044ed614 --- /dev/null +++ b/gfx/layers/apz/util/ScrollLinkedEffectDetector.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_ScrollLinkedEffectDetector_h +#define mozilla_layers_ScrollLinkedEffectDetector_h + +#include "mozilla/RefPtr.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(); + + explicit ScrollLinkedEffectDetector(dom::Document*); + ~ScrollLinkedEffectDetector(); + + private: + RefPtr<dom::Document> mDocument; +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_ScrollLinkedEffectDetector_h */ diff --git a/gfx/layers/apz/util/TouchActionHelper.cpp b/gfx/layers/apz/util/TouchActionHelper.cpp new file mode 100644 index 0000000000..1cac71467f --- /dev/null +++ b/gfx/layers/apz/util/TouchActionHelper.cpp @@ -0,0 +1,106 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TouchActionHelper.h" + +#include "mozilla/layers/IAPZCTreeManager.h" +#include "nsContainerFrame.h" +#include "nsIFrameInlines.h" +#include "nsIScrollableFrame.h" +#include "nsLayoutUtils.h" + +namespace mozilla { +namespace layers { + +static void UpdateAllowedBehavior(StyleTouchAction aTouchActionValue, + bool aConsiderPanning, + TouchBehaviorFlags& aOutBehavior) { + if (aTouchActionValue != StyleTouchAction::AUTO) { + // Double-tap-zooming need property value AUTO + aOutBehavior &= ~AllowedTouchBehavior::DOUBLE_TAP_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; + } + } +} + +TouchBehaviorFlags TouchActionHelper::GetAllowedTouchBehavior( + nsIWidget* aWidget, RelativeTo aRootFrame, + const LayoutDeviceIntPoint& aPoint) { + TouchBehaviorFlags behavior = AllowedTouchBehavior::VERTICAL_PAN | + AllowedTouchBehavior::HORIZONTAL_PAN | + AllowedTouchBehavior::PINCH_ZOOM | + AllowedTouchBehavior::DOUBLE_TAP_ZOOM; + + nsPoint relativePoint = + nsLayoutUtils::GetEventCoordinatesRelativeTo(aWidget, aPoint, aRootFrame); + + nsIFrame* target = nsLayoutUtils::GetFrameForPoint(aRootFrame, relativePoint); + if (!target) { + return behavior; + } + nsIScrollableFrame* nearestScrollableParent = + nsLayoutUtils::GetNearestScrollableFrame(target, 0); + nsIFrame* nearestScrollableFrame = do_QueryFrame(nearestScrollableParent); + + // We're walking up the DOM tree until we meet the element with touch behavior + // and accumulating touch-action restrictions of all elements in this chain. + // The exact quote from the spec, that clarifies more: + // To determine the effect of a touch, find the nearest ancestor (starting + // from the element itself) that has a default touch behavior. Then examine + // the touch-action property of each element between the hit tested element + // and the element with the default touch behavior (including both the hit + // tested element and the element with the default touch behavior). If the + // touch-action property of any of those elements disallows the default touch + // behavior, do nothing. Otherwise allow the element to start considering the + // touch for the purposes of executing a default touch behavior. + + // Currently we support only two touch behaviors: panning and zooming. + // For panning we walk up until we meet the first scrollable element (the + // element that supports panning) or root element. For zooming we walk up + // until the root element since Firefox currently supports only zooming of the + // root frame but not the subframes. + + bool considerPanning = true; + + for (nsIFrame* frame = target; frame && frame->GetContent() && behavior; + frame = frame->GetInFlowParent()) { + UpdateAllowedBehavior(nsLayoutUtils::GetTouchActionFromFrame(frame), + considerPanning, behavior); + + if (frame == nearestScrollableFrame) { + // We met the scrollable element, after it we shouldn't consider + // touch-action values for the purpose of panning but only for zooming. + considerPanning = false; + } + } + + return behavior; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/util/TouchActionHelper.h b/gfx/layers/apz/util/TouchActionHelper.h new file mode 100644 index 0000000000..723b276997 --- /dev/null +++ b/gfx/layers/apz/util/TouchActionHelper.h @@ -0,0 +1,39 @@ +/* -*- 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 nsIFrame; +class nsIWidget; + +namespace mozilla { +namespace layers { + +/* + * Helper class to figure out the allowed touch behavior for frames, as per + * the touch-action spec. + */ +class TouchActionHelper { + public: + /* + * Performs hit testing on content, finds frame that corresponds to the aPoint + * and retrieves touch-action css property value from it according the rules + * specified in the spec: + * http://www.w3.org/TR/pointerevents/#the-touch-action-css-property. + */ + static TouchBehaviorFlags GetAllowedTouchBehavior( + nsIWidget* aWidget, RelativeTo aRootFrame, + const LayoutDeviceIntPoint& aPoint); +}; + +} // namespace layers +} // namespace mozilla + +#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 */ |