tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

APZInputBridge.cpp (20303B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "mozilla/layers/APZInputBridge.h"
      8 
      9 #include "AsyncPanZoomController.h"
     10 #include "InputData.h"               // for MouseInput, etc
     11 #include "InputBlockState.h"         // for InputBlockState
     12 #include "OverscrollHandoffState.h"  // for OverscrollHandoffState
     13 #include "nsLayoutUtils.h"           // for IsSmoothScrollingEnabled
     14 #include "mozilla/EventForwards.h"
     15 #include "mozilla/dom/WheelEventBinding.h"  // for WheelEvent constants
     16 #include "mozilla/EventStateManager.h"      // for EventStateManager
     17 #include "mozilla/layers/APZThreadUtils.h"  // for AssertOnControllerThread, etc
     18 #include "mozilla/MouseEvents.h"            // for WidgetMouseEvent
     19 #include "mozilla/StaticPrefs_apz.h"
     20 #include "mozilla/StaticPrefs_general.h"
     21 #include "mozilla/StaticPrefs_test.h"
     22 #include "mozilla/TextEvents.h"           // for WidgetKeyboardEvent
     23 #include "mozilla/TouchEvents.h"          // for WidgetTouchEvent
     24 #include "mozilla/WheelHandlingHelper.h"  // for WheelDeltaHorizontalizer,
     25                                          //     WheelDeltaAdjustmentStrategy
     26 
     27 namespace mozilla {
     28 namespace layers {
     29 
     30 APZHandledResult::APZHandledResult(APZHandledPlace aPlace,
     31                                   const AsyncPanZoomController* aTarget,
     32                                   bool aPopulateDirectionsForUnhandled)
     33    : mPlace(aPlace) {
     34  MOZ_ASSERT(aTarget);
     35  switch (aPlace) {
     36    case APZHandledPlace::Unhandled:
     37      if (aTarget && aPopulateDirectionsForUnhandled) {
     38        mScrollableDirections = aTarget->ScrollableDirections();
     39        mOverscrollDirections = aTarget->GetAllowedHandoffDirections(
     40            HandoffConsumer::PullToRefresh);
     41      }
     42      break;
     43    case APZHandledPlace::HandledByContent:
     44      if (aTarget) {
     45        mScrollableDirections = aTarget->ScrollableDirections();
     46        mOverscrollDirections = aTarget->GetAllowedHandoffDirections(
     47            HandoffConsumer::PullToRefresh);
     48      }
     49      break;
     50    case APZHandledPlace::HandledByRoot: {
     51      MOZ_ASSERT(aTarget->IsRootContent());
     52      if (aTarget) {
     53        mScrollableDirections = aTarget->ScrollableDirections();
     54        mOverscrollDirections = aTarget->GetAllowedHandoffDirections(
     55            HandoffConsumer::PullToRefresh);
     56      }
     57      break;
     58    }
     59    default:
     60      MOZ_ASSERT_UNREACHABLE("Invalid APZHandledPlace");
     61      break;
     62  }
     63 }
     64 
     65 /* static */
     66 Maybe<APZHandledResult> APZHandledResult::Initialize(
     67    const AsyncPanZoomController* aInitialTarget,
     68    DispatchToContent aDispatchToContent) {
     69  if (!aInitialTarget->IsRootContent()) {
     70    // If the initial target is not the root, this will definitely not be
     71    // handled by the root. (The confirmed target is either the initial
     72    // target, or a descendant.)
     73    return Some(
     74        APZHandledResult{APZHandledPlace::HandledByContent, aInitialTarget});
     75  }
     76 
     77  if (!bool(aDispatchToContent)) {
     78    // If the initial target is the root and we don't need to dispatch to
     79    // content, the event will definitely be handled by the root.
     80    return Some(
     81        APZHandledResult{APZHandledPlace::HandledByRoot, aInitialTarget});
     82  }
     83 
     84  // Otherwise, we're not sure.
     85  return Nothing();
     86 }
     87 
     88 /* static */
     89 void APZHandledResult::UpdateForTouchEvent(
     90    Maybe<APZHandledResult>& aHandledResult, const InputBlockState& aBlock,
     91    PointerEventsConsumableFlags aConsumableFlags,
     92    const AsyncPanZoomController* aTarget,
     93    DispatchToContent aDispatchToContent) {
     94  // If the touch event's effect is disallowed by touch-action, treat it as if
     95  // a touch event listener had preventDefault()-ed it (i.e. return
     96  // HandledByContent, except we can do it eagerly rather than having to wait
     97  // for the listener to run).
     98  if (!aConsumableFlags.mAllowedByTouchAction) {
     99    aHandledResult =
    100        Some(APZHandledResult{APZHandledPlace::HandledByContent, aTarget});
    101    aHandledResult->mOverscrollDirections = ScrollDirections();
    102    return;
    103  }
    104 
    105  if (aHandledResult && !bool(aDispatchToContent) &&
    106      !aConsumableFlags.mHasRoom) {
    107    // Set result to Unhandled if we have no room to scroll, unless it
    108    // was HandledByContent because we're over a dispatch-to-content region,
    109    // in which case it should remain HandledByContent.
    110    aHandledResult->mPlace = APZHandledPlace::Unhandled;
    111  }
    112 
    113  if (aTarget && !aTarget->IsRootContent()) {
    114    // If the event targets a subframe but the subframe and its ancestors
    115    // are all scrolled to the top, we want an upward swipe to allow
    116    // triggering pull-to-refresh.
    117    bool mayTriggerPullToRefresh =
    118        aBlock.GetOverscrollHandoffChain()->ScrollingUpWillTriggerPullToRefresh(
    119            aTarget);
    120    if (mayTriggerPullToRefresh) {
    121      // Similar to what is done for the dynamic toolbar, we need to ensure
    122      // that if the input has the dispatch to content flag, we need to change
    123      // the handled result to Nothing(), so that GeckoView can wait for the
    124      // result.
    125      aHandledResult = bool(aDispatchToContent)
    126                           ? Nothing()
    127                           : Some(APZHandledResult{APZHandledPlace::Unhandled,
    128                                                   aTarget, true});
    129    }
    130 
    131    auto [mayMoveDynamicToolbar, rootApzc] =
    132        aBlock.GetOverscrollHandoffChain()->ScrollingDownWillMoveDynamicToolbar(
    133            aTarget);
    134    if (mayMoveDynamicToolbar) {
    135      MOZ_ASSERT(rootApzc && rootApzc->IsRootContent());
    136      // The event is actually consumed by a non-root APZC but scroll
    137      // positions in all relevant APZCs are at the bottom edge, so if there's
    138      // still contents covered by the dynamic toolbar we need to move the
    139      // dynamic toolbar to make the covered contents visible, thus we need
    140      // to tell it to GeckoView so we handle it as if it's consumed in the
    141      // root APZC.
    142      // IMPORTANT NOTE: If the incoming TargetConfirmationFlags has
    143      // mDispatchToContent, we need to change it to Nothing() so that
    144      // GeckoView can properly wait for results from the content on the
    145      // main-thread.
    146      aHandledResult =
    147          bool(aDispatchToContent)
    148              ? Nothing()
    149              : Some(APZHandledResult{aConsumableFlags.IsConsumable()
    150                                          ? APZHandledPlace::HandledByRoot
    151                                          : APZHandledPlace::Unhandled,
    152                                      rootApzc});
    153      if (aHandledResult && aHandledResult->IsHandledByRoot() &&
    154          !mayTriggerPullToRefresh) {
    155        MOZ_ASSERT(
    156            !(aTarget->ScrollableDirections() & SideBits::eBottom),
    157            "If we allowed moving the dynamic toolbar for the sub scroll "
    158            "container, the sub scroll container should NOT be scrollable to "
    159            "bottom");
    160 
    161        // In cases we didn't allow pull-to-refresh (!mayTriggerPullToRefresh),
    162        // it means the root scroll container is NOT overscrollable at top.
    163        aHandledResult->mOverscrollDirections -= ScrollDirection::eVertical;
    164      }
    165    }
    166  }
    167 }
    168 
    169 APZEventResult::APZEventResult()
    170    : mStatus(nsEventStatus_eIgnore),
    171      mInputBlockId(InputBlockState::NO_BLOCK_ID) {}
    172 
    173 APZEventResult::APZEventResult(
    174    const RefPtr<AsyncPanZoomController>& aInitialTarget,
    175    TargetConfirmationFlags aFlags)
    176    : APZEventResult() {
    177  mHandledResult = APZHandledResult::Initialize(aInitialTarget,
    178                                                aFlags.NeedDispatchToContent());
    179  aInitialTarget->GetGuid(&mTargetGuid);
    180 }
    181 
    182 void APZEventResult::SetStatusAsConsumeDoDefault(
    183    const InputBlockState& aBlock) {
    184  SetStatusAsConsumeDoDefault(aBlock.GetTargetApzc());
    185 }
    186 
    187 void APZEventResult::SetStatusAsConsumeDoDefault(
    188    const RefPtr<AsyncPanZoomController>& aTarget) {
    189  mStatus = nsEventStatus_eConsumeDoDefault;
    190  mHandledResult =
    191      Some(aTarget && aTarget->IsRootContent()
    192               ? APZHandledResult{APZHandledPlace::HandledByRoot, aTarget}
    193               : APZHandledResult{APZHandledPlace::HandledByContent, aTarget});
    194 }
    195 
    196 void APZEventResult::SetStatusForTouchEvent(
    197    const InputBlockState& aBlock, TargetConfirmationFlags aFlags,
    198    PointerEventsConsumableFlags aConsumableFlags,
    199    const AsyncPanZoomController* aTarget) {
    200  // Note, we need to continue setting mStatus to eIgnore in the {mHasRoom=true,
    201  // mAllowedByTouchAction=false} case because this is the behaviour expected by
    202  // APZEventState::ProcessTouchEvent() when it determines when to send a
    203  // `pointercancel` event. TODO: Use something more descriptive than
    204  // nsEventStatus for this purpose.
    205  mStatus = aConsumableFlags.IsConsumable() ? nsEventStatus_eConsumeDoDefault
    206                                            : nsEventStatus_eIgnore;
    207 
    208  APZHandledResult::UpdateForTouchEvent(mHandledResult, aBlock,
    209                                        aConsumableFlags, aTarget,
    210                                        aFlags.NeedDispatchToContent());
    211 }
    212 
    213 void APZEventResult::SetStatusForFastFling(
    214    const TouchBlockState& aBlock, TargetConfirmationFlags aFlags,
    215    PointerEventsConsumableFlags aConsumableFlags,
    216    const AsyncPanZoomController* aTarget) {
    217  MOZ_ASSERT(aBlock.IsDuringFastFling());
    218 
    219  // Set eConsumeNoDefault for fast fling since we don't want to send the event
    220  // to content at all.
    221  mStatus = nsEventStatus_eConsumeNoDefault;
    222 
    223  // Re-initialize with DispatchToContent::No, since being in a fast fling
    224  // means the event will definitely not be dispatched to content.
    225  mHandledResult = APZHandledResult::Initialize(aTarget, DispatchToContent::No);
    226 
    227  // In the case of fast fling, the event will never be sent to content, so we
    228  // want a result where `aDispatchToContent` is false whatever the original
    229  // `aFlags.mDispatchToContent` is.
    230  APZHandledResult::UpdateForTouchEvent(
    231      mHandledResult, aBlock, aConsumableFlags, aTarget, DispatchToContent::No);
    232 }
    233 
    234 static bool WillHandleMouseEvent(const WidgetMouseEventBase& aEvent) {
    235  return aEvent.mMessage == eMouseMove || aEvent.mMessage == eMouseDown ||
    236         aEvent.mMessage == eMouseUp || aEvent.mMessage == eDragEnd ||
    237         (StaticPrefs::test_events_async_enabled() &&
    238          aEvent.mMessage == eMouseHitTest);
    239 }
    240 
    241 /* static */
    242 Maybe<APZWheelAction> APZInputBridge::ActionForWheelEvent(
    243    WidgetWheelEvent* aEvent) {
    244  if (!(aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_LINE ||
    245        aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_PIXEL ||
    246        aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_PAGE)) {
    247    return Nothing();
    248  }
    249  return EventStateManager::APZWheelActionFor(aEvent);
    250 }
    251 
    252 APZEventResult APZInputBridge::ReceiveInputEvent(
    253    WidgetInputEvent& aEvent, InputBlockCallback&& aCallback) {
    254  APZThreadUtils::AssertOnControllerThread();
    255 
    256  APZEventResult result;
    257 
    258  switch (aEvent.mClass) {
    259    case eMouseEventClass:
    260    case eDragEventClass: {
    261      WidgetMouseEvent& mouseEvent = *aEvent.AsMouseEvent();
    262      if (WillHandleMouseEvent(mouseEvent)) {
    263        MouseInput input(mouseEvent);
    264        input.mOrigin =
    265            ScreenPoint(mouseEvent.mRefPoint.x, mouseEvent.mRefPoint.y);
    266 
    267        result = ReceiveInputEvent(input, std::move(aCallback));
    268 
    269        mouseEvent.mRefPoint = TruncatedToInt(ViewAs<LayoutDevicePixel>(
    270            input.mOrigin,
    271            PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent));
    272        mouseEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
    273        mouseEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
    274 #ifdef XP_MACOSX
    275        // It's not assumed that the click event has already been prevented,
    276        // except mousedown event with ctrl key is pressed where we prevent
    277        // click event from widget on Mac platform.
    278        MOZ_ASSERT_IF(!mouseEvent.IsControl() ||
    279                          mouseEvent.mMessage != eMouseDown ||
    280                          mouseEvent.mButton != MouseButton::ePrimary,
    281                      !mouseEvent.mClickEventPrevented);
    282 #else
    283        MOZ_ASSERT(
    284            !mouseEvent.mClickEventPrevented,
    285            "It's not assumed that the click event has already been prevented");
    286 #endif
    287        mouseEvent.mClickEventPrevented |= input.mPreventClickEvent;
    288        MOZ_ASSERT_IF(mouseEvent.mClickEventPrevented,
    289                      mouseEvent.mMessage == eMouseDown ||
    290                          mouseEvent.mMessage == eMouseUp);
    291        aEvent.mLayersId = input.mLayersId;
    292 
    293        if (mouseEvent.IsReal()) {
    294          UpdateWheelTransaction(mouseEvent.mRefPoint, mouseEvent.mMessage,
    295                                 Some(result.mTargetGuid));
    296        }
    297 
    298        return result;
    299      }
    300 
    301      if (mouseEvent.IsReal()) {
    302        UpdateWheelTransaction(mouseEvent.mRefPoint, mouseEvent.mMessage,
    303                               Nothing());
    304      }
    305 
    306      ProcessUnhandledEvent(&mouseEvent.mRefPoint, &result.mTargetGuid,
    307                            &aEvent.mFocusSequenceNumber, &aEvent.mLayersId);
    308      return result;
    309    }
    310    case eTouchEventClass: {
    311      WidgetTouchEvent& touchEvent = *aEvent.AsTouchEvent();
    312      MultiTouchInput touchInput(touchEvent);
    313      result = ReceiveInputEvent(touchInput, std::move(aCallback));
    314      // touchInput was modified in-place to possibly remove some
    315      // touch points (if we are overscrolled), and the coordinates were
    316      // modified using the APZ untransform. We need to copy these changes
    317      // back into the WidgetInputEvent.
    318      touchEvent.mTouches.Clear();
    319      touchEvent.mTouches.SetCapacity(touchInput.mTouches.Length());
    320      for (size_t i = 0; i < touchInput.mTouches.Length(); i++) {
    321        *touchEvent.mTouches.AppendElement() =
    322            touchInput.mTouches[i].ToNewDOMTouch();
    323      }
    324      touchEvent.mFlags.mHandledByAPZ = touchInput.mHandledByAPZ;
    325      touchEvent.mFocusSequenceNumber = touchInput.mFocusSequenceNumber;
    326      aEvent.mLayersId = touchInput.mLayersId;
    327      return result;
    328    }
    329    case eWheelEventClass: {
    330      WidgetWheelEvent& wheelEvent = *aEvent.AsWheelEvent();
    331 
    332      if (Maybe<APZWheelAction> action = ActionForWheelEvent(&wheelEvent)) {
    333        ScrollWheelInput::ScrollMode scrollMode =
    334            ScrollWheelInput::SCROLLMODE_INSTANT;
    335        if (nsLayoutUtils::IsSmoothScrollingEnabled() &&
    336            ((wheelEvent.mDeltaMode ==
    337                  dom::WheelEvent_Binding::DOM_DELTA_LINE &&
    338              StaticPrefs::general_smoothScroll_mouseWheel()) ||
    339             (wheelEvent.mDeltaMode ==
    340                  dom::WheelEvent_Binding::DOM_DELTA_PAGE &&
    341              StaticPrefs::general_smoothScroll_pages()))) {
    342          scrollMode = ScrollWheelInput::SCROLLMODE_SMOOTH;
    343        }
    344 
    345        WheelDeltaAdjustmentStrategy strategy =
    346            EventStateManager::GetWheelDeltaAdjustmentStrategy(wheelEvent);
    347        // Adjust the delta values of the wheel event if the current default
    348        // action is to horizontalize scrolling. I.e., deltaY values are set to
    349        // deltaX and deltaY and deltaZ values are set to 0.
    350        // If horizontalized, the delta values will be restored and its overflow
    351        // deltaX will become 0 when the WheelDeltaHorizontalizer instance is
    352        // being destroyed.
    353        WheelDeltaHorizontalizer horizontalizer(wheelEvent);
    354        if (WheelDeltaAdjustmentStrategy::eHorizontalize == strategy) {
    355          horizontalizer.Horizontalize();
    356        }
    357 
    358        // If the wheel event becomes no-op event, don't handle it as scroll.
    359        if (wheelEvent.mDeltaX || wheelEvent.mDeltaY) {
    360          ScreenPoint origin(wheelEvent.mRefPoint.x, wheelEvent.mRefPoint.y);
    361          ScrollWheelInput input(
    362              wheelEvent.mTimeStamp, 0, scrollMode,
    363              ScrollWheelInput::DeltaTypeForDeltaMode(wheelEvent.mDeltaMode),
    364              origin, wheelEvent.mDeltaX, wheelEvent.mDeltaY,
    365              wheelEvent.mAllowToOverrideSystemScrollSpeed, strategy);
    366          input.mAPZAction = action.value();
    367 
    368          // We add the user multiplier as a separate field, rather than
    369          // premultiplying it, because if the input is converted back to a
    370          // WidgetWheelEvent, then EventStateManager would apply the delta a
    371          // second time. We could in theory work around this by asking ESM to
    372          // customize the event much sooner, and then save the
    373          // "mCustomizedByUserPrefs" bit on ScrollWheelInput - but for now,
    374          // this seems easier.
    375          EventStateManager::GetUserPrefsForWheelEvent(
    376              &wheelEvent, &input.mUserDeltaMultiplierX,
    377              &input.mUserDeltaMultiplierY);
    378 
    379          result = ReceiveInputEvent(input, std::move(aCallback));
    380          wheelEvent.mRefPoint = TruncatedToInt(ViewAs<LayoutDevicePixel>(
    381              input.mOrigin, PixelCastJustification::
    382                                 LayoutDeviceIsScreenForUntransformedEvent));
    383          wheelEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
    384          wheelEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
    385          aEvent.mLayersId = input.mLayersId;
    386 
    387          return result;
    388        }
    389      }
    390 
    391      UpdateWheelTransaction(aEvent.mRefPoint, aEvent.mMessage, Nothing());
    392      ProcessUnhandledEvent(&aEvent.mRefPoint, &result.mTargetGuid,
    393                            &aEvent.mFocusSequenceNumber, &aEvent.mLayersId);
    394      MOZ_ASSERT(result.GetStatus() == nsEventStatus_eIgnore);
    395      return result;
    396    }
    397    case eKeyboardEventClass: {
    398      WidgetKeyboardEvent& keyboardEvent = *aEvent.AsKeyboardEvent();
    399 
    400      KeyboardInput input(keyboardEvent);
    401 
    402      result = ReceiveInputEvent(input, std::move(aCallback));
    403 
    404      keyboardEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
    405      keyboardEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
    406      return result;
    407    }
    408    default: {
    409      UpdateWheelTransaction(aEvent.mRefPoint, aEvent.mMessage, Nothing());
    410      ProcessUnhandledEvent(&aEvent.mRefPoint, &result.mTargetGuid,
    411                            &aEvent.mFocusSequenceNumber, &aEvent.mLayersId);
    412      return result;
    413    }
    414  }
    415 
    416  MOZ_ASSERT_UNREACHABLE("Invalid WidgetInputEvent type.");
    417  result.SetStatusAsConsumeNoDefault();
    418  return result;
    419 }
    420 
    421 std::ostream& operator<<(std::ostream& aOut, const SideBits& aSideBits) {
    422  if ((aSideBits & SideBits::eAll) == SideBits::eAll) {
    423    aOut << "all";
    424  } else {
    425    AutoTArray<nsCString, 4> strings;
    426    if (aSideBits & SideBits::eTop) {
    427      strings.AppendElement("top"_ns);
    428    }
    429    if (aSideBits & SideBits::eRight) {
    430      strings.AppendElement("right"_ns);
    431    }
    432    if (aSideBits & SideBits::eBottom) {
    433      strings.AppendElement("bottom"_ns);
    434    }
    435    if (aSideBits & SideBits::eLeft) {
    436      strings.AppendElement("left"_ns);
    437    }
    438    aOut << strings;
    439  }
    440  return aOut;
    441 }
    442 
    443 std::ostream& operator<<(std::ostream& aOut,
    444                         const ScrollDirections& aScrollDirections) {
    445  if (aScrollDirections.contains(EitherScrollDirection)) {
    446    aOut << "either";
    447  } else if (aScrollDirections.contains(HorizontalScrollDirection)) {
    448    aOut << "horizontal";
    449  } else if (aScrollDirections.contains(VerticalScrollDirection)) {
    450    aOut << "vertical";
    451  } else {
    452    aOut << "none";
    453  }
    454  return aOut;
    455 }
    456 
    457 std::ostream& operator<<(std::ostream& aOut,
    458                         const APZHandledPlace& aHandledPlace) {
    459  switch (aHandledPlace) {
    460    case APZHandledPlace::Unhandled:
    461      aOut << "unhandled";
    462      break;
    463    case APZHandledPlace::HandledByRoot: {
    464      aOut << "handled-by-root";
    465      break;
    466    }
    467    case APZHandledPlace::HandledByContent: {
    468      aOut << "handled-by-content";
    469      break;
    470    }
    471    case APZHandledPlace::Invalid: {
    472      aOut << "INVALID";
    473      break;
    474    }
    475  }
    476  return aOut;
    477 }
    478 
    479 std::ostream& operator<<(std::ostream& aOut,
    480                         const APZHandledResult& aHandledResult) {
    481  aOut << "handled: " << aHandledResult.mPlace << ", ";
    482  aOut << "scrollable: " << aHandledResult.mScrollableDirections << ", ";
    483  aOut << "overscroll: " << aHandledResult.mOverscrollDirections << std::endl;
    484  return aOut;
    485 }
    486 
    487 }  // namespace layers
    488 }  // namespace mozilla