/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "APZCCallbackHelper.h" #include "gfxPlatform.h" // For gfxPlatform::UseTiling #include "mozilla/AsyncEventDispatcher.h" #include "mozilla/EventForwards.h" #include "mozilla/dom/CustomEvent.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/MouseEventBinding.h" #include "mozilla/dom/BrowserParent.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/IntegerPrintfMacros.h" #include "mozilla/layers/RepaintRequest.h" #include "mozilla/layers/WebRenderLayerManager.h" #include "mozilla/layers/WebRenderBridgeChild.h" #include "mozilla/DisplayPortUtils.h" #include "mozilla/PresShell.h" #include "mozilla/ToString.h" #include "mozilla/ViewportUtils.h" #include "nsContainerFrame.h" #include "nsContentUtils.h" #include "nsIContent.h" #include "nsIDOMWindowUtils.h" #include "mozilla/dom/Document.h" #include "nsIInterfaceRequestorUtils.h" #include "nsIScrollableFrame.h" #include "nsLayoutUtils.h" #include "nsPrintfCString.h" #include "nsPIDOMWindow.h" #include "nsRefreshDriver.h" #include "nsString.h" #include "nsView.h" static mozilla::LazyLogModule sApzHlpLog("apz.helper"); #define APZCCH_LOG(...) MOZ_LOG(sApzHlpLog, LogLevel::Debug, (__VA_ARGS__)) static mozilla::LazyLogModule sDisplayportLog("apz.displayport"); namespace mozilla { namespace layers { using dom::BrowserParent; uint64_t APZCCallbackHelper::sLastTargetAPZCNotificationInputBlock = uint64_t(-1); static ScreenMargin RecenterDisplayPort(const ScreenMargin& aDisplayPort) { ScreenMargin margins = aDisplayPort; margins.right = margins.left = margins.LeftRight() / 2; margins.top = margins.bottom = margins.TopBottom() / 2; return margins; } static PresShell* GetPresShell(const nsIContent* aContent) { if (dom::Document* doc = aContent->GetComposedDoc()) { return doc->GetPresShell(); } return nullptr; } static CSSPoint ScrollFrameTo(nsIScrollableFrame* aFrame, const RepaintRequest& aRequest, bool& aSuccessOut) { aSuccessOut = false; CSSPoint targetScrollPosition = aRequest.GetLayoutScrollOffset(); if (!aFrame) { return targetScrollPosition; } CSSPoint geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition()); // If the repaint request was triggered due to a previous main-thread scroll // offset update sent to the APZ, then we don't need to do another scroll here // and we can just return. if (!aRequest.GetScrollOffsetUpdated()) { return geckoScrollPosition; } // If this frame is overflow:hidden, then the expectation is that it was // sized in a way that respects its scrollable boundaries. For the root // frame, this means that it cannot be scrolled in such a way that it moves // the layout viewport. For a non-root frame, this means that it cannot be // scrolled at all. // // In either case, |targetScrollPosition| should be the same as // |geckoScrollPosition| here. // // However, this is slightly racy. We query the overflow property of the // scroll frame at the time the repaint request arrives at the main thread // (i.e., right now), but APZ made the decision of whether or not to allow // scrolling based on the information it had at the time it processed the // scroll event. The overflow property could have changed at some time // between the two events and so APZ may have computed a scrollable region // that is larger than what is actually allowed. // // Currently, we allow the scroll position to change even though the frame is // overflow:hidden (that is, we take |targetScrollPosition|). If this turns // out to be problematic, an alternative solution would be to ignore the // scroll position change (that is, use |geckoScrollPosition|). if (aFrame->GetScrollStyles().mVertical == StyleOverflow::Hidden && targetScrollPosition.y != geckoScrollPosition.y) { NS_WARNING( nsPrintfCString( "APZCCH: targetScrollPosition.y (%f) != geckoScrollPosition.y (%f)", targetScrollPosition.y.value, geckoScrollPosition.y.value) .get()); } if (aFrame->GetScrollStyles().mHorizontal == StyleOverflow::Hidden && targetScrollPosition.x != geckoScrollPosition.x) { NS_WARNING( nsPrintfCString( "APZCCH: targetScrollPosition.x (%f) != geckoScrollPosition.x (%f)", targetScrollPosition.x.value, geckoScrollPosition.x.value) .get()); } // If the scrollable frame is currently in the middle of an async or smooth // scroll then we don't want to interrupt it (see bug 961280). // Also if the scrollable frame got a scroll request from a higher priority // origin since the last layers update, then we don't want to push our scroll // request because we'll clobber that one, which is bad. bool scrollInProgress = APZCCallbackHelper::IsScrollInProgress(aFrame); if (!scrollInProgress) { ScrollSnapTargetIds snapTargetIds = aRequest.GetLastSnapTargetIds(); aFrame->ScrollToCSSPixelsForApz(targetScrollPosition, std::move(snapTargetIds)); geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition()); aSuccessOut = true; } // Return the final scroll position after setting it so that anything that // relies on it can have an accurate value. Note that even if we set it above // re-querying it is a good idea because it may have gotten clamped or // rounded. return geckoScrollPosition; } /** * Scroll the scroll frame associated with |aContent| to the scroll position * requested in |aRequest|. * * Any difference between the requested and actual scroll positions is used to * update the callback-transform stored on the content, and return a new * display port. */ static DisplayPortMargins ScrollFrame(nsIContent* aContent, const RepaintRequest& aRequest) { // Scroll the window to the desired spot nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId()); if (sf) { sf->ResetScrollInfoIfNeeded(aRequest.GetScrollGeneration(), aRequest.GetScrollGenerationOnApz(), aRequest.GetScrollAnimationType(), nsIScrollableFrame::InScrollingGesture( aRequest.IsInScrollingGesture())); sf->SetScrollableByAPZ(!aRequest.IsScrollInfoLayer()); if (sf->IsRootScrollFrameOfDocument()) { if (!APZCCallbackHelper::IsScrollInProgress(sf)) { APZCCH_LOG("Setting VV offset to %s\n", ToString(aRequest.GetVisualScrollOffset()).c_str()); if (sf->SetVisualViewportOffset( CSSPoint::ToAppUnits(aRequest.GetVisualScrollOffset()), /* aRepaint = */ false)) { // sf can't be destroyed if SetVisualViewportOffset returned true. sf->MarkEverScrolled(); } } } } // sf might have been destroyed by the call to SetVisualViewportOffset, so // re-get it. sf = nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId()); bool scrollUpdated = false; auto displayPortMargins = DisplayPortMargins::ForScrollFrame(sf, aRequest.GetDisplayPortMargins()); CSSPoint apzScrollOffset = aRequest.GetVisualScrollOffset(); CSSPoint actualScrollOffset = ScrollFrameTo(sf, aRequest, scrollUpdated); CSSPoint scrollDelta = apzScrollOffset - actualScrollOffset; if (scrollUpdated) { if (aRequest.IsScrollInfoLayer()) { // In cases where the APZ scroll offset is different from the content // scroll offset, we want to interpret the margins as relative to the APZ // scroll offset except when the frame is not scrollable by APZ. // Therefore, if the layer is a scroll info layer, we leave the margins // as-is and they will be interpreted as relative to the content scroll // offset. if (nsIFrame* frame = aContent->GetPrimaryFrame()) { frame->SchedulePaint(); } } else { // Correct the display port due to the difference between the requested // and actual scroll offsets. displayPortMargins = DisplayPortMargins::FromAPZ(aRequest.GetDisplayPortMargins(), apzScrollOffset, actualScrollOffset); } } else if (aRequest.IsRootContent() && apzScrollOffset != aRequest.GetLayoutScrollOffset()) { // APZ uses the visual viewport's offset to calculate where to place the // display port, so the display port is misplaced when a pinch zoom occurs. // // We need to force a display port adjustment in the following paint to // account for a difference between the requested and actual scroll // offsets in repaints requested by // AsyncPanZoomController::NotifyLayersUpdated. displayPortMargins = DisplayPortMargins::FromAPZ( aRequest.GetDisplayPortMargins(), apzScrollOffset, actualScrollOffset); } else { // For whatever reason we couldn't update the scroll offset on the scroll // frame, which means the data APZ used for its displayport calculation is // stale. Fall back to a sane default behaviour. Note that we don't // tile-align the recentered displayport because tile-alignment depends on // the scroll position, and the scroll position here is out of our control. // See bug 966507 comment 21 for a more detailed explanation. displayPortMargins = DisplayPortMargins::ForScrollFrame( sf, RecenterDisplayPort(aRequest.GetDisplayPortMargins())); } // APZ transforms inputs assuming we applied the exact scroll offset it // requested (|apzScrollOffset|). Since we may not have, record the difference // between what APZ asked for and what we actually applied, and apply it to // input events to compensate. // Note that if the main-thread had a change in its scroll position, we don't // want to record that difference here, because it can be large and throw off // input events by a large amount. It is also going to be transient, because // any main-thread scroll position change will be synced to APZ and we will // get another repaint request when APZ confirms. In the interval while this // is happening we can just leave the callback transform as it was. bool mainThreadScrollChanged = sf && sf->CurrentScrollGeneration() != aRequest.GetScrollGeneration() && nsLayoutUtils::CanScrollOriginClobberApz(sf->LastScrollOrigin()); if (aContent && !mainThreadScrollChanged) { aContent->SetProperty(nsGkAtoms::apzCallbackTransform, new CSSPoint(scrollDelta), nsINode::DeleteProperty); } return displayPortMargins; } static void SetDisplayPortMargins(PresShell* aPresShell, nsIContent* aContent, const DisplayPortMargins& aDisplayPortMargins, CSSSize aDisplayPortBase) { if (!aContent) { return; } bool hadDisplayPort = DisplayPortUtils::HasDisplayPort(aContent); if (MOZ_LOG_TEST(sDisplayportLog, LogLevel::Debug)) { if (!hadDisplayPort) { mozilla::layers::ScrollableLayerGuid::ViewID viewID = mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID; nsLayoutUtils::FindIDFor(aContent, &viewID); MOZ_LOG( sDisplayportLog, LogLevel::Debug, ("APZCCH installing displayport margins %s on scrollId=%" PRIu64 "\n", ToString(aDisplayPortMargins).c_str(), viewID)); } } DisplayPortUtils::SetDisplayPortMargins( aContent, aPresShell, aDisplayPortMargins, hadDisplayPort ? DisplayPortUtils::ClearMinimalDisplayPortProperty::No : DisplayPortUtils::ClearMinimalDisplayPortProperty::Yes, 0); if (!hadDisplayPort) { DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors( aContent->GetPrimaryFrame()); } nsRect base(0, 0, aDisplayPortBase.width * AppUnitsPerCSSPixel(), aDisplayPortBase.height * AppUnitsPerCSSPixel()); DisplayPortUtils::SetDisplayPortBaseIfNotSet(aContent, base); } static void SetPaintRequestTime(nsIContent* aContent, const TimeStamp& aPaintRequestTime) { aContent->SetProperty(nsGkAtoms::paintRequestTime, new TimeStamp(aPaintRequestTime), nsINode::DeleteProperty); } void APZCCallbackHelper::NotifyLayerTransforms( const nsTArray& aTransforms) { MOZ_ASSERT(NS_IsMainThread()); for (const MatrixMessage& msg : aTransforms) { BrowserParent* parent = BrowserParent::GetBrowserParentFromLayersId(msg.GetLayersId()); if (parent) { parent->SetChildToParentConversionMatrix( ViewAs( msg.GetMatrix(), PixelCastJustification::ContentProcessIsLayerInUiProcess), msg.GetTopLevelViewportVisibleRectInBrowserCoords()); } } } void APZCCallbackHelper::UpdateRootFrame(const RepaintRequest& aRequest) { if (aRequest.GetScrollId() == ScrollableLayerGuid::NULL_SCROLL_ID) { return; } RefPtr content = nsLayoutUtils::FindContentFor(aRequest.GetScrollId()); if (!content) { return; } RefPtr presShell = GetPresShell(content); if (!presShell || aRequest.GetPresShellId() != presShell->GetPresShellId()) { return; } APZCCH_LOG("Handling request %s\n", ToString(aRequest).c_str()); if (nsLayoutUtils::AllowZoomingForDocument(presShell->GetDocument()) && aRequest.GetAsyncZoom().scale != 1.0) { // If zooming is disabled then we don't really want to let APZ fiddle // with these things. In theory setting the resolution here should be a // no-op, but setting the visual viewport size is bad because it can cause a // stale value to be returned by window.innerWidth/innerHeight (see bug // 1187792). float presShellResolution = presShell->GetResolution(); // If the pres shell resolution has changed on the content side side // the time this repaint request was fired, consider this request out of // date and drop it; setting a zoom based on the out-of-date resolution can // have the effect of getting us stuck with the stale resolution. // One might think that if the last ResolutionChangeOrigin was apz then the // pres shell resolutions should match but // that is not the case. We can get multiple repaint requests that has the // same pres shell resolution (because apz didn't receive a content layers // update inbetween) if the first has async zoom we apply that and chance // the content pres shell resolution and thus when handling the second // repaint request the pres shell resolution won't match. So that's why we // also check if the last resolution change origin was apz (aka 'us'). if (!FuzzyEqualsMultiplicative(presShellResolution, aRequest.GetPresShellResolution()) && presShell->GetLastResolutionChangeOrigin() != ResolutionChangeOrigin::Apz) { return; } // The pres shell resolution is updated by the the async zoom since the // last paint. // We want to calculate the new presshell resolution as // |aRequest.GetPresShellResolution() * aRequest.GetAsyncZoom()| but that // calculation can lead to small inaccuracies due to limited floating point // precision. Specifically, // clang-format off // asyncZoom = zoom / layerPixelsPerCSSPixel // = zoom / (devPixelsPerCSSPixel * cumulativeResolution) // clang-format on // Since this is a root frame we generally do not allow css transforms to // scale it, so it is very likely that cumulativeResolution == // presShellResoluion. So // clang-format off // newPresShellResoluion = presShellResoluion * asyncZoom // = presShellResoluion * zoom / (devPixelsPerCSSPixel * presShellResoluion) // = zoom / devPixelsPerCSSPixel // clang-format on // However, we want to keep the calculation general and so we do not assume // presShellResoluion == cumulativeResolution, but rather factor those // values out so they cancel and the floating point division has a very high // probability of being exactly 1. presShellResolution = (aRequest.GetPresShellResolution() / aRequest.GetCumulativeResolution().scale) * (aRequest.GetZoom() / aRequest.GetDevPixelsPerCSSPixel()).scale; presShell->SetResolutionAndScaleTo(presShellResolution, ResolutionChangeOrigin::Apz); // Changing the resolution will trigger a reflow which will cause the // main-thread scroll position to be realigned in layer pixels. This // (subpixel) scroll mutation can trigger a scroll update to APZ which // is undesirable. Instead of having that happen as part of the post-reflow // code, we force it to happen here with ScrollOrigin::Apz so that it // doesn't trigger a scroll update to APZ. nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId()); CSSPoint currentScrollPosition = CSSPoint::FromAppUnits(sf->GetScrollPosition()); ScrollSnapTargetIds snapTargetIds = aRequest.GetLastSnapTargetIds(); sf->ScrollToCSSPixelsForApz(currentScrollPosition, std::move(snapTargetIds)); } // Do this as late as possible since scrolling can flush layout. It also // adjusts the display port margins, so do it before we set those. DisplayPortMargins displayPortMargins = ScrollFrame(content, aRequest); SetDisplayPortMargins(presShell, content, displayPortMargins, aRequest.CalculateCompositedSizeInCssPixels()); SetPaintRequestTime(content, aRequest.GetPaintRequestTime()); } void APZCCallbackHelper::UpdateSubFrame(const RepaintRequest& aRequest) { if (aRequest.GetScrollId() == ScrollableLayerGuid::NULL_SCROLL_ID) { return; } RefPtr 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 = GetPresShell(content)) { SetDisplayPortMargins(presShell, content, displayPortMargins, aRequest.CalculateCompositedSizeInCssPixels()); } SetPaintRequestTime(content, aRequest.GetPaintRequestTime()); } bool APZCCallbackHelper::GetOrCreateScrollIdentifiers( nsIContent* aContent, uint32_t* aPresShellIdOut, ScrollableLayerGuid::ViewID* aViewIdOut) { if (!aContent) { return false; } *aViewIdOut = nsLayoutUtils::FindOrCreateIDFor(aContent); if (PresShell* presShell = GetPresShell(aContent)) { *aPresShellIdOut = presShell->GetPresShellId(); return true; } return false; } void APZCCallbackHelper::InitializeRootDisplayport(PresShell* aPresShell) { // Create a view-id and set a zero-margin displayport for the root element // of the root document in the chrome process. This ensures that the scroll // frame for this element gets an APZC, which in turn ensures that all content // in the chrome processes is covered by an APZC. // The displayport is zero-margin because this element is generally not // actually scrollable (if it is, APZC will set proper margins when it's // scrolled). if (!aPresShell) { return; } MOZ_ASSERT(aPresShell->GetDocument()); nsIContent* content = aPresShell->GetDocument()->GetDocumentElement(); if (!content) { return; } uint32_t presShellId; ScrollableLayerGuid::ViewID viewId; if (APZCCallbackHelper::GetOrCreateScrollIdentifiers(content, &presShellId, &viewId)) { MOZ_LOG( sDisplayportLog, LogLevel::Debug, ("Initializing root displayport on scrollId=%" PRIu64 "\n", viewId)); Maybe baseRect = DisplayPortUtils::GetRootDisplayportBase(aPresShell); if (baseRect) { DisplayPortUtils::SetDisplayPortBaseIfNotSet(content, *baseRect); } DisplayPortUtils::SetDisplayPortMargins( content, aPresShell, DisplayPortMargins::Empty(content), DisplayPortUtils::ClearMinimalDisplayPortProperty::Yes, 0); DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors( content->GetPrimaryFrame()); } } nsPresContext* APZCCallbackHelper::GetPresContextForContent( nsIContent* aContent) { dom::Document* doc = aContent->GetComposedDoc(); if (!doc) { return nullptr; } PresShell* presShell = doc->GetPresShell(); if (!presShell) { return nullptr; } return presShell->GetPresContext(); } PresShell* APZCCallbackHelper::GetRootContentDocumentPresShellForContent( nsIContent* aContent) { nsPresContext* context = GetPresContextForContent(aContent); if (!context) { return nullptr; } context = context->GetInProcessRootContentDocumentPresContext(); if (!context) { return nullptr; } return context->PresShell(); } nsEventStatus APZCCallbackHelper::DispatchWidgetEvent(WidgetGUIEvent& aEvent) { nsEventStatus status = nsEventStatus_eConsumeNoDefault; if (aEvent.mWidget) { aEvent.mWidget->DispatchEvent(&aEvent, status); } return status; } nsEventStatus APZCCallbackHelper::DispatchSynthesizedMouseEvent( EventMessage aMsg, const LayoutDevicePoint& aRefPoint, Modifiers aModifiers, int32_t aClickCount, nsIWidget* aWidget) { MOZ_ASSERT(aMsg == eMouseMove || aMsg == eMouseDown || aMsg == eMouseUp || aMsg == eMouseLongTap); WidgetMouseEvent event(true, aMsg, aWidget, WidgetMouseEvent::eReal, WidgetMouseEvent::eNormal); event.mRefPoint = LayoutDeviceIntPoint::Truncate(aRefPoint.x, aRefPoint.y); event.mButton = MouseButton::ePrimary; event.mButtons |= MouseButtonsFlag::ePrimaryFlag; event.mInputSource = dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH; if (aMsg == eMouseLongTap) { event.mFlags.mOnlyChromeDispatch = true; } if (aMsg != eMouseMove) { event.mClickCount = aClickCount; } event.mModifiers = aModifiers; // Real touch events will generate corresponding pointer events. We set // convertToPointer to false to prevent the synthesized mouse events generate // pointer events again. event.convertToPointer = false; return DispatchWidgetEvent(event); } PreventDefaultResult APZCCallbackHelper::DispatchMouseEvent( PresShell* aPresShell, const nsString& aType, const CSSPoint& aPoint, int32_t aButton, int32_t aClickCount, int32_t aModifiers, unsigned short aInputSourceArg, uint32_t aPointerId) { NS_ENSURE_TRUE(aPresShell, PreventDefaultResult::ByContent); PreventDefaultResult preventDefaultResult; nsContentUtils::SendMouseEvent( aPresShell, aType, aPoint.x, aPoint.y, aButton, nsIDOMWindowUtils::MOUSE_BUTTONS_NOT_SPECIFIED, aClickCount, aModifiers, /* aIgnoreRootScrollFrame = */ false, 0, aInputSourceArg, aPointerId, false, &preventDefaultResult, false, /* aIsWidgetEventSynthesized = */ false); return preventDefaultResult; } void APZCCallbackHelper::FireSingleTapEvent(const LayoutDevicePoint& aPoint, Modifiers aModifiers, int32_t aClickCount, nsIWidget* aWidget) { if (aWidget->Destroyed()) { return; } APZCCH_LOG("Dispatching single-tap component events to %s\n", ToString(aPoint).c_str()); DispatchSynthesizedMouseEvent(eMouseMove, aPoint, aModifiers, aClickCount, aWidget); DispatchSynthesizedMouseEvent(eMouseDown, aPoint, aModifiers, aClickCount, aWidget); DispatchSynthesizedMouseEvent(eMouseUp, aPoint, aModifiers, aClickCount, aWidget); } static dom::Element* GetDisplayportElementFor( nsIScrollableFrame* aScrollableFrame) { if (!aScrollableFrame) { return nullptr; } nsIFrame* scrolledFrame = aScrollableFrame->GetScrolledFrame(); if (!scrolledFrame) { return nullptr; } // |scrolledFrame| should at this point be the root content frame of the // nearest ancestor scrollable frame. The element corresponding to this // frame should be the one with the displayport set on it, so find that // element and return it. nsIContent* content = scrolledFrame->GetContent(); MOZ_ASSERT(content->IsElement()); // roc says this must be true return content->AsElement(); } static dom::Element* GetRootDocumentElementFor(nsIWidget* aWidget) { // This returns the root element that ChromeProcessController sets the // displayport on during initialization. if (nsView* view = nsView::GetViewFor(aWidget)) { if (PresShell* presShell = view->GetPresShell()) { MOZ_ASSERT(presShell->GetDocument()); return presShell->GetDocument()->GetDocumentElement(); } } return nullptr; } namespace { using FrameForPointOption = nsLayoutUtils::FrameForPointOption; // Determine the scrollable target frame for the given point and add it to // the target list. If the frame doesn't have a displayport, set one. // Return whether or not the frame had a displayport that has already been // painted (in this case, the caller can send the SetTargetAPZC notification // right away, rather than waiting for a transaction to propagate the // displayport to APZ first). static bool PrepareForSetTargetAPZCNotification( nsIWidget* aWidget, const LayersId& aLayersId, nsIFrame* aRootFrame, const LayoutDeviceIntPoint& aRefPoint, nsTArray* 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 dpElement = scrollAncestor ? GetDisplayportElementFor(scrollAncestor) : GetRootDocumentElementFor(aWidget); if (MOZ_LOG_TEST(sApzHlpLog, LogLevel::Debug)) { nsAutoString dpElementDesc; if (dpElement) { dpElement->Describe(dpElementDesc); } APZCCH_LOG("For event at %s found scrollable element %p (%s)\n", ToString(aRefPoint).c_str(), dpElement.get(), NS_LossyConvertUTF16toASCII(dpElementDesc).get()); } bool guidIsValid = APZCCallbackHelper::GetOrCreateScrollIdentifiers( dpElement, &(guid.mPresShellId), &(guid.mScrollId)); aTargets->AppendElement(guid); if (!guidIsValid) { return false; } if (DisplayPortUtils::HasNonMinimalNonZeroDisplayPort(dpElement)) { // If the element has a displayport but it hasn't been painted yet, // we want the caller to wait for the paint to happen, but we don't // need to set the displayport here since it's already been set. return !DisplayPortUtils::HasPaintedDisplayPort(dpElement); } if (!scrollAncestor) { // This can happen if the document element gets swapped out after // ChromeProcessController runs InitializeRootDisplayport. In this case // let's try to set a displayport again and bail out on this operation. APZCCH_LOG("Widget %p's document element %p didn't have a displayport\n", aWidget, dpElement.get()); APZCCallbackHelper::InitializeRootDisplayport(aRootFrame->PresShell()); return false; } APZCCH_LOG("%p didn't have a displayport, so setting one...\n", dpElement.get()); MOZ_LOG(sDisplayportLog, LogLevel::Debug, ("Activating displayport on scrollId=%" PRIu64 " for SetTargetAPZC\n", guid.mScrollId)); bool activated = DisplayPortUtils::CalculateAndSetDisplayPortMargins( scrollAncestor, DisplayPortUtils::RepaintMode::Repaint); if (!activated) { return false; } nsIFrame* frame = do_QueryFrame(scrollAncestor); DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(frame); return !DisplayPortUtils::HasPaintedDisplayPort(dpElement); } static void SendLayersDependentApzcTargetConfirmation( nsIWidget* aWidget, uint64_t aInputBlockId, nsTArray&& aTargets) { WindowRenderer* renderer = aWidget->GetWindowRenderer(); if (!renderer) { return; } if (WebRenderLayerManager* wrlm = renderer->AsWebRender()) { if (WebRenderBridgeChild* wrbc = wrlm->WrBridge()) { wrbc->SendSetConfirmedTargetAPZC(aInputBlockId, aTargets); } return; } } } // namespace DisplayportSetListener::DisplayportSetListener( nsIWidget* aWidget, nsPresContext* aPresContext, const uint64_t& aInputBlockId, nsTArray&& aTargets) : ManagedPostRefreshObserver(aPresContext), mWidget(aWidget), mInputBlockId(aInputBlockId), mTargets(std::move(aTargets)) { MOZ_ASSERT(!mAction, "Setting Action twice"); mAction = [instance = MOZ_KnownLive(this)](bool aWasCanceled) { instance->OnPostRefresh(); return Unregister::Yes; }; } DisplayportSetListener::~DisplayportSetListener() = default; void DisplayportSetListener::Register() { APZCCH_LOG("DisplayportSetListener::Register\n"); mPresContext->RegisterManagedPostRefreshObserver(this); } void DisplayportSetListener::OnPostRefresh() { APZCCH_LOG("Got refresh, sending target APZCs for input block %" PRIu64 "\n", mInputBlockId); SendLayersDependentApzcTargetConfirmation(mWidget, mInputBlockId, std::move(mTargets)); } already_AddRefed 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 targets; if (const WidgetTouchEvent* touchEvent = aEvent.AsTouchEvent()) { for (size_t i = 0; i < touchEvent->mTouches.Length(); i++) { waitForRefresh |= PrepareForSetTargetAPZCNotification( aWidget, aLayersId, rootFrame, touchEvent->mTouches[i]->mRefPoint, &targets); } } else if (const WidgetWheelEvent* wheelEvent = aEvent.AsWheelEvent()) { waitForRefresh = PrepareForSetTargetAPZCNotification( aWidget, aLayersId, rootFrame, wheelEvent->mRefPoint, &targets); } else if (const WidgetMouseEvent* mouseEvent = aEvent.AsMouseEvent()) { waitForRefresh = PrepareForSetTargetAPZCNotification( aWidget, aLayersId, rootFrame, mouseEvent->mRefPoint, &targets); } // TODO: Do other types of events need to be handled? if (!targets.IsEmpty()) { if (waitForRefresh) { APZCCH_LOG( "At least one target got a new displayport, need to wait for " "refresh\n"); return MakeAndAddRef( aWidget, presShell->GetPresContext(), aInputBlockId, std::move(targets)); } APZCCH_LOG("Sending target APZCs for input block %" PRIu64 "\n", aInputBlockId); aWidget->SetConfirmedTargetAPZC(aInputBlockId, targets); } } } return nullptr; } void APZCCallbackHelper::NotifyMozMouseScrollEvent( const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent) { nsCOMPtr targetContent = nsLayoutUtils::FindContentFor(aScrollId); if (!targetContent) { return; } RefPtr 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 observerService = mozilla::services::GetObserverService(); MOZ_ASSERT(observerService); observerService->NotifyObservers(nullptr, "apz-repaints-flushed", nullptr); } /* static */ bool APZCCallbackHelper::IsScrollInProgress(nsIScrollableFrame* aFrame) { using AnimationState = nsIScrollableFrame::AnimationState; return aFrame->ScrollAnimationState().contains(AnimationState::MainThread) || nsLayoutUtils::CanScrollOriginClobberApz(aFrame->LastScrollOrigin()); } /* static */ void APZCCallbackHelper::NotifyAsyncScrollbarDragInitiated( uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId, ScrollDirection aDirection) { MOZ_ASSERT(NS_IsMainThread()); if (nsIScrollableFrame* scrollFrame = nsLayoutUtils::FindScrollableFrameFor(aScrollId)) { scrollFrame->AsyncScrollbarDragInitiated(aDragBlockId, aDirection); } } /* static */ void APZCCallbackHelper::NotifyAsyncScrollbarDragRejected( const ScrollableLayerGuid::ViewID& aScrollId) { MOZ_ASSERT(NS_IsMainThread()); if (nsIScrollableFrame* scrollFrame = nsLayoutUtils::FindScrollableFrameFor(aScrollId)) { scrollFrame->AsyncScrollbarDragRejected(); } } /* static */ void APZCCallbackHelper::NotifyAsyncAutoscrollRejected( const ScrollableLayerGuid::ViewID& aScrollId) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr 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 observerService = mozilla::services::GetObserverService(); MOZ_ASSERT(observerService); nsAutoString data; data.AppendInt(aScrollId); observerService->NotifyObservers(nullptr, "apz:cancel-autoscroll", data.get()); } /* static */ void APZCCallbackHelper::NotifyScaleGestureComplete( const nsCOMPtr& aWidget, float aScale) { MOZ_ASSERT(NS_IsMainThread()); if (nsView* view = nsView::GetViewFor(aWidget)) { if (PresShell* presShell = view->GetPresShell()) { dom::Document* doc = presShell->GetDocument(); MOZ_ASSERT(doc); if (nsPIDOMWindowInner* win = doc->GetInnerWindow()) { dom::AutoJSAPI jsapi; if (!jsapi.Init(win)) { return; } JSContext* cx = jsapi.cx(); JS::Rooted detail(cx, JS::Float32Value(aScale)); RefPtr event = NS_NewDOMCustomEvent(doc, nullptr, nullptr); event->InitCustomEvent(cx, u"MozScaleGestureComplete"_ns, /* CanBubble */ true, /* Cancelable */ false, detail); event->SetTrusted(true); AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(doc, event); dispatcher->mOnlyChromeDispatch = ChromeOnlyDispatch::eYes; dispatcher->PostDOMEvent(); } } } } /* static */ void APZCCallbackHelper::NotifyPinchGesture( PinchGestureInput::PinchGestureType aType, const LayoutDevicePoint& aFocusPoint, LayoutDeviceCoord aSpanChange, Modifiers aModifiers, const nsCOMPtr& 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