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