From 9e3c08db40b8916968b9f30096c7be3f00ce9647 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 21 Apr 2024 13:44:51 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- gfx/layers/apz/public/APZInputBridge.h | 297 + gfx/layers/apz/public/APZPublicUtils.h | 82 + gfx/layers/apz/public/APZSampler.h | 154 + gfx/layers/apz/public/APZUpdater.h | 236 + gfx/layers/apz/public/CompositorController.h | 33 + gfx/layers/apz/public/GeckoContentController.h | 187 + .../apz/public/GeckoContentControllerTypes.h | 68 + gfx/layers/apz/public/IAPZCTreeManager.h | 150 + gfx/layers/apz/public/MatrixMessage.h | 64 + gfx/layers/apz/src/APZCTreeManager.cpp | 3742 +++++++++++ gfx/layers/apz/src/APZCTreeManager.h | 1064 ++++ gfx/layers/apz/src/APZInputBridge.cpp | 435 ++ gfx/layers/apz/src/APZPublicUtils.cpp | 111 + gfx/layers/apz/src/APZSampler.cpp | 216 + gfx/layers/apz/src/APZUpdater.cpp | 546 ++ gfx/layers/apz/src/APZUtils.cpp | 118 + gfx/layers/apz/src/APZUtils.h | 220 + gfx/layers/apz/src/AndroidAPZ.cpp | 36 + gfx/layers/apz/src/AndroidAPZ.h | 34 + gfx/layers/apz/src/AndroidFlingPhysics.cpp | 218 + gfx/layers/apz/src/AndroidFlingPhysics.h | 45 + gfx/layers/apz/src/AndroidVelocityTracker.cpp | 288 + gfx/layers/apz/src/AndroidVelocityTracker.h | 42 + gfx/layers/apz/src/AsyncDragMetrics.h | 54 + gfx/layers/apz/src/AsyncPanZoomAnimation.h | 101 + gfx/layers/apz/src/AsyncPanZoomController.cpp | 6654 ++++++++++++++++++++ gfx/layers/apz/src/AsyncPanZoomController.h | 1943 ++++++ gfx/layers/apz/src/AutoDirWheelDeltaAdjuster.h | 89 + gfx/layers/apz/src/AutoscrollAnimation.cpp | 93 + gfx/layers/apz/src/AutoscrollAnimation.h | 42 + gfx/layers/apz/src/Axis.cpp | 733 +++ gfx/layers/apz/src/Axis.h | 462 ++ gfx/layers/apz/src/CheckerboardEvent.cpp | 195 + gfx/layers/apz/src/CheckerboardEvent.h | 218 + gfx/layers/apz/src/DesktopFlingPhysics.h | 67 + gfx/layers/apz/src/DragTracker.cpp | 59 + gfx/layers/apz/src/DragTracker.h | 39 + gfx/layers/apz/src/ExpectedGeckoMetrics.cpp | 26 + gfx/layers/apz/src/ExpectedGeckoMetrics.h | 44 + gfx/layers/apz/src/FlingAccelerator.cpp | 128 + gfx/layers/apz/src/FlingAccelerator.h | 59 + gfx/layers/apz/src/FocusState.cpp | 225 + gfx/layers/apz/src/FocusState.h | 175 + gfx/layers/apz/src/FocusTarget.cpp | 233 + gfx/layers/apz/src/FocusTarget.h | 71 + gfx/layers/apz/src/GenericFlingAnimation.h | 207 + gfx/layers/apz/src/GenericScrollAnimation.cpp | 120 + gfx/layers/apz/src/GenericScrollAnimation.h | 59 + gfx/layers/apz/src/GestureEventListener.cpp | 663 ++ gfx/layers/apz/src/GestureEventListener.h | 285 + gfx/layers/apz/src/HitTestingTreeNode.cpp | 419 ++ gfx/layers/apz/src/HitTestingTreeNode.h | 270 + gfx/layers/apz/src/IAPZHitTester.cpp | 78 + gfx/layers/apz/src/IAPZHitTester.h | 91 + gfx/layers/apz/src/InputBlockState.cpp | 840 +++ gfx/layers/apz/src/InputBlockState.h | 544 ++ gfx/layers/apz/src/InputQueue.cpp | 1090 ++++ gfx/layers/apz/src/InputQueue.h | 277 + gfx/layers/apz/src/KeyboardMap.cpp | 170 + gfx/layers/apz/src/KeyboardMap.h | 118 + gfx/layers/apz/src/KeyboardScrollAction.cpp | 37 + gfx/layers/apz/src/KeyboardScrollAction.h | 48 + gfx/layers/apz/src/Overscroll.h | 250 + gfx/layers/apz/src/OverscrollHandoffState.cpp | 228 + gfx/layers/apz/src/OverscrollHandoffState.h | 203 + .../src/PotentialCheckerboardDurationTracker.cpp | 74 + .../apz/src/PotentialCheckerboardDurationTracker.h | 61 + gfx/layers/apz/src/QueuedInput.cpp | 44 + gfx/layers/apz/src/QueuedInput.h | 63 + gfx/layers/apz/src/RecentEventsBuffer.h | 83 + gfx/layers/apz/src/SampledAPZCState.cpp | 111 + gfx/layers/apz/src/SampledAPZCState.h | 72 + gfx/layers/apz/src/ScrollThumbUtils.cpp | 341 + gfx/layers/apz/src/ScrollThumbUtils.h | 51 + gfx/layers/apz/src/SimpleVelocityTracker.cpp | 135 + gfx/layers/apz/src/SimpleVelocityTracker.h | 54 + gfx/layers/apz/src/SmoothMsdScrollAnimation.cpp | 139 + gfx/layers/apz/src/SmoothMsdScrollAnimation.h | 61 + gfx/layers/apz/src/SmoothScrollAnimation.cpp | 46 + gfx/layers/apz/src/SmoothScrollAnimation.h | 37 + gfx/layers/apz/src/WRHitTester.cpp | 247 + gfx/layers/apz/src/WRHitTester.h | 26 + gfx/layers/apz/src/WheelScrollAnimation.cpp | 64 + gfx/layers/apz/src/WheelScrollAnimation.h | 30 + gfx/layers/apz/test/gtest/APZCBasicTester.h | 102 + gfx/layers/apz/test/gtest/APZCTreeManagerTester.h | 223 + gfx/layers/apz/test/gtest/APZTestAccess.cpp | 27 + gfx/layers/apz/test/gtest/APZTestAccess.h | 36 + gfx/layers/apz/test/gtest/APZTestCommon.cpp | 15 + gfx/layers/apz/test/gtest/APZTestCommon.h | 1051 ++++ gfx/layers/apz/test/gtest/InputUtils.h | 149 + gfx/layers/apz/test/gtest/MockHitTester.cpp | 38 + gfx/layers/apz/test/gtest/MockHitTester.h | 37 + gfx/layers/apz/test/gtest/TestAxisLock.cpp | 645 ++ gfx/layers/apz/test/gtest/TestBasic.cpp | 639 ++ gfx/layers/apz/test/gtest/TestEventRegions.cpp | 199 + gfx/layers/apz/test/gtest/TestEventResult.cpp | 476 ++ .../apz/test/gtest/TestFlingAcceleration.cpp | 252 + gfx/layers/apz/test/gtest/TestGestureDetector.cpp | 849 +++ gfx/layers/apz/test/gtest/TestHitTesting.cpp | 352 ++ gfx/layers/apz/test/gtest/TestInputQueue.cpp | 45 + gfx/layers/apz/test/gtest/TestOverscroll.cpp | 1991 ++++++ gfx/layers/apz/test/gtest/TestPanning.cpp | 251 + gfx/layers/apz/test/gtest/TestPinching.cpp | 675 ++ .../apz/test/gtest/TestPointerEventsConsumable.cpp | 500 ++ gfx/layers/apz/test/gtest/TestScrollHandoff.cpp | 809 +++ gfx/layers/apz/test/gtest/TestSnapping.cpp | 305 + .../apz/test/gtest/TestSnappingOnMomentum.cpp | 104 + .../apz/test/gtest/TestTransformNotifications.cpp | 567 ++ gfx/layers/apz/test/gtest/TestTreeManager.cpp | 347 + gfx/layers/apz/test/gtest/TestWRScrollData.cpp | 273 + gfx/layers/apz/test/gtest/TestWRScrollData.h | 63 + gfx/layers/apz/test/gtest/moz.build | 39 + .../test/gtest/mvm/TestMobileViewportManager.cpp | 220 + gfx/layers/apz/test/gtest/mvm/moz.build | 13 + .../test/mochitest/FissionTestHelperChild.sys.mjs | 157 + .../test/mochitest/FissionTestHelperParent.sys.mjs | 103 + .../test/mochitest/apz_test_native_event_utils.js | 1881 ++++++ gfx/layers/apz/test/mochitest/apz_test_utils.js | 1287 ++++ gfx/layers/apz/test/mochitest/browser.ini | 64 + .../browser_test_animations_without_apz_sampler.js | 134 + ...test_autoscrolling_in_extension_popup_window.js | 189 + .../browser_test_autoscrolling_in_oop_frame.js | 120 + .../browser_test_background_tab_load_scroll.js | 117 + .../browser_test_background_tab_scroll.js | 66 + .../browser_test_content_response_timeout.js | 88 + .../test/mochitest/browser_test_group_fission.js | 150 + .../test/mochitest/browser_test_position_sticky.js | 105 + .../mochitest/browser_test_reset_scaling_zoom.js | 44 + .../browser_test_scroll_thumb_dragging.js | 77 + ...ser_test_scrollbar_in_extension_popup_window.js | 138 + ...ser_test_scrolling_in_extension_popup_window.js | 128 + ..._inactive_scroller_in_extension_popup_window.js | 137 + .../browser_test_select_popup_position.js | 130 + .../apz/test/mochitest/browser_test_select_zoom.js | 195 + .../test/mochitest/browser_test_tab_drag_zoom.js | 103 + gfx/layers/apz/test/mochitest/green100x100.png | Bin 0 -> 255 bytes .../helper_background_tab_load_scroll.html | 147 + .../mochitest/helper_background_tab_scroll.html | 9 + .../test/mochitest/helper_basic_onetouchpinch.html | 90 + .../apz/test/mochitest/helper_basic_pan.html | 73 + .../apz/test/mochitest/helper_basic_scrollend.html | 92 + .../apz/test/mochitest/helper_basic_zoom.html | 71 + .../test/mochitest/helper_browser_test_utils.js | 11 + .../apz/test/mochitest/helper_bug1162771.html | 107 + .../apz/test/mochitest/helper_bug1271432.html | 573 ++ .../apz/test/mochitest/helper_bug1280013.html | 73 + .../apz/test/mochitest/helper_bug1285070.html | 44 + .../apz/test/mochitest/helper_bug1299195.html | 47 + .../apz/test/mochitest/helper_bug1326290.html | 63 + .../apz/test/mochitest/helper_bug1331693.html | 71 + .../apz/test/mochitest/helper_bug1346632.html | 89 + .../apz/test/mochitest/helper_bug1414336.html | 97 + .../apz/test/mochitest/helper_bug1462961.html | 74 + .../apz/test/mochitest/helper_bug1473108.html | 50 + .../apz/test/mochitest/helper_bug1490393-2.html | 65 + .../apz/test/mochitest/helper_bug1490393.html | 64 + .../helper_bug1502010_unconsumed_pan.html | 76 + ...per_bug1506497_touch_action_fixed_on_fixed.html | 96 + .../apz/test/mochitest/helper_bug1509575.html | 71 + .../helper_bug1519339_hidden_smoothscroll.html | 61 + ...elper_bug1544966_zoom_on_touch_action_none.html | 89 + .../apz/test/mochitest/helper_bug1550510.html | 66 + .../helper_bug1637113_main_thread_hit_test.html | 70 + .../helper_bug1637135_narrow_viewport.html | 60 + .../helper_bug1638441_fixed_pos_hit_test.html | 67 + .../mochitest/helper_bug1638458_contextmenu.html | 82 + ...elper_bug1648491_no_pointercancel_with_dtc.html | 89 + .../apz/test/mochitest/helper_bug1662800.html | 61 + ...3731_no_pointercancel_on_second_touchstart.html | 82 + .../apz/test/mochitest/helper_bug1669625.html | 79 + .../apz/test/mochitest/helper_bug1674935.html | 76 + ...170_pointercancel_on_touchaction_pinchzoom.html | 75 + .../apz/test/mochitest/helper_bug1695598.html | 123 + .../helper_bug1714934_mouseevent_buttons.html | 40 + .../apz/test/mochitest/helper_bug1719330.html | 65 + .../apz/test/mochitest/helper_bug1756529.html | 226 + .../apz/test/mochitest/helper_bug1780701.html | 70 + .../apz/test/mochitest/helper_bug1783936.html | 74 + .../apz/test/mochitest/helper_bug982141.html | 130 + .../apz/test/mochitest/helper_check_dp_size.html | 124 + .../helper_checkerboard_apzforcedisabled.html | 93 + .../helper_checkerboard_no_multiplier.html | 57 + .../mochitest/helper_checkerboard_scrollinfo.html | 91 + .../helper_checkerboard_zoom_during_load.html | 55 + .../helper_checkerboard_zoomoverflowhidden.html | 150 + gfx/layers/apz/test/mochitest/helper_click.html | 42 + .../helper_click_interrupt_animation.html | 96 + .../mochitest/helper_content_response_timeout.html | 26 + ...elper_disallow_doubletap_zoom_inside_oopif.html | 58 + .../test/mochitest/helper_displayport_expiry.html | 77 + gfx/layers/apz/test/mochitest/helper_div_pan.html | 43 + .../apz/test/mochitest/helper_dommousescroll.html | 33 + .../apz/test/mochitest/helper_doubletap_zoom.html | 50 + .../helper_doubletap_zoom_bug1702464.html | 90 + .../mochitest/helper_doubletap_zoom_fixedpos.html | 88 + .../helper_doubletap_zoom_fixedpos_overflow.html | 113 + .../mochitest/helper_doubletap_zoom_gencon.html | 101 + .../helper_doubletap_zoom_horizontal_center.html | 50 + .../helper_doubletap_zoom_hscrollable.html | 85 + .../helper_doubletap_zoom_hscrollable2.html | 109 + .../helper_doubletap_zoom_htmlelement.html | 76 + .../test/mochitest/helper_doubletap_zoom_img.html | 43 + .../helper_doubletap_zoom_large_overflow.html | 300 + .../mochitest/helper_doubletap_zoom_noscroll.html | 59 + .../mochitest/helper_doubletap_zoom_nothing.html | 46 + .../helper_doubletap_zoom_nothing_listener.html | 47 + .../mochitest/helper_doubletap_zoom_oopif.html | 50 + ...per_doubletap_zoom_scrolled_overflowhidden.html | 82 + .../mochitest/helper_doubletap_zoom_shadowdom.html | 69 + .../mochitest/helper_doubletap_zoom_small.html | 43 + .../mochitest/helper_doubletap_zoom_smooth.html | 161 + .../mochitest/helper_doubletap_zoom_square.html | 61 + .../mochitest/helper_doubletap_zoom_tablecell.html | 110 + .../mochitest/helper_doubletap_zoom_tallwide.html | 85 + .../mochitest/helper_doubletap_zoom_textarea.html | 43 + .../apz/test/mochitest/helper_drag_bug1719913.html | 91 + .../apz/test/mochitest/helper_drag_click.html | 69 + .../test/mochitest/helper_drag_root_scrollbar.html | 61 + .../apz/test/mochitest/helper_drag_scroll.html | 653 ++ .../mochitest/helper_drag_scrollbar_hittest.html | 100 + gfx/layers/apz/test/mochitest/helper_empty.html | 4 + .../helper_fission_animation_styling_in_oopif.html | 166 + ...ion_animation_styling_in_transformed_oopif.html | 130 + .../apz/test/mochitest/helper_fission_basic.html | 40 + .../helper_fission_checkerboard_severity.html | 138 + .../apz/test/mochitest/helper_fission_empty.html | 34 + .../helper_fission_event_region_override.html | 84 + .../helper_fission_force_empty_hit_region.html | 82 + ...fission_inactivescroller_positionedcontent.html | 120 + ...elper_fission_inactivescroller_under_oopif.html | 88 + .../helper_fission_initial_displayport.html | 105 + .../mochitest/helper_fission_irregular_areas.html | 101 + .../mochitest/helper_fission_large_subframe.html | 67 + .../mochitest/helper_fission_scroll_handoff.html | 50 + .../mochitest/helper_fission_scroll_oopif.html | 158 + .../mochitest/helper_fission_setResolution.html | 59 + .../apz/test/mochitest/helper_fission_tap.html | 87 + ...per_fission_tap_in_nested_iframe_on_zoomed.html | 106 + .../mochitest/helper_fission_tap_on_zoomed.html | 93 + .../apz/test/mochitest/helper_fission_touch.html | 99 + .../test/mochitest/helper_fission_transforms.html | 89 + .../apz/test/mochitest/helper_fission_utils.js | 130 + .../test/mochitest/helper_fixed_html_hittest.html | 61 + .../mochitest/helper_fixed_pos_displayport.html | 101 + .../helper_fixed_position_scroll_hittest.html | 51 + .../apz/test/mochitest/helper_fullscreen.html | 53 + .../mochitest/helper_hittest_backface_hidden.html | 67 + .../apz/test/mochitest/helper_hittest_basic.html | 141 + .../test/mochitest/helper_hittest_bug1119497.html | 54 + .../test/mochitest/helper_hittest_bug1257288.html | 74 + .../test/mochitest/helper_hittest_bug1715187.html | 69 + .../mochitest/helper_hittest_bug1715187_oopif.html | 13 + .../test/mochitest/helper_hittest_bug1715369.html | 74 + .../helper_hittest_bug1715369_iframe.html | 13 + .../mochitest/helper_hittest_bug1715369_oopif.html | 13 + .../mochitest/helper_hittest_bug1730606-1.html | 124 + .../mochitest/helper_hittest_bug1730606-2.html | 157 + .../mochitest/helper_hittest_bug1730606-3.html | 56 + .../mochitest/helper_hittest_bug1730606-4.html | 194 + .../mochitest/helper_hittest_checkerboard.html | 57 + .../test/mochitest/helper_hittest_clippath.html | 118 + .../helper_hittest_clipped_fixed_modal.html | 85 + .../mochitest/helper_hittest_deep_scene_stack.html | 57 + .../apz/test/mochitest/helper_hittest_fixed-2.html | 74 + .../apz/test/mochitest/helper_hittest_fixed-3.html | 113 + .../apz/test/mochitest/helper_hittest_fixed.html | 82 + .../test/mochitest/helper_hittest_fixed_bg.html | 53 + ...helper_hittest_fixed_in_scrolled_transform.html | 91 + .../helper_hittest_fixed_item_over_oop_iframe.html | 61 + .../mochitest/helper_hittest_float_bug1434846.html | 56 + .../mochitest/helper_hittest_float_bug1443518.html | 56 + ...helper_hittest_hidden_inactive_scrollframe.html | 55 + .../helper_hittest_hoisted_scrollinfo.html | 81 + .../helper_hittest_iframe_perspective-2.html | 69 + .../helper_hittest_iframe_perspective-3.html | 70 + .../helper_hittest_iframe_perspective.html | 60 + .../helper_hittest_iframe_perspective_child.html | 13 + ...elper_hittest_nested_transforms_bug1459696.html | 80 + .../test/mochitest/helper_hittest_obscuration.html | 77 + .../test/mochitest/helper_hittest_overscroll.html | 249 + .../helper_hittest_overscroll_contextmenu.html | 129 + .../helper_hittest_overscroll_subframe.html | 132 + .../helper_hittest_pointerevents_svg.html | 177 + .../apz/test/mochitest/helper_hittest_spam.html | 100 + .../helper_hittest_sticky_bug1478304.html | 58 + .../test/mochitest/helper_hittest_touchaction.html | 353 ++ .../mochitest/helper_horizontal_checkerboard.html | 65 + gfx/layers/apz/test/mochitest/helper_iframe1.html | 14 + gfx/layers/apz/test/mochitest/helper_iframe2.html | 14 + .../apz/test/mochitest/helper_iframe_pan.html | 49 + .../apz/test/mochitest/helper_iframe_textarea.html | 12 + .../test/mochitest/helper_interrupted_reflow.html | 712 +++ .../apz/test/mochitest/helper_key_scroll.html | 109 + gfx/layers/apz/test/mochitest/helper_long_tap.html | 166 + ...helper_main_thread_smooth_scroll_scrollend.html | 47 + .../helper_mainthread_scroll_bug1662379.html | 168 + .../test/mochitest/helper_minimum_scale_1_0.html | 46 + .../helper_no_scalable_with_initial_scale.html | 48 + .../mochitest/helper_onetouchpinch_nested.html | 103 + .../test/mochitest/helper_overflowhidden_zoom.html | 83 + .../apz/test/mochitest/helper_override_root.html | 62 + .../apz/test/mochitest/helper_override_subdoc.html | 15 + .../helper_overscroll_behavior_bug1425573.html | 44 + .../helper_overscroll_behavior_bug1425603.html | 76 + .../helper_overscroll_behavior_bug1494440.html | 50 + .../helper_overscroll_in_apz_test_data.html | 29 + .../helper_overscroll_in_subscroller.html | 165 + .../helper_position_fixed_scroll_handoff-1.html | 88 + .../helper_position_fixed_scroll_handoff-2.html | 65 + .../helper_position_fixed_scroll_handoff-3.html | 77 + .../helper_position_fixed_scroll_handoff-4.html | 79 + .../helper_position_fixed_scroll_handoff-5.html | 110 + .../mochitest/helper_position_sticky_flicker.html | 25 + .../helper_position_sticky_scroll_handoff.html | 88 + .../helper_programmatic_scroll_behavior.html | 81 + .../helper_relative_scroll_smoothness.html | 141 + .../mochitest/helper_reset_zoom_bug1818967.html | 55 + .../helper_scroll_anchoring_on_wheel.html | 59 + .../helper_scroll_anchoring_smooth_scroll.html | 54 + ...l_anchoring_smooth_scroll_with_set_timeout.html | 56 + .../helper_scroll_inactive_perspective.html | 45 + .../mochitest/helper_scroll_inactive_zindex.html | 46 + .../helper_scroll_into_view_bug1516056.html | 62 + .../helper_scroll_into_view_bug1562757.html | 64 + .../helper_scroll_linked_effect_by_wheel.html | 65 + .../helper_scroll_linked_effect_detector.html | 108 + .../mochitest/helper_scroll_on_position_fixed.html | 60 + .../mochitest/helper_scroll_over_scrollbar.html | 48 + .../helper_scroll_snap_no_valid_snap_position.html | 45 + ...lper_scroll_snap_not_resnap_during_panning.html | 93 + ..._snap_not_resnap_during_scrollbar_dragging.html | 105 + .../helper_scroll_snap_on_page_down_scroll.html | 84 + ...lper_scroll_snap_resnap_after_async_scroll.html | 81 + ...er_scroll_snap_resnap_after_async_scrollBy.html | 72 + .../helper_scroll_tables_perspective.html | 66 + .../mochitest/helper_scroll_thumb_dragging.html | 20 + .../helper_scrollbar_snap_bug1501062.html | 135 + .../mochitest/helper_scrollbarbutton_repeat.html | 101 + .../helper_scrollbarbuttonclick_checkerboard.html | 75 + .../test/mochitest/helper_scrollby_bug1531796.html | 36 + .../test/mochitest/helper_scrollend_bubbles.html | 99 + .../helper_scrollframe_activation_on_load.html | 89 + .../apz/test/mochitest/helper_scrollto_tap.html | 59 + .../apz/test/mochitest/helper_self_closer.html | 12 + .../test/mochitest/helper_smoothscroll_spam.html | 51 + .../helper_smoothscroll_spam_interleaved.html | 57 + .../apz/test/mochitest/helper_subframe_style.css | 15 + gfx/layers/apz/test/mochitest/helper_tall.html | 504 ++ gfx/layers/apz/test/mochitest/helper_tap.html | 32 + .../test/mochitest/helper_tap_default_passive.html | 81 + .../apz/test/mochitest/helper_tap_fullzoom.html | 33 + .../apz/test/mochitest/helper_tap_passive.html | 66 + .../helper_test_autoscrolling_in_oop_frame.html | 9 + .../mochitest/helper_test_reset_scaling_zoom.html | 23 + .../helper_test_select_popup_position.html | 23 + ...elect_popup_position_transformed_in_parent.html | 25 + .../helper_test_select_popup_position_zoomed.html | 25 + .../test/mochitest/helper_test_select_zoom.html | 43 + .../test/mochitest/helper_test_tab_drag_zoom.html | 18 + .../apz/test/mochitest/helper_touch_action.html | 123 + .../mochitest/helper_touch_action_complex.html | 137 + .../helper_touch_action_ordering_block.html | 39 + .../helper_touch_action_ordering_zindex.html | 37 + .../mochitest/helper_touch_action_regions.html | 345 + ...elper_touch_action_zero_opacity_bug1500864.html | 45 + .../helper_touch_drag_root_scrollbar.html | 51 + .../mochitest/helper_touchpad_pinch_and_pan.html | 49 + .../helper_transform_end_on_keyboard_scroll.html | 58 + .../helper_transform_end_on_wheel_scroll.html | 28 + .../helper_visual_scrollbars_pagescroll.html | 119 + .../mochitest/helper_visual_smooth_scroll.html | 53 + .../helper_visualscroll_clamp_restore.html | 63 + .../mochitest/helper_visualscroll_nonrcd_rsf.html | 88 + .../helper_wheelevents_handoff_on_iframe.html | 52 + ...helper_wheelevents_handoff_on_iframe_child.html | 11 + ...eelevents_handoff_on_non_scrollable_iframe.html | 113 + .../mochitest/helper_wide_crossorigin_iframe.html | 33 + .../helper_wide_crossorigin_iframe_child.html | 71 + ...helper_zoomToFocusedInput_fixed_bug1673511.html | 42 + .../helper_zoomToFocusedInput_iframe.html | 68 + .../helper_zoomToFocusedInput_multiline.html | 94 + .../helper_zoomToFocusedInput_nozoom.html | 39 + ...elper_zoomToFocusedInput_nozoom_bug1738696.html | 51 + .../helper_zoomToFocusedInput_scroll.html | 51 + .../helper_zoomToFocusedInput_touch-action.html | 67 + .../mochitest/helper_zoomToFocusedInput_zoom.html | 39 + .../helper_zoom_after_gpu_process_restart.html | 63 + .../test/mochitest/helper_zoom_keyboardscroll.html | 74 + .../apz/test/mochitest/helper_zoom_oopif.html | 54 + .../helper_zoom_out_clamped_scrollpos.html | 85 + .../helper_zoom_out_with_mainthread_clamping.html | 110 + .../apz/test/mochitest/helper_zoom_prevented.html | 75 + .../helper_zoom_restore_position_tabswitch.html | 74 + .../helper_zoom_with_dynamic_toolbar.html | 45 + .../test/mochitest/helper_zoom_with_touchpad.html | 110 + .../apz/test/mochitest/helper_zoomed_pan.html | 79 + gfx/layers/apz/test/mochitest/mochitest.ini | 125 + ...test_abort_smooth_scroll_by_instant_scroll.html | 51 + gfx/layers/apz/test/mochitest/test_bug1151667.html | 63 + gfx/layers/apz/test/mochitest/test_bug1253683.html | 59 + gfx/layers/apz/test/mochitest/test_bug1277814.html | 105 + .../apz/test/mochitest/test_bug1304689-2.html | 130 + gfx/layers/apz/test/mochitest/test_bug1304689.html | 134 + .../test/mochitest/test_frame_reconstruction.html | 218 + .../apz/test/mochitest/test_group_bug1534549.html | 37 + .../test/mochitest/test_group_checkerboarding.html | 83 + .../apz/test/mochitest/test_group_displayport.html | 31 + .../mochitest/test_group_double_tap_zoom-2.html | 89 + .../test/mochitest/test_group_double_tap_zoom.html | 66 + .../apz/test/mochitest/test_group_fullscreen.html | 32 + .../apz/test/mochitest/test_group_hittest-1.html | 59 + .../apz/test/mochitest/test_group_hittest-2.html | 72 + .../apz/test/mochitest/test_group_hittest-3.html | 50 + .../mochitest/test_group_hittest-overscroll.html | 54 + .../apz/test/mochitest/test_group_keyboard-2.html | 46 + .../apz/test/mochitest/test_group_keyboard.html | 60 + .../apz/test/mochitest/test_group_mainthread.html | 51 + .../mochitest/test_group_minimum_scale_size.html | 48 + .../apz/test/mochitest/test_group_mouseevents.html | 82 + .../apz/test/mochitest/test_group_overrides.html | 37 + .../apz/test/mochitest/test_group_overscroll.html | 35 + .../mochitest/test_group_overscroll_handoff.html | 46 + .../test/mochitest/test_group_pointerevents.html | 43 + .../test_group_programmatic_scroll_behavior.html | 38 + .../mochitest/test_group_scroll_linked_effect.html | 33 + .../apz/test/mochitest/test_group_scroll_snap.html | 67 + .../apz/test/mochitest/test_group_scrollend.html | 58 + .../test_group_scrollframe_activation.html | 49 + .../test/mochitest/test_group_touchevents-2.html | 69 + .../test/mochitest/test_group_touchevents-3.html | 47 + .../test/mochitest/test_group_touchevents-4.html | 54 + .../test/mochitest/test_group_touchevents-5.html | 51 + .../apz/test/mochitest/test_group_touchevents.html | 55 + .../apz/test/mochitest/test_group_wheelevents.html | 84 + .../apz/test/mochitest/test_group_zoom-2.html | 81 + gfx/layers/apz/test/mochitest/test_group_zoom.html | 80 + .../mochitest/test_group_zoomToFocusedInput.html | 49 + .../test/mochitest/test_interrupted_reflow.html | 38 + .../apz/test/mochitest/test_layerization.html | 312 + .../apz/test/mochitest/test_relative_update.html | 92 + .../mochitest/test_scroll_inactive_bug1190112.html | 553 ++ .../test_scroll_inactive_flattened_frame.html | 50 + .../mochitest/test_scroll_subframe_scrollbar.html | 116 + gfx/layers/apz/test/mochitest/test_smoothness.html | 71 + .../test_touch_listeners_impacting_wheel.html | 119 + .../apz/test/mochitest/test_wheel_scroll.html | 109 + .../test/mochitest/test_wheel_transactions.html | 150 + .../apz/test/reftest/async-scrollbar-1-h-ref.html | 8 + .../test/reftest/async-scrollbar-1-h-rtl-ref.html | 9 + .../apz/test/reftest/async-scrollbar-1-h-rtl.html | 13 + .../apz/test/reftest/async-scrollbar-1-h.html | 12 + .../reftest/async-scrollbar-1-v-fullzoom-ref.html | 8 + .../test/reftest/async-scrollbar-1-v-fullzoom.html | 14 + .../apz/test/reftest/async-scrollbar-1-v-ref.html | 8 + .../test/reftest/async-scrollbar-1-v-rtl-ref.html | 9 + .../apz/test/reftest/async-scrollbar-1-v-rtl.html | 13 + .../apz/test/reftest/async-scrollbar-1-v.html | 12 + .../apz/test/reftest/async-scrollbar-1-vh-ref.html | 8 + .../test/reftest/async-scrollbar-1-vh-rtl-ref.html | 9 + .../apz/test/reftest/async-scrollbar-1-vh-rtl.html | 13 + .../apz/test/reftest/async-scrollbar-1-vh.html | 12 + .../frame-reconstruction-scroll-clamping-ref.html | 27 + .../frame-reconstruction-scroll-clamping.html | 53 + .../apz/test/reftest/iframe-zoomed-child.html | 12 + gfx/layers/apz/test/reftest/iframe-zoomed-ref.html | 20 + gfx/layers/apz/test/reftest/iframe-zoomed.html | 25 + .../apz/test/reftest/initial-scale-1-ref.html | 9 + gfx/layers/apz/test/reftest/initial-scale-1.html | 9 + .../reftest/pinch-zoom-position-fixed-ref.html | 23 + .../test/reftest/pinch-zoom-position-fixed.html | 37 + .../reftest/pinch-zoom-position-sticky-ref.html | 27 + .../test/reftest/pinch-zoom-position-sticky.html | 30 + gfx/layers/apz/test/reftest/reftest.list | 50 + .../root-scrollbar-async-zoomed-in-ref.html | 8 + .../reftest/root-scrollbar-async-zoomed-in.html | 13 + .../root-scrollbar-async-zoomed-out-ref.html | 8 + .../reftest/root-scrollbar-async-zoomed-out.html | 13 + .../root-scrollbar-zoomed-in-async-scroll.html | 12 + .../test/reftest/root-scrollbar-zoomed-in-ref.html | 8 + .../apz/test/reftest/root-scrollbar-zoomed-in.html | 8 + .../root-scrollbar-zoomed-out-async-scroll.html | 12 + .../reftest/root-scrollbar-zoomed-out-ref.html | 8 + .../test/reftest/root-scrollbar-zoomed-out.html | 8 + .../apz/test/reftest/root-scrollbars-1-ref.html | 14 + gfx/layers/apz/test/reftest/root-scrollbars-1.html | 14 + .../apz/test/reftest/scaled-iframe-zoomed-ref.html | 21 + .../apz/test/reftest/scaled-iframe-zoomed.html | 26 + ...frame-scrollbar-zoomed-in-async-scroll-ref.html | 10 + .../subframe-scrollbar-zoomed-in-async-scroll.html | 15 + ...rame-scrollbar-zoomed-out-async-scroll-ref.html | 10 + ...subframe-scrollbar-zoomed-out-async-scroll.html | 15 + gfx/layers/apz/testutil/APZTestData.cpp | 124 + gfx/layers/apz/testutil/APZTestData.h | 252 + gfx/layers/apz/util/APZCCallbackHelper.cpp | 940 +++ gfx/layers/apz/util/APZCCallbackHelper.h | 195 + gfx/layers/apz/util/APZEventState.cpp | 603 ++ gfx/layers/apz/util/APZEventState.h | 190 + gfx/layers/apz/util/APZTaskRunnable.cpp | 142 + gfx/layers/apz/util/APZTaskRunnable.h | 89 + gfx/layers/apz/util/APZThreadUtils.cpp | 119 + gfx/layers/apz/util/APZThreadUtils.h | 75 + gfx/layers/apz/util/ActiveElementManager.cpp | 178 + gfx/layers/apz/util/ActiveElementManager.h | 97 + gfx/layers/apz/util/CheckerboardReportService.cpp | 217 + gfx/layers/apz/util/CheckerboardReportService.h | 138 + gfx/layers/apz/util/ChromeProcessController.cpp | 356 ++ gfx/layers/apz/util/ChromeProcessController.h | 102 + gfx/layers/apz/util/ContentProcessController.cpp | 123 + gfx/layers/apz/util/ContentProcessController.h | 93 + gfx/layers/apz/util/DoubleTapToZoom.cpp | 376 ++ gfx/layers/apz/util/DoubleTapToZoom.h | 62 + gfx/layers/apz/util/InputAPZContext.cpp | 65 + gfx/layers/apz/util/InputAPZContext.h | 69 + gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp | 48 + gfx/layers/apz/util/ScrollLinkedEffectDetector.h | 48 + .../apz/util/ScrollingInteractionContext.cpp | 29 + gfx/layers/apz/util/ScrollingInteractionContext.h | 40 + gfx/layers/apz/util/TouchActionHelper.cpp | 131 + gfx/layers/apz/util/TouchActionHelper.h | 46 + gfx/layers/apz/util/TouchCounter.cpp | 74 + gfx/layers/apz/util/TouchCounter.h | 36 + 522 files changed, 76752 insertions(+) create mode 100644 gfx/layers/apz/public/APZInputBridge.h create mode 100644 gfx/layers/apz/public/APZPublicUtils.h create mode 100644 gfx/layers/apz/public/APZSampler.h create mode 100644 gfx/layers/apz/public/APZUpdater.h create mode 100644 gfx/layers/apz/public/CompositorController.h create mode 100644 gfx/layers/apz/public/GeckoContentController.h create mode 100644 gfx/layers/apz/public/GeckoContentControllerTypes.h create mode 100644 gfx/layers/apz/public/IAPZCTreeManager.h create mode 100644 gfx/layers/apz/public/MatrixMessage.h create mode 100644 gfx/layers/apz/src/APZCTreeManager.cpp create mode 100644 gfx/layers/apz/src/APZCTreeManager.h create mode 100644 gfx/layers/apz/src/APZInputBridge.cpp create mode 100644 gfx/layers/apz/src/APZPublicUtils.cpp create mode 100644 gfx/layers/apz/src/APZSampler.cpp create mode 100644 gfx/layers/apz/src/APZUpdater.cpp create mode 100644 gfx/layers/apz/src/APZUtils.cpp create mode 100644 gfx/layers/apz/src/APZUtils.h create mode 100644 gfx/layers/apz/src/AndroidAPZ.cpp create mode 100644 gfx/layers/apz/src/AndroidAPZ.h create mode 100644 gfx/layers/apz/src/AndroidFlingPhysics.cpp create mode 100644 gfx/layers/apz/src/AndroidFlingPhysics.h create mode 100644 gfx/layers/apz/src/AndroidVelocityTracker.cpp create mode 100644 gfx/layers/apz/src/AndroidVelocityTracker.h create mode 100644 gfx/layers/apz/src/AsyncDragMetrics.h create mode 100644 gfx/layers/apz/src/AsyncPanZoomAnimation.h create mode 100644 gfx/layers/apz/src/AsyncPanZoomController.cpp create mode 100644 gfx/layers/apz/src/AsyncPanZoomController.h create mode 100644 gfx/layers/apz/src/AutoDirWheelDeltaAdjuster.h create mode 100644 gfx/layers/apz/src/AutoscrollAnimation.cpp create mode 100644 gfx/layers/apz/src/AutoscrollAnimation.h create mode 100644 gfx/layers/apz/src/Axis.cpp create mode 100644 gfx/layers/apz/src/Axis.h create mode 100644 gfx/layers/apz/src/CheckerboardEvent.cpp create mode 100644 gfx/layers/apz/src/CheckerboardEvent.h create mode 100644 gfx/layers/apz/src/DesktopFlingPhysics.h create mode 100644 gfx/layers/apz/src/DragTracker.cpp create mode 100644 gfx/layers/apz/src/DragTracker.h create mode 100644 gfx/layers/apz/src/ExpectedGeckoMetrics.cpp create mode 100644 gfx/layers/apz/src/ExpectedGeckoMetrics.h create mode 100644 gfx/layers/apz/src/FlingAccelerator.cpp create mode 100644 gfx/layers/apz/src/FlingAccelerator.h create mode 100644 gfx/layers/apz/src/FocusState.cpp create mode 100644 gfx/layers/apz/src/FocusState.h create mode 100644 gfx/layers/apz/src/FocusTarget.cpp create mode 100644 gfx/layers/apz/src/FocusTarget.h create mode 100644 gfx/layers/apz/src/GenericFlingAnimation.h create mode 100644 gfx/layers/apz/src/GenericScrollAnimation.cpp create mode 100644 gfx/layers/apz/src/GenericScrollAnimation.h create mode 100644 gfx/layers/apz/src/GestureEventListener.cpp create mode 100644 gfx/layers/apz/src/GestureEventListener.h create mode 100644 gfx/layers/apz/src/HitTestingTreeNode.cpp create mode 100644 gfx/layers/apz/src/HitTestingTreeNode.h create mode 100644 gfx/layers/apz/src/IAPZHitTester.cpp create mode 100644 gfx/layers/apz/src/IAPZHitTester.h create mode 100644 gfx/layers/apz/src/InputBlockState.cpp create mode 100644 gfx/layers/apz/src/InputBlockState.h create mode 100644 gfx/layers/apz/src/InputQueue.cpp create mode 100644 gfx/layers/apz/src/InputQueue.h create mode 100644 gfx/layers/apz/src/KeyboardMap.cpp create mode 100644 gfx/layers/apz/src/KeyboardMap.h create mode 100644 gfx/layers/apz/src/KeyboardScrollAction.cpp create mode 100644 gfx/layers/apz/src/KeyboardScrollAction.h create mode 100644 gfx/layers/apz/src/Overscroll.h create mode 100644 gfx/layers/apz/src/OverscrollHandoffState.cpp create mode 100644 gfx/layers/apz/src/OverscrollHandoffState.h create mode 100644 gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp create mode 100644 gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h create mode 100644 gfx/layers/apz/src/QueuedInput.cpp create mode 100644 gfx/layers/apz/src/QueuedInput.h create mode 100644 gfx/layers/apz/src/RecentEventsBuffer.h create mode 100644 gfx/layers/apz/src/SampledAPZCState.cpp create mode 100644 gfx/layers/apz/src/SampledAPZCState.h create mode 100644 gfx/layers/apz/src/ScrollThumbUtils.cpp create mode 100644 gfx/layers/apz/src/ScrollThumbUtils.h create mode 100644 gfx/layers/apz/src/SimpleVelocityTracker.cpp create mode 100644 gfx/layers/apz/src/SimpleVelocityTracker.h create mode 100644 gfx/layers/apz/src/SmoothMsdScrollAnimation.cpp create mode 100644 gfx/layers/apz/src/SmoothMsdScrollAnimation.h create mode 100644 gfx/layers/apz/src/SmoothScrollAnimation.cpp create mode 100644 gfx/layers/apz/src/SmoothScrollAnimation.h create mode 100644 gfx/layers/apz/src/WRHitTester.cpp create mode 100644 gfx/layers/apz/src/WRHitTester.h create mode 100644 gfx/layers/apz/src/WheelScrollAnimation.cpp create mode 100644 gfx/layers/apz/src/WheelScrollAnimation.h create mode 100644 gfx/layers/apz/test/gtest/APZCBasicTester.h create mode 100644 gfx/layers/apz/test/gtest/APZCTreeManagerTester.h create mode 100644 gfx/layers/apz/test/gtest/APZTestAccess.cpp create mode 100644 gfx/layers/apz/test/gtest/APZTestAccess.h create mode 100644 gfx/layers/apz/test/gtest/APZTestCommon.cpp create mode 100644 gfx/layers/apz/test/gtest/APZTestCommon.h create mode 100644 gfx/layers/apz/test/gtest/InputUtils.h create mode 100644 gfx/layers/apz/test/gtest/MockHitTester.cpp create mode 100644 gfx/layers/apz/test/gtest/MockHitTester.h create mode 100644 gfx/layers/apz/test/gtest/TestAxisLock.cpp create mode 100644 gfx/layers/apz/test/gtest/TestBasic.cpp create mode 100644 gfx/layers/apz/test/gtest/TestEventRegions.cpp create mode 100644 gfx/layers/apz/test/gtest/TestEventResult.cpp create mode 100644 gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp create mode 100644 gfx/layers/apz/test/gtest/TestGestureDetector.cpp create mode 100644 gfx/layers/apz/test/gtest/TestHitTesting.cpp create mode 100644 gfx/layers/apz/test/gtest/TestInputQueue.cpp create mode 100644 gfx/layers/apz/test/gtest/TestOverscroll.cpp create mode 100644 gfx/layers/apz/test/gtest/TestPanning.cpp create mode 100644 gfx/layers/apz/test/gtest/TestPinching.cpp create mode 100644 gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp create mode 100644 gfx/layers/apz/test/gtest/TestScrollHandoff.cpp create mode 100644 gfx/layers/apz/test/gtest/TestSnapping.cpp create mode 100644 gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp create mode 100644 gfx/layers/apz/test/gtest/TestTransformNotifications.cpp create mode 100644 gfx/layers/apz/test/gtest/TestTreeManager.cpp create mode 100644 gfx/layers/apz/test/gtest/TestWRScrollData.cpp create mode 100644 gfx/layers/apz/test/gtest/TestWRScrollData.h create mode 100644 gfx/layers/apz/test/gtest/moz.build create mode 100644 gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp create mode 100644 gfx/layers/apz/test/gtest/mvm/moz.build create mode 100644 gfx/layers/apz/test/mochitest/FissionTestHelperChild.sys.mjs create mode 100644 gfx/layers/apz/test/mochitest/FissionTestHelperParent.sys.mjs create mode 100644 gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js create mode 100644 gfx/layers/apz/test/mochitest/apz_test_utils.js create mode 100644 gfx/layers/apz/test/mochitest/browser.ini create mode 100644 gfx/layers/apz/test/mochitest/browser_test_animations_without_apz_sampler.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_extension_popup_window.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_oop_frame.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_background_tab_load_scroll.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_background_tab_scroll.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_content_response_timeout.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_group_fission.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_position_sticky.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_reset_scaling_zoom.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_scroll_thumb_dragging.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_scrollbar_in_extension_popup_window.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_scrolling_in_extension_popup_window.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_select_popup_position.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_select_zoom.js create mode 100644 gfx/layers/apz/test/mochitest/browser_test_tab_drag_zoom.js create mode 100644 gfx/layers/apz/test/mochitest/green100x100.png create mode 100644 gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_basic_onetouchpinch.html create mode 100644 gfx/layers/apz/test/mochitest/helper_basic_pan.html create mode 100644 gfx/layers/apz/test/mochitest/helper_basic_scrollend.html create mode 100644 gfx/layers/apz/test/mochitest/helper_basic_zoom.html create mode 100644 gfx/layers/apz/test/mochitest/helper_browser_test_utils.js create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1162771.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1271432.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1280013.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1285070.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1299195.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1326290.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1331693.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1346632.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1414336.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1462961.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1473108.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1490393-2.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1490393.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1502010_unconsumed_pan.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1506497_touch_action_fixed_on_fixed.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1509575.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1519339_hidden_smoothscroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1544966_zoom_on_touch_action_none.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1550510.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1637113_main_thread_hit_test.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1637135_narrow_viewport.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1638441_fixed_pos_hit_test.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1638458_contextmenu.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1648491_no_pointercancel_with_dtc.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1662800.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1663731_no_pointercancel_on_second_touchstart.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1669625.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1674935.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1695598.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1714934_mouseevent_buttons.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1719330.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1756529.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1780701.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug1783936.html create mode 100644 gfx/layers/apz/test/mochitest/helper_bug982141.html create mode 100644 gfx/layers/apz/test/mochitest/helper_check_dp_size.html create mode 100644 gfx/layers/apz/test/mochitest/helper_checkerboard_apzforcedisabled.html create mode 100644 gfx/layers/apz/test/mochitest/helper_checkerboard_no_multiplier.html create mode 100644 gfx/layers/apz/test/mochitest/helper_checkerboard_scrollinfo.html create mode 100644 gfx/layers/apz/test/mochitest/helper_checkerboard_zoom_during_load.html create mode 100644 gfx/layers/apz/test/mochitest/helper_checkerboard_zoomoverflowhidden.html create mode 100644 gfx/layers/apz/test/mochitest/helper_click.html create mode 100644 gfx/layers/apz/test/mochitest/helper_click_interrupt_animation.html create mode 100644 gfx/layers/apz/test/mochitest/helper_content_response_timeout.html create mode 100644 gfx/layers/apz/test/mochitest/helper_disallow_doubletap_zoom_inside_oopif.html create mode 100644 gfx/layers/apz/test/mochitest/helper_displayport_expiry.html create mode 100644 gfx/layers/apz/test/mochitest/helper_div_pan.html create mode 100644 gfx/layers/apz/test/mochitest/helper_dommousescroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_bug1702464.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos_overflow.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_gencon.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_horizontal_center.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable2.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_htmlelement.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_img.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_large_overflow.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_noscroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing_listener.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_scrolled_overflowhidden.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_shadowdom.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_small.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_smooth.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_square.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tablecell.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tallwide.html create mode 100644 gfx/layers/apz/test/mochitest/helper_doubletap_zoom_textarea.html create mode 100644 gfx/layers/apz/test/mochitest/helper_drag_bug1719913.html create mode 100644 gfx/layers/apz/test/mochitest/helper_drag_click.html create mode 100644 gfx/layers/apz/test/mochitest/helper_drag_root_scrollbar.html create mode 100644 gfx/layers/apz/test/mochitest/helper_drag_scroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_drag_scrollbar_hittest.html create mode 100644 gfx/layers/apz/test/mochitest/helper_empty.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_oopif.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_transformed_oopif.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_basic.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_checkerboard_severity.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_empty.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_event_region_override.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_force_empty_hit_region.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_positionedcontent.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_under_oopif.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_initial_displayport.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_irregular_areas.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_large_subframe.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_scroll_handoff.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_scroll_oopif.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_setResolution.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_tap.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_tap_in_nested_iframe_on_zoomed.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_tap_on_zoomed.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_touch.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_transforms.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fission_utils.js create mode 100644 gfx/layers/apz/test/mochitest/helper_fixed_html_hittest.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fixed_pos_displayport.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fixed_position_scroll_hittest.html create mode 100644 gfx/layers/apz/test/mochitest/helper_fullscreen.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_backface_hidden.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_basic.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_bug1119497.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_bug1257288.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_bug1715187.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_bug1715369.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_iframe.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-1.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-2.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-3.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-4.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_checkerboard.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_clippath.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_clipped_fixed_modal.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_deep_scene_stack.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_fixed-2.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_fixed-3.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_fixed.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_fixed_bg.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_fixed_in_scrolled_transform.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_fixed_item_over_oop_iframe.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_float_bug1434846.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_float_bug1443518.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_hidden_inactive_scrollframe.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_hoisted_scrollinfo.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-2.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-3.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_nested_transforms_bug1459696.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_obscuration.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_overscroll_contextmenu.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_overscroll_subframe.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_pointerevents_svg.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_spam.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_sticky_bug1478304.html create mode 100644 gfx/layers/apz/test/mochitest/helper_hittest_touchaction.html create mode 100644 gfx/layers/apz/test/mochitest/helper_horizontal_checkerboard.html create mode 100644 gfx/layers/apz/test/mochitest/helper_iframe1.html create mode 100644 gfx/layers/apz/test/mochitest/helper_iframe2.html create mode 100644 gfx/layers/apz/test/mochitest/helper_iframe_pan.html create mode 100644 gfx/layers/apz/test/mochitest/helper_iframe_textarea.html create mode 100644 gfx/layers/apz/test/mochitest/helper_interrupted_reflow.html create mode 100644 gfx/layers/apz/test/mochitest/helper_key_scroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_long_tap.html create mode 100644 gfx/layers/apz/test/mochitest/helper_main_thread_smooth_scroll_scrollend.html create mode 100644 gfx/layers/apz/test/mochitest/helper_mainthread_scroll_bug1662379.html create mode 100644 gfx/layers/apz/test/mochitest/helper_minimum_scale_1_0.html create mode 100644 gfx/layers/apz/test/mochitest/helper_no_scalable_with_initial_scale.html create mode 100644 gfx/layers/apz/test/mochitest/helper_onetouchpinch_nested.html create mode 100644 gfx/layers/apz/test/mochitest/helper_overflowhidden_zoom.html create mode 100644 gfx/layers/apz/test/mochitest/helper_override_root.html create mode 100644 gfx/layers/apz/test/mochitest/helper_override_subdoc.html create mode 100644 gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425573.html create mode 100644 gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425603.html create mode 100644 gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1494440.html create mode 100644 gfx/layers/apz/test/mochitest/helper_overscroll_in_apz_test_data.html create mode 100644 gfx/layers/apz/test/mochitest/helper_overscroll_in_subscroller.html create mode 100644 gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-1.html create mode 100644 gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-2.html create mode 100644 gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-3.html create mode 100644 gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-4.html create mode 100644 gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-5.html create mode 100644 gfx/layers/apz/test/mochitest/helper_position_sticky_flicker.html create mode 100644 gfx/layers/apz/test/mochitest/helper_position_sticky_scroll_handoff.html create mode 100644 gfx/layers/apz/test/mochitest/helper_programmatic_scroll_behavior.html create mode 100644 gfx/layers/apz/test/mochitest/helper_relative_scroll_smoothness.html create mode 100644 gfx/layers/apz/test/mochitest/helper_reset_zoom_bug1818967.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_anchoring_on_wheel.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll_with_set_timeout.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1516056.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1562757.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_by_wheel.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_detector.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_over_scrollbar.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_snap_no_valid_snap_position.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_panning.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_scrollbar_dragging.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_snap_on_page_down_scroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scrollBy.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_tables_perspective.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scroll_thumb_dragging.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scrollbar_snap_bug1501062.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scrollbarbutton_repeat.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scrollbarbuttonclick_checkerboard.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scrollby_bug1531796.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scrollend_bubbles.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scrollframe_activation_on_load.html create mode 100644 gfx/layers/apz/test/mochitest/helper_scrollto_tap.html create mode 100644 gfx/layers/apz/test/mochitest/helper_self_closer.html create mode 100644 gfx/layers/apz/test/mochitest/helper_smoothscroll_spam.html create mode 100644 gfx/layers/apz/test/mochitest/helper_smoothscroll_spam_interleaved.html create mode 100644 gfx/layers/apz/test/mochitest/helper_subframe_style.css create mode 100644 gfx/layers/apz/test/mochitest/helper_tall.html create mode 100644 gfx/layers/apz/test/mochitest/helper_tap.html create mode 100644 gfx/layers/apz/test/mochitest/helper_tap_default_passive.html create mode 100644 gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html create mode 100644 gfx/layers/apz/test/mochitest/helper_tap_passive.html create mode 100644 gfx/layers/apz/test/mochitest/helper_test_autoscrolling_in_oop_frame.html create mode 100644 gfx/layers/apz/test/mochitest/helper_test_reset_scaling_zoom.html create mode 100644 gfx/layers/apz/test/mochitest/helper_test_select_popup_position.html create mode 100644 gfx/layers/apz/test/mochitest/helper_test_select_popup_position_transformed_in_parent.html create mode 100644 gfx/layers/apz/test/mochitest/helper_test_select_popup_position_zoomed.html create mode 100644 gfx/layers/apz/test/mochitest/helper_test_select_zoom.html create mode 100644 gfx/layers/apz/test/mochitest/helper_test_tab_drag_zoom.html create mode 100644 gfx/layers/apz/test/mochitest/helper_touch_action.html create mode 100644 gfx/layers/apz/test/mochitest/helper_touch_action_complex.html create mode 100644 gfx/layers/apz/test/mochitest/helper_touch_action_ordering_block.html create mode 100644 gfx/layers/apz/test/mochitest/helper_touch_action_ordering_zindex.html create mode 100644 gfx/layers/apz/test/mochitest/helper_touch_action_regions.html create mode 100644 gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html create mode 100644 gfx/layers/apz/test/mochitest/helper_touch_drag_root_scrollbar.html create mode 100644 gfx/layers/apz/test/mochitest/helper_touchpad_pinch_and_pan.html create mode 100644 gfx/layers/apz/test/mochitest/helper_transform_end_on_keyboard_scroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_transform_end_on_wheel_scroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_visual_scrollbars_pagescroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_visual_smooth_scroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_visualscroll_clamp_restore.html create mode 100644 gfx/layers/apz/test/mochitest/helper_visualscroll_nonrcd_rsf.html create mode 100644 gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe.html create mode 100644 gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html create mode 100644 gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_non_scrollable_iframe.html create mode 100644 gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe.html create mode 100644 gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_fixed_bug1673511.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_multiline.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom_bug1738696.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_scroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_touch-action.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoom_after_gpu_process_restart.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoom_keyboardscroll.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoom_oopif.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoom_out_clamped_scrollpos.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoom_out_with_mainthread_clamping.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoom_prevented.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoom_restore_position_tabswitch.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoom_with_dynamic_toolbar.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoom_with_touchpad.html create mode 100644 gfx/layers/apz/test/mochitest/helper_zoomed_pan.html create mode 100644 gfx/layers/apz/test/mochitest/mochitest.ini create mode 100644 gfx/layers/apz/test/mochitest/test_abort_smooth_scroll_by_instant_scroll.html create mode 100644 gfx/layers/apz/test/mochitest/test_bug1151667.html create mode 100644 gfx/layers/apz/test/mochitest/test_bug1253683.html create mode 100644 gfx/layers/apz/test/mochitest/test_bug1277814.html create mode 100644 gfx/layers/apz/test/mochitest/test_bug1304689-2.html create mode 100644 gfx/layers/apz/test/mochitest/test_bug1304689.html create mode 100644 gfx/layers/apz/test/mochitest/test_frame_reconstruction.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_bug1534549.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_checkerboarding.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_displayport.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_double_tap_zoom.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_fullscreen.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_hittest-1.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_hittest-2.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_hittest-3.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_hittest-overscroll.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_keyboard-2.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_keyboard.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_mainthread.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_minimum_scale_size.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_mouseevents.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_overrides.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_overscroll.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_overscroll_handoff.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_pointerevents.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_programmatic_scroll_behavior.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_scroll_linked_effect.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_scroll_snap.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_scrollend.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_scrollframe_activation.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_touchevents-2.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_touchevents-3.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_touchevents-4.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_touchevents-5.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_touchevents.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_wheelevents.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_zoom-2.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_zoom.html create mode 100644 gfx/layers/apz/test/mochitest/test_group_zoomToFocusedInput.html create mode 100644 gfx/layers/apz/test/mochitest/test_interrupted_reflow.html create mode 100644 gfx/layers/apz/test/mochitest/test_layerization.html create mode 100644 gfx/layers/apz/test/mochitest/test_relative_update.html create mode 100644 gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html create mode 100644 gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html create mode 100644 gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html create mode 100644 gfx/layers/apz/test/mochitest/test_smoothness.html create mode 100644 gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html create mode 100644 gfx/layers/apz/test/mochitest/test_wheel_scroll.html create mode 100644 gfx/layers/apz/test/mochitest/test_wheel_transactions.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-h-ref.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl-ref.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-h.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom-ref.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-v-ref.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl-ref.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-v.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-vh-ref.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl-ref.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl.html create mode 100644 gfx/layers/apz/test/reftest/async-scrollbar-1-vh.html create mode 100644 gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping-ref.html create mode 100644 gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping.html create mode 100644 gfx/layers/apz/test/reftest/iframe-zoomed-child.html create mode 100644 gfx/layers/apz/test/reftest/iframe-zoomed-ref.html create mode 100644 gfx/layers/apz/test/reftest/iframe-zoomed.html create mode 100644 gfx/layers/apz/test/reftest/initial-scale-1-ref.html create mode 100644 gfx/layers/apz/test/reftest/initial-scale-1.html create mode 100644 gfx/layers/apz/test/reftest/pinch-zoom-position-fixed-ref.html create mode 100644 gfx/layers/apz/test/reftest/pinch-zoom-position-fixed.html create mode 100644 gfx/layers/apz/test/reftest/pinch-zoom-position-sticky-ref.html create mode 100644 gfx/layers/apz/test/reftest/pinch-zoom-position-sticky.html create mode 100644 gfx/layers/apz/test/reftest/reftest.list create mode 100644 gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in-ref.html create mode 100644 gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in.html create mode 100644 gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out-ref.html create mode 100644 gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out.html create mode 100644 gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-async-scroll.html create mode 100644 gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-ref.html create mode 100644 gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in.html create mode 100644 gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-async-scroll.html create mode 100644 gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-ref.html create mode 100644 gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out.html create mode 100644 gfx/layers/apz/test/reftest/root-scrollbars-1-ref.html create mode 100644 gfx/layers/apz/test/reftest/root-scrollbars-1.html create mode 100644 gfx/layers/apz/test/reftest/scaled-iframe-zoomed-ref.html create mode 100644 gfx/layers/apz/test/reftest/scaled-iframe-zoomed.html create mode 100644 gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll-ref.html create mode 100644 gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll.html create mode 100644 gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll-ref.html create mode 100644 gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll.html create mode 100644 gfx/layers/apz/testutil/APZTestData.cpp create mode 100644 gfx/layers/apz/testutil/APZTestData.h create mode 100644 gfx/layers/apz/util/APZCCallbackHelper.cpp create mode 100644 gfx/layers/apz/util/APZCCallbackHelper.h create mode 100644 gfx/layers/apz/util/APZEventState.cpp create mode 100644 gfx/layers/apz/util/APZEventState.h create mode 100644 gfx/layers/apz/util/APZTaskRunnable.cpp create mode 100644 gfx/layers/apz/util/APZTaskRunnable.h create mode 100644 gfx/layers/apz/util/APZThreadUtils.cpp create mode 100644 gfx/layers/apz/util/APZThreadUtils.h create mode 100644 gfx/layers/apz/util/ActiveElementManager.cpp create mode 100644 gfx/layers/apz/util/ActiveElementManager.h create mode 100644 gfx/layers/apz/util/CheckerboardReportService.cpp create mode 100644 gfx/layers/apz/util/CheckerboardReportService.h create mode 100644 gfx/layers/apz/util/ChromeProcessController.cpp create mode 100644 gfx/layers/apz/util/ChromeProcessController.h create mode 100644 gfx/layers/apz/util/ContentProcessController.cpp create mode 100644 gfx/layers/apz/util/ContentProcessController.h create mode 100644 gfx/layers/apz/util/DoubleTapToZoom.cpp create mode 100644 gfx/layers/apz/util/DoubleTapToZoom.h create mode 100644 gfx/layers/apz/util/InputAPZContext.cpp create mode 100644 gfx/layers/apz/util/InputAPZContext.h create mode 100644 gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp create mode 100644 gfx/layers/apz/util/ScrollLinkedEffectDetector.h create mode 100644 gfx/layers/apz/util/ScrollingInteractionContext.cpp create mode 100644 gfx/layers/apz/util/ScrollingInteractionContext.h create mode 100644 gfx/layers/apz/util/TouchActionHelper.cpp create mode 100644 gfx/layers/apz/util/TouchActionHelper.h create mode 100644 gfx/layers/apz/util/TouchCounter.cpp create mode 100644 gfx/layers/apz/util/TouchCounter.h (limited to 'gfx/layers/apz') diff --git a/gfx/layers/apz/public/APZInputBridge.h b/gfx/layers/apz/public/APZInputBridge.h new file mode 100644 index 0000000000..07ed39548d --- /dev/null +++ b/gfx/layers/apz/public/APZInputBridge.h @@ -0,0 +1,297 @@ +/* -*- 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_APZInputBridge_h +#define mozilla_layers_APZInputBridge_h + +#include "Units.h" // for LayoutDeviceIntPoint +#include "mozilla/EventForwards.h" // for WidgetInputEvent, nsEventStatus +#include "mozilla/layers/APZPublicUtils.h" // for APZWheelAction +#include "mozilla/layers/LayersTypes.h" // for ScrollDirections +#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid + +namespace mozilla { + +class InputData; + +namespace layers { + +class APZInputBridgeParent; +class AsyncPanZoomController; +class InputBlockState; +class TouchBlockState; +struct ScrollableLayerGuid; +struct TargetConfirmationFlags; +struct PointerEventsConsumableFlags; + +enum class APZHandledPlace : uint8_t { + Unhandled = 0, // we know for sure that the event will not be handled + // by either the root APZC or others + HandledByRoot = 1, // we know for sure that the event will be handled + // by the root content APZC + HandledByContent = 2, // we know for sure it will be handled by a non-root + // APZC or by an event listener using preventDefault() + // in a document + Invalid = 3, + Last = Invalid +}; + +struct APZHandledResult { + APZHandledPlace mPlace = APZHandledPlace::Invalid; + SideBits mScrollableDirections = SideBits::eNone; + ScrollDirections mOverscrollDirections = ScrollDirections(); + + APZHandledResult() = default; + // A constructor for cases where we have the target of the input block this + // event is part of, the target might be adjusted to be the root in the + // ScrollingDownWillMoveDynamicToolbar case. + // + // NOTE: There's a case where |aTarget| is the APZC for the root content but + // |aPlace| has to be `HandledByContent`, for example, the root content has + // an event handler using preventDefault() in the callback, so call sites of + // this function should be responsible to set a proper |aPlace|. + APZHandledResult(APZHandledPlace aPlace, + const AsyncPanZoomController* aTarget); + APZHandledResult(APZHandledPlace aPlace, SideBits aScrollableDirections, + ScrollDirections aOverscrollDirections) + : mPlace(aPlace), + mScrollableDirections(aScrollableDirections), + mOverscrollDirections(aOverscrollDirections) {} + + bool IsHandledByContent() const { + return mPlace == APZHandledPlace::HandledByContent; + } + bool IsHandledByRoot() const { + return mPlace == APZHandledPlace::HandledByRoot; + } + bool operator==(const APZHandledResult& aOther) const { + return mPlace == aOther.mPlace && + mScrollableDirections == aOther.mScrollableDirections && + mOverscrollDirections == aOther.mOverscrollDirections; + } +}; + +/** + * Represents the outcome of APZ receiving and processing an input event. + * This is returned from APZInputBridge::ReceiveInputEvent() and related APIs. + */ +struct APZEventResult { + /** + * Creates a default result with a status of eIgnore, no block ID, and empty + * target guid. + */ + APZEventResult(); + + /** + * Creates a result with a status of eIgnore, no block ID, the guid of the + * given initial target, and an APZHandledResult if we are sure the event + * is not going to be dispatched to contents. + */ + APZEventResult(const RefPtr& aInitialTarget, + TargetConfirmationFlags aFlags); + + void SetStatusAsConsumeNoDefault() { + mStatus = nsEventStatus_eConsumeNoDefault; + } + + void SetStatusAsIgnore() { mStatus = nsEventStatus_eIgnore; } + + // Set mStatus to nsEventStatus_eConsumeDoDefault and set mHandledResult + // depending on |aTarget|. + void SetStatusAsConsumeDoDefault( + const RefPtr& aTarget); + + // Set mStatus to nsEventStatus_eConsumeDoDefault, unlike above + // SetStatusAsConsumeDoDefault(const RefPtr&) this + // function doesn't mutate mHandledResult. + void SetStatusAsConsumeDoDefault() { + mStatus = nsEventStatus_eConsumeDoDefault; + } + + // Set mStatus to nsEventStatus_eConsumeDoDefault and set mHandledResult + // depending on |aBlock|'s target APZC. + void SetStatusAsConsumeDoDefault(const InputBlockState& aBlock); + // Set mStatus and mHandledResult for a touch event which is not dropped + // altogether (i.e. the status is not eConsumeNoDefault). + void SetStatusForTouchEvent(const InputBlockState& aBlock, + TargetConfirmationFlags aFlags, + PointerEventsConsumableFlags aConsumableFlags, + const AsyncPanZoomController* aTarget); + + // Set mStatus and mHandledResult during in a stat of fast fling. + void SetStatusForFastFling(const TouchBlockState& aBlock, + TargetConfirmationFlags aFlags, + PointerEventsConsumableFlags aConsumableFlags, + const AsyncPanZoomController* aTarget); + + // DO NOT USE THIS UpdateStatus DIRECTLY. THIS FUNCTION IS ONLY FOR + // SERIALIZATION / DESERIALIZATION OF THIS STRUCT IN IPC. + void UpdateStatus(nsEventStatus aStatus) { mStatus = aStatus; } + nsEventStatus GetStatus() const { return mStatus; }; + + // DO NOT USE THIS UpdateHandledResult DIRECTLY. THIS FUNCTION IS ONLY FOR + // SERIALIZATION / DESERIALIZATION OF THIS STRUCT IN IPC. + void UpdateHandledResult(const Maybe& aHandledResult) { + mHandledResult = aHandledResult; + } + const Maybe& GetHandledResult() const { + return mHandledResult; + } + + bool WillHaveDelayedResult() const { + return GetStatus() != nsEventStatus_eConsumeNoDefault && + !GetHandledResult(); + } + + private: + void UpdateHandledResult(const InputBlockState& aBlock, + PointerEventsConsumableFlags aConsumableFlags, + const AsyncPanZoomController* aTarget, + bool aDispatchToContent); + + /** + * A status flag indicated how APZ handled the event. + * The interpretation of each value is as follows: + * + * nsEventStatus_eConsumeNoDefault is returned to indicate the + * APZ is consuming this event and the caller should discard the event with + * extreme prejudice. The exact scenarios under which this is returned is + * implementation-dependent and may vary. + * nsEventStatus_eIgnore is returned to indicate that the APZ code didn't + * use this event. This might be because it was directed at a point on + * the screen where there was no APZ, or because the thing the user was + * trying to do was not allowed. (For example, attempting to pan a + * non-pannable document). + * nsEventStatus_eConsumeDoDefault is returned to indicate that the APZ + * code may have used this event to do some user-visible thing. Note that + * in some cases CONSUMED is returned even if the event was NOT used. This + * is because we cannot always know at the time of event delivery whether + * the event will be used or not. So we err on the side of sending + * CONSUMED when we are uncertain. + */ + nsEventStatus mStatus; + + /** + * This is: + * - set to HandledByRoot if we know for sure that the event will be handled + * by the root content APZC; + * - set to HandledByContent if we know for sure it will not be; + * - left empty if we are unsure. + */ + Maybe mHandledResult; + + public: + /** + * The guid of the APZC initially targeted by this event. + * This will usually be the APZC that handles the event, but in cases + * where the event is dispatched to content, it may end up being + * handled by a different APZC. + */ + ScrollableLayerGuid mTargetGuid; + /** + * If this event started or was added to an input block, the id of that + * input block, otherwise InputBlockState::NO_BLOCK_ID. + */ + uint64_t mInputBlockId; +}; + +/** + * This class lives in the main process, and is accessed via the controller + * thread (which is the process main thread for desktop, and the Java UI + * thread for Android). This class exposes a synchronous API to deliver + * incoming input events to APZ and modify them in-place to unapply the APZ + * async transform. If there is a GPU process, then this class does sync IPC + * calls over to the GPU process in order to accomplish this. Otherwise, + * APZCTreeManager overrides and implements these methods directly. + */ +class APZInputBridge { + public: + using InputBlockCallback = std::function; + + /** + * General handler for incoming input events. Manipulates the frame metrics + * based on what type of input it is. For example, a PinchGestureEvent will + * cause scaling. This should only be called externally to this class, and + * must be called on the controller thread. + * + * This function transforms |aEvent| to have its coordinates in DOM space. + * This is so that the event can be passed through the DOM and content can + * handle them. The event may need to be converted to a WidgetInputEvent + * by the caller if it wants to do this. + * + * @param aEvent input event object; is modified in-place + * @param aCallback an optional callback to be invoked when the input block is + * ready for handling, + * @return The result of processing the event. Refer to the documentation of + * APZEventResult and its field. + */ + virtual APZEventResult ReceiveInputEvent( + InputData& aEvent, + InputBlockCallback&& aCallback = InputBlockCallback()) = 0; + + /** + * WidgetInputEvent handler. Transforms |aEvent| (which is assumed to be an + * already-existing instance of an WidgetInputEvent which may be an + * WidgetTouchEvent) to have its coordinates in DOM space. This is so that the + * event can be passed through the DOM and content can handle them. + * + * NOTE: Be careful of invoking the WidgetInputEvent variant. This can only be + * called on the main thread. See widget/InputData.h for more information on + * why we have InputData and WidgetInputEvent separated. If this function is + * used, the controller thread must be the main thread, or undefined behaviour + * may occur. + * NOTE: On unix, mouse events are treated as touch and are forwarded + * to the appropriate apz as such. + * + * See documentation for other ReceiveInputEvent above. + */ + APZEventResult ReceiveInputEvent( + WidgetInputEvent& aEvent, + InputBlockCallback&& aCallback = InputBlockCallback()); + + // Returns the kind of wheel event action, if any, that will be (or was) + // performed by APZ. If this returns true, the event must not perform a + // synchronous scroll. + // + // Even if this returns Nothing(), all wheel events in APZ-aware widgets must + // be sent through APZ so they are transformed correctly for BrowserParent. + static Maybe ActionForWheelEvent(WidgetWheelEvent* aEvent); + + protected: + friend class APZInputBridgeParent; + + // Methods to help process WidgetInputEvents (or manage conversion to/from + // InputData) + + virtual void ProcessUnhandledEvent(LayoutDeviceIntPoint* aRefPoint, + ScrollableLayerGuid* aOutTargetGuid, + uint64_t* aOutFocusSequenceNumber, + LayersId* aOutLayersId) = 0; + + virtual void UpdateWheelTransaction( + LayoutDeviceIntPoint aRefPoint, EventMessage aEventMessage, + const Maybe& aTargetGuid) = 0; + + virtual ~APZInputBridge() = default; +}; + +std::ostream& operator<<(std::ostream& aOut, + const APZHandledResult& aHandledResult); + +// This enum class is used for communicating between APZ and the browser gesture +// support code. APZ needs to wait for the browser to send this response just +// like APZ waits for the content's response if there's an APZ ware event +// listener in the content process. +enum class BrowserGestureResponse : bool { + NotConsumed = 0, // Representing the browser doesn't consume the gesture + Consumed = 1, // Representing the browser has started consuming the gesture. +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_APZInputBridge_h diff --git a/gfx/layers/apz/public/APZPublicUtils.h b/gfx/layers/apz/public/APZPublicUtils.h new file mode 100644 index 0000000000..6433008b4c --- /dev/null +++ b/gfx/layers/apz/public/APZPublicUtils.h @@ -0,0 +1,82 @@ +/* -*- 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_APZPublicUtils_h +#define mozilla_layers_APZPublicUtils_h + +// This file is for APZ-related utilities that need to be consumed from outside +// of gfx/layers. For internal utilities, prefer APZUtils.h. + +#include +#include +#include "ScrollAnimationBezierPhysics.h" +#include "Units.h" +#include "mozilla/DefineEnum.h" +#include "mozilla/ScrollOrigin.h" +#include "mozilla/gfx/Point.h" +#include "mozilla/ScrollTypes.h" + +namespace mozilla { + +namespace layers { + +struct FrameMetrics; + +// clang-format off +MOZ_DEFINE_ENUM_CLASS_WITH_BASE(APZWheelAction, uint8_t, ( + Scroll, + PinchZoom +)) +// clang-format on + +namespace apz { + +/** + * Initializes the global state used in AsyncPanZoomController. + * This is normally called when it is first needed in the constructor + * of APZCTreeManager, but can be called manually to force it to be + * initialized earlier. + */ +void InitializeGlobalState(); + +/** + * See AsyncPanZoomController::CalculatePendingDisplayPort. This + * function simply delegates to that one, so that non-layers code + * never needs to include AsyncPanZoomController.h + */ +const ScreenMargin CalculatePendingDisplayPort( + const FrameMetrics& aFrameMetrics, const ParentLayerPoint& aVelocity); + +/** + * Returns a width and height multiplier, each of which is a power of two + * between 1 and 8 inclusive. The multiplier is chosen based on the provided + * base size, such that multiplier is larger when the base size is larger. + * The exact details are somewhat arbitrary and tuned by hand. + * This function is intended to only be used with WebRender, because that is + * the codepath that wants to use a larger displayport alignment, because + * moving the displayport is relatively expensive with WebRender. + */ +gfx::IntSize GetDisplayportAlignmentMultiplier(const ScreenSize& aBaseSize); + +/** + * Calculate the physics parameters for smooth scroll animations for the + * given origin, based on pref values. + */ +ScrollAnimationBezierPhysicsSettings ComputeBezierAnimationSettingsForOrigin( + ScrollOrigin aOrigin); + +/** + * Calculate if the scrolling should be instant or smooth based based on + * preferences and the origin + */ +ScrollMode GetScrollModeForOrigin(ScrollOrigin origin); + +} // namespace apz + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_APZPublicUtils_h diff --git a/gfx/layers/apz/public/APZSampler.h b/gfx/layers/apz/public/APZSampler.h new file mode 100644 index 0000000000..a870c2d688 --- /dev/null +++ b/gfx/layers/apz/public/APZSampler.h @@ -0,0 +1,154 @@ +/* -*- 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_APZSampler_h +#define mozilla_layers_APZSampler_h + +#include + +#include "apz/src/APZCTreeManager.h" +#include "base/platform_thread.h" // for PlatformThreadId +#include "mozilla/layers/APZUtils.h" +#include "mozilla/layers/SampleTime.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/webrender/WebRenderTypes.h" +#include "Units.h" +#include "VsyncSource.h" + +namespace mozilla { + +class TimeStamp; + +namespace wr { +struct Transaction; +class TransactionWrapper; +struct WrWindowId; +} // namespace wr + +namespace layers { + +struct ScrollbarData; + +/** + * This interface exposes APZ methods related to "sampling" (i.e. reading the + * async transforms produced by APZ). These methods should all be called on + * the sampler thread. + */ +class APZSampler { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(APZSampler) + + public: + APZSampler(const RefPtr& aApz, bool aIsUsingWebRender); + + // Whoever creates this sampler is responsible for calling Destroy() on it + // before releasing the owning refptr. + void Destroy(); + + void SetWebRenderWindowId(const wr::WindowId& aWindowId); + + /** + * This function is invoked from rust on the render backend thread when it + * is created. It effectively tells the APZSampler "the current thread is + * the sampler thread for this window id" and allows APZSampler to remember + * which thread it is. + */ + static void SetSamplerThread(const wr::WrWindowId& aWindowId); + static void SampleForWebRender(const wr::WrWindowId& aWindowId, + const uint64_t* aGeneratedFrameId, + wr::Transaction* aTransaction); + + void SetSampleTime(const SampleTime& aSampleTime); + void SampleForWebRender(const Maybe& aGeneratedFrameId, + wr::TransactionWrapper& aTxn); + + /** + * Similar to above GetCurrentAsyncTransform, but get the current transform + * with LayersId and ViewID. + * NOTE: This function should NOT be called on the compositor thread. + */ + AsyncTransform GetCurrentAsyncTransform( + const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId, + AsyncTransformComponents aComponents, + const MutexAutoLock& aProofOfMapLock) const; + + /** + * Returns the composition bounds of the APZC corresponding to the pair of + * |aLayersId| and |aScrollId|. + */ + ParentLayerRect GetCompositionBounds( + const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId, + const MutexAutoLock& aProofOfMapLock) const; + + struct ScrollOffsetAndRange { + CSSPoint mOffset; + CSSRect mRange; + }; + /** + * Returns the scroll offset and scroll range of the APZC corresponding to the + * pair of |aLayersId| and |aScrollId| + * + * Note: This is called from OMTA Sampler thread, or Compositor thread for + * testing. + */ + Maybe GetCurrentScrollOffsetAndRange( + const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId, + const MutexAutoLock& aProofOfMapLock) const; + + /** + * This can be used to assert that the current thread is the + * sampler thread (which samples the async transform). + * This does nothing if thread assertions are disabled. + */ + void AssertOnSamplerThread() const; + + /** + * Returns true if currently on the APZSampler's "sampler thread". + */ + bool IsSamplerThread() const; + + template + void CallWithMapLock(Callback& aCallback) { + mApz->CallWithMapLock(aCallback); + } + + protected: + virtual ~APZSampler(); + + static already_AddRefed GetSampler( + const wr::WrWindowId& aWindowId); + + private: + RefPtr mApz; + bool mIsUsingWebRender; + + // Used to manage the mapping from a WR window id to APZSampler. These are + // only used if WebRender is enabled. Both sWindowIdMap and mWindowId should + // only be used while holding the sWindowIdLock. Note that we use a + // StaticAutoPtr wrapper on sWindowIdMap to avoid a static initializer for the + // unordered_map. This also avoids the initializer/memory allocation in cases + // where we're not using WebRender. + static StaticMutex sWindowIdLock MOZ_UNANNOTATED; + static StaticAutoPtr>> + sWindowIdMap; + Maybe mWindowId; + + // Lock used to protected mSamplerThreadId + mutable Mutex mThreadIdLock MOZ_UNANNOTATED; + // If WebRender is enabled, this holds the thread id of the render backend + // thread (which is the sampler thread) for the compositor associated with + // this APZSampler instance. + Maybe mSamplerThreadId; + + Mutex mSampleTimeLock MOZ_UNANNOTATED; + // Can only be accessed or modified while holding mSampleTimeLock. + SampleTime mSampleTime; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_APZSampler_h diff --git a/gfx/layers/apz/public/APZUpdater.h b/gfx/layers/apz/public/APZUpdater.h new file mode 100644 index 0000000000..66d040b356 --- /dev/null +++ b/gfx/layers/apz/public/APZUpdater.h @@ -0,0 +1,236 @@ +/* -*- 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_APZUpdater_h +#define mozilla_layers_APZUpdater_h + +#include +#include + +#include "base/platform_thread.h" // for PlatformThreadId +#include "LayersTypes.h" +#include "mozilla/layers/APZTestData.h" +#include "mozilla/layers/WebRenderScrollData.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/webrender/WebRenderTypes.h" +#include "nsThreadUtils.h" +#include "Units.h" + +namespace mozilla { + +namespace layers { + +class APZCTreeManager; +class FocusTarget; +class WebRenderScrollData; + +/** + * This interface is used to send updates or otherwise mutate APZ internal + * state. These functions is usually called from the compositor thread in + * response to IPC messages. The method implementations internally redispatch + * themselves to the updater thread in the case where the compositor thread + * is not the updater thread. + */ +class APZUpdater { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(APZUpdater) + + public: + APZUpdater(const RefPtr& aApz, bool aConnectedToWebRender); + + bool HasTreeManager(const RefPtr& aApz); + void SetWebRenderWindowId(const wr::WindowId& aWindowId); + + /** + * This function is invoked from rust on the scene builder thread when it + * is created. It effectively tells the APZUpdater "the current thread is + * the updater thread for this window id" and allows APZUpdater to remember + * which thread it is. + */ + static void SetUpdaterThread(const wr::WrWindowId& aWindowId); + static void PrepareForSceneSwap(const wr::WrWindowId& aWindowId); + static void CompleteSceneSwap(const wr::WrWindowId& aWindowId, + const wr::WrPipelineInfo& aInfo); + static void ProcessPendingTasks(const wr::WrWindowId& aWindowId); + + void ClearTree(LayersId aRootLayersId); + void UpdateFocusState(LayersId aRootLayerTreeId, + LayersId aOriginatingLayersId, + const FocusTarget& aFocusTarget); + /** + * This should be called (in the WR-enabled case) when the compositor receives + * a new WebRenderScrollData for a layers id. The |aScrollData| parameter is + * the scroll data for |aOriginatingLayersId| and |aEpoch| is the + * corresponding epoch for the transaction that transferred the scroll data. + * This function will store the new scroll data and update the focus state and + * hit-testing tree. + */ + void UpdateScrollDataAndTreeState(LayersId aRootLayerTreeId, + LayersId aOriginatingLayersId, + const wr::Epoch& aEpoch, + WebRenderScrollData&& aScrollData); + /** + * This is called in the WR-enabled case when we get an empty transaction that + * has some scroll offset updates (from paint-skipped scrolling on the content + * side). This function will update the stored scroll offsets and the + * hit-testing tree. + */ + void UpdateScrollOffsets(LayersId aRootLayerTreeId, + LayersId aOriginatingLayersId, + ScrollUpdatesMap&& aUpdates, + uint32_t aPaintSequenceNumber); + + void NotifyLayerTreeAdopted(LayersId aLayersId, + const RefPtr& aOldUpdater); + void NotifyLayerTreeRemoved(LayersId aLayersId); + + bool GetAPZTestData(LayersId aLayersId, APZTestData* aOutData); + + void SetTestAsyncScrollOffset(LayersId aLayersId, + const ScrollableLayerGuid::ViewID& aScrollId, + const CSSPoint& aOffset); + void SetTestAsyncZoom(LayersId aLayersId, + const ScrollableLayerGuid::ViewID& aScrollId, + const LayerToParentLayerScale& aZoom); + + // This can only be called on the updater thread. + const WebRenderScrollData* GetScrollData(LayersId aLayersId) const; + + /** + * This can be used to assert that the current thread is the + * updater thread (which samples the async transform). + * This does nothing if thread assertions are disabled. + */ + void AssertOnUpdaterThread() const; + + /** + * Runs the given task on the APZ "updater thread" for this APZUpdater. If + * this function is called from the updater thread itself then the task is + * run immediately without getting queued. + * + * The layers id argument should be the id of the layer tree that is + * requesting this task to be run. Conceptually each layer tree has a separate + * task queue, so that if one layer tree is blocked waiting for a scene build + * then tasks for the other layer trees can still be processed. + */ + void RunOnUpdaterThread(LayersId aLayersId, already_AddRefed aTask); + + /** + * Returns true if currently on the APZUpdater's "updater thread". + */ + bool IsUpdaterThread() const; + + /** + * Dispatches the given task to the APZ "controller thread", but does it + * *from* the updater thread. That is, if the thread on which this function is + * called is not the updater thread, the task is first dispatched to the + * updater thread. When the updater thread runs it (or if this is called + * directly on the updater thread), that is when the task gets dispatched to + * the controller thread. The controller thread then actually runs the task. + * + * See the RunOnUpdaterThread method for details on the layers id argument. + */ + void RunOnControllerThread(LayersId aLayersId, + already_AddRefed aTask); + + void MarkAsDetached(LayersId aLayersId); + + protected: + virtual ~APZUpdater(); + + // Return true if the APZUpdater is connected to WebRender and is + // using a WebRender scene builder thread as its updater thread. + // This is only false during GTests, and a shutdown codepath during + // which we create a dummy APZUpdater. + bool IsConnectedToWebRender() const; + + static already_AddRefed GetUpdater( + const wr::WrWindowId& aWindowId); + + void ProcessQueue(); + + private: + RefPtr mApz; + bool mDestroyed; + bool mConnectedToWebRender; + + // Map from layers id to WebRenderScrollData. This can only be touched on + // the updater thread. + std::unordered_map + mScrollData; + + // Stores epoch state for a particular layers id. This structure is only + // accessed on the updater thread. + struct EpochState { + // The epoch for the most recent scroll data sent from the content side. + wr::Epoch mRequired; + // The epoch for the most recent scene built and swapped in on the WR side. + Maybe mBuilt; + // True if and only if the layers id is the root layers id for the + // compositor + bool mIsRoot; + + EpochState(); + + // Whether or not the state for this layers id is such that it blocks + // processing of tasks for the layer tree. This happens if the root layers + // id or a "visible" layers id has scroll data for an epoch newer than what + // has been built. A "visible" layers id is one that is attached to the full + // layer tree (i.e. there is a chain of reflayer items from the root layer + // tree to the relevant layer subtree). This is not always the case; for + // instance a content process may send the compositor layers for a document + // before the chrome has attached the remote iframe to the root document. + // Since WR only builds pipelines for "visible" layers ids, |mBuilt| being + // populated means that the layers id is "visible". + bool IsBlocked() const; + }; + + // Map from layers id to epoch state. + // This data structure can only be touched on the updater thread. + std::unordered_map mEpochData; + + // Used to manage the mapping from a WR window id to APZUpdater. These are + // only used if WebRender is enabled. Both sWindowIdMap and mWindowId should + // only be used while holding the sWindowIdLock. Note that we use a + // StaticAutoPtr wrapper on sWindowIdMap to avoid a static initializer for the + // unordered_map. This also avoids the initializer/memory allocation in cases + // where we're not using WebRender. + static StaticMutex sWindowIdLock MOZ_UNANNOTATED; + static StaticAutoPtr> sWindowIdMap; + Maybe mWindowId; + + // Lock used to protected mUpdaterThreadId; + mutable Mutex mThreadIdLock MOZ_UNANNOTATED; + // If WebRender and async scene building are enabled, this holds the thread id + // of the scene builder thread (which is the updater thread) for the + // compositor associated with this APZUpdater instance. It may be populated + // even if async scene building is not enabled, but in that case we don't + // care about the contents. + Maybe mUpdaterThreadId; + + // Helper struct that pairs each queued runnable with the layers id that it is + // associated with. This allows us to easily implement the conceptual + // separation of mUpdaterQueue into independent queues per layers id. + struct QueuedTask { + LayersId mLayersId; + RefPtr mRunnable; + }; + + // Lock used to protect mUpdaterQueue + Mutex mQueueLock MOZ_UNANNOTATED; + // Holds a queue of tasks to be run on the updater thread, when the updater + // thread is a WebRender thread, since it won't have a message loop we can + // dispatch to. Note that although this is a single queue it is conceptually + // separated into multiple ones, one per layers id. Tasks for a given layers + // id will always run in FIFO order, but there is no guaranteed ordering for + // tasks with different layers ids. + std::deque mUpdaterQueue; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_APZUpdater_h diff --git a/gfx/layers/apz/public/CompositorController.h b/gfx/layers/apz/public/CompositorController.h new file mode 100644 index 0000000000..ad71a8faf5 --- /dev/null +++ b/gfx/layers/apz/public/CompositorController.h @@ -0,0 +1,33 @@ +/* -*- 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_CompositorController_h +#define mozilla_layers_CompositorController_h + +#include "nsISupportsImpl.h" // for NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING +#include "mozilla/Maybe.h" +#include "mozilla/webrender/WebRenderTypes.h" + +namespace mozilla { +namespace layers { + +class CompositorController { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + /** + * Ask the compositor to schedule a new composite. + */ + virtual void ScheduleRenderOnCompositorThread(wr::RenderReasons aReasons) = 0; + + protected: + virtual ~CompositorController() = default; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_CompositorController_h diff --git a/gfx/layers/apz/public/GeckoContentController.h b/gfx/layers/apz/public/GeckoContentController.h new file mode 100644 index 0000000000..abfef68caa --- /dev/null +++ b/gfx/layers/apz/public/GeckoContentController.h @@ -0,0 +1,187 @@ +/* -*- 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_GeckoContentController_h +#define mozilla_layers_GeckoContentController_h + +#include "GeckoContentControllerTypes.h" +#include "InputData.h" // for PinchGestureInput +#include "LayersTypes.h" // for ScrollDirection +#include "Units.h" // for CSSPoint, CSSRect, etc +#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER2 +#include "mozilla/Attributes.h" // for MOZ_CAN_RUN_SCRIPT +#include "mozilla/DefineEnum.h" // for MOZ_DEFINE_ENUM +#include "mozilla/EventForwards.h" // for Modifiers +#include "mozilla/layers/APZThreadUtils.h" +#include "mozilla/layers/MatrixMessage.h" // for MatrixMessage +#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid, etc +#include "nsISupportsImpl.h" + +namespace mozilla { + +class Runnable; + +namespace layers { + +struct RepaintRequest; + +class GeckoContentController { + public: + using APZStateChange = GeckoContentController_APZStateChange; + using TapType = GeckoContentController_TapType; + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GeckoContentController) + + /** + * Notifies the content side of the most recently computed transforms for + * each layers subtree to the root. The nsTArray will contain one + * MatrixMessage for each layers id in the current APZ tree, along with the + * corresponding transform. + */ + virtual void NotifyLayerTransforms(nsTArray&& aTransforms) = 0; + + /** + * Requests a paint of the given RepaintRequest |aRequest| from Gecko. + * Implementations per-platform are responsible for actually handling this. + * + * This method must always be called on the repaint thread, which depends + * on the GeckoContentController. For ChromeProcessController it is the + * Gecko main thread, while for RemoteContentController it is the compositor + * thread where it can send IPDL messages. + */ + virtual void RequestContentRepaint(const RepaintRequest& aRequest) = 0; + + /** + * Requests handling of a tap event. |aPoint| is in LD pixels, relative to the + * current scroll offset. + */ + MOZ_CAN_RUN_SCRIPT + virtual void HandleTap(TapType aType, const LayoutDevicePoint& aPoint, + Modifiers aModifiers, const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId) = 0; + + /** + * When the apz.allow_zooming pref is set to false, the APZ will not + * translate pinch gestures to actual zooming. Instead, it will call this + * method to notify gecko of the pinch gesture, and allow it to deal with it + * however it wishes. Note that this function is not called if the pinch is + * prevented by content calling preventDefault() on the touch events, or via + * use of the touch-action property. + * @param aType One of PINCHGESTURE_START, PINCHGESTURE_SCALE, + * PINCHGESTURE_FINGERLIFTED, or PINCHGESTURE_END, indicating the phase + * of the pinch. + * @param aGuid The guid of the APZ that is detecting the pinch. This is + * generally the root APZC for the layers id. + * @param aFocusPoint The focus point of the pinch event. + * @param aSpanChange For the START or END event, this is always 0. + * For a SCALE event, this is the difference in span between the + * previous state and the new state. + * @param aModifiers The keyboard modifiers depressed during the pinch. + */ + virtual void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType, + const ScrollableLayerGuid& aGuid, + const LayoutDevicePoint& aFocusPoint, + LayoutDeviceCoord aSpanChange, + Modifiers aModifiers) = 0; + + /** + * Schedules a runnable to run on the controller thread at some time + * in the future. + * This method must always be called on the controller thread. + */ + virtual void PostDelayedTask(already_AddRefed aRunnable, + int aDelayMs) { + APZThreadUtils::DelayedDispatch(std::move(aRunnable), aDelayMs); + } + + /** + * Returns true if we are currently on the thread that can send repaint + * requests. + */ + virtual bool IsRepaintThread() = 0; + + /** + * Runs the given task on the "repaint" thread. + */ + virtual void DispatchToRepaintThread(already_AddRefed aTask) = 0; + + /** + * General notices of APZ state changes for consumers. + * |aGuid| identifies the APZC originating the state change. + * |aChange| identifies the type of state change + * |aArg| is used by some state changes to pass extra information (see + * the documentation for each state change above) + * |aInputBlockId| is populated for the |eStartTouch| and |eEndTouch| + * state changes and identifies the input block of the + * gesture that triggers the state change. + */ + virtual void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid, + APZStateChange aChange, int aArg = 0, + Maybe aInputBlockId = Nothing()) { + } + + /** + * Notify content of a MozMouseScrollFailed event. + */ + virtual void NotifyMozMouseScrollEvent( + const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent) {} + + /** + * Notify content that the repaint requests have been flushed. + */ + virtual void NotifyFlushComplete() = 0; + + /** + * If the async scrollbar-drag initiation code kicks in on the APZ side, then + * we need to let content know that we are dragging the scrollbar. Otherwise, + * by the time the mousedown events is handled by content, the scrollthumb + * could already have been moved via a RequestContentRepaint message at a + * new scroll position, and the mousedown might end up triggering a click-to- + * scroll on where the thumb used to be. + */ + virtual void NotifyAsyncScrollbarDragInitiated( + uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId, + ScrollDirection aDirection) = 0; + virtual void NotifyAsyncScrollbarDragRejected( + const ScrollableLayerGuid::ViewID& aScrollId) = 0; + + virtual void NotifyAsyncAutoscrollRejected( + const ScrollableLayerGuid::ViewID& aScrollId) = 0; + + virtual void CancelAutoscroll(const ScrollableLayerGuid& aGuid) = 0; + + virtual void NotifyScaleGestureComplete(const ScrollableLayerGuid& aGuid, + float aScale) = 0; + + virtual void UpdateOverscrollVelocity(const ScrollableLayerGuid& aGuid, + float aX, float aY, + bool aIsRootContent) {} + virtual void UpdateOverscrollOffset(const ScrollableLayerGuid& aGuid, + float aX, float aY, bool aIsRootContent) { + } + + GeckoContentController() = default; + + /** + * Needs to be called on the main thread. + */ + virtual void Destroy() {} + + /** + * Whether this is RemoteContentController. + */ + virtual bool IsRemote() { return false; } + + virtual PresShell* GetTopLevelPresShell() const { return nullptr; }; + + protected: + // Protected destructor, to discourage deletion outside of Release(): + virtual ~GeckoContentController() = default; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_GeckoContentController_h diff --git a/gfx/layers/apz/public/GeckoContentControllerTypes.h b/gfx/layers/apz/public/GeckoContentControllerTypes.h new file mode 100644 index 0000000000..8ab478eab5 --- /dev/null +++ b/gfx/layers/apz/public/GeckoContentControllerTypes.h @@ -0,0 +1,68 @@ +/* -*- 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_GeckoContentControllerTypes_h +#define mozilla_layers_GeckoContentControllerTypes_h + +#include "mozilla/DefineEnum.h" + +namespace mozilla { +namespace layers { + +// clang-format off +MOZ_DEFINE_ENUM_CLASS(GeckoContentController_APZStateChange, ( + /** + * APZ started modifying the view (including panning, zooming, and fling). + */ + eTransformBegin, + /** + * APZ finished modifying the view. + */ + eTransformEnd, + /** + * APZ started a touch. + * |aArg| is 1 if touch can be a pan, 0 otherwise. + */ + eStartTouch, + /** + * APZ started a pan. + */ + eStartPanning, + /** + * APZ finished processing a touch. + * |aArg| is 1 if touch was a click, 0 otherwise. + */ + eEndTouch +)); +// clang-format on + +/** + * Different types of tap-related events that can be sent in + * the HandleTap function. The names should be relatively self-explanatory. + * Note that the eLongTapUp will always be preceded by an eLongTap, but not + * all eLongTap notifications will be followed by an eLongTapUp (for instance, + * if the user moves their finger after triggering the long-tap but before + * lifting it). + * The difference between eDoubleTap and eSecondTap is subtle - the eDoubleTap + * is for an actual double-tap "gesture" while eSecondTap is for the same user + * input but where a double-tap gesture is not allowed. This is used to fire + * a click event with detail=2 to web content (similar to what a mouse double- + * click would do). + */ +// clang-format off +MOZ_DEFINE_ENUM_CLASS(GeckoContentController_TapType, ( + eSingleTap, + eDoubleTap, + eSecondTap, + eLongTap, + eLongTapUp +)); +// clang-format on + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_GeckoContentControllerTypes_h diff --git a/gfx/layers/apz/public/IAPZCTreeManager.h b/gfx/layers/apz/public/IAPZCTreeManager.h new file mode 100644 index 0000000000..cd0859d34d --- /dev/null +++ b/gfx/layers/apz/public/IAPZCTreeManager.h @@ -0,0 +1,150 @@ +/* -*- 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_IAPZCTreeManager_h +#define mozilla_layers_IAPZCTreeManager_h + +#include // for uint64_t, uint32_t + +#include "mozilla/layers/LayersTypes.h" // for TouchBehaviorFlags +#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid, etc +#include "mozilla/layers/ZoomConstraints.h" // for ZoomConstraints +#include "nsTArrayForwardDeclare.h" // for nsTArray, nsTArray_Impl, etc +#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, etc +#include "Units.h" // for CSSRect, etc + +namespace mozilla { +namespace layers { + +class APZInputBridge; +class KeyboardMap; +struct ZoomTarget; + +enum AllowedTouchBehavior { + NONE = 0, + VERTICAL_PAN = 1 << 0, + HORIZONTAL_PAN = 1 << 1, + PINCH_ZOOM = 1 << 2, + ANIMATING_ZOOM = 1 << 3, + UNKNOWN = 1 << 4 +}; + +enum ZoomToRectBehavior : uint32_t { + DEFAULT_BEHAVIOR = 0, + DISABLE_ZOOM_OUT = 1 << 0, + PAN_INTO_VIEW_ONLY = 1 << 1, + ONLY_ZOOM_TO_DEFAULT_SCALE = 1 << 2, +}; + +enum class BrowserGestureResponse : bool; + +class AsyncDragMetrics; +struct APZHandledResult; + +class IAPZCTreeManager { + NS_INLINE_DECL_THREADSAFE_VIRTUAL_REFCOUNTING(IAPZCTreeManager) + + public: + /** + * Set the keyboard shortcuts to use for translating keyboard events. + */ + virtual void SetKeyboardMap(const KeyboardMap& aKeyboardMap) = 0; + + /** + * Kicks an animation to zoom to a rect. This may be either a zoom out or zoom + * in. The actual animation is done on the sampler thread after being set + * up. |aRect| must be given in CSS pixels, relative to the document. + * |aFlags| is a combination of the ZoomToRectBehavior enum values. + */ + virtual void ZoomToRect(const ScrollableLayerGuid& aGuid, + const ZoomTarget& aZoomTarget, + const uint32_t aFlags = DEFAULT_BEHAVIOR) = 0; + + /** + * If we have touch listeners, this should always be called when we know + * definitively whether or not content has preventDefaulted any touch events + * that have come in. If |aPreventDefault| is true, any touch events in the + * queue will be discarded. This function must be called on the controller + * thread. + */ + virtual void ContentReceivedInputBlock(uint64_t aInputBlockId, + bool aPreventDefault) = 0; + + /** + * When the event regions code is enabled, this function should be invoked to + * to confirm the target of the input block. This is only needed in cases + * where the initial input event of the block hit a dispatch-to-content region + * but is safe to call for all input blocks. This function should always be + * invoked on the controller thread. + * The different elements in the array of targets correspond to the targets + * for the different touch points. In the case where the touch point has no + * target, or the target is not a scrollable frame, the target's |mScrollId| + * should be set to ScrollableLayerGuid::NULL_SCROLL_ID. + */ + virtual void SetTargetAPZC(uint64_t aInputBlockId, + const nsTArray& aTargets) = 0; + + /** + * Updates any zoom constraints contained in the tag. + * If the |aConstraints| is Nothing() then previously-provided constraints for + * the given |aGuid| are cleared. + */ + virtual void UpdateZoomConstraints( + const ScrollableLayerGuid& aGuid, + const Maybe& aConstraints) = 0; + + virtual void SetDPI(float aDpiValue) = 0; + + /** + * Sets allowed touch behavior values for current touch-session for specific + * input block (determined by aInputBlock). + * Should be invoked by the widget. Each value of the aValues arrays + * corresponds to the different touch point that is currently active. + * Must be called after receiving the TOUCH_START event that starts the + * touch-session. + * This must be called on the controller thread. + */ + virtual void SetAllowedTouchBehavior( + uint64_t aInputBlockId, const nsTArray& aValues) = 0; + + virtual void SetBrowserGestureResponse(uint64_t aInputBlockId, + BrowserGestureResponse aResponse) = 0; + + virtual void StartScrollbarDrag(const ScrollableLayerGuid& aGuid, + const AsyncDragMetrics& aDragMetrics) = 0; + + virtual bool StartAutoscroll(const ScrollableLayerGuid& aGuid, + const ScreenPoint& aAnchorLocation) = 0; + + virtual void StopAutoscroll(const ScrollableLayerGuid& aGuid) = 0; + + /** + * Function used to disable LongTap gestures. + * + * On slow running tests, drags and touch events can be misinterpreted + * as a long tap. This allows tests to disable long tap gesture detection. + */ + virtual void SetLongTapEnabled(bool aTapGestureEnabled) = 0; + + /** + * Returns an APZInputBridge interface that can be used to send input + * events to APZ in a synchronous manner. This will always be non-null, and + * the returned object's lifetime will match the lifetime of this + * IAPZCTreeManager implementation. + * It is only valid to call this function in the UI process. + */ + virtual APZInputBridge* InputBridge() = 0; + + protected: + // Discourage destruction outside of decref + + virtual ~IAPZCTreeManager() = default; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_IAPZCTreeManager_h diff --git a/gfx/layers/apz/public/MatrixMessage.h b/gfx/layers/apz/public/MatrixMessage.h new file mode 100644 index 0000000000..45050f7406 --- /dev/null +++ b/gfx/layers/apz/public/MatrixMessage.h @@ -0,0 +1,64 @@ +/* -*- 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_MatrixMessage_h +#define mozilla_layers_MatrixMessage_h + +#include "mozilla/Maybe.h" +#include "mozilla/gfx/Matrix.h" +#include "mozilla/layers/LayersTypes.h" +#include "Units.h" // for ScreenRect +#include "UnitTransforms.h" + +namespace mozilla { +namespace layers { +class MatrixMessage { + public: + // Don't use this one directly + MatrixMessage() = default; + + MatrixMessage(const Maybe& aMatrix, + const ScreenRect& aTopLevelViewportVisibleRectInBrowserCoords, + const LayersId& aLayersId) + : mMatrix(ToUnknownMatrix(aMatrix)), + mTopLevelViewportVisibleRectInBrowserCoords( + aTopLevelViewportVisibleRectInBrowserCoords), + mLayersId(aLayersId) {} + + inline Maybe GetMatrix() const { + return LayerToScreenMatrix4x4::FromUnknownMatrix(mMatrix); + } + + inline ScreenRect GetTopLevelViewportVisibleRectInBrowserCoords() const { + return mTopLevelViewportVisibleRectInBrowserCoords; + } + + inline const LayersId& GetLayersId() const { return mLayersId; } + + bool operator==(const MatrixMessage& aOther) const { + return aOther.mMatrix == mMatrix && + aOther.mTopLevelViewportVisibleRectInBrowserCoords == + mTopLevelViewportVisibleRectInBrowserCoords && + aOther.mLayersId == mLayersId; + } + + bool operator!=(const MatrixMessage& aOther) const { + return !(*this == aOther); + } + // Fields are public for IPC. Don't access directly + // elsewhere. + // Transform matrix to convert this layer to screen coordinate. + Maybe mMatrix; // Untyped for IPC + // The remote iframe document rectangle corresponding to this layer. + // The rectangle is the result of clipped out by ancestor async scrolling so + // that the rectangle will be empty if it's completely scrolled out of view. + ScreenRect mTopLevelViewportVisibleRectInBrowserCoords; + LayersId mLayersId; +}; +}; // namespace layers +}; // namespace mozilla + +#endif // mozilla_layers_MatrixMessage_h diff --git a/gfx/layers/apz/src/APZCTreeManager.cpp b/gfx/layers/apz/src/APZCTreeManager.cpp new file mode 100644 index 0000000000..9dd296dd97 --- /dev/null +++ b/gfx/layers/apz/src/APZCTreeManager.cpp @@ -0,0 +1,3742 @@ +/* -*- 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 +#include +#include "APZCTreeManager.h" +#include "AsyncPanZoomController.h" +#include "Compositor.h" // for Compositor +#include "DragTracker.h" // for DragTracker +#include "GenericFlingAnimation.h" // for FLING_LOG +#include "HitTestingTreeNode.h" // for HitTestingTreeNode +#include "InputBlockState.h" // for InputBlockState +#include "InputData.h" // for InputData, etc +#include "WRHitTester.h" // for WRHitTester +#include "mozilla/RecursiveMutex.h" +#include "mozilla/dom/MouseEventBinding.h" // for MouseEvent constants +#include "mozilla/dom/BrowserParent.h" // for AreRecordReplayTabsActive +#include "mozilla/dom/Touch.h" // for Touch +#include "mozilla/gfx/CompositorHitTestInfo.h" +#include "mozilla/gfx/LoggingConstants.h" +#include "mozilla/gfx/gfxVars.h" // for gfxVars +#include "mozilla/gfx/GPUParent.h" // for GPUParent +#include "mozilla/gfx/Logging.h" // for gfx::TreeLog +#include "mozilla/gfx/Point.h" // for Point +#include "mozilla/layers/APZSampler.h" // for APZSampler +#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread, etc +#include "mozilla/layers/APZUpdater.h" // for APZUpdater +#include "mozilla/layers/APZUtils.h" // for AsyncTransform +#include "mozilla/layers/AsyncDragMetrics.h" // for AsyncDragMetrics +#include "mozilla/layers/CompositorBridgeParent.h" // for CompositorBridgeParent, etc +#include "mozilla/layers/DoubleTapToZoom.h" // for ZoomTarget +#include "mozilla/layers/MatrixMessage.h" +#include "mozilla/layers/UiCompositorControllerParent.h" +#include "mozilla/layers/WebRenderScrollDataWrapper.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/mozalloc.h" // for operator new +#include "mozilla/Preferences.h" // for Preferences +#include "mozilla/StaticPrefs_accessibility.h" +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/ToString.h" +#include "mozilla/TouchEvents.h" +#include "mozilla/EventStateManager.h" // for WheelPrefs +#include "mozilla/webrender/WebRenderAPI.h" +#include "nsDebug.h" // for NS_WARNING +#include "nsPoint.h" // for nsIntPoint +#include "nsThreadUtils.h" // for NS_IsMainThread +#include "ScrollThumbUtils.h" // for ComputeTransformForScrollThumb +#include "OverscrollHandoffState.h" // for OverscrollHandoffState +#include "TreeTraversal.h" // for ForEachNode, BreadthFirstSearch, etc +#include "Units.h" // for ParentlayerPixel +#include "GestureEventListener.h" // for GestureEventListener::setLongTapEnabled +#include "UnitTransforms.h" // for ViewAs + +mozilla::LazyLogModule mozilla::layers::APZCTreeManager::sLog("apz.manager"); +#define APZCTM_LOG(...) \ + MOZ_LOG(APZCTreeManager::sLog, LogLevel::Debug, (__VA_ARGS__)) + +static mozilla::LazyLogModule sApzKeyLog("apz.key"); +#define APZ_KEY_LOG(...) MOZ_LOG(sApzKeyLog, LogLevel::Debug, (__VA_ARGS__)) + +namespace mozilla { +namespace layers { + +using mozilla::gfx::CompositorHitTestFlags; +using mozilla::gfx::CompositorHitTestInfo; +using mozilla::gfx::CompositorHitTestInvisibleToHit; +using mozilla::gfx::LOG_DEFAULT; + +typedef mozilla::gfx::Point Point; +typedef mozilla::gfx::Point4D Point4D; +typedef mozilla::gfx::Matrix4x4 Matrix4x4; + +typedef CompositorBridgeParent::LayerTreeState LayerTreeState; + +struct APZCTreeManager::TreeBuildingState { + TreeBuildingState(LayersId aRootLayersId, bool aIsFirstPaint, + LayersId aOriginatingLayersId, APZTestData* aTestData, + uint32_t aPaintSequence) + : mIsFirstPaint(aIsFirstPaint), + mOriginatingLayersId(aOriginatingLayersId), + mPaintLogger(aTestData, aPaintSequence) { + CompositorBridgeParent::CallWithIndirectShadowTree( + aRootLayersId, [this](LayerTreeState& aState) -> void { + mCompositorController = aState.GetCompositorController(); + }); + } + + typedef std::unordered_map + DeferredTransformMap; + + // State that doesn't change as we recurse in the tree building + RefPtr mCompositorController; + const bool mIsFirstPaint; + const LayersId mOriginatingLayersId; + const APZPaintLogHelper mPaintLogger; + + // State that is updated as we perform the tree build + + // A list of nodes that need to be destroyed at the end of the tree building. + // This is initialized with all nodes in the old tree, and nodes are removed + // from it as we reuse them in the new tree. + nsTArray> mNodesToDestroy; + + // This map is populated as we place APZCs into the new tree. Its purpose is + // to facilitate re-using the same APZC for different layers that scroll + // together (and thus have the same ScrollableLayerGuid). The presShellId + // doesn't matter for this purpose, and we move the map to the APZCTreeManager + // after we're done building, so it's useful to have the presshell-ignoring + // map for that. + std::unordered_map + mApzcMap; + + // This is populated with all the HitTestingTreeNodes that are scroll thumbs + // and have a scrollthumb animation id (which indicates that they need to be + // sampled for WebRender on the sampler thread). + std::vector mScrollThumbs; + // This is populated with all the scroll target nodes. We use in conjunction + // with mScrollThumbs to build APZCTreeManager::mScrollThumbInfo. + std::unordered_map + mScrollTargets; + + // During the tree building process, the perspective transform component + // of the ancestor transforms of some APZCs can be "deferred" to their + // children, meaning they are added to the children's ancestor transforms + // instead. Those deferred transforms are tracked here. + DeferredTransformMap mPerspectiveTransformsDeferredToChildren; + + // As we recurse down through the tree, this picks up the zoom animation id + // from a node in the layer tree, and propagates it downwards to the nearest + // APZC instance that is for an RCD node. Generally it will be set on the + // root node of the layers (sub-)tree, which may not be same as the RCD node + // for the subtree, and so we need this mechanism to ensure it gets propagated + // to the RCD's APZC instance. Once it is set on the APZC instance, the value + // is cleared back to Nothing(). Note that this is only used in the WebRender + // codepath. + Maybe mZoomAnimationId; + + // See corresponding members of APZCTreeManager. These are the same thing, but + // on the tree-walking state. They are populated while walking the tree in + // a layers update, and then moved into APZCTreeManager. + std::vector mFixedPositionInfo; + std::vector mRootScrollbarInfo; + std::vector mStickyPositionInfo; + + // As we recurse down through reflayers in the tree, this picks up the + // cumulative EventRegionsOverride flags from the reflayers, and is used to + // apply them to descendant layers. + std::stack mOverrideFlags; +}; + +class APZCTreeManager::CheckerboardFlushObserver : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + explicit CheckerboardFlushObserver(APZCTreeManager* aTreeManager) + : mTreeManager(aTreeManager) { + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr obsSvc = + mozilla::services::GetObserverService(); + MOZ_ASSERT(obsSvc); + if (obsSvc) { + obsSvc->AddObserver(this, "APZ:FlushActiveCheckerboard", false); + } + } + + void Unregister() { + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr obsSvc = + mozilla::services::GetObserverService(); + if (obsSvc) { + obsSvc->RemoveObserver(this, "APZ:FlushActiveCheckerboard"); + } + mTreeManager = nullptr; + } + + protected: + virtual ~CheckerboardFlushObserver() = default; + + private: + RefPtr mTreeManager; +}; + +NS_IMPL_ISUPPORTS(APZCTreeManager::CheckerboardFlushObserver, nsIObserver) + +NS_IMETHODIMP +APZCTreeManager::CheckerboardFlushObserver::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t*) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mTreeManager.get()); + + RecursiveMutexAutoLock lock(mTreeManager->mTreeLock); + if (mTreeManager->mRootNode) { + ForEachNode( + mTreeManager->mRootNode.get(), [](HitTestingTreeNode* aNode) { + if (aNode->IsPrimaryHolder()) { + MOZ_ASSERT(aNode->GetApzc()); + aNode->GetApzc()->FlushActiveCheckerboardReport(); + } + }); + } + if (XRE_IsGPUProcess()) { + if (gfx::GPUParent* gpu = gfx::GPUParent::GetSingleton()) { + nsCString topic("APZ:FlushActiveCheckerboard:Done"); + Unused << gpu->SendNotifyUiObservers(topic); + } + } else { + MOZ_ASSERT(XRE_IsParentProcess()); + nsCOMPtr obsSvc = + mozilla::services::GetObserverService(); + if (obsSvc) { + obsSvc->NotifyObservers(nullptr, "APZ:FlushActiveCheckerboard:Done", + nullptr); + } + } + return NS_OK; +} + +/** + * A RAII class used for setting the focus sequence number on input events + * as they are being processed. Any input event is assumed to be potentially + * focus changing unless explicitly marked otherwise. + */ +class MOZ_RAII AutoFocusSequenceNumberSetter { + public: + AutoFocusSequenceNumberSetter(FocusState& aFocusState, InputData& aEvent) + : mFocusState(aFocusState), mEvent(aEvent), mMayChangeFocus(true) {} + + void MarkAsNonFocusChanging() { mMayChangeFocus = false; } + + ~AutoFocusSequenceNumberSetter() { + if (mMayChangeFocus) { + mFocusState.ReceiveFocusChangingEvent(); + + APZ_KEY_LOG( + "Marking input with type=%d as focus changing with seq=%" PRIu64 "\n", + static_cast(mEvent.mInputType), + mFocusState.LastAPZProcessedEvent()); + } else { + APZ_KEY_LOG( + "Marking input with type=%d as non focus changing with seq=%" PRIu64 + "\n", + static_cast(mEvent.mInputType), + mFocusState.LastAPZProcessedEvent()); + } + + mEvent.mFocusSequenceNumber = mFocusState.LastAPZProcessedEvent(); + } + + private: + FocusState& mFocusState; + InputData& mEvent; + bool mMayChangeFocus; +}; + +APZCTreeManager::APZCTreeManager(LayersId aRootLayersId, + UniquePtr aHitTester) + : mTestSampleTime(Nothing(), "APZCTreeManager::mTestSampleTime"), + mInputQueue(new InputQueue()), + mRootLayersId(aRootLayersId), + mSampler(nullptr), + mUpdater(nullptr), + mTreeLock("APZCTreeLock"), + mMapLock("APZCMapLock"), + mRetainedTouchIdentifier(-1), + mInScrollbarTouchDrag(false), + mCurrentMousePosition(ScreenPoint(), + "APZCTreeManager::mCurrentMousePosition"), + mApzcTreeLog("apzctree"), + mTestDataLock("APZTestDataLock"), + mDPI(160.0), + mHitTester(std::move(aHitTester)), + mScrollGenerationLock("APZScrollGenerationLock") { + RefPtr self(this); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "layers::APZCTreeManager::APZCTreeManager", + [self] { self->mFlushObserver = new CheckerboardFlushObserver(self); })); + AsyncPanZoomController::InitializeGlobalState(); + mApzcTreeLog.ConditionOnPrefFunction(StaticPrefs::apz_printtree); + + if (!mHitTester) { + mHitTester = MakeUnique(); + } + mHitTester->Initialize(this); +} + +APZCTreeManager::~APZCTreeManager() = default; + +void APZCTreeManager::SetSampler(APZSampler* aSampler) { + // We're either setting the sampler or clearing it + MOZ_ASSERT((mSampler == nullptr) != (aSampler == nullptr)); + mSampler = aSampler; +} + +void APZCTreeManager::SetUpdater(APZUpdater* aUpdater) { + // We're either setting the updater or clearing it + MOZ_ASSERT((mUpdater == nullptr) != (aUpdater == nullptr)); + mUpdater = aUpdater; +} + +void APZCTreeManager::NotifyLayerTreeAdopted( + LayersId aLayersId, const RefPtr& aOldApzcTreeManager) { + AssertOnUpdaterThread(); + + if (aOldApzcTreeManager) { + aOldApzcTreeManager->mFocusState.RemoveFocusTarget(aLayersId); + // While we could move the focus target information from the old APZC tree + // manager into this one, it's safer to not do that, as we'll probably have + // that information repopulated soon anyway (on the next layers update). + } + + UniquePtr adoptedData; + if (aOldApzcTreeManager) { + MutexAutoLock lock(aOldApzcTreeManager->mTestDataLock); + auto it = aOldApzcTreeManager->mTestData.find(aLayersId); + if (it != aOldApzcTreeManager->mTestData.end()) { + adoptedData = std::move(it->second); + aOldApzcTreeManager->mTestData.erase(it); + } + } + if (adoptedData) { + MutexAutoLock lock(mTestDataLock); + mTestData[aLayersId] = std::move(adoptedData); + } +} + +void APZCTreeManager::NotifyLayerTreeRemoved(LayersId aLayersId) { + AssertOnUpdaterThread(); + + mFocusState.RemoveFocusTarget(aLayersId); + + { // scope lock + MutexAutoLock lock(mTestDataLock); + mTestData.erase(aLayersId); + } +} + +AsyncPanZoomController* APZCTreeManager::NewAPZCInstance( + LayersId aLayersId, GeckoContentController* aController) { + return new AsyncPanZoomController( + aLayersId, this, mInputQueue, aController, + AsyncPanZoomController::USE_GESTURE_DETECTOR); +} + +void APZCTreeManager::SetTestSampleTime(const Maybe& aTime) { + auto testSampleTime = mTestSampleTime.Lock(); + testSampleTime.ref() = aTime; +} + +SampleTime APZCTreeManager::GetFrameTime() { + auto testSampleTime = mTestSampleTime.Lock(); + if (testSampleTime.ref()) { + return SampleTime::FromTest(*testSampleTime.ref()); + } + return SampleTime::FromNow(); +} + +void APZCTreeManager::SetAllowedTouchBehavior( + uint64_t aInputBlockId, const nsTArray& aValues) { + if (!APZThreadUtils::IsControllerThread()) { + APZThreadUtils::RunOnControllerThread( + NewRunnableMethod>>( + "layers::APZCTreeManager::SetAllowedTouchBehavior", this, + &APZCTreeManager::SetAllowedTouchBehavior, aInputBlockId, + aValues.Clone())); + return; + } + + APZThreadUtils::AssertOnControllerThread(); + + mInputQueue->SetAllowedTouchBehavior(aInputBlockId, aValues); +} + +void APZCTreeManager::SetBrowserGestureResponse( + uint64_t aInputBlockId, BrowserGestureResponse aResponse) { + if (!APZThreadUtils::IsControllerThread()) { + APZThreadUtils::RunOnControllerThread( + NewRunnableMethod( + "layers::APZCTreeManager::SetBrowserGestureResponse", this, + &APZCTreeManager::SetBrowserGestureResponse, aInputBlockId, + aResponse)); + return; + } + + APZThreadUtils::AssertOnControllerThread(); + + mInputQueue->SetBrowserGestureResponse(aInputBlockId, aResponse); +} + +void APZCTreeManager::UpdateHitTestingTree( + const WebRenderScrollDataWrapper& aRoot, bool aIsFirstPaint, + LayersId aOriginatingLayersId, uint32_t aPaintSequenceNumber) { + AssertOnUpdaterThread(); + + RecursiveMutexAutoLock lock(mTreeLock); + + // For testing purposes, we log some data to the APZTestData associated with + // the layers id that originated this update. + APZTestData* testData = nullptr; + if (StaticPrefs::apz_test_logging_enabled()) { + MutexAutoLock lock(mTestDataLock); + UniquePtr ptr = MakeUnique(); + auto result = + mTestData.insert(std::make_pair(aOriginatingLayersId, std::move(ptr))); + testData = result.first->second.get(); + testData->StartNewPaint(aPaintSequenceNumber); + } + + TreeBuildingState state(mRootLayersId, aIsFirstPaint, aOriginatingLayersId, + testData, aPaintSequenceNumber); + + // We do this business with collecting the entire tree into an array because + // otherwise it's very hard to determine which APZC instances need to be + // destroyed. In the worst case, there are two scenarios: (a) a layer with an + // APZC is removed from the layer tree and (b) a layer with an APZC is moved + // in the layer tree from one place to a completely different place. In + // scenario (a) we would want to destroy the APZC while walking the layer tree + // and noticing that the layer/APZC is no longer there. But if we do that then + // we run into a problem in scenario (b) because we might encounter that layer + // later during the walk. To handle both of these we have to 'remember' that + // the layer was not found, and then do the destroy only at the end of the + // tree walk after we are sure that the layer was removed and not just + // transplanted elsewhere. Doing that as part of a recursive tree walk is hard + // and so maintaining a list and removing APZCs that are still alive is much + // simpler. + ForEachNode(mRootNode.get(), + [&state](HitTestingTreeNode* aNode) { + state.mNodesToDestroy.AppendElement(aNode); + }); + mRootNode = nullptr; + mAsyncZoomContainerSubtree = Nothing(); + int asyncZoomContainerNestingDepth = 0; + bool haveNestedAsyncZoomContainers = false; + nsTArray subtreesWithRootContentOutsideAsyncZoomContainer; + + if (aRoot) { + std::unordered_set seenLayersIds; + std::stack> indents; + std::stack ancestorTransforms; + HitTestingTreeNode* parent = nullptr; + HitTestingTreeNode* next = nullptr; + LayersId layersId = mRootLayersId; + seenLayersIds.insert(mRootLayersId); + ancestorTransforms.push(AncestorTransform()); + state.mOverrideFlags.push(EventRegionsOverride::NoOverride); + nsTArray> zoomConstraintsStack; + + // push a nothing to be used for anything outside an async zoom container + zoomConstraintsStack.AppendElement(Nothing()); + + mApzcTreeLog << "[start]\n"; + mTreeLock.AssertCurrentThreadIn(); + + ForEachNode( + aRoot, + [&](ScrollNode aLayerMetrics) { + if (auto asyncZoomContainerId = + aLayerMetrics.GetAsyncZoomContainerId()) { + if (asyncZoomContainerNestingDepth > 0) { + haveNestedAsyncZoomContainers = true; + } + mAsyncZoomContainerSubtree = Some(layersId); + ++asyncZoomContainerNestingDepth; + + auto it = mZoomConstraints.find( + ScrollableLayerGuid(layersId, 0, *asyncZoomContainerId)); + if (it != mZoomConstraints.end()) { + zoomConstraintsStack.AppendElement(Some(it->second)); + } else { + zoomConstraintsStack.AppendElement(Nothing()); + } + } + + if (aLayerMetrics.Metrics().IsRootContent()) { + MutexAutoLock lock(mMapLock); + mGeckoFixedLayerMargins = + aLayerMetrics.Metrics().GetFixedLayerMargins(); + } else { + MOZ_ASSERT(aLayerMetrics.Metrics().GetFixedLayerMargins() == + ScreenMargin(), + "fixed-layer-margins should be 0 on non-root layer"); + } + + // Note that this check happens after the potential increment of + // asyncZoomContainerNestingDepth, to allow the root content + // metadata to be on the same node as the async zoom container. + if (aLayerMetrics.Metrics().IsRootContent() && + asyncZoomContainerNestingDepth == 0) { + subtreesWithRootContentOutsideAsyncZoomContainer.AppendElement( + layersId); + } + + HitTestingTreeNode* node = PrepareNodeForLayer( + lock, aLayerMetrics, aLayerMetrics.Metrics(), layersId, + zoomConstraintsStack.LastElement(), ancestorTransforms.top(), + parent, next, state); + MOZ_ASSERT(node); + AsyncPanZoomController* apzc = node->GetApzc(); + aLayerMetrics.SetApzc(apzc); + + // GetScrollbarAnimationId is only set when webrender is enabled, + // which limits the extra thumb mapping work to the webrender-enabled + // case where it is needed. + // Note also that when webrender is enabled, a "valid" animation id + // is always nonzero, so we don't need to worry about handling the + // case where WR is enabled and the animation id is zero. + if (node->GetScrollbarAnimationId()) { + if (node->IsScrollThumbNode()) { + state.mScrollThumbs.push_back(node); + } else if (node->IsScrollbarContainerNode()) { + // Only scrollbar containers for the root have an animation id. + state.mRootScrollbarInfo.emplace_back( + *(node->GetScrollbarAnimationId()), + node->GetScrollbarDirection()); + } + } + + // GetFixedPositionAnimationId is only set when webrender is enabled. + if (node->GetFixedPositionAnimationId().isSome()) { + state.mFixedPositionInfo.emplace_back(node); + } + // GetStickyPositionAnimationId is only set when webrender is enabled. + if (node->GetStickyPositionAnimationId().isSome()) { + state.mStickyPositionInfo.emplace_back(node); + } + if (apzc && node->IsPrimaryHolder()) { + state.mScrollTargets[apzc->GetGuid()] = node; + } + + // Accumulate the CSS transform between layers that have an APZC. + // In the terminology of the big comment above + // APZCTreeManager::GetScreenToApzcTransform, if we are at layer M, + // then aAncestorTransform is NC * OC * PC, and we left-multiply MC + // and compute ancestorTransform to be MC * NC * OC * PC. This gets + // passed down as the ancestor transform to layer L when we recurse + // into the children below. If we are at a layer with an APZC, such as + // P, then we reset the ancestorTransform to just PC, to start the new + // accumulation as we go down. + AncestorTransform currentTransform{ + aLayerMetrics.GetTransform(), + aLayerMetrics.TransformIsPerspective()}; + if (!apzc) { + currentTransform = currentTransform * ancestorTransforms.top(); + } + ancestorTransforms.push(currentTransform); + + // Note that |node| at this point will not have any children, + // otherwise we we would have to set next to node->GetFirstChild(). + MOZ_ASSERT(!node->GetFirstChild()); + parent = node; + next = nullptr; + + // Update the layersId if we have a new one + if (Maybe newLayersId = aLayerMetrics.GetReferentId()) { + layersId = *newLayersId; + seenLayersIds.insert(layersId); + + // Propagate any event region override flags down into all + // descendant nodes from the reflayer that has the flag. This is an + // optimization to avoid having to walk up the tree to check the + // override flags. Note that we don't keep the flags on the reflayer + // itself, because the semantics of the flags are that they apply + // to all content in the layer subtree being referenced. This + // matters with the WR hit-test codepath, because this reflayer may + // be just one of many nodes associated with a particular APZC, and + // calling GetTargetNode with a guid may return any one of the + // nodes. If different nodes have different flags on them that can + // make the WR hit-test result incorrect, but being strict about + // only putting the flags on descendant layers avoids this problem. + state.mOverrideFlags.push(state.mOverrideFlags.top() | + aLayerMetrics.GetEventRegionsOverride()); + } + + indents.push(gfx::TreeAutoIndent(mApzcTreeLog)); + }, + [&](ScrollNode aLayerMetrics) { + if (aLayerMetrics.GetAsyncZoomContainerId()) { + --asyncZoomContainerNestingDepth; + zoomConstraintsStack.RemoveLastElement(); + } + if (aLayerMetrics.GetReferentId()) { + state.mOverrideFlags.pop(); + } + + next = parent; + parent = parent->GetParent(); + layersId = next->GetLayersId(); + ancestorTransforms.pop(); + indents.pop(); + }); + + mApzcTreeLog << "[end]\n"; + + MOZ_ASSERT( + !mAsyncZoomContainerSubtree || + !subtreesWithRootContentOutsideAsyncZoomContainer.Contains( + *mAsyncZoomContainerSubtree), + "If there is an async zoom container, all scroll nodes with root " + "content scroll metadata should be inside it"); + MOZ_ASSERT(!haveNestedAsyncZoomContainers, + "Should not have nested async zoom container"); + + // If we have perspective transforms deferred to children, do another + // walk of the tree and actually apply them to the children. + // We can't do this "as we go" in the previous traversal, because by the + // time we realize we need to defer a perspective transform for an APZC, + // we may already have processed a previous layer (including children + // found in its subtree) that shares that APZC. + if (!state.mPerspectiveTransformsDeferredToChildren.empty()) { + ForEachNode( + mRootNode.get(), [&state](HitTestingTreeNode* aNode) { + AsyncPanZoomController* apzc = aNode->GetApzc(); + if (!apzc) { + return; + } + if (!aNode->IsPrimaryHolder()) { + return; + } + + AsyncPanZoomController* parent = apzc->GetParent(); + if (!parent) { + return; + } + + auto it = + state.mPerspectiveTransformsDeferredToChildren.find(parent); + if (it != state.mPerspectiveTransformsDeferredToChildren.end()) { + apzc->SetAncestorTransform(AncestorTransform{ + it->second * apzc->GetAncestorTransform(), false}); + } + }); + } + + // Remove any layers ids for which we no longer have content from + // mDetachedLayersIds. + for (auto iter = mDetachedLayersIds.begin(); + iter != mDetachedLayersIds.end();) { + // unordered_set::erase() invalidates the iterator pointing to the + // element being erased, but returns an iterator to the next element. + if (seenLayersIds.find(*iter) == seenLayersIds.end()) { + iter = mDetachedLayersIds.erase(iter); + } else { + ++iter; + } + } + } + + // We do not support tree structures where the root node has siblings. + MOZ_ASSERT(!(mRootNode && mRootNode->GetPrevSibling())); + + { // scope lock and update our mApzcMap before we destroy all the unused + // APZC instances + MutexAutoLock lock(mMapLock); + mApzcMap = std::move(state.mApzcMap); + + for (auto& mapping : mApzcMap) { + AsyncPanZoomController* parent = mapping.second.apzc->GetParent(); + mapping.second.parent = parent ? Some(parent->GetGuid()) : Nothing(); + } + + mScrollThumbInfo.clear(); + // For non-webrender, state.mScrollThumbs will be empty so this will be a + // no-op. + for (HitTestingTreeNode* thumb : state.mScrollThumbs) { + MOZ_ASSERT(thumb->IsScrollThumbNode()); + ScrollableLayerGuid targetGuid(thumb->GetLayersId(), 0, + thumb->GetScrollTargetId()); + auto it = state.mScrollTargets.find(targetGuid); + if (it == state.mScrollTargets.end()) { + // It could be that |thumb| is a scrollthumb for content which didn't + // have an APZC, for example if the content isn't layerized. Regardless, + // we can't async-scroll it so we don't need to worry about putting it + // in mScrollThumbInfo. + continue; + } + HitTestingTreeNode* target = it->second; + mScrollThumbInfo.emplace_back( + *(thumb->GetScrollbarAnimationId()), thumb->GetTransform(), + thumb->GetScrollbarData(), targetGuid, target->GetTransform(), + target->IsAncestorOf(thumb)); + } + + mRootScrollbarInfo = std::move(state.mRootScrollbarInfo); + mFixedPositionInfo = std::move(state.mFixedPositionInfo); + mStickyPositionInfo = std::move(state.mStickyPositionInfo); + } + + for (size_t i = 0; i < state.mNodesToDestroy.Length(); i++) { + APZCTM_LOG("Destroying node at %p with APZC %p\n", + state.mNodesToDestroy[i].get(), + state.mNodesToDestroy[i]->GetApzc()); + state.mNodesToDestroy[i]->Destroy(); + } + + APZCTM_LOG("APZCTreeManager (%p)\n", this); + if (mRootNode && MOZ_LOG_TEST(sLog, LogLevel::Debug)) { + mRootNode->Dump(" "); + } + SendSubtreeTransformsToChromeMainThread(nullptr); +} + +void APZCTreeManager::UpdateFocusState(LayersId aRootLayerTreeId, + LayersId aOriginatingLayersId, + const FocusTarget& aFocusTarget) { + AssertOnUpdaterThread(); + + if (!StaticPrefs::apz_keyboard_enabled_AtStartup()) { + return; + } + + mFocusState.Update(aRootLayerTreeId, aOriginatingLayersId, aFocusTarget); +} + +void APZCTreeManager::SampleForWebRender(const Maybe& aVsyncId, + wr::TransactionWrapper& aTxn, + const SampleTime& aSampleTime) { + AssertOnSamplerThread(); + MutexAutoLock lock(mMapLock); + + RefPtr wrBridgeParent; + RefPtr controller; + CompositorBridgeParent::CallWithIndirectShadowTree( + mRootLayersId, [&](LayerTreeState& aState) -> void { + controller = aState.GetCompositorController(); + wrBridgeParent = aState.mWrBridge; + }); + + bool activeAnimations = AdvanceAnimationsInternal(lock, aSampleTime); + if (activeAnimations && controller) { + controller->ScheduleRenderOnCompositorThread( + wr::RenderReasons::ANIMATED_PROPERTY); + } + + nsTArray transforms; + + // Sample async transforms on scrollable layers. + for (const auto& mapping : mApzcMap) { + AsyncPanZoomController* apzc = mapping.second.apzc; + + if (Maybe payload = apzc->NotifyScrollSampling()) { + if (wrBridgeParent && aVsyncId) { + wrBridgeParent->AddPendingScrollPayload(*payload, *aVsyncId); + } + } + + if (StaticPrefs::apz_test_logging_enabled()) { + MutexAutoLock lock(mTestDataLock); + + ScrollableLayerGuid guid = apzc->GetGuid(); + auto it = mTestData.find(guid.mLayersId); + if (it != mTestData.end()) { + it->second->RecordSampledResult( + apzc->GetCurrentAsyncVisualViewport( + AsyncPanZoomController::eForCompositing) + .TopLeft(), + (aSampleTime.Time() - TimeStamp::ProcessCreation()) + .ToMicroseconds(), + guid.mLayersId, guid.mScrollId); + } + } + + if (Maybe zoomAnimationId = apzc->GetZoomAnimationId()) { + // for now we only support zooming on root content APZCs + MOZ_ASSERT(apzc->IsRootContent()); + + LayoutDeviceToParentLayerScale zoom = apzc->GetCurrentPinchZoomScale( + AsyncPanZoomController::eForCompositing); + + AsyncTransform asyncVisualTransform = apzc->GetCurrentAsyncTransform( + AsyncPanZoomController::eForCompositing, + AsyncTransformComponents{AsyncTransformComponent::eVisual}); + + transforms.AppendElement(wr::ToWrTransformProperty( + *zoomAnimationId, LayoutDeviceToParentLayerMatrix4x4::Scaling( + zoom.scale, zoom.scale, 1.0f) * + AsyncTransformComponentMatrix::Translation( + asyncVisualTransform.mTranslation))); + + aTxn.UpdateIsTransformAsyncZooming(*zoomAnimationId, + apzc->IsAsyncZooming()); + } + + nsTArray sampledOffsets = + apzc->GetSampledScrollOffsets(); + aTxn.UpdateScrollPosition(wr::AsPipelineId(apzc->GetGuid().mLayersId), + apzc->GetGuid().mScrollId, sampledOffsets); + +#if defined(MOZ_WIDGET_ANDROID) + // Send the root frame metrics to java through the UIController + RefPtr uiController = + UiCompositorControllerParent::GetFromRootLayerTreeId(mRootLayersId); + if (uiController && + apzc->UpdateRootFrameMetricsIfChanged(mLastRootMetrics)) { + uiController->NotifyUpdateScreenMetrics(mLastRootMetrics); + } +#endif + } + + // Now collect all the async transforms needed for the scrollthumbs. + for (const ScrollThumbInfo& info : mScrollThumbInfo) { + auto it = mApzcMap.find(info.mTargetGuid); + if (it == mApzcMap.end()) { + // It could be that |info| is a scrollthumb for content which didn't + // have an APZC, for example if the content isn't layerized. Regardless, + // we can't async-scroll it so we don't need to worry about putting it + // in mScrollThumbInfo. + continue; + } + AsyncPanZoomController* scrollTargetApzc = it->second.apzc; + MOZ_ASSERT(scrollTargetApzc); + LayerToParentLayerMatrix4x4 transform = + scrollTargetApzc->CallWithLastContentPaintMetrics( + [&](const FrameMetrics& aMetrics) { + return ComputeTransformForScrollThumb( + info.mThumbTransform * AsyncTransformMatrix(), + info.mTargetTransform.ToUnknownMatrix(), scrollTargetApzc, + aMetrics, info.mThumbData, info.mTargetIsAncestor); + }); + transforms.AppendElement( + wr::ToWrTransformProperty(info.mThumbAnimationId, transform)); + } + + // Move the root scrollbar in response to the dynamic toolbar transition. + for (const RootScrollbarInfo& info : mRootScrollbarInfo) { + // We only care about the horizontal scrollbar. + if (info.mScrollDirection == ScrollDirection::eHorizontal) { + ScreenPoint translation = + apz::ComputeFixedMarginsOffset(GetCompositorFixedLayerMargins(lock), + SideBits::eBottom, ScreenMargin()); + + LayerToParentLayerMatrix4x4 transform = + LayerToParentLayerMatrix4x4::Translation(ViewAs( + translation, PixelCastJustification::ScreenIsParentLayerForRoot)); + + transforms.AppendElement( + wr::ToWrTransformProperty(info.mScrollbarAnimationId, transform)); + } + } + + for (const FixedPositionInfo& info : mFixedPositionInfo) { + MOZ_ASSERT(info.mFixedPositionAnimationId.isSome()); + if (!IsFixedToRootContent(info, lock)) { + continue; + } + + ScreenPoint translation = apz::ComputeFixedMarginsOffset( + GetCompositorFixedLayerMargins(lock), info.mFixedPosSides, + mGeckoFixedLayerMargins); + + LayerToParentLayerMatrix4x4 transform = + LayerToParentLayerMatrix4x4::Translation(ViewAs( + translation, PixelCastJustification::ScreenIsParentLayerForRoot)); + + transforms.AppendElement( + wr::ToWrTransformProperty(*info.mFixedPositionAnimationId, transform)); + } + + for (const StickyPositionInfo& info : mStickyPositionInfo) { + MOZ_ASSERT(info.mStickyPositionAnimationId.isSome()); + SideBits sides = SidesStuckToRootContent(info, lock); + if (sides == SideBits::eNone) { + continue; + } + + ScreenPoint translation = apz::ComputeFixedMarginsOffset( + GetCompositorFixedLayerMargins(lock), sides, + // For sticky layers, we don't need to factor + // mGeckoFixedLayerMargins because Gecko doesn't shift the + // position of sticky elements for dynamic toolbar movements. + ScreenMargin()); + + LayerToParentLayerMatrix4x4 transform = + LayerToParentLayerMatrix4x4::Translation(ViewAs( + translation, PixelCastJustification::ScreenIsParentLayerForRoot)); + + transforms.AppendElement( + wr::ToWrTransformProperty(*info.mStickyPositionAnimationId, transform)); + } + + aTxn.AppendTransformProperties(transforms); +} + +ParentLayerRect APZCTreeManager::ComputeClippedCompositionBounds( + const MutexAutoLock& aProofOfMapLock, ClippedCompositionBoundsMap& aDestMap, + ScrollableLayerGuid aGuid) { + if (auto iter = aDestMap.find(aGuid); iter != aDestMap.end()) { + // We already computed it for this one, early-exit. This might happen + // because on a later iteration of mApzcMap we might encounter an ancestor + // of an APZC that we processed on an earlier iteration. In this case we + // would have computed the ancestor's clipped composition bounds when + // recursing up on the earlier iteration. + return iter->second; + } + + ParentLayerRect bounds = mApzcMap[aGuid].apzc->GetCompositionBounds(); + const auto& mapEntry = mApzcMap.find(aGuid); + MOZ_ASSERT(mapEntry != mApzcMap.end()); + if (mapEntry->second.parent.isNothing()) { + // Recursion base case, where the APZC with guid `aGuid` has no parent. + // In this case, we don't need to clip `bounds` any further and can just + // early exit. + aDestMap.emplace(aGuid, bounds); + return bounds; + } + + ScrollableLayerGuid parentGuid = mapEntry->second.parent.value(); + auto parentBoundsEntry = aDestMap.find(parentGuid); + // If aDestMap doesn't contain the parent entry yet, we recurse to compute + // that one first. + ParentLayerRect parentClippedBounds = + (parentBoundsEntry == aDestMap.end()) + ? ComputeClippedCompositionBounds(aProofOfMapLock, aDestMap, + parentGuid) + : parentBoundsEntry->second; + + // The parent layer's async transform applies to the current layer to take + // `bounds` into the same coordinate space as `parentClippedBounds`. However, + // we're going to do the inverse operation and unapply this transform to + // `parentClippedBounds` to bring it into the same coordinate space as + // `bounds`. + AsyncTransform appliesToLayer = + mApzcMap[parentGuid].apzc->GetCurrentAsyncTransform( + AsyncPanZoomController::eForCompositing); + + // Do the unapplication + LayerRect parentClippedBoundsInParentLayerSpace = + (parentClippedBounds - appliesToLayer.mTranslation) / + appliesToLayer.mScale; + + // And then clip `bounds` by the parent's comp bounds in the current space. + bounds = bounds.Intersect( + ViewAs(parentClippedBoundsInParentLayerSpace, + PixelCastJustification::MovingDownToChildren)); + + // Done! + aDestMap.emplace(aGuid, bounds); + return bounds; +} + +bool APZCTreeManager::AdvanceAnimationsInternal( + const MutexAutoLock& aProofOfMapLock, const SampleTime& aSampleTime) { + ClippedCompositionBoundsMap clippedCompBounds; + bool activeAnimations = false; + for (const auto& mapping : mApzcMap) { + AsyncPanZoomController* apzc = mapping.second.apzc; + // Note that this call is recursive, but it early-exits if called again + // with the same guid. So this loop is still amortized O(n) with respect to + // the number of APZCs. + ParentLayerRect clippedBounds = ComputeClippedCompositionBounds( + aProofOfMapLock, clippedCompBounds, mapping.first); + + apzc->ReportCheckerboard(aSampleTime, clippedBounds); + activeAnimations |= apzc->AdvanceAnimations(aSampleTime); + } + return activeAnimations; +} + +void APZCTreeManager::PrintLayerInfo(const ScrollNode& aLayer) { + if (StaticPrefs::apz_printtree() && aLayer.Dump(mApzcTreeLog) > 0) { + mApzcTreeLog << "\n"; + } +} + +// mTreeLock is held, and checked with static analysis +void APZCTreeManager::AttachNodeToTree(HitTestingTreeNode* aNode, + HitTestingTreeNode* aParent, + HitTestingTreeNode* aNextSibling) { + if (aNextSibling) { + aNextSibling->SetPrevSibling(aNode); + } else if (aParent) { + aParent->SetLastChild(aNode); + } else { + MOZ_ASSERT(!mRootNode); + mRootNode = aNode; + aNode->MakeRoot(); + } +} + +already_AddRefed APZCTreeManager::RecycleOrCreateNode( + const RecursiveMutexAutoLock& aProofOfTreeLock, TreeBuildingState& aState, + AsyncPanZoomController* aApzc, LayersId aLayersId) { + // Find a node without an APZC and return it. Note that unless the layer tree + // actually changes, this loop should generally do an early-return on the + // first iteration, so it should be cheap in the common case. + for (int32_t i = aState.mNodesToDestroy.Length() - 1; i >= 0; i--) { + RefPtr node = aState.mNodesToDestroy[i]; + if (node->IsRecyclable(aProofOfTreeLock)) { + aState.mNodesToDestroy.RemoveElementAt(i); + node->RecycleWith(aProofOfTreeLock, aApzc, aLayersId); + return node.forget(); + } + } + RefPtr node = + new HitTestingTreeNode(aApzc, false, aLayersId); + return node.forget(); +} + +void APZCTreeManager::StartScrollbarDrag(const ScrollableLayerGuid& aGuid, + const AsyncDragMetrics& aDragMetrics) { + if (!APZThreadUtils::IsControllerThread()) { + APZThreadUtils::RunOnControllerThread( + NewRunnableMethod( + "layers::APZCTreeManager::StartScrollbarDrag", this, + &APZCTreeManager::StartScrollbarDrag, aGuid, aDragMetrics)); + return; + } + + APZThreadUtils::AssertOnControllerThread(); + + RefPtr apzc = GetTargetAPZC(aGuid); + if (!apzc) { + NotifyScrollbarDragRejected(aGuid); + return; + } + + uint64_t inputBlockId = aDragMetrics.mDragStartSequenceNumber; + mInputQueue->ConfirmDragBlock(inputBlockId, apzc, aDragMetrics); +} + +bool APZCTreeManager::StartAutoscroll(const ScrollableLayerGuid& aGuid, + const ScreenPoint& aAnchorLocation) { + APZThreadUtils::AssertOnControllerThread(); + + RefPtr apzc = GetTargetAPZC(aGuid); + if (!apzc) { + if (XRE_IsGPUProcess()) { + // If we're in the compositor process, the "return false" will be + // ignored because the query comes over the PAPZCTreeManager protocol + // via an async message. In this case, send an explicit rejection + // message to content. + NotifyAutoscrollRejected(aGuid); + } + return false; + } + + apzc->StartAutoscroll(aAnchorLocation); + return true; +} + +void APZCTreeManager::StopAutoscroll(const ScrollableLayerGuid& aGuid) { + APZThreadUtils::AssertOnControllerThread(); + + if (RefPtr apzc = GetTargetAPZC(aGuid)) { + apzc->StopAutoscroll(); + } +} + +void APZCTreeManager::NotifyScrollbarDragInitiated( + uint64_t aDragBlockId, const ScrollableLayerGuid& aGuid, + ScrollDirection aDirection) const { + RefPtr controller = + GetContentController(aGuid.mLayersId); + if (controller) { + controller->NotifyAsyncScrollbarDragInitiated(aDragBlockId, aGuid.mScrollId, + aDirection); + } +} + +void APZCTreeManager::NotifyScrollbarDragRejected( + const ScrollableLayerGuid& aGuid) const { + RefPtr controller = + GetContentController(aGuid.mLayersId); + if (controller) { + controller->NotifyAsyncScrollbarDragRejected(aGuid.mScrollId); + } +} + +void APZCTreeManager::NotifyAutoscrollRejected( + const ScrollableLayerGuid& aGuid) const { + RefPtr controller = + GetContentController(aGuid.mLayersId); + MOZ_ASSERT(controller); + controller->NotifyAsyncAutoscrollRejected(aGuid.mScrollId); +} + +void SetHitTestData(HitTestingTreeNode* aNode, + const WebRenderScrollDataWrapper& aLayer, + const EventRegionsOverride& aOverrideFlags) { + aNode->SetHitTestData(aLayer.GetVisibleRegion(), + aLayer.GetRemoteDocumentSize(), + aLayer.GetTransformTyped(), aOverrideFlags, + aLayer.GetAsyncZoomContainerId()); +} + +HitTestingTreeNode* APZCTreeManager::PrepareNodeForLayer( + const RecursiveMutexAutoLock& aProofOfTreeLock, const ScrollNode& aLayer, + const FrameMetrics& aMetrics, LayersId aLayersId, + const Maybe& aZoomConstraints, + const AncestorTransform& aAncestorTransform, HitTestingTreeNode* aParent, + HitTestingTreeNode* aNextSibling, TreeBuildingState& aState) { + mTreeLock.AssertCurrentThreadIn(); // for static analysis + bool needsApzc = true; + if (!aMetrics.IsScrollable()) { + needsApzc = false; + } + + // XXX: As a future optimization we can probably stick these things on the + // TreeBuildingState, and update them as we change layers id during the + // traversal + RefPtr geckoContentController; + CompositorBridgeParent::CallWithIndirectShadowTree( + aLayersId, [&](LayerTreeState& lts) -> void { + geckoContentController = lts.mController; + }); + + if (!geckoContentController) { + needsApzc = false; + } + + if (Maybe zoomAnimationId = aLayer.GetZoomAnimationId()) { + aState.mZoomAnimationId = zoomAnimationId; + } + + RefPtr node = nullptr; + if (!needsApzc) { + // Note: if layer properties must be propagated to nodes, RecvUpdate in + // LayerTransactionParent.cpp must ensure that APZ will be notified + // when those properties change. + node = RecycleOrCreateNode(aProofOfTreeLock, aState, nullptr, aLayersId); + AttachNodeToTree(node, aParent, aNextSibling); + SetHitTestData(node, aLayer, aState.mOverrideFlags.top()); + node->SetScrollbarData(aLayer.GetScrollbarAnimationId(), + aLayer.GetScrollbarData()); + node->SetFixedPosData(aLayer.GetFixedPositionScrollContainerId(), + aLayer.GetFixedPositionSides(), + aLayer.GetFixedPositionAnimationId()); + node->SetStickyPosData(aLayer.GetStickyScrollContainerId(), + aLayer.GetStickyScrollRangeOuter(), + aLayer.GetStickyScrollRangeInner(), + aLayer.GetStickyPositionAnimationId()); + PrintLayerInfo(aLayer); + return node; + } + + AsyncPanZoomController* apzc = nullptr; + // If we get here, aLayer is a scrollable layer and somebody + // has registered a GeckoContentController for it, so we need to ensure + // it has an APZC instance to manage its scrolling. + + // aState.mApzcMap allows reusing the exact same APZC instance for different + // layers with the same FrameMetrics data. This is needed because in some + // cases content that is supposed to scroll together is split into multiple + // layers because of e.g. non-scrolling content interleaved in z-index order. + ScrollableLayerGuid guid(aLayersId, aMetrics.GetPresShellId(), + aMetrics.GetScrollId()); + auto insertResult = aState.mApzcMap.insert(std::make_pair( + guid, + ApzcMapData{static_cast(nullptr), Nothing()})); + if (!insertResult.second) { + apzc = insertResult.first->second.apzc; + PrintLayerInfo(aLayer); + } + APZCTM_LOG("Found APZC %p for layer %p with identifiers %" PRIx64 " %" PRId64 + "\n", + apzc, aLayer.GetLayer(), uint64_t(guid.mLayersId), guid.mScrollId); + + // If we haven't encountered a layer already with the same metrics, then we + // need to do the full reuse-or-make-an-APZC algorithm, which is contained + // inside the block below. + if (apzc == nullptr) { + apzc = aLayer.GetApzc(); + + // If the content represented by the scrollable layer has changed (which may + // be possible because of DLBI heuristics) then we don't want to keep using + // the same old APZC for the new content. Also, when reparenting a tab into + // a new window a layer might get moved to a different layer tree with a + // different APZCTreeManager. In these cases we don't want to reuse the same + // APZC, so null it out so we run through the code to find another one or + // create one. + if (apzc && (!apzc->Matches(guid) || !apzc->HasTreeManager(this))) { + apzc = nullptr; + } + + // See if we can find an APZC from the previous tree that matches the + // ScrollableLayerGuid from this layer. If there is one, then we know that + // the layout of the page changed causing the layer tree to be rebuilt, but + // the underlying content for the APZC is still there somewhere. Therefore, + // we want to find the APZC instance and continue using it here. + // + // We particularly want to find the primary-holder node from the previous + // tree that matches, because we don't want that node to get destroyed. If + // it does get destroyed, then the APZC will get destroyed along with it by + // definition, but we want to keep that APZC around in the new tree. + // We leave non-primary-holder nodes in the destroy list because we don't + // care about those nodes getting destroyed. + for (size_t i = 0; i < aState.mNodesToDestroy.Length(); i++) { + RefPtr n = aState.mNodesToDestroy[i]; + if (n->IsPrimaryHolder() && n->GetApzc() && n->GetApzc()->Matches(guid)) { + node = n; + if (apzc != nullptr) { + // If there is an APZC already then it should match the one from the + // old primary-holder node + MOZ_ASSERT(apzc == node->GetApzc()); + } + apzc = node->GetApzc(); + break; + } + } + + // The APZC we get off the layer may have been destroyed previously if the + // layer was inactive or omitted from the layer tree for whatever reason + // from a layers update. If it later comes back it will have a reference to + // a destroyed APZC and so we need to throw that out and make a new one. + bool newApzc = (apzc == nullptr || apzc->IsDestroyed()); + if (newApzc) { + apzc = NewAPZCInstance(aLayersId, geckoContentController); + apzc->SetCompositorController(aState.mCompositorController.get()); + MOZ_ASSERT(node == nullptr); + node = new HitTestingTreeNode(apzc, true, aLayersId); + } else { + // If we are re-using a node for this layer clear the tree pointers + // so that it doesn't continue pointing to nodes that might no longer + // be in the tree. These pointers will get reset properly as we continue + // building the tree. Also remove it from the set of nodes that are going + // to be destroyed, because it's going to remain active. + aState.mNodesToDestroy.RemoveElement(node); + node->SetPrevSibling(nullptr); + node->SetLastChild(nullptr); + } + + if (aMetrics.IsRootContent()) { + apzc->SetZoomAnimationId(aState.mZoomAnimationId); + aState.mZoomAnimationId = Nothing(); + } + + APZCTM_LOG( + "Using APZC %p for layer %p with identifiers %" PRIx64 " %" PRId64 "\n", + apzc, aLayer.GetLayer(), uint64_t(aLayersId), aMetrics.GetScrollId()); + + apzc->NotifyLayersUpdated(aLayer.Metadata(), aState.mIsFirstPaint, + aLayersId == aState.mOriginatingLayersId); + + // Since this is the first time we are encountering an APZC with this guid, + // the node holding it must be the primary holder. It may be newly-created + // or not, depending on whether it went through the newApzc branch above. + MOZ_ASSERT(node->IsPrimaryHolder() && node->GetApzc() && + node->GetApzc()->Matches(guid)); + + SetHitTestData(node, aLayer, aState.mOverrideFlags.top()); + apzc->SetAncestorTransform(aAncestorTransform); + + PrintLayerInfo(aLayer); + + // Bind the APZC instance into the tree of APZCs + AttachNodeToTree(node, aParent, aNextSibling); + + // For testing, log the parent scroll id of every APZC that has a + // parent. This allows test code to reconstruct the APZC tree. + // Note that we currently only do this for APZCs in the layer tree + // that originated the update, because the only identifying information + // we are logging about APZCs is the scroll id, and otherwise we could + // confuse APZCs from different layer trees with the same scroll id. + if (aLayersId == aState.mOriginatingLayersId) { + if (apzc->HasNoParentWithSameLayersId()) { + aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(), + "hasNoParentWithSameLayersId", true); + } else { + MOZ_ASSERT(apzc->GetParent()); + aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(), + "parentScrollId", + apzc->GetParent()->GetGuid().mScrollId); + } + if (aMetrics.IsRootContent()) { + aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(), "isRootContent", + true); + } + // Note that the async scroll offset is in ParentLayer pixels + aState.mPaintLogger.LogTestData( + aMetrics.GetScrollId(), "asyncScrollOffset", + apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForHitTesting)); + aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(), + "hasAsyncKeyScrolled", + apzc->TestHasAsyncKeyScrolled()); + } + + // We must update the zoom constraints even if the apzc isn't new because it + // might have moved. + if (node->IsPrimaryHolder()) { + if (aZoomConstraints) { + apzc->UpdateZoomConstraints(*aZoomConstraints); + +#ifdef DEBUG + auto it = mZoomConstraints.find(guid); + if (it != mZoomConstraints.end()) { + MOZ_ASSERT(it->second == *aZoomConstraints); + } + } else { + // We'd like to assert these things (if the first doesn't hold then at + // least the second) but neither are not true because xul root content + // gets zoomable zoom constraints, but which is not zoomable because it + // doesn't have a root scroll frame. + // clang-format off + // MOZ_ASSERT(mZoomConstraints.find(guid) == mZoomConstraints.end()); + // auto it = mZoomConstraints.find(guid); + // if (it != mZoomConstraints.end()) { + // MOZ_ASSERT(!it->second.mAllowZoom && !it->second.mAllowDoubleTapZoom); + // } + // clang-format on +#endif + } + } + + // Add a guid -> APZC mapping for the newly created APZC. + insertResult.first->second.apzc = apzc; + } else { + // We already built an APZC earlier in this tree walk, but we have another + // layer now that will also be using that APZC. The hit-test region on the + // APZC needs to be updated to deal with the new layer's hit region. + + node = RecycleOrCreateNode(aProofOfTreeLock, aState, apzc, aLayersId); + AttachNodeToTree(node, aParent, aNextSibling); + + // Even though different layers associated with a given APZC may be at + // different levels in the layer tree (e.g. one being an uncle of another), + // we require from Layout that the CSS transforms up to their common + // ancestor be roughly the same. There are cases in which the transforms + // are not exactly the same, for example if the parent is container layer + // for an opacity, and this container layer has a resolution-induced scale + // as its base transform and a prescale that is supposed to undo that scale. + // Due to floating point inaccuracies those transforms can end up not quite + // canceling each other. That's why we're using a fuzzy comparison here + // instead of an exact one. + // In addition, two ancestor transforms are allowed to differ if one of + // them contains a perspective transform component and the other does not. + // This represents situations where some content in a scrollable frame + // is subject to a perspective transform and other content does not. + // In such cases, go with the one that does not include the perspective + // component; the perspective transform is remembered and applied to the + // children instead. + auto ancestorTransform = aAncestorTransform.CombinedTransform(); + auto existingAncestorTransform = apzc->GetAncestorTransform(); + if (!ancestorTransform.FuzzyEqualsMultiplicative( + existingAncestorTransform)) { + typedef TreeBuildingState::DeferredTransformMap::value_type PairType; + if (!aAncestorTransform.ContainsPerspectiveTransform() && + !apzc->AncestorTransformContainsPerspective()) { + // If this content is being presented in a paginated fashion (e.g. + // print preview), the multiple layers may reflect multiple instances + // of the same display item being rendered on different pages. In such + // cases, it's expected that different instances can have different + // transforms, since each page renders a different part of the item. + if (!aLayer.Metadata().IsPaginatedPresentation()) { + if (ancestorTransform.IsFinite() && + existingAncestorTransform.IsFinite()) { + MOZ_ASSERT( + false, + "Two layers that scroll together have different ancestor " + "transforms"); + } else { + MOZ_ASSERT(ancestorTransform.IsFinite() == + existingAncestorTransform.IsFinite()); + } + } + } else if (!aAncestorTransform.ContainsPerspectiveTransform()) { + aState.mPerspectiveTransformsDeferredToChildren.insert( + PairType{apzc, apzc->GetAncestorTransformPerspective()}); + apzc->SetAncestorTransform(aAncestorTransform); + } else { + aState.mPerspectiveTransformsDeferredToChildren.insert( + PairType{apzc, aAncestorTransform.GetPerspectiveTransform()}); + } + } + + SetHitTestData(node, aLayer, aState.mOverrideFlags.top()); + } + + // Note: if layer properties must be propagated to nodes, RecvUpdate in + // LayerTransactionParent.cpp must ensure that APZ will be notified + // when those properties change. + node->SetScrollbarData(aLayer.GetScrollbarAnimationId(), + aLayer.GetScrollbarData()); + node->SetFixedPosData(aLayer.GetFixedPositionScrollContainerId(), + aLayer.GetFixedPositionSides(), + aLayer.GetFixedPositionAnimationId()); + node->SetStickyPosData(aLayer.GetStickyScrollContainerId(), + aLayer.GetStickyScrollRangeOuter(), + aLayer.GetStickyScrollRangeInner(), + aLayer.GetStickyPositionAnimationId()); + return node; +} + +template +static bool WillHandleInput(const PanGestureOrScrollWheelInput& aPanInput) { + if (!XRE_IsParentProcess() || !NS_IsMainThread()) { + return true; + } + + WidgetWheelEvent wheelEvent = aPanInput.ToWidgetEvent(nullptr); + return APZInputBridge::ActionForWheelEvent(&wheelEvent).isSome(); +} + +/*static*/ +void APZCTreeManager::FlushApzRepaints(LayersId aLayersId) { + // Previously, paints were throttled and therefore this method was used to + // ensure any pending paints were flushed. Now, paints are flushed + // immediately, so it is safe to simply send a notification now. + APZCTM_LOG("Flushing repaints for layers id 0x%" PRIx64 "\n", + uint64_t(aLayersId)); + RefPtr controller = GetContentController(aLayersId); +#ifndef MOZ_WIDGET_ANDROID + // On Android, this code is run in production and may actually get a nullptr + // controller here. On other platforms this code is test-only and should never + // get a nullptr. + MOZ_ASSERT(controller); +#endif + if (controller) { + controller->DispatchToRepaintThread(NewRunnableMethod( + "layers::GeckoContentController::NotifyFlushComplete", controller, + &GeckoContentController::NotifyFlushComplete)); + } +} + +void APZCTreeManager::MarkAsDetached(LayersId aLayersId) { + RecursiveMutexAutoLock lock(mTreeLock); + mDetachedLayersIds.insert(aLayersId); +} + +static bool HasNonLockModifier(Modifiers aModifiers) { + return (aModifiers & (MODIFIER_ALT | MODIFIER_ALTGRAPH | MODIFIER_CONTROL | + MODIFIER_FN | MODIFIER_META | MODIFIER_SHIFT | + MODIFIER_SYMBOL | MODIFIER_OS)) != 0; +} + +APZEventResult APZCTreeManager::ReceiveInputEvent( + InputData& aEvent, InputBlockCallback&& aCallback) { + APZThreadUtils::AssertOnControllerThread(); + InputHandlingState state{aEvent}; + + // Use a RAII class for updating the focus sequence number of this event + AutoFocusSequenceNumberSetter focusSetter(mFocusState, aEvent); + + switch (aEvent.mInputType) { + case MULTITOUCH_INPUT: { + MultiTouchInput& touchInput = aEvent.AsMultiTouchInput(); + ProcessTouchInput(state, touchInput); + break; + } + case MOUSE_INPUT: { + MouseInput& mouseInput = aEvent.AsMouseInput(); + mouseInput.mHandledByAPZ = true; + + SetCurrentMousePosition(mouseInput.mOrigin); + + bool startsDrag = DragTracker::StartsDrag(mouseInput); + if (startsDrag) { + // If this is the start of a drag we need to unambiguously know if it's + // going to land on a scrollbar or not. We can't apply an untransform + // here without knowing that, so we need to ensure the untransform is + // a no-op. + FlushRepaintsToClearScreenToGeckoTransform(); + } + + state.mHit = GetTargetAPZC(mouseInput.mOrigin); + bool hitScrollbar = (bool)state.mHit.mScrollbarNode; + + // When the mouse is outside the window we still want to handle dragging + // but we won't find an APZC. Fallback to root APZC then. + { // scope lock + RecursiveMutexAutoLock lock(mTreeLock); + if (!state.mHit.mTargetApzc && mRootNode) { + state.mHit.mTargetApzc = mRootNode->GetApzc(); + } + } + + if (state.mHit.mTargetApzc) { + if (StaticPrefs::apz_test_logging_enabled() && + mouseInput.mType == MouseInput::MOUSE_HITTEST) { + ScrollableLayerGuid guid = state.mHit.mTargetApzc->GetGuid(); + + MutexAutoLock lock(mTestDataLock); + auto it = mTestData.find(guid.mLayersId); + MOZ_ASSERT(it != mTestData.end()); + it->second->RecordHitResult(mouseInput.mOrigin, state.mHit.mHitResult, + guid.mLayersId, guid.mScrollId); + } + + TargetConfirmationFlags confFlags{state.mHit.mHitResult}; + state.mResult = mInputQueue->ReceiveInputEvent(state.mHit.mTargetApzc, + confFlags, mouseInput); + + // If we're starting an async scrollbar drag + bool apzDragEnabled = StaticPrefs::apz_drag_enabled(); + if (apzDragEnabled && startsDrag && state.mHit.mScrollbarNode && + state.mHit.mScrollbarNode->IsScrollThumbNode() && + state.mHit.mScrollbarNode->GetScrollbarData() + .mThumbIsAsyncDraggable) { + SetupScrollbarDrag(mouseInput, state.mHit.mScrollbarNode, + state.mHit.mTargetApzc.get()); + } + + if (state.mResult.GetStatus() == nsEventStatus_eConsumeDoDefault) { + // This input event is part of a drag block, so whether or not it is + // directed at a scrollbar depends on whether the drag block started + // on a scrollbar. + hitScrollbar = mInputQueue->IsDragOnScrollbar(hitScrollbar); + } + + if (!hitScrollbar) { + // The input was not targeted at a scrollbar, so we untransform it + // like we do for other content. Scrollbars are "special" because they + // have special handling in AsyncCompositionManager when resolution is + // applied. TODO: we should find a better way to deal with this. + ScreenToParentLayerMatrix4x4 transformToApzc = + GetScreenToApzcTransform(state.mHit.mTargetApzc); + ParentLayerToScreenMatrix4x4 transformToGecko = + GetApzcToGeckoTransformForHit(state.mHit); + ScreenToScreenMatrix4x4 outTransform = + transformToApzc * transformToGecko; + Maybe untransformedRefPoint = + UntransformBy(outTransform, mouseInput.mOrigin); + if (untransformedRefPoint) { + mouseInput.mOrigin = *untransformedRefPoint; + } + } else { + // Likewise, if the input was targeted at a scrollbar, we don't want + // to apply the callback transform in the main thread, so we remove + // the scrollid from the guid. We need to keep the layersId intact so + // that the response from the child process doesn't get discarded. + state.mResult.mTargetGuid.mScrollId = + ScrollableLayerGuid::NULL_SCROLL_ID; + } + } + break; + } + case SCROLLWHEEL_INPUT: { + FlushRepaintsToClearScreenToGeckoTransform(); + + // Do this before early return for Fission hit testing. + ScrollWheelInput& wheelInput = aEvent.AsScrollWheelInput(); + state.mHit = GetTargetAPZC(wheelInput.mOrigin); + + wheelInput.mHandledByAPZ = WillHandleInput(wheelInput); + if (!wheelInput.mHandledByAPZ) { + return state.Finish(*this, std::move(aCallback)); + } + + if (state.mHit.mTargetApzc) { + MOZ_ASSERT(state.mHit.mHitResult != CompositorHitTestInvisibleToHit); + + if (wheelInput.mAPZAction == APZWheelAction::PinchZoom) { + // The mousewheel may have hit a subframe, but we want to send the + // pinch-zoom events to the root-content APZC. + { + RecursiveMutexAutoLock lock(mTreeLock); + state.mHit.mTargetApzc = FindRootContentApzcForLayersId( + state.mHit.mTargetApzc->GetLayersId()); + } + if (state.mHit.mTargetApzc) { + SynthesizePinchGestureFromMouseWheel(wheelInput, + state.mHit.mTargetApzc); + } + state.mResult.SetStatusAsConsumeNoDefault(); + return state.Finish(*this, std::move(aCallback)); + } + + MOZ_ASSERT(wheelInput.mAPZAction == APZWheelAction::Scroll); + + // For wheel events, the call to ReceiveInputEvent below may result in + // scrolling, which changes the async transform. However, the event we + // want to pass to gecko should be the pre-scroll event coordinates, + // transformed into the gecko space. (pre-scroll because the mouse + // cursor is stationary during wheel scrolling, unlike touchmove + // events). Since we just flushed the pending repaints the transform to + // gecko space should only consist of overscroll-cancelling transforms. + ScreenToScreenMatrix4x4 transformToGecko = + GetScreenToApzcTransform(state.mHit.mTargetApzc) * + GetApzcToGeckoTransformForHit(state.mHit); + Maybe untransformedOrigin = + UntransformBy(transformToGecko, wheelInput.mOrigin); + + if (!untransformedOrigin) { + return state.Finish(*this, std::move(aCallback)); + } + + state.mResult = mInputQueue->ReceiveInputEvent( + state.mHit.mTargetApzc, + TargetConfirmationFlags{state.mHit.mHitResult}, wheelInput); + + // Update the out-parameters so they are what the caller expects. + wheelInput.mOrigin = *untransformedOrigin; + } + break; + } + case PANGESTURE_INPUT: { + FlushRepaintsToClearScreenToGeckoTransform(); + + // Do this before early return for Fission hit testing. + PanGestureInput& panInput = aEvent.AsPanGestureInput(); + state.mHit = GetTargetAPZC(panInput.mPanStartPoint); + + panInput.mHandledByAPZ = WillHandleInput(panInput); + if (!panInput.mHandledByAPZ) { + if (mInputQueue->GetCurrentPanGestureBlock()) { + if (state.mHit.mTargetApzc && + (panInput.mType == PanGestureInput::PANGESTURE_END || + panInput.mType == PanGestureInput::PANGESTURE_CANCELLED)) { + // If we've already been processing a pan gesture in an APZC but + // fall into this _if_ branch, which means this pan-end or + // pan-cancelled event will not be proccessed in the APZC, send a + // pan-interrupted event to stop any on-going work for the pan + // gesture, otherwise we will get stuck at an intermidiate state + // becasue we might no longer receive any events which will be + // handled by the APZC. + PanGestureInput panInterrupted( + PanGestureInput::PANGESTURE_INTERRUPTED, panInput.mTimeStamp, + panInput.mPanStartPoint, panInput.mPanDisplacement, + panInput.modifiers); + Unused << mInputQueue->ReceiveInputEvent( + state.mHit.mTargetApzc, + TargetConfirmationFlags{state.mHit.mHitResult}, panInterrupted); + } + } + return state.Finish(*this, std::move(aCallback)); + } + + // If/when we enable support for pan inputs off-main-thread, we'll need + // to duplicate this EventStateManager code or something. See the call to + // GetUserPrefsForWheelEvent in APZInputBridge.cpp for why these fields + // are stored separately. + MOZ_ASSERT(NS_IsMainThread()); + WidgetWheelEvent wheelEvent = panInput.ToWidgetEvent(nullptr); + EventStateManager::GetUserPrefsForWheelEvent( + &wheelEvent, &panInput.mUserDeltaMultiplierX, + &panInput.mUserDeltaMultiplierY); + + if (state.mHit.mTargetApzc) { + MOZ_ASSERT(state.mHit.mHitResult != CompositorHitTestInvisibleToHit); + + // For pan gesture events, the call to ReceiveInputEvent below may + // result in scrolling, which changes the async transform. However, the + // event we want to pass to gecko should be the pre-scroll event + // coordinates, transformed into the gecko space. (pre-scroll because + // the mouse cursor is stationary during pan gesture scrolling, unlike + // touchmove events). Since we just flushed the pending repaints the + // transform to gecko space should only consist of overscroll-cancelling + // transforms. + ScreenToScreenMatrix4x4 transformToGecko = + GetScreenToApzcTransform(state.mHit.mTargetApzc) * + GetApzcToGeckoTransformForHit(state.mHit); + Maybe untransformedStartPoint = + UntransformBy(transformToGecko, panInput.mPanStartPoint); + Maybe untransformedDisplacement = + UntransformVector(transformToGecko, panInput.mPanDisplacement, + panInput.mPanStartPoint); + + if (!untransformedStartPoint || !untransformedDisplacement) { + return state.Finish(*this, std::move(aCallback)); + } + + panInput.mOverscrollBehaviorAllowsSwipe = + state.mHit.mTargetApzc->OverscrollBehaviorAllowsSwipe(); + + state.mResult = mInputQueue->ReceiveInputEvent( + state.mHit.mTargetApzc, + TargetConfirmationFlags{state.mHit.mHitResult}, panInput); + + // Update the out-parameters so they are what the caller expects. + panInput.mPanStartPoint = *untransformedStartPoint; + panInput.mPanDisplacement = *untransformedDisplacement; + } + break; + } + case PINCHGESTURE_INPUT: { + PinchGestureInput& pinchInput = aEvent.AsPinchGestureInput(); + if (HasNonLockModifier(pinchInput.modifiers)) { + APZCTM_LOG("Discarding pinch input due to modifiers 0x%x\n", + pinchInput.modifiers); + return state.Finish(*this, std::move(aCallback)); + } + + state.mHit = GetTargetAPZC(pinchInput.mFocusPoint); + + // We always handle pinch gestures as pinch zooms. + pinchInput.mHandledByAPZ = true; + + if (state.mHit.mTargetApzc) { + MOZ_ASSERT(state.mHit.mHitResult != CompositorHitTestInvisibleToHit); + + if (!state.mHit.mTargetApzc->IsRootContent()) { + state.mHit.mTargetApzc = FindZoomableApzc(state.mHit.mTargetApzc); + } + } + + if (state.mHit.mTargetApzc) { + ScreenToScreenMatrix4x4 outTransform = + GetScreenToApzcTransform(state.mHit.mTargetApzc) * + GetApzcToGeckoTransformForHit(state.mHit); + Maybe untransformedFocusPoint = + UntransformBy(outTransform, pinchInput.mFocusPoint); + + if (!untransformedFocusPoint) { + return state.Finish(*this, std::move(aCallback)); + } + + state.mResult = mInputQueue->ReceiveInputEvent( + state.mHit.mTargetApzc, + TargetConfirmationFlags{state.mHit.mHitResult}, pinchInput); + + // Update the out-parameters so they are what the caller expects. + pinchInput.mFocusPoint = *untransformedFocusPoint; + } + break; + } + case TAPGESTURE_INPUT: { // note: no one currently sends these + TapGestureInput& tapInput = aEvent.AsTapGestureInput(); + state.mHit = GetTargetAPZC(tapInput.mPoint); + + if (state.mHit.mTargetApzc) { + MOZ_ASSERT(state.mHit.mHitResult != CompositorHitTestInvisibleToHit); + + ScreenToScreenMatrix4x4 outTransform = + GetScreenToApzcTransform(state.mHit.mTargetApzc) * + GetApzcToGeckoTransformForHit(state.mHit); + Maybe untransformedPoint = + UntransformBy(outTransform, tapInput.mPoint); + + if (!untransformedPoint) { + return state.Finish(*this, std::move(aCallback)); + } + + // Tap gesture events are not grouped into input blocks, and they're + // never queued in InputQueue, but processed right away. So, we only + // need to set |mTapGestureHitResult| for the duration of the + // InputQueue::ReceiveInputEvent() call. + { + RecursiveMutexAutoLock lock(mTreeLock); + mTapGestureHitResult = + mHitTester->CloneHitTestResult(lock, state.mHit); + } + + state.mResult = mInputQueue->ReceiveInputEvent( + state.mHit.mTargetApzc, + TargetConfirmationFlags{state.mHit.mHitResult}, tapInput); + + mTapGestureHitResult = HitTestResult(); + + // Update the out-parameters so they are what the caller expects. + tapInput.mPoint = *untransformedPoint; + } + break; + } + case KEYBOARD_INPUT: { + // Disable async keyboard scrolling when accessibility.browsewithcaret is + // enabled + if (!StaticPrefs::apz_keyboard_enabled_AtStartup() || + StaticPrefs::accessibility_browsewithcaret()) { + APZ_KEY_LOG("Skipping key input from invalid prefs\n"); + return state.Finish(*this, std::move(aCallback)); + } + + KeyboardInput& keyInput = aEvent.AsKeyboardInput(); + + // Try and find a matching shortcut for this keyboard input + Maybe shortcut = mKeyboardMap.FindMatch(keyInput); + + if (!shortcut) { + APZ_KEY_LOG("Skipping key input with no shortcut\n"); + + // If we don't have a shortcut for this key event, then we can keep our + // focus only if we know there are no key event listeners for this + // target + if (mFocusState.CanIgnoreKeyboardShortcutMisses()) { + focusSetter.MarkAsNonFocusChanging(); + } + return state.Finish(*this, std::move(aCallback)); + } + + // Check if this shortcut needs to be dispatched to content. Anything + // matching this is assumed to be able to change focus. + if (shortcut->mDispatchToContent) { + APZ_KEY_LOG("Skipping key input with dispatch-to-content shortcut\n"); + return state.Finish(*this, std::move(aCallback)); + } + + // We know we have an action to execute on whatever is the current focus + // target + const KeyboardScrollAction& action = shortcut->mAction; + + // The current focus target depends on which direction the scroll is to + // happen + Maybe targetGuid; + switch (action.mType) { + case KeyboardScrollAction::eScrollCharacter: { + targetGuid = mFocusState.GetHorizontalTarget(); + break; + } + case KeyboardScrollAction::eScrollLine: + case KeyboardScrollAction::eScrollPage: + case KeyboardScrollAction::eScrollComplete: { + targetGuid = mFocusState.GetVerticalTarget(); + break; + } + } + + // If we don't have a scroll target then either we have a stale focus + // target, the focused element has event listeners, or the focused element + // doesn't have a layerized scroll frame. In any case we need to dispatch + // to content. + if (!targetGuid) { + APZ_KEY_LOG("Skipping key input with no current focus target\n"); + return state.Finish(*this, std::move(aCallback)); + } + + RefPtr targetApzc = + GetTargetAPZC(targetGuid->mLayersId, targetGuid->mScrollId); + + if (!targetApzc) { + APZ_KEY_LOG("Skipping key input with focus target but no APZC\n"); + return state.Finish(*this, std::move(aCallback)); + } + + // Attach the keyboard scroll action to the input event for processing + // by the input queue. + keyInput.mAction = action; + + APZ_KEY_LOG("Dispatching key input with apzc=%p\n", targetApzc.get()); + + // Dispatch the event to the input queue. + state.mResult = mInputQueue->ReceiveInputEvent( + targetApzc, TargetConfirmationFlags{true}, keyInput); + + // Any keyboard event that is dispatched to the input queue at this point + // should have been consumed + MOZ_ASSERT(state.mResult.GetStatus() == nsEventStatus_eConsumeDoDefault || + state.mResult.GetStatus() == nsEventStatus_eConsumeNoDefault); + + keyInput.mHandledByAPZ = true; + focusSetter.MarkAsNonFocusChanging(); + + break; + } + } + return state.Finish(*this, std::move(aCallback)); +} + +static TouchBehaviorFlags ConvertToTouchBehavior( + const CompositorHitTestInfo& info) { + TouchBehaviorFlags result = AllowedTouchBehavior::UNKNOWN; + if (info == CompositorHitTestInvisibleToHit) { + result = AllowedTouchBehavior::NONE; + } else if (info.contains(CompositorHitTestFlags::eIrregularArea)) { + // Note that eApzAwareListeners and eInactiveScrollframe are similar + // to eIrregularArea in some respects, but are not relevant for the + // purposes of this function, which deals specifically with touch-action. + result = AllowedTouchBehavior::UNKNOWN; + } else { + result = AllowedTouchBehavior::VERTICAL_PAN | + AllowedTouchBehavior::HORIZONTAL_PAN | + AllowedTouchBehavior::PINCH_ZOOM | + AllowedTouchBehavior::ANIMATING_ZOOM; + if (info.contains(CompositorHitTestFlags::eTouchActionPanXDisabled)) { + result &= ~AllowedTouchBehavior::HORIZONTAL_PAN; + } + if (info.contains(CompositorHitTestFlags::eTouchActionPanYDisabled)) { + result &= ~AllowedTouchBehavior::VERTICAL_PAN; + } + if (info.contains(CompositorHitTestFlags::eTouchActionPinchZoomDisabled)) { + result &= ~AllowedTouchBehavior::PINCH_ZOOM; + } + if (info.contains( + CompositorHitTestFlags::eTouchActionAnimatingZoomDisabled)) { + result &= ~AllowedTouchBehavior::ANIMATING_ZOOM; + } + } + return result; +} + +APZCTreeManager::HitTestResult APZCTreeManager::GetTouchInputBlockAPZC( + const MultiTouchInput& aEvent, + nsTArray* aOutTouchBehaviors) { + HitTestResult hit; + if (aEvent.mTouches.Length() == 0) { + return hit; + } + + FlushRepaintsToClearScreenToGeckoTransform(); + + hit = GetTargetAPZC(aEvent.mTouches[0].mScreenPoint); + // Don't set a layers id on multi-touch events. + if (aEvent.mTouches.Length() != 1) { + hit.mLayersId = LayersId{0}; + } + + if (aOutTouchBehaviors) { + aOutTouchBehaviors->AppendElement(ConvertToTouchBehavior(hit.mHitResult)); + } + for (size_t i = 1; i < aEvent.mTouches.Length(); i++) { + HitTestResult hit2 = GetTargetAPZC(aEvent.mTouches[i].mScreenPoint); + if (aOutTouchBehaviors) { + aOutTouchBehaviors->AppendElement( + ConvertToTouchBehavior(hit2.mHitResult)); + } + hit.mTargetApzc = GetZoomableTarget(hit.mTargetApzc, hit2.mTargetApzc); + APZCTM_LOG("Using APZC %p as the root APZC for multi-touch\n", + hit.mTargetApzc.get()); + // A multi-touch gesture will not be a scrollbar drag, even if the + // first touch point happened to hit a scrollbar. + hit.mScrollbarNode.Clear(); + + // XXX we should probably be combining the hit results from the different + // touch points somehow, instead of just using the last one. + hit.mHitResult = hit2.mHitResult; + } + + return hit; +} + +APZEventResult APZCTreeManager::InputHandlingState::Finish( + APZCTreeManager& aTreeManager, InputBlockCallback&& aCallback) { + // The validity check here handles both the case where mHit was + // never populated (because this event did not trigger a hit-test), + // and the case where it was populated with an invalid LayersId + // (which can happen e.g. for multi-touch events). + if (mHit.mLayersId.IsValid()) { + mEvent.mLayersId = mHit.mLayersId; + } + + // Absorb events that are in targetted at a position in the gutter, + // unless they are fixed position elements. + if (mHit.mHitOverscrollGutter && mHit.mFixedPosSides == SideBits::eNone) { + mResult.SetStatusAsConsumeNoDefault(); + } + + // If the event will have a delayed result then add the callback to the + // APZCTreeManager. + if (aCallback && mResult.WillHaveDelayedResult()) { + aTreeManager.AddInputBlockCallback( + mResult.mInputBlockId, {mResult.GetStatus(), std::move(aCallback)}); + } + + return mResult; +} + +void APZCTreeManager::ProcessTouchInput(InputHandlingState& aState, + MultiTouchInput& aInput) { + aInput.mHandledByAPZ = true; + nsTArray touchBehaviors; + HitTestingTreeNodeAutoLock hitScrollbarNode; + if (aInput.mType == MultiTouchInput::MULTITOUCH_START) { + // If we are panned into overscroll and a second finger goes down, + // ignore that second touch point completely. The touch-start for it is + // dropped completely; subsequent touch events until the touch-end for it + // will have this touch point filtered out. + // (By contrast, if we're in overscroll but not panning, such as after + // putting two fingers down during an overscroll animation, we process the + // second touch and proceed to pinch.) + if (mTouchBlockHitResult.mTargetApzc && + mTouchBlockHitResult.mTargetApzc->IsInPanningState() && + BuildOverscrollHandoffChain(mTouchBlockHitResult.mTargetApzc) + ->HasOverscrolledApzc()) { + if (mRetainedTouchIdentifier == -1) { + mRetainedTouchIdentifier = + mTouchBlockHitResult.mTargetApzc->GetLastTouchIdentifier(); + } + + aState.mResult.SetStatusAsConsumeNoDefault(); + return; + } + + aState.mHit = GetTouchInputBlockAPZC(aInput, &touchBehaviors); + RecursiveMutexAutoLock lock(mTreeLock); + // Repopulate mTouchBlockHitResult from the input state. + mTouchBlockHitResult = mHitTester->CloneHitTestResult(lock, aState.mHit); + hitScrollbarNode = std::move(aState.mHit.mScrollbarNode); + + // Check if this event starts a scrollbar touch-drag. The conditions + // checked are similar to the ones we check for MOUSE_INPUT starting + // a scrollbar mouse-drag. + mInScrollbarTouchDrag = + StaticPrefs::apz_drag_enabled() && + StaticPrefs::apz_drag_touch_enabled() && hitScrollbarNode && + hitScrollbarNode->IsScrollThumbNode() && + hitScrollbarNode->GetScrollbarData().mThumbIsAsyncDraggable; + + MOZ_ASSERT(touchBehaviors.Length() == aInput.mTouches.Length()); + for (size_t i = 0; i < touchBehaviors.Length(); i++) { + APZCTM_LOG("Touch point has allowed behaviours 0x%02x\n", + touchBehaviors[i]); + if (touchBehaviors[i] == AllowedTouchBehavior::UNKNOWN) { + // If there's any unknown items in the list, throw it out and we'll + // wait for the main thread to send us a notification. + touchBehaviors.Clear(); + break; + } + } + } else if (mTouchBlockHitResult.mTargetApzc) { + APZCTM_LOG("Re-using APZC %p as continuation of event block\n", + mTouchBlockHitResult.mTargetApzc.get()); + RecursiveMutexAutoLock lock(mTreeLock); + aState.mHit = mHitTester->CloneHitTestResult(lock, mTouchBlockHitResult); + } + + if (mInScrollbarTouchDrag) { + aState.mResult = ProcessTouchInputForScrollbarDrag( + aInput, hitScrollbarNode, mTouchBlockHitResult.mHitResult); + } else { + // If we receive a touch-cancel, it means all touches are finished, so we + // can stop ignoring any that we were ignoring. + if (aInput.mType == MultiTouchInput::MULTITOUCH_CANCEL) { + mRetainedTouchIdentifier = -1; + } + + // If we are currently ignoring any touch points, filter them out from the + // set of touch points included in this event. Note that we modify aInput + // itself, so that the touch points are also filtered out when the caller + // passes the event on to content. + if (mRetainedTouchIdentifier != -1) { + for (size_t j = 0; j < aInput.mTouches.Length(); ++j) { + if (aInput.mTouches[j].mIdentifier != mRetainedTouchIdentifier) { + aInput.mTouches.RemoveElementAt(j); + if (!touchBehaviors.IsEmpty()) { + MOZ_ASSERT(touchBehaviors.Length() > j); + touchBehaviors.RemoveElementAt(j); + } + --j; + } + } + if (aInput.mTouches.IsEmpty()) { + aState.mResult.SetStatusAsConsumeNoDefault(); + return; + } + } + + if (mTouchBlockHitResult.mTargetApzc) { + MOZ_ASSERT(mTouchBlockHitResult.mHitResult != + CompositorHitTestInvisibleToHit); + + aState.mResult = mInputQueue->ReceiveInputEvent( + mTouchBlockHitResult.mTargetApzc, + TargetConfirmationFlags{mTouchBlockHitResult.mHitResult}, aInput, + touchBehaviors.IsEmpty() ? Nothing() + : Some(std::move(touchBehaviors))); + + // For computing the event to pass back to Gecko, use up-to-date + // transforms (i.e. not anything cached in an input block). This ensures + // that transformToApzc and transformToGecko are in sync. + // Note: we are not using ConvertToGecko() here, because we don't + // want to multiply transformToApzc and transformToGecko once + // for each touch point. + ScreenToParentLayerMatrix4x4 transformToApzc = + GetScreenToApzcTransform(mTouchBlockHitResult.mTargetApzc); + ParentLayerToScreenMatrix4x4 transformToGecko = + GetApzcToGeckoTransformForHit(mTouchBlockHitResult); + ScreenToScreenMatrix4x4 outTransform = transformToApzc * transformToGecko; + + for (size_t i = 0; i < aInput.mTouches.Length(); i++) { + SingleTouchData& touchData = aInput.mTouches[i]; + Maybe untransformedScreenPoint = + UntransformBy(outTransform, touchData.mScreenPoint); + if (!untransformedScreenPoint) { + aState.mResult.SetStatusAsIgnore(); + return; + } + touchData.mScreenPoint = *untransformedScreenPoint; + AdjustEventPointForDynamicToolbar(touchData.mScreenPoint, + mTouchBlockHitResult); + } + } + } + + mTouchCounter.Update(aInput); + + // If it's the end of the touch sequence then clear out variables so we + // don't keep dangling references and leak things. + if (mTouchCounter.GetActiveTouchCount() == 0) { + mTouchBlockHitResult = HitTestResult(); + mRetainedTouchIdentifier = -1; + mInScrollbarTouchDrag = false; + } +} + +void APZCTreeManager::AdjustEventPointForDynamicToolbar( + ScreenIntPoint& aEventPoint, const HitTestResult& aHit) { + if (aHit.mFixedPosSides != SideBits::eNone) { + MutexAutoLock lock(mMapLock); + aEventPoint -= RoundedToInt(apz::ComputeFixedMarginsOffset( + GetCompositorFixedLayerMargins(lock), aHit.mFixedPosSides, + mGeckoFixedLayerMargins)); + } else if (aHit.mNode && aHit.mNode->GetStickyPositionAnimationId()) { + SideBits sideBits = SideBits::eNone; + { + RecursiveMutexAutoLock lock(mTreeLock); + sideBits = SidesStuckToRootContent(aHit.mNode.Get(lock)); + } + MutexAutoLock lock(mMapLock); + aEventPoint -= RoundedToInt(apz::ComputeFixedMarginsOffset( + GetCompositorFixedLayerMargins(lock), sideBits, ScreenMargin())); + } +} + +static MouseInput::MouseType MultiTouchTypeToMouseType( + MultiTouchInput::MultiTouchType aType) { + switch (aType) { + case MultiTouchInput::MULTITOUCH_START: + return MouseInput::MOUSE_DOWN; + case MultiTouchInput::MULTITOUCH_MOVE: + return MouseInput::MOUSE_MOVE; + case MultiTouchInput::MULTITOUCH_END: + case MultiTouchInput::MULTITOUCH_CANCEL: + return MouseInput::MOUSE_UP; + } + MOZ_ASSERT_UNREACHABLE("Invalid multi-touch type"); + return MouseInput::MOUSE_NONE; +} + +APZEventResult APZCTreeManager::ProcessTouchInputForScrollbarDrag( + MultiTouchInput& aTouchInput, + const HitTestingTreeNodeAutoLock& aScrollThumbNode, + const gfx::CompositorHitTestInfo& aHitInfo) { + MOZ_ASSERT(mRetainedTouchIdentifier == -1); + MOZ_ASSERT(mTouchBlockHitResult.mTargetApzc); + MOZ_ASSERT(aTouchInput.mTouches.Length() == 1); + + // Synthesize a mouse event based on the touch event, so that we can + // reuse code in InputQueue and APZC for handling scrollbar mouse-drags. + MouseInput mouseInput{MultiTouchTypeToMouseType(aTouchInput.mType), + MouseInput::PRIMARY_BUTTON, + dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH, + MouseButtonsFlag::ePrimaryFlag, + aTouchInput.mTouches[0].mScreenPoint, + aTouchInput.mTimeStamp, + aTouchInput.modifiers}; + mouseInput.mHandledByAPZ = true; + + TargetConfirmationFlags targetConfirmed{aHitInfo}; + APZEventResult result; + result = mInputQueue->ReceiveInputEvent(mTouchBlockHitResult.mTargetApzc, + targetConfirmed, mouseInput); + + // |aScrollThumbNode| is non-null iff. this is the event that starts the drag. + // If so, set up the drag. + if (aScrollThumbNode) { + SetupScrollbarDrag(mouseInput, aScrollThumbNode, + mTouchBlockHitResult.mTargetApzc.get()); + } + + // Since the input was targeted at a scrollbar: + // - The original touch event (which will be sent on to content) will + // not be untransformed. + // - We don't want to apply the callback transform in the main thread, + // so we remove the scrollid from the guid. + // Both of these match the behaviour of mouse events that target a scrollbar; + // see the code for handling mouse events in ReceiveInputEvent() for + // additional explanation. + result.mTargetGuid.mScrollId = ScrollableLayerGuid::NULL_SCROLL_ID; + + return result; +} + +void APZCTreeManager::SetupScrollbarDrag( + MouseInput& aMouseInput, const HitTestingTreeNodeAutoLock& aScrollThumbNode, + AsyncPanZoomController* aApzc) { + DragBlockState* dragBlock = mInputQueue->GetCurrentDragBlock(); + if (!dragBlock) { + return; + } + + const ScrollbarData& thumbData = aScrollThumbNode->GetScrollbarData(); + MOZ_ASSERT(thumbData.mDirection.isSome()); + + // Record the thumb's position at the start of the drag. + // We snap back to this position if, during the drag, the mouse + // gets sufficiently far away from the scrollbar. + dragBlock->SetInitialThumbPos(thumbData.mThumbStart); + + // Under some conditions, we can confirm the drag block right away. + // Otherwise, we have to wait for a main-thread confirmation. + if (StaticPrefs::apz_drag_initial_enabled() && + // check that the scrollbar's target scroll frame is layerized + aScrollThumbNode->GetScrollTargetId() == aApzc->GetGuid().mScrollId && + !aApzc->IsScrollInfoLayer()) { + uint64_t dragBlockId = dragBlock->GetBlockId(); + // AsyncPanZoomController::HandleInputEvent() will call + // TransformToLocal() on the event, but we need its mLocalOrigin now + // to compute a drag start offset for the AsyncDragMetrics. + aMouseInput.TransformToLocal(aApzc->GetTransformToThis()); + OuterCSSCoord dragStart = + aApzc->ConvertScrollbarPoint(aMouseInput.mLocalOrigin, thumbData); + // ConvertScrollbarPoint() got the drag start offset relative to + // the scroll track. Now get it relative to the thumb. + // ScrollThumbData::mThumbStart stores the offset of the thumb + // relative to the scroll track at the time of the last paint. + // Since that paint, the thumb may have acquired an async transform + // due to async scrolling, so look that up and apply it. + LayerToParentLayerMatrix4x4 thumbTransform; + { + RecursiveMutexAutoLock lock(mTreeLock); + thumbTransform = ComputeTransformForNode(aScrollThumbNode.Get(lock)); + } + // Only consider the translation, since we do not support both + // zooming and scrollbar dragging on any platform. + OuterCSSCoord thumbStart = + thumbData.mThumbStart + + ((*thumbData.mDirection == ScrollDirection::eHorizontal) + ? thumbTransform._41 + : thumbTransform._42); + dragStart -= thumbStart; + + // Content can't prevent scrollbar dragging with preventDefault(), + // so we don't need to wait for a content response. It's important + // to do this before calling ConfirmDragBlock() since that can + // potentially process and consume the block. + dragBlock->SetContentResponse(false); + + NotifyScrollbarDragInitiated(dragBlockId, aApzc->GetGuid(), + *thumbData.mDirection); + + mInputQueue->ConfirmDragBlock( + dragBlockId, aApzc, + AsyncDragMetrics(aApzc->GetGuid().mScrollId, + aApzc->GetGuid().mPresShellId, dragBlockId, dragStart, + *thumbData.mDirection)); + } +} + +void APZCTreeManager::SynthesizePinchGestureFromMouseWheel( + const ScrollWheelInput& aWheelInput, + const RefPtr& aTarget) { + MOZ_ASSERT(aTarget); + + ScreenPoint focusPoint = aWheelInput.mOrigin; + + // Compute span values based on the wheel delta. + ScreenCoord oldSpan = 100; + ScreenCoord newSpan = oldSpan + aWheelInput.mDeltaY; + + // There's no ambiguity as to the target for pinch gesture events. + TargetConfirmationFlags confFlags{true}; + + PinchGestureInput pinchStart{PinchGestureInput::PINCHGESTURE_START, + PinchGestureInput::MOUSEWHEEL, + aWheelInput.mTimeStamp, + ExternalPoint(0, 0), + focusPoint, + oldSpan, + oldSpan, + aWheelInput.modifiers}; + PinchGestureInput pinchScale1{PinchGestureInput::PINCHGESTURE_SCALE, + PinchGestureInput::MOUSEWHEEL, + aWheelInput.mTimeStamp, + ExternalPoint(0, 0), + focusPoint, + oldSpan, + oldSpan, + aWheelInput.modifiers}; + PinchGestureInput pinchScale2{PinchGestureInput::PINCHGESTURE_SCALE, + PinchGestureInput::MOUSEWHEEL, + aWheelInput.mTimeStamp, + ExternalPoint(0, 0), + focusPoint, + oldSpan, + newSpan, + aWheelInput.modifiers}; + PinchGestureInput pinchEnd{PinchGestureInput::PINCHGESTURE_END, + PinchGestureInput::MOUSEWHEEL, + aWheelInput.mTimeStamp, + ExternalPoint(0, 0), + focusPoint, + newSpan, + newSpan, + aWheelInput.modifiers}; + + mInputQueue->ReceiveInputEvent(aTarget, confFlags, pinchStart); + mInputQueue->ReceiveInputEvent(aTarget, confFlags, pinchScale1); + mInputQueue->ReceiveInputEvent(aTarget, confFlags, pinchScale2); + mInputQueue->ReceiveInputEvent(aTarget, confFlags, pinchEnd); +} + +void APZCTreeManager::UpdateWheelTransaction( + LayoutDeviceIntPoint aRefPoint, EventMessage aEventMessage, + const Maybe& aTargetGuid) { + APZThreadUtils::AssertOnControllerThread(); + + WheelBlockState* txn = mInputQueue->GetActiveWheelTransaction(); + if (!txn) { + return; + } + + // If the transaction has simply timed out, we don't need to do anything + // else. + if (txn->MaybeTimeout(TimeStamp::Now())) { + return; + } + + switch (aEventMessage) { + case eMouseMove: + case eDragOver: { + ScreenIntPoint point = ViewAs( + aRefPoint, + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent); + + txn->OnMouseMove(point, aTargetGuid); + + return; + } + case eKeyPress: + case eKeyUp: + case eKeyDown: + case eMouseUp: + case eMouseDown: + case eMouseDoubleClick: + case eMouseAuxClick: + case eMouseClick: + case eContextMenu: + case eDrop: + txn->EndTransaction(); + return; + default: + break; + } +} + +void APZCTreeManager::ProcessUnhandledEvent(LayoutDeviceIntPoint* aRefPoint, + ScrollableLayerGuid* aOutTargetGuid, + uint64_t* aOutFocusSequenceNumber, + LayersId* aOutLayersId) { + APZThreadUtils::AssertOnControllerThread(); + + // Transform the aRefPoint. + // If the event hits an overscrolled APZC, instruct the caller to ignore it. + PixelCastJustification LDIsScreen = + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent; + ScreenIntPoint refPointAsScreen = ViewAs(*aRefPoint, LDIsScreen); + HitTestResult hit = GetTargetAPZC(refPointAsScreen); + if (aOutLayersId) { + *aOutLayersId = hit.mLayersId; + } + if (hit.mTargetApzc) { + MOZ_ASSERT(hit.mHitResult != CompositorHitTestInvisibleToHit); + hit.mTargetApzc->GetGuid(aOutTargetGuid); + ScreenToParentLayerMatrix4x4 transformToApzc = + GetScreenToApzcTransform(hit.mTargetApzc); + ParentLayerToScreenMatrix4x4 transformToGecko = + GetApzcToGeckoTransformForHit(hit); + ScreenToScreenMatrix4x4 outTransform = transformToApzc * transformToGecko; + Maybe untransformedRefPoint = + UntransformBy(outTransform, refPointAsScreen); + if (untransformedRefPoint) { + *aRefPoint = + ViewAs(*untransformedRefPoint, LDIsScreen); + } + } + + // Update the focus sequence number and attach it to the event + mFocusState.ReceiveFocusChangingEvent(); + *aOutFocusSequenceNumber = mFocusState.LastAPZProcessedEvent(); +} + +void APZCTreeManager::SetKeyboardMap(const KeyboardMap& aKeyboardMap) { + if (!APZThreadUtils::IsControllerThread()) { + APZThreadUtils::RunOnControllerThread(NewRunnableMethod( + "layers::APZCTreeManager::SetKeyboardMap", this, + &APZCTreeManager::SetKeyboardMap, aKeyboardMap)); + return; + } + + APZThreadUtils::AssertOnControllerThread(); + + mKeyboardMap = aKeyboardMap; +} + +void APZCTreeManager::ZoomToRect(const ScrollableLayerGuid& aGuid, + const ZoomTarget& aZoomTarget, + const uint32_t aFlags) { + if (!APZThreadUtils::IsControllerThread()) { + APZThreadUtils::RunOnControllerThread( + NewRunnableMethod( + "layers::APZCTreeManager::ZoomToRect", this, + &APZCTreeManager::ZoomToRect, aGuid, aZoomTarget, aFlags)); + return; + } + + // We could probably move this to run on the updater thread if needed, but + // either way we should restrict it to a single thread. For now let's use the + // controller thread. + APZThreadUtils::AssertOnControllerThread(); + + RefPtr apzc = GetTargetAPZC(aGuid); + if (apzc) { + apzc->ZoomToRect(aZoomTarget, aFlags); + } +} + +void APZCTreeManager::ContentReceivedInputBlock(uint64_t aInputBlockId, + bool aPreventDefault) { + if (!APZThreadUtils::IsControllerThread()) { + APZThreadUtils::RunOnControllerThread(NewRunnableMethod( + "layers::APZCTreeManager::ContentReceivedInputBlock", this, + &APZCTreeManager::ContentReceivedInputBlock, aInputBlockId, + aPreventDefault)); + return; + } + + APZThreadUtils::AssertOnControllerThread(); + + mInputQueue->ContentReceivedInputBlock(aInputBlockId, aPreventDefault); +} + +void APZCTreeManager::SetTargetAPZC( + uint64_t aInputBlockId, const nsTArray& aTargets) { + if (!APZThreadUtils::IsControllerThread()) { + APZThreadUtils::RunOnControllerThread( + NewRunnableMethod>>( + "layers::APZCTreeManager::SetTargetAPZC", this, + &layers::APZCTreeManager::SetTargetAPZC, aInputBlockId, + aTargets.Clone())); + return; + } + + RefPtr target = nullptr; + if (aTargets.Length() > 0) { + target = GetTargetAPZC(aTargets[0]); + } + for (size_t i = 1; i < aTargets.Length(); i++) { + RefPtr apzc = GetTargetAPZC(aTargets[i]); + target = GetZoomableTarget(target, apzc); + } + if (InputBlockState* block = mInputQueue->GetBlockForId(aInputBlockId)) { + if (block->AsPinchGestureBlock() && aTargets.Length() == 1) { + target = FindZoomableApzc(target); + } + } + mInputQueue->SetConfirmedTargetApzc(aInputBlockId, target); +} + +void APZCTreeManager::UpdateZoomConstraints( + const ScrollableLayerGuid& aGuid, + const Maybe& aConstraints) { + if (!GetUpdater()->IsUpdaterThread()) { + // This can happen if we're in the UI process and got a call directly from + // nsBaseWidget or from a content process over PAPZCTreeManager. In that + // case we get this call on the compositor thread, which may be different + // from the updater thread. It can also happen in the GPU process if that is + // enabled, since the call will go over PAPZCTreeManager and arrive on the + // compositor thread in the GPU process. + GetUpdater()->RunOnUpdaterThread( + aGuid.mLayersId, + NewRunnableMethod>( + "APZCTreeManager::UpdateZoomConstraints", this, + &APZCTreeManager::UpdateZoomConstraints, aGuid, aConstraints)); + return; + } + + AssertOnUpdaterThread(); + + // Propagate the zoom constraints down to the subtree, stopping at APZCs + // which have their own zoom constraints or are in a different layers id. + if (aConstraints) { + APZCTM_LOG("Recording constraints %s for guid %s\n", + ToString(aConstraints.value()).c_str(), ToString(aGuid).c_str()); + mZoomConstraints[aGuid] = aConstraints.ref(); + } else { + APZCTM_LOG("Removing constraints for guid %s\n", ToString(aGuid).c_str()); + mZoomConstraints.erase(aGuid); + } + + RecursiveMutexAutoLock lock(mTreeLock); + RefPtr node = DepthFirstSearchPostOrder( + mRootNode.get(), [&aGuid](HitTestingTreeNode* aNode) { + bool matches = false; + if (auto zoomId = aNode->GetAsyncZoomContainerId()) { + matches = ScrollableLayerGuid::EqualsIgnoringPresShell( + aGuid, ScrollableLayerGuid(aNode->GetLayersId(), 0, *zoomId)); + } + return matches; + }); + + // This does not hold because we can get zoom constraints updates before the + // layer tree update with the async zoom container (I assume). + // clang-format off + // MOZ_ASSERT(node || aConstraints.isNothing() || + // (!aConstraints->mAllowZoom && !aConstraints->mAllowDoubleTapZoom)); + // clang-format on + + // If there is no async zoom container then the zoom constraints should not + // allow zooming and building the HTT should have handled clearing the zoom + // constraints from all nodes so we don't have to handle doing anything in + // case there is no async zoom container. + + if (node && aConstraints) { + ForEachNode(node.get(), [&aConstraints, &node, &aGuid, + this](HitTestingTreeNode* aNode) { + if (aNode != node) { + // don't go into other async zoom containers + if (auto zoomId = aNode->GetAsyncZoomContainerId()) { + MOZ_ASSERT(!ScrollableLayerGuid::EqualsIgnoringPresShell( + aGuid, ScrollableLayerGuid(aNode->GetLayersId(), 0, *zoomId))); + return TraversalFlag::Skip; + } + if (AsyncPanZoomController* childApzc = aNode->GetApzc()) { + if (!ScrollableLayerGuid::EqualsIgnoringPresShell( + aGuid, childApzc->GetGuid())) { + // We can have subtrees with their own zoom constraints - leave + // these alone. + if (this->mZoomConstraints.find(childApzc->GetGuid()) != + this->mZoomConstraints.end()) { + return TraversalFlag::Skip; + } + } + } + } + if (aNode->IsPrimaryHolder()) { + MOZ_ASSERT(aNode->GetApzc()); + aNode->GetApzc()->UpdateZoomConstraints(aConstraints.ref()); + } + return TraversalFlag::Continue; + }); + } +} + +void APZCTreeManager::FlushRepaintsToClearScreenToGeckoTransform() { + // As the name implies, we flush repaint requests for the entire APZ tree in + // order to clear the screen-to-gecko transform (aka the "untransform" applied + // to incoming input events before they can be passed on to Gecko). + // + // The primary reason we do this is to avoid the problem where input events, + // after being untransformed, end up hit-testing differently in Gecko. This + // might happen in cases where the input event lands on content that is async- + // scrolled into view, but Gecko still thinks it is out of view given the + // visible area of a scrollframe. + // + // Another reason we want to clear the untransform is that if our APZ hit-test + // hits a dispatch-to-content region then that's an ambiguous result and we + // need to ask Gecko what actually got hit. In order to do this we need to + // untransform the input event into Gecko space - but to do that we need to + // know which APZC got hit! This leads to a circular dependency; the only way + // to get out of it is to make sure that the untransform for all the possible + // matched APZCs is the same. It is simplest to ensure that by flushing the + // pending repaint requests, which makes all of the untransforms empty (and + // therefore equal). + RecursiveMutexAutoLock lock(mTreeLock); + + ForEachNode(mRootNode.get(), [](HitTestingTreeNode* aNode) { + if (aNode->IsPrimaryHolder()) { + MOZ_ASSERT(aNode->GetApzc()); + aNode->GetApzc()->FlushRepaintForNewInputBlock(); + } + }); +} + +void APZCTreeManager::ClearTree() { + AssertOnUpdaterThread(); + + // Ensure that no references to APZCs are alive in any lingering input + // blocks. This breaks cycles from InputBlockState::mTargetApzc back to + // the InputQueue. + APZThreadUtils::RunOnControllerThread(NewRunnableMethod( + "layers::InputQueue::Clear", mInputQueue, &InputQueue::Clear)); + + RecursiveMutexAutoLock lock(mTreeLock); + + // Collect the nodes into a list, and then destroy each one. + // We can't destroy them as we collect them, because ForEachNode() + // does a pre-order traversal of the tree, and Destroy() nulls out + // the fields needed to reach the children of the node. + nsTArray> nodesToDestroy; + ForEachNode(mRootNode.get(), + [&nodesToDestroy](HitTestingTreeNode* aNode) { + nodesToDestroy.AppendElement(aNode); + }); + + for (size_t i = 0; i < nodesToDestroy.Length(); i++) { + nodesToDestroy[i]->Destroy(); + } + mRootNode = nullptr; + + { + // Also remove references to APZC instances in the map + MutexAutoLock lock(mMapLock); + mApzcMap.clear(); + } + + RefPtr self(this); + NS_DispatchToMainThread( + NS_NewRunnableFunction("layers::APZCTreeManager::ClearTree", [self] { + self->mFlushObserver->Unregister(); + self->mFlushObserver = nullptr; + })); +} + +RefPtr APZCTreeManager::GetRootNode() const { + RecursiveMutexAutoLock lock(mTreeLock); + return mRootNode; +} + +/** + * Transform a displacement from the ParentLayer coordinates of a source APZC + * to the ParentLayer coordinates of a target APZC. + * @param aTreeManager the tree manager for the APZC tree containing |aSource| + * and |aTarget| + * @param aSource the source APZC + * @param aTarget the target APZC + * @param aStartPoint the start point of the displacement + * @param aEndPoint the end point of the displacement + * @return true on success, false if aStartPoint or aEndPoint cannot be + * transformed into target's coordinate space + */ +static bool TransformDisplacement(APZCTreeManager* aTreeManager, + AsyncPanZoomController* aSource, + AsyncPanZoomController* aTarget, + ParentLayerPoint& aStartPoint, + ParentLayerPoint& aEndPoint) { + if (aSource == aTarget) { + return true; + } + + // Convert start and end points to Screen coordinates. + ParentLayerToScreenMatrix4x4 untransformToApzc = + aTreeManager->GetScreenToApzcTransform(aSource).Inverse(); + ScreenPoint screenStart = TransformBy(untransformToApzc, aStartPoint); + ScreenPoint screenEnd = TransformBy(untransformToApzc, aEndPoint); + + // Convert start and end points to aTarget's ParentLayer coordinates. + ScreenToParentLayerMatrix4x4 transformToApzc = + aTreeManager->GetScreenToApzcTransform(aTarget); + Maybe startPoint = + UntransformBy(transformToApzc, screenStart); + Maybe endPoint = UntransformBy(transformToApzc, screenEnd); + if (!startPoint || !endPoint) { + return false; + } + aEndPoint = *endPoint; + aStartPoint = *startPoint; + + return true; +} + +bool APZCTreeManager::DispatchScroll( + AsyncPanZoomController* aPrev, ParentLayerPoint& aStartPoint, + ParentLayerPoint& aEndPoint, + OverscrollHandoffState& aOverscrollHandoffState) { + const OverscrollHandoffChain& overscrollHandoffChain = + aOverscrollHandoffState.mChain; + uint32_t overscrollHandoffChainIndex = aOverscrollHandoffState.mChainIndex; + RefPtr next; + // If we have reached the end of the overscroll handoff chain, there is + // nothing more to scroll, so we ignore the rest of the pan gesture. + if (overscrollHandoffChainIndex >= overscrollHandoffChain.Length()) { + // Nothing more to scroll - ignore the rest of the pan gesture. + return false; + } + + next = overscrollHandoffChain.GetApzcAtIndex(overscrollHandoffChainIndex); + + if (next == nullptr || next->IsDestroyed()) { + return false; + } + + // Convert the start and end points from |aPrev|'s coordinate space to + // |next|'s coordinate space. + if (!TransformDisplacement(this, aPrev, next, aStartPoint, aEndPoint)) { + return false; + } + + // Scroll |next|. If this causes overscroll, it will call DispatchScroll() + // again with an incremented index. + if (!next->AttemptScroll(aStartPoint, aEndPoint, aOverscrollHandoffState)) { + // Transform |aStartPoint| and |aEndPoint| (which now represent the + // portion of the displacement that wasn't consumed by APZCs later + // in the handoff chain) back into |aPrev|'s coordinate space. This + // allows the caller (which is |aPrev|) to interpret the unconsumed + // displacement in its own coordinate space, and make use of it + // (e.g. by going into overscroll). + if (!TransformDisplacement(this, next, aPrev, aStartPoint, aEndPoint)) { + NS_WARNING("Failed to untransform scroll points during dispatch"); + } + return false; + } + + // Return true to indicate the scroll was consumed entirely. + return true; +} + +ParentLayerPoint APZCTreeManager::DispatchFling( + AsyncPanZoomController* aPrev, const FlingHandoffState& aHandoffState) { + // If immediate handoff is disallowed, do not allow handoff beyond the + // single APZC that's scrolled by the input block that triggered this fling. + if (aHandoffState.mIsHandoff && !StaticPrefs::apz_allow_immediate_handoff() && + aHandoffState.mScrolledApzc == aPrev) { + FLING_LOG("APZCTM dropping handoff due to disallowed immediate handoff\n"); + return aHandoffState.mVelocity; + } + + const OverscrollHandoffChain* chain = aHandoffState.mChain; + RefPtr current; + uint32_t overscrollHandoffChainLength = chain->Length(); + uint32_t startIndex; + + // The fling's velocity needs to be transformed from the screen coordinates + // of |aPrev| to the screen coordinates of |next|. To transform a velocity + // correctly, we need to convert it to a displacement. For now, we do this + // by anchoring it to a start point of (0, 0). + // TODO: For this to be correct in the presence of 3D transforms, we should + // use the end point of the touch that started the fling as the start point + // rather than (0, 0). + ParentLayerPoint startPoint; // (0, 0) + ParentLayerPoint endPoint; + + if (aHandoffState.mIsHandoff) { + startIndex = chain->IndexOf(aPrev) + 1; + + // IndexOf will return aOverscrollHandoffChain->Length() if + // |aPrev| is not found. + if (startIndex >= overscrollHandoffChainLength) { + return aHandoffState.mVelocity; + } + } else { + startIndex = 0; + } + + // This will store any velocity left over after the entire handoff. + ParentLayerPoint finalResidualVelocity = aHandoffState.mVelocity; + + ParentLayerPoint currentVelocity = aHandoffState.mVelocity; + for (; startIndex < overscrollHandoffChainLength; startIndex++) { + current = chain->GetApzcAtIndex(startIndex); + + // Make sure the apzc about to be handled can be handled + if (current == nullptr || current->IsDestroyed()) { + break; + } + + endPoint = startPoint + currentVelocity; + + RefPtr prevApzc = + (startIndex > 0) ? chain->GetApzcAtIndex(startIndex - 1) : nullptr; + + // Only transform when current apzc can be transformed with previous + if (prevApzc) { + if (!TransformDisplacement(this, prevApzc, current, startPoint, + endPoint)) { + break; + } + } + + ParentLayerPoint availableVelocity = (endPoint - startPoint); + ParentLayerPoint residualVelocity; + + FlingHandoffState transformedHandoffState = aHandoffState; + transformedHandoffState.mVelocity = availableVelocity; + + // Obey overscroll-behavior. + if (prevApzc) { + residualVelocity += prevApzc->AdjustHandoffVelocityForOverscrollBehavior( + transformedHandoffState.mVelocity); + } + + residualVelocity += current->AttemptFling(transformedHandoffState); + + // If there's no residual velocity, there's nothing more to hand off. + if (current->IsZero(residualVelocity)) { + return ParentLayerPoint(); + } + + // If any of the velocity available to be handed off was consumed, + // subtract the proportion of consumed velocity from finalResidualVelocity. + // Note: it's important to compare |residualVelocity| to |availableVelocity| + // here and not to |transformedHandoffState.mVelocity|, since the latter + // may have been modified by AdjustHandoffVelocityForOverscrollBehavior(). + if (!current->IsZero(availableVelocity.x - residualVelocity.x)) { + finalResidualVelocity.x *= (residualVelocity.x / availableVelocity.x); + } + if (!current->IsZero(availableVelocity.y - residualVelocity.y)) { + finalResidualVelocity.y *= (residualVelocity.y / availableVelocity.y); + } + + currentVelocity = residualVelocity; + } + + // Return any residual velocity left over after the entire handoff process. + return finalResidualVelocity; +} + +already_AddRefed APZCTreeManager::GetTargetAPZC( + const ScrollableLayerGuid& aGuid) { + RecursiveMutexAutoLock lock(mTreeLock); + RefPtr node = GetTargetNode(aGuid, nullptr); + MOZ_ASSERT(!node || node->GetApzc()); // any node returned must have an APZC + RefPtr apzc = node ? node->GetApzc() : nullptr; + return apzc.forget(); +} + +already_AddRefed APZCTreeManager::GetTargetAPZC( + const LayersId& aLayersId, + const ScrollableLayerGuid::ViewID& aScrollId) const { + MutexAutoLock lock(mMapLock); + return GetTargetAPZC(aLayersId, aScrollId, lock); +} + +already_AddRefed APZCTreeManager::GetTargetAPZC( + const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId, + const MutexAutoLock& aProofOfMapLock) const { + ScrollableLayerGuid guid(aLayersId, 0, aScrollId); + auto it = mApzcMap.find(guid); + RefPtr apzc = + (it != mApzcMap.end() ? it->second.apzc : nullptr); + return apzc.forget(); +} + +already_AddRefed APZCTreeManager::GetTargetNode( + const ScrollableLayerGuid& aGuid, GuidComparator aComparator) const { + mTreeLock.AssertCurrentThreadIn(); + RefPtr target = + DepthFirstSearchPostOrder( + mRootNode.get(), [&aGuid, &aComparator](HitTestingTreeNode* node) { + bool matches = false; + if (node->GetApzc()) { + if (aComparator) { + matches = aComparator(aGuid, node->GetApzc()->GetGuid()); + } else { + matches = node->GetApzc()->Matches(aGuid); + } + } + return matches; + }); + return target.forget(); +} + +APZCTreeManager::HitTestResult APZCTreeManager::GetTargetAPZC( + const ScreenPoint& aPoint) { + RecursiveMutexAutoLock lock(mTreeLock); + MOZ_ASSERT(mHitTester); + return mHitTester->GetAPZCAtPoint(aPoint, lock); +} + +APZCTreeManager::TargetApzcForNodeResult APZCTreeManager::FindHandoffParent( + const AsyncPanZoomController* aApzc) { + RefPtr node = GetTargetNode(aApzc->GetGuid(), nullptr); + while (node) { + auto result = GetTargetApzcForNode(node->GetParent()); + if (result.mApzc) { + // avoid infinite recursion in the overscroll handoff chain. + if (result.mApzc != aApzc) { + return result; + } + } + node = node->GetParent(); + } + + return {nullptr, false}; +} + +RefPtr +APZCTreeManager::BuildOverscrollHandoffChain( + const RefPtr& aInitialTarget) { + // Scroll grabbing is a mechanism that allows content to specify that + // the initial target of a pan should be not the innermost scrollable + // frame at the touch point (which is what GetTargetAPZC finds), but + // something higher up in the tree. + // It's not sufficient to just find the initial target, however, as + // overscroll can be handed off to another APZC. Without scroll grabbing, + // handoff just occurs from child to parent. With scroll grabbing, the + // handoff order can be different, so we build a chain of APZCs in the + // order in which scroll will be handed off to them. + + // Grab tree lock since we'll be walking the APZC tree. + RecursiveMutexAutoLock lock(mTreeLock); + + // Build the chain. If there is a scroll parent link, we use that. This is + // needed to deal with scroll info layers, because they participate in handoff + // but do not follow the expected layer tree structure. If there are no + // scroll parent links we just walk up the tree to find the scroll parent. + OverscrollHandoffChain* result = new OverscrollHandoffChain; + AsyncPanZoomController* apzc = aInitialTarget; + while (apzc != nullptr) { + result->Add(apzc); + + APZCTreeManager::TargetApzcForNodeResult handoffResult = + FindHandoffParent(apzc); + + if (!handoffResult.mIsFixed && !apzc->IsRootForLayersId() && + apzc->GetScrollHandoffParentId() == + ScrollableLayerGuid::NULL_SCROLL_ID) { + // This probably indicates a bug or missed case in layout code + NS_WARNING("Found a non-root APZ with no handoff parent"); + } + + // If `apzc` is inside fixed content, we want to hand off to the document's + // root APZC next. The scroll parent id wouldn't give us this because it's + // based on ASRs. + if (handoffResult.mIsFixed || apzc->GetScrollHandoffParentId() == + ScrollableLayerGuid::NULL_SCROLL_ID) { + apzc = handoffResult.mApzc; + continue; + } + + // Guard against a possible infinite-loop condition. If we hit this, the + // layout code that generates the handoff parents did something wrong. + MOZ_ASSERT(apzc->GetScrollHandoffParentId() != apzc->GetGuid().mScrollId); + RefPtr scrollParent = GetTargetAPZC( + apzc->GetGuid().mLayersId, apzc->GetScrollHandoffParentId()); + apzc = scrollParent.get(); + } + + // Now adjust the chain to account for scroll grabbing. Sorting is a bit + // of an overkill here, but scroll grabbing will likely be generalized + // to scroll priorities, so we might as well do it this way. + result->SortByScrollPriority(); + + // Print the overscroll chain for debugging. + for (uint32_t i = 0; i < result->Length(); ++i) { + APZCTM_LOG("OverscrollHandoffChain[%d] = %p\n", i, + result->GetApzcAtIndex(i).get()); + } + + return result; +} + +void APZCTreeManager::SetLongTapEnabled(bool aLongTapEnabled) { + if (!APZThreadUtils::IsControllerThread()) { + APZThreadUtils::RunOnControllerThread(NewRunnableMethod( + "layers::APZCTreeManager::SetLongTapEnabled", this, + &APZCTreeManager::SetLongTapEnabled, aLongTapEnabled)); + return; + } + + APZThreadUtils::AssertOnControllerThread(); + GestureEventListener::SetLongTapEnabled(aLongTapEnabled); +} + +void APZCTreeManager::AddInputBlockCallback( + uint64_t aInputBlockId, InputBlockCallbackInfo&& aCallbackInfo) { + APZThreadUtils::AssertOnControllerThread(); + mInputQueue->AddInputBlockCallback(aInputBlockId, std::move(aCallbackInfo)); +} + +void APZCTreeManager::FindScrollThumbNode( + const AsyncDragMetrics& aDragMetrics, LayersId aLayersId, + HitTestingTreeNodeAutoLock& aOutThumbNode) { + if (!aDragMetrics.mDirection) { + // The AsyncDragMetrics has not been initialized yet - there will be + // no matching node, so don't bother searching the tree. + return; + } + + RecursiveMutexAutoLock lock(mTreeLock); + + RefPtr result = DepthFirstSearch( + mRootNode.get(), [&aDragMetrics, &aLayersId](HitTestingTreeNode* aNode) { + return aNode->MatchesScrollDragMetrics(aDragMetrics, aLayersId); + }); + if (result) { + aOutThumbNode.Initialize(lock, result.forget(), mTreeLock); + } +} + +APZCTreeManager::TargetApzcForNodeResult APZCTreeManager::GetTargetApzcForNode( + const HitTestingTreeNode* aNode) { + for (const HitTestingTreeNode* n = aNode; + n && n->GetLayersId() == aNode->GetLayersId(); n = n->GetParent()) { + // For a fixed node, GetApzc() may return an APZC for content in the + // enclosing document, so we need to check GetFixedPosTarget() before + // GetApzc(). + if (n->GetFixedPosTarget() != ScrollableLayerGuid::NULL_SCROLL_ID) { + RefPtr fpTarget = + GetTargetAPZC(n->GetLayersId(), n->GetFixedPosTarget()); + APZCTM_LOG("Found target APZC %p using fixed-pos lookup on %" PRIu64 "\n", + fpTarget.get(), n->GetFixedPosTarget()); + return {fpTarget.get(), true}; + } + if (n->GetApzc()) { + APZCTM_LOG("Found target %p using ancestor lookup\n", n->GetApzc()); + return {n->GetApzc(), false}; + } + } + return {nullptr, false}; +} + +HitTestingTreeNode* APZCTreeManager::FindRootNodeForLayersId( + LayersId aLayersId) const { + mTreeLock.AssertCurrentThreadIn(); + + HitTestingTreeNode* resultNode = BreadthFirstSearch( + mRootNode.get(), [aLayersId](HitTestingTreeNode* aNode) { + AsyncPanZoomController* apzc = aNode->GetApzc(); + return apzc && apzc->GetLayersId() == aLayersId && + apzc->IsRootForLayersId(); + }); + return resultNode; +} + +already_AddRefed APZCTreeManager::FindZoomableApzc( + AsyncPanZoomController* aStart) const { + return GetZoomableTarget(aStart, aStart); +} + +ScreenMargin APZCTreeManager::GetCompositorFixedLayerMargins() const { + RecursiveMutexAutoLock lock(mTreeLock); + return mCompositorFixedLayerMargins; +} + +AsyncPanZoomController* APZCTreeManager::FindRootContentApzcForLayersId( + LayersId aLayersId) const { + mTreeLock.AssertCurrentThreadIn(); + + HitTestingTreeNode* resultNode = BreadthFirstSearch( + mRootNode.get(), [aLayersId](HitTestingTreeNode* aNode) { + AsyncPanZoomController* apzc = aNode->GetApzc(); + return apzc && apzc->GetLayersId() == aLayersId && + apzc->IsRootContent(); + }); + return resultNode ? resultNode->GetApzc() : nullptr; +} + +// clang-format off +/* The methods GetScreenToApzcTransform() and GetApzcToGeckoTransform() return + some useful transformations that input events may need applied. This is best + illustrated with an example. Consider a chain of layers, L, M, N, O, P, Q, R. Layer L + is the layer that corresponds to the argument |aApzc|, and layer R is the root + of the layer tree. Layer M is the parent of L, N is the parent of M, and so on. + When layer L is displayed to the screen by the compositor, the set of transforms that + are applied to L are (in order from top to bottom): + + L's CSS transform (hereafter referred to as transform matrix LC) + L's nontransient async transform (hereafter referred to as transform matrix LN) + L's transient async transform (hereafter referred to as transform matrix LT) + M's CSS transform (hereafter referred to as transform matrix MC) + M's nontransient async transform (hereafter referred to as transform matrix MN) + M's transient async transform (hereafter referred to as transform matrix MT) + ... + R's CSS transform (hereafter referred to as transform matrix RC) + R's nontransient async transform (hereafter referred to as transform matrix RN) + R's transient async transform (hereafter referred to as transform matrix RT) + + Also, for any layer, the async transform is the combination of its transient and non-transient + parts. That is, for any layer L: + LA === LN * LT + LA.Inverse() === LT.Inverse() * LN.Inverse() + + If we want user input to modify L's transient async transform, we have to first convert + user input from screen space to the coordinate space of L's transient async transform. Doing + this involves applying the following transforms (in order from top to bottom): + RT.Inverse() + RN.Inverse() + RC.Inverse() + ... + MT.Inverse() + MN.Inverse() + MC.Inverse() + This combined transformation is returned by GetScreenToApzcTransform(). + + Next, if we want user inputs sent to gecko for event-dispatching, we will need to strip + out all of the async transforms that are involved in this chain. This is because async + transforms are stored only in the compositor and gecko does not account for them when + doing display-list-based hit-testing for event dispatching. + Furthermore, because these input events are processed by Gecko in a FIFO queue that + includes other things (specifically paint requests), it is possible that by time the + input event reaches gecko, it will have painted something else. Therefore, we need to + apply another transform to the input events to account for the possible disparity between + what we know gecko last painted and the last paint request we sent to gecko. Let this + transform be represented by LD, MD, ... RD. + Therefore, given a user input in screen space, the following transforms need to be applied + (in order from top to bottom): + RT.Inverse() + RN.Inverse() + RC.Inverse() + ... + MT.Inverse() + MN.Inverse() + MC.Inverse() + LT.Inverse() + LN.Inverse() + LC.Inverse() + LC + LD + MC + MD + ... + RC + RD + This sequence can be simplified and refactored to the following: + GetScreenToApzcTransform() + LA.Inverse() + LD + MC + MD + ... + RC + RD + Since GetScreenToApzcTransform() can be obtained by calling that function, GetApzcToGeckoTransform() + returns the remaining transforms (LA.Inverse() * LD * ... * RD), so that the caller code can + combine it with GetScreenToApzcTransform() to get the final transform required in this case. + + Note that for many of these layers, there will be no AsyncPanZoomController attached, and + so the async transform will be the identity transform. So, in the example above, if layers + L and P have APZC instances attached, MT, MN, MD, NT, NN, ND, OT, ON, OD, QT, QN, QD, RT, + RN and RD will be identity transforms. + Additionally, for space-saving purposes, each APZC instance stores its layer's individual + CSS transform and the accumulation of CSS transforms to its parent APZC. So the APZC for + layer L would store LC and (MC * NC * OC), and the layer P would store PC and (QC * RC). + The APZC instances track the last dispatched paint request and so are able to calculate LD and + PD using those internally stored values. + The APZCs also obviously have LT, LN, PT, and PN, so all of the above transformation combinations + required can be generated. + */ +// clang-format on + +/* + * See the long comment above for a detailed explanation of this function. + */ +ScreenToParentLayerMatrix4x4 APZCTreeManager::GetScreenToApzcTransform( + const AsyncPanZoomController* aApzc) const { + Matrix4x4 result; + RecursiveMutexAutoLock lock(mTreeLock); + + // The comments below assume there is a chain of layers L..R with L and P + // having APZC instances as explained in the comment above. This function is + // called with aApzc at L, and the loop below performs one iteration, where + // parent is at P. The comments explain what values are stored in the + // variables at these two levels. All the comments use standard matrix + // notation where the leftmost matrix in a multiplication is applied first. + + // ancestorUntransform is PC.Inverse() * OC.Inverse() * NC.Inverse() * + // MC.Inverse() + Matrix4x4 ancestorUntransform = aApzc->GetAncestorTransform().Inverse(); + + // result is initialized to PC.Inverse() * OC.Inverse() * NC.Inverse() * + // MC.Inverse() + result = ancestorUntransform; + + for (AsyncPanZoomController* parent = aApzc->GetParent(); parent; + parent = parent->GetParent()) { + // ancestorUntransform is updated to RC.Inverse() * QC.Inverse() when parent + // == P + ancestorUntransform = parent->GetAncestorTransform().Inverse(); + // asyncUntransform is updated to PA.Inverse() when parent == P + Matrix4x4 asyncUntransform = parent + ->GetCurrentAsyncTransformWithOverscroll( + AsyncPanZoomController::eForHitTesting) + .Inverse() + .ToUnknownMatrix(); + // untransformSinceLastApzc is RC.Inverse() * QC.Inverse() * PA.Inverse() + Matrix4x4 untransformSinceLastApzc = ancestorUntransform * asyncUntransform; + + // result is RC.Inverse() * QC.Inverse() * PA.Inverse() * PC.Inverse() * + // OC.Inverse() * NC.Inverse() * MC.Inverse() + result = untransformSinceLastApzc * result; + + // The above value for result when parent == P matches the required output + // as explained in the comment above this method. Note that any missing + // terms are guaranteed to be identity transforms. + } + + return ViewAs(result); +} + +/* + * See the long comment above GetScreenToApzcTransform() for a detailed + * explanation of this function. + */ +ParentLayerToScreenMatrix4x4 APZCTreeManager::GetApzcToGeckoTransform( + const AsyncPanZoomController* aApzc, + const AsyncTransformComponents& aComponents) const { + Matrix4x4 result; + RecursiveMutexAutoLock lock(mTreeLock); + + // The comments below assume there is a chain of layers L..R with L and P + // having APZC instances as explained in the comment above. This function is + // called with aApzc at L, and the loop below performs one iteration, where + // parent is at P. The comments explain what values are stored in the + // variables at these two levels. All the comments use standard matrix + // notation where the leftmost matrix in a multiplication is applied first. + + // asyncUntransform is LA.Inverse() + Matrix4x4 asyncUntransform = + aApzc + ->GetCurrentAsyncTransformWithOverscroll( + AsyncPanZoomController::eForHitTesting, aComponents) + .Inverse() + .ToUnknownMatrix(); + + // aTransformToGeckoOut is initialized to LA.Inverse() * LD * MC * NC * OC * + // PC + result = asyncUntransform * + aApzc->GetTransformToLastDispatchedPaint(aComponents) * + aApzc->GetAncestorTransform(); + + for (AsyncPanZoomController* parent = aApzc->GetParent(); parent; + parent = parent->GetParent()) { + // aTransformToGeckoOut is LA.Inverse() * LD * MC * NC * OC * PC * PD * QC * + // RC + // + // Note: Do not pass the async transform components for the current target + // to the parent. + result = result * + parent->GetTransformToLastDispatchedPaint(LayoutAndVisual) * + parent->GetAncestorTransform(); + + // The above value for result when parent == P matches the required output + // as explained in the comment above this method. Note that any missing + // terms are guaranteed to be identity transforms. + } + + return ViewAs(result); +} + +ParentLayerToScreenMatrix4x4 APZCTreeManager::GetApzcToGeckoTransformForHit( + HitTestResult& aHitResult) const { + // Fixed content is only subject to the visual component of the async + // transform. + AsyncTransformComponents components = + aHitResult.mFixedPosSides == SideBits::eNone + ? LayoutAndVisual + : AsyncTransformComponents{AsyncTransformComponent::eVisual}; + return GetApzcToGeckoTransform(aHitResult.mTargetApzc, components); +} + +ScreenPoint APZCTreeManager::GetCurrentMousePosition() const { + auto pos = mCurrentMousePosition.Lock(); + return pos.ref(); +} + +void APZCTreeManager::SetCurrentMousePosition(const ScreenPoint& aNewPos) { + auto pos = mCurrentMousePosition.Lock(); + pos.ref() = aNewPos; +} + +static AsyncPanZoomController* GetApzcWithDifferentLayersIdByWalkingParents( + AsyncPanZoomController* aApzc) { + if (!aApzc) { + return nullptr; + } + AsyncPanZoomController* parent = aApzc->GetParent(); + while (parent && (parent->GetLayersId() == aApzc->GetLayersId())) { + parent = parent->GetParent(); + } + return parent; +} + +already_AddRefed APZCTreeManager::GetZoomableTarget( + AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const { + RecursiveMutexAutoLock lock(mTreeLock); + RefPtr apzc; + // For now, we only ever want to do pinching on the root-content APZC for + // a given layers id. + if (aApzc1 && aApzc2 && aApzc1->GetLayersId() == aApzc2->GetLayersId()) { + // If the two APZCs have the same layers id, find the root-content APZC + // for that layers id. Don't call CommonAncestor() because there may not + // be a common ancestor for the layers id (e.g. if one APZCs is inside a + // fixed-position element). + apzc = FindRootContentApzcForLayersId(aApzc1->GetLayersId()); + if (apzc) { + return apzc.forget(); + } + } + + // Otherwise, find the common ancestor (to reach a common layers id), and then + // walk up the apzc tree until we find a root-content APZC. + apzc = CommonAncestor(aApzc1, aApzc2); + RefPtr zoomable; + while (apzc && !zoomable) { + zoomable = FindRootContentApzcForLayersId(apzc->GetLayersId()); + apzc = GetApzcWithDifferentLayersIdByWalkingParents(apzc); + } + + return zoomable.forget(); +} + +Maybe APZCTreeManager::ConvertToGecko( + const ScreenIntPoint& aPoint, AsyncPanZoomController* aApzc) { + RecursiveMutexAutoLock lock(mTreeLock); + // TODO: The current check assumes that a touch gesture and a touchpad tap + // gesture can't both be active at the same time. If we turn on double-tap- + // to-zoom on a touchscreen platform like Windows or Linux, this assumption + // would no longer be valid, and we'd have to instead have TapGestureInput + // track and inform this function whether it was created from touch events. + const HitTestResult& hit = mInputQueue->GetCurrentTouchBlock() + ? mTouchBlockHitResult + : mTapGestureHitResult; + AsyncTransformComponents components = + hit.mFixedPosSides == SideBits::eNone + ? LayoutAndVisual + : AsyncTransformComponents{AsyncTransformComponent::eVisual}; + ScreenToScreenMatrix4x4 transformScreenToGecko = + GetScreenToApzcTransform(aApzc) * + GetApzcToGeckoTransform(aApzc, components); + Maybe geckoPoint = + UntransformBy(transformScreenToGecko, aPoint); + if (geckoPoint) { + AdjustEventPointForDynamicToolbar(*geckoPoint, hit); + } + return geckoPoint; +} + +already_AddRefed APZCTreeManager::CommonAncestor( + AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const { + mTreeLock.AssertCurrentThreadIn(); + RefPtr ancestor; + + // If either aApzc1 or aApzc2 is null, min(depth1, depth2) will be 0 and this + // function will return null. + + // Calculate depth of the APZCs in the tree + int depth1 = 0, depth2 = 0; + for (AsyncPanZoomController* parent = aApzc1; parent; + parent = parent->GetParent()) { + depth1++; + } + for (AsyncPanZoomController* parent = aApzc2; parent; + parent = parent->GetParent()) { + depth2++; + } + + // At most one of the following two loops will be executed; the deeper APZC + // pointer will get walked up to the depth of the shallower one. + int minDepth = depth1 < depth2 ? depth1 : depth2; + while (depth1 > minDepth) { + depth1--; + aApzc1 = aApzc1->GetParent(); + } + while (depth2 > minDepth) { + depth2--; + aApzc2 = aApzc2->GetParent(); + } + + // Walk up the ancestor chains of both APZCs, always staying at the same depth + // for either APZC, and return the the first common ancestor encountered. + while (true) { + if (aApzc1 == aApzc2) { + ancestor = aApzc1; + break; + } + if (depth1 <= 0) { + break; + } + aApzc1 = aApzc1->GetParent(); + aApzc2 = aApzc2->GetParent(); + } + return ancestor.forget(); +} + +bool APZCTreeManager::IsFixedToRootContent( + const HitTestingTreeNode* aNode) const { + MutexAutoLock lock(mMapLock); + return IsFixedToRootContent(FixedPositionInfo(aNode), lock); +} + +bool APZCTreeManager::IsFixedToRootContent( + const FixedPositionInfo& aFixedInfo, + const MutexAutoLock& aProofOfMapLock) const { + ScrollableLayerGuid::ViewID fixedTarget = aFixedInfo.mFixedPosTarget; + if (fixedTarget == ScrollableLayerGuid::NULL_SCROLL_ID) { + return false; + } + auto it = + mApzcMap.find(ScrollableLayerGuid(aFixedInfo.mLayersId, 0, fixedTarget)); + if (it == mApzcMap.end()) { + return false; + } + RefPtr targetApzc = it->second.apzc; + return targetApzc && targetApzc->IsRootContent(); +} + +SideBits APZCTreeManager::SidesStuckToRootContent( + const HitTestingTreeNode* aNode) const { + MutexAutoLock lock(mMapLock); + return SidesStuckToRootContent(StickyPositionInfo(aNode), lock); +} + +SideBits APZCTreeManager::SidesStuckToRootContent( + const StickyPositionInfo& aStickyInfo, + const MutexAutoLock& aProofOfMapLock) const { + SideBits result = SideBits::eNone; + + ScrollableLayerGuid::ViewID stickyTarget = aStickyInfo.mStickyPosTarget; + if (stickyTarget == ScrollableLayerGuid::NULL_SCROLL_ID) { + return result; + } + + // We support the dynamic toolbar at top and bottom. + if ((aStickyInfo.mFixedPosSides & SideBits::eTopBottom) == SideBits::eNone) { + return result; + } + + auto it = mApzcMap.find( + ScrollableLayerGuid(aStickyInfo.mLayersId, 0, stickyTarget)); + if (it == mApzcMap.end()) { + return result; + } + RefPtr stickyTargetApzc = it->second.apzc; + if (!stickyTargetApzc || !stickyTargetApzc->IsRootContent()) { + return result; + } + + ParentLayerPoint translation = + stickyTargetApzc + ->GetCurrentAsyncTransform( + AsyncPanZoomController::eForHitTesting, + AsyncTransformComponents{AsyncTransformComponent::eLayout}) + .mTranslation; + + if (apz::IsStuckAtTop(translation.y, aStickyInfo.mStickyScrollRangeInner, + aStickyInfo.mStickyScrollRangeOuter)) { + result |= SideBits::eTop; + } + if (apz::IsStuckAtBottom(translation.y, aStickyInfo.mStickyScrollRangeInner, + aStickyInfo.mStickyScrollRangeOuter)) { + result |= SideBits::eBottom; + } + return result; +} + +LayerToParentLayerMatrix4x4 APZCTreeManager::ComputeTransformForNode( + const HitTestingTreeNode* aNode) const { + mTreeLock.AssertCurrentThreadIn(); + // The async transforms applied here for hit-testing purposes, are intended + // to match the ones AsyncCompositionManager (or equivalent WebRender code) + // applies for rendering purposes. + // Note that with containerless scrolling, the layer structure looks like + // this: + // + // root container layer + // async zoom container layer + // scrollable content layers (with scroll metadata) + // fixed content layers (no scroll metadta, annotated isFixedPosition) + // scrollbar layers + // + // The intended async transforms in this case are: + // * On the async zoom container layer, the "visual" portion of the root + // content APZC's async transform (which includes the zoom, and async + // scrolling of the visual viewport relative to the layout viewport). + // * On the scrollable layers bearing the root content APZC's scroll + // metadata, the "layout" portion of the root content APZC's async + // transform (which includes async scrolling of the layout viewport + // relative to the scrollable rect origin). + if (AsyncPanZoomController* apzc = aNode->GetApzc()) { + // If the node represents scrollable content, apply the async transform + // from its APZC. + bool visualTransformIsInheritedFromAncestor = + /* we're the APZC whose visual transform might be on the async + zoom container */ + apzc->IsRootContent() && + /* there is an async zoom container on this subtree */ + mAsyncZoomContainerSubtree == Some(aNode->GetLayersId()) && + /* it's not us */ + !aNode->GetAsyncZoomContainerId(); + AsyncTransformComponents components = + visualTransformIsInheritedFromAncestor + ? AsyncTransformComponents{AsyncTransformComponent::eLayout} + : LayoutAndVisual; + return aNode->GetTransform() * + CompleteAsyncTransform(apzc->GetCurrentAsyncTransformWithOverscroll( + AsyncPanZoomController::eForHitTesting, components)); + } else if (aNode->GetAsyncZoomContainerId()) { + if (AsyncPanZoomController* rootContent = + FindRootContentApzcForLayersId(aNode->GetLayersId())) { + return aNode->GetTransform() * + CompleteAsyncTransform( + rootContent->GetCurrentAsyncTransformWithOverscroll( + AsyncPanZoomController::eForHitTesting, + {AsyncTransformComponent::eVisual})); + } + } else if (aNode->IsScrollThumbNode()) { + // If the node represents a scrollbar thumb, compute and apply the + // transformation that will be applied to the thumb in + // AsyncCompositionManager. + ScrollableLayerGuid guid{aNode->GetLayersId(), 0, + aNode->GetScrollTargetId()}; + if (RefPtr scrollTargetNode = GetTargetNode( + guid, &ScrollableLayerGuid::EqualsIgnoringPresShell)) { + AsyncPanZoomController* scrollTargetApzc = scrollTargetNode->GetApzc(); + MOZ_ASSERT(scrollTargetApzc); + return scrollTargetApzc->CallWithLastContentPaintMetrics( + [&](const FrameMetrics& aMetrics) { + return ComputeTransformForScrollThumb( + aNode->GetTransform() * AsyncTransformMatrix(), + scrollTargetNode->GetTransform().ToUnknownMatrix(), + scrollTargetApzc, aMetrics, aNode->GetScrollbarData(), + scrollTargetNode->IsAncestorOf(aNode)); + }); + } + } else if (IsFixedToRootContent(aNode)) { + ParentLayerPoint translation; + { + MutexAutoLock mapLock(mMapLock); + translation = ViewAs( + apz::ComputeFixedMarginsOffset( + GetCompositorFixedLayerMargins(mapLock), + aNode->GetFixedPosSides(), mGeckoFixedLayerMargins), + PixelCastJustification::ScreenIsParentLayerForRoot); + } + return aNode->GetTransform() * + CompleteAsyncTransform( + AsyncTransformComponentMatrix::Translation(translation)); + } + SideBits sides = SidesStuckToRootContent(aNode); + if (sides != SideBits::eNone) { + ParentLayerPoint translation; + { + MutexAutoLock mapLock(mMapLock); + translation = ViewAs( + apz::ComputeFixedMarginsOffset( + GetCompositorFixedLayerMargins(mapLock), sides, + // For sticky layers, we don't need to factor + // mGeckoFixedLayerMargins because Gecko doesn't shift the + // position of sticky elements for dynamic toolbar movements. + ScreenMargin()), + PixelCastJustification::ScreenIsParentLayerForRoot); + } + return aNode->GetTransform() * + CompleteAsyncTransform( + AsyncTransformComponentMatrix::Translation(translation)); + } + // Otherwise, the node does not have an async transform. + return aNode->GetTransform() * AsyncTransformMatrix(); +} + +already_AddRefed APZCTreeManager::GetWebRenderAPI() const { + RefPtr api; + CompositorBridgeParent::CallWithIndirectShadowTree( + mRootLayersId, [&](LayerTreeState& aState) -> void { + if (aState.mWrBridge) { + api = aState.mWrBridge->GetWebRenderAPI(); + } + }); + return api.forget(); +} + +/*static*/ +already_AddRefed APZCTreeManager::GetContentController( + LayersId aLayersId) { + RefPtr controller; + CompositorBridgeParent::CallWithIndirectShadowTree( + aLayersId, + [&](LayerTreeState& aState) -> void { controller = aState.mController; }); + return controller.forget(); +} + +ScreenMargin APZCTreeManager::GetCompositorFixedLayerMargins( + const MutexAutoLock& aProofOfMapLock) const { + ScreenMargin result = mCompositorFixedLayerMargins; + if (StaticPrefs::apz_fixed_margin_override_enabled()) { + result.top = StaticPrefs::apz_fixed_margin_override_top(); + result.bottom = StaticPrefs::apz_fixed_margin_override_bottom(); + } + return result; +} + +bool APZCTreeManager::GetAPZTestData(LayersId aLayersId, + APZTestData* aOutData) { + AssertOnUpdaterThread(); + + { // copy the relevant test data into aOutData while holding the + // mTestDataLock + MutexAutoLock lock(mTestDataLock); + auto it = mTestData.find(aLayersId); + if (it == mTestData.end()) { + return false; + } + *aOutData = *(it->second); + } + + { // add some additional "current state" into the returned APZTestData + MutexAutoLock mapLock(mMapLock); + + ClippedCompositionBoundsMap clippedCompBounds; + for (const auto& mapping : mApzcMap) { + if (mapping.first.mLayersId != aLayersId) { + continue; + } + + ParentLayerRect clippedBounds = ComputeClippedCompositionBounds( + mapLock, clippedCompBounds, mapping.first); + AsyncPanZoomController* apzc = mapping.second.apzc; + std::string viewId = std::to_string(mapping.first.mScrollId); + std::string apzcState; + if (apzc->GetCheckerboardMagnitude(clippedBounds)) { + apzcState += "checkerboarding,"; + } + if (apzc->IsOverscrolled()) { + apzcState += "overscrolled,"; + } + aOutData->RecordAdditionalData(viewId, apzcState); + } + } + return true; +} + +void APZCTreeManager::SendSubtreeTransformsToChromeMainThread( + const AsyncPanZoomController* aAncestor) { + RefPtr controller = + GetContentController(mRootLayersId); + if (!controller) { + return; + } + nsTArray messages; + bool underAncestor = (aAncestor == nullptr); + bool shouldNotify = false; + { + RecursiveMutexAutoLock lock(mTreeLock); + if (!mRootNode) { + // Event dispatched during shutdown, after ClearTree(). + // Note, mRootNode needs to be checked with mTreeLock held. + return; + } + // This formulation duplicates matrix multiplications closer + // to the root of the tree. For now, aiming for separation + // of concerns rather than minimum number of multiplications. + ForEachNode( + mRootNode.get(), + [&](HitTestingTreeNode* aNode) { + mTreeLock.AssertCurrentThreadIn(); + bool atAncestor = (aAncestor && aNode->GetApzc() == aAncestor); + MOZ_ASSERT(!(underAncestor && atAncestor)); + underAncestor |= atAncestor; + if (!underAncestor) { + return; + } + LayersId layersId = aNode->GetLayersId(); + HitTestingTreeNode* parent = aNode->GetParent(); + if (!parent) { + messages.AppendElement(MatrixMessage(Some(LayerToScreenMatrix4x4()), + ScreenRect(), layersId)); + } else if (layersId != parent->GetLayersId()) { + if (mDetachedLayersIds.find(layersId) != mDetachedLayersIds.end()) { + messages.AppendElement( + MatrixMessage(Nothing(), ScreenRect(), layersId)); + } else { + messages.AppendElement(MatrixMessage( + Some(parent->GetTransformToGecko()), + parent->GetRemoteDocumentScreenRect(), layersId)); + } + } + }, + [&](HitTestingTreeNode* aNode) { + bool atAncestor = (aAncestor && aNode->GetApzc() == aAncestor); + if (atAncestor) { + MOZ_ASSERT(underAncestor); + underAncestor = false; + } + }); + if (messages != mLastMessages) { + mLastMessages = messages; + shouldNotify = true; + } + } + if (shouldNotify) { + controller->NotifyLayerTransforms(std::move(messages)); + } +} + +void APZCTreeManager::SetFixedLayerMargins(ScreenIntCoord aTop, + ScreenIntCoord aBottom) { + MutexAutoLock lock(mMapLock); + mCompositorFixedLayerMargins.top = aTop; + mCompositorFixedLayerMargins.bottom = aBottom; +} + +/*static*/ +LayerToParentLayerMatrix4x4 APZCTreeManager::ComputeTransformForScrollThumb( + const LayerToParentLayerMatrix4x4& aCurrentTransform, + const Matrix4x4& aScrollableContentTransform, AsyncPanZoomController* aApzc, + const FrameMetrics& aMetrics, const ScrollbarData& aScrollbarData, + bool aScrollbarIsDescendant) { + return apz::ComputeTransformForScrollThumb( + aCurrentTransform, aScrollableContentTransform, aApzc, aMetrics, + aScrollbarData, aScrollbarIsDescendant); +} + +APZSampler* APZCTreeManager::GetSampler() const { + // We should always have a sampler here, since in practice the sampler + // is destroyed at the same time that this APZCTreeMAnager instance is. + MOZ_ASSERT(mSampler); + return mSampler; +} + +void APZCTreeManager::AssertOnSamplerThread() { + GetSampler()->AssertOnSamplerThread(); +} + +APZUpdater* APZCTreeManager::GetUpdater() const { + // We should always have an updater here, since in practice the updater + // is destroyed at the same time that this APZCTreeManager instance is. + MOZ_ASSERT(mUpdater); + return mUpdater; +} + +void APZCTreeManager::AssertOnUpdaterThread() { + GetUpdater()->AssertOnUpdaterThread(); +} + +MOZ_PUSH_IGNORE_THREAD_SAFETY +void APZCTreeManager::LockTree() { + AssertOnUpdaterThread(); + mTreeLock.Lock(); +} + +void APZCTreeManager::UnlockTree() { + AssertOnUpdaterThread(); + mTreeLock.Unlock(); +} +MOZ_POP_THREAD_SAFETY + +void APZCTreeManager::SetDPI(float aDpiValue) { + if (!APZThreadUtils::IsControllerThread()) { + APZThreadUtils::RunOnControllerThread( + NewRunnableMethod("layers::APZCTreeManager::SetDPI", this, + &APZCTreeManager::SetDPI, aDpiValue)); + return; + } + + APZThreadUtils::AssertOnControllerThread(); + mDPI = aDpiValue; +} + +float APZCTreeManager::GetDPI() const { + APZThreadUtils::AssertOnControllerThread(); + return mDPI; +} + +APZCTreeManager::FixedPositionInfo::FixedPositionInfo( + const HitTestingTreeNode* aNode) { + mFixedPositionAnimationId = aNode->GetFixedPositionAnimationId(); + mFixedPosSides = aNode->GetFixedPosSides(); + mFixedPosTarget = aNode->GetFixedPosTarget(); + mLayersId = aNode->GetLayersId(); +} + +APZCTreeManager::StickyPositionInfo::StickyPositionInfo( + const HitTestingTreeNode* aNode) { + mStickyPositionAnimationId = aNode->GetStickyPositionAnimationId(); + mFixedPosSides = aNode->GetFixedPosSides(); + mStickyPosTarget = aNode->GetStickyPosTarget(); + mLayersId = aNode->GetLayersId(); + mStickyScrollRangeInner = aNode->GetStickyScrollRangeInner(); + mStickyScrollRangeOuter = aNode->GetStickyScrollRangeOuter(); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/APZCTreeManager.h b/gfx/layers/apz/src/APZCTreeManager.h new file mode 100644 index 0000000000..fb0e98e350 --- /dev/null +++ b/gfx/layers/apz/src/APZCTreeManager.h @@ -0,0 +1,1064 @@ +/* -*- 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_APZCTreeManager_h +#define mozilla_layers_APZCTreeManager_h + +#include // for std::unordered_map + +#include "FocusState.h" // for FocusState +#include "HitTestingTreeNode.h" // for HitTestingTreeNodeAutoLock +#include "IAPZHitTester.h" // for IAPZHitTester::HitTestResult +#include "gfxPoint.h" // for gfxPoint +#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER2 +#include "mozilla/DataMutex.h" // for DataMutex +#include "mozilla/gfx/CompositorHitTestInfo.h" +#include "mozilla/gfx/Logging.h" // for gfx::TreeLog +#include "mozilla/gfx/Matrix.h" // for Matrix4x4 +#include "mozilla/layers/APZInputBridge.h" // for APZInputBridge +#include "mozilla/layers/APZTestData.h" // for APZTestData +#include "mozilla/layers/APZUtils.h" // for GeckoViewMetrics +#include "mozilla/layers/IAPZCTreeManager.h" // for IAPZCTreeManager +#include "mozilla/layers/ScrollbarData.h" +#include "mozilla/layers/LayersTypes.h" +#include "mozilla/layers/KeyboardMap.h" // for KeyboardMap +#include "mozilla/layers/TouchCounter.h" // for TouchCounter +#include "mozilla/layers/ZoomConstraints.h" // for ZoomConstraints +#include "mozilla/webrender/webrender_ffi.h" +#include "mozilla/RecursiveMutex.h" // for RecursiveMutex +#include "mozilla/RefPtr.h" // for RefPtr +#include "mozilla/TimeStamp.h" // for mozilla::TimeStamp +#include "mozilla/UniquePtr.h" // for UniquePtr +#include "nsCOMPtr.h" // for already_AddRefed +#include "nsTArray.h" + +namespace mozilla { +class MultiTouchInput; + +namespace wr { +class TransactionWrapper; +class WebRenderAPI; +} // namespace wr + +namespace layers { + +class Layer; +class AsyncPanZoomController; +class APZCTreeManagerParent; +class APZSampler; +class APZUpdater; +class CompositorBridgeParent; +class OverscrollHandoffChain; +struct OverscrollHandoffState; +class FocusTarget; +struct FlingHandoffState; +class InputQueue; +struct InputBlockCallbackInfo; +class GeckoContentController; +class HitTestingTreeNode; +class SampleTime; +class WebRenderScrollDataWrapper; +struct AncestorTransform; +struct ScrollThumbData; +struct ZoomTarget; + +/** + * ****************** NOTE ON LOCK ORDERING IN APZ ************************** + * + * To avoid deadlock, APZ imposes and respects a global ordering on threads + * and locks relevant to APZ. + * + * Please see the "Threading / Locking Overview" section of + * gfx/docs/AsyncPanZoom.rst (hosted in rendered form at + * https://firefox-source-docs.mozilla.org/gfx/gfx/AsyncPanZoom.html#threading-locking-overview) + * for what the ordering is, and what are the rules for respecting it. + * ************************************************************************** + */ + +/** + * This class manages the tree of AsyncPanZoomController instances. There is one + * instance of this class owned by each CompositorBridgeParent, and it contains + * as many AsyncPanZoomController instances as there are scrollable container + * layers. This class generally lives on the updater thread, although some + * functions may be called from other threads as noted; thread safety is ensured + * internally. + * + * The bulk of the work of this class happens as part of the + * UpdateHitTestingTree function, which is when a layer tree update is received + * by the compositor. This function walks through the layer tree and creates a + * tree of HitTestingTreeNode instances to match the layer tree and for use in + * hit-testing on the controller thread. APZC instances may be preserved across + * calls to this function if the corresponding layers are still present in the + * layer tree. + * + * The other functions on this class are used by various pieces of client code + * to notify the APZC instances of events relevant to them. This includes, for + * example, user input events that drive panning and zooming, changes to the + * scroll viewport area, and changes to pan/zoom constraints. + * + * Note that the ClearTree function MUST be called when this class is no longer + * needed; see the method documentation for details. + * + * Behaviour of APZ is controlled by a number of preferences shown + * \ref APZCPrefs "here". + */ +class APZCTreeManager : public IAPZCTreeManager, public APZInputBridge { + typedef mozilla::layers::AllowedTouchBehavior AllowedTouchBehavior; + typedef mozilla::layers::AsyncDragMetrics AsyncDragMetrics; + using HitTestResult = IAPZHitTester::HitTestResult; + + /** + * A result from APZCTreeManager::FindHandoffParent. + */ + struct TargetApzcForNodeResult { + // The APZC to handoff overscroll to. + AsyncPanZoomController* mApzc; + // Targeting a document's root APZC from content fixed to the document. + bool mIsFixed; + }; + + // Helper struct to hold some state while we build the hit-testing tree. The + // sole purpose of this struct is to shorten the argument list to + // UpdateHitTestingTree. All the state that we don't need to + // push on the stack during recursion and pop on unwind is stored here. + struct TreeBuildingState; + + public: + explicit APZCTreeManager(LayersId aRootLayersId, + UniquePtr aHitTester = nullptr); + + static mozilla::LazyLogModule sLog; + + void SetSampler(APZSampler* aSampler); + void SetUpdater(APZUpdater* aUpdater); + + /** + * Notifies this APZCTreeManager that the associated compositor is now + * responsible for managing another layers id, which got moved over from + * some other compositor. That other compositor's APZCTreeManager is also + * provided. This allows APZCTreeManager to transfer any necessary state + * from the old APZCTreeManager related to that layers id. + * This function must be called on the updater thread. + */ + void NotifyLayerTreeAdopted(LayersId aLayersId, + const RefPtr& aOldTreeManager); + + /** + * Notifies this APZCTreeManager that a layer tree being managed by the + * associated compositor has been removed/destroyed. Note that this does + * NOT get called during shutdown situations, when the root layer tree is + * also getting destroyed. + * This function must be called on the updater thread. + */ + void NotifyLayerTreeRemoved(LayersId aLayersId); + + /** + * Rebuild the focus state based on the focus target from the layer tree + * update that just occurred. This must be called on the updater thread. + * + * @param aRootLayerTreeId The layer tree ID of the root layer corresponding + * to this APZCTreeManager + * @param aOriginatingLayersId The layer tree ID of the layer corresponding to + * this layer tree update. + */ + void UpdateFocusState(LayersId aRootLayerTreeId, + LayersId aOriginatingLayersId, + const FocusTarget& aFocusTarget); + + /** + * Rebuild the hit-testing tree based on an incoming WebRender transaction. + * Preserve nodes and APZC instances where possible, but retire those whose + * layers are no longer in the layer tree. + * (Note: "layer tree" here refers to the tree of WebRenderLayerScrollData + * nodes sent as part of a WebRender transaction.) + * + * This must be called on the updater thread. + * + * @param aRoot The root of the (full) layer tree + * @param aOriginatingLayersId The layers id of the subtree that triggered + * this repaint, and to which aIsFirstPaint + * applies. + * @param aIsFirstPaint True if the transaction that this is called in + * response to included a first-paint. If this is true, + * the part of the tree that is affected by the + * first-paint flag is indicated by the + * aOriginatingLayersId parameter. + * @param aPaintSequenceNumber The sequence number of the paint that triggered + * this layer update. Note that every child + * process' layer subtree has its own sequence + * numbers. + */ + void UpdateHitTestingTree(const WebRenderScrollDataWrapper& aRoot, + bool aIsFirstPaint, LayersId aOriginatingLayersId, + uint32_t aPaintSequenceNumber); + + /** + * Called when webrender is enabled, from the sampler thread. This function + * populates the provided transaction with any async scroll offsets needed. + * It also advances APZ animations to the specified sample time, and requests + * another composite if there are still active animations. + * In effect it is the webrender equivalent of (part of) the code in + * AsyncCompositionManager. + */ + void SampleForWebRender(const Maybe& aVsyncId, + wr::TransactionWrapper& aTxn, + const SampleTime& aSampleTime); + + /** + * Refer to the documentation of APZInputBridge::ReceiveInputEvent() and + * APZEventResult. + */ + APZEventResult ReceiveInputEvent( + InputData& aEvent, + InputBlockCallback&& aCallback = InputBlockCallback()) override; + + /** + * Set the keyboard shortcuts to use for translating keyboard events. + */ + void SetKeyboardMap(const KeyboardMap& aKeyboardMap) override; + + /** + * Kicks an animation to zoom to a rect. This may be either a zoom out or zoom + * in. The actual animation is done on the sampler thread after being set + * up. |aRect| must be given in CSS pixels, relative to the document. + * |aFlags| is a combination of the ZoomToRectBehavior enum values. + */ + void ZoomToRect(const ScrollableLayerGuid& aGuid, + const ZoomTarget& aZoomTarget, + const uint32_t aFlags = DEFAULT_BEHAVIOR) override; + + /** + * If we have touch listeners, this should always be called when we know + * definitively whether or not content has preventDefaulted any touch events + * that have come in. If |aPreventDefault| is true, any touch events in the + * queue will be discarded. This function must be called on the controller + * thread. + */ + void ContentReceivedInputBlock(uint64_t aInputBlockId, + bool aPreventDefault) override; + + /** + * When the event regions code is enabled, this function should be invoked to + * to confirm the target of the input block. This is only needed in cases + * where the initial input event of the block hit a dispatch-to-content region + * but is safe to call for all input blocks. + * The different elements in the array of targets correspond to the targets + * for the different touch points. In the case where the touch point has no + * target, or the target is not a scrollable frame, the target's |mScrollId| + * should be set to ScrollableLayerGuid::NULL_SCROLL_ID. + * Note: For mouse events that start a scrollbar drag, both SetTargetAPZC() + * and StartScrollbarDrag() will be called, and the calls may happen + * in either order. That's fine - whichever arrives first will confirm + * the block, and StartScrollbarDrag() will fill in the drag metrics. + * If the block is confirmed before we have drag metrics, some events + * in the drag block may be handled as no-ops until the drag metrics + * arrive. + */ + void SetTargetAPZC(uint64_t aInputBlockId, + const nsTArray& aTargets) override; + + /** + * Updates any zoom constraints contained in the tag. + * If the |aConstraints| is Nothing() then previously-provided constraints for + * the given |aGuid| are cleared. + */ + void UpdateZoomConstraints( + const ScrollableLayerGuid& aGuid, + const Maybe& aConstraints) override; + + /** + * Calls Destroy() on all APZC instances attached to the tree, and resets the + * tree back to empty. This function must be called exactly once during the + * lifetime of this APZCTreeManager, when this APZCTreeManager is no longer + * needed. Failing to call this function may prevent objects from being freed + * properly. + * This must be called on the updater thread. + */ + void ClearTree(); + + /** + * Sets the dpi value used by all AsyncPanZoomControllers attached to this + * tree manager. + * DPI defaults to 160 if not set using SetDPI() at any point. + */ + void SetDPI(float aDpiValue) override; + + /** + * Returns the current dpi value in use. + */ + float GetDPI() const; + + /** + * Find the hit testing node for the scrollbar thumb that matches these + * drag metrics. Initializes aOutThumbNode with the node, if there is one. + */ + void FindScrollThumbNode(const AsyncDragMetrics& aDragMetrics, + LayersId aLayersId, + HitTestingTreeNodeAutoLock& aOutThumbNode); + + /** + * Sets allowed touch behavior values for current touch-session for specific + * input block (determined by aInputBlock). + * Should be invoked by the widget. Each value of the aValues arrays + * corresponds to the different touch point that is currently active. + * Must be called after receiving the TOUCH_START event that starts the + * touch-session. + */ + void SetAllowedTouchBehavior( + uint64_t aInputBlockId, + const nsTArray& aValues) override; + + void SetBrowserGestureResponse(uint64_t aInputBlockId, + BrowserGestureResponse aResponse) override; + + /** + * This is a callback for AsyncPanZoomController to call when it wants to + * scroll in response to a touch-move event, or when it needs to hand off + * overscroll to the next APZC. Note that because of scroll grabbing, the + * first APZC to scroll may not be the one that is receiving the touch events. + * + * |aPrev| is the APZC that received the touch events triggering the scroll + * (in the case of an initial scroll), or the last APZC to scroll (in the + * case of overscroll) + * |aStartPoint| and |aEndPoint| are in |aPrev|'s transformed screen + * coordinates (i.e. the same coordinates in which touch points are given to + * APZCs). The amount of (over)scroll is represented by two points rather + * than a displacement because with certain 3D transforms, the same + * displacement between different points in transformed coordinates can + * represent different displacements in untransformed coordinates. + * |aOverscrollHandoffChain| is the overscroll handoff chain used for + * determining the order in which scroll should be handed off between + * APZCs + * |aOverscrollHandoffChainIndex| is the next position in the overscroll + * handoff chain that should be scrolled. + * + * aStartPoint and aEndPoint will be modified depending on how much of the + * scroll each APZC consumes. This is to allow the sending APZC to go into + * an overscrolled state if no APZC further up in the handoff chain accepted + * the entire scroll. + * + * The function will return true if the entire scroll was consumed, and + * false otherwise. As this function also modifies aStartPoint and aEndPoint, + * when scroll is consumed, it should always the case that this function + * returns true if and only if IsZero(aStartPoint - aEndPoint), using the + * modified aStartPoint and aEndPoint after the function returns. + * + * The way this method works is best illustrated with an example. + * Consider three nested APZCs, A, B, and C, with C being the innermost one. + * Say B is scroll-grabbing. + * The touch events go to C because it's the innermost one (so e.g. taps + * should go through C), but the overscroll handoff chain is B -> C -> A + * because B is scroll-grabbing. + * For convenience I'll refer to the three APZC objects as A, B, and C, and + * to the tree manager object as TM. + * Here's what happens when C receives a touch-move event: + * - C.TrackTouch() calls TM.DispatchScroll() with index = 0. + * - TM.DispatchScroll() calls B.AttemptScroll() (since B is at index 0 in + * the chain). + * - B.AttemptScroll() scrolls B. If there is overscroll, it calls + * TM.DispatchScroll() with index = 1. + * - TM.DispatchScroll() calls C.AttemptScroll() (since C is at index 1 in + * the chain) + * - C.AttemptScroll() scrolls C. If there is overscroll, it calls + * TM.DispatchScroll() with index = 2. + * - TM.DispatchScroll() calls A.AttemptScroll() (since A is at index 2 in + * the chain) + * - A.AttemptScroll() scrolls A. If there is overscroll, it calls + * TM.DispatchScroll() with index = 3. + * - TM.DispatchScroll() discards the rest of the scroll as there are no + * more elements in the chain. + * + * Note: this should be used for panning only. For handing off overscroll for + * a fling, use DispatchFling(). + */ + bool DispatchScroll(AsyncPanZoomController* aPrev, + ParentLayerPoint& aStartPoint, + ParentLayerPoint& aEndPoint, + OverscrollHandoffState& aOverscrollHandoffState); + + /** + * This is a callback for AsyncPanZoomController to call when it wants to + * start a fling in response to a touch-end event, or when it needs to hand + * off a fling to the next APZC. Note that because of scroll grabbing, the + * first APZC to fling may not be the one that is receiving the touch events. + * + * @param aApzc the APZC that wants to start or hand off the fling + * @param aHandoffState a collection of state about the operation, + * which contains the following: + * + * mVelocity the current velocity of the fling, in |aApzc|'s screen + * pixels per millisecond + * mChain the chain of APZCs along which the fling + * should be handed off + * mIsHandoff is true if |aApzc| is handing off an existing fling (in + * this case the fling is given to the next APZC in the + * handoff chain after |aApzc|), and false is |aApzc| wants + * start a fling (in this case the fling is given to the + * first APZC in the chain) + * + * The return value is the "residual velocity", the portion of + * |aHandoffState.mVelocity| that was not consumed by APZCs in the + * handoff chain doing flings. + * The caller can use this value to determine whether it should consume + * the excess velocity by going into overscroll. + */ + ParentLayerPoint DispatchFling(AsyncPanZoomController* aApzc, + const FlingHandoffState& aHandoffState); + + void StartScrollbarDrag(const ScrollableLayerGuid& aGuid, + const AsyncDragMetrics& aDragMetrics) override; + + bool StartAutoscroll(const ScrollableLayerGuid& aGuid, + const ScreenPoint& aAnchorLocation) override; + + void StopAutoscroll(const ScrollableLayerGuid& aGuid) override; + + /* + * Build the chain of APZCs that will handle overscroll for a pan starting at + * |aInitialTarget|. + */ + RefPtr BuildOverscrollHandoffChain( + const RefPtr& aInitialTarget); + + /** + * Function used to disable LongTap gestures. + * + * On slow running tests, drags and touch events can be misinterpreted + * as a long tap. This allows tests to disable long tap gesture detection. + */ + void SetLongTapEnabled(bool aTapGestureEnabled) override; + + APZInputBridge* InputBridge() override { return this; } + + /** + * Add a callback to be invoked when |aInputBlockId| is ready for handling. + * + * Should only be used for input blocks that are not yet ready for handling + * at the time this is called. If the input block was already handled, + * the callback will never be called. + * + * Only one callback can be registered for an input block at a time. + * Subsequent attempts to register a callback for an input block will be + * ignored until the existing callback is triggered. + */ + void AddInputBlockCallback(uint64_t aInputBlockId, + InputBlockCallbackInfo&& aCallbackInfo); + + // Methods to help process WidgetInputEvents (or manage conversion to/from + // InputData) + + void ProcessUnhandledEvent(LayoutDeviceIntPoint* aRefPoint, + ScrollableLayerGuid* aOutTargetGuid, + uint64_t* aOutFocusSequenceNumber, + LayersId* aOutLayersId) override; + + void UpdateWheelTransaction( + LayoutDeviceIntPoint aRefPoint, EventMessage aEventMessage, + const Maybe& aTargetGuid) override; + + bool GetAPZTestData(LayersId aLayersId, APZTestData* aOutData); + + /** + * Iterates over the hit testing tree, collects LayersIds and associated + * transforms from layer coordinate space to root coordinate space, and + * sends these over to the main thread of the chrome process. If the provided + * |aAncestor| argument is non-null, then only the transforms for layer + * subtrees scrolled by the aAncestor (i.e. descendants of aAncestor) will be + * sent. + */ + void SendSubtreeTransformsToChromeMainThread( + const AsyncPanZoomController* aAncestor); + + /** + * Set fixed layer margins for dynamic toolbar. + */ + void SetFixedLayerMargins(ScreenIntCoord aTop, ScreenIntCoord aBottom); + + /** + * Refer to apz::ComputeTransformForScrollThumb() for a description + * of the parameters. + */ + static LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb( + const LayerToParentLayerMatrix4x4& aCurrentTransform, + const gfx::Matrix4x4& aScrollableContentTransform, + AsyncPanZoomController* aApzc, const FrameMetrics& aMetrics, + const ScrollbarData& aScrollbarData, bool aScrollbarIsDescendant); + + /** + * Dispatch a flush complete notification from the repaint thread of the + * content controller for the given layers id. + */ + static void FlushApzRepaints(LayersId aLayersId); + + /** + * Mark |aLayersId| as having been moved from the compositor that owns this + * tree manager to a compositor that doesn't use APZ. + * See |mDetachedLayersIds| for more details. + */ + void MarkAsDetached(LayersId aLayersId); + + // Assert that the current thread is the sampler thread for this APZCTM. + void AssertOnSamplerThread(); + // Assert that the current thread is the updater thread for this APZCTM. + void AssertOnUpdaterThread(); + + // Returns a pointer to the WebRenderAPI this APZCTreeManager is for. + // This might be null (for example, if WebRender is not enabled). + already_AddRefed GetWebRenderAPI() const; + + protected: + // Protected destructor, to discourage deletion outside of Release(): + virtual ~APZCTreeManager(); + + APZSampler* GetSampler() const; + APZUpdater* GetUpdater() const; + + // We need to allow APZUpdater to lock and unlock this tree during a WR + // scene swap. We do this using private helpers to avoid exposing these + // functions to the world. + private: + friend class APZUpdater; + void LockTree() MOZ_CAPABILITY_ACQUIRE(mTreeLock); + void UnlockTree() MOZ_CAPABILITY_RELEASE(mTreeLock); + + // Protected hooks for gtests subclass + virtual AsyncPanZoomController* NewAPZCInstance( + LayersId aLayersId, GeckoContentController* aController); + + public: + // Public hook for gtests subclass + virtual SampleTime GetFrameTime(); + + // Also used for controlling time during tests + void SetTestSampleTime(const Maybe& aTime); + + private: + mutable DataMutex> mTestSampleTime; + CopyableTArray mLastMessages; + + public: + /* Some helper functions to find an APZC given some identifying input. These + functions lock the tree of APZCs while they find the right one, and then + return an addref'd pointer to it. This allows caller code to just use the + target APZC without worrying about it going away. These are public for + testing code and generally should not be used by other production code. + */ + RefPtr GetRootNode() const; + HitTestResult GetTargetAPZC(const ScreenPoint& aPoint); + already_AddRefed GetTargetAPZC( + const LayersId& aLayersId, + const ScrollableLayerGuid::ViewID& aScrollId) const; + already_AddRefed GetTargetAPZC( + const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId, + const MutexAutoLock& aProofOfMapLock) const; + ScreenToParentLayerMatrix4x4 GetScreenToApzcTransform( + const AsyncPanZoomController* aApzc) const; + ParentLayerToScreenMatrix4x4 GetApzcToGeckoTransformForHit( + HitTestResult& aHitResult) const; + ParentLayerToScreenMatrix4x4 GetApzcToGeckoTransform( + const AsyncPanZoomController* aApzc, + const AsyncTransformComponents& aComponents) const; + ScreenPoint GetCurrentMousePosition() const; + void SetCurrentMousePosition(const ScreenPoint& aNewPos); + + /** + * Convert a screen point of an event targeting |aApzc| to Gecko + * coordinates. + */ + Maybe ConvertToGecko(const ScreenIntPoint& aPoint, + AsyncPanZoomController* aApzc); + + /** + * Find the zoomable APZC in the same layer subtree (i.e. with the same + * layers id) as the given APZC. + */ + already_AddRefed FindZoomableApzc( + AsyncPanZoomController* aStart) const; + + ScreenMargin GetCompositorFixedLayerMargins() const; + + void AdjustEventPointForDynamicToolbar(ScreenIntPoint& aEventPoint, + const HitTestResult& aHit); + + APZScrollGeneration NewAPZScrollGeneration() { + // In the production code this function gets only called from the sampler + // thread but in tests using nsIDOMWindowUtils.setAsyncScrollOffset this + // function gets called from the controller thread so we need to lock the + // mutex for this counter. + MutexAutoLock lock(mScrollGenerationLock); + return mScrollGenerationCounter.NewAPZGeneration(); + } + + template + void CallWithMapLock(Callback& aCallback) { + MutexAutoLock lock(mMapLock); + aCallback(lock); + } + + private: + using GuidComparator = ScrollableLayerGuid::Comparator; + using ScrollNode = WebRenderScrollDataWrapper; + + /* Helpers */ + + void AttachNodeToTree(HitTestingTreeNode* aNode, HitTestingTreeNode* aParent, + HitTestingTreeNode* aNextSibling) + MOZ_REQUIRES(mTreeLock); + already_AddRefed GetTargetAPZC( + const ScrollableLayerGuid& aGuid); + already_AddRefed GetTargetNode( + const ScrollableLayerGuid& aGuid, GuidComparator aComparator) const; + HitTestingTreeNode* FindTargetNode(HitTestingTreeNode* aNode, + const ScrollableLayerGuid& aGuid, + GuidComparator aComparator); + TargetApzcForNodeResult GetTargetApzcForNode(const HitTestingTreeNode* aNode); + TargetApzcForNodeResult FindHandoffParent( + const AsyncPanZoomController* aApzc); + HitTestingTreeNode* FindRootNodeForLayersId(LayersId aLayersId) const; + AsyncPanZoomController* FindRootContentApzcForLayersId( + LayersId aLayersId) const; + already_AddRefed GetZoomableTarget( + AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const; + already_AddRefed CommonAncestor( + AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const; + + struct FixedPositionInfo; + struct StickyPositionInfo; + + // Returns true if |aNode| is a fixed layer that is fixed to the root content + // APZC. + // The map lock is required within these functions; if the map lock is already + // being held by the caller, the second overload should be used. If the map + // lock is not being held at the call site, the first overload should be used. + bool IsFixedToRootContent(const HitTestingTreeNode* aNode) const; + bool IsFixedToRootContent(const FixedPositionInfo& aFixedInfo, + const MutexAutoLock& aProofOfMapLock) const; + + // Returns the vertical sides of |aNode| that are stuck to the root content. + // The map lock is required within these functions; if the map lock is already + // being held by the caller, the second overload should be used. If the map + // lock is not being held at the call site, the first overload should be used. + SideBits SidesStuckToRootContent(const HitTestingTreeNode* aNode) const; + SideBits SidesStuckToRootContent(const StickyPositionInfo& aStickyInfo, + const MutexAutoLock& aProofOfMapLock) const; + + /** + * Perform hit testing for a touch-start event. + * + * @param aEvent The touch-start event. + * + * The remaining parameters are out-parameter used to communicate additional + * return values: + * + * @param aOutTouchBehaviors + * The touch behaviours that should be allowed for this touch block. + + * @return The results of the hit test, including the APZC that was hit. + */ + HitTestResult GetTouchInputBlockAPZC( + const MultiTouchInput& aEvent, + nsTArray* aOutTouchBehaviors); + + /** + * A helper structure for use by ReceiveInputEvent() and its helpers. + */ + struct InputHandlingState { + // A reference to the event being handled. + InputData& mEvent; + + // The value that will be returned by ReceiveInputEvent(). + APZEventResult mResult; + + // If we performed a hit-test while handling this input event, or + // reused the result of a previous hit-test in the input block, + // this is populated with the result of the hit test. + HitTestResult mHit; + + // Called at the end of ReceiveInputEvent() to perform any final + // computations, and then return mResult. + // If the event will have a delayed result then this takes care of adding + // the specified callback to the APZCTreeManager. + APZEventResult Finish(APZCTreeManager& aTreeManager, + InputBlockCallback&& aCallback); + }; + + void ProcessTouchInput(InputHandlingState& aState, MultiTouchInput& aInput); + /** + * Given a mouse-down event that hit a scroll thumb node, set up APZ + * dragging of the scroll thumb. + * + * Must be called after the mouse event has been sent to InputQueue. + * + * @param aMouseInput The mouse-down event. + * @param aScrollThumbNode Tthe scroll thumb node that was hit. + * @param aApzc + * The APZC for the scroll frame scrolled by the scroll thumb, if that + * scroll frame is layerized. (A thumb can be layerized without its + * target scroll frame being layerized.) Otherwise, an enclosing APZC. + */ + void SetupScrollbarDrag(MouseInput& aMouseInput, + const HitTestingTreeNodeAutoLock& aScrollThumbNode, + AsyncPanZoomController* aApzc); + /** + * Process a touch event that's part of a scrollbar touch-drag gesture. + * + * @param aInput The touch event. + * @param aScrollThumbNode + * If this is the touch-start event, the node representing the scroll + * thumb we are starting to drag. Otherwise nullptr. + * @param aHitInfo + * The hit-test flags for the touch input. + * @return See ReceiveInputEvent() for what the return value means. + */ + APZEventResult ProcessTouchInputForScrollbarDrag( + MultiTouchInput& aInput, + const HitTestingTreeNodeAutoLock& aScrollThumbNode, + const gfx::CompositorHitTestInfo& aHitInfo); + void FlushRepaintsToClearScreenToGeckoTransform(); + + void SynthesizePinchGestureFromMouseWheel( + const ScrollWheelInput& aWheelInput, + const RefPtr& aTarget); + + already_AddRefed RecycleOrCreateNode( + const RecursiveMutexAutoLock& aProofOfTreeLock, TreeBuildingState& aState, + AsyncPanZoomController* aApzc, LayersId aLayersId); + HitTestingTreeNode* PrepareNodeForLayer( + const RecursiveMutexAutoLock& aProofOfTreeLock, const ScrollNode& aLayer, + const FrameMetrics& aMetrics, LayersId aLayersId, + const Maybe& aZoomConstraints, + const AncestorTransform& aAncestorTransform, HitTestingTreeNode* aParent, + HitTestingTreeNode* aNextSibling, TreeBuildingState& aState); + + void PrintLayerInfo(const ScrollNode& aLayer); + + void NotifyScrollbarDragInitiated(uint64_t aDragBlockId, + const ScrollableLayerGuid& aGuid, + ScrollDirection aDirection) const; + void NotifyScrollbarDragRejected(const ScrollableLayerGuid& aGuid) const; + void NotifyAutoscrollRejected(const ScrollableLayerGuid& aGuid) const; + + // Returns the transform that converts from |aNode|'s coordinates to + // the coordinates of |aNode|'s parent in the hit-testing tree. + // Requires the caller to hold mTreeLock. + LayerToParentLayerMatrix4x4 ComputeTransformForNode( + const HitTestingTreeNode* aNode) const MOZ_REQUIRES(mTreeLock); + + // Look up the GeckoContentController for the given layers id. + static already_AddRefed GetContentController( + LayersId aLayersId); + + bool AdvanceAnimationsInternal(const MutexAutoLock& aProofOfMapLock, + const SampleTime& aSampleTime); + + using ClippedCompositionBoundsMap = + std::unordered_map; + // This is a recursive function that populates `aDestMap` with the clipped + // composition bounds for the APZC corresponding to `aGuid` and returns those + // bounds as a convenience. It recurses to also populate `aDestMap` with that + // APZC's ancestors. In order to do this it needs to access mApzcMap + // and therefore requires the caller to hold the map lock. + ParentLayerRect ComputeClippedCompositionBounds( + const MutexAutoLock& aProofOfMapLock, + ClippedCompositionBoundsMap& aDestMap, ScrollableLayerGuid aGuid); + + ScreenMargin GetCompositorFixedLayerMargins( + const MutexAutoLock& aProofOfMapLock) const; + + protected: + /* The input queue where input events are held until we know enough to + * figure out where they're going. Protected so gtests can access it. + */ + RefPtr mInputQueue; + + private: + /* Layers id for the root CompositorBridgeParent that owns this + * APZCTreeManager. */ + LayersId mRootLayersId; + + /* Pointer to the APZSampler instance that is bound to this APZCTreeManager. + * The sampler has a RefPtr to this class, and this non-owning raw pointer + * back to the APZSampler is nulled out in the sampler's destructor, so this + * pointer should always be valid. + */ + APZSampler* MOZ_NON_OWNING_REF mSampler; + /* Pointer to the APZUpdater instance that is bound to this APZCTreeManager. + * The updater has a RefPtr to this class, and this non-owning raw pointer + * back to the APZUpdater is nulled out in the updater's destructor, so this + * pointer should always be valid. + */ + APZUpdater* MOZ_NON_OWNING_REF mUpdater; + + /* Whenever walking or mutating the tree rooted at mRootNode, mTreeLock must + * be held. This lock does not need to be held while manipulating a single + * APZC instance in isolation (that is, if its tree pointers are not being + * accessed or mutated). The lock also needs to be held when accessing the + * mRootNode instance variable, as that is considered part of the APZC tree + * management state. + * IMPORTANT: See the note about lock ordering at the top of this file. */ + mutable mozilla::RecursiveMutex mTreeLock; + RefPtr mRootNode MOZ_GUARDED_BY(mTreeLock); + + /* + * A set of LayersIds for which APZCTM should only send empty + * MatrixMessages via NotifyLayerTransform(). + * This is used in cases where a tab has been transferred to a non-APZ + * compositor (and thus will not receive MatrixMessages reflecting its new + * transforms) and we need to make sure it doesn't get stuck with transforms + * from its old tree manager (us). + * Acquire mTreeLock before accessing this. + */ + std::unordered_set mDetachedLayersIds + MOZ_GUARDED_BY(mTreeLock); + + /* If the current hit-testing tree contains an async zoom container + * node, this is set to the layers id of subtree that has the node. + */ + Maybe mAsyncZoomContainerSubtree; + + /** A lock that protects mApzcMap, mScrollThumbInfo, mRootScrollbarInfo, + * mFixedPositionInfo, and mStickyPositionInfo. + */ + mutable mozilla::Mutex mMapLock; + + /** + * Helper structure to store a bunch of things in mApzcMap so that they can + * be used from the sampler thread. + */ + struct ApzcMapData { + // A pointer to the APZC itself + RefPtr apzc; + // The parent APZC's guid, or Nothing() if there is no parent + Maybe parent; + }; + + /** + * A map for quick access to get some APZC data by guid, without having to + * acquire the tree lock. mMapLock must be acquired while accessing or + * modifying mApzcMap. + */ + std::unordered_map + mApzcMap; + /** + * A helper structure to store all the information needed to compute the + * async transform for a scrollthumb on the sampler thread. + */ + struct ScrollThumbInfo { + uint64_t mThumbAnimationId; + CSSTransformMatrix mThumbTransform; + ScrollbarData mThumbData; + ScrollableLayerGuid mTargetGuid; + CSSTransformMatrix mTargetTransform; + bool mTargetIsAncestor; + + ScrollThumbInfo(const uint64_t& aThumbAnimationId, + const CSSTransformMatrix& aThumbTransform, + const ScrollbarData& aThumbData, + const ScrollableLayerGuid& aTargetGuid, + const CSSTransformMatrix& aTargetTransform, + bool aTargetIsAncestor) + : mThumbAnimationId(aThumbAnimationId), + mThumbTransform(aThumbTransform), + mThumbData(aThumbData), + mTargetGuid(aTargetGuid), + mTargetTransform(aTargetTransform), + mTargetIsAncestor(aTargetIsAncestor) { + MOZ_ASSERT(mTargetGuid.mScrollId == mThumbData.mTargetViewId); + } + }; + /** + * If this APZCTreeManager is being used with WebRender, this vector gets + * populated during a layers update. It holds a package of information needed + * to compute and set the async transforms on scroll thumbs. This information + * is extracted from the HitTestingTreeNodes for the WebRender case because + * accessing the HitTestingTreeNodes requires holding the tree lock which + * we cannot do on the WR sampler thread. mScrollThumbInfo, however, can + * be accessed while just holding the mMapLock which is safe to do on the + * sampler thread. + * mMapLock must be acquired while accessing or modifying mScrollThumbInfo. + */ + std::vector mScrollThumbInfo; + + /** + * A helper structure to store all the information needed to compute the + * async transform for a scrollthumb on the sampler thread. + */ + struct RootScrollbarInfo { + uint64_t mScrollbarAnimationId; + ScrollDirection mScrollDirection; + + RootScrollbarInfo(const uint64_t& aScrollbarAnimationId, + const ScrollDirection aScrollDirection) + : mScrollbarAnimationId(aScrollbarAnimationId), + mScrollDirection(aScrollDirection) {} + }; + /** + * If this APZCTreeManager is being used with WebRender, this vector gets + * populated during a layers update. It holds a package of information needed + * to compute and set the async transforms on root scrollbars. This + * information is extracted from the HitTestingTreeNodes for the WebRender + * case because accessing the HitTestingTreeNodes requires holding the tree + * lock which we cannot do on the WR sampler thread. mRootScrollbarInfo, + * however, can be accessed while just holding the mMapLock which is safe to + * do on the sampler thread. + * mMapLock must be acquired while accessing or modifying mRootScrollbarInfo. + */ + std::vector mRootScrollbarInfo; + + /** + * A helper structure to store all the information needed to compute the + * async transform for a fixed position element on the sampler thread. + */ + struct FixedPositionInfo { + Maybe mFixedPositionAnimationId; + SideBits mFixedPosSides; + ScrollableLayerGuid::ViewID mFixedPosTarget; + LayersId mLayersId; + + explicit FixedPositionInfo(const HitTestingTreeNode* aNode); + }; + /** + * If this APZCTreeManager is being used with WebRender, this vector gets + * populated during a layers update. It holds a package of information needed + * to compute and set the async transforms on fixed position content. This + * information is extracted from the HitTestingTreeNodes for the WebRender + * case because accessing the HitTestingTreeNodes requires holding the tree + * lock which we cannot do on the WR sampler thread. mFixedPositionInfo, + * however, can be accessed while just holding the mMapLock which is safe to + * do on the sampler thread. mMapLock must be acquired while accessing or + * modifying mFixedPositionInfo. + */ + std::vector mFixedPositionInfo; + + /** + * A helper structure to store all the information needed to compute the + * async transform for a sticky position element on the sampler thread. + */ + struct StickyPositionInfo { + Maybe mStickyPositionAnimationId; + SideBits mFixedPosSides; + ScrollableLayerGuid::ViewID mStickyPosTarget; + LayersId mLayersId; + LayerRectAbsolute mStickyScrollRangeInner; + LayerRectAbsolute mStickyScrollRangeOuter; + + explicit StickyPositionInfo(const HitTestingTreeNode* aNode); + }; + /** + * If this APZCTreeManager is being used with WebRender, this vector gets + * populated during a layers update. It holds a package of information needed + * to compute and set the async transforms on sticky position content. This + * information is extracted from the HitTestingTreeNodes for the WebRender + * case because accessing the HitTestingTreeNodes requires holding the tree + * lock which we cannot do on the WR sampler thread. mStickyPositionInfo, + * however, can be accessed while just holding the mMapLock which is safe to + * do on the sampler thread. mMapLock must be acquired while accessing or + * modifying mStickyPositionInfo. + */ + std::vector mStickyPositionInfo; + + /* Holds the zoom constraints for scrollable layers, as determined by the + * the main-thread gecko code. This can only be accessed on the updater + * thread. */ + std::unordered_map + mZoomConstraints; + /* A list of keyboard shortcuts to use for translating keyboard inputs into + * keyboard actions. This is gathered on the main thread from XBL bindings. + * This must only be accessed on the controller thread. + */ + KeyboardMap mKeyboardMap; + /* This tracks the focus targets of chrome and content and whether we have + * a current focus target or whether we are waiting for a new confirmation. + */ + FocusState mFocusState; + /* This tracks the hit test result info for the current touch input block. + * In particular, it tracks the target APZC, the hit test flags, and the + * fixed pos sides. This is populated at the start of a touch block based + * on the hit-test result, and used for subsequent touch events in the block. + * This allows touch points to move outside the thing they started on, but + * still have the touch events delivered to the same initial APZC. This will + * only ever be touched on the input delivery thread, and so does not require + * locking. + */ + HitTestResult mTouchBlockHitResult; + /* Sometimes we want to ignore all touches except one. In such cases, this + * is set to the identifier of the touch we are not ignoring; in other cases, + * this is set to -1. + */ + int32_t mRetainedTouchIdentifier; + /* This tracks whether the current input block represents a touch-drag of + * a scrollbar. In this state, touch events are forwarded to content as touch + * events, but converted to mouse events before going into InputQueue and + * being handled by an APZC (to reuse the APZ code for scrollbar dragging + * with a mouse). + */ + bool mInScrollbarTouchDrag; + /* Tracks the number of touch points we are tracking that are currently on + * the screen. */ + TouchCounter mTouchCounter; + /* If a tap gesture event sent directly by widget code (rather than gesture + * detected from touch events by APZ) is being processed, this stores the + * result of hit testing for that tap gesture event. + */ + HitTestResult mTapGestureHitResult; + /* Stores the current mouse position in screen coordinates. + */ + mutable DataMutex mCurrentMousePosition; + /* Extra margins that should be applied to content that fixed wrt. the + * RCD-RSF, to account for the dynamic toolbar. + * Acquire mMapLock before accessing this. + */ + ScreenMargin mCompositorFixedLayerMargins; + /* Similar to above |mCompositorFixedLayerMargins|. But this value is the + * margins on the main-thread at the last time position:fixed elements were + * updated during the dynamic toolbar transitions. + * Acquire mMapLock before accessing this. + */ + ScreenMargin mGeckoFixedLayerMargins; + /* For logging the APZC tree for debugging (enabled by the apz.printtree + * pref). The purpose of using LOG_CRITICAL is so that you don't also need to + * change the gfx.logging.level pref to see the output. */ + gfx::TreeLog mApzcTreeLog; + + class CheckerboardFlushObserver; + friend class CheckerboardFlushObserver; + RefPtr mFlushObserver; + + // Map from layers id to APZTestData. Accesses and mutations must be + // protected by the mTestDataLock. + std::unordered_map, LayersId::HashFn> + mTestData; + mutable mozilla::Mutex mTestDataLock; + + // This must only be touched on the controller thread. + float mDPI; + + friend class IAPZHitTester; + UniquePtr mHitTester; + + // NOTE: This ScrollGenerationCounter needs to be per APZCTreeManager since + // the generation is bumped up on the sampler theread which is per + // APZCTreeManager. + ScrollGenerationCounter mScrollGenerationCounter; + mozilla::Mutex mScrollGenerationLock; + +#if defined(MOZ_WIDGET_ANDROID) + private: + // Last Frame metrics sent to java through UIController. + GeckoViewMetrics mLastRootMetrics; +#endif // defined(MOZ_WIDGET_ANDROID) +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_PanZoomController_h diff --git a/gfx/layers/apz/src/APZInputBridge.cpp b/gfx/layers/apz/src/APZInputBridge.cpp new file mode 100644 index 0000000000..bce801a6d2 --- /dev/null +++ b/gfx/layers/apz/src/APZInputBridge.cpp @@ -0,0 +1,435 @@ +/* -*- 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 "mozilla/layers/APZInputBridge.h" + +#include "AsyncPanZoomController.h" +#include "InputData.h" // for MouseInput, etc +#include "InputBlockState.h" // for InputBlockState +#include "OverscrollHandoffState.h" // for OverscrollHandoffState +#include "mozilla/EventForwards.h" +#include "mozilla/dom/WheelEventBinding.h" // for WheelEvent constants +#include "mozilla/EventStateManager.h" // for EventStateManager +#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread, etc +#include "mozilla/MouseEvents.h" // for WidgetMouseEvent +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/StaticPrefs_general.h" +#include "mozilla/StaticPrefs_test.h" +#include "mozilla/TextEvents.h" // for WidgetKeyboardEvent +#include "mozilla/TouchEvents.h" // for WidgetTouchEvent +#include "mozilla/WheelHandlingHelper.h" // for WheelDeltaHorizontalizer, + // WheelDeltaAdjustmentStrategy + +namespace mozilla { +namespace layers { + +APZEventResult::APZEventResult() + : mStatus(nsEventStatus_eIgnore), + mInputBlockId(InputBlockState::NO_BLOCK_ID) {} + +APZEventResult::APZEventResult( + const RefPtr& aInitialTarget, + TargetConfirmationFlags aFlags) + : APZEventResult() { + mHandledResult = [&]() -> Maybe { + if (!aInitialTarget->IsRootContent()) { + // If the initial target is not the root, this will definitely not be + // handled by the root. (The confirmed target is either the initial + // target, or a descendant.) + return Some( + APZHandledResult{APZHandledPlace::HandledByContent, aInitialTarget}); + } + + if (!aFlags.mDispatchToContent) { + // If the initial target is the root and we don't need to dispatch to + // content, the event will definitely be handled by the root. + return Some( + APZHandledResult{APZHandledPlace::HandledByRoot, aInitialTarget}); + } + + // Otherwise, we're not sure. + return Nothing(); + }(); + aInitialTarget->GetGuid(&mTargetGuid); +} + +void APZEventResult::SetStatusAsConsumeDoDefault( + const InputBlockState& aBlock) { + SetStatusAsConsumeDoDefault(aBlock.GetTargetApzc()); +} + +void APZEventResult::SetStatusAsConsumeDoDefault( + const RefPtr& aTarget) { + mStatus = nsEventStatus_eConsumeDoDefault; + mHandledResult = + Some(aTarget && aTarget->IsRootContent() + ? APZHandledResult{APZHandledPlace::HandledByRoot, aTarget} + : APZHandledResult{APZHandledPlace::HandledByContent, aTarget}); +} + +void APZEventResult::SetStatusForTouchEvent( + const InputBlockState& aBlock, TargetConfirmationFlags aFlags, + PointerEventsConsumableFlags aConsumableFlags, + const AsyncPanZoomController* aTarget) { + // Note, we need to continue setting mStatus to eIgnore in the {mHasRoom=true, + // mAllowedByTouchAction=false} case because this is the behaviour expected by + // APZEventState::ProcessTouchEvent() when it determines when to send a + // `pointercancel` event. TODO: Use something more descriptive than + // nsEventStatus for this purpose. + mStatus = aConsumableFlags.IsConsumable() ? nsEventStatus_eConsumeDoDefault + : nsEventStatus_eIgnore; + + UpdateHandledResult(aBlock, aConsumableFlags, aTarget, + aFlags.mDispatchToContent); +} + +void APZEventResult::UpdateHandledResult( + const InputBlockState& aBlock, + PointerEventsConsumableFlags aConsumableFlags, + const AsyncPanZoomController* aTarget, bool aDispatchToContent) { + // If the touch event's effect is disallowed by touch-action, treat it as if + // a touch event listener had preventDefault()-ed it (i.e. return + // HandledByContent, except we can do it eagerly rather than having to wait + // for the listener to run). + if (!aConsumableFlags.mAllowedByTouchAction) { + mHandledResult = + Some(APZHandledResult{APZHandledPlace::HandledByContent, aTarget}); + return; + } + + if (mHandledResult && !aDispatchToContent && !aConsumableFlags.mHasRoom) { + // Set result to Unhandled if we have no room to scroll, unless it + // was HandledByContent because we're over a dispatch-to-content region, + // in which case it should remain HandledByContent. + mHandledResult->mPlace = APZHandledPlace::Unhandled; + } + + if (aTarget && !aTarget->IsRootContent()) { + auto [result, rootApzc] = + aBlock.GetOverscrollHandoffChain()->ScrollingDownWillMoveDynamicToolbar( + aTarget); + if (result) { + MOZ_ASSERT(rootApzc && rootApzc->IsRootContent()); + // The event is actually consumed by a non-root APZC but scroll + // positions in all relevant APZCs are at the bottom edge, so if there's + // still contents covered by the dynamic toolbar we need to move the + // dynamic toolbar to make the covered contents visible, thus we need + // to tell it to GeckoView so we handle it as if it's consumed in the + // root APZC. + // IMPORTANT NOTE: If the incoming TargetConfirmationFlags has + // mDispatchToContent, we need to change it to Nothing() so that + // GeckoView can properly wait for results from the content on the + // main-thread. + mHandledResult = + aDispatchToContent + ? Nothing() + : Some(APZHandledResult{aConsumableFlags.IsConsumable() + ? APZHandledPlace::HandledByRoot + : APZHandledPlace::Unhandled, + rootApzc}); + } + } +} + +void APZEventResult::SetStatusForFastFling( + const TouchBlockState& aBlock, TargetConfirmationFlags aFlags, + PointerEventsConsumableFlags aConsumableFlags, + const AsyncPanZoomController* aTarget) { + MOZ_ASSERT(aBlock.IsDuringFastFling()); + + // Set eConsumeNoDefault for fast fling since we don't want to send the event + // to content at all. + mStatus = nsEventStatus_eConsumeNoDefault; + + // In the case of fast fling, the event will never be sent to content, so we + // want a result where `aDispatchToContent` is false whatever the original + // `aFlags.mDispatchToContent` is. + UpdateHandledResult(aBlock, aConsumableFlags, aTarget, false /* + aDispatchToContent */); +} + +static bool WillHandleMouseEvent(const WidgetMouseEventBase& aEvent) { + return aEvent.mMessage == eMouseMove || aEvent.mMessage == eMouseDown || + aEvent.mMessage == eMouseUp || aEvent.mMessage == eDragEnd || + (StaticPrefs::test_events_async_enabled() && + aEvent.mMessage == eMouseHitTest); +} + +/* static */ +Maybe APZInputBridge::ActionForWheelEvent( + WidgetWheelEvent* aEvent) { + if (!(aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_LINE || + aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_PIXEL || + aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_PAGE)) { + return Nothing(); + } + return EventStateManager::APZWheelActionFor(aEvent); +} + +APZEventResult APZInputBridge::ReceiveInputEvent( + WidgetInputEvent& aEvent, InputBlockCallback&& aCallback) { + APZThreadUtils::AssertOnControllerThread(); + + APZEventResult result; + + switch (aEvent.mClass) { + case eMouseEventClass: + case eDragEventClass: { + WidgetMouseEvent& mouseEvent = *aEvent.AsMouseEvent(); + if (WillHandleMouseEvent(mouseEvent)) { + MouseInput input(mouseEvent); + input.mOrigin = + ScreenPoint(mouseEvent.mRefPoint.x, mouseEvent.mRefPoint.y); + + result = ReceiveInputEvent(input, std::move(aCallback)); + + mouseEvent.mRefPoint = TruncatedToInt(ViewAs( + input.mOrigin, + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent)); + mouseEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ; + mouseEvent.mFocusSequenceNumber = input.mFocusSequenceNumber; +#ifdef XP_MACOSX + // It's not assumed that the click event has already been prevented, + // except mousedown event with ctrl key is pressed where we prevent + // click event from widget on Mac platform. + MOZ_ASSERT_IF(!mouseEvent.IsControl() || + mouseEvent.mMessage != eMouseDown || + mouseEvent.mButton != MouseButton::ePrimary, + !mouseEvent.mClickEventPrevented); +#else + MOZ_ASSERT( + !mouseEvent.mClickEventPrevented, + "It's not assumed that the click event has already been prevented"); +#endif + mouseEvent.mClickEventPrevented |= input.mPreventClickEvent; + MOZ_ASSERT_IF(mouseEvent.mClickEventPrevented, + mouseEvent.mMessage == eMouseDown || + mouseEvent.mMessage == eMouseUp); + aEvent.mLayersId = input.mLayersId; + + if (mouseEvent.IsReal()) { + UpdateWheelTransaction(mouseEvent.mRefPoint, mouseEvent.mMessage, + Some(result.mTargetGuid)); + } + + return result; + } + + if (mouseEvent.IsReal()) { + UpdateWheelTransaction(mouseEvent.mRefPoint, mouseEvent.mMessage, + Nothing()); + } + + ProcessUnhandledEvent(&mouseEvent.mRefPoint, &result.mTargetGuid, + &aEvent.mFocusSequenceNumber, &aEvent.mLayersId); + return result; + } + case eTouchEventClass: { + WidgetTouchEvent& touchEvent = *aEvent.AsTouchEvent(); + MultiTouchInput touchInput(touchEvent); + result = ReceiveInputEvent(touchInput, std::move(aCallback)); + // touchInput was modified in-place to possibly remove some + // touch points (if we are overscrolled), and the coordinates were + // modified using the APZ untransform. We need to copy these changes + // back into the WidgetInputEvent. + touchEvent.mTouches.Clear(); + touchEvent.mTouches.SetCapacity(touchInput.mTouches.Length()); + for (size_t i = 0; i < touchInput.mTouches.Length(); i++) { + *touchEvent.mTouches.AppendElement() = + touchInput.mTouches[i].ToNewDOMTouch(); + } + touchEvent.mFlags.mHandledByAPZ = touchInput.mHandledByAPZ; + touchEvent.mFocusSequenceNumber = touchInput.mFocusSequenceNumber; + aEvent.mLayersId = touchInput.mLayersId; + return result; + } + case eWheelEventClass: { + WidgetWheelEvent& wheelEvent = *aEvent.AsWheelEvent(); + + if (Maybe action = ActionForWheelEvent(&wheelEvent)) { + ScrollWheelInput::ScrollMode scrollMode = + ScrollWheelInput::SCROLLMODE_INSTANT; + if (StaticPrefs::general_smoothScroll() && + ((wheelEvent.mDeltaMode == + dom::WheelEvent_Binding::DOM_DELTA_LINE && + StaticPrefs::general_smoothScroll_mouseWheel()) || + (wheelEvent.mDeltaMode == + dom::WheelEvent_Binding::DOM_DELTA_PAGE && + StaticPrefs::general_smoothScroll_pages()))) { + scrollMode = ScrollWheelInput::SCROLLMODE_SMOOTH; + } + + WheelDeltaAdjustmentStrategy strategy = + EventStateManager::GetWheelDeltaAdjustmentStrategy(wheelEvent); + // Adjust the delta values of the wheel event if the current default + // action is to horizontalize scrolling. I.e., deltaY values are set to + // deltaX and deltaY and deltaZ values are set to 0. + // If horizontalized, the delta values will be restored and its overflow + // deltaX will become 0 when the WheelDeltaHorizontalizer instance is + // being destroyed. + WheelDeltaHorizontalizer horizontalizer(wheelEvent); + if (WheelDeltaAdjustmentStrategy::eHorizontalize == strategy) { + horizontalizer.Horizontalize(); + } + + // If the wheel event becomes no-op event, don't handle it as scroll. + if (wheelEvent.mDeltaX || wheelEvent.mDeltaY) { + ScreenPoint origin(wheelEvent.mRefPoint.x, wheelEvent.mRefPoint.y); + ScrollWheelInput input( + wheelEvent.mTimeStamp, 0, scrollMode, + ScrollWheelInput::DeltaTypeForDeltaMode(wheelEvent.mDeltaMode), + origin, wheelEvent.mDeltaX, wheelEvent.mDeltaY, + wheelEvent.mAllowToOverrideSystemScrollSpeed, strategy); + input.mAPZAction = action.value(); + + // We add the user multiplier as a separate field, rather than + // premultiplying it, because if the input is converted back to a + // WidgetWheelEvent, then EventStateManager would apply the delta a + // second time. We could in theory work around this by asking ESM to + // customize the event much sooner, and then save the + // "mCustomizedByUserPrefs" bit on ScrollWheelInput - but for now, + // this seems easier. + EventStateManager::GetUserPrefsForWheelEvent( + &wheelEvent, &input.mUserDeltaMultiplierX, + &input.mUserDeltaMultiplierY); + + result = ReceiveInputEvent(input, std::move(aCallback)); + wheelEvent.mRefPoint = TruncatedToInt(ViewAs( + input.mOrigin, PixelCastJustification:: + LayoutDeviceIsScreenForUntransformedEvent)); + wheelEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ; + wheelEvent.mFocusSequenceNumber = input.mFocusSequenceNumber; + aEvent.mLayersId = input.mLayersId; + + return result; + } + } + + UpdateWheelTransaction(aEvent.mRefPoint, aEvent.mMessage, Nothing()); + ProcessUnhandledEvent(&aEvent.mRefPoint, &result.mTargetGuid, + &aEvent.mFocusSequenceNumber, &aEvent.mLayersId); + MOZ_ASSERT(result.GetStatus() == nsEventStatus_eIgnore); + return result; + } + case eKeyboardEventClass: { + WidgetKeyboardEvent& keyboardEvent = *aEvent.AsKeyboardEvent(); + + KeyboardInput input(keyboardEvent); + + result = ReceiveInputEvent(input, std::move(aCallback)); + + keyboardEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ; + keyboardEvent.mFocusSequenceNumber = input.mFocusSequenceNumber; + return result; + } + default: { + UpdateWheelTransaction(aEvent.mRefPoint, aEvent.mMessage, Nothing()); + ProcessUnhandledEvent(&aEvent.mRefPoint, &result.mTargetGuid, + &aEvent.mFocusSequenceNumber, &aEvent.mLayersId); + return result; + } + } + + MOZ_ASSERT_UNREACHABLE("Invalid WidgetInputEvent type."); + result.SetStatusAsConsumeNoDefault(); + return result; +} + +APZHandledResult::APZHandledResult(APZHandledPlace aPlace, + const AsyncPanZoomController* aTarget) + : mPlace(aPlace) { + MOZ_ASSERT(aTarget); + switch (aPlace) { + case APZHandledPlace::Unhandled: + break; + case APZHandledPlace::HandledByContent: + if (aTarget) { + mScrollableDirections = aTarget->ScrollableDirections(); + mOverscrollDirections = aTarget->GetAllowedHandoffDirections(); + } + break; + case APZHandledPlace::HandledByRoot: { + MOZ_ASSERT(aTarget->IsRootContent()); + if (aTarget) { + mScrollableDirections = aTarget->ScrollableDirections(); + mOverscrollDirections = aTarget->GetAllowedHandoffDirections(); + } + break; + } + default: + MOZ_ASSERT_UNREACHABLE("Invalid APZHandledPlace"); + break; + } +} + +std::ostream& operator<<(std::ostream& aOut, const SideBits& aSideBits) { + if ((aSideBits & SideBits::eAll) == SideBits::eAll) { + aOut << "all"; + } else { + AutoTArray strings; + if (aSideBits & SideBits::eTop) { + strings.AppendElement("top"_ns); + } + if (aSideBits & SideBits::eRight) { + strings.AppendElement("right"_ns); + } + if (aSideBits & SideBits::eBottom) { + strings.AppendElement("bottom"_ns); + } + if (aSideBits & SideBits::eLeft) { + strings.AppendElement("left"_ns); + } + aOut << strings; + } + return aOut; +} + +std::ostream& operator<<(std::ostream& aOut, + const ScrollDirections& aScrollDirections) { + if (aScrollDirections.contains(EitherScrollDirection)) { + aOut << "either"; + } else if (aScrollDirections.contains(HorizontalScrollDirection)) { + aOut << "horizontal"; + } else if (aScrollDirections.contains(VerticalScrollDirection)) { + aOut << "vertical"; + } else { + aOut << "none"; + } + return aOut; +} + +std::ostream& operator<<(std::ostream& aOut, + const APZHandledPlace& aHandledPlace) { + switch (aHandledPlace) { + case APZHandledPlace::Unhandled: + aOut << "unhandled"; + break; + case APZHandledPlace::HandledByRoot: { + aOut << "handled-by-root"; + break; + } + case APZHandledPlace::HandledByContent: { + aOut << "handled-by-content"; + break; + } + case APZHandledPlace::Invalid: { + aOut << "INVALID"; + break; + } + } + return aOut; +} + +std::ostream& operator<<(std::ostream& aOut, + const APZHandledResult& aHandledResult) { + aOut << "handled: " << aHandledResult.mPlace << ", "; + aOut << "scrollable: " << aHandledResult.mScrollableDirections << ", "; + aOut << "overscroll: " << aHandledResult.mOverscrollDirections << std::endl; + return aOut; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/APZPublicUtils.cpp b/gfx/layers/apz/src/APZPublicUtils.cpp new file mode 100644 index 0000000000..6902e0738c --- /dev/null +++ b/gfx/layers/apz/src/APZPublicUtils.cpp @@ -0,0 +1,111 @@ +/* -*- 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 "mozilla/layers/APZPublicUtils.h" + +#include "AsyncPanZoomController.h" +#include "mozilla/HelperMacros.h" +#include "mozilla/StaticPrefs_general.h" + +namespace mozilla { +namespace layers { + +namespace apz { + +/*static*/ void InitializeGlobalState() { + MOZ_ASSERT(NS_IsMainThread()); + AsyncPanZoomController::InitializeGlobalState(); +} + +/*static*/ const ScreenMargin CalculatePendingDisplayPort( + const FrameMetrics& aFrameMetrics, const ParentLayerPoint& aVelocity) { + return AsyncPanZoomController::CalculatePendingDisplayPort( + aFrameMetrics, aVelocity, AsyncPanZoomController::ZoomInProgress::No); +} + +/*static*/ gfx::IntSize GetDisplayportAlignmentMultiplier( + const ScreenSize& aBaseSize) { + return AsyncPanZoomController::GetDisplayportAlignmentMultiplier(aBaseSize); +} + +ScrollAnimationBezierPhysicsSettings ComputeBezierAnimationSettingsForOrigin( + ScrollOrigin aOrigin) { + int32_t minMS = 0; + int32_t maxMS = 0; + bool isOriginSmoothnessEnabled = false; + +#define READ_DURATIONS(prefbase) \ + isOriginSmoothnessEnabled = StaticPrefs::general_smoothScroll() && \ + StaticPrefs::general_smoothScroll_##prefbase(); \ + if (isOriginSmoothnessEnabled) { \ + minMS = StaticPrefs::general_smoothScroll_##prefbase##_durationMinMS(); \ + maxMS = StaticPrefs::general_smoothScroll_##prefbase##_durationMaxMS(); \ + } + + switch (aOrigin) { + case ScrollOrigin::Pixels: + READ_DURATIONS(pixels) + break; + case ScrollOrigin::Lines: + READ_DURATIONS(lines) + break; + case ScrollOrigin::Pages: + READ_DURATIONS(pages) + break; + case ScrollOrigin::MouseWheel: + READ_DURATIONS(mouseWheel) + break; + case ScrollOrigin::Scrollbars: + READ_DURATIONS(scrollbars) + break; + default: + READ_DURATIONS(other) + break; + } + +#undef READ_DURATIONS + + if (isOriginSmoothnessEnabled) { + static const int32_t kSmoothScrollMaxAllowedAnimationDurationMS = 10000; + maxMS = clamped(maxMS, 0, kSmoothScrollMaxAllowedAnimationDurationMS); + minMS = clamped(minMS, 0, maxMS); + } + + // Keep the animation duration longer than the average event intervals + // (to "connect" consecutive scroll animations before the scroll comes to a + // stop). + double intervalRatio = + ((double)StaticPrefs::general_smoothScroll_durationToIntervalRatio()) / + 100.0; + + // Duration should be at least as long as the intervals -> ratio is at least 1 + intervalRatio = std::max(1.0, intervalRatio); + + return ScrollAnimationBezierPhysicsSettings{minMS, maxMS, intervalRatio}; +} + +ScrollMode GetScrollModeForOrigin(ScrollOrigin origin) { + if (!StaticPrefs::general_smoothScroll()) return ScrollMode::Instant; + switch (origin) { + case ScrollOrigin::Lines: + return StaticPrefs::general_smoothScroll_lines() ? ScrollMode::Smooth + : ScrollMode::Instant; + case ScrollOrigin::Pages: + return StaticPrefs::general_smoothScroll_pages() ? ScrollMode::Smooth + : ScrollMode::Instant; + case ScrollOrigin::Other: + return StaticPrefs::general_smoothScroll_other() ? ScrollMode::Smooth + : ScrollMode::Instant; + default: + MOZ_ASSERT(false, "Unknown keyboard scroll origin"); + return StaticPrefs::general_smoothScroll() ? ScrollMode::Smooth + : ScrollMode::Instant; + } +} + +} // namespace apz +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/APZSampler.cpp b/gfx/layers/apz/src/APZSampler.cpp new file mode 100644 index 0000000000..088fb6f7a0 --- /dev/null +++ b/gfx/layers/apz/src/APZSampler.cpp @@ -0,0 +1,216 @@ +/* -*- 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 "mozilla/layers/APZSampler.h" + +#include "AsyncPanZoomController.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/layers/APZThreadUtils.h" +#include "mozilla/layers/APZUtils.h" +#include "mozilla/layers/CompositorThread.h" +#include "mozilla/layers/SynchronousTask.h" +#include "TreeTraversal.h" +#include "mozilla/webrender/WebRenderAPI.h" + +namespace mozilla { +namespace layers { + +StaticMutex APZSampler::sWindowIdLock; +StaticAutoPtr>> + APZSampler::sWindowIdMap; + +APZSampler::APZSampler(const RefPtr& aApz, + bool aIsUsingWebRender) + : mApz(aApz), + mIsUsingWebRender(aIsUsingWebRender), + mThreadIdLock("APZSampler::mThreadIdLock"), + mSampleTimeLock("APZSampler::mSampleTimeLock") { + MOZ_ASSERT(aApz); + mApz->SetSampler(this); +} + +APZSampler::~APZSampler() { mApz->SetSampler(nullptr); } + +void APZSampler::Destroy() { + StaticMutexAutoLock lock(sWindowIdLock); + if (mWindowId) { + MOZ_ASSERT(sWindowIdMap); + sWindowIdMap->erase(wr::AsUint64(*mWindowId)); + } +} + +void APZSampler::SetWebRenderWindowId(const wr::WindowId& aWindowId) { + StaticMutexAutoLock lock(sWindowIdLock); + MOZ_ASSERT(!mWindowId); + mWindowId = Some(aWindowId); + if (!sWindowIdMap) { + sWindowIdMap = new std::unordered_map>(); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "APZSampler::ClearOnShutdown", [] { ClearOnShutdown(&sWindowIdMap); })); + } + (*sWindowIdMap)[wr::AsUint64(aWindowId)] = this; +} + +/*static*/ +void APZSampler::SetSamplerThread(const wr::WrWindowId& aWindowId) { + if (RefPtr sampler = GetSampler(aWindowId)) { + MutexAutoLock lock(sampler->mThreadIdLock); + sampler->mSamplerThreadId = Some(PlatformThread::CurrentId()); + } +} + +/*static*/ +void APZSampler::SampleForWebRender(const wr::WrWindowId& aWindowId, + const uint64_t* aGeneratedFrameId, + wr::Transaction* aTransaction) { + if (RefPtr sampler = GetSampler(aWindowId)) { + wr::TransactionWrapper txn(aTransaction); + Maybe vsyncId = + aGeneratedFrameId ? Some(VsyncId{*aGeneratedFrameId}) : Nothing(); + sampler->SampleForWebRender(vsyncId, txn); + } +} + +void APZSampler::SetSampleTime(const SampleTime& aSampleTime) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); + MutexAutoLock lock(mSampleTimeLock); + // This only gets called with WR, and the time provided is going to be + // the time at which the current vsync interval ends. i.e. it is the timestamp + // for the next vsync that will occur. + mSampleTime = aSampleTime; +} + +void APZSampler::SampleForWebRender(const Maybe& aVsyncId, + wr::TransactionWrapper& aTxn) { + AssertOnSamplerThread(); + SampleTime sampleTime; + { // scope lock + MutexAutoLock lock(mSampleTimeLock); + + // If mSampleTime is null we're in a startup phase where the + // WebRenderBridgeParent hasn't yet provided us with a sample time. + // If we're that early there probably aren't any APZ animations happening + // anyway, so using Timestamp::Now() should be fine. + SampleTime now = SampleTime::FromNow(); + sampleTime = mSampleTime.IsNull() ? now : mSampleTime; + } + mApz->SampleForWebRender(aVsyncId, aTxn, sampleTime); +} + +AsyncTransform APZSampler::GetCurrentAsyncTransform( + const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId, + AsyncTransformComponents aComponents, + const MutexAutoLock& aProofOfMapLock) const { + MOZ_ASSERT(!CompositorThreadHolder::IsInCompositorThread()); + AssertOnSamplerThread(); + + RefPtr apzc = + mApz->GetTargetAPZC(aLayersId, aScrollId, aProofOfMapLock); + if (!apzc) { + // It's possible that this function can get called even after the target + // APZC has been already destroyed because destroying the animation which + // triggers this function call is basically processed later than the APZC, + // i.e. queue mCompositorAnimationsToDelete in WebRenderBridgeParent and + // then remove in WebRenderBridgeParent::RemoveEpochDataPriorTo. + return AsyncTransform{}; + } + + return apzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing, + aComponents); +} + +ParentLayerRect APZSampler::GetCompositionBounds( + const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId, + const MutexAutoLock& aProofOfMapLock) const { + // This function can get called on the compositor in case of non WebRender + // get called on the sampler thread in case of WebRender. + AssertOnSamplerThread(); + + RefPtr apzc = + mApz->GetTargetAPZC(aLayersId, aScrollId, aProofOfMapLock); + if (!apzc) { + // On WebRender it's possible that this function can get called even after + // the target APZC has been already destroyed because destroying the + // animation which triggers this function call is basically processed later + // than the APZC one, i.e. queue mCompositorAnimationsToDelete in + // WebRenderBridgeParent and then remove them in + // WebRenderBridgeParent::RemoveEpochDataPriorTo. + return ParentLayerRect(); + } + + return apzc->GetCompositionBounds(); +} + +Maybe +APZSampler::GetCurrentScrollOffsetAndRange( + const LayersId& aLayersId, const ScrollableLayerGuid::ViewID& aScrollId, + const MutexAutoLock& aProofOfMapLock) const { + // Note: This is called from OMTA Sampler thread, or Compositor thread for + // testing. + + RefPtr apzc = + mApz->GetTargetAPZC(aLayersId, aScrollId, aProofOfMapLock); + if (!apzc) { + return Nothing(); + } + + return Some(ScrollOffsetAndRange{ + // FIXME: Use the one-frame delayed offset now. This doesn't take + // scroll-linked effets into accounts, so we have to fix this in the + // future. + apzc->GetCurrentAsyncVisualViewport( + AsyncPanZoomController::AsyncTransformConsumer::eForCompositing) + .TopLeft(), + apzc->GetCurrentScrollRangeInCssPixels()}); +} + +void APZSampler::AssertOnSamplerThread() const { + if (APZThreadUtils::GetThreadAssertionsEnabled()) { + MOZ_ASSERT(IsSamplerThread()); + } +} + +bool APZSampler::IsSamplerThread() const { + if (mIsUsingWebRender) { + // If the sampler thread id isn't set yet then we cannot be running on the + // sampler thread (because we will have the thread id before we run any + // other C++ code on it, and this function is only ever invoked from C++ + // code), so return false in that scenario. + MutexAutoLock lock(mThreadIdLock); + return mSamplerThreadId && PlatformThread::CurrentId() == *mSamplerThreadId; + } + return CompositorThreadHolder::IsInCompositorThread(); +} + +/*static*/ +already_AddRefed APZSampler::GetSampler( + const wr::WrWindowId& aWindowId) { + RefPtr sampler; + StaticMutexAutoLock lock(sWindowIdLock); + if (sWindowIdMap) { + auto it = sWindowIdMap->find(wr::AsUint64(aWindowId)); + if (it != sWindowIdMap->end()) { + sampler = it->second; + } + } + return sampler.forget(); +} + +} // namespace layers +} // namespace mozilla + +void apz_register_sampler(mozilla::wr::WrWindowId aWindowId) { + mozilla::layers::APZSampler::SetSamplerThread(aWindowId); +} + +void apz_sample_transforms(mozilla::wr::WrWindowId aWindowId, + const uint64_t* aGeneratedFrameId, + mozilla::wr::Transaction* aTransaction) { + mozilla::layers::APZSampler::SampleForWebRender(aWindowId, aGeneratedFrameId, + aTransaction); +} + +void apz_deregister_sampler(mozilla::wr::WrWindowId aWindowId) {} diff --git a/gfx/layers/apz/src/APZUpdater.cpp b/gfx/layers/apz/src/APZUpdater.cpp new file mode 100644 index 0000000000..2bbad6e1a7 --- /dev/null +++ b/gfx/layers/apz/src/APZUpdater.cpp @@ -0,0 +1,546 @@ +/* -*- 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 "mozilla/layers/APZUpdater.h" + +#include "APZCTreeManager.h" +#include "AsyncPanZoomController.h" +#include "base/task.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/layers/APZThreadUtils.h" +#include "mozilla/layers/CompositorThread.h" +#include "mozilla/layers/SynchronousTask.h" +#include "mozilla/layers/WebRenderScrollDataWrapper.h" +#include "mozilla/webrender/WebRenderAPI.h" + +namespace mozilla { +namespace layers { + +StaticMutex APZUpdater::sWindowIdLock; +StaticAutoPtr> + APZUpdater::sWindowIdMap; + +APZUpdater::APZUpdater(const RefPtr& aApz, + bool aConnectedToWebRender) + : mApz(aApz), + mDestroyed(false), + mConnectedToWebRender(aConnectedToWebRender), + mThreadIdLock("APZUpdater::ThreadIdLock"), + mQueueLock("APZUpdater::QueueLock") { + MOZ_ASSERT(aApz); + mApz->SetUpdater(this); +} + +APZUpdater::~APZUpdater() { + mApz->SetUpdater(nullptr); + + StaticMutexAutoLock lock(sWindowIdLock); + if (mWindowId) { + MOZ_ASSERT(sWindowIdMap); + // Ensure that ClearTree was called and the task got run + MOZ_ASSERT(sWindowIdMap->find(wr::AsUint64(*mWindowId)) == + sWindowIdMap->end()); + } +} + +bool APZUpdater::HasTreeManager(const RefPtr& aApz) { + return aApz.get() == mApz.get(); +} + +void APZUpdater::SetWebRenderWindowId(const wr::WindowId& aWindowId) { + StaticMutexAutoLock lock(sWindowIdLock); + MOZ_ASSERT(!mWindowId); + mWindowId = Some(aWindowId); + if (!sWindowIdMap) { + sWindowIdMap = new std::unordered_map(); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "APZUpdater::ClearOnShutdown", [] { ClearOnShutdown(&sWindowIdMap); })); + } + (*sWindowIdMap)[wr::AsUint64(aWindowId)] = this; +} + +/*static*/ +void APZUpdater::SetUpdaterThread(const wr::WrWindowId& aWindowId) { + if (RefPtr updater = GetUpdater(aWindowId)) { + MutexAutoLock lock(updater->mThreadIdLock); + updater->mUpdaterThreadId = Some(PlatformThread::CurrentId()); + } +} + +// Takes a conditional lock! +/*static*/ +void APZUpdater::PrepareForSceneSwap(const wr::WrWindowId& aWindowId) + MOZ_NO_THREAD_SAFETY_ANALYSIS { + if (RefPtr updater = GetUpdater(aWindowId)) { + updater->mApz->LockTree(); + } +} + +// Assumes we took a conditional lock! +/*static*/ +void APZUpdater::CompleteSceneSwap(const wr::WrWindowId& aWindowId, + const wr::WrPipelineInfo& aInfo) { + RefPtr updater = GetUpdater(aWindowId); + if (!updater) { + // This should only happen in cases where PrepareForSceneSwap also got a + // null updater. No updater-thread tasks get run between PrepareForSceneSwap + // and this function, so there is no opportunity for the updater mapping + // to have gotten removed from sWindowIdMap in between the two calls. + return; + } + updater->mApz->mTreeLock.AssertCurrentThreadIn(); + + for (const auto& removedPipeline : aInfo.removed_pipelines) { + LayersId layersId = wr::AsLayersId(removedPipeline.pipeline_id); + updater->mEpochData.erase(layersId); + } + // Reset the built info for all pipelines, then put it back for the ones + // that got built in this scene swap. + for (auto& i : updater->mEpochData) { + i.second.mBuilt = Nothing(); + } + for (const auto& epoch : aInfo.epochs) { + LayersId layersId = wr::AsLayersId(epoch.pipeline_id); + updater->mEpochData[layersId].mBuilt = Some(epoch.epoch); + } + + // Run any tasks that got unblocked, then unlock the tree. The order is + // important because we want to run all the tasks up to and including the + // UpdateHitTestingTree calls corresponding to the built epochs, and we + // want to run those before we release the lock (i.e. atomically with the + // scene swap). This ensures that any hit-tests always encounter a consistent + // state between the APZ tree and the built scene in WR. + // + // While we could add additional information to the queued tasks to figure + // out the minimal set of tasks we want to run here, it's easier and harmless + // to just run all the queued and now-unblocked tasks inside the lock. + // + // Note that the ProcessQueue here might remove the window id -> APZUpdater + // mapping from sWindowIdMap, but we still unlock the tree successfully to + // leave things in a good state. + updater->ProcessQueue(); + + updater->mApz->UnlockTree(); +} + +/*static*/ +void APZUpdater::ProcessPendingTasks(const wr::WrWindowId& aWindowId) { + if (RefPtr updater = GetUpdater(aWindowId)) { + updater->ProcessQueue(); + } +} + +void APZUpdater::ClearTree(LayersId aRootLayersId) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); + RefPtr self = this; + RunOnUpdaterThread(aRootLayersId, + NS_NewRunnableFunction("APZUpdater::ClearTree", [=]() { + self->mApz->ClearTree(); + self->mDestroyed = true; + + // Once ClearTree is called on the APZCTreeManager, we + // are in a shutdown phase. After this point it's ok if + // WebRender cannot get a hold of the updater via the + // window id, and it's a good point to remove the mapping + // and avoid leaving a dangling pointer to this object. + StaticMutexAutoLock lock(sWindowIdLock); + if (self->mWindowId) { + MOZ_ASSERT(sWindowIdMap); + sWindowIdMap->erase(wr::AsUint64(*(self->mWindowId))); + } + })); +} + +void APZUpdater::UpdateFocusState(LayersId aRootLayerTreeId, + LayersId aOriginatingLayersId, + const FocusTarget& aFocusTarget) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); + RunOnUpdaterThread(aOriginatingLayersId, + NewRunnableMethod( + "APZUpdater::UpdateFocusState", mApz, + &APZCTreeManager::UpdateFocusState, aRootLayerTreeId, + aOriginatingLayersId, aFocusTarget)); +} + +void APZUpdater::UpdateScrollDataAndTreeState( + LayersId aRootLayerTreeId, LayersId aOriginatingLayersId, + const wr::Epoch& aEpoch, WebRenderScrollData&& aScrollData) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); + RefPtr self = this; + // Insert an epoch requirement update into the queue, so that + // tasks inserted into the queue after this point only get executed + // once the epoch requirement is satisfied. In particular, the + // UpdateHitTestingTree call below needs to wait until the epoch requirement + // is satisfied, which is why it is a separate task in the queue. + RunOnUpdaterThread( + aOriginatingLayersId, + NS_NewRunnableFunction("APZUpdater::UpdateEpochRequirement", [=]() { + if (aRootLayerTreeId == aOriginatingLayersId) { + self->mEpochData[aOriginatingLayersId].mIsRoot = true; + } + self->mEpochData[aOriginatingLayersId].mRequired = aEpoch; + })); + RunOnUpdaterThread( + aOriginatingLayersId, + NS_NewRunnableFunction( + "APZUpdater::UpdateHitTestingTree", + [=, aScrollData = std::move(aScrollData)]() mutable { + auto isFirstPaint = aScrollData.IsFirstPaint(); + auto paintSequenceNumber = aScrollData.GetPaintSequenceNumber(); + + self->mScrollData[aOriginatingLayersId] = std::move(aScrollData); + auto root = self->mScrollData.find(aRootLayerTreeId); + if (root == self->mScrollData.end()) { + return; + } + self->mApz->UpdateHitTestingTree( + WebRenderScrollDataWrapper(*self, &(root->second)), + isFirstPaint, aOriginatingLayersId, paintSequenceNumber); + })); +} + +void APZUpdater::UpdateScrollOffsets(LayersId aRootLayerTreeId, + LayersId aOriginatingLayersId, + ScrollUpdatesMap&& aUpdates, + uint32_t aPaintSequenceNumber) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); + RefPtr self = this; + RunOnUpdaterThread( + aOriginatingLayersId, + NS_NewRunnableFunction( + "APZUpdater::UpdateScrollOffsets", + [=, updates = std::move(aUpdates)]() mutable { + self->mScrollData[aOriginatingLayersId].ApplyUpdates( + std::move(updates), aPaintSequenceNumber); + auto root = self->mScrollData.find(aRootLayerTreeId); + if (root == self->mScrollData.end()) { + return; + } + self->mApz->UpdateHitTestingTree( + WebRenderScrollDataWrapper(*self, &(root->second)), + /*isFirstPaint*/ false, aOriginatingLayersId, + aPaintSequenceNumber); + })); +} + +void APZUpdater::NotifyLayerTreeAdopted(LayersId aLayersId, + const RefPtr& aOldUpdater) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); + RunOnUpdaterThread(aLayersId, + NewRunnableMethod>( + "APZUpdater::NotifyLayerTreeAdopted", mApz, + &APZCTreeManager::NotifyLayerTreeAdopted, aLayersId, + aOldUpdater ? aOldUpdater->mApz : nullptr)); +} + +void APZUpdater::NotifyLayerTreeRemoved(LayersId aLayersId) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); + RefPtr self = this; + RunOnUpdaterThread( + aLayersId, + NS_NewRunnableFunction("APZUpdater::NotifyLayerTreeRemoved", [=]() { + self->mEpochData.erase(aLayersId); + self->mScrollData.erase(aLayersId); + self->mApz->NotifyLayerTreeRemoved(aLayersId); + })); +} + +bool APZUpdater::GetAPZTestData(LayersId aLayersId, APZTestData* aOutData) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); + + RefPtr apz = mApz; + bool ret = false; + SynchronousTask waiter("APZUpdater::GetAPZTestData"); + RunOnUpdaterThread( + aLayersId, NS_NewRunnableFunction("APZUpdater::GetAPZTestData", [&]() { + AutoCompleteTask notifier(&waiter); + ret = apz->GetAPZTestData(aLayersId, aOutData); + })); + + // Wait until the task posted above has run and populated aOutData and ret + waiter.Wait(); + + return ret; +} + +void APZUpdater::SetTestAsyncScrollOffset( + LayersId aLayersId, const ScrollableLayerGuid::ViewID& aScrollId, + const CSSPoint& aOffset) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); + RefPtr apz = mApz; + RunOnUpdaterThread( + aLayersId, + NS_NewRunnableFunction("APZUpdater::SetTestAsyncScrollOffset", [=]() { + RefPtr apzc = + apz->GetTargetAPZC(aLayersId, aScrollId); + if (apzc) { + apzc->SetTestAsyncScrollOffset(aOffset); + } else { + NS_WARNING("Unable to find APZC in SetTestAsyncScrollOffset"); + } + })); +} + +void APZUpdater::SetTestAsyncZoom(LayersId aLayersId, + const ScrollableLayerGuid::ViewID& aScrollId, + const LayerToParentLayerScale& aZoom) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); + RefPtr apz = mApz; + RunOnUpdaterThread( + aLayersId, NS_NewRunnableFunction("APZUpdater::SetTestAsyncZoom", [=]() { + RefPtr apzc = + apz->GetTargetAPZC(aLayersId, aScrollId); + if (apzc) { + apzc->SetTestAsyncZoom(aZoom); + } else { + NS_WARNING("Unable to find APZC in SetTestAsyncZoom"); + } + })); +} + +const WebRenderScrollData* APZUpdater::GetScrollData(LayersId aLayersId) const { + AssertOnUpdaterThread(); + auto it = mScrollData.find(aLayersId); + return (it == mScrollData.end() ? nullptr : &(it->second)); +} + +void APZUpdater::AssertOnUpdaterThread() const { + if (APZThreadUtils::GetThreadAssertionsEnabled()) { + MOZ_ASSERT(IsUpdaterThread()); + } +} + +void APZUpdater::RunOnUpdaterThread(LayersId aLayersId, + already_AddRefed aTask) { + RefPtr task = aTask; + + // In the scenario where IsConnectedToWebRender() is true, this function + // might get called early (before mUpdaterThreadId is set). In that case + // IsUpdaterThread() will return false and we'll queue the task onto + // mUpdaterQueue. This is fine; the task is still guaranteed to run (barring + // catastrophic failure) because the WakeSceneBuilder call will still trigger + // the callback to run tasks. + + if (IsUpdaterThread()) { + // This function should only be called from the updater thread in test + // scenarios where we are not connected to WebRender. If it were called from + // the updater thread when we are connected to WebRender, running the task + // right away would be incorrect (we'd need to check that |aLayersId| + // isn't blocked, and if it is then enqueue the task instead). + MOZ_ASSERT(!IsConnectedToWebRender()); + task->Run(); + return; + } + + if (IsConnectedToWebRender()) { + // If the updater thread is a WebRender thread, and we're not on it + // right now, save the task in the queue. We will run tasks from the queue + // during the callback from the updater thread, which we trigger by the + // call to WakeSceneBuilder. + + bool sendWakeMessage = true; + { // scope lock + MutexAutoLock lock(mQueueLock); + for (const auto& queuedTask : mUpdaterQueue) { + if (queuedTask.mLayersId == aLayersId) { + // If there's already a task in the queue with this layers id, then + // we must have previously sent a WakeSceneBuilder message (when + // adding the first task with this layers id to the queue). Either + // that hasn't been fully processed yet, or the layers id is blocked + // waiting for an epoch - in either case there's no point in sending + // another WakeSceneBuilder message. + sendWakeMessage = false; + break; + } + } + mUpdaterQueue.push_back(QueuedTask{aLayersId, task}); + } + if (sendWakeMessage) { + RefPtr api = mApz->GetWebRenderAPI(); + if (api) { + api->WakeSceneBuilder(); + } else { + // Not sure if this can happen, but it might be possible. If it does, + // the task is in the queue, but if we didn't get a WebRenderAPI it + // might never run, or it might run later if we manage to get a + // WebRenderAPI later. For now let's just emit a warning, this can + // probably be upgraded to an assert later. + NS_WARNING("Possibly dropping task posted to updater thread"); + } + } + return; + } + + if (CompositorThread()) { + CompositorThread()->Dispatch(task.forget()); + } else { + // Could happen during startup + NS_WARNING("Dropping task posted to updater thread"); + } +} + +bool APZUpdater::IsUpdaterThread() const { + if (IsConnectedToWebRender()) { + // If the updater thread id isn't set yet then we cannot be running on the + // updater thread (because we will have the thread id before we run any + // C++ code on it, and this function is only ever invoked from C++ code), + // so return false in that scenario. + MutexAutoLock lock(mThreadIdLock); + return mUpdaterThreadId && PlatformThread::CurrentId() == *mUpdaterThreadId; + } + return CompositorThreadHolder::IsInCompositorThread(); +} + +void APZUpdater::RunOnControllerThread(LayersId aLayersId, + already_AddRefed aTask) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); + + RefPtr task = aTask; + + RunOnUpdaterThread( + aLayersId, + NewRunnableFunction("APZUpdater::RunOnControllerThread", + &APZThreadUtils::RunOnControllerThread, + std::move(task), nsIThread::DISPATCH_NORMAL)); +} + +bool APZUpdater::IsConnectedToWebRender() const { + return mConnectedToWebRender; +} + +/*static*/ +already_AddRefed APZUpdater::GetUpdater( + const wr::WrWindowId& aWindowId) { + RefPtr updater; + StaticMutexAutoLock lock(sWindowIdLock); + if (sWindowIdMap) { + auto it = sWindowIdMap->find(wr::AsUint64(aWindowId)); + if (it != sWindowIdMap->end()) { + updater = it->second; + } + } + return updater.forget(); +} + +void APZUpdater::ProcessQueue() { + MOZ_ASSERT(!mDestroyed); + + { // scope lock to check for emptiness + MutexAutoLock lock(mQueueLock); + if (mUpdaterQueue.empty()) { + return; + } + } + + std::deque blockedTasks; + while (true) { + QueuedTask task; + + { // scope lock to extract a task + MutexAutoLock lock(mQueueLock); + if (mUpdaterQueue.empty()) { + // If we're done processing mUpdaterQueue, swap the tasks that are + // still blocked back in and finish + std::swap(mUpdaterQueue, blockedTasks); + break; + } + task = mUpdaterQueue.front(); + mUpdaterQueue.pop_front(); + } + + // We check the task to see if it is blocked. Note that while this + // ProcessQueue function is executing, a particular layers id cannot go + // from blocked to unblocked, because only CompleteSceneSwap can unblock + // a layers id, and that also runs on the updater thread. If somehow + // a layers id gets unblocked while we're processing the queue, then it + // might result in tasks getting executed out of order. + + auto it = mEpochData.find(task.mLayersId); + if (it != mEpochData.end() && it->second.IsBlocked()) { + // If this task is blocked, put it into the blockedTasks queue that + // we will replace mUpdaterQueue with + blockedTasks.push_back(task); + } else { + // Run and discard the task + task.mRunnable->Run(); + } + } + + if (mDestroyed) { + // If we get here, then we must have just run the ClearTree task for + // this updater. There might be tasks in the queue from content subtrees + // of this window that are blocked due to stale epochs. This can happen + // if the tasks were queued after the root pipeline was removed in + // WebRender, which prevents scene builds (and therefore prevents us + // from getting updated epochs via CompleteSceneSwap). See bug 1465658 + // comment 43 for some more context. + // To avoid leaking these tasks, we discard the contents of the queue. + // This happens during window shutdown so if we don't run the tasks it's + // not going to matter much. + MutexAutoLock lock(mQueueLock); + if (!mUpdaterQueue.empty()) { + mUpdaterQueue.clear(); + } + } +} + +void APZUpdater::MarkAsDetached(LayersId aLayersId) { + mApz->MarkAsDetached(aLayersId); +} + +APZUpdater::EpochState::EpochState() : mRequired{0}, mIsRoot(false) {} + +bool APZUpdater::EpochState::IsBlocked() const { + // The root is a special case because we basically assume it is "visible" + // even before it is built for the first time. This is because building the + // scene automatically makes it visible, and we need to make sure the APZ + // scroll data gets applied atomically with that happening. + // + // Layer subtrees on the other hand do not automatically become visible upon + // being built, because there must be a another layer tree update to change + // the visibility (i.e. an ancestor layer tree update that adds the necessary + // reflayer to complete the chain of reflayers). + // + // So in the case of non-visible subtrees, we know that no hit-test will + // actually end up hitting that subtree either before or after the scene swap, + // because the subtree will remain non-visible. That in turns means that we + // can apply the APZ scroll data for that subtree epoch before the scene is + // built, because it's not going to get used anyway. And that means we don't + // need to block the queue for non-visible subtrees. Which is a good thing, + // because in practice it seems like we often have non-visible subtrees sent + // to the compositor from content. + if (mIsRoot && !mBuilt) { + return true; + } + return mBuilt && (*mBuilt < mRequired); +} + +} // namespace layers +} // namespace mozilla + +// Rust callback implementations + +void apz_register_updater(mozilla::wr::WrWindowId aWindowId) { + mozilla::layers::APZUpdater::SetUpdaterThread(aWindowId); +} + +void apz_pre_scene_swap(mozilla::wr::WrWindowId aWindowId) { + mozilla::layers::APZUpdater::PrepareForSceneSwap(aWindowId); +} + +void apz_post_scene_swap(mozilla::wr::WrWindowId aWindowId, + const mozilla::wr::WrPipelineInfo* aInfo) { + mozilla::layers::APZUpdater::CompleteSceneSwap(aWindowId, *aInfo); +} + +void apz_run_updater(mozilla::wr::WrWindowId aWindowId) { + mozilla::layers::APZUpdater::ProcessPendingTasks(aWindowId); +} + +void apz_deregister_updater(mozilla::wr::WrWindowId aWindowId) { + // Run anything that's still left. + mozilla::layers::APZUpdater::ProcessPendingTasks(aWindowId); +} diff --git a/gfx/layers/apz/src/APZUtils.cpp b/gfx/layers/apz/src/APZUtils.cpp new file mode 100644 index 0000000000..843046c34a --- /dev/null +++ b/gfx/layers/apz/src/APZUtils.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 "mozilla/layers/APZUtils.h" + +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/StaticPrefs_layers.h" + +namespace mozilla { +namespace layers { + +namespace apz { + +bool IsCloseToHorizontal(float aAngle, float aThreshold) { + return (aAngle < aThreshold || aAngle > (M_PI - aThreshold)); +} + +bool IsCloseToVertical(float aAngle, float aThreshold) { + return (fabs(aAngle - (M_PI / 2)) < aThreshold); +} + +bool IsStuckAtBottom(gfxFloat aTranslation, + const LayerRectAbsolute& aInnerRange, + const LayerRectAbsolute& aOuterRange) { + // The item will be stuck at the bottom if the async scroll delta is in + // the range [aOuterRange.Y(), aInnerRange.Y()]. Since the translation + // is negated with repect to the async scroll delta (i.e. scrolling down + // produces a positive scroll delta and negative translation), we invert it + // and check to see if it falls in the specified range. + return aOuterRange.Y() <= -aTranslation && -aTranslation <= aInnerRange.Y(); +} + +bool IsStuckAtTop(gfxFloat aTranslation, const LayerRectAbsolute& aInnerRange, + const LayerRectAbsolute& aOuterRange) { + // Same as IsStuckAtBottom, except we want to check for the range + // [aInnerRange.YMost(), aOuterRange.YMost()]. + return aInnerRange.YMost() <= -aTranslation && + -aTranslation <= aOuterRange.YMost(); +} + +ScreenPoint ComputeFixedMarginsOffset( + const ScreenMargin& aCompositorFixedLayerMargins, SideBits aFixedSides, + const ScreenMargin& aGeckoFixedLayerMargins) { + // Work out the necessary translation, in screen space. + ScreenPoint translation; + + ScreenMargin effectiveMargin = + aCompositorFixedLayerMargins - aGeckoFixedLayerMargins; + if ((aFixedSides & SideBits::eLeftRight) == SideBits::eLeftRight) { + translation.x += (effectiveMargin.left - effectiveMargin.right) / 2; + } else if (aFixedSides & SideBits::eRight) { + translation.x -= effectiveMargin.right; + } else if (aFixedSides & SideBits::eLeft) { + translation.x += effectiveMargin.left; + } + + if ((aFixedSides & SideBits::eTopBottom) == SideBits::eTopBottom) { + translation.y += (effectiveMargin.top - effectiveMargin.bottom) / 2; + } else if (aFixedSides & SideBits::eBottom) { + translation.y -= effectiveMargin.bottom; + } else if (aFixedSides & SideBits::eTop) { + translation.y += effectiveMargin.top; + } + + return translation; +} + +bool AboutToCheckerboard(const FrameMetrics& aPaintedMetrics, + const FrameMetrics& aCompositorMetrics) { + // The main-thread code to compute the painted area can introduce some + // rounding error due to multiple unit conversions, so we inflate the rect by + // one app unit to account for that. + CSSRect painted = aPaintedMetrics.GetDisplayPort() + + aPaintedMetrics.GetLayoutScrollOffset(); + painted.Inflate(CSSMargin::FromAppUnits(nsMargin(1, 1, 1, 1))); + + // Inflate the rect by the danger zone. See the description of the danger zone + // prefs in AsyncPanZoomController.cpp for an explanation of this. + CSSRect visible = + CSSRect(aCompositorMetrics.GetVisualScrollOffset(), + aCompositorMetrics.CalculateBoundedCompositedSizeInCssPixels()); + visible.Inflate(ScreenSize(StaticPrefs::apz_danger_zone_x(), + StaticPrefs::apz_danger_zone_y()) / + aCompositorMetrics.DisplayportPixelsPerCSSPixel()); + + // Clamp both rects to the scrollable rect, because having either of those + // exceed the scrollable rect doesn't make sense, and could lead to false + // positives. + painted = painted.Intersect(aPaintedMetrics.GetScrollableRect()); + visible = visible.Intersect(aPaintedMetrics.GetScrollableRect()); + + return !painted.Contains(visible); +} + +SideBits GetOverscrollSideBits(const ParentLayerPoint& aOverscrollAmount) { + SideBits sides = SideBits::eNone; + + if (aOverscrollAmount.x < 0) { + sides |= SideBits::eLeft; + } else if (aOverscrollAmount.x > 0) { + sides |= SideBits::eRight; + } + + if (aOverscrollAmount.y < 0) { + sides |= SideBits::eTop; + } else if (aOverscrollAmount.y > 0) { + sides |= SideBits::eBottom; + } + + return sides; +} + +} // namespace apz +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/APZUtils.h b/gfx/layers/apz/src/APZUtils.h new file mode 100644 index 0000000000..8adaa71339 --- /dev/null +++ b/gfx/layers/apz/src/APZUtils.h @@ -0,0 +1,220 @@ +/* -*- 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_APZUtils_h +#define mozilla_layers_APZUtils_h + +// This file is for APZ-related utilities that are used by code in gfx/layers +// only. For APZ-related utilities used by the Rest of the World (widget/, +// layout/, dom/, IPDL protocols, etc.), use APZPublicUtils.h. +// Do not include this header from source files outside of gfx/layers. + +#include // for uint32_t +#include +#include "gfxTypes.h" +#include "FrameMetrics.h" +#include "LayersTypes.h" +#include "UnitTransforms.h" +#include "mozilla/gfx/CompositorHitTestInfo.h" +#include "mozilla/gfx/Point.h" +#include "mozilla/DefineEnum.h" +#include "mozilla/EnumSet.h" +#include "mozilla/FloatingPoint.h" + +namespace mozilla { + +namespace layers { + +enum CancelAnimationFlags : uint32_t { + Default = 0x0, /* Cancel all animations */ + ExcludeOverscroll = 0x1, /* Don't clear overscroll */ + ScrollSnap = 0x2, /* Snap to snap points */ + ExcludeWheel = 0x4, /* Don't stop wheel smooth-scroll animations */ + TriggeredExternally = 0x8, /* Cancellation was not triggered by APZ in + response to an input event */ +}; + +inline CancelAnimationFlags operator|(CancelAnimationFlags a, + CancelAnimationFlags b) { + return static_cast(static_cast(a) | + static_cast(b)); +} + +// clang-format off +enum class ScrollSource { + // Touch-screen. + Touchscreen, + + // Touchpad with gesture support. + Touchpad, + + // Mouse wheel. + Wheel, + + // Keyboard + Keyboard, +}; +// clang-format on + +inline bool ScrollSourceRespectsDisregardedDirections(ScrollSource aSource) { + return aSource == ScrollSource::Wheel || aSource == ScrollSource::Touchpad; +} + +inline bool ScrollSourceAllowsOverscroll(ScrollSource aSource) { + return aSource == ScrollSource::Touchpad || + aSource == ScrollSource::Touchscreen; +} + +// Epsilon to be used when comparing 'float' coordinate values +// with FuzzyEqualsAdditive. The rationale is that 'float' has 7 decimal +// digits of precision, and coordinate values should be no larger than in the +// ten thousands. Note also that the smallest legitimate difference in page +// coordinates is 1 app unit, which is 1/60 of a (CSS pixel), so this epsilon +// isn't too large. +const CSSCoord COORDINATE_EPSILON = 0.01f; + +inline bool IsZero(const CSSPoint& aPoint) { + return FuzzyEqualsAdditive(aPoint.x, CSSCoord(), COORDINATE_EPSILON) && + FuzzyEqualsAdditive(aPoint.y, CSSCoord(), COORDINATE_EPSILON); +} + +// Represents async transforms consisting of a scale and a translation. +struct AsyncTransform { + explicit AsyncTransform( + LayerToParentLayerScale aScale = LayerToParentLayerScale(), + ParentLayerPoint aTranslation = ParentLayerPoint()) + : mScale(aScale), mTranslation(aTranslation) {} + + operator AsyncTransformComponentMatrix() const { + return AsyncTransformComponentMatrix::Scaling(mScale.scale, mScale.scale, 1) + .PostTranslate(mTranslation.x, mTranslation.y, 0); + } + + bool operator==(const AsyncTransform& rhs) const { + return mTranslation == rhs.mTranslation && mScale == rhs.mScale; + } + + bool operator!=(const AsyncTransform& rhs) const { return !(*this == rhs); } + + LayerToParentLayerScale mScale; + ParentLayerPoint mTranslation; +}; + +// Deem an AsyncTransformComponentMatrix (obtained by multiplying together +// one or more AsyncTransformComponentMatrix objects) as constituting a +// complete async transform. +inline AsyncTransformMatrix CompleteAsyncTransform( + const AsyncTransformComponentMatrix& aMatrix) { + return ViewAs( + aMatrix, PixelCastJustification::MultipleAsyncTransforms); +} + +struct TargetConfirmationFlags final { + explicit TargetConfirmationFlags(bool aTargetConfirmed) + : mTargetConfirmed(aTargetConfirmed), + mRequiresTargetConfirmation(false), + mHitScrollbar(false), + mHitScrollThumb(false), + mDispatchToContent(false) {} + + explicit TargetConfirmationFlags( + const gfx::CompositorHitTestInfo& aHitTestInfo) + : mTargetConfirmed( + (aHitTestInfo != gfx::CompositorHitTestInvisibleToHit) && + (aHitTestInfo & gfx::CompositorHitTestDispatchToContent).isEmpty()), + mRequiresTargetConfirmation(aHitTestInfo.contains( + gfx::CompositorHitTestFlags::eRequiresTargetConfirmation)), + mHitScrollbar( + aHitTestInfo.contains(gfx::CompositorHitTestFlags::eScrollbar)), + mHitScrollThumb(aHitTestInfo.contains( + gfx::CompositorHitTestFlags::eScrollbarThumb)), + mDispatchToContent( + !(aHitTestInfo & gfx::CompositorHitTestDispatchToContent) + .isEmpty()) {} + + bool mTargetConfirmed : 1; + bool mRequiresTargetConfirmation : 1; + bool mHitScrollbar : 1; + bool mHitScrollThumb : 1; + bool mDispatchToContent : 1; +}; + +enum class AsyncTransformComponent { eLayout, eVisual }; + +using AsyncTransformComponents = EnumSet; + +constexpr AsyncTransformComponents LayoutAndVisual( + AsyncTransformComponent::eLayout, AsyncTransformComponent::eVisual); + +/** + * Metrics that GeckoView wants to know at every composite. + * These are the effective visual scroll offset and zoom level of + * the root content APZC at composition time. + */ +struct GeckoViewMetrics { + CSSPoint mVisualScrollOffset; + CSSToParentLayerScale mZoom; +}; + +namespace apz { + +/** + * Is aAngle within the given threshold of the horizontal axis? + * @param aAngle an angle in radians in the range [0, pi] + * @param aThreshold an angle in radians in the range [0, pi/2] + */ +bool IsCloseToHorizontal(float aAngle, float aThreshold); + +// As above, but for the vertical axis. +bool IsCloseToVertical(float aAngle, float aThreshold); + +// Returns true if a sticky layer with async translation |aTranslation| is +// stuck with a bottom margin. The inner/outer ranges are produced by the main +// thread at the last paint, and so |aTranslation| only needs to be the +// async translation from the last paint. +bool IsStuckAtBottom(gfxFloat aTranslation, + const LayerRectAbsolute& aInnerRange, + const LayerRectAbsolute& aOuterRange); + +// Returns true if a sticky layer with async translation |aTranslation| is +// stuck with a top margin. +bool IsStuckAtTop(gfxFloat aTranslation, const LayerRectAbsolute& aInnerRange, + const LayerRectAbsolute& aOuterRange); + +/** + * Compute the translation that should be applied to a layer that's fixed + * at |eFixedSides|, to respect the fixed layer margins |aFixedMargins|. + */ +ScreenPoint ComputeFixedMarginsOffset( + const ScreenMargin& aCompositorFixedLayerMargins, SideBits aFixedSides, + const ScreenMargin& aGeckoFixedLayerMargins); + +/** + * Takes the visible rect from the compositor metrics, adds a pref-based + * margin around it, and checks to see if it is contained inside the painted + * rect from the painted metrics. Returns true if it is contained, or false + * if not. Returning false means that a (relatively) small amount of async + * scrolling/zooming can result in the visible area going outside the painted + * area and resulting in visual checkerboarding. + * Note that this may return false positives for cases where the scrollframe + * in question is nested inside other scrollframes, as the composition bounds + * used to determine the visible rect may in fact be clipped by enclosing + * scrollframes, but that is not accounted for in this function. + */ +bool AboutToCheckerboard(const FrameMetrics& aPaintedMetrics, + const FrameMetrics& aCompositorMetrics); + +/** + * Returns SideBits where the given |aOverscrollAmount| overscrolls. + */ +SideBits GetOverscrollSideBits(const ParentLayerPoint& aOverscrollAmount); + +} // namespace apz + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_APZUtils_h diff --git a/gfx/layers/apz/src/AndroidAPZ.cpp b/gfx/layers/apz/src/AndroidAPZ.cpp new file mode 100644 index 0000000000..4895c893de --- /dev/null +++ b/gfx/layers/apz/src/AndroidAPZ.cpp @@ -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/. */ + +#include "AndroidAPZ.h" + +#include "AndroidFlingPhysics.h" +#include "AndroidVelocityTracker.h" +#include "AsyncPanZoomController.h" +#include "GenericFlingAnimation.h" +#include "OverscrollHandoffState.h" + +namespace mozilla { +namespace layers { + +AsyncPanZoomAnimation* AndroidSpecificState::CreateFlingAnimation( + AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState, + float aPLPPI) { + return new GenericFlingAnimation(aApzc, aHandoffState, + aPLPPI); +} + +UniquePtr AndroidSpecificState::CreateVelocityTracker( + Axis* aAxis) { + return MakeUnique(); +} + +/* static */ +void AndroidSpecificState::InitializeGlobalState() { + AndroidFlingPhysics::InitializeGlobalState(); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/AndroidAPZ.h b/gfx/layers/apz/src/AndroidAPZ.h new file mode 100644 index 0000000000..ab30b4e612 --- /dev/null +++ b/gfx/layers/apz/src/AndroidAPZ.h @@ -0,0 +1,34 @@ +/* -*- 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_AndroidAPZ_h_ +#define mozilla_layers_AndroidAPZ_h_ + +#include "AsyncPanZoomAnimation.h" +#include "AsyncPanZoomController.h" + +namespace mozilla { +namespace layers { + +class AndroidSpecificState : public PlatformSpecificStateBase { + public: + virtual AndroidSpecificState* AsAndroidSpecificState() override { + return this; + } + + virtual AsyncPanZoomAnimation* CreateFlingAnimation( + AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState, + float aPLPPI) override; + virtual UniquePtr CreateVelocityTracker( + Axis* aAxis) override; + + static void InitializeGlobalState(); +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_AndroidAPZ_h_ diff --git a/gfx/layers/apz/src/AndroidFlingPhysics.cpp b/gfx/layers/apz/src/AndroidFlingPhysics.cpp new file mode 100644 index 0000000000..d18f4be4d4 --- /dev/null +++ b/gfx/layers/apz/src/AndroidFlingPhysics.cpp @@ -0,0 +1,218 @@ +/* -*- 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 "AndroidFlingPhysics.h" + +#include + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla { +namespace layers { + +// The fling physics calculations implemented here are adapted from +// Chrome's implementation of fling physics on Android: +// https://cs.chromium.org/chromium/src/ui/events/android/scroller.cc?rcl=3ae3aaff927038a5c644926842cb0c31dea60c79 + +static double ComputeDeceleration(float aDPI) { + const float kFriction = 0.84f; + const float kGravityEarth = 9.80665f; + return kGravityEarth // g (m/s^2) + * 39.37f // inch/meter + * aDPI // pixels/inch + * kFriction; +} + +// == std::log(0.78f) / std::log(0.9f) +const float kDecelerationRate = 2.3582018f; + +// Default friction constant in android.view.ViewConfiguration. +static float GetFlingFriction() { + return StaticPrefs::apz_android_chrome_fling_physics_friction(); +} + +// Tension lines cross at (GetInflexion(), 1). +static float GetInflexion() { + // Clamp the inflexion to the range [0,1]. Values outside of this range + // do not make sense in the physics model, and for negative values the + // approximation used to compute the spline curve does not converge. + const float inflexion = + StaticPrefs::apz_android_chrome_fling_physics_inflexion(); + if (inflexion < 0.0f) { + return 0.0f; + } + if (inflexion > 1.0f) { + return 1.0f; + } + return inflexion; +} + +// Fling scroll is stopped when the scroll position is |kThresholdForFlingEnd| +// pixels or closer from the end. +static float GetThresholdForFlingEnd() { + return StaticPrefs::apz_android_chrome_fling_physics_stop_threshold(); +} + +static double ComputeSplineDeceleration(ParentLayerCoord aVelocity, + double aTuningCoeff) { + float velocityPerSec = aVelocity * 1000.0f; + return std::log(GetInflexion() * velocityPerSec / + (GetFlingFriction() * aTuningCoeff)); +} + +static TimeDuration ComputeFlingDuration(ParentLayerCoord aVelocity, + double aTuningCoeff) { + const double splineDecel = ComputeSplineDeceleration(aVelocity, aTuningCoeff); + const double timeSeconds = std::exp(splineDecel / (kDecelerationRate - 1.0)); + return TimeDuration::FromSeconds(timeSeconds); +} + +static ParentLayerCoord ComputeFlingDistance(ParentLayerCoord aVelocity, + double aTuningCoeff) { + const double splineDecel = ComputeSplineDeceleration(aVelocity, aTuningCoeff); + return GetFlingFriction() * aTuningCoeff * + std::exp(kDecelerationRate / (kDecelerationRate - 1.0) * splineDecel); +} + +struct SplineConstants { + public: + SplineConstants() { + const float kStartTension = 0.5f; + const float kEndTension = 1.0f; + const float kP1 = kStartTension * GetInflexion(); + const float kP2 = 1.0f - kEndTension * (1.0f - GetInflexion()); + + float xMin = 0.0f; + for (int i = 0; i < kNumSamples; i++) { + const float alpha = static_cast(i) / kNumSamples; + + float xMax = 1.0f; + float x, tx, coef; + // While the inflexion can be overridden by the user, it's clamped to + // [0,1]. For values in this range, the approximation algorithm below + // should converge in < 20 iterations. For good measure, we impose an + // iteration limit as well. + static const int sIterationLimit = 100; + int iterations = 0; + while (iterations++ < sIterationLimit) { + x = xMin + (xMax - xMin) / 2.0f; + coef = 3.0f * x * (1.0f - x); + tx = coef * ((1.0f - x) * kP1 + x * kP2) + x * x * x; + if (FuzzyEqualsAdditive(tx, alpha)) { + break; + } + if (tx > alpha) { + xMax = x; + } else { + xMin = x; + } + } + mSplinePositions[i] = coef * ((1.0f - x) * kStartTension + x) + x * x * x; + } + mSplinePositions[kNumSamples] = 1.0f; + } + + void CalculateCoefficients(float aTime, float* aOutDistanceCoef, + float* aOutVelocityCoef) { + *aOutDistanceCoef = 1.0f; + *aOutVelocityCoef = 0.0f; + const int index = static_cast(kNumSamples * aTime); + if (index < kNumSamples) { + const float tInf = static_cast(index) / kNumSamples; + const float dInf = mSplinePositions[index]; + const float tSup = static_cast(index + 1) / kNumSamples; + const float dSup = mSplinePositions[index + 1]; + *aOutVelocityCoef = (dSup - dInf) / (tSup - tInf); + *aOutDistanceCoef = dInf + (aTime - tInf) * *aOutVelocityCoef; + } + } + + private: + static const int kNumSamples = 100; + float mSplinePositions[kNumSamples + 1]; +}; + +StaticAutoPtr gSplineConstants; + +/* static */ +void AndroidFlingPhysics::InitializeGlobalState() { + gSplineConstants = new SplineConstants(); + ClearOnShutdown(&gSplineConstants); +} + +void AndroidFlingPhysics::Init(const ParentLayerPoint& aStartingVelocity, + float aPLPPI) { + mVelocity = aStartingVelocity.Length(); + // We should not have created a fling animation if there is no velocity. + MOZ_ASSERT(mVelocity != 0.0f); + const double tuningCoeff = ComputeDeceleration(aPLPPI); + mTargetDuration = ComputeFlingDuration(mVelocity, tuningCoeff); + MOZ_ASSERT(!mTargetDuration.IsZero()); + mDurationSoFar = TimeDuration(); + mLastPos = ParentLayerPoint(); + mCurrentPos = ParentLayerPoint(); + float coeffX = + mVelocity == 0 ? 1.0f : aStartingVelocity.x.value / mVelocity.value; + float coeffY = + mVelocity == 0 ? 1.0f : aStartingVelocity.y.value / mVelocity.value; + mTargetDistance = ComputeFlingDistance(mVelocity, tuningCoeff); + mTargetPos = + ParentLayerPoint(mTargetDistance * coeffX, mTargetDistance * coeffY); + const float hyp = mTargetPos.Length(); + if (FuzzyEqualsAdditive(hyp, 0.0f)) { + mDeltaNorm = ParentLayerPoint(1, 1); + } else { + mDeltaNorm = ParentLayerPoint(mTargetPos.x / hyp, mTargetPos.y / hyp); + } +} +void AndroidFlingPhysics::Sample(const TimeDuration& aDelta, + ParentLayerPoint* aOutVelocity, + ParentLayerPoint* aOutOffset) { + float newVelocity; + if (SampleImpl(aDelta, &newVelocity)) { + *aOutOffset = (mCurrentPos - mLastPos); + *aOutVelocity = ParentLayerPoint(mDeltaNorm.x * newVelocity, + mDeltaNorm.y * newVelocity); + mLastPos = mCurrentPos; + } else { + *aOutOffset = (mTargetPos - mLastPos); + *aOutVelocity = ParentLayerPoint(); + } +} + +bool AndroidFlingPhysics::SampleImpl(const TimeDuration& aDelta, + float* aOutVelocity) { + mDurationSoFar += aDelta; + if (mDurationSoFar >= mTargetDuration) { + return false; + } + + const float timeRatio = + mDurationSoFar.ToSeconds() / mTargetDuration.ToSeconds(); + float distanceCoef = 1.0f; + float velocityCoef = 0.0f; + gSplineConstants->CalculateCoefficients(timeRatio, &distanceCoef, + &velocityCoef); + + // The caller expects the velocity in pixels per _millisecond_. + *aOutVelocity = + velocityCoef * mTargetDistance / mTargetDuration.ToMilliseconds(); + + mCurrentPos = mTargetPos * distanceCoef; + + ParentLayerPoint remainder = mTargetPos - mCurrentPos; + const float threshold = GetThresholdForFlingEnd(); + if (fabsf(remainder.x) < threshold && fabsf(remainder.y) < threshold) { + return false; + } + + return true; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/AndroidFlingPhysics.h b/gfx/layers/apz/src/AndroidFlingPhysics.h new file mode 100644 index 0000000000..68fb53e804 --- /dev/null +++ b/gfx/layers/apz/src/AndroidFlingPhysics.h @@ -0,0 +1,45 @@ +/* -*- 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_AndroidFlingPhysics_h_ +#define mozilla_layers_AndroidFlingPhysics_h_ + +#include "AsyncPanZoomController.h" +#include "Units.h" +#include "mozilla/Assertions.h" + +namespace mozilla { +namespace layers { + +class AndroidFlingPhysics { + public: + void Init(const ParentLayerPoint& aVelocity, float aPLPPI); + void Sample(const TimeDuration& aDelta, ParentLayerPoint* aOutVelocity, + ParentLayerPoint* aOutOffset); + + static void InitializeGlobalState(); + + private: + // Returns false if the animation should end. + bool SampleImpl(const TimeDuration& aDelta, float* aOutVelocity); + + // Information pertaining to the current fling. + // This is initialized on each call to Init(). + ParentLayerCoord mVelocity; // diagonal velocity (length of velocity vector) + TimeDuration mTargetDuration; + TimeDuration mDurationSoFar; + ParentLayerPoint mLastPos; + ParentLayerPoint mCurrentPos; + ParentLayerCoord mTargetDistance; // diagonal distance + ParentLayerPoint mTargetPos; // really a target *offset* relative to the + // start position, which we don't track + ParentLayerPoint mDeltaNorm; // mTargetPos with length normalized to 1 +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_AndroidFlingPhysics_h_ diff --git a/gfx/layers/apz/src/AndroidVelocityTracker.cpp b/gfx/layers/apz/src/AndroidVelocityTracker.cpp new file mode 100644 index 0000000000..a355811a00 --- /dev/null +++ b/gfx/layers/apz/src/AndroidVelocityTracker.cpp @@ -0,0 +1,288 @@ +/* -*- 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 "AndroidVelocityTracker.h" + +#include "mozilla/StaticPrefs_apz.h" + +namespace mozilla { +namespace layers { + +// This velocity tracker implementation was adapted from Chromium's +// second-order unweighted least-squares velocity tracker strategy +// (https://cs.chromium.org/chromium/src/ui/events/gesture_detection/velocity_tracker.cc?l=101&rcl=9ea9a086d4f54c702ec9a38e55fb3eb8bbc2401b). + +// Threshold between position updates for determining that a pointer has +// stopped moving. Some input devices do not send move events in the +// case where a pointer has stopped. We need to detect this case so that we can +// accurately predict the velocity after the pointer starts moving again. +static const TimeDuration kAssumePointerMoveStoppedTime = + TimeDuration::FromMilliseconds(40); + +// The degree of the approximation. +static const uint8_t kDegree = 2; + +// The degree of the polynomial used in SolveLeastSquares(). +// This should be the degree of the approximation plus one. +static const uint8_t kPolyDegree = kDegree + 1; + +// Maximum size of position history. +static const uint8_t kHistorySize = 20; + +AndroidVelocityTracker::AndroidVelocityTracker() {} + +void AndroidVelocityTracker::StartTracking(ParentLayerCoord aPos, + TimeStamp aTimestamp) { + Clear(); + mHistory.AppendElement(std::make_pair(aTimestamp, aPos)); + mLastEventTime = aTimestamp; +} + +Maybe AndroidVelocityTracker::AddPosition(ParentLayerCoord aPos, + TimeStamp aTimestamp) { + if ((aTimestamp - mLastEventTime) >= kAssumePointerMoveStoppedTime) { + Clear(); + } + + if ((aTimestamp - mLastEventTime).ToMilliseconds() < 1.0) { + // If we get a sample within a millisecond of the previous one, + // just update its position. Two samples in the history with the + // same timestamp can lead to things like infinite velocities. + if (mHistory.Length() > 0) { + mHistory[mHistory.Length() - 1].second = aPos; + } + } else { + mHistory.AppendElement(std::make_pair(aTimestamp, aPos)); + if (mHistory.Length() > kHistorySize) { + mHistory.RemoveElementAt(0); + } + } + + mLastEventTime = aTimestamp; + + if (mHistory.Length() < 2) { + return Nothing(); + } + + auto start = mHistory[mHistory.Length() - 2]; + auto end = mHistory[mHistory.Length() - 1]; + auto velocity = + (end.second - start.second) / (end.first - start.first).ToMilliseconds(); + // The velocity needs to be negated because the positions represent + // touch positions, and the direction of scrolling is opposite to the + // direction of the finger's movement. + return Some(-velocity); +} + +static float VectorDot(const float* a, const float* b, uint32_t m) { + float r = 0; + while (m--) { + r += *(a++) * *(b++); + } + return r; +} + +static float VectorNorm(const float* a, uint32_t m) { + float r = 0; + while (m--) { + float t = *(a++); + r += t * t; + } + return sqrtf(r); +} + +/** + * Solves a linear least squares problem to obtain a N degree polynomial that + * fits the specified input data as nearly as possible. + * + * Returns true if a solution is found, false otherwise. + * + * The input consists of two vectors of data points X and Y with indices 0..m-1 + * along with a weight vector W of the same size. + * + * The output is a vector B with indices 0..n that describes a polynomial + * that fits the data, such the sum of W[i] * W[i] * abs(Y[i] - (B[0] + B[1] + * X[i] * + B[2] X[i]^2 ... B[n] X[i]^n)) for all i between 0 and m-1 is + * minimized. + * + * Accordingly, the weight vector W should be initialized by the caller with the + * reciprocal square root of the variance of the error in each input data point. + * In other words, an ideal choice for W would be W[i] = 1 / var(Y[i]) = 1 / + * stddev(Y[i]). + * The weights express the relative importance of each data point. If the + * weights are* all 1, then the data points are considered to be of equal + * importance when fitting the polynomial. It is a good idea to choose weights + * that diminish the importance of data points that may have higher than usual + * error margins. + * + * Errors among data points are assumed to be independent. W is represented + * here as a vector although in the literature it is typically taken to be a + * diagonal matrix. + * + * That is to say, the function that generated the input data can be + * approximated by y(x) ~= B[0] + B[1] x + B[2] x^2 + ... + B[n] x^n. + * + * The coefficient of determination (R^2) is also returned to describe the + * goodness of fit of the model for the given data. It is a value between 0 + * and 1, where 1 indicates perfect correspondence. + * + * This function first expands the X vector to a m by n matrix A such that + * A[i][0] = 1, A[i][1] = X[i], A[i][2] = X[i]^2, ..., A[i][n] = X[i]^n, then + * multiplies it by w[i]. + * + * Then it calculates the QR decomposition of A yielding an m by m orthonormal + * matrix Q and an m by n upper triangular matrix R. Because R is upper + * triangular (lower part is all zeroes), we can simplify the decomposition into + * an m by n matrix Q1 and a n by n matrix R1 such that A = Q1 R1. + * + * Finally we solve the system of linear equations given by + * R1 B = (Qtranspose W Y) to find B. + * + * For efficiency, we lay out A and Q column-wise in memory because we + * frequently operate on the column vectors. Conversely, we lay out R row-wise. + * + * http://en.wikipedia.org/wiki/Numerical_methods_for_linear_least_squares + * http://en.wikipedia.org/wiki/Gram-Schmidt + */ +static bool SolveLeastSquares(const float* x, const float* y, const float* w, + uint32_t m, uint32_t n, float* out_b) { + // MSVC does not support variable-length arrays (used by the original Android + // implementation of this function). +#if defined(COMPILER_MSVC) + const uint32_t M_ARRAY_LENGTH = VelocityTracker::kHistorySize; + const uint32_t N_ARRAY_LENGTH = VelocityTracker::kPolyDegree; + DCHECK_LE(m, M_ARRAY_LENGTH); + DCHECK_LE(n, N_ARRAY_LENGTH); +#else + const uint32_t M_ARRAY_LENGTH = m; + const uint32_t N_ARRAY_LENGTH = n; +#endif + + // Expand the X vector to a matrix A, pre-multiplied by the weights. + float a[N_ARRAY_LENGTH][M_ARRAY_LENGTH]; // column-major order + for (uint32_t h = 0; h < m; h++) { + a[0][h] = w[h]; + for (uint32_t i = 1; i < n; i++) { + a[i][h] = a[i - 1][h] * x[h]; + } + } + + // Apply the Gram-Schmidt process to A to obtain its QR decomposition. + + // Orthonormal basis, column-major order. + float q[N_ARRAY_LENGTH][M_ARRAY_LENGTH]; + // Upper triangular matrix, row-major order. + float r[N_ARRAY_LENGTH][N_ARRAY_LENGTH]; + for (uint32_t j = 0; j < n; j++) { + for (uint32_t h = 0; h < m; h++) { + q[j][h] = a[j][h]; + } + for (uint32_t i = 0; i < j; i++) { + float dot = VectorDot(&q[j][0], &q[i][0], m); + for (uint32_t h = 0; h < m; h++) { + q[j][h] -= dot * q[i][h]; + } + } + + float norm = VectorNorm(&q[j][0], m); + if (norm < 0.000001f) { + // vectors are linearly dependent or zero so no solution + return false; + } + + float invNorm = 1.0f / norm; + for (uint32_t h = 0; h < m; h++) { + q[j][h] *= invNorm; + } + for (uint32_t i = 0; i < n; i++) { + r[j][i] = i < j ? 0 : VectorDot(&q[j][0], &a[i][0], m); + } + } + + // Solve R B = Qt W Y to find B. This is easy because R is upper triangular. + // We just work from bottom-right to top-left calculating B's coefficients. + float wy[M_ARRAY_LENGTH]; + for (uint32_t h = 0; h < m; h++) { + wy[h] = y[h] * w[h]; + } + for (uint32_t i = n; i-- != 0;) { + out_b[i] = VectorDot(&q[i][0], wy, m); + for (uint32_t j = n - 1; j > i; j--) { + out_b[i] -= r[i][j] * out_b[j]; + } + out_b[i] /= r[i][i]; + } + + return true; +} + +Maybe AndroidVelocityTracker::ComputeVelocity(TimeStamp aTimestamp) { + if (mHistory.IsEmpty()) { + return Nothing{}; + } + + // Polynomial coefficients describing motion along the axis. + float xcoeff[kPolyDegree + 1]; + for (size_t i = 0; i <= kPolyDegree; i++) { + xcoeff[i] = 0; + } + + // Iterate over movement samples in reverse time order and collect samples. + float pos[kHistorySize]; + float w[kHistorySize]; + float time[kHistorySize]; + uint32_t m = 0; + int index = mHistory.Length() - 1; + const TimeDuration horizon = TimeDuration::FromMilliseconds( + StaticPrefs::apz_velocity_relevance_time_ms()); + const auto& newest_movement = mHistory[index]; + + do { + const auto& movement = mHistory[index]; + TimeDuration age = newest_movement.first - movement.first; + if (age > horizon) break; + + ParentLayerCoord position = movement.second; + pos[m] = position; + w[m] = 1.0f; + time[m] = + -static_cast(age.ToMilliseconds()) / 1000.0f; // in seconds + index--; + m++; + } while (index >= 0); + + if (m == 0) { + return Nothing{}; // no data + } + + // Calculate a least squares polynomial fit. + + // Polynomial degree (number of coefficients), or zero if no information is + // available. + uint32_t degree = kDegree; + if (degree > m - 1) { + degree = m - 1; + } + + if (degree >= 1) { // otherwise, no velocity data available + uint32_t n = degree + 1; + if (SolveLeastSquares(time, pos, w, m, n, xcoeff)) { + float velocity = xcoeff[1]; + + // The velocity needs to be negated because the positions represent + // touch positions, and the direction of scrolling is opposite to the + // direction of the finger's movement. + return Some(-velocity / 1000.0f); // convert to pixels per millisecond + } + } + + return Nothing{}; +} + +void AndroidVelocityTracker::Clear() { mHistory.Clear(); } + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/AndroidVelocityTracker.h b/gfx/layers/apz/src/AndroidVelocityTracker.h new file mode 100644 index 0000000000..40e346a9ea --- /dev/null +++ b/gfx/layers/apz/src/AndroidVelocityTracker.h @@ -0,0 +1,42 @@ +/* -*- 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_AndroidVelocityTracker_h +#define mozilla_layers_AndroidVelocityTracker_h + +#include +#include + +#include "Axis.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "nsTArray.h" + +namespace mozilla { +namespace layers { + +class AndroidVelocityTracker : public VelocityTracker { + public: + explicit AndroidVelocityTracker(); + void StartTracking(ParentLayerCoord aPos, TimeStamp aTimestamp) override; + Maybe AddPosition(ParentLayerCoord aPos, + TimeStamp aTimestamp) override; + Maybe ComputeVelocity(TimeStamp aTimestamp) override; + void Clear() override; + + private: + // A queue of (timestamp, position) pairs; these are the historical + // positions at the given timestamps. + nsTArray> mHistory; + // The last time an event was added to the tracker, or the null moment if no + // events have been added. + TimeStamp mLastEventTime; +}; + +} // namespace layers +} // namespace mozilla + +#endif diff --git a/gfx/layers/apz/src/AsyncDragMetrics.h b/gfx/layers/apz/src/AsyncDragMetrics.h new file mode 100644 index 0000000000..5374818a06 --- /dev/null +++ b/gfx/layers/apz/src/AsyncDragMetrics.h @@ -0,0 +1,54 @@ +/* -*- 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_DragMetrics_h +#define mozilla_layers_DragMetrics_h + +#include "mozilla/layers/ScrollableLayerGuid.h" +#include "LayersTypes.h" +#include "mozilla/Maybe.h" + +namespace IPC { +template +struct ParamTraits; +} // namespace IPC + +namespace mozilla { + +namespace layers { + +class AsyncDragMetrics { + friend struct IPC::ParamTraits; + + public: + // IPC constructor + AsyncDragMetrics() + : mViewId(0), + mPresShellId(0), + mDragStartSequenceNumber(0), + mScrollbarDragOffset(0) {} + + AsyncDragMetrics(const ScrollableLayerGuid::ViewID& aViewId, + uint32_t aPresShellId, uint64_t aDragStartSequenceNumber, + OuterCSSCoord aScrollbarDragOffset, + ScrollDirection aDirection) + : mViewId(aViewId), + mPresShellId(aPresShellId), + mDragStartSequenceNumber(aDragStartSequenceNumber), + mScrollbarDragOffset(aScrollbarDragOffset), + mDirection(Some(aDirection)) {} + + ScrollableLayerGuid::ViewID mViewId; + uint32_t mPresShellId; + uint64_t mDragStartSequenceNumber; + OuterCSSCoord mScrollbarDragOffset; // relative to the thumb's start offset + Maybe mDirection; +}; + +} // namespace layers +} // namespace mozilla + +#endif diff --git a/gfx/layers/apz/src/AsyncPanZoomAnimation.h b/gfx/layers/apz/src/AsyncPanZoomAnimation.h new file mode 100644 index 0000000000..127667afd9 --- /dev/null +++ b/gfx/layers/apz/src/AsyncPanZoomAnimation.h @@ -0,0 +1,101 @@ +/* -*- 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_AsyncPanZoomAnimation_h_ +#define mozilla_layers_AsyncPanZoomAnimation_h_ + +#include "APZUtils.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TimeStamp.h" +#include "nsISupportsImpl.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace layers { + +struct FrameMetrics; + +class WheelScrollAnimation; +class OverscrollAnimation; +class SmoothMsdScrollAnimation; +class SmoothScrollAnimation; + +class AsyncPanZoomAnimation { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AsyncPanZoomAnimation) + + public: + explicit AsyncPanZoomAnimation() = default; + + virtual bool DoSample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta) = 0; + + /** + * Attempt to handle a main-thread scroll offset update without cancelling + * the animation. This may or may not make sense depending on the type of + * the animation and whether the scroll update is relative or absolute. + * + * If the scroll update is relative, |aRelativeDelta| will contain the + * delta of the relative update. If the scroll update is absolute, + * |aRelativeDelta| will be Nothing() (the animation can check the APZC's + * FrameMetrics for the new absolute scroll offset if it wants to handle + * and absolute update). + * + * Returns whether the animation could handle the scroll update. If the + * return value is false, the animation will be cancelled. + */ + virtual bool HandleScrollOffsetUpdate(const Maybe& aRelativeDelta) { + return false; + } + + bool Sample(FrameMetrics& aFrameMetrics, const TimeDuration& aDelta) { + // In some situations, particularly when handoff is involved, it's possible + // for |aDelta| to be negative on the first call to sample. Ignore such a + // sample here, to avoid each derived class having to deal with this case. + if (aDelta.ToMilliseconds() <= 0) { + return true; + } + + return DoSample(aFrameMetrics, aDelta); + } + + /** + * Get the deferred tasks in |mDeferredTasks| and place them in |aTasks|. See + * |mDeferredTasks| for more information. Clears |mDeferredTasks|. + */ + nsTArray> TakeDeferredTasks() { + return std::move(mDeferredTasks); + } + + virtual WheelScrollAnimation* AsWheelScrollAnimation() { return nullptr; } + virtual SmoothMsdScrollAnimation* AsSmoothMsdScrollAnimation() { + return nullptr; + } + virtual SmoothScrollAnimation* AsSmoothScrollAnimation() { return nullptr; } + virtual OverscrollAnimation* AsOverscrollAnimation() { return nullptr; } + + virtual bool WantsRepaints() { return true; } + + virtual void Cancel(CancelAnimationFlags aFlags) {} + + virtual bool WasTriggeredByScript() const { return false; } + + protected: + // Protected destructor, to discourage deletion outside of Release(): + virtual ~AsyncPanZoomAnimation() = default; + + /** + * Tasks scheduled for execution after the APZC's mMonitor is released. + * Derived classes can add tasks here in Sample(), and the APZC can call + * ExecuteDeferredTasks() to execute them. + */ + nsTArray> mDeferredTasks; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_AsyncPanZoomAnimation_h_ diff --git a/gfx/layers/apz/src/AsyncPanZoomController.cpp b/gfx/layers/apz/src/AsyncPanZoomController.cpp new file mode 100644 index 0000000000..802d5b9dbf --- /dev/null +++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp @@ -0,0 +1,6654 @@ +/* -*- 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 "AsyncPanZoomController.h" // for AsyncPanZoomController, etc + +#include // for fabsf, fabs, atan2 +#include // for uint32_t, uint64_t +#include // for int32_t +#include // for max, min +#include // for std::make_pair + +#include "APZCTreeManager.h" // for APZCTreeManager +#include "AsyncPanZoomAnimation.h" // for AsyncPanZoomAnimation +#include "AutoDirWheelDeltaAdjuster.h" // for APZAutoDirWheelDeltaAdjuster +#include "AutoscrollAnimation.h" // for AutoscrollAnimation +#include "Axis.h" // for AxisX, AxisY, Axis, etc +#include "CheckerboardEvent.h" // for CheckerboardEvent +#include "Compositor.h" // for Compositor +#include "DesktopFlingPhysics.h" // for DesktopFlingPhysics +#include "FrameMetrics.h" // for FrameMetrics, etc +#include "GenericFlingAnimation.h" // for GenericFlingAnimation +#include "GestureEventListener.h" // for GestureEventListener +#include "HitTestingTreeNode.h" // for HitTestingTreeNode +#include "InputData.h" // for MultiTouchInput, etc +#include "InputBlockState.h" // for InputBlockState, TouchBlockState +#include "InputQueue.h" // for InputQueue +#include "Overscroll.h" // for OverscrollAnimation +#include "OverscrollHandoffState.h" // for OverscrollHandoffState +#include "SimpleVelocityTracker.h" // for SimpleVelocityTracker +#include "Units.h" // for CSSRect, CSSPoint, etc +#include "UnitTransforms.h" // for TransformTo +#include "base/message_loop.h" // for MessageLoop +#include "base/task.h" // for NewRunnableMethod, etc +#include "gfxTypes.h" // for gfxFloat +#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc +#include "mozilla/BasicEvents.h" // for Modifiers, MODIFIER_* +#include "mozilla/ClearOnShutdown.h" // for ClearOnShutdown +#include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction +#include "mozilla/EventForwards.h" // for nsEventStatus_* +#include "mozilla/EventStateManager.h" // for EventStateManager +#include "mozilla/MouseEvents.h" // for WidgetWheelEvent +#include "mozilla/Preferences.h" // for Preferences +#include "mozilla/RecursiveMutex.h" // for RecursiveMutexAutoLock, etc +#include "mozilla/RefPtr.h" // for RefPtr +#include "mozilla/ScrollTypes.h" +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/StaticPrefs_general.h" +#include "mozilla/StaticPrefs_gfx.h" +#include "mozilla/StaticPrefs_mousewheel.h" +#include "mozilla/StaticPrefs_layers.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_slider.h" +#include "mozilla/StaticPrefs_test.h" +#include "mozilla/StaticPrefs_toolkit.h" +#include "mozilla/Telemetry.h" // for Telemetry +#include "mozilla/TimeStamp.h" // for TimeDuration, TimeStamp +#include "mozilla/dom/CheckerboardReportService.h" // for CheckerboardEventStorage +// note: CheckerboardReportService.h actually lives in gfx/layers/apz/util/ +#include "mozilla/dom/Touch.h" // for Touch +#include "mozilla/gfx/gfxVars.h" // for gfxVars +#include "mozilla/gfx/BasePoint.h" // for BasePoint +#include "mozilla/gfx/BaseRect.h" // for BaseRect +#include "mozilla/gfx/Point.h" // for Point, RoundedToInt, etc +#include "mozilla/gfx/Rect.h" // for RoundedIn +#include "mozilla/gfx/ScaleFactor.h" // for ScaleFactor +#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread, etc +#include "mozilla/layers/APZUtils.h" // for AsyncTransform +#include "mozilla/layers/CompositorController.h" // for CompositorController +#include "mozilla/layers/DirectionUtils.h" // for GetAxis{Start,End,Length,Scale} +#include "mozilla/layers/APZPublicUtils.h" // for GetScrollMode +#include "mozilla/mozalloc.h" // for operator new, etc +#include "mozilla/Unused.h" // for unused +#include "nsAlgorithm.h" // for clamped +#include "nsCOMPtr.h" // for already_AddRefed +#include "nsDebug.h" // for NS_WARNING +#include "nsLayoutUtils.h" +#include "nsMathUtils.h" // for NS_hypot +#include "nsPoint.h" // for nsIntPoint +#include "nsStyleConsts.h" +#include "nsTArray.h" // for nsTArray, nsTArray_Impl, etc +#include "nsThreadUtils.h" // for NS_IsMainThread +#include "nsViewportInfo.h" // for ViewportMinScale(), ViewportMaxScale() +#include "prsystem.h" // for PR_GetPhysicalMemorySize +#include "mozilla/ipc/SharedMemoryBasic.h" // for SharedMemoryBasic +#include "ScrollSnap.h" // for ScrollSnapUtils +#include "ScrollAnimationPhysics.h" // for ComputeAcceleratedWheelDelta +#include "SmoothMsdScrollAnimation.h" +#include "SmoothScrollAnimation.h" +#include "WheelScrollAnimation.h" +#if defined(MOZ_WIDGET_ANDROID) +# include "AndroidAPZ.h" +#endif // defined(MOZ_WIDGET_ANDROID) + +static mozilla::LazyLogModule sApzCtlLog("apz.controller"); +#define APZC_LOG(...) MOZ_LOG(sApzCtlLog, LogLevel::Debug, (__VA_ARGS__)) +#define APZC_LOGV(...) MOZ_LOG(sApzCtlLog, LogLevel::Verbose, (__VA_ARGS__)) + +// Log to the apz.controller log with additional info from the APZC +#define APZC_LOG_DETAIL(fmt, apzc, ...) \ + APZC_LOG("%p(%s scrollId=%" PRIu64 "): " fmt, (apzc), \ + (apzc)->IsRootContent() ? "root" : "subframe", \ + (apzc)->GetScrollId(), ##__VA_ARGS__) + +#define APZC_LOG_FM_COMMON(fm, prefix, level, ...) \ + if (MOZ_LOG_TEST(sApzCtlLog, level)) { \ + std::stringstream ss; \ + ss << nsPrintfCString(prefix, __VA_ARGS__).get() << ":" << fm; \ + MOZ_LOG(sApzCtlLog, level, ("%s\n", ss.str().c_str())); \ + } +#define APZC_LOG_FM(fm, prefix, ...) \ + APZC_LOG_FM_COMMON(fm, prefix, LogLevel::Debug, __VA_ARGS__) +#define APZC_LOGV_FM(fm, prefix, ...) \ + APZC_LOG_FM_COMMON(fm, prefix, LogLevel::Verbose, __VA_ARGS__) + +namespace mozilla { +namespace layers { + +typedef mozilla::layers::AllowedTouchBehavior AllowedTouchBehavior; +typedef GeckoContentController::APZStateChange APZStateChange; +typedef GeckoContentController::TapType TapType; +typedef mozilla::gfx::Point Point; +typedef mozilla::gfx::Matrix4x4 Matrix4x4; + +// Choose between platform-specific implementations. +#ifdef MOZ_WIDGET_ANDROID +typedef WidgetOverscrollEffect OverscrollEffect; +typedef AndroidSpecificState PlatformSpecificState; +#else +typedef GenericOverscrollEffect OverscrollEffect; +typedef PlatformSpecificStateBase + PlatformSpecificState; // no extra state, just use the base class +#endif + +/** + * \page APZCPrefs APZ preferences + * + * The following prefs are used to control the behaviour of the APZC. + * The default values are provided in StaticPrefList.yaml. + * + * \li\b apz.allow_double_tap_zooming + * Pref that allows or disallows double tap to zoom + * + * \li\b apz.allow_immediate_handoff + * If set to true, scroll can be handed off from one APZC to another within + * a single input block. If set to false, a single input block can only + * scroll one APZC. + * + * \li\b apz.allow_zooming_out + * If set to true, APZ will allow zooming out past the initial scale on + * desktop. This is false by default to match Chrome's behaviour. + * + * \li\b apz.android.chrome_fling_physics.friction + * A tunable parameter for Chrome fling physics on Android that governs + * how quickly a fling animation slows down due to friction (and therefore + * also how far it reaches). Should be in the range [0-1]. + * + * \li\b apz.android.chrome_fling_physics.inflexion + * A tunable parameter for Chrome fling physics on Android that governs + * the shape of the fling curve. Should be in the range [0-1]. + * + * \li\b apz.android.chrome_fling_physics.stop_threshold + * A tunable parameter for Chrome fling physics on Android that governs + * how close the fling animation has to get to its target destination + * before it stops. + * Units: ParentLayer pixels + * + * \li\b apz.autoscroll.enabled + * If set to true, autoscrolling is driven by APZ rather than the content + * process main thread. + * + * \li\b apz.axis_lock.mode + * The preferred axis locking style. See AxisLockMode for possible values. + * + * \li\b apz.axis_lock.lock_angle + * Angle from axis within which we stay axis-locked.\n + * Units: radians + * + * \li\b apz.axis_lock.breakout_threshold + * Distance in inches the user must pan before axis lock can be broken.\n + * Units: (real-world, i.e. screen) inches + * + * \li\b apz.axis_lock.breakout_angle + * Angle at which axis lock can be broken.\n + * Units: radians + * + * \li\b apz.axis_lock.direct_pan_angle + * If the angle from an axis to the line drawn by a pan move is less than + * this value, we can assume that panning can be done in the allowed direction + * (horizontal or vertical).\n + * Currently used only for touch-action css property stuff and was addded to + * keep behaviour consistent with IE.\n + * Units: radians + * + * \li\b apz.content_response_timeout + * Amount of time before we timeout response from content. For example, if + * content is being unruly/slow and we don't get a response back within this + * time, we will just pretend that content did not preventDefault any touch + * events we dispatched to it.\n + * Units: milliseconds + * + * \li\b apz.danger_zone_x + * \li\b apz.danger_zone_y + * When drawing high-res tiles, we drop down to drawing low-res tiles + * when we know we can't keep up with the scrolling. The way we determine + * this is by checking if we are entering the "danger zone", which is the + * boundary of the painted content. For example, if the painted content + * goes from y=0...1000 and the visible portion is y=250...750 then + * we're far from checkerboarding. If we get to y=490...990 though then we're + * only 10 pixels away from showing checkerboarding so we are probably in + * a state where we can't keep up with scrolling. The danger zone prefs specify + * how wide this margin is; in the above example a y-axis danger zone of 10 + * pixels would make us drop to low-res at y=490...990.\n + * This value is in screen pixels. + * + * \li\b apz.disable_for_scroll_linked_effects + * Setting this pref to true will disable APZ scrolling on documents where + * scroll-linked effects are detected. A scroll linked effect is detected if + * positioning or transform properties are updated inside a scroll event + * dispatch; we assume that such an update is in response to the scroll event + * and is therefore a scroll-linked effect which will be laggy with APZ + * scrolling. + * + * \li\b apz.displayport_expiry_ms + * While a scrollable frame is scrolling async, we set a displayport on it + * to make sure it is layerized. However this takes up memory, so once the + * scrolling stops we want to remove the displayport. This pref controls how + * long after scrolling stops the displayport is removed. A value of 0 will + * disable the expiry behavior entirely. + * Units: milliseconds + * + * \li\b apz.drag.enabled + * Setting this pref to true will cause APZ to handle mouse-dragging of + * scrollbar thumbs. + * + * \li\b apz.drag.initial.enabled + * Setting this pref to true will cause APZ to try to handle mouse-dragging + * of scrollbar thumbs without an initial round-trip to content to start it + * if possible. Only has an effect if apz.drag.enabled is also true. + * + * \li\b apz.drag.touch.enabled + * Setting this pref to true will cause APZ to handle touch-dragging of + * scrollbar thumbs. Only has an effect if apz.drag.enabled is also true. + * + * \li\b apz.enlarge_displayport_when_clipped + * Pref that enables enlarging of the displayport along one axis when the + * generated displayport's size is beyond that of the scrollable rect on the + * opposite axis. + * + * \li\b apz.fling_accel_min_fling_velocity + * The minimum velocity of the second fling, and the minimum velocity of the + * previous fling animation at the point of interruption, for the new fling to + * be considered for fling acceleration. + * Units: screen pixels per milliseconds + * + * \li\b apz.fling_accel_min_pan_velocity + * The minimum velocity during the pan gesture that causes a fling for that + * fling to be considered for fling acceleration. + * Units: screen pixels per milliseconds + * + * \li\b apz.fling_accel_max_pause_interval_ms + * The maximum time that is allowed to elapse between the touch start event that + * interrupts the previous fling, and the touch move that initiates panning for + * the current fling, for that fling to be considered for fling acceleration. + * Units: milliseconds + * + * \li\b apz.fling_accel_base_mult + * \li\b apz.fling_accel_supplemental_mult + * When applying an acceleration on a fling, the new computed velocity is + * (new_fling_velocity * base_mult) + (old_velocity * supplemental_mult). + * The base_mult and supplemental_mult multiplier values are controlled by + * these prefs. Note that "old_velocity" here is the initial velocity of the + * previous fling _after_ acceleration was applied to it (if applicable). + * + * \li\b apz.fling_curve_function_x1 + * \li\b apz.fling_curve_function_y1 + * \li\b apz.fling_curve_function_x2 + * \li\b apz.fling_curve_function_y2 + * \li\b apz.fling_curve_threshold_inches_per_ms + * These five parameters define a Bezier curve function and threshold used to + * increase the actual velocity relative to the user's finger velocity. When the + * finger velocity is below the threshold (or if the threshold is not positive), + * the velocity is used as-is. If the finger velocity exceeds the threshold + * velocity, then the function defined by the curve is applied on the part of + * the velocity that exceeds the threshold. Note that the upper bound of the + * velocity is still specified by the \b apz.max_velocity_inches_per_ms pref, + * and the function will smoothly curve the velocity from the threshold to the + * max. In general the function parameters chosen should define an ease-out + * curve in order to increase the velocity in this range, or an ease-in curve to + * decrease the velocity. A straight-line curve is equivalent to disabling the + * curve entirely by setting the threshold to -1. The max velocity pref must + * also be set in order for the curving to take effect, as it defines the upper + * bound of the velocity curve.\n + * The points (x1, y1) and (x2, y2) used as the two intermediate control points + * in the cubic bezier curve; the first and last points are (0,0) and (1,1).\n + * Some example values for these prefs can be found at\n + * https://searchfox.org/mozilla-central/rev/f82d5c549f046cb64ce5602bfd894b7ae807c8f8/dom/animation/ComputedTimingFunction.cpp#27-33 + * + * \li\b apz.fling_friction + * Amount of friction applied during flings. This is used in the following + * formula: v(t1) = v(t0) * (1 - f)^(t1 - t0), where v(t1) is the velocity + * for a new sample, v(t0) is the velocity at the previous sample, f is the + * value of this pref, and (t1 - t0) is the amount of time, in milliseconds, + * that has elapsed between the two samples.\n + * NOTE: Not currently used in Android fling calculations. + * + * \li\b apz.fling_min_velocity_threshold + * Minimum velocity for a fling to actually kick off. If the user pans and lifts + * their finger such that the velocity is smaller than or equal to this amount, + * no fling is initiated.\n + * Units: screen pixels per millisecond + * + * \li\b apz.fling_stop_on_tap_threshold + * When flinging, if the velocity is above this number, then a tap on the + * screen will stop the fling without dispatching a tap to content. If the + * velocity is below this threshold a tap will also be dispatched. + * Note: when modifying this pref be sure to run the APZC gtests as some of + * them depend on the value of this pref.\n + * Units: screen pixels per millisecond + * + * \li\b apz.fling_stopped_threshold + * When flinging, if the velocity goes below this number, we just stop the + * animation completely. This is to prevent asymptotically approaching 0 + * velocity and rerendering unnecessarily.\n + * Units: screen pixels per millisecond.\n + * NOTE: Should not be set to anything + * other than 0.0 for Android except for tests to disable flings. + * + * \li\b apz.keyboard.enabled + * Determines whether scrolling with the keyboard will be allowed to be handled + * by APZ. + * + * \li\b apz.keyboard.passive-listeners + * When enabled, APZ will interpret the passive event listener flag to mean + * that the event listener won't change the focused element or selection of + * the page. With this, web content can use passive key listeners and not have + * keyboard APZ disabled. + * + * \li\b apz.max_tap_time + * Maximum time for a touch on the screen and corresponding lift of the finger + * to be considered a tap. This also applies to double taps, except that it is + * used both for the interval between the first touchdown and first touchup, + * and for the interval between the first touchup and the second touchdown.\n + * Units: milliseconds. + * + * \li\b apz.max_velocity_inches_per_ms + * Maximum velocity. Velocity will be capped at this value if a faster fling + * occurs. Negative values indicate unlimited velocity.\n + * Units: (real-world, i.e. screen) inches per millisecond + * + * \li\b apz.max_velocity_queue_size + * Maximum size of velocity queue. The queue contains last N velocity records. + * On touch end we calculate the average velocity in order to compensate + * touch/mouse drivers misbehaviour. + * + * \li\b apz.min_skate_speed + * Minimum amount of speed along an axis before we switch to "skate" multipliers + * rather than using the "stationary" multipliers.\n + * Units: CSS pixels per millisecond + * + * \li\b apz.one_touch_pinch.enabled + * Whether or not the "one-touch-pinch" gesture (for zooming with one finger) + * is enabled or not. + * + * \li\b apz.overscroll.enabled + * Pref that enables overscrolling. If this is disabled, excess scroll that + * cannot be handed off is discarded. + * + * \li\b apz.overscroll.min_pan_distance_ratio + * The minimum ratio of the pan distance along one axis to the pan distance + * along the other axis needed to initiate overscroll along the first axis + * during panning. + * + * \li\b apz.overscroll.stretch_factor + * How much overscrolling can stretch content along an axis. + * The maximum stretch along an axis is a factor of (1 + kStretchFactor). + * (So if kStretchFactor is 0, you can't stretch at all; if kStretchFactor + * is 1, you can stretch at most by a factor of 2). + * + * \li\b apz.overscroll.stop_distance_threshold + * \li\b apz.overscroll.stop_velocity_threshold + * Thresholds for stopping the overscroll animation. When both the distance + * and the velocity fall below their thresholds, we stop oscillating.\n + * Units: screen pixels (for distance) + * screen pixels per millisecond (for velocity) + * + * \li\b apz.overscroll.spring_stiffness + * The spring stiffness constant for the overscroll mass-spring-damper model. + * + * \li\b apz.overscroll.damping + * The damping constant for the overscroll mass-spring-damper model. + * + * \li\b apz.overscroll.max_velocity + * The maximum velocity (in ParentLayerPixels per millisecond) allowed when + * initiating the overscroll snap-back animation. + * + * \li\b apz.paint_skipping.enabled + * When APZ is scrolling and sending repaint requests to the main thread, often + * the main thread doesn't actually need to do a repaint. This pref allows the + * main thread to skip doing those repaints in cases where it doesn't need to. + * + * \li\b apz.pinch_lock.mode + * The preferred pinch locking style. See PinchLockMode for possible values. + * + * \li\b apz.pinch_lock.scroll_lock_threshold + * Pinch locking is triggered if the user scrolls more than this distance + * and pinches less than apz.pinch_lock.span_lock_threshold.\n + * Units: (real-world, i.e. screen) inches + * + * \li\b apz.pinch_lock.span_breakout_threshold + * Distance in inches the user must pinch before lock can be broken.\n + * Units: (real-world, i.e. screen) inches measured between two touch points + * + * \li\b apz.pinch_lock.span_lock_threshold + * Pinch locking is triggered if the user pinches less than this distance + * and scrolls more than apz.pinch_lock.scroll_lock_threshold.\n + * Units: (real-world, i.e. screen) inches measured between two touch points + * + * \li\b apz.pinch_lock.buffer_max_age + * To ensure that pinch locking threshold calculations are not affected by + * variations in touch screen sensitivity, calculations draw from a buffer of + * recent events. This preference specifies the maximum time that events are + * held in this buffer. + * Units: milliseconds + * + * \li\b apz.popups.enabled + * Determines whether APZ is used for XUL popup widgets with remote content. + * Ideally, this should always be true, but it is currently not well tested, and + * has known issues, so needs to be prefable. + * + * \li\b apz.record_checkerboarding + * Whether or not to record detailed info on checkerboarding events. + * + * \li\b apz.second_tap_tolerance + * Constant describing the tolerance in distance we use, multiplied by the + * device DPI, within which a second tap is counted as part of a gesture + * continuing from the first tap. Making this larger allows the user more + * distance between the first and second taps in a "double tap" or "one touch + * pinch" gesture.\n + * Units: (real-world, i.e. screen) inches + * + * \li\b apz.test.logging_enabled + * Enable logging of APZ test data (see bug 961289). + * + * \li\b apz.touch_move_tolerance + * See the description for apz.touch_start_tolerance below. This is a similar + * threshold, except it is used to suppress touchmove events from being + * delivered to content for NON-scrollable frames (or more precisely, for APZCs + * where ArePointerEventsConsumable returns false).\n Units: (real-world, i.e. + * screen) inches + * + * \li\b apz.touch_start_tolerance + * Constant describing the tolerance in distance we use, multiplied by the + * device DPI, before we start panning the screen. This is to prevent us from + * accidentally processing taps as touch moves, and from very short/accidental + * touches moving the screen. touchmove events are also not delivered to content + * within this distance on scrollable frames.\n + * Units: (real-world, i.e. screen) inches + * + * \li\b apz.velocity_bias + * How much to adjust the displayport in the direction of scrolling. This value + * is multiplied by the velocity and added to the displayport offset. + * + * \li\b apz.velocity_relevance_time_ms + * When computing a fling velocity from the most recently stored velocity + * information, only velocities within the most X milliseconds are used. + * This pref controls the value of X.\n + * Units: ms + * + * \li\b apz.x_skate_size_multiplier + * \li\b apz.y_skate_size_multiplier + * The multiplier we apply to the displayport size if it is skating (current + * velocity is above \b apz.min_skate_speed). We prefer to increase the size of + * the Y axis because it is more natural in the case that a user is reading a + * page page that scrolls up/down. Note that one, both or neither of these may + * be used at any instant.\n In general we want \b + * apz.[xy]_skate_size_multiplier to be smaller than the corresponding + * stationary size multiplier because when panning fast we would like to paint + * less and get faster, more predictable paint times. When panning slowly we + * can afford to paint more even though it's slower. + * + * \li\b apz.x_stationary_size_multiplier + * \li\b apz.y_stationary_size_multiplier + * The multiplier we apply to the displayport size if it is not skating (see + * documentation for the skate size multipliers above). + * + * \li\b apz.x_skate_highmem_adjust + * \li\b apz.y_skate_highmem_adjust + * On high memory systems, we adjust the displayport during skating + * to be larger so we can reduce checkerboarding. + * + * \li\b apz.zoom_animation_duration_ms + * This controls how long the zoom-to-rect animation takes.\n + * Units: ms + * + * \li\b apz.scale_repaint_delay_ms + * How long to delay between repaint requests during a scale. + * A negative number prevents repaint requests during a scale.\n + * Units: ms + */ + +/** + * Computed time function used for sampling frames of a zoom to animation. + */ +StaticAutoPtr gZoomAnimationFunction; + +/** + * Computed time function used for curving up velocity when it gets high. + */ +StaticAutoPtr gVelocityCurveFunction; + +/** + * The estimated duration of a paint for the purposes of calculating a new + * displayport, in milliseconds. + */ +static const double kDefaultEstimatedPaintDurationMs = 50; + +/** + * Returns true if this is a high memory system and we can use + * extra memory for a larger displayport to reduce checkerboarding. + */ +static bool gIsHighMemSystem = false; +static bool IsHighMemSystem() { return gIsHighMemSystem; } + +AsyncPanZoomAnimation* PlatformSpecificStateBase::CreateFlingAnimation( + AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState, + float aPLPPI) { + return new GenericFlingAnimation(aApzc, aHandoffState, + aPLPPI); +} + +UniquePtr PlatformSpecificStateBase::CreateVelocityTracker( + Axis* aAxis) { + return MakeUnique(aAxis); +} + +SampleTime AsyncPanZoomController::GetFrameTime() const { + APZCTreeManager* treeManagerLocal = GetApzcTreeManager(); + return treeManagerLocal ? treeManagerLocal->GetFrameTime() + : SampleTime::FromNow(); +} + +bool AsyncPanZoomController::IsZero(const ParentLayerPoint& aPoint) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + const auto zoom = Metrics().GetZoom(); + + if (zoom == CSSToParentLayerScale(0)) { + return true; + } + + return layers::IsZero(aPoint / zoom); +} + +bool AsyncPanZoomController::IsZero(ParentLayerCoord aCoord) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + const auto zoom = Metrics().GetZoom(); + + if (zoom == CSSToParentLayerScale(0)) { + return true; + } + + return FuzzyEqualsAdditive((aCoord / zoom), CSSCoord(), COORDINATE_EPSILON); +} + +bool AsyncPanZoomController::FuzzyGreater(ParentLayerCoord aCoord1, + ParentLayerCoord aCoord2) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + const auto zoom = Metrics().GetZoom(); + + if (zoom == CSSToParentLayerScale(0)) { + return false; + } + + return (aCoord1 - aCoord2) / zoom > COORDINATE_EPSILON; +} + +class MOZ_STACK_CLASS StateChangeNotificationBlocker final { + public: + explicit StateChangeNotificationBlocker(AsyncPanZoomController* aApzc) + : mApzc(aApzc) { + RecursiveMutexAutoLock lock(mApzc->mRecursiveMutex); + mInitialState = mApzc->mState; + mApzc->mNotificationBlockers++; + } + + ~StateChangeNotificationBlocker() { + AsyncPanZoomController::PanZoomState newState; + { + RecursiveMutexAutoLock lock(mApzc->mRecursiveMutex); + mApzc->mNotificationBlockers--; + newState = mApzc->mState; + } + mApzc->DispatchStateChangeNotification(mInitialState, newState); + } + + private: + AsyncPanZoomController* mApzc; + AsyncPanZoomController::PanZoomState mInitialState; +}; + +/** + * An RAII class to temporarily apply async test attributes to the provided + * AsyncPanZoomController. + * + * This class should be used in the implementation of any AsyncPanZoomController + * method that queries the async scroll offset or async zoom (this includes + * the async layout viewport offset, since modifying the async scroll offset + * may result in the layout viewport moving as well). + */ +class MOZ_RAII AutoApplyAsyncTestAttributes final { + public: + explicit AutoApplyAsyncTestAttributes( + const AsyncPanZoomController*, + const RecursiveMutexAutoLock& aProofOfLock); + ~AutoApplyAsyncTestAttributes(); + + private: + AsyncPanZoomController* mApzc; + FrameMetrics mPrevFrameMetrics; + ParentLayerPoint mPrevOverscroll; + const RecursiveMutexAutoLock& mProofOfLock; +}; + +AutoApplyAsyncTestAttributes::AutoApplyAsyncTestAttributes( + const AsyncPanZoomController* aApzc, + const RecursiveMutexAutoLock& aProofOfLock) + // Having to use const_cast here seems less ugly than the alternatives + // of making several members of AsyncPanZoomController that + // ApplyAsyncTestAttributes() modifies |mutable|, or several methods that + // query the async transforms non-const. + : mApzc(const_cast(aApzc)), + mPrevFrameMetrics(aApzc->Metrics()), + mPrevOverscroll(aApzc->GetOverscrollAmountInternal()), + mProofOfLock(aProofOfLock) { + mApzc->ApplyAsyncTestAttributes(aProofOfLock); +} + +AutoApplyAsyncTestAttributes::~AutoApplyAsyncTestAttributes() { + mApzc->UnapplyAsyncTestAttributes(mProofOfLock, mPrevFrameMetrics, + mPrevOverscroll); +} + +class ZoomAnimation : public AsyncPanZoomAnimation { + public: + ZoomAnimation(AsyncPanZoomController& aApzc, const CSSPoint& aStartOffset, + const CSSToParentLayerScale& aStartZoom, + const CSSPoint& aEndOffset, + const CSSToParentLayerScale& aEndZoom) + : mApzc(aApzc), + mTotalDuration(TimeDuration::FromMilliseconds( + StaticPrefs::apz_zoom_animation_duration_ms())), + mStartOffset(aStartOffset), + mStartZoom(aStartZoom), + mEndOffset(aEndOffset), + mEndZoom(aEndZoom) {} + + virtual bool DoSample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta) override { + mDuration += aDelta; + double animPosition = mDuration / mTotalDuration; + + if (animPosition >= 1.0) { + aFrameMetrics.SetZoom(mEndZoom); + mApzc.SetVisualScrollOffset(mEndOffset); + return false; + } + + // Sample the zoom at the current time point. The sampled zoom + // will affect the final computed resolution. + float sampledPosition = + gZoomAnimationFunction->At(animPosition, /* aBeforeFlag = */ false); + + // We scale the scrollOffset linearly with sampledPosition, so the zoom + // needs to scale inversely to match. + if (mStartZoom == CSSToParentLayerScale(0) || + mEndZoom == CSSToParentLayerScale(0)) { + return false; + } + + aFrameMetrics.SetZoom( + CSSToParentLayerScale(1 / (sampledPosition / mEndZoom.scale + + (1 - sampledPosition) / mStartZoom.scale))); + + mApzc.SetVisualScrollOffset(CSSPoint::FromUnknownPoint(gfx::Point( + mEndOffset.x * sampledPosition + mStartOffset.x * (1 - sampledPosition), + mEndOffset.y * sampledPosition + + mStartOffset.y * (1 - sampledPosition)))); + return true; + } + + virtual bool WantsRepaints() override { return true; } + + private: + AsyncPanZoomController& mApzc; + + TimeDuration mDuration; + const TimeDuration mTotalDuration; + + // Old metrics from before we started a zoom animation. This is only valid + // when we are in the "ANIMATED_ZOOM" state. This is used so that we can + // interpolate between the start and end frames. We only use the + // |mViewportScrollOffset| and |mResolution| fields on this. + CSSPoint mStartOffset; + CSSToParentLayerScale mStartZoom; + + // Target metrics for a zoom to animation. This is only valid when we are in + // the "ANIMATED_ZOOM" state. We only use the |mViewportScrollOffset| and + // |mResolution| fields on this. + CSSPoint mEndOffset; + CSSToParentLayerScale mEndZoom; +}; + +/*static*/ +void AsyncPanZoomController::InitializeGlobalState() { + static bool sInitialized = false; + if (sInitialized) return; + sInitialized = true; + + MOZ_ASSERT(NS_IsMainThread()); + + gZoomAnimationFunction = new StyleComputedTimingFunction( + StyleComputedTimingFunction::Keyword(StyleTimingKeyword::Ease)); + ClearOnShutdown(&gZoomAnimationFunction); + gVelocityCurveFunction = + new StyleComputedTimingFunction(StyleComputedTimingFunction::CubicBezier( + StaticPrefs::apz_fling_curve_function_x1_AtStartup(), + StaticPrefs::apz_fling_curve_function_y1_AtStartup(), + StaticPrefs::apz_fling_curve_function_x2_AtStartup(), + StaticPrefs::apz_fling_curve_function_y2_AtStartup())); + ClearOnShutdown(&gVelocityCurveFunction); + + uint64_t sysmem = PR_GetPhysicalMemorySize(); + uint64_t threshold = 1LL << 32; // 4 GB in bytes + gIsHighMemSystem = sysmem >= threshold; + + PlatformSpecificState::InitializeGlobalState(); +} + +AsyncPanZoomController::AsyncPanZoomController( + LayersId aLayersId, APZCTreeManager* aTreeManager, + const RefPtr& aInputQueue, + GeckoContentController* aGeckoContentController, GestureBehavior aGestures) + : mLayersId(aLayersId), + mGeckoContentController(aGeckoContentController), + mRefPtrMonitor("RefPtrMonitor"), + // mTreeManager must be initialized before GetFrameTime() is called + mTreeManager(aTreeManager), + mRecursiveMutex("AsyncPanZoomController"), + mLastContentPaintMetrics(mLastContentPaintMetadata.GetMetrics()), + mPanDirRestricted(false), + mPinchLocked(false), + mPinchEventBuffer(TimeDuration::FromMilliseconds( + StaticPrefs::apz_pinch_lock_buffer_max_age_AtStartup())), + mZoomConstraints(false, false, + mScrollMetadata.GetMetrics().GetDevPixelsPerCSSPixel() * + ViewportMinScale() / ParentLayerToScreenScale(1), + mScrollMetadata.GetMetrics().GetDevPixelsPerCSSPixel() * + ViewportMaxScale() / ParentLayerToScreenScale(1)), + mLastSampleTime(GetFrameTime()), + mLastCheckerboardReport(GetFrameTime()), + mLastNotifiedZoom(), + mOverscrollEffect(MakeUnique(*this)), + mState(NOTHING), + mX(this), + mY(this), + mNotificationBlockers(0), + mInputQueue(aInputQueue), + mPinchPaintTimerSet(false), + mDelayedTransformEnd(false), + mTestAttributeAppliers(0), + mTestHasAsyncKeyScrolled(false), + mCheckerboardEventLock("APZCBELock") { + if (aGestures == USE_GESTURE_DETECTOR) { + mGestureEventListener = new GestureEventListener(this); + } + // Put one default-constructed sampled state in the queue. + RecursiveMutexAutoLock lock(mRecursiveMutex); + mSampledState.emplace_back(); +} + +AsyncPanZoomController::~AsyncPanZoomController() { MOZ_ASSERT(IsDestroyed()); } + +PlatformSpecificStateBase* AsyncPanZoomController::GetPlatformSpecificState() { + if (!mPlatformSpecificState) { + mPlatformSpecificState = MakeUnique(); + } + return mPlatformSpecificState.get(); +} + +already_AddRefed +AsyncPanZoomController::GetGeckoContentController() const { + MonitorAutoLock lock(mRefPtrMonitor); + RefPtr controller = mGeckoContentController; + return controller.forget(); +} + +already_AddRefed +AsyncPanZoomController::GetGestureEventListener() const { + MonitorAutoLock lock(mRefPtrMonitor); + RefPtr listener = mGestureEventListener; + return listener.forget(); +} + +const RefPtr& AsyncPanZoomController::GetInputQueue() const { + return mInputQueue; +} + +void AsyncPanZoomController::Destroy() { + AssertOnUpdaterThread(); + + CancelAnimation(CancelAnimationFlags::ScrollSnap); + + { // scope the lock + MonitorAutoLock lock(mRefPtrMonitor); + mGeckoContentController = nullptr; + mGestureEventListener = nullptr; + } + mParent = nullptr; + mTreeManager = nullptr; +} + +bool AsyncPanZoomController::IsDestroyed() const { + return mTreeManager == nullptr; +} + +float AsyncPanZoomController::GetDPI() const { + if (APZCTreeManager* localPtr = mTreeManager) { + return localPtr->GetDPI(); + } + // If this APZC has been destroyed then this value is not going to be + // used for anything that the user will end up seeing, so we can just + // return 0. + return 0.0; +} + +ScreenCoord AsyncPanZoomController::GetTouchStartTolerance() const { + return (StaticPrefs::apz_touch_start_tolerance() * GetDPI()); +} + +ScreenCoord AsyncPanZoomController::GetTouchMoveTolerance() const { + return (StaticPrefs::apz_touch_move_tolerance() * GetDPI()); +} + +ScreenCoord AsyncPanZoomController::GetSecondTapTolerance() const { + return (StaticPrefs::apz_second_tap_tolerance() * GetDPI()); +} + +/* static */ AsyncPanZoomController::AxisLockMode +AsyncPanZoomController::GetAxisLockMode() { + return static_cast(StaticPrefs::apz_axis_lock_mode()); +} + +bool AsyncPanZoomController::UsingStatefulAxisLock() const { + return (GetAxisLockMode() == STANDARD || GetAxisLockMode() == STICKY); +} + +/* static */ AsyncPanZoomController::PinchLockMode +AsyncPanZoomController::GetPinchLockMode() { + return static_cast(StaticPrefs::apz_pinch_lock_mode()); +} + +PointerEventsConsumableFlags AsyncPanZoomController::ArePointerEventsConsumable( + TouchBlockState* aBlock, const MultiTouchInput& aInput) { + uint32_t touchPoints = aInput.mTouches.Length(); + if (touchPoints == 0) { + // Cant' do anything with zero touch points + return {false, false}; + } + + // This logic is simplified, erring on the side of returning true if we're + // not sure. It's safer to pretend that we can consume the event and then + // not be able to than vice-versa. But at the same time, we should try hard + // to return an accurate result, because returning true can trigger a + // pointercancel event to web content, which can break certain features + // that are using touch-action and handling the pointermove events. + // + // Note that in particular this function can return true if APZ is waiting on + // the main thread for touch-action information. In this scenario, the + // APZEventState::MainThreadAgreesEventsAreConsumableByAPZ() function tries + // to use the main-thread touch-action information to filter out false + // positives. + // + // We could probably enhance this logic to determine things like "we're + // not pannable, so we can only zoom in, and the zoom is already maxed + // out, so we're not zoomable either" but no need for that at this point. + + bool pannableX = aBlock->GetOverscrollHandoffChain()->CanScrollInDirection( + this, ScrollDirection::eHorizontal); + bool touchActionAllowsX = aBlock->TouchActionAllowsPanningX(); + bool pannableY = (aBlock->GetOverscrollHandoffChain()->CanScrollInDirection( + this, ScrollDirection::eVertical) || + // In the case of the root APZC with any dynamic toolbar, it + // shoule be pannable if there is room moving the dynamic + // toolbar. + (IsRootContent() && CanVerticalScrollWithDynamicToolbar())); + bool touchActionAllowsY = aBlock->TouchActionAllowsPanningY(); + + bool pannable; + bool touchActionAllowsPanning; + + Maybe panDirection = + aBlock->GetBestGuessPanDirection(aInput); + if (panDirection == Some(ScrollDirection::eVertical)) { + pannable = pannableY; + touchActionAllowsPanning = touchActionAllowsY; + } else if (panDirection == Some(ScrollDirection::eHorizontal)) { + pannable = pannableX; + touchActionAllowsPanning = touchActionAllowsX; + } else { + // If we don't have a guessed pan direction, err on the side of returning + // true. + pannable = pannableX || pannableY; + touchActionAllowsPanning = touchActionAllowsX || touchActionAllowsY; + } + + if (touchPoints == 1) { + return {pannable, touchActionAllowsPanning}; + } + + bool zoomable = ZoomConstraintsAllowZoom(); + bool touchActionAllowsZoom = aBlock->TouchActionAllowsPinchZoom(); + + return {pannable || zoomable, + touchActionAllowsPanning || touchActionAllowsZoom}; +} + +nsEventStatus AsyncPanZoomController::HandleDragEvent( + const MouseInput& aEvent, const AsyncDragMetrics& aDragMetrics, + OuterCSSCoord aInitialThumbPos) { + // RDM is a special case where touch events will be synthesized in response + // to mouse events, and APZ will receive both even though RDM prevent-defaults + // the mouse events. This is because mouse events don't opt into APZ waiting + // to check if the event has been prevent-defaulted and are still processed + // as a result. To handle this, have APZ ignore mouse events when RDM and + // touch simulation are active. + bool isRDMTouchSimulationActive = false; + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + isRDMTouchSimulationActive = + mScrollMetadata.GetIsRDMTouchSimulationActive(); + } + + if (!StaticPrefs::apz_drag_enabled() || isRDMTouchSimulationActive) { + return nsEventStatus_eIgnore; + } + + if (!GetApzcTreeManager()) { + return nsEventStatus_eConsumeNoDefault; + } + + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + if (aEvent.mType == MouseInput::MouseType::MOUSE_UP) { + if (mState == SCROLLBAR_DRAG) { + APZC_LOG("%p ending drag\n", this); + SetState(NOTHING); + } + + SnapBackIfOverscrolled(); + + return nsEventStatus_eConsumeNoDefault; + } + } + + HitTestingTreeNodeAutoLock node; + GetApzcTreeManager()->FindScrollThumbNode(aDragMetrics, mLayersId, node); + if (!node) { + APZC_LOG("%p unable to find scrollthumb node with viewid %" PRIu64 "\n", + this, aDragMetrics.mViewId); + return nsEventStatus_eConsumeNoDefault; + } + + if (aEvent.mType == MouseInput::MouseType::MOUSE_DOWN) { + APZC_LOG("%p starting scrollbar drag\n", this); + SetState(SCROLLBAR_DRAG); + } + + if (aEvent.mType != MouseInput::MouseType::MOUSE_MOVE) { + APZC_LOG("%p discarding event of type %d\n", this, aEvent.mType); + return nsEventStatus_eConsumeNoDefault; + } + + const ScrollbarData& scrollbarData = node->GetScrollbarData(); + MOZ_ASSERT(scrollbarData.mScrollbarLayerType == + layers::ScrollbarLayerType::Thumb); + MOZ_ASSERT(scrollbarData.mDirection.isSome()); + ScrollDirection direction = *scrollbarData.mDirection; + + bool isMouseAwayFromThumb = false; + if (int snapMultiplier = StaticPrefs::slider_snapMultiplier_AtStartup()) { + // It's fine to ignore the async component of the thumb's transform, + // because any async transform of the thumb will be in the direction of + // scrolling, but here we're interested in the other direction. + ParentLayerRect thumbRect = + (node->GetTransform() * AsyncTransformMatrix()) + .TransformBounds(LayerRect(node->GetVisibleRegion().GetBounds())); + ScrollDirection otherDirection = GetPerpendicularDirection(direction); + ParentLayerCoord distance = + GetAxisStart(otherDirection, thumbRect.DistanceTo(aEvent.mLocalOrigin)); + ParentLayerCoord thumbWidth = GetAxisLength(otherDirection, thumbRect); + // Avoid triggering this condition spuriously when the thumb is + // offscreen and its visible region is therefore empty. + if (thumbWidth > 0 && thumbWidth * snapMultiplier < distance) { + isMouseAwayFromThumb = true; + APZC_LOG("%p determined mouse is away from thumb, will snap\n", this); + } + } + + RecursiveMutexAutoLock lock(mRecursiveMutex); + OuterCSSCoord thumbPosition; + if (isMouseAwayFromThumb) { + thumbPosition = aInitialThumbPos; + } else { + thumbPosition = ConvertScrollbarPoint(aEvent.mLocalOrigin, scrollbarData) - + aDragMetrics.mScrollbarDragOffset; + } + + OuterCSSCoord maxThumbPos = scrollbarData.mScrollTrackLength; + maxThumbPos -= scrollbarData.mThumbLength; + + float scrollPercent = + maxThumbPos.value == 0.0f ? 0.0f : (float)(thumbPosition / maxThumbPos); + APZC_LOG("%p scrollbar dragged to %f percent\n", this, scrollPercent); + + CSSCoord minScrollPosition = + GetAxisStart(direction, Metrics().GetScrollableRect().TopLeft()); + CSSCoord maxScrollPosition = + GetAxisStart(direction, Metrics().GetScrollableRect().BottomRight()) - + GetAxisLength(direction, Metrics().CalculateCompositedSizeInCssPixels()); + CSSCoord scrollPosition = + minScrollPosition + + (scrollPercent * (maxScrollPosition - minScrollPosition)); + + scrollPosition = std::max(scrollPosition, minScrollPosition); + scrollPosition = std::min(scrollPosition, maxScrollPosition); + + CSSPoint scrollOffset = Metrics().GetVisualScrollOffset(); + if (direction == ScrollDirection::eHorizontal) { + scrollOffset.x = scrollPosition; + } else { + scrollOffset.y = scrollPosition; + } + APZC_LOG("%p set scroll offset to %s from scrollbar drag\n", this, + ToString(scrollOffset).c_str()); + SetVisualScrollOffset(scrollOffset); + ScheduleCompositeAndMaybeRepaint(); + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::HandleInputEvent( + const InputData& aEvent, + const ScreenToParentLayerMatrix4x4& aTransformToApzc) { + APZThreadUtils::AssertOnControllerThread(); + + nsEventStatus rv = nsEventStatus_eIgnore; + + switch (aEvent.mInputType) { + case MULTITOUCH_INPUT: { + MultiTouchInput multiTouchInput = aEvent.AsMultiTouchInput(); + RefPtr listener = GetGestureEventListener(); + if (listener) { + // We only care about screen coordinates in the gesture listener, + // so we don't bother transforming the event to parent layer coordinates + rv = listener->HandleInputEvent(multiTouchInput); + if (rv == nsEventStatus_eConsumeNoDefault) { + return rv; + } + } + + if (!multiTouchInput.TransformToLocal(aTransformToApzc)) { + return rv; + } + + switch (multiTouchInput.mType) { + case MultiTouchInput::MULTITOUCH_START: + rv = OnTouchStart(multiTouchInput); + break; + case MultiTouchInput::MULTITOUCH_MOVE: + rv = OnTouchMove(multiTouchInput); + break; + case MultiTouchInput::MULTITOUCH_END: + rv = OnTouchEnd(multiTouchInput); + break; + case MultiTouchInput::MULTITOUCH_CANCEL: + rv = OnTouchCancel(multiTouchInput); + break; + } + break; + } + case PANGESTURE_INPUT: { + PanGestureInput panGestureInput = aEvent.AsPanGestureInput(); + if (!panGestureInput.TransformToLocal(aTransformToApzc)) { + return rv; + } + + switch (panGestureInput.mType) { + case PanGestureInput::PANGESTURE_MAYSTART: + rv = OnPanMayBegin(panGestureInput); + break; + case PanGestureInput::PANGESTURE_CANCELLED: + rv = OnPanCancelled(panGestureInput); + break; + case PanGestureInput::PANGESTURE_START: + rv = OnPanBegin(panGestureInput); + break; + case PanGestureInput::PANGESTURE_PAN: + rv = OnPan(panGestureInput, FingersOnTouchpad::Yes); + break; + case PanGestureInput::PANGESTURE_END: + rv = OnPanEnd(panGestureInput); + break; + case PanGestureInput::PANGESTURE_MOMENTUMSTART: + rv = OnPanMomentumStart(panGestureInput); + break; + case PanGestureInput::PANGESTURE_MOMENTUMPAN: + rv = OnPan(panGestureInput, FingersOnTouchpad::No); + break; + case PanGestureInput::PANGESTURE_MOMENTUMEND: + rv = OnPanMomentumEnd(panGestureInput); + break; + case PanGestureInput::PANGESTURE_INTERRUPTED: + rv = OnPanInterrupted(panGestureInput); + break; + } + break; + } + case MOUSE_INPUT: { + MouseInput mouseInput = aEvent.AsMouseInput(); + if (!mouseInput.TransformToLocal(aTransformToApzc)) { + return rv; + } + break; + } + case SCROLLWHEEL_INPUT: { + ScrollWheelInput scrollInput = aEvent.AsScrollWheelInput(); + if (!scrollInput.TransformToLocal(aTransformToApzc)) { + return rv; + } + + rv = OnScrollWheel(scrollInput); + break; + } + case PINCHGESTURE_INPUT: { + // The APZCTreeManager should take care of ensuring that only root-content + // APZCs get pinch inputs. + MOZ_ASSERT(IsRootContent()); + PinchGestureInput pinchInput = aEvent.AsPinchGestureInput(); + if (!pinchInput.TransformToLocal(aTransformToApzc)) { + return rv; + } + + rv = HandleGestureEvent(pinchInput); + break; + } + case TAPGESTURE_INPUT: { + TapGestureInput tapInput = aEvent.AsTapGestureInput(); + if (!tapInput.TransformToLocal(aTransformToApzc)) { + return rv; + } + + rv = HandleGestureEvent(tapInput); + break; + } + case KEYBOARD_INPUT: { + const KeyboardInput& keyInput = aEvent.AsKeyboardInput(); + rv = OnKeyboard(keyInput); + break; + } + } + + return rv; +} + +nsEventStatus AsyncPanZoomController::HandleGestureEvent( + const InputData& aEvent) { + APZThreadUtils::AssertOnControllerThread(); + + nsEventStatus rv = nsEventStatus_eIgnore; + + switch (aEvent.mInputType) { + case PINCHGESTURE_INPUT: { + // This may be invoked via a one-touch-pinch gesture from + // GestureEventListener. In that case we want redirect it to the enclosing + // root-content APZC. + if (!IsRootContent()) { + if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) { + if (RefPtr root = + treeManagerLocal->FindZoomableApzc(this)) { + rv = root->HandleGestureEvent(aEvent); + } + } + break; + } + PinchGestureInput pinchGestureInput = aEvent.AsPinchGestureInput(); + pinchGestureInput.TransformToLocal(GetTransformToThis()); + switch (pinchGestureInput.mType) { + case PinchGestureInput::PINCHGESTURE_START: + rv = OnScaleBegin(pinchGestureInput); + break; + case PinchGestureInput::PINCHGESTURE_SCALE: + rv = OnScale(pinchGestureInput); + break; + case PinchGestureInput::PINCHGESTURE_FINGERLIFTED: + case PinchGestureInput::PINCHGESTURE_END: + rv = OnScaleEnd(pinchGestureInput); + break; + } + break; + } + case TAPGESTURE_INPUT: { + TapGestureInput tapGestureInput = aEvent.AsTapGestureInput(); + tapGestureInput.TransformToLocal(GetTransformToThis()); + switch (tapGestureInput.mType) { + case TapGestureInput::TAPGESTURE_LONG: + rv = OnLongPress(tapGestureInput); + break; + case TapGestureInput::TAPGESTURE_LONG_UP: + rv = OnLongPressUp(tapGestureInput); + break; + case TapGestureInput::TAPGESTURE_UP: + rv = OnSingleTapUp(tapGestureInput); + break; + case TapGestureInput::TAPGESTURE_CONFIRMED: + rv = OnSingleTapConfirmed(tapGestureInput); + break; + case TapGestureInput::TAPGESTURE_DOUBLE: + // This means that double tapping on an oop iframe "works" in that we + // don't try (and fail) to zoom the oop iframe. But it also means it + // is impossible to zoom to some content inside that oop iframe. + // Instead the best we can do is zoom to the oop iframe itself. This + // is consistent with what Chrome and Safari currently do. Allowing + // zooming to content inside an oop iframe would be decently + // complicated and it doesn't seem worth it. Bug 1715179 is on file + // for this. + if (!IsRootContent()) { + if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) { + if (RefPtr root = + treeManagerLocal->FindZoomableApzc(this)) { + rv = root->OnDoubleTap(tapGestureInput); + } + } + break; + } + rv = OnDoubleTap(tapGestureInput); + break; + case TapGestureInput::TAPGESTURE_SECOND: + rv = OnSecondTap(tapGestureInput); + break; + case TapGestureInput::TAPGESTURE_CANCEL: + rv = OnCancelTap(tapGestureInput); + break; + } + break; + } + default: + MOZ_ASSERT_UNREACHABLE("Unhandled input event"); + break; + } + + return rv; +} + +void AsyncPanZoomController::StartAutoscroll(const ScreenPoint& aPoint) { + // Cancel any existing animation. + CancelAnimation(); + + SetState(AUTOSCROLL); + StartAnimation(new AutoscrollAnimation(*this, aPoint)); +} + +void AsyncPanZoomController::StopAutoscroll() { + if (mState == AUTOSCROLL) { + CancelAnimation(TriggeredExternally); + } +} + +nsEventStatus AsyncPanZoomController::OnTouchStart( + const MultiTouchInput& aEvent) { + APZC_LOG_DETAIL("got a touch-start in state %s\n", this, + ToString(mState).c_str()); + mPanDirRestricted = false; + + switch (mState) { + case FLING: + case ANIMATING_ZOOM: + case SMOOTH_SCROLL: + case SMOOTHMSD_SCROLL: + case OVERSCROLL_ANIMATION: + case WHEEL_SCROLL: + case KEYBOARD_SCROLL: + case PAN_MOMENTUM: + case AUTOSCROLL: + MOZ_ASSERT(GetCurrentTouchBlock()); + GetCurrentTouchBlock()->GetOverscrollHandoffChain()->CancelAnimations( + ExcludeOverscroll); + [[fallthrough]]; + case SCROLLBAR_DRAG: + case NOTHING: { + ParentLayerPoint point = GetFirstTouchPoint(aEvent); + mLastTouch.mPosition = mStartTouch = GetFirstExternalTouchPoint(aEvent); + StartTouch(point, aEvent.mTimeStamp); + if (RefPtr controller = + GetGeckoContentController()) { + MOZ_ASSERT(GetCurrentTouchBlock()); + controller->NotifyAPZStateChange( + GetGuid(), APZStateChange::eStartTouch, + GetCurrentTouchBlock()->GetOverscrollHandoffChain()->CanBePanned( + this), + Some(GetCurrentTouchBlock()->GetBlockId())); + } + mLastTouch.mTimeStamp = mTouchStartTime = aEvent.mTimeStamp; + SetState(TOUCHING); + break; + } + case TOUCHING: + case PANNING: + case PANNING_LOCKED_X: + case PANNING_LOCKED_Y: + case PINCHING: + NS_WARNING("Received impossible touch in OnTouchStart"); + break; + } + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnTouchMove( + const MultiTouchInput& aEvent) { + APZC_LOG_DETAIL("got a touch-move in state %s\n", this, + ToString(mState).c_str()); + switch (mState) { + case FLING: + case SMOOTHMSD_SCROLL: + case NOTHING: + case ANIMATING_ZOOM: + // May happen if the user double-taps and drags without lifting after the + // second tap. Ignore the move if this happens. + return nsEventStatus_eIgnore; + + case TOUCHING: { + ScreenCoord panThreshold = GetTouchStartTolerance(); + ExternalPoint extPoint = GetFirstExternalTouchPoint(aEvent); + Maybe> splitEvent; + + // We intentionally skip the UpdateWithTouchAtDevicePoint call when the + // panThreshold is zero. This ensures more deterministic behaviour during + // testing. If we call that, Axis::mPos gets updated to the point of this + // touchmove event, but we "consume" the move to overcome the + // panThreshold, so it's hard to pan a specific amount reliably from a + // mochitest. + if (panThreshold > 0.0f) { + const float vectorLength = PanVector(extPoint).Length(); + + if (vectorLength < panThreshold) { + UpdateWithTouchAtDevicePoint(aEvent); + mLastTouch = {extPoint, aEvent.mTimeStamp}; + + return nsEventStatus_eIgnore; + } + + splitEvent = MaybeSplitTouchMoveEvent(aEvent, panThreshold, + vectorLength, extPoint); + + UpdateWithTouchAtDevicePoint(splitEvent ? splitEvent->first : aEvent); + } + + nsEventStatus result; + const MultiTouchInput& firstEvent = + splitEvent ? splitEvent->first : aEvent; + + MOZ_ASSERT(GetCurrentTouchBlock()); + if (GetCurrentTouchBlock()->TouchActionAllowsPanningXY()) { + // In the calls to StartPanning() below, the first argument needs to be + // the External position of |firstEvent|. + // However, instead of computing that using + // GetFirstExternalTouchPoint(firstEvent), we pass |extPoint| which + // has been modified by MaybeSplitTouchMoveEvent() to the desired + // value. This is a workaround for the fact that recomputing the + // External point would require a round-trip through |mScreenPoint| + // which is an integer. + + // User tries to trigger a touch behavior. If allowed touch behavior is + // vertical pan + horizontal pan (touch-action value is equal to AUTO) + // we can return ConsumeNoDefault status immediately to trigger cancel + // event further. + // It should happen independent of the parent type (whether it is + // scrolling or not). + StartPanning(extPoint, firstEvent.mTimeStamp); + result = nsEventStatus_eConsumeNoDefault; + } else { + result = StartPanning(extPoint, firstEvent.mTimeStamp); + } + + if (splitEvent && IsInPanningState()) { + TrackTouch(splitEvent->second); + return nsEventStatus_eConsumeNoDefault; + } + + return result; + } + + case PANNING: + case PANNING_LOCKED_X: + case PANNING_LOCKED_Y: + case PAN_MOMENTUM: + TrackTouch(aEvent); + return nsEventStatus_eConsumeNoDefault; + + case PINCHING: + // The scale gesture listener should have handled this. + NS_WARNING( + "Gesture listener should have handled pinching in OnTouchMove."); + return nsEventStatus_eIgnore; + + case SMOOTH_SCROLL: + case WHEEL_SCROLL: + case KEYBOARD_SCROLL: + case OVERSCROLL_ANIMATION: + case AUTOSCROLL: + case SCROLLBAR_DRAG: + // Should not receive a touch-move in the OVERSCROLL_ANIMATION state + // as touch blocks that begin in an overscrolled state cancel the + // animation. The same is true for wheel scroll animations. + NS_WARNING("Received impossible touch in OnTouchMove"); + break; + } + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnTouchEnd( + const MultiTouchInput& aEvent) { + APZC_LOG_DETAIL("got a touch-end in state %s\n", this, + ToString(mState).c_str()); + OnTouchEndOrCancel(); + + // In case no touch behavior triggered previously we can avoid sending + // scroll events or requesting content repaint. This condition is added + // to make tests consistent - in case touch-action is NONE (and therefore + // no pans/zooms can be performed) we expected neither scroll or repaint + // events. + if (mState != NOTHING) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + } + + switch (mState) { + case FLING: + // Should never happen. + NS_WARNING("Received impossible touch end in OnTouchEnd."); + [[fallthrough]]; + case ANIMATING_ZOOM: + case SMOOTHMSD_SCROLL: + case NOTHING: + // May happen if the user double-taps and drags without lifting after the + // second tap. Ignore if this happens. + return nsEventStatus_eIgnore; + + case TOUCHING: + // We may have some velocity stored on the axis from move events + // that were not big enough to trigger scrolling. Clear that out. + SetVelocityVector(ParentLayerPoint(0, 0)); + MOZ_ASSERT(GetCurrentTouchBlock()); + APZC_LOG("%p still has %u touch points active\n", this, + GetCurrentTouchBlock()->GetActiveTouchCount()); + // In cases where the user is panning, then taps the second finger without + // entering a pinch, we will arrive here when the second finger is lifted. + // However the first finger is still down so we want to remain in state + // TOUCHING. + if (GetCurrentTouchBlock()->GetActiveTouchCount() == 0) { + // It's possible we may be overscrolled if the user tapped during a + // previous overscroll pan. Make sure to snap back in this situation. + // An ancestor APZC could be overscrolled instead of this APZC, so + // walk the handoff chain as well. + GetCurrentTouchBlock() + ->GetOverscrollHandoffChain() + ->SnapBackOverscrolledApzc(this); + mFlingAccelerator.Reset(); + // SnapBackOverscrolledApzc() will put any APZC it causes to snap back + // into the OVERSCROLL_ANIMATION state. If that's not us, since we're + // done TOUCHING enter the NOTHING state. + if (mState != OVERSCROLL_ANIMATION) { + SetState(NOTHING); + } + } + return nsEventStatus_eIgnore; + + case PANNING: + case PANNING_LOCKED_X: + case PANNING_LOCKED_Y: + case PAN_MOMENTUM: { + MOZ_ASSERT(GetCurrentTouchBlock()); + EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::Yes); + return HandleEndOfPan(); + } + case PINCHING: + SetState(NOTHING); + // Scale gesture listener should have handled this. + NS_WARNING( + "Gesture listener should have handled pinching in OnTouchEnd."); + return nsEventStatus_eIgnore; + + case SMOOTH_SCROLL: + case WHEEL_SCROLL: + case KEYBOARD_SCROLL: + case OVERSCROLL_ANIMATION: + case AUTOSCROLL: + case SCROLLBAR_DRAG: + // Should not receive a touch-end in the OVERSCROLL_ANIMATION state + // as touch blocks that begin in an overscrolled state cancel the + // animation. The same is true for WHEEL_SCROLL. + NS_WARNING("Received impossible touch in OnTouchEnd"); + break; + } + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnTouchCancel( + const MultiTouchInput& aEvent) { + APZC_LOG_DETAIL("got a touch-cancel in state %s\n", this, + ToString(mState).c_str()); + OnTouchEndOrCancel(); + CancelAnimationAndGestureState(); + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnScaleBegin( + const PinchGestureInput& aEvent) { + APZC_LOG_DETAIL("got a scale-begin in state %s\n", this, + ToString(mState).c_str()); + + mPinchLocked = false; + mPinchPaintTimerSet = false; + // Note that there may not be a touch block at this point, if we received the + // PinchGestureEvent directly from widget code without any touch events. + if (HasReadyTouchBlock() && + !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) { + return nsEventStatus_eIgnore; + } + + // For platforms that don't support APZ zooming, dispatch a message to the + // content controller, it may want to do something else with this gesture. + // FIXME: bug 1525793 -- this may need to handle zooming or not on a + // per-document basis. + if (!StaticPrefs::apz_allow_zooming()) { + if (RefPtr controller = + GetGeckoContentController()) { + APZC_LOG("%p notifying controller of pinch gesture start\n", this); + controller->NotifyPinchGesture( + aEvent.mType, GetGuid(), + ViewAs( + aEvent.mFocusPoint, + PixelCastJustification:: + LayoutDeviceIsScreenForUntransformedEvent), + 0, aEvent.modifiers); + } + } + + SetState(PINCHING); + Telemetry::Accumulate(Telemetry::APZ_ZOOM_PINCHSOURCE, (int)aEvent.mSource); + SetVelocityVector(ParentLayerPoint(0, 0)); + RecursiveMutexAutoLock lock(mRecursiveMutex); + mLastZoomFocus = + aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft(); + + mPinchEventBuffer.push(aEvent); + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) { + APZC_LOG_DETAIL("got a scale in state %s\n", this, ToString(mState).c_str()); + + if (HasReadyTouchBlock() && + !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) { + return nsEventStatus_eIgnore; + } + + if (mState != PINCHING) { + return nsEventStatus_eConsumeNoDefault; + } + + mPinchEventBuffer.push(aEvent); + HandlePinchLocking(aEvent); + bool allowZoom = ZoomConstraintsAllowZoom() && !mPinchLocked; + + // If we are pinch-locked, this is a two-finger pan. + // Tracking panning distance and velocity. + // UpdateWithTouchAtDevicePoint() acquires the tree lock, so + // it cannot be called while the mRecursiveMutex lock is held. + if (mPinchLocked) { + mX.UpdateWithTouchAtDevicePoint(aEvent.mLocalFocusPoint.x, + aEvent.mTimeStamp); + mY.UpdateWithTouchAtDevicePoint(aEvent.mLocalFocusPoint.y, + aEvent.mTimeStamp); + } + + // FIXME: bug 1525793 -- this may need to handle zooming or not on a + // per-document basis. + if (!StaticPrefs::apz_allow_zooming()) { + if (RefPtr controller = + GetGeckoContentController()) { + APZC_LOG("%p notifying controller of pinch gesture\n", this); + controller->NotifyPinchGesture( + aEvent.mType, GetGuid(), + ViewAs( + aEvent.mFocusPoint, + PixelCastJustification:: + LayoutDeviceIsScreenForUntransformedEvent), + ViewAs( + aEvent.mCurrentSpan - aEvent.mPreviousSpan, + PixelCastJustification:: + LayoutDeviceIsScreenForUntransformedEvent), + aEvent.modifiers); + } + } + + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + // Only the root APZC is zoomable, and the root APZC is not allowed to have + // different x and y scales. If it did, the calculations in this function + // would have to be adjusted (as e.g. it would no longer be valid to take + // the minimum or maximum of the ratios of the widths and heights of the + // page rect and the composition bounds). + MOZ_ASSERT(Metrics().IsRootContent()); + + CSSToParentLayerScale userZoom = Metrics().GetZoom(); + ParentLayerPoint focusPoint = + aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft(); + CSSPoint cssFocusPoint; + if (Metrics().GetZoom() != CSSToParentLayerScale(0)) { + cssFocusPoint = focusPoint / Metrics().GetZoom(); + } + + ParentLayerPoint focusChange = mLastZoomFocus - focusPoint; + mLastZoomFocus = focusPoint; + // If displacing by the change in focus point will take us off page bounds, + // then reduce the displacement such that it doesn't. + focusChange.x -= mX.DisplacementWillOverscrollAmount(focusChange.x); + focusChange.y -= mY.DisplacementWillOverscrollAmount(focusChange.y); + if (userZoom != CSSToParentLayerScale(0)) { + ScrollBy(focusChange / userZoom); + } + + // If the span is zero or close to it, we don't want to process this zoom + // change because we're going to get wonky numbers for the spanRatio. So + // let's bail out here. Note that we do this after the focus-change-scroll + // above, so that if we have a pinch with zero span but changing focus, + // such as generated by some Synaptics touchpads on Windows, we still + // scroll properly. + float prevSpan = aEvent.mPreviousSpan; + if (fabsf(prevSpan) <= EPSILON || fabsf(aEvent.mCurrentSpan) <= EPSILON) { + // We might have done a nonzero ScrollBy above, so update metrics and + // repaint/recomposite + ScheduleCompositeAndMaybeRepaint(); + return nsEventStatus_eConsumeNoDefault; + } + float spanRatio = aEvent.mCurrentSpan / aEvent.mPreviousSpan; + + // When we zoom in with focus, we can zoom too much towards the boundaries + // that we actually go over them. These are the needed displacements along + // either axis such that we don't overscroll the boundaries when zooming. + CSSPoint neededDisplacement; + + CSSToParentLayerScale realMinZoom = mZoomConstraints.mMinZoom; + CSSToParentLayerScale realMaxZoom = mZoomConstraints.mMaxZoom; + realMinZoom.scale = + std::max(realMinZoom.scale, Metrics().GetCompositionBounds().Width() / + Metrics().GetScrollableRect().Width()); + realMinZoom.scale = + std::max(realMinZoom.scale, Metrics().GetCompositionBounds().Height() / + Metrics().GetScrollableRect().Height()); + if (realMaxZoom < realMinZoom) { + realMaxZoom = realMinZoom; + } + + bool doScale = allowZoom && ((spanRatio > 1.0 && userZoom < realMaxZoom) || + (spanRatio < 1.0 && userZoom > realMinZoom)); + + if (doScale) { + spanRatio = clamped(spanRatio, realMinZoom.scale / userZoom.scale, + realMaxZoom.scale / userZoom.scale); + + // Note that the spanRatio here should never put us into OVERSCROLL_BOTH + // because up above we clamped it. + neededDisplacement.x = + -mX.ScaleWillOverscrollAmount(spanRatio, cssFocusPoint.x); + neededDisplacement.y = + -mY.ScaleWillOverscrollAmount(spanRatio, cssFocusPoint.y); + + ScaleWithFocus(spanRatio, cssFocusPoint); + + if (neededDisplacement != CSSPoint()) { + ScrollBy(neededDisplacement); + } + + // We don't want to redraw on every scale, so throttle it. + if (!mPinchPaintTimerSet) { + const int delay = StaticPrefs::apz_scale_repaint_delay_ms(); + if (delay >= 0) { + if (RefPtr controller = + GetGeckoContentController()) { + mPinchPaintTimerSet = true; + controller->PostDelayedTask( + NewRunnableMethod( + "layers::AsyncPanZoomController::" + "DoDelayedRequestContentRepaint", + this, + &AsyncPanZoomController::DoDelayedRequestContentRepaint), + delay); + } + } + } else if (apz::AboutToCheckerboard(mLastContentPaintMetrics, + Metrics())) { + // If we already scheduled a throttled repaint request but are also + // in danger of checkerboarding soon, trigger the repaint request to + // go out immediately. This should reduce the amount of time we spend + // checkerboarding. + // + // Note that if we remain in this "about to + // checkerboard" state over a period of time with multiple pinch input + // events (which is quite likely), then we will flip-flop between taking + // the above branch (!mPinchPaintTimerSet) and this branch (which will + // flush the repaint request and reset mPinchPaintTimerSet to false). + // This is sort of desirable because it halves the number of repaint + // requests we send, and therefore reduces IPC traffic. + // Keep in mind that many of these repaint requests will be ignored on + // the main-thread anyway due to the resolution mismatch - the first + // repaint request will be honored because APZ's notion of the painted + // resolution matches the actual main thread resolution, but that first + // repaint request will change the resolution on the main thread. + // Subsequent repaint requests will be ignored in APZCCallbackHelper, at + // https://searchfox.org/mozilla-central/rev/e0eb861a187f0bb6d994228f2e0e49b2c9ee455e/gfx/layers/apz/util/APZCCallbackHelper.cpp#331-338, + // until we receive a NotifyLayersUpdated call that re-syncs APZ's + // notion of the painted resolution to the main thread. These ignored + // repaint requests are contributing to IPC traffic needlessly, and so + // halving the number of repaint requests (as mentioned above) seems + // desirable. + DoDelayedRequestContentRepaint(); + } + } else { + // Trigger a repaint request after scrolling. + RequestContentRepaint(); + } + + // We did a ScrollBy call above even if we didn't do a scale, so we + // should composite for that. + ScheduleComposite(); + } + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnScaleEnd( + const PinchGestureInput& aEvent) { + APZC_LOG_DETAIL("got a scale-end in state %s\n", this, + ToString(mState).c_str()); + + mPinchPaintTimerSet = false; + + if (HasReadyTouchBlock() && + !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) { + return nsEventStatus_eIgnore; + } + + // FIXME: bug 1525793 -- this may need to handle zooming or not on a + // per-document basis. + if (!StaticPrefs::apz_allow_zooming()) { + if (RefPtr controller = + GetGeckoContentController()) { + controller->NotifyPinchGesture( + aEvent.mType, GetGuid(), + ViewAs( + aEvent.mFocusPoint, + PixelCastJustification:: + LayoutDeviceIsScreenForUntransformedEvent), + 0, aEvent.modifiers); + } + } + + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + ScheduleComposite(); + RequestContentRepaint(); + } + + mPinchEventBuffer.clear(); + + if (aEvent.mType == PinchGestureInput::PINCHGESTURE_FINGERLIFTED) { + // One finger is still down, so transition to a TOUCHING state + if (!mPinchLocked) { + mPanDirRestricted = false; + mLastTouch.mPosition = mStartTouch = + ToExternalPoint(aEvent.mScreenOffset, aEvent.mFocusPoint); + mLastTouch.mTimeStamp = mTouchStartTime = aEvent.mTimeStamp; + StartTouch(aEvent.mLocalFocusPoint, aEvent.mTimeStamp); + SetState(TOUCHING); + } else { + // If we are pinch locked, StartTouch() was already called + // when we entered the pinch lock. + StartPanning(ToExternalPoint(aEvent.mScreenOffset, aEvent.mFocusPoint), + aEvent.mTimeStamp); + } + } else { + // Otherwise, handle the gesture being completely done. + + // Some of the code paths below, like ScrollSnap() or HandleEndOfPan(), + // may start an animation, but otherwise we want to end up in the NOTHING + // state. To avoid state change notification churn, we use a + // notification blocker. + bool stateWasPinching = (mState == PINCHING); + StateChangeNotificationBlocker blocker(this); + SetState(NOTHING); + + if (ZoomConstraintsAllowZoom()) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + // We can get into a situation where we are overscrolled at the end of a + // pinch if we go into overscroll with a two-finger pan, and then turn + // that into a pinch by increasing the span sufficiently. In such a case, + // there is no snap-back animation to get us out of overscroll, so we need + // to get out of it somehow. + // Moreover, in cases of scroll handoff, the overscroll can be on an APZC + // further up in the handoff chain rather than on the current APZC, so + // we need to clear overscroll along the entire handoff chain. + if (HasReadyTouchBlock()) { + GetCurrentTouchBlock()->GetOverscrollHandoffChain()->ClearOverscroll(); + } else { + ClearOverscroll(); + } + // Along with clearing the overscroll, we also want to snap to the nearest + // snap point as appropriate. + ScrollSnap(ScrollSnapFlags::IntendedEndPosition); + } else { + // when zoom is not allowed + EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::Yes); + if (stateWasPinching) { + // still pinching + if (HasReadyTouchBlock()) { + return HandleEndOfPan(); + } + } + } + } + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::HandleEndOfPan() { + MOZ_ASSERT(!mAnimation); + MOZ_ASSERT(GetCurrentTouchBlock() || GetCurrentPanGestureBlock()); + GetCurrentInputBlock()->GetOverscrollHandoffChain()->FlushRepaints(); + ParentLayerPoint flingVelocity = GetVelocityVector(); + + // Clear our velocities; if DispatchFling() gives the fling to us, + // the fling velocity gets *added* to our existing velocity in + // AcceptFling(). + SetVelocityVector(ParentLayerPoint(0, 0)); + // Clear our state so that we don't stay in the PANNING state + // if DispatchFling() gives the fling to somone else. However, + // don't send the state change notification until we've determined + // what our final state is to avoid notification churn. + StateChangeNotificationBlocker blocker(this); + SetState(NOTHING); + + APZC_LOG("%p starting a fling animation if %f > %f\n", this, + flingVelocity.Length().value, + StaticPrefs::apz_fling_min_velocity_threshold()); + + if (flingVelocity.Length() <= + StaticPrefs::apz_fling_min_velocity_threshold()) { + // Relieve overscroll now if needed, since we will not transition to a fling + // animation and then an overscroll animation, and relieve it then. + GetCurrentInputBlock() + ->GetOverscrollHandoffChain() + ->SnapBackOverscrolledApzc(this); + mFlingAccelerator.Reset(); + return nsEventStatus_eConsumeNoDefault; + } + + // Make a local copy of the tree manager pointer and check that it's not + // null before calling DispatchFling(). This is necessary because Destroy(), + // which nulls out mTreeManager, could be called concurrently. + if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) { + const FlingHandoffState handoffState{ + flingVelocity, + GetCurrentInputBlock()->GetOverscrollHandoffChain(), + Some(mTouchStartRestingTimeBeforePan), + mMinimumVelocityDuringPan.valueOr(0), + false /* not handoff */, + GetCurrentInputBlock()->GetScrolledApzc()}; + treeManagerLocal->DispatchFling(this, handoffState); + } + return nsEventStatus_eConsumeNoDefault; +} + +Maybe AsyncPanZoomController::ConvertToGecko( + const ScreenIntPoint& aPoint) { + if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) { + if (Maybe layoutPoint = + treeManagerLocal->ConvertToGecko(aPoint, this)) { + return Some(LayoutDevicePoint(ViewAs( + *layoutPoint, + PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent))); + } + } + return Nothing(); +} + +OuterCSSCoord AsyncPanZoomController::ConvertScrollbarPoint( + const ParentLayerPoint& aScrollbarPoint, + const ScrollbarData& aThumbData) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + CSSPoint scrollbarPoint; + if (Metrics().GetZoom() != CSSToParentLayerScale(0)) { + // First, get it into the right coordinate space. + scrollbarPoint = aScrollbarPoint / Metrics().GetZoom(); + } + + // The scrollbar can be transformed with the frame but the pres shell + // resolution is only applied to the scroll frame. + OuterCSSPoint outerScrollbarPoint = + scrollbarPoint * Metrics().GetCSSToOuterCSSScale(); + + // Now, get it to be relative to the beginning of the scroll track. + OuterCSSRect cssCompositionBound = + Metrics().CalculateCompositionBoundsInOuterCssPixels(); + return GetAxisStart(*aThumbData.mDirection, outerScrollbarPoint) - + GetAxisStart(*aThumbData.mDirection, cssCompositionBound) - + aThumbData.mScrollTrackStart; +} + +static bool AllowsScrollingMoreThanOnePage(double aMultiplier) { + return Abs(aMultiplier) >= + EventStateManager::MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL; +} + +ParentLayerPoint AsyncPanZoomController::GetScrollWheelDelta( + const ScrollWheelInput& aEvent) const { + return GetScrollWheelDelta(aEvent, aEvent.mDeltaX, aEvent.mDeltaY, + aEvent.mUserDeltaMultiplierX, + aEvent.mUserDeltaMultiplierY); +} + +ParentLayerPoint AsyncPanZoomController::GetScrollWheelDelta( + const ScrollWheelInput& aEvent, double aDeltaX, double aDeltaY, + double aMultiplierX, double aMultiplierY) const { + ParentLayerSize scrollAmount; + ParentLayerSize pageScrollSize; + + { + // Grab the lock to access the frame metrics. + RecursiveMutexAutoLock lock(mRecursiveMutex); + LayoutDeviceIntSize scrollAmountLD = mScrollMetadata.GetLineScrollAmount(); + LayoutDeviceIntSize pageScrollSizeLD = + mScrollMetadata.GetPageScrollAmount(); + scrollAmount = scrollAmountLD / Metrics().GetDevPixelsPerCSSPixel() * + Metrics().GetZoom(); + pageScrollSize = pageScrollSizeLD / Metrics().GetDevPixelsPerCSSPixel() * + Metrics().GetZoom(); + } + + ParentLayerPoint delta; + switch (aEvent.mDeltaType) { + case ScrollWheelInput::SCROLLDELTA_LINE: { + delta.x = aDeltaX * scrollAmount.width; + delta.y = aDeltaY * scrollAmount.height; + break; + } + case ScrollWheelInput::SCROLLDELTA_PAGE: { + delta.x = aDeltaX * pageScrollSize.width; + delta.y = aDeltaY * pageScrollSize.height; + break; + } + case ScrollWheelInput::SCROLLDELTA_PIXEL: { + delta = ToParentLayerCoordinates(ScreenPoint(aDeltaX, aDeltaY), + aEvent.mOrigin); + break; + } + } + + // Apply user-set multipliers. + delta.x *= aMultiplierX; + delta.y *= aMultiplierY; + + // For the conditions under which we allow system scroll overrides, see + // WidgetWheelEvent::OverriddenDelta{X,Y}. + // Note that we do *not* restrict this to the root content, see bug 1217715 + // for discussion on this. + if (StaticPrefs::mousewheel_system_scroll_override_enabled() && + !aEvent.IsCustomizedByUserPrefs() && + aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_LINE && + aEvent.mAllowToOverrideSystemScrollSpeed) { + delta.x = WidgetWheelEvent::ComputeOverriddenDelta(delta.x, false); + delta.y = WidgetWheelEvent::ComputeOverriddenDelta(delta.y, true); + } + + // If this is a line scroll, and this event was part of a scroll series, then + // it might need extra acceleration. See WheelHandlingHelper.cpp. + if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_LINE && + aEvent.mScrollSeriesNumber > 0) { + int32_t start = StaticPrefs::mousewheel_acceleration_start(); + if (start >= 0 && aEvent.mScrollSeriesNumber >= uint32_t(start)) { + int32_t factor = StaticPrefs::mousewheel_acceleration_factor(); + if (factor > 0) { + delta.x = ComputeAcceleratedWheelDelta( + delta.x, aEvent.mScrollSeriesNumber, factor); + delta.y = ComputeAcceleratedWheelDelta( + delta.y, aEvent.mScrollSeriesNumber, factor); + } + } + } + + // We shouldn't scroll more than one page at once except when the + // user preference is large. + if (!AllowsScrollingMoreThanOnePage(aMultiplierX) && + Abs(delta.x) > pageScrollSize.width) { + delta.x = (delta.x >= 0) ? pageScrollSize.width : -pageScrollSize.width; + } + if (!AllowsScrollingMoreThanOnePage(aMultiplierY) && + Abs(delta.y) > pageScrollSize.height) { + delta.y = (delta.y >= 0) ? pageScrollSize.height : -pageScrollSize.height; + } + + return delta; +} + +nsEventStatus AsyncPanZoomController::OnKeyboard(const KeyboardInput& aEvent) { + // Mark that this APZC has async key scrolled + mTestHasAsyncKeyScrolled = true; + + // Calculate the destination for this keyboard scroll action + CSSPoint destination = GetKeyboardDestination(aEvent.mAction); + ScrollOrigin scrollOrigin = + SmoothScrollAnimation::GetScrollOriginForAction(aEvent.mAction.mType); + Maybe snapTarget = MaybeAdjustDestinationForScrollSnapping( + aEvent, destination, GetScrollSnapFlagsForKeyboardAction(aEvent.mAction)); + ScrollMode scrollMode = apz::GetScrollModeForOrigin(scrollOrigin); + + RecordScrollPayload(aEvent.mTimeStamp); + // If the scrolling is instant, then scroll immediately to the destination + if (scrollMode == ScrollMode::Instant) { + CancelAnimation(); + + ParentLayerPoint startPoint, endPoint; + + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + // CallDispatchScroll interprets the start and end points as the start and + // end of a touch scroll so they need to be reversed. + startPoint = destination * Metrics().GetZoom(); + endPoint = Metrics().GetVisualScrollOffset() * Metrics().GetZoom(); + } + + ParentLayerPoint delta = endPoint - startPoint; + + ScreenPoint distance = ToScreenCoordinates( + ParentLayerPoint(fabs(delta.x), fabs(delta.y)), startPoint); + + OverscrollHandoffState handoffState( + *mInputQueue->GetCurrentKeyboardBlock()->GetOverscrollHandoffChain(), + distance, ScrollSource::Keyboard); + + CallDispatchScroll(startPoint, endPoint, handoffState); + ParentLayerPoint remainingDelta = endPoint - startPoint; + if (remainingDelta != delta) { + // If any scrolling happened, set KEYBOARD_SCROLL explicitly so that it + // will trigger a TransformEnd notification. + SetState(KEYBOARD_SCROLL); + } + + if (snapTarget) { + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mLastSnapTargetIds = std::move(snapTarget->mTargetIds); + } + } + SetState(NOTHING); + + return nsEventStatus_eConsumeDoDefault; + } + + // The lock must be held across the entire update operation, so the + // compositor doesn't end the animation before we get a chance to + // update it. + RecursiveMutexAutoLock lock(mRecursiveMutex); + + if (snapTarget) { + // If we're scroll snapping, use a smooth scroll animation to get + // the desired physics. Note that SmoothMsdScrollTo() will re-use an + // existing smooth scroll animation if there is one. + APZC_LOG("%p keyboard scrolling to snap point %s\n", this, + ToString(destination).c_str()); + SmoothMsdScrollTo(std::move(*snapTarget), ScrollTriggeredByScript::No); + return nsEventStatus_eConsumeDoDefault; + } + + // Use a keyboard scroll animation to scroll, reusing an existing one if it + // exists + if (mState != KEYBOARD_SCROLL) { + CancelAnimation(); + SetState(KEYBOARD_SCROLL); + + nsPoint initialPosition = + CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset()); + StartAnimation( + new SmoothScrollAnimation(*this, initialPosition, scrollOrigin)); + } + + // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and then + // to appunits/second. + nsPoint velocity; + if (Metrics().GetZoom() != CSSToParentLayerScale(0)) { + velocity = + CSSPoint::ToAppUnits(ParentLayerPoint(mX.GetVelocity() * 1000.0f, + mY.GetVelocity() * 1000.0f) / + Metrics().GetZoom()); + } + + SmoothScrollAnimation* animation = mAnimation->AsSmoothScrollAnimation(); + MOZ_ASSERT(animation); + + animation->UpdateDestination(aEvent.mTimeStamp, + CSSPixel::ToAppUnits(destination), + nsSize(velocity.x, velocity.y)); + + return nsEventStatus_eConsumeDoDefault; +} + +CSSPoint AsyncPanZoomController::GetKeyboardDestination( + const KeyboardScrollAction& aAction) const { + CSSSize lineScrollSize; + CSSSize pageScrollSize; + CSSPoint scrollOffset; + CSSRect scrollRect; + ParentLayerRect compositionBounds; + + { + // Grab the lock to access the frame metrics. + RecursiveMutexAutoLock lock(mRecursiveMutex); + + lineScrollSize = mScrollMetadata.GetLineScrollAmount() / + Metrics().GetDevPixelsPerCSSPixel(); + pageScrollSize = mScrollMetadata.GetPageScrollAmount() / + Metrics().GetDevPixelsPerCSSPixel(); + + scrollOffset = GetCurrentAnimationDestination(lock).valueOr( + Metrics().GetVisualScrollOffset()); + + scrollRect = Metrics().GetScrollableRect(); + compositionBounds = Metrics().GetCompositionBounds(); + } + + // Calculate the scroll destination based off of the scroll type and direction + CSSPoint scrollDestination = scrollOffset; + + switch (aAction.mType) { + case KeyboardScrollAction::eScrollCharacter: { + int32_t scrollDistance = + StaticPrefs::toolkit_scrollbox_horizontalScrollDistance(); + + if (aAction.mForward) { + scrollDestination.x += scrollDistance * lineScrollSize.width; + } else { + scrollDestination.x -= scrollDistance * lineScrollSize.width; + } + break; + } + case KeyboardScrollAction::eScrollLine: { + int32_t scrollDistance = + StaticPrefs::toolkit_scrollbox_verticalScrollDistance(); + if (scrollDistance * lineScrollSize.height <= + compositionBounds.Height()) { + if (aAction.mForward) { + scrollDestination.y += scrollDistance * lineScrollSize.height; + } else { + scrollDestination.y -= scrollDistance * lineScrollSize.height; + } + break; + } + [[fallthrough]]; + } + case KeyboardScrollAction::eScrollPage: { + if (aAction.mForward) { + scrollDestination.y += pageScrollSize.height; + } else { + scrollDestination.y -= pageScrollSize.height; + } + break; + } + case KeyboardScrollAction::eScrollComplete: { + if (aAction.mForward) { + scrollDestination.y = scrollRect.YMost(); + } else { + scrollDestination.y = scrollRect.Y(); + } + break; + } + } + + return scrollDestination; +} + +ScrollSnapFlags AsyncPanZoomController::GetScrollSnapFlagsForKeyboardAction( + const KeyboardScrollAction& aAction) const { + switch (aAction.mType) { + case KeyboardScrollAction::eScrollCharacter: + case KeyboardScrollAction::eScrollLine: + return ScrollSnapFlags::IntendedDirection; + case KeyboardScrollAction::eScrollPage: + return ScrollSnapFlags::IntendedDirection | + ScrollSnapFlags::IntendedEndPosition; + case KeyboardScrollAction::eScrollComplete: + return ScrollSnapFlags::IntendedEndPosition; + } + return ScrollSnapFlags::Disabled; +} + +ParentLayerPoint AsyncPanZoomController::GetDeltaForEvent( + const InputData& aEvent) const { + ParentLayerPoint delta; + if (aEvent.mInputType == SCROLLWHEEL_INPUT) { + delta = GetScrollWheelDelta(aEvent.AsScrollWheelInput()); + } else if (aEvent.mInputType == PANGESTURE_INPUT) { + const PanGestureInput& panInput = aEvent.AsPanGestureInput(); + delta = ToParentLayerCoordinates(panInput.UserMultipliedPanDisplacement(), + panInput.mPanStartPoint); + } + return delta; +} + +CSSRect AsyncPanZoomController::GetCurrentScrollRangeInCssPixels() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return Metrics().CalculateScrollRange(); +} + +// Return whether or not the underlying layer can be scrolled on either axis. +bool AsyncPanZoomController::CanScroll(const InputData& aEvent) const { + ParentLayerPoint delta = GetDeltaForEvent(aEvent); + if (!delta.x && !delta.y) { + return false; + } + + if (SCROLLWHEEL_INPUT == aEvent.mInputType) { + const ScrollWheelInput& scrollWheelInput = aEvent.AsScrollWheelInput(); + // If it's a wheel scroll, we first check if it is an auto-dir scroll. + // 1. For an auto-dir scroll, check if it's delta should be adjusted, if it + // is, then we can conclude it must be scrollable; otherwise, fall back + // to checking if it is scrollable without adjusting its delta. + // 2. For a non-auto-dir scroll, simply check if it is scrollable without + // adjusting its delta. + RecursiveMutexAutoLock lock(mRecursiveMutex); + if (scrollWheelInput.IsAutoDir(mScrollMetadata.ForceMousewheelAutodir())) { + auto deltaX = scrollWheelInput.mDeltaX; + auto deltaY = scrollWheelInput.mDeltaY; + bool isRTL = + IsContentOfHonouredTargetRightToLeft(scrollWheelInput.HonoursRoot( + mScrollMetadata.ForceMousewheelAutodirHonourRoot())); + APZAutoDirWheelDeltaAdjuster adjuster(deltaX, deltaY, mX, mY, isRTL); + if (adjuster.ShouldBeAdjusted()) { + // If we detect that the delta values should be adjusted for an auto-dir + // wheel scroll, then it is impossible to be an unscrollable scroll. + return true; + } + } + return CanScrollWithWheel(delta); + } + return CanScroll(delta); +} + +ScrollDirections AsyncPanZoomController::GetAllowedHandoffDirections() const { + ScrollDirections result; + RecursiveMutexAutoLock lock(mRecursiveMutex); + + // In Fission there can be non-scrollable APZCs. It's unclear whether + // overscroll-behavior should be respected for these + // (see https://github.com/w3c/csswg-drafts/issues/6523) but + // we currently don't, to match existing practice. + const bool isScrollable = mX.CanScroll() || mY.CanScroll(); + const bool isRoot = IsRootContent(); + if ((!isScrollable && !isRoot) || mX.OverscrollBehaviorAllowsHandoff()) { + result += ScrollDirection::eHorizontal; + } + if ((!isScrollable && !isRoot) || mY.OverscrollBehaviorAllowsHandoff()) { + result += ScrollDirection::eVertical; + } + return result; +} + +bool AsyncPanZoomController::CanScroll(const ParentLayerPoint& aDelta) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mX.CanScroll(ParentLayerCoord(aDelta.x)) || + mY.CanScroll(ParentLayerCoord(aDelta.y)); +} + +bool AsyncPanZoomController::CanScrollWithWheel( + const ParentLayerPoint& aDelta) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + // For more details about the concept of a disregarded direction, refer to the + // code in struct ScrollMetadata which defines mDisregardedDirection. + Maybe disregardedDirection = + mScrollMetadata.GetDisregardedDirection(); + if (mX.CanScroll(ParentLayerCoord(aDelta.x)) && + disregardedDirection != Some(ScrollDirection::eHorizontal)) { + return true; + } + if (mY.CanScroll(ParentLayerCoord(aDelta.y)) && + disregardedDirection != Some(ScrollDirection::eVertical)) { + return true; + } + return false; +} + +bool AsyncPanZoomController::CanScroll(ScrollDirection aDirection) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + switch (aDirection) { + case ScrollDirection::eHorizontal: + return mX.CanScroll(); + case ScrollDirection::eVertical: + return mY.CanScroll(); + } + MOZ_ASSERT_UNREACHABLE("Invalid value"); + return false; +} + +bool AsyncPanZoomController::CanVerticalScrollWithDynamicToolbar() const { + MOZ_ASSERT(IsRootContent()); + + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mY.CanVerticalScrollWithDynamicToolbar(); +} + +bool AsyncPanZoomController::CanScrollDownwards() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mY.CanScrollTo(eSideBottom); +} + +SideBits AsyncPanZoomController::ScrollableDirections() const { + SideBits result; + { // scope lock to respect lock ordering with APZCTreeManager::mTreeLock + // which will be acquired in the `GetCompositorFixedLayerMargins` below. + RecursiveMutexAutoLock lock(mRecursiveMutex); + result = mX.ScrollableDirections() | mY.ScrollableDirections(); + } + + if (IsRootContent()) { + if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) { + ScreenMargin fixedLayerMargins = + treeManagerLocal->GetCompositorFixedLayerMargins(); + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + result |= mY.ScrollableDirectionsWithDynamicToolbar(fixedLayerMargins); + } + } + } + + return result; +} + +bool AsyncPanZoomController::IsContentOfHonouredTargetRightToLeft( + bool aHonoursRoot) const { + if (aHonoursRoot) { + return mScrollMetadata.IsAutoDirRootContentRTL(); + } + RecursiveMutexAutoLock lock(mRecursiveMutex); + return Metrics().IsHorizontalContentRightToLeft(); +} + +bool AsyncPanZoomController::AllowScrollHandoffInCurrentBlock() const { + bool result = mInputQueue->AllowScrollHandoff(); + if (!StaticPrefs::apz_allow_immediate_handoff()) { + if (InputBlockState* currentBlock = GetCurrentInputBlock()) { + // Do not allow handoff beyond the first APZC to scroll. + if (currentBlock->GetScrolledApzc() == this) { + result = false; + APZC_LOG("%p dropping handoff; AllowImmediateHandoff=false\n", this); + } + } + } + return result; +} + +void AsyncPanZoomController::DoDelayedRequestContentRepaint() { + if (!IsDestroyed() && mPinchPaintTimerSet) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + RequestContentRepaint(); + } + mPinchPaintTimerSet = false; +} + +void AsyncPanZoomController::DoDelayedTransformEndNotification( + PanZoomState aOldState) { + if (!IsDestroyed() && IsDelayedTransformEndSet()) { + DispatchStateChangeNotification(aOldState, NOTHING); + } + SetDelayedTransformEnd(false); +} + +static void AdjustDeltaForAllowedScrollDirections( + ParentLayerPoint& aDelta, + const ScrollDirections& aAllowedScrollDirections) { + if (!aAllowedScrollDirections.contains(ScrollDirection::eHorizontal)) { + aDelta.x = 0; + } + if (!aAllowedScrollDirections.contains(ScrollDirection::eVertical)) { + aDelta.y = 0; + } +} + +nsEventStatus AsyncPanZoomController::OnScrollWheel( + const ScrollWheelInput& aEvent) { + // Get the scroll wheel's delta values in parent-layer pixels. But before + // getting the values, we need to check if it is an auto-dir scroll and if it + // should be adjusted, if both answers are yes, let's adjust X and Y values + // first, and then get the delta values in parent-layer pixels based on the + // adjusted values. + bool adjustedByAutoDir = false; + auto deltaX = aEvent.mDeltaX; + auto deltaY = aEvent.mDeltaY; + ParentLayerPoint delta; + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + if (aEvent.IsAutoDir(mScrollMetadata.ForceMousewheelAutodir())) { + // It's an auto-dir scroll, so check if its delta should be adjusted, if + // so, adjust it. + bool isRTL = IsContentOfHonouredTargetRightToLeft(aEvent.HonoursRoot( + mScrollMetadata.ForceMousewheelAutodirHonourRoot())); + APZAutoDirWheelDeltaAdjuster adjuster(deltaX, deltaY, mX, mY, isRTL); + if (adjuster.ShouldBeAdjusted()) { + adjuster.Adjust(); + adjustedByAutoDir = true; + } + } + } + // Ensure the calls to GetScrollWheelDelta are outside the mRecursiveMutex + // lock since these calls may acquire the APZ tree lock. Holding + // mRecursiveMutex while acquiring the APZ tree lock is lock ordering + // violation. + if (adjustedByAutoDir) { + // If the original delta values have been adjusted, we pass them to + // replace the original delta values in |aEvent| so that the delta values + // in parent-layer pixels are caculated based on the adjusted values, not + // the original ones. + // Pay special attention to the last two parameters. They are in a swaped + // order so that they still correspond to their delta after adjustment. + delta = GetScrollWheelDelta(aEvent, deltaX, deltaY, + aEvent.mUserDeltaMultiplierY, + aEvent.mUserDeltaMultiplierX); + } else { + // If the original delta values haven't been adjusted by auto-dir, just pass + // the |aEvent| and caculate the delta values in parent-layer pixels based + // on the original delta values from |aEvent|. + delta = GetScrollWheelDelta(aEvent); + } + + APZC_LOG("%p got a scroll-wheel with delta in parent-layer pixels: %s\n", + this, ToString(delta).c_str()); + + if (adjustedByAutoDir) { + MOZ_ASSERT(delta.x || delta.y, + "Adjusted auto-dir delta values can never be all-zero."); + APZC_LOG("%p got a scroll-wheel with adjusted auto-dir delta values\n", + this); + } else if ((delta.x || delta.y) && !CanScrollWithWheel(delta)) { + // We can't scroll this apz anymore, so we simply drop the event. + if (mInputQueue->GetActiveWheelTransaction() && + StaticPrefs::test_mousescroll()) { + if (RefPtr controller = + GetGeckoContentController()) { + controller->NotifyMozMouseScrollEvent(GetScrollId(), + u"MozMouseScrollFailed"_ns); + } + } + return nsEventStatus_eConsumeNoDefault; + } + + MOZ_ASSERT(mInputQueue->GetCurrentWheelBlock()); + AdjustDeltaForAllowedScrollDirections( + delta, mInputQueue->GetCurrentWheelBlock()->GetAllowedScrollDirections()); + + if (delta.x == 0 && delta.y == 0) { + // Avoid spurious state changes and unnecessary work + return nsEventStatus_eIgnore; + } + + switch (aEvent.mScrollMode) { + case ScrollWheelInput::SCROLLMODE_INSTANT: { + // Wheel events from "clicky" mouse wheels trigger scroll snapping to the + // next snap point. Check for this, and adjust the delta to take into + // account the snap point. + CSSPoint startPosition; + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + startPosition = Metrics().GetVisualScrollOffset(); + } + Maybe snapTarget = + MaybeAdjustDeltaForScrollSnappingOnWheelInput(aEvent, delta, + startPosition); + + ScreenPoint distance = ToScreenCoordinates( + ParentLayerPoint(fabs(delta.x), fabs(delta.y)), aEvent.mLocalOrigin); + + CancelAnimation(); + + OverscrollHandoffState handoffState( + *mInputQueue->GetCurrentWheelBlock()->GetOverscrollHandoffChain(), + distance, ScrollSource::Wheel); + ParentLayerPoint startPoint = aEvent.mLocalOrigin; + ParentLayerPoint endPoint = aEvent.mLocalOrigin - delta; + RecordScrollPayload(aEvent.mTimeStamp); + + CallDispatchScroll(startPoint, endPoint, handoffState); + ParentLayerPoint remainingDelta = endPoint - startPoint; + if (remainingDelta != delta) { + // If any scrolling happened, set WHEEL_SCROLL explicitly so that it + // will trigger a TransformEnd notification. + SetState(WHEEL_SCROLL); + } + + if (snapTarget) { + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mLastSnapTargetIds = std::move(snapTarget->mTargetIds); + } + } + SetState(NOTHING); + + // The calls above handle their own locking; moreover, + // ToScreenCoordinates() and CallDispatchScroll() can grab the tree lock. + RecursiveMutexAutoLock lock(mRecursiveMutex); + RequestContentRepaint(); + + break; + } + + case ScrollWheelInput::SCROLLMODE_SMOOTH: { + // The lock must be held across the entire update operation, so the + // compositor doesn't end the animation before we get a chance to + // update it. + RecursiveMutexAutoLock lock(mRecursiveMutex); + + RecordScrollPayload(aEvent.mTimeStamp); + // Perform scroll snapping if appropriate. + // If we're already in a wheel scroll or smooth scroll animation, + // the delta is applied to its destination, not to the current + // scroll position. Take this into account when finding a snap point. + CSSPoint startPosition = GetCurrentAnimationDestination(lock).valueOr( + Metrics().GetVisualScrollOffset()); + + if (Maybe snapTarget = + MaybeAdjustDeltaForScrollSnappingOnWheelInput(aEvent, delta, + startPosition)) { + // If we're scroll snapping, use a smooth scroll animation to get + // the desired physics. Note that SmoothMsdScrollTo() will re-use an + // existing smooth scroll animation if there is one. + APZC_LOG("%p wheel scrolling to snap point %s\n", this, + ToString(startPosition).c_str()); + SmoothMsdScrollTo(std::move(*snapTarget), ScrollTriggeredByScript::No); + break; + } + + // Otherwise, use a wheel scroll animation, also reusing one if possible. + if (mState != WHEEL_SCROLL) { + CancelAnimation(); + SetState(WHEEL_SCROLL); + + nsPoint initialPosition = + CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset()); + StartAnimation(new WheelScrollAnimation(*this, initialPosition, + aEvent.mDeltaType)); + } + // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and + // then to appunits/second. + + nsPoint deltaInAppUnits; + nsPoint velocity; + if (Metrics().GetZoom() != CSSToParentLayerScale(0)) { + deltaInAppUnits = CSSPoint::ToAppUnits(delta / Metrics().GetZoom()); + velocity = + CSSPoint::ToAppUnits(ParentLayerPoint(mX.GetVelocity() * 1000.0f, + mY.GetVelocity() * 1000.0f) / + Metrics().GetZoom()); + } + + WheelScrollAnimation* animation = mAnimation->AsWheelScrollAnimation(); + animation->UpdateDelta(aEvent.mTimeStamp, deltaInAppUnits, + nsSize(velocity.x, velocity.y)); + break; + } + } + + return nsEventStatus_eConsumeNoDefault; +} + +void AsyncPanZoomController::NotifyMozMouseScrollEvent( + const nsString& aString) const { + RefPtr controller = GetGeckoContentController(); + if (!controller) { + return; + } + controller->NotifyMozMouseScrollEvent(GetScrollId(), aString); +} + +nsEventStatus AsyncPanZoomController::OnPanMayBegin( + const PanGestureInput& aEvent) { + APZC_LOG_DETAIL("got a pan-maybegin in state %s\n", this, + ToString(mState).c_str()); + + StartTouch(aEvent.mLocalPanStartPoint, aEvent.mTimeStamp); + MOZ_ASSERT(GetCurrentPanGestureBlock()); + GetCurrentPanGestureBlock()->GetOverscrollHandoffChain()->CancelAnimations(); + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnPanCancelled( + const PanGestureInput& aEvent) { + APZC_LOG_DETAIL("got a pan-cancelled in state %s\n", this, + ToString(mState).c_str()); + + mX.CancelGesture(); + mY.CancelGesture(); + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnPanBegin( + const PanGestureInput& aEvent) { + APZC_LOG_DETAIL("got a pan-begin in state %s\n", this, + ToString(mState).c_str()); + + if (mState == SMOOTHMSD_SCROLL) { + // SMOOTHMSD_SCROLL scrolls are cancelled by pan gestures. + CancelAnimation(); + } + + StartTouch(aEvent.mLocalPanStartPoint, aEvent.mTimeStamp); + + if (!UsingStatefulAxisLock()) { + SetState(PANNING); + } else { + float dx = aEvent.mPanDisplacement.x, dy = aEvent.mPanDisplacement.y; + + if (dx != 0.0f || dy != 0.0f) { + double angle = atan2(dy, dx); // range [-pi, pi] + angle = fabs(angle); // range [0, pi] + HandlePanning(angle); + } else { + SetState(PANNING); + } + } + + // Call into OnPan in order to process any delta included in this event. + OnPan(aEvent, FingersOnTouchpad::Yes); + + return nsEventStatus_eConsumeNoDefault; +} + +std::tuple +AsyncPanZoomController::GetDisplacementsForPanGesture( + const PanGestureInput& aEvent) { + // Note that there is a multiplier that applies onto the "physical" pan + // displacement (how much the user's fingers moved) that produces the + // "logical" pan displacement (how much the page should move). For some of the + // code below it makes more sense to use the physical displacement rather than + // the logical displacement, and vice-versa. + ScreenPoint physicalPanDisplacement = aEvent.mPanDisplacement; + ParentLayerPoint logicalPanDisplacement = + aEvent.UserMultipliedLocalPanDisplacement(); + if (aEvent.mDeltaType == PanGestureInput::PANDELTA_PAGE) { + // Pan events with page units are used by Gtk, so this replicates Gtk: + // https://gitlab.gnome.org/GNOME/gtk/blob/c734c7e9188b56f56c3a504abee05fa40c5475ac/gtk/gtkrange.c#L3065-3073 + CSSSize pageScrollSize; + CSSToParentLayerScale zoom; + { + // Grab the lock to access the frame metrics. + RecursiveMutexAutoLock lock(mRecursiveMutex); + pageScrollSize = mScrollMetadata.GetPageScrollAmount() / + Metrics().GetDevPixelsPerCSSPixel(); + zoom = Metrics().GetZoom(); + } + // scrollUnit* is in units of "ParentLayer pixels per page proportion"... + auto scrollUnitWidth = std::min(std::pow(pageScrollSize.width, 2.0 / 3.0), + pageScrollSize.width / 2.0) * + zoom.scale; + auto scrollUnitHeight = std::min(std::pow(pageScrollSize.height, 2.0 / 3.0), + pageScrollSize.height / 2.0) * + zoom.scale; + // ... and pan displacements are in units of "page proportion count" + // here, so the products of them and scrollUnit* are in ParentLayer pixels + ParentLayerPoint physicalPanDisplacementPL( + physicalPanDisplacement.x * scrollUnitWidth, + physicalPanDisplacement.y * scrollUnitHeight); + physicalPanDisplacement = ToScreenCoordinates(physicalPanDisplacementPL, + aEvent.mLocalPanStartPoint); + logicalPanDisplacement.x *= scrollUnitWidth; + logicalPanDisplacement.y *= scrollUnitHeight; + + // Accelerate (decelerate) any pans by raising it to a user configurable + // power (apz.touch_acceleration_factor_x, apz.touch_acceleration_factor_y) + // + // Confine input for pow() to greater than or equal to 0 to avoid domain + // errors with non-integer exponents + if (mX.GetVelocity() != 0) { + float absVelocity = std::abs(mX.GetVelocity()); + logicalPanDisplacement.x *= + std::pow(absVelocity, + StaticPrefs::apz_touch_acceleration_factor_x()) / + absVelocity; + } + + if (mY.GetVelocity() != 0) { + float absVelocity = std::abs(mY.GetVelocity()); + logicalPanDisplacement.y *= + std::pow(absVelocity, + StaticPrefs::apz_touch_acceleration_factor_y()) / + absVelocity; + } + } + + MOZ_ASSERT(GetCurrentPanGestureBlock()); + AdjustDeltaForAllowedScrollDirections( + logicalPanDisplacement, + GetCurrentPanGestureBlock()->GetAllowedScrollDirections()); + + if (GetAxisLockMode() == DOMINANT_AXIS) { + // Given a pan gesture and both directions have a delta, implement + // dominant axis scrolling and only use the delta for the larger + // axis. + if (logicalPanDisplacement.y != 0 && logicalPanDisplacement.x != 0) { + if (fabs(logicalPanDisplacement.y) >= fabs(logicalPanDisplacement.x)) { + logicalPanDisplacement.x = 0; + physicalPanDisplacement.x = 0; + } else { + logicalPanDisplacement.y = 0; + physicalPanDisplacement.y = 0; + } + } + } + + return {logicalPanDisplacement, physicalPanDisplacement}; +} + +nsEventStatus AsyncPanZoomController::OnPan( + const PanGestureInput& aEvent, FingersOnTouchpad aFingersOnTouchpad) { + APZC_LOG_DETAIL("got a pan-pan in state %s\n", this, + ToString(GetState()).c_str()); + + if (GetState() == SMOOTHMSD_SCROLL) { + if (aFingersOnTouchpad == FingersOnTouchpad::No) { + // When a SMOOTHMSD_SCROLL scroll is being processed on a frame, mouse + // wheel and trackpad momentum scroll position updates will not cancel the + // SMOOTHMSD_SCROLL scroll animations, enabling scripts that depend on + // them to be responsive without forcing the user to wait for the momentum + // scrolling to completely stop. + return nsEventStatus_eConsumeNoDefault; + } + + // SMOOTHMSD_SCROLL scrolls are cancelled by pan gestures. + CancelAnimation(); + } + + if (GetState() == NOTHING) { + // This event block was interrupted by something else. If the user's fingers + // are still on on the touchpad we want to resume scrolling, otherwise we + // ignore the rest of the scroll gesture. + if (aFingersOnTouchpad == FingersOnTouchpad::No) { + return nsEventStatus_eConsumeNoDefault; + } + // Resume / restart the pan. + // PanBegin will call back into this function with mState == PANNING. + return OnPanBegin(aEvent); + } + + auto [logicalPanDisplacement, physicalPanDisplacement] = + GetDisplacementsForPanGesture(aEvent); + + { + // Grab the lock to protect the animation from being canceled on the updater + // thread. + RecursiveMutexAutoLock lock(mRecursiveMutex); + MOZ_ASSERT_IF(GetState() == OVERSCROLL_ANIMATION, mAnimation); + + if (GetState() == OVERSCROLL_ANIMATION && mAnimation && + aFingersOnTouchpad == FingersOnTouchpad::No) { + // If there is an on-going overscroll animation, we tell the animation + // whether the displacements should be handled by the animation or not. + MOZ_ASSERT(mAnimation->AsOverscrollAnimation()); + if (RefPtr overscrollAnimation = + mAnimation->AsOverscrollAnimation()) { + overscrollAnimation->HandlePanMomentum(logicalPanDisplacement); + // And then as a result of the above call, if the animation is currently + // affecting on the axis, drop the displacement value on the axis so + // that we stop further oversrolling on the axis. + if (overscrollAnimation->IsManagingXAxis()) { + logicalPanDisplacement.x = 0; + physicalPanDisplacement.x = 0; + } + if (overscrollAnimation->IsManagingYAxis()) { + logicalPanDisplacement.y = 0; + physicalPanDisplacement.y = 0; + } + } + } + } + + HandlePanningUpdate(physicalPanDisplacement); + + MOZ_ASSERT(GetCurrentPanGestureBlock()); + ScreenPoint panDistance(fabs(physicalPanDisplacement.x), + fabs(physicalPanDisplacement.y)); + OverscrollHandoffState handoffState( + *GetCurrentPanGestureBlock()->GetOverscrollHandoffChain(), panDistance, + ScrollSource::Touchpad); + + // Create fake "touch" positions that will result in the desired scroll + // motion. Note that the pan displacement describes the change in scroll + // position: positive displacement values mean that the scroll position + // increases. However, an increase in scroll position means that the scrolled + // contents are moved to the left / upwards. Since our simulated "touches" + // determine the motion of the scrolled contents, not of the scroll position, + // they need to move in the opposite direction of the pan displacement. + ParentLayerPoint startPoint = aEvent.mLocalPanStartPoint; + ParentLayerPoint endPoint = + aEvent.mLocalPanStartPoint - logicalPanDisplacement; + if (logicalPanDisplacement != ParentLayerPoint()) { + // Don't expect a composite to be triggered if the displacement is zero + RecordScrollPayload(aEvent.mTimeStamp); + } + + const ParentLayerPoint velocity = GetVelocityVector(); + bool consumed = CallDispatchScroll(startPoint, endPoint, handoffState); + + const ParentLayerPoint visualDisplacement = ToParentLayerCoordinates( + handoffState.mTotalMovement, aEvent.mPanStartPoint); + // We need to update the axis velocity in order to get a useful display port + // size and position. We need to do so even if this is a momentum pan (i.e. + // aFingersOnTouchpad == No); in that case the "with touch" part is not + // really appropriate, so we may want to rethink this at some point. + // Note that we have to make all simulated positions relative to + // Axis::GetPos(), because the current position is an invented position, and + // because resetting the position to the mouse position (e.g. + // aEvent.mLocalStartPoint) would mess up velocity calculation. (This is + // the only caller of UpdateWithTouchAtDevicePoint() for pan events, so + // there is no risk of other calls resetting the position.) + // Also note that if there is an on-going overscroll animation in the axis, + // we shouldn't call UpdateWithTouchAtDevicePoint because the call changes + // the velocity which should be managed by the overscroll animation. + // Finally, note that we do this *after* CallDispatchScroll(), so that the + // position we use reflects the actual amount of movement that occurred + // (in particular, if we're in overscroll, if reflects the amount of movement + // *after* applying resistance). This is important because we want the axis + // velocity to track the visual movement speed of the page. + if (visualDisplacement.x != 0) { + mX.UpdateWithTouchAtDevicePoint(mX.GetPos() - visualDisplacement.x, + aEvent.mTimeStamp); + } + if (visualDisplacement.y != 0) { + mY.UpdateWithTouchAtDevicePoint(mY.GetPos() - visualDisplacement.y, + aEvent.mTimeStamp); + } + + if (aFingersOnTouchpad == FingersOnTouchpad::No) { + if (IsOverscrolled() && GetState() != OVERSCROLL_ANIMATION) { + StartOverscrollAnimation(velocity, GetOverscrollSideBits()); + } else if (!consumed) { + // If there is unconsumed scroll and we're in the momentum part of the + // pan gesture, terminate the momentum scroll. This prevents momentum + // scroll events from unexpectedly causing scrolling later if somehow + // the APZC becomes scrollable again in this direction (e.g. if the user + // uses some other input method to scroll in the opposite direction). + SetState(NOTHING); + } + } + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnPanEnd(const PanGestureInput& aEvent) { + APZC_LOG_DETAIL("got a pan-end in state %s\n", this, + ToString(mState).c_str()); + + // This can happen if the OS sends a second pan-end event after the first one + // has already started an overscroll animation or entered a fling state. + // This has been observed on some Wayland versions. + PanZoomState currentState = GetState(); + if (currentState == OVERSCROLL_ANIMATION || currentState == NOTHING || + currentState == FLING) { + return nsEventStatus_eIgnore; + } + + if (aEvent.mPanDisplacement != ScreenPoint{}) { + // Call into OnPan in order to process the delta included in this event. + OnPan(aEvent, FingersOnTouchpad::Yes); + } + + // Do not unlock the axis lock at the end of a pan gesture. The axis lock + // should extend into the momentum scroll. + EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::No); + + // Use HandleEndOfPan for fling on platforms that don't + // emit momentum events (Gtk). + if (aEvent.mSimulateMomentum) { + return HandleEndOfPan(); + } + + MOZ_ASSERT(GetCurrentPanGestureBlock()); + RefPtr overscrollHandoffChain = + GetCurrentPanGestureBlock()->GetOverscrollHandoffChain(); + + // Call SnapBackOverscrolledApzcForMomentum regardless whether this APZC is + // overscrolled or not since overscroll animations for ancestor APZCs in this + // overscroll handoff chain might have been cancelled by the current pan + // gesture block. + overscrollHandoffChain->SnapBackOverscrolledApzcForMomentum( + this, GetVelocityVector()); + // If this APZC is overscrolled, the above SnapBackOverscrolledApzcForMomentum + // triggers an overscroll animation. When we're finished with the overscroll + // animation, the state will be reset and a TransformEnd will be sent to the + // main thread. + currentState = GetState(); + if (currentState != OVERSCROLL_ANIMATION) { + // Do not send a state change notification to the content controller here. + // Instead queue a delayed task to dispatch the notification if no + // momentum pan or scroll snap follows the pan-end. + RefPtr controller = GetGeckoContentController(); + if (controller) { + SetDelayedTransformEnd(true); + controller->PostDelayedTask( + NewRunnableMethod( + "layers::AsyncPanZoomController::" + "DoDelayedTransformEndNotification", + this, &AsyncPanZoomController::DoDelayedTransformEndNotification, + currentState), + StaticPrefs::apz_scrollend_event_content_delay_ms()); + SetStateNoContentControllerDispatch(NOTHING); + } else { + SetState(NOTHING); + } + } + + // Drop any velocity on axes where we don't have room to scroll anyways + // (in this APZC, or an APZC further in the handoff chain). + // This ensures that we don't enlarge the display port unnecessarily. + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + if (!overscrollHandoffChain->CanScrollInDirection( + this, ScrollDirection::eHorizontal)) { + mX.SetVelocity(0); + } + if (!overscrollHandoffChain->CanScrollInDirection( + this, ScrollDirection::eVertical)) { + mY.SetVelocity(0); + } + } + + RequestContentRepaint(); + ScrollSnapToDestination(); + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnPanMomentumStart( + const PanGestureInput& aEvent) { + APZC_LOG_DETAIL("got a pan-momentumstart in state %s\n", this, + ToString(mState).c_str()); + + if (mState == SMOOTHMSD_SCROLL || mState == OVERSCROLL_ANIMATION) { + return nsEventStatus_eConsumeNoDefault; + } + + if (IsDelayedTransformEndSet()) { + // Do not send another TransformBegin notification if we have not + // delivered a corresponding TransformEnd. Also ensure that any + // queued transform-end due to a pan-end is not sent. Instead rely + // on the transform-end sent due to the momentum pan. + SetDelayedTransformEnd(false); + SetStateNoContentControllerDispatch(PAN_MOMENTUM); + } else { + SetState(PAN_MOMENTUM); + } + + // Call into OnPan in order to process any delta included in this event. + OnPan(aEvent, FingersOnTouchpad::No); + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnPanMomentumEnd( + const PanGestureInput& aEvent) { + APZC_LOG_DETAIL("got a pan-momentumend in state %s\n", this, + ToString(mState).c_str()); + + if (mState == OVERSCROLL_ANIMATION) { + return nsEventStatus_eConsumeNoDefault; + } + + // Call into OnPan in order to process any delta included in this event. + OnPan(aEvent, FingersOnTouchpad::No); + + // We need to reset the velocity to zero. We don't really have a "touch" + // here because the touch has already ended long before the momentum + // animation started, but I guess it doesn't really matter for now. + mX.CancelGesture(); + mY.CancelGesture(); + SetState(NOTHING); + + RequestContentRepaint(); + + return nsEventStatus_eConsumeNoDefault; +} + +nsEventStatus AsyncPanZoomController::OnPanInterrupted( + const PanGestureInput& aEvent) { + APZC_LOG_DETAIL("got a pan-interrupted in state %s\n", this, + ToString(mState).c_str()); + + CancelAnimation(); + + return nsEventStatus_eIgnore; +} + +nsEventStatus AsyncPanZoomController::OnLongPress( + const TapGestureInput& aEvent) { + APZC_LOG_DETAIL("got a long-press in state %s\n", this, + ToString(mState).c_str()); + RefPtr controller = GetGeckoContentController(); + if (controller) { + if (Maybe geckoScreenPoint = + ConvertToGecko(aEvent.mPoint)) { + TouchBlockState* touch = GetCurrentTouchBlock(); + if (!touch) { + APZC_LOG( + "%p dropping long-press because some non-touch block interrupted " + "it\n", + this); + return nsEventStatus_eIgnore; + } + if (touch->IsDuringFastFling()) { + APZC_LOG("%p dropping long-press because of fast fling\n", this); + return nsEventStatus_eIgnore; + } + uint64_t blockId = GetInputQueue()->InjectNewTouchBlock(this); + controller->HandleTap(TapType::eLongTap, *geckoScreenPoint, + aEvent.modifiers, GetGuid(), blockId); + return nsEventStatus_eConsumeNoDefault; + } + } + return nsEventStatus_eIgnore; +} + +nsEventStatus AsyncPanZoomController::OnLongPressUp( + const TapGestureInput& aEvent) { + APZC_LOG_DETAIL("got a long-tap-up in state %s\n", this, + ToString(mState).c_str()); + return GenerateSingleTap(TapType::eLongTapUp, aEvent.mPoint, + aEvent.modifiers); +} + +nsEventStatus AsyncPanZoomController::GenerateSingleTap( + TapType aType, const ScreenIntPoint& aPoint, + mozilla::Modifiers aModifiers) { + RefPtr controller = GetGeckoContentController(); + if (controller) { + if (Maybe geckoScreenPoint = ConvertToGecko(aPoint)) { + TouchBlockState* touch = GetCurrentTouchBlock(); + // |touch| may be null in the case where this function is + // invoked by GestureEventListener on a timeout. In that case we already + // verified that the single tap is allowed so we let it through. + // XXX there is a bug here that in such a case the touch block that + // generated this tap will not get its mSingleTapOccurred flag set. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1256344#c6 + if (touch) { + if (touch->IsDuringFastFling()) { + APZC_LOG( + "%p dropping single-tap because it was during a fast-fling\n", + this); + return nsEventStatus_eIgnore; + } + touch->SetSingleTapOccurred(); + } + // Because this may be being running as part of + // APZCTreeManager::ReceiveInputEvent, calling controller->HandleTap + // directly might mean that content receives the single tap message before + // the corresponding touch-up. To avoid that we schedule the singletap + // message to run on the next spin of the event loop. See bug 965381 for + // the issue this was causing. + APZC_LOG("posting runnable for HandleTap from GenerateSingleTap"); + RefPtr runnable = + NewRunnableMethod( + "layers::GeckoContentController::HandleTap", controller, + &GeckoContentController::HandleTap, aType, *geckoScreenPoint, + aModifiers, GetGuid(), touch ? touch->GetBlockId() : 0); + + controller->PostDelayedTask(runnable.forget(), 0); + return nsEventStatus_eConsumeNoDefault; + } + } + return nsEventStatus_eIgnore; +} + +void AsyncPanZoomController::OnTouchEndOrCancel() { + if (RefPtr controller = GetGeckoContentController()) { + MOZ_ASSERT(GetCurrentTouchBlock()); + controller->NotifyAPZStateChange( + GetGuid(), APZStateChange::eEndTouch, + GetCurrentTouchBlock()->SingleTapOccurred(), + Some(GetCurrentTouchBlock()->GetBlockId())); + } +} + +nsEventStatus AsyncPanZoomController::OnSingleTapUp( + const TapGestureInput& aEvent) { + APZC_LOG_DETAIL("got a single-tap-up in state %s\n", this, + ToString(mState).c_str()); + // If mZoomConstraints.mAllowDoubleTapZoom is true we wait for a call to + // OnSingleTapConfirmed before sending event to content + MOZ_ASSERT(GetCurrentTouchBlock()); + if (!(ZoomConstraintsAllowDoubleTapZoom() && + GetCurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) { + return GenerateSingleTap(TapType::eSingleTap, aEvent.mPoint, + aEvent.modifiers); + } + return nsEventStatus_eIgnore; +} + +nsEventStatus AsyncPanZoomController::OnSingleTapConfirmed( + const TapGestureInput& aEvent) { + APZC_LOG_DETAIL("got a single-tap-confirmed in state %s\n", this, + ToString(mState).c_str()); + return GenerateSingleTap(TapType::eSingleTap, aEvent.mPoint, + aEvent.modifiers); +} + +nsEventStatus AsyncPanZoomController::OnDoubleTap( + const TapGestureInput& aEvent) { + APZC_LOG_DETAIL("got a double-tap in state %s\n", this, + ToString(mState).c_str()); + RefPtr controller = GetGeckoContentController(); + if (controller) { + if (ZoomConstraintsAllowDoubleTapZoom() && + (!GetCurrentTouchBlock() || + GetCurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) { + if (Maybe geckoScreenPoint = + ConvertToGecko(aEvent.mPoint)) { + controller->HandleTap( + TapType::eDoubleTap, *geckoScreenPoint, aEvent.modifiers, GetGuid(), + GetCurrentTouchBlock() ? GetCurrentTouchBlock()->GetBlockId() : 0); + } + } + return nsEventStatus_eConsumeNoDefault; + } + return nsEventStatus_eIgnore; +} + +nsEventStatus AsyncPanZoomController::OnSecondTap( + const TapGestureInput& aEvent) { + APZC_LOG_DETAIL("got a second-tap in state %s\n", this, + ToString(mState).c_str()); + return GenerateSingleTap(TapType::eSecondTap, aEvent.mPoint, + aEvent.modifiers); +} + +nsEventStatus AsyncPanZoomController::OnCancelTap( + const TapGestureInput& aEvent) { + APZC_LOG_DETAIL("got a cancel-tap in state %s\n", this, + ToString(mState).c_str()); + // XXX: Implement this. + return nsEventStatus_eIgnore; +} + +ScreenToParentLayerMatrix4x4 AsyncPanZoomController::GetTransformToThis() + const { + if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) { + return treeManagerLocal->GetScreenToApzcTransform(this); + } + return ScreenToParentLayerMatrix4x4(); +} + +ScreenPoint AsyncPanZoomController::ToScreenCoordinates( + const ParentLayerPoint& aVector, const ParentLayerPoint& aAnchor) const { + return TransformVector(GetTransformToThis().Inverse(), aVector, aAnchor); +} + +// TODO: figure out a good way to check the w-coordinate is positive and return +// the result +ParentLayerPoint AsyncPanZoomController::ToParentLayerCoordinates( + const ScreenPoint& aVector, const ScreenPoint& aAnchor) const { + return TransformVector(GetTransformToThis(), aVector, aAnchor); +} + +ParentLayerPoint AsyncPanZoomController::ToParentLayerCoordinates( + const ScreenPoint& aVector, const ExternalPoint& aAnchor) const { + return ToParentLayerCoordinates( + aVector, + ViewAs(aAnchor, PixelCastJustification::ExternalIsScreen)); +} + +ExternalPoint AsyncPanZoomController::ToExternalPoint( + const ExternalPoint& aScreenOffset, const ScreenPoint& aScreenPoint) { + return aScreenOffset + + ViewAs(aScreenPoint, + PixelCastJustification::ExternalIsScreen); +} + +ScreenPoint AsyncPanZoomController::PanVector(const ExternalPoint& aPos) const { + return ScreenPoint(fabs(aPos.x - mStartTouch.x), + fabs(aPos.y - mStartTouch.y)); +} + +bool AsyncPanZoomController::Contains(const ScreenIntPoint& aPoint) const { + ScreenToParentLayerMatrix4x4 transformToThis = GetTransformToThis(); + Maybe point = UntransformBy(transformToThis, aPoint); + if (!point) { + return false; + } + + ParentLayerIntRect cb; + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + GetFrameMetrics().GetCompositionBounds().ToIntRect(&cb); + } + return cb.Contains(*point); +} + +bool AsyncPanZoomController::IsInOverscrollGutter( + const ScreenPoint& aHitTestPoint) const { + if (!IsPhysicallyOverscrolled()) { + return false; + } + + Maybe apzcPoint = + UntransformBy(GetTransformToThis(), aHitTestPoint); + if (!apzcPoint) return false; + return IsInOverscrollGutter(*apzcPoint); +} + +bool AsyncPanZoomController::IsInOverscrollGutter( + const ParentLayerPoint& aHitTestPoint) const { + ParentLayerRect compositionBounds; + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + compositionBounds = GetFrameMetrics().GetCompositionBounds(); + } + if (!compositionBounds.Contains(aHitTestPoint)) { + // Point is outside of scrollable element's bounds altogether. + return false; + } + auto overscrollTransform = GetOverscrollTransform(eForHitTesting); + ParentLayerPoint overscrollUntransformed = + overscrollTransform.Inverse().TransformPoint(aHitTestPoint); + + if (compositionBounds.Contains(overscrollUntransformed)) { + // Point is over scrollable content. + return false; + } + + // Point is in gutter. + return true; +} + +bool AsyncPanZoomController::IsOverscrolled() const { + return mOverscrollEffect->IsOverscrolled(); +} + +bool AsyncPanZoomController::IsPhysicallyOverscrolled() const { + // As an optimization, avoid calling Apply/UnapplyAsyncTestAttributes + // unless we're in a test environment where we need it. + if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + AutoApplyAsyncTestAttributes testAttributeApplier(this, lock); + return mX.IsOverscrolled() || mY.IsOverscrolled(); + } + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mX.IsOverscrolled() || mY.IsOverscrolled(); +} + +bool AsyncPanZoomController::IsInInvalidOverscroll() const { + return mX.IsInInvalidOverscroll() || mY.IsInInvalidOverscroll(); +} + +ParentLayerPoint AsyncPanZoomController::PanStart() const { + return ParentLayerPoint(mX.PanStart(), mY.PanStart()); +} + +const ParentLayerPoint AsyncPanZoomController::GetVelocityVector() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return ParentLayerPoint(mX.GetVelocity(), mY.GetVelocity()); +} + +void AsyncPanZoomController::SetVelocityVector( + const ParentLayerPoint& aVelocityVector) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mX.SetVelocity(aVelocityVector.x); + mY.SetVelocity(aVelocityVector.y); +} + +void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle) { + // Handling of cross sliding will need to be added in this method after + // touch-action released enabled by default. + MOZ_ASSERT(GetCurrentTouchBlock()); + RefPtr overscrollHandoffChain = + GetCurrentInputBlock()->GetOverscrollHandoffChain(); + bool canScrollHorizontal = + !mX.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection( + this, ScrollDirection::eHorizontal); + bool canScrollVertical = + !mY.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection( + this, ScrollDirection::eVertical); + if (GetCurrentTouchBlock()->TouchActionAllowsPanningXY()) { + if (canScrollHorizontal && canScrollVertical) { + if (apz::IsCloseToHorizontal(aAngle, + StaticPrefs::apz_axis_lock_lock_angle())) { + mY.SetAxisLocked(true); + SetState(PANNING_LOCKED_X); + } else if (apz::IsCloseToVertical( + aAngle, StaticPrefs::apz_axis_lock_lock_angle())) { + mX.SetAxisLocked(true); + SetState(PANNING_LOCKED_Y); + } else { + SetState(PANNING); + } + } else if (canScrollHorizontal || canScrollVertical) { + SetState(PANNING); + } else { + SetState(NOTHING); + } + } else if (GetCurrentTouchBlock()->TouchActionAllowsPanningX()) { + // Using bigger angle for panning to keep behavior consistent + // with IE. + if (apz::IsCloseToHorizontal( + aAngle, StaticPrefs::apz_axis_lock_direct_pan_angle())) { + mY.SetAxisLocked(true); + SetState(PANNING_LOCKED_X); + mPanDirRestricted = true; + } else { + // Don't treat these touches as pan/zoom movements since 'touch-action' + // value requires it. + SetState(NOTHING); + } + } else if (GetCurrentTouchBlock()->TouchActionAllowsPanningY()) { + if (apz::IsCloseToVertical(aAngle, + StaticPrefs::apz_axis_lock_direct_pan_angle())) { + mX.SetAxisLocked(true); + SetState(PANNING_LOCKED_Y); + mPanDirRestricted = true; + } else { + SetState(NOTHING); + } + } else { + SetState(NOTHING); + } + if (!IsInPanningState()) { + // If we didn't enter a panning state because touch-action disallowed it, + // make sure to clear any leftover velocity from the pre-threshold + // touchmoves. + mX.SetVelocity(0); + mY.SetVelocity(0); + } +} + +void AsyncPanZoomController::HandlePanning(double aAngle) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + MOZ_ASSERT(GetCurrentInputBlock()); + RefPtr overscrollHandoffChain = + GetCurrentInputBlock()->GetOverscrollHandoffChain(); + bool canScrollHorizontal = + !mX.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection( + this, ScrollDirection::eHorizontal); + bool canScrollVertical = + !mY.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection( + this, ScrollDirection::eVertical); + + MOZ_ASSERT(UsingStatefulAxisLock()); + + if (!canScrollHorizontal || !canScrollVertical) { + SetState(PANNING); + } else if (apz::IsCloseToHorizontal( + aAngle, StaticPrefs::apz_axis_lock_lock_angle())) { + mY.SetAxisLocked(true); + if (canScrollHorizontal) { + SetState(PANNING_LOCKED_X); + } + } else if (apz::IsCloseToVertical(aAngle, + StaticPrefs::apz_axis_lock_lock_angle())) { + mX.SetAxisLocked(true); + if (canScrollVertical) { + SetState(PANNING_LOCKED_Y); + } + } else { + SetState(PANNING); + } +} + +void AsyncPanZoomController::HandlePanningUpdate( + const ScreenPoint& aPanDistance) { + // If we're axis-locked, check if the user is trying to break the lock + if (GetAxisLockMode() == STICKY && !mPanDirRestricted) { + ParentLayerPoint vector = + ToParentLayerCoordinates(aPanDistance, mStartTouch); + + float angle = atan2f(vector.y, vector.x); // range [-pi, pi] + angle = fabsf(angle); // range [0, pi] + + float breakThreshold = + StaticPrefs::apz_axis_lock_breakout_threshold() * GetDPI(); + + if (fabs(aPanDistance.x) > breakThreshold || + fabs(aPanDistance.y) > breakThreshold) { + switch (mState) { + case PANNING_LOCKED_X: + if (!apz::IsCloseToHorizontal( + angle, StaticPrefs::apz_axis_lock_breakout_angle())) { + mY.SetAxisLocked(false); + // If we are within the lock angle from the Y axis, lock + // onto the Y axis. + if (apz::IsCloseToVertical( + angle, StaticPrefs::apz_axis_lock_lock_angle())) { + mX.SetAxisLocked(true); + SetState(PANNING_LOCKED_Y); + } else { + SetState(PANNING); + } + } + break; + + case PANNING_LOCKED_Y: + if (!apz::IsCloseToVertical( + angle, StaticPrefs::apz_axis_lock_breakout_angle())) { + mX.SetAxisLocked(false); + // If we are within the lock angle from the X axis, lock + // onto the X axis. + if (apz::IsCloseToHorizontal( + angle, StaticPrefs::apz_axis_lock_lock_angle())) { + mY.SetAxisLocked(true); + SetState(PANNING_LOCKED_X); + } else { + SetState(PANNING); + } + } + break; + + case PANNING: + HandlePanning(angle); + break; + + default: + break; + } + } + } +} + +void AsyncPanZoomController::HandlePinchLocking( + const PinchGestureInput& aEvent) { + // Focus change and span distance calculated from an event buffer + // Used to handle pinch locking irrespective of touch screen sensitivity + // Note: both values fall back to the same value as + // their un-buffered counterparts if there is only one (the latest) + // event in the buffer. ie: when the touch screen is dispatching + // events slower than the lifetime of the buffer + ParentLayerCoord bufferedSpanDistance; + ParentLayerPoint focusPoint, bufferedFocusChange; + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + focusPoint = mPinchEventBuffer.back().mLocalFocusPoint - + Metrics().GetCompositionBounds().TopLeft(); + ParentLayerPoint bufferedLastZoomFocus = + (mPinchEventBuffer.size() > 1) + ? mPinchEventBuffer.front().mLocalFocusPoint - + Metrics().GetCompositionBounds().TopLeft() + : mLastZoomFocus; + + bufferedFocusChange = bufferedLastZoomFocus - focusPoint; + bufferedSpanDistance = fabsf(mPinchEventBuffer.front().mPreviousSpan - + mPinchEventBuffer.back().mCurrentSpan); + } + + // Convert to screen coordinates + ScreenCoord spanDistance = + ToScreenCoordinates(ParentLayerPoint(0, bufferedSpanDistance), focusPoint) + .Length(); + ScreenPoint focusChange = + ToScreenCoordinates(bufferedFocusChange, focusPoint); + + if (mPinchLocked) { + if (GetPinchLockMode() == PINCH_STICKY) { + ScreenCoord spanBreakoutThreshold = + StaticPrefs::apz_pinch_lock_span_breakout_threshold() * GetDPI(); + mPinchLocked = !(spanDistance > spanBreakoutThreshold); + } + } else { + if (GetPinchLockMode() != PINCH_FREE) { + ScreenCoord spanLockThreshold = + StaticPrefs::apz_pinch_lock_span_lock_threshold() * GetDPI(); + ScreenCoord scrollLockThreshold = + StaticPrefs::apz_pinch_lock_scroll_lock_threshold() * GetDPI(); + + if (spanDistance < spanLockThreshold && + focusChange.Length() > scrollLockThreshold) { + mPinchLocked = true; + + // We are transitioning to a two-finger pan that could trigger + // a fling at its end, so start tracking velocity. + StartTouch(aEvent.mLocalFocusPoint, aEvent.mTimeStamp); + } + } + } +} + +nsEventStatus AsyncPanZoomController::StartPanning( + const ExternalPoint& aStartPoint, const TimeStamp& aEventTime) { + ParentLayerPoint vector = + ToParentLayerCoordinates(PanVector(aStartPoint), mStartTouch); + double angle = atan2(vector.y, vector.x); // range [-pi, pi] + angle = fabs(angle); // range [0, pi] + + RecursiveMutexAutoLock lock(mRecursiveMutex); + HandlePanningWithTouchAction(angle); + + if (IsInPanningState()) { + mTouchStartRestingTimeBeforePan = aEventTime - mTouchStartTime; + mMinimumVelocityDuringPan = Nothing(); + + if (RefPtr controller = + GetGeckoContentController()) { + controller->NotifyAPZStateChange(GetGuid(), + APZStateChange::eStartPanning); + } + return nsEventStatus_eConsumeNoDefault; + } + // Don't consume an event that didn't trigger a panning. + return nsEventStatus_eIgnore; +} + +void AsyncPanZoomController::UpdateWithTouchAtDevicePoint( + const MultiTouchInput& aEvent) { + const SingleTouchData& touchData = aEvent.mTouches[0]; + // Take historical touch data into account in order to improve the accuracy + // of the velocity estimate. On many Android devices, the touch screen samples + // at a higher rate than vsync (e.g. 100Hz vs 60Hz), and the historical data + // lets us take advantage of those high-rate samples. + for (const auto& historicalData : touchData.mHistoricalData) { + ParentLayerPoint historicalPoint = historicalData.mLocalScreenPoint; + mX.UpdateWithTouchAtDevicePoint(historicalPoint.x, + historicalData.mTimeStamp); + mY.UpdateWithTouchAtDevicePoint(historicalPoint.y, + historicalData.mTimeStamp); + } + ParentLayerPoint point = touchData.mLocalScreenPoint; + mX.UpdateWithTouchAtDevicePoint(point.x, aEvent.mTimeStamp); + mY.UpdateWithTouchAtDevicePoint(point.y, aEvent.mTimeStamp); +} + +Maybe AsyncPanZoomController::NotifyScrollSampling() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mSampledState.front().TakeScrollPayload(); +} + +bool AsyncPanZoomController::AttemptScroll( + ParentLayerPoint& aStartPoint, ParentLayerPoint& aEndPoint, + OverscrollHandoffState& aOverscrollHandoffState) { + // "start - end" rather than "end - start" because e.g. moving your finger + // down (*positive* direction along y axis) causes the vertical scroll offset + // to *decrease* as the page follows your finger. + ParentLayerPoint displacement = aStartPoint - aEndPoint; + + ParentLayerPoint overscroll; // will be used outside monitor block + + // If the direction of panning is reversed within the same input block, + // a later event in the block could potentially scroll an APZC earlier + // in the handoff chain, than an earlier event in the block (because + // the earlier APZC was scrolled to its extent in the original direction). + // We want to disallow this. + bool scrollThisApzc = false; + if (InputBlockState* block = GetCurrentInputBlock()) { + scrollThisApzc = + !block->GetScrolledApzc() || block->IsDownchainOfScrolledApzc(this); + } + + ParentLayerPoint adjustedDisplacement; + if (scrollThisApzc) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + bool respectDisregardedDirections = + ScrollSourceRespectsDisregardedDirections( + aOverscrollHandoffState.mScrollSource); + bool forcesVerticalOverscroll = respectDisregardedDirections && + mScrollMetadata.GetDisregardedDirection() == + Some(ScrollDirection::eVertical); + bool forcesHorizontalOverscroll = + respectDisregardedDirections && + mScrollMetadata.GetDisregardedDirection() == + Some(ScrollDirection::eHorizontal); + + bool yChanged = + mY.AdjustDisplacement(displacement.y, adjustedDisplacement.y, + overscroll.y, forcesVerticalOverscroll); + bool xChanged = + mX.AdjustDisplacement(displacement.x, adjustedDisplacement.x, + overscroll.x, forcesHorizontalOverscroll); + if (xChanged || yChanged) { + ScheduleComposite(); + } + + if (!IsZero(adjustedDisplacement) && + Metrics().GetZoom() != CSSToParentLayerScale(0)) { + ScrollBy(adjustedDisplacement / Metrics().GetZoom()); + if (InputBlockState* block = GetCurrentInputBlock()) { + bool displacementIsUserVisible = true; + + { // Release the APZC lock before calling ToScreenCoordinates which + // acquires the APZ tree lock. Note that this just unlocks the mutex + // once, so if we're locking it multiple times on the callstack then + // this will be insufficient. + RecursiveMutexAutoUnlock unlock(mRecursiveMutex); + + ScreenIntPoint screenDisplacement = RoundedToInt( + ToScreenCoordinates(adjustedDisplacement, aStartPoint)); + // If the displacement we just applied rounds to zero in screen space, + // then it's probably not going to be visible to the user. In that + // case let's not mark this APZC as scrolled, so that even if the + // immediate handoff pref is disabled, we'll allow doing the handoff + // to the next APZC. + if (screenDisplacement == ScreenIntPoint()) { + displacementIsUserVisible = false; + } + } + if (displacementIsUserVisible) { + block->SetScrolledApzc(this); + } + } + // Note that in the case of instant scrolling, the last snap target ids + // will be set after AttemptScroll call so that we can clobber them + // unconditionally here. + mLastSnapTargetIds = ScrollSnapTargetIds{}; + ScheduleCompositeAndMaybeRepaint(); + } + + // Adjust the start point to reflect the consumed portion of the scroll. + aStartPoint = aEndPoint + overscroll; + } else { + overscroll = displacement; + } + + // Accumulate the amount of actual scrolling that occurred into the handoff + // state. Note that ToScreenCoordinates() needs to be called outside the + // mutex. + if (!IsZero(adjustedDisplacement)) { + aOverscrollHandoffState.mTotalMovement += + ToScreenCoordinates(adjustedDisplacement, aEndPoint); + } + + // If we consumed the entire displacement as a normal scroll, great. + if (IsZero(overscroll)) { + return true; + } + + if (AllowScrollHandoffInCurrentBlock()) { + // If there is overscroll, first try to hand it off to an APZC later + // in the handoff chain to consume (either as a normal scroll or as + // overscroll). + // Note: "+ overscroll" rather than "- overscroll" because "overscroll" + // is what's left of "displacement", and "displacement" is "start - end". + ++aOverscrollHandoffState.mChainIndex; + bool consumed = + CallDispatchScroll(aStartPoint, aEndPoint, aOverscrollHandoffState); + if (consumed) { + return true; + } + + overscroll = aStartPoint - aEndPoint; + MOZ_ASSERT(!IsZero(overscroll)); + } + + // If there is no APZC later in the handoff chain that accepted the + // overscroll, try to accept it ourselves. We only accept it if we + // are pannable. + if (ScrollSourceAllowsOverscroll(aOverscrollHandoffState.mScrollSource)) { + APZC_LOG("%p taking overscroll during panning\n", this); + + ParentLayerPoint prevVisualOverscroll = GetOverscrollAmount(); + + OverscrollForPanning(overscroll, aOverscrollHandoffState.mPanDistance); + + // Accumulate the amount of change to the overscroll that occurred into the + // handoff state. Note that the input amount, |overscroll|, is turned into + // some smaller visual overscroll amount (queried via GetOverscrollAmount()) + // by applying resistance (Axis::ApplyResistance()), and it's the latter we + // want to count towards OverscrollHandoffState::mTotalMovement. + ParentLayerPoint visualOverscrollChange = + GetOverscrollAmount() - prevVisualOverscroll; + if (!IsZero(visualOverscrollChange)) { + aOverscrollHandoffState.mTotalMovement += + ToScreenCoordinates(visualOverscrollChange, aEndPoint); + } + } + + aStartPoint = aEndPoint + overscroll; + + return IsZero(overscroll); +} + +void AsyncPanZoomController::OverscrollForPanning( + ParentLayerPoint& aOverscroll, const ScreenPoint& aPanDistance) { + // Only allow entering overscroll along an axis if the pan distance along + // that axis is greater than the pan distance along the other axis by a + // configurable factor. If we are already overscrolled, don't check this. + if (!IsOverscrolled()) { + if (aPanDistance.x < + StaticPrefs::apz_overscroll_min_pan_distance_ratio() * aPanDistance.y) { + aOverscroll.x = 0; + } + if (aPanDistance.y < + StaticPrefs::apz_overscroll_min_pan_distance_ratio() * aPanDistance.x) { + aOverscroll.y = 0; + } + } + + OverscrollBy(aOverscroll); +} + +ScrollDirections AsyncPanZoomController::GetOverscrollableDirections() const { + ScrollDirections result; + + RecursiveMutexAutoLock lock(mRecursiveMutex); + + // If the target has the disregarded direction, it means it's single line + // text control, thus we don't want to overscroll in both directions. + if (mScrollMetadata.GetDisregardedDirection()) { + return result; + } + + if (mX.CanScroll() && mX.OverscrollBehaviorAllowsOverscrollEffect()) { + result += ScrollDirection::eHorizontal; + } + + if (mY.CanScroll() && mY.OverscrollBehaviorAllowsOverscrollEffect()) { + result += ScrollDirection::eVertical; + } + + return result; +} + +void AsyncPanZoomController::OverscrollBy(ParentLayerPoint& aOverscroll) { + if (!StaticPrefs::apz_overscroll_enabled()) { + return; + } + + RecursiveMutexAutoLock lock(mRecursiveMutex); + // Do not go into overscroll in a direction in which we have no room to + // scroll to begin with. + ScrollDirections overscrollableDirections = GetOverscrollableDirections(); + if (IsZero(aOverscroll.x)) { + overscrollableDirections -= ScrollDirection::eHorizontal; + } + if (IsZero(aOverscroll.y)) { + overscrollableDirections -= ScrollDirection::eVertical; + } + + mOverscrollEffect->ConsumeOverscroll(aOverscroll, overscrollableDirections); +} + +RefPtr +AsyncPanZoomController::BuildOverscrollHandoffChain() { + if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) { + return treeManagerLocal->BuildOverscrollHandoffChain(this); + } + + // This APZC IsDestroyed(). To avoid callers having to special-case this + // scenario, just build a 1-element chain containing ourselves. + OverscrollHandoffChain* result = new OverscrollHandoffChain; + result->Add(this); + return result; +} + +ParentLayerPoint AsyncPanZoomController::AttemptFling( + const FlingHandoffState& aHandoffState) { + // The PLPPI computation acquires the tree lock, so it needs to be performed + // on the controller thread, and before the APZC lock is acquired. + APZThreadUtils::AssertOnControllerThread(); + float PLPPI = ComputePLPPI(PanStart(), aHandoffState.mVelocity); + + RecursiveMutexAutoLock lock(mRecursiveMutex); + + if (!IsPannable()) { + return aHandoffState.mVelocity; + } + + // We may have a pre-existing velocity for whatever reason (for example, + // a previously handed off fling). We don't want to clobber that. + APZC_LOG("%p accepting fling with velocity %s\n", this, + ToString(aHandoffState.mVelocity).c_str()); + ParentLayerPoint residualVelocity = aHandoffState.mVelocity; + if (mX.CanScroll()) { + mX.SetVelocity(mX.GetVelocity() + aHandoffState.mVelocity.x); + residualVelocity.x = 0; + } + if (mY.CanScroll()) { + mY.SetVelocity(mY.GetVelocity() + aHandoffState.mVelocity.y); + residualVelocity.y = 0; + } + + // If we're not scrollable in at least one of the directions in which we + // were handed velocity, don't start a fling animation. + // The |IsFinite()| condition should only fail when running some tests + // that generate events faster than the clock resolution. + ParentLayerPoint velocity = GetVelocityVector(); + if (!velocity.IsFinite() || + velocity.Length() <= StaticPrefs::apz_fling_min_velocity_threshold()) { + // Relieve overscroll now if needed, since we will not transition to a fling + // animation and then an overscroll animation, and relieve it then. + aHandoffState.mChain->SnapBackOverscrolledApzc(this); + return residualVelocity; + } + + // If there's a scroll snap point near the predicted fling destination, + // scroll there using a smooth scroll animation. Otherwise, start a + // fling animation. + ScrollSnapToDestination(); + if (mState != SMOOTHMSD_SCROLL) { + SetState(FLING); + AsyncPanZoomAnimation* fling = + GetPlatformSpecificState()->CreateFlingAnimation(*this, aHandoffState, + PLPPI); + StartAnimation(fling); + } + + return residualVelocity; +} + +float AsyncPanZoomController::ComputePLPPI(ParentLayerPoint aPoint, + ParentLayerPoint aDirection) const { + // Avoid division-by-zero. + if (aDirection == ParentLayerPoint()) { + return GetDPI(); + } + + // Convert |aDirection| into a unit vector. + aDirection = aDirection / aDirection.Length(); + + // Place the vector at |aPoint| and convert to screen coordinates. + // The length of the resulting vector is the number of Screen coordinates + // that equal 1 ParentLayer coordinate in the given direction. + float screenPerParent = ToScreenCoordinates(aDirection, aPoint).Length(); + + // Finally, factor in the DPI scale. + return GetDPI() / screenPerParent; +} + +Maybe AsyncPanZoomController::GetCurrentAnimationDestination( + const RecursiveMutexAutoLock& aProofOfLock) const { + if (mState == WHEEL_SCROLL) { + return Some(mAnimation->AsWheelScrollAnimation()->GetDestination()); + } + if (mState == SMOOTH_SCROLL) { + return Some(mAnimation->AsSmoothScrollAnimation()->GetDestination()); + } + if (mState == SMOOTHMSD_SCROLL) { + return Some(mAnimation->AsSmoothMsdScrollAnimation()->GetDestination()); + } + if (mState == KEYBOARD_SCROLL) { + return Some(mAnimation->AsSmoothScrollAnimation()->GetDestination()); + } + + return Nothing(); +} + +ParentLayerPoint +AsyncPanZoomController::AdjustHandoffVelocityForOverscrollBehavior( + ParentLayerPoint& aHandoffVelocity) const { + ParentLayerPoint residualVelocity; + ScrollDirections handoffDirections = GetAllowedHandoffDirections(); + if (!handoffDirections.contains(ScrollDirection::eHorizontal)) { + residualVelocity.x = aHandoffVelocity.x; + aHandoffVelocity.x = 0; + } + if (!handoffDirections.contains(ScrollDirection::eVertical)) { + residualVelocity.y = aHandoffVelocity.y; + aHandoffVelocity.y = 0; + } + return residualVelocity; +} + +bool AsyncPanZoomController::OverscrollBehaviorAllowsSwipe() const { + // Swipe navigation is a "non-local" overscroll behavior like handoff. + return GetAllowedHandoffDirections().contains(ScrollDirection::eHorizontal); +} + +void AsyncPanZoomController::HandleFlingOverscroll( + const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits, + const RefPtr& aOverscrollHandoffChain, + const RefPtr& aScrolledApzc) { + APZCTreeManager* treeManagerLocal = GetApzcTreeManager(); + if (treeManagerLocal) { + const FlingHandoffState handoffState{ + aVelocity, aOverscrollHandoffChain, Nothing(), + 0, true /* handoff */, aScrolledApzc}; + ParentLayerPoint residualVelocity = + treeManagerLocal->DispatchFling(this, handoffState); + FLING_LOG("APZC %p left with residual velocity %s\n", this, + ToString(residualVelocity).c_str()); + if (!IsZero(residualVelocity) && IsPannable() && + StaticPrefs::apz_overscroll_enabled()) { + // Obey overscroll-behavior. + RecursiveMutexAutoLock lock(mRecursiveMutex); + if (!mX.OverscrollBehaviorAllowsOverscrollEffect()) { + residualVelocity.x = 0; + } + if (!mY.OverscrollBehaviorAllowsOverscrollEffect()) { + residualVelocity.y = 0; + } + + if (!IsZero(residualVelocity)) { + mOverscrollEffect->RelieveOverscroll(residualVelocity, + aOverscrollSideBits); + } + } + } +} + +void AsyncPanZoomController::HandleSmoothScrollOverscroll( + const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits) { + // We must call BuildOverscrollHandoffChain from this deferred callback + // function in order to avoid a deadlock when acquiring the tree lock. + HandleFlingOverscroll(aVelocity, aOverscrollSideBits, + BuildOverscrollHandoffChain(), nullptr); +} + +void AsyncPanZoomController::SmoothScrollTo(const CSSPoint& aDestination, + const ScrollOrigin& aOrigin) { + // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and then + // to appunits/second. + nsPoint destination = CSSPoint::ToAppUnits(aDestination); + nsSize velocity; + if (Metrics().GetZoom() != CSSToParentLayerScale(0)) { + velocity = CSSSize::ToAppUnits(ParentLayerSize(mX.GetVelocity() * 1000.0f, + mY.GetVelocity() * 1000.0f) / + Metrics().GetZoom()); + } + + if (mState == SMOOTH_SCROLL && mAnimation) { + RefPtr animation( + mAnimation->AsSmoothScrollAnimation()); + if (animation->GetScrollOrigin() == aOrigin) { + APZC_LOG("%p updating destination on existing animation\n", this); + animation->UpdateDestination(GetFrameTime().Time(), destination, + velocity); + return; + } + } + + CancelAnimation(); + SetState(SMOOTH_SCROLL); + nsPoint initialPosition = + CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset()); + RefPtr animation = + new SmoothScrollAnimation(*this, initialPosition, aOrigin); + animation->UpdateDestination(GetFrameTime().Time(), destination, velocity); + StartAnimation(animation.get()); +} + +void AsyncPanZoomController::SmoothMsdScrollTo( + CSSSnapTarget&& aDestination, ScrollTriggeredByScript aTriggeredByScript) { + if (mState == SMOOTHMSD_SCROLL && mAnimation) { + APZC_LOG("%p updating destination on existing animation\n", this); + RefPtr animation( + static_cast(mAnimation.get())); + animation->SetDestination(aDestination.mPosition, + std::move(aDestination.mTargetIds), + aTriggeredByScript); + } else { + CancelAnimation(); + SetState(SMOOTHMSD_SCROLL); + // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s. + CSSPoint initialVelocity; + if (Metrics().GetZoom() != CSSToParentLayerScale(0)) { + initialVelocity = ParentLayerPoint(mX.GetVelocity() * 1000.0f, + mY.GetVelocity() * 1000.0f) / + Metrics().GetZoom(); + } + + StartAnimation(new SmoothMsdScrollAnimation( + *this, Metrics().GetVisualScrollOffset(), initialVelocity, + aDestination.mPosition, + StaticPrefs::layout_css_scroll_behavior_spring_constant(), + StaticPrefs::layout_css_scroll_behavior_damping_ratio(), + std::move(aDestination.mTargetIds), aTriggeredByScript)); + } +} + +void AsyncPanZoomController::StartOverscrollAnimation( + const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits) { + MOZ_ASSERT(mState != OVERSCROLL_ANIMATION); + + SetState(OVERSCROLL_ANIMATION); + + ParentLayerPoint velocity = aVelocity; + AdjustDeltaForAllowedScrollDirections(velocity, + GetOverscrollableDirections()); + StartAnimation(new OverscrollAnimation(*this, velocity, aOverscrollSideBits)); +} + +bool AsyncPanZoomController::CallDispatchScroll( + ParentLayerPoint& aStartPoint, ParentLayerPoint& aEndPoint, + OverscrollHandoffState& aOverscrollHandoffState) { + // Make a local copy of the tree manager pointer and check if it's not + // null before calling DispatchScroll(). This is necessary because + // Destroy(), which nulls out mTreeManager, could be called concurrently. + APZCTreeManager* treeManagerLocal = GetApzcTreeManager(); + if (!treeManagerLocal) { + return false; + } + + // Obey overscroll-behavior. + ParentLayerPoint endPoint = aEndPoint; + if (aOverscrollHandoffState.mChainIndex > 0) { + ScrollDirections handoffDirections = GetAllowedHandoffDirections(); + if (!handoffDirections.contains(ScrollDirection::eHorizontal)) { + endPoint.x = aStartPoint.x; + } + if (!handoffDirections.contains(ScrollDirection::eVertical)) { + endPoint.y = aStartPoint.y; + } + if (aStartPoint == endPoint) { + // Handoff not allowed in either direction - don't even bother. + return false; + } + } + + return treeManagerLocal->DispatchScroll(this, aStartPoint, endPoint, + aOverscrollHandoffState); +} + +void AsyncPanZoomController::RecordScrollPayload(const TimeStamp& aTimeStamp) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + if (!mScrollPayload) { + mScrollPayload = Some( + CompositionPayload{CompositionPayloadType::eAPZScroll, aTimeStamp}); + } +} + +void AsyncPanZoomController::StartTouch(const ParentLayerPoint& aPoint, + TimeStamp aTimestamp) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mX.StartTouch(aPoint.x, aTimestamp); + mY.StartTouch(aPoint.y, aTimestamp); +} + +void AsyncPanZoomController::EndTouch(TimeStamp aTimestamp, + Axis::ClearAxisLock aClearAxisLock) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mX.EndTouch(aTimestamp, aClearAxisLock); + mY.EndTouch(aTimestamp, aClearAxisLock); +} + +void AsyncPanZoomController::TrackTouch(const MultiTouchInput& aEvent) { + ExternalPoint extPoint = GetFirstExternalTouchPoint(aEvent); + ScreenPoint panVector = PanVector(extPoint); + HandlePanningUpdate(panVector); + + ParentLayerPoint prevTouchPoint(mX.GetPos(), mY.GetPos()); + ParentLayerPoint touchPoint = GetFirstTouchPoint(aEvent); + + UpdateWithTouchAtDevicePoint(aEvent); + + auto velocity = GetVelocityVector().Length(); + if (mMinimumVelocityDuringPan) { + mMinimumVelocityDuringPan = + Some(std::min(*mMinimumVelocityDuringPan, velocity)); + } else { + mMinimumVelocityDuringPan = Some(velocity); + } + + if (prevTouchPoint != touchPoint) { + MOZ_ASSERT(GetCurrentTouchBlock()); + OverscrollHandoffState handoffState( + *GetCurrentTouchBlock()->GetOverscrollHandoffChain(), panVector, + ScrollSource::Touchscreen); + RecordScrollPayload(aEvent.mTimeStamp); + CallDispatchScroll(prevTouchPoint, touchPoint, handoffState); + } +} + +ParentLayerPoint AsyncPanZoomController::GetFirstTouchPoint( + const MultiTouchInput& aEvent) { + return ((SingleTouchData&)aEvent.mTouches[0]).mLocalScreenPoint; +} + +ExternalPoint AsyncPanZoomController::GetFirstExternalTouchPoint( + const MultiTouchInput& aEvent) { + return ToExternalPoint(aEvent.mScreenOffset, + ((SingleTouchData&)aEvent.mTouches[0]).mScreenPoint); +} + +ParentLayerPoint AsyncPanZoomController::GetOverscrollAmount() const { + if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + AutoApplyAsyncTestAttributes testAttributeApplier(this, lock); + return GetOverscrollAmountInternal(); + } + RecursiveMutexAutoLock lock(mRecursiveMutex); + return GetOverscrollAmountInternal(); +} + +ParentLayerPoint AsyncPanZoomController::GetOverscrollAmountInternal() const { + return {mX.GetOverscroll(), mY.GetOverscroll()}; +} + +SideBits AsyncPanZoomController::GetOverscrollSideBits() const { + return apz::GetOverscrollSideBits({mX.GetOverscroll(), mY.GetOverscroll()}); +} + +void AsyncPanZoomController::RestoreOverscrollAmount( + const ParentLayerPoint& aOverscroll) { + mX.RestoreOverscroll(aOverscroll.x); + mY.RestoreOverscroll(aOverscroll.y); +} + +void AsyncPanZoomController::StartAnimation(AsyncPanZoomAnimation* aAnimation) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mAnimation = aAnimation; + mLastSampleTime = GetFrameTime(); + ScheduleComposite(); +} + +void AsyncPanZoomController::CancelAnimation(CancelAnimationFlags aFlags) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + APZC_LOG_DETAIL("running CancelAnimation(0x%x) in state %s\n", this, aFlags, + ToString(mState).c_str()); + + if ((aFlags & ExcludeWheel) && mState == WHEEL_SCROLL) { + return; + } + + if (mAnimation) { + mAnimation->Cancel(aFlags); + } + + SetState(NOTHING); + mLastSnapTargetIds = ScrollSnapTargetIds{}; + mAnimation = nullptr; + // Since there is no animation in progress now the axes should + // have no velocity either. If we are dropping the velocity from a non-zero + // value we should trigger a repaint as the displayport margins are dependent + // on the velocity and the last repaint request might not have good margins + // any more. + bool repaint = !IsZero(GetVelocityVector()); + mX.SetVelocity(0); + mY.SetVelocity(0); + mX.SetAxisLocked(false); + mY.SetAxisLocked(false); + // Setting the state to nothing and cancelling the animation can + // preempt normal mechanisms for relieving overscroll, so we need to clear + // overscroll here. + if (!(aFlags & ExcludeOverscroll) && IsOverscrolled()) { + ClearOverscroll(); + repaint = true; + } + // Similar to relieving overscroll, we also need to snap to any snap points + // if appropriate. + if (aFlags & CancelAnimationFlags::ScrollSnap) { + ScrollSnap(ScrollSnapFlags::IntendedEndPosition); + } + if (repaint) { + RequestContentRepaint(); + ScheduleComposite(); + } +} + +void AsyncPanZoomController::ClearOverscroll() { + mOverscrollEffect->ClearOverscroll(); +} + +void AsyncPanZoomController::ClearPhysicalOverscroll() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mX.ClearOverscroll(); + mY.ClearOverscroll(); +} + +void AsyncPanZoomController::SetCompositorController( + CompositorController* aCompositorController) { + mCompositorController = aCompositorController; +} + +void AsyncPanZoomController::SetVisualScrollOffset(const CSSPoint& aOffset) { + Metrics().SetVisualScrollOffset(aOffset); + Metrics().RecalculateLayoutViewportOffset(); +} + +void AsyncPanZoomController::ClampAndSetVisualScrollOffset( + const CSSPoint& aOffset) { + Metrics().ClampAndSetVisualScrollOffset(aOffset); + Metrics().RecalculateLayoutViewportOffset(); +} + +void AsyncPanZoomController::ScrollBy(const CSSPoint& aOffset) { + SetVisualScrollOffset(Metrics().GetVisualScrollOffset() + aOffset); +} + +void AsyncPanZoomController::ScrollByAndClamp(const CSSPoint& aOffset) { + ClampAndSetVisualScrollOffset(Metrics().GetVisualScrollOffset() + aOffset); +} + +void AsyncPanZoomController::ScaleWithFocus(float aScale, + const CSSPoint& aFocus) { + Metrics().ZoomBy(aScale); + // We want to adjust the scroll offset such that the CSS point represented by + // aFocus remains at the same position on the screen before and after the + // change in zoom. The below code accomplishes this; see + // https://bugzilla.mozilla.org/show_bug.cgi?id=923431#c6 for an in-depth + // explanation of how. + SetVisualScrollOffset((Metrics().GetVisualScrollOffset() + aFocus) - + (aFocus / aScale)); +} + +/*static*/ +gfx::IntSize AsyncPanZoomController::GetDisplayportAlignmentMultiplier( + const ScreenSize& aBaseSize) { + gfx::IntSize multiplier(1, 1); + float baseWidth = aBaseSize.width; + while (baseWidth > 500) { + baseWidth /= 2; + multiplier.width *= 2; + if (multiplier.width >= 8) { + break; + } + } + float baseHeight = aBaseSize.height; + while (baseHeight > 500) { + baseHeight /= 2; + multiplier.height *= 2; + if (multiplier.height >= 8) { + break; + } + } + return multiplier; +} + +/** + * Enlarges the displayport along both axes based on the velocity. + */ +static CSSSize CalculateDisplayPortSize( + const CSSSize& aCompositionSize, const CSSPoint& aVelocity, + AsyncPanZoomController::ZoomInProgress aZoomInProgress, + const CSSToScreenScale2D& aDpPerCSS) { + bool xIsStationarySpeed = + fabsf(aVelocity.x) < StaticPrefs::apz_min_skate_speed(); + bool yIsStationarySpeed = + fabsf(aVelocity.y) < StaticPrefs::apz_min_skate_speed(); + float xMultiplier = xIsStationarySpeed + ? StaticPrefs::apz_x_stationary_size_multiplier() + : StaticPrefs::apz_x_skate_size_multiplier(); + float yMultiplier = yIsStationarySpeed + ? StaticPrefs::apz_y_stationary_size_multiplier() + : StaticPrefs::apz_y_skate_size_multiplier(); + + if (IsHighMemSystem() && !xIsStationarySpeed) { + xMultiplier += StaticPrefs::apz_x_skate_highmem_adjust(); + } + + if (IsHighMemSystem() && !yIsStationarySpeed) { + yMultiplier += StaticPrefs::apz_y_skate_highmem_adjust(); + } + + if (aZoomInProgress == AsyncPanZoomController::ZoomInProgress::Yes) { + // If a zoom is in progress, we will be making content visible on the + // x and y axes in equal proportion, because the zoom operation scales + // equally on the x and y axes. The default multipliers computed above are + // biased towards the y-axis since that's where most scrolling occurs, but + // in the case of zooming, we should really use equal multipliers on both + // axes. This does that while preserving the total displayport area + // quantity (aCompositionSize.Area() * xMultiplier * yMultiplier). + // Note that normally changing the shape of the displayport is expensive + // and should be avoided, but if a zoom is in progress the displayport + // is likely going to be fully repainted anyway due to changes in resolution + // so there should be no marginal cost to also changing the shape of it. + float areaMultiplier = xMultiplier * yMultiplier; + xMultiplier = sqrt(areaMultiplier); + yMultiplier = xMultiplier; + } + + // Scale down the margin multipliers by the alignment multiplier because + // the alignment code will expand the displayport outward to the multiplied + // alignment. This is not necessary for correctness, but for performance; + // if we don't do this the displayport can end up much larger. The math here + // is actually just scaling the part of the multipler that is > 1, so that + // we never end up with xMultiplier or yMultiplier being less than 1 (that + // would result in a guaranteed checkerboarding situation). Note that the + // calculation doesn't cancel exactly the increased margin from applying + // the alignment multiplier, but this is simple and should provide + // reasonable behaviour in most cases. + gfx::IntSize alignmentMultipler = + AsyncPanZoomController::GetDisplayportAlignmentMultiplier( + aCompositionSize * aDpPerCSS); + if (xMultiplier > 1) { + xMultiplier = ((xMultiplier - 1) / alignmentMultipler.width) + 1; + } + if (yMultiplier > 1) { + yMultiplier = ((yMultiplier - 1) / alignmentMultipler.height) + 1; + } + + return aCompositionSize * CSSSize(xMultiplier, yMultiplier); +} + +/** + * Ensures that the displayport is at least as large as the visible area + * inflated by the danger zone. If this is not the case then the + * "AboutToCheckerboard" function in TiledContentClient.cpp will return true + * even in the stable state. + */ +static CSSSize ExpandDisplayPortToDangerZone( + const CSSSize& aDisplayPortSize, const FrameMetrics& aFrameMetrics) { + CSSSize dangerZone(0.0f, 0.0f); + if (aFrameMetrics.DisplayportPixelsPerCSSPixel().xScale != 0 && + aFrameMetrics.DisplayportPixelsPerCSSPixel().yScale != 0) { + dangerZone = ScreenSize(StaticPrefs::apz_danger_zone_x(), + StaticPrefs::apz_danger_zone_y()) / + aFrameMetrics.DisplayportPixelsPerCSSPixel(); + } + const CSSSize compositionSize = + aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels(); + + const float xSize = std::max(aDisplayPortSize.width, + compositionSize.width + (2 * dangerZone.width)); + + const float ySize = + std::max(aDisplayPortSize.height, + compositionSize.height + (2 * dangerZone.height)); + + return CSSSize(xSize, ySize); +} + +/** + * Attempts to redistribute any area in the displayport that would get clipped + * by the scrollable rect, or be inaccessible due to disabled scrolling, to the + * other axis, while maintaining total displayport area. + */ +static void RedistributeDisplayPortExcess(CSSSize& aDisplayPortSize, + const CSSRect& aScrollableRect) { + // As aDisplayPortSize.height * aDisplayPortSize.width does not change, + // we are just scaling by the ratio and its inverse. + if (aDisplayPortSize.height > aScrollableRect.Height()) { + aDisplayPortSize.width *= + (aDisplayPortSize.height / aScrollableRect.Height()); + aDisplayPortSize.height = aScrollableRect.Height(); + } else if (aDisplayPortSize.width > aScrollableRect.Width()) { + aDisplayPortSize.height *= + (aDisplayPortSize.width / aScrollableRect.Width()); + aDisplayPortSize.width = aScrollableRect.Width(); + } +} + +/* static */ +const ScreenMargin AsyncPanZoomController::CalculatePendingDisplayPort( + const FrameMetrics& aFrameMetrics, const ParentLayerPoint& aVelocity, + ZoomInProgress aZoomInProgress) { + if (aFrameMetrics.IsScrollInfoLayer()) { + // Don't compute margins. Since we can't asynchronously scroll this frame, + // we don't want to paint anything more than the composition bounds. + return ScreenMargin(); + } + + CSSSize compositionSize = + aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels(); + CSSPoint velocity; + if (aFrameMetrics.GetZoom() != CSSToParentLayerScale(0)) { + velocity = aVelocity / aFrameMetrics.GetZoom(); // avoid division by zero + } + CSSRect scrollableRect = aFrameMetrics.GetExpandedScrollableRect(); + + // Calculate the displayport size based on how fast we're moving along each + // axis. + CSSSize displayPortSize = + CalculateDisplayPortSize(compositionSize, velocity, aZoomInProgress, + aFrameMetrics.DisplayportPixelsPerCSSPixel()); + + displayPortSize = + ExpandDisplayPortToDangerZone(displayPortSize, aFrameMetrics); + + if (StaticPrefs::apz_enlarge_displayport_when_clipped()) { + RedistributeDisplayPortExcess(displayPortSize, scrollableRect); + } + + // We calculate a "displayport" here which is relative to the scroll offset. + // Note that the scroll offset we have here in the APZ code may not be the + // same as the base rect that gets used on the layout side when the + // displayport margins are actually applied, so it is important to only + // consider the displayport as margins relative to a scroll offset rather than + // relative to something more unchanging like the scrollable rect origin. + + // Center the displayport based on its expansion over the composition size. + CSSRect displayPort((compositionSize.width - displayPortSize.width) / 2.0f, + (compositionSize.height - displayPortSize.height) / 2.0f, + displayPortSize.width, displayPortSize.height); + + // Offset the displayport, depending on how fast we're moving and the + // estimated time it takes to paint, to try to minimise checkerboarding. + float paintFactor = kDefaultEstimatedPaintDurationMs; + displayPort.MoveBy(velocity * paintFactor * StaticPrefs::apz_velocity_bias()); + + APZC_LOGV_FM(aFrameMetrics, + "Calculated displayport as %s from velocity %s zooming %d paint " + "time %f metrics", + ToString(displayPort).c_str(), ToString(aVelocity).c_str(), + (int)aZoomInProgress, paintFactor); + + CSSMargin cssMargins; + cssMargins.left = -displayPort.X(); + cssMargins.top = -displayPort.Y(); + cssMargins.right = + displayPort.Width() - compositionSize.width - cssMargins.left; + cssMargins.bottom = + displayPort.Height() - compositionSize.height - cssMargins.top; + + return cssMargins * aFrameMetrics.DisplayportPixelsPerCSSPixel(); +} + +void AsyncPanZoomController::ScheduleComposite() { + if (mCompositorController) { + mCompositorController->ScheduleRenderOnCompositorThread( + wr::RenderReasons::APZ); + } +} + +void AsyncPanZoomController::ScheduleCompositeAndMaybeRepaint() { + ScheduleComposite(); + RequestContentRepaint(); +} + +void AsyncPanZoomController::FlushRepaintForOverscrollHandoff() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + RequestContentRepaint(); +} + +void AsyncPanZoomController::FlushRepaintForNewInputBlock() { + APZC_LOG("%p flushing repaint for new input block\n", this); + + RecursiveMutexAutoLock lock(mRecursiveMutex); + RequestContentRepaint(); +} + +bool AsyncPanZoomController::SnapBackIfOverscrolled() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + if (SnapBackIfOverscrolledForMomentum(ParentLayerPoint(0, 0))) { + return true; + } + // If we don't kick off an overscroll animation, we still need to snap to any + // nearby snap points, assuming we haven't already done so when we started + // this fling + if (mState != FLING) { + ScrollSnap(ScrollSnapFlags::IntendedEndPosition); + } + return false; +} + +bool AsyncPanZoomController::SnapBackIfOverscrolledForMomentum( + const ParentLayerPoint& aVelocity) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + // It's possible that we're already in the middle of an overscroll + // animation - if so, don't start a new one. + if (IsOverscrolled() && mState != OVERSCROLL_ANIMATION) { + APZC_LOG("%p is overscrolled, starting snap-back\n", this); + mOverscrollEffect->RelieveOverscroll(aVelocity, GetOverscrollSideBits()); + return true; + } + return false; +} + +bool AsyncPanZoomController::IsFlingingFast() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + if (mState == FLING && GetVelocityVector().Length() > + StaticPrefs::apz_fling_stop_on_tap_threshold()) { + APZC_LOG("%p is moving fast\n", this); + return true; + } + return false; +} + +bool AsyncPanZoomController::IsPannable() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mX.CanScroll() || mY.CanScroll(); +} + +bool AsyncPanZoomController::IsScrollInfoLayer() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return Metrics().IsScrollInfoLayer(); +} + +int32_t AsyncPanZoomController::GetLastTouchIdentifier() const { + RefPtr listener = GetGestureEventListener(); + return listener ? listener->GetLastTouchIdentifier() : -1; +} + +void AsyncPanZoomController::RequestContentRepaint( + RepaintUpdateType aUpdateType) { + // Reinvoke this method on the repaint thread if it's not there already. It's + // important to do this before the call to CalculatePendingDisplayPort, so + // that CalculatePendingDisplayPort uses the most recent available version of + // Metrics(). just before the paint request is dispatched to content. + RefPtr controller = GetGeckoContentController(); + if (!controller) { + return; + } + if (!controller->IsRepaintThread()) { + // Even though we want to do the actual repaint request on the repaint + // thread, we want to update the expected gecko metrics synchronously. + // Otherwise we introduce a race condition where we might read from the + // expected gecko metrics on the controller thread before or after it gets + // updated on the repaint thread, when in fact we always want the updated + // version when reading. + { // scope lock + RecursiveMutexAutoLock lock(mRecursiveMutex); + mExpectedGeckoMetrics.UpdateFrom(Metrics()); + } + + // use the local variable to resolve the function overload. + auto func = + static_cast( + &AsyncPanZoomController::RequestContentRepaint); + controller->DispatchToRepaintThread(NewRunnableMethod( + "layers::AsyncPanZoomController::RequestContentRepaint", this, func, + aUpdateType)); + return; + } + + MOZ_ASSERT(controller->IsRepaintThread()); + + RecursiveMutexAutoLock lock(mRecursiveMutex); + ParentLayerPoint velocity = GetVelocityVector(); + ScreenMargin displayportMargins = CalculatePendingDisplayPort( + Metrics(), velocity, + (mState == PINCHING || mState == ANIMATING_ZOOM) ? ZoomInProgress::Yes + : ZoomInProgress::No); + Metrics().SetPaintRequestTime(TimeStamp::Now()); + RequestContentRepaint(velocity, displayportMargins, aUpdateType); +} + +static CSSRect GetDisplayPortRect(const FrameMetrics& aFrameMetrics, + const ScreenMargin& aDisplayportMargins) { + // This computation is based on what happens in CalculatePendingDisplayPort. + // If that changes then this might need to change too. + // Note that the display port rect APZ computes is relative to the visual + // scroll offset. It's adjusted to be relative to the layout scroll offset + // when the main thread processes a repaint request (in + // APZCCallbackHelper::AdjustDisplayPortForScrollDelta()) and ultimately + // applied (in DisplayPortUtils::GetDisplayPort()) in this adjusted form. + CSSRect baseRect(aFrameMetrics.GetVisualScrollOffset(), + aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels()); + baseRect.Inflate(aDisplayportMargins / + aFrameMetrics.DisplayportPixelsPerCSSPixel()); + return baseRect; +} + +void AsyncPanZoomController::RequestContentRepaint( + const ParentLayerPoint& aVelocity, const ScreenMargin& aDisplayportMargins, + RepaintUpdateType aUpdateType) { + mRecursiveMutex.AssertCurrentThreadIn(); + + RefPtr controller = GetGeckoContentController(); + if (!controller) { + return; + } + MOZ_ASSERT(controller->IsRepaintThread()); + + APZScrollAnimationType animationType = APZScrollAnimationType::No; + if (mAnimation) { + animationType = mAnimation->WasTriggeredByScript() + ? APZScrollAnimationType::TriggeredByScript + : APZScrollAnimationType::TriggeredByUserInput; + } + RepaintRequest request(Metrics(), aDisplayportMargins, aUpdateType, + animationType, mScrollGeneration, mLastSnapTargetIds, + IsInScrollingGesture()); + + if (request.IsRootContent() && request.GetZoom() != mLastNotifiedZoom && + mState != PINCHING && mState != ANIMATING_ZOOM) { + controller->NotifyScaleGestureComplete( + GetGuid(), + (request.GetZoom() / request.GetDevPixelsPerCSSPixel()).scale); + mLastNotifiedZoom = request.GetZoom(); + } + + // If we're trying to paint what we already think is painted, discard this + // request since it's a pointless paint. + if (request.GetDisplayPortMargins().WithinEpsilonOf( + mLastPaintRequestMetrics.GetDisplayPortMargins(), EPSILON) && + request.GetVisualScrollOffset().WithinEpsilonOf( + mLastPaintRequestMetrics.GetVisualScrollOffset(), EPSILON) && + request.GetPresShellResolution() == + mLastPaintRequestMetrics.GetPresShellResolution() && + request.GetZoom() == mLastPaintRequestMetrics.GetZoom() && + request.GetLayoutViewport().WithinEpsilonOf( + mLastPaintRequestMetrics.GetLayoutViewport(), EPSILON) && + request.GetScrollGeneration() == + mLastPaintRequestMetrics.GetScrollGeneration() && + request.GetScrollUpdateType() == + mLastPaintRequestMetrics.GetScrollUpdateType() && + request.GetScrollAnimationType() == + mLastPaintRequestMetrics.GetScrollAnimationType() && + request.GetLastSnapTargetIds() == + mLastPaintRequestMetrics.GetLastSnapTargetIds()) { + return; + } + + APZC_LOGV("%p requesting content repaint %s", this, + ToString(request).c_str()); + { // scope lock + MutexAutoLock lock(mCheckerboardEventLock); + if (mCheckerboardEvent && mCheckerboardEvent->IsRecordingTrace()) { + std::stringstream info; + info << " velocity " << aVelocity; + std::string str = info.str(); + mCheckerboardEvent->UpdateRendertraceProperty( + CheckerboardEvent::RequestedDisplayPort, + GetDisplayPortRect(Metrics(), aDisplayportMargins), str); + } + } + + controller->RequestContentRepaint(request); + mExpectedGeckoMetrics.UpdateFrom(Metrics()); + mLastPaintRequestMetrics = request; + + // We're holding the APZC lock here, so redispatch this so we can get + // the tree lock without the APZC lock. + controller->DispatchToRepaintThread( + NewRunnableMethod( + "layers::APZCTreeManager::SendSubtreeTransformsToChromeMainThread", + GetApzcTreeManager(), + &APZCTreeManager::SendSubtreeTransformsToChromeMainThread, this)); +} + +bool AsyncPanZoomController::UpdateAnimation( + const RecursiveMutexAutoLock& aProofOfLock, const SampleTime& aSampleTime, + nsTArray>* aOutDeferredTasks) { + AssertOnSamplerThread(); + + // This function may get called multiple with the same sample time, if we + // composite multiple times at the same timestamp. + // However we only want to do one animation step per composition so we need + // to deduplicate these calls first. + if (mLastSampleTime == aSampleTime) { + return !!mAnimation; + } + + // We're at a new timestamp, so advance to the next sample in the deque, if + // there is one. That one will be used for all the code that reads the + // eForCompositing transforms in this vsync interval. + AdvanceToNextSample(); + + // And then create a new sample, which will be used in the *next* vsync + // interval. We do the sample at this point and not later in order to try + // and enforce one frame delay between computing the async transform and + // compositing it to the screen. This one-frame delay gives code running on + // the main thread a chance to try and respond to the scroll position change, + // so that e.g. a main-thread animation can stay in sync with user-driven + // scrolling or a compositor animation. + bool needComposite = SampleCompositedAsyncTransform(aProofOfLock); + + TimeDuration sampleTimeDelta = aSampleTime - mLastSampleTime; + mLastSampleTime = aSampleTime; + + if (needComposite || mAnimation) { + // Bump the scroll generation before we call RequestContentRepaint below + // so that the RequestContentRepaint call will surely use the new + // generation. + if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) { + mScrollGeneration = treeManagerLocal->NewAPZScrollGeneration(); + } + } + + if (mAnimation) { + bool continueAnimation = mAnimation->Sample(Metrics(), sampleTimeDelta); + bool wantsRepaints = mAnimation->WantsRepaints(); + *aOutDeferredTasks = mAnimation->TakeDeferredTasks(); + if (!continueAnimation) { + SetState(NOTHING); + if (mAnimation->AsSmoothMsdScrollAnimation()) { + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mLastSnapTargetIds = + mAnimation->AsSmoothMsdScrollAnimation()->TakeSnapTargetIds(); + } + } + mAnimation = nullptr; + } + // Request a repaint at the end of the animation in case something such as a + // call to NotifyLayersUpdated was invoked during the animation and Gecko's + // current state is some intermediate point of the animation. + if (!continueAnimation || wantsRepaints) { + RequestContentRepaint(); + } + needComposite = true; + } + return needComposite; +} + +AsyncTransformComponentMatrix AsyncPanZoomController::GetOverscrollTransform( + AsyncTransformConsumer aMode) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + AutoApplyAsyncTestAttributes testAttributeApplier(this, lock); + + if (aMode == eForCompositing && mScrollMetadata.IsApzForceDisabled()) { + return AsyncTransformComponentMatrix(); + } + + if (!IsPhysicallyOverscrolled()) { + return AsyncTransformComponentMatrix(); + } + + // The overscroll effect is a simple translation by the overscroll offset. + ParentLayerPoint overscrollOffset(-mX.GetOverscroll(), -mY.GetOverscroll()); + return AsyncTransformComponentMatrix().PostTranslate(overscrollOffset.x, + overscrollOffset.y, 0); +} + +bool AsyncPanZoomController::AdvanceAnimations(const SampleTime& aSampleTime) { + AssertOnSamplerThread(); + + // Don't send any state-change notifications until the end of the function, + // because we may go through some intermediate states while we finish + // animations and start new ones. + StateChangeNotificationBlocker blocker(this); + + // The eventual return value of this function. The compositor needs to know + // whether or not to advance by a frame as soon as it can. For example, if a + // fling is happening, it has to keep compositing so that the animation is + // smooth. If an animation frame is requested, it is the compositor's + // responsibility to schedule a composite. + bool requestAnimationFrame = false; + nsTArray> deferredTasks; + + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + { // scope lock + CSSRect visibleRect = GetVisibleRect(lock); + MutexAutoLock lock2(mCheckerboardEventLock); + // Update RendertraceProperty before UpdateAnimation() call, since + // the UpdateAnimation() updates effective ScrollOffset for next frame + // if APZFrameDelay is enabled. + if (mCheckerboardEvent) { + mCheckerboardEvent->UpdateRendertraceProperty( + CheckerboardEvent::UserVisible, visibleRect); + } + } + + requestAnimationFrame = UpdateAnimation(lock, aSampleTime, &deferredTasks); + } + // Execute any deferred tasks queued up by mAnimation's Sample() (called by + // UpdateAnimation()). This needs to be done after the monitor is released + // since the tasks are allowed to call APZCTreeManager methods which can grab + // the tree lock. + for (uint32_t i = 0; i < deferredTasks.Length(); ++i) { + APZThreadUtils::RunOnControllerThread(std::move(deferredTasks[i])); + } + + // If any of the deferred tasks starts a new animation, it will request a + // new composite directly, so we can just return requestAnimationFrame here. + return requestAnimationFrame; +} + +ParentLayerPoint AsyncPanZoomController::GetCurrentAsyncScrollOffset( + AsyncTransformConsumer aMode) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + AutoApplyAsyncTestAttributes testAttributeApplier(this, lock); + + return GetEffectiveScrollOffset(aMode, lock) * GetEffectiveZoom(aMode, lock); +} + +CSSRect AsyncPanZoomController::GetCurrentAsyncVisualViewport( + AsyncTransformConsumer aMode) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + AutoApplyAsyncTestAttributes testAttributeApplier(this, lock); + + return CSSRect( + GetEffectiveScrollOffset(aMode, lock), + FrameMetrics::CalculateCompositedSizeInCssPixels( + Metrics().GetCompositionBounds(), GetEffectiveZoom(aMode, lock))); +} + +AsyncTransform AsyncPanZoomController::GetCurrentAsyncTransform( + AsyncTransformConsumer aMode, AsyncTransformComponents aComponents, + std::size_t aSampleIndex) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + AutoApplyAsyncTestAttributes testAttributeApplier(this, lock); + + CSSToParentLayerScale effectiveZoom; + if (aComponents.contains(AsyncTransformComponent::eVisual)) { + effectiveZoom = GetEffectiveZoom(aMode, lock, aSampleIndex); + } else { + effectiveZoom = + Metrics().LayersPixelsPerCSSPixel() * LayerToParentLayerScale(1.0f); + } + + LayerToParentLayerScale compositedAsyncZoom = + effectiveZoom / Metrics().LayersPixelsPerCSSPixel(); + + ParentLayerPoint translation; + if (aComponents.contains(AsyncTransformComponent::eVisual)) { + // There is no "lastPaintVisualOffset" to subtract here; the visual offset + // is entirely async. + + CSSPoint currentVisualOffset = + GetEffectiveScrollOffset(aMode, lock, aSampleIndex) - + GetEffectiveLayoutViewport(aMode, lock, aSampleIndex).TopLeft(); + + translation += currentVisualOffset * effectiveZoom; + } + if (aComponents.contains(AsyncTransformComponent::eLayout)) { + CSSPoint lastPaintLayoutOffset; + if (mLastContentPaintMetrics.IsScrollable()) { + lastPaintLayoutOffset = mLastContentPaintMetrics.GetLayoutScrollOffset(); + } + + CSSPoint currentLayoutOffset = + GetEffectiveLayoutViewport(aMode, lock, aSampleIndex).TopLeft(); + + translation += + (currentLayoutOffset - lastPaintLayoutOffset) * effectiveZoom; + } + + return AsyncTransform(compositedAsyncZoom, -translation); +} + +AsyncTransformComponentMatrix +AsyncPanZoomController::GetCurrentAsyncTransformWithOverscroll( + AsyncTransformConsumer aMode, AsyncTransformComponents aComponents, + std::size_t aSampleIndex) const { + AsyncTransformComponentMatrix asyncTransform = + GetCurrentAsyncTransform(aMode, aComponents, aSampleIndex); + // The overscroll transform is considered part of the layout component of + // the async transform, because it should not apply to fixed content. + if (aComponents.contains(AsyncTransformComponent::eLayout)) { + return asyncTransform * GetOverscrollTransform(aMode); + } + return asyncTransform; +} + +LayoutDeviceToParentLayerScale AsyncPanZoomController::GetCurrentPinchZoomScale( + AsyncTransformConsumer aMode) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + AutoApplyAsyncTestAttributes testAttributeApplier(this, lock); + CSSToParentLayerScale scale = GetEffectiveZoom(aMode, lock); + return scale / Metrics().GetDevPixelsPerCSSPixel(); +} + +AutoTArray +AsyncPanZoomController::GetSampledScrollOffsets() const { + AssertOnSamplerThread(); + + RecursiveMutexAutoLock lock(mRecursiveMutex); + + const AsyncTransformComponents asyncTransformComponents = + GetZoomAnimationId() + ? AsyncTransformComponents{AsyncTransformComponent::eLayout} + : LayoutAndVisual; + + // If layerTranslation includes only the layout component of the async + // transform then it has not been scaled by the async zoom, so we want to + // divide it by the resolution. If layerTranslation includes the visual + // component, then we should use the pinch zoom scale, which includes the + // async zoom. However, we only use LayoutAndVisual for non-zoomable APZCs, + // so it makes no difference. + LayoutDeviceToParentLayerScale resolution = + GetCumulativeResolution() * LayerToParentLayerScale(1.0f); + + AutoTArray sampledOffsets; + + for (std::deque::size_type index = 0; + index < mSampledState.size(); index++) { + ParentLayerPoint layerTranslation = + GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing, + asyncTransformComponents, index) + .mTranslation; + + // Include the overscroll transform here in scroll offsets transform + // to ensure that we do not overscroll fixed content. + layerTranslation = + GetOverscrollTransform(AsyncPanZoomController::eForCompositing) + .TransformPoint(layerTranslation); + // The positive translation means the painted content is supposed to + // move down (or to the right), and that corresponds to a reduction in + // the scroll offset. Since we are effectively giving WR the async + // scroll delta here, we want to negate the translation. + LayoutDevicePoint asyncScrollDelta = -layerTranslation / resolution; + sampledOffsets.AppendElement(wr::SampledScrollOffset{ + wr::ToLayoutVector2D(asyncScrollDelta), + wr::ToWrAPZScrollGeneration(mSampledState[index].Generation())}); + } + + return sampledOffsets; +} + +bool AsyncPanZoomController::SuppressAsyncScrollOffset() const { + return mScrollMetadata.IsApzForceDisabled() || + (Metrics().IsMinimalDisplayPort() && + StaticPrefs::apz_prefer_jank_minimal_displayports()); +} + +CSSRect AsyncPanZoomController::GetEffectiveLayoutViewport( + AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock, + std::size_t aSampleIndex) const { + if (aMode == eForCompositing && SuppressAsyncScrollOffset()) { + return mLastContentPaintMetrics.GetLayoutViewport(); + } + if (aMode == eForCompositing) { + return mSampledState[aSampleIndex].GetLayoutViewport(); + } + return Metrics().GetLayoutViewport(); +} + +CSSPoint AsyncPanZoomController::GetEffectiveScrollOffset( + AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock, + std::size_t aSampleIndex) const { + if (aMode == eForCompositing && SuppressAsyncScrollOffset()) { + return mLastContentPaintMetrics.GetVisualScrollOffset(); + } + if (aMode == eForCompositing) { + return mSampledState[aSampleIndex].GetVisualScrollOffset(); + } + return Metrics().GetVisualScrollOffset(); +} + +CSSToParentLayerScale AsyncPanZoomController::GetEffectiveZoom( + AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock, + std::size_t aSampleIndex) const { + if (aMode == eForCompositing && SuppressAsyncScrollOffset()) { + return mLastContentPaintMetrics.GetZoom(); + } + if (aMode == eForCompositing) { + return mSampledState[aSampleIndex].GetZoom(); + } + return Metrics().GetZoom(); +} + +void AsyncPanZoomController::AdvanceToNextSample() { + AssertOnSamplerThread(); + RecursiveMutexAutoLock lock(mRecursiveMutex); + // Always keep at least one state in mSampledState. + if (mSampledState.size() > 1) { + mSampledState.pop_front(); + } +} + +bool AsyncPanZoomController::SampleCompositedAsyncTransform( + const RecursiveMutexAutoLock& aProofOfLock) { + MOZ_ASSERT(mSampledState.size() <= 2); + bool sampleChanged = (mSampledState.back() != SampledAPZCState(Metrics())); + mSampledState.emplace_back(Metrics(), std::move(mScrollPayload), + mScrollGeneration); + return sampleChanged; +} + +void AsyncPanZoomController::ResampleCompositedAsyncTransform( + const RecursiveMutexAutoLock& aProofOfLock) { + // This only gets called during testing situations, so the fact that this + // drops the scroll payload from mSampledState.front() is not really a + // problem. + if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) { + mScrollGeneration = treeManagerLocal->NewAPZScrollGeneration(); + } + mSampledState.front() = SampledAPZCState(Metrics(), {}, mScrollGeneration); +} + +void AsyncPanZoomController::ApplyAsyncTestAttributes( + const RecursiveMutexAutoLock& aProofOfLock) { + if (mTestAttributeAppliers == 0) { + if (mTestAsyncScrollOffset != CSSPoint() || + mTestAsyncZoom != LayerToParentLayerScale()) { + // TODO Currently we update Metrics() and resample, which will cause + // the very latest user input to get immediately captured in the sample, + // and may defeat our attempt at "frame delay" (i.e. delaying the user + // input from affecting composition by one frame). + // Instead, maybe we should just apply the mTest* stuff directly to + // mSampledState.front(). We can even save/restore that SampledAPZCState + // instance in the AutoApplyAsyncTestAttributes instead of Metrics(). + Metrics().ZoomBy(mTestAsyncZoom.scale); + CSSPoint asyncScrollPosition = Metrics().GetVisualScrollOffset(); + CSSPoint requestedPoint = + asyncScrollPosition + this->mTestAsyncScrollOffset; + CSSPoint clampedPoint = + Metrics().CalculateScrollRange().ClampPoint(requestedPoint); + CSSPoint difference = mTestAsyncScrollOffset - clampedPoint; + + ScrollByAndClamp(mTestAsyncScrollOffset); + + if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) { + ParentLayerPoint overscroll = difference * Metrics().GetZoom(); + OverscrollBy(overscroll); + } + ResampleCompositedAsyncTransform(aProofOfLock); + } + } + ++mTestAttributeAppliers; +} + +void AsyncPanZoomController::UnapplyAsyncTestAttributes( + const RecursiveMutexAutoLock& aProofOfLock, + const FrameMetrics& aPrevFrameMetrics, + const ParentLayerPoint& aPrevOverscroll) { + MOZ_ASSERT(mTestAttributeAppliers >= 1); + --mTestAttributeAppliers; + if (mTestAttributeAppliers == 0) { + if (mTestAsyncScrollOffset != CSSPoint() || + mTestAsyncZoom != LayerToParentLayerScale()) { + Metrics() = aPrevFrameMetrics; + RestoreOverscrollAmount(aPrevOverscroll); + ResampleCompositedAsyncTransform(aProofOfLock); + } + } +} + +Matrix4x4 AsyncPanZoomController::GetTransformToLastDispatchedPaint( + const AsyncTransformComponents& aComponents) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + CSSPoint componentOffset; + + // The computation of the componentOffset should roughly be the negation + // of the translation in GetCurrentAsyncTransform() with the expected + // gecko metrics substituted for the effective scroll offsets. + if (aComponents.contains(AsyncTransformComponent::eVisual)) { + componentOffset += mExpectedGeckoMetrics.GetLayoutScrollOffset() - + mExpectedGeckoMetrics.GetVisualScrollOffset(); + } + + if (aComponents.contains(AsyncTransformComponent::eLayout)) { + CSSPoint lastPaintLayoutOffset; + + if (mLastContentPaintMetrics.IsScrollable()) { + lastPaintLayoutOffset = mLastContentPaintMetrics.GetLayoutScrollOffset(); + } + + componentOffset += + lastPaintLayoutOffset - mExpectedGeckoMetrics.GetLayoutScrollOffset(); + } + + LayerPoint scrollChange = componentOffset * + mLastContentPaintMetrics.GetDevPixelsPerCSSPixel() * + mLastContentPaintMetrics.GetCumulativeResolution(); + + // We're interested in the async zoom change. Factor out the content scale + // that may change when dragging the window to a monitor with a different + // content scale. + LayoutDeviceToParentLayerScale lastContentZoom = + mLastContentPaintMetrics.GetZoom() / + mLastContentPaintMetrics.GetDevPixelsPerCSSPixel(); + LayoutDeviceToParentLayerScale lastDispatchedZoom = + mExpectedGeckoMetrics.GetZoom() / + mExpectedGeckoMetrics.GetDevPixelsPerCSSPixel(); + float zoomChange = 1.0; + if (aComponents.contains(AsyncTransformComponent::eVisual) && + lastDispatchedZoom != LayoutDeviceToParentLayerScale(0)) { + zoomChange = lastContentZoom.scale / lastDispatchedZoom.scale; + } + return Matrix4x4::Translation(scrollChange.x, scrollChange.y, 0) + .PostScale(zoomChange, zoomChange, 1); +} + +CSSRect AsyncPanZoomController::GetVisibleRect( + const RecursiveMutexAutoLock& aProofOfLock) const { + AutoApplyAsyncTestAttributes testAttributeApplier(this, aProofOfLock); + CSSPoint currentScrollOffset = GetEffectiveScrollOffset( + AsyncPanZoomController::eForCompositing, aProofOfLock); + CSSRect visible = CSSRect(currentScrollOffset, + Metrics().CalculateCompositedSizeInCssPixels()); + return visible; +} + +static CSSRect GetPaintedRect(const FrameMetrics& aFrameMetrics) { + CSSRect displayPort = aFrameMetrics.GetDisplayPort(); + if (displayPort.IsEmpty()) { + // Fallback to use the viewport if the diplayport hasn't been set. + // This situation often happens non-scrollable iframe's root scroller in + // Fission. + return aFrameMetrics.GetVisualViewport(); + } + + return displayPort + aFrameMetrics.GetLayoutScrollOffset(); +} + +uint32_t AsyncPanZoomController::GetCheckerboardMagnitude( + const ParentLayerRect& aClippedCompositionBounds) const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + CSSRect painted = GetPaintedRect(mLastContentPaintMetrics); + painted.Inflate(CSSMargin::FromAppUnits( + nsMargin(1, 1, 1, 1))); // fuzz for rounding error + + CSSRect visible = GetVisibleRect(lock); // relative to scrolled frame origin + if (visible.IsEmpty() || painted.Contains(visible)) { + // early-exit if we're definitely not checkerboarding + return 0; + } + + // aClippedCompositionBounds and Metrics().GetCompositionBounds() are both + // relative to the layer tree origin. + // The "*RelativeToItself*" variables are relative to the comp bounds origin + ParentLayerRect visiblePartOfCompBoundsRelativeToItself = + aClippedCompositionBounds - Metrics().GetCompositionBounds().TopLeft(); + CSSRect visiblePartOfCompBoundsRelativeToItselfInCssSpace; + if (Metrics().GetZoom() != CSSToParentLayerScale(0)) { + visiblePartOfCompBoundsRelativeToItselfInCssSpace = + (visiblePartOfCompBoundsRelativeToItself / Metrics().GetZoom()); + } + + // This one is relative to the scrolled frame origin, same as `visible` + CSSRect visiblePartOfCompBoundsInCssSpace = + visiblePartOfCompBoundsRelativeToItselfInCssSpace + visible.TopLeft(); + + visible = visible.Intersect(visiblePartOfCompBoundsInCssSpace); + + CSSIntRegion checkerboard; + // Round so as to minimize checkerboarding; if we're only showing fractional + // pixels of checkerboarding it's not really worth counting + checkerboard.Sub(RoundedIn(visible), RoundedOut(painted)); + uint32_t area = checkerboard.Area(); + if (area) { + APZC_LOG_FM(Metrics(), + "%p is currently checkerboarding (painted %s visible %s)", this, + ToString(painted).c_str(), ToString(visible).c_str()); + } + return area; +} + +void AsyncPanZoomController::ReportCheckerboard( + const SampleTime& aSampleTime, + const ParentLayerRect& aClippedCompositionBounds) { + if (mLastCheckerboardReport == aSampleTime) { + // This function will get called multiple times for each APZC on a single + // composite (once for each layer it is attached to). Only report the + // checkerboard once per composite though. + return; + } + mLastCheckerboardReport = aSampleTime; + + bool recordTrace = StaticPrefs::apz_record_checkerboarding(); + bool forTelemetry = Telemetry::CanRecordBase(); + uint32_t magnitude = GetCheckerboardMagnitude(aClippedCompositionBounds); + + // IsInTransformingState() acquires the APZC lock and thus needs to + // be called before acquiring mCheckerboardEventLock. + bool inTransformingState = IsInTransformingState(); + + MutexAutoLock lock(mCheckerboardEventLock); + if (!mCheckerboardEvent && (recordTrace || forTelemetry)) { + mCheckerboardEvent = MakeUnique(recordTrace); + } + mPotentialCheckerboardTracker.InTransform(inTransformingState, + recordTrace || forTelemetry); + if (magnitude) { + mPotentialCheckerboardTracker.CheckerboardSeen(); + } + UpdateCheckerboardEvent(lock, magnitude); +} + +void AsyncPanZoomController::UpdateCheckerboardEvent( + const MutexAutoLock& aProofOfLock, uint32_t aMagnitude) { + if (mCheckerboardEvent && mCheckerboardEvent->RecordFrameInfo(aMagnitude)) { + // This checkerboard event is done. Report some metrics to telemetry. + mozilla::Telemetry::Accumulate(mozilla::Telemetry::CHECKERBOARD_SEVERITY, + mCheckerboardEvent->GetSeverity()); + mozilla::Telemetry::Accumulate(mozilla::Telemetry::CHECKERBOARD_PEAK, + mCheckerboardEvent->GetPeak()); + mozilla::Telemetry::Accumulate( + mozilla::Telemetry::CHECKERBOARD_DURATION, + (uint32_t)mCheckerboardEvent->GetDuration().ToMilliseconds()); + + // mCheckerboardEvent only gets created if we are supposed to record + // telemetry so we always pass true for aRecordTelemetry. + mPotentialCheckerboardTracker.CheckerboardDone( + /* aRecordTelemetry = */ true); + + if (StaticPrefs::apz_record_checkerboarding()) { + // if the pref is enabled, also send it to the storage class. it may be + // chosen for public display on about:checkerboard, the hall of fame for + // checkerboard events. + uint32_t severity = mCheckerboardEvent->GetSeverity(); + std::string log = mCheckerboardEvent->GetLog(); + CheckerboardEventStorage::Report(severity, log); + } + mCheckerboardEvent = nullptr; + } +} + +void AsyncPanZoomController::FlushActiveCheckerboardReport() { + MutexAutoLock lock(mCheckerboardEventLock); + // Pretend like we got a frame with 0 pixels checkerboarded. This will + // terminate the checkerboard event and flush it out + UpdateCheckerboardEvent(lock, 0); +} + +void AsyncPanZoomController::NotifyLayersUpdated( + const ScrollMetadata& aScrollMetadata, bool aIsFirstPaint, + bool aThisLayerTreeUpdated) { + AssertOnUpdaterThread(); + + RecursiveMutexAutoLock lock(mRecursiveMutex); + bool isDefault = mScrollMetadata.IsDefault(); + + const FrameMetrics& aLayerMetrics = aScrollMetadata.GetMetrics(); + + if ((aScrollMetadata == mLastContentPaintMetadata) && !isDefault) { + // No new information here, skip it. + APZC_LOGV("%p NotifyLayersUpdated short-circuit\n", this); + return; + } + + // If the Metrics scroll offset is different from the last scroll offset + // that the main-thread sent us, then we know that the user has been doing + // something that triggers a scroll. This check is the APZ equivalent of the + // check on the main-thread at + // https://hg.mozilla.org/mozilla-central/file/97a52326b06a/layout/generic/nsGfxScrollFrame.cpp#l4050 + // There is code below (the use site of userScrolled) that prevents a + // restored- scroll-position update from overwriting a user scroll, again + // equivalent to how the main thread code does the same thing. + // XXX Suspicious comparison between layout and visual scroll offsets. + // This may not do the right thing when we're zoomed in. + CSSPoint lastScrollOffset = mLastContentPaintMetrics.GetLayoutScrollOffset(); + bool userScrolled = !FuzzyEqualsAdditive(Metrics().GetVisualScrollOffset().x, + lastScrollOffset.x) || + !FuzzyEqualsAdditive(Metrics().GetVisualScrollOffset().y, + lastScrollOffset.y); + + if (aScrollMetadata.DidContentGetPainted()) { + mLastContentPaintMetadata = aScrollMetadata; + } + + mScrollMetadata.SetScrollParentId(aScrollMetadata.GetScrollParentId()); + APZC_LOGV_FM(aLayerMetrics, + "%p got a NotifyLayersUpdated with aIsFirstPaint=%d, " + "aThisLayerTreeUpdated=%d", + this, aIsFirstPaint, aThisLayerTreeUpdated); + + { // scope lock + MutexAutoLock lock(mCheckerboardEventLock); + if (mCheckerboardEvent && mCheckerboardEvent->IsRecordingTrace()) { + std::string str; + if (aThisLayerTreeUpdated) { + if (!aLayerMetrics.GetPaintRequestTime().IsNull()) { + // Note that we might get the paint request time as non-null, but with + // aThisLayerTreeUpdated false. That can happen if we get a layer + // transaction from a different process right after we get the layer + // transaction with aThisLayerTreeUpdated == true. In this case we + // want to ignore the paint request time because it was already dumped + // in the previous layer transaction. + TimeDuration paintTime = + TimeStamp::Now() - aLayerMetrics.GetPaintRequestTime(); + std::stringstream info; + info << " painttime " << paintTime.ToMilliseconds(); + str = info.str(); + } else { + // This might be indicative of a wasted paint particularly if it + // happens during a checkerboard event. + str = " (this layertree updated)"; + } + } + mCheckerboardEvent->UpdateRendertraceProperty( + CheckerboardEvent::Page, aLayerMetrics.GetScrollableRect()); + mCheckerboardEvent->UpdateRendertraceProperty( + CheckerboardEvent::PaintedDisplayPort, GetPaintedRect(aLayerMetrics), + str); + } + } + + // The main thread may send us a visual scroll offset update. This is + // different from a layout viewport offset update in that the layout viewport + // offset is limited to the layout scroll range, while the visual viewport + // offset is not. + // However, there are some conditions in which the layout update will clobber + // the visual update, and we want to ignore the visual update in those cases. + // This variable tracks that. + bool ignoreVisualUpdate = false; + + // TODO if we're in a drag and scrollOffsetUpdated is set then we want to + // ignore it + + bool needContentRepaint = false; + RepaintUpdateType contentRepaintType = RepaintUpdateType::eNone; + bool viewportSizeUpdated = false; + bool needToReclampScroll = false; + + if ((aIsFirstPaint && aThisLayerTreeUpdated) || isDefault || + Metrics().IsRootContent() != aLayerMetrics.IsRootContent()) { + if (Metrics().IsRootContent() && !aLayerMetrics.IsRootContent()) { + // We only support zooming on root content APZCs + SetZoomAnimationId(Nothing()); + } + + // Initialize our internal state to something sane when the content + // that was just painted is something we knew nothing about previously + CancelAnimation(); + + // Keep our existing scroll generation, if there are scroll updates. In this + // case we'll update our scroll generation when processing the scroll update + // array below. If there are no scroll updates, take the generation from the + // incoming metrics. Bug 1662019 will simplify this later. + ScrollGeneration oldScrollGeneration = Metrics().GetScrollGeneration(); + mScrollMetadata = aScrollMetadata; + if (!aScrollMetadata.GetScrollUpdates().IsEmpty()) { + Metrics().SetScrollGeneration(oldScrollGeneration); + } + + mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics); + + for (auto& sampledState : mSampledState) { + sampledState.UpdateScrollProperties(Metrics()); + sampledState.UpdateZoomProperties(Metrics()); + } + + if (aLayerMetrics.HasNonZeroDisplayPortMargins()) { + // A non-zero display port margin here indicates a displayport has + // been set by a previous APZC for the content at this guid. The + // scrollable rect may have changed since then, making the margins + // wrong, so we need to calculate a new display port. + // It is important that we request a repaint here only when we need to + // otherwise we will end up setting a display port on every frame that + // gets a view id. + APZC_LOG("%p detected non-empty margins which probably need updating\n", + this); + needContentRepaint = true; + } + } else { + // If we're not taking the aLayerMetrics wholesale we still need to pull + // in some things into our local Metrics() because these things are + // determined by Gecko and our copy in Metrics() may be stale. + + if (Metrics().GetLayoutViewport().Size() != + aLayerMetrics.GetLayoutViewport().Size()) { + CSSRect layoutViewport = Metrics().GetLayoutViewport(); + // The offset will be updated if necessary via + // RecalculateLayoutViewportOffset(). + layoutViewport.SizeTo(aLayerMetrics.GetLayoutViewport().Size()); + Metrics().SetLayoutViewport(layoutViewport); + + needContentRepaint = true; + viewportSizeUpdated = true; + } + + // TODO: Rely entirely on |aScrollMetadata.IsResolutionUpdated()| to + // determine which branch to take, and drop the other conditions. + CSSToParentLayerScale oldZoom = Metrics().GetZoom(); + if (FuzzyEqualsAdditive( + Metrics().GetCompositionBoundsWidthIgnoringScrollbars(), + aLayerMetrics.GetCompositionBoundsWidthIgnoringScrollbars()) && + Metrics().GetDevPixelsPerCSSPixel() == + aLayerMetrics.GetDevPixelsPerCSSPixel() && + !viewportSizeUpdated && !aScrollMetadata.IsResolutionUpdated()) { + // Any change to the pres shell resolution was requested by APZ and is + // already included in our zoom; however, other components of the + // cumulative resolution (a parent document's pres-shell resolution, or + // the css-driven resolution) may have changed, and we need to update + // our zoom to reflect that. Note that we can't just take + // aLayerMetrics.mZoom because the APZ may have additional async zoom + // since the repaint request. + float totalResolutionChange = 1.0; + + if (Metrics().GetCumulativeResolution() != LayoutDeviceToLayerScale(0)) { + totalResolutionChange = aLayerMetrics.GetCumulativeResolution().scale / + Metrics().GetCumulativeResolution().scale; + } + + float presShellResolutionChange = aLayerMetrics.GetPresShellResolution() / + Metrics().GetPresShellResolution(); + if (presShellResolutionChange != 1.0f) { + needContentRepaint = true; + } + Metrics().ZoomBy(totalResolutionChange / presShellResolutionChange); + for (auto& sampledState : mSampledState) { + sampledState.ZoomBy(totalResolutionChange / presShellResolutionChange); + } + } else { + // Take the new zoom as either device scale or composition width or + // viewport size got changed (e.g. due to orientation change, or content + // changing the meta-viewport tag), or the main thread originated a + // resolution change for another reason (e.g. Ctrl+0 was pressed to + // reset the zoom). + Metrics().SetZoom(aLayerMetrics.GetZoom()); + for (auto& sampledState : mSampledState) { + sampledState.UpdateZoomProperties(aLayerMetrics); + } + Metrics().SetDevPixelsPerCSSPixel( + aLayerMetrics.GetDevPixelsPerCSSPixel()); + } + + if (Metrics().GetZoom() != oldZoom) { + // If the zoom changed, the scroll range in CSS pixels may have changed + // even if the composition bounds didn't. + needToReclampScroll = true; + } + + mExpectedGeckoMetrics.UpdateZoomFrom(aLayerMetrics); + + if (!Metrics().GetScrollableRect().IsEqualEdges( + aLayerMetrics.GetScrollableRect())) { + Metrics().SetScrollableRect(aLayerMetrics.GetScrollableRect()); + needContentRepaint = true; + needToReclampScroll = true; + } + if (!Metrics().GetCompositionBounds().IsEqualEdges( + aLayerMetrics.GetCompositionBounds())) { + Metrics().SetCompositionBounds(aLayerMetrics.GetCompositionBounds()); + needToReclampScroll = true; + } + Metrics().SetCompositionBoundsWidthIgnoringScrollbars( + aLayerMetrics.GetCompositionBoundsWidthIgnoringScrollbars()); + + if (Metrics().IsRootContent() && + Metrics().GetCompositionSizeWithoutDynamicToolbar() != + aLayerMetrics.GetCompositionSizeWithoutDynamicToolbar()) { + Metrics().SetCompositionSizeWithoutDynamicToolbar( + aLayerMetrics.GetCompositionSizeWithoutDynamicToolbar()); + needToReclampScroll = true; + } + Metrics().SetBoundingCompositionSize( + aLayerMetrics.GetBoundingCompositionSize()); + Metrics().SetPresShellResolution(aLayerMetrics.GetPresShellResolution()); + Metrics().SetCumulativeResolution(aLayerMetrics.GetCumulativeResolution()); + Metrics().SetTransformToAncestorScale( + aLayerMetrics.GetTransformToAncestorScale()); + mScrollMetadata.SetHasScrollgrab(aScrollMetadata.GetHasScrollgrab()); + mScrollMetadata.SetLineScrollAmount(aScrollMetadata.GetLineScrollAmount()); + mScrollMetadata.SetPageScrollAmount(aScrollMetadata.GetPageScrollAmount()); + mScrollMetadata.SetSnapInfo(ScrollSnapInfo(aScrollMetadata.GetSnapInfo())); + mScrollMetadata.SetIsLayersIdRoot(aScrollMetadata.IsLayersIdRoot()); + mScrollMetadata.SetIsAutoDirRootContentRTL( + aScrollMetadata.IsAutoDirRootContentRTL()); + Metrics().SetIsScrollInfoLayer(aLayerMetrics.IsScrollInfoLayer()); + Metrics().SetHasNonZeroDisplayPortMargins( + aLayerMetrics.HasNonZeroDisplayPortMargins()); + Metrics().SetMinimalDisplayPort(aLayerMetrics.IsMinimalDisplayPort()); + mScrollMetadata.SetForceDisableApz(aScrollMetadata.IsApzForceDisabled()); + mScrollMetadata.SetIsRDMTouchSimulationActive( + aScrollMetadata.GetIsRDMTouchSimulationActive()); + mScrollMetadata.SetForceMousewheelAutodir( + aScrollMetadata.ForceMousewheelAutodir()); + mScrollMetadata.SetForceMousewheelAutodirHonourRoot( + aScrollMetadata.ForceMousewheelAutodirHonourRoot()); + mScrollMetadata.SetIsPaginatedPresentation( + aScrollMetadata.IsPaginatedPresentation()); + mScrollMetadata.SetDisregardedDirection( + aScrollMetadata.GetDisregardedDirection()); + mScrollMetadata.SetOverscrollBehavior( + aScrollMetadata.GetOverscrollBehavior()); + } + + bool scrollOffsetUpdated = false; + bool smoothScrollRequested = false; + bool didCancelAnimation = false; + Maybe cumulativeRelativeDelta; + for (const auto& scrollUpdate : aScrollMetadata.GetScrollUpdates()) { + APZC_LOG("%p processing scroll update %s\n", this, + ToString(scrollUpdate).c_str()); + if (!(Metrics().GetScrollGeneration() < scrollUpdate.GetGeneration())) { + // This is stale, let's ignore it + APZC_LOG("%p scrollupdate generation stale, dropping\n", this); + continue; + } + Metrics().SetScrollGeneration(scrollUpdate.GetGeneration()); + + MOZ_ASSERT(scrollUpdate.GetOrigin() != ScrollOrigin::Apz); + if (userScrolled && + !nsLayoutUtils::CanScrollOriginClobberApz(scrollUpdate.GetOrigin())) { + APZC_LOG("%p scrollupdate cannot clobber APZ userScrolled\n", this); + continue; + } + // XXX: if we get here, |scrollUpdate| is clobbering APZ, so we may want + // to reset |userScrolled| back to false so that subsequent scrollUpdates + // in this loop don't get dropped by the check above. Need to add a test + // that exercises this scenario, as we don't currently have one. + + if (scrollUpdate.GetMode() == ScrollMode::Smooth || + scrollUpdate.GetMode() == ScrollMode::SmoothMsd) { + smoothScrollRequested = true; + + // Requests to animate the visual scroll position override requests to + // simply update the visual scroll offset to a particular point. Since + // we have an animation request, we set ignoreVisualUpdate to true to + // indicate we don't need to apply the visual scroll update in + // aLayerMetrics. + ignoreVisualUpdate = true; + + // For relative updates we want to add the relative offset to any existing + // destination, or the current visual offset if there is no existing + // destination. + CSSPoint base = GetCurrentAnimationDestination(lock).valueOr( + Metrics().GetVisualScrollOffset()); + + CSSPoint destination; + if (scrollUpdate.GetType() == ScrollUpdateType::Relative) { + CSSPoint delta = + scrollUpdate.GetDestination() - scrollUpdate.GetSource(); + APZC_LOG("%p relative smooth scrolling from %s by %s\n", this, + ToString(base).c_str(), ToString(delta).c_str()); + destination = Metrics().CalculateScrollRange().ClampPoint(base + delta); + } else if (scrollUpdate.GetType() == ScrollUpdateType::PureRelative) { + CSSPoint delta = scrollUpdate.GetDelta(); + APZC_LOG("%p pure-relative smooth scrolling from %s by %s\n", this, + ToString(base).c_str(), ToString(delta).c_str()); + destination = Metrics().CalculateScrollRange().ClampPoint(base + delta); + } else { + APZC_LOG("%p smooth scrolling to %s\n", this, + ToString(scrollUpdate.GetDestination()).c_str()); + destination = scrollUpdate.GetDestination(); + } + + if (scrollUpdate.GetMode() == ScrollMode::SmoothMsd) { + SmoothMsdScrollTo( + CSSSnapTarget{destination, scrollUpdate.GetSnapTargetIds()}, + scrollUpdate.GetScrollTriggeredByScript()); + } else { + MOZ_ASSERT(scrollUpdate.GetMode() == ScrollMode::Smooth); + MOZ_ASSERT(!scrollUpdate.WasTriggeredByScript()); + SmoothScrollTo(destination, scrollUpdate.GetOrigin()); + } + continue; + } + + MOZ_ASSERT(scrollUpdate.GetMode() == ScrollMode::Instant || + scrollUpdate.GetMode() == ScrollMode::Normal); + + // If the layout update is of a higher priority than the visual update, then + // we don't want to apply the visual update. + // If the layout update is of a clobbering type (or a smooth scroll request, + // which is handled above) then it takes precedence over an eRestore visual + // update. But we also allow the possibility for the main thread to ask us + // to scroll both the layout and visual viewports to distinct (but + // compatible) locations (via e.g. both updates being of a non-clobbering/ + // eRestore type). + if (nsLayoutUtils::CanScrollOriginClobberApz(scrollUpdate.GetOrigin()) && + aLayerMetrics.GetVisualScrollUpdateType() != + FrameMetrics::eMainThread) { + ignoreVisualUpdate = true; + } + + Maybe relativeDelta; + if (scrollUpdate.GetType() == ScrollUpdateType::Relative) { + APZC_LOG( + "%p relative updating scroll offset from %s by %s\n", this, + ToString(Metrics().GetVisualScrollOffset()).c_str(), + ToString(scrollUpdate.GetDestination() - scrollUpdate.GetSource()) + .c_str()); + + scrollOffsetUpdated = true; + + // It's possible that the main thread has ignored an APZ scroll offset + // update for the pending relative scroll that we have just received. + // When this happens, we need to send a new scroll offset update with + // the combined scroll offset or else the main thread may have an + // incorrect scroll offset for a period of time. + if (Metrics().HasPendingScroll(aLayerMetrics)) { + needContentRepaint = true; + contentRepaintType = RepaintUpdateType::eUserAction; + } + + relativeDelta = + Some(Metrics().ApplyRelativeScrollUpdateFrom(scrollUpdate)); + Metrics().RecalculateLayoutViewportOffset(); + } else if (scrollUpdate.GetType() == ScrollUpdateType::PureRelative) { + APZC_LOG("%p pure-relative updating scroll offset from %s by %s\n", this, + ToString(Metrics().GetVisualScrollOffset()).c_str(), + ToString(scrollUpdate.GetDelta()).c_str()); + + scrollOffsetUpdated = true; + + // Always need a repaint request with a repaint type for pure relative + // scrolls because apz is doing the scroll at the main thread's request. + // The main thread has not updated it's scroll offset yet, it is depending + // on apz to tell it where to scroll. + needContentRepaint = true; + contentRepaintType = RepaintUpdateType::eVisualUpdate; + + // We have to ignore a visual scroll offset update otherwise it will + // clobber the relative scrolling we are about to do. We perform + // visualScrollOffset = visualScrollOffset + delta. Then the + // visualScrollOffsetUpdated block below will do visualScrollOffset = + // aLayerMetrics.GetVisualDestination(). We need visual scroll offset + // updates to be incorporated into this scroll update loop to properly fix + // this. + ignoreVisualUpdate = true; + + relativeDelta = + Some(Metrics().ApplyPureRelativeScrollUpdateFrom(scrollUpdate)); + Metrics().RecalculateLayoutViewportOffset(); + } else { + APZC_LOG("%p updating scroll offset from %s to %s\n", this, + ToString(Metrics().GetVisualScrollOffset()).c_str(), + ToString(scrollUpdate.GetDestination()).c_str()); + bool offsetChanged = Metrics().ApplyScrollUpdateFrom(scrollUpdate); + Metrics().RecalculateLayoutViewportOffset(); + + if (offsetChanged || scrollUpdate.GetMode() != ScrollMode::Instant || + scrollUpdate.GetType() != ScrollUpdateType::Absolute || + scrollUpdate.GetOrigin() != ScrollOrigin::None) { + // We get a NewScrollFrame update for newly created scroll frames. Only + // if this was not a NewScrollFrame update or the offset changed do we + // request repaint. This is important so that we don't request repaint + // for every new content and set a full display port on it. + scrollOffsetUpdated = true; + } + } + + if (relativeDelta) { + cumulativeRelativeDelta = + !cumulativeRelativeDelta + ? relativeDelta + : Some(*cumulativeRelativeDelta + *relativeDelta); + } else { + // If the scroll update is not relative, clobber the cumulative delta, + // i.e. later updates win. + cumulativeRelativeDelta.reset(); + } + + // If an animation is underway, tell it about the scroll offset update. + // Some animations can handle some scroll offset updates and continue + // running. Those that can't will return false, and we cancel them. + if (ShouldCancelAnimationForScrollUpdate(relativeDelta)) { + // Cancel the animation (which might also trigger a repaint request) + // after we update the scroll offset above. Otherwise we can be left + // in a state where things are out of sync. + CancelAnimation(); + didCancelAnimation = true; + } + } + + if (scrollOffsetUpdated) { + for (auto& sampledState : mSampledState) { + if (!didCancelAnimation && cumulativeRelativeDelta.isSome()) { + sampledState.UpdateScrollPropertiesWithRelativeDelta( + Metrics(), *cumulativeRelativeDelta); + } else { + sampledState.UpdateScrollProperties(Metrics()); + } + } + + // Because of the scroll generation update, any inflight paint requests + // are going to be ignored by layout, and so mExpectedGeckoMetrics becomes + // incorrect for the purposes of calculating the LD transform. To correct + // this we need to update mExpectedGeckoMetrics to be the last thing we + // know was painted by Gecko. + mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics); + + // Since the scroll offset has changed, we need to recompute the + // displayport margins and send them to layout. Otherwise there might be + // scenarios where for example we scroll from the top of a page (where the + // top displayport margin is zero) to the bottom of a page, which will + // result in a displayport that doesn't extend upwards at all. + // Note that even if the CancelAnimation call above requested a repaint + // this is fine because we already have repaint request deduplication. + needContentRepaint = true; + // Since the main-thread scroll offset changed we should trigger a + // recomposite to make sure it becomes user-visible. + ScheduleComposite(); + } else if (needToReclampScroll) { + // Even if we didn't accept a new scroll offset from content, the + // scrollable rect or composition bounds may have changed in a way that + // makes our local scroll offset out of bounds, so re-clamp it. + ClampAndSetVisualScrollOffset(Metrics().GetVisualScrollOffset()); + for (auto& sampledState : mSampledState) { + sampledState.ClampVisualScrollOffset(Metrics()); + } + } + + // If our scroll range changed (for example, because the page dynamically + // loaded new content, thereby increasing the size of the scrollable rect), + // and we're overscrolled, being overscrolled may no longer be a valid + // state (for example, we may no longer be at the edge of our scroll range), + // so clear overscroll and discontinue any overscroll animation. + // Ideas for improvements here: + // - Instead of collapsing the overscroll gutter, try to "fill it" + // with newly loaded content. This would basically entail checking + // if (GetVisualScrollOffset() + GetOverscrollAmount()) is a valid + // visual scroll offset in our new scroll range, and if so, scrolling + // there. + if (needToReclampScroll) { + if (IsInInvalidOverscroll()) { + if (mState == OVERSCROLL_ANIMATION) { + CancelAnimation(); + } else if (IsOverscrolled()) { + ClearOverscroll(); + } + } + } + + if (smoothScrollRequested && !scrollOffsetUpdated) { + mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics); + // Need to acknowledge the request. + needContentRepaint = true; + } + + // If `isDefault` is true, this APZC is a "new" one (this is the first time + // it's getting a NotifyLayersUpdated call). In this case we want to apply the + // visual scroll offset from the main thread to our scroll offset. + // The main thread may also ask us to scroll the visual viewport to a + // particular location. However, in all cases, we want to ignore the visual + // offset update if ignoreVisualUpdate is true, because we're clobbering + // the visual update with a layout update. + bool visualScrollOffsetUpdated = + !ignoreVisualUpdate && + (isDefault || + aLayerMetrics.GetVisualScrollUpdateType() != FrameMetrics::eNone); + + if (visualScrollOffsetUpdated) { + APZC_LOG("%p updating visual scroll offset from %s to %s (updateType %d)\n", + this, ToString(Metrics().GetVisualScrollOffset()).c_str(), + ToString(aLayerMetrics.GetVisualDestination()).c_str(), + (int)aLayerMetrics.GetVisualScrollUpdateType()); + bool offsetChanged = Metrics().ClampAndSetVisualScrollOffset( + aLayerMetrics.GetVisualDestination()); + + // If this is the first time we got metrics for this content (isDefault) and + // the update type was none and the offset didn't change then we don't have + // to do anything. This is important because we don't want to request + // repaint on the initial NotifyLayersUpdated for every content and thus set + // a full display port. + if (aLayerMetrics.GetVisualScrollUpdateType() == FrameMetrics::eNone && + !offsetChanged) { + visualScrollOffsetUpdated = false; + } + } + if (visualScrollOffsetUpdated) { + // The rest of this branch largely follows the code in the + // |if (scrollOffsetUpdated)| branch above. Eventually it should get + // merged into that branch. + Metrics().RecalculateLayoutViewportOffset(); + for (auto& sampledState : mSampledState) { + sampledState.UpdateScrollProperties(Metrics()); + } + mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics); + if (ShouldCancelAnimationForScrollUpdate(Nothing())) { + CancelAnimation(); + } + // The main thread did not actually paint a displayport at the target + // visual offset, so we need to ask it to repaint. We need to set the + // contentRepaintType to something other than eNone, otherwise the main + // thread will short-circuit the repaint request. + // Don't do this for eRestore visual updates as a repaint coming from APZ + // breaks the scroll offset restoration mechanism. + needContentRepaint = true; + if (aLayerMetrics.GetVisualScrollUpdateType() == + FrameMetrics::eMainThread) { + contentRepaintType = RepaintUpdateType::eVisualUpdate; + } + ScheduleComposite(); + } + + if (viewportSizeUpdated) { + // While we want to accept the main thread's layout viewport _size_, + // its position may be out of date in light of async scrolling, to + // adjust it if necessary to make sure it continues to enclose the + // visual viewport. + // Note: it's important to do this _after_ we've accepted any + // updated composition bounds. + Metrics().RecalculateLayoutViewportOffset(); + } + + if (needContentRepaint) { + // This repaint request could be driven by a user action if we accept a + // relative scroll offset update + RequestContentRepaint(contentRepaintType); + } +} + +FrameMetrics& AsyncPanZoomController::Metrics() { + mRecursiveMutex.AssertCurrentThreadIn(); + return mScrollMetadata.GetMetrics(); +} + +const FrameMetrics& AsyncPanZoomController::Metrics() const { + mRecursiveMutex.AssertCurrentThreadIn(); + return mScrollMetadata.GetMetrics(); +} + +GeckoViewMetrics AsyncPanZoomController::GetGeckoViewMetrics() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return GeckoViewMetrics{GetEffectiveScrollOffset(eForCompositing, lock), + GetEffectiveZoom(eForCompositing, lock)}; +} + +bool AsyncPanZoomController::UpdateRootFrameMetricsIfChanged( + GeckoViewMetrics& aMetrics) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + if (!Metrics().IsRootContent()) { + return false; + } + + GeckoViewMetrics newMetrics = GetGeckoViewMetrics(); + bool hasChanged = RoundedToInt(aMetrics.mVisualScrollOffset) != + RoundedToInt(newMetrics.mVisualScrollOffset) || + aMetrics.mZoom != newMetrics.mZoom; + + if (hasChanged) { + aMetrics = newMetrics; + } + + return hasChanged; +} + +const FrameMetrics& AsyncPanZoomController::GetFrameMetrics() const { + return Metrics(); +} + +const ScrollMetadata& AsyncPanZoomController::GetScrollMetadata() const { + mRecursiveMutex.AssertCurrentThreadIn(); + return mScrollMetadata; +} + +void AsyncPanZoomController::AssertOnSamplerThread() const { + if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) { + treeManagerLocal->AssertOnSamplerThread(); + } +} + +void AsyncPanZoomController::AssertOnUpdaterThread() const { + if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) { + treeManagerLocal->AssertOnUpdaterThread(); + } +} + +APZCTreeManager* AsyncPanZoomController::GetApzcTreeManager() const { + mRecursiveMutex.AssertNotCurrentThreadIn(); + return mTreeManager; +} + +void AsyncPanZoomController::ZoomToRect(const ZoomTarget& aZoomTarget, + const uint32_t aFlags) { + CSSRect rect = aZoomTarget.targetRect; + if (!rect.IsFinite()) { + NS_WARNING("ZoomToRect got called with a non-finite rect; ignoring..."); + return; + } + + if (rect.IsEmpty() && (aFlags & DISABLE_ZOOM_OUT)) { + // Double-tap-to-zooming uses an empty rect to mean "zoom out". + // If zooming out is disabled, an empty rect is nonsensical + // and will produce undesirable scrolling. + NS_WARNING( + "ZoomToRect got called with an empty rect and zoom out disabled; " + "ignoring..."); + return; + } + + SetState(ANIMATING_ZOOM); + + { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + MOZ_ASSERT(Metrics().IsRootContent()); + + const float defaultZoomInAmount = + StaticPrefs::apz_doubletapzoom_defaultzoomin(); + + ParentLayerRect compositionBounds = Metrics().GetCompositionBounds(); + CSSRect cssPageRect = Metrics().GetScrollableRect(); + CSSPoint scrollOffset = Metrics().GetVisualScrollOffset(); + CSSSize sizeBeforeZoom = Metrics().CalculateCompositedSizeInCssPixels(); + CSSToParentLayerScale currentZoom = Metrics().GetZoom(); + CSSToParentLayerScale targetZoom; + + // The minimum zoom to prevent over-zoom-out. + // If the zoom factor is lower than this (i.e. we are zoomed more into the + // page), then the CSS content rect, in layers pixels, will be smaller than + // the composition bounds. If this happens, we can't fill the target + // composited area with this frame. + CSSToParentLayerScale localMinZoom( + std::max(compositionBounds.Width() / cssPageRect.Width(), + compositionBounds.Height() / cssPageRect.Height())); + + localMinZoom.scale = + clamped(localMinZoom.scale, mZoomConstraints.mMinZoom.scale, + mZoomConstraints.mMaxZoom.scale); + + localMinZoom = std::max(mZoomConstraints.mMinZoom, localMinZoom); + CSSToParentLayerScale localMaxZoom = + std::max(localMinZoom, mZoomConstraints.mMaxZoom); + + if (!rect.IsEmpty()) { + // Intersect the zoom-to-rect to the CSS rect to make sure it fits. + rect = rect.Intersect(cssPageRect); + targetZoom = CSSToParentLayerScale( + std::min(compositionBounds.Width() / rect.Width(), + compositionBounds.Height() / rect.Height())); + if (aFlags & DISABLE_ZOOM_OUT) { + targetZoom = std::max(targetZoom, currentZoom); + } + } + + // 1. If the rect is empty, the content-side logic for handling a double-tap + // requested that we zoom out. + // 2. currentZoom is equal to mZoomConstraints.mMaxZoom and user still + // double-tapping it + // Treat these cases as a request to zoom out as much as possible + // unless cantZoomOutBehavior == ZoomIn and currentZoom + // is equal to localMinZoom and user still double-tapping it, then try to + // zoom in a small amount to provide feedback to the user. + bool zoomOut = false; + // True if we are already zoomed out and we are asked to either stay there + // or zoom out more and cantZoomOutBehavior == ZoomIn. + bool zoomInDefaultAmount = false; + if (aFlags & DISABLE_ZOOM_OUT) { + zoomOut = false; + } else { + if (rect.IsEmpty()) { + if (currentZoom == localMinZoom && + aZoomTarget.cantZoomOutBehavior == CantZoomOutBehavior::ZoomIn && + (defaultZoomInAmount != 1.f)) { + zoomInDefaultAmount = true; + } else { + zoomOut = true; + } + } else if (currentZoom == localMaxZoom && targetZoom >= localMaxZoom) { + zoomOut = true; + } + } + + // already at min zoom and asked to zoom out further + if (!zoomOut && currentZoom == localMinZoom && targetZoom <= localMinZoom && + aZoomTarget.cantZoomOutBehavior == CantZoomOutBehavior::ZoomIn && + (defaultZoomInAmount != 1.f)) { + zoomInDefaultAmount = true; + } + MOZ_ASSERT(!(zoomInDefaultAmount && zoomOut)); + + if (zoomInDefaultAmount) { + targetZoom = + CSSToParentLayerScale(currentZoom.scale * defaultZoomInAmount); + } + + if (zoomOut) { + targetZoom = localMinZoom; + } + + if (aFlags & PAN_INTO_VIEW_ONLY) { + targetZoom = currentZoom; + } else if (aFlags & ONLY_ZOOM_TO_DEFAULT_SCALE) { + CSSToParentLayerScale zoomAtDefaultScale = + Metrics().GetDevPixelsPerCSSPixel() * + LayoutDeviceToParentLayerScale(1.0); + if (targetZoom.scale > zoomAtDefaultScale.scale) { + // Only change the zoom if we are less than the default zoom + if (currentZoom.scale < zoomAtDefaultScale.scale) { + targetZoom = zoomAtDefaultScale; + } else { + targetZoom = currentZoom; + } + } + } + + targetZoom.scale = + clamped(targetZoom.scale, localMinZoom.scale, localMaxZoom.scale); + + FrameMetrics endZoomToMetrics = Metrics(); + endZoomToMetrics.SetZoom(CSSToParentLayerScale(targetZoom)); + CSSSize sizeAfterZoom = + endZoomToMetrics.CalculateCompositedSizeInCssPixels(); + + if (zoomInDefaultAmount || zoomOut) { + // For the zoom out case we should always center what was visible + // otherwise it feels like we are scrolling as well as zooming out. For + // the non-zoomOut case, if we've been provided a pointer location, zoom + // around that, otherwise just zoom in to the center of what's currently + // visible. + if (!zoomOut && aZoomTarget.documentRelativePointerPosition.isSome()) { + rect = CSSRect(aZoomTarget.documentRelativePointerPosition->x - + sizeAfterZoom.width / 2, + aZoomTarget.documentRelativePointerPosition->y - + sizeAfterZoom.height / 2, + sizeAfterZoom.Width(), sizeAfterZoom.Height()); + } else { + rect = CSSRect( + scrollOffset.x + (sizeBeforeZoom.width - sizeAfterZoom.width) / 2, + scrollOffset.y + (sizeBeforeZoom.height - sizeAfterZoom.height) / 2, + sizeAfterZoom.Width(), sizeAfterZoom.Height()); + } + + rect = rect.Intersect(cssPageRect); + } + + // Check if we can fit the full elementBoundingRect. + if (!aZoomTarget.targetRect.IsEmpty() && !zoomOut && + aZoomTarget.elementBoundingRect.isSome()) { + MOZ_ASSERT(aZoomTarget.elementBoundingRect->Contains(rect)); + CSSRect elementBoundingRect = + aZoomTarget.elementBoundingRect->Intersect(cssPageRect); + if (elementBoundingRect.width <= sizeAfterZoom.width && + elementBoundingRect.height <= sizeAfterZoom.height) { + rect = elementBoundingRect; + } + } + + // Vertically center the zoomed element in the screen. + if (!zoomOut && (sizeAfterZoom.height > rect.Height())) { + rect.MoveByY(-(sizeAfterZoom.height - rect.Height()) * 0.5f); + if (rect.Y() < 0.0f) { + rect.MoveToY(0.0f); + } + } + + // Horizontally center the zoomed element in the screen. + if (!zoomOut && (sizeAfterZoom.width > rect.Width())) { + rect.MoveByX(-(sizeAfterZoom.width - rect.Width()) * 0.5f); + if (rect.X() < 0.0f) { + rect.MoveToX(0.0f); + } + } + + bool intersectRectAgain = false; + // If we can't zoom out enough to show the full rect then shift the rect we + // are able to show to center what was visible. + // Note that this calculation works no matter the relation of sizeBeforeZoom + // to sizeAfterZoom, ie whether we are increasing or decreasing zoom. + if (!zoomOut && (sizeAfterZoom.height < rect.Height())) { + rect.y = + scrollOffset.y + (sizeBeforeZoom.height - sizeAfterZoom.height) / 2; + rect.height = sizeAfterZoom.Height(); + + intersectRectAgain = true; + } + + if (!zoomOut && (sizeAfterZoom.width < rect.Width())) { + rect.x = + scrollOffset.x + (sizeBeforeZoom.width - sizeAfterZoom.width) / 2; + rect.width = sizeAfterZoom.Width(); + + intersectRectAgain = true; + } + if (intersectRectAgain) { + rect = rect.Intersect(cssPageRect); + } + + // If any of these conditions are met, the page will be overscrolled after + // zoomed. Attempting to scroll outside of the valid scroll range will cause + // problems. + if (rect.Y() + sizeAfterZoom.height > cssPageRect.YMost()) { + rect.MoveToY(std::max(cssPageRect.Y(), + cssPageRect.YMost() - sizeAfterZoom.height)); + } + if (rect.Y() < cssPageRect.Y()) { + rect.MoveToY(cssPageRect.Y()); + } + if (rect.X() + sizeAfterZoom.width > cssPageRect.XMost()) { + rect.MoveToX( + std::max(cssPageRect.X(), cssPageRect.XMost() - sizeAfterZoom.width)); + } + if (rect.X() < cssPageRect.X()) { + rect.MoveToY(cssPageRect.X()); + } + + endZoomToMetrics.SetVisualScrollOffset(rect.TopLeft()); + endZoomToMetrics.RecalculateLayoutViewportOffset(); + + StartAnimation(new ZoomAnimation( + *this, Metrics().GetVisualScrollOffset(), Metrics().GetZoom(), + endZoomToMetrics.GetVisualScrollOffset(), endZoomToMetrics.GetZoom())); + + RequestContentRepaint(RepaintUpdateType::eUserAction); + } +} + +InputBlockState* AsyncPanZoomController::GetCurrentInputBlock() const { + return GetInputQueue()->GetCurrentBlock(); +} + +TouchBlockState* AsyncPanZoomController::GetCurrentTouchBlock() const { + return GetInputQueue()->GetCurrentTouchBlock(); +} + +PanGestureBlockState* AsyncPanZoomController::GetCurrentPanGestureBlock() + const { + return GetInputQueue()->GetCurrentPanGestureBlock(); +} + +PinchGestureBlockState* AsyncPanZoomController::GetCurrentPinchGestureBlock() + const { + return GetInputQueue()->GetCurrentPinchGestureBlock(); +} + +void AsyncPanZoomController::ResetTouchInputState() { + MultiTouchInput cancel(MultiTouchInput::MULTITOUCH_CANCEL, 0, + TimeStamp::Now(), 0); + RefPtr listener = GetGestureEventListener(); + if (listener) { + listener->HandleInputEvent(cancel); + } + CancelAnimationAndGestureState(); + // Clear overscroll along the entire handoff chain, in case an APZC + // later in the chain is overscrolled. + if (TouchBlockState* block = GetCurrentTouchBlock()) { + block->GetOverscrollHandoffChain()->ClearOverscroll(); + } +} + +void AsyncPanZoomController::ResetPanGestureInputState() { + // No point sending a PANGESTURE_INTERRUPTED as all it does is + // call CancelAnimation(), which we also do here. + CancelAnimationAndGestureState(); + // Clear overscroll along the entire handoff chain, in case an APZC + // later in the chain is overscrolled. + if (PanGestureBlockState* block = GetCurrentPanGestureBlock()) { + block->GetOverscrollHandoffChain()->ClearOverscroll(); + } +} + +void AsyncPanZoomController::CancelAnimationAndGestureState() { + mX.CancelGesture(); + mY.CancelGesture(); + CancelAnimation(CancelAnimationFlags::ScrollSnap); +} + +bool AsyncPanZoomController::HasReadyTouchBlock() const { + return GetInputQueue()->HasReadyTouchBlock(); +} + +bool AsyncPanZoomController::CanHandleScrollOffsetUpdate(PanZoomState aState) { + return aState == PAN_MOMENTUM || aState == TOUCHING || IsPanningState(aState); +} + +bool AsyncPanZoomController::ShouldCancelAnimationForScrollUpdate( + const Maybe& aRelativeDelta) { + // Never call CancelAnimation() for a no-op relative update. + if (aRelativeDelta == Some(CSSPoint())) { + return false; + } + + if (mAnimation) { + return !mAnimation->HandleScrollOffsetUpdate(aRelativeDelta); + } + + return !CanHandleScrollOffsetUpdate(mState); +} + +AsyncPanZoomController::PanZoomState +AsyncPanZoomController::SetStateNoContentControllerDispatch( + PanZoomState aNewState) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + APZC_LOG_DETAIL("changing from state %s to %s\n", this, + ToString(mState).c_str(), ToString(aNewState).c_str()); + PanZoomState oldState = mState; + mState = aNewState; + return oldState; +} + +void AsyncPanZoomController::SetState(PanZoomState aNewState) { + // When a state transition to a transforming state is occuring and a delayed + // transform end notification exists, send the TransformEnd notification + // before the TransformBegin notification is sent for the input state change. + if (IsTransformingState(aNewState) && IsDelayedTransformEndSet()) { + MOZ_ASSERT(!IsTransformingState(mState)); + SetDelayedTransformEnd(false); + DispatchStateChangeNotification(PANNING, NOTHING); + } + + PanZoomState oldState = SetStateNoContentControllerDispatch(aNewState); + + DispatchStateChangeNotification(oldState, aNewState); +} + +auto AsyncPanZoomController::GetState() const -> PanZoomState { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mState; +} + +void AsyncPanZoomController::DispatchStateChangeNotification( + PanZoomState aOldState, PanZoomState aNewState) { + { // scope the lock + RecursiveMutexAutoLock lock(mRecursiveMutex); + if (mNotificationBlockers > 0) { + return; + } + } + + if (RefPtr controller = GetGeckoContentController()) { + if (!IsTransformingState(aOldState) && IsTransformingState(aNewState)) { + controller->NotifyAPZStateChange(GetGuid(), + APZStateChange::eTransformBegin); + } else if (IsTransformingState(aOldState) && + !IsTransformingState(aNewState)) { + controller->NotifyAPZStateChange(GetGuid(), + APZStateChange::eTransformEnd); + } + } +} + +bool AsyncPanZoomController::IsInTransformingState() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return IsTransformingState(mState); +} + +bool AsyncPanZoomController::IsTransformingState(PanZoomState aState) { + return !(aState == NOTHING || aState == TOUCHING); +} + +bool AsyncPanZoomController::IsPanningState(PanZoomState aState) { + return (aState == PANNING || aState == PANNING_LOCKED_X || + aState == PANNING_LOCKED_Y); +} + +bool AsyncPanZoomController::IsInPanningState() const { + return IsPanningState(mState); +} + +bool AsyncPanZoomController::IsInScrollingGesture() const { + return IsPanningState(mState) || mState == SCROLLBAR_DRAG || + mState == TOUCHING || mState == PINCHING; +} + +bool AsyncPanZoomController::IsDelayedTransformEndSet() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mDelayedTransformEnd; +} + +void AsyncPanZoomController::SetDelayedTransformEnd(bool aDelayedTransformEnd) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mDelayedTransformEnd = aDelayedTransformEnd; +} + +void AsyncPanZoomController::UpdateZoomConstraints( + const ZoomConstraints& aConstraints) { + if ((MOZ_LOG_TEST(sApzCtlLog, LogLevel::Debug) && + (aConstraints != mZoomConstraints)) || + MOZ_LOG_TEST(sApzCtlLog, LogLevel::Verbose)) { + APZC_LOG("%p updating zoom constraints to %d %d %f %f\n", this, + aConstraints.mAllowZoom, aConstraints.mAllowDoubleTapZoom, + aConstraints.mMinZoom.scale, aConstraints.mMaxZoom.scale); + } + + if (std::isnan(aConstraints.mMinZoom.scale) || + std::isnan(aConstraints.mMaxZoom.scale)) { + NS_WARNING("APZC received zoom constraints with NaN values; dropping..."); + return; + } + + RecursiveMutexAutoLock lock(mRecursiveMutex); + CSSToParentLayerScale min = Metrics().GetDevPixelsPerCSSPixel() * + ViewportMinScale() / ParentLayerToScreenScale(1); + CSSToParentLayerScale max = Metrics().GetDevPixelsPerCSSPixel() * + ViewportMaxScale() / ParentLayerToScreenScale(1); + + // inf float values and other bad cases should be sanitized by the code below. + mZoomConstraints.mAllowZoom = aConstraints.mAllowZoom; + mZoomConstraints.mAllowDoubleTapZoom = aConstraints.mAllowDoubleTapZoom; + mZoomConstraints.mMinZoom = + (min > aConstraints.mMinZoom ? min : aConstraints.mMinZoom); + mZoomConstraints.mMaxZoom = + (max > aConstraints.mMaxZoom ? aConstraints.mMaxZoom : max); + if (mZoomConstraints.mMaxZoom < mZoomConstraints.mMinZoom) { + mZoomConstraints.mMaxZoom = mZoomConstraints.mMinZoom; + } +} + +bool AsyncPanZoomController::ZoomConstraintsAllowZoom() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mZoomConstraints.mAllowZoom; +} + +bool AsyncPanZoomController::ZoomConstraintsAllowDoubleTapZoom() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mZoomConstraints.mAllowDoubleTapZoom; +} + +void AsyncPanZoomController::PostDelayedTask(already_AddRefed aTask, + int aDelayMs) { + APZThreadUtils::AssertOnControllerThread(); + RefPtr task = aTask; + RefPtr controller = GetGeckoContentController(); + if (controller) { + controller->PostDelayedTask(task.forget(), aDelayMs); + } + // If there is no controller, that means this APZC has been destroyed, and + // we probably don't need to run the task. It will get destroyed when the + // RefPtr goes out of scope. +} + +bool AsyncPanZoomController::Matches(const ScrollableLayerGuid& aGuid) { + return aGuid == GetGuid(); +} + +bool AsyncPanZoomController::HasTreeManager( + const APZCTreeManager* aTreeManager) const { + return GetApzcTreeManager() == aTreeManager; +} + +void AsyncPanZoomController::GetGuid(ScrollableLayerGuid* aGuidOut) const { + if (aGuidOut) { + *aGuidOut = GetGuid(); + } +} + +ScrollableLayerGuid AsyncPanZoomController::GetGuid() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return ScrollableLayerGuid(mLayersId, Metrics().GetPresShellId(), + Metrics().GetScrollId()); +} + +void AsyncPanZoomController::SetTestAsyncScrollOffset(const CSSPoint& aPoint) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mTestAsyncScrollOffset = aPoint; + ScheduleComposite(); +} + +void AsyncPanZoomController::SetTestAsyncZoom( + const LayerToParentLayerScale& aZoom) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mTestAsyncZoom = aZoom; + ScheduleComposite(); +} + +Maybe AsyncPanZoomController::FindSnapPointNear( + const CSSPoint& aDestination, ScrollUnit aUnit, + ScrollSnapFlags aSnapFlags) { + mRecursiveMutex.AssertCurrentThreadIn(); + APZC_LOG("%p scroll snapping near %s\n", this, + ToString(aDestination).c_str()); + CSSRect scrollRange = Metrics().CalculateScrollRange(); + if (auto snapTarget = ScrollSnapUtils::GetSnapPointForDestination( + mScrollMetadata.GetSnapInfo(), aUnit, aSnapFlags, + CSSRect::ToAppUnits(scrollRange), + CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset()), + CSSPoint::ToAppUnits(aDestination))) { + CSSPoint cssSnapPoint = CSSPoint::FromAppUnits(snapTarget->mPosition); + // GetSnapPointForDestination() can produce a destination that's outside + // of the scroll frame's scroll range. Clamp it here (this matches the + // behaviour of the main-thread code path, which clamps it in + // nsGfxScrollFrame::ScrollTo()). + return Some(CSSSnapTarget{scrollRange.ClampPoint(cssSnapPoint), + snapTarget->mTargetIds}); + } + return Nothing(); +} + +Maybe> +AsyncPanZoomController::MaybeSplitTouchMoveEvent( + const MultiTouchInput& aOriginalEvent, ScreenCoord aPanThreshold, + float aVectorLength, ExternalPoint& aExtPoint) { + if (aVectorLength <= aPanThreshold) { + return Nothing(); + } + + auto splitEvent = std::make_pair(aOriginalEvent, aOriginalEvent); + + SingleTouchData& firstTouchData = splitEvent.first.mTouches[0]; + SingleTouchData& secondTouchData = splitEvent.second.mTouches[0]; + + firstTouchData.mHistoricalData.Clear(); + secondTouchData.mHistoricalData.Clear(); + + ExternalPoint destination = aExtPoint; + ExternalPoint thresholdPosition; + + const float ratio = aPanThreshold / aVectorLength; + thresholdPosition.x = mStartTouch.x + ratio * (destination.x - mStartTouch.x); + thresholdPosition.y = mStartTouch.y + ratio * (destination.y - mStartTouch.y); + + TouchSample start{mLastTouch}; + // To compute the timestamp of the first event (which is at the threshold), + // use linear interpolation with the starting point |start| being the last + // event that's before the threshold, and the end point |end| being the first + // event after the threshold. + + // The initial choice for |start| is the last touch event before + // |aOriginalEvent|, and the initial choice for |end| is |aOriginalEvent|. + + // However, the historical data points stored in |aOriginalEvent| may contain + // intermediate positions that can serve as tighter bounds for the + // interpolation. + TouchSample end{destination, aOriginalEvent.mTimeStamp}; + + for (const auto& historicalData : + aOriginalEvent.mTouches[0].mHistoricalData) { + ExternalPoint histExtPoint = ToExternalPoint(aOriginalEvent.mScreenOffset, + historicalData.mScreenPoint); + + if (PanVector(histExtPoint).Length() < + PanVector(thresholdPosition).Length()) { + start = {histExtPoint, historicalData.mTimeStamp}; + } else { + break; + } + } + + for (const SingleTouchData::HistoricalTouchData& histData : + Reversed(aOriginalEvent.mTouches[0].mHistoricalData)) { + ExternalPoint histExtPoint = + ToExternalPoint(aOriginalEvent.mScreenOffset, histData.mScreenPoint); + + if (PanVector(histExtPoint).Length() > + PanVector(thresholdPosition).Length()) { + end = {histExtPoint, histData.mTimeStamp}; + } else { + break; + } + } + + const float totalLength = + ScreenPoint(fabs(end.mPosition.x - start.mPosition.x), + fabs(end.mPosition.y - start.mPosition.y)) + .Length(); + const float thresholdLength = + ScreenPoint(fabs(thresholdPosition.x - start.mPosition.x), + fabs(thresholdPosition.y - start.mPosition.y)) + .Length(); + const float splitRatio = thresholdLength / totalLength; + + splitEvent.first.mTimeStamp = + start.mTimeStamp + + (end.mTimeStamp - start.mTimeStamp).MultDouble(splitRatio); + + for (const auto& historicalData : + aOriginalEvent.mTouches[0].mHistoricalData) { + if (historicalData.mTimeStamp > splitEvent.first.mTimeStamp) { + secondTouchData.mHistoricalData.AppendElement(historicalData); + } else { + firstTouchData.mHistoricalData.AppendElement(historicalData); + } + } + + firstTouchData.mScreenPoint = RoundedToInt( + ViewAs(thresholdPosition - splitEvent.first.mScreenOffset, + PixelCastJustification::ExternalIsScreen)); + + // Recompute firstTouchData.mLocalScreenPoint. + splitEvent.first.TransformToLocal(GetTransformToThis()); + + // Pass |thresholdPosition| back out to the caller via |aExtPoint| + aExtPoint = thresholdPosition; + + return Some(splitEvent); +} + +void AsyncPanZoomController::ScrollSnapNear(const CSSPoint& aDestination, + ScrollSnapFlags aSnapFlags) { + if (Maybe snapTarget = FindSnapPointNear( + aDestination, ScrollUnit::DEVICE_PIXELS, aSnapFlags)) { + if (snapTarget->mPosition != Metrics().GetVisualScrollOffset()) { + APZC_LOG("%p smooth scrolling to snap point %s\n", this, + ToString(snapTarget->mPosition).c_str()); + SmoothMsdScrollTo(std::move(*snapTarget), ScrollTriggeredByScript::No); + } + } +} + +void AsyncPanZoomController::ScrollSnap(ScrollSnapFlags aSnapFlags) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + ScrollSnapNear(Metrics().GetVisualScrollOffset(), aSnapFlags); +} + +void AsyncPanZoomController::ScrollSnapToDestination() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + + float friction = StaticPrefs::apz_fling_friction(); + ParentLayerPoint velocity(mX.GetVelocity(), mY.GetVelocity()); + ParentLayerPoint predictedDelta; + // "-velocity / log(1.0 - friction)" is the integral of the deceleration + // curve modeled for flings in the "Axis" class. + if (velocity.x != 0.0f && friction != 0.0f) { + predictedDelta.x = -velocity.x / log(1.0 - friction); + } + if (velocity.y != 0.0f && friction != 0.0f) { + predictedDelta.y = -velocity.y / log(1.0 - friction); + } + + // If the fling will overscroll, don't scroll snap, because then the user + // user would not see any overscroll animation. + bool flingWillOverscroll = + IsOverscrolled() && ((velocity.x.value * mX.GetOverscroll() >= 0) || + (velocity.y.value * mY.GetOverscroll() >= 0)); + if (flingWillOverscroll) { + return; + } + + CSSPoint startPosition = Metrics().GetVisualScrollOffset(); + ScrollSnapFlags snapFlags = ScrollSnapFlags::IntendedEndPosition; + if (predictedDelta != ParentLayerPoint()) { + snapFlags |= ScrollSnapFlags::IntendedDirection; + } + if (Maybe snapTarget = MaybeAdjustDeltaForScrollSnapping( + ScrollUnit::DEVICE_PIXELS, snapFlags, predictedDelta, + startPosition)) { + APZC_LOG( + "%p fling snapping. friction: %f velocity: %f, %f " + "predictedDelta: %f, %f position: %f, %f " + "snapDestination: %f, %f\n", + this, friction, velocity.x.value, velocity.y.value, + predictedDelta.x.value, predictedDelta.y.value, + Metrics().GetVisualScrollOffset().x.value, + Metrics().GetVisualScrollOffset().y.value, startPosition.x.value, + startPosition.y.value); + + // Ensure that any queued transform-end due to a pan-end is not + // sent. Instead rely on the transform-end sent due to the + // scroll snap animation. + SetDelayedTransformEnd(false); + + SmoothMsdScrollTo(std::move(*snapTarget), ScrollTriggeredByScript::No); + } +} + +Maybe AsyncPanZoomController::MaybeAdjustDeltaForScrollSnapping( + ScrollUnit aUnit, ScrollSnapFlags aSnapFlags, ParentLayerPoint& aDelta, + CSSPoint& aStartPosition) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + CSSToParentLayerScale zoom = Metrics().GetZoom(); + if (zoom == CSSToParentLayerScale(0)) { + return Nothing(); + } + CSSPoint destination = Metrics().CalculateScrollRange().ClampPoint( + aStartPosition + (aDelta / zoom)); + + if (Maybe snapTarget = + FindSnapPointNear(destination, aUnit, aSnapFlags)) { + aDelta = (snapTarget->mPosition - aStartPosition) * zoom; + aStartPosition = snapTarget->mPosition; + return snapTarget; + } + return Nothing(); +} + +Maybe +AsyncPanZoomController::MaybeAdjustDeltaForScrollSnappingOnWheelInput( + const ScrollWheelInput& aEvent, ParentLayerPoint& aDelta, + CSSPoint& aStartPosition) { + // Don't scroll snap for pixel scrolls. This matches the main thread + // behaviour in EventStateManager::DoScrollText(). + if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_PIXEL) { + return Nothing(); + } + + // Note that this MaybeAdjustDeltaForScrollSnappingOnWheelInput also gets + // called for pan gestures at least on older Mac and Windows. In such cases + // `aEvent.mDeltaType` is `SCROLLDELTA_PIXEL` which should be filtered out by + // the above `if` block, so we assume all incoming `aEvent` are purely wheel + // events, thus we basically use `IntendedDirection` here. + // If we want to change the behavior, i.e. we want to do scroll snap for + // such cases as well, we need to use `IntendedEndPoint`. + ScrollSnapFlags snapFlags = ScrollSnapFlags::IntendedDirection; + if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_PAGE) { + // On Windows there are a couple of cases where scroll events happen with + // SCROLLDELTA_PAGE, in such case we consider it's a page scroll. + snapFlags |= ScrollSnapFlags::IntendedEndPosition; + } + return MaybeAdjustDeltaForScrollSnapping( + ScrollWheelInput::ScrollUnitForDeltaType(aEvent.mDeltaType), + ScrollSnapFlags::IntendedDirection, aDelta, aStartPosition); +} + +Maybe +AsyncPanZoomController::MaybeAdjustDestinationForScrollSnapping( + const KeyboardInput& aEvent, CSSPoint& aDestination, + ScrollSnapFlags aSnapFlags) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + ScrollUnit unit = KeyboardScrollAction::GetScrollUnit(aEvent.mAction.mType); + + if (Maybe snapPoint = + FindSnapPointNear(aDestination, unit, aSnapFlags)) { + aDestination = snapPoint->mPosition; + return snapPoint; + } + return Nothing(); +} + +void AsyncPanZoomController::SetZoomAnimationId( + const Maybe& aZoomAnimationId) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mZoomAnimationId = aZoomAnimationId; +} + +Maybe AsyncPanZoomController::GetZoomAnimationId() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mZoomAnimationId; +} + +std::ostream& operator<<(std::ostream& aOut, + const AsyncPanZoomController::PanZoomState& aState) { + switch (aState) { + case AsyncPanZoomController::PanZoomState::NOTHING: + aOut << "NOTHING"; + break; + case AsyncPanZoomController::PanZoomState::FLING: + aOut << "FLING"; + break; + case AsyncPanZoomController::PanZoomState::TOUCHING: + aOut << "TOUCHING"; + break; + case AsyncPanZoomController::PanZoomState::PANNING: + aOut << "PANNING"; + break; + case AsyncPanZoomController::PanZoomState::PANNING_LOCKED_X: + aOut << "PANNING_LOCKED_X"; + break; + case AsyncPanZoomController::PanZoomState::PANNING_LOCKED_Y: + aOut << "PANNING_LOCKED_Y"; + break; + case AsyncPanZoomController::PanZoomState::PAN_MOMENTUM: + aOut << "PAN_MOMENTUM"; + break; + case AsyncPanZoomController::PanZoomState::PINCHING: + aOut << "PINCHING"; + break; + case AsyncPanZoomController::PanZoomState::ANIMATING_ZOOM: + aOut << "ANIMATING_ZOOM"; + break; + case AsyncPanZoomController::PanZoomState::OVERSCROLL_ANIMATION: + aOut << "OVERSCROLL_ANIMATION"; + break; + case AsyncPanZoomController::PanZoomState::SMOOTH_SCROLL: + aOut << "SMOOTH_SCROLL"; + break; + case AsyncPanZoomController::PanZoomState::SMOOTHMSD_SCROLL: + aOut << "SMOOTHMSD_SCROLL"; + break; + case AsyncPanZoomController::PanZoomState::WHEEL_SCROLL: + aOut << "WHEEL_SCROLL"; + break; + case AsyncPanZoomController::PanZoomState::KEYBOARD_SCROLL: + aOut << "KEYBOARD_SCROLL"; + break; + case AsyncPanZoomController::PanZoomState::AUTOSCROLL: + aOut << "AUTOSCROLL"; + break; + case AsyncPanZoomController::PanZoomState::SCROLLBAR_DRAG: + aOut << "SCROLLBAR_DRAG"; + break; + default: + aOut << "UNKNOWN_STATE"; + break; + } + return aOut; +} + +bool operator==(const PointerEventsConsumableFlags& aLhs, + const PointerEventsConsumableFlags& aRhs) { + return (aLhs.mHasRoom == aRhs.mHasRoom) && + (aLhs.mAllowedByTouchAction == aRhs.mAllowedByTouchAction); +} + +std::ostream& operator<<(std::ostream& aOut, + const PointerEventsConsumableFlags& aFlags) { + aOut << std::boolalpha << "{ hasRoom: " << aFlags.mHasRoom + << ", allowedByTouchAction: " << aFlags.mAllowedByTouchAction << "}"; + return aOut; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/AsyncPanZoomController.h b/gfx/layers/apz/src/AsyncPanZoomController.h new file mode 100644 index 0000000000..1f589df826 --- /dev/null +++ b/gfx/layers/apz/src/AsyncPanZoomController.h @@ -0,0 +1,1943 @@ +/* -*- 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_AsyncPanZoomController_h +#define mozilla_layers_AsyncPanZoomController_h + +#include "mozilla/layers/GeckoContentController.h" +#include "mozilla/layers/RepaintRequest.h" +#include "mozilla/layers/SampleTime.h" +#include "mozilla/layers/ScrollbarData.h" +#include "mozilla/layers/ZoomConstraints.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "mozilla/Monitor.h" +#include "mozilla/RecursiveMutex.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ScrollTypes.h" +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/UniquePtr.h" +#include "InputData.h" +#include "Axis.h" // for Axis, Side, etc. +#include "ExpectedGeckoMetrics.h" +#include "FlingAccelerator.h" +#include "InputQueue.h" +#include "APZUtils.h" +#include "LayersTypes.h" +#include "mozilla/gfx/Matrix.h" +#include "nsRegion.h" +#include "nsTArray.h" +#include "PotentialCheckerboardDurationTracker.h" +#include "RecentEventsBuffer.h" // for RecentEventsBuffer +#include "SampledAPZCState.h" + +#include + +namespace mozilla { + +namespace ipc { + +class SharedMemoryBasic; + +} // namespace ipc + +namespace wr { +struct SampledScrollOffset; +} // namespace wr + +namespace layers { + +class AsyncDragMetrics; +class APZCTreeManager; +struct ScrollableLayerGuid; +class CompositorController; +class GestureEventListener; +struct AsyncTransform; +class AsyncPanZoomAnimation; +class StackScrollerFlingAnimation; +template +class GenericFlingAnimation; +class AndroidFlingPhysics; +class DesktopFlingPhysics; +class InputBlockState; +struct FlingHandoffState; +class TouchBlockState; +class PanGestureBlockState; +class OverscrollHandoffChain; +struct OverscrollHandoffState; +class StateChangeNotificationBlocker; +class CheckerboardEvent; +class OverscrollEffectBase; +class WidgetOverscrollEffect; +class GenericOverscrollEffect; +class AndroidSpecificState; +struct KeyboardScrollAction; +struct ZoomTarget; + +namespace apz { +struct AsyncScrollThumbTransformer; +} + +// Base class for grouping platform-specific APZC state variables. +class PlatformSpecificStateBase { + public: + virtual ~PlatformSpecificStateBase() = default; + virtual AndroidSpecificState* AsAndroidSpecificState() { return nullptr; } + // PLPPI = "ParentLayer pixels per (Screen) inch" + virtual AsyncPanZoomAnimation* CreateFlingAnimation( + AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState, + float aPLPPI); + virtual UniquePtr CreateVelocityTracker(Axis* aAxis); + + static void InitializeGlobalState() {} +}; + +/* + * Represents a transform from the ParentLayer coordinate space of an APZC + * to the ParentLayer coordinate space of its parent APZC. + * Each layer along the way contributes to the transform. We track + * contributions that are perspective transforms separately, as sometimes + * these require special handling. + */ +struct AncestorTransform { + gfx::Matrix4x4 mTransform; + gfx::Matrix4x4 mPerspectiveTransform; + + AncestorTransform() = default; + + AncestorTransform(const gfx::Matrix4x4& aTransform, + bool aTransformIsPerspective) { + (aTransformIsPerspective ? mPerspectiveTransform : mTransform) = aTransform; + } + + AncestorTransform(const gfx::Matrix4x4& aTransform, + const gfx::Matrix4x4& aPerspectiveTransform) + : mTransform(aTransform), mPerspectiveTransform(aPerspectiveTransform) {} + + gfx::Matrix4x4 CombinedTransform() const { + return mTransform * mPerspectiveTransform; + } + + bool ContainsPerspectiveTransform() const { + return !mPerspectiveTransform.IsIdentity(); + } + + gfx::Matrix4x4 GetPerspectiveTransform() const { + return mPerspectiveTransform; + } + + friend AncestorTransform operator*(const AncestorTransform& aA, + const AncestorTransform& aB) { + return AncestorTransform{ + aA.mTransform * aB.mTransform, + aA.mPerspectiveTransform * aB.mPerspectiveTransform}; + } +}; + +// Flags returned by AsyncPanZoomController::ArePointerEventsConsumable(). +// See the function for more details. +struct PointerEventsConsumableFlags { + // The APZC has room to pan or zoom in response to the touch event. + bool mHasRoom = false; + + // The panning or zooming is allowed by the touch-action property. + bool mAllowedByTouchAction = false; + + bool IsConsumable() const { return mHasRoom && mAllowedByTouchAction; } + friend bool operator==(const PointerEventsConsumableFlags& aLhs, + const PointerEventsConsumableFlags& aRhs); + friend std::ostream& operator<<(std::ostream& aOut, + const PointerEventsConsumableFlags& aFlags); +}; + +/** + * Controller for all panning and zooming logic. Any time a user input is + * detected and it must be processed in some way to affect what the user sees, + * it goes through here. Listens for any input event from InputData and can + * optionally handle WidgetGUIEvent-derived touch events, but this must be done + * on the main thread. Note that this class completely cross-platform. + * + * Input events originate on the UI thread of the platform that this runs on, + * and are then sent to this class. This class processes the event in some way; + * for example, a touch move will usually lead to a panning of content (though + * of course there are exceptions, such as if content preventDefaults the event, + * or if the target frame is not scrollable). The compositor interacts with this + * class by locking it and querying it for the current transform matrix based on + * the panning and zooming logic that was invoked on the UI thread. + * + * Currently, each outer DOM window (i.e. a website in a tab, but not any + * subframes) has its own AsyncPanZoomController. In the future, to support + * asynchronously scrolled subframes, we want to have one AsyncPanZoomController + * per frame. + */ +class AsyncPanZoomController { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AsyncPanZoomController) + + typedef mozilla::MonitorAutoLock MonitorAutoLock; + typedef mozilla::gfx::Matrix4x4 Matrix4x4; + typedef mozilla::layers::RepaintRequest::ScrollOffsetUpdateType + RepaintUpdateType; + + public: + enum GestureBehavior { + // The platform code is responsible for forwarding gesture events here. We + // will not attempt to generate gesture events from MultiTouchInputs. + DEFAULT_GESTURES, + // An instance of GestureEventListener is used to detect gestures. This is + // handled completely internally within this class. + USE_GESTURE_DETECTOR + }; + + /** + * Gets the DPI from the tree manager. + */ + float GetDPI() const; + + /** + * Constant describing the tolerance in distance we use, multiplied by the + * device DPI, before we start panning the screen. This is to prevent us from + * accidentally processing taps as touch moves, and from very short/accidental + * touches moving the screen. + * Note: It's an abuse of the 'Coord' class to use it to represent a 2D + * distance, but it's the closest thing we currently have. + */ + ScreenCoord GetTouchStartTolerance() const; + /** + * Same as GetTouchStartTolerance, but the tolerance for how far the touch + * has to move before it starts allowing touchmove events to be dispatched + * to content, for non-scrollable content. + */ + ScreenCoord GetTouchMoveTolerance() const; + /** + * Same as GetTouchStartTolerance, but the tolerance for how close the second + * tap has to be to the first tap in order to be counted as part of a + * multi-tap gesture (double-tap or one-touch-pinch). + */ + ScreenCoord GetSecondTapTolerance() const; + + AsyncPanZoomController(LayersId aLayersId, APZCTreeManager* aTreeManager, + const RefPtr& aInputQueue, + GeckoContentController* aController, + GestureBehavior aGestures = DEFAULT_GESTURES); + + // -------------------------------------------------------------------------- + // These methods must only be called on the gecko thread. + // + + /** + * Read the various prefs and do any global initialization for all APZC + * instances. This must be run on the gecko thread before any APZC instances + * are actually used for anything meaningful. + */ + static void InitializeGlobalState(); + + // -------------------------------------------------------------------------- + // These methods must only be called on the controller/UI thread. + // + + /** + * Kicks an animation to zoom to a rect. This may be either a zoom out or zoom + * in. The actual animation is done on the sampler thread after being set + * up. + */ + void ZoomToRect(const ZoomTarget& aZoomTarget, const uint32_t aFlags); + + /** + * Updates any zoom constraints contained in the tag. + */ + void UpdateZoomConstraints(const ZoomConstraints& aConstraints); + + /** + * Schedules a runnable to run on the controller/UI thread at some time + * in the future. + */ + void PostDelayedTask(already_AddRefed aTask, int aDelayMs); + + // -------------------------------------------------------------------------- + // These methods must only be called on the sampler thread. + // + + /** + * Advances any animations currently running to the given timestamp. + * This may be called multiple times with the same timestamp. + * + * The return value indicates whether or not any currently running animation + * should continue. If true, the compositor should schedule another composite. + */ + bool AdvanceAnimations(const SampleTime& aSampleTime); + + bool UpdateAnimation(const RecursiveMutexAutoLock& aProofOfLock, + const SampleTime& aSampleTime, + nsTArray>* aOutDeferredTasks); + + // -------------------------------------------------------------------------- + // These methods must only be called on the updater thread. + // + + /** + * A shadow layer update has arrived. |aScrollMetdata| is the new + * ScrollMetadata for the container layer corresponding to this APZC. + * |aIsFirstPaint| is a flag passed from the shadow + * layers code indicating that the scroll metadata being sent with this call + * are the initial metadata and the initial paint of the frame has just + * happened. + */ + void NotifyLayersUpdated(const ScrollMetadata& aScrollMetadata, + bool aIsFirstPaint, bool aThisLayerTreeUpdated); + + /** + * The platform implementation must set the compositor controller so that we + * can request composites. + */ + void SetCompositorController(CompositorController* aCompositorController); + + // -------------------------------------------------------------------------- + // These methods can be called from any thread. + // + + /** + * Shut down the controller/UI thread state and prepare to be + * deleted (which may happen from any thread). + */ + void Destroy(); + + /** + * Returns true if Destroy() has already been called on this APZC instance. + */ + bool IsDestroyed() const; + + /** + * Returns the transform to take something from the coordinate space of the + * last thing we know gecko painted, to the coordinate space of the last thing + * we asked gecko to paint. In cases where that last request has not yet been + * processed, this is needed to transform input events properly into a space + * gecko will understand. + */ + Matrix4x4 GetTransformToLastDispatchedPaint( + const AsyncTransformComponents& aComponents = LayoutAndVisual) const; + + /** + * Returns the number of CSS pixels of checkerboard according to the metrics + * in this APZC. The argument provided by the caller is the composition bounds + * of this APZC, additionally clipped by the composition bounds of any + * ancestor APZCs, accounting for all the async transforms. + */ + uint32_t GetCheckerboardMagnitude( + const ParentLayerRect& aClippedCompositionBounds) const; + + /** + * Report the number of CSSPixel-milliseconds of checkerboard to telemetry. + * See GetCheckerboardMagnitude for documentation of the + * aClippedCompositionBounds argument that needs to be provided by the caller. + */ + void ReportCheckerboard(const SampleTime& aSampleTime, + const ParentLayerRect& aClippedCompositionBounds); + + /** + * Flush any active checkerboard report that's in progress. This basically + * pretends like any in-progress checkerboard event has terminated, and pushes + * out the report to the checkerboard reporting service and telemetry. If the + * checkerboard event has not really finished, it will start a new event + * on the next composite. + */ + void FlushActiveCheckerboardReport(); + + /** + * See documentation on corresponding method in APZPublicUtils.h + */ + static gfx::IntSize GetDisplayportAlignmentMultiplier( + const ScreenSize& aBaseSize); + + enum class ZoomInProgress { + No, + Yes, + }; + + /** + * Recalculates the displayport. Ideally, this should paint an area bigger + * than the composite-to dimensions so that when you scroll down, you don't + * checkerboard immediately. This includes a bunch of logic, including + * algorithms to bias painting in the direction of the velocity and other + * such things. + */ + static const ScreenMargin CalculatePendingDisplayPort( + const FrameMetrics& aFrameMetrics, const ParentLayerPoint& aVelocity, + ZoomInProgress aZoomInProgress); + + nsEventStatus HandleDragEvent(const MouseInput& aEvent, + const AsyncDragMetrics& aDragMetrics, + OuterCSSCoord aInitialThumbPos); + + /** + * Handler for events which should not be intercepted by the touch listener. + */ + nsEventStatus HandleInputEvent( + const InputData& aEvent, + const ScreenToParentLayerMatrix4x4& aTransformToApzc); + + /** + * Handler for gesture events. + * Currently some gestures are detected in GestureEventListener that calls + * APZC back through this handler in order to avoid recursive calls to + * APZC::HandleInputEvent() which is supposed to do the work for + * ReceiveInputEvent(). + */ + nsEventStatus HandleGestureEvent(const InputData& aEvent); + + /** + * Start autoscrolling this APZC, anchored at the provided location. + */ + void StartAutoscroll(const ScreenPoint& aAnchorLocation); + + /** + * Stop autoscrolling this APZC. + */ + void StopAutoscroll(); + + /** + * Populates the provided object (if non-null) with the scrollable guid of + * this apzc. + */ + void GetGuid(ScrollableLayerGuid* aGuidOut) const; + + /** + * Returns the scrollable guid of this apzc. + */ + ScrollableLayerGuid GetGuid() const; + + /** + * Returns true if this APZC instance is for the layer identified by the guid. + */ + bool Matches(const ScrollableLayerGuid& aGuid); + + /** + * Returns true if the tree manager of this APZC is the same as the one + * passed in. + */ + bool HasTreeManager(const APZCTreeManager* aTreeManager) const; + + void StartAnimation(AsyncPanZoomAnimation* aAnimation); + + /** + * Cancels any currently running animation. + * aFlags is a bit-field to provide specifics of how to cancel the animation. + * See CancelAnimationFlags. + */ + void CancelAnimation(CancelAnimationFlags aFlags = Default); + + /** + * Clear any overscroll on this APZC. + */ + void ClearOverscroll(); + void ClearPhysicalOverscroll(); + + /** + * Returns whether this APZC is for an element marked with the 'scrollgrab' + * attribute. + */ + bool HasScrollgrab() const { return mScrollMetadata.GetHasScrollgrab(); } + + /** + * Returns whether this APZC has scroll snap points. + */ + bool HasScrollSnapping() const { + return mScrollMetadata.GetSnapInfo().HasScrollSnapping(); + } + + /** + * Returns whether this APZC has room to be panned (in any direction). + */ + bool IsPannable() const; + + /** + * Returns whether this APZC represents a scroll info layer. + */ + bool IsScrollInfoLayer() const; + + /** + * Returns true if the APZC has been flung with a velocity greater than the + * stop-on-tap fling velocity threshold (which is pref-controlled). + */ + bool IsFlingingFast() const; + + /** + * Returns whether this APZC is currently autoscrolling. + */ + bool IsAutoscroll() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mState == AUTOSCROLL; + } + + /** + * Returns the identifier of the touch in the last touch event processed by + * this APZC. This should only be called when the last touch event contained + * only one touch. + */ + int32_t GetLastTouchIdentifier() const; + + /** + * Returns the matrix that transforms points from global screen space into + * this APZC's ParentLayer space. + * To respect the lock ordering, mRecursiveMutex must NOT be held when calling + * this function (since this function acquires the tree lock). + */ + ScreenToParentLayerMatrix4x4 GetTransformToThis() const; + + /** + * Convert the vector |aVector|, rooted at the point |aAnchor|, from + * this APZC's ParentLayer coordinates into screen coordinates. + * The anchor is necessary because with 3D tranforms, the location of the + * vector can affect the result of the transform. + * To respect the lock ordering, mRecursiveMutex must NOT be held when calling + * this function (since this function acquires the tree lock). + */ + ScreenPoint ToScreenCoordinates(const ParentLayerPoint& aVector, + const ParentLayerPoint& aAnchor) const; + + /** + * Convert the vector |aVector|, rooted at the point |aAnchor|, from + * screen coordinates into this APZC's ParentLayer coordinates. + * The anchor is necessary because with 3D tranforms, the location of the + * vector can affect the result of the transform. + * To respect the lock ordering, mRecursiveMutex must NOT be held when calling + * this function (since this function acquires the tree lock). + */ + ParentLayerPoint ToParentLayerCoordinates(const ScreenPoint& aVector, + const ScreenPoint& aAnchor) const; + + /** + * Same as above, but uses an ExternalPoint as the anchor. + */ + ParentLayerPoint ToParentLayerCoordinates(const ScreenPoint& aVector, + const ExternalPoint& aAnchor) const; + + /** + * Combines an offset defined as an external point, with a window-relative + * offset to give an absolute external point. + */ + static ExternalPoint ToExternalPoint(const ExternalPoint& aScreenOffset, + const ScreenPoint& aScreenPoint); + + /** + * Gets a vector where the head is the given point, and the tail is + * the touch start position. + */ + ScreenPoint PanVector(const ExternalPoint& aPos) const; + + // Return whether or not a wheel event will be able to scroll in either + // direction. + bool CanScroll(const InputData& aEvent) const; + + // Return the directions in which this APZC allows handoff (as governed by + // overscroll-behavior). + ScrollDirections GetAllowedHandoffDirections() const; + + // Return the directions in which this APZC allows overscrolling. + ScrollDirections GetOverscrollableDirections() const; + + // Return whether or not a scroll delta will be able to scroll in either + // direction. + bool CanScroll(const ParentLayerPoint& aDelta) const; + + // Return whether or not a scroll delta will be able to scroll in either + // direction with wheel. + bool CanScrollWithWheel(const ParentLayerPoint& aDelta) const; + + // Return whether or not there is room to scroll this APZC + // in the given direction. + bool CanScroll(ScrollDirection aDirection) const; + + // Return the directions in which this APZC is able to scroll. + SideBits ScrollableDirections() const; + + // Return true if there is room to scroll along with moving the dynamic + // toolbar. + // + // NOTE: This function should be used only for the root content APZC. + bool CanVerticalScrollWithDynamicToolbar() const; + + // Return true if there is room to scroll downwards. + bool CanScrollDownwards() const; + + /** + * Convert a point on the scrollbar from this APZC's ParentLayer coordinates + * to OuterCSS coordinates relative to the beginning of the scroll track. + * Only the component in the direction of scrolling is returned. + */ + OuterCSSCoord ConvertScrollbarPoint(const ParentLayerPoint& aScrollbarPoint, + const ScrollbarData& aThumbData) const; + + void NotifyMozMouseScrollEvent(const nsString& aString) const; + + bool OverscrollBehaviorAllowsSwipe() const; + + //|Metrics()| and |Metrics() const| are getter functions that both return + // mScrollMetadata.mMetrics + + const FrameMetrics& Metrics() const; + FrameMetrics& Metrics(); + + /** + * Get the GeckoViewMetrics to be sent to Gecko for the current composite. + */ + GeckoViewMetrics GetGeckoViewMetrics() const; + + // Helper function to compare root frame metrics and update them + // Returns true when the metrics have changed and were updated. + bool UpdateRootFrameMetricsIfChanged(GeckoViewMetrics& aMetrics); + + // Returns the cached current frame time. + SampleTime GetFrameTime() const; + + bool IsZero(const ParentLayerPoint& aPoint) const; + bool IsZero(ParentLayerCoord aCoord) const; + + bool FuzzyGreater(ParentLayerCoord aCoord1, ParentLayerCoord aCoord2) const; + + private: + // Get whether the horizontal content of the honoured target of auto-dir + // scrolling starts from right to left. If you don't know of auto-dir + // scrolling or what a honoured target means, + // @see mozilla::WheelDeltaAdjustmentStrategy + bool IsContentOfHonouredTargetRightToLeft(bool aHonoursRoot) const; + + protected: + // Protected destructor, to discourage deletion outside of Release(): + virtual ~AsyncPanZoomController(); + + /** + * Helper method for touches beginning. Sets everything up for panning and any + * multitouch gestures. + */ + nsEventStatus OnTouchStart(const MultiTouchInput& aEvent); + + /** + * Helper method for touches moving. Does any transforms needed when panning. + */ + nsEventStatus OnTouchMove(const MultiTouchInput& aEvent); + + /** + * Helper method for touches ending. Redraws the screen if necessary and does + * any cleanup after a touch has ended. + */ + nsEventStatus OnTouchEnd(const MultiTouchInput& aEvent); + + /** + * Helper method for touches being cancelled. Treated roughly the same as a + * touch ending (OnTouchEnd()). + */ + nsEventStatus OnTouchCancel(const MultiTouchInput& aEvent); + + /** + * Helper method for scales beginning. Distinct from the OnTouch* handlers in + * that this implies some outside implementation has determined that the user + * is pinching. + */ + nsEventStatus OnScaleBegin(const PinchGestureInput& aEvent); + + /** + * Helper method for scaling. As the user moves their fingers when pinching, + * this changes the scale of the page. + */ + nsEventStatus OnScale(const PinchGestureInput& aEvent); + + /** + * Helper method for scales ending. Redraws the screen if necessary and does + * any cleanup after a scale has ended. + */ + nsEventStatus OnScaleEnd(const PinchGestureInput& aEvent); + + /** + * Helper methods for handling pan events. + */ + nsEventStatus OnPanMayBegin(const PanGestureInput& aEvent); + nsEventStatus OnPanCancelled(const PanGestureInput& aEvent); + nsEventStatus OnPanBegin(const PanGestureInput& aEvent); + enum class FingersOnTouchpad { + Yes, + No, + }; + nsEventStatus OnPan(const PanGestureInput& aEvent, + FingersOnTouchpad aFingersOnTouchpad); + nsEventStatus OnPanEnd(const PanGestureInput& aEvent); + nsEventStatus OnPanMomentumStart(const PanGestureInput& aEvent); + nsEventStatus OnPanMomentumEnd(const PanGestureInput& aEvent); + nsEventStatus HandleEndOfPan(); + nsEventStatus OnPanInterrupted(const PanGestureInput& aEvent); + + /** + * Helper methods for handling scroll wheel events. + */ + nsEventStatus OnScrollWheel(const ScrollWheelInput& aEvent); + + /** + * Gets the scroll wheel delta's values in parent-layer pixels from the + * original delta's values of a wheel input. + */ + ParentLayerPoint GetScrollWheelDelta(const ScrollWheelInput& aEvent) const; + + /** + * This function is like GetScrollWheelDelta(aEvent). + * The difference is the four added parameters provide values as alternatives + * to the original wheel input's delta values, so |aEvent|'s delta values are + * ignored in this function, we only use some other member variables and + * functions of |aEvent|. + */ + ParentLayerPoint GetScrollWheelDelta(const ScrollWheelInput& aEvent, + double aDeltaX, double aDeltaY, + double aMultiplierX, + double aMultiplierY) const; + + /** + * This deleted function is used for: + * 1. avoiding accidental implicit value type conversions of input delta + * values when callers intend to call the above function; + * 2. decoupling the manual relationship between the delta value type and the + * above function. If by any chance the defined delta value type in + * ScrollWheelInput has changed, this will automatically result in build + * time failure, so we can learn of it the first time and accordingly + * redefine those parameters' value types in the above function. + */ + template + ParentLayerPoint GetScrollWheelDelta(ScrollWheelInput&, T, T, T, T) = delete; + + /** + * Helper methods for handling keyboard events. + */ + nsEventStatus OnKeyboard(const KeyboardInput& aEvent); + + CSSPoint GetKeyboardDestination(const KeyboardScrollAction& aAction) const; + + // Returns the corresponding ScrollSnapFlags for the given |aAction|. + // See https://drafts.csswg.org/css-scroll-snap/#scroll-types + ScrollSnapFlags GetScrollSnapFlagsForKeyboardAction( + const KeyboardScrollAction& aAction) const; + + /** + * Helper methods for long press gestures. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY + nsEventStatus OnLongPress(const TapGestureInput& aEvent); + nsEventStatus OnLongPressUp(const TapGestureInput& aEvent); + + /** + * Helper method for single tap gestures. + */ + nsEventStatus OnSingleTapUp(const TapGestureInput& aEvent); + + /** + * Helper method for a single tap confirmed. + */ + nsEventStatus OnSingleTapConfirmed(const TapGestureInput& aEvent); + + /** + * Helper method for double taps. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY + nsEventStatus OnDoubleTap(const TapGestureInput& aEvent); + + /** + * Helper method for double taps where the double-tap gesture is disabled. + */ + nsEventStatus OnSecondTap(const TapGestureInput& aEvent); + + /** + * Helper method to cancel any gesture currently going to Gecko. Used + * primarily when a user taps the screen over some clickable content but then + * pans down instead of letting go (i.e. to cancel a previous touch so that a + * new one can properly take effect. + */ + nsEventStatus OnCancelTap(const TapGestureInput& aEvent); + + /** + * The following five methods modify the scroll offset. For the APZC + * representing the RCD-RSF, they also recalculate the offset of the layout + * viewport. + */ + + /** + * Scroll the scroll frame to an X,Y offset. + */ + void SetVisualScrollOffset(const CSSPoint& aOffset); + + /** + * Scroll the scroll frame to an X,Y offset, clamping the resulting scroll + * offset to the scroll range. + */ + void ClampAndSetVisualScrollOffset(const CSSPoint& aOffset); + + /** + * Scroll the scroll frame by an X,Y offset. + * The resulting scroll offset is not clamped to the scrollable rect; + * the caller must ensure it stays within range. + */ + void ScrollBy(const CSSPoint& aOffset); + + /** + * Scroll the scroll frame by an X,Y offset, clamping the resulting + * scroll offset to the scroll range. + */ + void ScrollByAndClamp(const CSSPoint& aOffset); + + /** + * Scales the viewport by an amount (note that it multiplies this scale in to + * the current scale, it doesn't set it to |aScale|). Also considers a focus + * point so that the page zooms inward/outward from that point. + */ + void ScaleWithFocus(float aScale, const CSSPoint& aFocus); + + /** + * Schedules a composite on the compositor thread. + */ + void ScheduleComposite(); + + /** + * Schedules a composite, and if enough time has elapsed since the last + * paint, a paint. + */ + void ScheduleCompositeAndMaybeRepaint(); + + /** + * Gets the start point of the current touch. + * This only makes sense if a touch is currently happening and OnTouchMove() + * or the equivalent for pan gestures is being invoked. + */ + ParentLayerPoint PanStart() const; + + /** + * Gets a vector of the velocities of each axis. + */ + const ParentLayerPoint GetVelocityVector() const; + + /** + * Sets the velocities of each axis. + */ + void SetVelocityVector(const ParentLayerPoint& aVelocityVector); + + /** + * Gets the first touch point from a MultiTouchInput. This gets only + * the first one and assumes the rest are either missing or not relevant. + */ + ParentLayerPoint GetFirstTouchPoint(const MultiTouchInput& aEvent); + + /** + * Gets the relevant point in the event + * (eg. first touch, or pinch focus point) of the given InputData. + */ + ExternalPoint GetExternalPoint(const InputData& aEvent); + + /** + * Gets the relevant point in the event, in external screen coordinates. + */ + ExternalPoint GetFirstExternalTouchPoint(const MultiTouchInput& aEvent); + + /** + * Gets the amount by which this APZC is overscrolled along both axes. + */ + ParentLayerPoint GetOverscrollAmount() const; + + private: + // Internal version of GetOverscrollAmount() which does not set + // the test async properties. + ParentLayerPoint GetOverscrollAmountInternal() const; + + protected: + /** + * Returns SideBits where this APZC is overscrolled. + */ + SideBits GetOverscrollSideBits() const; + + /** + * Restore the amount by which this APZC is overscrolled along both axes + * to the specified amount. This is for test-related use; overscrolling + * as a result of user input should happen via OverscrollBy(). + */ + void RestoreOverscrollAmount(const ParentLayerPoint& aOverscroll); + + /** + * Sets the panning state basing on the pan direction angle and current + * touch-action value. + */ + void HandlePanningWithTouchAction(double angle); + + /** + * Sets the panning state ignoring the touch action value. + */ + void HandlePanning(double angle); + + /** + * Update the panning state and axis locks. + */ + void HandlePanningUpdate(const ScreenPoint& aDelta); + + /** + * Set and update the pinch lock + */ + void HandlePinchLocking(const PinchGestureInput& aEvent); + + /** + * Sets up anything needed for panning. This takes us out of the "TOUCHING" + * state and starts actually panning us. We provide the physical pixel + * position of the start point so that the pan gesture is calculated + * regardless of if the window/GeckoView moved during the pan. + */ + nsEventStatus StartPanning(const ExternalPoint& aStartPoint, + const TimeStamp& aEventTime); + + /** + * Wrapper for Axis::UpdateWithTouchAtDevicePoint(). Calls this function for + * both axes and factors in the time delta from the last update. + */ + void UpdateWithTouchAtDevicePoint(const MultiTouchInput& aEvent); + + /** + * Does any panning required due to a new touch event. + */ + void TrackTouch(const MultiTouchInput& aEvent); + + /** + * Register the start of a touch or pan gesture at the given position and + * time. + */ + void StartTouch(const ParentLayerPoint& aPoint, TimeStamp aTimestamp); + + /** + * Register the end of a touch or pan gesture at the given time. + */ + void EndTouch(TimeStamp aTimestamp, Axis::ClearAxisLock aClearAxisLock); + + /** + * Utility function to send updated FrameMetrics to Gecko so that it can paint + * the displayport area. Calls into GeckoContentController to do the actual + * work. This call will use the current metrics. If this function is called + * from a non-main thread, it will redispatch itself to the main thread, and + * use the latest metrics during the redispatch. + */ + void RequestContentRepaint( + RepaintUpdateType aUpdateType = RepaintUpdateType::eUserAction); + + /** + * Send Metrics() to Gecko to trigger a repaint. This function may filter + * duplicate calls with the same metrics. This function must be called on the + * main thread. + */ + void RequestContentRepaint(const ParentLayerPoint& aVelocity, + const ScreenMargin& aDisplayportMargins, + RepaintUpdateType aUpdateType); + + /** + * Gets the current frame metrics. This is *not* the Gecko copy stored in the + * layers code. + */ + const FrameMetrics& GetFrameMetrics() const; + + /** + * Gets the current scroll metadata. This is *not* the Gecko copy stored in + * the layers code/ + */ + const ScrollMetadata& GetScrollMetadata() const; + + /** + * Gets the pointer to the apzc tree manager. All the access to tree manager + * should be made via this method and not via private variable since this + * method ensures that no lock is set. + */ + APZCTreeManager* GetApzcTreeManager() const; + + void AssertOnSamplerThread() const; + void AssertOnUpdaterThread() const; + + /** + * Convert ScreenPoint relative to the screen to LayoutDevicePoint relative + * to the parent document. This excludes the transient compositor transform. + * NOTE: This must be converted to LayoutDevicePoint relative to the child + * document before sending over IPC to a child process. + */ + Maybe ConvertToGecko(const ScreenIntPoint& aPoint); + + enum AxisLockMode { + FREE, /* No locking at all */ + STANDARD, /* Default axis locking mode that remains locked until pan ends */ + STICKY, /* Allow lock to be broken, with hysteresis */ + DOMINANT_AXIS, /* Only allow movement on one axis */ + }; + + static AxisLockMode GetAxisLockMode(); + + bool UsingStatefulAxisLock() const; + + enum PinchLockMode { + PINCH_FREE, /* No locking at all */ + PINCH_STANDARD, /* Default pinch locking mode that remains locked until + pinch gesture ends*/ + PINCH_STICKY, /* Allow lock to be broken, with hysteresis */ + }; + + static PinchLockMode GetPinchLockMode(); + + // Helper function for OnSingleTapUp(), OnSingleTapConfirmed(), and + // OnLongPressUp(). + nsEventStatus GenerateSingleTap(GeckoContentController::TapType aType, + const ScreenIntPoint& aPoint, + mozilla::Modifiers aModifiers); + + // Common processing at the end of a touch block. + void OnTouchEndOrCancel(); + + LayersId mLayersId; + RefPtr mCompositorController; + + /* Access to the following two fields is protected by the mRefPtrMonitor, + since they are accessed on the UI thread but can be cleared on the + updater thread. */ + RefPtr mGeckoContentController; + RefPtr mGestureEventListener; + mutable Monitor mRefPtrMonitor MOZ_UNANNOTATED; + + // This is a raw pointer to avoid introducing a reference cycle between + // AsyncPanZoomController and APZCTreeManager. Since these objects don't + // live on the main thread, we can't use the cycle collector with them. + // The APZCTreeManager owns the lifetime of the APZCs, so nulling this + // pointer out in Destroy() will prevent accessing deleted memory. + Atomic mTreeManager; + + /* Utility functions that return a addrefed pointer to the corresponding + * fields. */ + already_AddRefed GetGeckoContentController() const; + already_AddRefed GetGestureEventListener() const; + + PlatformSpecificStateBase* GetPlatformSpecificState(); + + /** + * Convenience functions to get the corresponding fields of mZoomContraints + * while holding mRecursiveMutex. + */ + bool ZoomConstraintsAllowZoom() const; + bool ZoomConstraintsAllowDoubleTapZoom() const; + + protected: + // Both |mScrollMetadata| and |mLastContentPaintMetrics| are protected by the + // monitor. Do not read from or modify them without locking. + ScrollMetadata mScrollMetadata; + + // Protects |mScrollMetadata|, |mLastContentPaintMetrics|, |mState| and + // |mLastSnapTargetIds|. Before manipulating |mScrollMetadata|, + // |mLastContentPaintMetrics| or |mLastSnapTargetIds| the monitor should be + // held. When setting |mState|, either the SetState() function can be used, or + // the monitor can be held and then |mState| updated. + // IMPORTANT: See the note about lock ordering at the top of + // APZCTreeManager.h. This is mutable to allow entering it from 'const' + // methods; doing otherwise would significantly limit what methods could be + // 'const'. + // FIXME: Please keep in mind that due to some existing coupled relationships + // among the class members, we should be aware of indirect usage of the + // monitor-protected members. That is, although this monitor isn't required to + // be held before manipulating non-protected class members, some functions on + // those members might indirectly manipulate the protected members; in such + // cases, the monitor should still be held. Let's take mX.CanScroll for + // example: + // Axis::CanScroll(ParentLayerCoord) calls Axis::CanScroll() which calls + // Axis::GetPageLength() which calls Axis::GetFrameMetrics() which calls + // AsyncPanZoomController::GetFrameMetrics(), therefore, this monitor should + // be held before calling the CanScroll function of |mX| and |mY|. These + // coupled relationships bring us the burden of taking care of when the + // monitor should be held, so they should be decoupled in the future. + mutable RecursiveMutex mRecursiveMutex MOZ_UNANNOTATED; + + private: + // Metadata of the container layer corresponding to this APZC. This is + // stored here so that it is accessible from the UI/controller thread. + // These are the metrics at last content paint, the most recent + // values we were notified of in NotifyLayersUpdate(). Since it represents + // the Gecko state, it should be used as a basis for untransformation when + // sending messages back to Gecko. + ScrollMetadata mLastContentPaintMetadata; + FrameMetrics& mLastContentPaintMetrics; // for convenience, refers to + // mLastContentPaintMetadata.mMetrics + // The last content repaint request. + RepaintRequest mLastPaintRequestMetrics; + // The metrics that we expect content to have. This is updated when we + // request a content repaint, and when we receive a shadow layers update. + // This allows us to transform events into Gecko's coordinate space. + ExpectedGeckoMetrics mExpectedGeckoMetrics; + + // This holds important state from the Metrics() at previous times + // SampleCompositedAsyncTransform() was called. This will always have at least + // one item. mRecursiveMutex must be held when using or modifying this member. + // Samples should be inserted to the "back" of the deque and extracted from + // the "front". + std::deque mSampledState; + + // Groups state variables that are specific to a platform. + // Initialized on first use. + UniquePtr mPlatformSpecificState; + + // This flag is set to true when we are in a axis-locked pan as a result of + // the touch-action CSS property. + bool mPanDirRestricted; + + // This flag is set to true when we are in a pinch-locked state. ie: user + // is performing a two-finger pan rather than a pinch gesture + bool mPinchLocked; + + // Stores the pinch events that occured within a given timeframe. Used to + // calculate the focusChange and spanDistance within a fixed timeframe. + // RecentEventsBuffer is not threadsafe. Should only be accessed on the + // controller thread. + RecentEventsBuffer mPinchEventBuffer; + + // Most up-to-date constraints on zooming. These should always be reasonable + // values; for example, allowing a min zoom of 0.0 can cause very bad things + // to happen. Hold mRecursiveMutex when accessing this. + ZoomConstraints mZoomConstraints; + + // The last time the compositor has sampled the content transform for this + // frame. + SampleTime mLastSampleTime; + + // The last sample time at which we submitted a checkerboarding report. + SampleTime mLastCheckerboardReport; + + // Stores the previous focus point if there is a pinch gesture happening. Used + // to allow panning by moving multiple fingers (thus moving the focus point). + ParentLayerPoint mLastZoomFocus; + + // Stores the previous zoom level at which we last sent a ScaleGestureComplete + // notification. + CSSToParentLayerScale mLastNotifiedZoom; + + // Accessing mAnimation needs to be protected by mRecursiveMutex + RefPtr mAnimation; + + UniquePtr mOverscrollEffect; + + // Zoom animation id, used for zooming in WebRender. This should only be + // set on the APZC instance for the root content document (i.e. the one we + // support zooming on), and is only used if WebRender is enabled. The + // animation id itself refers to the transform animation id that was set on + // the stacking context in the WR display list. By changing the transform + // associated with this id, we can adjust the scaling that WebRender applies, + // thereby controlling the zoom. + Maybe mZoomAnimationId; + + // Position on screen where user first put their finger down. + ExternalPoint mStartTouch; + + // Accessing mScrollPayload needs to be protected by mRecursiveMutex + Maybe mScrollPayload; + + // Representing sampled scroll offset generation, this value is bumped up + // every time this APZC sampled new scroll offset. + APZScrollGeneration mScrollGeneration; + + friend class Axis; + + public: + Maybe NotifyScrollSampling(); + + /** + * Invoke |callable|, passing |mLastContentPaintMetrics| as argument, + * while holding the APZC lock required to access |mLastContentPaintMetrics|. + * This allows code outside of an AsyncPanZoomController method implementation + * to access |mLastContentPaintMetrics| without having to make a copy of it. + * Passes through the return value of |callable|. + */ + template + auto CallWithLastContentPaintMetrics(const Callable& callable) const + -> decltype(callable(mLastContentPaintMetrics)) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return callable(mLastContentPaintMetrics); + } + + void SetZoomAnimationId(const Maybe& aZoomAnimationId); + Maybe GetZoomAnimationId() const; + + /* =================================================================== + * The functions and members in this section are used to expose + * the current async transform state to callers. + */ + public: + /** + * Allows consumers of async transforms to specify for what purpose they are + * using the async transform: + * + * |eForHitTesting| is intended for hit-testing and other uses that need + * the most up-to-date transform, reflecting all events + * that have been processed so far, even if the transform + * is not yet reflected visually. + * |eForCompositing| is intended for the transform that should be reflected + * visually. + * + * For example, if an APZC has metrics with the mForceDisableApz flag set, + * then the |eForCompositing| async transform will be empty, while the + * |eForHitTesting| async transform will reflect processed input events + * regardless of mForceDisableApz. + */ + enum AsyncTransformConsumer { + eForHitTesting, + eForCompositing, + }; + + /** + * Get the current scroll offset of the scrollable frame corresponding + * to this APZC, including the effects of any asynchronous panning and + * zooming, in ParentLayer pixels. + */ + ParentLayerPoint GetCurrentAsyncScrollOffset( + AsyncTransformConsumer aMode) const; + + /** + * Get the current visual viewport of the scrollable frame corresponding + * to this APZC, including the effects of any asynchronous panning and + * zooming, in CSS pixels. + */ + CSSRect GetCurrentAsyncVisualViewport(AsyncTransformConsumer aMode) const; + + /** + * Return a visual effect that reflects this apzc's + * overscrolled state, if any. + */ + AsyncTransformComponentMatrix GetOverscrollTransform( + AsyncTransformConsumer aMode) const; + + /** + * Returns the incremental transformation corresponding to the async pan/zoom + * in progress. That is, when this transform is multiplied with the layer's + * existing transform, it will make the layer appear with the desired pan/zoom + * amount. + * The transform can have both scroll and zoom components; the caller can + * request just one or the other, or both, via the |aComponents| parameter. + * When only the eLayout component is requested, the returned translation + * should really be a LayerPoint, rather than a ParentLayerPoint, as it will + * not be scaled by the asynchronous zoom. + * |aMode| specifies whether the async transform is queried for the purpose of + * hit testing (eHitTesting) in which case the latest values from |Metrics()| + * are used, or for compositing (eCompositing) in which case a sampled value + * from |mSampledState| is used. + * |aSampleIndex| specifies which sample in |mSampledState| to use. + */ + AsyncTransform GetCurrentAsyncTransform( + AsyncTransformConsumer aMode, + AsyncTransformComponents aComponents = LayoutAndVisual, + std::size_t aSampleIndex = 0) const; + + /** + * Returns the same transform as GetCurrentAsyncTransform(), but includes + * any transform due to axis over-scroll. + */ + AsyncTransformComponentMatrix GetCurrentAsyncTransformWithOverscroll( + AsyncTransformConsumer aMode, + AsyncTransformComponents aComponents = LayoutAndVisual, + std::size_t aSampleIndex = 0) const; + + AutoTArray GetSampledScrollOffsets() const; + + /** + * Returns the "zoom" bits of the transform. This includes both the rasterized + * (layout device to layer scale) and async (layer scale to parent layer + * scale) components of the zoom. + */ + LayoutDeviceToParentLayerScale GetCurrentPinchZoomScale( + AsyncTransformConsumer aMode) const; + + ParentLayerRect GetCompositionBounds() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mScrollMetadata.GetMetrics().GetCompositionBounds(); + } + + LayoutDeviceToLayerScale GetCumulativeResolution() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mScrollMetadata.GetMetrics().GetCumulativeResolution(); + } + + // Returns the delta for the given InputData. + ParentLayerPoint GetDeltaForEvent(const InputData& aEvent) const; + + /** + * Get the current scroll range of the scrollable frame coreesponding to this + * APZC. + */ + CSSRect GetCurrentScrollRangeInCssPixels() const; + + private: + /** + * Advances to the next sample, if there is one, the list of sampled states + * stored in mSampledState. This will make the result of + * |GetCurrentAsyncTransform(eForCompositing)| and similar functions reflect + * the async scroll offset and zoom of the next sample. See also + * SampleCompositedAsyncTransform which creates the samples. + */ + void AdvanceToNextSample(); + + /** + * Samples the composited async transform, storing the result into + * mSampledState. This will make the result of + * |GetCurrentAsyncTransform(eForCompositing)| and similar functions reflect + * the async scroll offset and zoom stored in |Metrics()| when the sample + * is activated via some future call to |AdvanceToNextSample|. + * + * Returns true if the newly sampled value is different from the last + * sampled value. + */ + bool SampleCompositedAsyncTransform( + const RecursiveMutexAutoLock& aProofOfLock); + + /** + * Updates the sample at the front of mSampledState with the latest + * metrics. This makes the result of + * |GetCurrentAsyncTransform(eForCompositing)| reflect the current Metrics(). + */ + void ResampleCompositedAsyncTransform( + const RecursiveMutexAutoLock& aProofOfLock); + + /* + * Helper functions to query the async layout viewport, scroll offset, and + * zoom either directly from |Metrics()|, or from cached variables that + * store the required value from the last time it was sampled by calling + * SampleCompositedAsyncTransform(), depending on who is asking. + */ + CSSRect GetEffectiveLayoutViewport(AsyncTransformConsumer aMode, + const RecursiveMutexAutoLock& aProofOfLock, + std::size_t aSampleIndex = 0) const; + CSSPoint GetEffectiveScrollOffset(AsyncTransformConsumer aMode, + const RecursiveMutexAutoLock& aProofOfLock, + std::size_t aSampleIndex = 0) const; + CSSToParentLayerScale GetEffectiveZoom( + AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock, + std::size_t aSampleIndex = 0) const; + + /** + * Returns the visible portion of the content scrolled by this APZC, in + * CSS pixels. The caller must have acquired the mRecursiveMutex lock. + */ + CSSRect GetVisibleRect(const RecursiveMutexAutoLock& aProofOfLock) const; + + /** + * Returns a pair of displacements both in logical/physical units for + * |aEvent|. + */ + std::tuple GetDisplacementsForPanGesture( + const PanGestureInput& aEvent); + + private: + friend class AutoApplyAsyncTestAttributes; + + bool SuppressAsyncScrollOffset() const; + + /** + * Applies |mTestAsyncScrollOffset| and |mTestAsyncZoom| to this + * AsyncPanZoomController. Calls |SampleCompositedAsyncTransform| to ensure + * that the GetCurrentAsync* functions consider the test offset and zoom in + * their computations. + */ + void ApplyAsyncTestAttributes(const RecursiveMutexAutoLock& aProofOfLock); + + /** + * Sets this AsyncPanZoomController's FrameMetrics to |aPrevFrameMetrics| and + * calls |SampleCompositedAsyncTransform| to unapply any test values applied + * by |ApplyAsyncTestAttributes|. + */ + void UnapplyAsyncTestAttributes(const RecursiveMutexAutoLock& aProofOfLock, + const FrameMetrics& aPrevFrameMetrics, + const ParentLayerPoint& aPrevOverscroll); + + /* =================================================================== + * The functions and members in this section are used to manage + * the state that tracks what this APZC is doing with the input events. + */ + protected: + enum PanZoomState { + NOTHING, /* no touch-start events received */ + FLING, /* all touches removed, but we're still scrolling page */ + TOUCHING, /* one touch-start event received */ + + PANNING, /* panning the frame */ + PANNING_LOCKED_X, /* touch-start followed by move (i.e. panning with axis + lock) X axis */ + PANNING_LOCKED_Y, /* as above for Y axis */ + + PAN_MOMENTUM, /* like PANNING, but controlled by momentum PanGestureInput + events */ + + PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */ + ANIMATING_ZOOM, /* animated zoom to a new rect */ + OVERSCROLL_ANIMATION, /* Spring-based animation used to relieve overscroll + once the finger is lifted. */ + SMOOTH_SCROLL, /* Smooth scrolling to destination, with physics + controlled by prefs specific to the scroll origin. */ + SMOOTHMSD_SCROLL, /* SmoothMSD scrolling to destination. Used by + CSSOM-View smooth scroll-behavior */ + WHEEL_SCROLL, /* Smooth scrolling to a destination for a wheel event. */ + KEYBOARD_SCROLL, /* Smooth scrolling to a destination for a keyboard event. + */ + AUTOSCROLL, /* Autoscroll animation. */ + SCROLLBAR_DRAG /* Async scrollbar drag. */ + }; + // This is in theory protected by |mRecursiveMutex|; that is, it should be + // held whenever this is updated. In practice though... see bug 897017. + PanZoomState mState; + + AxisX mX; + AxisY mY; + + /** + * Returns wheter the given input state is a user pan-gesture. + * + * Note: momentum pan gesture states are not considered a panning state. + */ + static bool IsPanningState(PanZoomState aState); + + /** + * Returns wheter a delayed transform end is queued. + */ + bool IsDelayedTransformEndSet(); + + /** + * Returns wheter a delayed transform end is queued. + */ + void SetDelayedTransformEnd(bool aDelayedTransformEnd); + + /** + * Returns whether the specified PanZoomState does not need to be reset when + * a scroll offset update is processed. + */ + static bool CanHandleScrollOffsetUpdate(PanZoomState aState); + + /** + * Determine whether a main-thread scroll offset update should result in + * a call to CancelAnimation() (which interrupts in-progress animations and + * gestures). + * + * If the update is a relative update, |aRelativeDelta| contains its amount. + * If the update is not a relative update, GetMetrics() should already reflect + * the new offset at the time of the call. + */ + bool ShouldCancelAnimationForScrollUpdate( + const Maybe& aRelativeDelta); + + private: + friend class StateChangeNotificationBlocker; + /** + * A counter of how many StateChangeNotificationBlockers are active. + * A non-zero count will prevent state change notifications from + * being dispatched. Only code that holds mRecursiveMutex should touch this. + */ + int mNotificationBlockers; + + /** + * Helper to set the current state, without content controller events + * for the state change. This is useful in cases where the content + * controller events may need to be delayed. + */ + PanZoomState SetStateNoContentControllerDispatch(PanZoomState aNewState); + + /** + * Helper to set the current state. Holds mRecursiveMutex before actually + * setting it and fires content controller events based on state changes. + * Always set the state using this call, do not set it directly. + */ + void SetState(PanZoomState aNewState); + /** + * Helper for getting the current state which acquires mRecursiveMutex + * before accessing the field. + */ + PanZoomState GetState() const; + /** + * Fire content controller notifications about state changes, assuming no + * StateChangeNotificationBlocker has been activated. + */ + void DispatchStateChangeNotification(PanZoomState aOldState, + PanZoomState aNewState); + /** + * Internal helpers for checking general state of this apzc. + */ + bool IsInTransformingState() const; + static bool IsTransformingState(PanZoomState aState); + + /* =================================================================== + * The functions and members in this section are used to manage + * blocks of touch events and the state needed to deal with content + * listeners. + */ + public: + /** + * Flush a repaint request if one is needed, without throttling it with the + * paint throttler. + */ + void FlushRepaintForNewInputBlock(); + + /** + * Given an input event and the touch block it belongs to, check if the + * event can lead to a panning/zooming behavior. + * This is used for logic related to the pointer events spec (figuring out + * when to dispatch the pointercancel event), as well as an input to the + * computation of the APZHandledResult for the event (used on Android to + * govern dynamic toolbar and pull-to-refresh behaviour). + */ + PointerEventsConsumableFlags ArePointerEventsConsumable( + TouchBlockState* aBlock, const MultiTouchInput& aInput); + + /** + * Clear internal state relating to touch input handling. + */ + void ResetTouchInputState(); + + /** + Clear internal state relating to pan gesture input handling. + */ + void ResetPanGestureInputState(); + + /** + * Gets a ref to the input queue that is shared across the entire tree + * manager. + */ + const RefPtr& GetInputQueue() const; + + private: + void CancelAnimationAndGestureState(); + + RefPtr mInputQueue; + InputBlockState* GetCurrentInputBlock() const; + TouchBlockState* GetCurrentTouchBlock() const; + bool HasReadyTouchBlock() const; + + PanGestureBlockState* GetCurrentPanGestureBlock() const; + PinchGestureBlockState* GetCurrentPinchGestureBlock() const; + + private: + /* =================================================================== + * The functions and members in this section are used to manage + * fling animations, smooth scroll animations, and overscroll + * during a fling or smooth scroll. + */ + public: + /** + * Attempt a fling with the velocity specified in |aHandoffState|. + * |aHandoffState.mIsHandoff| should be true iff. the fling was handed off + * from a previous APZC, and determines whether acceleration is applied + * to the fling. + * We only accept the fling in the direction(s) in which we are pannable. + * Returns the "residual velocity", i.e. the portion of + * |aHandoffState.mVelocity| that this APZC did not consume. + */ + ParentLayerPoint AttemptFling(const FlingHandoffState& aHandoffState); + + ParentLayerPoint AdjustHandoffVelocityForOverscrollBehavior( + ParentLayerPoint& aHandoffVelocity) const; + + private: + friend class StackScrollerFlingAnimation; + friend class AutoscrollAnimation; + template + friend class GenericFlingAnimation; + friend class AndroidFlingPhysics; + friend class DesktopFlingPhysics; + friend class OverscrollAnimation; + friend class SmoothMsdScrollAnimation; + friend class GenericScrollAnimation; + friend class WheelScrollAnimation; + friend class ZoomAnimation; + + friend class GenericOverscrollEffect; + friend class WidgetOverscrollEffect; + friend struct apz::AsyncScrollThumbTransformer; + + FlingAccelerator mFlingAccelerator; + + // Indicates if the repaint-during-pinch timer is currently set + bool mPinchPaintTimerSet; + + // Indicates a delayed transform end notification is queued, and the + // transform-end timer is currently set. mRecursiveMutex must be held + // when using or modifying this member. + bool mDelayedTransformEnd; + + // Deal with overscroll resulting from a fling animation. This is only ever + // called on APZC instances that were actually performing a fling. + // The overscroll is handled by trying to hand the fling off to an APZC + // later in the handoff chain, or if there are no takers, continuing the + // fling and entering an overscrolled state. + void HandleFlingOverscroll( + const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits, + const RefPtr& aOverscrollHandoffChain, + const RefPtr& aScrolledApzc); + + void HandleSmoothScrollOverscroll(const ParentLayerPoint& aVelocity, + SideBits aOverscrollSideBits); + + // Start an overscroll animation with the given initial velocity. + void StartOverscrollAnimation(const ParentLayerPoint& aVelocity, + SideBits aOverscrollSideBits); + + // Start a smooth-scrolling animation to the given destination, with physics + // based on the prefs for the indicated origin. + void SmoothScrollTo(const CSSPoint& aDestination, + const ScrollOrigin& aOrigin); + + // Start a smooth-scrolling animation to the given destination, with MSD + // physics that is suited for scroll-snapping. + void SmoothMsdScrollTo(CSSSnapTarget&& aDestination, + ScrollTriggeredByScript aTriggeredByScript); + + // Returns whether overscroll is allowed during an event. + bool AllowScrollHandoffInCurrentBlock() const; + + // Invoked by the pinch repaint timer. + void DoDelayedRequestContentRepaint(); + + // Invoked by the on pan-end handler to ensure that scrollend is only + // fired once when a momentum pan or scroll snap is triggered as a + // result of the pan gesture. + void DoDelayedTransformEndNotification(PanZoomState aOldState); + + // Compute the number of ParentLayer pixels per (Screen) inch at the given + // point and in the given direction. + float ComputePLPPI(ParentLayerPoint aPoint, + ParentLayerPoint aDirection) const; + + Maybe GetCurrentAnimationDestination( + const RecursiveMutexAutoLock& aProofOfLock) const; + + /* =================================================================== + * The functions and members in this section are used to make ancestor chains + * out of APZC instances. These chains can only be walked or manipulated + * while holding the lock in the associated APZCTreeManager instance. + */ + public: + void SetParent(AsyncPanZoomController* aParent) { mParent = aParent; } + + AsyncPanZoomController* GetParent() const { return mParent; } + + /* Returns true if there is no APZC higher in the tree with the same + * layers id. + */ + bool HasNoParentWithSameLayersId() const { + return !mParent || (mParent->mLayersId != mLayersId); + } + + bool IsRootForLayersId() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mScrollMetadata.IsLayersIdRoot(); + } + + bool IsRootContent() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return Metrics().IsRootContent(); + } + + private: + // |mTreeManager| belongs in this section but it's declaration is a bit + // further above due to initialization-order constraints. + + RefPtr mParent; + + /* =================================================================== + * The functions and members in this section are used for scrolling, + * including handing off scroll to another APZC, and overscrolling. + */ + + ScrollableLayerGuid::ViewID GetScrollId() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return Metrics().GetScrollId(); + } + + public: + ScrollableLayerGuid::ViewID GetScrollHandoffParentId() const { + return mScrollMetadata.GetScrollParentId(); + } + + /** + * Attempt to scroll in response to a touch-move from |aStartPoint| to + * |aEndPoint|, which are in our (transformed) screen coordinates. + * Due to overscroll handling, there may not actually have been a touch-move + * at these points, but this function will scroll as if there had been. + * If this attempt causes overscroll (i.e. the layer cannot be scrolled + * by the entire amount requested), the overscroll is passed back to the + * tree manager via APZCTreeManager::DispatchScroll(). If the tree manager + * does not find an APZC further in the handoff chain to accept the + * overscroll, and this APZC is pannable, this APZC enters an overscrolled + * state. + * |aOverscrollHandoffChain| and |aOverscrollHandoffChainIndex| are used by + * the tree manager to keep track of which APZC to hand off the overscroll + * to; this function increments the chain and the index and passes it on to + * APZCTreeManager::DispatchScroll() in the event of overscroll. + * Returns true iff. this APZC, or an APZC further down the + * handoff chain, accepted the scroll (possibly entering an overscrolled + * state). If this returns false, the caller APZC knows that it should enter + * an overscrolled state itself if it can. + * aStartPoint and aEndPoint are modified depending on how much of the + * scroll gesture was consumed by APZCs in the handoff chain. + */ + bool AttemptScroll(ParentLayerPoint& aStartPoint, ParentLayerPoint& aEndPoint, + OverscrollHandoffState& aOverscrollHandoffState); + + void FlushRepaintForOverscrollHandoff(); + + /** + * If overscrolled, start a snap-back animation and return true. Even if not + * overscrolled, this function tries to snap back to if there's an applicable + * scroll snap point. + * Otherwise return false. + */ + bool SnapBackIfOverscrolled(); + + /** + * NOTE: Similar to above but this function doesn't snap back to the scroll + * snap point. + */ + bool SnapBackIfOverscrolledForMomentum(const ParentLayerPoint& aVelocity); + + /** + * Build the chain of APZCs along which scroll will be handed off when + * this APZC receives input events. + * + * Notes on lifetime and const-correctness: + * - The returned handoff chain is |const|, to indicate that it cannot be + * changed after being built. + * - When passing the chain to a function that uses it without storing it, + * pass it by reference-to-const (as in |const OverscrollHandoffChain&|). + * - When storing the chain, store it by RefPtr-to-const (as in + * |RefPtr|). This ensures the chain is + * kept alive. Note that queueing a task that uses the chain as an + * argument constitutes storing, as the task may outlive its queuer. + * - When passing the chain to a function that will store it, pass it as + * |const RefPtr&|. This allows the + * function to copy it into the |RefPtr| + * that will store it, while avoiding an unnecessary copy (and thus + * AddRef() and Release()) when passing it. + */ + RefPtr BuildOverscrollHandoffChain(); + + private: + /** + * A helper function for calling APZCTreeManager::DispatchScroll(). + * Guards against the case where the APZC is being concurrently destroyed + * (and thus mTreeManager is being nulled out). + */ + bool CallDispatchScroll(ParentLayerPoint& aStartPoint, + ParentLayerPoint& aEndPoint, + OverscrollHandoffState& aOverscrollHandoffState); + + void RecordScrollPayload(const TimeStamp& aTimeStamp); + + /** + * A helper function for overscrolling during panning. This is a wrapper + * around OverscrollBy() that also implements restrictions on entering + * overscroll based on the pan angle. + */ + void OverscrollForPanning(ParentLayerPoint& aOverscroll, + const ScreenPoint& aPanDistance); + + /** + * Try to overscroll by 'aOverscroll'. + * If we are pannable on a particular axis, that component of 'aOverscroll' + * is transferred to any existing overscroll. + */ + void OverscrollBy(ParentLayerPoint& aOverscroll); + + /* =================================================================== + * The functions and members in this section are used to maintain the + * area that this APZC instance is responsible for. This is used when + * hit-testing to see which APZC instance should handle touch events. + */ + public: + void SetAncestorTransform(const AncestorTransform& aAncestorTransform) { + mAncestorTransform = aAncestorTransform; + } + + Matrix4x4 GetAncestorTransform() const { + return mAncestorTransform.CombinedTransform(); + } + + bool AncestorTransformContainsPerspective() const { + return mAncestorTransform.ContainsPerspectiveTransform(); + } + + // Return the perspective transform component of the ancestor transform. + Matrix4x4 GetAncestorTransformPerspective() const { + return mAncestorTransform.GetPerspectiveTransform(); + } + + // Returns whether or not this apzc contains the given screen point within + // its composition bounds. + bool Contains(const ScreenIntPoint& aPoint) const; + + bool IsInOverscrollGutter(const ScreenPoint& aHitTestPoint) const; + bool IsInOverscrollGutter(const ParentLayerPoint& aHitTestPoint) const; + + bool IsOverscrolled() const; + + bool IsOverscrollAnimationRunning() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mState == OVERSCROLL_ANIMATION; + } + + // IsPhysicallyOverscrolled() checks whether the APZC is overscrolled + // by an overscroll effect which applies a transform to the APZC's contents. + bool IsPhysicallyOverscrolled() const; + + private: + bool IsInInvalidOverscroll() const; + + public: + bool IsInPanningState() const; + + // Returns whether being in the middle of a gesture. E.g., this APZC has + // started handling a pan gesture but hasn't yet received pan-end, etc. + bool IsInScrollingGesture() const; + + private: + /* This is the cumulative CSS transform for all the layers from (and + * including) the parent APZC down to (but excluding) this one, and excluding + * any perspective transforms. */ + AncestorTransform mAncestorTransform; + + /* =================================================================== + * The functions and members in this section are used for testing + * and assertion purposes only. + */ + public: + /** + * Gets whether this APZC has performed async key scrolling. + */ + bool TestHasAsyncKeyScrolled() const { return mTestHasAsyncKeyScrolled; } + + /** + * Set an extra offset for testing async scrolling. + */ + void SetTestAsyncScrollOffset(const CSSPoint& aPoint); + /** + * Set an extra offset for testing async scrolling. + */ + void SetTestAsyncZoom(const LayerToParentLayerScale& aZoom); + + LayersId GetLayersId() const { return mLayersId; } + + bool IsAsyncZooming() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mState == PINCHING || mState == ANIMATING_ZOOM; + } + + private: + // The timestamp of the latest touch start event. + TimeStamp mTouchStartTime; + // Used for interpolating touch events that cross the touch-start + // tolerance threshold. + struct TouchSample { + ExternalPoint mPosition; + TimeStamp mTimeStamp; + }; + // Information about the latest touch event. + // This is only populated when we're in the TOUCHING state + // (and thus the last touch event has only one touch point). + TouchSample mLastTouch; + // The time duration between mTouchStartTime and the touchmove event that + // started the pan (the touchmove event that transitioned this APZC from the + // TOUCHING state to one of the PANNING* states). Only valid while this APZC + // is in a panning state. + TimeDuration mTouchStartRestingTimeBeforePan; + Maybe mMinimumVelocityDuringPan; + // This variable needs to be protected by |mRecursiveMutex|. + ScrollSnapTargetIds mLastSnapTargetIds; + // Extra offset to add to the async scroll position for testing + CSSPoint mTestAsyncScrollOffset; + // Extra zoom to include in the aync zoom for testing + LayerToParentLayerScale mTestAsyncZoom; + uint8_t mTestAttributeAppliers; + // Flag to track whether or not this APZC has ever async key scrolled. + bool mTestHasAsyncKeyScrolled; + + /* =================================================================== + * The functions and members in this section are used for checkerboard + * recording. + */ + private: + // Helper function to update the in-progress checkerboard event, if any. + void UpdateCheckerboardEvent(const MutexAutoLock& aProofOfLock, + uint32_t aMagnitude); + + // Mutex protecting mCheckerboardEvent + Mutex mCheckerboardEventLock MOZ_UNANNOTATED; + // This is created when this APZC instance is first included as part of a + // composite. If a checkerboard event takes place, this is destroyed at the + // end of the event, and a new one is created on the next composite. + UniquePtr mCheckerboardEvent; + // This is used to track the total amount of time that we could reasonably + // be checkerboarding. Combined with other info, this allows us to + // meaningfully say how frequently users actually encounter checkerboarding. + PotentialCheckerboardDurationTracker mPotentialCheckerboardTracker; + + /* =================================================================== + * The functions in this section are used for CSS scroll snapping. + */ + + // If moving |aStartPosition| by |aDelta| should trigger scroll snapping, + // adjust |aDelta| to reflect the snapping (that is, make it a delta that will + // take us to the desired snap point). The delta is interpreted as being + // relative to |aStartPosition|, and if a target snap point is found, + // |aStartPosition| is also updated, to the value of the snap point. + // |aUnit| affects the snapping behaviour (see ScrollSnapUtils:: + // GetSnapPointForDestination). + // Returns true iff. a target snap point was found. + Maybe MaybeAdjustDeltaForScrollSnapping( + ScrollUnit aUnit, ScrollSnapFlags aSnapFlags, ParentLayerPoint& aDelta, + CSSPoint& aStartPosition); + + // A wrapper function of MaybeAdjustDeltaForScrollSnapping for + // ScrollWheelInput. + Maybe MaybeAdjustDeltaForScrollSnappingOnWheelInput( + const ScrollWheelInput& aEvent, ParentLayerPoint& aDelta, + CSSPoint& aStartPosition); + + Maybe MaybeAdjustDestinationForScrollSnapping( + const KeyboardInput& aEvent, CSSPoint& aDestination, + ScrollSnapFlags aSnapFlags); + + // Snap to a snap position nearby the current scroll position, if appropriate. + void ScrollSnap(ScrollSnapFlags aSnapFlags); + + // Snap to a snap position nearby the destination predicted based on the + // current velocity, if appropriate. + void ScrollSnapToDestination(); + + // Snap to a snap position nearby the provided destination, if appropriate. + void ScrollSnapNear(const CSSPoint& aDestination, ScrollSnapFlags aSnapFlags); + + // Find a snap point near |aDestination| that we should snap to. + // Returns the snap point if one was found, or an empty Maybe otherwise. + // |aUnit| affects the snapping behaviour (see ScrollSnapUtils:: + // GetSnapPointForDestination). It should generally be determined by the + // type of event that's triggering the scroll. + Maybe FindSnapPointNear(const CSSPoint& aDestination, + ScrollUnit aUnit, + ScrollSnapFlags aSnapFlags); + + // If |aOriginalEvent| crosses the touch-start tolerance threshold, split it + // into two events: one that just reaches the threshold, and the remainder. + // + // |aPanThreshold| is the touch-start tolerance, and |aVectorLength| is + // the length of the vector from the touch-start position to |aOriginalEvent|. + // These values could be computed from |aOriginalEvent| but they are + // passed in for convenience since the caller also needs to compute them. + // + // |aExtPoint| is the position of |aOriginalEvent| in External coordinates, + // and in case of a split is modified by the function to reflect the position + // of of the first event. This is a workaround for the fact that recomputing + // the External position from the returned event would require a round-trip + // through |mScreenPoint| which is an integer. + Maybe> MaybeSplitTouchMoveEvent( + const MultiTouchInput& aOriginalEvent, ScreenCoord aPanThreshold, + float aVectorLength, ExternalPoint& aExtPoint); + + friend std::ostream& operator<<( + std::ostream& aOut, const AsyncPanZoomController::PanZoomState& aState); +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_PanZoomController_h diff --git a/gfx/layers/apz/src/AutoDirWheelDeltaAdjuster.h b/gfx/layers/apz/src/AutoDirWheelDeltaAdjuster.h new file mode 100644 index 0000000000..187641514a --- /dev/null +++ b/gfx/layers/apz/src/AutoDirWheelDeltaAdjuster.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __mozilla_layers_AutoDirWheelDeltaAdjuster_h__ +#define __mozilla_layers_AutoDirWheelDeltaAdjuster_h__ + +#include "Axis.h" // for AxisX, AxisY, Side +#include "mozilla/WheelHandlingHelper.h" // for AutoDirWheelDeltaAdjuster + +namespace mozilla { +namespace layers { + +/** + * About AutoDirWheelDeltaAdjuster: + * For an AutoDir wheel scroll, there's some situations where we should adjust a + * wheel event's delta values. AutoDirWheelDeltaAdjuster converts delta values + * for AutoDir scrolling. An AutoDir wheel scroll lets the user scroll a frame + * with only one scrollbar, using either a vertical or a horzizontal wheel. + * For more detail about the concept of AutoDir scrolling, see the comments in + * AutoDirWheelDeltaAdjuster. + * + * This is the APZ implementation of AutoDirWheelDeltaAdjuster. + */ +class MOZ_STACK_CLASS APZAutoDirWheelDeltaAdjuster final + : public AutoDirWheelDeltaAdjuster { + public: + /** + * @param aDeltaX DeltaX for a wheel event whose delta values will + * be adjusted upon calling adjust() when + * ShouldBeAdjusted() returns true. + * @param aDeltaY DeltaY for a wheel event, like DeltaX. + * @param aAxisX The X axis information provider for the current + * frame, such as whether the frame can be scrolled + * horizontally, leftwards or rightwards. + * @param aAxisY The Y axis information provider for the current + * frame, such as whether the frame can be scrolled + * vertically, upwards or downwards. + * @param aIsHorizontalContentRightToLeft + * Indicates whether the horizontal content starts + * at rightside. This value will decide which edge + * the adjusted scroll goes towards, in other words, + * it will decide the sign of the adjusted delta + * values). For detailed information, see + * IsHorizontalContentRightToLeft() in + * the base class AutoDirWheelDeltaAdjuster. + */ + APZAutoDirWheelDeltaAdjuster(double& aDeltaX, double& aDeltaY, + const AxisX& aAxisX, const AxisY& aAxisY, + bool aIsHorizontalContentRightToLeft) + : AutoDirWheelDeltaAdjuster(aDeltaX, aDeltaY), + mAxisX(aAxisX), + mAxisY(aAxisY), + mIsHorizontalContentRightToLeft(aIsHorizontalContentRightToLeft) {} + + private: + virtual bool CanScrollAlongXAxis() const override { + return mAxisX.CanScroll(); + } + virtual bool CanScrollAlongYAxis() const override { + return mAxisY.CanScroll(); + } + virtual bool CanScrollUpwards() const override { + return mAxisY.CanScrollTo(eSideTop); + } + virtual bool CanScrollDownwards() const override { + return mAxisY.CanScrollTo(eSideBottom); + } + virtual bool CanScrollLeftwards() const override { + return mAxisX.CanScrollTo(eSideLeft); + } + virtual bool CanScrollRightwards() const override { + return mAxisX.CanScrollTo(eSideRight); + } + virtual bool IsHorizontalContentRightToLeft() const override { + return mIsHorizontalContentRightToLeft; + } + + const AxisX& mAxisX; + const AxisY& mAxisY; + bool mIsHorizontalContentRightToLeft; +}; + +} // namespace layers +} // namespace mozilla + +#endif // __mozilla_layers_AutoDirWheelDeltaAdjuster_h__ diff --git a/gfx/layers/apz/src/AutoscrollAnimation.cpp b/gfx/layers/apz/src/AutoscrollAnimation.cpp new file mode 100644 index 0000000000..8d4b8fca10 --- /dev/null +++ b/gfx/layers/apz/src/AutoscrollAnimation.cpp @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AutoscrollAnimation.h" + +#include // for sqrtf() + +#include "AsyncPanZoomController.h" +#include "APZCTreeManager.h" +#include "FrameMetrics.h" +#include "mozilla/Telemetry.h" // for Telemetry + +namespace mozilla { +namespace layers { + +// Helper function for AutoscrollAnimation::DoSample(). +// Basically copied as-is from toolkit/actors/AutoScrollChild.jsm. +static float Accelerate(ScreenCoord curr, ScreenCoord start) { + static const int speed = 12; + float val = (curr - start) / speed; + if (val > 1) { + return val * sqrtf(val) - 1; + } + if (val < -1) { + return val * sqrtf(-val) + 1; + } + return 0; +} + +AutoscrollAnimation::AutoscrollAnimation(AsyncPanZoomController& aApzc, + const ScreenPoint& aAnchorLocation) + : mApzc(aApzc), mAnchorLocation(aAnchorLocation) {} + +bool AutoscrollAnimation::DoSample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta) { + APZCTreeManager* treeManager = mApzc.GetApzcTreeManager(); + if (!treeManager) { + return false; + } + + ScreenPoint mouseLocation = treeManager->GetCurrentMousePosition(); + + // The implementation of this function closely mirrors that of its main- + // thread equivalent, the autoscrollLoop() function in + // toolkit/actors/AutoScrollChild.jsm. + + // Avoid long jumps when the browser hangs for more than |maxTimeDelta| ms. + static const TimeDuration maxTimeDelta = TimeDuration::FromMilliseconds(100); + TimeDuration timeDelta = TimeDuration::Min(aDelta, maxTimeDelta); + + float timeCompensation = timeDelta.ToMilliseconds() / 20; + + // Notes: + // - The main-thread implementation rounds the scroll delta to an integer, + // and keeps track of the fractional part as an "error". It does this + // because it uses Window.scrollBy() or Element.scrollBy() to perform + // the scrolling, and those functions truncate the fractional part of + // the offset. APZ does no such truncation, so there's no need to keep + // track of the fractional part separately. + // - The Accelerate() function takes Screen coordinates as inputs, but + // its output is interpreted as CSS coordinates. This is intentional, + // insofar as autoscrollLoop() does the same thing. + CSSPoint scrollDelta{ + Accelerate(mouseLocation.x, mAnchorLocation.x) * timeCompensation, + Accelerate(mouseLocation.y, mAnchorLocation.y) * timeCompensation}; + + mApzc.ScrollByAndClamp(scrollDelta); + + // An autoscroll animation never ends of its own accord. + // It can be stopped in response to various input events, in which case + // AsyncPanZoomController::StopAutoscroll() will stop it via + // CancelAnimation(). + return true; +} + +void AutoscrollAnimation::Cancel(CancelAnimationFlags aFlags) { + // The cancellation was initiated by browser.js, so there's no need to + // notify it. + if (aFlags & TriggeredExternally) { + return; + } + + if (RefPtr controller = + mApzc.GetGeckoContentController()) { + controller->CancelAutoscroll(mApzc.GetGuid()); + } +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/AutoscrollAnimation.h b/gfx/layers/apz/src/AutoscrollAnimation.h new file mode 100644 index 0000000000..a37f6d473a --- /dev/null +++ b/gfx/layers/apz/src/AutoscrollAnimation.h @@ -0,0 +1,42 @@ +/* -*- 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_AutocrollAnimation_h_ +#define mozilla_layers_AutocrollAnimation_h_ + +#include "AsyncPanZoomAnimation.h" + +namespace mozilla { +namespace layers { + +class AsyncPanZoomController; + +class AutoscrollAnimation : public AsyncPanZoomAnimation { + public: + AutoscrollAnimation(AsyncPanZoomController& aApzc, + const ScreenPoint& aAnchorLocation); + + bool DoSample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta) override; + + bool HandleScrollOffsetUpdate( + const Maybe& aRelativeDelta) override { + // Autoscroll works using screen space coordinates, so there's no work we + // need to do to handle either a relative or an absolute scroll update. + return true; + } + + void Cancel(CancelAnimationFlags aFlags) override; + + private: + AsyncPanZoomController& mApzc; + ScreenPoint mAnchorLocation; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_AutoscrollAnimation_h_ diff --git a/gfx/layers/apz/src/Axis.cpp b/gfx/layers/apz/src/Axis.cpp new file mode 100644 index 0000000000..f0842864d8 --- /dev/null +++ b/gfx/layers/apz/src/Axis.cpp @@ -0,0 +1,733 @@ +/* -*- 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 "Axis.h" + +#include // for fabsf, pow, powf +#include // for max + +#include "APZCTreeManager.h" // for APZCTreeManager +#include "AsyncPanZoomController.h" // for AsyncPanZoomController +#include "FrameMetrics.h" // for FrameMetrics +#include "SimpleVelocityTracker.h" // for FrameMetrics +#include "mozilla/Attributes.h" // for final +#include "mozilla/Preferences.h" // for Preferences +#include "mozilla/gfx/Rect.h" // for RoundedIn +#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread +#include "mozilla/mozalloc.h" // for operator new +#include "nsMathUtils.h" // for NS_lround +#include "nsPrintfCString.h" // for nsPrintfCString +#include "nsThreadUtils.h" // for NS_DispatchToMainThread, etc +#include "nscore.h" // for NS_IMETHOD + +static mozilla::LazyLogModule sApzAxsLog("apz.axis"); +#define AXIS_LOG(...) MOZ_LOG(sApzAxsLog, LogLevel::Debug, (__VA_ARGS__)) + +namespace mozilla { +namespace layers { + +bool FuzzyEqualsCoordinate(CSSCoord aValue1, CSSCoord aValue2) { + return FuzzyEqualsAdditive(aValue1, aValue2, COORDINATE_EPSILON) || + FuzzyEqualsMultiplicative(aValue1, aValue2); +} + +Axis::Axis(AsyncPanZoomController* aAsyncPanZoomController) + : mPos(0), + mVelocity(0.0f, "Axis::mVelocity"), + mAxisLocked(false), + mAsyncPanZoomController(aAsyncPanZoomController), + mOverscroll(0), + mMSDModel(0.0, 0.0, 0.0, StaticPrefs::apz_overscroll_spring_stiffness(), + StaticPrefs::apz_overscroll_damping()), + mVelocityTracker(mAsyncPanZoomController->GetPlatformSpecificState() + ->CreateVelocityTracker(this)) {} + +float Axis::ToLocalVelocity(float aVelocityInchesPerMs) const { + ScreenPoint velocity = + MakePoint(aVelocityInchesPerMs * mAsyncPanZoomController->GetDPI()); + // Use ToScreenCoordinates() to convert a point rather than a vector by + // treating the point as a vector, and using (0, 0) as the anchor. + ScreenPoint panStart = mAsyncPanZoomController->ToScreenCoordinates( + mAsyncPanZoomController->PanStart(), ParentLayerPoint()); + ParentLayerPoint localVelocity = + mAsyncPanZoomController->ToParentLayerCoordinates(velocity, panStart); + return localVelocity.Length(); +} + +void Axis::UpdateWithTouchAtDevicePoint(ParentLayerCoord aPos, + TimeStamp aTimestamp) { + // mVelocityTracker is controller-thread only + APZThreadUtils::AssertOnControllerThread(); + + mPos = aPos; + + AXIS_LOG("%p|%s got position %f\n", mAsyncPanZoomController, Name(), + mPos.value); + if (Maybe newVelocity = + mVelocityTracker->AddPosition(aPos, aTimestamp)) { + DoSetVelocity(mAxisLocked ? 0 : *newVelocity); + AXIS_LOG("%p|%s velocity from tracker is %f%s\n", mAsyncPanZoomController, + Name(), *newVelocity, + mAxisLocked ? ", but we are axis locked" : ""); + } +} + +void Axis::StartTouch(ParentLayerCoord aPos, TimeStamp aTimestamp) { + mStartPos = aPos; + mPos = aPos; + mVelocityTracker->StartTracking(aPos, aTimestamp); + mAxisLocked = false; +} + +bool Axis::AdjustDisplacement(ParentLayerCoord aDisplacement, + ParentLayerCoord& aDisplacementOut, + ParentLayerCoord& aOverscrollAmountOut, + bool aForceOverscroll /* = false */) { + if (mAxisLocked) { + aOverscrollAmountOut = 0; + aDisplacementOut = 0; + return false; + } + if (aForceOverscroll) { + aOverscrollAmountOut = aDisplacement; + aDisplacementOut = 0; + return false; + } + + ParentLayerCoord displacement = aDisplacement; + + // First consume any overscroll in the opposite direction along this axis. + ParentLayerCoord consumedOverscroll = 0; + if (mOverscroll > 0 && aDisplacement < 0) { + consumedOverscroll = std::min(mOverscroll, -aDisplacement); + } else if (mOverscroll < 0 && aDisplacement > 0) { + consumedOverscroll = 0.f - std::min(-mOverscroll, aDisplacement); + } + mOverscroll -= consumedOverscroll; + displacement += consumedOverscroll; + + if (consumedOverscroll != 0.0f) { + AXIS_LOG("%p|%s changed overscroll amount to %f\n", mAsyncPanZoomController, + Name(), mOverscroll.value); + } + + // Split the requested displacement into an allowed displacement that does + // not overscroll, and an overscroll amount. + aOverscrollAmountOut = DisplacementWillOverscrollAmount(displacement); + if (aOverscrollAmountOut != 0.0f) { + // No need to have a velocity along this axis anymore; it won't take us + // anywhere, so we're just spinning needlessly. + AXIS_LOG("%p|%s has overscrolled, clearing velocity\n", + mAsyncPanZoomController, Name()); + DoSetVelocity(0.0f); + displacement -= aOverscrollAmountOut; + } + aDisplacementOut = displacement; + return fabsf(consumedOverscroll) > EPSILON; +} + +ParentLayerCoord Axis::ApplyResistance( + ParentLayerCoord aRequestedOverscroll) const { + // 'resistanceFactor' is a value between 0 and 1/16, which: + // - tends to 1/16 as the existing overscroll tends to 0 + // - tends to 0 as the existing overscroll tends to the composition length + // The actual overscroll is the requested overscroll multiplied by this + // factor. + float resistanceFactor = + (1 - fabsf(GetOverscroll()) / GetCompositionLength()) / 16; + float result = resistanceFactor < 0 ? ParentLayerCoord(0) + : aRequestedOverscroll * resistanceFactor; + result = clamped(result, -8.0f, 8.0f); + return result; +} + +void Axis::OverscrollBy(ParentLayerCoord aOverscroll) { + MOZ_ASSERT(CanScroll()); + // We can get some spurious calls to OverscrollBy() with near-zero values + // due to rounding error. Ignore those (they might trip the asserts below.) + if (mAsyncPanZoomController->IsZero(aOverscroll)) { + return; + } + EndOverscrollAnimation(); + aOverscroll = ApplyResistance(aOverscroll); + if (aOverscroll > 0) { +#ifdef DEBUG + if (!IsScrolledToEnd()) { + nsPrintfCString message( + "composition end (%f) is not equal (within error) to page end (%f)\n", + GetCompositionEnd().value, GetPageEnd().value); + NS_ASSERTION(false, message.get()); + MOZ_CRASH("GFX: Overscroll issue > 0"); + } +#endif + MOZ_ASSERT(mOverscroll >= 0); + } else if (aOverscroll < 0) { +#ifdef DEBUG + if (!IsScrolledToStart()) { + nsPrintfCString message( + "composition origin (%f) is not equal (within error) to page origin " + "(%f)\n", + GetOrigin().value, GetPageStart().value); + NS_ASSERTION(false, message.get()); + MOZ_CRASH("GFX: Overscroll issue < 0"); + } +#endif + MOZ_ASSERT(mOverscroll <= 0); + } + mOverscroll += aOverscroll; + + AXIS_LOG("%p|%s changed overscroll amount to %f\n", mAsyncPanZoomController, + Name(), mOverscroll.value); +} + +ParentLayerCoord Axis::GetOverscroll() const { return mOverscroll; } + +void Axis::RestoreOverscroll(ParentLayerCoord aOverscroll) { + mOverscroll = aOverscroll; +} + +void Axis::StartOverscrollAnimation(float aVelocity) { + const float maxVelocity = StaticPrefs::apz_overscroll_max_velocity(); + aVelocity = clamped(aVelocity / 2.0f, -maxVelocity, maxVelocity); + SetVelocity(aVelocity); + mMSDModel.SetPosition(mOverscroll); + // Convert velocity from ParentLayerCoords/millisecond to + // ParentLayerCoords/second. + mMSDModel.SetVelocity(DoGetVelocity() * 1000.0); + + AXIS_LOG( + "%p|%s beginning overscroll animation with amount %f and velocity %f\n", + mAsyncPanZoomController, Name(), mOverscroll.value, DoGetVelocity()); +} + +void Axis::EndOverscrollAnimation() { + mMSDModel.SetPosition(0.0); + mMSDModel.SetVelocity(0.0); +} + +bool Axis::SampleOverscrollAnimation(const TimeDuration& aDelta, + SideBits aOverscrollSideBits) { + mMSDModel.Simulate(aDelta); + mOverscroll = mMSDModel.GetPosition(); + + if (((aOverscrollSideBits & (SideBits::eTop | SideBits::eLeft)) && + mOverscroll > 0) || + ((aOverscrollSideBits & (SideBits::eBottom | SideBits::eRight)) && + mOverscroll < 0)) { + // Stop the overscroll model immediately if it's going to get across the + // boundary. + mMSDModel.SetPosition(0.0); + mMSDModel.SetVelocity(0.0); + } + + AXIS_LOG("%p|%s changed overscroll amount to %f\n", mAsyncPanZoomController, + Name(), mOverscroll.value); + + if (mMSDModel.IsFinished(1.0)) { + // "Jump" to the at-rest state. The jump shouldn't be noticeable as the + // velocity and overscroll are already low. + AXIS_LOG("%p|%s oscillation dropped below threshold, going to rest\n", + mAsyncPanZoomController, Name()); + ClearOverscroll(); + DoSetVelocity(0); + return false; + } + + // Otherwise, continue the animation. + return true; +} + +bool Axis::IsOverscrollAnimationRunning() const { + return !mMSDModel.IsFinished(1.0); +} + +bool Axis::IsOverscrollAnimationAlive() const { + // Unlike IsOverscrollAnimationRunning, check the position and the velocity to + // be sure that the animation has started but hasn't yet finished. + return mMSDModel.GetPosition() != 0.0 || mMSDModel.GetVelocity() != 0.0; +} + +bool Axis::IsOverscrolled() const { return mOverscroll != 0.f; } + +bool Axis::IsScrolledToStart() const { + const auto zoom = GetFrameMetrics().GetZoom(); + + if (zoom == CSSToParentLayerScale(0)) { + return true; + } + + return FuzzyEqualsCoordinate(GetOrigin() / zoom, GetPageStart() / zoom); +} + +bool Axis::IsScrolledToEnd() const { + const auto zoom = GetFrameMetrics().GetZoom(); + + if (zoom == CSSToParentLayerScale(0)) { + return true; + } + + return FuzzyEqualsCoordinate(GetCompositionEnd() / zoom, GetPageEnd() / zoom); +} + +bool Axis::IsInInvalidOverscroll() const { + if (mOverscroll > 0) { + return !IsScrolledToEnd(); + } else if (mOverscroll < 0) { + return !IsScrolledToStart(); + } + return false; +} + +void Axis::ClearOverscroll() { + EndOverscrollAnimation(); + mOverscroll = 0; +} + +ParentLayerCoord Axis::PanStart() const { return mStartPos; } + +ParentLayerCoord Axis::PanDistance() const { return fabs(mPos - mStartPos); } + +ParentLayerCoord Axis::PanDistance(ParentLayerCoord aPos) const { + return fabs(aPos - mStartPos); +} + +void Axis::EndTouch(TimeStamp aTimestamp, ClearAxisLock aClearAxisLock) { + // mVelocityQueue is controller-thread only + APZThreadUtils::AssertOnControllerThread(); + + // If the velocity tracker wasn't able to compute a velocity, zero out + // the velocity to make sure we don't get a fling based on some old and + // no-longer-relevant value of mVelocity. Also if the axis is locked then + // just reset the velocity to 0 since we don't need any velocity to carry + // into the fling. + if (mAxisLocked) { + DoSetVelocity(0); + } else if (Maybe velocity = + mVelocityTracker->ComputeVelocity(aTimestamp)) { + DoSetVelocity(*velocity); + } else { + DoSetVelocity(0); + } + if (aClearAxisLock == ClearAxisLock::Yes) { + mAxisLocked = false; + } + AXIS_LOG("%p|%s ending touch, computed velocity %f\n", + mAsyncPanZoomController, Name(), DoGetVelocity()); +} + +void Axis::CancelGesture() { + // mVelocityQueue is controller-thread only + APZThreadUtils::AssertOnControllerThread(); + + AXIS_LOG("%p|%s cancelling touch, clearing velocity queue\n", + mAsyncPanZoomController, Name()); + DoSetVelocity(0.0f); + mVelocityTracker->Clear(); + SetAxisLocked(false); +} + +bool Axis::CanScroll() const { + return mAsyncPanZoomController->FuzzyGreater(GetPageLength(), + GetCompositionLength()); +} + +bool Axis::CanScroll(CSSCoord aDelta) const { + return CanScroll(aDelta * GetFrameMetrics().GetZoom()); +} + +bool Axis::CanScroll(ParentLayerCoord aDelta) const { + if (!CanScroll()) { + return false; + } + + const auto zoom = GetFrameMetrics().GetZoom(); + CSSCoord availableToScroll = 0; + + if (zoom != CSSToParentLayerScale(0)) { + availableToScroll = + ParentLayerCoord( + fabs(DisplacementWillOverscrollAmount(aDelta) - aDelta)) / + zoom; + } + + return availableToScroll > COORDINATE_EPSILON; +} + +CSSCoord Axis::ClampOriginToScrollableRect(CSSCoord aOrigin) const { + CSSToParentLayerScale zoom = GetFrameMetrics().GetZoom(); + ParentLayerCoord origin = aOrigin * zoom; + ParentLayerCoord result; + if (origin < GetPageStart()) { + result = GetPageStart(); + } else if (origin + GetCompositionLength() > GetPageEnd()) { + result = GetPageEnd() - GetCompositionLength(); + } else { + return aOrigin; + } + if (zoom == CSSToParentLayerScale(0)) { + return aOrigin; + } + return result / zoom; +} + +bool Axis::CanScrollNow() const { return !mAxisLocked && CanScroll(); } + +ParentLayerCoord Axis::DisplacementWillOverscrollAmount( + ParentLayerCoord aDisplacement) const { + ParentLayerCoord newOrigin = GetOrigin() + aDisplacement; + ParentLayerCoord newCompositionEnd = GetCompositionEnd() + aDisplacement; + // If the current pan plus a displacement takes the window to the left of or + // above the current page rect. + bool minus = newOrigin < GetPageStart(); + // If the current pan plus a displacement takes the window to the right of or + // below the current page rect. + bool plus = newCompositionEnd > GetPageEnd(); + if (minus && plus) { + // Don't handle overscrolled in both directions; a displacement can't cause + // this, it must have already been zoomed out too far. + return 0; + } + if (minus) { + return newOrigin - GetPageStart(); + } + if (plus) { + return newCompositionEnd - GetPageEnd(); + } + return 0; +} + +CSSCoord Axis::ScaleWillOverscrollAmount(float aScale, CSSCoord aFocus) const { + // Internally, do computations in ParentLayer coordinates *before* the scale + // is applied. + CSSToParentLayerScale zoom = GetFrameMetrics().GetZoom(); + ParentLayerCoord focus = aFocus * zoom; + ParentLayerCoord originAfterScale = (GetOrigin() + focus) - (focus / aScale); + + bool both = ScaleWillOverscrollBothSides(aScale); + bool minus = GetPageStart() - originAfterScale > COORDINATE_EPSILON; + bool plus = + (originAfterScale + (GetCompositionLength() / aScale)) - GetPageEnd() > + COORDINATE_EPSILON; + + if ((minus && plus) || both) { + // If we ever reach here it's a bug in the client code. + MOZ_ASSERT(false, + "In an OVERSCROLL_BOTH condition in ScaleWillOverscrollAmount"); + return 0; + } + if (minus && zoom != CSSToParentLayerScale(0)) { + return (originAfterScale - GetPageStart()) / zoom; + } + if (plus && zoom != CSSToParentLayerScale(0)) { + return (originAfterScale + (GetCompositionLength() / aScale) - + GetPageEnd()) / + zoom; + } + return 0; +} + +bool Axis::IsAxisLocked() const { return mAxisLocked; } + +float Axis::GetVelocity() const { return mAxisLocked ? 0 : DoGetVelocity(); } + +void Axis::SetVelocity(float aVelocity) { + AXIS_LOG("%p|%s direct-setting velocity to %f\n", mAsyncPanZoomController, + Name(), aVelocity); + DoSetVelocity(aVelocity); +} + +ParentLayerCoord Axis::GetCompositionEnd() const { + return GetOrigin() + GetCompositionLength(); +} + +ParentLayerCoord Axis::GetPageEnd() const { + return GetPageStart() + GetPageLength(); +} + +ParentLayerCoord Axis::GetScrollRangeEnd() const { + return GetPageEnd() - GetCompositionLength(); +} + +ParentLayerCoord Axis::GetOrigin() const { + ParentLayerPoint origin = + GetFrameMetrics().GetVisualScrollOffset() * GetFrameMetrics().GetZoom(); + return GetPointOffset(origin); +} + +ParentLayerCoord Axis::GetCompositionLength() const { + return GetRectLength(GetFrameMetrics().GetCompositionBounds()); +} + +ParentLayerCoord Axis::GetPageStart() const { + ParentLayerRect pageRect = GetFrameMetrics().GetExpandedScrollableRect() * + GetFrameMetrics().GetZoom(); + return GetRectOffset(pageRect); +} + +ParentLayerCoord Axis::GetPageLength() const { + ParentLayerRect pageRect = GetFrameMetrics().GetExpandedScrollableRect() * + GetFrameMetrics().GetZoom(); + return GetRectLength(pageRect); +} + +bool Axis::ScaleWillOverscrollBothSides(float aScale) const { + const FrameMetrics& metrics = GetFrameMetrics(); + ParentLayerRect screenCompositionBounds = + metrics.GetCompositionBounds() / ParentLayerToParentLayerScale(aScale); + return GetRectLength(screenCompositionBounds) - GetPageLength() > + COORDINATE_EPSILON; +} + +float Axis::DoGetVelocity() const { + auto velocity = mVelocity.Lock(); + return velocity.ref(); +} +void Axis::DoSetVelocity(float aVelocity) { + auto velocity = mVelocity.Lock(); + velocity.ref() = aVelocity; +} + +const FrameMetrics& Axis::GetFrameMetrics() const { + return mAsyncPanZoomController->GetFrameMetrics(); +} + +const ScrollMetadata& Axis::GetScrollMetadata() const { + return mAsyncPanZoomController->GetScrollMetadata(); +} + +bool Axis::OverscrollBehaviorAllowsHandoff() const { + // Scroll handoff is a "non-local" overscroll behavior, so it's allowed + // with "auto" and disallowed with "contain" and "none". + return GetOverscrollBehavior() == OverscrollBehavior::Auto; +} + +bool Axis::OverscrollBehaviorAllowsOverscrollEffect() const { + // An overscroll effect is a "local" overscroll behavior, so it's allowed + // with "auto" and "contain" and disallowed with "none". + return GetOverscrollBehavior() != OverscrollBehavior::None; +} + +AxisX::AxisX(AsyncPanZoomController* aAsyncPanZoomController) + : Axis(aAsyncPanZoomController) {} + +CSSCoord AxisX::GetPointOffset(const CSSPoint& aPoint) const { + return aPoint.x; +} + +OuterCSSCoord AxisX::GetPointOffset(const OuterCSSPoint& aPoint) const { + return aPoint.x; +} + +ParentLayerCoord AxisX::GetPointOffset(const ParentLayerPoint& aPoint) const { + return aPoint.x; +} + +CSSToParentLayerScale AxisX::GetAxisScale( + const CSSToParentLayerScale2D& aScale) const { + return CSSToParentLayerScale(aScale.xScale); +} + +ParentLayerCoord AxisX::GetRectLength(const ParentLayerRect& aRect) const { + return aRect.Width(); +} + +CSSCoord AxisX::GetRectLength(const CSSRect& aRect) const { + return aRect.Width(); +} + +ParentLayerCoord AxisX::GetRectOffset(const ParentLayerRect& aRect) const { + return aRect.X(); +} + +CSSCoord AxisX::GetRectOffset(const CSSRect& aRect) const { return aRect.X(); } + +float AxisX::GetTransformScale( + const AsyncTransformComponentMatrix& aMatrix) const { + return aMatrix._11; +} + +ParentLayerCoord AxisX::GetTransformTranslation( + const AsyncTransformComponentMatrix& aMatrix) const { + return aMatrix._41; +} + +void AxisX::PostScale(AsyncTransformComponentMatrix& aMatrix, + float aScale) const { + aMatrix.PostScale(aScale, 1.f, 1.f); +} + +void AxisX::PostTranslate(AsyncTransformComponentMatrix& aMatrix, + ParentLayerCoord aTranslation) const { + aMatrix.PostTranslate(aTranslation, 0, 0); +} + +ScreenPoint AxisX::MakePoint(ScreenCoord aCoord) const { + return ScreenPoint(aCoord, 0); +} + +const char* AxisX::Name() const { return "X"; } + +bool AxisX::CanScrollTo(Side aSide) const { + switch (aSide) { + case eSideLeft: + return CanScroll(CSSCoord(-COORDINATE_EPSILON * 2)); + case eSideRight: + return CanScroll(CSSCoord(COORDINATE_EPSILON * 2)); + default: + MOZ_ASSERT_UNREACHABLE("aSide is out of valid values"); + return false; + } +} + +SideBits AxisX::ScrollableDirections() const { + SideBits directions = SideBits::eNone; + + if (CanScrollTo(eSideLeft)) { + directions |= SideBits::eLeft; + } + if (CanScrollTo(eSideRight)) { + directions |= SideBits::eRight; + } + + return directions; +} + +OverscrollBehavior AxisX::GetOverscrollBehavior() const { + return GetScrollMetadata().GetOverscrollBehavior().mBehaviorX; +} + +AxisY::AxisY(AsyncPanZoomController* aAsyncPanZoomController) + : Axis(aAsyncPanZoomController) {} + +CSSCoord AxisY::GetPointOffset(const CSSPoint& aPoint) const { + return aPoint.y; +} + +OuterCSSCoord AxisY::GetPointOffset(const OuterCSSPoint& aPoint) const { + return aPoint.y; +} + +ParentLayerCoord AxisY::GetPointOffset(const ParentLayerPoint& aPoint) const { + return aPoint.y; +} + +CSSToParentLayerScale AxisY::GetAxisScale( + const CSSToParentLayerScale2D& aScale) const { + return CSSToParentLayerScale(aScale.yScale); +} + +ParentLayerCoord AxisY::GetRectLength(const ParentLayerRect& aRect) const { + return aRect.Height(); +} + +CSSCoord AxisY::GetRectLength(const CSSRect& aRect) const { + return aRect.Height(); +} + +ParentLayerCoord AxisY::GetRectOffset(const ParentLayerRect& aRect) const { + return aRect.Y(); +} + +CSSCoord AxisY::GetRectOffset(const CSSRect& aRect) const { return aRect.Y(); } + +float AxisY::GetTransformScale( + const AsyncTransformComponentMatrix& aMatrix) const { + return aMatrix._22; +} + +ParentLayerCoord AxisY::GetTransformTranslation( + const AsyncTransformComponentMatrix& aMatrix) const { + return aMatrix._42; +} + +void AxisY::PostScale(AsyncTransformComponentMatrix& aMatrix, + float aScale) const { + aMatrix.PostScale(1.f, aScale, 1.f); +} + +void AxisY::PostTranslate(AsyncTransformComponentMatrix& aMatrix, + ParentLayerCoord aTranslation) const { + aMatrix.PostTranslate(0, aTranslation, 0); +} + +ScreenPoint AxisY::MakePoint(ScreenCoord aCoord) const { + return ScreenPoint(0, aCoord); +} + +const char* AxisY::Name() const { return "Y"; } + +bool AxisY::CanScrollTo(Side aSide) const { + switch (aSide) { + case eSideTop: + return CanScroll(CSSCoord(-COORDINATE_EPSILON * 2)); + case eSideBottom: + return CanScroll(CSSCoord(COORDINATE_EPSILON * 2)); + default: + MOZ_ASSERT_UNREACHABLE("aSide is out of valid values"); + return false; + } +} + +SideBits AxisY::ScrollableDirections() const { + SideBits directions = SideBits::eNone; + + if (CanScrollTo(eSideTop)) { + directions |= SideBits::eTop; + } + if (CanScrollTo(eSideBottom)) { + directions |= SideBits::eBottom; + } + + return directions; +} + +bool AxisY::HasDynamicToolbar() const { + return GetCompositionLengthWithoutDynamicToolbar() != ParentLayerCoord(0); +} + +SideBits AxisY::ScrollableDirectionsWithDynamicToolbar( + const ScreenMargin& aFixedLayerMargins) const { + MOZ_ASSERT(mAsyncPanZoomController->IsRootContent()); + + SideBits directions = ScrollableDirections(); + + if (HasDynamicToolbar()) { + ParentLayerCoord toolbarHeight = + GetCompositionLength() - GetCompositionLengthWithoutDynamicToolbar(); + + ParentLayerMargin fixedLayerMargins = ViewAs( + aFixedLayerMargins, PixelCastJustification::ScreenIsParentLayerForRoot); + + if (!mAsyncPanZoomController->IsZero(fixedLayerMargins.bottom)) { + directions |= SideBits::eTop; + } + if (mAsyncPanZoomController->FuzzyGreater( + aFixedLayerMargins.bottom + toolbarHeight, 0)) { + directions |= SideBits::eBottom; + } + } + + return directions; +} + +bool AxisY::CanVerticalScrollWithDynamicToolbar() const { + return !HasDynamicToolbar() + ? CanScroll() + : mAsyncPanZoomController->FuzzyGreater( + GetPageLength(), + GetCompositionLengthWithoutDynamicToolbar()); +} + +OverscrollBehavior AxisY::GetOverscrollBehavior() const { + return GetScrollMetadata().GetOverscrollBehavior().mBehaviorY; +} + +ParentLayerCoord AxisY::GetCompositionLengthWithoutDynamicToolbar() const { + return GetFrameMetrics().GetCompositionSizeWithoutDynamicToolbar().Height(); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/Axis.h b/gfx/layers/apz/src/Axis.h new file mode 100644 index 0000000000..072c0a297b --- /dev/null +++ b/gfx/layers/apz/src/Axis.h @@ -0,0 +1,462 @@ +/* -*- 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_Axis_h +#define mozilla_layers_Axis_h + +#include // for int32_t + +#include "APZUtils.h" +#include "AxisPhysicsMSDModel.h" +#include "mozilla/DataMutex.h" // for DataMutex +#include "mozilla/gfx/Types.h" // for Side +#include "mozilla/TimeStamp.h" // for TimeDuration +#include "nsTArray.h" // for nsTArray +#include "Units.h" + +namespace mozilla { +namespace layers { + +const float EPSILON = 0.0001f; + +/** + * Compare two coordinates for equality, accounting for rounding error. + * Use both FuzzyEqualsAdditive() with COORDINATE_EPISLON, which accounts for + * things like the error introduced by rounding during a round-trip to app + * units, and FuzzyEqualsMultiplicative(), which accounts for accumulated error + * due to floating-point operations (which can be larger than COORDINATE_EPISLON + * for sufficiently large coordinate values). + */ +bool FuzzyEqualsCoordinate(CSSCoord aValue1, CSSCoord aValue2); + +struct FrameMetrics; +class AsyncPanZoomController; + +/** + * Interface for computing velocities along the axis based on + * position samples. + */ +class VelocityTracker { + public: + virtual ~VelocityTracker() = default; + + /** + * Start tracking velocity along this axis, starting with the given + * initial position and corresponding timestamp. + */ + virtual void StartTracking(ParentLayerCoord aPos, TimeStamp aTimestamp) = 0; + /** + * Record a new position along this axis, at the given timestamp. + * Returns the average velocity between the last sample and this one, or + * or Nothing() if a reasonable average cannot be computed. + */ + virtual Maybe AddPosition(ParentLayerCoord aPos, + TimeStamp aTimestamp) = 0; + /** + * Compute an estimate of the axis's current velocity, based on recent + * position samples. It's up to implementation how many samples to consider + * and how to perform the computation. + * If the tracker doesn't have enough samples to compute a result, it + * may return Nothing{}. + */ + virtual Maybe ComputeVelocity(TimeStamp aTimestamp) = 0; + /** + * Clear all state in the velocity tracker. + */ + virtual void Clear() = 0; +}; + +/** + * Helper class to maintain each axis of movement (X,Y) for panning and zooming. + * Note that everything here is specific to one axis; that is, the X axis knows + * nothing about the Y axis and vice versa. + */ +class Axis { + public: + explicit Axis(AsyncPanZoomController* aAsyncPanZoomController); + + /** + * Notify this Axis that a new touch has been received, including a timestamp + * for when the touch was received. This triggers a recalculation of velocity. + * This can also used for pan gesture events. For those events, |aPos| is + * an invented position corresponding to the mouse position plus any + * accumulated displacements over the course of the pan gesture. + */ + void UpdateWithTouchAtDevicePoint(ParentLayerCoord aPos, + TimeStamp aTimestamp); + + public: + /** + * Notify this Axis that a touch has begun, i.e. the user has put their finger + * on the screen but has not yet tried to pan. + */ + void StartTouch(ParentLayerCoord aPos, TimeStamp aTimestamp); + + /** + * Helper enum class for specifying if EndTouch() should clear the axis lock. + */ + enum class ClearAxisLock { Yes, No }; + + /** + * Notify this Axis that a touch has ended gracefully. This may perform + * recalculations of the axis velocity. + */ + void EndTouch(TimeStamp aTimestamp, ClearAxisLock aClearAxisLock); + + /** + * Notify this Axis that the gesture has ended forcefully. Useful for stopping + * flings when a user puts their finger down in the middle of one (i.e. to + * stop a previous touch including its fling so that a new one can take its + * place). + */ + void CancelGesture(); + + /** + * Takes a requested displacement to the position of this axis, and adjusts it + * to account for overscroll (which might decrease the displacement; this is + * to prevent the viewport from overscrolling the page rect), and axis locking + * (which might prevent any displacement from happening). If overscroll + * ocurred, its amount is written to |aOverscrollAmountOut|. + * The |aDisplacementOut| parameter is set to the adjusted displacement, and + * the function returns true if and only if internal overscroll amounts were + * changed. + */ + bool AdjustDisplacement(ParentLayerCoord aDisplacement, + ParentLayerCoord& aDisplacementOut, + ParentLayerCoord& aOverscrollAmountOut, + bool aForceOverscroll = false); + + /** + * Overscrolls this axis by the requested amount in the requested direction. + * The axis must be at the end of its scroll range in this direction. + */ + void OverscrollBy(ParentLayerCoord aOverscroll); + + /** + * Return the amount of overscroll on this axis, in ParentLayer pixels. + * + * If this amount is nonzero, the relevant component of + * mAsyncPanZoomController->Metrics().mScrollOffset must be at its + * extreme allowed value in the relevant direction (that is, it must be at + * its maximum value if we are overscrolled at our composition length, and + * at its minimum value if we are overscrolled at the origin). + */ + ParentLayerCoord GetOverscroll() const; + + /** + * Restore the amount by which this axis is overscrolled to the specified + * amount. This is for test-related use; overscrolling as a result of user + * input should happen via OverscrollBy(). + */ + void RestoreOverscroll(ParentLayerCoord aOverscroll); + + /** + * Start an overscroll animation with the given initial velocity. + */ + void StartOverscrollAnimation(float aVelocity); + + /** + * Sample the snap-back animation to relieve overscroll. + * |aDelta| is the time since the last sample, |aOverscrollSideBits| is + * the direction where the overscroll happens on this axis. + */ + bool SampleOverscrollAnimation(const TimeDuration& aDelta, + SideBits aOverscrollSideBits); + + /** + * Stop an overscroll animation. + */ + void EndOverscrollAnimation(); + + /** + * Return whether this axis is overscrolled in either direction. + */ + bool IsOverscrolled() const; + + /** + * Return true if this axis is overscrolled but its scroll offset + * has changed in a way that makes the oversrolled state no longer + * valid (for example, it is overscrolled at the top but the + * scroll offset is no longer zero). + */ + bool IsInInvalidOverscroll() const; + + /** + * Clear any overscroll amount on this axis. + */ + void ClearOverscroll(); + + /** + * Returns whether the overscroll animation is alive. + */ + bool IsOverscrollAnimationAlive() const; + + /** + * Returns whether the overscroll animation is running. + * Note that unlike the above IsOverscrollAnimationAlive, this function + * returns false even if the animation is still there but is very close to + * the destination position and its velocity is quite low, i.e. it's time to + * finish. + */ + bool IsOverscrollAnimationRunning() const; + + /** + * Gets the starting position of the touch supplied in StartTouch(). + */ + ParentLayerCoord PanStart() const; + + /** + * Gets the distance between the starting position of the touch supplied in + * StartTouch() and the current touch from the last + * UpdateWithTouchAtDevicePoint(). + */ + ParentLayerCoord PanDistance() const; + + /** + * Gets the distance between the starting position of the touch supplied in + * StartTouch() and the supplied position. + */ + ParentLayerCoord PanDistance(ParentLayerCoord aPos) const; + + /** + * Returns true if the page has room to be scrolled along this axis. + */ + bool CanScroll() const; + + /** + * Returns whether this axis can scroll any more in a particular direction. + */ + bool CanScroll(CSSCoord aDelta) const; + bool CanScroll(ParentLayerCoord aDelta) const; + + /** + * Returns true if the page has room to be scrolled along this axis + * and this axis is not scroll-locked. + */ + bool CanScrollNow() const; + + /** + * Clamp a point to the page's scrollable bounds. That is, a scroll + * destination to the returned point will not contain any overscroll. + */ + CSSCoord ClampOriginToScrollableRect(CSSCoord aOrigin) const; + + void SetAxisLocked(bool aAxisLocked) { mAxisLocked = aAxisLocked; } + + /** + * Gets the raw velocity of this axis at this moment. + */ + float GetVelocity() const; + + /** + * Sets the raw velocity of this axis at this moment. + * Intended to be called only when the axis "takes over" a velocity from + * another APZC, in which case there are no touch points available to call + * UpdateWithTouchAtDevicePoint. In other circumstances, + * UpdateWithTouchAtDevicePoint should be used and the velocity calculated + * there. + */ + void SetVelocity(float aVelocity); + + /** + * If a displacement will overscroll the axis, this returns the amount and in + * what direction. + */ + ParentLayerCoord DisplacementWillOverscrollAmount( + ParentLayerCoord aDisplacement) const; + + /** + * If a scale will overscroll the axis, this returns the amount and in what + * direction. + * + * |aFocus| is the point at which the scale is focused at. We will offset the + * scroll offset in such a way that it remains in the same place on the page + * relative. + * + * Note: Unlike most other functions in Axis, this functions operates in + * CSS coordinates so there is no confusion as to whether the + * ParentLayer coordinates it operates in are before or after the scale + * is applied. + */ + CSSCoord ScaleWillOverscrollAmount(float aScale, CSSCoord aFocus) const; + + /** + * Checks if an axis will overscroll in both directions by computing the + * content rect and checking that its height/width (depending on the axis) + * does not overextend past the viewport. + * + * This gets called by ScaleWillOverscroll(). + */ + bool ScaleWillOverscrollBothSides(float aScale) const; + + /** + * Returns true if movement on this axis is locked. + */ + bool IsAxisLocked() const; + + ParentLayerCoord GetOrigin() const; + ParentLayerCoord GetCompositionLength() const; + ParentLayerCoord GetPageStart() const; + ParentLayerCoord GetPageLength() const; + ParentLayerCoord GetCompositionEnd() const; + ParentLayerCoord GetPageEnd() const; + ParentLayerCoord GetScrollRangeEnd() const; + + bool IsScrolledToStart() const; + bool IsScrolledToEnd() const; + + ParentLayerCoord GetPos() const { return mPos; } + + bool OverscrollBehaviorAllowsHandoff() const; + bool OverscrollBehaviorAllowsOverscrollEffect() const; + + virtual CSSToParentLayerScale GetAxisScale( + const CSSToParentLayerScale2D& aScale) const = 0; + virtual CSSCoord GetPointOffset(const CSSPoint& aPoint) const = 0; + virtual OuterCSSCoord GetPointOffset(const OuterCSSPoint& aPoint) const = 0; + virtual ParentLayerCoord GetPointOffset( + const ParentLayerPoint& aPoint) const = 0; + virtual ParentLayerCoord GetRectLength( + const ParentLayerRect& aRect) const = 0; + virtual CSSCoord GetRectLength(const CSSRect& aRect) const = 0; + virtual ParentLayerCoord GetRectOffset( + const ParentLayerRect& aRect) const = 0; + virtual CSSCoord GetRectOffset(const CSSRect& aRect) const = 0; + virtual float GetTransformScale( + const AsyncTransformComponentMatrix& aMatrix) const = 0; + virtual ParentLayerCoord GetTransformTranslation( + const AsyncTransformComponentMatrix& aMatrix) const = 0; + virtual void PostScale(AsyncTransformComponentMatrix& aMatrix, + float aScale) const = 0; + virtual void PostTranslate(AsyncTransformComponentMatrix& aMatrix, + ParentLayerCoord aTranslation) const = 0; + + virtual ScreenPoint MakePoint(ScreenCoord aCoord) const = 0; + + const void* OpaqueApzcPointer() const { return mAsyncPanZoomController; } + + virtual const char* Name() const = 0; + + // Convert a velocity from global inches/ms into ParentLayerCoords/ms. + float ToLocalVelocity(float aVelocityInchesPerMs) const; + + protected: + // A position along the axis, used during input event processing to + // track velocities (and for touch gestures, to track the length of + // the gesture). For touch events, this represents the position of + // the finger (or in the case of two-finger scrolling, the midpoint + // of the two fingers). For pan gesture events, this represents an + // invented position corresponding to the mouse position at the start + // of the pan, plus deltas representing the displacement of the pan. + ParentLayerCoord mPos; + + ParentLayerCoord mStartPos; + // The velocity can be accessed from multiple threads (e.g. APZ + // controller thread and APZ sampler thread), so needs to be + // protected by a mutex. + // Units: ParentLayerCoords per millisecond + mutable DataMutex mVelocity; + bool mAxisLocked; // Whether movement on this axis is locked. + AsyncPanZoomController* mAsyncPanZoomController; + + // The amount by which we are overscrolled; see GetOverscroll(). + ParentLayerCoord mOverscroll; + + // The mass-spring-damper model for overscroll physics. + AxisPhysicsMSDModel mMSDModel; + + // Used to track velocity over a series of input events and compute + // a resulting velocity to use for e.g. starting a fling animation. + // This member can only be accessed on the controller/UI thread. + UniquePtr mVelocityTracker; + + float DoGetVelocity() const; + void DoSetVelocity(float aVelocity); + + const FrameMetrics& GetFrameMetrics() const; + const ScrollMetadata& GetScrollMetadata() const; + + // Do not use this function directly, use + // AsyncPanZoomController::GetAllowedHandoffDirections instead. + virtual OverscrollBehavior GetOverscrollBehavior() const = 0; + + // Adjust a requested overscroll amount for resistance, yielding a smaller + // actual overscroll amount. + ParentLayerCoord ApplyResistance(ParentLayerCoord aOverscroll) const; + + // Helper function for SampleOverscrollAnimation(). + void StepOverscrollAnimation(double aStepDurationMilliseconds); +}; + +class AxisX : public Axis { + public: + explicit AxisX(AsyncPanZoomController* mAsyncPanZoomController); + CSSToParentLayerScale GetAxisScale( + const CSSToParentLayerScale2D& aScale) const override; + CSSCoord GetPointOffset(const CSSPoint& aPoint) const override; + OuterCSSCoord GetPointOffset(const OuterCSSPoint& aPoint) const override; + ParentLayerCoord GetPointOffset( + const ParentLayerPoint& aPoint) const override; + ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const override; + CSSCoord GetRectLength(const CSSRect& aRect) const override; + ParentLayerCoord GetRectOffset(const ParentLayerRect& aRect) const override; + CSSCoord GetRectOffset(const CSSRect& aRect) const override; + float GetTransformScale( + const AsyncTransformComponentMatrix& aMatrix) const override; + ParentLayerCoord GetTransformTranslation( + const AsyncTransformComponentMatrix& aMatrix) const override; + void PostScale(AsyncTransformComponentMatrix& aMatrix, + float aScale) const override; + void PostTranslate(AsyncTransformComponentMatrix& aMatrix, + ParentLayerCoord aTranslation) const override; + ScreenPoint MakePoint(ScreenCoord aCoord) const override; + const char* Name() const override; + bool CanScrollTo(Side aSide) const; + SideBits ScrollableDirections() const; + + private: + OverscrollBehavior GetOverscrollBehavior() const override; +}; + +class AxisY : public Axis { + public: + explicit AxisY(AsyncPanZoomController* mAsyncPanZoomController); + CSSCoord GetPointOffset(const CSSPoint& aPoint) const override; + OuterCSSCoord GetPointOffset(const OuterCSSPoint& aPoint) const override; + ParentLayerCoord GetPointOffset( + const ParentLayerPoint& aPoint) const override; + CSSToParentLayerScale GetAxisScale( + const CSSToParentLayerScale2D& aScale) const override; + ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const override; + CSSCoord GetRectLength(const CSSRect& aRect) const override; + ParentLayerCoord GetRectOffset(const ParentLayerRect& aRect) const override; + CSSCoord GetRectOffset(const CSSRect& aRect) const override; + float GetTransformScale( + const AsyncTransformComponentMatrix& aMatrix) const override; + ParentLayerCoord GetTransformTranslation( + const AsyncTransformComponentMatrix& aMatrix) const override; + void PostScale(AsyncTransformComponentMatrix& aMatrix, + float aScale) const override; + void PostTranslate(AsyncTransformComponentMatrix& aMatrix, + ParentLayerCoord aTranslation) const override; + ScreenPoint MakePoint(ScreenCoord aCoord) const override; + const char* Name() const override; + bool CanScrollTo(Side aSide) const; + bool CanVerticalScrollWithDynamicToolbar() const; + SideBits ScrollableDirections() const; + SideBits ScrollableDirectionsWithDynamicToolbar( + const ScreenMargin& aFixedLayerMargins) const; + + private: + OverscrollBehavior GetOverscrollBehavior() const override; + ParentLayerCoord GetCompositionLengthWithoutDynamicToolbar() const; + bool HasDynamicToolbar() const; +}; + +} // namespace layers +} // namespace mozilla + +#endif diff --git a/gfx/layers/apz/src/CheckerboardEvent.cpp b/gfx/layers/apz/src/CheckerboardEvent.cpp new file mode 100644 index 0000000000..8f518c7383 --- /dev/null +++ b/gfx/layers/apz/src/CheckerboardEvent.cpp @@ -0,0 +1,195 @@ +/* -*- 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 "CheckerboardEvent.h" +#include "mozilla/Logging.h" + +#include // for std::sort + +static mozilla::LazyLogModule sApzCheckLog("apz.checkerboard"); + +namespace mozilla { +namespace layers { + +// Relatively arbitrary limit to prevent a perma-checkerboard event from +// eating up gobs of memory. Ideally we shouldn't have perma-checkerboarding +// but better to guard against it. +#define LOG_LENGTH_LIMIT (50 * 1024) + +const char* CheckerboardEvent::sDescriptions[] = { + "page", + "painted displayport", + "requested displayport", + "viewport", +}; + +const char* CheckerboardEvent::sColors[] = { + "brown", + "lightgreen", + "yellow", + "red", +}; + +CheckerboardEvent::CheckerboardEvent(bool aRecordTrace) + : mRecordTrace(aRecordTrace), + mOriginTime(TimeStamp::Now()), + mCheckerboardingActive(false), + mLastSampleTime(mOriginTime), + mFrameCount(0), + mTotalPixelMs(0), + mPeakPixels(0), + mRendertraceLock("Rendertrace") {} + +uint32_t CheckerboardEvent::GetSeverity() { + // Scale the total into a 32-bit value + return (uint32_t)sqrt((double)mTotalPixelMs); +} + +uint32_t CheckerboardEvent::GetPeak() { return mPeakPixels; } + +TimeDuration CheckerboardEvent::GetDuration() { return mEndTime - mStartTime; } + +std::string CheckerboardEvent::GetLog() { + MonitorAutoLock lock(mRendertraceLock); + return mRendertraceInfo.str(); +} + +bool CheckerboardEvent::IsRecordingTrace() { return mRecordTrace; } + +void CheckerboardEvent::UpdateRendertraceProperty( + RendertraceProperty aProperty, const CSSRect& aRect, + const std::string& aExtraInfo) { + if (!mRecordTrace) { + return; + } + MonitorAutoLock lock(mRendertraceLock); + if (!mCheckerboardingActive) { + mBufferedProperties[aProperty].Update(aProperty, aRect, aExtraInfo, lock); + } else { + LogInfo(aProperty, TimeStamp::Now(), aRect, aExtraInfo, lock); + } +} + +void CheckerboardEvent::LogInfo(RendertraceProperty aProperty, + const TimeStamp& aTimestamp, + const CSSRect& aRect, + const std::string& aExtraInfo, + const MonitorAutoLock& aProofOfLock) { + MOZ_ASSERT(mRecordTrace); + if (mRendertraceInfo.tellp() >= LOG_LENGTH_LIMIT) { + // The log is already long enough, don't put more things into it. We'll + // append a truncation message when this event ends. + return; + } + // The log is consumed by the page at about:checkerboard. The format is not + // formally specced, but an informal description can be found at + // https://searchfox.org/mozilla-central/rev/d866b96d74ec2a63f09ee418f048d23f4fd379a2/toolkit/components/aboutcheckerboard/content/aboutCheckerboard.js#86 + mRendertraceInfo << "RENDERTRACE " + << (aTimestamp - mOriginTime).ToMilliseconds() << " rect " + << sColors[aProperty] << " " << aRect.X() << " " << aRect.Y() + << " " << aRect.Width() << " " << aRect.Height() << " " + << "// " << sDescriptions[aProperty] << aExtraInfo + << std::endl; +} + +bool CheckerboardEvent::RecordFrameInfo(uint32_t aCssPixelsCheckerboarded) { + TimeStamp sampleTime = TimeStamp::Now(); + bool eventEnding = false; + if (aCssPixelsCheckerboarded > 0) { + if (!mCheckerboardingActive) { + StartEvent(); + } + MOZ_ASSERT(mCheckerboardingActive); + MOZ_ASSERT(sampleTime >= mLastSampleTime); + mTotalPixelMs += + (uint64_t)((sampleTime - mLastSampleTime).ToMilliseconds() * + aCssPixelsCheckerboarded); + if (aCssPixelsCheckerboarded > mPeakPixels) { + mPeakPixels = aCssPixelsCheckerboarded; + } + mFrameCount++; + } else { + if (mCheckerboardingActive) { + StopEvent(); + eventEnding = true; + } + MOZ_ASSERT(!mCheckerboardingActive); + } + mLastSampleTime = sampleTime; + return eventEnding; +} + +void CheckerboardEvent::StartEvent() { + MOZ_LOG(sApzCheckLog, LogLevel::Debug, ("Starting checkerboard event")); + MOZ_ASSERT(!mCheckerboardingActive); + mCheckerboardingActive = true; + mStartTime = TimeStamp::Now(); + + if (!mRecordTrace) { + return; + } + MonitorAutoLock lock(mRendertraceLock); + std::vector history; + for (PropertyBuffer& bufferedProperty : mBufferedProperties) { + bufferedProperty.Flush(history, lock); + } + std::sort(history.begin(), history.end()); + for (const PropertyValue& p : history) { + LogInfo(p.mProperty, p.mTimeStamp, p.mRect, p.mExtraInfo, lock); + } + mRendertraceInfo << " -- checkerboarding starts below --" << std::endl; +} + +void CheckerboardEvent::StopEvent() { + MOZ_LOG(sApzCheckLog, LogLevel::Debug, ("Stopping checkerboard event")); + mCheckerboardingActive = false; + mEndTime = TimeStamp::Now(); + + if (!mRecordTrace) { + return; + } + MonitorAutoLock lock(mRendertraceLock); + if (mRendertraceInfo.tellp() >= LOG_LENGTH_LIMIT) { + mRendertraceInfo << "[logging aborted due to length limitations]\n"; + } + mRendertraceInfo << "Checkerboarded for " << mFrameCount << " frames (" + << (mEndTime - mStartTime).ToMilliseconds() << " ms), " + << mPeakPixels << " peak, " << GetSeverity() << " severity." + << std::endl; +} + +bool CheckerboardEvent::PropertyValue::operator<( + const PropertyValue& aOther) const { + if (mTimeStamp < aOther.mTimeStamp) { + return true; + } else if (mTimeStamp > aOther.mTimeStamp) { + return false; + } + return mProperty < aOther.mProperty; +} + +CheckerboardEvent::PropertyBuffer::PropertyBuffer() : mIndex(0) {} + +void CheckerboardEvent::PropertyBuffer::Update( + RendertraceProperty aProperty, const CSSRect& aRect, + const std::string& aExtraInfo, const MonitorAutoLock& aProofOfLock) { + mValues[mIndex] = {aProperty, TimeStamp::Now(), aRect, aExtraInfo}; + mIndex = (mIndex + 1) % BUFFER_SIZE; +} + +void CheckerboardEvent::PropertyBuffer::Flush( + std::vector& aOut, const MonitorAutoLock& aProofOfLock) { + for (uint32_t i = 0; i < BUFFER_SIZE; i++) { + uint32_t ix = (mIndex + i) % BUFFER_SIZE; + if (!mValues[ix].mTimeStamp.IsNull()) { + aOut.push_back(mValues[ix]); + mValues[ix].mTimeStamp = TimeStamp(); + } + } +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/CheckerboardEvent.h b/gfx/layers/apz/src/CheckerboardEvent.h new file mode 100644 index 0000000000..ad7fa83b2b --- /dev/null +++ b/gfx/layers/apz/src/CheckerboardEvent.h @@ -0,0 +1,218 @@ +/* -*- 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_CheckerboardEvent_h +#define mozilla_layers_CheckerboardEvent_h + +#include "mozilla/DefineEnum.h" +#include "mozilla/Monitor.h" +#include "mozilla/TimeStamp.h" +#include +#include "Units.h" +#include + +namespace mozilla { +namespace layers { + +/** + * This class records information relevant to one "checkerboard event", which is + * a contiguous set of frames where a given APZC was checkerboarding. The intent + * of this class is to record enough information that it can provide actionable + * steps to reduce the occurrence of checkerboarding. Furthermore, it records + * information about the severity of the checkerboarding so as to allow + * prioritizing the debugging of some checkerboarding events over others. + */ +class CheckerboardEvent final { + public: + // clang-format off + MOZ_DEFINE_ENUM_AT_CLASS_SCOPE( + RendertraceProperty, ( + Page, + PaintedDisplayPort, + RequestedDisplayPort, + UserVisible + )); + // clang-format on + + static const char* sDescriptions[sRendertracePropertyCount]; + static const char* sColors[sRendertracePropertyCount]; + + public: + explicit CheckerboardEvent(bool aRecordTrace); + + /** + * Gets the "severity" of the checkerboard event. This doesn't have units, + * it's just useful for comparing two checkerboard events to see which one + * is worse, for some implementation-specific definition of "worse". + */ + uint32_t GetSeverity(); + + /** + * Gets the number of CSS pixels that were checkerboarded at the peak of the + * checkerboard event. + */ + uint32_t GetPeak(); + + /** + * Gets the length of the checkerboard event. + */ + TimeDuration GetDuration(); + + /** + * Gets the raw log of the checkerboard event. This can be called any time, + * although it really only makes sense to pull once the event is done, after + * RecordFrameInfo returns true. + */ + std::string GetLog(); + + /** + * Returns true iff this event is recording a detailed trace of the event. + * This is the argument passed in to the constructor. + */ + bool IsRecordingTrace(); + + /** + * Provide a new value for one of the rects that is tracked for + * checkerboard events. + */ + void UpdateRendertraceProperty(RendertraceProperty aProperty, + const CSSRect& aRect, + const std::string& aExtraInfo = std::string()); + + /** + * Provide the number of CSS pixels that are checkerboarded in a composite + * at the current time. + * @return true if the checkerboard event has completed. The caller should + * stop updating this object once this happens. + */ + bool RecordFrameInfo(uint32_t aCssPixelsCheckerboarded); + + private: + /** + * Helper method to do stuff when checkeboarding starts. + */ + void StartEvent(); + /** + * Helper method to do stuff when checkerboarding stops. + */ + void StopEvent(); + + /** + * Helper method to log a rendertrace property and its value to the + * rendertrace info buffer (mRendertraceInfo). + */ + void LogInfo(RendertraceProperty aProperty, const TimeStamp& aTimestamp, + const CSSRect& aRect, const std::string& aExtraInfo, + const MonitorAutoLock& aProofOfLock); + + /** + * Helper struct that holds a single rendertrace property value. + */ + struct PropertyValue { + RendertraceProperty mProperty; + TimeStamp mTimeStamp; + CSSRect mRect; + std::string mExtraInfo; + + bool operator<(const PropertyValue& aOther) const; + }; + + /** + * A circular buffer that stores the most recent BUFFER_SIZE values of a + * given property. + */ + class PropertyBuffer { + public: + PropertyBuffer(); + /** + * Add a new value to the buffer, overwriting the oldest one if needed. + */ + void Update(RendertraceProperty aProperty, const CSSRect& aRect, + const std::string& aExtraInfo, + const MonitorAutoLock& aProofOfLock); + /** + * Dump the recorded values, oldest to newest, to the given vector, and + * remove them from this buffer. + */ + void Flush(std::vector& aOut, + const MonitorAutoLock& aProofOfLock); + + private: + static const uint32_t BUFFER_SIZE = 5; + + /** + * The index of the oldest value in the buffer. This is the next index + * that will be written to. + */ + uint32_t mIndex; + PropertyValue mValues[BUFFER_SIZE]; + }; + + private: + /** + * If true, we should log the various properties during the checkerboard + * event. If false, we only need to record things we need for telemetry + * measures. + */ + const bool mRecordTrace; + /** + * A base time so that the other timestamps can be turned into durations. + */ + const TimeStamp mOriginTime; + /** + * Whether or not a checkerboard event is currently occurring. + */ + bool mCheckerboardingActive; + + /** + * The start time of the checkerboard event. + */ + TimeStamp mStartTime; + /** + * The end time of the checkerboard event. + */ + TimeStamp mEndTime; + /** + * The sample time of the last frame recorded. + */ + TimeStamp mLastSampleTime; + /** + * The number of contiguous frames with checkerboard. + */ + uint32_t mFrameCount; + /** + * The total number of pixel-milliseconds of checkerboarding visible to + * the user during the checkerboarding event. + */ + uint64_t mTotalPixelMs; + /** + * The largest number of pixels of checkerboarding visible to the user + * during any one frame, during this checkerboarding event. + */ + uint32_t mPeakPixels; + + /** + * Monitor that needs to be acquired before touching mBufferedProperties + * or mRendertraceInfo. + */ + mutable Monitor mRendertraceLock MOZ_UNANNOTATED; + /** + * A circular buffer to store some properties. This is used before the + * checkerboarding actually starts, so that we have some data on what + * was happening before the checkerboarding started. + */ + PropertyBuffer mBufferedProperties[sRendertracePropertyCount]; + /** + * The rendertrace info buffer that gives us info on what was happening + * during the checkerboard event. + */ + std::ostringstream mRendertraceInfo; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_CheckerboardEvent_h diff --git a/gfx/layers/apz/src/DesktopFlingPhysics.h b/gfx/layers/apz/src/DesktopFlingPhysics.h new file mode 100644 index 0000000000..e93cc07a23 --- /dev/null +++ b/gfx/layers/apz/src/DesktopFlingPhysics.h @@ -0,0 +1,67 @@ +/* -*- 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_DesktopFlingPhysics_h_ +#define mozilla_layers_DesktopFlingPhysics_h_ + +#include "AsyncPanZoomController.h" +#include "Units.h" +#include "mozilla/Assertions.h" +#include "mozilla/StaticPrefs_apz.h" + +namespace mozilla { +namespace layers { + +class DesktopFlingPhysics { + public: + void Init(const ParentLayerPoint& aStartingVelocity, + float aPLPPI /* unused */) { + mVelocity = aStartingVelocity; + } + void Sample(const TimeDuration& aDelta, ParentLayerPoint* aOutVelocity, + ParentLayerPoint* aOutOffset) { + float friction = StaticPrefs::apz_fling_friction(); + float threshold = StaticPrefs::apz_fling_stopped_threshold(); + + mVelocity = ParentLayerPoint( + ApplyFrictionOrCancel(mVelocity.x, aDelta, friction, threshold), + ApplyFrictionOrCancel(mVelocity.y, aDelta, friction, threshold)); + + *aOutVelocity = mVelocity; + *aOutOffset = mVelocity * aDelta.ToMilliseconds(); + } + + private: + /** + * Applies friction to the given velocity and returns the result, or + * returns zero if the velocity is too low. + * |aVelocity| is the incoming velocity. + * |aDelta| is the amount of time that has passed since the last time + * friction was applied. + * |aFriction| is the amount of friction to apply. + * |aThreshold| is the velocity below which the fling is cancelled. + */ + static float ApplyFrictionOrCancel(float aVelocity, + const TimeDuration& aDelta, + float aFriction, float aThreshold) { + if (fabsf(aVelocity) <= aThreshold) { + // If the velocity is very low, just set it to 0 and stop the fling, + // otherwise we'll just asymptotically approach 0 and the user won't + // actually see any changes. + return 0.0f; + } + + aVelocity *= pow(1.0f - aFriction, float(aDelta.ToMilliseconds())); + return aVelocity; + } + + ParentLayerPoint mVelocity; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_DesktopFlingPhysics_h_ diff --git a/gfx/layers/apz/src/DragTracker.cpp b/gfx/layers/apz/src/DragTracker.cpp new file mode 100644 index 0000000000..aa3b2a34f7 --- /dev/null +++ b/gfx/layers/apz/src/DragTracker.cpp @@ -0,0 +1,59 @@ +/* -*- 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 "DragTracker.h" + +#include "InputData.h" +#include "mozilla/Logging.h" + +static mozilla::LazyLogModule sApzDrgLog("apz.drag"); +#define DRAG_LOG(...) MOZ_LOG(sApzDrgLog, LogLevel::Debug, (__VA_ARGS__)) + +namespace mozilla { +namespace layers { + +DragTracker::DragTracker() : mInDrag(false) {} + +/*static*/ +bool DragTracker::StartsDrag(const MouseInput& aInput) { + return aInput.IsLeftButton() && aInput.mType == MouseInput::MOUSE_DOWN; +} + +/*static*/ +bool DragTracker::EndsDrag(const MouseInput& aInput) { + // On Windows, we don't receive a MOUSE_UP at the end of a drag if an + // actual drag session took place. As a backup, we detect the end of the + // drag using the MOUSE_DRAG_END event, which normally is routed directly + // to content, but we're specially routing to APZ for this purpose. Bug + // 1265105 tracks a solution to this at the Windows widget layer; once + // that is implemented, this workaround can be removed. + return (aInput.IsLeftButton() && aInput.mType == MouseInput::MOUSE_UP) || + aInput.mType == MouseInput::MOUSE_DRAG_END; +} + +void DragTracker::Update(const MouseInput& aInput) { + if (StartsDrag(aInput)) { + DRAG_LOG("Starting drag\n"); + mInDrag = true; + } else if (EndsDrag(aInput)) { + DRAG_LOG("Ending drag\n"); + mInDrag = false; + mOnScrollbar = Nothing(); + } +} + +bool DragTracker::InDrag() const { return mInDrag; } + +bool DragTracker::IsOnScrollbar(bool aOnScrollbar) { + if (!mOnScrollbar) { + DRAG_LOG("Setting hitscrollbar %d\n", aOnScrollbar); + mOnScrollbar = Some(aOnScrollbar); + } + return mOnScrollbar.value(); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/DragTracker.h b/gfx/layers/apz/src/DragTracker.h new file mode 100644 index 0000000000..92678d53c1 --- /dev/null +++ b/gfx/layers/apz/src/DragTracker.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_DragTracker_h +#define mozilla_layers_DragTracker_h + +#include "mozilla/EventForwards.h" +#include "mozilla/Maybe.h" + +namespace mozilla { + +class MouseInput; + +namespace layers { + +// DragTracker simply tracks a sequence of mouse inputs and allows us to tell +// if we are in a drag or not (i.e. the left mouse button went down and hasn't +// gone up yet). +class DragTracker { + public: + DragTracker(); + static bool StartsDrag(const MouseInput& aInput); + static bool EndsDrag(const MouseInput& aInput); + void Update(const MouseInput& aInput); + bool InDrag() const; + bool IsOnScrollbar(bool aOnScrollbar); + + private: + Maybe mOnScrollbar; + bool mInDrag; +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_DragTracker_h */ diff --git a/gfx/layers/apz/src/ExpectedGeckoMetrics.cpp b/gfx/layers/apz/src/ExpectedGeckoMetrics.cpp new file mode 100644 index 0000000000..26b94e94d4 --- /dev/null +++ b/gfx/layers/apz/src/ExpectedGeckoMetrics.cpp @@ -0,0 +1,26 @@ +/* -*- 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 "ExpectedGeckoMetrics.h" +#include "FrameMetrics.h" + +namespace mozilla { +namespace layers { + +void ExpectedGeckoMetrics::UpdateFrom(const FrameMetrics& aMetrics) { + mVisualScrollOffset = aMetrics.GetVisualScrollOffset(); + mLayoutScrollOffset = aMetrics.GetLayoutScrollOffset(); + mZoom = aMetrics.GetZoom(); + mDevPixelsPerCSSPixel = aMetrics.GetDevPixelsPerCSSPixel(); +} + +void ExpectedGeckoMetrics::UpdateZoomFrom(const FrameMetrics& aMetrics) { + mZoom = aMetrics.GetZoom(); + mDevPixelsPerCSSPixel = aMetrics.GetDevPixelsPerCSSPixel(); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/ExpectedGeckoMetrics.h b/gfx/layers/apz/src/ExpectedGeckoMetrics.h new file mode 100644 index 0000000000..ca5aa76eba --- /dev/null +++ b/gfx/layers/apz/src/ExpectedGeckoMetrics.h @@ -0,0 +1,44 @@ +/* -*- 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_ExpectedGeckoMetrics_h +#define mozilla_layers_ExpectedGeckoMetrics_h + +#include "Units.h" + +namespace mozilla { +namespace layers { + +struct FrameMetrics; + +// A class that stores a subset of the FrameMetrics information +// than an APZC instance expects Gecko to have (either the +// metrics that were most recently sent to Gecko, or the ones +// most recently received from Gecko). +class ExpectedGeckoMetrics { + public: + ExpectedGeckoMetrics() = default; + void UpdateFrom(const FrameMetrics& aMetrics); + void UpdateZoomFrom(const FrameMetrics& aMetrics); + + const CSSPoint& GetVisualScrollOffset() const { return mVisualScrollOffset; } + const CSSPoint& GetLayoutScrollOffset() const { return mLayoutScrollOffset; } + const CSSToParentLayerScale& GetZoom() const { return mZoom; } + const CSSToLayoutDeviceScale& GetDevPixelsPerCSSPixel() const { + return mDevPixelsPerCSSPixel; + } + + private: + CSSPoint mVisualScrollOffset; + CSSPoint mLayoutScrollOffset; + CSSToParentLayerScale mZoom; + CSSToLayoutDeviceScale mDevPixelsPerCSSPixel; +}; + +} // namespace layers +} // namespace mozilla + +#endif diff --git a/gfx/layers/apz/src/FlingAccelerator.cpp b/gfx/layers/apz/src/FlingAccelerator.cpp new file mode 100644 index 0000000000..f37d04c77b --- /dev/null +++ b/gfx/layers/apz/src/FlingAccelerator.cpp @@ -0,0 +1,128 @@ +/* -*- 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 "FlingAccelerator.h" + +#include "mozilla/StaticPrefs_apz.h" + +#include "GenericFlingAnimation.h" // for FLING_LOG and FlingHandoffState + +namespace mozilla { +namespace layers { + +void FlingAccelerator::Reset() { + mPreviousFlingStartingVelocity = ParentLayerPoint{}; + mPreviousFlingCancelVelocity = ParentLayerPoint{}; + mIsTracking = false; +} + +static bool SameDirection(float aVelocity1, float aVelocity2) { + return (aVelocity1 == 0.0f) || (aVelocity2 == 0.0f) || + (IsNegative(aVelocity1) == IsNegative(aVelocity2)); +} + +static float Accelerate(float aBase, float aSupplemental) { + return (aBase * StaticPrefs::apz_fling_accel_base_mult()) + + (aSupplemental * StaticPrefs::apz_fling_accel_supplemental_mult()); +} + +ParentLayerPoint FlingAccelerator::GetFlingStartingVelocity( + const SampleTime& aNow, const ParentLayerPoint& aVelocity, + const FlingHandoffState& aHandoffState) { + // If the fling should be accelerated and is in the same direction as the + // previous fling, boost the velocity to be the sum of the two. Check separate + // axes separately because we could have two vertical flings with small + // horizontal components on the opposite side of zero, and we still want the + // y-fling to get accelerated. + ParentLayerPoint velocity = aVelocity; + if (ShouldAccelerate(aNow, aVelocity, aHandoffState)) { + if (velocity.x != 0 && + SameDirection(velocity.x, mPreviousFlingStartingVelocity.x)) { + velocity.x = Accelerate(velocity.x, mPreviousFlingStartingVelocity.x); + FLING_LOG("%p Applying fling x-acceleration from %f to %f (delta %f)\n", + this, aVelocity.x.value, velocity.x.value, + mPreviousFlingStartingVelocity.x.value); + } + if (velocity.y != 0 && + SameDirection(velocity.y, mPreviousFlingStartingVelocity.y)) { + velocity.y = Accelerate(velocity.y, mPreviousFlingStartingVelocity.y); + FLING_LOG("%p Applying fling y-acceleration from %f to %f (delta %f)\n", + this, aVelocity.y.value, velocity.y.value, + mPreviousFlingStartingVelocity.y.value); + } + } + + Reset(); + + mPreviousFlingStartingVelocity = velocity; + mIsTracking = true; + + return velocity; +} + +bool FlingAccelerator::ShouldAccelerate( + const SampleTime& aNow, const ParentLayerPoint& aVelocity, + const FlingHandoffState& aHandoffState) const { + if (!IsTracking()) { + FLING_LOG("%p Fling accelerator was reset, not accelerating.\n", this); + return false; + } + + if (!aHandoffState.mTouchStartRestingTime) { + FLING_LOG("%p Don't have a touch start resting time, not accelerating.\n", + this); + return false; + } + + double msBetweenTouchStartAndPanStart = + aHandoffState.mTouchStartRestingTime->ToMilliseconds(); + FLING_LOG( + "%p ShouldAccelerate with pan velocity %f pixels/ms, min pan velocity %f " + "pixels/ms, previous fling cancel velocity %f pixels/ms, time elapsed " + "since starting previous time between touch start and pan " + "start %fms.\n", + this, float(aVelocity.Length()), float(aHandoffState.mMinPanVelocity), + float(mPreviousFlingCancelVelocity.Length()), + float(msBetweenTouchStartAndPanStart)); + + if (aVelocity.Length() < StaticPrefs::apz_fling_accel_min_fling_velocity()) { + FLING_LOG("%p Fling velocity too low (%f), not accelerating.\n", this, + float(aVelocity.Length())); + return false; + } + + if (aHandoffState.mMinPanVelocity < + StaticPrefs::apz_fling_accel_min_pan_velocity()) { + FLING_LOG( + "%p Panning velocity was too slow at some point during the pan (%f), " + "not accelerating.\n", + this, float(aHandoffState.mMinPanVelocity)); + return false; + } + + if (mPreviousFlingCancelVelocity.Length() < + StaticPrefs::apz_fling_accel_min_fling_velocity()) { + FLING_LOG( + "%p The previous fling animation had slowed down too much when it was " + "interrupted (%f), not accelerating.\n", + this, float(mPreviousFlingCancelVelocity.Length())); + return false; + } + + if (msBetweenTouchStartAndPanStart >= + StaticPrefs::apz_fling_accel_max_pause_interval_ms()) { + FLING_LOG( + "%p Too much time (%fms) elapsed between touch start and pan start, " + "not accelerating.\n", + this, msBetweenTouchStartAndPanStart); + return false; + } + + return true; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/FlingAccelerator.h b/gfx/layers/apz/src/FlingAccelerator.h new file mode 100644 index 0000000000..49e9a7b257 --- /dev/null +++ b/gfx/layers/apz/src/FlingAccelerator.h @@ -0,0 +1,59 @@ +/* -*- 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_FlingAccelerator_h +#define mozilla_layers_FlingAccelerator_h + +#include "mozilla/layers/SampleTime.h" +#include "Units.h" + +namespace mozilla { +namespace layers { + +struct FlingHandoffState; + +/** + * This class is used to track state that is used when determining whether a + * fling should be accelerated. + */ +class FlingAccelerator final { + public: + FlingAccelerator() {} + + // Resets state so that the next fling will not be accelerated. + void Reset(); + + // Returns false after a reset or before the first fling. + bool IsTracking() const { return mIsTracking; } + + // Starts a new fling, and returns the (potentially accelerated) velocity that + // should be used for that fling. + ParentLayerPoint GetFlingStartingVelocity( + const SampleTime& aNow, const ParentLayerPoint& aVelocity, + const FlingHandoffState& aHandoffState); + + void ObserveFlingCanceled(const ParentLayerPoint& aVelocity) { + mPreviousFlingCancelVelocity = aVelocity; + } + + protected: + bool ShouldAccelerate(const SampleTime& aNow, + const ParentLayerPoint& aVelocity, + const FlingHandoffState& aHandoffState) const; + + // The initial velocity of the most recent fling. + ParentLayerPoint mPreviousFlingStartingVelocity; + // The velocity that the previous fling animation had at the point it was + // interrupted. + ParentLayerPoint mPreviousFlingCancelVelocity; + // Whether the upcoming fling is eligible for acceleration. + bool mIsTracking = false; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_FlingAccelerator_h diff --git a/gfx/layers/apz/src/FocusState.cpp b/gfx/layers/apz/src/FocusState.cpp new file mode 100644 index 0000000000..b7510bcef4 --- /dev/null +++ b/gfx/layers/apz/src/FocusState.cpp @@ -0,0 +1,225 @@ +/* -*- 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 "FocusState.h" + +#include "mozilla/Logging.h" +#include "mozilla/layers/APZThreadUtils.h" + +static mozilla::LazyLogModule sApzFstLog("apz.focusstate"); +#define FS_LOG(...) MOZ_LOG(sApzFstLog, LogLevel::Debug, (__VA_ARGS__)) + +namespace mozilla { +namespace layers { + +FocusState::FocusState() + : mMutex("FocusStateMutex"), + mLastAPZProcessedEvent(1), + mLastContentProcessedEvent(0), + mFocusHasKeyEventListeners(false), + mReceivedUpdate(false), + mFocusLayersId{0}, + mFocusHorizontalTarget(ScrollableLayerGuid::NULL_SCROLL_ID), + mFocusVerticalTarget(ScrollableLayerGuid::NULL_SCROLL_ID) {} + +uint64_t FocusState::LastAPZProcessedEvent() const { + APZThreadUtils::AssertOnControllerThread(); + MutexAutoLock lock(mMutex); + + return mLastAPZProcessedEvent; +} + +bool FocusState::IsCurrent(const MutexAutoLock& aProofOfLock) const { + FS_LOG("Checking IsCurrent() with cseq=%" PRIu64 ", aseq=%" PRIu64 "\n", + mLastContentProcessedEvent, mLastAPZProcessedEvent); + + MOZ_ASSERT(mLastContentProcessedEvent <= mLastAPZProcessedEvent); + return mLastContentProcessedEvent == mLastAPZProcessedEvent; +} + +void FocusState::ReceiveFocusChangingEvent() { + APZThreadUtils::AssertOnControllerThread(); + MutexAutoLock lock(mMutex); + + if (!mReceivedUpdate) { + // In the initial state don't advance mLastAPZProcessedEvent because we + // might blow away the information that we're in a freshly-restarted GPU + // process. This information (i.e. that mLastAPZProcessedEvent == 1) needs + // to be preserved until the first call to Update() which will then advance + // mLastAPZProcessedEvent to match the content-side sequence number. + return; + } + mLastAPZProcessedEvent += 1; + FS_LOG("Focus changing event incremented aseq to %" PRIu64 "\n", + mLastAPZProcessedEvent); +} + +void FocusState::Update(LayersId aRootLayerTreeId, + LayersId aOriginatingLayersId, + const FocusTarget& aState) { + // This runs on the updater thread, it's not worth passing around extra raw + // pointers just to assert it. + + MutexAutoLock lock(mMutex); + + FS_LOG("Update with rlt=%" PRIu64 ", olt=%" PRIu64 ", ft=(%s, %" PRIu64 ")\n", + aRootLayerTreeId.mId, aOriginatingLayersId.mId, aState.Type(), + aState.mSequenceNumber); + mReceivedUpdate = true; + + // Update the focus tree with the latest target + mFocusTree[aOriginatingLayersId] = aState; + + // Reset our internal state so we can recalculate it + mFocusHasKeyEventListeners = false; + mFocusLayersId = aRootLayerTreeId; + mFocusHorizontalTarget = ScrollableLayerGuid::NULL_SCROLL_ID; + mFocusVerticalTarget = ScrollableLayerGuid::NULL_SCROLL_ID; + + // To update the focus state for the entire APZCTreeManager, we need + // to traverse the focus tree to find the current leaf which is the global + // focus target we can use for async keyboard scrolling + while (true) { + auto currentNode = mFocusTree.find(mFocusLayersId); + if (currentNode == mFocusTree.end()) { + FS_LOG("Setting target to nil (cannot find lt=%" PRIu64 ")\n", + mFocusLayersId.mId); + return; + } + + const FocusTarget& target = currentNode->second; + + // Accumulate event listener flags on the path to the focus target + mFocusHasKeyEventListeners |= target.mFocusHasKeyEventListeners; + + // Match on the data stored in mData + // The match functions return true or false depending on whether the + // enclosing method, FocusState::Update, should return or continue to the + // next iteration of the while loop, respectively. + struct FocusTargetDataMatcher { + FocusState& mFocusState; + const uint64_t mSequenceNumber; + + bool operator()(const FocusTarget::NoFocusTarget& aNoFocusTarget) { + FS_LOG("Setting target to nil (reached a nil target) with seq=%" PRIu64 + "\n", + mSequenceNumber); + + // Mark what sequence number this target has for debugging purposes so + // we can always accurately report on whether we are stale or not + mFocusState.mLastContentProcessedEvent = mSequenceNumber; + + // If this focus state was just created and content has experienced more + // events then us, then assume we were recreated and sync focus sequence + // numbers. + if (mFocusState.mLastAPZProcessedEvent == 1 && + mFocusState.mLastContentProcessedEvent > + mFocusState.mLastAPZProcessedEvent) { + mFocusState.mLastAPZProcessedEvent = + mFocusState.mLastContentProcessedEvent; + } + return true; + } + + bool operator()(const LayersId& aRefLayerId) { + // Guard against infinite loops + MOZ_ASSERT(mFocusState.mFocusLayersId != aRefLayerId); + if (mFocusState.mFocusLayersId == aRefLayerId) { + FS_LOG( + "Setting target to nil (bailing out of infinite loop, lt=%" PRIu64 + ")\n", + mFocusState.mFocusLayersId.mId); + return true; + } + + FS_LOG("Looking for target in lt=%" PRIu64 "\n", aRefLayerId.mId); + + // The focus target is in a child layer tree + mFocusState.mFocusLayersId = aRefLayerId; + return false; + } + + bool operator()(const FocusTarget::ScrollTargets& aScrollTargets) { + FS_LOG("Setting target to h=%" PRIu64 ", v=%" PRIu64 + ", and seq=%" PRIu64 "\n", + aScrollTargets.mHorizontal, aScrollTargets.mVertical, + mSequenceNumber); + + // This is the global focus target + mFocusState.mFocusHorizontalTarget = aScrollTargets.mHorizontal; + mFocusState.mFocusVerticalTarget = aScrollTargets.mVertical; + + // Mark what sequence number this target has so we can determine whether + // it is stale or not + mFocusState.mLastContentProcessedEvent = mSequenceNumber; + + // If this focus state was just created and content has experienced more + // events then us, then assume we were recreated and sync focus sequence + // numbers. + if (mFocusState.mLastAPZProcessedEvent == 1 && + mFocusState.mLastContentProcessedEvent > + mFocusState.mLastAPZProcessedEvent) { + mFocusState.mLastAPZProcessedEvent = + mFocusState.mLastContentProcessedEvent; + } + return true; + } + }; // struct FocusTargetDataMatcher + + if (target.mData.match( + FocusTargetDataMatcher{*this, target.mSequenceNumber})) { + return; + } + } +} + +void FocusState::RemoveFocusTarget(LayersId aLayersId) { + // This runs on the updater thread, it's not worth passing around extra raw + // pointers just to assert it. + MutexAutoLock lock(mMutex); + + mFocusTree.erase(aLayersId); +} + +Maybe FocusState::GetHorizontalTarget() const { + APZThreadUtils::AssertOnControllerThread(); + MutexAutoLock lock(mMutex); + + // There is not a scrollable layer to async scroll if + // 1. We aren't current + // 2. There are event listeners that could change the focus + // 3. The target has not been layerized + if (!IsCurrent(lock) || mFocusHasKeyEventListeners || + mFocusHorizontalTarget == ScrollableLayerGuid::NULL_SCROLL_ID) { + return Nothing(); + } + return Some(ScrollableLayerGuid(mFocusLayersId, 0, mFocusHorizontalTarget)); +} + +Maybe FocusState::GetVerticalTarget() const { + APZThreadUtils::AssertOnControllerThread(); + MutexAutoLock lock(mMutex); + + // There is not a scrollable layer to async scroll if: + // 1. We aren't current + // 2. There are event listeners that could change the focus + // 3. The target has not been layerized + if (!IsCurrent(lock) || mFocusHasKeyEventListeners || + mFocusVerticalTarget == ScrollableLayerGuid::NULL_SCROLL_ID) { + return Nothing(); + } + return Some(ScrollableLayerGuid(mFocusLayersId, 0, mFocusVerticalTarget)); +} + +bool FocusState::CanIgnoreKeyboardShortcutMisses() const { + APZThreadUtils::AssertOnControllerThread(); + MutexAutoLock lock(mMutex); + + return IsCurrent(lock) && !mFocusHasKeyEventListeners; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/FocusState.h b/gfx/layers/apz/src/FocusState.h new file mode 100644 index 0000000000..14d536be3e --- /dev/null +++ b/gfx/layers/apz/src/FocusState.h @@ -0,0 +1,175 @@ +/* -*- 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_FocusState_h +#define mozilla_layers_FocusState_h + +#include // for std::unordered_map +#include // for std::unordered_set + +#include "mozilla/layers/FocusTarget.h" // for FocusTarget +#include "mozilla/layers/ScrollableLayerGuid.h" // for ViewID +#include "mozilla/Mutex.h" // for Mutex + +namespace mozilla { +namespace layers { + +/** + * This class is used for tracking chrome and content focus targets and + * calculating global focus information from them for use by APZCTreeManager + * for async keyboard scrolling. + * + * # Calculating the element to scroll + * + * Chrome and content processes have independently focused elements. This makes + * it difficult to calculate the global focused element and its scrollable + * frame from the chrome or content side. So instead we send the local focus + * information from each process to here and then calculate the global focus + * information. This local information resides in a `focus target`. + * + * A focus target indicates that either: + * 1. The focused element is a remote browser along with its layer tree ID + * 2. The focused element is not scrollable + * 3. The focused element is scrollable along with the ViewID's of its + scrollable layers + * + * Using this information we can determine the global focus information by + * starting at the focus target of the root layer tree ID and following remote + * browsers until we reach a scrollable or non-scrollable focus target. + * + * # Determinism and sequence numbers + * + * The focused element in content can be changed within any javascript code. And + * javascript can run in response to an event or at any moment from `setTimeout` + * and others. This makes it impossible to always have the current focus + * information in APZ as it can be changed asynchronously at any moment. If we + * don't have the latest focus information, we may incorrectly scroll a target + * when we shouldn't. + * + * A tradeoff is designed here whereby we will maintain deterministic focus + * changes for user input, but not for other javascript code. The reasoning + * here is that `setTimeout` and others are already non-deterministic and so it + * might not be as breaking to web content. + * + * To maintain deterministic focus changes for a given stream of user inputs, + * we invalidate our focus state whenever we receive a user input that may + * trigger event listeners. We then attach a new sequence number to these + * events and dispatch them to content. Content will then include the latest + * sequence number it has processed to every focus update. Using this we can + * determine whether any potentially focus changing events have yet to be + * handled by content. + * + * Once we have received the latest focus sequence number from content, we know + * that all event listeners triggered by user inputs, and their resulting focus + * changes, have been processed and so we have a current target that we can use + * again. + */ +class FocusState final { + public: + FocusState(); + + /** + * The sequence number of the last potentially focus changing event processed + * by APZ. This number starts at one and increases monotonically. This number + * will never be zero as that is used to catch uninitialized focus sequence + * numbers on input events. + */ + uint64_t LastAPZProcessedEvent() const; + + /** + * Notify focus state of a potentially focus changing event. This will + * increment the current focus sequence number. The new value can be gotten + * from LastAPZProcessedEvent(). + */ + void ReceiveFocusChangingEvent(); + + /** + * Update the internal focus tree and recalculate the global focus target for + * a focus target update received from chrome or content. + * + * @param aRootLayerTreeId the layer tree ID of the root layer for the + parent APZCTreeManager + * @param aOriginatingLayersId the layer tree ID that this focus target + belongs to + */ + void Update(LayersId aRootLayerTreeId, LayersId aOriginatingLayersId, + const FocusTarget& aTarget); + + /** + * Removes a focus target by its layer tree ID. + */ + void RemoveFocusTarget(LayersId aLayersId); + + /** + * Gets the scrollable layer that should be horizontally scrolled for a key + * event, if any. The returned ScrollableLayerGuid doesn't contain a + * presShellId, and so it should not be used in comparisons. + * + * No scrollable layer is returned if any of the following are true: + * 1. We don't have a current focus target + * 2. There are event listeners that could change the focus + * 3. The target has not been layerized + */ + Maybe GetHorizontalTarget() const; + /** + * The same as GetHorizontalTarget() but for vertical scrolling. + */ + Maybe GetVerticalTarget() const; + + /** + * Gets whether it is safe to not increment the focus sequence number for an + * unmatched keyboard event. + */ + bool CanIgnoreKeyboardShortcutMisses() const; + + private: + /** + * Whether the current focus state is known to be current or else if an event + * has been processed that could change the focus but we have not received an + * update with a new confirmed target. + * This can only be called by methods that have already acquired mMutex; they + * have to pass their lock as compile-time proof. + */ + bool IsCurrent(const MutexAutoLock& aLock) const; + + private: + // All methods should hold this lock, since this class is accessed via both + // the updater and controller threads. + mutable Mutex mMutex MOZ_UNANNOTATED; + + // The set of focus targets received indexed by their layer tree ID + std::unordered_map mFocusTree; + + // The focus sequence number of the last potentially focus changing event + // processed by APZ. This number starts at one and increases monotonically. + // We don't worry about wrap around here because at a pace of 100 + // increments/sec, it would take 5.85*10^9 years before we would wrap around. + // This number will never be zero as that is used to catch uninitialized focus + // sequence numbers on input events. + uint64_t mLastAPZProcessedEvent; + // The focus sequence number last received in a focus update. + uint64_t mLastContentProcessedEvent; + + // A flag whether there is a key listener on the event target chain for the + // focused element + bool mFocusHasKeyEventListeners; + // A flag that is false until the first call to Update(). + bool mReceivedUpdate; + + // The layer tree ID which contains the scrollable frame of the focused + // element + LayersId mFocusLayersId; + // The scrollable layer corresponding to the scrollable frame that is used to + // scroll the focused element. This depends on the direction the user is + // scrolling. + ScrollableLayerGuid::ViewID mFocusHorizontalTarget; + ScrollableLayerGuid::ViewID mFocusVerticalTarget; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_FocusState_h diff --git a/gfx/layers/apz/src/FocusTarget.cpp b/gfx/layers/apz/src/FocusTarget.cpp new file mode 100644 index 0000000000..f1f6463e5c --- /dev/null +++ b/gfx/layers/apz/src/FocusTarget.cpp @@ -0,0 +1,233 @@ +/* -*- 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 "mozilla/layers/FocusTarget.h" +#include "mozilla/dom/BrowserBridgeChild.h" // for BrowserBridgeChild +#include "mozilla/dom/EventTarget.h" // for EventTarget +#include "mozilla/dom/RemoteBrowser.h" // For RemoteBrowser +#include "mozilla/EventDispatcher.h" // for EventDispatcher +#include "mozilla/PresShell.h" // For PresShell +#include "mozilla/StaticPrefs_apz.h" +#include "nsIContentInlines.h" // for nsINode::IsEditable() +#include "nsLayoutUtils.h" // for nsLayoutUtils + +static mozilla::LazyLogModule sApzFtgLog("apz.focustarget"); +#define FT_LOG(...) MOZ_LOG(sApzFtgLog, LogLevel::Debug, (__VA_ARGS__)) + +using namespace mozilla::dom; +using namespace mozilla::layout; + +namespace mozilla { +namespace layers { + +static PresShell* GetRetargetEventPresShell(PresShell* aRootPresShell) { + MOZ_ASSERT(aRootPresShell); + + // Use the last focused window in this PresShell and its + // associated PresShell + nsCOMPtr window = + aRootPresShell->GetFocusedDOMWindowInOurWindow(); + if (!window) { + return nullptr; + } + + RefPtr retargetEventDoc = window->GetExtantDoc(); + if (!retargetEventDoc) { + return nullptr; + } + + return retargetEventDoc->GetPresShell(); +} + +// _BOUNDARY because Dispatch() with `targets` must not handle the event. +MOZ_CAN_RUN_SCRIPT_BOUNDARY static bool HasListenersForKeyEvents( + nsIContent* aContent) { + if (!aContent) { + return false; + } + + WidgetEvent event(true, eVoidEvent); + nsTArray targets; + nsresult rv = EventDispatcher::Dispatch(aContent, nullptr, &event, nullptr, + nullptr, nullptr, &targets); + NS_ENSURE_SUCCESS(rv, false); + for (size_t i = 0; i < targets.Length(); i++) { + if (targets[i]->HasNonSystemGroupListenersForUntrustedKeyEvents()) { + return true; + } + } + return false; +} + +// _BOUNDARY because Dispatch() with `targets` must not handle the event. +MOZ_CAN_RUN_SCRIPT_BOUNDARY static bool HasListenersForNonPassiveKeyEvents( + nsIContent* aContent) { + if (!aContent) { + return false; + } + + WidgetEvent event(true, eVoidEvent); + nsTArray targets; + nsresult rv = EventDispatcher::Dispatch(aContent, nullptr, &event, nullptr, + nullptr, nullptr, &targets); + NS_ENSURE_SUCCESS(rv, false); + for (size_t i = 0; i < targets.Length(); i++) { + if (targets[i] + ->HasNonPassiveNonSystemGroupListenersForUntrustedKeyEvents()) { + return true; + } + } + return false; +} + +static bool IsEditableNode(nsINode* aNode) { + return aNode && aNode->IsEditable(); +} + +FocusTarget::FocusTarget() + : mSequenceNumber(0), + mFocusHasKeyEventListeners(false), + mData(AsVariant(NoFocusTarget())) {} + +FocusTarget::FocusTarget(PresShell* aRootPresShell, + uint64_t aFocusSequenceNumber) + : mSequenceNumber(aFocusSequenceNumber), + mFocusHasKeyEventListeners(false), + mData(AsVariant(NoFocusTarget())) { + MOZ_ASSERT(aRootPresShell); + MOZ_ASSERT(NS_IsMainThread()); + + // Key events can be retargeted to a child PresShell when there is an iframe + RefPtr presShell = GetRetargetEventPresShell(aRootPresShell); + + if (!presShell) { + FT_LOG("Creating nil target with seq=%" PRIu64 + " (can't find retargeted presshell)\n", + aFocusSequenceNumber); + + return; + } + + RefPtr document = presShell->GetDocument(); + if (!document) { + FT_LOG("Creating nil target with seq=%" PRIu64 " (no document)\n", + aFocusSequenceNumber); + + return; + } + + // Find the focused content and use it to determine whether there are key + // event listeners or whether key events will be targeted at a different + // process through a remote browser. + nsCOMPtr focusedContent = + presShell->GetFocusedContentInOurWindow(); + nsCOMPtr keyEventTarget = focusedContent; + + // If there is no focused element then event dispatch goes to the body of + // the page if it exists or the root element. + if (!keyEventTarget) { + keyEventTarget = document->GetUnfocusedKeyEventTarget(); + } + + // Check if there are key event listeners that could prevent default or change + // the focus or selection of the page. + if (StaticPrefs::apz_keyboard_passive_listeners()) { + mFocusHasKeyEventListeners = + HasListenersForNonPassiveKeyEvents(keyEventTarget.get()); + } else { + mFocusHasKeyEventListeners = HasListenersForKeyEvents(keyEventTarget.get()); + } + + // Check if the key event target is content editable or if the document + // is in design mode. + if (IsEditableNode(keyEventTarget) || IsEditableNode(document)) { + FT_LOG("Creating nil target with seq=%" PRIu64 + ", kl=%d (disabling for editable node)\n", + aFocusSequenceNumber, static_cast(mFocusHasKeyEventListeners)); + + return; + } + + // Check if the key event target is a remote browser + if (RemoteBrowser* remoteBrowser = RemoteBrowser::GetFrom(keyEventTarget)) { + LayersId layersId = remoteBrowser->GetLayersId(); + + // The globally focused element for scrolling is in a remote layer tree + if (layersId.IsValid()) { + FT_LOG("Creating reflayer target with seq=%" PRIu64 ", kl=%d, lt=%" PRIu64 + "\n", + aFocusSequenceNumber, mFocusHasKeyEventListeners, layersId.mId); + + mData = AsVariant(std::move(layersId)); + return; + } + + FT_LOG("Creating nil target with seq=%" PRIu64 + ", kl=%d (remote browser missing layers id)\n", + aFocusSequenceNumber, mFocusHasKeyEventListeners); + + return; + } + + // The content to scroll is either the focused element or the focus node of + // the selection. It's difficult to determine if an element is an interactive + // element requiring async keyboard scrolling to be disabled. So we only + // allow async key scrolling based on the selection, which doesn't have + // this problem and is more common. + if (focusedContent) { + FT_LOG("Creating nil target with seq=%" PRIu64 + ", kl=%d (disabling for focusing an element)\n", + aFocusSequenceNumber, mFocusHasKeyEventListeners); + + return; + } + + nsCOMPtr selectedContent = + presShell->GetSelectedContentForScrolling(); + + // Gather the scrollable frames that would be scrolled in each direction + // for this scroll target + nsIScrollableFrame* horizontal = + presShell->GetScrollableFrameToScrollForContent( + selectedContent.get(), HorizontalScrollDirection); + nsIScrollableFrame* vertical = + presShell->GetScrollableFrameToScrollForContent(selectedContent.get(), + VerticalScrollDirection); + + // We might have the globally focused element for scrolling. Gather a ViewID + // for the horizontal and vertical scroll targets of this element. + ScrollTargets target; + target.mHorizontal = nsLayoutUtils::FindIDForScrollableFrame(horizontal); + target.mVertical = nsLayoutUtils::FindIDForScrollableFrame(vertical); + mData = AsVariant(target); + + FT_LOG("Creating scroll target with seq=%" PRIu64 ", kl=%d, h=%" PRIu64 + ", v=%" PRIu64 "\n", + aFocusSequenceNumber, mFocusHasKeyEventListeners, target.mHorizontal, + target.mVertical); +} + +bool FocusTarget::operator==(const FocusTarget& aRhs) const { + return mSequenceNumber == aRhs.mSequenceNumber && + mFocusHasKeyEventListeners == aRhs.mFocusHasKeyEventListeners && + mData == aRhs.mData; +} + +const char* FocusTarget::Type() const { + if (mData.is()) { + return "LayersId"; + } + if (mData.is()) { + return "ScrollTargets"; + } + if (mData.is()) { + return "NoFocusTarget"; + } + return ""; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/FocusTarget.h b/gfx/layers/apz/src/FocusTarget.h new file mode 100644 index 0000000000..f4caa5d070 --- /dev/null +++ b/gfx/layers/apz/src/FocusTarget.h @@ -0,0 +1,71 @@ +/* -*- 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_FocusTarget_h +#define mozilla_layers_FocusTarget_h + +#include // for int32_t, uint32_t + +#include "mozilla/DefineEnum.h" // for MOZ_DEFINE_ENUM +#include "mozilla/layers/ScrollableLayerGuid.h" // for ViewID +#include "mozilla/Variant.h" // for Variant +#include "mozilla/Maybe.h" // for Maybe + +namespace mozilla { + +class PresShell; + +namespace layers { + +/** + * This class is used for communicating information about the currently focused + * element of a document and the scrollable frames to use when keyboard + * scrolling it. It is created on the main thread at paint-time, but is then + * passed over IPC to the compositor/APZ code. + */ +class FocusTarget final { + public: + struct ScrollTargets { + ScrollableLayerGuid::ViewID mHorizontal; + ScrollableLayerGuid::ViewID mVertical; + + bool operator==(const ScrollTargets& aRhs) const { + return (mHorizontal == aRhs.mHorizontal && mVertical == aRhs.mVertical); + } + }; + + // We need this to represent the case where mData has no focus target data + // because we can't have an empty variant + struct NoFocusTarget { + bool operator==(const NoFocusTarget& aRhs) const { return true; } + }; + + FocusTarget(); + + /** + * Construct a focus target for the specified top level PresShell + */ + FocusTarget(PresShell* aRootPresShell, uint64_t aFocusSequenceNumber); + + bool operator==(const FocusTarget& aRhs) const; + + const char* Type() const; + + public: + // The content sequence number recorded at the time of this class's creation + uint64_t mSequenceNumber; + + // Whether there are keydown, keypress, or keyup event listeners + // in the event target chain of the focused element + bool mFocusHasKeyEventListeners; + + mozilla::Variant mData; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_FocusTarget_h diff --git a/gfx/layers/apz/src/GenericFlingAnimation.h b/gfx/layers/apz/src/GenericFlingAnimation.h new file mode 100644 index 0000000000..82f981ae0a --- /dev/null +++ b/gfx/layers/apz/src/GenericFlingAnimation.h @@ -0,0 +1,207 @@ +/* -*- 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_GenericFlingAnimation_h_ +#define mozilla_layers_GenericFlingAnimation_h_ + +#include "APZUtils.h" +#include "AsyncPanZoomAnimation.h" +#include "AsyncPanZoomController.h" +#include "FrameMetrics.h" +#include "Units.h" +#include "OverscrollHandoffState.h" +#include "mozilla/Assertions.h" +#include "mozilla/Monitor.h" +#include "mozilla/RefPtr.h" +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/ToString.h" +#include "nsThreadUtils.h" + +static mozilla::LazyLogModule sApzFlgLog("apz.fling"); +#define FLING_LOG(...) MOZ_LOG(sApzFlgLog, LogLevel::Debug, (__VA_ARGS__)) + +namespace mozilla { +namespace layers { + +/** + * The FlingPhysics template parameter determines the physics model + * that the fling animation follows. It must have the following methods: + * + * - Default constructor. + * + * - Init(const ParentLayerPoint& aStartingVelocity, float aPLPPI). + * Called at the beginning of the fling, with the fling's starting velocity, + * and the number of ParentLayer pixels per (Screen) inch at the point of + * the fling's start in the fling's direction. + * + * - Sample(const TimeDuration& aDelta, + * ParentLayerPoint* aOutVelocity, + * ParentLayerPoint* aOutOffset); + * Called on each sample of the fling. + * |aDelta| is the time elapsed since the last sample. + * |aOutVelocity| should be the desired velocity after the current sample, + * in ParentLayer pixels per millisecond. + * |aOutOffset| should be the desired _delta_ to the scroll offset after + * the current sample. |aOutOffset| should _not_ be clamped to the APZC's + * scrollable bounds; the caller will do the clamping, and it needs to + * know the unclamped value to handle handoff/overscroll correctly. + */ +template +class GenericFlingAnimation : public AsyncPanZoomAnimation, + public FlingPhysics { + public: + GenericFlingAnimation(AsyncPanZoomController& aApzc, + const FlingHandoffState& aHandoffState, float aPLPPI) + : mApzc(aApzc), + mOverscrollHandoffChain(aHandoffState.mChain), + mScrolledApzc(aHandoffState.mScrolledApzc) { + MOZ_ASSERT(mOverscrollHandoffChain); + + // Drop any velocity on axes where we don't have room to scroll anyways + // (in this APZC, or an APZC further in the handoff chain). + // This ensures that we don't take the 'overscroll' path in Sample() + // on account of one axis which can't scroll having a velocity. + if (!mOverscrollHandoffChain->CanScrollInDirection( + &mApzc, ScrollDirection::eHorizontal)) { + RecursiveMutexAutoLock lock(mApzc.mRecursiveMutex); + mApzc.mX.SetVelocity(0); + } + if (!mOverscrollHandoffChain->CanScrollInDirection( + &mApzc, ScrollDirection::eVertical)) { + RecursiveMutexAutoLock lock(mApzc.mRecursiveMutex); + mApzc.mY.SetVelocity(0); + } + + if (aHandoffState.mIsHandoff) { + // Only apply acceleration in the APZC that originated the fling, not in + // APZCs further down the handoff chain during handoff. + mApzc.mFlingAccelerator.Reset(); + } + + ParentLayerPoint velocity = + mApzc.mFlingAccelerator.GetFlingStartingVelocity( + aApzc.GetFrameTime(), mApzc.GetVelocityVector(), aHandoffState); + + mApzc.SetVelocityVector(velocity); + + FlingPhysics::Init(mApzc.GetVelocityVector(), aPLPPI); + } + + /** + * Advances a fling by an interpolated amount based on the passed in |aDelta|. + * This should be called whenever sampling the content transform for this + * frame. Returns true if the fling animation should be advanced by one frame, + * or false if there is no fling or the fling has ended. + */ + virtual bool DoSample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta) override { + CSSToParentLayerScale zoom(aFrameMetrics.GetZoom()); + if (zoom == CSSToParentLayerScale(0)) { + return false; + } + + ParentLayerPoint velocity; + ParentLayerPoint offset; + FlingPhysics::Sample(aDelta, &velocity, &offset); + + mApzc.SetVelocityVector(velocity); + + // If we shouldn't continue the fling, let's just stop and repaint. + if (IsZero(velocity / zoom)) { + FLING_LOG("%p ending fling animation. overscrolled=%d\n", &mApzc, + mApzc.IsOverscrolled()); + // This APZC or an APZC further down the handoff chain may be be + // overscrolled. Start a snap-back animation on the overscrolled APZC. + // Note: + // This needs to be a deferred task even though it can safely run + // while holding mRecursiveMutex, because otherwise, if the overscrolled + // APZC is this one, then the SetState(NOTHING) in UpdateAnimation will + // stomp on the SetState(SNAP_BACK) it does. + mDeferredTasks.AppendElement(NewRunnableMethod( + "layers::OverscrollHandoffChain::SnapBackOverscrolledApzc", + mOverscrollHandoffChain.get(), + &OverscrollHandoffChain::SnapBackOverscrolledApzc, &mApzc)); + return false; + } + + // Ordinarily we might need to do a ScheduleComposite if either of + // the following AdjustDisplacement calls returns true, but this + // is already running as part of a FlingAnimation, so we'll be compositing + // per frame of animation anyway. + ParentLayerPoint overscroll; + ParentLayerPoint adjustedOffset; + mApzc.mX.AdjustDisplacement(offset.x, adjustedOffset.x, overscroll.x); + mApzc.mY.AdjustDisplacement(offset.y, adjustedOffset.y, overscroll.y); + if (aFrameMetrics.GetZoom() != CSSToParentLayerScale(0)) { + mApzc.ScrollBy(adjustedOffset / aFrameMetrics.GetZoom()); + } + + // The fling may have caused us to reach the end of our scroll range. + if (!IsZero(overscroll / zoom)) { + // Hand off the fling to the next APZC in the overscroll handoff chain. + + // We may have reached the end of the scroll range along one axis but + // not the other. In such a case we only want to hand off the relevant + // component of the fling. + if (mApzc.IsZero(overscroll.x)) { + velocity.x = 0; + } else if (mApzc.IsZero(overscroll.y)) { + velocity.y = 0; + } + + // To hand off the fling, we attempt to find a target APZC and start a new + // fling with the same velocity on that APZC. For simplicity, the actual + // overscroll of the current sample is discarded rather than being handed + // off. The compositor should sample animations sufficiently frequently + // that this is not noticeable. The target APZC is chosen by seeing if + // there is an APZC further in the handoff chain which is pannable; if + // there isn't, we take the new fling ourselves, entering an overscrolled + // state. + // Note: APZC is holding mRecursiveMutex, so directly calling + // HandleFlingOverscroll() (which acquires the tree lock) would violate + // the lock ordering. Instead we schedule HandleFlingOverscroll() to be + // called after mRecursiveMutex is released. + FLING_LOG("%p fling went into overscroll, handing off with velocity %s\n", + &mApzc, ToString(velocity).c_str()); + mDeferredTasks.AppendElement( + NewRunnableMethod, + RefPtr>( + "layers::AsyncPanZoomController::HandleFlingOverscroll", &mApzc, + &AsyncPanZoomController::HandleFlingOverscroll, velocity, + apz::GetOverscrollSideBits(overscroll), mOverscrollHandoffChain, + mScrolledApzc)); + + // If there is a remaining velocity on this APZC, continue this fling + // as well. (This fling and the handed-off fling will run concurrently.) + // Note that AdjustDisplacement() will have zeroed out the velocity + // along the axes where we're overscrolled. + return !IsZero(mApzc.GetVelocityVector() / zoom); + } + + return true; + } + + void Cancel(CancelAnimationFlags aFlags) override { + mApzc.mFlingAccelerator.ObserveFlingCanceled(mApzc.GetVelocityVector()); + } + + virtual bool HandleScrollOffsetUpdate( + const Maybe& aRelativeDelta) override { + return true; + } + + private: + AsyncPanZoomController& mApzc; + RefPtr mOverscrollHandoffChain; + RefPtr mScrolledApzc; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_GenericFlingAnimation_h_ diff --git a/gfx/layers/apz/src/GenericScrollAnimation.cpp b/gfx/layers/apz/src/GenericScrollAnimation.cpp new file mode 100644 index 0000000000..9320482295 --- /dev/null +++ b/gfx/layers/apz/src/GenericScrollAnimation.cpp @@ -0,0 +1,120 @@ +/* -*- 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 "GenericScrollAnimation.h" + +#include "AsyncPanZoomController.h" +#include "FrameMetrics.h" +#include "nsPoint.h" +#include "ScrollAnimationPhysics.h" +#include "ScrollAnimationBezierPhysics.h" +#include "ScrollAnimationMSDPhysics.h" +#include "mozilla/StaticPrefs_general.h" + +namespace mozilla { +namespace layers { + +GenericScrollAnimation::GenericScrollAnimation( + AsyncPanZoomController& aApzc, const nsPoint& aInitialPosition, + const ScrollAnimationBezierPhysicsSettings& aSettings) + : mApzc(aApzc), mFinalDestination(aInitialPosition) { + // ScrollAnimationBezierPhysics (despite it's name) handles the case of + // general.smoothScroll being disabled whereas ScrollAnimationMSDPhysics does + // not (ie it scrolls smoothly). + if (StaticPrefs::general_smoothScroll() && + StaticPrefs::general_smoothScroll_msdPhysics_enabled()) { + mAnimationPhysics = MakeUnique(aInitialPosition); + } else { + mAnimationPhysics = + MakeUnique(aInitialPosition, aSettings); + } +} + +void GenericScrollAnimation::UpdateDelta(TimeStamp aTime, const nsPoint& aDelta, + const nsSize& aCurrentVelocity) { + mFinalDestination += aDelta; + + Update(aTime, aCurrentVelocity); +} + +void GenericScrollAnimation::UpdateDestination(TimeStamp aTime, + const nsPoint& aDestination, + const nsSize& aCurrentVelocity) { + mFinalDestination = aDestination; + + Update(aTime, aCurrentVelocity); +} + +void GenericScrollAnimation::Update(TimeStamp aTime, + const nsSize& aCurrentVelocity) { + // Clamp the final destination to the scrollable area. + CSSPoint clamped = CSSPoint::FromAppUnits(mFinalDestination); + clamped.x = mApzc.mX.ClampOriginToScrollableRect(clamped.x); + clamped.y = mApzc.mY.ClampOriginToScrollableRect(clamped.y); + mFinalDestination = CSSPoint::ToAppUnits(clamped); + + mAnimationPhysics->Update(aTime, mFinalDestination, aCurrentVelocity); +} + +bool GenericScrollAnimation::DoSample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta) { + TimeStamp now = mApzc.GetFrameTime().Time(); + CSSToParentLayerScale zoom(aFrameMetrics.GetZoom()); + if (zoom == CSSToParentLayerScale(0)) { + return false; + } + + // If the animation is finished, make sure the final position is correct by + // using one last displacement. Otherwise, compute the delta via the timing + // function as normal. + bool finished = mAnimationPhysics->IsFinished(now); + nsPoint sampledDest = mAnimationPhysics->PositionAt(now); + ParentLayerPoint displacement = (CSSPoint::FromAppUnits(sampledDest) - + aFrameMetrics.GetVisualScrollOffset()) * + zoom; + + if (finished) { + mApzc.mX.SetVelocity(0); + mApzc.mY.SetVelocity(0); + } else if (!IsZero(displacement / zoom)) { + // Convert velocity from AppUnits/Seconds to ParentLayerCoords/Milliseconds + nsSize velocity = mAnimationPhysics->VelocityAt(now); + ParentLayerPoint velocityPL = + CSSPoint::FromAppUnits(nsPoint(velocity.width, velocity.height)) * zoom; + mApzc.mX.SetVelocity(velocityPL.x / 1000.0); + mApzc.mY.SetVelocity(velocityPL.y / 1000.0); + } + // Note: we ignore overscroll for generic animations. + ParentLayerPoint adjustedOffset, overscroll; + mApzc.mX.AdjustDisplacement( + displacement.x, adjustedOffset.x, overscroll.x, + mDirectionForcedToOverscroll == Some(ScrollDirection::eHorizontal)); + mApzc.mY.AdjustDisplacement( + displacement.y, adjustedOffset.y, overscroll.y, + mDirectionForcedToOverscroll == Some(ScrollDirection::eVertical)); + // If we expected to scroll, but there's no more scroll range on either axis, + // then end the animation early. Note that the initial displacement could be 0 + // if the compositor ran very quickly (<1ms) after the animation was created. + // When that happens we want to make sure the animation continues. + if (!IsZero(displacement / zoom) && IsZero(adjustedOffset / zoom)) { + // Nothing more to do - end the animation. + return false; + } + mApzc.ScrollBy(adjustedOffset / zoom); + return !finished; +} + +bool GenericScrollAnimation::HandleScrollOffsetUpdate( + const Maybe& aRelativeDelta) { + if (aRelativeDelta) { + mAnimationPhysics->ApplyContentShift(*aRelativeDelta); + return true; + } + return false; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/GenericScrollAnimation.h b/gfx/layers/apz/src/GenericScrollAnimation.h new file mode 100644 index 0000000000..56a64dc5ec --- /dev/null +++ b/gfx/layers/apz/src/GenericScrollAnimation.h @@ -0,0 +1,59 @@ +/* -*- 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_GenericScrollAnimation_h_ +#define mozilla_layers_GenericScrollAnimation_h_ + +#include "AsyncPanZoomAnimation.h" + +namespace mozilla { + +struct ScrollAnimationBezierPhysicsSettings; +class ScrollAnimationPhysics; + +namespace layers { + +class AsyncPanZoomController; + +class GenericScrollAnimation : public AsyncPanZoomAnimation { + public: + GenericScrollAnimation(AsyncPanZoomController& aApzc, + const nsPoint& aInitialPosition, + const ScrollAnimationBezierPhysicsSettings& aSettings); + + bool DoSample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta) override; + + bool HandleScrollOffsetUpdate(const Maybe& aRelativeDelta) override; + + void UpdateDelta(TimeStamp aTime, const nsPoint& aDelta, + const nsSize& aCurrentVelocity); + void UpdateDestination(TimeStamp aTime, const nsPoint& aDestination, + const nsSize& aCurrentVelocity); + + CSSPoint GetDestination() const { + return CSSPoint::FromAppUnits(mFinalDestination); + } + + private: + void Update(TimeStamp aTime, const nsSize& aCurrentVelocity); + + protected: + AsyncPanZoomController& mApzc; + UniquePtr mAnimationPhysics; + nsPoint mFinalDestination; + // If a direction is forced to overscroll, it means it's axis in that + // direction is locked, and scroll in that direction is treated as overscroll + // of an equal amount, which, for example, may then bubble up a scroll action + // to its parent, or may behave as whatever an overscroll occurence requires + // to behave + Maybe mDirectionForcedToOverscroll; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_GenericScrollAnimation_h_ diff --git a/gfx/layers/apz/src/GestureEventListener.cpp b/gfx/layers/apz/src/GestureEventListener.cpp new file mode 100644 index 0000000000..b54674b593 --- /dev/null +++ b/gfx/layers/apz/src/GestureEventListener.cpp @@ -0,0 +1,663 @@ +/* -*- 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 "GestureEventListener.h" +#include // for max +#include // for fabsf +#include // for size_t +#include "AsyncPanZoomController.h" // for AsyncPanZoomController +#include "InputBlockState.h" // for TouchBlockState +#include "base/task.h" // for CancelableTask, etc +#include "InputBlockState.h" // for TouchBlockState +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/StaticPrefs_ui.h" +#include "nsDebug.h" // for NS_WARNING +#include "nsMathUtils.h" // for NS_hypot + +static mozilla::LazyLogModule sApzGelLog("apz.gesture"); +#define GEL_LOG(...) MOZ_LOG(sApzGelLog, LogLevel::Debug, (__VA_ARGS__)) + +namespace mozilla { +namespace layers { + +/** + * Amount of span or focus change needed to take us from the + * GESTURE_WAITING_PINCH state to the GESTURE_PINCH state. This is measured as + * either a change in distance between the fingers used to compute the span + * ratio, or the a change in position of the focus point between the two + * fingers. + */ +static const float PINCH_START_THRESHOLD = 35.0f; + +/** + * Determines how fast a one touch pinch zooms in and out. The greater the + * value, the faster it zooms. + */ +static const float ONE_TOUCH_PINCH_SPEED = 0.005f; + +static bool sLongTapEnabled = true; + +static ScreenPoint GetCurrentFocus(const MultiTouchInput& aEvent) { + const ScreenPoint& firstTouch = aEvent.mTouches[0].mScreenPoint; + const ScreenPoint& secondTouch = aEvent.mTouches[1].mScreenPoint; + return (firstTouch + secondTouch) / 2; +} + +static ScreenCoord GetCurrentSpan(const MultiTouchInput& aEvent) { + const ScreenPoint& firstTouch = aEvent.mTouches[0].mScreenPoint; + const ScreenPoint& secondTouch = aEvent.mTouches[1].mScreenPoint; + ScreenPoint delta = secondTouch - firstTouch; + return delta.Length(); +} + +ScreenCoord GestureEventListener::GetYSpanFromGestureStartPoint() { + // use the position that began the one-touch-pinch gesture rather + // mTouchStartPosition + const ScreenPoint start = mOneTouchPinchStartPosition; + const ScreenPoint& current = mTouches[0].mScreenPoint; + return current.y - start.y; +} + +static TapGestureInput CreateTapEvent(const MultiTouchInput& aTouch, + TapGestureInput::TapGestureType aType) { + return TapGestureInput(aType, aTouch.mTimeStamp, + aTouch.mTouches[0].mScreenPoint, aTouch.modifiers); +} + +GestureEventListener::GestureEventListener( + AsyncPanZoomController* aAsyncPanZoomController) + : mAsyncPanZoomController(aAsyncPanZoomController), + mState(GESTURE_NONE), + mSpanChange(0.0f), + mPreviousSpan(0.0f), + mFocusChange(0.0f), + mLastTouchInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0), + mLastTapInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0), + mLongTapTimeoutTask(nullptr), + mMaxTapTimeoutTask(nullptr) {} + +GestureEventListener::~GestureEventListener() = default; + +nsEventStatus GestureEventListener::HandleInputEvent( + const MultiTouchInput& aEvent) { + GEL_LOG("Receiving event type %d with %zu touches in state %d\n", + aEvent.mType, aEvent.mTouches.Length(), mState); + + nsEventStatus rv = nsEventStatus_eIgnore; + + // Cache the current event since it may become the single or long tap that we + // send. + mLastTouchInput = aEvent; + + switch (aEvent.mType) { + case MultiTouchInput::MULTITOUCH_START: + mTouches.Clear(); + // Cache every touch. + for (size_t i = 0; i < aEvent.mTouches.Length(); i++) { + mTouches.AppendElement(aEvent.mTouches[i]); + } + + if (aEvent.mTouches.Length() == 1) { + rv = HandleInputTouchSingleStart(); + } else { + rv = HandleInputTouchMultiStart(); + } + break; + case MultiTouchInput::MULTITOUCH_MOVE: + // Update the screen points of the cached touches. + for (size_t i = 0; i < aEvent.mTouches.Length(); i++) { + for (size_t j = 0; j < mTouches.Length(); j++) { + if (aEvent.mTouches[i].mIdentifier == mTouches[j].mIdentifier) { + mTouches[j].mScreenPoint = aEvent.mTouches[i].mScreenPoint; + mTouches[j].mLocalScreenPoint = + aEvent.mTouches[i].mLocalScreenPoint; + } + } + } + rv = HandleInputTouchMove(); + break; + case MultiTouchInput::MULTITOUCH_END: + // Remove the cache of the touch that ended. + for (size_t i = 0; i < aEvent.mTouches.Length(); i++) { + for (size_t j = 0; j < mTouches.Length(); j++) { + if (aEvent.mTouches[i].mIdentifier == mTouches[j].mIdentifier) { + mTouches.RemoveElementAt(j); + break; + } + } + } + + rv = HandleInputTouchEnd(); + break; + case MultiTouchInput::MULTITOUCH_CANCEL: + mTouches.Clear(); + rv = HandleInputTouchCancel(); + break; + } + + return rv; +} + +int32_t GestureEventListener::GetLastTouchIdentifier() const { + if (mTouches.Length() != 1) { + NS_WARNING( + "GetLastTouchIdentifier() called when last touch event " + "did not have one touch"); + } + return mTouches.IsEmpty() ? -1 : mTouches[0].mIdentifier; +} + +/* static */ +void GestureEventListener::SetLongTapEnabled(bool aLongTapEnabled) { + sLongTapEnabled = aLongTapEnabled; +} + +/* static */ +bool GestureEventListener::IsLongTapEnabled() { return sLongTapEnabled; } + +void GestureEventListener::EnterFirstSingleTouchDown() { + SetState(GESTURE_FIRST_SINGLE_TOUCH_DOWN); + mTouchStartPosition = mLastTouchInput.mTouches[0].mScreenPoint; + mTouchStartOffset = mLastTouchInput.mScreenOffset; + + if (sLongTapEnabled) { + CreateLongTapTimeoutTask(); + } + CreateMaxTapTimeoutTask(); +} + +nsEventStatus GestureEventListener::HandleInputTouchSingleStart() { + switch (mState) { + case GESTURE_NONE: + EnterFirstSingleTouchDown(); + break; + case GESTURE_FIRST_SINGLE_TOUCH_UP: + if (SecondTapIsFar()) { + // If the second tap goes down far away from the first, then bail out + // of any gesture that includes the first tap. + CancelLongTapTimeoutTask(); + CancelMaxTapTimeoutTask(); + mSingleTapSent = Nothing(); + + // But still allow the second tap to participate in a gesture + // (e.g. lead to a single tap, or a double tap if an additional + // tap occurs near the same location). + EnterFirstSingleTouchDown(); + } else { + // Otherwise, reset the touch start position so that, if this turns into + // a one-touch-pinch gesture, it uses the second tap's down position as + // the focus, rather than the first tap's. + mTouchStartPosition = mLastTouchInput.mTouches[0].mScreenPoint; + mTouchStartOffset = mLastTouchInput.mScreenOffset; + SetState(GESTURE_SECOND_SINGLE_TOUCH_DOWN); + } + break; + default: + NS_WARNING("Unhandled state upon single touch start"); + SetState(GESTURE_NONE); + break; + } + + return nsEventStatus_eIgnore; +} + +nsEventStatus GestureEventListener::HandleInputTouchMultiStart() { + nsEventStatus rv = nsEventStatus_eIgnore; + + switch (mState) { + case GESTURE_NONE: + SetState(GESTURE_MULTI_TOUCH_DOWN); + break; + case GESTURE_FIRST_SINGLE_TOUCH_DOWN: + CancelLongTapTimeoutTask(); + CancelMaxTapTimeoutTask(); + SetState(GESTURE_MULTI_TOUCH_DOWN); + // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event + rv = nsEventStatus_eConsumeNoDefault; + break; + case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: + CancelLongTapTimeoutTask(); + SetState(GESTURE_MULTI_TOUCH_DOWN); + // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event + rv = nsEventStatus_eConsumeNoDefault; + break; + case GESTURE_FIRST_SINGLE_TOUCH_UP: + case GESTURE_SECOND_SINGLE_TOUCH_DOWN: + // Cancel wait for double tap + CancelMaxTapTimeoutTask(); + MOZ_ASSERT(mSingleTapSent.isSome()); + if (!mSingleTapSent.value()) { + TriggerSingleTapConfirmedEvent(); + } + mSingleTapSent = Nothing(); + SetState(GESTURE_MULTI_TOUCH_DOWN); + // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event + rv = nsEventStatus_eConsumeNoDefault; + break; + case GESTURE_LONG_TOUCH_DOWN: + SetState(GESTURE_MULTI_TOUCH_DOWN); + break; + case GESTURE_MULTI_TOUCH_DOWN: + case GESTURE_PINCH: + // Prevent APZC::OnTouchStart() from handling MULTITOUCH_START event + rv = nsEventStatus_eConsumeNoDefault; + break; + default: + NS_WARNING("Unhandled state upon multitouch start"); + SetState(GESTURE_NONE); + break; + } + + return rv; +} + +bool GestureEventListener::MoveDistanceExceeds(ScreenCoord aThreshold) const { + ExternalPoint start = AsyncPanZoomController::ToExternalPoint( + mTouchStartOffset, mTouchStartPosition); + ExternalPoint end = AsyncPanZoomController::ToExternalPoint( + mLastTouchInput.mScreenOffset, mLastTouchInput.mTouches[0].mScreenPoint); + return (start - end).Length() > aThreshold; +} + +bool GestureEventListener::MoveDistanceIsLarge() const { + return MoveDistanceExceeds(mAsyncPanZoomController->GetTouchStartTolerance()); +} + +bool GestureEventListener::SecondTapIsFar() const { + // Allow a little more room here, because the is actually lifting their finger + // off the screen before replacing it, and that tends to have more error than + // wiggling the finger while on the screen. + return MoveDistanceExceeds(mAsyncPanZoomController->GetSecondTapTolerance()); +} + +nsEventStatus GestureEventListener::HandleInputTouchMove() { + nsEventStatus rv = nsEventStatus_eIgnore; + + switch (mState) { + case GESTURE_NONE: + // Ignore this input signal as the corresponding events get handled by + // APZC + break; + + case GESTURE_LONG_TOUCH_DOWN: + if (MoveDistanceIsLarge()) { + // So that we don't fire a long-tap-up if the user moves around after a + // long-tap + SetState(GESTURE_NONE); + } + break; + + case GESTURE_FIRST_SINGLE_TOUCH_DOWN: + case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: { + // If we move too much, bail out of the tap. + if (MoveDistanceIsLarge()) { + CancelLongTapTimeoutTask(); + CancelMaxTapTimeoutTask(); + mSingleTapSent = Nothing(); + SetState(GESTURE_NONE); + } + break; + } + + // The user has performed a double tap, but not lifted her finger. + case GESTURE_SECOND_SINGLE_TOUCH_DOWN: { + // If touch has moved noticeably (within StaticPrefs::apz_max_tap_time()), + // change state. + if (MoveDistanceIsLarge()) { + CancelLongTapTimeoutTask(); + CancelMaxTapTimeoutTask(); + mSingleTapSent = Nothing(); + if (!StaticPrefs::apz_one_touch_pinch_enabled()) { + // If the one-touch-pinch feature is disabled, bail out of the double- + // tap gesture instead. + SetState(GESTURE_NONE); + break; + } + + SetState(GESTURE_ONE_TOUCH_PINCH); + + ScreenCoord currentSpan = 1.0f; + ScreenPoint currentFocus = mTouchStartPosition; + + // save the position that the one-touch-pinch gesture actually begins + mOneTouchPinchStartPosition = mLastTouchInput.mTouches[0].mScreenPoint; + + PinchGestureInput pinchEvent( + PinchGestureInput::PINCHGESTURE_START, PinchGestureInput::ONE_TOUCH, + mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset, + currentFocus, currentSpan, currentSpan, mLastTouchInput.modifiers); + + rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent); + + mPreviousSpan = currentSpan; + mPreviousFocus = currentFocus; + } + break; + } + + case GESTURE_MULTI_TOUCH_DOWN: { + if (mLastTouchInput.mTouches.Length() < 2) { + NS_WARNING( + "Wrong input: less than 2 moving points in " + "GESTURE_MULTI_TOUCH_DOWN state"); + break; + } + + ScreenCoord currentSpan = GetCurrentSpan(mLastTouchInput); + ScreenPoint currentFocus = GetCurrentFocus(mLastTouchInput); + + mSpanChange += fabsf(currentSpan - mPreviousSpan); + mFocusChange += (currentFocus - mPreviousFocus).Length(); + if (mSpanChange > PINCH_START_THRESHOLD || + mFocusChange > PINCH_START_THRESHOLD) { + SetState(GESTURE_PINCH); + PinchGestureInput pinchEvent( + PinchGestureInput::PINCHGESTURE_START, PinchGestureInput::TOUCH, + mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset, + currentFocus, currentSpan, currentSpan, mLastTouchInput.modifiers); + + rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent); + } else { + // Prevent APZC::OnTouchMove from processing a move event when two + // touches are active + rv = nsEventStatus_eConsumeNoDefault; + } + + mPreviousSpan = currentSpan; + mPreviousFocus = currentFocus; + break; + } + + case GESTURE_PINCH: { + if (mLastTouchInput.mTouches.Length() < 2) { + NS_WARNING( + "Wrong input: less than 2 moving points in GESTURE_PINCH state"); + // Prevent APZC::OnTouchMove() from handling this wrong input + rv = nsEventStatus_eConsumeNoDefault; + break; + } + + ScreenCoord currentSpan = GetCurrentSpan(mLastTouchInput); + + PinchGestureInput pinchEvent( + PinchGestureInput::PINCHGESTURE_SCALE, PinchGestureInput::TOUCH, + mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset, + GetCurrentFocus(mLastTouchInput), currentSpan, mPreviousSpan, + mLastTouchInput.modifiers); + + rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent); + mPreviousSpan = currentSpan; + + break; + } + + case GESTURE_ONE_TOUCH_PINCH: { + ScreenCoord currentSpan = GetYSpanFromGestureStartPoint(); + float effectiveSpan = + 1.0f + (fabsf(currentSpan.value) * ONE_TOUCH_PINCH_SPEED); + ScreenPoint currentFocus = mTouchStartPosition; + + // Invert zoom. + if (currentSpan.value < 0) { + effectiveSpan = 1.0f / effectiveSpan; + } + + PinchGestureInput pinchEvent( + PinchGestureInput::PINCHGESTURE_SCALE, PinchGestureInput::ONE_TOUCH, + mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset, + currentFocus, effectiveSpan, mPreviousSpan, + mLastTouchInput.modifiers); + + rv = mAsyncPanZoomController->HandleGestureEvent(pinchEvent); + mPreviousSpan = effectiveSpan; + + break; + } + + default: + NS_WARNING("Unhandled state upon touch move"); + SetState(GESTURE_NONE); + break; + } + + return rv; +} + +nsEventStatus GestureEventListener::HandleInputTouchEnd() { + // We intentionally do not pass apzc return statuses up since + // it may cause apzc stay in the touching state even after + // gestures are completed (please see Bug 1013378 for reference). + + nsEventStatus rv = nsEventStatus_eIgnore; + + switch (mState) { + case GESTURE_NONE: + // GEL doesn't have a dedicated state for PANNING handled in APZC thus + // ignore. + break; + + case GESTURE_FIRST_SINGLE_TOUCH_DOWN: { + CancelLongTapTimeoutTask(); + CancelMaxTapTimeoutTask(); + nsEventStatus tapupStatus = mAsyncPanZoomController->HandleGestureEvent( + CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_UP)); + mSingleTapSent = Some(tapupStatus != nsEventStatus_eIgnore); + SetState(GESTURE_FIRST_SINGLE_TOUCH_UP); + CreateMaxTapTimeoutTask(); + break; + } + + case GESTURE_SECOND_SINGLE_TOUCH_DOWN: { + CancelMaxTapTimeoutTask(); + MOZ_ASSERT(mSingleTapSent.isSome()); + mAsyncPanZoomController->HandleGestureEvent(CreateTapEvent( + mLastTouchInput, mSingleTapSent.value() + ? TapGestureInput::TAPGESTURE_SECOND + : TapGestureInput::TAPGESTURE_DOUBLE)); + mSingleTapSent = Nothing(); + SetState(GESTURE_NONE); + break; + } + + case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: + CancelLongTapTimeoutTask(); + SetState(GESTURE_NONE); + TriggerSingleTapConfirmedEvent(); + break; + + case GESTURE_LONG_TOUCH_DOWN: { + SetState(GESTURE_NONE); + mAsyncPanZoomController->HandleGestureEvent( + CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_LONG_UP)); + break; + } + + case GESTURE_MULTI_TOUCH_DOWN: + if (mTouches.Length() < 2) { + SetState(GESTURE_NONE); + } + break; + + case GESTURE_PINCH: + if (mTouches.Length() < 2) { + SetState(GESTURE_NONE); + PinchGestureInput::PinchGestureType type = + PinchGestureInput::PINCHGESTURE_END; + ScreenPoint point; + if (mTouches.Length() == 1) { + // As user still keeps one finger down the event's focus point should + // contain meaningful data. + type = PinchGestureInput::PINCHGESTURE_FINGERLIFTED; + point = mTouches[0].mScreenPoint; + } + PinchGestureInput pinchEvent(type, PinchGestureInput::TOUCH, + mLastTouchInput.mTimeStamp, + mLastTouchInput.mScreenOffset, point, 1.0f, + 1.0f, mLastTouchInput.modifiers); + mAsyncPanZoomController->HandleGestureEvent(pinchEvent); + } + + rv = nsEventStatus_eConsumeNoDefault; + + break; + + case GESTURE_ONE_TOUCH_PINCH: { + SetState(GESTURE_NONE); + PinchGestureInput pinchEvent( + PinchGestureInput::PINCHGESTURE_END, PinchGestureInput::ONE_TOUCH, + mLastTouchInput.mTimeStamp, mLastTouchInput.mScreenOffset, + ScreenPoint(), 1.0f, 1.0f, mLastTouchInput.modifiers); + mAsyncPanZoomController->HandleGestureEvent(pinchEvent); + + rv = nsEventStatus_eConsumeNoDefault; + + break; + } + + default: + NS_WARNING("Unhandled state upon touch end"); + SetState(GESTURE_NONE); + break; + } + + return rv; +} + +nsEventStatus GestureEventListener::HandleInputTouchCancel() { + mSingleTapSent = Nothing(); + SetState(GESTURE_NONE); + CancelMaxTapTimeoutTask(); + CancelLongTapTimeoutTask(); + return nsEventStatus_eIgnore; +} + +void GestureEventListener::HandleInputTimeoutLongTap() { + GEL_LOG("Running long-tap timeout task in state %d\n", mState); + + mLongTapTimeoutTask = nullptr; + + switch (mState) { + case GESTURE_FIRST_SINGLE_TOUCH_DOWN: + // just in case MaxTapTime > ContextMenuDelay cancel MaxTap timer + // and fall through + CancelMaxTapTimeoutTask(); + [[fallthrough]]; + case GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN: { + SetState(GESTURE_LONG_TOUCH_DOWN); + mAsyncPanZoomController->HandleGestureEvent( + CreateTapEvent(mLastTouchInput, TapGestureInput::TAPGESTURE_LONG)); + break; + } + default: + NS_WARNING("Unhandled state upon long tap timeout"); + SetState(GESTURE_NONE); + break; + } +} + +void GestureEventListener::HandleInputTimeoutMaxTap(bool aDuringFastFling) { + GEL_LOG("Running max-tap timeout task in state %d\n", mState); + + mMaxTapTimeoutTask = nullptr; + + if (mState == GESTURE_FIRST_SINGLE_TOUCH_DOWN) { + SetState(GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN); + } else if (mState == GESTURE_FIRST_SINGLE_TOUCH_UP || + mState == GESTURE_SECOND_SINGLE_TOUCH_DOWN) { + MOZ_ASSERT(mSingleTapSent.isSome()); + if (!aDuringFastFling && !mSingleTapSent.value()) { + TriggerSingleTapConfirmedEvent(); + } + mSingleTapSent = Nothing(); + SetState(GESTURE_NONE); + } else { + NS_WARNING("Unhandled state upon MAX_TAP timeout"); + SetState(GESTURE_NONE); + } +} + +void GestureEventListener::TriggerSingleTapConfirmedEvent() { + mAsyncPanZoomController->HandleGestureEvent( + CreateTapEvent(mLastTapInput, TapGestureInput::TAPGESTURE_CONFIRMED)); +} + +void GestureEventListener::SetState(GestureState aState) { + mState = aState; + + if (mState == GESTURE_NONE) { + mSpanChange = 0.0f; + mPreviousSpan = 0.0f; + mFocusChange = 0.0f; + } else if (mState == GESTURE_MULTI_TOUCH_DOWN) { + mPreviousSpan = GetCurrentSpan(mLastTouchInput); + mPreviousFocus = GetCurrentFocus(mLastTouchInput); + } +} + +void GestureEventListener::CancelLongTapTimeoutTask() { + if (mState == GESTURE_SECOND_SINGLE_TOUCH_DOWN) { + // being in this state means the task has been canceled already + return; + } + + if (mLongTapTimeoutTask) { + mLongTapTimeoutTask->Cancel(); + mLongTapTimeoutTask = nullptr; + } +} + +void GestureEventListener::CreateLongTapTimeoutTask() { + RefPtr task = NewCancelableRunnableMethod( + "layers::GestureEventListener::HandleInputTimeoutLongTap", this, + &GestureEventListener::HandleInputTimeoutLongTap); + + mLongTapTimeoutTask = task; + + TouchBlockState* block = + mAsyncPanZoomController->GetInputQueue()->GetCurrentTouchBlock(); + MOZ_ASSERT(block); + long alreadyElapsed = + static_cast(block->GetTimeSinceBlockStart().ToMilliseconds()); + long remainingDelay = + StaticPrefs::ui_click_hold_context_menus_delay() - alreadyElapsed; + mAsyncPanZoomController->PostDelayedTask(task.forget(), + std::max(0L, remainingDelay)); +} + +void GestureEventListener::CancelMaxTapTimeoutTask() { + if (mState == GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN) { + // being in this state means the timer has just been triggered + return; + } + + if (mMaxTapTimeoutTask) { + mMaxTapTimeoutTask->Cancel(); + mMaxTapTimeoutTask = nullptr; + } +} + +void GestureEventListener::CreateMaxTapTimeoutTask() { + mLastTapInput = mLastTouchInput; + + TouchBlockState* block = + mAsyncPanZoomController->GetInputQueue()->GetCurrentTouchBlock(); + MOZ_ASSERT(block); + RefPtr task = NewCancelableRunnableMethod( + "layers::GestureEventListener::HandleInputTimeoutMaxTap", this, + &GestureEventListener::HandleInputTimeoutMaxTap, + block->IsDuringFastFling()); + + mMaxTapTimeoutTask = task; + + long alreadyElapsed = + static_cast(block->GetTimeSinceBlockStart().ToMilliseconds()); + long remainingDelay = StaticPrefs::apz_max_tap_time() - alreadyElapsed; + mAsyncPanZoomController->PostDelayedTask(task.forget(), + std::max(0L, remainingDelay)); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/GestureEventListener.h b/gfx/layers/apz/src/GestureEventListener.h new file mode 100644 index 0000000000..aa51889fdd --- /dev/null +++ b/gfx/layers/apz/src/GestureEventListener.h @@ -0,0 +1,285 @@ +/* -*- 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_GestureEventListener_h +#define mozilla_layers_GestureEventListener_h + +#include "InputData.h" // for MultiTouchInput, etc +#include "Units.h" +#include "mozilla/EventForwards.h" // for nsEventStatus +#include "mozilla/RefPtr.h" // for RefPtr +#include "nsISupportsImpl.h" +#include "nsTArray.h" // for nsTArray + +namespace mozilla { + +class CancelableRunnable; + +namespace layers { + +class AsyncPanZoomController; + +/** + * Platform-non-specific, generalized gesture event listener. This class + * intercepts all touches events on their way to AsyncPanZoomController and + * determines whether or not they are part of a gesture. + * + * For example, seeing that two fingers are on the screen means that the user + * wants to do a pinch gesture, so we don't forward the touches along to + * AsyncPanZoomController since it will think that they are just trying to pan + * the screen. Instead, we generate a PinchGestureInput and send that. If the + * touch event is not part of a gesture, we just return nsEventStatus_eIgnore + * and AsyncPanZoomController is expected to handle it. + */ +class GestureEventListener final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GestureEventListener) + + explicit GestureEventListener( + AsyncPanZoomController* aAsyncPanZoomController); + + // -------------------------------------------------------------------------- + // These methods must only be called on the controller/UI thread. + // + + /** + * General input handler for a touch event. If the touch event is not a part + * of a gesture, then we pass it along to AsyncPanZoomController. Otherwise, + * it gets consumed here and never forwarded along. + */ + nsEventStatus HandleInputEvent(const MultiTouchInput& aEvent); + + /** + * Returns the identifier of the touch in the last touch event processed by + * this GestureEventListener. This should only be called when the last touch + * event contained only one touch. + */ + int32_t GetLastTouchIdentifier() const; + + /** + * Function used to disable long tap gestures. + * + * On slow running tests, drags and touch events can be misinterpreted + * as a long tap. This allows tests to disable long tap gesture detection. + */ + static void SetLongTapEnabled(bool aLongTapEnabled); + static bool IsLongTapEnabled(); + + private: + // Private destructor, to discourage deletion outside of Release(): + ~GestureEventListener(); + + /** + * States of GEL finite-state machine. + */ + enum GestureState { + // This is the initial and final state of any gesture. + // In this state there's no gesture going on, and we don't think we're + // about to enter one. + // Allowed next states: GESTURE_FIRST_SINGLE_TOUCH_DOWN, + // GESTURE_MULTI_TOUCH_DOWN. + GESTURE_NONE, + + // A touch start with a single touch point has just happened. + // After having gotten into this state we start timers for MAX_TAP_TIME and + // StaticPrefs::ui_click_hold_context_menus_delay(). + // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE, + // GESTURE_FIRST_SINGLE_TOUCH_UP, + // GESTURE_LONG_TOUCH_DOWN, + // GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN. + GESTURE_FIRST_SINGLE_TOUCH_DOWN, + + // While in GESTURE_FIRST_SINGLE_TOUCH_DOWN state a MAX_TAP_TIME timer got + // triggered. Now we'll trigger either a single tap if a user lifts her + // finger or a long tap if StaticPrefs::ui_click_hold_context_menus_delay() + // happens first. + // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_NONE, + // GESTURE_LONG_TOUCH_DOWN. + GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN, + + // A user put her finger down and lifted it up quickly enough. + // After having gotten into this state we clear the timer for MAX_TAP_TIME. + // Allowed next states: GESTURE_SECOND_SINGLE_TOUCH_DOWN, GESTURE_NONE, + // GESTURE_MULTI_TOUCH_DOWN. + GESTURE_FIRST_SINGLE_TOUCH_UP, + + // A user put down her finger again right after a single tap thus the + // gesture can't be a single tap, but rather a double tap. But we're + // still not sure about that until the user lifts her finger again. + // Allowed next states: GESTURE_MULTI_TOUCH_DOWN, GESTURE_ONE_TOUCH_PINCH, + // GESTURE_NONE. + GESTURE_SECOND_SINGLE_TOUCH_DOWN, + + // A long touch has happened, but the user still keeps her finger down. + // We'll trigger a "long tap up" event when the finger is up. + // Allowed next states: GESTURE_NONE, GESTURE_MULTI_TOUCH_DOWN. + GESTURE_LONG_TOUCH_DOWN, + + // We have detected that two or more fingers are on the screen, but there + // hasn't been enough movement yet to make us start actually zooming the + // screen. + // Allowed next states: GESTURE_PINCH, GESTURE_NONE + GESTURE_MULTI_TOUCH_DOWN, + + // There are two or more fingers on the screen, and the user has already + // pinched enough for us to start zooming the screen. + // Allowed next states: GESTURE_NONE + GESTURE_PINCH, + + // The user has double tapped, but not lifted her finger, and moved her + // finger more than PINCH_START_THRESHOLD. + // Allowed next states: GESTURE_NONE. + GESTURE_ONE_TOUCH_PINCH + }; + + /** + * These HandleInput* functions comprise input alphabet of the GEL + * finite-state machine triggering state transitions. + */ + nsEventStatus HandleInputTouchSingleStart(); + nsEventStatus HandleInputTouchMultiStart(); + nsEventStatus HandleInputTouchEnd(); + nsEventStatus HandleInputTouchMove(); + nsEventStatus HandleInputTouchCancel(); + void HandleInputTimeoutLongTap(); + void HandleInputTimeoutMaxTap(bool aDuringFastFling); + + void EnterFirstSingleTouchDown(); + + void TriggerSingleTapConfirmedEvent(); + + bool MoveDistanceExceeds(ScreenCoord aThreshold) const; + bool MoveDistanceIsLarge() const; + bool SecondTapIsFar() const; + + /** + * Returns current vertical span, counting from the where the gesture first + * began (after a brief delay detecting the gesture from first touch). + */ + ScreenCoord GetYSpanFromGestureStartPoint(); + + /** + * Do actual state transition and reset substates. + */ + void SetState(GestureState aState); + + RefPtr mAsyncPanZoomController; + + /** + * Array containing all active touches. When a touch happens it, gets added to + * this array, even if we choose not to handle it. When it ends, we remove it. + * We need to maintain this array in order to detect the end of the + * "multitouch" states because touch start events contain all current touches, + * but touch end events contain only those touches that have gone. + */ + nsTArray mTouches; + + /** + * Current state we're dealing with. + */ + GestureState mState; + + /** + * Total change in span since we detected a pinch gesture. Only used when we + * are in the |GESTURE_WAITING_PINCH| state and need to know how far zoomed + * out we are compared to our original pinch span. Note that this does _not_ + * continue to be updated once we jump into the |GESTURE_PINCH| state. + */ + ScreenCoord mSpanChange; + + /** + * Previous span calculated for the purposes of setting inside a + * PinchGestureInput. + */ + ScreenCoord mPreviousSpan; + + /* Properties similar to mSpanChange and mPreviousSpan, but for the focus */ + ScreenCoord mFocusChange; + ScreenPoint mPreviousFocus; + + /** + * Cached copy of the last touch input. + */ + MultiTouchInput mLastTouchInput; + + /** + * Cached copy of the last tap gesture input. + * In the situation when we have a tap followed by a pinch we lose info + * about tap since we keep only last input and to dispatch it correctly + * we save last tap copy into this variable. + * For more info see bug 947892. + */ + MultiTouchInput mLastTapInput; + + /** + * Position of the last touch that exceeds the GetTouchStartTolerance when + * performing a one-touch-pinch gesture; using the mTouchStartPosition is + * slightly inaccurate because by the time the touch position has passed + * the threshold for the gesture, there is already a span that the zoom + * is calculated from, instead of starting at 1.0 when the threshold gets + * passed. + */ + ScreenPoint mOneTouchPinchStartPosition; + + /** + * Position of the last touch starting. This is only valid during an attempt + * to determine if a touch is a tap. If a touch point moves away from + * mTouchStartPosition to the distance greater than + * AsyncPanZoomController::GetTouchStartTolerance() while in + * GESTURE_FIRST_SINGLE_TOUCH_DOWN, GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN + * or GESTURE_SECOND_SINGLE_TOUCH_DOWN then we're certain the gesture is + * not tap. + */ + ScreenPoint mTouchStartPosition; + + /** + * We store the window/GeckoView's display offset as well, so we can + * track the user's physical touch movements in absolute display coordinates. + */ + ExternalPoint mTouchStartOffset; + + /** + * Task used to timeout a long tap. This gets posted to the UI thread such + * that it runs a time when a single tap happens. We cache it so that + * we can cancel it if any other touch event happens. + * + * The task is supposed to be non-null if in GESTURE_FIRST_SINGLE_TOUCH_DOWN + * and GESTURE_FIRST_SINGLE_TOUCH_MAX_TAP_DOWN states. + * + * CancelLongTapTimeoutTask: Cancel the mLongTapTimeoutTask and also set + * it to null. + */ + RefPtr mLongTapTimeoutTask; + void CancelLongTapTimeoutTask(); + void CreateLongTapTimeoutTask(); + + /** + * Task used to timeout a single tap or a double tap. + * + * The task is supposed to be non-null if in GESTURE_FIRST_SINGLE_TOUCH_DOWN, + * GESTURE_FIRST_SINGLE_TOUCH_UP and GESTURE_SECOND_SINGLE_TOUCH_DOWN states. + * + * CancelMaxTapTimeoutTask: Cancel the mMaxTapTimeoutTask and also set + * it to null. + */ + RefPtr mMaxTapTimeoutTask; + void CancelMaxTapTimeoutTask(); + void CreateMaxTapTimeoutTask(); + + /** + * Tracks whether the single-tap event was already sent to content. This is + * needed because it affects how the double-tap gesture, if detected, is + * handled. The value is only valid in states GESTURE_FIRST_SINGLE_TOUCH_UP + * and GESTURE_SECOND_SINGLE_TOUCH_DOWN; to more easily catch violations it is + * stored in a Maybe which is set to Nothing() at all other times. + */ + Maybe mSingleTapSent; +}; + +} // namespace layers +} // namespace mozilla + +#endif diff --git a/gfx/layers/apz/src/HitTestingTreeNode.cpp b/gfx/layers/apz/src/HitTestingTreeNode.cpp new file mode 100644 index 0000000000..d25a9c1d5f --- /dev/null +++ b/gfx/layers/apz/src/HitTestingTreeNode.cpp @@ -0,0 +1,419 @@ +/* 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 "HitTestingTreeNode.h" +#include + +#include "AsyncPanZoomController.h" // for AsyncPanZoomController +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/gfx/Point.h" // for Point4D +#include "mozilla/layers/APZUtils.h" // for AsyncTransform, CompleteAsyncTransform +#include "mozilla/layers/AsyncDragMetrics.h" // for AsyncDragMetrics +#include "mozilla/ToString.h" // for ToString +#include "nsPrintfCString.h" // for nsPrintfCString +#include "UnitTransforms.h" // for ViewAs + +static mozilla::LazyLogModule sApzMgrLog("apz.manager"); + +namespace mozilla { +namespace layers { + +using gfx::CompositorHitTestInfo; + +HitTestingTreeNode::HitTestingTreeNode(AsyncPanZoomController* aApzc, + bool aIsPrimaryHolder, + LayersId aLayersId) + : mApzc(aApzc), + mIsPrimaryApzcHolder(aIsPrimaryHolder), + mLockCount(0), + mLayersId(aLayersId), + mFixedPosTarget(ScrollableLayerGuid::NULL_SCROLL_ID), + mStickyPosTarget(ScrollableLayerGuid::NULL_SCROLL_ID), + mOverride(EventRegionsOverride::NoOverride) { + if (mIsPrimaryApzcHolder) { + MOZ_ASSERT(mApzc); + } + MOZ_ASSERT(!mApzc || mApzc->GetLayersId() == mLayersId); +} + +void HitTestingTreeNode::RecycleWith( + const RecursiveMutexAutoLock& aProofOfTreeLock, + AsyncPanZoomController* aApzc, LayersId aLayersId) { + MOZ_ASSERT(IsRecyclable(aProofOfTreeLock)); + Destroy(); // clear out tree pointers + mApzc = aApzc; + mLayersId = aLayersId; + MOZ_ASSERT(!mApzc || mApzc->GetLayersId() == mLayersId); + // The caller is expected to call appropriate setters (SetHitTestData, + // SetScrollbarData, SetFixedPosData, SetStickyPosData, etc.) to repopulate + // all the data fields before using this node for "real work". Otherwise + // those data fields may contain stale information from the previous use + // of this node object. +} + +HitTestingTreeNode::~HitTestingTreeNode() = default; + +void HitTestingTreeNode::Destroy() { + // This runs on the updater thread, it's not worth passing around extra raw + // pointers just to assert it. + + mPrevSibling = nullptr; + mLastChild = nullptr; + mParent = nullptr; + + if (mApzc) { + if (mIsPrimaryApzcHolder) { + mApzc->Destroy(); + } + mApzc = nullptr; + } +} + +bool HitTestingTreeNode::IsRecyclable( + const RecursiveMutexAutoLock& aProofOfTreeLock) { + return !(IsPrimaryHolder() || (mLockCount > 0)); +} + +void HitTestingTreeNode::SetLastChild(HitTestingTreeNode* aChild) { + mLastChild = aChild; + if (aChild) { + aChild->mParent = this; + + if (aChild->GetApzc()) { + AsyncPanZoomController* parent = GetNearestContainingApzc(); + // We assume that HitTestingTreeNodes with an ancestor/descendant + // relationship cannot both point to the same APZC instance. This + // assertion only covers a subset of cases in which that might occur, + // but it's better than nothing. + MOZ_ASSERT(aChild->GetApzc() != parent); + aChild->SetApzcParent(parent); + } + } +} + +void HitTestingTreeNode::SetScrollbarData( + const Maybe& aScrollbarAnimationId, + const ScrollbarData& aScrollbarData) { + mScrollbarAnimationId = aScrollbarAnimationId; + mScrollbarData = aScrollbarData; +} + +bool HitTestingTreeNode::MatchesScrollDragMetrics( + const AsyncDragMetrics& aDragMetrics, LayersId aLayersId) const { + return IsScrollThumbNode() && mLayersId == aLayersId && + mScrollbarData.mDirection == aDragMetrics.mDirection && + mScrollbarData.mTargetViewId == aDragMetrics.mViewId; +} + +bool HitTestingTreeNode::IsScrollThumbNode() const { + return mScrollbarData.mScrollbarLayerType == + layers::ScrollbarLayerType::Thumb; +} + +bool HitTestingTreeNode::IsScrollbarNode() const { + return mScrollbarData.mScrollbarLayerType != layers::ScrollbarLayerType::None; +} + +bool HitTestingTreeNode::IsScrollbarContainerNode() const { + return mScrollbarData.mScrollbarLayerType == + layers::ScrollbarLayerType::Container; +} + +ScrollDirection HitTestingTreeNode::GetScrollbarDirection() const { + MOZ_ASSERT(IsScrollbarNode()); + MOZ_ASSERT(mScrollbarData.mDirection.isSome()); + return *mScrollbarData.mDirection; +} + +ScrollableLayerGuid::ViewID HitTestingTreeNode::GetScrollTargetId() const { + return mScrollbarData.mTargetViewId; +} + +Maybe HitTestingTreeNode::GetScrollbarAnimationId() const { + return mScrollbarAnimationId; +} + +const ScrollbarData& HitTestingTreeNode::GetScrollbarData() const { + return mScrollbarData; +} + +void HitTestingTreeNode::SetFixedPosData( + ScrollableLayerGuid::ViewID aFixedPosTarget, SideBits aFixedPosSides, + const Maybe& aFixedPositionAnimationId) { + mFixedPosTarget = aFixedPosTarget; + mFixedPosSides = aFixedPosSides; + mFixedPositionAnimationId = aFixedPositionAnimationId; +} + +ScrollableLayerGuid::ViewID HitTestingTreeNode::GetFixedPosTarget() const { + return mFixedPosTarget; +} + +SideBits HitTestingTreeNode::GetFixedPosSides() const { return mFixedPosSides; } + +Maybe HitTestingTreeNode::GetFixedPositionAnimationId() const { + return mFixedPositionAnimationId; +} + +void HitTestingTreeNode::SetPrevSibling(HitTestingTreeNode* aSibling) { + mPrevSibling = aSibling; + if (aSibling) { + aSibling->mParent = mParent; + + if (aSibling->GetApzc()) { + AsyncPanZoomController* parent = + mParent ? mParent->GetNearestContainingApzc() : nullptr; + aSibling->SetApzcParent(parent); + } + } +} + +void HitTestingTreeNode::SetStickyPosData( + ScrollableLayerGuid::ViewID aStickyPosTarget, + const LayerRectAbsolute& aScrollRangeOuter, + const LayerRectAbsolute& aScrollRangeInner, + const Maybe& aStickyPositionAnimationId) { + mStickyPosTarget = aStickyPosTarget; + mStickyScrollRangeOuter = aScrollRangeOuter; + mStickyScrollRangeInner = aScrollRangeInner; + mStickyPositionAnimationId = aStickyPositionAnimationId; +} + +ScrollableLayerGuid::ViewID HitTestingTreeNode::GetStickyPosTarget() const { + return mStickyPosTarget; +} + +const LayerRectAbsolute& HitTestingTreeNode::GetStickyScrollRangeOuter() const { + return mStickyScrollRangeOuter; +} + +const LayerRectAbsolute& HitTestingTreeNode::GetStickyScrollRangeInner() const { + return mStickyScrollRangeInner; +} + +Maybe HitTestingTreeNode::GetStickyPositionAnimationId() const { + return mStickyPositionAnimationId; +} + +void HitTestingTreeNode::MakeRoot() { + mParent = nullptr; + + if (GetApzc()) { + SetApzcParent(nullptr); + } +} + +HitTestingTreeNode* HitTestingTreeNode::GetFirstChild() const { + HitTestingTreeNode* child = GetLastChild(); + while (child && child->GetPrevSibling()) { + child = child->GetPrevSibling(); + } + return child; +} + +HitTestingTreeNode* HitTestingTreeNode::GetLastChild() const { + return mLastChild; +} + +HitTestingTreeNode* HitTestingTreeNode::GetPrevSibling() const { + return mPrevSibling; +} + +HitTestingTreeNode* HitTestingTreeNode::GetParent() const { return mParent; } + +bool HitTestingTreeNode::IsAncestorOf(const HitTestingTreeNode* aOther) const { + for (const HitTestingTreeNode* cur = aOther; cur; cur = cur->GetParent()) { + if (cur == this) { + return true; + } + } + return false; +} + +AsyncPanZoomController* HitTestingTreeNode::GetApzc() const { return mApzc; } + +AsyncPanZoomController* HitTestingTreeNode::GetNearestContainingApzc() const { + for (const HitTestingTreeNode* n = this; n; n = n->GetParent()) { + if (n->GetApzc()) { + return n->GetApzc(); + } + } + return nullptr; +} + +bool HitTestingTreeNode::IsPrimaryHolder() const { + return mIsPrimaryApzcHolder; +} + +LayersId HitTestingTreeNode::GetLayersId() const { return mLayersId; } + +void HitTestingTreeNode::SetHitTestData( + const LayerIntRegion& aVisibleRegion, + const LayerIntSize& aRemoteDocumentSize, + const CSSTransformMatrix& aTransform, const EventRegionsOverride& aOverride, + const Maybe& aAsyncZoomContainerId) { + mVisibleRegion = aVisibleRegion; + mRemoteDocumentSize = aRemoteDocumentSize; + mTransform = aTransform; + mOverride = aOverride; + mAsyncZoomContainerId = aAsyncZoomContainerId; +} + +EventRegionsOverride HitTestingTreeNode::GetEventRegionsOverride() const { + return mOverride; +} + +const CSSTransformMatrix& HitTestingTreeNode::GetTransform() const { + return mTransform; +} + +LayerToScreenMatrix4x4 HitTestingTreeNode::GetTransformToGecko() const { + if (mParent) { + LayerToParentLayerMatrix4x4 thisToParent = + mTransform * AsyncTransformMatrix(); + if (mApzc) { + thisToParent = + thisToParent * ViewAs( + mApzc->GetTransformToLastDispatchedPaint()); + } + ParentLayerToScreenMatrix4x4 parentToRoot = + ViewAs( + mParent->GetTransformToGecko(), + PixelCastJustification::MovingDownToChildren); + return thisToParent * parentToRoot; + } + + return ViewAs( + mTransform * AsyncTransformMatrix(), + PixelCastJustification::ScreenIsParentLayerForRoot); +} + +const LayerIntRegion& HitTestingTreeNode::GetVisibleRegion() const { + return mVisibleRegion; +} + +ScreenRect HitTestingTreeNode::GetRemoteDocumentScreenRect() const { + ScreenRect result = TransformBy( + GetTransformToGecko(), + IntRectToRect(LayerIntRect(LayerIntPoint(), mRemoteDocumentSize))); + + for (const HitTestingTreeNode* node = this; node; node = node->GetParent()) { + if (!node->GetApzc()) { + continue; + } + + ParentLayerRect compositionBounds = node->GetApzc()->GetCompositionBounds(); + if (compositionBounds.IsEmpty()) { + return ScreenRect(); + } + + ScreenRect scrollPortOnScreenCoordinate = TransformBy( + node->GetParent() ? node->GetParent()->GetTransformToGecko() + : LayerToScreenMatrix4x4(), + ViewAs(compositionBounds, + PixelCastJustification::MovingDownToChildren)); + if (scrollPortOnScreenCoordinate.IsEmpty()) { + return ScreenRect(); + } + + result = result.Intersect(scrollPortOnScreenCoordinate); + if (result.IsEmpty()) { + return ScreenRect(); + } + } + return result; +} + +Maybe HitTestingTreeNode::GetAsyncZoomContainerId() + const { + return mAsyncZoomContainerId; +} + +void HitTestingTreeNode::Dump(const char* aPrefix) const { + MOZ_LOG( + sApzMgrLog, LogLevel::Debug, + ("%sHitTestingTreeNode (%p) APZC (%p) g=(%s) %s%s%s t=(%s) " + "%s%s\n", + aPrefix, this, mApzc.get(), + mApzc ? ToString(mApzc->GetGuid()).c_str() + : nsPrintfCString("l=0x%" PRIx64, uint64_t(mLayersId)).get(), + (mOverride & EventRegionsOverride::ForceDispatchToContent) ? "fdtc " + : "", + (mOverride & EventRegionsOverride::ForceEmptyHitRegion) ? "fehr " : "", + (mFixedPosTarget != ScrollableLayerGuid::NULL_SCROLL_ID) + ? nsPrintfCString("fixed=%" PRIu64 " ", mFixedPosTarget).get() + : "", + ToString(mTransform).c_str(), + mScrollbarData.mDirection.isSome() ? " scrollbar" : "", + IsScrollThumbNode() ? " scrollthumb" : "")); + + if (!mLastChild) { + return; + } + + // Dump the children in order from first child to last child + std::stack children; + for (HitTestingTreeNode* child = mLastChild.get(); child; + child = child->mPrevSibling) { + children.push(child); + } + nsPrintfCString childPrefix("%s ", aPrefix); + while (!children.empty()) { + children.top()->Dump(childPrefix.get()); + children.pop(); + } +} + +void HitTestingTreeNode::SetApzcParent(AsyncPanZoomController* aParent) { + // precondition: GetApzc() is non-null + MOZ_ASSERT(GetApzc() != nullptr); + if (IsPrimaryHolder()) { + GetApzc()->SetParent(aParent); + } else { + MOZ_ASSERT(GetApzc()->GetParent() == aParent); + } +} + +void HitTestingTreeNode::Lock(const RecursiveMutexAutoLock& aProofOfTreeLock) { + mLockCount++; +} + +void HitTestingTreeNode::Unlock( + const RecursiveMutexAutoLock& aProofOfTreeLock) { + MOZ_ASSERT(mLockCount > 0); + mLockCount--; +} + +HitTestingTreeNodeAutoLock::HitTestingTreeNodeAutoLock() + : mTreeMutex(nullptr) {} + +HitTestingTreeNodeAutoLock::~HitTestingTreeNodeAutoLock() { Clear(); } + +void HitTestingTreeNodeAutoLock::Initialize( + const RecursiveMutexAutoLock& aProofOfTreeLock, + already_AddRefed aNode, RecursiveMutex& aTreeMutex) { + MOZ_ASSERT(!mNode); + + mNode = aNode; + mTreeMutex = &aTreeMutex; + + mNode->Lock(aProofOfTreeLock); +} + +void HitTestingTreeNodeAutoLock::Clear() { + if (!mNode) { + return; + } + MOZ_ASSERT(mTreeMutex); + + { // scope lock + RecursiveMutexAutoLock lock(*mTreeMutex); + mNode->Unlock(lock); + } + mNode = nullptr; + mTreeMutex = nullptr; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/HitTestingTreeNode.h b/gfx/layers/apz/src/HitTestingTreeNode.h new file mode 100644 index 0000000000..ed20f9f561 --- /dev/null +++ b/gfx/layers/apz/src/HitTestingTreeNode.h @@ -0,0 +1,270 @@ +/* -*- 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_HitTestingTreeNode_h +#define mozilla_layers_HitTestingTreeNode_h + +#include "mozilla/gfx/CompositorHitTestInfo.h" +#include "mozilla/gfx/Matrix.h" // for Matrix4x4 +#include "mozilla/layers/LayersTypes.h" // for EventRegions +#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid +#include "mozilla/layers/ScrollbarData.h" // for ScrollbarData +#include "mozilla/Maybe.h" // for Maybe +#include "mozilla/RecursiveMutex.h" // for RecursiveMutexAutoLock +#include "mozilla/RefPtr.h" // for nsRefPtr +namespace mozilla { +namespace layers { + +class AsyncDragMetrics; +class AsyncPanZoomController; + +/** + * This class represents a node in a tree that is used by the APZCTreeManager + * to do hit testing. The tree is roughly a copy of the layer tree, but will + * contain multiple nodes in cases where the layer has multiple FrameMetrics. + * In other words, the structure of this tree should be identical to the + * WebRenderScrollDataWrapper tree (see documentation in + * WebRenderScrollDataWrapper.h). + * + * Not all HitTestingTreeNode instances will have an APZC associated with them; + * only HitTestingTreeNodes that correspond to layers with scrollable metrics + * have APZCs. + * Multiple HitTestingTreeNode instances may share the same underlying APZC + * instance if the layers they represent share the same scrollable metrics (i.e. + * are part of the same animated geometry root). If this happens, exactly one of + * the HitTestingTreeNode instances will be designated as the "primary holder" + * of the APZC. When this primary holder is destroyed, it will destroy the APZC + * along with it; in contrast, destroying non-primary-holder nodes will not + * destroy the APZC. + * Code should not make assumptions about which of the nodes will be the + * primary holder, only that that there will be exactly one for each APZC in + * the tree. + * + * The reason this tree exists at all is so that we can do hit-testing on the + * thread that we receive input on (referred to the as the controller thread in + * APZ terminology), which may be different from the compositor thread. + * Accessing the compositor layer tree can only be done on the compositor + * thread, and so it is simpler to make a copy of the hit-testing related + * properties into a separate tree. + * + * The tree pointers on the node (mLastChild, etc.) can only be manipulated + * while holding the APZ tree lock. Any code that wishes to use a + * HitTestingTreeNode outside of holding the tree lock should do so by using + * the HitTestingTreeNodeAutoLock wrapper, which prevents the node from + * being recycled (and also holds a RefPtr to the node to prevent it from + * getting freed). + */ +class HitTestingTreeNode { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(HitTestingTreeNode); + + private: + ~HitTestingTreeNode(); + + public: + HitTestingTreeNode(AsyncPanZoomController* aApzc, bool aIsPrimaryHolder, + LayersId aLayersId); + void RecycleWith(const RecursiveMutexAutoLock& aProofOfTreeLock, + AsyncPanZoomController* aApzc, LayersId aLayersId); + // Clears the tree pointers on the node, thereby breaking RefPtr cycles. This + // can trigger free'ing of this and other HitTestingTreeNode instances. + void Destroy(); + + // Returns true if and only if the node is available for recycling as part + // of a hit-testing tree update. Note that this node can have Destroy() called + // on it whether or not it is recyclable. + bool IsRecyclable(const RecursiveMutexAutoLock& aProofOfTreeLock); + + /* Tree construction methods */ + + void SetLastChild(HitTestingTreeNode* aChild); + void SetPrevSibling(HitTestingTreeNode* aSibling); + void MakeRoot(); + + /* Tree walking methods. GetFirstChild is O(n) in the number of children. The + * other tree walking methods are all O(1). */ + + HitTestingTreeNode* GetFirstChild() const; + HitTestingTreeNode* GetLastChild() const; + HitTestingTreeNode* GetPrevSibling() const; + HitTestingTreeNode* GetParent() const; + + bool IsAncestorOf(const HitTestingTreeNode* aOther) const; + + /* APZC related methods */ + + AsyncPanZoomController* GetApzc() const; + AsyncPanZoomController* GetNearestContainingApzc() const; + bool IsPrimaryHolder() const; + LayersId GetLayersId() const; + + /* Hit test related methods */ + + void SetHitTestData( + const LayerIntRegion& aVisibleRegion, + const LayerIntSize& aRemoteDocumentSize, + const CSSTransformMatrix& aTransform, + const EventRegionsOverride& aOverride, + const Maybe& aAsyncZoomContainerId); + + /* Scrollbar info */ + + void SetScrollbarData(const Maybe& aScrollbarAnimationId, + const ScrollbarData& aScrollbarData); + bool MatchesScrollDragMetrics(const AsyncDragMetrics& aDragMetrics, + LayersId aLayersId) const; + bool IsScrollbarNode() const; // Scroll thumb or scrollbar container layer. + bool IsScrollbarContainerNode() const; // Scrollbar container layer. + // This can only be called if IsScrollbarNode() is true + ScrollDirection GetScrollbarDirection() const; + bool IsScrollThumbNode() const; // Scroll thumb container layer. + ScrollableLayerGuid::ViewID GetScrollTargetId() const; + const ScrollbarData& GetScrollbarData() const; + Maybe GetScrollbarAnimationId() const; + + /* Fixed pos info */ + + void SetFixedPosData(ScrollableLayerGuid::ViewID aFixedPosTarget, + SideBits aFixedPosSides, + const Maybe& aFixedPositionAnimationId); + ScrollableLayerGuid::ViewID GetFixedPosTarget() const; + SideBits GetFixedPosSides() const; + Maybe GetFixedPositionAnimationId() const; + + /* Sticky pos info */ + void SetStickyPosData(ScrollableLayerGuid::ViewID aStickyPosTarget, + const LayerRectAbsolute& aScrollRangeOuter, + const LayerRectAbsolute& aScrollRangeInner, + const Maybe& aStickyPositionAnimationId); + ScrollableLayerGuid::ViewID GetStickyPosTarget() const; + const LayerRectAbsolute& GetStickyScrollRangeOuter() const; + const LayerRectAbsolute& GetStickyScrollRangeInner() const; + Maybe GetStickyPositionAnimationId() const; + + /* Returns the mOverride flag. */ + EventRegionsOverride GetEventRegionsOverride() const; + const CSSTransformMatrix& GetTransform() const; + /* This is similar to APZCTreeManager::GetApzcToGeckoTransform but without + * the async bits. It's used on the main-thread for transforming coordinates + * across a BrowserParent/BrowserChild interface.*/ + LayerToScreenMatrix4x4 GetTransformToGecko() const; + const LayerIntRegion& GetVisibleRegion() const; + + /* Returns the screen coordinate rectangle of remote iframe corresponding to + * this node. The rectangle is the result of clipped by ancestor async + * scrolling. */ + ScreenRect GetRemoteDocumentScreenRect() const; + + Maybe GetAsyncZoomContainerId() const; + + /* Debug helpers */ + void Dump(const char* aPrefix = "") const; + + private: + friend class HitTestingTreeNodeAutoLock; + // Functions that are private but called from HitTestingTreeNodeAutoLock + void Lock(const RecursiveMutexAutoLock& aProofOfTreeLock); + void Unlock(const RecursiveMutexAutoLock& aProofOfTreeLock); + + void SetApzcParent(AsyncPanZoomController* aApzc); + + RefPtr mLastChild; + RefPtr mPrevSibling; + RefPtr mParent; + + RefPtr mApzc; + bool mIsPrimaryApzcHolder; + int mLockCount; + + LayersId mLayersId; + + // This is only set if WebRender is enabled, and only for HTTNs + // where IsScrollThumbNode() returns true. It holds the animation id that we + // use to move the thumb node to reflect async scrolling. + Maybe mScrollbarAnimationId; + + // This is set for scrollbar Container and Thumb layers. + ScrollbarData mScrollbarData; + + // This is only set if WebRender is enabled. It holds the animation id that + // we use to adjust fixed position content for the toolbar. + Maybe mFixedPositionAnimationId; + + ScrollableLayerGuid::ViewID mFixedPosTarget; + SideBits mFixedPosSides; + + ScrollableLayerGuid::ViewID mStickyPosTarget; + LayerRectAbsolute mStickyScrollRangeOuter; + LayerRectAbsolute mStickyScrollRangeInner; + // This is only set if WebRender is enabled. It holds the animation id that + // we use to adjust sticky position content for the toolbar. + Maybe mStickyPositionAnimationId; + + LayerIntRegion mVisibleRegion; + + /* The size of remote iframe on the corresponding layer coordinate. + * It's empty if this node is not for remote iframe. */ + LayerIntSize mRemoteDocumentSize; + + /* This is the transform from layer L. This does NOT include any async + * transforms. */ + CSSTransformMatrix mTransform; + + /* If the layer is the async zoom container layer then this will hold the id. + */ + Maybe mAsyncZoomContainerId; + + /* Indicates whether or not the event regions on this node need to be + * overridden in a certain way. */ + EventRegionsOverride mOverride; +}; + +/** + * A class that allows safe usage of a HitTestingTreeNode outside of the APZ + * tree lock. In general, this class should be Initialize()'d inside the tree + * lock (enforced by the proof-of-lock to Initialize), and then can be returned + * to a scope outside the tree lock and used safely. Upon destruction or + * Clear() being called, it unlocks the underlying node at which point it can + * be recycled or freed. + */ +class HitTestingTreeNodeAutoLock final { + public: + HitTestingTreeNodeAutoLock(); + ~HitTestingTreeNodeAutoLock(); + // Make it move-only. Note that the default implementations of the move + // constructor and assignment operator are correct: they'll call the + // move constructor of mNode, which will null out mNode on the moved-from + // object, and Clear() will early-exit when the moved-from object's + // destructor is called. + HitTestingTreeNodeAutoLock(HitTestingTreeNodeAutoLock&&) = default; + HitTestingTreeNodeAutoLock& operator=(HitTestingTreeNodeAutoLock&&) = default; + + void Initialize(const RecursiveMutexAutoLock& aProofOfTreeLock, + already_AddRefed aNode, + RecursiveMutex& aTreeMutex); + void Clear(); + + // Convenience operators to simplify the using code. + explicit operator bool() const { return !!mNode; } + bool operator!() const { return !mNode; } + HitTestingTreeNode* operator->() const { return mNode.get(); } + + // Allow getting back a raw pointer to the node, but only inside the scope + // of the tree lock. The caller is responsible for ensuring that they do not + // use the raw pointer outside that scope. + HitTestingTreeNode* Get( + mozilla::RecursiveMutexAutoLock& aProofOfTreeLock) const { + return mNode.get(); + } + + private: + RefPtr mNode; + RecursiveMutex* mTreeMutex; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_HitTestingTreeNode_h diff --git a/gfx/layers/apz/src/IAPZHitTester.cpp b/gfx/layers/apz/src/IAPZHitTester.cpp new file mode 100644 index 0000000000..884efe97e8 --- /dev/null +++ b/gfx/layers/apz/src/IAPZHitTester.cpp @@ -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/. */ + +#include "IAPZHitTester.h" +#include "APZCTreeManager.h" +#include "AsyncPanZoomController.h" + +namespace mozilla { +namespace layers { + +IAPZHitTester::HitTestResult IAPZHitTester::CloneHitTestResult( + RecursiveMutexAutoLock& aProofOfTreeLock, + const IAPZHitTester::HitTestResult& aHitTestResult) const { + HitTestResult result; + + result.mTargetApzc = aHitTestResult.mTargetApzc; + result.mHitResult = aHitTestResult.mHitResult; + result.mLayersId = aHitTestResult.mLayersId; + result.mFixedPosSides = aHitTestResult.mFixedPosSides; + result.mHitOverscrollGutter = aHitTestResult.mHitOverscrollGutter; + + RefPtr scrollbarNode = + aHitTestResult.mScrollbarNode.Get(aProofOfTreeLock); + RefPtr node = aHitTestResult.mNode.Get(aProofOfTreeLock); + + if (aHitTestResult.mScrollbarNode) { + InitializeHitTestingTreeNodeAutoLock(result.mScrollbarNode, + aProofOfTreeLock, scrollbarNode); + } + if (aHitTestResult.mNode) { + InitializeHitTestingTreeNodeAutoLock(result.mNode, aProofOfTreeLock, node); + } + + return result; +} + +LayersId IAPZHitTester::GetRootLayersId() const { + return mTreeManager->mRootLayersId; +} + +HitTestingTreeNode* IAPZHitTester::GetRootNode() const { + mTreeManager->mTreeLock.AssertCurrentThreadIn(); + return mTreeManager->mRootNode; +} + +HitTestingTreeNode* IAPZHitTester::FindRootNodeForLayersId( + LayersId aLayersId) const { + return mTreeManager->FindRootNodeForLayersId(aLayersId); +} + +AsyncPanZoomController* IAPZHitTester::FindRootApzcForLayersId( + LayersId aLayersId) const { + HitTestingTreeNode* resultNode = FindRootNodeForLayersId(aLayersId); + return resultNode ? resultNode->GetApzc() : nullptr; +} + +already_AddRefed IAPZHitTester::GetTargetNode( + const ScrollableLayerGuid& aGuid, + ScrollableLayerGuid::Comparator aComparator) { + // Acquire the tree lock so that derived classes can call this from + // methods other than GetAPZCAtPoint(). + RecursiveMutexAutoLock lock(mTreeManager->mTreeLock); + return mTreeManager->GetTargetNode(aGuid, aComparator); +} + +void IAPZHitTester::InitializeHitTestingTreeNodeAutoLock( + HitTestingTreeNodeAutoLock& aAutoLock, + const RecursiveMutexAutoLock& aProofOfTreeLock, + RefPtr& aNode) const { + aAutoLock.Initialize(aProofOfTreeLock, aNode.forget(), + mTreeManager->mTreeLock); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/IAPZHitTester.h b/gfx/layers/apz/src/IAPZHitTester.h new file mode 100644 index 0000000000..8822b40ea8 --- /dev/null +++ b/gfx/layers/apz/src/IAPZHitTester.h @@ -0,0 +1,91 @@ +/* -*- 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_IAPZHitTester_h +#define mozilla_layers_IAPZHitTester_h + +#include "HitTestingTreeNode.h" // for HitTestingTreeNodeAutoLock +#include "mozilla/RefPtr.h" +#include "mozilla/gfx/CompositorHitTestInfo.h" +#include "mozilla/layers/LayersTypes.h" + +namespace mozilla { +namespace layers { + +class AsyncPanZoomController; +class APZCTreeManager; + +class IAPZHitTester { + public: + virtual ~IAPZHitTester() = default; + + // Not a constructor because we want external code to be able to pass a hit + // tester to the APZCTreeManager constructor, which will then initialize it. + void Initialize(APZCTreeManager* aTreeManager) { + mTreeManager = aTreeManager; + } + + // Represents the results of an APZ hit test. + struct HitTestResult { + // The APZC targeted by the hit test. + RefPtr mTargetApzc; + // The applicable hit test flags. + gfx::CompositorHitTestInfo mHitResult; + // The layers id of the content that was hit. + // This effectively identifiers the process that was hit for + // Fission purposes. + LayersId mLayersId; + // If a scrollbar was hit, this will be populated with the + // scrollbar node. The AutoLock allows accessing the scrollbar + // node without having to hold the tree lock. + HitTestingTreeNodeAutoLock mScrollbarNode; + // If content that is fixed to the root-content APZC was hit, + // the sides of the viewport to which the content is fixed. + SideBits mFixedPosSides = SideBits::eNone; + // If a fixed/sticky position element was hit, this will be populated with + // the hit-testing tree node. The AutoLock allows accessing the node + // without having to hold the tree lock. + HitTestingTreeNodeAutoLock mNode; + // This is set to true If mTargetApzc is overscrolled and the + // event targeted the gap space ("gutter") created by the overscroll. + bool mHitOverscrollGutter = false; + + HitTestResult() = default; + // Make it move-only. + HitTestResult(HitTestResult&&) = default; + HitTestResult& operator=(HitTestResult&&) = default; + }; + + virtual HitTestResult GetAPZCAtPoint( + const ScreenPoint& aHitTestPoint, + const RecursiveMutexAutoLock& aProofOfTreeLock) = 0; + + HitTestResult CloneHitTestResult(RecursiveMutexAutoLock& aProofOfTreeLock, + const HitTestResult& aHitTestResult) const; + + protected: + APZCTreeManager* mTreeManager = nullptr; + + // We are a friend of APZCTreeManager but our derived classes + // are not. Wrap a few private members of APZCTreeManager for + // use by derived classes. + LayersId GetRootLayersId() const; + HitTestingTreeNode* GetRootNode() const; + HitTestingTreeNode* FindRootNodeForLayersId(LayersId aLayersId) const; + AsyncPanZoomController* FindRootApzcForLayersId(LayersId aLayersId) const; + already_AddRefed GetTargetNode( + const ScrollableLayerGuid& aGuid, + ScrollableLayerGuid::Comparator aComparator); + void InitializeHitTestingTreeNodeAutoLock( + HitTestingTreeNodeAutoLock& aAutoLock, + const RecursiveMutexAutoLock& aProofOfTreeLock, + RefPtr& aNode) const; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_IAPZHitTester_h diff --git a/gfx/layers/apz/src/InputBlockState.cpp b/gfx/layers/apz/src/InputBlockState.cpp new file mode 100644 index 0000000000..b492af0215 --- /dev/null +++ b/gfx/layers/apz/src/InputBlockState.cpp @@ -0,0 +1,840 @@ +/* -*- 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 "InputBlockState.h" + +#include "APZUtils.h" +#include "AsyncPanZoomController.h" // for AsyncPanZoomController + +#include "mozilla/MouseEvents.h" +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_mousewheel.h" +#include "mozilla/StaticPrefs_test.h" +#include "mozilla/Telemetry.h" // for Telemetry +#include "mozilla/ToString.h" +#include "mozilla/layers/IAPZCTreeManager.h" // for AllowedTouchBehavior +#include "OverscrollHandoffState.h" +#include "QueuedInput.h" + +static mozilla::LazyLogModule sApzIbsLog("apz.inputstate"); +#define TBS_LOG(...) MOZ_LOG(sApzIbsLog, LogLevel::Debug, (__VA_ARGS__)) + +namespace mozilla { +namespace layers { + +static uint64_t sBlockCounter = InputBlockState::NO_BLOCK_ID + 1; + +InputBlockState::InputBlockState( + const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags) + : mTargetApzc(aTargetApzc), + mRequiresTargetConfirmation(aFlags.mRequiresTargetConfirmation), + mBlockId(sBlockCounter++), + mTransformToApzc(aTargetApzc->GetTransformToThis()) { + // We should never be constructed with a nullptr target. + MOZ_ASSERT(mTargetApzc); + mOverscrollHandoffChain = mTargetApzc->BuildOverscrollHandoffChain(); + // If a new block starts on a scrollthumb and we have APZ scrollbar + // dragging enabled, defer confirmation until we get the drag metrics + // for the thumb. + bool startingDrag = StaticPrefs::apz_drag_enabled() && aFlags.mHitScrollThumb; + mTargetConfirmed = aFlags.mTargetConfirmed && !startingDrag + ? TargetConfirmationState::eConfirmed + : TargetConfirmationState::eUnconfirmed; +} + +bool InputBlockState::SetConfirmedTargetApzc( + const RefPtr& aTargetApzc, + TargetConfirmationState aState, InputData* aFirstInput, + bool aForScrollbarDrag) { + MOZ_ASSERT(aState == TargetConfirmationState::eConfirmed || + aState == TargetConfirmationState::eTimedOut); + + if (mTargetConfirmed == TargetConfirmationState::eTimedOut && + aState == TargetConfirmationState::eConfirmed) { + // The main thread finally responded. We had already timed out the + // confirmation, but we want to update the state internally so that we + // can record the time for telemetry purposes. + mTargetConfirmed = TargetConfirmationState::eTimedOutAndMainThreadResponded; + } + // Sometimes, bugs in compositor hit testing can lead to APZ confirming + // a different target than the main thread. If this happens for a drag + // block created for a scrollbar drag, the consequences can be fairly + // user-unfriendly, such as the scrollbar not being draggable at all, + // or it scrolling the contents of the wrong scrollframe. In debug + // builds, we assert in this situation, so that the + // underlying compositor hit testing bug can be fixed. In release builds, + // however, we just silently accept the main thread's confirmed target, + // which will produce the expected behaviour (apart from drag events + // received so far being dropped). + if (AsDragBlock() && aForScrollbarDrag && + mTargetConfirmed == TargetConfirmationState::eConfirmed && + aState == TargetConfirmationState::eConfirmed && mTargetApzc && + aTargetApzc && mTargetApzc->GetGuid() != aTargetApzc->GetGuid()) { + MOZ_ASSERT(false, + "APZ and main thread confirmed scrollbar drag block with " + "different targets"); + UpdateTargetApzc(aTargetApzc); + return true; + } + + if (mTargetConfirmed != TargetConfirmationState::eUnconfirmed) { + return false; + } + mTargetConfirmed = aState; + + TBS_LOG("%p got confirmed target APZC %p\n", this, mTargetApzc.get()); + if (mTargetApzc == aTargetApzc) { + // The confirmed target is the same as the tentative one, so we're done. + return true; + } + + TBS_LOG("%p replacing unconfirmed target %p with real target %p\n", this, + mTargetApzc.get(), aTargetApzc.get()); + + UpdateTargetApzc(aTargetApzc); + return true; +} + +void InputBlockState::UpdateTargetApzc( + const RefPtr& aTargetApzc) { + if (mTargetApzc == aTargetApzc) { + MOZ_ASSERT_UNREACHABLE( + "The new target APZC should be different from the old one"); + return; + } + + if (mTargetApzc) { + // Restore overscroll state on the previous target APZC and ancestor APZCs + // in the scroll handoff chain other than the new one. + mTargetApzc->SnapBackIfOverscrolled(); + + uint32_t i = mOverscrollHandoffChain->IndexOf(mTargetApzc) + 1; + for (; i < mOverscrollHandoffChain->Length(); i++) { + AsyncPanZoomController* apzc = mOverscrollHandoffChain->GetApzcAtIndex(i); + if (apzc != aTargetApzc) { + MOZ_ASSERT(!apzc->IsOverscrolled() || + apzc->IsOverscrollAnimationRunning()); + apzc->SnapBackIfOverscrolled(); + } + } + } + + // note that aTargetApzc MAY be null here. + mTargetApzc = aTargetApzc; + mTransformToApzc = aTargetApzc ? aTargetApzc->GetTransformToThis() + : ScreenToParentLayerMatrix4x4(); + mOverscrollHandoffChain = + (mTargetApzc ? mTargetApzc->BuildOverscrollHandoffChain() : nullptr); +} + +const RefPtr& InputBlockState::GetTargetApzc() const { + return mTargetApzc; +} + +const RefPtr& +InputBlockState::GetOverscrollHandoffChain() const { + return mOverscrollHandoffChain; +} + +uint64_t InputBlockState::GetBlockId() const { return mBlockId; } + +bool InputBlockState::IsTargetConfirmed() const { + return mTargetConfirmed != TargetConfirmationState::eUnconfirmed; +} + +bool InputBlockState::HasReceivedRealConfirmedTarget() const { + return mTargetConfirmed == TargetConfirmationState::eConfirmed || + mTargetConfirmed == + TargetConfirmationState::eTimedOutAndMainThreadResponded; +} + +bool InputBlockState::ShouldDropEvents() const { + return mRequiresTargetConfirmation && + (mTargetConfirmed != TargetConfirmationState::eConfirmed); +} + +bool InputBlockState::IsDownchainOf(AsyncPanZoomController* aA, + AsyncPanZoomController* aB) const { + if (aA == aB) { + return true; + } + + bool seenA = false; + for (size_t i = 0; i < mOverscrollHandoffChain->Length(); ++i) { + AsyncPanZoomController* apzc = mOverscrollHandoffChain->GetApzcAtIndex(i); + if (apzc == aB) { + return seenA; + } + if (apzc == aA) { + seenA = true; + } + } + return false; +} + +void InputBlockState::SetScrolledApzc(AsyncPanZoomController* aApzc) { + // An input block should only have one scrolled APZC. + MOZ_ASSERT(!mScrolledApzc || (StaticPrefs::apz_allow_immediate_handoff() + ? IsDownchainOf(mScrolledApzc, aApzc) + : mScrolledApzc == aApzc)); + + mScrolledApzc = aApzc; +} + +AsyncPanZoomController* InputBlockState::GetScrolledApzc() const { + return mScrolledApzc; +} + +bool InputBlockState::IsDownchainOfScrolledApzc( + AsyncPanZoomController* aApzc) const { + MOZ_ASSERT(aApzc && mScrolledApzc); + + return IsDownchainOf(mScrolledApzc, aApzc); +} + +void InputBlockState::DispatchEvent(const InputData& aEvent) const { + GetTargetApzc()->HandleInputEvent(aEvent, mTransformToApzc); +} + +CancelableBlockState::CancelableBlockState( + const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags) + : InputBlockState(aTargetApzc, aFlags), + mPreventDefault(false), + mContentResponded(false), + mContentResponseTimerExpired(false) {} + +bool CancelableBlockState::SetContentResponse(bool aPreventDefault) { + if (mContentResponded) { + return false; + } + TBS_LOG("%p got content response %d with timer expired %d\n", this, + aPreventDefault, mContentResponseTimerExpired); + mPreventDefault = aPreventDefault; + mContentResponded = true; + return true; +} + +bool CancelableBlockState::TimeoutContentResponse() { + if (mContentResponseTimerExpired) { + return false; + } + TBS_LOG("%p got content timer expired with response received %d\n", this, + mContentResponded); + if (!mContentResponded) { + mPreventDefault = false; + } + mContentResponseTimerExpired = true; + return true; +} + +bool CancelableBlockState::IsContentResponseTimerExpired() const { + return mContentResponseTimerExpired; +} + +bool CancelableBlockState::IsDefaultPrevented() const { + MOZ_ASSERT(mContentResponded || mContentResponseTimerExpired); + return mPreventDefault; +} + +bool CancelableBlockState::IsReadyForHandling() const { + if (!IsTargetConfirmed()) { + return false; + } + return mContentResponded || mContentResponseTimerExpired; +} + +bool CancelableBlockState::ShouldDropEvents() const { + return InputBlockState::ShouldDropEvents() || IsDefaultPrevented(); +} + +DragBlockState::DragBlockState( + const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags, const MouseInput& aInitialEvent) + : CancelableBlockState(aTargetApzc, aFlags), mReceivedMouseUp(false) {} + +bool DragBlockState::HasReceivedMouseUp() { return mReceivedMouseUp; } + +void DragBlockState::MarkMouseUpReceived() { mReceivedMouseUp = true; } + +void DragBlockState::SetInitialThumbPos(OuterCSSCoord aThumbPos) { + mInitialThumbPos = aThumbPos; +} + +void DragBlockState::SetDragMetrics(const AsyncDragMetrics& aDragMetrics) { + mDragMetrics = aDragMetrics; +} + +void DragBlockState::DispatchEvent(const InputData& aEvent) const { + MouseInput mouseInput = aEvent.AsMouseInput(); + if (!mouseInput.TransformToLocal(mTransformToApzc)) { + return; + } + + GetTargetApzc()->HandleDragEvent(mouseInput, mDragMetrics, mInitialThumbPos); +} + +bool DragBlockState::MustStayActive() { return !mReceivedMouseUp; } + +const char* DragBlockState::Type() { return "drag"; } +// This is used to track the current wheel transaction. +static uint64_t sLastWheelBlockId = InputBlockState::NO_BLOCK_ID; + +WheelBlockState::WheelBlockState( + const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags, const ScrollWheelInput& aInitialEvent) + : CancelableBlockState(aTargetApzc, aFlags), + mScrollSeriesCounter(0), + mTransactionEnded(false) { + sLastWheelBlockId = GetBlockId(); + + if (aFlags.mTargetConfirmed) { + // Find the nearest APZC in the overscroll handoff chain that is scrollable. + // If we get a content confirmation later that the apzc is different, then + // content should have found a scrollable apzc, so we don't need to handle + // that case. + RefPtr apzc = + mOverscrollHandoffChain->FindFirstScrollable(aInitialEvent, + &mAllowedScrollDirections); + + if (apzc) { + if (apzc != GetTargetApzc()) { + UpdateTargetApzc(apzc); + } + } else if (!mOverscrollHandoffChain->CanBePanned( + mOverscrollHandoffChain->GetApzcAtIndex(0))) { + // If there's absolutely nothing scrollable start a transaction and mark + // this as such to we know to store our EventTime. + mIsScrollable = false; + } else { + // Scrollable, but not in this direction. + EndTransaction(); + } + } +} + +bool WheelBlockState::SetContentResponse(bool aPreventDefault) { + if (aPreventDefault) { + EndTransaction(); + } + return CancelableBlockState::SetContentResponse(aPreventDefault); +} + +bool WheelBlockState::SetConfirmedTargetApzc( + const RefPtr& aTargetApzc, + TargetConfirmationState aState, InputData* aFirstInput, + bool aForScrollbarDrag) { + // The APZC that we find via APZCCallbackHelpers may not be the same APZC + // ESM or OverscrollHandoff would have computed. Make sure we get the right + // one by looking for the first apzc the next pending event can scroll. + RefPtr apzc = aTargetApzc; + if (apzc && aFirstInput) { + apzc = apzc->BuildOverscrollHandoffChain()->FindFirstScrollable( + *aFirstInput, &mAllowedScrollDirections); + } + + InputBlockState::SetConfirmedTargetApzc(apzc, aState, aFirstInput, + aForScrollbarDrag); + return true; +} + +void WheelBlockState::Update(ScrollWheelInput& aEvent) { + // We might not be in a transaction if the block never started in a + // transaction - for example, if nothing was scrollable. + if (!InTransaction()) { + return; + } + + // The current "scroll series" is a like a sub-transaction. It has a separate + // timeout of 80ms. Since we need to compute wheel deltas at different phases + // of a transaction (for example, when it is updated, and later when the + // event action is taken), we affix the scroll series counter to the event. + // This makes GetScrollWheelDelta() consistent. + if (!mLastEventTime.IsNull() && + (aEvent.mTimeStamp - mLastEventTime).ToMilliseconds() > + StaticPrefs::mousewheel_scroll_series_timeout()) { + mScrollSeriesCounter = 0; + } + aEvent.mScrollSeriesNumber = ++mScrollSeriesCounter; + + // If we can't scroll in the direction of the wheel event, we don't update + // the last move time. This allows us to timeout a transaction even if the + // mouse isn't moving. + // + // We skip this check if the target is not yet confirmed, so that when it is + // confirmed, we don't timeout the transaction. + RefPtr apzc = GetTargetApzc(); + if (mIsScrollable && IsTargetConfirmed() && !apzc->CanScroll(aEvent)) { + return; + } + + // Update the time of the last known good event, and reset the mouse move + // time to null. This will reset the delays on both the general transaction + // timeout and the mouse-move-in-frame timeout. + mLastEventTime = aEvent.mTimeStamp; + mLastMouseMove = TimeStamp(); +} + +bool WheelBlockState::MustStayActive() { return !mTransactionEnded; } + +const char* WheelBlockState::Type() { return "scroll wheel"; } + +bool WheelBlockState::ShouldAcceptNewEvent() const { + if (!InTransaction()) { + // If we're not in a transaction, start a new one. + return false; + } + + RefPtr apzc = GetTargetApzc(); + if (apzc->IsDestroyed()) { + return false; + } + + return true; +} + +bool WheelBlockState::MaybeTimeout(const ScrollWheelInput& aEvent) { + MOZ_ASSERT(InTransaction()); + + if (MaybeTimeout(aEvent.mTimeStamp)) { + return true; + } + + if (!mLastMouseMove.IsNull()) { + // If there's a recent mouse movement, we can time out the transaction + // early. + TimeDuration duration = TimeStamp::Now() - mLastMouseMove; + if (duration.ToMilliseconds() >= + StaticPrefs::mousewheel_transaction_ignoremovedelay()) { + TBS_LOG("%p wheel transaction timed out after mouse move\n", this); + EndTransaction(); + return true; + } + } + + return false; +} + +bool WheelBlockState::MaybeTimeout(const TimeStamp& aTimeStamp) { + MOZ_ASSERT(InTransaction()); + + // End the transaction if the event occurred > 1.5s after the most recently + // seen wheel event. + TimeDuration duration = aTimeStamp - mLastEventTime; + if (duration.ToMilliseconds() < + StaticPrefs::mousewheel_transaction_timeout()) { + return false; + } + + TBS_LOG("%p wheel transaction timed out\n", this); + + if (StaticPrefs::test_mousescroll()) { + RefPtr apzc = GetTargetApzc(); + apzc->NotifyMozMouseScrollEvent(u"MozMouseScrollTransactionTimeout"_ns); + } + + EndTransaction(); + return true; +} + +void WheelBlockState::OnMouseMove( + const ScreenIntPoint& aPoint, + const Maybe& aTargetGuid) { + MOZ_ASSERT(InTransaction()); + + if (!GetTargetApzc()->Contains(aPoint) || + // If the mouse moved over to a different APZC, `mIsScrollable` + // may no longer be false and needs to be recomputed. + (!mIsScrollable && aTargetGuid.isSome() && + aTargetGuid.value() != GetTargetApzc()->GetGuid())) { + EndTransaction(); + return; + } + + if (mLastMouseMove.IsNull()) { + // If the cursor is moving inside the frame, and it is more than the + // ignoremovedelay time since the last scroll operation, we record + // this as the most recent mouse movement. + TimeStamp now = TimeStamp::Now(); + TimeDuration duration = now - mLastEventTime; + if (duration.ToMilliseconds() >= + StaticPrefs::mousewheel_transaction_ignoremovedelay()) { + mLastMouseMove = now; + } + } +} + +void WheelBlockState::UpdateTargetApzc( + const RefPtr& aTargetApzc) { + InputBlockState::UpdateTargetApzc(aTargetApzc); + + // If we found there was no target apzc, then we end the transaction. + if (!GetTargetApzc()) { + EndTransaction(); + } +} + +bool WheelBlockState::InTransaction() const { + // We consider a wheel block to be in a transaction if it has a confirmed + // target and is the most recent wheel input block to be created. + if (GetBlockId() != sLastWheelBlockId) { + return false; + } + + if (mTransactionEnded) { + return false; + } + + MOZ_ASSERT(GetTargetApzc()); + return true; +} + +bool WheelBlockState::AllowScrollHandoff() const { + // If we're in a wheel transaction, we do not allow overscroll handoff until + // a new event ends the wheel transaction. + return !IsTargetConfirmed() || !InTransaction(); +} + +void WheelBlockState::EndTransaction() { + TBS_LOG("%p ending wheel transaction\n", this); + mTransactionEnded = true; +} + +PanGestureBlockState::PanGestureBlockState( + const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags, const PanGestureInput& aInitialEvent) + : CancelableBlockState(aTargetApzc, aFlags), + mInterrupted(false), + mWaitingForContentResponse(false), + mWaitingForBrowserGestureResponse(false), + mStartedBrowserGesture(false) { + if (aFlags.mTargetConfirmed) { + // Find the nearest APZC in the overscroll handoff chain that is scrollable. + // If we get a content confirmation later that the apzc is different, then + // content should have found a scrollable apzc, so we don't need to handle + // that case. + RefPtr apzc = + mOverscrollHandoffChain->FindFirstScrollable(aInitialEvent, + &mAllowedScrollDirections); + + if (apzc && apzc != GetTargetApzc()) { + UpdateTargetApzc(apzc); + } + } +} + +bool PanGestureBlockState::SetConfirmedTargetApzc( + const RefPtr& aTargetApzc, + TargetConfirmationState aState, InputData* aFirstInput, + bool aForScrollbarDrag) { + // The APZC that we find via APZCCallbackHelpers may not be the same APZC + // ESM or OverscrollHandoff would have computed. Make sure we get the right + // one by looking for the first apzc the next pending event can scroll. + RefPtr apzc = aTargetApzc; + if (apzc && aFirstInput) { + RefPtr scrollableApzc = + apzc->BuildOverscrollHandoffChain()->FindFirstScrollable( + *aFirstInput, &mAllowedScrollDirections); + if (scrollableApzc) { + apzc = scrollableApzc; + } + } + + InputBlockState::SetConfirmedTargetApzc(apzc, aState, aFirstInput, + aForScrollbarDrag); + return true; +} + +bool PanGestureBlockState::MustStayActive() { return !mInterrupted; } + +const char* PanGestureBlockState::Type() { return "pan gesture"; } + +bool PanGestureBlockState::SetContentResponse(bool aPreventDefault) { + if (aPreventDefault) { + TBS_LOG("%p setting interrupted flag\n", this); + mInterrupted = true; + } + bool stateChanged = CancelableBlockState::SetContentResponse(aPreventDefault); + if (mWaitingForContentResponse) { + mWaitingForContentResponse = false; + stateChanged = true; + } + return stateChanged; +} + +bool PanGestureBlockState::IsReadyForHandling() const { + if (!CancelableBlockState::IsReadyForHandling()) { + return false; + } + return !mWaitingForBrowserGestureResponse && + (!mWaitingForContentResponse || IsContentResponseTimerExpired()); +} + +bool PanGestureBlockState::ShouldDropEvents() const { + return CancelableBlockState::ShouldDropEvents() || mStartedBrowserGesture; +} + +bool PanGestureBlockState::TimeoutContentResponse() { + // Reset mWaitingForBrowserGestureResponse here so that we will not wait for + // the response forever. + mWaitingForBrowserGestureResponse = false; + return CancelableBlockState::TimeoutContentResponse(); +} + +bool PanGestureBlockState::AllowScrollHandoff() const { return false; } + +void PanGestureBlockState::SetNeedsToWaitForContentResponse( + bool aWaitForContentResponse) { + mWaitingForContentResponse = aWaitForContentResponse; +} + +void PanGestureBlockState::SetNeedsToWaitForBrowserGestureResponse( + bool aWaitForBrowserGestureResponse) { + mWaitingForBrowserGestureResponse = aWaitForBrowserGestureResponse; +} + +void PanGestureBlockState::SetBrowserGestureResponse( + BrowserGestureResponse aResponse) { + mWaitingForBrowserGestureResponse = false; + mStartedBrowserGesture = bool(aResponse); +} + +PinchGestureBlockState::PinchGestureBlockState( + const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags) + : CancelableBlockState(aTargetApzc, aFlags), + mInterrupted(false), + mWaitingForContentResponse(false) {} + +bool PinchGestureBlockState::MustStayActive() { return true; } + +const char* PinchGestureBlockState::Type() { return "pinch gesture"; } + +bool PinchGestureBlockState::SetContentResponse(bool aPreventDefault) { + if (aPreventDefault) { + TBS_LOG("%p setting interrupted flag\n", this); + mInterrupted = true; + } + bool stateChanged = CancelableBlockState::SetContentResponse(aPreventDefault); + if (mWaitingForContentResponse) { + mWaitingForContentResponse = false; + stateChanged = true; + } + return stateChanged; +} + +bool PinchGestureBlockState::IsReadyForHandling() const { + if (!CancelableBlockState::IsReadyForHandling()) { + return false; + } + return !mWaitingForContentResponse || IsContentResponseTimerExpired(); +} + +void PinchGestureBlockState::SetNeedsToWaitForContentResponse( + bool aWaitForContentResponse) { + mWaitingForContentResponse = aWaitForContentResponse; +} + +TouchBlockState::TouchBlockState( + const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags, TouchCounter& aCounter) + : CancelableBlockState(aTargetApzc, aFlags), + mAllowedTouchBehaviorSet(false), + mDuringFastFling(false), + mSingleTapOccurred(false), + mInSlop(false), + mTouchCounter(aCounter), + mStartTime(GetTargetApzc()->GetFrameTime().Time()) { + TBS_LOG("Creating %p\n", this); +} + +bool TouchBlockState::SetAllowedTouchBehaviors( + const nsTArray& aBehaviors) { + if (mAllowedTouchBehaviorSet) { + return false; + } + TBS_LOG("%p got allowed touch behaviours for %zu points\n", this, + aBehaviors.Length()); + mAllowedTouchBehaviors.AppendElements(aBehaviors); + mAllowedTouchBehaviorSet = true; + return true; +} + +bool TouchBlockState::GetAllowedTouchBehaviors( + nsTArray& aOutBehaviors) const { + if (!mAllowedTouchBehaviorSet) { + return false; + } + aOutBehaviors.AppendElements(mAllowedTouchBehaviors); + return true; +} + +bool TouchBlockState::HasAllowedTouchBehaviors() const { + return mAllowedTouchBehaviorSet; +} + +void TouchBlockState::CopyPropertiesFrom(const TouchBlockState& aOther) { + TBS_LOG("%p copying properties from %p\n", this, &aOther); + MOZ_ASSERT(aOther.mAllowedTouchBehaviorSet || + aOther.IsContentResponseTimerExpired()); + SetAllowedTouchBehaviors(aOther.mAllowedTouchBehaviors); + mTransformToApzc = aOther.mTransformToApzc; +} + +bool TouchBlockState::IsReadyForHandling() const { + if (!CancelableBlockState::IsReadyForHandling()) { + return false; + } + + return mAllowedTouchBehaviorSet || IsContentResponseTimerExpired(); +} + +void TouchBlockState::SetDuringFastFling() { + TBS_LOG("%p setting fast-motion flag\n", this); + mDuringFastFling = true; +} + +bool TouchBlockState::IsDuringFastFling() const { return mDuringFastFling; } + +void TouchBlockState::SetSingleTapOccurred() { + TBS_LOG("%p setting single-tap-occurred flag\n", this); + mSingleTapOccurred = true; +} + +bool TouchBlockState::SingleTapOccurred() const { return mSingleTapOccurred; } + +bool TouchBlockState::MustStayActive() { return true; } + +const char* TouchBlockState::Type() { return "touch"; } + +TimeDuration TouchBlockState::GetTimeSinceBlockStart() const { + return GetTargetApzc()->GetFrameTime().Time() - mStartTime; +} + +void TouchBlockState::DispatchEvent(const InputData& aEvent) const { + MOZ_ASSERT(aEvent.mInputType == MULTITOUCH_INPUT); + mTouchCounter.Update(aEvent.AsMultiTouchInput()); + CancelableBlockState::DispatchEvent(aEvent); +} + +bool TouchBlockState::TouchActionAllowsPinchZoom() const { + // Pointer events specification requires that all touch points allow zoom. + for (auto& behavior : mAllowedTouchBehaviors) { + if (!(behavior & AllowedTouchBehavior::PINCH_ZOOM)) { + return false; + } + } + return true; +} + +bool TouchBlockState::TouchActionAllowsDoubleTapZoom() const { + for (auto& behavior : mAllowedTouchBehaviors) { + if (!(behavior & AllowedTouchBehavior::ANIMATING_ZOOM)) { + return false; + } + } + return true; +} + +bool TouchBlockState::TouchActionAllowsPanningX() const { + if (mAllowedTouchBehaviors.IsEmpty()) { + // Default to allowed + return true; + } + TouchBehaviorFlags flags = mAllowedTouchBehaviors[0]; + return (flags & AllowedTouchBehavior::HORIZONTAL_PAN); +} + +bool TouchBlockState::TouchActionAllowsPanningY() const { + if (mAllowedTouchBehaviors.IsEmpty()) { + // Default to allowed + return true; + } + TouchBehaviorFlags flags = mAllowedTouchBehaviors[0]; + return (flags & AllowedTouchBehavior::VERTICAL_PAN); +} + +bool TouchBlockState::TouchActionAllowsPanningXY() const { + if (mAllowedTouchBehaviors.IsEmpty()) { + // Default to allowed + return true; + } + TouchBehaviorFlags flags = mAllowedTouchBehaviors[0]; + return (flags & AllowedTouchBehavior::HORIZONTAL_PAN) && + (flags & AllowedTouchBehavior::VERTICAL_PAN); +} + +bool TouchBlockState::UpdateSlopState(const MultiTouchInput& aInput, + bool aApzcCanConsumeEvents) { + if (aInput.mType == MultiTouchInput::MULTITOUCH_START) { + // this is by definition the first event in this block. If it's the first + // touch, then we enter a slop state. + mInSlop = (aInput.mTouches.Length() == 1); + if (mInSlop) { + mSlopOrigin = aInput.mTouches[0].mScreenPoint; + TBS_LOG("%p entering slop with origin %s\n", this, + ToString(mSlopOrigin).c_str()); + } + return false; + } + if (mInSlop) { + ScreenCoord threshold = 0; + // If the target was confirmed to null then the threshold doesn't + // matter anyway since the events will never be processed. + if (const RefPtr& apzc = GetTargetApzc()) { + threshold = aApzcCanConsumeEvents ? apzc->GetTouchStartTolerance() + : apzc->GetTouchMoveTolerance(); + } + bool stayInSlop = + (aInput.mType == MultiTouchInput::MULTITOUCH_MOVE) && + (aInput.mTouches.Length() == 1) && + ((aInput.mTouches[0].mScreenPoint - mSlopOrigin).Length() < threshold); + if (!stayInSlop) { + // we're out of the slop zone, and will stay out for the remainder of + // this block + TBS_LOG("%p exiting slop\n", this); + mInSlop = false; + } + } + return mInSlop; +} + +bool TouchBlockState::IsInSlop() const { return mInSlop; } + +Maybe TouchBlockState::GetBestGuessPanDirection( + const MultiTouchInput& aInput) { + if (aInput.mType != MultiTouchInput::MULTITOUCH_MOVE || + aInput.mTouches.Length() != 1) { + return Nothing(); + } + ScreenPoint vector = aInput.mTouches[0].mScreenPoint - mSlopOrigin; + double angle = atan2(vector.y, vector.x); // range [-pi, pi] + angle = fabs(angle); // range [0, pi] + + double angleThreshold = TouchActionAllowsPanningXY() + ? StaticPrefs::apz_axis_lock_lock_angle() + : StaticPrefs::apz_axis_lock_direct_pan_angle(); + if (apz::IsCloseToHorizontal(angle, angleThreshold)) { + return Some(ScrollDirection::eHorizontal); + } + if (apz::IsCloseToVertical(angle, angleThreshold)) { + return Some(ScrollDirection::eVertical); + } + return Nothing(); +} + +uint32_t TouchBlockState::GetActiveTouchCount() const { + return mTouchCounter.GetActiveTouchCount(); +} + +KeyboardBlockState::KeyboardBlockState( + const RefPtr& aTargetApzc) + : InputBlockState(aTargetApzc, TargetConfirmationFlags{true}) {} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/InputBlockState.h b/gfx/layers/apz/src/InputBlockState.h new file mode 100644 index 0000000000..6320eda6d6 --- /dev/null +++ b/gfx/layers/apz/src/InputBlockState.h @@ -0,0 +1,544 @@ +/* -*- 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_InputBlockState_h +#define mozilla_layers_InputBlockState_h + +#include "InputData.h" // for MultiTouchInput +#include "mozilla/RefCounted.h" // for RefCounted +#include "mozilla/RefPtr.h" // for RefPtr +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/gfx/Matrix.h" // for Matrix4x4 +#include "mozilla/layers/APZUtils.h" +#include "mozilla/layers/LayersTypes.h" // for TouchBehaviorFlags +#include "mozilla/layers/AsyncDragMetrics.h" +#include "mozilla/layers/TouchCounter.h" +#include "mozilla/TimeStamp.h" // for TimeStamp +#include "nsTArray.h" // for nsTArray + +namespace mozilla { +namespace layers { + +class AsyncPanZoomController; +class OverscrollHandoffChain; +class CancelableBlockState; +class TouchBlockState; +class WheelBlockState; +class DragBlockState; +class PanGestureBlockState; +class PinchGestureBlockState; +class KeyboardBlockState; +enum class BrowserGestureResponse : bool; + +/** + * A base class that stores state common to various input blocks. + * Note that the InputBlockState constructor acquires the tree lock, so callers + * from inside AsyncPanZoomController should ensure that the APZC lock is not + * held. + */ +class InputBlockState : public RefCounted { + public: + MOZ_DECLARE_REFCOUNTED_TYPENAME(InputBlockState) + + static const uint64_t NO_BLOCK_ID = 0; + + enum class TargetConfirmationState : uint8_t { + eUnconfirmed, + eTimedOut, + eTimedOutAndMainThreadResponded, + eConfirmed + }; + + InputBlockState(const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags); + virtual ~InputBlockState() = default; + + virtual CancelableBlockState* AsCancelableBlock() { return nullptr; } + virtual TouchBlockState* AsTouchBlock() { return nullptr; } + virtual WheelBlockState* AsWheelBlock() { return nullptr; } + virtual DragBlockState* AsDragBlock() { return nullptr; } + virtual PanGestureBlockState* AsPanGestureBlock() { return nullptr; } + virtual PinchGestureBlockState* AsPinchGestureBlock() { return nullptr; } + virtual KeyboardBlockState* AsKeyboardBlock() { return nullptr; } + + virtual bool SetConfirmedTargetApzc( + const RefPtr& aTargetApzc, + TargetConfirmationState aState, InputData* aFirstInput, + bool aForScrollbarDrag); + const RefPtr& GetTargetApzc() const; + const RefPtr& GetOverscrollHandoffChain() const; + uint64_t GetBlockId() const; + + bool IsTargetConfirmed() const; + bool HasReceivedRealConfirmedTarget() const; + + virtual bool ShouldDropEvents() const; + + void SetScrolledApzc(AsyncPanZoomController* aApzc); + AsyncPanZoomController* GetScrolledApzc() const; + bool IsDownchainOfScrolledApzc(AsyncPanZoomController* aApzc) const; + + /** + * Dispatch the event to the target APZC. Mostly this is a hook for + * subclasses to do any per-event processing they need to. + */ + virtual void DispatchEvent(const InputData& aEvent) const; + + /** + * Return true if this input block must stay active if it would otherwise + * be removed as the last item in the pending queue. + */ + virtual bool MustStayActive() = 0; + + protected: + virtual void UpdateTargetApzc( + const RefPtr& aTargetApzc); + + private: + // Checks whether |aA| is an ancestor of |aB| (or the same as |aB|) in + // |mOverscrollHandoffChain|. + bool IsDownchainOf(AsyncPanZoomController* aA, + AsyncPanZoomController* aB) const; + + private: + RefPtr mTargetApzc; + TargetConfirmationState mTargetConfirmed; + bool mRequiresTargetConfirmation; + const uint64_t mBlockId; + + // The APZC that was actually scrolled by events in this input block. + // This is used in configurations where a single input block is only + // allowed to scroll a single APZC (configurations where + // StaticPrefs::apz_allow_immediate_handoff() is false). Set the first time an + // input event in this block scrolls an APZC. + RefPtr mScrolledApzc; + + protected: + RefPtr mOverscrollHandoffChain; + + // Used to transform events from global screen space to |mTargetApzc|'s + // screen space. It's cached at the beginning of the input block so that + // all events in the block are in the same coordinate space. + ScreenToParentLayerMatrix4x4 mTransformToApzc; +}; + +/** + * This class represents a set of events that can be cancelled by web content + * via event listeners. + * + * Each cancelable input block can be cancelled by web content, and + * this information is stored in the mPreventDefault flag. Because web + * content runs on the Gecko main thread, we cannot always wait for web + * content's response. Instead, there is a timeout that sets this flag in the + * case where web content doesn't respond in time. The mContentResponded and + * mContentResponseTimerExpired flags indicate which of these scenarios + * occurred. + */ +class CancelableBlockState : public InputBlockState { + public: + CancelableBlockState(const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags); + + CancelableBlockState* AsCancelableBlock() override { return this; } + + /** + * Record whether or not content cancelled this block of events. + * @param aPreventDefault true iff the block is cancelled. + * @return false if this block has already received a response from + * web content, true if not. + */ + virtual bool SetContentResponse(bool aPreventDefault); + + /** + * Record that content didn't respond in time. + * @return false if this block already timed out, true if not. + */ + virtual bool TimeoutContentResponse(); + + /** + * Checks if the content response timer has already expired. + */ + bool IsContentResponseTimerExpired() const; + + /** + * @return true iff web content cancelled this block of events. + */ + bool IsDefaultPrevented() const; + + /** + * @return true iff this block has received all the information needed + * to properly dispatch the events in the block. + */ + virtual bool IsReadyForHandling() const; + + /** + * Return a descriptive name for the block kind. + */ + virtual const char* Type() = 0; + + bool ShouldDropEvents() const override; + + private: + bool mPreventDefault; + bool mContentResponded; + bool mContentResponseTimerExpired; +}; + +/** + * A single block of wheel events. + */ +class WheelBlockState : public CancelableBlockState { + public: + WheelBlockState(const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags, + const ScrollWheelInput& aEvent); + + bool SetContentResponse(bool aPreventDefault) override; + bool MustStayActive() override; + const char* Type() override; + bool SetConfirmedTargetApzc(const RefPtr& aTargetApzc, + TargetConfirmationState aState, + InputData* aFirstInput, + bool aForScrollbarDrag) override; + + WheelBlockState* AsWheelBlock() override { return this; } + + /** + * Determine whether this wheel block is accepting new events. + */ + bool ShouldAcceptNewEvent() const; + + /** + * Call to check whether a wheel event will cause the current transaction to + * timeout. + */ + bool MaybeTimeout(const ScrollWheelInput& aEvent); + + /** + * Called from APZCTM when a mouse move or drag+drop event occurs, before + * the event has been processed. + */ + void OnMouseMove(const ScreenIntPoint& aPoint, + const Maybe& aTargetGuid); + + /** + * Returns whether or not the block is participating in a wheel transaction. + * This means that the block is the most recent input block to be created, + * and no events have occurred that would require scrolling a different + * frame. + * + * @return True if in a transaction, false otherwise. + */ + bool InTransaction() const; + + /** + * Mark the block as no longer participating in a wheel transaction. This + * will force future wheel events to begin a new input block. + */ + void EndTransaction(); + + /** + * @return Whether or not overscrolling is prevented for this wheel block. + */ + bool AllowScrollHandoff() const; + + /** + * Called to check and possibly end the transaction due to a timeout. + * + * @return True if the transaction ended, false otherwise. + */ + bool MaybeTimeout(const TimeStamp& aTimeStamp); + + /** + * Update the wheel transaction state for a new event. + */ + void Update(ScrollWheelInput& aEvent); + + ScrollDirections GetAllowedScrollDirections() const { + return mAllowedScrollDirections; + } + + protected: + void UpdateTargetApzc( + const RefPtr& aTargetApzc) override; + + private: + TimeStamp mLastEventTime; + TimeStamp mLastMouseMove; + uint32_t mScrollSeriesCounter; + bool mTransactionEnded; + bool mIsScrollable = true; + ScrollDirections mAllowedScrollDirections; +}; + +/** + * A block of mouse events that are part of a drag + */ +class DragBlockState : public CancelableBlockState { + public: + DragBlockState(const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags, const MouseInput& aEvent); + + bool MustStayActive() override; + const char* Type() override; + + bool HasReceivedMouseUp(); + void MarkMouseUpReceived(); + + DragBlockState* AsDragBlock() override { return this; } + + void SetInitialThumbPos(OuterCSSCoord aThumbPos); + void SetDragMetrics(const AsyncDragMetrics& aDragMetrics); + + void DispatchEvent(const InputData& aEvent) const override; + + private: + AsyncDragMetrics mDragMetrics; + OuterCSSCoord mInitialThumbPos; + bool mReceivedMouseUp; +}; + +/** + * A single block of pan gesture events. + */ +class PanGestureBlockState : public CancelableBlockState { + public: + PanGestureBlockState(const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags, + const PanGestureInput& aEvent); + + bool SetContentResponse(bool aPreventDefault) override; + bool IsReadyForHandling() const override; + bool MustStayActive() override; + const char* Type() override; + bool SetConfirmedTargetApzc(const RefPtr& aTargetApzc, + TargetConfirmationState aState, + InputData* aFirstInput, + bool aForScrollbarDrag) override; + + PanGestureBlockState* AsPanGestureBlock() override { return this; } + + bool ShouldDropEvents() const override; + + bool TimeoutContentResponse() override; + + /** + * @return Whether or not overscrolling is prevented for this block. + */ + bool AllowScrollHandoff() const; + + bool WasInterrupted() const { return mInterrupted; } + + void SetNeedsToWaitForContentResponse(bool aWaitForContentResponse); + void SetNeedsToWaitForBrowserGestureResponse( + bool aWaitForBrowserGestureResponse); + void SetBrowserGestureResponse(BrowserGestureResponse aResponse); + + ScrollDirections GetAllowedScrollDirections() const { + return mAllowedScrollDirections; + } + + private: + bool mInterrupted; + bool mWaitingForContentResponse; + // A pan gesture may be used for browser's swipe gestures so APZ needs to wait + // for the response from the browser whether the gesture has been used for + // swipe or not. This `mWaitingForBrowserGestureResponse` flag represents the + // waiting state. And below `mStartedBrowserGesture` represents the response + // from the browser. + bool mWaitingForBrowserGestureResponse; + bool mStartedBrowserGesture; + ScrollDirections mAllowedScrollDirections; +}; + +/** + * A single block of pinch gesture events. + */ +class PinchGestureBlockState : public CancelableBlockState { + public: + PinchGestureBlockState(const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags); + + bool SetContentResponse(bool aPreventDefault) override; + bool IsReadyForHandling() const override; + bool MustStayActive() override; + const char* Type() override; + + PinchGestureBlockState* AsPinchGestureBlock() override { return this; } + + bool WasInterrupted() const { return mInterrupted; } + + void SetNeedsToWaitForContentResponse(bool aWaitForContentResponse); + + private: + bool mInterrupted; + bool mWaitingForContentResponse; +}; + +/** + * This class represents a single touch block. A touch block is + * a set of touch events that can be cancelled by web content via + * touch event listeners. + * + * Every touch-start event creates a new touch block. In this case, the + * touch block consists of the touch-start, followed by all touch events + * up to but not including the next touch-start (except in the case where + * a long-tap happens, see below). Note that in particular we cannot know + * when a touch block ends until the next one is started. Most touch + * blocks are created by receipt of a touch-start event. + * + * Every long-tap event also creates a new touch block, since it can also + * be consumed by web content. In this case, when the long-tap event is + * dispatched to web content, a new touch block is started to hold the remaining + * touch events, up to but not including the next touch start (or long-tap). + * + * Additionally, if touch-action is enabled, each touch block should + * have a set of allowed touch behavior flags; one for each touch point. + * This also requires running code on the Gecko main thread, and so may + * be populated with some latency. The mAllowedTouchBehaviorSet and + * mAllowedTouchBehaviors variables track this information. + */ +class TouchBlockState : public CancelableBlockState { + public: + explicit TouchBlockState(const RefPtr& aTargetApzc, + TargetConfirmationFlags aFlags, + TouchCounter& aTouchCounter); + + TouchBlockState* AsTouchBlock() override { return this; } + + /** + * Set the allowed touch behavior flags for this block. + * @return false if this block already has these flags set, true if not. + */ + bool SetAllowedTouchBehaviors(const nsTArray& aBehaviors); + /** + * If the allowed touch behaviors have been set, populate them into + * |aOutBehaviors| and return true. Else, return false. + */ + bool GetAllowedTouchBehaviors( + nsTArray& aOutBehaviors) const; + + /** + * Returns true if the allowed touch behaviours have been set, or if touch + * action is disabled. + */ + bool HasAllowedTouchBehaviors() const; + + /** + * Copy various properties from another block. + */ + void CopyPropertiesFrom(const TouchBlockState& aOther); + + /** + * @return true iff this block has received all the information needed + * to properly dispatch the events in the block. + */ + bool IsReadyForHandling() const override; + + /** + * Sets a flag that indicates this input block occurred while the APZ was + * in a state of fast flinging. This affects gestures that may be produced + * from input events in this block. + */ + void SetDuringFastFling(); + /** + * @return true iff SetDuringFastFling was called on this block. + */ + bool IsDuringFastFling() const; + /** + * Set the single-tap-occurred flag that indicates that this touch block + * triggered a single tap event. + */ + void SetSingleTapOccurred(); + /** + * @return true iff the single-tap-occurred flag is set on this block. + */ + bool SingleTapOccurred() const; + + /** + * @return false iff touch-action is enabled and the allowed touch behaviors + * for this touch block do not allow pinch-zooming. + */ + bool TouchActionAllowsPinchZoom() const; + /** + * @return false iff touch-action is enabled and the allowed touch behaviors + * for this touch block do not allow double-tap zooming. + */ + bool TouchActionAllowsDoubleTapZoom() const; + /** + * @return false iff touch-action is enabled and the allowed touch behaviors + * for the first touch point do not allow panning in the specified + * direction(s). + */ + bool TouchActionAllowsPanningX() const; + bool TouchActionAllowsPanningY() const; + bool TouchActionAllowsPanningXY() const; + + /** + * Notifies the input block of an incoming touch event so that the block can + * update its internal slop state. "Slop" refers to the area around the + * initial touchstart where we drop touchmove events so that content doesn't + * see them. The |aApzcCanConsumeEvents| parameter is factored into how large + * the slop area is - if this is true the slop area is larger. + * @return true iff the provided event is a touchmove in the slop area and + * so should not be sent to content. + */ + bool UpdateSlopState(const MultiTouchInput& aInput, + bool aApzcCanConsumeEvents); + bool IsInSlop() const; + + /** + * Based on the slop origin and the given input event, return a best guess + * as to the pan direction of this touch block. Returns Nothing() if no guess + * can be made. + */ + Maybe GetBestGuessPanDirection( + const MultiTouchInput& aInput); + + /** + * Returns the number of touch points currently active. + */ + uint32_t GetActiveTouchCount() const; + + void DispatchEvent(const InputData& aEvent) const override; + bool MustStayActive() override; + const char* Type() override; + TimeDuration GetTimeSinceBlockStart() const; + + private: + nsTArray mAllowedTouchBehaviors; + bool mAllowedTouchBehaviorSet; + bool mDuringFastFling; + bool mSingleTapOccurred; + bool mInSlop; + ScreenIntPoint mSlopOrigin; + // A reference to the InputQueue's touch counter + TouchCounter& mTouchCounter; + TimeStamp mStartTime; +}; + +/** + * This class represents a set of keyboard inputs targeted at the same Apzc. + */ +class KeyboardBlockState : public InputBlockState { + public: + explicit KeyboardBlockState( + const RefPtr& aTargetApzc); + + KeyboardBlockState* AsKeyboardBlock() override { return this; } + + bool MustStayActive() override { return false; } + + /** + * @return Whether or not overscrolling is prevented for this keyboard block. + */ + bool AllowScrollHandoff() const { return false; } +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_InputBlockState_h diff --git a/gfx/layers/apz/src/InputQueue.cpp b/gfx/layers/apz/src/InputQueue.cpp new file mode 100644 index 0000000000..b6c7a05830 --- /dev/null +++ b/gfx/layers/apz/src/InputQueue.cpp @@ -0,0 +1,1090 @@ +/* -*- 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 "InputQueue.h" + +#include "AsyncPanZoomController.h" + +#include "GestureEventListener.h" +#include "InputBlockState.h" +#include "mozilla/EventForwards.h" +#include "mozilla/layers/APZInputBridge.h" +#include "mozilla/layers/APZThreadUtils.h" +#include "mozilla/ToString.h" +#include "OverscrollHandoffState.h" +#include "QueuedInput.h" +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_ui.h" + +static mozilla::LazyLogModule sApzInpLog("apz.inputqueue"); +#define INPQ_LOG(...) MOZ_LOG(sApzInpLog, LogLevel::Debug, (__VA_ARGS__)) + +namespace mozilla { +namespace layers { + +InputQueue::InputQueue() = default; + +InputQueue::~InputQueue() { mQueuedInputs.Clear(); } + +APZEventResult InputQueue::ReceiveInputEvent( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, InputData& aEvent, + const Maybe>& aTouchBehaviors) { + APZThreadUtils::AssertOnControllerThread(); + + AutoRunImmediateTimeout timeoutRunner{this}; + + switch (aEvent.mInputType) { + case MULTITOUCH_INPUT: { + const MultiTouchInput& event = aEvent.AsMultiTouchInput(); + return ReceiveTouchInput(aTarget, aFlags, event, aTouchBehaviors); + } + + case SCROLLWHEEL_INPUT: { + const ScrollWheelInput& event = aEvent.AsScrollWheelInput(); + return ReceiveScrollWheelInput(aTarget, aFlags, event); + } + + case PANGESTURE_INPUT: { + const PanGestureInput& event = aEvent.AsPanGestureInput(); + return ReceivePanGestureInput(aTarget, aFlags, event); + } + + case PINCHGESTURE_INPUT: { + const PinchGestureInput& event = aEvent.AsPinchGestureInput(); + return ReceivePinchGestureInput(aTarget, aFlags, event); + } + + case MOUSE_INPUT: { + MouseInput& event = aEvent.AsMouseInput(); + return ReceiveMouseInput(aTarget, aFlags, event); + } + + case KEYBOARD_INPUT: { + // Every keyboard input must have a confirmed target + MOZ_ASSERT(aTarget && aFlags.mTargetConfirmed); + + const KeyboardInput& event = aEvent.AsKeyboardInput(); + return ReceiveKeyboardInput(aTarget, aFlags, event); + } + + default: { + // The `mStatus` for other input type is only used by tests, so just + // pass through the return value of HandleInputEvent() for now. + APZEventResult result(aTarget, aFlags); + nsEventStatus status = + aTarget->HandleInputEvent(aEvent, aTarget->GetTransformToThis()); + switch (status) { + case nsEventStatus_eIgnore: + result.SetStatusAsIgnore(); + break; + case nsEventStatus_eConsumeNoDefault: + result.SetStatusAsConsumeNoDefault(); + break; + case nsEventStatus_eConsumeDoDefault: + result.SetStatusAsConsumeDoDefault(aTarget); + break; + default: + MOZ_ASSERT_UNREACHABLE("An invalid status"); + break; + } + return result; + } + } +} + +APZEventResult InputQueue::ReceiveTouchInput( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, const MultiTouchInput& aEvent, + const Maybe>& aTouchBehaviors) { + APZEventResult result(aTarget, aFlags); + + RefPtr block; + bool waitingForContentResponse = false; + if (aEvent.mType == MultiTouchInput::MULTITOUCH_START) { + nsTArray currentBehaviors; + bool haveBehaviors = false; + if (mActiveTouchBlock) { + haveBehaviors = + mActiveTouchBlock->GetAllowedTouchBehaviors(currentBehaviors); + // If the behaviours aren't set, but the main-thread response timer on + // the block is expired we still treat it as though it has behaviors, + // because in that case we still want to interrupt the fast-fling and + // use the default behaviours. + haveBehaviors |= mActiveTouchBlock->IsContentResponseTimerExpired(); + } + + block = StartNewTouchBlock(aTarget, aFlags, false); + INPQ_LOG("started new touch block %p id %" PRIu64 " for target %p\n", + block.get(), block->GetBlockId(), aTarget.get()); + + // XXX using the chain from |block| here may be wrong in cases where the + // target isn't confirmed and the real target turns out to be something + // else. For now assume this is rare enough that it's not an issue. + if (mQueuedInputs.IsEmpty() && aEvent.mTouches.Length() == 1 && + block->GetOverscrollHandoffChain()->HasFastFlungApzc() && + haveBehaviors) { + // If we're already in a fast fling, and a single finger goes down, then + // we want special handling for the touch event, because it shouldn't get + // delivered to content. Note that we don't set this flag when going + // from a fast fling to a pinch state (i.e. second finger goes down while + // the first finger is moving). + block->SetDuringFastFling(); + block->SetConfirmedTargetApzc( + aTarget, InputBlockState::TargetConfirmationState::eConfirmed, + nullptr /* the block was just created so it has no events */, + false /* not a scrollbar drag */); + block->SetAllowedTouchBehaviors(currentBehaviors); + INPQ_LOG("block %p tagged as fast-motion\n", block.get()); + } else if (aTouchBehaviors) { + // If this block isn't started during a fast-fling, and APZCTM has + // provided touch behavior information, then put it on the block so + // that the ArePointerEventsConsumable call below can use it. + block->SetAllowedTouchBehaviors(*aTouchBehaviors); + } + + CancelAnimationsForNewBlock(block); + + waitingForContentResponse = MaybeRequestContentResponse(aTarget, block); + } else { + // for touch inputs that don't start a block, APZCTM shouldn't be giving + // us any touch behaviors. + MOZ_ASSERT(aTouchBehaviors.isNothing()); + + block = mActiveTouchBlock.get(); + if (!block) { + NS_WARNING( + "Received a non-start touch event while no touch blocks active!"); + return result; + } + + INPQ_LOG("received new touch event (type=%d) in block %p\n", aEvent.mType, + block.get()); + } + + result.mInputBlockId = block->GetBlockId(); + + // Note that the |aTarget| the APZCTM sent us may contradict the confirmed + // target set on the block. In this case the confirmed target (which may be + // null) should take priority. This is equivalent to just always using the + // target (confirmed or not) from the block. + RefPtr target = block->GetTargetApzc(); + + // XXX calling ArePointerEventsConsumable on |target| may be wrong here if + // the target isn't confirmed and the real target turns out to be something + // else. For now assume this is rare enough that it's not an issue. + PointerEventsConsumableFlags consumableFlags; + if (target) { + consumableFlags = target->ArePointerEventsConsumable(block, aEvent); + } + if (block->IsDuringFastFling()) { + INPQ_LOG("dropping event due to block %p being in fast motion\n", + block.get()); + result.SetStatusForFastFling(*block, aFlags, consumableFlags, target); + } else { // handling depends on ArePointerEventsConsumable() + bool consumable = consumableFlags.IsConsumable(); + if (block->UpdateSlopState(aEvent, consumable)) { + INPQ_LOG("dropping event due to block %p being in %sslop\n", block.get(), + consumable ? "" : "mini-"); + result.SetStatusAsConsumeNoDefault(); + } else { + result.SetStatusForTouchEvent(*block, aFlags, consumableFlags, target); + } + } + mQueuedInputs.AppendElement(MakeUnique(aEvent, *block)); + ProcessQueue(); + + // If this block just started and is waiting for a content response, but + // also in a slop state (i.e. touchstart gets delivered to content but + // not any touchmoves), then we might end up in a situation where we don't + // get the content response until the timeout is hit because we never exit + // the slop state. But if that timeout is longer than the long-press timeout, + // then the long-press gets delayed too. Avoid that by scheduling a callback + // with the long-press timeout that will force the block to get processed. + int32_t longTapTimeout = StaticPrefs::ui_click_hold_context_menus_delay(); + int32_t contentTimeout = StaticPrefs::apz_content_response_timeout(); + if (waitingForContentResponse && longTapTimeout < contentTimeout && + block->IsInSlop() && GestureEventListener::IsLongTapEnabled()) { + MOZ_ASSERT(aEvent.mType == MultiTouchInput::MULTITOUCH_START); + MOZ_ASSERT(!block->IsDuringFastFling()); + RefPtr maybeLongTap = NewRunnableMethod( + "layers::InputQueue::MaybeLongTapTimeout", this, + &InputQueue::MaybeLongTapTimeout, block->GetBlockId()); + INPQ_LOG("scheduling maybe-long-tap timeout for target %p\n", + aTarget.get()); + aTarget->PostDelayedTask(maybeLongTap.forget(), longTapTimeout); + } + + return result; +} + +APZEventResult InputQueue::ReceiveMouseInput( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, MouseInput& aEvent) { + APZEventResult result(aTarget, aFlags); + + // On a new mouse down we can have a new target so we must force a new block + // with a new target. + bool newBlock = DragTracker::StartsDrag(aEvent); + + RefPtr block = newBlock ? nullptr : mActiveDragBlock.get(); + if (block && block->HasReceivedMouseUp()) { + block = nullptr; + } + + if (!block && mDragTracker.InDrag()) { + // If there's no current drag block, but we're getting a move with a button + // down, we need to start a new drag block because we're obviously already + // in the middle of a drag (it probably got interrupted by something else). + INPQ_LOG( + "got a drag event outside a drag block, need to create a block to hold " + "it\n"); + newBlock = true; + } + + mDragTracker.Update(aEvent); + + if (!newBlock && !block) { + // This input event is not in a drag block, so we're not doing anything + // with it, return eIgnore. + return result; + } + + if (!block) { + MOZ_ASSERT(newBlock); + block = new DragBlockState(aTarget, aFlags, aEvent); + + INPQ_LOG( + "started new drag block %p id %" PRIu64 + "for %sconfirmed target %p; on scrollbar: %d; on scrollthumb: %d\n", + block.get(), block->GetBlockId(), aFlags.mTargetConfirmed ? "" : "un", + aTarget.get(), aFlags.mHitScrollbar, aFlags.mHitScrollThumb); + + mActiveDragBlock = block; + + if (aFlags.mHitScrollThumb || !aFlags.mHitScrollbar) { + // If we're running autoscroll, we'll always cancel it during the + // following call of CancelAnimationsForNewBlock. At this time, + // we don't want to fire `click` event on the web content for web-compat + // with Chrome. Therefore, we notify widget of it with the flag. + if ((aEvent.mType == MouseInput::MOUSE_DOWN || + aEvent.mType == MouseInput::MOUSE_UP) && + block->GetOverscrollHandoffChain()->HasAutoscrollApzc()) { + aEvent.mPreventClickEvent = true; + } + CancelAnimationsForNewBlock(block); + } + MaybeRequestContentResponse(aTarget, block); + } + + result.mInputBlockId = block->GetBlockId(); + + mQueuedInputs.AppendElement(MakeUnique(aEvent, *block)); + ProcessQueue(); + + if (DragTracker::EndsDrag(aEvent)) { + block->MarkMouseUpReceived(); + } + + // The event is part of a drag block and could potentially cause + // scrolling, so return DoDefault. + result.SetStatusAsConsumeDoDefault(*block); + return result; +} + +APZEventResult InputQueue::ReceiveScrollWheelInput( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, const ScrollWheelInput& aEvent) { + APZEventResult result(aTarget, aFlags); + + RefPtr block = mActiveWheelBlock.get(); + // If the block is not accepting new events we'll create a new input block + // (and therefore a new wheel transaction). + if (block && + (!block->ShouldAcceptNewEvent() || block->MaybeTimeout(aEvent))) { + block = nullptr; + } + + MOZ_ASSERT(!block || block->InTransaction()); + + if (!block) { + block = new WheelBlockState(aTarget, aFlags, aEvent); + INPQ_LOG("started new scroll wheel block %p id %" PRIu64 + " for %starget %p\n", + block.get(), block->GetBlockId(), + aFlags.mTargetConfirmed ? "confirmed " : "", aTarget.get()); + + mActiveWheelBlock = block; + + CancelAnimationsForNewBlock(block, ExcludeWheel); + MaybeRequestContentResponse(aTarget, block); + } else { + INPQ_LOG("received new wheel event in block %p\n", block.get()); + } + + result.mInputBlockId = block->GetBlockId(); + + // Note that the |aTarget| the APZCTM sent us may contradict the confirmed + // target set on the block. In this case the confirmed target (which may be + // null) should take priority. This is equivalent to just always using the + // target (confirmed or not) from the block, which is what + // ProcessQueue() does. + mQueuedInputs.AppendElement(MakeUnique(aEvent, *block)); + + // The WheelBlockState needs to affix a counter to the event before we process + // it. Note that the counter is affixed to the copy in the queue rather than + // |aEvent|. + block->Update(mQueuedInputs.LastElement()->Input()->AsScrollWheelInput()); + + ProcessQueue(); + + result.SetStatusAsConsumeDoDefault(*block); + return result; +} + +APZEventResult InputQueue::ReceiveKeyboardInput( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, const KeyboardInput& aEvent) { + APZEventResult result(aTarget, aFlags); + + RefPtr block = mActiveKeyboardBlock.get(); + + // If the block is targeting a different Apzc than this keyboard event then + // we'll create a new input block + if (block && block->GetTargetApzc() != aTarget) { + block = nullptr; + } + + if (!block) { + block = new KeyboardBlockState(aTarget); + INPQ_LOG("started new keyboard block %p id %" PRIu64 " for target %p\n", + block.get(), block->GetBlockId(), aTarget.get()); + + mActiveKeyboardBlock = block; + } else { + INPQ_LOG("received new keyboard event in block %p\n", block.get()); + } + + result.mInputBlockId = block->GetBlockId(); + + mQueuedInputs.AppendElement(MakeUnique(aEvent, *block)); + + ProcessQueue(); + + // If APZ is allowing passive listeners then we must dispatch the event to + // content, otherwise we can consume the event. + if (StaticPrefs::apz_keyboard_passive_listeners()) { + result.SetStatusAsConsumeDoDefault(*block); + } else { + result.SetStatusAsConsumeNoDefault(); + } + return result; +} + +static bool CanScrollTargetHorizontally(const PanGestureInput& aInitialEvent, + PanGestureBlockState* aBlock) { + PanGestureInput horizontalComponent = aInitialEvent; + horizontalComponent.mPanDisplacement.y = 0; + ScrollDirections allowedScrollDirections; + RefPtr horizontallyScrollableAPZC = + aBlock->GetOverscrollHandoffChain()->FindFirstScrollable( + horizontalComponent, &allowedScrollDirections, + OverscrollHandoffChain::IncludeOverscroll::No); + return horizontallyScrollableAPZC && + horizontallyScrollableAPZC == aBlock->GetTargetApzc() && + allowedScrollDirections.contains(ScrollDirection::eHorizontal); +} + +APZEventResult InputQueue::ReceivePanGestureInput( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, const PanGestureInput& aEvent) { + APZEventResult result(aTarget, aFlags); + + if (aEvent.mType == PanGestureInput::PANGESTURE_MAYSTART || + aEvent.mType == PanGestureInput::PANGESTURE_CANCELLED) { + // Ignore these events for now. + result.SetStatusAsConsumeDoDefault(aTarget); + return result; + } + + if (aEvent.mType == PanGestureInput::PANGESTURE_INTERRUPTED) { + if (RefPtr block = mActivePanGestureBlock.get()) { + mQueuedInputs.AppendElement(MakeUnique(aEvent, *block)); + ProcessQueue(); + } + result.SetStatusAsIgnore(); + return result; + } + + RefPtr block; + if (aEvent.mType != PanGestureInput::PANGESTURE_START) { + block = mActivePanGestureBlock.get(); + } + + PanGestureInput event = aEvent; + + // Below `SetStatusAsConsumeDoDefault()` preserves `mHandledResult` of + // `result` which was set in the ctor of APZEventResult at the top of this + // function based on `aFlag` so that the `mHandledResult` value is reliable to + // tell whether the event will be handled by the root content APZC at least + // for swipe-navigation stuff. E.g. if a pan-start event scrolled the root + // scroll container, we don't need to anything for swipe-navigation. + result.SetStatusAsConsumeDoDefault(); + + if (!block || block->WasInterrupted()) { + if (event.mType == PanGestureInput::PANGESTURE_MOMENTUMSTART || + event.mType == PanGestureInput::PANGESTURE_MOMENTUMPAN || + event.mType == PanGestureInput::PANGESTURE_MOMENTUMEND) { + // If there are momentum events after an interruption, discard them. + // However, if there is a non-momentum event (indicating the user + // continued scrolling on the touchpad), a new input block is started + // by turning the event into a pan-start below. + return result; + } + if (event.mType != PanGestureInput::PANGESTURE_START) { + // Only PANGESTURE_START events are allowed to start a new pan gesture + // block, but we really want to start a new block here, so we magically + // turn this input into a PANGESTURE_START. + INPQ_LOG( + "transmogrifying pan input %d to PANGESTURE_START for new block\n", + event.mType); + event.mType = PanGestureInput::PANGESTURE_START; + } + block = new PanGestureBlockState(aTarget, aFlags, event); + INPQ_LOG("started new pan gesture block %p id %" PRIu64 " for target %p\n", + block.get(), block->GetBlockId(), aTarget.get()); + + mActivePanGestureBlock = block; + + CancelAnimationsForNewBlock(block); + const bool waitingForContentResponse = + MaybeRequestContentResponse(aTarget, block); + + if (event.AllowsSwipe() && !CanScrollTargetHorizontally(event, block)) { + // We will ask the browser whether this pan event is going to be used for + // swipe or not, so we need to wait the response. + block->SetNeedsToWaitForBrowserGestureResponse(true); + if (!waitingForContentResponse) { + ScheduleMainThreadTimeout(aTarget, block); + } + if (aFlags.mTargetConfirmed) { + // This event may trigger a swipe gesture, depending on what our caller + // wants to do it. We need to suspend handling of this block until we + // get a content response which will tell us whether to proceed or abort + // the block. + block->SetNeedsToWaitForContentResponse(true); + + // Inform our caller that we haven't scrolled in response to the event + // and that a swipe can be started from this event if desired. + result.SetStatusAsIgnore(); + } + } + } else { + INPQ_LOG("received new pan event (type=%d) in block %p\n", aEvent.mType, + block.get()); + } + + result.mInputBlockId = block->GetBlockId(); + + // Note that the |aTarget| the APZCTM sent us may contradict the confirmed + // target set on the block. In this case the confirmed target (which may be + // null) should take priority. This is equivalent to just always using the + // target (confirmed or not) from the block, which is what + // ProcessQueue() does. + mQueuedInputs.AppendElement(MakeUnique(event, *block)); + ProcessQueue(); + + return result; +} + +APZEventResult InputQueue::ReceivePinchGestureInput( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, const PinchGestureInput& aEvent) { + APZEventResult result(aTarget, aFlags); + + RefPtr block; + if (aEvent.mType != PinchGestureInput::PINCHGESTURE_START) { + block = mActivePinchGestureBlock.get(); + } + + result.SetStatusAsConsumeDoDefault(aTarget); + + if (!block || block->WasInterrupted()) { + if (aEvent.mType != PinchGestureInput::PINCHGESTURE_START) { + // Only PINCHGESTURE_START events are allowed to start a new pinch gesture + // block. + INPQ_LOG("pinchgesture block %p was interrupted %d\n", block.get(), + block ? block->WasInterrupted() : 0); + return result; + } + block = new PinchGestureBlockState(aTarget, aFlags); + INPQ_LOG("started new pinch gesture block %p id %" PRIu64 + " for target %p\n", + block.get(), block->GetBlockId(), aTarget.get()); + + mActivePinchGestureBlock = block; + block->SetNeedsToWaitForContentResponse(true); + + CancelAnimationsForNewBlock(block); + MaybeRequestContentResponse(aTarget, block); + } else { + INPQ_LOG("received new pinch event (type=%d) in block %p\n", aEvent.mType, + block.get()); + } + + result.mInputBlockId = block->GetBlockId(); + + // Note that the |aTarget| the APZCTM sent us may contradict the confirmed + // target set on the block. In this case the confirmed target (which may be + // null) should take priority. This is equivalent to just always using the + // target (confirmed or not) from the block, which is what + // ProcessQueue() does. + mQueuedInputs.AppendElement(MakeUnique(aEvent, *block)); + ProcessQueue(); + + return result; +} + +void InputQueue::CancelAnimationsForNewBlock(InputBlockState* aBlock, + CancelAnimationFlags aExtraFlags) { + // We want to cancel animations here as soon as possible (i.e. without waiting + // for content responses) because a finger has gone down and we don't want to + // keep moving the content under the finger. However, to prevent "future" + // touchstart events from interfering with "past" animations (i.e. from a + // previous touch block that is still being processed) we only do this + // animation-cancellation if there are no older touch blocks still in the + // queue. + if (mQueuedInputs.IsEmpty()) { + aBlock->GetOverscrollHandoffChain()->CancelAnimations( + aExtraFlags | ExcludeOverscroll | ScrollSnap); + } +} + +bool InputQueue::MaybeRequestContentResponse( + const RefPtr& aTarget, + CancelableBlockState* aBlock) { + bool waitForMainThread = false; + if (aBlock->IsTargetConfirmed()) { + // Content won't prevent-default this, so we can just set the flag directly. + INPQ_LOG("not waiting for content response on block %p\n", aBlock); + aBlock->SetContentResponse(false); + } else { + waitForMainThread = true; + } + if (aBlock->AsTouchBlock() && + !aBlock->AsTouchBlock()->HasAllowedTouchBehaviors()) { + INPQ_LOG("waiting for main thread touch-action info on block %p\n", aBlock); + waitForMainThread = true; + } + if (waitForMainThread) { + // We either don't know for sure if aTarget is the right APZC, or we may + // need to wait to give content the opportunity to prevent-default the + // touch events. Either way we schedule a timeout so the main thread stuff + // can run. + ScheduleMainThreadTimeout(aTarget, aBlock); + } + return waitForMainThread; +} + +uint64_t InputQueue::InjectNewTouchBlock(AsyncPanZoomController* aTarget) { + AutoRunImmediateTimeout timeoutRunner{this}; + TouchBlockState* block = + StartNewTouchBlock(aTarget, TargetConfirmationFlags{true}, + /* aCopyPropertiesFromCurrent = */ true); + INPQ_LOG("injecting new touch block %p with id %" PRIu64 " and target %p\n", + block, block->GetBlockId(), aTarget); + ScheduleMainThreadTimeout(aTarget, block); + return block->GetBlockId(); +} + +TouchBlockState* InputQueue::StartNewTouchBlock( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, bool aCopyPropertiesFromCurrent) { + TouchBlockState* newBlock = + new TouchBlockState(aTarget, aFlags, mTouchCounter); + if (aCopyPropertiesFromCurrent) { + // We should never enter here without a current touch block, because this + // codepath is invoked from the OnLongPress handler in + // AsyncPanZoomController, which should bail out if there is no current + // touch block. + MOZ_ASSERT(GetCurrentTouchBlock()); + newBlock->CopyPropertiesFrom(*GetCurrentTouchBlock()); + } + + mActiveTouchBlock = newBlock; + return newBlock; +} + +InputBlockState* InputQueue::GetCurrentBlock() const { + APZThreadUtils::AssertOnControllerThread(); + return mQueuedInputs.IsEmpty() ? nullptr : mQueuedInputs[0]->Block(); +} + +TouchBlockState* InputQueue::GetCurrentTouchBlock() const { + InputBlockState* block = GetCurrentBlock(); + return block ? block->AsTouchBlock() : mActiveTouchBlock.get(); +} + +WheelBlockState* InputQueue::GetCurrentWheelBlock() const { + InputBlockState* block = GetCurrentBlock(); + return block ? block->AsWheelBlock() : mActiveWheelBlock.get(); +} + +DragBlockState* InputQueue::GetCurrentDragBlock() const { + InputBlockState* block = GetCurrentBlock(); + return block ? block->AsDragBlock() : mActiveDragBlock.get(); +} + +PanGestureBlockState* InputQueue::GetCurrentPanGestureBlock() const { + InputBlockState* block = GetCurrentBlock(); + return block ? block->AsPanGestureBlock() : mActivePanGestureBlock.get(); +} + +PinchGestureBlockState* InputQueue::GetCurrentPinchGestureBlock() const { + InputBlockState* block = GetCurrentBlock(); + return block ? block->AsPinchGestureBlock() : mActivePinchGestureBlock.get(); +} + +KeyboardBlockState* InputQueue::GetCurrentKeyboardBlock() const { + InputBlockState* block = GetCurrentBlock(); + return block ? block->AsKeyboardBlock() : mActiveKeyboardBlock.get(); +} + +WheelBlockState* InputQueue::GetActiveWheelTransaction() const { + WheelBlockState* block = mActiveWheelBlock.get(); + if (!block || !block->InTransaction()) { + return nullptr; + } + return block; +} + +bool InputQueue::HasReadyTouchBlock() const { + return !mQueuedInputs.IsEmpty() && + mQueuedInputs[0]->Block()->AsTouchBlock() && + mQueuedInputs[0]->Block()->AsTouchBlock()->IsReadyForHandling(); +} + +bool InputQueue::AllowScrollHandoff() const { + if (GetCurrentWheelBlock()) { + return GetCurrentWheelBlock()->AllowScrollHandoff(); + } + if (GetCurrentPanGestureBlock()) { + return GetCurrentPanGestureBlock()->AllowScrollHandoff(); + } + if (GetCurrentKeyboardBlock()) { + return GetCurrentKeyboardBlock()->AllowScrollHandoff(); + } + return true; +} + +bool InputQueue::IsDragOnScrollbar(bool aHitScrollbar) { + if (!mDragTracker.InDrag()) { + return false; + } + // Now that we know we are in a drag, get the info from the drag tracker. + // We keep it in the tracker rather than the block because the block can get + // interrupted by something else (like a wheel event) and then a new block + // will get created without the info we want. The tracker will persist though. + return mDragTracker.IsOnScrollbar(aHitScrollbar); +} + +void InputQueue::ScheduleMainThreadTimeout( + const RefPtr& aTarget, + CancelableBlockState* aBlock) { + INPQ_LOG("scheduling main thread timeout for target %p\n", aTarget.get()); + RefPtr timeoutTask = NewRunnableMethod( + "layers::InputQueue::MainThreadTimeout", this, + &InputQueue::MainThreadTimeout, aBlock->GetBlockId()); + int32_t timeout = StaticPrefs::apz_content_response_timeout(); + if (timeout == 0) { + // If the timeout is zero, treat it as a request to ignore any main + // thread confirmation and unconditionally use fallback behaviour for + // when a timeout is reached. This codepath is used by tests that + // want to exercise the fallback behaviour. + // To ensure the fallback behaviour is used unconditionally, the timeout + // is run right away instead of using PostDelayedTask(). However, + // we can't run it right here, because MainThreadTimeout() expects that + // the input block has at least one input event in mQueuedInputs, and + // the event that triggered this call may not have been added to + // mQueuedInputs yet. + mImmediateTimeout = std::move(timeoutTask); + } else { + aTarget->PostDelayedTask(timeoutTask.forget(), timeout); + } +} + +InputBlockState* InputQueue::GetBlockForId(uint64_t aInputBlockId) { + return FindBlockForId(aInputBlockId, nullptr); +} + +void InputQueue::AddInputBlockCallback(uint64_t aInputBlockId, + InputBlockCallbackInfo&& aCallbackInfo) { + mInputBlockCallbacks.insert(InputBlockCallbackMap::value_type( + aInputBlockId, std::move(aCallbackInfo))); +} + +InputBlockState* InputQueue::FindBlockForId(uint64_t aInputBlockId, + InputData** aOutFirstInput) { + for (const auto& queuedInput : mQueuedInputs) { + if (queuedInput->Block()->GetBlockId() == aInputBlockId) { + if (aOutFirstInput) { + *aOutFirstInput = queuedInput->Input(); + } + return queuedInput->Block(); + } + } + + InputBlockState* block = nullptr; + if (mActiveTouchBlock && mActiveTouchBlock->GetBlockId() == aInputBlockId) { + block = mActiveTouchBlock.get(); + } else if (mActiveWheelBlock && + mActiveWheelBlock->GetBlockId() == aInputBlockId) { + block = mActiveWheelBlock.get(); + } else if (mActiveDragBlock && + mActiveDragBlock->GetBlockId() == aInputBlockId) { + block = mActiveDragBlock.get(); + } else if (mActivePanGestureBlock && + mActivePanGestureBlock->GetBlockId() == aInputBlockId) { + block = mActivePanGestureBlock.get(); + } else if (mActivePinchGestureBlock && + mActivePinchGestureBlock->GetBlockId() == aInputBlockId) { + block = mActivePinchGestureBlock.get(); + } else if (mActiveKeyboardBlock && + mActiveKeyboardBlock->GetBlockId() == aInputBlockId) { + block = mActiveKeyboardBlock.get(); + } + // Since we didn't encounter this block while iterating through mQueuedInputs, + // it must have no events associated with it at the moment. + if (aOutFirstInput) { + *aOutFirstInput = nullptr; + } + return block; +} + +void InputQueue::MainThreadTimeout(uint64_t aInputBlockId) { + // It's possible that this function gets called after the controller thread + // was discarded during shutdown. + if (!APZThreadUtils::IsControllerThreadAlive()) { + return; + } + APZThreadUtils::AssertOnControllerThread(); + + INPQ_LOG("got a main thread timeout; block=%" PRIu64 "\n", aInputBlockId); + bool success = false; + InputData* firstInput = nullptr; + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, &firstInput); + if (inputBlock && inputBlock->AsCancelableBlock()) { + CancelableBlockState* block = inputBlock->AsCancelableBlock(); + // time out the touch-listener response and also confirm the existing + // target apzc in the case where the main thread doesn't get back to us + // fast enough. + success = block->TimeoutContentResponse(); + success |= block->SetConfirmedTargetApzc( + block->GetTargetApzc(), + InputBlockState::TargetConfirmationState::eTimedOut, firstInput, + // This actually could be a scrollbar drag, but we pass + // aForScrollbarDrag=false because for scrollbar drags, + // SetConfirmedTargetApzc() will also be called by ConfirmDragBlock(), + // and we pass aForScrollbarDrag=true there. + false); + } else if (inputBlock) { + NS_WARNING("input block is not a cancelable block"); + } + if (success) { + ProcessQueue(); + } +} + +void InputQueue::MaybeLongTapTimeout(uint64_t aInputBlockId) { + // It's possible that this function gets called after the controller thread + // was discarded during shutdown. + if (!APZThreadUtils::IsControllerThreadAlive()) { + return; + } + APZThreadUtils::AssertOnControllerThread(); + + INPQ_LOG("got a maybe-long-tap timeout; block=%" PRIu64 "\n", aInputBlockId); + + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr); + MOZ_ASSERT(!inputBlock || inputBlock->AsTouchBlock()); + if (inputBlock && inputBlock->AsTouchBlock()->IsInSlop()) { + // If the block is still in slop, it won't have sent a touchmove to content + // and so content will not have sent a content response. But also it means + // the touchstart should trigger a long-press gesture so let's force the + // block to get processed now. + MainThreadTimeout(aInputBlockId); + } +} + +void InputQueue::ContentReceivedInputBlock(uint64_t aInputBlockId, + bool aPreventDefault) { + APZThreadUtils::AssertOnControllerThread(); + + INPQ_LOG("got a content response; block=%" PRIu64 " preventDefault=%d\n", + aInputBlockId, aPreventDefault); + bool success = false; + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr); + if (inputBlock && inputBlock->AsCancelableBlock()) { + CancelableBlockState* block = inputBlock->AsCancelableBlock(); + success = block->SetContentResponse(aPreventDefault); + } else if (inputBlock) { + NS_WARNING("input block is not a cancelable block"); + } + if (success) { + ProcessQueue(); + } +} + +void InputQueue::SetConfirmedTargetApzc( + uint64_t aInputBlockId, const RefPtr& aTargetApzc) { + APZThreadUtils::AssertOnControllerThread(); + + INPQ_LOG("got a target apzc; block=%" PRIu64 " guid=%s\n", aInputBlockId, + aTargetApzc ? ToString(aTargetApzc->GetGuid()).c_str() : ""); + bool success = false; + InputData* firstInput = nullptr; + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, &firstInput); + if (inputBlock && inputBlock->AsCancelableBlock()) { + CancelableBlockState* block = inputBlock->AsCancelableBlock(); + success = block->SetConfirmedTargetApzc( + aTargetApzc, InputBlockState::TargetConfirmationState::eConfirmed, + firstInput, + // This actually could be a scrollbar drag, but we pass + // aForScrollbarDrag=false because for scrollbar drags, + // SetConfirmedTargetApzc() will also be called by ConfirmDragBlock(), + // and we pass aForScrollbarDrag=true there. + false); + } else if (inputBlock) { + NS_WARNING("input block is not a cancelable block"); + } + if (success) { + ProcessQueue(); + } +} + +void InputQueue::ConfirmDragBlock( + uint64_t aInputBlockId, const RefPtr& aTargetApzc, + const AsyncDragMetrics& aDragMetrics) { + APZThreadUtils::AssertOnControllerThread(); + + INPQ_LOG("got a target apzc; block=%" PRIu64 " guid=%s dragtarget=%" PRIu64 + "\n", + aInputBlockId, + aTargetApzc ? ToString(aTargetApzc->GetGuid()).c_str() : "", + aDragMetrics.mViewId); + bool success = false; + InputData* firstInput = nullptr; + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, &firstInput); + if (inputBlock && inputBlock->AsDragBlock()) { + DragBlockState* block = inputBlock->AsDragBlock(); + block->SetDragMetrics(aDragMetrics); + success = block->SetConfirmedTargetApzc( + aTargetApzc, InputBlockState::TargetConfirmationState::eConfirmed, + firstInput, + /* aForScrollbarDrag = */ true); + } + if (success) { + ProcessQueue(); + } +} + +void InputQueue::SetAllowedTouchBehavior( + uint64_t aInputBlockId, const nsTArray& aBehaviors) { + APZThreadUtils::AssertOnControllerThread(); + + INPQ_LOG("got allowed touch behaviours; block=%" PRIu64 "\n", aInputBlockId); + bool success = false; + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr); + if (inputBlock && inputBlock->AsTouchBlock()) { + TouchBlockState* block = inputBlock->AsTouchBlock(); + success = block->SetAllowedTouchBehaviors(aBehaviors); + } else if (inputBlock) { + NS_WARNING("input block is not a touch block"); + } + if (success) { + ProcessQueue(); + } +} + +void InputQueue::SetBrowserGestureResponse(uint64_t aInputBlockId, + BrowserGestureResponse aResponse) { + InputBlockState* inputBlock = FindBlockForId(aInputBlockId, nullptr); + + if (inputBlock && inputBlock->AsPanGestureBlock()) { + PanGestureBlockState* block = inputBlock->AsPanGestureBlock(); + block->SetBrowserGestureResponse(aResponse); + } else if (inputBlock) { + NS_WARNING("input block is not a pan gesture block"); + } + ProcessQueue(); +} + +static APZHandledResult GetHandledResultFor( + const AsyncPanZoomController* aApzc, + const InputBlockState& aCurrentInputBlock, nsEventStatus aEagerStatus) { + if (aCurrentInputBlock.ShouldDropEvents()) { + return APZHandledResult{APZHandledPlace::HandledByContent, aApzc}; + } + + if (!aApzc) { + return APZHandledResult{APZHandledPlace::HandledByContent, aApzc}; + } + + if (aApzc->IsRootContent()) { + // If the eager status was eIgnore, we would have returned an eager result + // of Unhandled if there had been no event handler. Now that we know the + // event handler did not preventDefault() the input block, return Unhandled + // as the delayed result. + // FIXME: A more accurate implementation would be to re-do the entire + // computation that determines the status (i.e. calling + // ArePointerEventsConsumable()) with the confirmed target APZC. + return (aEagerStatus == nsEventStatus_eConsumeDoDefault && + aApzc->CanVerticalScrollWithDynamicToolbar()) + ? APZHandledResult{APZHandledPlace::HandledByRoot, aApzc} + : APZHandledResult{APZHandledPlace::Unhandled, aApzc}; + } + + auto [result, rootApzc] = aCurrentInputBlock.GetOverscrollHandoffChain() + ->ScrollingDownWillMoveDynamicToolbar(aApzc); + if (!result) { + return APZHandledResult{APZHandledPlace::HandledByContent, aApzc}; + } + + // Return `HandledByRoot` if scroll positions in all relevant APZC are at the + // bottom edge and if there are contents covered by the dynamic toolbar. + MOZ_ASSERT(rootApzc && rootApzc->IsRootContent()); + return APZHandledResult{APZHandledPlace::HandledByRoot, rootApzc}; +} + +void InputQueue::ProcessQueue() { + APZThreadUtils::AssertOnControllerThread(); + + while (!mQueuedInputs.IsEmpty()) { + InputBlockState* curBlock = mQueuedInputs[0]->Block(); + CancelableBlockState* cancelable = curBlock->AsCancelableBlock(); + if (cancelable && !cancelable->IsReadyForHandling()) { + break; + } + + INPQ_LOG( + "processing input from block %p; preventDefault %d shouldDropEvents %d " + "target %p\n", + curBlock, cancelable && cancelable->IsDefaultPrevented(), + curBlock->ShouldDropEvents(), curBlock->GetTargetApzc().get()); + RefPtr target = curBlock->GetTargetApzc(); + + // If there is an input block callback registered for this + // input block, invoke it. + auto it = mInputBlockCallbacks.find(curBlock->GetBlockId()); + if (it != mInputBlockCallbacks.end()) { + APZHandledResult handledResult = + GetHandledResultFor(target, *curBlock, it->second.mEagerStatus); + it->second.mCallback(curBlock->GetBlockId(), handledResult); + // The callback is one-shot; discard it after calling it. + mInputBlockCallbacks.erase(it); + } + + // target may be null here if the initial target was unconfirmed and then + // we later got a confirmed null target. in that case drop the events. + if (target) { + // If the event is targeting a different APZC than the previous one, + // we want to clear the previous APZC's gesture state regardless of + // whether we're actually dispatching the event or not. + if (mLastActiveApzc && mLastActiveApzc != target && + mTouchCounter.GetActiveTouchCount() > 0) { + mLastActiveApzc->ResetTouchInputState(); + } + if (curBlock->ShouldDropEvents()) { + if (curBlock->AsTouchBlock()) { + target->ResetTouchInputState(); + } else if (curBlock->AsPanGestureBlock()) { + target->ResetPanGestureInputState(); + } + } else { + UpdateActiveApzc(target); + curBlock->DispatchEvent(*(mQueuedInputs[0]->Input())); + } + } + mQueuedInputs.RemoveElementAt(0); + } + + if (CanDiscardBlock(mActiveTouchBlock)) { + mActiveTouchBlock = nullptr; + } + if (CanDiscardBlock(mActiveWheelBlock)) { + mActiveWheelBlock = nullptr; + } + if (CanDiscardBlock(mActiveDragBlock)) { + mActiveDragBlock = nullptr; + } + if (CanDiscardBlock(mActivePanGestureBlock)) { + mActivePanGestureBlock = nullptr; + } + if (CanDiscardBlock(mActivePinchGestureBlock)) { + mActivePinchGestureBlock = nullptr; + } + if (CanDiscardBlock(mActiveKeyboardBlock)) { + mActiveKeyboardBlock = nullptr; + } +} + +bool InputQueue::CanDiscardBlock(InputBlockState* aBlock) { + if (!aBlock || + (aBlock->AsCancelableBlock() && + !aBlock->AsCancelableBlock()->IsReadyForHandling()) || + aBlock->MustStayActive()) { + return false; + } + InputData* firstInput = nullptr; + FindBlockForId(aBlock->GetBlockId(), &firstInput); + if (firstInput) { + // The block has at least one input event still in the queue, so it's + // not depleted + return false; + } + return true; +} + +void InputQueue::UpdateActiveApzc( + const RefPtr& aNewActive) { + mLastActiveApzc = aNewActive; +} + +void InputQueue::Clear() { + // On Android, where the controller thread is the Android UI thread, + // it's possible for this to be called after the main thread has + // already run the shutdown task that clears the state used to + // implement APZThreadUtils::AssertOnControllerThread(). + // In such cases, we still want to perform the cleanup. + if (APZThreadUtils::IsControllerThreadAlive()) { + APZThreadUtils::AssertOnControllerThread(); + } + + mQueuedInputs.Clear(); + mActiveTouchBlock = nullptr; + mActiveWheelBlock = nullptr; + mActiveDragBlock = nullptr; + mActivePanGestureBlock = nullptr; + mActivePinchGestureBlock = nullptr; + mActiveKeyboardBlock = nullptr; + mLastActiveApzc = nullptr; +} + +InputQueue::AutoRunImmediateTimeout::AutoRunImmediateTimeout(InputQueue* aQueue) + : mQueue(aQueue) { + MOZ_ASSERT(!mQueue->mImmediateTimeout); +} + +InputQueue::AutoRunImmediateTimeout::~AutoRunImmediateTimeout() { + if (mQueue->mImmediateTimeout) { + mQueue->mImmediateTimeout->Run(); + mQueue->mImmediateTimeout = nullptr; + } +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/InputQueue.h b/gfx/layers/apz/src/InputQueue.h new file mode 100644 index 0000000000..8a015c24f3 --- /dev/null +++ b/gfx/layers/apz/src/InputQueue.h @@ -0,0 +1,277 @@ +/* -*- 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_InputQueue_h +#define mozilla_layers_InputQueue_h + +#include "APZUtils.h" +#include "DragTracker.h" +#include "InputData.h" +#include "mozilla/EventForwards.h" +#include "mozilla/layers/TouchCounter.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "nsTArray.h" + +#include + +namespace mozilla { + +class InputData; +class MultiTouchInput; +class ScrollWheelInput; + +namespace layers { + +class AsyncPanZoomController; +class InputBlockState; +class CancelableBlockState; +class TouchBlockState; +class WheelBlockState; +class DragBlockState; +class PanGestureBlockState; +class PinchGestureBlockState; +class KeyboardBlockState; +class AsyncDragMetrics; +class QueuedInput; +struct APZEventResult; +struct APZHandledResult; +enum class BrowserGestureResponse : bool; + +using InputBlockCallback = std::function; + +struct InputBlockCallbackInfo { + nsEventStatus mEagerStatus; + InputBlockCallback mCallback; +}; + +/** + * This class stores incoming input events, associated with "input blocks", + * until they are ready for handling. + */ +class InputQueue { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(InputQueue) + + public: + InputQueue(); + + /** + * Notifies the InputQueue of a new incoming input event. The APZC that the + * input event was targeted to should be provided in the |aTarget| parameter. + * See the documentation on APZCTreeManager::ReceiveInputEvent for info on + * return values from this function. + */ + APZEventResult ReceiveInputEvent( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, InputData& aEvent, + const Maybe>& aTouchBehaviors = Nothing()); + /** + * This function should be invoked to notify the InputQueue when web content + * decides whether or not it wants to cancel a block of events. The block + * id to which this applies should be provided in |aInputBlockId|. + */ + void ContentReceivedInputBlock(uint64_t aInputBlockId, bool aPreventDefault); + /** + * This function should be invoked to notify the InputQueue once the target + * APZC to handle an input block has been confirmed. In practice this should + * generally be decidable upon receipt of the input event, but in some cases + * we may need to query the layout engine to know for sure. The input block + * this applies to should be specified via the |aInputBlockId| parameter. + */ + void SetConfirmedTargetApzc( + uint64_t aInputBlockId, + const RefPtr& aTargetApzc); + /** + * This function is invoked to confirm that the drag block should be handled + * by the APZ. + */ + void ConfirmDragBlock(uint64_t aInputBlockId, + const RefPtr& aTargetApzc, + const AsyncDragMetrics& aDragMetrics); + /** + * This function should be invoked to notify the InputQueue of the touch- + * action properties for the different touch points in an input block. The + * input block this applies to should be specified by the |aInputBlockId| + * parameter. If touch-action is not enabled on the platform, this function + * does nothing and need not be called. + */ + void SetAllowedTouchBehavior(uint64_t aInputBlockId, + const nsTArray& aBehaviors); + /** + * Adds a new touch block at the end of the input queue that has the same + * allowed touch behaviour flags as the the touch block currently being + * processed. This should only be called when processing of a touch block + * triggers the creation of a new touch block. Returns the input block id + * of the the newly-created block. + */ + uint64_t InjectNewTouchBlock(AsyncPanZoomController* aTarget); + /** + * Returns the pending input block at the head of the queue, if there is one. + * This may return null if there all input events have been processed. + */ + InputBlockState* GetCurrentBlock() const; + /* + * Returns the current pending input block as a specific kind of block. If + * GetCurrentBlock() returns null, these functions additionally check the + * mActiveXXXBlock field of the corresponding input type to see if there is + * a depleted but still active input block, and returns that if found. These + * functions may return null if no block is found. + */ + TouchBlockState* GetCurrentTouchBlock() const; + WheelBlockState* GetCurrentWheelBlock() const; + DragBlockState* GetCurrentDragBlock() const; + PanGestureBlockState* GetCurrentPanGestureBlock() const; + PinchGestureBlockState* GetCurrentPinchGestureBlock() const; + KeyboardBlockState* GetCurrentKeyboardBlock() const; + /** + * Returns true iff the pending block at the head of the queue is a touch + * block and is ready for handling. + */ + bool HasReadyTouchBlock() const; + /** + * If there is an active wheel transaction, returns the WheelBlockState + * representing the transaction. Otherwise, returns null. "Active" in this + * function name is the same kind of "active" as in mActiveWheelBlock - that + * is, new incoming wheel events will go into the "active" block. + */ + WheelBlockState* GetActiveWheelTransaction() const; + /** + * Remove all input blocks from the input queue. + */ + void Clear(); + /** + * Whether the current pending block allows scroll handoff. + */ + bool AllowScrollHandoff() const; + /** + * If there is currently a drag in progress, return whether or not it was + * targeted at a scrollbar. If the drag was newly-created and doesn't know, + * use the provided |aOnScrollbar| to populate that information. + */ + bool IsDragOnScrollbar(bool aOnScrollbar); + + InputBlockState* GetBlockForId(uint64_t aInputBlockId); + + void AddInputBlockCallback(uint64_t aInputBlockId, + InputBlockCallbackInfo&& aCallback); + + void SetBrowserGestureResponse(uint64_t aInputBlockId, + BrowserGestureResponse aResponse); + + private: + ~InputQueue(); + + // RAII class for automatically running a timeout task that may + // need to be run immediately after an event has been queued. + class AutoRunImmediateTimeout final { + public: + explicit AutoRunImmediateTimeout(InputQueue* aQueue); + ~AutoRunImmediateTimeout(); + + private: + InputQueue* mQueue; + }; + + TouchBlockState* StartNewTouchBlock( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, bool aCopyPropertiesFromCurrent); + + /** + * If animations are present for the current pending input block, cancel + * them as soon as possible. + */ + void CancelAnimationsForNewBlock(InputBlockState* aBlock, + CancelAnimationFlags aExtraFlags = Default); + + /** + * If we need to wait for a content response, schedule that now. Returns true + * if the timeout was scheduled, false otherwise. + */ + bool MaybeRequestContentResponse( + const RefPtr& aTarget, + CancelableBlockState* aBlock); + + APZEventResult ReceiveTouchInput( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, const MultiTouchInput& aEvent, + const Maybe>& aTouchBehaviors); + APZEventResult ReceiveMouseInput( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, MouseInput& aEvent); + APZEventResult ReceiveScrollWheelInput( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, const ScrollWheelInput& aEvent); + APZEventResult ReceivePanGestureInput( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, const PanGestureInput& aEvent); + APZEventResult ReceivePinchGestureInput( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, const PinchGestureInput& aEvent); + APZEventResult ReceiveKeyboardInput( + const RefPtr& aTarget, + TargetConfirmationFlags aFlags, const KeyboardInput& aEvent); + + /** + * Helper function that searches mQueuedInputs for the first block matching + * the given id, and returns it. If |aOutFirstInput| is non-null, it is + * populated with a pointer to the first input in mQueuedInputs that + * corresponds to the block, or null if no such input was found. Note that + * even if there are no inputs in mQueuedInputs, this function can return + * non-null if the block id provided matches one of the depleted-but-still- + * active blocks (mActiveTouchBlock, mActiveWheelBlock, etc.). + */ + InputBlockState* FindBlockForId(uint64_t aInputBlockId, + InputData** aOutFirstInput); + void ScheduleMainThreadTimeout(const RefPtr& aTarget, + CancelableBlockState* aBlock); + void MainThreadTimeout(uint64_t aInputBlockId); + void MaybeLongTapTimeout(uint64_t aInputBlockId); + void ProcessQueue(); + bool CanDiscardBlock(InputBlockState* aBlock); + void UpdateActiveApzc(const RefPtr& aNewActive); + + private: + // The queue of input events that have not yet been fully processed. + // This member must only be accessed on the controller/UI thread. + nsTArray> mQueuedInputs; + + // These are the most recently created blocks of each input type. They are + // "active" in the sense that new inputs of that type are associated with + // them. Note that these pointers may be null if no inputs of the type have + // arrived, or if the inputs for the type formed a complete block that was + // then discarded. + RefPtr mActiveTouchBlock; + RefPtr mActiveWheelBlock; + RefPtr mActiveDragBlock; + RefPtr mActivePanGestureBlock; + RefPtr mActivePinchGestureBlock; + RefPtr mActiveKeyboardBlock; + + // The APZC to which the last event was delivered + RefPtr mLastActiveApzc; + + // Track touches so we know when to clear mLastActiveApzc + TouchCounter mTouchCounter; + + // Track mouse inputs so we know if we're in a drag or not + DragTracker mDragTracker; + + // Temporarily stores a timeout task that needs to be run as soon as + // as the event that triggered it has been queued. + RefPtr mImmediateTimeout; + + // Maps input block ids to callbacks that will be invoked when the input block + // is ready for handling. + using InputBlockCallbackMap = + std::unordered_map; + InputBlockCallbackMap mInputBlockCallbacks; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_InputQueue_h diff --git a/gfx/layers/apz/src/KeyboardMap.cpp b/gfx/layers/apz/src/KeyboardMap.cpp new file mode 100644 index 0000000000..9444037be6 --- /dev/null +++ b/gfx/layers/apz/src/KeyboardMap.cpp @@ -0,0 +1,170 @@ +/* -*- 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 "mozilla/layers/KeyboardMap.h" + +#include "mozilla/TextEvents.h" // for IgnoreModifierState, ShortcutKeyCandidate + +namespace mozilla { +namespace layers { + +KeyboardShortcut::KeyboardShortcut() + : mKeyCode(0), + mCharCode(0), + mModifiers(0), + mModifiersMask(0), + mEventType(KeyboardInput::KeyboardEventType::KEY_OTHER), + mDispatchToContent(false) {} + +KeyboardShortcut::KeyboardShortcut(KeyboardInput::KeyboardEventType aEventType, + uint32_t aKeyCode, uint32_t aCharCode, + Modifiers aModifiers, + Modifiers aModifiersMask, + const KeyboardScrollAction& aAction) + : mAction(aAction), + mKeyCode(aKeyCode), + mCharCode(aCharCode), + mModifiers(aModifiers), + mModifiersMask(aModifiersMask), + mEventType(aEventType), + mDispatchToContent(false) {} + +KeyboardShortcut::KeyboardShortcut(KeyboardInput::KeyboardEventType aEventType, + uint32_t aKeyCode, uint32_t aCharCode, + Modifiers aModifiers, + Modifiers aModifiersMask) + : mKeyCode(aKeyCode), + mCharCode(aCharCode), + mModifiers(aModifiers), + mModifiersMask(aModifiersMask), + mEventType(aEventType), + mDispatchToContent(true) {} + +/* static */ +void KeyboardShortcut::AppendHardcodedShortcuts( + nsTArray& aShortcuts) { + // Tab + KeyboardShortcut tab1; + tab1.mDispatchToContent = true; + tab1.mKeyCode = NS_VK_TAB; + tab1.mCharCode = 0; + tab1.mModifiers = 0; + tab1.mModifiersMask = 0; + tab1.mEventType = KeyboardInput::KEY_PRESS; + aShortcuts.AppendElement(tab1); + + // F6 + KeyboardShortcut tab2; + tab2.mDispatchToContent = true; + tab2.mKeyCode = NS_VK_F6; + tab2.mCharCode = 0; + tab2.mModifiers = 0; + tab2.mModifiersMask = 0; + tab2.mEventType = KeyboardInput::KEY_PRESS; + aShortcuts.AppendElement(tab2); +} + +bool KeyboardShortcut::Matches(const KeyboardInput& aInput, + const IgnoreModifierState& aIgnore, + uint32_t aOverrideCharCode) const { + return mEventType == aInput.mType && MatchesKey(aInput, aOverrideCharCode) && + MatchesModifiers(aInput, aIgnore); +} + +bool KeyboardShortcut::MatchesKey(const KeyboardInput& aInput, + uint32_t aOverrideCharCode) const { + // Compare by the key code if we have one + if (!mCharCode) { + return mKeyCode == aInput.mKeyCode; + } + + // We are comparing by char code + uint32_t charCode; + + // If we are comparing against a shortcut candidate then we might + // have an override char code + if (aOverrideCharCode) { + charCode = aOverrideCharCode; + } else { + charCode = aInput.mCharCode; + } + + // Both char codes must be in lowercase to compare correctly + if (IS_IN_BMP(charCode)) { + charCode = ToLowerCase(static_cast(charCode)); + } + + return mCharCode == charCode; +} + +bool KeyboardShortcut::MatchesModifiers( + const KeyboardInput& aInput, const IgnoreModifierState& aIgnore) const { + Modifiers modifiersMask = mModifiersMask; + + // If we are ignoring Shift or OS, then unset that part of the mask + if (aIgnore.mOS) { + modifiersMask &= ~MODIFIER_OS; + } + if (aIgnore.mShift) { + modifiersMask &= ~MODIFIER_SHIFT; + } + + // Mask off the modifiers we are ignoring from the keyboard input + return (aInput.modifiers & modifiersMask) == mModifiers; +} + +KeyboardMap::KeyboardMap(nsTArray&& aShortcuts) + : mShortcuts(aShortcuts) {} + +KeyboardMap::KeyboardMap() = default; + +Maybe KeyboardMap::FindMatch( + const KeyboardInput& aEvent) const { + // If there are no shortcut candidates, then just search with with the + // keyboard input + if (aEvent.mShortcutCandidates.IsEmpty()) { + return FindMatchInternal(aEvent, IgnoreModifierState()); + } + + // Otherwise do a search with each shortcut candidate in order + for (auto& key : aEvent.mShortcutCandidates) { + IgnoreModifierState ignoreModifierState; + ignoreModifierState.mShift = key.mIgnoreShift; + + auto match = FindMatchInternal(aEvent, ignoreModifierState, key.mCharCode); + if (match) { + return match; + } + } + return Nothing(); +} + +Maybe KeyboardMap::FindMatchInternal( + const KeyboardInput& aEvent, const IgnoreModifierState& aIgnore, + uint32_t aOverrideCharCode) const { + for (auto& shortcut : mShortcuts) { + if (shortcut.Matches(aEvent, aIgnore, aOverrideCharCode)) { + return Some(shortcut); + } + } + +#ifdef XP_WIN + // Windows native applications ignore Windows-Logo key state when checking + // shortcut keys even if the key is pressed. Therefore, if there is no + // shortcut key which exactly matches current modifier state, we should + // retry to look for a shortcut key without the Windows-Logo key press. + if (!aIgnore.mOS && (aEvent.modifiers & MODIFIER_OS)) { + IgnoreModifierState ignoreModifierState(aIgnore); + ignoreModifierState.mOS = true; + return FindMatchInternal(aEvent, ignoreModifierState, aOverrideCharCode); + } +#endif + + return Nothing(); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/KeyboardMap.h b/gfx/layers/apz/src/KeyboardMap.h new file mode 100644 index 0000000000..32ec8ea61d --- /dev/null +++ b/gfx/layers/apz/src/KeyboardMap.h @@ -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/. */ + +#ifndef mozilla_layers_KeyboardMap_h +#define mozilla_layers_KeyboardMap_h + +#include // for uint32_t + +#include "InputData.h" // for KeyboardInput +#include "nsIScrollableFrame.h" // for nsIScrollableFrame::ScrollUnit +#include "nsTArray.h" // for nsTArray +#include "mozilla/Maybe.h" // for mozilla::Maybe +#include "KeyboardScrollAction.h" // for KeyboardScrollAction + +namespace mozilla { + +struct IgnoreModifierState; + +namespace layers { + +class KeyboardMap; + +/** + * This class is an off main-thread for scrolling commands. + */ +class KeyboardShortcut final { + public: + KeyboardShortcut(); + + /** + * Create a keyboard shortcut that when matched can be handled by executing + * the specified keyboard action. + */ + KeyboardShortcut(KeyboardInput::KeyboardEventType aEventType, + uint32_t aKeyCode, uint32_t aCharCode, Modifiers aModifiers, + Modifiers aModifiersMask, + const KeyboardScrollAction& aAction); + + /** + * Create a keyboard shortcut that when matched should be handled by ignoring + * the keyboard event and dispatching it to content. + */ + KeyboardShortcut(KeyboardInput::KeyboardEventType aEventType, + uint32_t aKeyCode, uint32_t aCharCode, Modifiers aModifiers, + Modifiers aModifiersMask); + + /** + * There are some default actions for keyboard inputs that are hardcoded in + * EventStateManager instead of being represented as XBL handlers. This adds + * keyboard shortcuts to match these inputs and dispatch them to content. + */ + static void AppendHardcodedShortcuts(nsTArray& aShortcuts); + + protected: + friend mozilla::layers::KeyboardMap; + + bool Matches(const KeyboardInput& aInput, const IgnoreModifierState& aIgnore, + uint32_t aOverrideCharCode = 0) const; + + private: + bool MatchesKey(const KeyboardInput& aInput, + uint32_t aOverrideCharCode) const; + bool MatchesModifiers(const KeyboardInput& aInput, + const IgnoreModifierState& aIgnore) const; + + public: + // The action to perform when this shortcut is matched, + // and not flagged to be dispatched to content + KeyboardScrollAction mAction; + + // Only one of mKeyCode or mCharCode may be non-zero + // whichever one is non-zero is the one to compare when matching + uint32_t mKeyCode; + uint32_t mCharCode; + + // The modifiers that must be active for this shortcut + Modifiers mModifiers; + // The modifiers to compare when matching this shortcut + Modifiers mModifiersMask; + + // The type of keyboard event to match against + KeyboardInput::KeyboardEventType mEventType; + + // Whether events matched by this must be dispatched to content + bool mDispatchToContent; +}; + +/** + * A keyboard map is an off main-thread for scrolling commands. + */ +class KeyboardMap final { + public: + KeyboardMap(); + explicit KeyboardMap(nsTArray&& aShortcuts); + + const nsTArray& Shortcuts() const { return mShortcuts; } + + /** + * Search through the internal list of shortcuts for a match for the input + * event + */ + Maybe FindMatch(const KeyboardInput& aEvent) const; + + private: + Maybe FindMatchInternal( + const KeyboardInput& aEvent, const IgnoreModifierState& aIgnore, + uint32_t aOverrideCharCode = 0) const; + + CopyableTArray mShortcuts; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_KeyboardMap_h diff --git a/gfx/layers/apz/src/KeyboardScrollAction.cpp b/gfx/layers/apz/src/KeyboardScrollAction.cpp new file mode 100644 index 0000000000..42d9a8bff2 --- /dev/null +++ b/gfx/layers/apz/src/KeyboardScrollAction.cpp @@ -0,0 +1,37 @@ +/* -*- 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 "mozilla/layers/KeyboardScrollAction.h" + +namespace mozilla { +namespace layers { + +/* static */ ScrollUnit KeyboardScrollAction::GetScrollUnit( + KeyboardScrollAction::KeyboardScrollActionType aDeltaType) { + switch (aDeltaType) { + case KeyboardScrollAction::eScrollCharacter: + return ScrollUnit::LINES; + case KeyboardScrollAction::eScrollLine: + return ScrollUnit::LINES; + case KeyboardScrollAction::eScrollPage: + return ScrollUnit::PAGES; + case KeyboardScrollAction::eScrollComplete: + return ScrollUnit::WHOLE; + } + + // Silence an overzealous warning + return ScrollUnit::WHOLE; +} + +KeyboardScrollAction::KeyboardScrollAction() + : mType(KeyboardScrollAction::eScrollCharacter), mForward(false) {} + +KeyboardScrollAction::KeyboardScrollAction(KeyboardScrollActionType aType, + bool aForward) + : mType(aType), mForward(aForward) {} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/KeyboardScrollAction.h b/gfx/layers/apz/src/KeyboardScrollAction.h new file mode 100644 index 0000000000..780006c1b3 --- /dev/null +++ b/gfx/layers/apz/src/KeyboardScrollAction.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layers_KeyboardScrollAction_h +#define mozilla_layers_KeyboardScrollAction_h + +#include // for uint8_t + +#include "mozilla/ScrollTypes.h" +#include "mozilla/DefineEnum.h" // for MOZ_DEFINE_ENUM + +namespace mozilla { +namespace layers { + +/** + * This class represents a scrolling action to be performed on a scrollable + * layer. + */ +struct KeyboardScrollAction final { + public: + // clang-format off + MOZ_DEFINE_ENUM_WITH_BASE_AT_CLASS_SCOPE( + KeyboardScrollActionType, uint8_t, ( + eScrollCharacter, + eScrollLine, + eScrollPage, + eScrollComplete + )); + // clang-format on + + static ScrollUnit GetScrollUnit(KeyboardScrollActionType aDeltaType); + + KeyboardScrollAction(); + KeyboardScrollAction(KeyboardScrollActionType aType, bool aForward); + + // The type of scroll to perform for this action + KeyboardScrollActionType mType; + // Whether to scroll forward or backward along the axis of this action type + bool mForward; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_KeyboardScrollAction_h diff --git a/gfx/layers/apz/src/Overscroll.h b/gfx/layers/apz/src/Overscroll.h new file mode 100644 index 0000000000..1fb7c3e487 --- /dev/null +++ b/gfx/layers/apz/src/Overscroll.h @@ -0,0 +1,250 @@ +/* -*- 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_Overscroll_h +#define mozilla_layers_Overscroll_h + +#include "AsyncPanZoomAnimation.h" +#include "AsyncPanZoomController.h" +#include "mozilla/TimeStamp.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace layers { + +// Animation used by GenericOverscrollEffect. +class OverscrollAnimation : public AsyncPanZoomAnimation { + public: + OverscrollAnimation(AsyncPanZoomController& aApzc, + const ParentLayerPoint& aVelocity, + SideBits aOverscrollSideBits) + : mApzc(aApzc), mOverscrollSideBits(aOverscrollSideBits) { + MOZ_ASSERT( + (mOverscrollSideBits & SideBits::eTopBottom) != SideBits::eTopBottom && + (mOverscrollSideBits & SideBits::eLeftRight) != + SideBits::eLeftRight, + "Don't allow overscrolling on both sides at the same time"); + if ((aOverscrollSideBits & SideBits::eLeftRight) != SideBits::eNone) { + mApzc.mX.StartOverscrollAnimation(aVelocity.x); + } + if ((aOverscrollSideBits & SideBits::eTopBottom) != SideBits::eNone) { + mApzc.mY.StartOverscrollAnimation(aVelocity.y); + } + } + virtual ~OverscrollAnimation() { + mApzc.mX.EndOverscrollAnimation(); + mApzc.mY.EndOverscrollAnimation(); + } + + virtual bool DoSample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta) override { + // Can't inline these variables due to short-circuit evaluation. + bool continueX = mApzc.mX.IsOverscrollAnimationAlive() && + mApzc.mX.SampleOverscrollAnimation( + aDelta, mOverscrollSideBits & SideBits::eLeftRight); + bool continueY = mApzc.mY.IsOverscrollAnimationAlive() && + mApzc.mY.SampleOverscrollAnimation( + aDelta, mOverscrollSideBits & SideBits::eTopBottom); + if (!continueX && !continueY) { + // If we got into overscroll from a fling, that fling did not request a + // fling snap to avoid a resulting scrollTo from cancelling the overscroll + // animation too early. We do still want to request a fling snap, though, + // in case the end of the axis at which we're overscrolled is not a valid + // snap point, so we request one now. If there are no snap points, this + // will do nothing. If there are snap points, we'll get a scrollTo that + // snaps us back to the nearest valid snap point. The scroll snapping is + // done in a deferred task, otherwise the state change to NOTHING caused + // by the overscroll animation ending would clobber a possible state + // change to SMOOTH_SCROLL in ScrollSnap(). + mDeferredTasks.AppendElement(NewRunnableMethod( + "layers::AsyncPanZoomController::ScrollSnap", &mApzc, + &AsyncPanZoomController::ScrollSnap, + ScrollSnapFlags::IntendedDirection | + ScrollSnapFlags::IntendedEndPosition)); + return false; + } + return true; + } + + virtual bool WantsRepaints() override { return false; } + + // Tell the overscroll animation about the pan momentum event. For each axis, + // the overscroll animation may start, stop, or continue managing that axis in + // response to the pan momentum event + void HandlePanMomentum(const ParentLayerPoint& aDisplacement) { + float xOverscroll = mApzc.mX.GetOverscroll(); + if ((xOverscroll > 0 && aDisplacement.x > 0) || + (xOverscroll < 0 && aDisplacement.x < 0)) { + if (!mApzc.mX.IsOverscrollAnimationRunning()) { + // Start a new overscroll animation on this axis, if there is no + // overscroll animation running and if the pan momentum displacement + // the pan momentum displacement is the same direction of the current + // overscroll. + mApzc.mX.StartOverscrollAnimation(mApzc.mX.GetVelocity()); + mOverscrollSideBits |= + xOverscroll > 0 ? SideBits::eRight : SideBits::eLeft; + } + } else if ((xOverscroll > 0 && aDisplacement.x < 0) || + (xOverscroll < 0 && aDisplacement.x > 0)) { + // Otherwise, stop the animation in the direction so that it won't clobber + // subsequent pan momentum scrolling. + mApzc.mX.EndOverscrollAnimation(); + } + + // Same as above but for Y axis. + float yOverscroll = mApzc.mY.GetOverscroll(); + if ((yOverscroll > 0 && aDisplacement.y > 0) || + (yOverscroll < 0 && aDisplacement.y < 0)) { + if (!mApzc.mY.IsOverscrollAnimationRunning()) { + mApzc.mY.StartOverscrollAnimation(mApzc.mY.GetVelocity()); + mOverscrollSideBits |= + yOverscroll > 0 ? SideBits::eBottom : SideBits::eTop; + } + } else if ((yOverscroll > 0 && aDisplacement.y < 0) || + (yOverscroll < 0 && aDisplacement.y > 0)) { + mApzc.mY.EndOverscrollAnimation(); + } + } + + ScrollDirections GetDirections() const { + ScrollDirections directions; + if (mApzc.mX.IsOverscrollAnimationRunning()) { + directions += ScrollDirection::eHorizontal; + } + if (mApzc.mY.IsOverscrollAnimationRunning()) { + directions += ScrollDirection::eVertical; + } + return directions; + }; + + OverscrollAnimation* AsOverscrollAnimation() override { return this; } + + bool IsManagingXAxis() const { + return mApzc.mX.IsOverscrollAnimationRunning(); + } + bool IsManagingYAxis() const { + return mApzc.mY.IsOverscrollAnimationRunning(); + } + + private: + AsyncPanZoomController& mApzc; + SideBits mOverscrollSideBits; +}; + +// Base class for different overscroll effects; +class OverscrollEffectBase { + public: + virtual ~OverscrollEffectBase() = default; + + // Try to increase the amount of overscroll by |aOverscroll|. Limited to + // directions contained in |aOverscrollableDirections|. Components of + // |aOverscroll| in directions that are successfully consumed are dropped. + virtual void ConsumeOverscroll( + ParentLayerPoint& aOverscroll, + ScrollDirections aOverscrollableDirections) = 0; + + // Relieve overscroll. Depending on the implementation, the relief may + // be immediate, or gradual (e.g. after an animation) but this starts + // the process. |aVelocity| is the current velocity of the APZC, and + // |aOverscrollSideBits| contains the side(s) at which the APZC is + // overscrolled. + virtual void RelieveOverscroll(const ParentLayerPoint& aVelocity, + SideBits aOverscrollSideBits) = 0; + + virtual bool IsOverscrolled() const = 0; + + // Similarly to RelieveOverscroll(), but has immediate effect + // (no animation). + virtual void ClearOverscroll() = 0; +}; + +// A generic overscroll effect, implemented by AsyncPanZoomController itself. +class GenericOverscrollEffect : public OverscrollEffectBase { + public: + explicit GenericOverscrollEffect(AsyncPanZoomController& aApzc) + : mApzc(aApzc) {} + + void ConsumeOverscroll(ParentLayerPoint& aOverscroll, + ScrollDirections aOverscrollableDirections) override { + if (aOverscrollableDirections.contains(ScrollDirection::eHorizontal)) { + mApzc.mX.OverscrollBy(aOverscroll.x); + aOverscroll.x = 0; + } + + if (aOverscrollableDirections.contains(ScrollDirection::eVertical)) { + mApzc.mY.OverscrollBy(aOverscroll.y); + aOverscroll.y = 0; + } + + if (!aOverscrollableDirections.isEmpty()) { + mApzc.ScheduleComposite(); + } + } + + void RelieveOverscroll(const ParentLayerPoint& aVelocity, + SideBits aOverscrollSideBits) override { + mApzc.StartOverscrollAnimation(aVelocity, aOverscrollSideBits); + } + + bool IsOverscrolled() const override { + return mApzc.IsPhysicallyOverscrolled(); + } + + void ClearOverscroll() override { mApzc.ClearPhysicalOverscroll(); } + + private: + AsyncPanZoomController& mApzc; +}; + +// A widget-specific overscroll effect, implemented by the widget via +// GeckoContentController. +class WidgetOverscrollEffect : public OverscrollEffectBase { + public: + explicit WidgetOverscrollEffect(AsyncPanZoomController& aApzc) + : mApzc(aApzc), mIsOverscrolled(false) {} + + void ConsumeOverscroll(ParentLayerPoint& aOverscroll, + ScrollDirections aOverscrollableDirections) override { + RefPtr controller = + mApzc.GetGeckoContentController(); + if (controller && !aOverscrollableDirections.isEmpty()) { + mIsOverscrolled = true; + controller->UpdateOverscrollOffset(mApzc.GetGuid(), aOverscroll.x, + aOverscroll.y, mApzc.IsRootContent()); + aOverscroll = ParentLayerPoint(); + } + } + + void RelieveOverscroll(const ParentLayerPoint& aVelocity, + SideBits aOverscrollSideBits) override { + RefPtr controller = + mApzc.GetGeckoContentController(); + // From APZC's point of view, consider it to no longer be overscrolled + // as soon as RelieveOverscroll() is called. The widget may use a + // delay or animation until the relieving of the overscroll is complete, + // but we don't have any insight into that. + mIsOverscrolled = false; + if (controller) { + controller->UpdateOverscrollVelocity(mApzc.GetGuid(), aVelocity.x, + aVelocity.y, mApzc.IsRootContent()); + } + } + + bool IsOverscrolled() const override { return mIsOverscrolled; } + + void ClearOverscroll() override { + RelieveOverscroll(ParentLayerPoint(), SideBits() /* ignored */); + } + + private: + AsyncPanZoomController& mApzc; + bool mIsOverscrolled; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_Overscroll_h diff --git a/gfx/layers/apz/src/OverscrollHandoffState.cpp b/gfx/layers/apz/src/OverscrollHandoffState.cpp new file mode 100644 index 0000000000..ed38f6fa7a --- /dev/null +++ b/gfx/layers/apz/src/OverscrollHandoffState.cpp @@ -0,0 +1,228 @@ +/* -*- 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 "OverscrollHandoffState.h" + +#include // for std::stable_sort +#include "mozilla/Assertions.h" +#include "mozilla/FloatingPoint.h" +#include "AsyncPanZoomController.h" + +namespace mozilla { +namespace layers { + +OverscrollHandoffChain::~OverscrollHandoffChain() = default; + +void OverscrollHandoffChain::Add(AsyncPanZoomController* aApzc) { + mChain.push_back(aApzc); +} + +struct CompareByScrollPriority { + bool operator()(const RefPtr& a, + const RefPtr& b) const { + return a->HasScrollgrab() && !b->HasScrollgrab(); + } +}; + +void OverscrollHandoffChain::SortByScrollPriority() { + // The sorting being stable ensures that the relative order between + // non-scrollgrabbing APZCs remains child -> parent. + // (The relative order between scrollgrabbing APZCs will also remain + // child -> parent, though that's just an artefact of the implementation + // and users of 'scrollgrab' should not rely on this.) + std::stable_sort(mChain.begin(), mChain.end(), CompareByScrollPriority()); +} + +const RefPtr& OverscrollHandoffChain::GetApzcAtIndex( + uint32_t aIndex) const { + MOZ_ASSERT(aIndex < Length()); + return mChain[aIndex]; +} + +uint32_t OverscrollHandoffChain::IndexOf( + const AsyncPanZoomController* aApzc) const { + uint32_t i; + for (i = 0; i < Length(); ++i) { + if (mChain[i] == aApzc) { + break; + } + } + return i; +} + +void OverscrollHandoffChain::ForEachApzc(APZCMethod aMethod) const { + for (uint32_t i = 0; i < Length(); ++i) { + (mChain[i]->*aMethod)(); + } +} + +bool OverscrollHandoffChain::AnyApzc(APZCPredicate aPredicate) const { + MOZ_ASSERT(Length() > 0); + for (uint32_t i = 0; i < Length(); ++i) { + if ((mChain[i]->*aPredicate)()) { + return true; + } + } + return false; +} + +void OverscrollHandoffChain::FlushRepaints() const { + ForEachApzc(&AsyncPanZoomController::FlushRepaintForOverscrollHandoff); +} + +void OverscrollHandoffChain::CancelAnimations( + CancelAnimationFlags aFlags) const { + MOZ_ASSERT(Length() > 0); + for (uint32_t i = 0; i < Length(); ++i) { + mChain[i]->CancelAnimation(aFlags); + } +} + +void OverscrollHandoffChain::ClearOverscroll() const { + ForEachApzc(&AsyncPanZoomController::ClearOverscroll); +} + +void OverscrollHandoffChain::SnapBackOverscrolledApzc( + const AsyncPanZoomController* aStart) const { + uint32_t i = IndexOf(aStart); + for (; i < Length(); ++i) { + AsyncPanZoomController* apzc = mChain[i]; + if (!apzc->IsDestroyed()) { + apzc->SnapBackIfOverscrolled(); + } + } +} + +void OverscrollHandoffChain::SnapBackOverscrolledApzcForMomentum( + const AsyncPanZoomController* aStart, + const ParentLayerPoint& aVelocity) const { + uint32_t i = IndexOf(aStart); + for (; i < Length(); ++i) { + AsyncPanZoomController* apzc = mChain[i]; + if (!apzc->IsDestroyed()) { + apzc->SnapBackIfOverscrolledForMomentum(aVelocity); + } + } +} + +bool OverscrollHandoffChain::CanBePanned( + const AsyncPanZoomController* aApzc) const { + // Find |aApzc| in the handoff chain. + uint32_t i = IndexOf(aApzc); + + // See whether any APZC in the handoff chain starting from |aApzc| + // has room to be panned. + for (uint32_t j = i; j < Length(); ++j) { + if (mChain[j]->IsPannable()) { + return true; + } + } + + return false; +} + +bool OverscrollHandoffChain::CanScrollInDirection( + const AsyncPanZoomController* aApzc, ScrollDirection aDirection) const { + // Find |aApzc| in the handoff chain. + uint32_t i = IndexOf(aApzc); + + // See whether any APZC in the handoff chain starting from |aApzc| + // has room to scroll in the given direction. + for (uint32_t j = i; j < Length(); ++j) { + if (mChain[j]->CanScroll(aDirection)) { + return true; + } + } + + return false; +} + +bool OverscrollHandoffChain::HasOverscrolledApzc() const { + return AnyApzc(&AsyncPanZoomController::IsOverscrolled); +} + +bool OverscrollHandoffChain::HasFastFlungApzc() const { + return AnyApzc(&AsyncPanZoomController::IsFlingingFast); +} + +bool OverscrollHandoffChain::HasAutoscrollApzc() const { + return AnyApzc(&AsyncPanZoomController::IsAutoscroll); +} + +RefPtr OverscrollHandoffChain::FindFirstScrollable( + const InputData& aInput, ScrollDirections* aOutAllowedScrollDirections, + IncludeOverscroll aIncludeOverscroll) const { + // Start by allowing scrolling in both directions. As we do handoff + // overscroll-behavior may restrict one or both of the directions. + *aOutAllowedScrollDirections += ScrollDirection::eVertical; + *aOutAllowedScrollDirections += ScrollDirection::eHorizontal; + + for (size_t i = 0; i < Length(); i++) { + if (mChain[i]->CanScroll(aInput)) { + return mChain[i]; + } + + // If there is any directions we allow overscroll effects on the root + // content APZC (i.e. the overscroll-behavior of the root one is not + // `none`), we consider the APZC can be scrollable in terms of pan gestures + // because it causes overscrolling even if it's not able to scroll to the + // direction. + if (StaticPrefs::apz_overscroll_enabled() && bool(aIncludeOverscroll) && + // FIXME: Bug 1707491: Drop this pan gesture input check. + aInput.mInputType == PANGESTURE_INPUT && mChain[i]->IsRootContent()) { + // Check whether the root content APZC is also overscrollable governed by + // overscroll-behavior in the same directions where we allow scrolling + // handoff and where we are going to scroll, if it matches we do handoff + // to the root content APZC. + // In other words, if the root content is not scrollable, we don't + // handoff. + ScrollDirections allowedOverscrollDirections = + mChain[i]->GetOverscrollableDirections(); + ParentLayerPoint delta = mChain[i]->GetDeltaForEvent(aInput); + if (mChain[i]->IsZero(delta.x)) { + allowedOverscrollDirections -= ScrollDirection::eHorizontal; + } + if (mChain[i]->IsZero(delta.y)) { + allowedOverscrollDirections -= ScrollDirection::eVertical; + } + + allowedOverscrollDirections &= *aOutAllowedScrollDirections; + if (!allowedOverscrollDirections.isEmpty()) { + *aOutAllowedScrollDirections = allowedOverscrollDirections; + return mChain[i]; + } + } + + *aOutAllowedScrollDirections &= mChain[i]->GetAllowedHandoffDirections(); + if (aOutAllowedScrollDirections->isEmpty()) { + return nullptr; + } + } + return nullptr; +} + +std::tuple +OverscrollHandoffChain::ScrollingDownWillMoveDynamicToolbar( + const AsyncPanZoomController* aApzc) const { + MOZ_ASSERT(aApzc && !aApzc->IsRootContent(), + "Should be used for non-root APZC"); + + for (uint32_t i = IndexOf(aApzc); i < Length(); i++) { + if (mChain[i]->IsRootContent()) { + bool scrollable = mChain[i]->CanVerticalScrollWithDynamicToolbar(); + return {scrollable, scrollable ? mChain[i].get() : nullptr}; + } + + if (mChain[i]->CanScrollDownwards()) { + return {false, nullptr}; + } + } + + return {false, nullptr}; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/OverscrollHandoffState.h b/gfx/layers/apz/src/OverscrollHandoffState.h new file mode 100644 index 0000000000..90a22f259c --- /dev/null +++ b/gfx/layers/apz/src/OverscrollHandoffState.h @@ -0,0 +1,203 @@ +/* -*- 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_OverscrollHandoffChain_h +#define mozilla_layers_OverscrollHandoffChain_h + +#include +#include "mozilla/RefPtr.h" // for RefPtr +#include "nsISupportsImpl.h" // for NS_INLINE_DECL_THREADSAFE_REFCOUNTING +#include "APZUtils.h" // for CancelAnimationFlags +#include "mozilla/layers/LayersTypes.h" // for Layer::ScrollDirection +#include "Units.h" // for ScreenPoint + +namespace mozilla { + +class InputData; + +namespace layers { + +class AsyncPanZoomController; + +/** + * This class represents the chain of APZCs along which overscroll is handed + * off. It is created by APZCTreeManager by starting from an initial APZC which + * is the target for input events, and following the scroll parent ID links + * (often but not always corresponding to parent pointers in the APZC tree), + * then adjusting for scrollgrab. + */ +class OverscrollHandoffChain { + protected: + // Reference-counted classes cannot have public destructors. + ~OverscrollHandoffChain(); + + public: + // Threadsafe so that the controller and sampler threads can both maintain + // nsRefPtrs to the same handoff chain. + // Mutable so that we can pass around the class by + // RefPtr and thus enforce that, once built, + // the chain is not modified. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OverscrollHandoffChain) + + /* + * Methods for building the handoff chain. + * These should be used only by + * AsyncPanZoomController::BuildOverscrollHandoffChain(). + */ + void Add(AsyncPanZoomController* aApzc); + void SortByScrollPriority(); + + /* + * Methods for accessing the handoff chain. + */ + uint32_t Length() const { return mChain.size(); } + const RefPtr& GetApzcAtIndex(uint32_t aIndex) const; + // Returns Length() if |aApzc| is not on this chain. + uint32_t IndexOf(const AsyncPanZoomController* aApzc) const; + + /* + * Convenience methods for performing operations on APZCs in the chain. + */ + + // Flush repaints all the way up the chain. + void FlushRepaints() const; + + // Cancel animations all the way up the chain. + void CancelAnimations(CancelAnimationFlags aFlags = Default) const; + + // Clear overscroll all the way up the chain. + void ClearOverscroll() const; + + // Snap back the APZC that is overscrolled on the subset of the chain from + // |aStart| onwards, if any. + void SnapBackOverscrolledApzc(const AsyncPanZoomController* aStart) const; + + // Similar to above SnapbackOverscrolledApzc but for pan gestures with + // momentum events, this function doesn't end up calling each APZC's + // ScrollSnap. + // |aVelocity| is the initial velocity of |aStart|. + void SnapBackOverscrolledApzcForMomentum( + const AsyncPanZoomController* aStart, + const ParentLayerPoint& aVelocity) const; + + // Determine whether the given APZC, or any APZC further in the chain, + // has room to be panned. + bool CanBePanned(const AsyncPanZoomController* aApzc) const; + + // Determine whether the given APZC, or any APZC further in the chain, + // can scroll in the given direction. + bool CanScrollInDirection(const AsyncPanZoomController* aApzc, + ScrollDirection aDirection) const; + + // Determine whether any APZC along this handoff chain is overscrolled. + bool HasOverscrolledApzc() const; + + // Determine whether any APZC along this handoff chain has been flung fast. + bool HasFastFlungApzc() const; + + // Determine whether any APZC along this handoff chain is autoscroll. + bool HasAutoscrollApzc() const; + + // Find the first APZC in this handoff chain that can be scrolled by |aInput|. + // Since overscroll-behavior can restrict handoff in some directions, + // |aOutAllowedScrollDirections| is populated with the scroll directions + // in which scrolling of the returned APZC is allowed. + // |aIncludeOverscroll| is an optional flag whether to consider overscrollable + // as scrollable or not. + enum class IncludeOverscroll : bool { No, Yes }; + RefPtr FindFirstScrollable( + const InputData& aInput, ScrollDirections* aOutAllowedScrollDirections, + IncludeOverscroll aIncludeOverscroll = IncludeOverscroll::Yes) const; + + // Return a pair of true and the root content APZC if all non-root APZCs in + // this handoff chain starting from |aApzc| are not able to scroll downwards + // (i.e. there is no room to scroll downwards in each APZC respectively) and + // there is any contents covered by the dynamic toolbar, otherwise return a + // pair of false and nullptr. + std::tuple + ScrollingDownWillMoveDynamicToolbar( + const AsyncPanZoomController* aApzc) const; + + private: + std::vector> mChain; + + typedef void (AsyncPanZoomController::*APZCMethod)(); + typedef bool (AsyncPanZoomController::*APZCPredicate)() const; + void ForEachApzc(APZCMethod aMethod) const; + bool AnyApzc(APZCPredicate aPredicate) const; +}; + +/** + * This class groups the state maintained during overscroll handoff. + */ +struct OverscrollHandoffState { + OverscrollHandoffState(const OverscrollHandoffChain& aChain, + const ScreenPoint& aPanDistance, + ScrollSource aScrollSource) + : mChain(aChain), + mChainIndex(0), + mPanDistance(aPanDistance), + mScrollSource(aScrollSource) {} + + // The chain of APZCs along which we hand off scroll. + // This is const to indicate that the chain does not change over the + // course of handoff. + const OverscrollHandoffChain& mChain; + + // The index of the APZC in the chain that we are currently giving scroll to. + // This is non-const to indicate that this changes over the course of handoff. + uint32_t mChainIndex; + + // The total distance since touch-start of the pan that triggered the + // handoff. This is const to indicate that it does not change over the + // course of handoff. + // The x/y components of this are non-negative. + const ScreenPoint mPanDistance; + + ScrollSource mScrollSource; + + // The total amount of actual movement that this scroll caused, including + // scrolling and changes to overscroll. This starts at zero and is accumulated + // over the course of the handoff. + ScreenPoint mTotalMovement; +}; + +/* + * This class groups the state maintained during fling handoff. + */ +struct FlingHandoffState { + // The velocity of the fling being handed off. + ParentLayerPoint mVelocity; + + // The chain of APZCs along which we hand off the fling. + // Unlike in OverscrollHandoffState, this is stored by RefPtr because + // otherwise it may not stay alive for the entire handoff. + RefPtr mChain; + + // The time duration between the touch start and the touch move that started + // the pan gesture which triggered this fling. In other words, the time it + // took for the finger to move enough to cross the touch slop threshold. + // Nothing if this fling was not immediately caused by a touch pan. + Maybe mTouchStartRestingTime; + + // The slowest panning velocity encountered during the pan that triggered this + // fling. + ParentLayerCoord mMinPanVelocity; + + // Whether handoff has happened by this point, or we're still process + // the original fling. + bool mIsHandoff; + + // The single APZC that was scrolled by the pan that started this fling. + // The fling is only allowed to scroll this APZC, too. + // Used only if immediate scroll handoff is disallowed. + RefPtr mScrolledApzc; +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_OverscrollHandoffChain_h */ diff --git a/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.cpp new file mode 100644 index 0000000000..f3d5538ba0 --- /dev/null +++ b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.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 "PotentialCheckerboardDurationTracker.h" + +#include "mozilla/Telemetry.h" // for Telemetry + +namespace mozilla { +namespace layers { + +PotentialCheckerboardDurationTracker::PotentialCheckerboardDurationTracker() + : mInCheckerboard(false), mInTransform(false) {} + +void PotentialCheckerboardDurationTracker::CheckerboardSeen() { + // This might get called while mInCheckerboard is already true + if (!Tracking()) { + mCurrentPeriodStart = TimeStamp::Now(); + } + mInCheckerboard = true; +} + +void PotentialCheckerboardDurationTracker::CheckerboardDone( + bool aRecordTelemetry) { + MOZ_ASSERT(Tracking()); + mInCheckerboard = false; + if (!Tracking()) { + if (aRecordTelemetry) { + mozilla::Telemetry::AccumulateTimeDelta( + mozilla::Telemetry::CHECKERBOARD_POTENTIAL_DURATION, + mCurrentPeriodStart); + } + } +} + +void PotentialCheckerboardDurationTracker::InTransform(bool aInTransform, + bool aRecordTelemetry) { + if (aInTransform == mInTransform) { + // no-op + return; + } + + if (!Tracking()) { + // Because !Tracking(), mInTransform must be false, and so aInTransform + // must be true (or we would have early-exited this function already). + // Therefore, we are starting a potential checkerboard period. + mInTransform = aInTransform; + mCurrentPeriodStart = TimeStamp::Now(); + return; + } + + mInTransform = aInTransform; + + if (!Tracking()) { + // Tracking() must have been true at the start of this function, or we + // would have taken the other !Tracking branch above. If it's false now, + // it means we just stopped tracking, so we are ending a potential + // checkerboard period. + if (aRecordTelemetry) { + mozilla::Telemetry::AccumulateTimeDelta( + mozilla::Telemetry::CHECKERBOARD_POTENTIAL_DURATION, + mCurrentPeriodStart); + } + } +} + +bool PotentialCheckerboardDurationTracker::Tracking() const { + return mInTransform || mInCheckerboard; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h new file mode 100644 index 0000000000..58786f32af --- /dev/null +++ b/gfx/layers/apz/src/PotentialCheckerboardDurationTracker.h @@ -0,0 +1,61 @@ +/* -*- 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_PotentialCheckerboardDurationTracker_h +#define mozilla_layers_PotentialCheckerboardDurationTracker_h + +#include "mozilla/TimeStamp.h" + +namespace mozilla { +namespace layers { + +/** + * This class allows the owner to track the duration of time considered + * "potentially checkerboarding". This is the union of two possibly-intersecting + * sets of time periods. The first set is that in which checkerboarding was + * actually happening, since by definition it could potentially be happening. + * The second set is that in which the APZC is actively transforming content + * in the compositor, since it could potentially transform it so as to display + * checkerboarding to the user. + * The caller of this class calls the appropriate methods to indicate the start + * and stop of these two sets, and this class manages accumulating the union + * of the various durations. + */ +class PotentialCheckerboardDurationTracker { + public: + PotentialCheckerboardDurationTracker(); + + /** + * This should be called if checkerboarding is encountered. It can be called + * multiple times during a checkerboard event. + */ + void CheckerboardSeen(); + /** + * This should be called when checkerboarding is done. It must have been + * preceded by one or more calls to CheckerboardSeen(). + */ + void CheckerboardDone(bool aRecordTelemetry); + + /** + * This should be called at composition time, to indicate if the APZC is in + * a transforming state or not. + */ + void InTransform(bool aInTransform, bool aRecordTelemetry); + + private: + bool Tracking() const; + + private: + bool mInCheckerboard; + bool mInTransform; + + TimeStamp mCurrentPeriodStart; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_PotentialCheckerboardDurationTracker_h diff --git a/gfx/layers/apz/src/QueuedInput.cpp b/gfx/layers/apz/src/QueuedInput.cpp new file mode 100644 index 0000000000..87ffe7250e --- /dev/null +++ b/gfx/layers/apz/src/QueuedInput.cpp @@ -0,0 +1,44 @@ +/* -*- 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 "QueuedInput.h" + +#include "AsyncPanZoomController.h" +#include "InputBlockState.h" +#include "InputData.h" +#include "OverscrollHandoffState.h" + +namespace mozilla { +namespace layers { + +QueuedInput::QueuedInput(const MultiTouchInput& aInput, TouchBlockState& aBlock) + : mInput(MakeUnique(aInput)), mBlock(&aBlock) {} + +QueuedInput::QueuedInput(const ScrollWheelInput& aInput, + WheelBlockState& aBlock) + : mInput(MakeUnique(aInput)), mBlock(&aBlock) {} + +QueuedInput::QueuedInput(const MouseInput& aInput, DragBlockState& aBlock) + : mInput(MakeUnique(aInput)), mBlock(&aBlock) {} + +QueuedInput::QueuedInput(const PanGestureInput& aInput, + PanGestureBlockState& aBlock) + : mInput(MakeUnique(aInput)), mBlock(&aBlock) {} + +QueuedInput::QueuedInput(const PinchGestureInput& aInput, + PinchGestureBlockState& aBlock) + : mInput(MakeUnique(aInput)), mBlock(&aBlock) {} + +QueuedInput::QueuedInput(const KeyboardInput& aInput, + KeyboardBlockState& aBlock) + : mInput(MakeUnique(aInput)), mBlock(&aBlock) {} + +InputData* QueuedInput::Input() { return mInput.get(); } + +InputBlockState* QueuedInput::Block() { return mBlock.get(); } + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/QueuedInput.h b/gfx/layers/apz/src/QueuedInput.h new file mode 100644 index 0000000000..fcc2f2090a --- /dev/null +++ b/gfx/layers/apz/src/QueuedInput.h @@ -0,0 +1,63 @@ +/* -*- 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_QueuedInput_h +#define mozilla_layers_QueuedInput_h + +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { + +class InputData; +class MultiTouchInput; +class ScrollWheelInput; +class MouseInput; +class PanGestureInput; +class PinchGestureInput; +class KeyboardInput; + +namespace layers { + +class InputBlockState; +class TouchBlockState; +class WheelBlockState; +class DragBlockState; +class PanGestureBlockState; +class PinchGestureBlockState; +class KeyboardBlockState; + +/** + * This lightweight class holds a pointer to an input event that has not yet + * been completely processed, along with the input block that the input event + * is associated with. + */ +class QueuedInput { + public: + QueuedInput(const MultiTouchInput& aInput, TouchBlockState& aBlock); + QueuedInput(const ScrollWheelInput& aInput, WheelBlockState& aBlock); + QueuedInput(const MouseInput& aInput, DragBlockState& aBlock); + QueuedInput(const PanGestureInput& aInput, PanGestureBlockState& aBlock); + QueuedInput(const PinchGestureInput& aInput, PinchGestureBlockState& aBlock); + QueuedInput(const KeyboardInput& aInput, KeyboardBlockState& aBlock); + + InputData* Input(); + InputBlockState* Block(); + + private: + // A copy of the input event that is provided to the constructor. This must + // be non-null, and is owned by this QueuedInput instance (hence the + // UniquePtr). + UniquePtr mInput; + // A pointer to the block that the input event is associated with. This must + // be non-null. + RefPtr mBlock; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_QueuedInput_h diff --git a/gfx/layers/apz/src/RecentEventsBuffer.h b/gfx/layers/apz/src/RecentEventsBuffer.h new file mode 100644 index 0000000000..d1ae5797af --- /dev/null +++ b/gfx/layers/apz/src/RecentEventsBuffer.h @@ -0,0 +1,83 @@ +/* -*- 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_RecentEventsBuffer_h +#define mozilla_layers_RecentEventsBuffer_h + +#include + +#include "mozilla/TimeStamp.h" + +namespace mozilla { +namespace layers { +/** + * RecentEventsBuffer: maintains an age constrained buffer of events + * + * Intended for use with elements of type InputData, but the only requirement + * is a member "mTimeStamp" of type TimeStamp + */ +template +class RecentEventsBuffer { + public: + explicit RecentEventsBuffer(TimeDuration maxAge); + + void push(Event event); + void clear(); + + typedef typename std::deque::size_type size_type; + size_type size() { return mBuffer.size(); } + + // Delegate to container for iterators + typedef typename std::deque::iterator iterator; + typedef typename std::deque::const_iterator const_iterator; + iterator begin() { return mBuffer.begin(); } + iterator end() { return mBuffer.end(); } + const_iterator cbegin() const { return mBuffer.cbegin(); } + const_iterator cend() const { return mBuffer.cend(); } + + // Also delegate for front/back + typedef typename std::deque::reference reference; + typedef typename std::deque::const_reference const_reference; + reference front() { return mBuffer.front(); } + reference back() { return mBuffer.back(); } + const_reference front() const { return mBuffer.front(); } + const_reference back() const { return mBuffer.back(); } + + private: + TimeDuration mMaxAge; + std::deque mBuffer; +}; + +template +RecentEventsBuffer::RecentEventsBuffer(TimeDuration maxAge) + : mMaxAge(maxAge), mBuffer() {} + +template +void RecentEventsBuffer::push(Event event) { + // Events must be pushed in chronological order + MOZ_ASSERT(mBuffer.empty() || mBuffer.back().mTimeStamp <= event.mTimeStamp); + + mBuffer.push_back(event); + + // Flush all events older than the given lifetime + TimeStamp bound = event.mTimeStamp - mMaxAge; + while (!mBuffer.empty()) { + if (mBuffer.front().mTimeStamp >= bound) { + break; + } + mBuffer.pop_front(); + } +} + +template +void RecentEventsBuffer::clear() { + mBuffer.clear(); +} + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_RecentEventsBuffer_h diff --git a/gfx/layers/apz/src/SampledAPZCState.cpp b/gfx/layers/apz/src/SampledAPZCState.cpp new file mode 100644 index 0000000000..712a46a3b1 --- /dev/null +++ b/gfx/layers/apz/src/SampledAPZCState.cpp @@ -0,0 +1,111 @@ +/* -*- 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 "SampledAPZCState.h" +#include "APZUtils.h" + +namespace mozilla { +namespace layers { + +SampledAPZCState::SampledAPZCState() {} + +SampledAPZCState::SampledAPZCState(const FrameMetrics& aMetrics) + : mLayoutViewport(aMetrics.GetLayoutViewport()), + mVisualScrollOffset(aMetrics.GetVisualScrollOffset()), + mZoom(aMetrics.GetZoom()) { + RemoveFractionalAsyncDelta(); +} + +SampledAPZCState::SampledAPZCState(const FrameMetrics& aMetrics, + Maybe&& aPayload, + APZScrollGeneration aGeneration) + : mLayoutViewport(aMetrics.GetLayoutViewport()), + mVisualScrollOffset(aMetrics.GetVisualScrollOffset()), + mZoom(aMetrics.GetZoom()), + mScrollPayload(std::move(aPayload)), + mGeneration(aGeneration) { + RemoveFractionalAsyncDelta(); +} + +bool SampledAPZCState::operator==(const SampledAPZCState& aOther) const { + // The payload doesn't factor into equality, that just comes along for + // the ride. + return mLayoutViewport.IsEqualEdges(aOther.mLayoutViewport) && + mVisualScrollOffset == aOther.mVisualScrollOffset && + mZoom == aOther.mZoom; +} + +bool SampledAPZCState::operator!=(const SampledAPZCState& aOther) const { + return !(*this == aOther); +} + +Maybe SampledAPZCState::TakeScrollPayload() { + return std::move(mScrollPayload); +} + +void SampledAPZCState::UpdateScrollProperties(const FrameMetrics& aMetrics) { + mLayoutViewport = aMetrics.GetLayoutViewport(); + mVisualScrollOffset = aMetrics.GetVisualScrollOffset(); +} + +void SampledAPZCState::UpdateScrollPropertiesWithRelativeDelta( + const FrameMetrics& aMetrics, const CSSPoint& aRelativeDelta) { + mVisualScrollOffset += aRelativeDelta; + KeepLayoutViewportEnclosingVisualViewport(aMetrics); +} + +void SampledAPZCState::UpdateZoomProperties(const FrameMetrics& aMetrics) { + mZoom = aMetrics.GetZoom(); +} + +void SampledAPZCState::ClampVisualScrollOffset(const FrameMetrics& aMetrics) { + // Make sure that we use the local mZoom to do these calculations, because the + // one on aMetrics might be newer. + CSSRect scrollRange = FrameMetrics::CalculateScrollRange( + aMetrics.GetScrollableRect(), aMetrics.GetCompositionBounds(), mZoom); + mVisualScrollOffset = scrollRange.ClampPoint(mVisualScrollOffset); + + KeepLayoutViewportEnclosingVisualViewport(aMetrics); +} + +void SampledAPZCState::ZoomBy(float aScale) { mZoom.scale *= aScale; } + +void SampledAPZCState::RemoveFractionalAsyncDelta() { + // This function is a performance hack. With non-WebRender, having small + // fractional deltas between the layout offset and scroll offset on + // container layers can trigger the creation of a temporary surface during + // composition, because it produces a non-integer translation that doesn't + // play well with layer clips. So we detect the case where the delta is + // uselessly small (0.01 parentlayer pixels or less) and tweak the sampled + // scroll offset to eliminate it. By doing this here at sample time rather + // than elsewhere in the pipeline we are least likely to break assumptions + // and invariants elsewhere in the code, since sampling effectively takes + // a snapshot of APZ state (decoupling it from APZ assumptions) and provides + // it as an input to the compositor (so all compositor state should be + // internally consistent based on this input). + if (mLayoutViewport.TopLeft() == mVisualScrollOffset) { + return; + } + const ParentLayerCoord EPSILON = 0.01; + ParentLayerPoint paintedOffset = mLayoutViewport.TopLeft() * mZoom; + ParentLayerPoint asyncOffset = mVisualScrollOffset * mZoom; + if (FuzzyEqualsAdditive(paintedOffset.x, asyncOffset.x, EPSILON) && + FuzzyEqualsAdditive(paintedOffset.y, asyncOffset.y, EPSILON)) { + mVisualScrollOffset = mLayoutViewport.TopLeft(); + } +} + +void SampledAPZCState::KeepLayoutViewportEnclosingVisualViewport( + const FrameMetrics& aMetrics) { + FrameMetrics::KeepLayoutViewportEnclosingVisualViewport( + CSSRect(mVisualScrollOffset, + FrameMetrics::CalculateCompositedSizeInCssPixels( + aMetrics.GetCompositionBounds(), mZoom)), + aMetrics.GetScrollableRect(), mLayoutViewport); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/SampledAPZCState.h b/gfx/layers/apz/src/SampledAPZCState.h new file mode 100644 index 0000000000..a521eeaf87 --- /dev/null +++ b/gfx/layers/apz/src/SampledAPZCState.h @@ -0,0 +1,72 @@ +/* -*- 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_SampledAPZCState_h +#define mozilla_layers_SampledAPZCState_h + +#include "FrameMetrics.h" +#include "mozilla/Maybe.h" +#include "mozilla/ScrollGeneration.h" + +namespace mozilla { +namespace layers { + +class SampledAPZCState { + public: + SampledAPZCState(); + explicit SampledAPZCState(const FrameMetrics& aMetrics); + SampledAPZCState(const FrameMetrics& aMetrics, + Maybe&& aPayload, + APZScrollGeneration aGeneration); + + bool operator==(const SampledAPZCState& aOther) const; + bool operator!=(const SampledAPZCState& aOther) const; + + CSSRect GetLayoutViewport() const { return mLayoutViewport; } + CSSPoint GetVisualScrollOffset() const { return mVisualScrollOffset; } + CSSToParentLayerScale GetZoom() const { return mZoom; } + Maybe TakeScrollPayload(); + const APZScrollGeneration& Generation() const { return mGeneration; } + + void UpdateScrollProperties(const FrameMetrics& aMetrics); + void UpdateScrollPropertiesWithRelativeDelta(const FrameMetrics& aMetrics, + const CSSPoint& aRelativeDelta); + + void UpdateZoomProperties(const FrameMetrics& aMetrics); + + /** + * Re-clamp mVisualScrollOffset to the scroll range specified by the provided + * metrics. This only needs to be called if the scroll offset changes + * outside of AsyncPanZoomController::SampleCompositedAsyncTransform(). + * It also recalculates mLayoutViewport so that it continues to enclose + * the visual viewport. This only needs to be called if the + * layout viewport changes outside of SampleCompositedAsyncTransform(). + */ + void ClampVisualScrollOffset(const FrameMetrics& aMetrics); + + void ZoomBy(float aScale); + + private: + // These variables cache the layout viewport, scroll offset, and zoom stored + // in |Metrics()| at the time this class was constructed. + CSSRect mLayoutViewport; + CSSPoint mVisualScrollOffset; + CSSToParentLayerScale mZoom; + // An optional payload that rides along with the sampled state. + Maybe mScrollPayload; + APZScrollGeneration mGeneration; + + void RemoveFractionalAsyncDelta(); + // A handy wrapper to call + // FrameMetrics::KeepLayoutViewportEnclosingVisualViewport with this + // SampledAPZCState and the given |aMetrics|. + void KeepLayoutViewportEnclosingVisualViewport(const FrameMetrics& aMetrics); +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_SampledAPZCState_h diff --git a/gfx/layers/apz/src/ScrollThumbUtils.cpp b/gfx/layers/apz/src/ScrollThumbUtils.cpp new file mode 100644 index 0000000000..814fa59759 --- /dev/null +++ b/gfx/layers/apz/src/ScrollThumbUtils.cpp @@ -0,0 +1,341 @@ +/* -*- 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 "ScrollThumbUtils.h" +#include "AsyncPanZoomController.h" +#include "FrameMetrics.h" +#include "UnitTransforms.h" +#include "Units.h" +#include "gfxPlatform.h" +#include "mozilla/gfx/Matrix.h" +#include "mozilla/StaticPrefs_toolkit.h" + +namespace mozilla { +namespace layers { +namespace apz { + +struct AsyncScrollThumbTransformer { + // Inputs + const LayerToParentLayerMatrix4x4& mCurrentTransform; + const gfx::Matrix4x4& mScrollableContentTransform; + AsyncPanZoomController* mApzc; + const FrameMetrics& mMetrics; + const ScrollbarData& mScrollbarData; + bool mScrollbarIsDescendant; + + // Intermediate results + AsyncTransformComponentMatrix mAsyncTransform; + AsyncTransformComponentMatrix mScrollbarTransform; + + LayerToParentLayerMatrix4x4 ComputeTransform(); + + private: + // Helper functions for ComputeTransform(). + + // If the thumb's orientation is along |aAxis|, add transformations + // of the thumb into |mScrollbarTransform|. + void ApplyTransformForAxis(const Axis& aAxis); + + enum class ScrollThumbExtent { Start, End }; + + // Scale the thumb by |aScale| along |aAxis|, while keeping constant the + // position of the top denoted by |aExtent|. + void ScaleThumbBy(const Axis& aAxis, float aScale, ScrollThumbExtent aExtent); + + // Translate the thumb along |aAxis| by |aTranslation| in "scrollbar space" + // (CSS pixels along the scrollbar track, similar to e.g. + // |mScrollbarData.mThumbStart|). + void TranslateThumb(const Axis& aAxis, OuterCSSCoord aTranslation); +}; + +void AsyncScrollThumbTransformer::TranslateThumb(const Axis& aAxis, + OuterCSSCoord aTranslation) { + aAxis.PostTranslate( + mScrollbarTransform, + ViewAs(aTranslation, + PixelCastJustification::CSSPixelsOfSurroundingContent) * + mMetrics.GetDevPixelsPerCSSPixel() * + LayoutDeviceToParentLayerScale(1.0)); +} + +void AsyncScrollThumbTransformer::ScaleThumbBy(const Axis& aAxis, float aScale, + ScrollThumbExtent aExtent) { + // To keep the position of the top of the thumb constant, the thumb needs to + // translated to compensate for the scale applied. The origin with respect to + // which the scale is applied is the origin of the layer tree, rather than + // the origin of the scroll thumb. This means that the space between the + // origin and the top of thumb (including the part of the scrollbar track + // above the thumb, the part of the scrollbar above the track (i.e. a + // scrollbar button if present), plus whatever content is above the scroll + // frame) is scaled too, effectively translating the thumb. We undo that + // translation here. (One can think of the adjustment being done to the + // translation here as a change of basis. We have a method to help with that, + // Matrix4x4::ChangeBasis(), but it wouldn't necessarily make the code cleaner + // in this case). + const OuterCSSCoord scrollTrackOrigin = + aAxis.GetPointOffset( + mMetrics.CalculateCompositionBoundsInOuterCssPixels().TopLeft()) + + mScrollbarData.mScrollTrackStart; + OuterCSSCoord thumbExtent = scrollTrackOrigin + mScrollbarData.mThumbStart; + if (aExtent == ScrollThumbExtent::End) { + thumbExtent += mScrollbarData.mThumbLength; + } + const OuterCSSCoord thumbExtentScaled = thumbExtent * aScale; + const OuterCSSCoord thumbExtentDelta = thumbExtentScaled - thumbExtent; + + aAxis.PostScale(mScrollbarTransform, aScale); + TranslateThumb(aAxis, -thumbExtentDelta); +} + +void AsyncScrollThumbTransformer::ApplyTransformForAxis(const Axis& aAxis) { + ParentLayerCoord asyncScroll = aAxis.GetTransformTranslation(mAsyncTransform); + const float asyncZoom = aAxis.GetTransformScale(mAsyncTransform); + const ParentLayerCoord overscroll = + aAxis.GetPointOffset(mApzc->GetOverscrollAmount()); + + bool haveAsyncZoom = !FuzzyEqualsAdditive(asyncZoom, 1.f); + if (!haveAsyncZoom && mApzc->IsZero(asyncScroll) && + mApzc->IsZero(overscroll)) { + return; + } + + OuterCSSCoord translation; + float scale = 1.0; + + bool recalcMode = StaticPrefs::apz_scrollthumb_recalc(); + if (recalcMode) { + // In this branch (taken when apz.scrollthumb.recalc=true), |translation| + // and |scale| are computed using the approach implemented in bug 1554795 + // of fully recalculating the desired position and size using the logic + // that attempts to closely match the main-thread calculation. + + const CSSRect visualViewportRect = mApzc->GetCurrentAsyncVisualViewport( + AsyncPanZoomController::eForCompositing); + const CSSCoord visualViewportLength = + aAxis.GetRectLength(visualViewportRect); + + const CSSCoord maxMinPosDifference = + CSSCoord( + aAxis.GetRectLength(mMetrics.GetScrollableRect()).Truncated()) - + visualViewportLength; + + OuterCSSCoord effectiveThumbLength = mScrollbarData.mThumbLength; + + if (haveAsyncZoom) { + // The calculations here closely follow the main thread calculations at + // https://searchfox.org/mozilla-central/rev/0bf957f909ae1f3d19b43fd4edfc277342554836/layout/generic/nsGfxScrollFrame.cpp#6902-6927 + // and + // https://searchfox.org/mozilla-central/rev/0bf957f909ae1f3d19b43fd4edfc277342554836/layout/xul/nsSliderFrame.cpp#587-614 + // Any modifications there should be reflected here as well. + const CSSCoord pageIncrementMin = + static_cast(visualViewportLength * 0.8); + CSSCoord pageIncrement; + + CSSToLayoutDeviceScale deviceScale = mMetrics.GetDevPixelsPerCSSPixel(); + if (*mScrollbarData.mDirection == ScrollDirection::eVertical) { + const CSSCoord lineScrollAmount = + (mApzc->GetScrollMetadata().GetLineScrollAmount() / deviceScale) + .height; + const double kScrollMultiplier = + StaticPrefs::toolkit_scrollbox_verticalScrollDistance(); + CSSCoord increment = lineScrollAmount * kScrollMultiplier; + + pageIncrement = + std::max(visualViewportLength - increment, pageIncrementMin); + } else { + pageIncrement = pageIncrementMin; + } + + float ratio = pageIncrement / (maxMinPosDifference + pageIncrement); + + OuterCSSCoord desiredThumbLength{ + std::max(mScrollbarData.mThumbMinLength, + mScrollbarData.mScrollTrackLength * ratio)}; + + // Round the thumb length to an integer number of LayoutDevice pixels, to + // match the main-thread behaviour. + auto outerDeviceScale = ViewAs( + deviceScale, PixelCastJustification::CSSPixelsOfSurroundingContent); + desiredThumbLength = + LayoutDeviceCoord((desiredThumbLength * outerDeviceScale).Rounded()) / + outerDeviceScale; + + effectiveThumbLength = desiredThumbLength; + + scale = desiredThumbLength / mScrollbarData.mThumbLength; + } + + // Subtracting the offset of the scrollable rect is needed for right-to-left + // pages. + const CSSCoord curPos = aAxis.GetRectOffset(visualViewportRect) - + aAxis.GetRectOffset(mMetrics.GetScrollableRect()); + + const CSSToOuterCSSScale thumbPosRatio( + (maxMinPosDifference != 0) + ? float((mScrollbarData.mScrollTrackLength - effectiveThumbLength) / + maxMinPosDifference) + : 1.f); + + const OuterCSSCoord desiredThumbPos = curPos * thumbPosRatio; + + translation = desiredThumbPos - mScrollbarData.mThumbStart; + } else { + // In this branch (taken when apz.scrollthumb.recalc=false), |translation| + // and |scale| are computed using the pre-bug1554795 approach of turning + // the async scroll and zoom deltas into transforms to apply to the + // main-thread thumb position and size. + + // The scroll thumb needs to be scaled in the direction of scrolling by the + // inverse of the async zoom. This is because zooming in decreases the + // fraction of the whole srollable rect that is in view. + scale = 1.f / asyncZoom; + + // Note: |metrics.GetZoom()| doesn't yet include the async zoom. + CSSToParentLayerScale effectiveZoom = + CSSToParentLayerScale(mMetrics.GetZoom().scale * asyncZoom); + + if (gfxPlatform::UseDesktopZoomingScrollbars()) { + // As computed by GetCurrentAsyncTransform, asyncScrollY is + // asyncScrollY = -(GetEffectiveScrollOffset - + // mLastContentPaintMetrics.GetLayoutScrollOffset()) * + // effectiveZoom + // where GetEffectiveScrollOffset includes the visual viewport offset that + // the main thread knows about plus any async scrolling to the visual + // viewport offset that the main thread does not (yet) know about. We want + // asyncScrollY to be + // asyncScrollY = -(GetEffectiveScrollOffset - + // mLastContentPaintMetrics.GetVisualScrollOffset()) * effectiveZoom + // because the main thread positions the scrollbars at the visual viewport + // offset that it knows about. (aMetrics is mLastContentPaintMetrics) + + asyncScroll -= aAxis.GetPointOffset((mMetrics.GetLayoutScrollOffset() - + mMetrics.GetVisualScrollOffset()) * + effectiveZoom); + } + + // Here we convert the scrollbar thumb ratio into a true unitless ratio by + // dividing out the conversion factor from the scrollframe's parent's space + // to the scrollframe's space. + float unitlessThumbRatio = mScrollbarData.mThumbRatio / + (mMetrics.GetPresShellResolution() * asyncZoom); + + // The scroll thumb needs to be translated in opposite direction of the + // async scroll. This is because scrolling down, which translates the layer + // content up, should result in moving the scroll thumb down. + ParentLayerCoord translationPL = -asyncScroll * unitlessThumbRatio; + + // The translation we computed is in the scroll frame's ParentLayer space. + // This includes the full cumulative resolution, even if we are a subframe. + // However, the resulting transform is used in a context where the scrollbar + // is already subject to the resolutions of enclosing scroll frames. To + // avoid double application of these enclosing resolutions, divide them out, + // leaving only the local resolution if any. + translationPL /= (mMetrics.GetCumulativeResolution().scale / + mMetrics.GetPresShellResolution()); + + // Convert translation to CSS pixels as this is what TranslateThumb expects. + translation = ViewAs( + translationPL / (mMetrics.GetDevPixelsPerCSSPixel() * + LayoutDeviceToParentLayerScale(1.0)), + PixelCastJustification::CSSPixelsOfSurroundingContent); + } + + // When scaling the thumb to account for the async zoom, keep the position + // of the start of the thumb (which corresponds to the scroll offset) + // constant. + if (haveAsyncZoom) { + ScaleThumbBy(aAxis, scale, ScrollThumbExtent::Start); + } + + // If the page is overscrolled, additionally squish the thumb in accordance + // with the overscroll amount. + if (overscroll != 0) { + float overscrollScale = + 1.0f - (std::abs(overscroll.value) / + aAxis.GetRectLength(mMetrics.GetCompositionBounds())); + MOZ_ASSERT(overscrollScale > 0.0f && overscrollScale <= 1.0f); + // If we're overscrolled at the top, keep the top of the thumb in place + // as we squish it. If we're overscrolled at the bottom, keep the bottom of + // the thumb in place. + ScaleThumbBy( + aAxis, overscrollScale, + overscroll < 0 ? ScrollThumbExtent::Start : ScrollThumbExtent::End); + } + + TranslateThumb(aAxis, translation); +} + +LayerToParentLayerMatrix4x4 AsyncScrollThumbTransformer::ComputeTransform() { + // We only apply the transform if the scroll-target layer has non-container + // children (i.e. when it has some possibly-visible content). This is to + // avoid moving scroll-bars in the situation that only a scroll information + // layer has been built for a scroll frame, as this would result in a + // disparity between scrollbars and visible content. + if (mMetrics.IsScrollInfoLayer()) { + return LayerToParentLayerMatrix4x4{}; + } + + MOZ_RELEASE_ASSERT(mApzc); + + mAsyncTransform = + mApzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing); + + // |mAsyncTransform| represents the amount by which we have scrolled and + // zoomed since the last paint. Because the scrollbar was sized and positioned + // based on the painted content, we need to adjust it based on asyncTransform + // so that it reflects what the user is actually seeing now. + if (*mScrollbarData.mDirection == ScrollDirection::eVertical) { + ApplyTransformForAxis(mApzc->mY); + } + if (*mScrollbarData.mDirection == ScrollDirection::eHorizontal) { + ApplyTransformForAxis(mApzc->mX); + } + + LayerToParentLayerMatrix4x4 transform = + mCurrentTransform * mScrollbarTransform; + + AsyncTransformComponentMatrix compensation; + // If the scrollbar layer is a child of the content it is a scrollbar for, + // then we need to adjust for any async transform (including an overscroll + // transform) on the content. This needs to be cancelled out because layout + // positions and sizes the scrollbar on the assumption that there is no async + // transform, and without this adjustment the scrollbar will end up in the + // wrong place. + // + // Note that since the async transform is applied on top of the content's + // regular transform, we need to make sure to unapply the async transform in + // the same coordinate space. This requires applying the content transform + // and then unapplying it after unapplying the async transform. + if (mScrollbarIsDescendant) { + AsyncTransformComponentMatrix overscroll = + mApzc->GetOverscrollTransform(AsyncPanZoomController::eForCompositing); + gfx::Matrix4x4 asyncUntransform = + (mAsyncTransform * overscroll).Inverse().ToUnknownMatrix(); + const gfx::Matrix4x4& contentTransform = mScrollableContentTransform; + gfx::Matrix4x4 contentUntransform = contentTransform.Inverse(); + + compensation *= ViewAs( + contentTransform * asyncUntransform * contentUntransform); + } + transform = transform * compensation; + + return transform; +} + +LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb( + const LayerToParentLayerMatrix4x4& aCurrentTransform, + const gfx::Matrix4x4& aScrollableContentTransform, + AsyncPanZoomController* aApzc, const FrameMetrics& aMetrics, + const ScrollbarData& aScrollbarData, bool aScrollbarIsDescendant) { + return AsyncScrollThumbTransformer{ + aCurrentTransform, aScrollableContentTransform, aApzc, aMetrics, + aScrollbarData, aScrollbarIsDescendant} + .ComputeTransform(); +} + +} // namespace apz +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/ScrollThumbUtils.h b/gfx/layers/apz/src/ScrollThumbUtils.h new file mode 100644 index 0000000000..49a45de3a7 --- /dev/null +++ b/gfx/layers/apz/src/ScrollThumbUtils.h @@ -0,0 +1,51 @@ +/* -*- 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_ScrollThumbUtils_h +#define mozilla_layers_ScrollThumbUtils_h + +#include "LayersTypes.h" +#include "Units.h" + +namespace mozilla { +namespace layers { + +class AsyncPanZoomController; + +struct FrameMetrics; +struct ScrollbarData; + +namespace apz { +/** + * Compute the updated shadow transform for a scroll thumb layer that + * reflects async scrolling of the associated scroll frame. + * + * @param aCurrentTransform The current shadow transform on the scroll thumb + * layer, as returned by Layer::GetLocalTransform() or similar. + * @param aScrollableContentTransform The current content transform on the + * scrollable content, as returned by Layer::GetTransform(). + * @param aApzc The APZC that scrolls the scroll frame. + * @param aMetrics The metrics associated with the scroll frame, reflecting + * the last paint of the associated content. Note: this metrics should + * NOT reflect async scrolling or zooming, i.e. they should be the layer + * tree's copy of the metrics, or APZC's last-content-paint metrics. + * @param aScrollbarData The scrollbar data for the the scroll thumb layer. + * @param aScrollbarIsDescendant True iff. the scroll thumb layer is a + * descendant of the layer bearing the scroll frame's metrics. + * @return The new shadow transform for the scroll thumb layer, including + * any pre- or post-scales. + */ +LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb( + const LayerToParentLayerMatrix4x4& aCurrentTransform, + const gfx::Matrix4x4& aScrollableContentTransform, + AsyncPanZoomController* aApzc, const FrameMetrics& aMetrics, + const ScrollbarData& aScrollbarData, bool aScrollbarIsDescendant); + +} // namespace apz +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_ScrollThumbUtils_h diff --git a/gfx/layers/apz/src/SimpleVelocityTracker.cpp b/gfx/layers/apz/src/SimpleVelocityTracker.cpp new file mode 100644 index 0000000000..87cae10d51 --- /dev/null +++ b/gfx/layers/apz/src/SimpleVelocityTracker.cpp @@ -0,0 +1,135 @@ +/* -*- 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 "SimpleVelocityTracker.h" + +#include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/StaticPtr.h" // for StaticAutoPtr + +static mozilla::LazyLogModule sApzSvtLog("apz.simplevelocitytracker"); +#define SVT_LOG(...) MOZ_LOG(sApzSvtLog, LogLevel::Debug, (__VA_ARGS__)) + +namespace mozilla { +namespace layers { + +// When we compute the velocity we do so by taking two input events and +// dividing the distance delta over the time delta. In some cases the time +// delta can be really small, which can make the velocity computation very +// volatile. To avoid this we impose a minimum time delta below which we do +// not recompute the velocity. +const TimeDuration MIN_VELOCITY_SAMPLE_TIME = TimeDuration::FromMilliseconds(5); + +extern StaticAutoPtr gVelocityCurveFunction; + +SimpleVelocityTracker::SimpleVelocityTracker(Axis* aAxis) + : mAxis(aAxis), mVelocitySamplePos(0) {} + +void SimpleVelocityTracker::StartTracking(ParentLayerCoord aPos, + TimeStamp aTimestamp) { + Clear(); + mVelocitySampleTime = aTimestamp; + mVelocitySamplePos = aPos; +} + +Maybe SimpleVelocityTracker::AddPosition(ParentLayerCoord aPos, + TimeStamp aTimestamp) { + if (aTimestamp <= mVelocitySampleTime + MIN_VELOCITY_SAMPLE_TIME) { + // See also the comment on MIN_VELOCITY_SAMPLE_TIME. + // We don't update either mVelocitySampleTime or mVelocitySamplePos so that + // eventually when we do get an event with the required time delta we use + // the corresponding distance delta as well. + SVT_LOG("%p|%s skipping velocity computation for small time delta %f ms\n", + mAxis->OpaqueApzcPointer(), mAxis->Name(), + (aTimestamp - mVelocitySampleTime).ToMilliseconds()); + return Nothing(); + } + + float newVelocity = + (float)(mVelocitySamplePos - aPos) / + (float)(aTimestamp - mVelocitySampleTime).ToMilliseconds(); + + newVelocity = ApplyFlingCurveToVelocity(newVelocity); + + SVT_LOG("%p|%s updating velocity to %f with touch\n", + mAxis->OpaqueApzcPointer(), mAxis->Name(), newVelocity); + mVelocitySampleTime = aTimestamp; + mVelocitySamplePos = aPos; + + AddVelocityToQueue(aTimestamp, newVelocity); + + return Some(newVelocity); +} + +Maybe SimpleVelocityTracker::ComputeVelocity(TimeStamp aTimestamp) { + float velocity = 0; + int count = 0; + for (const auto& e : mVelocityQueue) { + TimeDuration timeDelta = (aTimestamp - e.first); + if (timeDelta < TimeDuration::FromMilliseconds( + StaticPrefs::apz_velocity_relevance_time_ms())) { + count++; + velocity += e.second; + } + } + mVelocityQueue.Clear(); + if (count > 1) { + velocity /= count; + } + return Some(velocity); +} + +void SimpleVelocityTracker::Clear() { mVelocityQueue.Clear(); } + +void SimpleVelocityTracker::AddVelocityToQueue(TimeStamp aTimestamp, + float aVelocity) { + mVelocityQueue.AppendElement(std::make_pair(aTimestamp, aVelocity)); + if (mVelocityQueue.Length() > + StaticPrefs::apz_max_velocity_queue_size_AtStartup()) { + mVelocityQueue.RemoveElementAt(0); + } +} + +float SimpleVelocityTracker::ApplyFlingCurveToVelocity(float aVelocity) const { + float newVelocity = aVelocity; + if (StaticPrefs::apz_max_velocity_inches_per_ms() > 0.0f) { + bool velocityIsNegative = (newVelocity < 0); + newVelocity = fabs(newVelocity); + + float maxVelocity = + mAxis->ToLocalVelocity(StaticPrefs::apz_max_velocity_inches_per_ms()); + newVelocity = std::min(newVelocity, maxVelocity); + + if (StaticPrefs::apz_fling_curve_threshold_inches_per_ms() > 0.0f && + StaticPrefs::apz_fling_curve_threshold_inches_per_ms() < + StaticPrefs::apz_max_velocity_inches_per_ms()) { + float curveThreshold = mAxis->ToLocalVelocity( + StaticPrefs::apz_fling_curve_threshold_inches_per_ms()); + if (newVelocity > curveThreshold) { + // here, 0 < curveThreshold < newVelocity <= maxVelocity, so we apply + // the curve + float scale = maxVelocity - curveThreshold; + float funcInput = (newVelocity - curveThreshold) / scale; + float funcOutput = + gVelocityCurveFunction->At(funcInput, /* aBeforeFlag = */ false); + float curvedVelocity = (funcOutput * scale) + curveThreshold; + SVT_LOG("%p|%s curving up velocity from %f to %f\n", + mAxis->OpaqueApzcPointer(), mAxis->Name(), newVelocity, + curvedVelocity); + newVelocity = curvedVelocity; + } + } + + if (velocityIsNegative) { + newVelocity = -newVelocity; + } + } + + return newVelocity; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/SimpleVelocityTracker.h b/gfx/layers/apz/src/SimpleVelocityTracker.h new file mode 100644 index 0000000000..1778dee065 --- /dev/null +++ b/gfx/layers/apz/src/SimpleVelocityTracker.h @@ -0,0 +1,54 @@ +/* -*- 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_VelocityTracker_h +#define mozilla_layers_VelocityTracker_h + +#include +#include + +#include "Axis.h" +#include "mozilla/Attributes.h" +#include "nsTArray.h" + +namespace mozilla { +namespace layers { + +class SimpleVelocityTracker : public VelocityTracker { + public: + explicit SimpleVelocityTracker(Axis* aAxis); + void StartTracking(ParentLayerCoord aPos, TimeStamp aTimestamp) override; + Maybe AddPosition(ParentLayerCoord aPos, + TimeStamp aTimestamp) override; + Maybe ComputeVelocity(TimeStamp aTimestamp) override; + void Clear() override; + + private: + void AddVelocityToQueue(TimeStamp aTimestamp, float aVelocity); + float ApplyFlingCurveToVelocity(float aVelocity) const; + + // The Axis that uses this velocity tracker. + // This is a raw pointer because the Axis owns the velocity tracker + // by UniquePtr, so the velocity tracker cannot outlive the Axis. + Axis* MOZ_NON_OWNING_REF mAxis; + + // A queue of (timestamp, velocity) pairs; these are the historical + // velocities at the given timestamps. Velocities are in screen pixels per ms. + // This member can only be accessed on the controller/UI thread. + nsTArray> mVelocityQueue; + + // mVelocitySampleTime and mVelocitySamplePos are the time and position + // used in the last velocity sampling. They get updated when a new sample is + // taken (which may not happen on every input event, if the time delta is too + // small). + TimeStamp mVelocitySampleTime; + ParentLayerCoord mVelocitySamplePos; +}; + +} // namespace layers +} // namespace mozilla + +#endif diff --git a/gfx/layers/apz/src/SmoothMsdScrollAnimation.cpp b/gfx/layers/apz/src/SmoothMsdScrollAnimation.cpp new file mode 100644 index 0000000000..8342dc157f --- /dev/null +++ b/gfx/layers/apz/src/SmoothMsdScrollAnimation.cpp @@ -0,0 +1,139 @@ +/* -*- 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 "SmoothMsdScrollAnimation.h" +#include "AsyncPanZoomController.h" + +namespace mozilla { +namespace layers { + +SmoothMsdScrollAnimation::SmoothMsdScrollAnimation( + AsyncPanZoomController& aApzc, const CSSPoint& aInitialPosition, + const CSSPoint& aInitialVelocity, const CSSPoint& aDestination, + double aSpringConstant, double aDampingRatio, + ScrollSnapTargetIds&& aSnapTargetIds, + ScrollTriggeredByScript aTriggeredByScript) + : mApzc(aApzc), + mXAxisModel(aInitialPosition.x, aDestination.x, aInitialVelocity.x, + aSpringConstant, aDampingRatio), + mYAxisModel(aInitialPosition.y, aDestination.y, aInitialVelocity.y, + aSpringConstant, aDampingRatio), + mSnapTargetIds(std::move(aSnapTargetIds)), + mTriggeredByScript(aTriggeredByScript) {} + +bool SmoothMsdScrollAnimation::DoSample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta) { + CSSToParentLayerScale zoom(aFrameMetrics.GetZoom()); + if (zoom == CSSToParentLayerScale(0)) { + return false; + } + CSSPoint oneParentLayerPixel = + ParentLayerPoint(1, 1) / aFrameMetrics.GetZoom(); + if (mXAxisModel.IsFinished(oneParentLayerPixel.x) && + mYAxisModel.IsFinished(oneParentLayerPixel.y)) { + // Set the scroll offset to the exact destination. If we allow the scroll + // offset to end up being a bit off from the destination, we can get + // artefacts like "scroll to the next snap point in this direction" + // scrolling to the snap point we're already supposed to be at. + mApzc.ClampAndSetVisualScrollOffset( + CSSPoint(mXAxisModel.GetDestination(), mYAxisModel.GetDestination())); + return false; + } + + mXAxisModel.Simulate(aDelta); + mYAxisModel.Simulate(aDelta); + + CSSPoint position = + CSSPoint(mXAxisModel.GetPosition(), mYAxisModel.GetPosition()); + CSSPoint css_velocity = + CSSPoint(mXAxisModel.GetVelocity(), mYAxisModel.GetVelocity()); + + // Convert from pixels/second to pixels/ms + ParentLayerPoint velocity = + ParentLayerPoint(css_velocity.x, css_velocity.y) / 1000.0f; + + // Keep the velocity updated for the Axis class so that any animations + // chained off of the smooth scroll will inherit it. + if (mXAxisModel.IsFinished(oneParentLayerPixel.x)) { + mApzc.mX.SetVelocity(0); + } else { + mApzc.mX.SetVelocity(velocity.x); + } + if (mYAxisModel.IsFinished(oneParentLayerPixel.y)) { + mApzc.mY.SetVelocity(0); + } else { + mApzc.mY.SetVelocity(velocity.y); + } + // If we overscroll, hand off to a fling animation that will complete the + // spring back. + ParentLayerPoint displacement = + (position - aFrameMetrics.GetVisualScrollOffset()) * zoom; + + ParentLayerPoint overscroll; + ParentLayerPoint adjustedOffset; + mApzc.mX.AdjustDisplacement(displacement.x, adjustedOffset.x, overscroll.x); + mApzc.mY.AdjustDisplacement(displacement.y, adjustedOffset.y, overscroll.y); + mApzc.ScrollBy(adjustedOffset / zoom); + // The smooth scroll may have caused us to reach the end of our scroll + // range. This can happen if either the + // layout.css.scroll-behavior.damping-ratio preference is set to less than 1 + // (underdamped) or if a smooth scroll inherits velocity from a fling + // gesture. + if (!IsZero(overscroll / zoom)) { + // Hand off a fling with the remaining momentum to the next APZC in the + // overscroll handoff chain. + + // We may have reached the end of the scroll range along one axis but + // not the other. In such a case we only want to hand off the relevant + // component of the fling. + if (mApzc.IsZero(overscroll.x)) { + velocity.x = 0; + } else if (mApzc.IsZero(overscroll.y)) { + velocity.y = 0; + } + + // To hand off the fling, we attempt to find a target APZC and start a new + // fling with the same velocity on that APZC. For simplicity, the actual + // overscroll of the current sample is discarded rather than being handed + // off. The compositor should sample animations sufficiently frequently + // that this is not noticeable. The target APZC is chosen by seeing if + // there is an APZC further in the handoff chain which is pannable; if + // there isn't, we take the new fling ourselves, entering an overscrolled + // state. + // Note: APZC is holding mRecursiveMutex, so directly calling + // HandleSmoothScrollOverscroll() (which acquires the tree lock) would + // violate the lock ordering. Instead we schedule + // HandleSmoothScrollOverscroll() to be called after mRecursiveMutex is + // released. + mDeferredTasks.AppendElement(NewRunnableMethod( + "layers::AsyncPanZoomController::HandleSmoothScrollOverscroll", &mApzc, + &AsyncPanZoomController::HandleSmoothScrollOverscroll, velocity, + apz::GetOverscrollSideBits(overscroll))); + return false; + } + + return true; +} + +void SmoothMsdScrollAnimation::SetDestination( + const CSSPoint& aNewDestination, ScrollSnapTargetIds&& aSnapTargetIds, + ScrollTriggeredByScript aTriggeredByScript) { + mXAxisModel.SetDestination(aNewDestination.x); + mYAxisModel.SetDestination(aNewDestination.y); + mSnapTargetIds = std::move(aSnapTargetIds); + mTriggeredByScript = aTriggeredByScript; +} + +CSSPoint SmoothMsdScrollAnimation::GetDestination() const { + return CSSPoint(mXAxisModel.GetDestination(), mYAxisModel.GetDestination()); +} + +SmoothMsdScrollAnimation* +SmoothMsdScrollAnimation::AsSmoothMsdScrollAnimation() { + return this; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/SmoothMsdScrollAnimation.h b/gfx/layers/apz/src/SmoothMsdScrollAnimation.h new file mode 100644 index 0000000000..1f2247c473 --- /dev/null +++ b/gfx/layers/apz/src/SmoothMsdScrollAnimation.h @@ -0,0 +1,61 @@ +/* -*- 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_SmoothMsdScrollAnimation_h_ +#define mozilla_layers_SmoothMsdScrollAnimation_h_ + +#include "AsyncPanZoomAnimation.h" +#include "mozilla/layers/AxisPhysicsMSDModel.h" +#include "mozilla/ScrollPositionUpdate.h" + +namespace mozilla { +namespace layers { + +class AsyncPanZoomController; + +class SmoothMsdScrollAnimation final : public AsyncPanZoomAnimation { + public: + SmoothMsdScrollAnimation(AsyncPanZoomController& aApzc, + const CSSPoint& aInitialPosition, + const CSSPoint& aInitialVelocity, + const CSSPoint& aDestination, double aSpringConstant, + double aDampingRatio, + ScrollSnapTargetIds&& aSnapTargetIds, + ScrollTriggeredByScript aTriggeredByScript); + + /** + * Advances a smooth scroll simulation based on the time passed in |aDelta|. + * This should be called whenever sampling the content transform for this + * frame. Returns true if the smooth scroll should be advanced by one frame, + * or false if the smooth scroll has ended. + */ + bool DoSample(FrameMetrics& aFrameMetrics, + const TimeDuration& aDelta) override; + + void SetDestination(const CSSPoint& aNewDestination, + ScrollSnapTargetIds&& aSnapTargetIds, + ScrollTriggeredByScript aTriggeredByScript); + CSSPoint GetDestination() const; + SmoothMsdScrollAnimation* AsSmoothMsdScrollAnimation() override; + + bool WasTriggeredByScript() const override { + return mTriggeredByScript == ScrollTriggeredByScript::Yes; + } + + ScrollSnapTargetIds TakeSnapTargetIds() { return std::move(mSnapTargetIds); } + + private: + AsyncPanZoomController& mApzc; + AxisPhysicsMSDModel mXAxisModel; + AxisPhysicsMSDModel mYAxisModel; + ScrollSnapTargetIds mSnapTargetIds; + ScrollTriggeredByScript mTriggeredByScript; +}; + +} // namespace layers +} // namespace mozilla + +#endif diff --git a/gfx/layers/apz/src/SmoothScrollAnimation.cpp b/gfx/layers/apz/src/SmoothScrollAnimation.cpp new file mode 100644 index 0000000000..266c027a55 --- /dev/null +++ b/gfx/layers/apz/src/SmoothScrollAnimation.cpp @@ -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/. */ + +#include "SmoothScrollAnimation.h" +#include "ScrollAnimationBezierPhysics.h" +#include "mozilla/layers/APZPublicUtils.h" + +namespace mozilla { +namespace layers { + +SmoothScrollAnimation::SmoothScrollAnimation(AsyncPanZoomController& aApzc, + const nsPoint& aInitialPosition, + ScrollOrigin aOrigin) + : GenericScrollAnimation( + aApzc, aInitialPosition, + apz::ComputeBezierAnimationSettingsForOrigin(aOrigin)), + mOrigin(aOrigin) {} + +SmoothScrollAnimation* SmoothScrollAnimation::AsSmoothScrollAnimation() { + return this; +} + +ScrollOrigin SmoothScrollAnimation::GetScrollOrigin() const { return mOrigin; } + +ScrollOrigin SmoothScrollAnimation::GetScrollOriginForAction( + KeyboardScrollAction::KeyboardScrollActionType aAction) { + switch (aAction) { + case KeyboardScrollAction::eScrollCharacter: + case KeyboardScrollAction::eScrollLine: { + return ScrollOrigin::Lines; + } + case KeyboardScrollAction::eScrollPage: + return ScrollOrigin::Pages; + case KeyboardScrollAction::eScrollComplete: + return ScrollOrigin::Other; + default: + MOZ_ASSERT(false, "Unknown keyboard scroll action type"); + return ScrollOrigin::Other; + } +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/SmoothScrollAnimation.h b/gfx/layers/apz/src/SmoothScrollAnimation.h new file mode 100644 index 0000000000..1143744cc1 --- /dev/null +++ b/gfx/layers/apz/src/SmoothScrollAnimation.h @@ -0,0 +1,37 @@ +/* -*- 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_SmoothScrollAnimation_h_ +#define mozilla_layers_SmoothScrollAnimation_h_ + +#include "GenericScrollAnimation.h" +#include "mozilla/ScrollOrigin.h" +#include "mozilla/layers/KeyboardScrollAction.h" + +namespace mozilla { +namespace layers { + +class AsyncPanZoomController; + +class SmoothScrollAnimation : public GenericScrollAnimation { + public: + SmoothScrollAnimation(AsyncPanZoomController& aApzc, + const nsPoint& aInitialPosition, + ScrollOrigin aScrollOrigin); + + SmoothScrollAnimation* AsSmoothScrollAnimation() override; + ScrollOrigin GetScrollOrigin() const; + static ScrollOrigin GetScrollOriginForAction( + KeyboardScrollAction::KeyboardScrollActionType aAction); + + private: + ScrollOrigin mOrigin; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_SmoothScrollAnimation_h_ diff --git a/gfx/layers/apz/src/WRHitTester.cpp b/gfx/layers/apz/src/WRHitTester.cpp new file mode 100644 index 0000000000..873400976f --- /dev/null +++ b/gfx/layers/apz/src/WRHitTester.cpp @@ -0,0 +1,247 @@ +/* -*- 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 "WRHitTester.h" +#include "AsyncPanZoomController.h" +#include "APZCTreeManager.h" +#include "TreeTraversal.h" // for BreadthFirstSearch +#include "mozilla/gfx/CompositorHitTestInfo.h" +#include "mozilla/webrender/WebRenderAPI.h" +#include "nsDebug.h" // for NS_ASSERTION +#include "nsIXULRuntime.h" // for FissionAutostart +#include "mozilla/gfx/Matrix.h" + +#define APZCTM_LOG(...) \ + MOZ_LOG(APZCTreeManager::sLog, LogLevel::Debug, (__VA_ARGS__)) + +namespace mozilla { +namespace layers { + +using mozilla::gfx::CompositorHitTestFlags; +using mozilla::gfx::CompositorHitTestInvisibleToHit; + +static bool CheckCloseToIdentity(const gfx::Matrix4x4& aMatrix) { + // We allow a factor of 1/2048 in the multiply part of the matrix, so that if + // we multiply by a point on a screen of size 2048 we would be off by at most + // 1 pixel approximately. + const float multiplyEps = 1 / 2048.f; + // We allow 1 pixel in the translate part of the matrix. + const float translateEps = 1.f; + + if (!FuzzyEqualsAdditive(aMatrix._11, 1.f, multiplyEps) || + !FuzzyEqualsAdditive(aMatrix._12, 0.f, multiplyEps) || + !FuzzyEqualsAdditive(aMatrix._13, 0.f, multiplyEps) || + !FuzzyEqualsAdditive(aMatrix._14, 0.f, multiplyEps) || + !FuzzyEqualsAdditive(aMatrix._21, 0.f, multiplyEps) || + !FuzzyEqualsAdditive(aMatrix._22, 1.f, multiplyEps) || + !FuzzyEqualsAdditive(aMatrix._23, 0.f, multiplyEps) || + !FuzzyEqualsAdditive(aMatrix._24, 0.f, multiplyEps) || + !FuzzyEqualsAdditive(aMatrix._31, 0.f, multiplyEps) || + !FuzzyEqualsAdditive(aMatrix._32, 0.f, multiplyEps) || + !FuzzyEqualsAdditive(aMatrix._33, 1.f, multiplyEps) || + !FuzzyEqualsAdditive(aMatrix._34, 0.f, multiplyEps) || + !FuzzyEqualsAdditive(aMatrix._41, 0.f, translateEps) || + !FuzzyEqualsAdditive(aMatrix._42, 0.f, translateEps) || + !FuzzyEqualsAdditive(aMatrix._43, 0.f, translateEps) || + !FuzzyEqualsAdditive(aMatrix._44, 1.f, multiplyEps)) { + return false; + } + return true; +} + +// Checks that within the constraints of floating point math we can invert it +// reasonably enough that multiplying by the computed inverse is close to the +// identity. +static bool CheckInvertibleWithFinitePrecision(const gfx::Matrix4x4& aMatrix) { + auto inverse = aMatrix.MaybeInverse(); + if (inverse.isNothing()) { + // Should we return false? + return true; + } + if (!CheckCloseToIdentity(aMatrix * *inverse)) { + return false; + } + if (!CheckCloseToIdentity(*inverse * aMatrix)) { + return false; + } + return true; +} + +IAPZHitTester::HitTestResult WRHitTester::GetAPZCAtPoint( + const ScreenPoint& aHitTestPoint, + const RecursiveMutexAutoLock& aProofOfTreeLock) { + HitTestResult hit; + RefPtr wr = mTreeManager->GetWebRenderAPI(); + if (!wr) { + // If WebRender isn't running, fall back to the root APZC. + // This is mostly for the benefit of GTests which do not + // run a WebRender instance, but gracefully falling back + // here allows those tests which are not specifically + // testing the hit-test algorithm to still work. + hit.mTargetApzc = FindRootApzcForLayersId(GetRootLayersId()); + hit.mHitResult = CompositorHitTestFlags::eVisibleToHitTest; + return hit; + } + + APZCTM_LOG("Hit-testing point %s with WR\n", ToString(aHitTestPoint).c_str()); + std::vector results = + wr->HitTest(wr::ToWorldPoint(aHitTestPoint)); + + Maybe chosenResult; + for (const wr::WrHitResult& result : results) { + ScrollableLayerGuid guid{result.mLayersId, 0, result.mScrollId}; + APZCTM_LOG("Examining result with guid %s hit info 0x%x... ", + ToString(guid).c_str(), result.mHitInfo.serialize()); + if (result.mHitInfo == CompositorHitTestInvisibleToHit) { + APZCTM_LOG("skipping due to invisibility.\n"); + continue; + } + RefPtr node = + GetTargetNode(guid, &ScrollableLayerGuid::EqualsIgnoringPresShell); + if (!node) { + APZCTM_LOG("no corresponding node found, falling back to root.\n"); + +#ifdef DEBUG + // We can enter here during normal codepaths for cases where the + // nsDisplayCompositorHitTestInfo item emitted a scrollId of + // NULL_SCROLL_ID to the webrender display list. The semantics of that + // is to fall back to the root APZC for the layers id, so that's what + // we do here. + // If we enter this codepath and scrollId is not NULL_SCROLL_ID, then + // that's more likely to be due to a race condition between rebuilding + // the APZ tree and updating the WR scene/hit-test information, resulting + // in WR giving us a hit result for a scene that is not active in APZ. + // Such a scenario would need debugging and fixing. + // In non-Fission mode, make this assertion non-fatal because there is + // a known issue related to inactive scroll frames that can cause this + // to fire (see bug 1634763), which is fixed in Fission mode and not + // worth fixing in non-Fission mode. + if (FissionAutostart()) { + MOZ_ASSERT(result.mScrollId == ScrollableLayerGuid::NULL_SCROLL_ID); + } else { + NS_ASSERTION( + result.mScrollId == ScrollableLayerGuid::NULL_SCROLL_ID, + "Inconsistency between WebRender display list and APZ scroll data"); + } +#endif + node = FindRootNodeForLayersId(result.mLayersId); + if (!node) { + // Should never happen, but handle gracefully in release builds just + // in case. + MOZ_ASSERT(false); + chosenResult = Some(result); + break; + } + } + MOZ_ASSERT(node->GetApzc()); // any node returned must have an APZC + EventRegionsOverride flags = node->GetEventRegionsOverride(); + if (flags & EventRegionsOverride::ForceEmptyHitRegion) { + // This result is inside a subtree that is invisible to hit-testing. + APZCTM_LOG("skipping due to FEHR subtree.\n"); + continue; + } + + if (!CheckInvertibleWithFinitePrecision( + mTreeManager->GetScreenToApzcTransform(node->GetApzc()) + .ToUnknownMatrix())) { + APZCTM_LOG("skipping due to check inverse accuracy\n"); + continue; + } + + APZCTM_LOG("selecting as chosen result.\n"); + chosenResult = Some(result); + hit.mTargetApzc = node->GetApzc(); + if (flags & EventRegionsOverride::ForceDispatchToContent) { + chosenResult->mHitInfo += CompositorHitTestFlags::eApzAwareListeners; + } + break; + } + if (!chosenResult) { + return hit; + } + + MOZ_ASSERT(hit.mTargetApzc); + hit.mLayersId = chosenResult->mLayersId; + ScrollableLayerGuid::ViewID scrollId = chosenResult->mScrollId; + gfx::CompositorHitTestInfo hitInfo = chosenResult->mHitInfo; + Maybe animationId = chosenResult->mAnimationId; + SideBits sideBits = chosenResult->mSideBits; + + APZCTM_LOG("Successfully matched APZC %p (hit result 0x%x)\n", + hit.mTargetApzc.get(), hitInfo.serialize()); + + const bool isScrollbar = + hitInfo.contains(gfx::CompositorHitTestFlags::eScrollbar); + const bool isScrollbarThumb = + hitInfo.contains(gfx::CompositorHitTestFlags::eScrollbarThumb); + const ScrollDirection direction = + hitInfo.contains(gfx::CompositorHitTestFlags::eScrollbarVertical) + ? ScrollDirection::eVertical + : ScrollDirection::eHorizontal; + HitTestingTreeNode* scrollbarNode = nullptr; + if (isScrollbar || isScrollbarThumb) { + scrollbarNode = BreadthFirstSearch( + GetRootNode(), [&](HitTestingTreeNode* aNode) { + return (aNode->GetLayersId() == hit.mLayersId) && + (aNode->IsScrollbarNode() == isScrollbar) && + (aNode->IsScrollThumbNode() == isScrollbarThumb) && + (aNode->GetScrollbarDirection() == direction) && + (aNode->GetScrollTargetId() == scrollId); + }); + } + + hit.mHitResult = hitInfo; + + if (scrollbarNode) { + RefPtr scrollbarRef = scrollbarNode; + InitializeHitTestingTreeNodeAutoLock(hit.mScrollbarNode, aProofOfTreeLock, + scrollbarRef); + } + + hit.mFixedPosSides = sideBits; + if (animationId.isSome()) { + RefPtr positionedNode = nullptr; + + positionedNode = BreadthFirstSearch( + GetRootNode(), [&](HitTestingTreeNode* aNode) { + return (aNode->GetFixedPositionAnimationId() == animationId || + aNode->GetStickyPositionAnimationId() == animationId); + }); + + if (positionedNode) { + MOZ_ASSERT(positionedNode->GetLayersId() == chosenResult->mLayersId, + "Found node layers id does not match the hit result"); + MOZ_ASSERT((positionedNode->GetFixedPositionAnimationId().isSome() || + positionedNode->GetStickyPositionAnimationId().isSome()), + "A a matching fixed/sticky position node should be found"); + InitializeHitTestingTreeNodeAutoLock(hit.mNode, aProofOfTreeLock, + positionedNode); + } + +#if defined(MOZ_WIDGET_ANDROID) + if (hit.mNode && hit.mNode->GetFixedPositionAnimationId().isSome()) { + // If the hit element is a fixed position element, the side bits from + // the hit-result item tag are used. For now just ensure that these + // match what is found in the hit-testing tree node. + MOZ_ASSERT(sideBits == hit.mNode->GetFixedPosSides(), + "Fixed position side bits do not match"); + } else if (hit.mTargetApzc && hit.mTargetApzc->IsRootContent()) { + // If the hit element is not a fixed position element, then the hit test + // result item's side bits should not be populated. + MOZ_ASSERT(sideBits == SideBits::eNone, + "Hit test results have side bits only for pos:fixed"); + } +#endif + } + + hit.mHitOverscrollGutter = + hit.mTargetApzc && hit.mTargetApzc->IsInOverscrollGutter(aHitTestPoint); + + return hit; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/WRHitTester.h b/gfx/layers/apz/src/WRHitTester.h new file mode 100644 index 0000000000..abb9de1a66 --- /dev/null +++ b/gfx/layers/apz/src/WRHitTester.h @@ -0,0 +1,26 @@ +/* -*- 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_WRHitTester_h +#define mozilla_layers_WRHitTester_h + +#include "IAPZHitTester.h" + +namespace mozilla { +namespace layers { + +// IAPZHitTester implementation for WebRender. +class WRHitTester : public IAPZHitTester { + public: + virtual HitTestResult GetAPZCAtPoint( + const ScreenPoint& aHitTestPoint, + const RecursiveMutexAutoLock& aProofOfTreeLock) override; +}; + +} // namespace layers +} // namespace mozilla + +#endif // define mozilla_layers_WRHitTester_h diff --git a/gfx/layers/apz/src/WheelScrollAnimation.cpp b/gfx/layers/apz/src/WheelScrollAnimation.cpp new file mode 100644 index 0000000000..6203bcb8fa --- /dev/null +++ b/gfx/layers/apz/src/WheelScrollAnimation.cpp @@ -0,0 +1,64 @@ +/* -*- 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 "WheelScrollAnimation.h" + +#include +#include "AsyncPanZoomController.h" +#include "mozilla/StaticPrefs_general.h" +#include "nsPoint.h" +#include "ScrollAnimationBezierPhysics.h" + +namespace mozilla { +namespace layers { + +static ScrollAnimationBezierPhysicsSettings SettingsForDeltaType( + ScrollWheelInput::ScrollDeltaType aDeltaType) { + int32_t minMS = 0; + int32_t maxMS = 0; + + switch (aDeltaType) { + case ScrollWheelInput::SCROLLDELTA_PAGE: + maxMS = clamped(StaticPrefs::general_smoothScroll_pages_durationMaxMS(), + 0, 10000); + minMS = clamped(StaticPrefs::general_smoothScroll_pages_durationMinMS(), + 0, maxMS); + break; + case ScrollWheelInput::SCROLLDELTA_PIXEL: + maxMS = clamped(StaticPrefs::general_smoothScroll_pixels_durationMaxMS(), + 0, 10000); + minMS = clamped(StaticPrefs::general_smoothScroll_pixels_durationMinMS(), + 0, maxMS); + break; + case ScrollWheelInput::SCROLLDELTA_LINE: + maxMS = + clamped(StaticPrefs::general_smoothScroll_mouseWheel_durationMaxMS(), + 0, 10000); + minMS = + clamped(StaticPrefs::general_smoothScroll_mouseWheel_durationMinMS(), + 0, maxMS); + break; + } + + // The pref is 100-based int percentage, while mIntervalRatio is 1-based ratio + double intervalRatio = + ((double)StaticPrefs::general_smoothScroll_durationToIntervalRatio()) / + 100.0; + intervalRatio = std::max(1.0, intervalRatio); + return ScrollAnimationBezierPhysicsSettings{minMS, maxMS, intervalRatio}; +} + +WheelScrollAnimation::WheelScrollAnimation( + AsyncPanZoomController& aApzc, const nsPoint& aInitialPosition, + ScrollWheelInput::ScrollDeltaType aDeltaType) + : GenericScrollAnimation(aApzc, aInitialPosition, + SettingsForDeltaType(aDeltaType)) { + mDirectionForcedToOverscroll = + mApzc.mScrollMetadata.GetDisregardedDirection(); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/src/WheelScrollAnimation.h b/gfx/layers/apz/src/WheelScrollAnimation.h new file mode 100644 index 0000000000..7c039ef3fd --- /dev/null +++ b/gfx/layers/apz/src/WheelScrollAnimation.h @@ -0,0 +1,30 @@ +/* -*- 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_WheelScrollAnimation_h_ +#define mozilla_layers_WheelScrollAnimation_h_ + +#include "GenericScrollAnimation.h" +#include "InputData.h" + +namespace mozilla { +namespace layers { + +class AsyncPanZoomController; + +class WheelScrollAnimation : public GenericScrollAnimation { + public: + WheelScrollAnimation(AsyncPanZoomController& aApzc, + const nsPoint& aInitialPosition, + ScrollWheelInput::ScrollDeltaType aDeltaType); + + WheelScrollAnimation* AsWheelScrollAnimation() override { return this; } +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_WheelScrollAnimation_h_ diff --git a/gfx/layers/apz/test/gtest/APZCBasicTester.h b/gfx/layers/apz/test/gtest/APZCBasicTester.h new file mode 100644 index 0000000000..621a3d37be --- /dev/null +++ b/gfx/layers/apz/test/gtest/APZCBasicTester.h @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layers_APZCBasicTester_h +#define mozilla_layers_APZCBasicTester_h + +/** + * Defines a test fixture used for testing a single APZC. + */ + +#include "APZTestCommon.h" + +#include "mozilla/layers/APZSampler.h" +#include "mozilla/layers/APZUpdater.h" + +class APZCBasicTester : public APZCTesterBase { + public: + explicit APZCBasicTester( + AsyncPanZoomController::GestureBehavior aGestureBehavior = + AsyncPanZoomController::DEFAULT_GESTURES) + : mGestureBehavior(aGestureBehavior) {} + + protected: + virtual void SetUp() { + APZCTesterBase::SetUp(); + APZThreadUtils::SetThreadAssertionsEnabled(false); + APZThreadUtils::SetControllerThread(NS_GetCurrentThread()); + + tm = new TestAPZCTreeManager(mcc); + updater = new APZUpdater(tm, false); + sampler = new APZSampler(tm, false); + apzc = + new TestAsyncPanZoomController(LayersId{0}, mcc, tm, mGestureBehavior); + apzc->SetFrameMetrics(TestFrameMetrics()); + apzc->GetScrollMetadata().SetIsLayersIdRoot(true); + } + + /** + * Get the APZC's scroll range in CSS pixels. + */ + CSSRect GetScrollRange() const { + const FrameMetrics& metrics = apzc->GetFrameMetrics(); + return CSSRect(metrics.GetScrollableRect().TopLeft(), + metrics.GetScrollableRect().Size() - + metrics.CalculateCompositedSizeInCssPixels()); + } + + virtual void TearDown() { + while (mcc->RunThroughDelayedTasks()) + ; + apzc->Destroy(); + tm->ClearTree(); + tm->ClearContentController(); + + APZCTesterBase::TearDown(); + } + + void MakeApzcWaitForMainThread() { apzc->SetWaitForMainThread(); } + + void MakeApzcZoomable() { + apzc->UpdateZoomConstraints(ZoomConstraints( + true, true, CSSToParentLayerScale(0.25f), CSSToParentLayerScale(4.0f))); + } + + void MakeApzcUnzoomable() { + apzc->UpdateZoomConstraints(ZoomConstraints(false, false, + CSSToParentLayerScale(1.0f), + CSSToParentLayerScale(1.0f))); + } + + /** + * Sample animations once, 1 ms later than the last sample. + */ + void SampleAnimationOnce() { + const TimeDuration increment = TimeDuration::FromMilliseconds(1); + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + mcc->AdvanceBy(increment); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + } + /** + * Sample animations one frame, 17 ms later than the last sample. + */ + void SampleAnimationOneFrame() { + const TimeDuration increment = TimeDuration::FromMilliseconds(17); + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + mcc->AdvanceBy(increment); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + } + + AsyncPanZoomController::GestureBehavior mGestureBehavior; + RefPtr tm; + RefPtr sampler; + RefPtr updater; + RefPtr apzc; +}; + +#endif // mozilla_layers_APZCBasicTester_h diff --git a/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h b/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h new file mode 100644 index 0000000000..8a2f9d749b --- /dev/null +++ b/gfx/layers/apz/test/gtest/APZCTreeManagerTester.h @@ -0,0 +1,223 @@ +/* -*- 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_APZCTreeManagerTester_h +#define mozilla_layers_APZCTreeManagerTester_h + +/** + * Defines a test fixture used for testing multiple APZCs interacting in + * an APZCTreeManager. + */ + +#include "APZTestAccess.h" +#include "APZTestCommon.h" +#include "gfxPlatform.h" +#include "MockHitTester.h" +#include "apz/src/WRHitTester.h" + +#include "mozilla/layers/APZSampler.h" +#include "mozilla/layers/APZUpdater.h" +#include "mozilla/layers/WebRenderScrollDataWrapper.h" + +class APZCTreeManagerTester : public APZCTesterBase { + protected: + APZCTreeManagerTester() : mHitTester(MakeUnique()) {} + + virtual void SetUp() { + APZCTesterBase::SetUp(); + + APZThreadUtils::SetThreadAssertionsEnabled(false); + APZThreadUtils::SetControllerThread(NS_GetCurrentThread()); + + manager = new TestAPZCTreeManager(mcc, std::move(mHitTester)); + updater = new APZUpdater(manager, false); + sampler = new APZSampler(manager, false); + } + + virtual void TearDown() { + while (mcc->RunThroughDelayedTasks()) + ; + manager->ClearTree(); + manager->ClearContentController(); + + APZCTesterBase::TearDown(); + } + + /** + * Sample animations once for all APZCs, 1 ms later than the last sample and + * return whether there is still any active animations or not. + */ + bool SampleAnimationsOnce() { + const TimeDuration increment = TimeDuration::FromMilliseconds(1); + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + mcc->AdvanceBy(increment); + + bool activeAnimations = false; + + for (size_t i = 0; i < layers.GetLayerCount(); ++i) { + if (TestAsyncPanZoomController* apzc = ApzcOf(layers[i])) { + activeAnimations |= + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + } + } + + return activeAnimations; + } + + // A convenience function for letting a test modify the frame metrics + // stored on a particular layer. + template + void ModifyFrameMetrics(WebRenderLayerScrollData* aLayer, + Callback aCallback) { + MOZ_ASSERT(aLayer->GetScrollMetadataCount() == 1); + ScrollMetadata& metadataRef = + APZTestAccess::GetScrollMetadataMut(*aLayer, layers, 0); + aCallback(metadataRef, metadataRef.GetMetrics()); + } + + // A convenience wrapper for manager->UpdateHitTestingTree(). + void UpdateHitTestingTree(uint32_t aPaintSequenceNumber = 0) { + manager->UpdateHitTestingTree(WebRenderScrollDataWrapper{*updater, &layers}, + /* is first paint = */ false, LayersId{0}, + aPaintSequenceNumber); + } + + void CreateScrollData(const char* aTreeShape, + const LayerIntRegion* aVisibleRegions = nullptr, + const gfx::Matrix4x4* aTransforms = nullptr) { + layers = TestWRScrollData::Create(aTreeShape, *updater, aVisibleRegions, + aTransforms); + root = layers[0]; + } + + void CreateMockHitTester() { + mHitTester = MakeUnique(); + // Save a pointer in a separate variable, because SetUp() will + // move the value out of mHitTester. + mMockHitTester = static_cast(mHitTester.get()); + } + void QueueMockHitResult(ScrollableLayerGuid::ViewID aScrollId, + gfx::CompositorHitTestInfo aHitInfo = + gfx::CompositorHitTestFlags::eVisibleToHitTest) { + MOZ_ASSERT(mMockHitTester); + mMockHitTester->QueueHitResult(aScrollId, aHitInfo); + } + + RefPtr manager; + RefPtr sampler; + RefPtr updater; + TestWRScrollData layers; + WebRenderLayerScrollData* root = nullptr; + + UniquePtr mHitTester; + MockHitTester* mMockHitTester = nullptr; + + protected: + static ScrollMetadata BuildScrollMetadata( + ScrollableLayerGuid::ViewID aScrollId, const CSSRect& aScrollableRect, + const ParentLayerRect& aCompositionBounds) { + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetScrollId(aScrollId); + // By convention in this test file, START_SCROLL_ID is the root, so mark it + // as such. + if (aScrollId == ScrollableLayerGuid::START_SCROLL_ID) { + metadata.SetIsLayersIdRoot(true); + } + metrics.SetCompositionBounds(aCompositionBounds); + metrics.SetScrollableRect(aScrollableRect); + metrics.SetLayoutScrollOffset(CSSPoint(0, 0)); + metadata.SetPageScrollAmount(LayoutDeviceIntSize(50, 100)); + metadata.SetLineScrollAmount(LayoutDeviceIntSize(5, 10)); + return metadata; + } + + void SetScrollMetadata(WebRenderLayerScrollData* aLayer, + const ScrollMetadata& aMetadata) { + MOZ_ASSERT(aLayer->GetScrollMetadataCount() <= 1, + "This function does not support multiple ScrollMetadata on a " + "single layer"); + if (aLayer->GetScrollMetadataCount() == 0) { + // Add new metrics + aLayer->AppendScrollMetadata(layers, aMetadata); + } else { + // Overwrite existing metrics + ModifyFrameMetrics( + aLayer, [&](ScrollMetadata& aSm, FrameMetrics&) { aSm = aMetadata; }); + } + } + + void SetScrollMetadata(WebRenderLayerScrollData* aLayer, + const nsTArray& aMetadata) { + // The reason for this restriction is that WebRenderLayerScrollData does not + // have an API to *remove* previous metadata. + MOZ_ASSERT(aLayer->GetScrollMetadataCount() == 0, + "This function can only be used on layers which do not yet have " + "scroll metadata"); + for (const ScrollMetadata& metadata : aMetadata) { + aLayer->AppendScrollMetadata(layers, metadata); + } + } + + void SetScrollableFrameMetrics(WebRenderLayerScrollData* aLayer, + ScrollableLayerGuid::ViewID aScrollId, + CSSRect aScrollableRect = CSSRect(-1, -1, -1, + -1)) { + auto localTransform = aLayer->GetTransformTyped() * AsyncTransformMatrix(); + ParentLayerIntRect compositionBounds = + RoundedToInt(localTransform.TransformBounds( + LayerRect(aLayer->GetVisibleRegion().GetBounds()))); + ScrollMetadata metadata = BuildScrollMetadata( + aScrollId, aScrollableRect, ParentLayerRect(compositionBounds)); + SetScrollMetadata(aLayer, metadata); + } + + bool HasScrollableFrameMetrics(const WebRenderLayerScrollData* aLayer) const { + for (uint32_t i = 0; i < aLayer->GetScrollMetadataCount(); i++) { + if (aLayer->GetScrollMetadata(layers, i).GetMetrics().IsScrollable()) { + return true; + } + } + return false; + } + + void SetScrollHandoff(WebRenderLayerScrollData* aChild, + WebRenderLayerScrollData* aParent) { + ModifyFrameMetrics(aChild, [&](ScrollMetadata& aSm, FrameMetrics&) { + aSm.SetScrollParentId( + aParent->GetScrollMetadata(layers, 0).GetMetrics().GetScrollId()); + }); + } + + TestAsyncPanZoomController* ApzcOf(WebRenderLayerScrollData* aLayer) { + EXPECT_EQ(1u, aLayer->GetScrollMetadataCount()); + return ApzcOf(aLayer, 0); + } + + TestAsyncPanZoomController* ApzcOf(WebRenderLayerScrollData* aLayer, + uint32_t aIndex) { + EXPECT_LT(aIndex, aLayer->GetScrollMetadataCount()); + // Unlike Layer, WebRenderLayerScrollData does not store the associated + // APZCs, so look it up using the tree manager instead. + RefPtr apzc = manager->GetTargetAPZC( + LayersId{0}, + aLayer->GetScrollMetadata(layers, aIndex).GetMetrics().GetScrollId()); + return (TestAsyncPanZoomController*)apzc.get(); + } + + void CreateSimpleScrollingLayer() { + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 200, 200), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 500, 500)); + } +}; + +#endif // mozilla_layers_APZCTreeManagerTester_h diff --git a/gfx/layers/apz/test/gtest/APZTestAccess.cpp b/gfx/layers/apz/test/gtest/APZTestAccess.cpp new file mode 100644 index 0000000000..d55d7711f8 --- /dev/null +++ b/gfx/layers/apz/test/gtest/APZTestAccess.cpp @@ -0,0 +1,27 @@ +/* -*- 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 "APZTestAccess.h" +#include "mozilla/layers/WebRenderScrollData.h" + +namespace mozilla { +namespace layers { + +/*static*/ +void APZTestAccess::InitializeForTest(WebRenderLayerScrollData& aLayer, + int32_t aDescendantCount) { + aLayer.InitializeForTest(aDescendantCount); +} + +/*static*/ +ScrollMetadata& APZTestAccess::GetScrollMetadataMut( + WebRenderLayerScrollData& aLayer, WebRenderScrollData& aOwner, + size_t aIndex) { + return aLayer.GetScrollMetadataMut(aOwner, aIndex); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/test/gtest/APZTestAccess.h b/gfx/layers/apz/test/gtest/APZTestAccess.h new file mode 100644 index 0000000000..a56fb10a1a --- /dev/null +++ b/gfx/layers/apz/test/gtest/APZTestAccess.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_APZTestAccess_h +#define mozilla_layers_APZTestAccess_h + +#include // for size_t +#include // for int32_t + +namespace mozilla { +namespace layers { + +struct ScrollMetadata; +class WebRenderLayerScrollData; +class WebRenderScrollData; + +// The only purpose of this class is to serve as a single type that can be +// the target of a "friend class" declaration in APZ classes that want to +// give APZ test code access to their private members. +// APZ test code can then access those members via this class. +class APZTestAccess { + public: + static void InitializeForTest(WebRenderLayerScrollData& aLayer, + int32_t aDescendantCount); + static ScrollMetadata& GetScrollMetadataMut(WebRenderLayerScrollData& aLayer, + WebRenderScrollData& aOwner, + size_t aIndex); +}; + +} // namespace layers +} // namespace mozilla + +#endif diff --git a/gfx/layers/apz/test/gtest/APZTestCommon.cpp b/gfx/layers/apz/test/gtest/APZTestCommon.cpp new file mode 100644 index 0000000000..5276531a26 --- /dev/null +++ b/gfx/layers/apz/test/gtest/APZTestCommon.cpp @@ -0,0 +1,15 @@ +/* -*- 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 "APZTestCommon.h" + +AsyncPanZoomController* TestAPZCTreeManager::NewAPZCInstance( + LayersId aLayersId, GeckoContentController* aController) { + MockContentControllerDelayed* mcc = + static_cast(aController); + return new TestAsyncPanZoomController( + aLayersId, mcc, this, AsyncPanZoomController::USE_GESTURE_DETECTOR); +} diff --git a/gfx/layers/apz/test/gtest/APZTestCommon.h b/gfx/layers/apz/test/gtest/APZTestCommon.h new file mode 100644 index 0000000000..e4552bdd11 --- /dev/null +++ b/gfx/layers/apz/test/gtest/APZTestCommon.h @@ -0,0 +1,1051 @@ +/* -*- 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_APZTestCommon_h +#define mozilla_layers_APZTestCommon_h + +/** + * Defines a set of mock classes and utility functions/classes for + * writing APZ gtests. + */ + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "mozilla/Attributes.h" +#include "mozilla/layers/GeckoContentController.h" +#include "mozilla/layers/CompositorBridgeParent.h" +#include "mozilla/layers/APZThreadUtils.h" +#include "mozilla/layers/MatrixMessage.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/TypedEnumBits.h" +#include "mozilla/UniquePtr.h" +#include "apz/src/APZCTreeManager.h" +#include "apz/src/AsyncPanZoomController.h" +#include "apz/src/HitTestingTreeNode.h" +#include "base/task.h" +#include "gfxPlatform.h" +#include "TestWRScrollData.h" +#include "UnitTransforms.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::layers; +using ::testing::_; +using ::testing::AtLeast; +using ::testing::AtMost; +using ::testing::InSequence; +using ::testing::MockFunction; +using ::testing::NiceMock; +typedef mozilla::layers::GeckoContentController::TapType TapType; + +inline TimeStamp GetStartupTime() { + static TimeStamp sStartupTime = TimeStamp::Now(); + return sStartupTime; +} + +inline uint32_t MillisecondsSinceStartup(TimeStamp aTime) { + return (aTime - GetStartupTime()).ToMilliseconds(); +} + +// Some helper functions for constructing input event objects suitable to be +// passed either to an APZC (which expects an transformed point), or to an APZTM +// (which expects an untransformed point). We handle both cases by setting both +// the transformed and untransformed fields to the same value. +inline SingleTouchData CreateSingleTouchData(int32_t aIdentifier, + const ScreenIntPoint& aPoint) { + SingleTouchData touch(aIdentifier, aPoint, ScreenSize(0, 0), 0, 0); + touch.mLocalScreenPoint = ParentLayerPoint(aPoint.x, aPoint.y); + return touch; +} + +// Convenience wrapper for CreateSingleTouchData() that takes loose coordinates. +inline SingleTouchData CreateSingleTouchData(int32_t aIdentifier, + ScreenIntCoord aX, + ScreenIntCoord aY) { + return CreateSingleTouchData(aIdentifier, ScreenIntPoint(aX, aY)); +} + +inline PinchGestureInput CreatePinchGestureInput( + PinchGestureInput::PinchGestureType aType, const ScreenPoint& aFocus, + float aCurrentSpan, float aPreviousSpan, TimeStamp timestamp) { + ParentLayerPoint localFocus(aFocus.x, aFocus.y); + PinchGestureInput result(aType, PinchGestureInput::UNKNOWN, timestamp, + ExternalPoint(0, 0), aFocus, aCurrentSpan, + aPreviousSpan, 0); + return result; +} + +template +class ScopedGfxSetting { + public: + ScopedGfxSetting(const std::function& aGetPrefFunc, + const std::function& aSetPrefFunc, SetArg aVal) + : mSetPrefFunc(aSetPrefFunc) { + mOldVal = aGetPrefFunc(); + aSetPrefFunc(aVal); + } + + ~ScopedGfxSetting() { mSetPrefFunc(mOldVal); } + + private: + std::function mSetPrefFunc; + Storage mOldVal; +}; + +static inline constexpr auto kDefaultTouchBehavior = + AllowedTouchBehavior::VERTICAL_PAN | AllowedTouchBehavior::HORIZONTAL_PAN | + AllowedTouchBehavior::PINCH_ZOOM | AllowedTouchBehavior::ANIMATING_ZOOM; + +#define FRESH_PREF_VAR_PASTE(id, line) id##line +#define FRESH_PREF_VAR_EXPAND(id, line) FRESH_PREF_VAR_PASTE(id, line) +#define FRESH_PREF_VAR FRESH_PREF_VAR_EXPAND(pref, __LINE__) + +#define SCOPED_GFX_PREF_BOOL(prefName, prefValue) \ + ScopedGfxSetting FRESH_PREF_VAR( \ + [=]() { return Preferences::GetBool(prefName); }, \ + [=](bool aPrefValue) { Preferences::SetBool(prefName, aPrefValue); }, \ + prefValue) + +#define SCOPED_GFX_PREF_INT(prefName, prefValue) \ + ScopedGfxSetting FRESH_PREF_VAR( \ + [=]() { return Preferences::GetInt(prefName); }, \ + [=](int32_t aPrefValue) { Preferences::SetInt(prefName, aPrefValue); }, \ + prefValue) + +#define SCOPED_GFX_PREF_FLOAT(prefName, prefValue) \ + ScopedGfxSetting FRESH_PREF_VAR( \ + [=]() { return Preferences::GetFloat(prefName); }, \ + [=](float aPrefValue) { Preferences::SetFloat(prefName, aPrefValue); }, \ + prefValue) + +class MockContentController : public GeckoContentController { + public: + MOCK_METHOD1(NotifyLayerTransforms, void(nsTArray&&)); + MOCK_METHOD1(RequestContentRepaint, void(const RepaintRequest&)); + MOCK_METHOD5(HandleTap, void(TapType, const LayoutDevicePoint&, Modifiers, + const ScrollableLayerGuid&, uint64_t)); + MOCK_METHOD5(NotifyPinchGesture, + void(PinchGestureInput::PinchGestureType, + const ScrollableLayerGuid&, const LayoutDevicePoint&, + LayoutDeviceCoord, Modifiers)); + // Can't use the macros with already_AddRefed :( + void PostDelayedTask(already_AddRefed aTask, int aDelayMs) { + RefPtr task = aTask; + } + bool IsRepaintThread() { return NS_IsMainThread(); } + void DispatchToRepaintThread(already_AddRefed aTask) { + NS_DispatchToMainThread(std::move(aTask)); + } + MOCK_METHOD4(NotifyAPZStateChange, + void(const ScrollableLayerGuid& aGuid, APZStateChange aChange, + int aArg, Maybe aInputBlockId)); + MOCK_METHOD0(NotifyFlushComplete, void()); + MOCK_METHOD3(NotifyAsyncScrollbarDragInitiated, + void(uint64_t, const ScrollableLayerGuid::ViewID&, + ScrollDirection aDirection)); + MOCK_METHOD1(NotifyAsyncScrollbarDragRejected, + void(const ScrollableLayerGuid::ViewID&)); + MOCK_METHOD1(NotifyAsyncAutoscrollRejected, + void(const ScrollableLayerGuid::ViewID&)); + MOCK_METHOD1(CancelAutoscroll, void(const ScrollableLayerGuid&)); + MOCK_METHOD2(NotifyScaleGestureComplete, + void(const ScrollableLayerGuid&, float aScale)); + MOCK_METHOD4(UpdateOverscrollVelocity, + void(const ScrollableLayerGuid&, float, float, bool)); + MOCK_METHOD4(UpdateOverscrollOffset, + void(const ScrollableLayerGuid&, float, float, bool)); +}; + +class MockContentControllerDelayed : public MockContentController { + public: + MockContentControllerDelayed() + : mTime(SampleTime::FromTest(GetStartupTime())) {} + + const TimeStamp& Time() { return mTime.Time(); } + const SampleTime& GetSampleTime() { return mTime; } + + void AdvanceByMillis(int aMillis) { + AdvanceBy(TimeDuration::FromMilliseconds(aMillis)); + } + + void AdvanceBy(const TimeDuration& aIncrement) { + SampleTime target = mTime + aIncrement; + while (mTaskQueue.Length() > 0 && mTaskQueue[0].second <= target) { + RunNextDelayedTask(); + } + mTime = target; + } + + void PostDelayedTask(already_AddRefed aTask, int aDelayMs) { + RefPtr task = aTask; + SampleTime runAtTime = mTime + TimeDuration::FromMilliseconds(aDelayMs); + int insIndex = mTaskQueue.Length(); + while (insIndex > 0) { + if (mTaskQueue[insIndex - 1].second <= runAtTime) { + break; + } + insIndex--; + } + mTaskQueue.InsertElementAt(insIndex, std::make_pair(task, runAtTime)); + } + + // Run all the tasks in the queue, returning the number of tasks + // run. Note that if a task queues another task while running, that + // new task will not be run. Therefore, there may be still be tasks + // in the queue after this function is called. Only when the return + // value is 0 is the queue guaranteed to be empty. + int RunThroughDelayedTasks() { + nsTArray, SampleTime>> runQueue = + std::move(mTaskQueue); + int numTasks = runQueue.Length(); + for (int i = 0; i < numTasks; i++) { + mTime = runQueue[i].second; + runQueue[i].first->Run(); + + // Deleting the task is important in order to release the reference to + // the callee object. + runQueue[i].first = nullptr; + } + return numTasks; + } + + private: + void RunNextDelayedTask() { + std::pair, SampleTime> next = mTaskQueue[0]; + mTaskQueue.RemoveElementAt(0); + mTime = next.second; + next.first->Run(); + // Deleting the task is important in order to release the reference to + // the callee object. + next.first = nullptr; + } + + // The following array is sorted by timestamp (tasks are inserted in order by + // timestamp). + nsTArray, SampleTime>> mTaskQueue; + SampleTime mTime; +}; + +class TestAPZCTreeManager : public APZCTreeManager { + public: + explicit TestAPZCTreeManager(MockContentControllerDelayed* aMcc, + UniquePtr aHitTester = nullptr) + : APZCTreeManager(LayersId{0}, std::move(aHitTester)), mcc(aMcc) {} + + RefPtr GetInputQueue() const { return mInputQueue; } + + void ClearContentController() { mcc = nullptr; } + + /** + * This function is not currently implemented. + * See bug 1468804 for more information. + **/ + void CancelAnimation() { EXPECT_TRUE(false); } + + APZEventResult ReceiveInputEvent( + InputData& aEvent, + InputBlockCallback&& aCallback = InputBlockCallback()) override { + APZEventResult result = + APZCTreeManager::ReceiveInputEvent(aEvent, std::move(aCallback)); + if (aEvent.mInputType == PANGESTURE_INPUT && + // In the APZCTreeManager::ReceiveInputEvent some type of pan gesture + // events are marked as `mHandledByAPZ = false` (e.g. with Ctrl key + // modifier which causes reflow zoom), in such cases the events will + // never be processed by InputQueue so we shouldn't try to invoke + // AllowsSwipe() here. + aEvent.AsPanGestureInput().mHandledByAPZ && + aEvent.AsPanGestureInput().AllowsSwipe()) { + SetBrowserGestureResponse(result.mInputBlockId, + BrowserGestureResponse::NotConsumed); + } + return result; + } + + protected: + AsyncPanZoomController* NewAPZCInstance( + LayersId aLayersId, GeckoContentController* aController) override; + + SampleTime GetFrameTime() override { return mcc->GetSampleTime(); } + + private: + RefPtr mcc; +}; + +class TestAsyncPanZoomController : public AsyncPanZoomController { + public: + TestAsyncPanZoomController(LayersId aLayersId, + MockContentControllerDelayed* aMcc, + TestAPZCTreeManager* aTreeManager, + GestureBehavior aBehavior = DEFAULT_GESTURES) + : AsyncPanZoomController(aLayersId, aTreeManager, + aTreeManager->GetInputQueue(), aMcc, aBehavior), + mWaitForMainThread(false), + mcc(aMcc) {} + + APZEventResult ReceiveInputEvent( + InputData& aEvent, + const Maybe>& aTouchBehaviors = Nothing()) { + // This is a function whose signature matches exactly the ReceiveInputEvent + // on APZCTreeManager. This allows us to templates for functions like + // TouchDown, TouchUp, etc so that we can reuse the code for dispatching + // events into both APZC and APZCTM. + APZEventResult result = GetInputQueue()->ReceiveInputEvent( + this, TargetConfirmationFlags{!mWaitForMainThread}, aEvent, + aTouchBehaviors); + + if (aEvent.mInputType == PANGESTURE_INPUT && + aEvent.AsPanGestureInput().AllowsSwipe()) { + GetInputQueue()->SetBrowserGestureResponse( + result.mInputBlockId, BrowserGestureResponse::NotConsumed); + } + return result; + } + + void ContentReceivedInputBlock(uint64_t aInputBlockId, bool aPreventDefault) { + GetInputQueue()->ContentReceivedInputBlock(aInputBlockId, aPreventDefault); + } + + void ConfirmTarget(uint64_t aInputBlockId) { + RefPtr target = this; + GetInputQueue()->SetConfirmedTargetApzc(aInputBlockId, target); + } + + void SetAllowedTouchBehavior(uint64_t aInputBlockId, + const nsTArray& aBehaviors) { + GetInputQueue()->SetAllowedTouchBehavior(aInputBlockId, aBehaviors); + } + + void SetFrameMetrics(const FrameMetrics& metrics) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + Metrics() = metrics; + } + + void SetScrollMetadata(const ScrollMetadata& aMetadata) { + RecursiveMutexAutoLock lock(mRecursiveMutex); + mScrollMetadata = aMetadata; + } + + FrameMetrics& GetFrameMetrics() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mScrollMetadata.GetMetrics(); + } + + ScrollMetadata& GetScrollMetadata() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mScrollMetadata; + } + + const FrameMetrics& GetFrameMetrics() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + return mScrollMetadata.GetMetrics(); + } + + using AsyncPanZoomController::GetOverscrollAmount; + using AsyncPanZoomController::GetVelocityVector; + + void AssertStateIsReset() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(NOTHING, mState); + } + + void AssertStateIsFling() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(FLING, mState); + } + + void AssertStateIsSmoothScroll() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(SMOOTH_SCROLL, mState); + } + + void AssertStateIsSmoothMsdScroll() const { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(SMOOTHMSD_SCROLL, mState); + } + + void AssertStateIsPanningLockedY() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(PANNING_LOCKED_Y, mState); + } + + void AssertStateIsPanningLockedX() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(PANNING_LOCKED_X, mState); + } + + void AssertStateIsPanning() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(PANNING, mState); + } + + void AssertStateIsPanMomentum() { + RecursiveMutexAutoLock lock(mRecursiveMutex); + EXPECT_EQ(PAN_MOMENTUM, mState); + } + + void SetAxisLocked(ScrollDirections aDirections, bool aLockValue) { + if (aDirections.contains(ScrollDirection::eVertical)) { + mY.SetAxisLocked(aLockValue); + } + if (aDirections.contains(ScrollDirection::eHorizontal)) { + mX.SetAxisLocked(aLockValue); + } + } + + void AssertNotAxisLocked() const { + EXPECT_FALSE(mY.IsAxisLocked()); + EXPECT_FALSE(mX.IsAxisLocked()); + } + + void AssertAxisLocked(ScrollDirection aDirection) const { + switch (aDirection) { + case ScrollDirection::eHorizontal: + EXPECT_TRUE(mY.IsAxisLocked()); + EXPECT_FALSE(mX.IsAxisLocked()); + break; + case ScrollDirection::eVertical: + EXPECT_TRUE(mX.IsAxisLocked()); + EXPECT_FALSE(mY.IsAxisLocked()); + break; + default: + FAIL() << "input direction must be either vertical or horizontal"; + } + } + + void AdvanceAnimationsUntilEnd( + const TimeDuration& aIncrement = TimeDuration::FromMilliseconds(10)) { + while (AdvanceAnimations(mcc->GetSampleTime())) { + mcc->AdvanceBy(aIncrement); + } + } + + bool SampleContentTransformForFrame( + AsyncTransform* aOutTransform, ParentLayerPoint& aScrollOffset, + const TimeDuration& aIncrement = TimeDuration::FromMilliseconds(0)) { + mcc->AdvanceBy(aIncrement); + bool ret = AdvanceAnimations(mcc->GetSampleTime()); + if (aOutTransform) { + *aOutTransform = + GetCurrentAsyncTransform(AsyncPanZoomController::eForHitTesting); + } + aScrollOffset = + GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + return ret; + } + + CSSPoint GetCompositedScrollOffset() const { + return GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForCompositing) / + GetFrameMetrics().GetZoom(); + } + + void SetWaitForMainThread() { mWaitForMainThread = true; } + + bool IsOverscrollAnimationRunning() const { + return mState == PanZoomState::OVERSCROLL_ANIMATION; + } + + private: + bool mWaitForMainThread; + MockContentControllerDelayed* mcc; +}; + +class APZCTesterBase : public ::testing::Test { + public: + APZCTesterBase() { mcc = new NiceMock(); } + + void SetUp() override { + gfxPlatform::GetPlatform(); + // This pref is changed in Pan() without using SCOPED_GFX_PREF + // because the modified value needs to be in place until the touch + // events are processed, which may not happen until the input queue + // is flushed in TearDown(). So, we save and restore its value here. + mTouchStartTolerance = StaticPrefs::apz_touch_start_tolerance(); + } + + void TearDown() override { + Preferences::SetFloat("apz.touch_start_tolerance", mTouchStartTolerance); + } + + enum class PanOptions { + None = 0, + KeepFingerDown = 0x1, + /* + * Do not adjust the touch-start coordinates to overcome the touch-start + * tolerance threshold. If this option is passed, it's up to the caller + * to pass in coordinates that are sufficient to overcome the touch-start + * tolerance *and* cause the desired amount of scrolling. + */ + ExactCoordinates = 0x2, + NoFling = 0x4 + }; + + enum class PinchOptions { + None = 0, + LiftFinger1 = 0x1, + LiftFinger2 = 0x2, + /* + * The bitwise OR result of (LiftFinger1 | LiftFinger2). + * Defined explicitly here because it is used as the default + * argument for PinchWithTouchInput which is defined BEFORE the + * definition of operator| for this class. + */ + LiftBothFingers = 0x3 + }; + + template + APZEventResult Tap(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, TimeDuration aTapLength, + nsEventStatus (*aOutEventStatuses)[2] = nullptr, + uint64_t* aOutInputBlockId = nullptr); + + template + void TapAndCheckStatus(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, TimeDuration aTapLength); + + template + void Pan(const RefPtr& aTarget, + const ScreenIntPoint& aTouchStart, const ScreenIntPoint& aTouchEnd, + PanOptions aOptions = PanOptions::None, + nsTArray* aAllowedTouchBehaviors = nullptr, + nsEventStatus (*aOutEventStatuses)[4] = nullptr, + uint64_t* aOutInputBlockId = nullptr); + + /* + * A version of Pan() that only takes y coordinates rather than (x, y) points + * for the touch start and end points, and uses 10 for the x coordinates. + * This is for convenience, as most tests only need to pan in one direction. + */ + template + void Pan(const RefPtr& aTarget, int aTouchStartY, + int aTouchEndY, PanOptions aOptions = PanOptions::None, + nsTArray* aAllowedTouchBehaviors = nullptr, + nsEventStatus (*aOutEventStatuses)[4] = nullptr, + uint64_t* aOutInputBlockId = nullptr); + + /* + * Dispatches mock touch events to the apzc and checks whether apzc properly + * consumed them and triggered scrolling behavior. + */ + template + void PanAndCheckStatus(const RefPtr& aTarget, int aTouchStartY, + int aTouchEndY, bool aExpectConsumed, + nsTArray* aAllowedTouchBehaviors, + uint64_t* aOutInputBlockId = nullptr); + + template + void DoubleTap(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, + nsEventStatus (*aOutEventStatuses)[4] = nullptr, + uint64_t (*aOutInputBlockIds)[2] = nullptr); + + template + void DoubleTapAndCheckStatus(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, + uint64_t (*aOutInputBlockIds)[2] = nullptr); + + template + void PinchWithTouchInput( + const RefPtr& aTarget, const ScreenIntPoint& aFocus, + const ScreenIntPoint& aSecondFocus, float aScale, int& inputId, + nsTArray* aAllowedTouchBehaviors = nullptr, + nsEventStatus (*aOutEventStatuses)[4] = nullptr, + uint64_t* aOutInputBlockId = nullptr, + PinchOptions aOptions = PinchOptions::LiftBothFingers, + bool aVertical = false); + + // Pinch with one focus point. Zooms in place with no panning + template + void PinchWithTouchInput( + const RefPtr& aTarget, const ScreenIntPoint& aFocus, + float aScale, int& inputId, + nsTArray* aAllowedTouchBehaviors = nullptr, + nsEventStatus (*aOutEventStatuses)[4] = nullptr, + uint64_t* aOutInputBlockId = nullptr, + PinchOptions aOptions = PinchOptions::LiftBothFingers, + bool aVertical = false); + + template + void PinchWithTouchInputAndCheckStatus( + const RefPtr& aTarget, const ScreenIntPoint& aFocus, + float aScale, int& inputId, bool aShouldTriggerPinch, + nsTArray* aAllowedTouchBehaviors); + + template + void PinchWithPinchInput(const RefPtr& aTarget, + const ScreenIntPoint& aFocus, + const ScreenIntPoint& aSecondFocus, float aScale, + nsEventStatus (*aOutEventStatuses)[3] = nullptr); + + template + void PinchWithPinchInputAndCheckStatus(const RefPtr& aTarget, + const ScreenIntPoint& aFocus, + float aScale, + bool aShouldTriggerPinch); + + protected: + RefPtr mcc; + + private: + float mTouchStartTolerance; +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(APZCTesterBase::PanOptions) +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(APZCTesterBase::PinchOptions) + +template +APZEventResult APZCTesterBase::Tap(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, + TimeDuration aTapLength, + nsEventStatus (*aOutEventStatuses)[2], + uint64_t* aOutInputBlockId) { + APZEventResult touchDownResult = TouchDown(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[0] = touchDownResult.GetStatus(); + } + if (aOutInputBlockId) { + *aOutInputBlockId = touchDownResult.mInputBlockId; + } + mcc->AdvanceBy(aTapLength); + + // If touch-action is enabled then simulate the allowed touch behaviour + // notification that the main thread is supposed to deliver. + if (touchDownResult.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(aTarget, touchDownResult.mInputBlockId); + } + + APZEventResult touchUpResult = TouchUp(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[1] = touchUpResult.GetStatus(); + } + return touchDownResult; +} + +template +void APZCTesterBase::TapAndCheckStatus(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, + TimeDuration aTapLength) { + nsEventStatus statuses[2]; + Tap(aTarget, aPoint, aTapLength, &statuses); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[1]); +} + +template +void APZCTesterBase::Pan(const RefPtr& aTarget, + const ScreenIntPoint& aTouchStart, + const ScreenIntPoint& aTouchEnd, PanOptions aOptions, + nsTArray* aAllowedTouchBehaviors, + nsEventStatus (*aOutEventStatuses)[4], + uint64_t* aOutInputBlockId) { + // Reduce the move tolerance to a tiny value. + // We can't use a scoped pref because this value might be read at some later + // time when the events are actually processed, rather than when we deliver + // them. + const float touchStartTolerance = 0.1f; + const float panThreshold = touchStartTolerance * aTarget->GetDPI(); + Preferences::SetFloat("apz.touch_start_tolerance", touchStartTolerance); + Preferences::SetFloat("apz.touch_move_tolerance", 0.0f); + int overcomeTouchToleranceX = 0; + int overcomeTouchToleranceY = 0; + if (!(aOptions & PanOptions::ExactCoordinates)) { + // Have the direction of the adjustment to overcome the touch tolerance + // match the direction of the entire gesture, otherwise we run into + // trouble such as accidentally activating the axis lock. + if (aTouchStart.x != aTouchEnd.x && aTouchStart.y != aTouchEnd.y) { + // Tests that need to avoid rounding error here can arrange for + // panThreshold to be 10 (by setting the DPI to 100), which makes sure + // that these are the legs in a Pythagorean triple where panThreshold is + // the hypotenuse. Watch out for changes of APZCPinchTester::mDPI. + overcomeTouchToleranceX = panThreshold / 10 * 6; + overcomeTouchToleranceY = panThreshold / 10 * 8; + } else if (aTouchStart.x != aTouchEnd.x) { + overcomeTouchToleranceX = panThreshold; + } else if (aTouchStart.y != aTouchEnd.y) { + overcomeTouchToleranceY = panThreshold; + } + } + + const TimeDuration TIME_BETWEEN_TOUCH_EVENT = + TimeDuration::FromMilliseconds(20); + + // Even if the caller doesn't care about the block id, we need it to set the + // allowed touch behaviour below, so make sure aOutInputBlockId is non-null. + uint64_t blockId; + if (!aOutInputBlockId) { + aOutInputBlockId = &blockId; + } + + // Make sure the move is large enough to not be handled as a tap + APZEventResult result = + TouchDown(aTarget, + ScreenIntPoint(aTouchStart.x + overcomeTouchToleranceX, + aTouchStart.y + overcomeTouchToleranceY), + mcc->Time()); + if (aOutInputBlockId) { + *aOutInputBlockId = result.mInputBlockId; + } + if (aOutEventStatuses) { + (*aOutEventStatuses)[0] = result.GetStatus(); + } + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Allowed touch behaviours must be set after sending touch-start. + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + if (aAllowedTouchBehaviors) { + EXPECT_EQ(1UL, aAllowedTouchBehaviors->Length()); + aTarget->SetAllowedTouchBehavior(*aOutInputBlockId, + *aAllowedTouchBehaviors); + } else { + SetDefaultAllowedTouchBehavior(aTarget, *aOutInputBlockId); + } + } + + result = TouchMove(aTarget, aTouchStart, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[1] = result.GetStatus(); + } + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + const int numSteps = 3; + auto stepVector = (aTouchEnd - aTouchStart) / numSteps; + for (int k = 1; k < numSteps; k++) { + auto stepPoint = aTouchStart + stepVector * k; + Unused << TouchMove(aTarget, stepPoint, mcc->Time()); + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + } + + result = TouchMove(aTarget, aTouchEnd, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[2] = result.GetStatus(); + } + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + if (!(aOptions & PanOptions::KeepFingerDown)) { + result = TouchUp(aTarget, aTouchEnd, mcc->Time()); + } else { + result.SetStatusAsIgnore(); + } + if (aOutEventStatuses) { + (*aOutEventStatuses)[3] = result.GetStatus(); + } + + if ((aOptions & PanOptions::NoFling)) { + aTarget->CancelAnimation(); + } + + // Don't increment the time here. Animations started on touch-up, such as + // flings, are affected by elapsed time, and we want to be able to sample + // them immediately after they start, without time having elapsed. +} + +template +void APZCTesterBase::Pan(const RefPtr& aTarget, int aTouchStartY, + int aTouchEndY, PanOptions aOptions, + nsTArray* aAllowedTouchBehaviors, + nsEventStatus (*aOutEventStatuses)[4], + uint64_t* aOutInputBlockId) { + Pan(aTarget, ScreenIntPoint(10, aTouchStartY), ScreenIntPoint(10, aTouchEndY), + aOptions, aAllowedTouchBehaviors, aOutEventStatuses, aOutInputBlockId); +} + +template +void APZCTesterBase::PanAndCheckStatus( + const RefPtr& aTarget, int aTouchStartY, int aTouchEndY, + bool aExpectConsumed, nsTArray* aAllowedTouchBehaviors, + uint64_t* aOutInputBlockId) { + nsEventStatus statuses[4]; // down, move, move, up + Pan(aTarget, aTouchStartY, aTouchEndY, PanOptions::None, + aAllowedTouchBehaviors, &statuses, aOutInputBlockId); + + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]); + + nsEventStatus touchMoveStatus; + if (aExpectConsumed) { + touchMoveStatus = nsEventStatus_eConsumeDoDefault; + } else { + touchMoveStatus = nsEventStatus_eIgnore; + } + EXPECT_EQ(touchMoveStatus, statuses[1]); + EXPECT_EQ(touchMoveStatus, statuses[2]); +} + +template +void APZCTesterBase::DoubleTap(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, + nsEventStatus (*aOutEventStatuses)[4], + uint64_t (*aOutInputBlockIds)[2]) { + APZEventResult result = TouchDown(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[0] = result.GetStatus(); + } + if (aOutInputBlockIds) { + (*aOutInputBlockIds)[0] = result.mInputBlockId; + } + mcc->AdvanceByMillis(10); + + // If touch-action is enabled then simulate the allowed touch behaviour + // notification that the main thread is supposed to deliver. + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(aTarget, result.mInputBlockId); + } + + result = TouchUp(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[1] = result.GetStatus(); + } + mcc->AdvanceByMillis(10); + result = TouchDown(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[2] = result.GetStatus(); + } + if (aOutInputBlockIds) { + (*aOutInputBlockIds)[1] = result.mInputBlockId; + } + mcc->AdvanceByMillis(10); + + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(aTarget, result.mInputBlockId); + } + + result = TouchUp(aTarget, aPoint, mcc->Time()); + if (aOutEventStatuses) { + (*aOutEventStatuses)[3] = result.GetStatus(); + } +} + +template +void APZCTesterBase::DoubleTapAndCheckStatus( + const RefPtr& aTarget, const ScreenIntPoint& aPoint, + uint64_t (*aOutInputBlockIds)[2]) { + nsEventStatus statuses[4]; + DoubleTap(aTarget, aPoint, &statuses, aOutInputBlockIds); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[1]); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[2]); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[3]); +} + +template +void APZCTesterBase::PinchWithTouchInput( + const RefPtr& aTarget, const ScreenIntPoint& aFocus, + float aScale, int& inputId, nsTArray* aAllowedTouchBehaviors, + nsEventStatus (*aOutEventStatuses)[4], uint64_t* aOutInputBlockId, + PinchOptions aOptions, bool aVertical) { + // Perform a pinch gesture with the same start & end focus point + PinchWithTouchInput(aTarget, aFocus, aFocus, aScale, inputId, + aAllowedTouchBehaviors, aOutEventStatuses, + aOutInputBlockId, aOptions, aVertical); +} + +template +void APZCTesterBase::PinchWithTouchInput( + const RefPtr& aTarget, const ScreenIntPoint& aFocus, + const ScreenIntPoint& aSecondFocus, float aScale, int& inputId, + nsTArray* aAllowedTouchBehaviors, + nsEventStatus (*aOutEventStatuses)[4], uint64_t* aOutInputBlockId, + PinchOptions aOptions, bool aVertical) { + // Having pinch coordinates in float type may cause problems with + // high-precision scale values since SingleTouchData accepts integer value. + // But for trivial tests it should be ok. + const float pinchLength = 100.0; + const float pinchLengthScaled = pinchLength * aScale; + + const float pinchLengthX = aVertical ? 0 : pinchLength; + const float pinchLengthScaledX = aVertical ? 0 : pinchLengthScaled; + const float pinchLengthY = aVertical ? pinchLength : 0; + const float pinchLengthScaledY = aVertical ? pinchLengthScaled : 0; + + // Even if the caller doesn't care about the block id, we need it to set the + // allowed touch behaviour below, so make sure aOutInputBlockId is non-null. + uint64_t blockId; + if (!aOutInputBlockId) { + aOutInputBlockId = &blockId; + } + + const TimeDuration TIME_BETWEEN_TOUCH_EVENT = + TimeDuration::FromMilliseconds(20); + + MultiTouchInput mtiStart = + MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0); + mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId, aFocus)); + mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, aFocus)); + APZEventResult result; + result = aTarget->ReceiveInputEvent(mtiStart); + if (aOutInputBlockId) { + *aOutInputBlockId = result.mInputBlockId; + } + if (aOutEventStatuses) { + (*aOutEventStatuses)[0] = result.GetStatus(); + } + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + if (aAllowedTouchBehaviors) { + EXPECT_EQ(2UL, aAllowedTouchBehaviors->Length()); + aTarget->SetAllowedTouchBehavior(*aOutInputBlockId, + *aAllowedTouchBehaviors); + } else { + SetDefaultAllowedTouchBehavior(aTarget, *aOutInputBlockId, 2); + } + + ScreenIntPoint pinchStartPoint1(aFocus.x - int32_t(pinchLengthX), + aFocus.y - int32_t(pinchLengthY)); + ScreenIntPoint pinchStartPoint2(aFocus.x + int32_t(pinchLengthX), + aFocus.y + int32_t(pinchLengthY)); + + MultiTouchInput mtiMove1 = + MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mtiMove1.mTouches.AppendElement( + CreateSingleTouchData(inputId, pinchStartPoint1)); + mtiMove1.mTouches.AppendElement( + CreateSingleTouchData(inputId + 1, pinchStartPoint2)); + result = aTarget->ReceiveInputEvent(mtiMove1); + if (aOutEventStatuses) { + (*aOutEventStatuses)[1] = result.GetStatus(); + } + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Pinch instantly but move in steps. + const int numSteps = 3; + auto stepVector = (aSecondFocus - aFocus) / numSteps; + for (int k = 1; k < numSteps; k++) { + ScreenIntPoint stepFocus = aFocus + stepVector * k; + ScreenIntPoint stepPoint1(stepFocus.x - int32_t(pinchLengthScaledX), + stepFocus.y - int32_t(pinchLengthScaledY)); + ScreenIntPoint stepPoint2(stepFocus.x + int32_t(pinchLengthScaledX), + stepFocus.y + int32_t(pinchLengthScaledY)); + MultiTouchInput mtiMoveStep = + MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mtiMoveStep.mTouches.AppendElement( + CreateSingleTouchData(inputId, stepPoint1)); + mtiMoveStep.mTouches.AppendElement( + CreateSingleTouchData(inputId + 1, stepPoint2)); + Unused << aTarget->ReceiveInputEvent(mtiMoveStep); + + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + } + + ScreenIntPoint pinchEndPoint1(aSecondFocus.x - int32_t(pinchLengthScaledX), + aSecondFocus.y - int32_t(pinchLengthScaledY)); + ScreenIntPoint pinchEndPoint2(aSecondFocus.x + int32_t(pinchLengthScaledX), + aSecondFocus.y + int32_t(pinchLengthScaledY)); + + MultiTouchInput mtiMove2 = + MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mtiMove2.mTouches.AppendElement( + CreateSingleTouchData(inputId, pinchEndPoint1)); + mtiMove2.mTouches.AppendElement( + CreateSingleTouchData(inputId + 1, pinchEndPoint2)); + result = aTarget->ReceiveInputEvent(mtiMove2); + if (aOutEventStatuses) { + (*aOutEventStatuses)[2] = result.GetStatus(); + } + + if (aOptions & (PinchOptions::LiftFinger1 | PinchOptions::LiftFinger2)) { + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + MultiTouchInput mtiEnd = + MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0); + if (aOptions & PinchOptions::LiftFinger1) { + mtiEnd.mTouches.AppendElement( + CreateSingleTouchData(inputId, pinchEndPoint1)); + } + if (aOptions & PinchOptions::LiftFinger2) { + mtiEnd.mTouches.AppendElement( + CreateSingleTouchData(inputId + 1, pinchEndPoint2)); + } + result = aTarget->ReceiveInputEvent(mtiEnd); + if (aOutEventStatuses) { + (*aOutEventStatuses)[3] = result.GetStatus(); + } + } + + inputId += 2; +} + +template +void APZCTesterBase::PinchWithTouchInputAndCheckStatus( + const RefPtr& aTarget, const ScreenIntPoint& aFocus, + float aScale, int& inputId, bool aShouldTriggerPinch, + nsTArray* aAllowedTouchBehaviors) { + nsEventStatus statuses[4]; // down, move, move, up + PinchWithTouchInput(aTarget, aFocus, aScale, inputId, aAllowedTouchBehaviors, + &statuses); + + nsEventStatus expectedMoveStatus = aShouldTriggerPinch + ? nsEventStatus_eConsumeDoDefault + : nsEventStatus_eIgnore; + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]); + EXPECT_EQ(expectedMoveStatus, statuses[1]); + EXPECT_EQ(expectedMoveStatus, statuses[2]); +} + +template +void APZCTesterBase::PinchWithPinchInput( + const RefPtr& aTarget, const ScreenIntPoint& aFocus, + const ScreenIntPoint& aSecondFocus, float aScale, + nsEventStatus (*aOutEventStatuses)[3]) { + const TimeDuration TIME_BETWEEN_PINCH_INPUT = + TimeDuration::FromMilliseconds(50); + + auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START, + aFocus, 10.0, 10.0, mcc->Time()); + APZEventResult actual = aTarget->ReceiveInputEvent(event); + if (aOutEventStatuses) { + (*aOutEventStatuses)[0] = actual.GetStatus(); + } + mcc->AdvanceBy(TIME_BETWEEN_PINCH_INPUT); + + event = + CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, + aSecondFocus, 10.0 * aScale, 10.0, mcc->Time()); + actual = aTarget->ReceiveInputEvent(event); + if (aOutEventStatuses) { + (*aOutEventStatuses)[1] = actual.GetStatus(); + } + mcc->AdvanceBy(TIME_BETWEEN_PINCH_INPUT); + + event = + CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_END, aSecondFocus, + 10.0 * aScale, 10.0 * aScale, mcc->Time()); + actual = aTarget->ReceiveInputEvent(event); + if (aOutEventStatuses) { + (*aOutEventStatuses)[2] = actual.GetStatus(); + } +} + +template +void APZCTesterBase::PinchWithPinchInputAndCheckStatus( + const RefPtr& aTarget, const ScreenIntPoint& aFocus, + float aScale, bool aShouldTriggerPinch) { + nsEventStatus statuses[3]; // scalebegin, scale, scaleend + PinchWithPinchInput(aTarget, aFocus, aFocus, aScale, &statuses); + + nsEventStatus expectedStatus = aShouldTriggerPinch + ? nsEventStatus_eConsumeDoDefault + : nsEventStatus_eIgnore; + EXPECT_EQ(expectedStatus, statuses[0]); + EXPECT_EQ(expectedStatus, statuses[1]); +} + +inline FrameMetrics TestFrameMetrics() { + FrameMetrics fm; + + fm.SetDisplayPort(CSSRect(0, 0, 10, 10)); + fm.SetCompositionBounds(ParentLayerRect(0, 0, 10, 10)); + fm.SetScrollableRect(CSSRect(0, 0, 100, 100)); + + return fm; +} + +#endif // mozilla_layers_APZTestCommon_h diff --git a/gfx/layers/apz/test/gtest/InputUtils.h b/gfx/layers/apz/test/gtest/InputUtils.h new file mode 100644 index 0000000000..74f4b640b3 --- /dev/null +++ b/gfx/layers/apz/test/gtest/InputUtils.h @@ -0,0 +1,149 @@ +/* -*- 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_InputUtils_h +#define mozilla_layers_InputUtils_h + +/** + * Defines a set of utility functions for generating input events + * to an APZC/APZCTM during APZ gtests. + */ + +#include "APZTestCommon.h" + +/* The InputReceiver template parameter used in the helper functions below needs + * to be a class that implements functions with the signatures: + * APZEventResult ReceiveInputEvent(const InputData& aEvent); + * void SetAllowedTouchBehavior(uint64_t aInputBlockId, + * const nsTArray& aBehaviours); + * The classes that currently implement these are APZCTreeManager and + * TestAsyncPanZoomController. Using this template allows us to test individual + * APZC instances in isolation and also an entire APZ tree, while using the same + * code to dispatch input events. + */ + +template +void SetDefaultAllowedTouchBehavior(const RefPtr& aTarget, + uint64_t aInputBlockId, + int touchPoints = 1) { + nsTArray defaultBehaviors; + // use the default value where everything is allowed + for (int i = 0; i < touchPoints; i++) { + defaultBehaviors.AppendElement( + mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN | + mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN | + mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM | + mozilla::layers::AllowedTouchBehavior::ANIMATING_ZOOM); + } + aTarget->SetAllowedTouchBehavior(aInputBlockId, defaultBehaviors); +} + +inline MultiTouchInput CreateMultiTouchInput( + MultiTouchInput::MultiTouchType aType, TimeStamp aTime) { + return MultiTouchInput(aType, MillisecondsSinceStartup(aTime), aTime, 0); +} + +template +APZEventResult TouchDown(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, TimeStamp aTime) { + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, aTime); + mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint)); + return aTarget->ReceiveInputEvent(mti); +} + +template +APZEventResult TouchMove(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, TimeStamp aTime) { + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, aTime); + mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint)); + return aTarget->ReceiveInputEvent(mti); +} + +template +APZEventResult TouchUp(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, TimeStamp aTime) { + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, aTime); + mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint)); + return aTarget->ReceiveInputEvent(mti); +} + +template +APZEventResult Wheel(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, const ScreenPoint& aDelta, + TimeStamp aTime) { + ScrollWheelInput input(aTime, 0, ScrollWheelInput::SCROLLMODE_INSTANT, + ScrollWheelInput::SCROLLDELTA_PIXEL, aPoint, aDelta.x, + aDelta.y, false, WheelDeltaAdjustmentStrategy::eNone); + return aTarget->ReceiveInputEvent(input); +} + +template +APZEventResult SmoothWheel(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, + const ScreenPoint& aDelta, TimeStamp aTime) { + ScrollWheelInput input(aTime, 0, ScrollWheelInput::SCROLLMODE_SMOOTH, + ScrollWheelInput::SCROLLDELTA_LINE, aPoint, aDelta.x, + aDelta.y, false, WheelDeltaAdjustmentStrategy::eNone); + return aTarget->ReceiveInputEvent(input); +} + +template +APZEventResult MouseDown(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, TimeStamp aTime) { + MouseInput input(MouseInput::MOUSE_DOWN, + MouseInput::ButtonType::PRIMARY_BUTTON, 0, 0, aPoint, aTime, + 0); + return aTarget->ReceiveInputEvent(input); +} + +template +APZEventResult MouseMove(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, TimeStamp aTime) { + MouseInput input(MouseInput::MOUSE_MOVE, + MouseInput::ButtonType::PRIMARY_BUTTON, 0, 0, aPoint, aTime, + 0); + return aTarget->ReceiveInputEvent(input); +} + +template +APZEventResult MouseUp(const RefPtr& aTarget, + const ScreenIntPoint& aPoint, TimeStamp aTime) { + MouseInput input(MouseInput::MOUSE_UP, MouseInput::ButtonType::PRIMARY_BUTTON, + 0, 0, aPoint, aTime, 0); + return aTarget->ReceiveInputEvent(input); +} + +template +APZEventResult PanGesture(PanGestureInput::PanGestureType aType, + const RefPtr& aTarget, + const ScreenIntPoint& aPoint, + const ScreenPoint& aDelta, TimeStamp aTime, + Modifiers aModifiers = MODIFIER_NONE, + bool aSimulateMomentum = false) { + PanGestureInput input(aType, aTime, aPoint, aDelta, aModifiers); + input.mSimulateMomentum = aSimulateMomentum; + if constexpr (std::is_same_v) { + // In the case of TestAsyncPanZoomController we know for sure that the + // event will be handled by APZ so set it explicitly. + input.mHandledByAPZ = true; + } + return aTarget->ReceiveInputEvent(input); +} + +template +APZEventResult PanGestureWithModifiers(PanGestureInput::PanGestureType aType, + Modifiers aModifiers, + const RefPtr& aTarget, + const ScreenIntPoint& aPoint, + const ScreenPoint& aDelta, + TimeStamp aTime) { + return PanGesture(aType, aTarget, aPoint, aDelta, aTime, aModifiers); +} + +#endif // mozilla_layers_InputUtils_h diff --git a/gfx/layers/apz/test/gtest/MockHitTester.cpp b/gfx/layers/apz/test/gtest/MockHitTester.cpp new file mode 100644 index 0000000000..025866ca65 --- /dev/null +++ b/gfx/layers/apz/test/gtest/MockHitTester.cpp @@ -0,0 +1,38 @@ +/* -*- 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 "MockHitTester.h" +#include "apz/src/AsyncPanZoomController.h" +#include "mozilla/layers/ScrollableLayerGuid.h" + +namespace mozilla::layers { + +IAPZHitTester::HitTestResult MockHitTester::GetAPZCAtPoint( + const ScreenPoint& aHitTestPoint, + const RecursiveMutexAutoLock& aProofOfTreeLock) { + MOZ_ASSERT(!mQueuedResults.empty()); + HitTestResult result = std::move(mQueuedResults.front()); + mQueuedResults.pop(); + return result; +} + +void MockHitTester::QueueHitResult(ScrollableLayerGuid::ViewID aScrollId, + gfx::CompositorHitTestInfo aHitInfo) { + LayersId layersId = GetRootLayersId(); // currently this is all the tests use + RefPtr node = + GetTargetNode(ScrollableLayerGuid(layersId, 0, aScrollId), + ScrollableLayerGuid::EqualsIgnoringPresShell); + MOZ_ASSERT(node); + AsyncPanZoomController* apzc = node->GetApzc(); + MOZ_ASSERT(apzc); + HitTestResult result; + result.mTargetApzc = apzc; + result.mHitResult = aHitInfo; + result.mLayersId = layersId; + mQueuedResults.push(std::move(result)); +} + +} // namespace mozilla::layers diff --git a/gfx/layers/apz/test/gtest/MockHitTester.h b/gfx/layers/apz/test/gtest/MockHitTester.h new file mode 100644 index 0000000000..9c91b31152 --- /dev/null +++ b/gfx/layers/apz/test/gtest/MockHitTester.h @@ -0,0 +1,37 @@ +/* -*- 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_MockHitTester_h +#define mozilla_layers_MockHitTester_h + +#include "apz/src/IAPZHitTester.h" +#include "mozilla/gfx/CompositorHitTestInfo.h" + +#include + +namespace mozilla::layers { + +// IAPZHitTester implementation for APZ gtests. +// This does not actually perform hit-testing, it just allows +// the test code to specify the expected hit test results. +class MockHitTester final : public IAPZHitTester { + public: + HitTestResult GetAPZCAtPoint( + const ScreenPoint& aHitTestPoint, + const RecursiveMutexAutoLock& aProofOfTreeLock) override; + + // Queue a hit test result whose target APZC is the APZC + // with scroll id |aScrollId|, and the provided hit test flags. + void QueueHitResult(ScrollableLayerGuid::ViewID aScrollId, + gfx::CompositorHitTestInfo aHitInfo); + + private: + std::queue mQueuedResults; +}; + +} // namespace mozilla::layers + +#endif // define mozilla_layers_MockHitTester_h diff --git a/gfx/layers/apz/test/gtest/TestAxisLock.cpp b/gfx/layers/apz/test/gtest/TestAxisLock.cpp new file mode 100644 index 0000000000..8b0df3e8a2 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestAxisLock.cpp @@ -0,0 +1,645 @@ +/* -*- 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 "APZCTreeManagerTester.h" +#include "APZTestCommon.h" + +#include "InputUtils.h" +#include "gtest/gtest.h" + +#include + +class APZCAxisLockCompatTester : public APZCTreeManagerTester, + public testing::WithParamInterface { + public: + APZCAxisLockCompatTester() : oldAxisLockMode(0) { CreateMockHitTester(); } + + int oldAxisLockMode; + + UniquePtr registration; + + RefPtr apzc; + + void SetUp() { + APZCTreeManagerTester::SetUp(); + + oldAxisLockMode = Preferences::GetInt("apz.axis_lock.mode"); + + Preferences::SetInt("apz.axis_lock.mode", GetParam()); + } + + void TearDown() { + APZCTreeManagerTester::TearDown(); + + Preferences::SetInt("apz.axis_lock.mode", oldAxisLockMode); + } + + static std::string PrintFromParam(const testing::TestParamInfo& info) { + switch (info.param) { + case 0: + return "FREE"; + case 1: + return "STANDARD"; + case 2: + return "STICKY"; + case 3: + return "DOMINANT_AXIS"; + default: + return "UNKNOWN"; + } + } +}; + +class APZCAxisLockTester : public APZCTreeManagerTester { + public: + APZCAxisLockTester() { CreateMockHitTester(); } + + UniquePtr registration; + + RefPtr apzc; + + void SetupBasicTest() { + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 500, 500)); + + registration = MakeUnique(LayersId{0}, mcc); + + UpdateHitTestingTree(); + } + + void BreakStickyAxisLockTestGesture(const ScrollDirections& aDirections) { + float panX = 0; + float panY = 0; + + if (aDirections.contains(ScrollDirection::eVertical)) { + panY = 30; + } + if (aDirections.contains(ScrollDirection::eHorizontal)) { + panX = 30; + } + + // Kick off the gesture that may lock onto an axis + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(panX, panY), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(panX, panY), mcc->Time()); + } + + void BreakStickyAxisLockTest(const ScrollDirections& aDirections) { + // Create the gesture for the test. + BreakStickyAxisLockTestGesture(aDirections); + + // Based on the scroll direction(s) ensure the state is what we expect. + if (aDirections == ScrollDirection::eVertical) { + apzc->AssertStateIsPanningLockedY(); + apzc->AssertAxisLocked(ScrollDirection::eVertical); + EXPECT_GT(apzc->GetVelocityVector().y, 0); + EXPECT_EQ(apzc->GetVelocityVector().x, 0); + } else if (aDirections == ScrollDirection::eHorizontal) { + apzc->AssertStateIsPanningLockedX(); + apzc->AssertAxisLocked(ScrollDirection::eHorizontal); + EXPECT_GT(apzc->GetVelocityVector().x, 0); + EXPECT_EQ(apzc->GetVelocityVector().y, 0); + } else { + apzc->AssertStateIsPanning(); + apzc->AssertNotAxisLocked(); + EXPECT_GT(apzc->GetVelocityVector().x, 0); + EXPECT_GT(apzc->GetVelocityVector().y, 0); + } + + // Cleanup for next test. + apzc->AdvanceAnimationsUntilEnd(); + } +}; + +TEST_F(APZCAxisLockTester, BasicDominantAxisUse) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 4.0f); + + SetupBasicTest(); + + apzc = ApzcOf(root); + + // Kick off the initial gesture that triggers the momentum scroll. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + + // Should be in a PANNING_LOCKED_Y state with no horizontal velocity. + apzc->AssertStateIsPanningLockedY(); + apzc->AssertAxisLocked(ScrollDirection::eVertical); + EXPECT_GT(apzc->GetVelocityVector().y, 0); + EXPECT_EQ(apzc->GetVelocityVector().x, 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Ensure that we have not panned on the horizontal axis. + ParentLayerPoint panEndOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting); + EXPECT_EQ(panEndOffset.x, 0); + + // The lock onto the Y axis extends into momentum scroll. + apzc->AssertAxisLocked(ScrollDirection::eVertical); + + // Start the momentum scroll. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager, + ScreenIntPoint(50, 50), ScreenPoint(30, 90), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + ScreenIntPoint(50, 50), ScreenPoint(10, 30), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + ScreenIntPoint(50, 50), ScreenPoint(10, 30), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // In momentum locking mode, we should still be locked onto the Y axis. + apzc->AssertStateIsPanMomentum(); + apzc->AssertAxisLocked(ScrollDirection::eVertical); + EXPECT_GT(apzc->GetVelocityVector().y, 0); + EXPECT_EQ(apzc->GetVelocityVector().x, 0); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, manager, + ScreenIntPoint(50, 50), ScreenPoint(0, 0), mcc->Time()); + + // After momentum scroll end, ensure we are no longer locked onto an axis. + apzc->AssertNotAxisLocked(); + + // Wait until the end of the animation and ensure the final state is + // reasonable. + apzc->AdvanceAnimationsUntilEnd(); + ParentLayerPoint finalOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting); + + // Ensure we have scrolled some amount on the Y axis in momentum scroll. + EXPECT_GT(finalOffset.y, panEndOffset.y); + EXPECT_EQ(finalOffset.x, 0.0f); +} + +TEST_F(APZCAxisLockTester, NewGestureBreaksMomentumAxisLock) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 4.0f); + + SetupBasicTest(); + + apzc = ApzcOf(root); + + // Kick off the initial gesture that triggers the momentum scroll. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(2, 1), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(30, 15), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(30, 15), mcc->Time()); + + // Should be in a PANNING_LOCKED_X state with no vertical velocity. + apzc->AssertStateIsPanningLockedX(); + apzc->AssertAxisLocked(ScrollDirection::eHorizontal); + EXPECT_GT(apzc->GetVelocityVector().x, 0); + EXPECT_EQ(apzc->GetVelocityVector().y, 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Double check that we have not panned on the vertical axis. + ParentLayerPoint panEndOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting); + EXPECT_EQ(panEndOffset.y, 0); + + // Ensure that the axis locks extends into momentum scroll. + apzc->AssertAxisLocked(ScrollDirection::eHorizontal); + + // Start the momentum scroll. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager, + ScreenIntPoint(50, 50), ScreenPoint(80, 40), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + ScreenIntPoint(50, 50), ScreenPoint(20, 10), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + ScreenIntPoint(50, 50), ScreenPoint(20, 10), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // In momentum locking mode, we should still be locked onto the X axis. + apzc->AssertStateIsPanMomentum(); + apzc->AssertAxisLocked(ScrollDirection::eHorizontal); + EXPECT_GT(apzc->GetVelocityVector().x, 0); + EXPECT_EQ(apzc->GetVelocityVector().y, 0); + + ParentLayerPoint beforeBreakOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting); + EXPECT_EQ(beforeBreakOffset.y, 0); + // Ensure we have scrolled some amount on the X axis in momentum scroll. + EXPECT_GT(beforeBreakOffset.x, panEndOffset.x); + + // Kick off the gesture that breaks the lock onto the X axis. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + ParentLayerPoint afterBreakOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + + // The lock onto the X axis should be broken and we now should be locked + // onto the Y axis. + apzc->AssertStateIsPanningLockedY(); + apzc->AssertAxisLocked(ScrollDirection::eVertical); + EXPECT_GT(apzc->GetVelocityVector().y, 0); + EXPECT_EQ(apzc->GetVelocityVector().x, 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // The lock onto the Y axis extends into momentum scroll. + apzc->AssertAxisLocked(ScrollDirection::eVertical); + + // Wait until the end of the animation and ensure the final state is + // reasonable. + apzc->AdvanceAnimationsUntilEnd(); + ParentLayerPoint finalOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting); + + EXPECT_GT(finalOffset.y, 0); + // Ensure that we did not scroll on the X axis after the vertical scroll + // started. + EXPECT_EQ(finalOffset.x, afterBreakOffset.x); +} + +TEST_F(APZCAxisLockTester, BreakStickyAxisLock) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 6.0f); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.breakout_angle", M_PI / 6.0f); + + SetupBasicTest(); + + apzc = ApzcOf(root); + + // Start a gesture to get us locked onto the Y axis. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Ensure that we have locked onto the Y axis. + apzc->AssertStateIsPanningLockedY(); + + // Test switch to locking onto the X axis. + BreakStickyAxisLockTest(ScrollDirection::eHorizontal); + + // Test switch back to locking onto the Y axis. + BreakStickyAxisLockTest(ScrollDirection::eVertical); + + // Test breaking all axis locks from a Y axis lock. + BreakStickyAxisLockTest(ScrollDirections(ScrollDirection::eHorizontal, + ScrollDirection::eVertical)); + + // We should be in a panning state. + apzc->AssertStateIsPanning(); + apzc->AssertNotAxisLocked(); + + // Lock back to the X axis. + BreakStickyAxisLockTestGesture(ScrollDirection::eHorizontal); + + // End the gesture. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Start a gesture to get us locked onto the X axis. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(2, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Ensure that we have locked onto the X axis. + apzc->AssertStateIsPanningLockedX(); + + // Test breaking all axis locks from a X axis lock. + BreakStickyAxisLockTest(ScrollDirections(ScrollDirection::eHorizontal, + ScrollDirection::eVertical)); + + // We should be in a panning state. + apzc->AssertStateIsPanning(); + apzc->AssertNotAxisLocked(); + + // Test switch back to locking onto the Y axis. + BreakStickyAxisLockTest(ScrollDirection::eVertical); +} + +TEST_F(APZCAxisLockTester, BreakAxisLockByLockAngle) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 4.0f); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.breakout_angle", M_PI / 8.0f); + + SetupBasicTest(); + + apzc = ApzcOf(root); + + // Start a gesture to get us locked onto the Y axis. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Ensure that we have locked onto the Y axis. + apzc->AssertStateIsPanningLockedY(); + + // Stay within 45 degrees from the X axis, and more than 22.5 degrees from + // the Y axis. This should break the Y lock and lock us to the X axis. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(12, 10), mcc->Time()); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Ensure that we have locked onto the X axis. + apzc->AssertStateIsPanningLockedX(); + + // End the gesture. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + apzc->AdvanceAnimations(mcc->GetSampleTime()); +} + +TEST_F(APZCAxisLockTester, TestDominantAxisScrolling) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 3); + + int panY; + int panX; + + SetupBasicTest(); + + apzc = ApzcOf(root); + + ParentLayerPoint lastOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + + // In dominant axis mode, test pan gesture events with varying gesture + // angles and ensure that we only pan on one axis. + for (panX = 0, panY = 50; panY >= 0; panY -= 10, panX += 5) { + // Gesture that should be locked onto one axis + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, + ScreenIntPoint(50, 50), ScreenIntPoint(panX, panY), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(static_cast(panX), static_cast(panY)), + mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + apzc->AdvanceAnimationsUntilEnd(); + + ParentLayerPoint scrollOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForHitTesting); + + if (panX > panY) { + // If we're closer to the X axis ensure that we moved on the horizontal + // axis and there was no movement on the vertical axis. + EXPECT_GT(scrollOffset.x, lastOffset.x); + EXPECT_EQ(scrollOffset.y, lastOffset.y); + } else { + // If we're closer to the Y axis ensure that we moved on the vertical + // axis and there was no movement on the horizontal axis. + EXPECT_GT(scrollOffset.y, lastOffset.y); + EXPECT_EQ(scrollOffset.x, lastOffset.x); + } + + lastOffset = scrollOffset; + } +} + +TEST_F(APZCAxisLockTester, TestCanScrollWithAxisLock) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2); + + SetupBasicTest(); + + apzc = ApzcOf(root); + + // The axis locks do not impact CanScroll() + apzc->SetAxisLocked(ScrollDirection::eHorizontal, true); + EXPECT_EQ(apzc->CanScroll(ParentLayerPoint(10, 0)), true); + + apzc->SetAxisLocked(ScrollDirection::eHorizontal, false); + apzc->SetAxisLocked(ScrollDirection::eVertical, true); + EXPECT_EQ(apzc->CanScroll(ParentLayerPoint(0, 10)), true); +} + +TEST_F(APZCAxisLockTester, TestScrollHandoffAxisLockConflict) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2); + + // Create two scrollable frames. One parent frame with one child. + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 500, 500)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 500, 500)); + SetScrollHandoff(layers[1], root); + + registration = MakeUnique(LayersId{0}, mcc); + + UpdateHitTestingTree(); + + RefPtr rootApzc = ApzcOf(root); + apzc = ApzcOf(layers[1]); + + // Create a gesture on the y-axis that should lock the x axis. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(0, 15), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimationsUntilEnd(); + + // We are locked onto the y-axis. + apzc->AssertAxisLocked(ScrollDirection::eVertical); + + // There should be movement in the child. + ParentLayerPoint childCurrentOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting); + EXPECT_GT(childCurrentOffset.y, 0); + EXPECT_EQ(childCurrentOffset.x, 0); + + // There should be no movement in the parent. + ParentLayerPoint parentCurrentOffset = rootApzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting); + EXPECT_EQ(parentCurrentOffset.y, 0); + EXPECT_EQ(parentCurrentOffset.x, 0); + + // Create a gesture on the x-axis, that should be directed + // at the child, even if the x-axis is locked. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(2, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimationsUntilEnd(); + + // We broke the y-axis lock and are now locked onto the x-axis. + apzc->AssertAxisLocked(ScrollDirection::eHorizontal); + + // There should be some movement in the child on the x-axis. + ParentLayerPoint childFinalOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting); + EXPECT_GT(childFinalOffset.x, 0); + + // There should still be no movement in the parent. + ParentLayerPoint parentFinalOffset = rootApzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting); + EXPECT_EQ(parentFinalOffset.y, 0); + EXPECT_EQ(parentFinalOffset.x, 0); +} + +// The delta from the initial pan gesture should be reflected in the +// current offset for all axis locking modes. +TEST_P(APZCAxisLockCompatTester, TestPanGestureStart) { + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 500, 500)); + + registration = MakeUnique(LayersId{0}, mcc); + + UpdateHitTestingTree(); + + apzc = ApzcOf(root); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimationsUntilEnd(); + ParentLayerPoint currentOffset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting); + + EXPECT_EQ(currentOffset.x, 0); + EXPECT_EQ(currentOffset.y, 10); +} + +// All APZCAxisLockCompatTester tests should be run for each apz.axis_lock.mode. +// If another mode is added, the value should be added to this list. +INSTANTIATE_TEST_SUITE_P(APZCAxisLockCompat, APZCAxisLockCompatTester, + testing::Values(0, 1, 2, 3), + APZCAxisLockCompatTester::PrintFromParam); diff --git a/gfx/layers/apz/test/gtest/TestBasic.cpp b/gfx/layers/apz/test/gtest/TestBasic.cpp new file mode 100644 index 0000000000..52cebfccd4 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestBasic.cpp @@ -0,0 +1,639 @@ +/* -*- 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 "APZCBasicTester.h" +#include "APZTestCommon.h" + +#include "InputUtils.h" + +static ScrollGenerationCounter sGenerationCounter; + +TEST_F(APZCBasicTester, Overzoom) { + // the visible area of the document in CSS pixels is x=10 y=0 w=100 h=100 + FrameMetrics fm; + fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + fm.SetScrollableRect(CSSRect(0, 0, 125, 150)); + fm.SetVisualScrollOffset(CSSPoint(10, 0)); + fm.SetZoom(CSSToParentLayerScale(1.0)); + fm.SetIsRootContent(true); + apzc->SetFrameMetrics(fm); + + MakeApzcZoomable(); + + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(1); + + PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(50, 50), 0.5, true); + + fm = apzc->GetFrameMetrics(); + EXPECT_EQ(0.8f, fm.GetZoom().scale); + // bug 936721 - PGO builds introduce rounding error so + // use a fuzzy match instead + EXPECT_LT(std::abs(fm.GetVisualScrollOffset().x), 1e-5); + EXPECT_LT(std::abs(fm.GetVisualScrollOffset().y), 1e-5); +} + +TEST_F(APZCBasicTester, ZoomLimits) { + SCOPED_GFX_PREF_FLOAT("apz.min_zoom", 0.9f); + SCOPED_GFX_PREF_FLOAT("apz.max_zoom", 2.0f); + + // the visible area of the document in CSS pixels is x=10 y=0 w=100 h=100 + FrameMetrics fm; + fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + fm.SetScrollableRect(CSSRect(0, 0, 125, 150)); + fm.SetZoom(CSSToParentLayerScale(1.0)); + fm.SetIsRootContent(true); + apzc->SetFrameMetrics(fm); + + MakeApzcZoomable(); + + // This should take the zoom scale to 0.8, but we've capped it at 0.9. + PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(50, 50), 0.5, true); + + fm = apzc->GetFrameMetrics(); + EXPECT_EQ(0.9f, fm.GetZoom().scale); + + // This should take the zoom scale to 2.7, but we've capped it at 2. + PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(50, 50), 3, true); + + fm = apzc->GetFrameMetrics(); + EXPECT_EQ(2.0f, fm.GetZoom().scale); +} + +TEST_F(APZCBasicTester, SimpleTransform) { + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + + EXPECT_EQ(ParentLayerPoint(), pointOut); + EXPECT_EQ(AsyncTransform(), viewTransformOut); +} + +TEST_F(APZCBasicTester, ComplexTransform) { + // This test assumes there is a page that gets rendered to + // two layers. In CSS pixels, the first layer is 50x50 and + // the second layer is 25x50. The widget scale factor is 3.0 + // and the presShell resolution is 2.0. Therefore, these layers + // end up being 300x300 and 150x300 in layer pixels. + // + // The second (child) layer has an additional CSS transform that + // stretches it by 2.0 on the x-axis. Therefore, after applying + // CSS transforms, the two layers are the same size in screen + // pixels. + // + // The screen itself is 24x24 in screen pixels (therefore 4x4 in + // CSS pixels). The displayport is 1 extra CSS pixel on all + // sides. + + RefPtr childApzc = + new TestAsyncPanZoomController(LayersId{0}, mcc, tm); + + const char* treeShape = "x(x)"; + // LayerID 0 1 + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 300, 300), + LayerIntRect(0, 0, 150, 300), + }; + Matrix4x4 transforms[] = { + Matrix4x4(), + Matrix4x4(), + }; + transforms[0].PostScale( + 0.5f, 0.5f, + 1.0f); // this results from the 2.0 resolution on the root layer + transforms[1].PostScale( + 2.0f, 1.0f, + 1.0f); // this is the 2.0 x-axis CSS transform on the child layer + + auto layers = TestWRScrollData::Create(treeShape, *updater, + layerVisibleRegion, transforms); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 24, 24)); + metrics.SetDisplayPort(CSSRect(-1, -1, 6, 6)); + metrics.SetVisualScrollOffset(CSSPoint(10, 10)); + metrics.SetLayoutViewport(CSSRect(10, 10, 8, 8)); + metrics.SetScrollableRect(CSSRect(0, 0, 50, 50)); + metrics.SetCumulativeResolution(LayoutDeviceToLayerScale(2)); + metrics.SetPresShellResolution(2.0f); + metrics.SetZoom(CSSToParentLayerScale(6)); + metrics.SetDevPixelsPerCSSPixel(CSSToLayoutDeviceScale(3)); + metrics.SetScrollId(ScrollableLayerGuid::START_SCROLL_ID); + + ScrollMetadata childMetadata = metadata; + FrameMetrics& childMetrics = childMetadata.GetMetrics(); + childMetrics.SetScrollId(ScrollableLayerGuid::START_SCROLL_ID + 1); + + layers[0]->AppendScrollMetadata(layers, metadata); + layers[1]->AppendScrollMetadata(layers, childMetadata); + + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + + // Both the parent and child layer should behave exactly the same here, + // because the CSS transform on the child layer does not affect the + // SampleContentTransformForFrame code + + // initial transform + apzc->SetFrameMetrics(metrics); + apzc->NotifyLayersUpdated(metadata, true, true); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint()), + viewTransformOut); + EXPECT_EQ(ParentLayerPoint(60, 60), pointOut); + + childApzc->SetFrameMetrics(childMetrics); + childApzc->NotifyLayersUpdated(childMetadata, true, true); + childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ(AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint()), + viewTransformOut); + EXPECT_EQ(ParentLayerPoint(60, 60), pointOut); + + // do an async scroll by 5 pixels and check the transform + metrics.ScrollBy(CSSPoint(5, 0)); + apzc->SetFrameMetrics(metrics); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ( + AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint(-30, 0)), + viewTransformOut); + EXPECT_EQ(ParentLayerPoint(90, 60), pointOut); + + childMetrics.ScrollBy(CSSPoint(5, 0)); + childApzc->SetFrameMetrics(childMetrics); + childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ( + AsyncTransform(LayerToParentLayerScale(1), ParentLayerPoint(-30, 0)), + viewTransformOut); + EXPECT_EQ(ParentLayerPoint(90, 60), pointOut); + + // do an async zoom of 1.5x and check the transform + metrics.ZoomBy(1.5f); + apzc->SetFrameMetrics(metrics); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ( + AsyncTransform(LayerToParentLayerScale(1.5), ParentLayerPoint(-45, 0)), + viewTransformOut); + EXPECT_EQ(ParentLayerPoint(135, 90), pointOut); + + childMetrics.ZoomBy(1.5f); + childApzc->SetFrameMetrics(childMetrics); + childApzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ( + AsyncTransform(LayerToParentLayerScale(1.5), ParentLayerPoint(-45, 0)), + viewTransformOut); + EXPECT_EQ(ParentLayerPoint(135, 90), pointOut); + + childApzc->Destroy(); +} + +TEST_F(APZCBasicTester, Fling) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + int touchStart = 50; + int touchEnd = 10; + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + + // Fling down. Each step scroll further down + Pan(apzc, touchStart, touchEnd); + ParentLayerPoint lastPoint; + for (int i = 1; i < 50; i += 1) { + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut, + TimeDuration::FromMilliseconds(1)); + EXPECT_GT(pointOut.y, lastPoint.y); + lastPoint = pointOut; + } +} + +#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android +TEST_F(APZCBasicTester, ResumeInterruptedTouchDrag_Bug1592435) { + // Start a touch-drag and scroll some amount, not lifting the finger. + SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 1.0f / 1000.0f); + ScreenIntPoint touchPos(10, 50); + uint64_t touchBlock = TouchDown(apzc, touchPos, mcc->Time()).mInputBlockId; + SetDefaultAllowedTouchBehavior(apzc, touchBlock); + for (int i = 0; i < 20; ++i) { + touchPos.y -= 1; + mcc->AdvanceByMillis(1); + TouchMove(apzc, touchPos, mcc->Time()); + } + + // Take note of the scroll offset before the interruption. + CSSPoint scrollOffsetBeforeInterruption = + apzc->GetFrameMetrics().GetVisualScrollOffset(); + + // Have the main thread interrupt the touch-drag by sending + // a main thread scroll update to a nearby location. + CSSPoint mainThreadOffset = scrollOffsetBeforeInterruption; + mainThreadOffset.y -= 5; + ScrollMetadata metadata = apzc->GetScrollMetadata(); + metadata.GetMetrics().SetLayoutScrollOffset(mainThreadOffset); + nsTArray scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll( + ScrollOrigin::Other, CSSPoint::ToAppUnits(mainThreadOffset))); + metadata.SetScrollUpdates(scrollUpdates); + metadata.GetMetrics().SetScrollGeneration( + scrollUpdates.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata, false, true); + + // Continue and finish the touch-drag gesture. + for (int i = 0; i < 20; ++i) { + touchPos.y -= 1; + mcc->AdvanceByMillis(1); + TouchMove(apzc, touchPos, mcc->Time()); + } + + // Check that the portion of the touch-drag that occurred after + // the interruption caused additional scrolling. + CSSPoint finalScrollOffset = apzc->GetFrameMetrics().GetVisualScrollOffset(); + EXPECT_GT(finalScrollOffset.y, scrollOffsetBeforeInterruption.y); + + // Now do the same thing, but for a visual scroll update. + scrollOffsetBeforeInterruption = + apzc->GetFrameMetrics().GetVisualScrollOffset(); + mainThreadOffset = scrollOffsetBeforeInterruption; + mainThreadOffset.y -= 5; + metadata = apzc->GetScrollMetadata(); + metadata.GetMetrics().SetVisualDestination(mainThreadOffset); + metadata.GetMetrics().SetScrollGeneration( + sGenerationCounter.NewMainThreadGeneration()); + metadata.GetMetrics().SetVisualScrollUpdateType(FrameMetrics::eMainThread); + scrollUpdates.Clear(); + metadata.SetScrollUpdates(scrollUpdates); + apzc->NotifyLayersUpdated(metadata, false, true); + for (int i = 0; i < 20; ++i) { + touchPos.y -= 1; + mcc->AdvanceByMillis(1); + TouchMove(apzc, touchPos, mcc->Time()); + } + finalScrollOffset = apzc->GetFrameMetrics().GetVisualScrollOffset(); + EXPECT_GT(finalScrollOffset.y, scrollOffsetBeforeInterruption.y); + + // Clean up by ending the touch gesture. + mcc->AdvanceByMillis(1); + TouchUp(apzc, touchPos, mcc->Time()); +} +#endif + +TEST_F(APZCBasicTester, RelativeScrollOffset) { + // Set up initial conditions: zoomed in, layout offset at (100, 100), + // visual offset at (120, 120); the relative offset is therefore (20, 20). + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetScrollableRect(CSSRect(0, 0, 1000, 1000)); + metrics.SetLayoutViewport(CSSRect(100, 100, 100, 100)); + metrics.SetZoom(CSSToParentLayerScale(2.0)); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetVisualScrollOffset(CSSPoint(120, 120)); + metrics.SetIsRootContent(true); + apzc->SetFrameMetrics(metrics); + + // Scroll the layout viewport to (200, 200). + ScrollMetadata mainThreadMetadata = metadata; + FrameMetrics& mainThreadMetrics = mainThreadMetadata.GetMetrics(); + mainThreadMetrics.SetLayoutScrollOffset(CSSPoint(200, 200)); + nsTArray scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll( + ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(200, 200)))); + mainThreadMetadata.SetScrollUpdates(scrollUpdates); + mainThreadMetrics.SetScrollGeneration( + scrollUpdates.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(mainThreadMetadata, /*isFirstPaint=*/false, + /*thisLayerTreeUpdated=*/true); + + // Check that the relative offset has been preserved. + metrics = apzc->GetFrameMetrics(); + EXPECT_EQ(metrics.GetLayoutScrollOffset(), CSSPoint(200, 200)); + EXPECT_EQ(metrics.GetVisualScrollOffset(), CSSPoint(220, 220)); +} + +TEST_F(APZCBasicTester, MultipleSmoothScrollsSmooth) { + SCOPED_GFX_PREF_BOOL("general.smoothScroll", true); + // We want to test that if we send multiple smooth scroll requests that we + // still smoothly animate, ie that we get non-zero change every frame while + // the animation is running. + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 10000)); + metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100)); + metrics.SetZoom(CSSToParentLayerScale(1.0)); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + metrics.SetIsRootContent(true); + apzc->SetFrameMetrics(metrics); + + // Structure of this test. + // -send a pure relative smooth scroll request via NotifyLayersUpdated + // -advance animations a few times, check that scroll offset is increasing + // after the first few advances + // -send a pure relative smooth scroll request via NotifyLayersUpdated + // -advance animations a few times, check that scroll offset is increasing + // -send a pure relative smooth scroll request via NotifyLayersUpdated + // -advance animations a few times, check that scroll offset is increasing + + ScrollMetadata metadata2 = metadata; + nsTArray scrollUpdates2; + scrollUpdates2.AppendElement(ScrollPositionUpdate::NewPureRelativeScroll( + ScrollOrigin::Other, ScrollMode::Smooth, + CSSPoint::ToAppUnits(CSSPoint(0, 200)))); + metadata2.SetScrollUpdates(scrollUpdates2); + metadata2.GetMetrics().SetScrollGeneration( + scrollUpdates2.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata2, /*isFirstPaint=*/false, + /*thisLayerTreeUpdated=*/true); + + // Get the animation going + for (uint32_t i = 0; i < 3; i++) { + SampleAnimationOneFrame(); + } + + float offset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing) + .y; + ASSERT_GT(offset, 0); + float lastOffset = offset; + + for (uint32_t i = 0; i < 2; i++) { + for (uint32_t j = 0; j < 3; j++) { + SampleAnimationOneFrame(); + offset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForCompositing) + .y; + ASSERT_GT(offset, lastOffset); + lastOffset = offset; + } + + ScrollMetadata metadata3 = metadata; + nsTArray scrollUpdates3; + scrollUpdates3.AppendElement(ScrollPositionUpdate::NewPureRelativeScroll( + ScrollOrigin::Other, ScrollMode::Smooth, + CSSPoint::ToAppUnits(CSSPoint(0, 200)))); + metadata3.SetScrollUpdates(scrollUpdates3); + metadata3.GetMetrics().SetScrollGeneration( + scrollUpdates3.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata3, /*isFirstPaint=*/false, + /*thisLayerTreeUpdated=*/true); + } + + for (uint32_t j = 0; j < 7; j++) { + SampleAnimationOneFrame(); + offset = apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForCompositing) + .y; + ASSERT_GT(offset, lastOffset); + lastOffset = offset; + } +} + +class APZCSmoothScrollTester : public APZCBasicTester { + public: + // Test that a smooth scroll animation correctly handles its destination + // being updated by a relative scroll delta. + void TestSmoothScrollDestinationUpdate() { + // Set up scroll frame. Starting scroll position is (0, 0). + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 10000)); + metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100)); + metrics.SetZoom(CSSToParentLayerScale(1.0)); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + metrics.SetIsRootContent(true); + apzc->SetFrameMetrics(metrics); + + // Start smooth scroll via main-thread request. + nsTArray scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewPureRelativeScroll( + ScrollOrigin::Other, ScrollMode::Smooth, + CSSPoint::ToAppUnits(CSSPoint(0, 1000)))); + metadata.SetScrollUpdates(scrollUpdates); + metrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata, false, true); + + // Sample the smooth scroll animation until we get past y=500. + apzc->AssertStateIsSmoothScroll(); + float y = 0; + while (y < 500) { + SampleAnimationOneFrame(); + y = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + } + + // Send a relative scroll of y = -400. + scrollUpdates.Clear(); + scrollUpdates.AppendElement(ScrollPositionUpdate::NewRelativeScroll( + CSSPoint::ToAppUnits(CSSPoint(0, 500)), + CSSPoint::ToAppUnits(CSSPoint(0, 100)))); + metadata.SetScrollUpdates(scrollUpdates); + metrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata, false, false); + + // Verify the relative scroll was applied but didn't cancel the animation. + float y2 = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + ASSERT_EQ(y2, y - 400); + apzc->AssertStateIsSmoothScroll(); + + // Sample the animation again and check that it respected the relative + // scroll. + SampleAnimationOneFrame(); + float y3 = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + ASSERT_GT(y3, y2); + ASSERT_LT(y3, 500); + + // Continue animation until done and check that it ended up at a correctly + // adjusted destination. + apzc->AdvanceAnimationsUntilEnd(); + float y4 = apzc->GetFrameMetrics().GetVisualScrollOffset().y; + ASSERT_EQ(y4, 600); // 1000 (initial destination) - 400 (relative scroll) + } +}; + +TEST_F(APZCSmoothScrollTester, SmoothScrollDestinationUpdateBezier) { + SCOPED_GFX_PREF_BOOL("general.smoothScroll", true); + SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", false); + TestSmoothScrollDestinationUpdate(); +} + +TEST_F(APZCSmoothScrollTester, SmoothScrollDestinationUpdateMsd) { + SCOPED_GFX_PREF_BOOL("general.smoothScroll", true); + SCOPED_GFX_PREF_BOOL("general.smoothScroll.msdPhysics.enabled", true); + TestSmoothScrollDestinationUpdate(); +} + +TEST_F(APZCBasicTester, ZoomAndScrollableRectChangeAfterZoomChange) { + // We want to check that a small scrollable rect change (which causes us to + // reclamp our scroll position, including in the sampled state) does not move + // the scroll offset in the sample state based the zoom in the apzc, only + // based on the zoom in the sampled state. + + // First we zoom in to the right hand side. Then start zooming out, then send + // a scrollable rect change and check that it doesn't change the sampled state + // scroll offset. + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + metrics.SetZoom(CSSToParentLayerScale(1.0)); + metrics.SetIsRootContent(true); + apzc->SetFrameMetrics(metrics); + + MakeApzcZoomable(); + + // Zoom to right side. + ZoomTarget zoomTarget{CSSRect(75, 25, 25, 25)}; + apzc->ZoomToRect(zoomTarget, 0); + + // Run the animation to completion, should take 250ms/16.67ms = 15 frames, but + // do extra to make sure. + for (uint32_t i = 0; i < 30; i++) { + SampleAnimationOneFrame(); + } + + EXPECT_FALSE(apzc->IsAsyncZooming()); + + // Zoom out. + ZoomTarget zoomTarget2{CSSRect(0, 0, 100, 100)}; + apzc->ZoomToRect(zoomTarget2, 0); + + // Run the animation a few times to get it going. + for (uint32_t i = 0; i < 2; i++) { + SampleAnimationOneFrame(); + } + + // Check that it is decreasing in scale. + float prevScale = + apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing) + .scale; + for (uint32_t i = 0; i < 2; i++) { + SampleAnimationOneFrame(); + float scale = + apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing) + .scale; + ASSERT_GT(prevScale, scale); + prevScale = scale; + } + + float offset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing) + .x; + + // Change the scrollable rect slightly to trigger a reclamp. + ScrollMetadata metadata2 = metadata; + metadata2.GetMetrics().SetScrollableRect(CSSRect(0, 0, 100, 1000.2)); + apzc->NotifyLayersUpdated(metadata2, /*isFirstPaint=*/false, + /*thisLayerTreeUpdated=*/true); + + float newOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForCompositing) + .x; + + ASSERT_EQ(newOffset, offset); +} + +TEST_F(APZCBasicTester, ZoomToRectAndCompositionBoundsChange) { + // We want to check that content sending a composition bounds change (due to + // addition of scrollbars) during a zoom animation does not cause us to take + // the out of date content resolution. + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetCompositionBoundsWidthIgnoringScrollbars(ParentLayerCoord{100}); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + metrics.SetLayoutViewport(CSSRect(0, 0, 100, 100)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + metrics.SetZoom(CSSToParentLayerScale(1.0)); + metrics.SetIsRootContent(true); + apzc->SetFrameMetrics(metrics); + + MakeApzcZoomable(); + + // Start a zoom to a rect. + ZoomTarget zoomTarget{CSSRect(25, 25, 25, 25)}; + apzc->ZoomToRect(zoomTarget, 0); + + // Run the animation a few times to get it going. + // Check that it is increasing in scale. + float prevScale = + apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing) + .scale; + for (uint32_t i = 0; i < 3; i++) { + SampleAnimationOneFrame(); + float scale = + apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing) + .scale; + ASSERT_GE(scale, prevScale); + prevScale = scale; + } + + EXPECT_TRUE(apzc->IsAsyncZooming()); + + // Simulate the appearance of a scrollbar by reducing the width of + // the composition bounds, while keeping + // mCompositionBoundsWidthIgnoringScrollbars unchanged. + ScrollMetadata metadata2 = metadata; + metadata2.GetMetrics().SetCompositionBounds(ParentLayerRect(0, 0, 90, 100)); + apzc->NotifyLayersUpdated(metadata2, /*isFirstPaint=*/false, + /*thisLayerTreeUpdated=*/true); + + float scale = + apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing) + .scale; + + ASSERT_EQ(scale, prevScale); + + // Run the rest of the animation to completion, should take 250ms/16.67ms = 15 + // frames total, but do extra to make sure. + for (uint32_t i = 0; i < 30; i++) { + SampleAnimationOneFrame(); + scale = + apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing) + .scale; + ASSERT_GE(scale, prevScale); + prevScale = scale; + } + + EXPECT_FALSE(apzc->IsAsyncZooming()); +} + +TEST_F(APZCBasicTester, StartTolerance) { + SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 10 / tm->GetDPI()); + + FrameMetrics fm; + fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + fm.SetScrollableRect(CSSRect(0, 0, 100, 300)); + fm.SetVisualScrollOffset(CSSPoint(0, 50)); + fm.SetIsRootContent(true); + apzc->SetFrameMetrics(fm); + + uint64_t touchBlock = TouchDown(apzc, {50, 50}, mcc->Time()).mInputBlockId; + SetDefaultAllowedTouchBehavior(apzc, touchBlock); + + CSSPoint initialScrollOffset = + apzc->GetFrameMetrics().GetVisualScrollOffset(); + + mcc->AdvanceByMillis(1); + TouchMove(apzc, {50, 70}, mcc->Time()); + + // Expect 10 pixels of scrolling: the distance from (50,50) to (50,70) + // minus the 10-pixel touch start tolerance. + ASSERT_EQ(initialScrollOffset.y - 10, + apzc->GetFrameMetrics().GetVisualScrollOffset().y); + + mcc->AdvanceByMillis(1); + TouchMove(apzc, {50, 90}, mcc->Time()); + + // Expect 30 pixels of scrolling: the distance from (50,50) to (50,90) + // minus the 10-pixel touch start tolerance. + ASSERT_EQ(initialScrollOffset.y - 30, + apzc->GetFrameMetrics().GetVisualScrollOffset().y); + + // Clean up by ending the touch gesture. + mcc->AdvanceByMillis(1); + TouchUp(apzc, {50, 90}, mcc->Time()); +} diff --git a/gfx/layers/apz/test/gtest/TestEventRegions.cpp b/gfx/layers/apz/test/gtest/TestEventRegions.cpp new file mode 100644 index 0000000000..0b4564b49f --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestEventRegions.cpp @@ -0,0 +1,199 @@ +/* -*- 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 "APZCTreeManagerTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" +#include "mozilla/layers/LayersTypes.h" + +class APZEventRegionsTester : public APZCTreeManagerTester { + protected: + UniquePtr registration; + TestAsyncPanZoomController* rootApzc; + + void CreateEventRegionsLayerTree1() { + const char* treeShape = "x(xx)"; + LayerIntRegion layerVisibleRegions[] = { + LayerIntRect(0, 0, 200, 200), // root + LayerIntRect(0, 0, 100, 200), // left half + LayerIntRect(0, 100, 200, 100), // bottom half + }; + CreateScrollData(treeShape, layerVisibleRegions); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID); + SetScrollableFrameMetrics(layers[1], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollableFrameMetrics(layers[2], + ScrollableLayerGuid::START_SCROLL_ID + 2); + SetScrollHandoff(layers[1], root); + SetScrollHandoff(layers[2], root); + + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + } + + void CreateEventRegionsLayerTree2() { + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegions[] = { + LayerIntRect(0, 0, 100, 500), + LayerIntRect(0, 150, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegions); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID); + + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + } + + void CreateBug1117712LayerTree() { + const char* treeShape = "x(x(x)x)"; + // LayerID 0 1 2 3 + // 0 is the root + // 1 is a container layer whose sole purpose to make a non-empty ancestor + // transform for 2, so that 2's screen-to-apzc and apzc-to-gecko + // transforms are different from 3's. + // 2 is a small layer that is the actual target + // 3 is a big layer obscuring 2 with a dispatch-to-content region + LayerIntRegion layerVisibleRegions[] = { + LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 0, 0), + LayerIntRect(0, 0, 10, 10), + LayerIntRect(0, 0, 100, 100), + }; + Matrix4x4 layerTransforms[] = { + Matrix4x4(), + Matrix4x4::Translation(50, 0, 0), + Matrix4x4(), + Matrix4x4(), + }; + CreateScrollData(treeShape, layerVisibleRegions, layerTransforms); + + SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 10, 10)); + SetScrollableFrameMetrics(layers[3], + ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 100)); + SetScrollHandoff(layers[3], layers[2]); + + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + } +}; + +class APZEventRegionsTesterMock : public APZEventRegionsTester { + public: + APZEventRegionsTesterMock() { CreateMockHitTester(); } +}; + +TEST_F(APZEventRegionsTesterMock, HitRegionImmediateResponse) { + CreateEventRegionsLayerTree1(); + + TestAsyncPanZoomController* root = ApzcOf(layers[0]); + TestAsyncPanZoomController* left = ApzcOf(layers[1]); + TestAsyncPanZoomController* bottom = ApzcOf(layers[2]); + + MockFunction check; + { + InSequence s; + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, _, left->GetGuid(), _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped on left")); + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, _, _, bottom->GetGuid(), _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped on bottom")); + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, _, root->GetGuid(), _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped on root")); + EXPECT_CALL(check, Call("Tap pending on d-t-c region")); + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, _, _, bottom->GetGuid(), _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped on bottom again")); + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, _, left->GetGuid(), _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped on left this time")); + } + + TimeDuration tapDuration = TimeDuration::FromMilliseconds(100); + + // Tap in the exposed hit regions of each of the layers once and ensure + // the clicks are dispatched right away + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Tap(manager, ScreenIntPoint(10, 10), tapDuration); + mcc->RunThroughDelayedTasks(); // this runs the tap event + check.Call("Tapped on left"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 2); + Tap(manager, ScreenIntPoint(110, 110), tapDuration); + mcc->RunThroughDelayedTasks(); // this runs the tap event + check.Call("Tapped on bottom"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + Tap(manager, ScreenIntPoint(110, 10), tapDuration); + mcc->RunThroughDelayedTasks(); // this runs the tap event + check.Call("Tapped on root"); + + // Now tap on the dispatch-to-content region where the layers overlap + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 2, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + Tap(manager, ScreenIntPoint(10, 110), tapDuration); + mcc->RunThroughDelayedTasks(); // this runs the main-thread timeout + check.Call("Tap pending on d-t-c region"); + mcc->RunThroughDelayedTasks(); // this runs the tap event + check.Call("Tapped on bottom again"); + + // Now let's do that again, but simulate a main-thread response + uint64_t inputBlockId = 0; + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 2, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + Tap(manager, ScreenIntPoint(10, 110), tapDuration, nullptr, &inputBlockId); + nsTArray targets; + targets.AppendElement(left->GetGuid()); + manager->SetTargetAPZC(inputBlockId, targets); + while (mcc->RunThroughDelayedTasks()) + ; // this runs the tap event + check.Call("Tapped on left this time"); +} + +TEST_F(APZEventRegionsTesterMock, HitRegionAccumulatesChildren) { + CreateEventRegionsLayerTree2(); + + // Tap in the area of the child layer that's not directly included in the + // parent layer's hit region. Verify that it comes out of the APZC's + // content controller, which indicates the input events got routed correctly + // to the APZC. + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, _, _, rootApzc->GetGuid(), _)) + .Times(1); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + Tap(manager, ScreenIntPoint(10, 160), TimeDuration::FromMilliseconds(100)); +} + +TEST_F(APZEventRegionsTesterMock, Bug1117712) { + CreateBug1117712LayerTree(); + + TestAsyncPanZoomController* apzc2 = ApzcOf(layers[2]); + + // These touch events should hit the dispatch-to-content region of layers[3] + // and so get queued with that APZC as the tentative target. + uint64_t inputBlockId = 0; + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + Tap(manager, ScreenIntPoint(55, 5), TimeDuration::FromMilliseconds(100), + nullptr, &inputBlockId); + // But now we tell the APZ that really it hit layers[2], and expect the tap + // to be delivered at the correct coordinates. + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(55, 5), 0, + apzc2->GetGuid(), _)) + .Times(1); + + nsTArray targets; + targets.AppendElement(apzc2->GetGuid()); + manager->SetTargetAPZC(inputBlockId, targets); +} diff --git a/gfx/layers/apz/test/gtest/TestEventResult.cpp b/gfx/layers/apz/test/gtest/TestEventResult.cpp new file mode 100644 index 0000000000..90d17ee511 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestEventResult.cpp @@ -0,0 +1,476 @@ +/* -*- 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 "APZCTreeManagerTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" +#include "mozilla/EventForwards.h" +#include "mozilla/layers/LayersTypes.h" +#include + +class APZEventResultTester : public APZCTreeManagerTester { + protected: + UniquePtr registration; + + void UpdateOverscrollBehavior(OverscrollBehavior aX, OverscrollBehavior aY) { + ModifyFrameMetrics(root, [aX, aY](ScrollMetadata& sm, FrameMetrics& _) { + OverscrollBehaviorInfo overscroll; + overscroll.mBehaviorX = aX; + overscroll.mBehaviorY = aY; + sm.SetOverscrollBehavior(overscroll); + }); + UpdateHitTestingTree(); + } + + void SetScrollOffsetOnMainThread(const CSSPoint& aPoint) { + RefPtr apzc = ApzcOf(root); + + ScrollMetadata metadata = apzc->GetScrollMetadata(); + metadata.GetMetrics().SetLayoutScrollOffset(aPoint); + nsTArray scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll( + ScrollOrigin::Other, CSSPoint::ToAppUnits(aPoint))); + metadata.SetScrollUpdates(scrollUpdates); + metadata.GetMetrics().SetScrollGeneration( + scrollUpdates.LastElement().GetGeneration()); + apzc->NotifyLayersUpdated(metadata, /*aIsFirstPaint=*/false, + /*aThisLayerTreeUpdated=*/true); + } + + void CreateScrollableRootLayer() { + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegions[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegions); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 200, 200)); + ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) { + metrics.SetIsRootContent(true); + }); + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + } + + enum class PreventDefaultFlag { No, Yes }; + std::tuple TapDispatchToContent( + const ScreenIntPoint& aPoint, PreventDefaultFlag aPreventDefaultFlag) { + APZEventResult result = + Tap(manager, aPoint, TimeDuration::FromMilliseconds(100)); + + APZHandledResult delayedAnswer{APZHandledPlace::Invalid, SideBits::eNone, + ScrollDirections()}; + manager->AddInputBlockCallback( + result.mInputBlockId, + {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) { + EXPECT_EQ(id, result.mInputBlockId); + delayedAnswer = answer; + }}); + manager->SetAllowedTouchBehavior(result.mInputBlockId, + {AllowedTouchBehavior::VERTICAL_PAN}); + manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid}); + manager->ContentReceivedInputBlock( + result.mInputBlockId, aPreventDefaultFlag == PreventDefaultFlag::Yes); + return {result, delayedAnswer}; + } + + void OverscrollDirectionsWithEventHandlerTest( + PreventDefaultFlag aPreventDefaultFlag) { + UpdateHitTestingTree(); + + APZHandledPlace expectedPlace = + aPreventDefaultFlag == PreventDefaultFlag::No + ? APZHandledPlace::HandledByRoot + : APZHandledPlace::HandledByContent; + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight, + EitherScrollDirection})); + } + + // overscroll-behavior: contain, contain. + UpdateOverscrollBehavior(OverscrollBehavior::Contain, + OverscrollBehavior::Contain); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight, + ScrollDirections()})); + } + + // overscroll-behavior: none, none. + UpdateOverscrollBehavior(OverscrollBehavior::None, + OverscrollBehavior::None); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight, + ScrollDirections()})); + } + + // overscroll-behavior: auto, none. + UpdateOverscrollBehavior(OverscrollBehavior::Auto, + OverscrollBehavior::None); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight, + HorizontalScrollDirection})); + } + + // overscroll-behavior: none, auto. + UpdateOverscrollBehavior(OverscrollBehavior::None, + OverscrollBehavior::Auto); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight, + VerticalScrollDirection})); + } + } + + void ScrollableDirectionsWithEventHandlerTest( + PreventDefaultFlag aPreventDefaultFlag) { + UpdateHitTestingTree(); + + APZHandledPlace expectedPlace = + aPreventDefaultFlag == PreventDefaultFlag::No + ? APZHandledPlace::HandledByRoot + : APZHandledPlace::HandledByContent; + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight, + EitherScrollDirection})); + } + + // scroll down a bit. + SetScrollOffsetOnMainThread(CSSPoint(0, 10)); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ(delayedHandledResult, + (APZHandledResult{ + expectedPlace, + SideBits::eTop | SideBits::eBottom | SideBits::eRight, + EitherScrollDirection})); + } + + // scroll to the bottom edge + SetScrollOffsetOnMainThread(CSSPoint(0, 100)); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eRight | SideBits::eTop, + EitherScrollDirection})); + } + + // scroll to right a bit. + SetScrollOffsetOnMainThread(CSSPoint(10, 100)); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, + SideBits::eLeft | SideBits::eRight | SideBits::eTop, + EitherScrollDirection})); + } + + // scroll to the right edge. + SetScrollOffsetOnMainThread(CSSPoint(100, 100)); + { + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + auto [result, delayedHandledResult] = + TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + EXPECT_EQ( + delayedHandledResult, + (APZHandledResult{expectedPlace, SideBits::eTop | SideBits::eLeft, + EitherScrollDirection})); + } + } +}; + +TEST_F(APZEventResultTester, OverscrollDirections) { + CreateScrollableRootLayer(); + + TimeDuration tapDuration = TimeDuration::FromMilliseconds(100); + + // The default value of overscroll-behavior is auto. + APZEventResult result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections, + EitherScrollDirection); + + // overscroll-behavior: contain, contain. + UpdateOverscrollBehavior(OverscrollBehavior::Contain, + OverscrollBehavior::Contain); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections, + ScrollDirections()); + + // overscroll-behavior: none, none. + UpdateOverscrollBehavior(OverscrollBehavior::None, OverscrollBehavior::None); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections, + ScrollDirections()); + + // overscroll-behavior: auto, none. + UpdateOverscrollBehavior(OverscrollBehavior::Auto, OverscrollBehavior::None); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections, + HorizontalScrollDirection); + + // overscroll-behavior: none, auto. + UpdateOverscrollBehavior(OverscrollBehavior::None, OverscrollBehavior::Auto); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections, + VerticalScrollDirection); +} + +TEST_F(APZEventResultTester, ScrollableDirections) { + CreateScrollableRootLayer(); + + TimeDuration tapDuration = TimeDuration::FromMilliseconds(100); + + APZEventResult result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + // scrollable to down/right. + EXPECT_EQ(result.GetHandledResult()->mScrollableDirections, + SideBits::eBottom | SideBits::eRight); + + // scroll down a bit. + SetScrollOffsetOnMainThread(CSSPoint(0, 10)); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + // also scrollable toward top. + EXPECT_EQ(result.GetHandledResult()->mScrollableDirections, + SideBits::eTop | SideBits::eBottom | SideBits::eRight); + + // scroll to the bottom edge + SetScrollOffsetOnMainThread(CSSPoint(0, 100)); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mScrollableDirections, + SideBits::eRight | SideBits::eTop); + + // scroll to right a bit. + SetScrollOffsetOnMainThread(CSSPoint(10, 100)); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mScrollableDirections, + SideBits::eLeft | SideBits::eRight | SideBits::eTop); + + // scroll to the right edge. + SetScrollOffsetOnMainThread(CSSPoint(100, 100)); + result = Tap(manager, ScreenIntPoint(50, 50), tapDuration); + EXPECT_EQ(result.GetHandledResult()->mScrollableDirections, + SideBits::eLeft | SideBits::eTop); +} + +class APZEventResultTesterMock : public APZEventResultTester { + public: + APZEventResultTesterMock() { CreateMockHitTester(); } +}; + +TEST_F(APZEventResultTesterMock, OverscrollDirectionsWithEventHandler) { + CreateScrollableRootLayer(); + + OverscrollDirectionsWithEventHandlerTest(PreventDefaultFlag::No); +} + +TEST_F(APZEventResultTesterMock, + OverscrollDirectionsWithPreventDefaultEventHandler) { + CreateScrollableRootLayer(); + + OverscrollDirectionsWithEventHandlerTest(PreventDefaultFlag::Yes); +} + +TEST_F(APZEventResultTesterMock, ScrollableDirectionsWithEventHandler) { + CreateScrollableRootLayer(); + + ScrollableDirectionsWithEventHandlerTest(PreventDefaultFlag::No); +} + +TEST_F(APZEventResultTesterMock, + ScrollableDirectionsWithPreventDefaultEventHandler) { + CreateScrollableRootLayer(); + + ScrollableDirectionsWithEventHandlerTest(PreventDefaultFlag::Yes); +} + +// Test that APZEventResult::GetHandledResult() is correctly +// populated. +TEST_F(APZEventResultTesterMock, HandledByRootApzcFlag) { + // Create simple layer tree containing a dispatch-to-content region + // that covers part but not all of its area. + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegions[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegions); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) { + metrics.SetIsRootContent(true); + }); + // away from the scrolling container layer. + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + + // Tap the top half and check that we report that the event was + // handled by the root APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + APZEventResult result = + TouchDown(manager, ScreenIntPoint(50, 25), mcc->Time()); + TouchUp(manager, ScreenIntPoint(50, 25), mcc->Time()); + EXPECT_EQ(result.GetHandledResult(), + Some(APZHandledResult{APZHandledPlace::HandledByRoot, + SideBits::eBottom, EitherScrollDirection})); + + // Tap the bottom half and check that we report that we're not + // sure whether the event was handled by the root APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time()); + TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time()); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + + // Register an input block callback that will tell us the + // delayed answer. + APZHandledResult delayedAnswer{APZHandledPlace::Invalid, SideBits::eNone, + ScrollDirections()}; + manager->AddInputBlockCallback( + result.mInputBlockId, + {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) { + EXPECT_EQ(id, result.mInputBlockId); + delayedAnswer = answer; + }}); + + // Send APZ the relevant notifications to allow it to process the + // input block. + manager->SetAllowedTouchBehavior(result.mInputBlockId, + {AllowedTouchBehavior::VERTICAL_PAN}); + manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid}); + manager->ContentReceivedInputBlock(result.mInputBlockId, + /*aPreventDefault=*/false); + + // Check that we received the delayed answer and it is what we expect. + EXPECT_EQ(delayedAnswer, + (APZHandledResult{APZHandledPlace::HandledByRoot, SideBits::eBottom, + EitherScrollDirection})); + + // Now repeat the tap on the bottom half, but simulate a prevent-default. + // This time, we expect a delayed answer of `HandledByContent`. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time()); + TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time()); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + manager->AddInputBlockCallback( + result.mInputBlockId, + {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) { + EXPECT_EQ(id, result.mInputBlockId); + delayedAnswer = answer; + }}); + manager->SetAllowedTouchBehavior(result.mInputBlockId, + {AllowedTouchBehavior::VERTICAL_PAN}); + manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid}); + manager->ContentReceivedInputBlock(result.mInputBlockId, + /*aPreventDefault=*/true); + EXPECT_EQ(delayedAnswer, + (APZHandledResult{APZHandledPlace::HandledByContent, + SideBits::eBottom, EitherScrollDirection})); + + // Shrink the scrollable area, now it's no longer scrollable. + ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) { + metrics.SetScrollableRect(CSSRect(0, 0, 100, 100)); + }); + UpdateHitTestingTree(); + // Now repeat the tap on the bottom half with an event handler. + // This time, we expect a delayed answer of `Unhandled`. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time()); + TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time()); + EXPECT_EQ(result.GetHandledResult(), Nothing()); + manager->AddInputBlockCallback( + result.mInputBlockId, + {result.GetStatus(), [&](uint64_t id, const APZHandledResult& answer) { + EXPECT_EQ(id, result.mInputBlockId); + delayedAnswer = answer; + }}); + manager->SetAllowedTouchBehavior(result.mInputBlockId, + {AllowedTouchBehavior::VERTICAL_PAN}); + manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid}); + manager->ContentReceivedInputBlock(result.mInputBlockId, + /*aPreventDefault=*/false); + EXPECT_EQ(delayedAnswer, + (APZHandledResult{APZHandledPlace::Unhandled, SideBits::eNone, + ScrollDirections()})); + + // Repeat the tap on the bottom half, with no event handler. + // Make sure we get an eager answer of `Unhandled`. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time()); + TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time()); + EXPECT_EQ(result.GetStatus(), nsEventStatus_eIgnore); + EXPECT_EQ(result.GetHandledResult(), + Some(APZHandledResult{APZHandledPlace::Unhandled, SideBits::eNone, + EitherScrollDirection})); +} diff --git a/gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp b/gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp new file mode 100644 index 0000000000..986025bddc --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestFlingAcceleration.cpp @@ -0,0 +1,252 @@ +/* -*- 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 +#include "APZCTreeManagerTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" + +class APZCFlingAccelerationTester : public APZCTreeManagerTester { + protected: + void SetUp() { + APZCTreeManagerTester::SetUp(); + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 800, 1000), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 800, 50000)); + // Scroll somewhere into the middle of the scroll range, so that we have + // lots of space to scroll in both directions. + ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aMetrics.SetVisualScrollUpdateType( + FrameMetrics::ScrollOffsetUpdateType::eMainThread); + aMetrics.SetVisualDestination(CSSPoint(0, 25000)); + }); + + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + + apzc = ApzcOf(root); + } + + void ExecutePanGesture100Hz(const ScreenIntPoint& aStartPoint, + std::initializer_list aYDeltas) { + APZEventResult result = TouchDown(apzc, aStartPoint, mcc->Time()); + + // Allowed touch behaviours must be set after sending touch-start. + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId); + } + + const TimeDuration kTouchTimeDelta100Hz = + TimeDuration::FromMilliseconds(10); + + ScreenIntPoint currentLocation = aStartPoint; + for (int32_t delta : aYDeltas) { + mcc->AdvanceBy(kTouchTimeDelta100Hz); + if (delta != 0) { + currentLocation.y += delta; + Unused << TouchMove(apzc, currentLocation, mcc->Time()); + } + } + + Unused << TouchUp(apzc, currentLocation, mcc->Time()); + } + + void ExecuteWait(const TimeDuration& aDuration) { + TimeDuration remaining = aDuration; + const TimeDuration TIME_BETWEEN_FRAMES = + TimeDuration::FromSeconds(1) / int64_t(60); + while (remaining.ToMilliseconds() > 0) { + mcc->AdvanceBy(TIME_BETWEEN_FRAMES); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + remaining -= TIME_BETWEEN_FRAMES; + } + } + + RefPtr apzc; + UniquePtr registration; +}; + +enum class UpOrDown : uint8_t { Up, Down }; + +// This is a macro so that the assertions print useful line numbers. +#define CHECK_VELOCITY(aUpOrDown, aLowerBound, aUpperBound) \ + do { \ + auto vel = apzc->GetVelocityVector(); \ + if (UpOrDown::aUpOrDown == UpOrDown::Up) { \ + EXPECT_LT(vel.y, 0.0); \ + } else { \ + EXPECT_GT(vel.y, 0.0); \ + } \ + EXPECT_GE(vel.Length(), aLowerBound); \ + EXPECT_LE(vel.Length(), aUpperBound); \ + } while (0) + +// These tests have the following pattern: Two flings are executed, with a bit +// of wait time in between. The deltas in each pan gesture have been captured +// from a real phone, from touch events triggered by real fingers. +// We check the velocity at the end to detect whether the fling was accelerated +// or not. As an additional safety precaution, we also check the velocities for +// the first fling, so that changes in behavior are easier to analyze. +// One added challenge of this test is the fact that it has to work with on +// multiple platforms, and we use different velocity estimation strategies and +// different fling physics depending on the platform. +// The upper and lower bounds for the velocities were chosen in such a way that +// the test passes on all platforms. At the time of writing, we usually end up +// with higher velocities on Android than on Desktop, so the observed velocities +// on Android became the upper bounds and the observed velocities on Desktop +// becaume the lower bounds, each rounded out to a multiple of 0.1. + +TEST_F(APZCFlingAccelerationTester, TwoNormalFlingsShouldAccelerate) { + ExecutePanGesture100Hz(ScreenIntPoint{665, 1244}, + {0, 0, -21, -44, -52, -55, -53, -49, -46, -47}); + CHECK_VELOCITY(Down, 4.5, 6.8); + + ExecuteWait(TimeDuration::FromMilliseconds(375)); + CHECK_VELOCITY(Down, 2.2, 5.1); + + ExecutePanGesture100Hz(ScreenIntPoint{623, 1211}, + {-6, -51, -55, 0, -53, -57, -60, -60, -56}); + CHECK_VELOCITY(Down, 9.0, 14.0); +} + +TEST_F(APZCFlingAccelerationTester, TwoFastFlingsShouldAccelerate) { + ExecutePanGesture100Hz(ScreenIntPoint{764, 714}, + {9, 30, 49, 60, 64, 64, 62, 59, 51}); + CHECK_VELOCITY(Up, 5.0, 7.5); + + ExecuteWait(TimeDuration::FromMilliseconds(447)); + CHECK_VELOCITY(Up, 2.3, 5.2); + + ExecutePanGesture100Hz(ScreenIntPoint{743, 739}, + {7, 0, 38, 66, 75, 146, 0, 119}); + CHECK_VELOCITY(Up, 13.0, 20.0); +} + +TEST_F(APZCFlingAccelerationTester, + FlingsInOppositeDirectionShouldNotAccelerate) { + ExecutePanGesture100Hz(ScreenIntPoint{728, 1381}, + {0, 0, 0, -12, -24, -32, -43, -46, 0}); + CHECK_VELOCITY(Down, 2.9, 5.3); + + ExecuteWait(TimeDuration::FromMilliseconds(153)); + CHECK_VELOCITY(Down, 2.1, 4.8); + + ExecutePanGesture100Hz(ScreenIntPoint{698, 1059}, + {0, 0, 14, 61, 41, 0, 45, 35}); + CHECK_VELOCITY(Up, 3.2, 4.3); +} + +TEST_F(APZCFlingAccelerationTester, + ShouldNotAccelerateWhenPreviousFlingHasSlowedDown) { + ExecutePanGesture100Hz(ScreenIntPoint{748, 1046}, + {0, 9, 15, 23, 31, 30, 0, 34, 31, 29, 28, 24, 24, 11}); + CHECK_VELOCITY(Up, 2.2, 3.0); + ExecuteWait(TimeDuration::FromMilliseconds(498)); + CHECK_VELOCITY(Up, 0.5, 1.0); + ExecutePanGesture100Hz(ScreenIntPoint{745, 1056}, + {0, 10, 17, 29, 29, 33, 33, 0, 31, 27, 13}); + CHECK_VELOCITY(Up, 1.8, 2.7); +} + +TEST_F(APZCFlingAccelerationTester, ShouldNotAccelerateWhenPausedAtStartOfPan) { + ExecutePanGesture100Hz( + ScreenIntPoint{711, 1468}, + {0, 0, 0, 0, -8, 0, -18, -32, -50, -57, -66, -68, -63, -60}); + CHECK_VELOCITY(Down, 6.2, 8.6); + + ExecuteWait(TimeDuration::FromMilliseconds(285)); + CHECK_VELOCITY(Down, 3.4, 7.4); + + ExecutePanGesture100Hz( + ScreenIntPoint{658, 1352}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -8, -18, -34, -53, -70, -75, -75, -64}); + CHECK_VELOCITY(Down, 6.7, 9.1); +} + +TEST_F(APZCFlingAccelerationTester, ShouldNotAccelerateWhenPausedDuringPan) { + ExecutePanGesture100Hz( + ScreenIntPoint{732, 1423}, + {0, 0, 0, -5, 0, -15, -41, -71, -90, -93, -85, -64, -44}); + CHECK_VELOCITY(Down, 7.5, 10.1); + + ExecuteWait(TimeDuration::FromMilliseconds(204)); + CHECK_VELOCITY(Down, 4.8, 9.4); + + ExecutePanGesture100Hz( + ScreenIntPoint{651, 1372}, + {0, 0, 0, -6, 0, -16, -26, -41, -49, -65, -66, -61, -50, -35, -24, + -17, -11, -8, -6, -5, -4, -3, -2, -2, -2, -2, -2, -2, -2, -2, + -3, -4, -5, -7, -9, -10, -10, -12, -18, -25, -23, -28, -30, -24}); + CHECK_VELOCITY(Down, 2.5, 3.4); +} + +TEST_F(APZCFlingAccelerationTester, + ShouldNotAccelerateWhenOppositeDirectionDuringPan) { + ExecutePanGesture100Hz(ScreenIntPoint{663, 1371}, + {0, 0, 0, -5, -18, -31, -49, -56, -61, -54, -55}); + CHECK_VELOCITY(Down, 5.4, 7.1); + + ExecuteWait(TimeDuration::FromMilliseconds(255)); + CHECK_VELOCITY(Down, 3.1, 6.0); + + ExecutePanGesture100Hz( + ScreenIntPoint{726, 930}, + {0, 0, 0, 0, 30, 0, 19, 24, 32, 30, 37, 33, + 33, 32, 25, 23, 23, 18, 13, 9, 5, 3, 1, 0, + -7, -19, -38, -53, -68, -79, -85, -73, -64, -54}); + CHECK_VELOCITY(Down, 7.0, 10.0); +} + +TEST_F(APZCFlingAccelerationTester, + ShouldAccelerateAfterLongWaitIfVelocityStillHigh) { + // Reduce friction with the "Desktop" fling physics a little, so that it + // behaves more similarly to the Android fling physics, and has enough + // velocity after the wait time to allow for acceleration. + SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0.0012); + + ExecutePanGesture100Hz(ScreenIntPoint{739, 1424}, + {0, 0, -5, -10, -20, 0, -110, -86, 0, -102, -105}); + CHECK_VELOCITY(Down, 6.3, 9.4); + + ExecuteWait(TimeDuration::FromMilliseconds(1117)); + CHECK_VELOCITY(Down, 1.6, 3.3); + + ExecutePanGesture100Hz(ScreenIntPoint{726, 1380}, + {0, -8, 0, -30, -60, -87, -104, -111}); + CHECK_VELOCITY(Down, 13.0, 23.0); +} + +TEST_F(APZCFlingAccelerationTester, ShouldNotAccelerateAfterCanceledWithTap) { + // First, build up a lot of speed. + ExecutePanGesture100Hz(ScreenIntPoint{569, 710}, + {11, 2, 107, 18, 148, 57, 133, 159, 21}); + ExecuteWait(TimeDuration::FromMilliseconds(154)); + ExecutePanGesture100Hz(ScreenIntPoint{581, 650}, + {12, 68, 0, 162, 78, 140, 167}); + ExecuteWait(TimeDuration::FromMilliseconds(123)); + ExecutePanGesture100Hz(ScreenIntPoint{568, 723}, {11, 0, 79, 91, 131, 171}); + ExecuteWait(TimeDuration::FromMilliseconds(123)); + ExecutePanGesture100Hz(ScreenIntPoint{598, 678}, + {8, 55, 22, 87, 117, 220, 54}); + ExecuteWait(TimeDuration::FromMilliseconds(134)); + ExecutePanGesture100Hz(ScreenIntPoint{585, 854}, {45, 137, 107, 102, 79}); + ExecuteWait(TimeDuration::FromMilliseconds(246)); + + // Then, interrupt with a tap. + ExecutePanGesture100Hz(ScreenIntPoint{566, 812}, {0, 0, 0, 0}); + ExecuteWait(TimeDuration::FromMilliseconds(869)); + + // Then do a regular fling. + ExecutePanGesture100Hz(ScreenIntPoint{599, 819}, + {0, 0, 8, 35, 8, 38, 29, 37}); + + CHECK_VELOCITY(Up, 2.8, 4.2); +} diff --git a/gfx/layers/apz/test/gtest/TestGestureDetector.cpp b/gfx/layers/apz/test/gtest/TestGestureDetector.cpp new file mode 100644 index 0000000000..ad5f379ba8 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestGestureDetector.cpp @@ -0,0 +1,849 @@ +/* -*- 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 "APZCBasicTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" +#include "mozilla/StaticPrefs_apz.h" + +// Note: There are additional tests that test gesture detection behaviour +// with multiple APZCs in TestTreeManager.cpp. + +class APZCGestureDetectorTester : public APZCBasicTester { + public: + APZCGestureDetectorTester() + : APZCBasicTester(AsyncPanZoomController::USE_GESTURE_DETECTOR) {} + + protected: + FrameMetrics GetPinchableFrameMetrics() { + FrameMetrics fm; + fm.SetCompositionBounds(ParentLayerRect(200, 200, 100, 200)); + fm.SetScrollableRect(CSSRect(0, 0, 980, 1000)); + fm.SetVisualScrollOffset(CSSPoint(300, 300)); + fm.SetZoom(CSSToParentLayerScale(2.0)); + // APZC only allows zooming on the root scrollable frame. + fm.SetIsRootContent(true); + // the visible area of the document in CSS pixels is x=300 y=300 w=50 h=100 + return fm; + } +}; + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCGestureDetectorTester, Pan_After_Pinch) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.lock_angle", M_PI / 6.0f); + SCOPED_GFX_PREF_FLOAT("apz.axis_lock.breakout_angle", M_PI / 8.0f); + + FrameMetrics originalMetrics = GetPinchableFrameMetrics(); + apzc->SetFrameMetrics(originalMetrics); + + MakeApzcZoomable(); + + // Test parameters + float zoomAmount = 1.25; + float pinchLength = 100.0; + float pinchLengthScaled = pinchLength * zoomAmount; + int focusX = 250; + int focusY = 300; + int panDistance = 20; + const TimeDuration TIME_BETWEEN_TOUCH_EVENT = + TimeDuration::FromMilliseconds(50); + + int firstFingerId = 0; + int secondFingerId = firstFingerId + 1; + + // Put fingers down + MultiTouchInput mti = + MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, focusX, focusY)); + mti.mTouches.AppendElement( + CreateSingleTouchData(secondFingerId, focusX, focusY)); + apzc->ReceiveInputEvent(mti, Some(nsTArray{kDefaultTouchBehavior})); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Spread fingers out to enter the pinch state + mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, focusX - pinchLength, focusY)); + mti.mTouches.AppendElement( + CreateSingleTouchData(secondFingerId, focusX + pinchLength, focusY)); + apzc->ReceiveInputEvent(mti); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Do the actual pinch of 1.25x + mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY)); + mti.mTouches.AppendElement(CreateSingleTouchData( + secondFingerId, focusX + pinchLengthScaled, focusY)); + apzc->ReceiveInputEvent(mti); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Verify that the zoom changed, just to make sure our code above did what it + // was supposed to. + FrameMetrics zoomedMetrics = apzc->GetFrameMetrics(); + float newZoom = zoomedMetrics.GetZoom().scale; + EXPECT_EQ(originalMetrics.GetZoom().scale * zoomAmount, newZoom); + + // Now we lift one finger... + mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0); + mti.mTouches.AppendElement(CreateSingleTouchData( + secondFingerId, focusX + pinchLengthScaled, focusY)); + apzc->ReceiveInputEvent(mti); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // ... and pan with the remaining finger. This pan just breaks through the + // distance threshold. + focusY += StaticPrefs::apz_touch_start_tolerance() * tm->GetDPI(); + mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY)); + apzc->ReceiveInputEvent(mti); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // This one does an actual pan of 20 pixels + focusY += panDistance; + mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY)); + apzc->ReceiveInputEvent(mti); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Lift the remaining finger + mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY)); + apzc->ReceiveInputEvent(mti); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + // Verify that we scrolled + FrameMetrics finalMetrics = apzc->GetFrameMetrics(); + EXPECT_EQ(zoomedMetrics.GetVisualScrollOffset().y - (panDistance / newZoom), + finalMetrics.GetVisualScrollOffset().y); + + // Clear out any remaining fling animation and pending tasks + apzc->AdvanceAnimationsUntilEnd(); + while (mcc->RunThroughDelayedTasks()) + ; + apzc->AssertStateIsReset(); +} +#endif + +TEST_F(APZCGestureDetectorTester, Pan_With_Tap) { + SCOPED_GFX_PREF_FLOAT("apz.touch_start_tolerance", 0.1); + + FrameMetrics originalMetrics = GetPinchableFrameMetrics(); + apzc->SetFrameMetrics(originalMetrics); + + // Making the APZC zoomable isn't really needed for the correct operation of + // this test, but it could help catch regressions where we accidentally enter + // a pinch state. + MakeApzcZoomable(); + + // Test parameters + int touchX = 250; + int touchY = 300; + int panDistance = 20; + + int firstFingerId = 0; + int secondFingerId = firstFingerId + 1; + + const float panThreshold = + StaticPrefs::apz_touch_start_tolerance() * tm->GetDPI(); + + // Put finger down + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + apzc->ReceiveInputEvent(mti, Some(nsTArray{kDefaultTouchBehavior})); + + // Start a pan, break through the threshold + touchY += panThreshold; + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + apzc->ReceiveInputEvent(mti); + + // Do an actual pan for a bit + touchY += panDistance; + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + apzc->ReceiveInputEvent(mti); + + // Put a second finger down + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + mti.mTouches.AppendElement( + CreateSingleTouchData(secondFingerId, touchX + 10, touchY)); + apzc->ReceiveInputEvent(mti, Some(nsTArray{kDefaultTouchBehavior})); + + // Lift the second finger + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(secondFingerId, touchX + 10, touchY)); + apzc->ReceiveInputEvent(mti); + + // Bust through the threshold again + touchY += panThreshold; + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + apzc->ReceiveInputEvent(mti); + + // Do some more actual panning + touchY += panDistance; + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + apzc->ReceiveInputEvent(mti); + + // Lift the first finger + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time()); + mti.mTouches.AppendElement( + CreateSingleTouchData(firstFingerId, touchX, touchY)); + apzc->ReceiveInputEvent(mti); + + // Verify that we scrolled + FrameMetrics finalMetrics = apzc->GetFrameMetrics(); + float zoom = finalMetrics.GetZoom().scale; + EXPECT_EQ( + originalMetrics.GetVisualScrollOffset().y - (panDistance * 2 / zoom), + finalMetrics.GetVisualScrollOffset().y); + + // Clear out any remaining fling animation and pending tasks + apzc->AdvanceAnimationsUntilEnd(); + while (mcc->RunThroughDelayedTasks()) + ; + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, SecondTapIsFar_Bug1586496) { + // Test that we receive two single-tap events when two tap gestures are + // close in time but far in distance. + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, 0, apzc->GetGuid(), _)) + .Times(2); + + TimeDuration brief = + TimeDuration::FromMilliseconds(StaticPrefs::apz_max_tap_time() / 10.0); + + ScreenIntPoint point(10, 10); + Tap(apzc, point, brief); + + mcc->AdvanceBy(brief); + + point.x += apzc->GetSecondTapTolerance() * 2; + point.y += apzc->GetSecondTapTolerance() * 2; + + Tap(apzc, point, brief); +} + +class APZCFlingStopTester : public APZCGestureDetectorTester { + protected: + // Start a fling, and then tap while the fling is ongoing. When + // aSlow is false, the tap will happen while the fling is at a + // high velocity, and we check that the tap doesn't trigger sending a tap + // to content. If aSlow is true, the tap will happen while the fling + // is at a slow velocity, and we check that the tap does trigger sending + // a tap to content. See bug 1022956. + void DoFlingStopTest(bool aSlow) { + int touchStart = 50; + int touchEnd = 10; + + // Start the fling down. + Pan(apzc, touchStart, touchEnd); + // The touchstart from the pan will leave some cancelled tasks in the queue, + // clear them out + + // If we want to tap while the fling is fast, let the fling advance for 10ms + // only. If we want the fling to slow down more, advance to 2000ms. These + // numbers may need adjusting if our friction and threshold values change, + // but they should be deterministic at least. + int timeDelta = aSlow ? 2000 : 10; + int tapCallsExpected = aSlow ? 2 : 1; + + // Advance the fling animation by timeDelta milliseconds. + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + apzc->SampleContentTransformForFrame( + &viewTransformOut, pointOut, TimeDuration::FromMilliseconds(timeDelta)); + + // Deliver a tap to abort the fling. Ensure that we get a SingleTap + // call out of it if and only if the fling is slow. + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, _, 0, apzc->GetGuid(), _)) + .Times(tapCallsExpected); + Tap(apzc, ScreenIntPoint(10, 10), 0); + while (mcc->RunThroughDelayedTasks()) + ; + + // Deliver another tap, to make sure that taps are flowing properly once + // the fling is aborted. + Tap(apzc, ScreenIntPoint(100, 100), 0); + while (mcc->RunThroughDelayedTasks()) + ; + + // Verify that we didn't advance any further after the fling was aborted, in + // either case. + ParentLayerPoint finalPointOut; + apzc->SampleContentTransformForFrame(&viewTransformOut, finalPointOut); + EXPECT_EQ(pointOut.x, finalPointOut.x); + EXPECT_EQ(pointOut.y, finalPointOut.y); + + apzc->AssertStateIsReset(); + } + + void DoFlingStopWithSlowListener(bool aPreventDefault) { + MakeApzcWaitForMainThread(); + + int touchStart = 50; + int touchEnd = 10; + uint64_t blockId = 0; + + // Start the fling down. + Pan(apzc, touchStart, touchEnd, PanOptions::None, nullptr, nullptr, + &blockId); + apzc->ConfirmTarget(blockId); + apzc->ContentReceivedInputBlock(blockId, false); + + // Sample the fling a couple of times to ensure it's going. + ParentLayerPoint point, finalPoint; + AsyncTransform viewTransform; + apzc->SampleContentTransformForFrame(&viewTransform, point, + TimeDuration::FromMilliseconds(10)); + apzc->SampleContentTransformForFrame(&viewTransform, finalPoint, + TimeDuration::FromMilliseconds(10)); + EXPECT_GT(finalPoint.y, point.y); + + // Now we put our finger down to stop the fling + blockId = + TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()).mInputBlockId; + + // Re-sample to make sure it hasn't moved + apzc->SampleContentTransformForFrame(&viewTransform, point, + TimeDuration::FromMilliseconds(10)); + EXPECT_EQ(finalPoint.x, point.x); + EXPECT_EQ(finalPoint.y, point.y); + + // respond to the touchdown that stopped the fling. + // even if we do a prevent-default on it, the animation should remain + // stopped. + apzc->ContentReceivedInputBlock(blockId, aPreventDefault); + + // Verify the page hasn't moved + apzc->SampleContentTransformForFrame(&viewTransform, point, + TimeDuration::FromMilliseconds(70)); + EXPECT_EQ(finalPoint.x, point.x); + EXPECT_EQ(finalPoint.y, point.y); + + // clean up + TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time()); + + apzc->AssertStateIsReset(); + } +}; + +TEST_F(APZCFlingStopTester, FlingStop) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + DoFlingStopTest(false); +} + +TEST_F(APZCFlingStopTester, FlingStopTap) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + DoFlingStopTest(true); +} + +TEST_F(APZCFlingStopTester, FlingStopSlowListener) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + DoFlingStopWithSlowListener(false); +} + +TEST_F(APZCFlingStopTester, FlingStopPreventDefault) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + DoFlingStopWithSlowListener(true); +} + +TEST_F(APZCGestureDetectorTester, ShortPress) { + MakeApzcUnzoomable(); + + MockFunction check; + { + InSequence s; + // This verifies that the single tap notification is sent after the + // touchup is fully processed. The ordering here is important. + EXPECT_CALL(check, Call("pre-tap")); + EXPECT_CALL(check, Call("post-tap")); + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), + 0, apzc->GetGuid(), _)) + .Times(1); + } + + check.Call("pre-tap"); + TapAndCheckStatus(apzc, ScreenIntPoint(10, 10), + TimeDuration::FromMilliseconds(100)); + check.Call("post-tap"); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, MediumPress) { + MakeApzcUnzoomable(); + + MockFunction check; + { + InSequence s; + // This verifies that the single tap notification is sent after the + // touchup is fully processed. The ordering here is important. + EXPECT_CALL(check, Call("pre-tap")); + EXPECT_CALL(check, Call("post-tap")); + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), + 0, apzc->GetGuid(), _)) + .Times(1); + } + + check.Call("pre-tap"); + TapAndCheckStatus(apzc, ScreenIntPoint(10, 10), + TimeDuration::FromMilliseconds(400)); + check.Call("post-tap"); + + apzc->AssertStateIsReset(); +} + +class APZCLongPressTester : public APZCGestureDetectorTester { + protected: + void DoLongPressTest(uint32_t aBehavior) { + MakeApzcUnzoomable(); + + APZEventResult result = + TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus()); + uint64_t blockId = result.mInputBlockId; + + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + // SetAllowedTouchBehavior() must be called after sending touch-start. + nsTArray allowedTouchBehaviors; + allowedTouchBehaviors.AppendElement(aBehavior); + apzc->SetAllowedTouchBehavior(blockId, allowedTouchBehaviors); + } + // Have content "respond" to the touchstart + apzc->ContentReceivedInputBlock(blockId, false); + + MockFunction check; + + { + InSequence s; + + EXPECT_CALL(check, Call("preHandleLongTap")); + blockId++; + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), + 0, apzc->GetGuid(), blockId)) + .Times(1); + EXPECT_CALL(check, Call("postHandleLongTap")); + + EXPECT_CALL(check, Call("preHandleLongTapUp")); + EXPECT_CALL(*mcc, + HandleTap(TapType::eLongTapUp, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(1); + EXPECT_CALL(check, Call("postHandleLongTapUp")); + } + + // Manually invoke the longpress while the touch is currently down. + check.Call("preHandleLongTap"); + mcc->RunThroughDelayedTasks(); + check.Call("postHandleLongTap"); + + // Dispatching the longpress event starts a new touch block, which + // needs a new content response and also has a pending timeout task + // in the queue. Deal with those here. We do the content response first + // with preventDefault=false, and then we run the timeout task which + // "loses the race" and does nothing. + apzc->ContentReceivedInputBlock(blockId, false); + mcc->AdvanceByMillis(1000); + + // Finally, simulate lifting the finger. Since the long-press wasn't + // prevent-defaulted, we should get a long-tap-up event. + check.Call("preHandleLongTapUp"); + result = TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time()); + mcc->RunThroughDelayedTasks(); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus()); + check.Call("postHandleLongTapUp"); + + apzc->AssertStateIsReset(); + } + + void DoLongPressPreventDefaultTest(uint32_t aBehavior) { + MakeApzcUnzoomable(); + + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0); + + int touchX = 10, touchStartY = 10, touchEndY = 50; + + APZEventResult result = + TouchDown(apzc, ScreenIntPoint(touchX, touchStartY), mcc->Time()); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus()); + uint64_t blockId = result.mInputBlockId; + + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + // SetAllowedTouchBehavior() must be called after sending touch-start. + nsTArray allowedTouchBehaviors; + allowedTouchBehaviors.AppendElement(aBehavior); + apzc->SetAllowedTouchBehavior(blockId, allowedTouchBehaviors); + } + // Have content "respond" to the touchstart + apzc->ContentReceivedInputBlock(blockId, false); + + MockFunction check; + + { + InSequence s; + + EXPECT_CALL(check, Call("preHandleLongTap")); + blockId++; + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, + LayoutDevicePoint(touchX, touchStartY), 0, + apzc->GetGuid(), blockId)) + .Times(1); + EXPECT_CALL(check, Call("postHandleLongTap")); + } + + // Manually invoke the longpress while the touch is currently down. + check.Call("preHandleLongTap"); + mcc->RunThroughDelayedTasks(); + check.Call("postHandleLongTap"); + + // There should be a TimeoutContentResponse task in the queue still, + // waiting for the response from the longtap event dispatched above. + // Send the signal that content has handled the long-tap, and then run + // the timeout task (it will be a no-op because the content "wins" the + // race. This takes the place of the "contextmenu" event. + apzc->ContentReceivedInputBlock(blockId, true); + mcc->AdvanceByMillis(1000); + + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + mti.mTouches.AppendElement(SingleTouchData( + 0, ParentLayerPoint(touchX, touchEndY), ScreenSize(0, 0), 0, 0)); + result = apzc->ReceiveInputEvent(mti); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus()); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, + LayoutDevicePoint(touchX, touchEndY), 0, + apzc->GetGuid(), _)) + .Times(0); + result = TouchUp(apzc, ScreenIntPoint(touchX, touchEndY), mcc->Time()); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, result.GetStatus()); + + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + + EXPECT_EQ(ParentLayerPoint(), pointOut); + EXPECT_EQ(AsyncTransform(), viewTransformOut); + + apzc->AssertStateIsReset(); + } +}; + +TEST_F(APZCLongPressTester, LongPress) { + DoLongPressTest(kDefaultTouchBehavior); +} + +TEST_F(APZCLongPressTester, LongPressPreventDefault) { + DoLongPressPreventDefaultTest(kDefaultTouchBehavior); +} + +TEST_F(APZCGestureDetectorTester, DoubleTap) { + MakeApzcWaitForMainThread(); + MakeApzcZoomable(); + + apzc->GetFrameMetrics().SetIsRootContent(true); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(0); + EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(1); + + uint64_t blockIds[2]; + DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds); + + // responses to the two touchstarts + apzc->ContentReceivedInputBlock(blockIds[0], false); + apzc->ContentReceivedInputBlock(blockIds[1], false); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, DoubleTapNotZoomable) { + MakeApzcWaitForMainThread(); + MakeApzcUnzoomable(); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(1); + EXPECT_CALL(*mcc, HandleTap(TapType::eSecondTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(1); + EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(0); + + uint64_t blockIds[2]; + DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds); + + // responses to the two touchstarts + apzc->ContentReceivedInputBlock(blockIds[0], false); + apzc->ContentReceivedInputBlock(blockIds[1], false); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, DoubleTapPreventDefaultFirstOnly) { + MakeApzcWaitForMainThread(); + MakeApzcZoomable(); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(1); + EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(0); + + uint64_t blockIds[2]; + DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds); + + // responses to the two touchstarts + apzc->ContentReceivedInputBlock(blockIds[0], true); + apzc->ContentReceivedInputBlock(blockIds[1], false); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, DoubleTapPreventDefaultBoth) { + MakeApzcWaitForMainThread(); + MakeApzcZoomable(); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(0); + EXPECT_CALL(*mcc, HandleTap(TapType::eDoubleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(0); + + uint64_t blockIds[2]; + DoubleTapAndCheckStatus(apzc, ScreenIntPoint(10, 10), &blockIds); + + // responses to the two touchstarts + apzc->ContentReceivedInputBlock(blockIds[0], true); + apzc->ContentReceivedInputBlock(blockIds[1], true); + + apzc->AssertStateIsReset(); +} + +// Test for bug 947892 +// We test whether we dispatch tap event when the tap is followed by pinch. +TEST_F(APZCGestureDetectorTester, TapFollowedByPinch) { + MakeApzcZoomable(); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(1); + + Tap(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100)); + + int inputId = 0; + MultiTouchInput mti; + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20), + ScreenSize(0, 0), 0, 0)); + mti.mTouches.AppendElement(SingleTouchData( + inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0)); + apzc->ReceiveInputEvent(mti); + + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time()); + mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20), + ScreenSize(0, 0), 0, 0)); + mti.mTouches.AppendElement(SingleTouchData( + inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0)); + apzc->ReceiveInputEvent(mti); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, TapFollowedByMultipleTouches) { + MakeApzcZoomable(); + + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(1); + + Tap(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100)); + + int inputId = 0; + MultiTouchInput mti; + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20), + ScreenSize(0, 0), 0, 0)); + apzc->ReceiveInputEvent(mti, Some(nsTArray{kDefaultTouchBehavior})); + + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20), + ScreenSize(0, 0), 0, 0)); + mti.mTouches.AppendElement(SingleTouchData( + inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0)); + apzc->ReceiveInputEvent(mti, Some(nsTArray{kDefaultTouchBehavior})); + + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, mcc->Time()); + mti.mTouches.AppendElement(SingleTouchData(inputId, ParentLayerPoint(20, 20), + ScreenSize(0, 0), 0, 0)); + mti.mTouches.AppendElement(SingleTouchData( + inputId + 1, ParentLayerPoint(10, 10), ScreenSize(0, 0), 0, 0)); + apzc->ReceiveInputEvent(mti); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCGestureDetectorTester, LongPressInterruptedByWheel) { + // Since we try to allow concurrent input blocks of different types to + // co-exist, the wheel block shouldn't interrupt the long-press detection. + // But more importantly, this shouldn't crash, which is what it did at one + // point in time. + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _)).Times(1); + + APZEventResult result = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()); + uint64_t touchBlockId = result.mInputBlockId; + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(apzc, touchBlockId); + } + mcc->AdvanceByMillis(10); + uint64_t wheelBlockId = + Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time()) + .mInputBlockId; + EXPECT_NE(touchBlockId, wheelBlockId); + mcc->AdvanceByMillis(1000); +} + +TEST_F(APZCGestureDetectorTester, TapTimeoutInterruptedByWheel) { + // In this test, even though the wheel block comes right after the tap, the + // tap should still be dispatched because it completes fully before the wheel + // block arrived. + EXPECT_CALL(*mcc, HandleTap(TapType::eSingleTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(1); + + // We make the APZC zoomable so the gesture detector needs to wait to + // distinguish between tap and double-tap. During that timeout is when we + // insert the wheel event. + MakeApzcZoomable(); + + uint64_t touchBlockId = 0; + Tap(apzc, ScreenIntPoint(10, 10), TimeDuration::FromMilliseconds(100), + nullptr, &touchBlockId); + mcc->AdvanceByMillis(10); + uint64_t wheelBlockId = + Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time()) + .mInputBlockId; + EXPECT_NE(touchBlockId, wheelBlockId); + while (mcc->RunThroughDelayedTasks()) + ; +} + +TEST_F(APZCGestureDetectorTester, LongPressWithInputQueueDelay) { + // In this test, we ensure that any time spent waiting in the input queue for + // the content response is subtracted from the long-press timeout in the + // GestureEventListener. In this test the content response timeout is longer + // than the long-press timeout. + SCOPED_GFX_PREF_INT("apz.content_response_timeout", 60); + SCOPED_GFX_PREF_INT("ui.click_hold_context_menus.delay", 30); + + MakeApzcWaitForMainThread(); + + MockFunction check; + + { + InSequence s; + EXPECT_CALL(check, Call("pre long-tap dispatch")); + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(1); + EXPECT_CALL(check, Call("post long-tap dispatch")); + } + + // Touch down + APZEventResult result = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()); + uint64_t touchBlockId = result.mInputBlockId; + // Simulate content response after 10ms + mcc->AdvanceByMillis(10); + apzc->ContentReceivedInputBlock(touchBlockId, false); + apzc->SetAllowedTouchBehavior(touchBlockId, {kDefaultTouchBehavior}); + apzc->ConfirmTarget(touchBlockId); + // Ensure long-tap event happens within 20ms after that + check.Call("pre long-tap dispatch"); + mcc->AdvanceByMillis(20); + check.Call("post long-tap dispatch"); +} + +TEST_F(APZCGestureDetectorTester, LongPressWithInputQueueDelay2) { + // Similar to the previous test, except this time we don't simulate the + // content response at all, and still expect the long-press to happen on + // schedule. + SCOPED_GFX_PREF_INT("apz.content_response_timeout", 60); + SCOPED_GFX_PREF_INT("ui.click_hold_context_menus.delay", 30); + + MakeApzcWaitForMainThread(); + + MockFunction check; + + { + InSequence s; + EXPECT_CALL(check, Call("pre long-tap dispatch")); + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(1); + EXPECT_CALL(check, Call("post long-tap dispatch")); + } + + // Touch down + TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()); + // Ensure the long-tap happens within 30ms even though there's no content + // response. + check.Call("pre long-tap dispatch"); + mcc->AdvanceByMillis(30); + check.Call("post long-tap dispatch"); +} + +TEST_F(APZCGestureDetectorTester, LongPressWithInputQueueDelay3) { + // Similar to the previous test, except now we have the long-press delay + // being longer than the content response timeout. + SCOPED_GFX_PREF_INT("apz.content_response_timeout", 30); + SCOPED_GFX_PREF_INT("ui.click_hold_context_menus.delay", 60); + + MakeApzcWaitForMainThread(); + + MockFunction check; + + { + InSequence s; + EXPECT_CALL(check, Call("pre long-tap dispatch")); + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, LayoutDevicePoint(10, 10), 0, + apzc->GetGuid(), _)) + .Times(1); + EXPECT_CALL(check, Call("post long-tap dispatch")); + } + + // Touch down + TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()); + // Ensure the long-tap happens at the 60ms mark even though the input event + // waits in the input queue for the full content response timeout of 30ms + mcc->AdvanceByMillis(59); + check.Call("pre long-tap dispatch"); + mcc->AdvanceByMillis(1); + check.Call("post long-tap dispatch"); +} diff --git a/gfx/layers/apz/test/gtest/TestHitTesting.cpp b/gfx/layers/apz/test/gtest/TestHitTesting.cpp new file mode 100644 index 0000000000..04f1e40f5d --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestHitTesting.cpp @@ -0,0 +1,352 @@ +/* -*- 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 "APZCTreeManagerTester.h" +#include "APZTestCommon.h" + +#include "InputUtils.h" + +class APZHitTestingTester : public APZCTreeManagerTester { + protected: + ScreenToParentLayerMatrix4x4 transformToApzc; + ParentLayerToScreenMatrix4x4 transformToGecko; + + already_AddRefed GetTargetAPZC( + const ScreenPoint& aPoint) { + RefPtr hit = + manager->GetTargetAPZC(aPoint).mTargetApzc; + if (hit) { + transformToApzc = manager->GetScreenToApzcTransform(hit.get()); + transformToGecko = + manager->GetApzcToGeckoTransform(hit.get(), LayoutAndVisual); + } + return hit.forget(); + } + + protected: + void DisableApzOn(WebRenderLayerScrollData* aLayer) { + ModifyFrameMetrics(aLayer, [](ScrollMetadata& aSm, FrameMetrics&) { + aSm.SetForceDisableApz(true); + }); + } + + void CreateComplexMultiLayerTree() { + const char* treeShape = "x(xx(x)xx(x(x)xx))"; + // LayerID 0 12 3 45 6 7 89 + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 300, 400), // root(0) + LayerIntRect(0, 0, 100, 100), // layer(1) in top-left + LayerIntRect(50, 50, 200, 300), // layer(2) centered in root(0) + LayerIntRect(50, 50, 200, + 300), // layer(3) fully occupying parent layer(2) + LayerIntRect(0, 200, 100, 100), // layer(4) in bottom-left + LayerIntRect(200, 0, 100, + 400), // layer(5) along the right 100px of root(0) + LayerIntRect(200, 0, 100, 200), // layer(6) taking up the top + // half of parent layer(5) + LayerIntRect(200, 0, 100, + 200), // layer(7) fully occupying parent layer(6) + LayerIntRect(200, 200, 100, + 100), // layer(8) in bottom-right (below (6)) + LayerIntRect(200, 300, 100, + 100), // layer(9) in bottom-right (below (8)) + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID); + SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID); + SetScrollableFrameMetrics(layers[4], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollableFrameMetrics(layers[6], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollableFrameMetrics(layers[7], + ScrollableLayerGuid::START_SCROLL_ID + 2); + SetScrollableFrameMetrics(layers[8], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollableFrameMetrics(layers[9], + ScrollableLayerGuid::START_SCROLL_ID + 3); + } + + void CreateBug1148350LayerTree() { + const char* treeShape = "x(x)"; + // LayerID 0 1 + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 200, 200), + LayerIntRect(0, 0, 200, 200), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID); + } +}; + +TEST_F(APZHitTestingTester, ComplexMultiLayerTree) { + CreateComplexMultiLayerTree(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + /* The layer tree looks like this: + + 0 + |----|--+--|----| + 1 2 4 5 + | /|\ + 3 6 8 9 + | + 7 + + Layers 1,2 have the same APZC + Layers 4,6,8 have the same APZC + Layer 7 has an APZC + Layer 9 has an APZC + */ + + TestAsyncPanZoomController* nullAPZC = nullptr; + // Ensure all the scrollable layers have an APZC + + EXPECT_FALSE(HasScrollableFrameMetrics(layers[0])); + EXPECT_NE(nullAPZC, ApzcOf(layers[1])); + EXPECT_NE(nullAPZC, ApzcOf(layers[2])); + EXPECT_FALSE(HasScrollableFrameMetrics(layers[3])); + EXPECT_NE(nullAPZC, ApzcOf(layers[4])); + EXPECT_FALSE(HasScrollableFrameMetrics(layers[5])); + EXPECT_NE(nullAPZC, ApzcOf(layers[6])); + EXPECT_NE(nullAPZC, ApzcOf(layers[7])); + EXPECT_NE(nullAPZC, ApzcOf(layers[8])); + EXPECT_NE(nullAPZC, ApzcOf(layers[9])); + // Ensure those that scroll together have the same APZCs + EXPECT_EQ(ApzcOf(layers[1]), ApzcOf(layers[2])); + EXPECT_EQ(ApzcOf(layers[4]), ApzcOf(layers[6])); + EXPECT_EQ(ApzcOf(layers[8]), ApzcOf(layers[6])); + // Ensure those that don't scroll together have different APZCs + EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[4])); + EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[7])); + EXPECT_NE(ApzcOf(layers[1]), ApzcOf(layers[9])); + EXPECT_NE(ApzcOf(layers[4]), ApzcOf(layers[7])); + EXPECT_NE(ApzcOf(layers[4]), ApzcOf(layers[9])); + EXPECT_NE(ApzcOf(layers[7]), ApzcOf(layers[9])); + // Ensure the APZC parent chains are set up correctly + TestAsyncPanZoomController* layers1_2 = ApzcOf(layers[1]); + TestAsyncPanZoomController* layers4_6_8 = ApzcOf(layers[4]); + TestAsyncPanZoomController* layer7 = ApzcOf(layers[7]); + TestAsyncPanZoomController* layer9 = ApzcOf(layers[9]); + EXPECT_EQ(nullptr, layers1_2->GetParent()); + EXPECT_EQ(nullptr, layers4_6_8->GetParent()); + EXPECT_EQ(layers4_6_8, layer7->GetParent()); + EXPECT_EQ(nullptr, layer9->GetParent()); + // Ensure the hit-testing tree looks like the layer tree + RefPtr root = manager->GetRootNode(); + RefPtr node5 = root->GetLastChild(); + RefPtr node4 = node5->GetPrevSibling(); + RefPtr node2 = node4->GetPrevSibling(); + RefPtr node1 = node2->GetPrevSibling(); + RefPtr node3 = node2->GetLastChild(); + RefPtr node9 = node5->GetLastChild(); + RefPtr node8 = node9->GetPrevSibling(); + RefPtr node6 = node8->GetPrevSibling(); + RefPtr node7 = node6->GetLastChild(); + EXPECT_EQ(nullptr, node1->GetPrevSibling()); + EXPECT_EQ(nullptr, node3->GetPrevSibling()); + EXPECT_EQ(nullptr, node6->GetPrevSibling()); + EXPECT_EQ(nullptr, node7->GetPrevSibling()); + EXPECT_EQ(nullptr, node1->GetLastChild()); + EXPECT_EQ(nullptr, node3->GetLastChild()); + EXPECT_EQ(nullptr, node4->GetLastChild()); + EXPECT_EQ(nullptr, node7->GetLastChild()); + EXPECT_EQ(nullptr, node8->GetLastChild()); + EXPECT_EQ(nullptr, node9->GetLastChild()); + + // Assertions about hit-testing have been ported to mochitest, + // in helper_hittest_bug1730606-4.html. +} + +TEST_F(APZHitTestingTester, TestRepaintFlushOnNewInputBlock) { + // The main purpose of this test is to verify that touch-start events (or + // anything that starts a new input block) don't ever get untransformed. This + // should always hold because the APZ code should flush repaints when we start + // a new input block and the transform to gecko space should be empty. + + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + RefPtr apzcroot = ApzcOf(root); + + // At this point, the following holds (all coordinates in screen pixels): + // layers[0] has content from (0,0)-(500,500), clipped by composition bounds + // (0,0)-(200,200) + + MockFunction check; + + { + InSequence s; + + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1)); + EXPECT_CALL(check, Call("post-first-touch-start")); + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1)); + EXPECT_CALL(check, Call("post-second-fling")); + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(1)); + EXPECT_CALL(check, Call("post-second-touch-start")); + } + + // This first pan will move the APZC by 50 pixels, and dispatch a paint + // request. + Pan(apzcroot, 100, 50, PanOptions::NoFling); + + // Verify that a touch start doesn't get untransformed + ScreenIntPoint touchPoint(50, 50); + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement( + SingleTouchData(0, touchPoint, ScreenSize(0, 0), 0, 0)); + + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, + manager->ReceiveInputEvent(mti).GetStatus()); + EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint); + check.Call("post-first-touch-start"); + + // Send a touchend to clear state + mti.mType = MultiTouchInput::MULTITOUCH_END; + manager->ReceiveInputEvent(mti); + + mcc->AdvanceByMillis(1000); + + // Now do two pans. The first of these will dispatch a repaint request, as + // above. The second will get stuck in the paint throttler because the first + // one doesn't get marked as "completed", so this will result in a non-empty + // LD transform. (Note that any outstanding repaint requests from the first + // half of this test don't impact this half because we advance the time by 1 + // second, which will trigger the max-wait-exceeded codepath in the paint + // throttler). + Pan(apzcroot, 100, 50, PanOptions::NoFling); + check.Call("post-second-fling"); + Pan(apzcroot, 100, 50, PanOptions::NoFling); + + // Ensure that a touch start again doesn't get untransformed by flushing + // a repaint + mti.mType = MultiTouchInput::MULTITOUCH_START; + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, + manager->ReceiveInputEvent(mti).GetStatus()); + EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint); + check.Call("post-second-touch-start"); + + mti.mType = MultiTouchInput::MULTITOUCH_END; + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, + manager->ReceiveInputEvent(mti).GetStatus()); + EXPECT_EQ(touchPoint, mti.mTouches[0].mScreenPoint); +} + +TEST_F(APZHitTestingTester, TestRepaintFlushOnWheelEvents) { + // The purpose of this test is to ensure that wheel events trigger a repaint + // flush as per bug 1166871, and that the wheel event untransform is a no-op. + + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + TestAsyncPanZoomController* apzcroot = ApzcOf(root); + + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(AtLeast(3)); + ScreenPoint origin(100, 50); + for (int i = 0; i < 3; i++) { + ScrollWheelInput swi(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT, + ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 10, + false, WheelDeltaAdjustmentStrategy::eNone); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, + manager->ReceiveInputEvent(swi).GetStatus()); + EXPECT_EQ(origin, swi.mOrigin); + + AsyncTransform viewTransform; + ParentLayerPoint point; + apzcroot->SampleContentTransformForFrame(&viewTransform, point); + EXPECT_EQ(0, point.x); + EXPECT_EQ((i + 1) * 10, point.y); + EXPECT_EQ(0, viewTransform.mTranslation.x); + EXPECT_EQ((i + 1) * -10, viewTransform.mTranslation.y); + + mcc->AdvanceByMillis(5); + } +} + +TEST_F(APZHitTestingTester, TestForceDisableApz) { + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + DisableApzOn(root); + TestAsyncPanZoomController* apzcroot = ApzcOf(root); + + ScreenPoint origin(100, 50); + ScrollWheelInput swi(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT, + ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 10, + false, WheelDeltaAdjustmentStrategy::eNone); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, + manager->ReceiveInputEvent(swi).GetStatus()); + EXPECT_EQ(origin, swi.mOrigin); + + AsyncTransform viewTransform; + ParentLayerPoint point; + apzcroot->SampleContentTransformForFrame(&viewTransform, point); + // Since APZ is force-disabled, we expect to see the async transform via + // the NORMAL AsyncMode, but not via the RESPECT_FORCE_DISABLE AsyncMode. + EXPECT_EQ(0, point.x); + EXPECT_EQ(10, point.y); + EXPECT_EQ(0, viewTransform.mTranslation.x); + EXPECT_EQ(-10, viewTransform.mTranslation.y); + viewTransform = apzcroot->GetCurrentAsyncTransform( + AsyncPanZoomController::eForCompositing); + point = apzcroot->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForCompositing); + EXPECT_EQ(0, point.x); + EXPECT_EQ(0, point.y); + EXPECT_EQ(0, viewTransform.mTranslation.x); + EXPECT_EQ(0, viewTransform.mTranslation.y); + + mcc->AdvanceByMillis(10); + + // With untransforming events we should get normal behaviour (in this case, + // no noticeable untransform, because the repaint request already got + // flushed). + swi = ScrollWheelInput(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT, + ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 0, + false, WheelDeltaAdjustmentStrategy::eNone); + EXPECT_EQ(nsEventStatus_eConsumeDoDefault, + manager->ReceiveInputEvent(swi).GetStatus()); + EXPECT_EQ(origin, swi.mOrigin); +} + +TEST_F(APZHitTestingTester, Bug1148350) { + CreateBug1148350LayerTree(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + MockFunction check; + { + InSequence s; + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, LayoutDevicePoint(100, 100), 0, + ApzcOf(layers[1])->GetGuid(), _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped without transform")); + EXPECT_CALL(*mcc, + HandleTap(TapType::eSingleTap, LayoutDevicePoint(100, 100), 0, + ApzcOf(layers[1])->GetGuid(), _)) + .Times(1); + EXPECT_CALL(check, Call("Tapped with interleaved transform")); + } + + Tap(manager, ScreenIntPoint(100, 100), TimeDuration::FromMilliseconds(100)); + mcc->RunThroughDelayedTasks(); + check.Call("Tapped without transform"); + + uint64_t blockId = + TouchDown(manager, ScreenIntPoint(100, 100), mcc->Time()).mInputBlockId; + SetDefaultAllowedTouchBehavior(manager, blockId); + mcc->AdvanceByMillis(100); + + layers[0]->SetVisibleRegion(LayerIntRegion(LayerIntRect(0, 50, 200, 150))); + layers[0]->SetTransform(Matrix4x4::Translation(0, 50, 0)); + UpdateHitTestingTree(); + + TouchUp(manager, ScreenIntPoint(100, 100), mcc->Time()); + mcc->RunThroughDelayedTasks(); + check.Call("Tapped with interleaved transform"); +} diff --git a/gfx/layers/apz/test/gtest/TestInputQueue.cpp b/gfx/layers/apz/test/gtest/TestInputQueue.cpp new file mode 100644 index 0000000000..6e47340da5 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestInputQueue.cpp @@ -0,0 +1,45 @@ +/* -*- 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 "APZCTreeManagerTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" + +// Test of scenario described in bug 1269067 - that a continuing mouse drag +// doesn't interrupt a wheel scrolling animation +TEST_F(APZCTreeManagerTester, WheelInterruptedByMouseDrag) { + // Set up a scrollable layer + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + RefPtr apzc = ApzcOf(root); + + // First start the mouse drag + uint64_t dragBlockId = + MouseDown(apzc, ScreenIntPoint(5, 5), mcc->Time()).mInputBlockId; + uint64_t tmpBlockId = + MouseMove(apzc, ScreenIntPoint(6, 6), mcc->Time()).mInputBlockId; + EXPECT_EQ(dragBlockId, tmpBlockId); + + // Insert the wheel event, check that it has a new block id + uint64_t wheelBlockId = + SmoothWheel(apzc, ScreenIntPoint(6, 6), ScreenPoint(0, 1), mcc->Time()) + .mInputBlockId; + EXPECT_NE(dragBlockId, wheelBlockId); + + // Continue the drag, check that the block id is the same as before + tmpBlockId = MouseMove(apzc, ScreenIntPoint(7, 5), mcc->Time()).mInputBlockId; + EXPECT_EQ(dragBlockId, tmpBlockId); + + // Finish the wheel animation + apzc->AdvanceAnimationsUntilEnd(); + + // Check that it scrolled + ParentLayerPoint scroll = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + EXPECT_EQ(scroll.x, 0); + EXPECT_EQ(scroll.y, 10); // We scrolled 1 "line" or 10 pixels +} diff --git a/gfx/layers/apz/test/gtest/TestOverscroll.cpp b/gfx/layers/apz/test/gtest/TestOverscroll.cpp new file mode 100644 index 0000000000..10327871fb --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestOverscroll.cpp @@ -0,0 +1,1991 @@ +/* -*- 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 "APZCBasicTester.h" +#include "APZCTreeManagerTester.h" +#include "APZTestCommon.h" +#include "mozilla/layers/WebRenderScrollDataWrapper.h" + +#include "InputUtils.h" + +class APZCOverscrollTester : public APZCBasicTester { + public: + explicit APZCOverscrollTester( + AsyncPanZoomController::GestureBehavior aGestureBehavior = + AsyncPanZoomController::DEFAULT_GESTURES) + : APZCBasicTester(aGestureBehavior) {} + + protected: + UniquePtr registration; + + void TestOverscroll() { + // Pan sufficiently to hit overscroll behavior + PanIntoOverscroll(); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost()); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); + } + + void PanIntoOverscroll() { + int touchStart = 500; + int touchEnd = 10; + Pan(apzc, touchStart, touchEnd); + EXPECT_TRUE(apzc->IsOverscrolled()); + } + + /** + * Sample animations until we recover from overscroll. + * @param aExpectedScrollOffset the expected reported scroll offset + * throughout the animation + */ + void SampleAnimationUntilRecoveredFromOverscroll( + const ParentLayerPoint& aExpectedScrollOffset) { + const TimeDuration increment = TimeDuration::FromMilliseconds(1); + bool recoveredFromOverscroll = false; + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + while (apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut)) { + // The reported scroll offset should be the same throughout. + EXPECT_EQ(aExpectedScrollOffset, pointOut); + + // Trigger computation of the overscroll tranform, to make sure + // no assetions fire during the calculation. + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + + if (!apzc->IsOverscrolled()) { + recoveredFromOverscroll = true; + } + + mcc->AdvanceBy(increment); + } + EXPECT_TRUE(recoveredFromOverscroll); + apzc->AssertStateIsReset(); + } + + ScrollableLayerGuid CreateSimpleRootScrollableForWebRender() { + ScrollableLayerGuid guid; + guid.mScrollId = ScrollableLayerGuid::START_SCROLL_ID; + guid.mLayersId = LayersId{0}; + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + metrics.SetScrollId(guid.mScrollId); + metadata.SetIsLayersIdRoot(true); + + WebRenderLayerScrollData rootLayerScrollData; + rootLayerScrollData.InitializeRoot(0); + WebRenderScrollData scrollData; + rootLayerScrollData.AppendScrollMetadata(scrollData, metadata); + scrollData.AddLayerData(std::move(rootLayerScrollData)); + + registration = MakeUnique(guid.mLayersId, mcc); + tm->UpdateHitTestingTree(WebRenderScrollDataWrapper(*updater, &scrollData), + false, guid.mLayersId, 0); + return guid; + } +}; + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, FlingIntoOverscroll) { + // Enable overscrolling. + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + // Scroll down by 25 px. Don't fling for simplicity. + Pan(apzc, 50, 25, PanOptions::NoFling); + + // Now scroll back up by 20px, this time flinging after. + // The fling should cover the remaining 5 px of room to scroll, then + // go into overscroll, and finally snap-back to recover from overscroll. + Pan(apzc, 25, 45); + const TimeDuration increment = TimeDuration::FromMilliseconds(1); + bool reachedOverscroll = false; + bool recoveredFromOverscroll = false; + while (apzc->AdvanceAnimations(mcc->GetSampleTime())) { + if (!reachedOverscroll && apzc->IsOverscrolled()) { + reachedOverscroll = true; + } + if (reachedOverscroll && !apzc->IsOverscrolled()) { + recoveredFromOverscroll = true; + } + mcc->AdvanceBy(increment); + } + EXPECT_TRUE(reachedOverscroll); + EXPECT_TRUE(recoveredFromOverscroll); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, OverScrollPanning) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + TestOverscroll(); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +// Tests that an overscroll animation doesn't trigger an assertion failure +// in the case where a sample has a velocity of zero. +TEST_F(APZCOverscrollTester, OverScroll_Bug1152051a) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Doctor the prefs to make the velocity zero at the end of the first sample. + + // This ensures our incoming velocity to the overscroll animation is + // a round(ish) number, 4.9 (that being the distance of the pan before + // overscroll, which is 500 - 10 = 490 pixels, divided by the duration of + // the pan, which is 100 ms). + SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0); + + TestOverscroll(); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +// Tests that ending an overscroll animation doesn't leave around state that +// confuses the next overscroll animation. +TEST_F(APZCOverscrollTester, OverScroll_Bug1152051b) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + SCOPED_GFX_PREF_FLOAT("apz.overscroll.stop_distance_threshold", 0.1f); + + // Pan sufficiently to hit overscroll behavior + PanIntoOverscroll(); + + // Sample animations once, to give the fling animation started on touch-up + // a chance to realize it's overscrolled, and schedule a call to + // HandleFlingOverscroll(). + SampleAnimationOnce(); + + // This advances the time and runs the HandleFlingOverscroll task scheduled in + // the previous call, which starts an overscroll animation. It then samples + // the overscroll animation once, to get it to initialize the first overscroll + // sample. + SampleAnimationOnce(); + + // Do a touch-down to cancel the overscroll animation, and then a touch-up + // to schedule a new one since we're still overscrolled. We don't pan because + // panning can trigger functions that clear the overscroll animation state + // in other ways. + APZEventResult result = TouchDown(apzc, ScreenIntPoint(10, 10), mcc->Time()); + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId); + } + TouchUp(apzc, ScreenIntPoint(10, 10), mcc->Time()); + + // Sample the second overscroll animation to its end. + // If the ending of the first overscroll animation fails to clear state + // properly, this will assert. + ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost()); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +// Tests that the page doesn't get stuck in an +// overscroll animation after a low-velocity pan. +TEST_F(APZCOverscrollTester, OverScrollAfterLowVelocityPan_Bug1343775) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Pan into overscroll with a velocity less than the + // apz.fling_min_velocity_threshold preference. + Pan(apzc, 10, 30); + + EXPECT_TRUE(apzc->IsOverscrolled()); + + apzc->AdvanceAnimationsUntilEnd(); + + // Check that we recovered from overscroll. + EXPECT_FALSE(apzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, OverScrollAbort) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Pan sufficiently to hit overscroll behavior + int touchStart = 500; + int touchEnd = 10; + Pan(apzc, touchStart, touchEnd); + EXPECT_TRUE(apzc->IsOverscrolled()); + + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + + // This sample call will run to the end of the fling animation + // and will schedule the overscroll animation. + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut, + TimeDuration::FromMilliseconds(10000)); + EXPECT_TRUE(apzc->IsOverscrolled()); + + // At this point, we have an active overscroll animation. + // Check that cancelling the animation clears the overscroll. + apzc->CancelAnimation(); + EXPECT_FALSE(apzc->IsOverscrolled()); + apzc->AssertStateIsReset(); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, OverScrollPanningAbort) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Pan sufficiently to hit overscroll behaviour. Keep the finger down so + // the pan does not end. + int touchStart = 500; + int touchEnd = 10; + Pan(apzc, touchStart, touchEnd, PanOptions::KeepFingerDown); + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Check that calling CancelAnimation() while the user is still panning + // (and thus no fling or snap-back animation has had a chance to start) + // clears the overscroll. + apzc->CancelAnimation(); + EXPECT_FALSE(apzc->IsOverscrolled()); + apzc->AssertStateIsReset(); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android +TEST_F(APZCOverscrollTester, OverscrollByVerticalPanGestures) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, 0); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, StuckInOverscroll_Bug1767337) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Send two PANGESTURE_END in a row, to see if the second one gets us + // stuck in overscroll. + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE, true); + SampleAnimationOnce(); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE, true); + + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, 0); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, OverscrollByVerticalAndHorizontalPanGestures) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-10, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, 0), mcc->Time()); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, 0); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, OverscrollByPanMomentumGestures) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // Make sure we are not yet in overscrolled region. + EXPECT_TRUE(!apzc->IsOverscrolled()); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost()); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, IgnoreMomemtumDuringOverscroll) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + float yMost = GetScrollRange().YMost(); + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, yMost / 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, yMost), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, yMost / 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // Make sure we've started an overscroll animation. + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + + // And check the overscrolled transform value before/after calling PanGesture + // to make sure the overscroll amount isn't affected by momentum events. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + AsyncTransformComponentMatrix overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_EQ(overscrolledTransform, apzc->GetOverscrollTransform( + AsyncPanZoomController::eForHitTesting)); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time()); + EXPECT_EQ(overscrolledTransform, apzc->GetOverscrollTransform( + AsyncPanZoomController::eForHitTesting)); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time()); + EXPECT_EQ(overscrolledTransform, apzc->GetOverscrollTransform( + AsyncPanZoomController::eForHitTesting)); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 2), mcc->Time()); + EXPECT_EQ(overscrolledTransform, apzc->GetOverscrollTransform( + AsyncPanZoomController::eForHitTesting)); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_EQ(overscrolledTransform, apzc->GetOverscrollTransform( + AsyncPanZoomController::eForHitTesting)); + + // Check that we've recovered from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, GetScrollRange().YMost()); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, VerticalOnlyOverscroll) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Make the content scrollable only vertically. + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + apzc->SetFrameMetrics(metrics); + + // Scroll up into overscroll a bit. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-10, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + // Now it's overscrolled. + EXPECT_TRUE(apzc->IsOverscrolled()); + AsyncTransformComponentMatrix overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + // The overscroll shouldn't happen horizontally. + EXPECT_TRUE(overscrolledTransform._41 == 0); + // Happens only vertically. + EXPECT_TRUE(overscrolledTransform._42 != 0); + + // Send pan momentum events including horizontal bits. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(-10, -100), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + // The overscroll shouldn't happen horizontally. + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(-5, -50), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, -2), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, 0); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, VerticalOnlyOverscrollByPanMomentum) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Make the content scrollable only vertically. + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + // Scrolls the content down a bit. + metrics.SetVisualScrollOffset(CSSPoint(0, 50)); + apzc->SetFrameMetrics(metrics); + + // Scroll up a bit where overscroll will not happen. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // Make sure it's not yet overscrolled. + EXPECT_TRUE(!apzc->IsOverscrolled()); + + // Send pan momentum events including horizontal bits. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(-10, -100), mcc->Time()); + // Now it's overscrolled. + EXPECT_TRUE(apzc->IsOverscrolled()); + + AsyncTransformComponentMatrix overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + // But the overscroll shouldn't happen horizontally. + EXPECT_TRUE(overscrolledTransform._41 == 0); + // Happens only vertically. + EXPECT_TRUE(overscrolledTransform._42 != 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(-5, -50), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, -2), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + overscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + EXPECT_TRUE(overscrolledTransform._41 == 0); + EXPECT_TRUE(overscrolledTransform._42 != 0); + + // Check that we recover from overscroll via an animation. + ParentLayerPoint expectedScrollOffset(0, 0); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, DisallowOverscrollInSingleLineTextControl) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Create a horizontal scrollable frame with `vertical disregarded direction`. + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 10)); + metrics.SetScrollableRect(CSSRect(0, 0, 1000, 10)); + apzc->SetFrameMetrics(metrics); + metadata.SetDisregardedDirection(Some(ScrollDirection::eVertical)); + apzc->NotifyLayersUpdated(metadata, /*aIsFirstPaint=*/false, + /*aThisLayerTreeUpdated=*/true); + + // Try to overscroll up and left with pan gestures. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 5), + ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 5), + ScreenPoint(-10, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 5), + ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 5), + ScreenPoint(0, 0), mcc->Time()); + + // No overscrolling should happen. + EXPECT_TRUE(!apzc->IsOverscrolled()); + + // Send pan momentum events too. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 5), ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 5), ScreenPoint(-100, -100), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 5), ScreenPoint(-50, -50), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 5), ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 5), ScreenPoint(0, 0), mcc->Time()); + // No overscrolling should happen either. + EXPECT_TRUE(!apzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android +// Tests that horizontal overscroll animation keeps running with vertical +// pan momentum scrolling. +TEST_F(APZCOverscrollTester, + HorizontalOverscrollAnimationWithVerticalPanMomentumScrolling) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 1000, 5000)); + apzc->SetFrameMetrics(metrics); + + // Try to overscroll left with pan gestures. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-10, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // Make sure we've started an overscroll animation. + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + AsyncTransformComponentMatrix initialOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + + // Send lengthy downward momentums to make sure the overscroll animation + // doesn't clobber the momentums scrolling. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll amount on X axis has started being managed by the overscroll + // animation. + AsyncTransformComponentMatrix currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + EXPECT_NE(initialOverscrolledTransform._41, currentOverscrolledTransform._41); + // There is no overscroll on Y axis. + EXPECT_EQ(currentOverscrolledTransform._42, 0); + ParentLayerPoint scrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + // The scroll offset shouldn't be changed by the overscroll animation. + EXPECT_EQ(scrollOffset.y, 0); + + // Simple gesture on the Y axis to ensure that we can send a vertical + // momentum scroll + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + ParentLayerPoint offsetAfterPan = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on both axes shouldn't be changed by this pan + // momentum start event since the displacement is zero. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll amount should be managed by the overscroll animation. + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + scrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + // Not yet started scrolling. + EXPECT_EQ(scrollOffset.y, offsetAfterPan.y); + EXPECT_EQ(scrollOffset.x, 0); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + + // Send a long pan momentum. + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on X axis shouldn't be changed by this momentum pan. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + // Now it started scrolling vertically. + scrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + EXPECT_GT(scrollOffset.y, 0); + EXPECT_EQ(scrollOffset.x, 0); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll on X axis keeps being managed by the overscroll animation. + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + // The scroll offset on Y axis shouldn't be changed by the overscroll + // animation. + EXPECT_EQ(scrollOffset.y, apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::eForHitTesting) + .y); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + scrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on X axis shouldn't be changed by this momentum pan. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + // Scrolling keeps going by momentum. + EXPECT_GT( + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting) + .y, + scrollOffset.y); + + scrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 10), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // Scrolling keeps going by momentum. + EXPECT_GT( + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting) + .y, + scrollOffset.y); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + scrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // This momentum event doesn't change the scroll offset since its + // displacement is zero. + EXPECT_EQ( + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting) + .y, + scrollOffset.y); + + // Check that we recover from the horizontal overscroll via the animation. + ParentLayerPoint expectedScrollOffset(0, scrollOffset.y); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android +// Similar to above +// HorizontalOverscrollAnimationWithVerticalPanMomentumScrolling, +// but having OverscrollAnimation on both axes initially. +TEST_F(APZCOverscrollTester, + BothAxesOverscrollAnimationWithPanMomentumScrolling) { + // TODO: This test currently requires gestures that cause movement on both + // axis, which excludes DOMINANT_AXIS locking mode. The gestures should be + // broken up into multiple gestures to cause the overscroll. + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 2); + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 1000, 5000)); + apzc->SetFrameMetrics(metrics); + + // Try to overscroll up and left with pan gestures. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-10, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // Make sure we've started an overscroll animation. + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + AsyncTransformComponentMatrix initialOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + + // Send lengthy downward momentums to make sure the overscroll animation + // doesn't clobber the momentums scrolling. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll amount has started being managed by the overscroll + // animation. + AsyncTransformComponentMatrix currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + EXPECT_NE(initialOverscrolledTransform._41, currentOverscrolledTransform._41); + EXPECT_NE(initialOverscrolledTransform._42, currentOverscrolledTransform._42); + + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on both axes shouldn't be changed by this pan + // momentum start event since the displacement is zero. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // Still being managed by the overscroll animation. + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + EXPECT_NE( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + // Send a long pan momentum. + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on X axis shouldn't be changed by this momentum pan. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + // But now the overscroll amount on Y axis should be changed by this momentum + // pan. + EXPECT_NE( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42); + // Actually it's no longer overscrolled. + EXPECT_EQ( + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42, + 0); + + ParentLayerPoint currentScrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + // Now it started scrolling. + EXPECT_GT(currentScrollOffset.y, 0); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll on X axis keeps being managed by the overscroll animation. + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + // But the overscroll on Y axis is no longer affected by the overscroll + // animation. + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42); + // The scroll offset on Y axis shouldn't be changed by the overscroll + // animation. + EXPECT_EQ( + currentScrollOffset.y, + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting) + .y); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + currentScrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 100), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on X axis shouldn't be changed by this momentum pan. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + // Keeping no overscrolling on Y axis. + EXPECT_EQ( + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42, + 0); + // Scrolling keeps going by momentum. + EXPECT_GT( + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting) + .y, + currentScrollOffset.y); + + currentScrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 10), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // Keeping no overscrolling on Y axis. + EXPECT_EQ( + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42, + 0); + // Scrolling keeps going by momentum. + EXPECT_GT( + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting) + .y, + currentScrollOffset.y); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + currentScrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // Keeping no overscrolling on Y axis. + EXPECT_EQ( + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42, + 0); + // This momentum event doesn't change the scroll offset since its + // displacement is zero. + EXPECT_EQ( + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting) + .y, + currentScrollOffset.y); + + // Check that we recover from the horizontal overscroll via the animation. + ParentLayerPoint expectedScrollOffset(0, currentScrollOffset.y); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Maybe fails on Android +// This is another variant of +// HorizontalOverscrollAnimationWithVerticalPanMomentumScrolling. In this test, +// after a horizontal overscroll animation started, upwards pan moments happen, +// thus there should be a new vertical overscroll animation in addition to +// the horizontal one. +TEST_F( + APZCOverscrollTester, + VerticalOverscrollAnimationInAdditionToExistingHorizontalOverscrollAnimation) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 1000, 5000)); + // Scrolls the content 50px down. + metrics.SetVisualScrollOffset(CSSPoint(0, 50)); + apzc->SetFrameMetrics(metrics); + + // Try to overscroll left with pan gestures. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-10, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(-2, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // Make sure we've started an overscroll animation. + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + AsyncTransformComponentMatrix initialOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + + // Send lengthy __upward__ momentums to make sure the overscroll animation + // doesn't clobber the momentums scrolling. + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll amount on X axis has started being managed by the overscroll + // animation. + AsyncTransformComponentMatrix currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + EXPECT_NE(initialOverscrolledTransform._41, currentOverscrolledTransform._41); + // There is no overscroll on Y axis. + EXPECT_EQ( + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42, + 0); + ParentLayerPoint scrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + // The scroll offset shouldn't be changed by the overscroll animation. + EXPECT_EQ(scrollOffset.y, 50); + + // Simple gesture on the Y axis to ensure that we can send a vertical + // momentum scroll + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + ParentLayerPoint offsetAfterPan = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on both axes shouldn't be changed by this pan + // momentum start event since the displacement is zero. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll amount should be managed by the overscroll animation. + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + scrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + // Not yet started scrolling. + EXPECT_EQ(scrollOffset.y, offsetAfterPan.y); + EXPECT_EQ(scrollOffset.x, 0); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + + // Send a long pan momentum. + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, -200), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on X axis shouldn't be changed by this momentum pan. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + // Now it started scrolling vertically. + scrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + EXPECT_EQ(scrollOffset.y, 0); + EXPECT_EQ(scrollOffset.x, 0); + // Actually it's also vertically overscrolled. + EXPECT_GT( + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42, + 0); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // The overscroll on X axis keeps being managed by the overscroll animation. + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + // The overscroll on Y Axis hasn't been changed by the overscroll animation at + // this moment, sine the last displacement was consumed in the last pan + // momentum. + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, -100), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on X axis shouldn't be changed by this momentum pan. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + // Now the overscroll amount on Y axis shouldn't be changed by this momentum + // pan either. + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + EXPECT_NE( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + // And now the overscroll on Y Axis should be also managed by the overscroll + // animation. + EXPECT_NE( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, -10), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + // The overscroll amount on both axes shouldn't be changed by momentum event. + EXPECT_EQ( + currentOverscrolledTransform._41, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._41); + EXPECT_EQ( + currentOverscrolledTransform._42, + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting)._42); + + currentOverscrolledTransform = + apzc->GetOverscrollTransform(AsyncPanZoomController::eForHitTesting); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, apzc, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + + // Check that we recover from the horizontal overscroll via the animation. + ParentLayerPoint expectedScrollOffset(0, 0); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTester, OverscrollByPanGesturesInterruptedByReflowZoom) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + SCOPED_GFX_PREF_INT("mousewheel.with_control.action", 3); // reflow zoom. + + // A sanity check that pan gestures with ctrl modifier will not be handled by + // APZ. + PanGestureInput panInput(PanGestureInput::PANGESTURE_START, mcc->Time(), + ScreenIntPoint(5, 5), ScreenPoint(0, -2), + MODIFIER_CONTROL); + WidgetWheelEvent wheelEvent = panInput.ToWidgetEvent(nullptr); + EXPECT_FALSE(APZInputBridge::ActionForWheelEvent(&wheelEvent).isSome()); + + ScrollableLayerGuid rootGuid = CreateSimpleRootScrollableForWebRender(); + RefPtr apzc = + tm->GetTargetAPZC(rootGuid.mLayersId, rootGuid.mScrollId); + + PanGesture(PanGestureInput::PANGESTURE_START, tm, ScreenIntPoint(50, 80), + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, tm, ScreenIntPoint(50, 80), + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Make sure overscrolling has started. + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Press ctrl until PANGESTURE_END. + PanGestureWithModifiers(PanGestureInput::PANGESTURE_PAN, MODIFIER_CONTROL, tm, + ScreenIntPoint(50, 80), ScreenPoint(0, -2), + mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + // At this moment (i.e. PANGESTURE_PAN), still in overscrolling state. + EXPECT_TRUE(apzc->IsOverscrolled()); + + PanGestureWithModifiers(PanGestureInput::PANGESTURE_END, MODIFIER_CONTROL, tm, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), + mcc->Time()); + // The overscrolling state should have been restored. + EXPECT_TRUE(!apzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTester, SmoothTransitionFromPanToAnimation) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + // Start scrolled down to y=500px. + metrics.SetVisualScrollOffset(CSSPoint(0, 500)); + apzc->SetFrameMetrics(metrics); + + int frameLength = 10; // milliseconds; 10 to keep the math simple + float panVelocity = 10; // pixels per millisecond + int panPixelsPerFrame = frameLength * panVelocity; // 100 pixels per frame + + ScreenIntPoint panPoint(50, 50); + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(0, -1), mcc->Time()); + // Pan up for 6 frames at 100 pixels per frame. This should reduce + // the vertical scroll offset from 500 to 0, and get us into overscroll. + for (int i = 0; i < 6; ++i) { + mcc->AdvanceByMillis(frameLength); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, -panPixelsPerFrame), mcc->Time()); + } + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Pan further into overscroll at the same input velocity, enough + // for the frames while we are in overscroll to dominate the computation + // in the velocity tracker. + // Importantly, while the input velocity is still 100 pixels per frame, + // in the overscrolled state the page only visual moves by at most 8 pixels + // per frame. + int frames = StaticPrefs::apz_velocity_relevance_time_ms() / frameLength; + for (int i = 0; i < frames; ++i) { + mcc->AdvanceByMillis(frameLength); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, -panPixelsPerFrame), mcc->Time()); + } + EXPECT_TRUE(apzc->IsOverscrolled()); + + // End the pan, allowing an overscroll animation to start. + mcc->AdvanceByMillis(frameLength); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0), + mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + + // Check that the velocity reflects the actual movement (no more than 8 + // pixels/frame ==> 0.8 pixels per millisecond), not the input velocity + // (100 pixels/frame ==> 10 pixels per millisecond). This ensures that + // the transition from the pan to the animation appears smooth. + // (Note: velocities are negative since they are upwards.) + EXPECT_LT(apzc->GetVelocityVector().y, 0); + EXPECT_GT(apzc->GetVelocityVector().y, -0.8); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTester, NoOverscrollForMousewheel) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + // Start scrolled down just a few pixels from the top. + metrics.SetVisualScrollOffset(CSSPoint(0, 3)); + // Set line and page scroll amounts. Otherwise, even though Wheel() uses + // SCROLLDELTA_PIXEL, the wheel handling code will get confused by things + // like the "don't scroll more than one page" check. + metadata.SetPageScrollAmount(LayoutDeviceIntSize(50, 100)); + metadata.SetLineScrollAmount(LayoutDeviceIntSize(5, 10)); + apzc->SetScrollMetadata(metadata); + + // Send a wheel with enough delta to scrollto y=0 *and* overscroll. + Wheel(apzc, ScreenIntPoint(10, 10), ScreenPoint(0, -10), mcc->Time()); + + // Check that we did not actually go into overscroll. + EXPECT_FALSE(apzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTester, ClickWhileOverscrolled) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + apzc->SetFrameMetrics(metrics); + + // Pan into overscroll at the top. + ScreenIntPoint panPoint(50, 50); + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(0, -1), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, -100), mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->GetOverscrollAmount().y < 0); // overscrolled at top + + // End the pan. This should start an overscroll animation. + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0), + mcc->Time()); + EXPECT_TRUE(apzc->GetOverscrollAmount().y < 0); // overscrolled at top + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + + // Send a mouse-down. This should interrupt the animation but not relieve + // overscroll yet. + ParentLayerPoint overscrollBefore = apzc->GetOverscrollAmount(); + MouseDown(apzc, panPoint, mcc->Time()); + EXPECT_FALSE(apzc->IsOverscrollAnimationRunning()); + EXPECT_EQ(overscrollBefore, apzc->GetOverscrollAmount()); + + // Send a mouse-up. This should start an overscroll animation again. + MouseUp(apzc, panPoint, mcc->Time()); + EXPECT_TRUE(apzc->IsOverscrollAnimationRunning()); + + SampleAnimationUntilRecoveredFromOverscroll(ParentLayerPoint(0, 0)); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTester, DynamicallyLoadingContent) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + metrics.SetVisualScrollOffset(CSSPoint(0, 0)); + apzc->SetFrameMetrics(metrics); + + // Pan to the bottom of the page, and further, into overscroll. + ScreenIntPoint panPoint(50, 50); + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(0, 1), mcc->Time()); + for (int i = 0; i < 12; ++i) { + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, 100), mcc->Time()); + } + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->GetOverscrollAmount().y > 0); // overscrolled at bottom + + // Grow the scrollable rect at the bottom, simulating the page loading content + // dynamically. + CSSRect scrollableRect = metrics.GetScrollableRect(); + scrollableRect.height += 500; + metrics.SetScrollableRect(scrollableRect); + apzc->NotifyLayersUpdated(metadata, false, true); + + // Check that the modified scrollable rect cleared the overscroll. + EXPECT_FALSE(apzc->IsOverscrolled()); + + // Pan back up to the top, and further, into overscroll. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(0, -1), mcc->Time()); + for (int i = 0; i < 12; ++i) { + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, -100), mcc->Time()); + } + EXPECT_TRUE(apzc->IsOverscrolled()); + ParentLayerPoint overscrollAmount = apzc->GetOverscrollAmount(); + EXPECT_TRUE(overscrollAmount.y < 0); // overscrolled at top + + // Grow the scrollable rect at the bottom again. + scrollableRect = metrics.GetScrollableRect(); + scrollableRect.height += 500; + metrics.SetScrollableRect(scrollableRect); + apzc->NotifyLayersUpdated(metadata, false, true); + + // Check that the modified scrollable rect did NOT clear overscroll at the + // top. + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_EQ(overscrollAmount, + apzc->GetOverscrollAmount()); // overscroll did not change at all +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTester, SmallAmountOfOverscroll) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + + // Do vertical overscroll first. + ScreenIntPoint panPoint(50, 50); + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0), + mcc->Time()); + mcc->AdvanceByMillis(10); + + // Then do small horizontal overscroll which will be considered as "finished" + // by our overscroll animation physics model. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(-0.1, 0), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(-0.2, 0), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0), + mcc->Time()); + mcc->AdvanceByMillis(10); + + EXPECT_TRUE(apzc->IsOverscrolled()); + EXPECT_TRUE(apzc->GetOverscrollAmount().y < 0); // overscrolled at top + EXPECT_TRUE(apzc->GetOverscrollAmount().x < 0); // and overscrolled at left + + // Then do vertical scroll. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, panPoint, + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, panPoint, + ScreenPoint(0, 100), mcc->Time()); + mcc->AdvanceByMillis(10); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, panPoint, ScreenPoint(0, 0), + mcc->Time()); + + ParentLayerPoint scrollOffset = + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting); + EXPECT_GT(scrollOffset.y, 0); // Make sure the vertical scroll offset is + // greater than zero. + + // The small horizontal overscroll amount should be restored to zero. + ParentLayerPoint expectedScrollOffset(0, scrollOffset.y); + SampleAnimationUntilRecoveredFromOverscroll(expectedScrollOffset); +} +#endif + +#ifdef MOZ_WIDGET_ANDROID // Only applies to WidgetOverscrollEffect +TEST_F(APZCOverscrollTester, StuckInOverscroll_Bug1786452) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + ScrollMetadata metadata; + FrameMetrics& metrics = metadata.GetMetrics(); + metrics.SetCompositionBounds(ParentLayerRect(0, 0, 100, 100)); + metrics.SetScrollableRect(CSSRect(0, 0, 100, 1000)); + + // Over the course of the test, expect one or more calls to + // UpdateOverscrollOffset(), followed by a call to UpdateOverscrollVelocity(). + // The latter ensures the widget has a chance to end its overscroll effect. + InSequence s; + EXPECT_CALL(*mcc, UpdateOverscrollOffset(_, _, _, _)).Times(AtLeast(1)); + EXPECT_CALL(*mcc, UpdateOverscrollVelocity(_, _, _, _)).Times(1); + + // Pan into overscroll, keeping the finger down + ScreenIntPoint startPoint(10, 500); + ScreenIntPoint endPoint(10, 10); + Pan(apzc, startPoint, endPoint, PanOptions::KeepFingerDown); + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Linger a while to cause the velocity to drop to very low or zero + mcc->AdvanceByMillis(100); + TouchMove(apzc, endPoint, mcc->Time()); + EXPECT_LT(apzc->GetVelocityVector().Length(), + StaticPrefs::apz_fling_min_velocity_threshold()); + EXPECT_TRUE(apzc->IsOverscrolled()); + + // Lift the finger + mcc->AdvanceByMillis(20); + TouchUp(apzc, endPoint, mcc->Time()); + EXPECT_FALSE(apzc->IsOverscrolled()); +} +#endif + +class APZCOverscrollTesterMock : public APZCTreeManagerTester { + public: + APZCOverscrollTesterMock() { CreateMockHitTester(); } + + UniquePtr registration; + TestAsyncPanZoomController* rootApzc; +}; + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTesterMock, OverscrollHandoff) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50)}; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 200, 200)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + // same size as the visible region so that + // the container is not scrollable in any directions + // actually. This is simulating overflow: hidden + // iframe document in Fission, though we don't set + // a different layers id. + CSSRect(0, 0, 100, 50)); + + SetScrollHandoff(layers[1], root); + + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetFrameMetrics().SetIsRootContent(true); + + // A pan gesture on the child scroller (which is not scrollable though). + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20), + ScreenPoint(0, -2), mcc->Time()); + EXPECT_TRUE(rootApzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTesterMock, VerticalOverscrollHandoffToScrollableRoot) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Create a layer tree having two vertical scrollable layers. + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50)}; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 200)); + + SetScrollHandoff(layers[1], root); + + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetFrameMetrics().SetIsRootContent(true); + + // A vertical pan gesture on the child scroller which will be handed off to + // the root APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20), + ScreenPoint(0, -2), mcc->Time()); + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTesterMock, NoOverscrollHandoffToNonScrollableRoot) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Create a layer tree having non-scrollable root and a vertical scrollable + // child. + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50)}; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 100)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 200)); + + SetScrollHandoff(layers[1], root); + + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetFrameMetrics().SetIsRootContent(true); + + // A vertical pan gesture on the child scroller which should not be handed + // off the root APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20), + ScreenPoint(0, -2), mcc->Time()); + EXPECT_FALSE(rootApzc->IsOverscrolled()); + EXPECT_TRUE(ApzcOf(layers[1])->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCOverscrollTesterMock, NoOverscrollHandoffOrthogonalPanGesture) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Create a layer tree having horizontal scrollable root and a vertical + // scrollable child. + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50)}; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 200, 100)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 200)); + + SetScrollHandoff(layers[1], root); + + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetFrameMetrics().SetIsRootContent(true); + + // A vertical pan gesture on the child scroller which should not be handed + // off the root APZC because the root APZC is not scrollable vertically. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 20), + ScreenPoint(0, -2), mcc->Time()); + EXPECT_FALSE(rootApzc->IsOverscrolled()); + EXPECT_TRUE(ApzcOf(layers[1])->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTesterMock, + RetriggerCancelledOverscrollAnimationByNewPanGesture) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Create a layer tree having vertical scrollable root and a horizontal + // scrollable child. + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50)}; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 200, 50)); + + SetScrollHandoff(layers[1], root); + + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetFrameMetrics().SetIsRootContent(true); + + ScreenIntPoint panPoint(50, 20); + // A vertical pan gesture on the child scroller which should be handed off the + // root APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, panPoint, + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint, + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint, + ScreenPoint(0, 0), mcc->Time()); + + // The root APZC should be overscrolled and the child APZC should not be. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled()); + + mcc->AdvanceByMillis(10); + + // Make sure the root APZC is still overscrolled. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + + // Start a new horizontal pan gesture on the child scroller which should be + // handled by the child APZC now. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + APZEventResult result = PanGesture(PanGestureInput::PANGESTURE_START, manager, + panPoint, ScreenPoint(-2, 0), mcc->Time()); + // The above horizontal pan start event was flagged as "this event may trigger + // swipe" and either the root scrollable frame or the horizontal child + // scrollable frame is not scrollable in the pan start direction, thus the pan + // start event run into the short circuit path for swipe-to-navigation in + // InputQueue::ReceivePanGestureInput, which means it's waiting for the + // content response, so we need to respond explicitly here. + manager->ContentReceivedInputBlock(result.mInputBlockId, false); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint, + ScreenPoint(-10, 0), mcc->Time()); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint, + ScreenPoint(0, 0), mcc->Time()); + + // Now both APZCs should be overscrolled. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_TRUE(ApzcOf(layers[1])->IsOverscrolled()); + + // Sample all animations until all of them have been finished. + while (SampleAnimationsOnce()) + ; + + // After the animations finished, all overscrolled states should have been + // restored. + EXPECT_FALSE(rootApzc->IsOverscrolled()); + EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTesterMock, RetriggeredOverscrollAnimationVelocity) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + // Setup two nested vertical scrollable frames. + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50)}; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 200)); + + SetScrollHandoff(layers[1], root); + + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetFrameMetrics().SetIsRootContent(true); + + ScreenIntPoint panPoint(50, 20); + // A vertical upward pan gesture on the child scroller which should be handed + // off the root APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, panPoint, + ScreenPoint(0, -2), mcc->Time()); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint, + ScreenPoint(0, -10), mcc->Time()); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint, + ScreenPoint(0, 0), mcc->Time()); + + // The root APZC should be overscrolled and the child APZC should not be. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_FALSE(ApzcOf(layers[1])->IsOverscrolled()); + + mcc->AdvanceByMillis(10); + + // Make sure the root APZC is still overscrolled and there's an overscroll + // animation. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_TRUE(rootApzc->IsOverscrollAnimationRunning()); + + // And make sure the overscroll animation's velocity is a certain amount in + // the upward direction. + EXPECT_LT(rootApzc->GetVelocityVector().y, 0); + + // Start a new downward pan gesture on the child scroller which + // should be handled by the child APZC now. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_START, manager, panPoint, + ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(10); + // The new pan-start gesture stops the overscroll animation at this moment. + EXPECT_TRUE(!rootApzc->IsOverscrollAnimationRunning()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, panPoint, + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(10); + // There's no overscroll animation yet even if the root APZC is still + // overscrolled. + EXPECT_TRUE(!rootApzc->IsOverscrollAnimationRunning()); + EXPECT_TRUE(rootApzc->IsOverscrolled()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + PanGesture(PanGestureInput::PANGESTURE_END, manager, panPoint, + ScreenPoint(0, 10), mcc->Time()); + + // Now an overscroll animation should have been triggered by the pan-end + // gesture. + EXPECT_TRUE(rootApzc->IsOverscrollAnimationRunning()); + EXPECT_TRUE(rootApzc->IsOverscrolled()); + // And the newly created overscroll animation's positions should never exceed + // 0. + while (SampleAnimationsOnce()) { + EXPECT_LE(rootApzc->GetOverscrollAmount().y, 0); + } +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Only applies to GenericOverscrollEffect +TEST_F(APZCOverscrollTesterMock, OverscrollIntoPreventDefault) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegions[] = {LayerIntRect(0, 0, 100, 100)}; + CreateScrollData(treeShape, layerVisibleRegions); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + + // Start a pan gesture a few pixels below the 20px DTC region. + ScreenIntPoint cursorLocation(10, 25); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + APZEventResult result = + PanGesture(PanGestureInput::PANGESTURE_START, manager, cursorLocation, + ScreenPoint(0, -2), mcc->Time()); + + // At this point, we should be overscrolled. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + + // Pan further, until the DTC region is under the cursor. + // Note that, due to ApplyResistance(), we need a large input delta to cause a + // visual transform enough to bridge the 5px to the DTC region. + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, cursorLocation, + ScreenPoint(0, -100), mcc->Time()); + + // At this point, we are still overscrolled. Record the overscroll amount. + EXPECT_TRUE(rootApzc->IsOverscrolled()); + float overscrollY = rootApzc->GetOverscrollAmount().y; + + // Send a content response with preventDefault = true. + manager->SetAllowedTouchBehavior(result.mInputBlockId, + {AllowedTouchBehavior::VERTICAL_PAN}); + manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid}); + manager->ContentReceivedInputBlock(result.mInputBlockId, + /*aPreventDefault=*/true); + + // The content response has the effect of interrupting the input block + // but no processing happens yet (as there are no events in the block). + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_EQ(overscrollY, rootApzc->GetOverscrollAmount().y); + + // Send one more pan event. This starts a new, *unconfirmed* input block + // (via the "transmogrify" codepath). + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + result = PanGesture(PanGestureInput::PANGESTURE_PAN, manager, cursorLocation, + ScreenPoint(0, -10), mcc->Time()); + + // No overscroll occurs (the event is waiting in the queue for confirmation). + EXPECT_TRUE(rootApzc->IsOverscrolled()); + EXPECT_EQ(overscrollY, rootApzc->GetOverscrollAmount().y); + + // preventDefault the new event as well + manager->SetAllowedTouchBehavior(result.mInputBlockId, + {AllowedTouchBehavior::VERTICAL_PAN}); + manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid}); + manager->ContentReceivedInputBlock(result.mInputBlockId, + /*aPreventDefault=*/true); + + // This should trigger clearing the overscrolling and resetting the state. + EXPECT_FALSE(rootApzc->IsOverscrolled()); + rootApzc->AssertStateIsReset(); + + // If there are momentum events after this point, they should not cause + // further scrolling or overscorll. + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + result = PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager, + cursorLocation, ScreenPoint(0, -100), mcc->Time()); + mcc->AdvanceByMillis(10); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + result = PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + cursorLocation, ScreenPoint(0, -100), mcc->Time()); + EXPECT_FALSE(rootApzc->IsOverscrolled()); + EXPECT_EQ(rootApzc->GetFrameMetrics().GetVisualScrollOffset(), + CSSPoint(0, 0)); +} +#endif diff --git a/gfx/layers/apz/test/gtest/TestPanning.cpp b/gfx/layers/apz/test/gtest/TestPanning.cpp new file mode 100644 index 0000000000..886b0fec99 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestPanning.cpp @@ -0,0 +1,251 @@ +/* -*- 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 "APZCBasicTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" +#include "gtest/gtest.h" + +class APZCPanningTester : public APZCBasicTester { + protected: + void DoPanTest(bool aShouldTriggerScroll, bool aShouldBeConsumed, + uint32_t aBehavior) { + if (aShouldTriggerScroll) { + // Three repaint request for each pan. + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(6); + } else { + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0); + } + + int touchStart = 50; + int touchEnd = 10; + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + + nsTArray allowedTouchBehaviors; + allowedTouchBehaviors.AppendElement(aBehavior); + + // Pan down + PanAndCheckStatus(apzc, touchStart, touchEnd, aShouldBeConsumed, + &allowedTouchBehaviors); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + + if (aShouldTriggerScroll) { + EXPECT_EQ(ParentLayerPoint(0, -(touchEnd - touchStart)), pointOut); + EXPECT_NE(AsyncTransform(), viewTransformOut); + } else { + EXPECT_EQ(ParentLayerPoint(), pointOut); + EXPECT_EQ(AsyncTransform(), viewTransformOut); + } + + // Clear the fling from the previous pan, or stopping it will + // consume the next touchstart + apzc->CancelAnimation(); + + // Pan back + PanAndCheckStatus(apzc, touchEnd, touchStart, aShouldBeConsumed, + &allowedTouchBehaviors); + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + + EXPECT_EQ(ParentLayerPoint(), pointOut); + EXPECT_EQ(AsyncTransform(), viewTransformOut); + } + + void DoPanWithPreventDefaultTest() { + MakeApzcWaitForMainThread(); + + int touchStart = 50; + int touchEnd = 10; + ParentLayerPoint pointOut; + AsyncTransform viewTransformOut; + uint64_t blockId = 0; + + // Pan down + nsTArray allowedTouchBehaviors; + allowedTouchBehaviors.AppendElement( + mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN); + PanAndCheckStatus(apzc, touchStart, touchEnd, true, &allowedTouchBehaviors, + &blockId); + + // Send the signal that content has handled and preventDefaulted the touch + // events. This flushes the event queue. + apzc->ContentReceivedInputBlock(blockId, true); + + apzc->SampleContentTransformForFrame(&viewTransformOut, pointOut); + EXPECT_EQ(ParentLayerPoint(), pointOut); + EXPECT_EQ(AsyncTransform(), viewTransformOut); + + apzc->AssertStateIsReset(); + } +}; + +// In the each of the following 4 pan tests we are performing two pan gestures: +// vertical pan from top to bottom and back - from bottom to top. According to +// the pointer-events/touch-action spec AUTO and PAN_Y touch-action values allow +// vertical scrolling while NONE and PAN_X forbid it. The first parameter of +// DoPanTest method specifies this behavior. However, the events will be marked +// as consumed even if the behavior in PAN_X, because the user could move their +// finger horizontally too - APZ has no way of knowing beforehand and so must +// consume the events. +TEST_F(APZCPanningTester, PanWithTouchActionAuto) { + // Velocity bias can cause extra repaint requests. + SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0); + DoPanTest(true, true, + mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN | + mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN); +} + +TEST_F(APZCPanningTester, PanWithTouchActionNone) { + // Velocity bias can cause extra repaint requests. + SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0); + DoPanTest(false, false, 0); +} + +TEST_F(APZCPanningTester, PanWithTouchActionPanX) { + // Velocity bias can cause extra repaint requests. + SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0); + DoPanTest(false, false, + mozilla::layers::AllowedTouchBehavior::HORIZONTAL_PAN); +} + +TEST_F(APZCPanningTester, PanWithTouchActionPanY) { + // Velocity bias can cause extra repaint requests. + SCOPED_GFX_PREF_FLOAT("apz.velocity_bias", 0.0); + DoPanTest(true, true, mozilla::layers::AllowedTouchBehavior::VERTICAL_PAN); +} + +TEST_F(APZCPanningTester, PanWithPreventDefault) { + DoPanWithPreventDefaultTest(); +} + +TEST_F(APZCPanningTester, PanWithHistoricalTouchData) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0); + + // Simulate the same pan gesture, in three different ways. + // We start at y=50, with a 50ms resting period at the start of the pan. + // Then we accelerate the finger upwards towards y=10, reaching a 10px/10ms + // velocity towards the end of the panning motion. + // + // The first simulation fires touch move events with 10ms gaps. + // The second simulation skips two of the touch move events, simulating + // "jank". The third simulation also skips those two events, but reports the + // missed positions in the following event's historical coordinates. + // + // Consequently, the first and third simulation should estimate the same + // velocities, whereas the second simulation should estimate a different + // velocity because it is missing data. + + // First simulation: full data + + APZEventResult result = TouchDown(apzc, ScreenIntPoint(0, 50), mcc->Time()); + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId); + } + + mcc->AdvanceByMillis(50); + result = TouchMove(apzc, ScreenIntPoint(0, 45), mcc->Time()); + mcc->AdvanceByMillis(10); + result = TouchMove(apzc, ScreenIntPoint(0, 40), mcc->Time()); + mcc->AdvanceByMillis(10); + result = TouchMove(apzc, ScreenIntPoint(0, 30), mcc->Time()); + mcc->AdvanceByMillis(10); + result = TouchMove(apzc, ScreenIntPoint(0, 20), mcc->Time()); + result = TouchUp(apzc, ScreenIntPoint(0, 20), mcc->Time()); + auto velocityFromFullDataAsSeparateEvents = apzc->GetVelocityVector(); + apzc->CancelAnimation(); + + mcc->AdvanceByMillis(100); + + // Second simulation: partial data + + result = TouchDown(apzc, ScreenIntPoint(0, 50), mcc->Time()); + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId); + } + + mcc->AdvanceByMillis(50); + result = TouchMove(apzc, ScreenIntPoint(0, 45), mcc->Time()); + mcc->AdvanceByMillis(30); + result = TouchMove(apzc, ScreenIntPoint(0, 20), mcc->Time()); + result = TouchUp(apzc, ScreenIntPoint(0, 20), mcc->Time()); + auto velocityFromPartialData = apzc->GetVelocityVector(); + apzc->CancelAnimation(); + + mcc->AdvanceByMillis(100); + + // Third simulation: full data via historical data + + result = TouchDown(apzc, ScreenIntPoint(0, 50), mcc->Time()); + if (result.GetStatus() != nsEventStatus_eConsumeNoDefault) { + SetDefaultAllowedTouchBehavior(apzc, result.mInputBlockId); + } + + mcc->AdvanceByMillis(50); + result = TouchMove(apzc, ScreenIntPoint(0, 45), mcc->Time()); + mcc->AdvanceByMillis(30); + + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + auto singleTouchData = CreateSingleTouchData(0, ScreenIntPoint(0, 20)); + singleTouchData.mHistoricalData.AppendElement( + SingleTouchData::HistoricalTouchData{ + mcc->Time() - TimeDuration::FromMilliseconds(20), + ScreenIntPoint(0, 40), + {}, + {}, + 0.0f, + 0.0f}); + singleTouchData.mHistoricalData.AppendElement( + SingleTouchData::HistoricalTouchData{ + mcc->Time() - TimeDuration::FromMilliseconds(10), + ScreenIntPoint(0, 30), + {}, + {}, + 0.0f, + 0.0f}); + mti.mTouches.AppendElement(singleTouchData); + result = apzc->ReceiveInputEvent(mti); + + result = TouchUp(apzc, ScreenIntPoint(0, 20), mcc->Time()); + auto velocityFromFullDataViaHistory = apzc->GetVelocityVector(); + apzc->CancelAnimation(); + + EXPECT_EQ(velocityFromFullDataAsSeparateEvents, + velocityFromFullDataViaHistory); + EXPECT_NE(velocityFromPartialData, velocityFromFullDataViaHistory); +} + +TEST_F(APZCPanningTester, DuplicatePanEndEvents_Bug1833950) { + // Send a pan gesture that triggers a fling animation at the end. + // Note that we need at least two _PAN events to have enough samples + // in the velocity tracker to compute a fling velocity. + PanGesture(PanGestureInput::PANGESTURE_START, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_PAN, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE, + /*aSimulateMomentum=*/true); + + // Give the fling animation a chance to start. + SampleAnimationOnce(); + apzc->AssertStateIsFling(); + + // Send a duplicate pan-end event. + // This test is just intended to check that doing this doesn't + // trigger an assertion failure in debug mode. + PanGesture(PanGestureInput::PANGESTURE_END, apzc, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time(), MODIFIER_NONE, + /*aSimulateMomentum=*/true); +} diff --git a/gfx/layers/apz/test/gtest/TestPinching.cpp b/gfx/layers/apz/test/gtest/TestPinching.cpp new file mode 100644 index 0000000000..f6d1280bf3 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestPinching.cpp @@ -0,0 +1,675 @@ +/* -*- 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 "APZCBasicTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" +#include "mozilla/StaticPrefs_apz.h" + +class APZCPinchTester : public APZCBasicTester { + private: + // This (multiplied by apz.touch_start_tolerance) needs to be the hypotenuse + // in a Pythagorean triple, along with overcomeTouchToleranceX and + // overcomeTouchToleranceY from APZCTesterBase::Pan(). + // This is because APZCTesterBase::Pan(), when run without the + // PanOptions::ExactCoordinates option, will need to first overcome the + // touch start tolerance by performing a move of exactly + // (apz.touch_start_tolerance * DPI) length. + // When moving on both axes at once, we need to use integers for both legs + // (overcomeTouchToleranceX and overcomeTouchToleranceY) while making sure + // that the hypotenuse is also a round integer number (hence Pythagorean + // triples). (The hypotenuse is the length of the movement in this case.) + static const int mDPI = 100; + + public: + explicit APZCPinchTester( + AsyncPanZoomController::GestureBehavior aGestureBehavior = + AsyncPanZoomController::DEFAULT_GESTURES) + : APZCBasicTester(aGestureBehavior) {} + + void SetUp() override { + APZCBasicTester::SetUp(); + tm->SetDPI(mDPI); + } + + protected: + FrameMetrics GetPinchableFrameMetrics() { + FrameMetrics fm; + fm.SetCompositionBounds(ParentLayerRect(0, 0, 100, 200)); + fm.SetScrollableRect(CSSRect(0, 0, 980, 1000)); + fm.SetVisualScrollOffset(CSSPoint(300, 300)); + fm.SetLayoutViewport(CSSRect(300, 300, 100, 200)); + fm.SetZoom(CSSToParentLayerScale(2.0)); + // APZC only allows zooming on the root scrollable frame. + fm.SetIsRootContent(true); + // the visible area of the document in CSS pixels is x=300 y=300 w=50 h=100 + return fm; + } + + void DoPinchTest(bool aShouldTriggerPinch, + nsTArray* aAllowedTouchBehaviors = nullptr) { + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcZoomable(); + + if (aShouldTriggerPinch) { + // One repaint request for each gesture. + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(2); + } else { + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(0); + } + + int touchInputId = 0; + if (mGestureBehavior == AsyncPanZoomController::USE_GESTURE_DETECTOR) { + PinchWithTouchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 1.25, + touchInputId, aShouldTriggerPinch, + aAllowedTouchBehaviors); + } else { + PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 1.25, + aShouldTriggerPinch); + } + + apzc->AssertStateIsReset(); + + FrameMetrics fm = apzc->GetFrameMetrics(); + + if (aShouldTriggerPinch) { + // the visible area of the document in CSS pixels is now x=325 y=330 w=40 + // h=80 + EXPECT_EQ(2.5f, fm.GetZoom().scale); + EXPECT_EQ(325, fm.GetVisualScrollOffset().x); + EXPECT_EQ(330, fm.GetVisualScrollOffset().y); + } else { + // The frame metrics should stay the same since touch-action:none makes + // apzc ignore pinch gestures. + EXPECT_EQ(2.0f, fm.GetZoom().scale); + EXPECT_EQ(300, fm.GetVisualScrollOffset().x); + EXPECT_EQ(300, fm.GetVisualScrollOffset().y); + } + + // part 2 of the test, move to the top-right corner of the page and pinch + // and make sure we stay in the correct spot + fm.SetZoom(CSSToParentLayerScale(2.0)); + fm.SetVisualScrollOffset(CSSPoint(930, 5)); + apzc->SetFrameMetrics(fm); + // the visible area of the document in CSS pixels is x=930 y=5 w=50 h=100 + + if (mGestureBehavior == AsyncPanZoomController::USE_GESTURE_DETECTOR) { + PinchWithTouchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 0.5, + touchInputId, aShouldTriggerPinch, + aAllowedTouchBehaviors); + } else { + PinchWithPinchInputAndCheckStatus(apzc, ScreenIntPoint(250, 300), 0.5, + aShouldTriggerPinch); + } + + apzc->AssertStateIsReset(); + + fm = apzc->GetFrameMetrics(); + + if (aShouldTriggerPinch) { + // the visible area of the document in CSS pixels is now x=805 y=0 w=100 + // h=200 + EXPECT_EQ(1.0f, fm.GetZoom().scale); + EXPECT_EQ(805, fm.GetVisualScrollOffset().x); + EXPECT_EQ(0, fm.GetVisualScrollOffset().y); + } else { + EXPECT_EQ(2.0f, fm.GetZoom().scale); + EXPECT_EQ(930, fm.GetVisualScrollOffset().x); + EXPECT_EQ(5, fm.GetVisualScrollOffset().y); + } + } +}; + +class APZCPinchGestureDetectorTester : public APZCPinchTester { + public: + APZCPinchGestureDetectorTester() + : APZCPinchTester(AsyncPanZoomController::USE_GESTURE_DETECTOR) {} + + void DoPinchWithPreventDefaultTest() { + FrameMetrics originalMetrics = GetPinchableFrameMetrics(); + apzc->SetFrameMetrics(originalMetrics); + + MakeApzcWaitForMainThread(); + MakeApzcZoomable(); + + int touchInputId = 0; + uint64_t blockId = 0; + PinchWithTouchInput(apzc, ScreenIntPoint(250, 300), 1.25, touchInputId, + nullptr, nullptr, &blockId); + + // Send the prevent-default notification for the touch block + apzc->ContentReceivedInputBlock(blockId, true); + + // verify the metrics didn't change (i.e. the pinch was ignored) + FrameMetrics fm = apzc->GetFrameMetrics(); + EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom()); + EXPECT_EQ(originalMetrics.GetVisualScrollOffset().x, + fm.GetVisualScrollOffset().x); + EXPECT_EQ(originalMetrics.GetVisualScrollOffset().y, + fm.GetVisualScrollOffset().y); + + apzc->AssertStateIsReset(); + } +}; + +class APZCPinchLockingTester : public APZCPinchTester { + private: + ScreenIntPoint mFocus; + float mSpan; + int mPinchLockBufferMaxAge; + + public: + APZCPinchLockingTester() + : APZCPinchTester(AsyncPanZoomController::USE_GESTURE_DETECTOR), + mFocus(ScreenIntPoint(200, 300)), + mSpan(10.0) {} + + virtual void SetUp() { + mPinchLockBufferMaxAge = + StaticPrefs::apz_pinch_lock_buffer_max_age_AtStartup(); + + APZCPinchTester::SetUp(); + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcZoomable(); + + auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START, + mFocus, mSpan, mSpan, mcc->Time()); + apzc->ReceiveInputEvent(event); + mcc->AdvanceBy(TimeDuration::FromMilliseconds(mPinchLockBufferMaxAge + 1)); + } + + void twoFingerPan() { + ScreenCoord panDistance = + StaticPrefs::apz_pinch_lock_scroll_lock_threshold() * 1.2 * + tm->GetDPI(); + + mFocus = ScreenIntPoint((int)(mFocus.x.value + panDistance), + (int)(mFocus.y.value)); + + auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, + mFocus, mSpan, mSpan, mcc->Time()); + apzc->ReceiveInputEvent(event); + mcc->AdvanceBy(TimeDuration::FromMilliseconds(mPinchLockBufferMaxAge + 1)); + } + + void twoFingerZoom() { + float pinchDistance = + StaticPrefs::apz_pinch_lock_span_breakout_threshold() * 1.2 * + tm->GetDPI(); + + float newSpan = mSpan + pinchDistance; + + auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, + mFocus, newSpan, mSpan, mcc->Time()); + apzc->ReceiveInputEvent(event); + mcc->AdvanceBy(TimeDuration::FromMilliseconds(mPinchLockBufferMaxAge + 1)); + mSpan = newSpan; + } + + bool isPinchLockActive() { + FrameMetrics originalMetrics = apzc->GetFrameMetrics(); + + // Send a small scale input to the APZC + float pinchDistance = + StaticPrefs::apz_pinch_lock_span_breakout_threshold() * 0.8 * + tm->GetDPI(); + auto event = + CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, mFocus, + mSpan + pinchDistance, mSpan, mcc->Time()); + apzc->ReceiveInputEvent(event); + + FrameMetrics result = apzc->GetFrameMetrics(); + bool lockActive = originalMetrics.GetZoom() == result.GetZoom() && + originalMetrics.GetVisualScrollOffset().x == + result.GetVisualScrollOffset().x && + originalMetrics.GetVisualScrollOffset().y == + result.GetVisualScrollOffset().y; + + // Avoid side effects, reset to original frame metrics + apzc->SetFrameMetrics(originalMetrics); + return lockActive; + } +}; + +TEST_F(APZCPinchGestureDetectorTester, + Pinch_UseGestureDetector_TouchActionNone) { + nsTArray behaviors = {mozilla::layers::AllowedTouchBehavior::NONE, + mozilla::layers::AllowedTouchBehavior::NONE}; + DoPinchTest(false, &behaviors); +} + +TEST_F(APZCPinchGestureDetectorTester, + Pinch_UseGestureDetector_TouchActionZoom) { + nsTArray behaviors; + behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM); + behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM); + DoPinchTest(true, &behaviors); +} + +TEST_F(APZCPinchGestureDetectorTester, + Pinch_UseGestureDetector_TouchActionNotAllowZoom) { + nsTArray behaviors; + behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::NONE); + behaviors.AppendElement(mozilla::layers::AllowedTouchBehavior::PINCH_ZOOM); + DoPinchTest(false, &behaviors); +} + +TEST_F(APZCPinchGestureDetectorTester, + Pinch_UseGestureDetector_TouchActionNone_NoAPZZoom) { + SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false); + + // Since we are preventing the pinch action via touch-action we should not be + // sending the pinch gesture notifications that would normally be sent when + // apz_allow_zooming is false. + EXPECT_CALL(*mcc, NotifyPinchGesture(_, _, _, _, _)).Times(0); + nsTArray behaviors = {mozilla::layers::AllowedTouchBehavior::NONE, + mozilla::layers::AllowedTouchBehavior::NONE}; + DoPinchTest(false, &behaviors); +} + +TEST_F(APZCPinchGestureDetectorTester, Pinch_PreventDefault) { + DoPinchWithPreventDefaultTest(); +} + +TEST_F(APZCPinchGestureDetectorTester, Pinch_PreventDefault_NoAPZZoom) { + SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false); + + // Since we are preventing the pinch action we should not be sending the pinch + // gesture notifications that would normally be sent when apz_allow_zooming is + // false. + EXPECT_CALL(*mcc, NotifyPinchGesture(_, _, _, _, _)).Times(0); + + DoPinchWithPreventDefaultTest(); +} + +TEST_F(APZCPinchGestureDetectorTester, Panning_TwoFingerFling_ZoomDisabled) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcUnzoomable(); + + // Perform a two finger pan + int touchInputId = 0; + uint64_t blockId = 0; + PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), ScreenIntPoint(100, 100), + 1, touchInputId, nullptr, nullptr, &blockId); + + // Expect to be in a flinging state + apzc->AssertStateIsFling(); +} + +TEST_F(APZCPinchGestureDetectorTester, Pinch_DoesntFling_ZoomDisabled) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcUnzoomable(); + + // Perform a pinch + int touchInputId = 0; + uint64_t blockId = 0; + + PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), ScreenIntPoint(100, 100), + 2, touchInputId, nullptr, nullptr, &blockId, + PinchOptions::LiftFinger2, true); + + // Lift second finger after a pause + mcc->AdvanceBy(TimeDuration::FromMilliseconds(50)); + TouchUp(apzc, ScreenIntPoint(100, 100), mcc->Time()); + + // Pinch should not trigger a fling + EXPECT_EQ(apzc->GetVelocityVector().y, 0); +} + +TEST_F(APZCPinchGestureDetectorTester, Panning_TwoFingerFling_ZoomEnabled) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcZoomable(); + + // Perform a two finger pan + int touchInputId = 0; + uint64_t blockId = 0; + PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), ScreenIntPoint(100, 100), + 1, touchInputId, nullptr, nullptr, &blockId); + + // Expect to NOT be in flinging state + apzc->AssertStateIsReset(); +} + +TEST_F(APZCPinchGestureDetectorTester, + Panning_TwoThenOneFingerFling_ZoomEnabled) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcZoomable(); + + // Perform a two finger pan lifting only the first finger + int touchInputId = 0; + uint64_t blockId = 0; + PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), ScreenIntPoint(100, 100), + 1, touchInputId, nullptr, nullptr, &blockId, + PinchOptions::LiftFinger2); + + // Lift second finger after a pause + mcc->AdvanceBy(TimeDuration::FromMilliseconds(50)); + TouchUp(apzc, ScreenIntPoint(100, 100), mcc->Time()); + + // This gesture should activate the pinch lock, and result + // in a fling even if the page is zoomable. + apzc->AssertStateIsFling(); +} + +TEST_F(APZCPinchGestureDetectorTester, + Panning_TwoThenOneFingerFling_ZoomDisabled) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcUnzoomable(); + + // Perform a two finger pan lifting only the first finger + int touchInputId = 0; + uint64_t blockId = 0; + PinchWithTouchInput(apzc, ScreenIntPoint(100, 200), ScreenIntPoint(100, 100), + 1, touchInputId, nullptr, nullptr, &blockId, + PinchOptions::LiftFinger2); + + // Lift second finger after a pause + mcc->AdvanceBy(TimeDuration::FromMilliseconds(50)); + TouchUp(apzc, ScreenIntPoint(100, 100), mcc->Time()); + + // This gesture should activate the pinch lock and result in a fling + apzc->AssertStateIsFling(); +} + +TEST_F(APZCPinchTester, Panning_TwoFinger_ZoomDisabled) { + // set up APZ + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcUnzoomable(); + + nsEventStatus statuses[3]; // scalebegin, scale, scaleend + PinchWithPinchInput(apzc, ScreenIntPoint(250, 350), ScreenIntPoint(200, 300), + 10, &statuses); + + FrameMetrics fm = apzc->GetFrameMetrics(); + + // It starts from (300, 300), then moves the focus point from (250, 350) to + // (200, 300) pans by (50, 50) screen pixels, but there is a 2x zoom, which + // causes the scroll offset to change by half of that (25, 25) pixels. + EXPECT_EQ(325, fm.GetVisualScrollOffset().x); + EXPECT_EQ(325, fm.GetVisualScrollOffset().y); + EXPECT_EQ(2.0, fm.GetZoom().scale); +} + +TEST_F(APZCPinchTester, Panning_Beyond_LayoutViewport) { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 0); + + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcZoomable(); + + // Case 1 - visual viewport is still inside layout viewport. + Pan(apzc, 350, 300, PanOptions::NoFling); + FrameMetrics fm = apzc->GetFrameMetrics(); + // It starts from (300, 300) pans by (0, 50) screen pixels, but there is a + // 2x zoom, which causes the scroll offset to change by half of that (0, 25). + // But the visual viewport is still inside the layout viewport. + EXPECT_EQ(300, fm.GetVisualScrollOffset().x); + EXPECT_EQ(325, fm.GetVisualScrollOffset().y); + EXPECT_EQ(300, fm.GetLayoutViewport().X()); + EXPECT_EQ(300, fm.GetLayoutViewport().Y()); + + // Case 2 - visual viewport crosses the bottom boundary of the layout + // viewport. + Pan(apzc, 525, 325, PanOptions::NoFling); + fm = apzc->GetFrameMetrics(); + // It starts from (300, 325) pans by (0, 200) screen pixels, but there is a + // 2x zoom, which causes the scroll offset to change by half of that + // (0, 100). The visual viewport crossed the bottom boundary of the layout + // viewport by 25px. + EXPECT_EQ(300, fm.GetVisualScrollOffset().x); + EXPECT_EQ(425, fm.GetVisualScrollOffset().y); + EXPECT_EQ(300, fm.GetLayoutViewport().X()); + EXPECT_EQ(325, fm.GetLayoutViewport().Y()); + + // Case 3 - visual viewport crosses the top boundary of the layout viewport. + Pan(apzc, 425, 775, PanOptions::NoFling); + fm = apzc->GetFrameMetrics(); + // It starts from (300, 425) pans by (0, -350) screen pixels, but there is a + // 2x zoom, which causes the scroll offset to change by half of that + // (0, -175). The visual viewport crossed the top of the layout viewport by + // 75px. + EXPECT_EQ(300, fm.GetVisualScrollOffset().x); + EXPECT_EQ(250, fm.GetVisualScrollOffset().y); + EXPECT_EQ(300, fm.GetLayoutViewport().X()); + EXPECT_EQ(250, fm.GetLayoutViewport().Y()); + + // Case 4 - visual viewport crosses the left boundary of the layout viewport. + Pan(apzc, ScreenIntPoint(150, 10), ScreenIntPoint(350, 10), + PanOptions::NoFling); + fm = apzc->GetFrameMetrics(); + // It starts from (300, 250) pans by (-200, 0) screen pixels, but there is a + // 2x zoom, which causes the scroll offset to change by half of that + // (-100, 0). The visual viewport crossed the left boundary of the layout + // viewport by 100px. + EXPECT_EQ(200, fm.GetVisualScrollOffset().x); + EXPECT_EQ(250, fm.GetVisualScrollOffset().y); + EXPECT_EQ(200, fm.GetLayoutViewport().X()); + EXPECT_EQ(250, fm.GetLayoutViewport().Y()); + + // Case 5 - visual viewport crosses the right boundary of the layout viewport. + Pan(apzc, ScreenIntPoint(350, 10), ScreenIntPoint(150, 10), + PanOptions::NoFling); + fm = apzc->GetFrameMetrics(); + // It starts from (200, 250) pans by (200, 0) screen pixels, but there is a + // 2x zoom, which causes the scroll offset to change by half of that + // (100, 0). The visual viewport crossed the right boundary of the layout + // viewport by 50px. + EXPECT_EQ(300, fm.GetVisualScrollOffset().x); + EXPECT_EQ(250, fm.GetVisualScrollOffset().y); + EXPECT_EQ(250, fm.GetLayoutViewport().X()); + EXPECT_EQ(250, fm.GetLayoutViewport().Y()); + + // Case 6 - visual viewport crosses both the vertical and horizontal + // boundaries of the layout viewport by moving diagonally towards the + // top-right corner. + Pan(apzc, ScreenIntPoint(350, 200), ScreenIntPoint(150, 400), + PanOptions::NoFling); + fm = apzc->GetFrameMetrics(); + // It starts from (300, 250) pans by (200, -200) screen pixels, but there is + // a 2x zoom, which causes the scroll offset to change by half of that + // (100, -100). The visual viewport moved by (100, -100) outside the + // boundary of the layout viewport. + EXPECT_EQ(400, fm.GetVisualScrollOffset().x); + EXPECT_EQ(150, fm.GetVisualScrollOffset().y); + EXPECT_EQ(350, fm.GetLayoutViewport().X()); + EXPECT_EQ(150, fm.GetLayoutViewport().Y()); +} + +TEST_F(APZCPinchGestureDetectorTester, Pinch_APZZoom_Disabled) { + SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false); + + FrameMetrics originalMetrics = GetPinchableFrameMetrics(); + apzc->SetFrameMetrics(originalMetrics); + + // When apz_allow_zooming is false, the ZoomConstraintsClient produces + // ZoomConstraints with mAllowZoom set to false. + MakeApzcUnzoomable(); + + // With apz_allow_zooming false, we expect the NotifyPinchGesture function to + // get called as the pinch progresses, but the metrics shouldn't change. + EXPECT_CALL(*mcc, + NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_START, + apzc->GetGuid(), _, LayoutDeviceCoord(0), _)) + .Times(1); + EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_SCALE, + apzc->GetGuid(), _, _, _)) + .Times(AtLeast(1)); + EXPECT_CALL(*mcc, + NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_END, + apzc->GetGuid(), _, LayoutDeviceCoord(0), _)) + .Times(1); + + int touchInputId = 0; + uint64_t blockId = 0; + PinchWithTouchInput(apzc, ScreenIntPoint(250, 300), 1.25, touchInputId, + nullptr, nullptr, &blockId); + + // verify the metrics didn't change (i.e. the pinch was ignored inside APZ) + FrameMetrics fm = apzc->GetFrameMetrics(); + EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom()); + EXPECT_EQ(originalMetrics.GetVisualScrollOffset().x, + fm.GetVisualScrollOffset().x); + EXPECT_EQ(originalMetrics.GetVisualScrollOffset().y, + fm.GetVisualScrollOffset().y); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCPinchGestureDetectorTester, Pinch_NoSpan) { + SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false); + + FrameMetrics originalMetrics = GetPinchableFrameMetrics(); + apzc->SetFrameMetrics(originalMetrics); + + // When apz_allow_zooming is false, the ZoomConstraintsClient produces + // ZoomConstraints with mAllowZoom set to false. + MakeApzcUnzoomable(); + + // With apz_allow_zooming false, we expect the NotifyPinchGesture function to + // get called as the pinch progresses, but the metrics shouldn't change. + EXPECT_CALL(*mcc, + NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_START, + apzc->GetGuid(), _, LayoutDeviceCoord(0), _)) + .Times(1); + EXPECT_CALL(*mcc, NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_SCALE, + apzc->GetGuid(), _, _, _)) + .Times(AtLeast(1)); + EXPECT_CALL(*mcc, + NotifyPinchGesture(PinchGestureInput::PINCHGESTURE_END, + apzc->GetGuid(), _, LayoutDeviceCoord(0), _)) + .Times(1); + + int inputId = 0; + ScreenIntPoint focus(250, 300); + + // Do a pinch holding a zero span and moving the focus by y=100 + + const TimeDuration TIME_BETWEEN_TOUCH_EVENT = + TimeDuration::FromMilliseconds(50); + const auto touchBehaviors = Some(nsTArray{kDefaultTouchBehavior}); + + MultiTouchInput mtiStart = + MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0); + mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId, focus)); + mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus)); + apzc->ReceiveInputEvent(mtiStart, touchBehaviors); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + focus.y -= 35 + 1; // this is to get over the PINCH_START_THRESHOLD in + // GestureEventListener.cpp + MultiTouchInput mtiMove1 = + MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId, focus)); + mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus)); + apzc->ReceiveInputEvent(mtiMove1); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + focus.y -= 100; // do a two-finger scroll of 100 screen pixels + MultiTouchInput mtiMove2 = + MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0); + mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId, focus)); + mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus)); + apzc->ReceiveInputEvent(mtiMove2); + mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT); + + MultiTouchInput mtiEnd = + MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0); + mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId, focus)); + mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus)); + apzc->ReceiveInputEvent(mtiEnd); + + // Done, check the metrics to make sure we scrolled by 100 screen pixels, + // which is 50 CSS pixels for the pinchable frame metrics. + + FrameMetrics fm = apzc->GetFrameMetrics(); + EXPECT_EQ(originalMetrics.GetZoom(), fm.GetZoom()); + EXPECT_EQ(originalMetrics.GetVisualScrollOffset().x, + fm.GetVisualScrollOffset().x); + EXPECT_EQ(originalMetrics.GetVisualScrollOffset().y + 50, + fm.GetVisualScrollOffset().y); + + apzc->AssertStateIsReset(); +} + +TEST_F(APZCPinchTester, Pinch_TwoFinger_APZZoom_Disabled_Bug1354185) { + // Set up APZ such that mZoomConstraints.mAllowZoom is false. + SCOPED_GFX_PREF_BOOL("apz.allow_zooming", false); + apzc->SetFrameMetrics(GetPinchableFrameMetrics()); + MakeApzcUnzoomable(); + + // We expect a repaint request for scrolling. + EXPECT_CALL(*mcc, RequestContentRepaint(_)).Times(1); + + // Send only the PINCHGESTURE_START and PINCHGESTURE_SCALE events, + // in order to trigger a call to AsyncPanZoomController::OnScale + // but not to AsyncPanZoomController::OnScaleEnd. + ScreenIntPoint aFocus(250, 350); + ScreenIntPoint aSecondFocus(200, 300); + float aScale = 10; + auto event = CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START, + aFocus, 10.0, 10.0, mcc->Time()); + apzc->ReceiveInputEvent(event); + + event = + CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, + aSecondFocus, 10.0f * aScale, 10.0, mcc->Time()); + apzc->ReceiveInputEvent(event); +} + +TEST_F(APZCPinchLockingTester, Pinch_Locking_Free) { + SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 0); // PINCH_FREE + + twoFingerPan(); + EXPECT_FALSE(isPinchLockActive()); +} + +TEST_F(APZCPinchLockingTester, Pinch_Locking_Normal_Lock) { + SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 1); // PINCH_NORMAL + + twoFingerPan(); + EXPECT_TRUE(isPinchLockActive()); +} + +TEST_F(APZCPinchLockingTester, Pinch_Locking_Normal_Lock_Break) { + SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 1); // PINCH_NORMAL + + twoFingerPan(); + twoFingerZoom(); + EXPECT_TRUE(isPinchLockActive()); +} + +TEST_F(APZCPinchLockingTester, Pinch_Locking_Sticky_Lock) { + SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 2); // PINCH_STICKY + + twoFingerPan(); + EXPECT_TRUE(isPinchLockActive()); +} + +TEST_F(APZCPinchLockingTester, Pinch_Locking_Sticky_Lock_Break) { + SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 2); // PINCH_STICKY + + twoFingerPan(); + twoFingerZoom(); + EXPECT_FALSE(isPinchLockActive()); +} + +TEST_F(APZCPinchLockingTester, Pinch_Locking_Sticky_Lock_Break_Lock) { + SCOPED_GFX_PREF_INT("apz.pinch_lock.mode", 2); // PINCH_STICKY + + twoFingerPan(); + twoFingerZoom(); + twoFingerPan(); + EXPECT_TRUE(isPinchLockActive()); +} diff --git a/gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp b/gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp new file mode 100644 index 0000000000..1946baafe6 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestPointerEventsConsumable.cpp @@ -0,0 +1,500 @@ +/* -*- 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 "gtest/gtest.h" + +#include "APZCTreeManagerTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" +#include "apz/src/AsyncPanZoomController.h" +#include "apz/src/InputBlockState.h" +#include "apz/src/OverscrollHandoffState.h" +#include "mozilla/layers/IAPZCTreeManager.h" + +class APZCArePointerEventsConsumable : public APZCTreeManagerTester { + public: + APZCArePointerEventsConsumable() { CreateMockHitTester(); } + + void CreateSingleElementTree() { + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 500, 500)); + + registration = MakeUnique(LayersId{0}, mcc); + + UpdateHitTestingTree(); + + ApzcOf(root)->GetFrameMetrics().SetIsRootContent(true); + } + + void CreateScrollHandoffTree() { + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 200, 200), + LayerIntRect(50, 50, 100, 100)}; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 300, 300)); + SetScrollableFrameMetrics(layers[1], + ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 200, 200)); + SetScrollHandoff(layers[1], root); + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + + ApzcOf(root)->GetFrameMetrics().SetIsRootContent(true); + } + + RefPtr CreateTouchBlockStateForApzc( + const RefPtr& aApzc) { + TouchCounter counter{}; + TargetConfirmationFlags flags{true}; + + return new TouchBlockState(aApzc, flags, counter); + } + + void UpdateOverscrollBehavior(ScrollableLayerGuid::ViewID aScrollId, + OverscrollBehavior aX, OverscrollBehavior aY) { + auto* layer = layers[aScrollId - ScrollableLayerGuid::START_SCROLL_ID]; + ModifyFrameMetrics(layer, [aX, aY](ScrollMetadata& sm, FrameMetrics& _) { + OverscrollBehaviorInfo overscroll; + overscroll.mBehaviorX = aX; + overscroll.mBehaviorY = aY; + sm.SetOverscrollBehavior(overscroll); + }); + UpdateHitTestingTree(); + } + + UniquePtr registration; +}; + +TEST_F(APZCArePointerEventsConsumable, EmptyInput) { + CreateSingleElementTree(); + + RefPtr apzc = ApzcOf(root); + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + + MultiTouchInput touchInput = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + + const PointerEventsConsumableFlags expected{false, false}; + const PointerEventsConsumableFlags actual = + apzc->ArePointerEventsConsumable(blockState, touchInput); + EXPECT_EQ(expected, actual); +} + +TEST_F(APZCArePointerEventsConsumable, ScrollHorizontally) { + CreateSingleElementTree(); + + RefPtr apzc = ApzcOf(root); + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + + // Create touch with horizontal 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(10, 10), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(30, 10), ScreenSize(0, 0), 0, 0)); + + blockState->UpdateSlopState(touchStart, false); + + PointerEventsConsumableFlags actual{}; + PointerEventsConsumableFlags expected{}; + + // Scroll area 500x500, room to pan x, room to pan y + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Scroll area 100x100, no room to pan x, no room to pan y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100}); + expected = {false, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Scroll area 500x100, room to pan x, no room to pan y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 500, 100}); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Scroll area 100x500, no room to pan x, room to pan y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 500}); + expected = {false, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); +} + +TEST_F(APZCArePointerEventsConsumable, ScrollVertically) { + CreateSingleElementTree(); + + RefPtr apzc = ApzcOf(root); + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(10, 10), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(10, 30), ScreenSize(0, 0), 0, 0)); + + blockState->UpdateSlopState(touchStart, false); + + PointerEventsConsumableFlags actual{}; + PointerEventsConsumableFlags expected{}; + + // Scroll area 500x500, room to pan x, room to pan y + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Scroll area 100x100, no room to pan x, no room to pan y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100}); + expected = {false, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Scroll area 500x100, room to pan x, no room to pan y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 500, 100}); + expected = {false, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Scroll area 100x500, no room to pan x, room to pan y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 500}); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); +} + +TEST_F(APZCArePointerEventsConsumable, NestedElementCanScroll) { + CreateScrollHandoffTree(); + + RefPtr apzc = ApzcOf(layers[1]); + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0)); + + blockState->UpdateSlopState(touchStart, false); + + const PointerEventsConsumableFlags expected{true, true}; + const PointerEventsConsumableFlags actual = + apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); +} + +TEST_F(APZCArePointerEventsConsumable, NestedElementCannotScroll) { + CreateScrollHandoffTree(); + + RefPtr apzc = ApzcOf(layers[1]); + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0)); + + blockState->UpdateSlopState(touchStart, false); + + PointerEventsConsumableFlags actual{}; + PointerEventsConsumableFlags expected{}; + + // Set the nested element to have no room to scroll. + // Because of the overscroll handoff, we still have room to scroll + // in the parent element. + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100}); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Set overscroll handoff for the nested element to none. + // Because no handoff will happen, we are not able to use the parent's + // room to scroll. + // Bug 1814886: Once fixed, change expected value to {false, true}. + UpdateOverscrollBehavior(ScrollableLayerGuid::START_SCROLL_ID + 1, + OverscrollBehavior::None, OverscrollBehavior::None); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); +} + +TEST_F(APZCArePointerEventsConsumable, NotScrollableButZoomable) { + CreateSingleElementTree(); + + RefPtr apzc = ApzcOf(root); + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0)); + + blockState->UpdateSlopState(touchStart, false); + + // Make the root have no room to scroll + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100}); + + // Make zoomable + apzc->UpdateZoomConstraints(ZoomConstraints( + true, true, CSSToParentLayerScale(0.25f), CSSToParentLayerScale(4.0f))); + + PointerEventsConsumableFlags actual{}; + PointerEventsConsumableFlags expected{}; + + expected = {false, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + // Add a second touch point and therefore make the APZC consider + // zoom use cases as well. + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 90), ScreenSize(0, 0), 0, 0)); + + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); +} + +TEST_F(APZCArePointerEventsConsumable, TouchActionsProhibitAll) { + CreateSingleElementTree(); + + RefPtr apzc = ApzcOf(root); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0)); + + PointerEventsConsumableFlags expected{}; + PointerEventsConsumableFlags actual{}; + + { + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE}); + expected = {true, false}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } + + // Convert touch input to two-finger pinch + touchStart.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(80, 80), ScreenSize(0, 0), 0, 0)); + touchMove.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(90, 90), ScreenSize(0, 0), 0, 0)); + + { + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE}); + expected = {true, false}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } +} + +TEST_F(APZCArePointerEventsConsumable, TouchActionsAllowVerticalScrolling) { + CreateSingleElementTree(); + + RefPtr apzc = ApzcOf(root); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 80), ScreenSize(0, 0), 0, 0)); + + PointerEventsConsumableFlags expected{}; + PointerEventsConsumableFlags actual{}; + + { + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE}); + expected = {true, false}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } + + { + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::VERTICAL_PAN}); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } +} + +TEST_F(APZCArePointerEventsConsumable, TouchActionsAllowHorizontalScrolling) { + CreateSingleElementTree(); + + RefPtr apzc = ApzcOf(root); + + // Create touch with horizontal 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(80, 60), ScreenSize(0, 0), 0, 0)); + + PointerEventsConsumableFlags expected{}; + PointerEventsConsumableFlags actual{}; + + { + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE}); + expected = {true, false}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } + + { + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors( + {AllowedTouchBehavior::HORIZONTAL_PAN}); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } +} + +TEST_F(APZCArePointerEventsConsumable, TouchActionsAllowPinchZoom) { + CreateSingleElementTree(); + + RefPtr apzc = ApzcOf(root); + + // Create two-finger pinch + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 60), ScreenSize(0, 0), 0, 0)); + touchStart.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(80, 80), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(50, 50), ScreenSize(0, 0), 0, 0)); + touchMove.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(90, 90), ScreenSize(0, 0), 0, 0)); + + PointerEventsConsumableFlags expected{}; + PointerEventsConsumableFlags actual{}; + + { + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::NONE}); + expected = {true, false}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } + + { + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + blockState->UpdateSlopState(touchStart, false); + + blockState->SetAllowedTouchBehaviors({AllowedTouchBehavior::PINCH_ZOOM}); + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + } +} + +TEST_F(APZCArePointerEventsConsumable, DynamicToolbar) { + CreateSingleElementTree(); + + RefPtr apzc = ApzcOf(root); + RefPtr blockState = CreateTouchBlockStateForApzc(apzc); + + // Create touch with vertical 20 unit scroll + MultiTouchInput touchStart = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + touchStart.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 30), ScreenSize(0, 0), 0, 0)); + + MultiTouchInput touchMove = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, mcc->Time()); + touchMove.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(60, 40), ScreenSize(0, 0), 0, 0)); + + blockState->UpdateSlopState(touchStart, false); + + // Restrict size of scrollable area: No room to pan X, no room to pan Y + apzc->GetFrameMetrics().SetScrollableRect(CSSRect{0, 0, 100, 100}); + + PointerEventsConsumableFlags actual{}; + PointerEventsConsumableFlags expected{}; + + expected = {false, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); + + apzc->GetFrameMetrics().SetCompositionSizeWithoutDynamicToolbar( + ParentLayerSize{100, 90}); + UpdateHitTestingTree(); + + expected = {true, true}; + actual = apzc->ArePointerEventsConsumable(blockState, touchMove); + EXPECT_EQ(expected, actual); +} diff --git a/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp b/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp new file mode 100644 index 0000000000..8f497dabe6 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestScrollHandoff.cpp @@ -0,0 +1,809 @@ +/* -*- 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 "APZCTreeManagerTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" + +class APZScrollHandoffTester : public APZCTreeManagerTester { + protected: + UniquePtr registration; + TestAsyncPanZoomController* rootApzc; + + void CreateScrollHandoffLayerTree1() { + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 50, 100, 50)}; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 200, 200)); + SetScrollableFrameMetrics(layers[1], + ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 100)); + SetScrollHandoff(layers[1], root); + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetFrameMetrics().SetIsRootContent( + true); // make root APZC zoomable + } + + void CreateScrollHandoffLayerTree2() { + const char* treeShape = "x(x(x))"; + LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 50, 100, 50)}; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 200, 200)); + SetScrollableFrameMetrics(layers[1], + ScrollableLayerGuid::START_SCROLL_ID + 2, + CSSRect(-100, -100, 200, 200)); + SetScrollableFrameMetrics(layers[2], + ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 100)); + SetScrollHandoff(layers[1], root); + SetScrollHandoff(layers[2], layers[1]); + // No ScopedLayerTreeRegistration as that just needs to be done once per + // test and this is the second layer tree for a particular test. + MOZ_ASSERT(registration); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + } + + void CreateScrollHandoffLayerTree3() { + const char* treeShape = "x(x(x)x(x))"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), // root + LayerIntRect(0, 0, 100, 50), // scrolling parent 1 + LayerIntRect(0, 0, 100, 50), // scrolling child 1 + LayerIntRect(0, 50, 100, 50), // scrolling parent 2 + LayerIntRect(0, 50, 100, 50) // scrolling child 2 + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 100)); + SetScrollableFrameMetrics(layers[1], + ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 100)); + SetScrollableFrameMetrics(layers[2], + ScrollableLayerGuid::START_SCROLL_ID + 2, + CSSRect(0, 0, 100, 100)); + SetScrollableFrameMetrics(layers[3], + ScrollableLayerGuid::START_SCROLL_ID + 3, + CSSRect(0, 50, 100, 100)); + SetScrollableFrameMetrics(layers[4], + ScrollableLayerGuid::START_SCROLL_ID + 4, + CSSRect(0, 50, 100, 100)); + SetScrollHandoff(layers[1], layers[0]); + SetScrollHandoff(layers[3], layers[0]); + SetScrollHandoff(layers[2], layers[1]); + SetScrollHandoff(layers[4], layers[3]); + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + } + + // Creates a layer tree with a parent layer that is only scrollable + // horizontally, and a child layer that is only scrollable vertically. + void CreateScrollHandoffLayerTree4() { + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 100)}; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 200, 100)); + SetScrollableFrameMetrics(layers[1], + ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 200)); + SetScrollHandoff(layers[1], root); + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + } + + // Creates a layer tree with a parent layer that is not scrollable, and a + // child layer that is only scrollable vertically. + void CreateScrollHandoffLayerTree5() { + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), // scrolling parent + LayerIntRect(0, 50, 100, 50) // scrolling child + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 100)); + SetScrollableFrameMetrics(layers[1], + ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 200)); + SetScrollHandoff(layers[1], root); + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + } + + void CreateScrollgrabLayerTree(bool makeParentScrollable = true) { + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), // scroll-grabbing parent + LayerIntRect(0, 20, 100, 80) // child + }; + CreateScrollData(treeShape, layerVisibleRegion); + float parentHeight = makeParentScrollable ? 120 : 100; + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, parentHeight)); + SetScrollableFrameMetrics(layers[1], + ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 800)); + SetScrollHandoff(layers[1], root); + registration = MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + rootApzc = ApzcOf(root); + rootApzc->GetScrollMetadata().SetHasScrollgrab(true); + } + + void TestFlingAcceleration() { + // Jack up the fling acceleration multiplier so we can easily determine + // whether acceleration occured. + const float kAcceleration = 100.0f; + SCOPED_GFX_PREF_FLOAT("apz.fling_accel_base_mult", kAcceleration); + SCOPED_GFX_PREF_FLOAT("apz.fling_accel_min_fling_velocity", 0.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_accel_min_pan_velocity", 0.0); + + RefPtr childApzc = ApzcOf(layers[1]); + + // Pan once, enough to fully scroll the scrollgrab parent and then scroll + // and fling the child. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Pan(manager, 70, 40); + + // Give the fling animation a chance to start. + SampleAnimationsOnce(); + + float childVelocityAfterFling1 = childApzc->GetVelocityVector().y; + + // Pan again. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Pan(manager, 70, 40); + + // Give the fling animation a chance to start. + // This time it should be accelerated. + SampleAnimationsOnce(); + + float childVelocityAfterFling2 = childApzc->GetVelocityVector().y; + + // We should have accelerated once. + // The division by 2 is to account for friction. + EXPECT_GT(childVelocityAfterFling2, + childVelocityAfterFling1 * kAcceleration / 2); + + // We should not have accelerated twice. + // The division by 4 is to account for friction. + EXPECT_LE(childVelocityAfterFling2, + childVelocityAfterFling1 * kAcceleration * kAcceleration / 4); + } + + void TestCrossApzcAxisLock() { + SCOPED_GFX_PREF_INT("apz.axis_lock.mode", 1); + + CreateScrollHandoffLayerTree1(); + + RefPtr childApzc = ApzcOf(layers[1]); + Pan(childApzc, ScreenIntPoint(10, 60), ScreenIntPoint(15, 90), + PanOptions::KeepFingerDown | PanOptions::ExactCoordinates); + + childApzc->AssertAxisLocked(ScrollDirection::eVertical); + childApzc->AssertStateIsPanningLockedY(); + } +}; + +class APZScrollHandoffTesterMock : public APZScrollHandoffTester { + public: + APZScrollHandoffTesterMock() { CreateMockHitTester(); } +}; + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +// Here we test that if the processing of a touch block is deferred while we +// wait for content to send a prevent-default message, overscroll is still +// handed off correctly when the block is processed. +TEST_F(APZScrollHandoffTester, DeferredInputEventProcessing) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); + + // Set up the APZC tree. + CreateScrollHandoffLayerTree1(); + + RefPtr childApzc = ApzcOf(layers[1]); + + // Enable touch-listeners so that we can separate the queueing of input + // events from them being processed. + childApzc->SetWaitForMainThread(); + + // Queue input events for a pan. + uint64_t blockId = 0; + Pan(childApzc, 90, 30, PanOptions::NoFling, nullptr, nullptr, &blockId); + + // Allow the pan to be processed. + childApzc->ContentReceivedInputBlock(blockId, false); + childApzc->ConfirmTarget(blockId); + + // Make sure overscroll was handed off correctly. + EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y); + EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +// Here we test that if the layer structure changes in between two input +// blocks being queued, and the first block is only processed after the second +// one has been queued, overscroll handoff for the first block follows +// the original layer structure while overscroll handoff for the second block +// follows the new layer structure. +TEST_F(APZScrollHandoffTester, LayerStructureChangesWhileEventsArePending) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); + + // Set up an initial APZC tree. + CreateScrollHandoffLayerTree1(); + + RefPtr childApzc = ApzcOf(layers[1]); + + // Enable touch-listeners so that we can separate the queueing of input + // events from them being processed. + childApzc->SetWaitForMainThread(); + + // Queue input events for a pan. + uint64_t blockId = 0; + Pan(childApzc, 90, 30, PanOptions::NoFling, nullptr, nullptr, &blockId); + + // Modify the APZC tree to insert a new APZC 'middle' into the handoff chain + // between the child and the root. + CreateScrollHandoffLayerTree2(); + WebRenderLayerScrollData* middle = layers[1]; + childApzc->SetWaitForMainThread(); + TestAsyncPanZoomController* middleApzc = ApzcOf(middle); + + // Queue input events for another pan. + uint64_t secondBlockId = 0; + Pan(childApzc, 30, 90, PanOptions::NoFling, nullptr, nullptr, &secondBlockId); + + // Allow the first pan to be processed. + childApzc->ContentReceivedInputBlock(blockId, false); + childApzc->ConfirmTarget(blockId); + + // Make sure things have scrolled according to the handoff chain in + // place at the time the touch-start of the first pan was queued. + EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y); + EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y); + EXPECT_EQ(0, middleApzc->GetFrameMetrics().GetVisualScrollOffset().y); + + // Allow the second pan to be processed. + childApzc->ContentReceivedInputBlock(secondBlockId, false); + childApzc->ConfirmTarget(secondBlockId); + + // Make sure things have scrolled according to the handoff chain in + // place at the time the touch-start of the second pan was queued. + EXPECT_EQ(0, childApzc->GetFrameMetrics().GetVisualScrollOffset().y); + EXPECT_EQ(10, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y); + EXPECT_EQ(-10, middleApzc->GetFrameMetrics().GetVisualScrollOffset().y); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +// Test that putting a second finger down on an APZC while a down-chain APZC +// is overscrolled doesn't result in being stuck in overscroll. +TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1073250) { + // Enable overscrolling. + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + CreateScrollHandoffLayerTree1(); + + TestAsyncPanZoomController* child = ApzcOf(layers[1]); + + // Pan, causing the parent APZC to overscroll. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + Pan(manager, 10, 40, PanOptions::KeepFingerDown); + EXPECT_FALSE(child->IsOverscrolled()); + EXPECT_TRUE(rootApzc->IsOverscrolled()); + + // Put a second finger down. + MultiTouchInput secondFingerDown = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + // Use the same touch identifier for the first touch (0) as Pan(). (A bit + // hacky.) + secondFingerDown.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(10, 40), ScreenSize(0, 0), 0, 0)); + secondFingerDown.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 20), ScreenSize(0, 0), 0, 0)); + manager->ReceiveInputEvent(secondFingerDown); + + // Release the fingers. + MultiTouchInput fingersUp = secondFingerDown; + fingersUp.mType = MultiTouchInput::MULTITOUCH_END; + manager->ReceiveInputEvent(fingersUp); + + // Allow any animations to run their course. + child->AdvanceAnimationsUntilEnd(); + rootApzc->AdvanceAnimationsUntilEnd(); + + // Make sure nothing is overscrolled. + EXPECT_FALSE(child->IsOverscrolled()); + EXPECT_FALSE(rootApzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +// This is almost exactly like StuckInOverscroll_Bug1073250, except the +// APZC receiving the input events for the first touch block is the child +// (and thus not the same APZC that overscrolls, which is the parent). +TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1231228) { + // Enable overscrolling. + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + CreateScrollHandoffLayerTree1(); + + TestAsyncPanZoomController* child = ApzcOf(layers[1]); + + // Pan, causing the parent APZC to overscroll. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Pan(manager, 60, 90, PanOptions::KeepFingerDown); + EXPECT_FALSE(child->IsOverscrolled()); + EXPECT_TRUE(rootApzc->IsOverscrolled()); + + // Put a second finger down. + MultiTouchInput secondFingerDown = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + // Use the same touch identifier for the first touch (0) as Pan(). (A bit + // hacky.) + secondFingerDown.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(10, 40), ScreenSize(0, 0), 0, 0)); + secondFingerDown.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 20), ScreenSize(0, 0), 0, 0)); + manager->ReceiveInputEvent(secondFingerDown); + + // Release the fingers. + MultiTouchInput fingersUp = secondFingerDown; + fingersUp.mType = MultiTouchInput::MULTITOUCH_END; + manager->ReceiveInputEvent(fingersUp); + + // Allow any animations to run their course. + child->AdvanceAnimationsUntilEnd(); + rootApzc->AdvanceAnimationsUntilEnd(); + + // Make sure nothing is overscrolled. + EXPECT_FALSE(child->IsOverscrolled()); + EXPECT_FALSE(rootApzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZScrollHandoffTester, StuckInOverscroll_Bug1240202a) { + // Enable overscrolling. + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + CreateScrollHandoffLayerTree1(); + + TestAsyncPanZoomController* child = ApzcOf(layers[1]); + + // Pan, causing the parent APZC to overscroll. + Pan(manager, 60, 90, PanOptions::KeepFingerDown); + EXPECT_FALSE(child->IsOverscrolled()); + EXPECT_TRUE(rootApzc->IsOverscrolled()); + + // Lift the finger, triggering an overscroll animation + // (but don't allow it to run). + TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time()); + + // Put the finger down again, interrupting the animation + // and entering the TOUCHING state. + TouchDown(manager, ScreenIntPoint(10, 90), mcc->Time()); + + // Lift the finger once again. + TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time()); + + // Allow any animations to run their course. + child->AdvanceAnimationsUntilEnd(); + rootApzc->AdvanceAnimationsUntilEnd(); + + // Make sure nothing is overscrolled. + EXPECT_FALSE(child->IsOverscrolled()); + EXPECT_FALSE(rootApzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZScrollHandoffTesterMock, StuckInOverscroll_Bug1240202b) { + // Enable overscrolling. + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + CreateScrollHandoffLayerTree1(); + + TestAsyncPanZoomController* child = ApzcOf(layers[1]); + + // Pan, causing the parent APZC to overscroll. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Pan(manager, 60, 90, PanOptions::KeepFingerDown); + EXPECT_FALSE(child->IsOverscrolled()); + EXPECT_TRUE(rootApzc->IsOverscrolled()); + + // Lift the finger, triggering an overscroll animation + // (but don't allow it to run). + TouchUp(manager, ScreenIntPoint(10, 90), mcc->Time()); + + // Put the finger down again, interrupting the animation + // and entering the TOUCHING state. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + TouchDown(manager, ScreenIntPoint(10, 90), mcc->Time()); + + // Put a second finger down. Since we're in the TOUCHING state, + // the "are we panned into overscroll" check will fail and we + // will not ignore the second finger, instead entering the + // PINCHING state. + MultiTouchInput secondFingerDown(MultiTouchInput::MULTITOUCH_START, 0, + TimeStamp(), 0); + // Use the same touch identifier for the first touch (0) as TouchDown(). (A + // bit hacky.) + secondFingerDown.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(10, 90), ScreenSize(0, 0), 0, 0)); + secondFingerDown.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(10, 80), ScreenSize(0, 0), 0, 0)); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + manager->ReceiveInputEvent(secondFingerDown); + + // Release the fingers. + MultiTouchInput fingersUp = secondFingerDown; + fingersUp.mType = MultiTouchInput::MULTITOUCH_END; + manager->ReceiveInputEvent(fingersUp); + + // Allow any animations to run their course. + child->AdvanceAnimationsUntilEnd(); + rootApzc->AdvanceAnimationsUntilEnd(); + + // Make sure nothing is overscrolled. + EXPECT_FALSE(child->IsOverscrolled()); + EXPECT_FALSE(rootApzc->IsOverscrolled()); +} +#endif + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZScrollHandoffTester, OpposingConstrainedAxes_Bug1201098) { + // Enable overscrolling. + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + CreateScrollHandoffLayerTree4(); + + RefPtr childApzc = ApzcOf(layers[1]); + + // Pan, causing the child APZC to overscroll. + Pan(childApzc, 50, 60); + + // Make sure only the child is overscrolled. + EXPECT_TRUE(childApzc->IsOverscrolled()); + EXPECT_FALSE(rootApzc->IsOverscrolled()); +} +#endif + +// Test that flinging in a direction where one component of the fling goes into +// overscroll but the other doesn't, results in just the one component being +// handed off to the parent, while the original APZC continues flinging in the +// other direction. +TEST_F(APZScrollHandoffTesterMock, PartialFlingHandoff) { + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + CreateScrollHandoffLayerTree1(); + + // Fling up and to the left. The child APZC has room to scroll up, but not + // to the left, so the horizontal component of the fling should be handed + // off to the parent APZC. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Pan(manager, ScreenIntPoint(90, 90), ScreenIntPoint(55, 55)); + + RefPtr parent = ApzcOf(layers[0]); + RefPtr child = ApzcOf(layers[1]); + + // Advance the child's fling animation once to give the partial handoff + // a chance to occur. + mcc->AdvanceByMillis(10); + child->AdvanceAnimations(mcc->GetSampleTime()); + + // Assert that partial handoff has occurred. + child->AssertStateIsFling(); + parent->AssertStateIsFling(); +} + +// Here we test that if two flings are happening simultaneously, overscroll +// is handed off correctly for each. +TEST_F(APZScrollHandoffTester, SimultaneousFlings) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + // Set up an initial APZC tree. + CreateScrollHandoffLayerTree3(); + + RefPtr parent1 = ApzcOf(layers[1]); + RefPtr child1 = ApzcOf(layers[2]); + RefPtr parent2 = ApzcOf(layers[3]); + RefPtr child2 = ApzcOf(layers[4]); + + // Pan on the lower child. + Pan(child2, 45, 5); + + // Pan on the upper child. + Pan(child1, 95, 55); + + // Check that child1 and child2 are in a FLING state. + child1->AssertStateIsFling(); + child2->AssertStateIsFling(); + + // Advance the animations on child1 and child2 until their end. + child1->AdvanceAnimationsUntilEnd(); + child2->AdvanceAnimationsUntilEnd(); + + // Check that the flings have been handed off to the parents. + child1->AssertStateIsReset(); + parent1->AssertStateIsFling(); + child2->AssertStateIsReset(); + parent2->AssertStateIsFling(); +} + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZScrollHandoffTester, Scrollgrab) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); + + // Set up the layer tree + CreateScrollgrabLayerTree(); + + RefPtr childApzc = ApzcOf(layers[1]); + + // Pan on the child, enough to fully scroll the scrollgrab parent (20 px) + // and leave some more (another 15 px) for the child. + Pan(childApzc, 80, 45); + + // Check that the parent and child have scrolled as much as we expect. + EXPECT_EQ(20, rootApzc->GetFrameMetrics().GetVisualScrollOffset().y); + EXPECT_EQ(15, childApzc->GetFrameMetrics().GetVisualScrollOffset().y); +} +#endif + +TEST_F(APZScrollHandoffTester, ScrollgrabFling) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + // Set up the layer tree + CreateScrollgrabLayerTree(); + + RefPtr childApzc = ApzcOf(layers[1]); + + // Pan on the child, not enough to fully scroll the scrollgrab parent. + Pan(childApzc, 80, 70); + + // Check that it is the scrollgrab parent that's in a fling, not the child. + rootApzc->AssertStateIsFling(); + childApzc->AssertStateIsReset(); +} + +TEST_F(APZScrollHandoffTesterMock, ScrollgrabFlingAcceleration1) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + CreateScrollgrabLayerTree(true /* make parent scrollable */); + + // Note: Usually, fling acceleration does not work across handoff, because our + // fling acceleration code does not propagate the "fling cancel velocity" + // across handoff. However, this test sets apz.fling_min_velocity_threshold to + // zero, so the "fling cancel velocity" is allowed to be zero, and fling + // acceleration succeeds, almost by accident. + TestFlingAcceleration(); +} + +TEST_F(APZScrollHandoffTesterMock, ScrollgrabFlingAcceleration2) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", true); + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + CreateScrollgrabLayerTree(false /* do not make parent scrollable */); + TestFlingAcceleration(); +} + +TEST_F(APZScrollHandoffTester, ImmediateHandoffDisallowed_Pan) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", false); + + CreateScrollHandoffLayerTree1(); + + RefPtr parentApzc = ApzcOf(layers[0]); + RefPtr childApzc = ApzcOf(layers[1]); + + // Pan on the child, enough to scroll it to its end and have scroll + // left to hand off. Since immediate handoff is disallowed, we expect + // the leftover scroll not to be handed off. + Pan(childApzc, 60, 5); + + // Verify that the parent has not scrolled. + EXPECT_EQ(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y); + EXPECT_EQ(0, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y); + + // Pan again on the child. This time, since the child was scrolled to + // its end when the gesture began, we expect the scroll to be handed off. + Pan(childApzc, 60, 50); + + // Verify that the parent scrolled. + EXPECT_EQ(10, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y); +} + +TEST_F(APZScrollHandoffTester, ImmediateHandoffDisallowed_Fling) { + SCOPED_GFX_PREF_BOOL("apz.allow_immediate_handoff", false); + SCOPED_GFX_PREF_FLOAT("apz.fling_min_velocity_threshold", 0.0f); + + CreateScrollHandoffLayerTree1(); + + RefPtr parentApzc = ApzcOf(layers[0]); + RefPtr childApzc = ApzcOf(layers[1]); + + // Pan on the child, enough to get very close to the end, so that the + // subsequent fling reaches the end and has leftover velocity to hand off. + Pan(childApzc, 60, 2); + + // Allow the fling to run its course. + childApzc->AdvanceAnimationsUntilEnd(); + parentApzc->AdvanceAnimationsUntilEnd(); + + // Verify that the parent has not scrolled. + // The first comparison needs to be an ASSERT_NEAR because the fling + // computations are such that the final scroll position can be within + // COORDINATE_EPSILON of the end rather than right at the end. + ASSERT_NEAR(50, childApzc->GetFrameMetrics().GetVisualScrollOffset().y, + COORDINATE_EPSILON); + EXPECT_EQ(0, parentApzc->GetFrameMetrics().GetVisualScrollOffset().y); + + // Pan again on the child. This time, since the child was scrolled to + // its end when the gesture began, we expect the scroll to be handed off. + Pan(childApzc, 60, 40); + + // Allow the fling to run its course. The fling should also be handed off. + childApzc->AdvanceAnimationsUntilEnd(); + parentApzc->AdvanceAnimationsUntilEnd(); + + // Verify that the parent scrolled from the fling. + EXPECT_GT(parentApzc->GetFrameMetrics().GetVisualScrollOffset().y, 10); +} + +TEST_F(APZScrollHandoffTester, CrossApzcAxisLock_TouchAction) { + TestCrossApzcAxisLock(); +} + +TEST_F(APZScrollHandoffTesterMock, WheelHandoffAfterDirectionReversal) { + // Explicitly set the wheel transaction timeout pref because the test relies + // on its value. + SCOPED_GFX_PREF_INT("mousewheel.transaction.timeout", 1500); + + // Set up a basic scroll handoff layer tree. + CreateScrollHandoffLayerTree1(); + + rootApzc = ApzcOf(layers[0]); + RefPtr childApzc = ApzcOf(layers[1]); + FrameMetrics& rootMetrics = rootApzc->GetFrameMetrics(); + FrameMetrics& childMetrics = childApzc->GetFrameMetrics(); + CSSRect childScrollRange = childMetrics.CalculateScrollRange(); + + EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y); + EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y); + + ScreenIntPoint cursorLocation(10, 60); // positioned to hit the subframe + ScreenPoint upwardDelta(0, -10); + ScreenPoint downwardDelta(0, 10); + + // First wheel upwards. This will have no effect because we're already + // scrolled to the top. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Wheel(manager, cursorLocation, upwardDelta, mcc->Time()); + EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y); + EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y); + + // Now wheel downwards 6 times. This should scroll the child, and get it + // to the bottom of its 50px scroll range. + for (size_t i = 0; i < 6; ++i) { + mcc->AdvanceByMillis(100); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Wheel(manager, cursorLocation, downwardDelta, mcc->Time()); + } + EXPECT_EQ(0, rootMetrics.GetVisualScrollOffset().y); + EXPECT_EQ(childScrollRange.YMost(), childMetrics.GetVisualScrollOffset().y); + + // Wheel downwards an additional 16 times, with 100ms increments. + // This should be enough to overcome the 1500ms wheel transaction timeout + // and start scrolling the root. + for (size_t i = 0; i < 16; ++i) { + mcc->AdvanceByMillis(100); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Wheel(manager, cursorLocation, downwardDelta, mcc->Time()); + } + EXPECT_EQ(childScrollRange.YMost(), childMetrics.GetVisualScrollOffset().y); + EXPECT_GT(rootMetrics.GetVisualScrollOffset().y, 0); +} + +TEST_F(APZScrollHandoffTesterMock, WheelHandoffNonscrollable) { + // Set up a basic scroll layer tree. + CreateScrollHandoffLayerTree5(); + + RefPtr childApzc = ApzcOf(layers[1]); + FrameMetrics& childMetrics = childApzc->GetFrameMetrics(); + + EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y); + + ScreenPoint downwardDelta(0, 10); + // Positioned to hit the nonscrollable parent frame + ScreenIntPoint nonscrollableLocation(40, 10); + // Positioned to hit the scrollable subframe + ScreenIntPoint scrollableLocation(40, 60); + + // Start the wheel transaction on a nonscrollable parent frame. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + Wheel(manager, nonscrollableLocation, downwardDelta, mcc->Time()); + EXPECT_EQ(0, childMetrics.GetVisualScrollOffset().y); + + // Mouse moves to a scrollable subframe. This should end the transaction. + mcc->AdvanceByMillis(100); + MouseInput mouseInput(MouseInput::MOUSE_MOVE, + MouseInput::ButtonType::PRIMARY_BUTTON, 0, 0, + scrollableLocation, mcc->Time(), 0); + WidgetMouseEvent mouseEvent = mouseInput.ToWidgetEvent(nullptr); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + ((APZInputBridge*)manager.get())->ReceiveInputEvent(mouseEvent); + + // Wheel downward should scroll the subframe. + mcc->AdvanceByMillis(100); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + Wheel(manager, scrollableLocation, downwardDelta, mcc->Time()); + EXPECT_GT(childMetrics.GetVisualScrollOffset().y, 0); +} + +TEST_F(APZScrollHandoffTesterMock, ChildCloseToEndOfScrollRange) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + CreateScrollHandoffLayerTree1(); + + RefPtr childApzc = ApzcOf(layers[1]); + + FrameMetrics& rootMetrics = rootApzc->GetFrameMetrics(); + FrameMetrics& childMetrics = childApzc->GetFrameMetrics(); + + // Zoom the page in by 3x. This needs to be reflected in the zoom level + // and composition bounds of both APZCs. + rootMetrics.SetZoom(CSSToParentLayerScale(3.0)); + rootMetrics.SetCompositionBounds(ParentLayerRect(0, 0, 300, 300)); + childMetrics.SetZoom(CSSToParentLayerScale(3.0)); + childMetrics.SetCompositionBounds(ParentLayerRect(0, 150, 300, 150)); + + // Scroll the child APZC very close to the end of the scroll range. + // The scroll offset is chosen such that in CSS pixels it has 0.01 pixels + // room to scroll (less than COORDINATE_EPSILON = 0.02), but in ParentLayer + // pixels it has 0.03 pixels room (greater than COORDINATE_EPSILON). + childMetrics.SetVisualScrollOffset(CSSPoint(0, 49.99)); + + EXPECT_FALSE(childApzc->IsOverscrolled()); + + CSSPoint childBefore = childApzc->GetFrameMetrics().GetVisualScrollOffset(); + CSSPoint parentBefore = rootApzc->GetFrameMetrics().GetVisualScrollOffset(); + + // Synthesize a pan gesture that tries to scroll the child further down. + PanGesture(PanGestureInput::PANGESTURE_START, childApzc, + ScreenIntPoint(10, 20), ScreenPoint(0, 40), mcc->Time()); + mcc->AdvanceByMillis(5); + childApzc->AdvanceAnimations(mcc->GetSampleTime()); + + PanGesture(PanGestureInput::PANGESTURE_END, childApzc, ScreenIntPoint(10, 21), + ScreenPoint(0, 0), mcc->Time()); + + CSSPoint childAfter = childApzc->GetFrameMetrics().GetVisualScrollOffset(); + CSSPoint parentAfter = rootApzc->GetFrameMetrics().GetVisualScrollOffset(); + + bool childScrolled = (childBefore != childAfter); + bool parentScrolled = (parentBefore != parentAfter); + + // Check that either the child or the parent scrolled. + // (With the current implementation of comparing quantities to + // COORDINATE_EPSILON in CSS units, it will be the parent, but the important + // thing is that at least one of the child or parent scroll, i.e. we're not + // stuck in a situation where no scroll offset is changing). + EXPECT_TRUE(childScrolled || parentScrolled); +} diff --git a/gfx/layers/apz/test/gtest/TestSnapping.cpp b/gfx/layers/apz/test/gtest/TestSnapping.cpp new file mode 100644 index 0000000000..13bb1e5591 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestSnapping.cpp @@ -0,0 +1,305 @@ +/* -*- 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 "APZCTreeManagerTester.h" +#include "APZTestCommon.h" + +#include "InputUtils.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_mousewheel.h" + +class APZCSnappingTesterMock : public APZCTreeManagerTester { + public: + APZCSnappingTesterMock() { CreateMockHitTester(); } +}; + +TEST_F(APZCSnappingTesterMock, Bug1265510) { + const char* treeShape = "x(x)"; + LayerIntRegion layerVisibleRegion[] = {LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 100, 100, 100)}; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID + 1, + CSSRect(0, 0, 100, 200)); + SetScrollHandoff(layers[1], root); + + ScrollSnapInfo snap; + snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory; + snap.mSnapportSize = CSSSize::ToAppUnits( + layerVisibleRegion[0].GetBounds().Size() * LayerToCSSScale(1.0)); + + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(0 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 0, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{1})); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(100 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{2})); + + ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics&) { + aSm.SetSnapInfo(ScrollSnapInfo(snap)); + }); + + UniquePtr registration = + MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + + TestAsyncPanZoomController* outer = ApzcOf(layers[0]); + TestAsyncPanZoomController* inner = ApzcOf(layers[1]); + + // Position the mouse near the bottom of the outer frame and scroll by 60px. + // (6 lines of 10px each). APZC will actually scroll to y=100 because of the + // mandatory snap coordinate there. + TimeStamp now = mcc->Time(); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + SmoothWheel(manager, ScreenIntPoint(50, 80), ScreenPoint(0, 6), now); + // Advance in 5ms increments until we've scrolled by 70px. At this point, the + // closest snap point is y=100, and the inner frame should be under the mouse + // cursor. + while (outer + ->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting) + .y < 70) { + mcc->AdvanceByMillis(5); + outer->AdvanceAnimations(mcc->GetSampleTime()); + } + // Now do another wheel in a new transaction. This should start scrolling the + // inner frame; we verify that it does by checking the inner scroll position. + TimeStamp newTransactionTime = + now + TimeDuration::FromMilliseconds( + StaticPrefs::mousewheel_transaction_timeout() + 100); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1); + SmoothWheel(manager, ScreenIntPoint(50, 80), ScreenPoint(0, 6), + newTransactionTime); + inner->AdvanceAnimationsUntilEnd(); + EXPECT_LT( + 0.0f, + inner + ->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting) + .y); + + // However, the outer frame should also continue to the snap point, otherwise + // it is demonstrating incorrect behaviour by violating the mandatory + // snapping. + outer->AdvanceAnimationsUntilEnd(); + EXPECT_EQ( + 100.0f, + outer + ->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting) + .y); +} + +TEST_F(APZCSnappingTesterMock, Snap_After_Pinch) { + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 200)); + + // Set up some basic scroll snapping + ScrollSnapInfo snap; + snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory; + snap.mSnapportSize = CSSSize::ToAppUnits( + layerVisibleRegion[0].GetBounds().Size() * LayerToCSSScale(1.0)); + + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(0 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 0, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{1})); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(100 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{2})); + + // Save the scroll snap info on the root APZC. + // Also mark the root APZC as "root content", since APZC only allows + // zooming on the root content APZC. + ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aSm.SetSnapInfo(ScrollSnapInfo(snap)); + aMetrics.SetIsRootContent(true); + }); + + UniquePtr registration = + MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + + RefPtr apzc = ApzcOf(root); + + // Allow zooming + apzc->UpdateZoomConstraints(ZoomConstraints( + true, true, CSSToParentLayerScale(0.25f), CSSToParentLayerScale(4.0f))); + + PinchWithPinchInput(apzc, ScreenIntPoint(50, 50), ScreenIntPoint(50, 50), + 1.2f); + + apzc->AssertStateIsSmoothMsdScroll(); +} + +// Currently fails on Android because on the platform we have a different +// VelocityTracker. +#ifndef MOZ_WIDGET_ANDROID +TEST_F(APZCSnappingTesterMock, SnapOnPanEndWithZeroVelocity) { + // Use pref values for desktop everywhere. + SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0.002); + SCOPED_GFX_PREF_FLOAT("apz.fling_stopped_threshold", 0.01); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x1", 0.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x2", 1.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y1", 0.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y2", 1.0); + SCOPED_GFX_PREF_INT("apz.velocity_relevance_time_ms", 100); + + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 400)); + + // Set up two snap points, 30 and 100. + ScrollSnapInfo snap; + snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory; + snap.mSnapportSize = CSSSize::ToAppUnits( + layerVisibleRegion[0].GetBounds().Size() * LayerToCSSScale(1.0)); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(30 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 30, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{1})); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(100 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{2})); + + // Save the scroll snap info on the root APZC. + ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aSm.SetSnapInfo(ScrollSnapInfo(snap)); + }); + + UniquePtr registration = + MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + + RefPtr apzc = ApzcOf(root); + + // Send a series of pan gestures to scroll to position at 50. + const ScreenIntPoint position = ScreenIntPoint(50, 30); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, position, + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, position, + ScreenPoint(0, 40), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Make sure the velocity just before sending a pan-end is zero. + EXPECT_EQ(apzc->GetVelocityVector().y, 0); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, position, + ScreenPoint(0, 0), mcc->Time()); + + // Now a smooth animation has been triggered for snapping to 30. + apzc->AssertStateIsSmoothMsdScroll(); + + apzc->AdvanceAnimationsUntilEnd(); + // The snapped position should be 30 rather than 100 because it's the nearest + // snap point. + EXPECT_EQ( + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting) + .y, + 30); +} + +// Smililar to above SnapOnPanEndWithZeroVelocity but with positive velocity so +// that the snap position would be the one in the scrolling direction. +TEST_F(APZCSnappingTesterMock, SnapOnPanEndWithPositiveVelocity) { + // Use pref values for desktop everywhere. + SCOPED_GFX_PREF_FLOAT("apz.fling_friction", 0.002); + SCOPED_GFX_PREF_FLOAT("apz.fling_stopped_threshold", 0.01); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x1", 0.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_x2", 1.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y1", 0.0); + SCOPED_GFX_PREF_FLOAT("apz.fling_curve_function_y2", 1.0); + SCOPED_GFX_PREF_INT("apz.velocity_relevance_time_ms", 100); + + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 400)); + + // Set up two snap points, 30 and 100. + ScrollSnapInfo snap; + snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory; + snap.mSnapportSize = CSSSize::ToAppUnits( + layerVisibleRegion[0].GetBounds().Size() * LayerToCSSScale(1.0)); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(30 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 30, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{1})); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(100 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{2})); + + // Save the scroll snap info on the root APZC. + ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aSm.SetSnapInfo(ScrollSnapInfo(snap)); + }); + + UniquePtr registration = + MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + + RefPtr apzc = ApzcOf(root); + + // Send a series of pan gestures that a pan-end event happens at 65 + const ScreenIntPoint position = ScreenIntPoint(50, 30); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, position, + ScreenPoint(0, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, position, + ScreenPoint(0, 35), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, position, + ScreenPoint(0, 20), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + + // There should be positive velocity in this case. + EXPECT_GT(apzc->GetVelocityVector().y, 0); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, position, + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + + // A smooth animation has been triggered by the pan-end event above. + apzc->AssertStateIsSmoothMsdScroll(); + + apzc->AdvanceAnimationsUntilEnd(); + EXPECT_EQ( + apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::eForHitTesting) + .y, + 100); +} +#endif diff --git a/gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp b/gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp new file mode 100644 index 0000000000..02b1d6798c --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestSnappingOnMomentum.cpp @@ -0,0 +1,104 @@ +/* -*- 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 "APZCTreeManagerTester.h" +#include "APZTestCommon.h" + +#include "InputUtils.h" +#include "mozilla/StaticPrefs_layout.h" + +class APZCSnappingOnMomentumTesterMock : public APZCTreeManagerTester { + public: + APZCSnappingOnMomentumTesterMock() { CreateMockHitTester(); } +}; + +TEST_F(APZCSnappingOnMomentumTesterMock, Snap_On_Momentum) { + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 500)); + + // Set up some basic scroll snapping + ScrollSnapInfo snap; + snap.mScrollSnapStrictnessY = StyleScrollSnapStrictness::Mandatory; + snap.mSnapportSize = CSSSize::ToAppUnits( + layerVisibleRegion[0].GetBounds().Size() * LayerToCSSScale(1.0)); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(0 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 0, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{1})); + snap.mSnapTargets.AppendElement(ScrollSnapInfo::SnapTarget( + Nothing(), Some(100 * AppUnitsPerCSSPixel()), + CSSRect::ToAppUnits(CSSRect(0, 100, 10, 10)), StyleScrollSnapStop::Normal, + ScrollSnapTargetId{2})); + + ModifyFrameMetrics(root, [&](ScrollMetadata& aSm, FrameMetrics&) { + aSm.SetSnapInfo(ScrollSnapInfo(snap)); + }); + + UniquePtr registration = + MakeUnique(LayersId{0}, mcc); + UpdateHitTestingTree(); + + RefPtr apzc = ApzcOf(root); + + TimeStamp now = mcc->Time(); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 80), + ScreenPoint(0, 2), now); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, ScreenIntPoint(50, 80), + ScreenPoint(0, 25), mcc->Time()); + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, manager, ScreenIntPoint(50, 80), + ScreenPoint(0, 25), mcc->Time()); + + // The velocity should be positive when panning with positive displacement. + EXPECT_GT(apzc->GetVelocityVector().y, 3.0); + + mcc->AdvanceByMillis(5); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 80), + ScreenPoint(0, 0), mcc->Time()); + + // After lifting the fingers, the velocity should be zero and a smooth + // animation should have been triggered for scroll snap. + EXPECT_EQ(apzc->GetVelocityVector().y, 0); + apzc->AssertStateIsSmoothMsdScroll(); + + mcc->AdvanceByMillis(5); + + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager, + ScreenIntPoint(50, 80), ScreenPoint(0, 200), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + ScreenIntPoint(50, 80), ScreenPoint(0, 50), mcc->Time()); + mcc->AdvanceByMillis(10); + apzc->AdvanceAnimations(mcc->GetSampleTime()); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, manager, + ScreenIntPoint(50, 80), ScreenPoint(0, 0), mcc->Time()); + + apzc->AdvanceAnimationsUntilEnd(); + EXPECT_EQ( + 100.0f, + apzc->GetCurrentAsyncScrollOffset( + AsyncPanZoomController::AsyncTransformConsumer::eForHitTesting) + .y); +} diff --git a/gfx/layers/apz/test/gtest/TestTransformNotifications.cpp b/gfx/layers/apz/test/gtest/TestTransformNotifications.cpp new file mode 100644 index 0000000000..53b7aa297f --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestTransformNotifications.cpp @@ -0,0 +1,567 @@ +/* -*- 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 "APZCBasicTester.h" +#include "APZCTreeManagerTester.h" +#include "APZTestCommon.h" +#include "mozilla/layers/WebRenderScrollDataWrapper.h" +#include "apz/util/APZEventState.h" + +#include "InputUtils.h" + +class APZCTransformNotificationTester : public APZCTreeManagerTester { + public: + explicit APZCTransformNotificationTester() { CreateMockHitTester(); } + + UniquePtr mRegistration; + + RefPtr mRootApzc; + + void SetupBasicTest() { + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 500, 500)); + + mRegistration = MakeUnique(LayersId{0}, mcc); + + UpdateHitTestingTree(); + + mRootApzc = ApzcOf(root); + } + + void SetupNonScrollableTest() { + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 100, 100)); + + mRegistration = MakeUnique(LayersId{0}, mcc); + + UpdateHitTestingTree(); + + mRootApzc = ApzcOf(root); + + mRootApzc->GetFrameMetrics().SetIsRootContent(true); + } +}; + +TEST_F(APZCTransformNotificationTester, PanningTransformNotifications) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + SetupBasicTest(); + + // Scroll down by 25 px. Ensure we only get one set of + // state change notifications. + // + // Then, scroll back up by 20px, this time flinging after. + // The fling should cover the remaining 5 px of room to scroll, then + // go into overscroll, and finally snap-back to recover from overscroll. + // Again, ensure we only get one set of state change notifications for + // this entire procedure. + + MockFunction check; + { + InSequence s; + EXPECT_CALL(check, Call("Simple pan")); + EXPECT_CALL( + *mcc, NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eStartTouch, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eStartPanning, _, _)) + .Times(1); + EXPECT_CALL(*mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eEndTouch, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + EXPECT_CALL(check, Call("Complex pan")); + EXPECT_CALL( + *mcc, NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eStartTouch, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eStartPanning, _, _)) + .Times(1); + EXPECT_CALL(*mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eEndTouch, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Simple pan"); + Pan(mRootApzc, 50, 25, PanOptions::NoFling); + check.Call("Complex pan"); + Pan(mRootApzc, 25, 45); + mRootApzc->AdvanceAnimationsUntilEnd(); + check.Call("Done"); +} + +TEST_F(APZCTransformNotificationTester, PanWithMomentumTransformNotifications) { + SetupBasicTest(); + + MockFunction check; + { + InSequence s; + EXPECT_CALL(check, Call("Pan Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Panning")); + EXPECT_CALL(check, Call("Pan End")); + EXPECT_CALL(check, Call("Momentum Start")); + + EXPECT_CALL(check, Call("Momentum Pan")); + EXPECT_CALL(check, Call("Momentum End")); + // The TransformEnd should only be sent after the momentum pan. + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Pan Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Panning"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Pan End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Momentum Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMSTART, manager, + ScreenIntPoint(50, 50), ScreenPoint(30, 90), mcc->Time()); + mcc->AdvanceByMillis(10); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Momentum Pan"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMPAN, manager, + ScreenIntPoint(50, 50), ScreenPoint(10, 30), mcc->Time()); + mcc->AdvanceByMillis(10); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Momentum End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_MOMENTUMEND, manager, + ScreenIntPoint(50, 50), ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(10); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Done"); +} + +TEST_F(APZCTransformNotificationTester, + PanWithoutMomentumTransformNotifications) { + // Ensure that the TransformEnd delay is 100ms. + SCOPED_GFX_PREF_INT("apz.scrollend-event.content.delay_ms", 100); + + SetupBasicTest(); + + MockFunction check; + { + InSequence s; + EXPECT_CALL(check, Call("Pan Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Panning")); + EXPECT_CALL(check, Call("Pan End")); + EXPECT_CALL(check, Call("TransformEnd delay")); + // The TransformEnd should only be sent after the pan gesture and 100ms + // timer fire. + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Pan Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Panning"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Pan End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(55); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("TransformEnd delay"); + mcc->AdvanceByMillis(55); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Done"); +} + +TEST_F(APZCTransformNotificationTester, + PanFollowedByNewPanTransformNotifications) { + // Ensure that the TransformEnd delay is 100ms. + SCOPED_GFX_PREF_INT("apz.scrollend-event.content.delay_ms", 100); + + SetupBasicTest(); + + MockFunction check; + { + InSequence s; + EXPECT_CALL(check, Call("Pan Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Panning")); + EXPECT_CALL(check, Call("Pan End")); + // The TransformEnd delay should be cut short and delivered before the + // new pan gesture begins. + EXPECT_CALL(check, Call("New Pan Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + EXPECT_CALL(check, Call("New Pan End")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Pan Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Panning"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Pan End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(55); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("New Pan Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("New Pan End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(105); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Done"); +} + +TEST_F(APZCTransformNotificationTester, + PanFollowedByWheelTransformNotifications) { + // Ensure that the TransformEnd delay is 100ms. + SCOPED_GFX_PREF_INT("apz.scrollend-event.content.delay_ms", 100); + + SetupBasicTest(); + + MockFunction check; + { + InSequence s; + EXPECT_CALL(check, Call("Pan Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Panning")); + EXPECT_CALL(check, Call("Pan End")); + // The TransformEnd delay should be cut short and delivered before the + // new wheel event begins. + EXPECT_CALL(check, Call("Wheel Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + EXPECT_CALL(check, Call("Wheel End")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Pan Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Panning"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50), + ScreenPoint(15, 30), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Pan End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(55); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Wheel Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + SmoothWheel(manager, ScreenIntPoint(50, 50), ScreenPoint(10, 10), + mcc->Time()); + mcc->AdvanceByMillis(10); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Wheel End"); + + mRootApzc->AdvanceAnimationsUntilEnd(); + + check.Call("Done"); +} + +#ifndef MOZ_WIDGET_ANDROID // Currently fails on Android +TEST_F(APZCTransformNotificationTester, PanOverscrollTransformNotifications) { + SCOPED_GFX_PREF_BOOL("apz.overscroll.enabled", true); + + SetupBasicTest(); + + MockFunction check; + { + InSequence s; + EXPECT_CALL(check, Call("Pan Start")); + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformBegin, _, _)) + .Times(1); + + EXPECT_CALL(check, Call("Panning Into Overscroll")); + EXPECT_CALL(check, Call("Pan End")); + EXPECT_CALL(check, Call("Overscroll Animation End")); + // The TransformEnd should only be sent after the overscroll animation + // completes. + EXPECT_CALL( + *mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eTransformEnd, _, _)) + .Times(1); + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Pan Start"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_START, manager, ScreenIntPoint(50, 50), + ScreenIntPoint(1, 2), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Panning Into Overscroll"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_PAN, mRootApzc, ScreenIntPoint(50, 50), + ScreenPoint(15, -30), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Ensure that we have overscrolled. + EXPECT_TRUE(mRootApzc->IsOverscrolled()); + + check.Call("Pan End"); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + PanGesture(PanGestureInput::PANGESTURE_END, manager, ScreenIntPoint(50, 50), + ScreenPoint(0, 0), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + // Wait for the overscroll animation to complete and the TransformEnd + // notification to be sent. + check.Call("Overscroll Animation End"); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimationsUntilEnd(); + EXPECT_FALSE(mRootApzc->IsOverscrolled()); + + check.Call("Done"); +} +#endif + +TEST_F(APZCTransformNotificationTester, ScrollableTouchStateChange) { + // Create a scroll frame with available space for a scroll. + SetupBasicTest(); + + MockFunction check; + { + EXPECT_CALL(check, Call("Start")); + // We receive a touch-start with the flag indicating that the + // touch-start occurred over a scrollable element. + EXPECT_CALL( + *mcc, NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eStartTouch, 1, _)) + .Times(1); + + EXPECT_CALL(*mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eEndTouch, 1, _)) + .Times(1); + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Start"); + + // Conduct a touch down and touch up in the scrollable element, + // and ensure the correct state change notifications are sent. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + TouchDown(mRootApzc, ScreenIntPoint(10, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + TouchUp(mRootApzc, ScreenIntPoint(10, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Done"); +} + +TEST_F(APZCTransformNotificationTester, NonScrollableTouchStateChange) { + // Create a non-scrollable frame with no space to scroll. + SetupNonScrollableTest(); + + MockFunction check; + { + EXPECT_CALL(check, Call("Start")); + // We receive a touch-start with the flag indicating that the + // touch-start occurred over a non-scrollable element. + EXPECT_CALL( + *mcc, NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eStartTouch, 0, _)) + .Times(1); + + EXPECT_CALL(*mcc, + NotifyAPZStateChange( + _, GeckoContentController::APZStateChange::eEndTouch, 1, _)) + .Times(1); + EXPECT_CALL(check, Call("Done")); + } + + check.Call("Start"); + + // Conduct a touch down and touch up in the non-scrollable element, + // and ensure the correct state change notifications are sent. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + TouchDown(mRootApzc, ScreenIntPoint(10, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID); + TouchUp(mRootApzc, ScreenIntPoint(10, 10), mcc->Time()); + mcc->AdvanceByMillis(5); + mRootApzc->AdvanceAnimations(mcc->GetSampleTime()); + + check.Call("Done"); +} diff --git a/gfx/layers/apz/test/gtest/TestTreeManager.cpp b/gfx/layers/apz/test/gtest/TestTreeManager.cpp new file mode 100644 index 0000000000..963a400cb8 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestTreeManager.cpp @@ -0,0 +1,347 @@ +/* -*- 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 "APZCTreeManagerTester.h" +#include "APZTestCommon.h" +#include "InputUtils.h" +#include "Units.h" + +class APZCTreeManagerGenericTester : public APZCTreeManagerTester { + protected: + void CreateSimpleScrollingLayer() { + const char* treeShape = "x"; + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 200, 200), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID, + CSSRect(0, 0, 500, 500)); + } + + void CreateSimpleMultiLayerTree() { + const char* treeShape = "x(xx)"; + // LayerID 0 12 + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 50), + LayerIntRect(0, 50, 100, 50), + }; + CreateScrollData(treeShape, layerVisibleRegion); + } + + void CreatePotentiallyLeakingTree() { + const char* treeShape = "x(x(x(x))x(x(x)))"; + // LayerID 0 1 2 3 4 5 6 + CreateScrollData(treeShape); + SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID); + SetScrollableFrameMetrics(layers[2], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollableFrameMetrics(layers[5], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollableFrameMetrics(layers[3], + ScrollableLayerGuid::START_SCROLL_ID + 2); + SetScrollableFrameMetrics(layers[6], + ScrollableLayerGuid::START_SCROLL_ID + 3); + } + + void CreateTwoLayerTree(int32_t aRootContentLayerIndex) { + const char* treeShape = "x(x)"; + // LayerID 0 1 + LayerIntRegion layerVisibleRegion[] = { + LayerIntRect(0, 0, 100, 100), + LayerIntRect(0, 0, 100, 100), + }; + CreateScrollData(treeShape, layerVisibleRegion); + SetScrollableFrameMetrics(layers[0], ScrollableLayerGuid::START_SCROLL_ID); + SetScrollableFrameMetrics(layers[1], + ScrollableLayerGuid::START_SCROLL_ID + 1); + SetScrollHandoff(layers[1], layers[0]); + + // Make layers[aRootContentLayerIndex] the root content + ModifyFrameMetrics(layers[aRootContentLayerIndex], + [](ScrollMetadata& sm, FrameMetrics& fm) { + fm.SetIsRootContent(true); + }); + } +}; + +TEST_F(APZCTreeManagerGenericTester, ScrollablePaintedLayers) { + CreateSimpleMultiLayerTree(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + + // both layers have the same scrollId + SetScrollableFrameMetrics(layers[1], ScrollableLayerGuid::START_SCROLL_ID); + SetScrollableFrameMetrics(layers[2], ScrollableLayerGuid::START_SCROLL_ID); + UpdateHitTestingTree(); + + TestAsyncPanZoomController* nullAPZC = nullptr; + // so they should have the same APZC + EXPECT_FALSE(HasScrollableFrameMetrics(layers[0])); + EXPECT_NE(nullAPZC, ApzcOf(layers[1])); + EXPECT_NE(nullAPZC, ApzcOf(layers[2])); + EXPECT_EQ(ApzcOf(layers[1]), ApzcOf(layers[2])); +} + +TEST_F(APZCTreeManagerGenericTester, Bug1068268) { + CreatePotentiallyLeakingTree(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + + UpdateHitTestingTree(); + RefPtr root = manager->GetRootNode(); + RefPtr node2 = root->GetFirstChild()->GetFirstChild(); + RefPtr node5 = root->GetLastChild()->GetLastChild(); + + EXPECT_EQ(ApzcOf(layers[2]), node5->GetApzc()); + EXPECT_EQ(ApzcOf(layers[2]), node2->GetApzc()); + EXPECT_EQ(ApzcOf(layers[0]), ApzcOf(layers[2])->GetParent()); + EXPECT_EQ(ApzcOf(layers[2]), ApzcOf(layers[5])); + + EXPECT_EQ(node2->GetFirstChild(), node2->GetLastChild()); + EXPECT_EQ(ApzcOf(layers[3]), node2->GetLastChild()->GetApzc()); + EXPECT_EQ(node5->GetFirstChild(), node5->GetLastChild()); + EXPECT_EQ(ApzcOf(layers[6]), node5->GetLastChild()->GetApzc()); + EXPECT_EQ(ApzcOf(layers[2]), ApzcOf(layers[3])->GetParent()); + EXPECT_EQ(ApzcOf(layers[5]), ApzcOf(layers[6])->GetParent()); +} + +class APZCTreeManagerGenericTesterMock : public APZCTreeManagerGenericTester { + public: + APZCTreeManagerGenericTesterMock() { CreateMockHitTester(); } +}; + +TEST_F(APZCTreeManagerGenericTesterMock, Bug1194876) { + // Create a layer tree with parent and child scrollable layers, with the + // child being the root content. + CreateTwoLayerTree(1); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + uint64_t blockId; + nsTArray targets; + + // First touch goes down, APZCTM will hit layers[1] because it is on top of + // layers[0], but we tell it the real target APZC is layers[0]. + MultiTouchInput mti; + mti = CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(25, 50), ScreenSize(0, 0), 0, 0)); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + blockId = manager->ReceiveInputEvent(mti).mInputBlockId; + manager->ContentReceivedInputBlock(blockId, false); + targets.AppendElement(ApzcOf(layers[0])->GetGuid()); + manager->SetTargetAPZC(blockId, targets); + + // Around here, the above touch will get processed by ApzcOf(layers[0]) + + // Second touch goes down (first touch remains down), APZCTM will again hit + // layers[1]. Again we tell it both touches landed on layers[0], but because + // layers[1] is the RCD layer, it will end up being the multitouch target. + mti.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(75, 50), ScreenSize(0, 0), 0, 0)); + // Each touch will get hit-tested, so queue two hit-test results. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + blockId = manager->ReceiveInputEvent(mti).mInputBlockId; + manager->ContentReceivedInputBlock(blockId, false); + targets.AppendElement(ApzcOf(layers[0])->GetGuid()); + manager->SetTargetAPZC(blockId, targets); + + // Around here, the above multi-touch will get processed by ApzcOf(layers[1]). + // We want to ensure that ApzcOf(layers[0]) has had its state cleared, because + // otherwise it will do things like dispatch spurious long-tap events. + + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _)).Times(0); +} + +TEST_F(APZCTreeManagerGenericTesterMock, TargetChangesMidGesture_Bug1570559) { + // Create a layer tree with parent and child scrollable layers, with the + // parent being the root content. + CreateTwoLayerTree(0); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + uint64_t blockId; + nsTArray targets; + + // First touch goes down. APZCTM hits the child layer because it is on top + // (and we confirm this target), but do not prevent-default the event, causing + // the child APZC's gesture detector to start a long-tap timeout task. + MultiTouchInput mti = + CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_START, mcc->Time()); + mti.mTouches.AppendElement( + SingleTouchData(0, ScreenIntPoint(25, 50), ScreenSize(0, 0), 0, 0)); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + blockId = manager->ReceiveInputEvent(mti).mInputBlockId; + manager->ContentReceivedInputBlock(blockId, /* default prevented = */ false); + targets.AppendElement(ApzcOf(layers[1])->GetGuid()); + manager->SetTargetAPZC(blockId, targets); + + // Second touch goes down (first touch remains down). APZCTM again hits the + // child and we confirm this, but multi-touch events are routed to the root + // content APZC which is the parent. This event is prevent-defaulted, so we + // clear the parent's gesture state. The bug is that we fail to clear the + // child's gesture state. + mti.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(75, 50), ScreenSize(0, 0), 0, 0)); + // Each touch will get hit-tested, so queue two hit-test results. + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID + 1, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + blockId = manager->ReceiveInputEvent(mti).mInputBlockId; + manager->ContentReceivedInputBlock(blockId, /* default prevented = */ true); + targets.AppendElement(ApzcOf(layers[1])->GetGuid()); + manager->SetTargetAPZC(blockId, targets); + + // If we've failed to clear the child's gesture state, then the long tap + // timeout task will fire in TearDown() and a long-tap will be dispatched. + EXPECT_CALL(*mcc, HandleTap(TapType::eLongTap, _, _, _, _)).Times(0); +} + +TEST_F(APZCTreeManagerGenericTesterMock, Bug1198900) { + // This is just a test that cancels a wheel event to make sure it doesn't + // crash. + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + ScreenPoint origin(100, 50); + ScrollWheelInput swi(mcc->Time(), 0, ScrollWheelInput::SCROLLMODE_INSTANT, + ScrollWheelInput::SCROLLDELTA_PIXEL, origin, 0, 10, + false, WheelDeltaAdjustmentStrategy::eNone); + uint64_t blockId; + QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID, + {CompositorHitTestFlags::eVisibleToHitTest, + CompositorHitTestFlags::eIrregularArea}); + blockId = manager->ReceiveInputEvent(swi).mInputBlockId; + manager->ContentReceivedInputBlock(blockId, /* preventDefault= */ true); +} + +// The next two tests check that APZ clamps the scroll offset it composites even +// if the main thread fails to do so. (The main thread will always clamp its +// scroll offset internally, but it may not send APZ the clamped version for +// scroll offset synchronization reasons.) +TEST_F(APZCTreeManagerTester, Bug1551582) { + // The simple layer tree has a scrollable rect of 500x500 and a composition + // bounds of 200x200, leading to a scroll range of (0,0,300,300). + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + // Simulate the main thread scrolling to the end of the scroll range. + ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aMetrics.SetLayoutScrollOffset(CSSPoint(300, 300)); + nsTArray scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll( + ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(300, 300)))); + aSm.SetScrollUpdates(scrollUpdates); + aMetrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration()); + }); + UpdateHitTestingTree(); + + // Sanity check. + RefPtr apzc = ApzcOf(root); + CSSPoint compositedScrollOffset = apzc->GetCompositedScrollOffset(); + EXPECT_EQ(CSSPoint(300, 300), compositedScrollOffset); + + // Simulate the main thread shrinking the scrollable rect to 400x400 (and + // thereby the scroll range to (0,0,200,200) without sending a new scroll + // offset update for the clamped scroll position (200,200). + ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aMetrics.SetScrollableRect(CSSRect(0, 0, 400, 400)); + }); + UpdateHitTestingTree(); + + // Check that APZ has clamped the scroll offset to (200,200) for us. + compositedScrollOffset = apzc->GetCompositedScrollOffset(); + EXPECT_EQ(CSSPoint(200, 200), compositedScrollOffset); +} +TEST_F(APZCTreeManagerTester, Bug1557424) { + // The simple layer tree has a scrollable rect of 500x500 and a composition + // bounds of 200x200, leading to a scroll range of (0,0,300,300). + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + + // Simulate the main thread scrolling to the end of the scroll range. + ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aMetrics.SetLayoutScrollOffset(CSSPoint(300, 300)); + nsTArray scrollUpdates; + scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll( + ScrollOrigin::Other, CSSPoint::ToAppUnits(CSSPoint(300, 300)))); + aSm.SetScrollUpdates(scrollUpdates); + aMetrics.SetScrollGeneration(scrollUpdates.LastElement().GetGeneration()); + }); + UpdateHitTestingTree(); + + // Sanity check. + RefPtr apzc = ApzcOf(root); + CSSPoint compositedScrollOffset = apzc->GetCompositedScrollOffset(); + EXPECT_EQ(CSSPoint(300, 300), compositedScrollOffset); + + // Simulate the main thread expanding the composition bounds to 300x300 (and + // thereby shrinking the scroll range to (0,0,200,200) without sending a new + // scroll offset update for the clamped scroll position (200,200). + ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + aMetrics.SetCompositionBounds(ParentLayerRect(0, 0, 300, 300)); + }); + UpdateHitTestingTree(); + + // Check that APZ has clamped the scroll offset to (200,200) for us. + compositedScrollOffset = apzc->GetCompositedScrollOffset(); + EXPECT_EQ(CSSPoint(200, 200), compositedScrollOffset); +} + +TEST_F(APZCTreeManagerTester, Bug1805601) { + // The simple layer tree has a scrollable rect of 500x500 and a composition + // bounds of 200x200, leading to a scroll range of (0,0,300,300) at unit zoom. + CreateSimpleScrollingLayer(); + ScopedLayerTreeRegistration registration(LayersId{0}, mcc); + UpdateHitTestingTree(); + RefPtr apzc = ApzcOf(root); + FrameMetrics& compositorMetrics = apzc->GetFrameMetrics(); + EXPECT_EQ(CSSRect(0, 0, 300, 300), compositorMetrics.CalculateScrollRange()); + + // Zoom the page in by 2x. This needs to be reflected in each of the pres + // shell resolution, cumulative resolution, and zoom. This makes the scroll + // range (0,0,400,400). + compositorMetrics.SetZoom(CSSToParentLayerScale(2.0)); + EXPECT_EQ(CSSRect(0, 0, 400, 400), compositorMetrics.CalculateScrollRange()); + + // Scroll to an area inside the 2x scroll range but outside the original one. + compositorMetrics.ClampAndSetVisualScrollOffset(CSSPoint(350, 350)); + EXPECT_EQ(CSSPoint(350, 350), compositorMetrics.GetVisualScrollOffset()); + + // Simulate a main-thread update where the zoom is reset to 1x but the visual + // scroll offset is unmodified. + ModifyFrameMetrics(root, [](ScrollMetadata& aSm, FrameMetrics& aMetrics) { + // Changes to |compositorMetrics| are not reflected in |aMetrics|, which + // is the "layer tree" copy, so we don't need to explicitly set the zoom to + // 1.0 (it still has that as the initial value), but we do need to set + // the visual scroll offset to the same value the APZ copy has. + aMetrics.SetVisualScrollOffset(CSSPoint(350, 350)); + + // Needed to get APZ to accept the 1.0 zoom in |aMetrics|, otherwise + // it will act as though its zoom is newer (e.g. an async zoom that hasn't + // been repainted yet) and ignore ours. + aSm.SetResolutionUpdated(true); + }); + UpdateHitTestingTree(); + + // Check that APZ clamped the scroll offset. + EXPECT_EQ(CSSRect(0, 0, 300, 300), compositorMetrics.CalculateScrollRange()); + EXPECT_EQ(CSSPoint(300, 300), compositorMetrics.GetVisualScrollOffset()); +} diff --git a/gfx/layers/apz/test/gtest/TestWRScrollData.cpp b/gfx/layers/apz/test/gtest/TestWRScrollData.cpp new file mode 100644 index 0000000000..e267e58e90 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestWRScrollData.cpp @@ -0,0 +1,273 @@ +/* -*- 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 "TestWRScrollData.h" +#include "APZTestAccess.h" +#include "gtest/gtest.h" +#include "FrameMetrics.h" +#include "gfxPlatform.h" +#include "mozilla/layers/APZUpdater.h" +#include "mozilla/layers/LayersTypes.h" +#include "mozilla/layers/ScrollableLayerGuid.h" +#include "mozilla/layers/WebRenderScrollDataWrapper.h" +#include "mozilla/UniquePtr.h" +#include "apz/src/APZCTreeManager.h" + +using mozilla::layers::APZCTreeManager; +using mozilla::layers::APZUpdater; +using mozilla::layers::LayersId; +using mozilla::layers::ScrollableLayerGuid; +using mozilla::layers::ScrollMetadata; +using mozilla::layers::TestWRScrollData; +using mozilla::layers::WebRenderLayerScrollData; +using mozilla::layers::WebRenderScrollDataWrapper; + +/* static */ +TestWRScrollData TestWRScrollData::Create(const char* aTreeShape, + const APZUpdater& aUpdater, + const LayerIntRegion* aVisibleRegions, + const gfx::Matrix4x4* aTransforms) { + // The WebRenderLayerScrollData tree needs to be created in a fairly + // particular way (for example, each node needs to know the number of + // descendants it has), so this function takes care to create the nodes + // in the same order as WebRenderCommandBuilder would. + TestWRScrollData result; + const size_t len = strlen(aTreeShape); + // "Layer index" in this function refers to the index by which a layer will + // be accessible via TestWRScrollData::GetLayer(), and matches the order + // in which the layer appears in |aTreeShape|. + size_t currentLayerIndex = 0; + struct LayerEntry { + size_t mLayerIndex; + int32_t mDescendantCount = 0; + }; + // Layers we have encountered in |aTreeShape|, but have not built a + // WebRenderLayerScrollData for. (It can only be built after its + // descendants have been encountered and counted.) + std::stack pendingLayers; + std::vector finishedLayers; + // Tracks the level of nesting of '(' characters. Starts at 1 to account + // for the root layer. + size_t depth = 1; + // Helper function for finishing a layer once all its descendants have been + // encountered. + auto finishLayer = [&] { + MOZ_ASSERT(!pendingLayers.empty()); + LayerEntry entry = pendingLayers.top(); + + WebRenderLayerScrollData layer; + APZTestAccess::InitializeForTest(layer, entry.mDescendantCount); + if (aVisibleRegions) { + layer.SetVisibleRegion(aVisibleRegions[entry.mLayerIndex]); + } + if (aTransforms) { + layer.SetTransform(aTransforms[entry.mLayerIndex]); + } + finishedLayers.push_back(std::move(layer)); + + // |finishedLayers| stores the layers in a different order than they + // appeared in |aTreeShape|. To be able to access layers by their layer + // index, keep a mapping from layer index to index in |finishedLayers|. + result.mIndexMap.emplace(entry.mLayerIndex, finishedLayers.size() - 1); + + pendingLayers.pop(); + + // Keep track of descendant counts. The +1 is for the layer just finished. + if (!pendingLayers.empty()) { + pendingLayers.top().mDescendantCount += (entry.mDescendantCount + 1); + } + }; + for (size_t i = 0; i < len; ++i) { + if (aTreeShape[i] == '(') { + ++depth; + } else if (aTreeShape[i] == ')') { + if (pendingLayers.size() <= 1) { + printf("Invalid tree shape: too many ')'\n"); + MOZ_CRASH(); + } + finishLayer(); // finish last layer at current depth + --depth; + } else { + if (aTreeShape[i] != 'x') { + printf("The only allowed character to represent a layer is 'x'\n"); + MOZ_CRASH(); + } + if (depth == pendingLayers.size()) { + // We have a previous layer at this same depth to finish. + if (depth <= 1) { + printf("The tree is only allowed to have one root\n"); + MOZ_CRASH(); + } + finishLayer(); + } + MOZ_ASSERT(depth == pendingLayers.size() + 1); + pendingLayers.push({currentLayerIndex}); + ++currentLayerIndex; + } + } + if (pendingLayers.size() != 1) { + printf("Invalid tree shape: '(' and ')' not balanced\n"); + MOZ_CRASH(); + } + finishLayer(); // finish root layer + + // As in WebRenderCommandBuilder, the layers need to be added to the + // WebRenderScrollData in reverse of the order in which they were built. + for (auto it = finishedLayers.rbegin(); it != finishedLayers.rend(); ++it) { + result.AddLayerData(std::move(*it)); + } + // mIndexMap also needs to be adjusted to accout for the reversal above. + for (auto& [layerIndex, storedIndex] : result.mIndexMap) { + (void)layerIndex; // suppress -Werror=unused-variable + storedIndex = result.GetLayerCount() - storedIndex - 1; + } + + return result; +} + +const WebRenderLayerScrollData* TestWRScrollData::operator[]( + size_t aLayerIndex) const { + auto it = mIndexMap.find(aLayerIndex); + if (it == mIndexMap.end()) { + return nullptr; + } + return GetLayerData(it->second); +} + +WebRenderLayerScrollData* TestWRScrollData::operator[](size_t aLayerIndex) { + auto it = mIndexMap.find(aLayerIndex); + if (it == mIndexMap.end()) { + return nullptr; + } + return GetLayerData(it->second); +} + +void TestWRScrollData::SetScrollMetadata( + size_t aLayerIndex, const nsTArray& aMetadata) { + WebRenderLayerScrollData* layer = operator[](aLayerIndex); + MOZ_ASSERT(layer); + for (const ScrollMetadata& metadata : aMetadata) { + layer->AppendScrollMetadata(*this, metadata); + } +} + +class WebRenderScrollDataWrapperTester : public ::testing::Test { + protected: + virtual void SetUp() { + // This ensures ScrollMetadata::sNullMetadata is initialized. + gfxPlatform::GetPlatform(); + + mManager = new APZCTreeManager(LayersId{0}); + mUpdater = new APZUpdater(mManager, false); + } + + RefPtr mManager; + RefPtr mUpdater; +}; + +TEST_F(WebRenderScrollDataWrapperTester, SimpleTree) { + auto layers = TestWRScrollData::Create("x(x(x(xx)x(x)))", *mUpdater); + WebRenderScrollDataWrapper w0(*mUpdater, &layers); + + ASSERT_EQ(layers[0], w0.GetLayer()); + WebRenderScrollDataWrapper w1 = w0.GetLastChild(); + ASSERT_EQ(layers[1], w1.GetLayer()); + ASSERT_FALSE(w1.GetPrevSibling().IsValid()); + WebRenderScrollDataWrapper w5 = w1.GetLastChild(); + ASSERT_EQ(layers[5], w5.GetLayer()); + WebRenderScrollDataWrapper w6 = w5.GetLastChild(); + ASSERT_EQ(layers[6], w6.GetLayer()); + ASSERT_FALSE(w6.GetLastChild().IsValid()); + WebRenderScrollDataWrapper w2 = w5.GetPrevSibling(); + ASSERT_EQ(layers[2], w2.GetLayer()); + ASSERT_FALSE(w2.GetPrevSibling().IsValid()); + WebRenderScrollDataWrapper w4 = w2.GetLastChild(); + ASSERT_EQ(layers[4], w4.GetLayer()); + ASSERT_FALSE(w4.GetLastChild().IsValid()); + WebRenderScrollDataWrapper w3 = w4.GetPrevSibling(); + ASSERT_EQ(layers[3], w3.GetLayer()); + ASSERT_FALSE(w3.GetLastChild().IsValid()); + ASSERT_FALSE(w3.GetPrevSibling().IsValid()); +} + +static ScrollMetadata MakeMetadata(ScrollableLayerGuid::ViewID aId) { + ScrollMetadata metadata; + metadata.GetMetrics().SetScrollId(aId); + return metadata; +} + +TEST_F(WebRenderScrollDataWrapperTester, MultiFramemetricsTree) { + auto layers = TestWRScrollData::Create("x(x(x(xx)x(x)))", *mUpdater); + + nsTArray metadata; + metadata.InsertElementAt(0, + MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + + 0)); // topmost of root layer + metadata.InsertElementAt(0, + MakeMetadata(ScrollableLayerGuid::NULL_SCROLL_ID)); + metadata.InsertElementAt( + 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 1)); + metadata.InsertElementAt( + 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 2)); + metadata.InsertElementAt(0, + MakeMetadata(ScrollableLayerGuid::NULL_SCROLL_ID)); + metadata.InsertElementAt( + 0, MakeMetadata( + ScrollableLayerGuid::NULL_SCROLL_ID)); // bottom of root layer + layers.SetScrollMetadata(0, metadata); + + metadata.Clear(); + metadata.InsertElementAt( + 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 3)); + layers.SetScrollMetadata(1, metadata); + + metadata.Clear(); + metadata.InsertElementAt( + 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 4)); + layers.SetScrollMetadata(2, metadata); + + metadata.Clear(); + metadata.InsertElementAt( + 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 5)); + layers.SetScrollMetadata(4, metadata); + + metadata.Clear(); + metadata.InsertElementAt(0, + MakeMetadata(ScrollableLayerGuid::NULL_SCROLL_ID)); + metadata.InsertElementAt( + 0, MakeMetadata(ScrollableLayerGuid::START_SCROLL_ID + 6)); + layers.SetScrollMetadata(5, metadata); + + WebRenderScrollDataWrapper wrapper(*mUpdater, &layers); + nsTArray expectedLayers; + expectedLayers.AppendElement(layers[0]); + expectedLayers.AppendElement(layers[0]); + expectedLayers.AppendElement(layers[0]); + expectedLayers.AppendElement(layers[0]); + expectedLayers.AppendElement(layers[0]); + expectedLayers.AppendElement(layers[0]); + expectedLayers.AppendElement(layers[1]); + expectedLayers.AppendElement(layers[5]); + expectedLayers.AppendElement(layers[5]); + expectedLayers.AppendElement(layers[6]); + nsTArray expectedIds; + expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 0); + expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID); + expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 1); + expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 2); + expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID); + expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID); + expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 3); + expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID); + expectedIds.AppendElement(ScrollableLayerGuid::START_SCROLL_ID + 6); + expectedIds.AppendElement(ScrollableLayerGuid::NULL_SCROLL_ID); + for (int i = 0; i < 10; i++) { + ASSERT_EQ(expectedLayers[i], wrapper.GetLayer()); + ASSERT_EQ(expectedIds[i], wrapper.Metrics().GetScrollId()); + wrapper = wrapper.GetLastChild(); + } + ASSERT_FALSE(wrapper.IsValid()); +} diff --git a/gfx/layers/apz/test/gtest/TestWRScrollData.h b/gfx/layers/apz/test/gtest/TestWRScrollData.h new file mode 100644 index 0000000000..92e79aaee0 --- /dev/null +++ b/gfx/layers/apz/test/gtest/TestWRScrollData.h @@ -0,0 +1,63 @@ +/* -*- 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_TestWRScrollData_h +#define mozilla_layers_TestWRScrollData_h + +#include "mozilla/gfx/MatrixFwd.h" +#include "mozilla/layers/WebRenderScrollData.h" + +namespace mozilla { +namespace layers { + +class APZUpdater; + +// Extends WebRenderScrollData with some methods useful for gtests. +class TestWRScrollData : public WebRenderScrollData { + public: + TestWRScrollData() = default; + TestWRScrollData(TestWRScrollData&& aOther) = default; + TestWRScrollData& operator=(TestWRScrollData&& aOther) = default; + + /* + * Create a WebRenderLayerScrollData tree described by |aTreeShape|. + * |aTreeShape| is expected to be a string where each character is + * either 'x' to indicate a node in the tree, or a '(' or ')' to indicate + * the start/end of a subtree. + * + * Example "x(x(x(xx)x))" would yield: + * x + * | + * x + * / \ + * x x + * / \ + * x x + * + * The caller may optionally provide visible regions and/or transforms + * for the nodes. If provided, the array should contain one element + * for each node, in the same order as in |aTreeShape|. + */ + static TestWRScrollData Create( + const char* aTreeShape, const APZUpdater& aUpdater, + const LayerIntRegion* aVisibleRegions = nullptr, + const gfx::Matrix4x4* aTransforms = nullptr); + + // These methods allow accessing and manipulating layers based on an index + // representing the order in which they appear in |aTreeShape|. + WebRenderLayerScrollData* operator[](size_t aLayerIndex); + const WebRenderLayerScrollData* operator[](size_t aLayerIndex) const; + void SetScrollMetadata(size_t aLayerIndex, + const nsTArray& aMetadata); + + private: + std::map mIndexMap; // Used to implement GetLayer() +}; + +} // namespace layers +} // namespace mozilla + +#endif diff --git a/gfx/layers/apz/test/gtest/moz.build b/gfx/layers/apz/test/gtest/moz.build new file mode 100644 index 0000000000..e6fb799008 --- /dev/null +++ b/gfx/layers/apz/test/gtest/moz.build @@ -0,0 +1,39 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES += [ + "APZTestAccess.cpp", + "APZTestCommon.cpp", + "MockHitTester.cpp", + "TestAxisLock.cpp", + "TestBasic.cpp", + "TestEventRegions.cpp", + "TestEventResult.cpp", + "TestFlingAcceleration.cpp", + "TestGestureDetector.cpp", + "TestHitTesting.cpp", + "TestInputQueue.cpp", + "TestOverscroll.cpp", + "TestPanning.cpp", + "TestPinching.cpp", + "TestPointerEventsConsumable.cpp", + "TestScrollHandoff.cpp", + "TestSnapping.cpp", + "TestSnappingOnMomentum.cpp", + "TestTransformNotifications.cpp", + "TestTreeManager.cpp", + "TestWRScrollData.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "/gfx/2d", + "/gfx/cairo/cairo/src", + "/gfx/layers", +] + +FINAL_LIBRARY = "xul-gtest" diff --git a/gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp b/gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp new file mode 100644 index 0000000000..cd7e0a7d0d --- /dev/null +++ b/gfx/layers/apz/test/gtest/mvm/TestMobileViewportManager.cpp @@ -0,0 +1,220 @@ +/* -*- 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 "gtest/gtest.h" +#include "gmock/gmock.h" + +#include + +#include "MobileViewportManager.h" +#include "mozilla/MVMContext.h" +#include "mozilla/dom/Event.h" + +using namespace mozilla; + +class MockMVMContext : public MVMContext { + using AutoSizeFlag = nsViewportInfo::AutoSizeFlag; + using AutoScaleFlag = nsViewportInfo::AutoScaleFlag; + using ZoomFlag = nsViewportInfo::ZoomFlag; + + // A "layout function" is a function that computes the content size + // as a function of the ICB size. + using LayoutFunction = std::function; + + public: + // MVMContext methods we don't care to implement. + MOCK_METHOD3(AddEventListener, + void(const nsAString& aType, nsIDOMEventListener* aListener, + bool aUseCapture)); + MOCK_METHOD3(RemoveEventListener, + void(const nsAString& aType, nsIDOMEventListener* aListener, + bool aUseCapture)); + MOCK_METHOD3(AddObserver, void(nsIObserver* aObserver, const char* aTopic, + bool aOwnsWeak)); + MOCK_METHOD2(RemoveObserver, + void(nsIObserver* aObserver, const char* aTopic)); + MOCK_METHOD0(Destroy, void()); + + MOCK_METHOD1(SetVisualViewportSize, void(const CSSSize& aSize)); + MOCK_METHOD0(PostVisualViewportResizeEventByDynamicToolbar, void()); + MOCK_METHOD0(UpdateDisplayPortMargins, void()); + + void SetMVM(MobileViewportManager* aMVM) { mMVM = aMVM; } + + // MVMContext method implementations. + nsViewportInfo GetViewportInfo(const ScreenIntSize& aDisplaySize) const { + // This is a very basic approximation of what Document::GetViewportInfo() + // does in the most common cases. + // Ideally, we would invoke the algorithm in Document::GetViewportInfo() + // itself, but that would require refactoring it a bit to remove + // dependencies on the actual Document which we don't have available in + // this test harness. + CSSSize viewportSize = mDisplaySize / mDeviceScale; + if (mAutoSizeFlag == AutoSizeFlag::FixedSize) { + viewportSize = CSSSize(mFixedViewportWidth, + mFixedViewportWidth * (float(mDisplaySize.height) / + mDisplaySize.width)); + } + return nsViewportInfo(mDefaultScale, mMinScale, mMaxScale, viewportSize, + mAutoSizeFlag, mAutoScaleFlag, mZoomFlag, + dom::ViewportFitType::Auto); + } + CSSToLayoutDeviceScale CSSToDevPixelScale() const { return mDeviceScale; } + float GetResolution() const { return mResolution; } + bool SubjectMatchesDocument(nsISupports* aSubject) const { return true; } + Maybe CalculateScrollableRectForRSF() const { + return Some(CSSRect(CSSPoint(), mContentSize)); + } + bool IsResolutionUpdatedByApz() const { return false; } + LayoutDeviceMargin ScrollbarAreaToExcludeFromCompositionBounds() const { + return LayoutDeviceMargin(); + } + Maybe GetContentViewerSize() const { + return Some(mDisplaySize); + } + bool AllowZoomingForDocument() const { return true; } + bool IsInReaderMode() const { return false; } + bool IsDocumentLoading() const { return false; } + + void SetResolutionAndScaleTo(float aResolution, + ResolutionChangeOrigin aOrigin) { + mResolution = aResolution; + mMVM->ResolutionUpdated(aOrigin); + } + void Reflow(const CSSSize& aNewSize) { + mICBSize = aNewSize; + mContentSize = mLayoutFunction(mICBSize); + } + + // Allow test code to modify the input metrics. + void SetMinScale(CSSToScreenScale aMinScale) { mMinScale = aMinScale; } + void SetMaxScale(CSSToScreenScale aMaxScale) { mMaxScale = aMaxScale; } + void SetInitialScale(CSSToScreenScale aInitialScale) { + mDefaultScale = aInitialScale; + mAutoScaleFlag = AutoScaleFlag::FixedScale; + } + void SetFixedViewportWidth(CSSCoord aWidth) { + mFixedViewportWidth = aWidth; + mAutoSizeFlag = AutoSizeFlag::FixedSize; + } + void SetDisplaySize(const LayoutDeviceIntSize& aNewDisplaySize) { + mDisplaySize = aNewDisplaySize; + } + void SetLayoutFunction(const LayoutFunction& aLayoutFunction) { + mLayoutFunction = aLayoutFunction; + } + + // Allow test code to query the output metrics. + CSSSize GetICBSize() const { return mICBSize; } + CSSSize GetContentSize() const { return mContentSize; } + + private: + // Input metrics, with some sensible defaults. + LayoutDeviceIntSize mDisplaySize{300, 600}; + CSSToScreenScale mDefaultScale{1.0f}; + CSSToScreenScale mMinScale{0.25f}; + CSSToScreenScale mMaxScale{10.0f}; + CSSToLayoutDeviceScale mDeviceScale{1.0f}; + CSSCoord mFixedViewportWidth; + AutoSizeFlag mAutoSizeFlag = AutoSizeFlag::AutoSize; + AutoScaleFlag mAutoScaleFlag = AutoScaleFlag::AutoScale; + ZoomFlag mZoomFlag = ZoomFlag::AllowZoom; + // As a default layout function, just set the content size to the ICB size. + LayoutFunction mLayoutFunction = [](CSSSize aICBSize) { return aICBSize; }; + + // Output metrics. + float mResolution = 1.0f; + CSSSize mICBSize; + CSSSize mContentSize; + + MobileViewportManager* mMVM = nullptr; +}; + +class MVMTester : public ::testing::Test { + public: + MVMTester() + : mMVMContext(new MockMVMContext()), + mMVM(new MobileViewportManager( + mMVMContext, + MobileViewportManager::ManagerType::VisualAndMetaViewport)) { + mMVMContext->SetMVM(mMVM.get()); + } + + void Resize(const LayoutDeviceIntSize& aNewDisplaySize) { + mMVMContext->SetDisplaySize(aNewDisplaySize); + mMVM->RequestReflow(false); + } + + protected: + RefPtr mMVMContext; + RefPtr mMVM; +}; + +TEST_F(MVMTester, ZoomBoundsRespectedAfterRotation_Bug1536755) { + // Set up initial conditions. + mMVMContext->SetDisplaySize(LayoutDeviceIntSize(600, 300)); + mMVMContext->SetInitialScale(CSSToScreenScale(1.0f)); + mMVMContext->SetMinScale(CSSToScreenScale(1.0f)); + mMVMContext->SetMaxScale(CSSToScreenScale(1.0f)); + // Set a layout function that simulates a page which is twice + // as tall as it is wide. + mMVMContext->SetLayoutFunction([](CSSSize aICBSize) { + return CSSSize(aICBSize.width, aICBSize.width * 2); + }); + + // Perform an initial viewport computation and reflow, and + // sanity-check the results. + mMVM->SetInitialViewport(); + EXPECT_EQ(CSSSize(600, 300), mMVMContext->GetICBSize()); + EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize()); + EXPECT_EQ(1.0f, mMVMContext->GetResolution()); + + // Now rotate the screen, and check that the minimum and maximum + // scales are still respected after the rotation. + Resize(LayoutDeviceIntSize(300, 600)); + EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetICBSize()); + EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetContentSize()); + EXPECT_EQ(1.0f, mMVMContext->GetResolution()); +} + +TEST_F(MVMTester, LandscapeToPortraitRotation_Bug1523844) { + // Set up initial conditions. + mMVMContext->SetDisplaySize(LayoutDeviceIntSize(300, 600)); + // Set a layout function that simulates a page with a fixed + // content size that's as wide as the screen in one orientation + // (and wider in the other). + mMVMContext->SetLayoutFunction( + [](CSSSize aICBSize) { return CSSSize(600, 1200); }); + + // Simulate a "DOMMetaAdded" event being fired before calling + // SetInitialViewport(). This matches what typically happens + // during real usage (the MVM receives the "DOMMetaAdded" + // before the "load", and it's the "load" that calls + // SetInitialViewport()), and is important to trigger this + // bug, because it causes the MVM to be stuck with an + // "mRestoreResolution" (prior to the fix). + mMVM->HandleDOMMetaAdded(); + + // Perform an initial viewport computation and reflow, and + // sanity-check the results. + mMVM->SetInitialViewport(); + EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetICBSize()); + EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize()); + EXPECT_EQ(0.5f, mMVMContext->GetResolution()); + + // Rotate to landscape. + Resize(LayoutDeviceIntSize(600, 300)); + EXPECT_EQ(CSSSize(600, 300), mMVMContext->GetICBSize()); + EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize()); + EXPECT_EQ(1.0f, mMVMContext->GetResolution()); + + // Rotate back to portrait and check that we have returned + // to the portrait resolution. + Resize(LayoutDeviceIntSize(300, 600)); + EXPECT_EQ(CSSSize(300, 600), mMVMContext->GetICBSize()); + EXPECT_EQ(CSSSize(600, 1200), mMVMContext->GetContentSize()); + EXPECT_EQ(0.5f, mMVMContext->GetResolution()); +} diff --git a/gfx/layers/apz/test/gtest/mvm/moz.build b/gfx/layers/apz/test/gtest/mvm/moz.build new file mode 100644 index 0000000000..0fa985307b --- /dev/null +++ b/gfx/layers/apz/test/gtest/mvm/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES += [ + "TestMobileViewportManager.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" diff --git a/gfx/layers/apz/test/mochitest/FissionTestHelperChild.sys.mjs b/gfx/layers/apz/test/mochitest/FissionTestHelperChild.sys.mjs new file mode 100644 index 0000000000..c0695b7abb --- /dev/null +++ b/gfx/layers/apz/test/mochitest/FissionTestHelperChild.sys.mjs @@ -0,0 +1,157 @@ +// This code runs in the content process that holds the window to which +// this actor is attached. There is one instance of this class for each +// "inner window" (i.e. one per content document, including iframes/nested +// iframes). +// There is a 1:1 relationship between instances of this class and +// FissionTestHelperParent instances, and the pair are entangled such +// that they can communicate with each other regardless of which process +// they live in. + +export class FissionTestHelperChild extends JSWindowActorChild { + constructor() { + super(); + this._msgCounter = 0; + this._oopifResponsePromiseResolvers = []; + } + + cw() { + return this.contentWindow.wrappedJSObject; + } + + initialize() { + // This exports a bunch of things into the content window so that + // the test can access them. Most things are scoped inside the + // FissionTestHelper object on the window to avoid polluting the global + // namespace. + + let cw = this.cw(); + Cu.exportFunction( + (cond, msg) => this.sendAsyncMessage("ok", { cond, msg }), + cw, + { defineAs: "ok" } + ); + Cu.exportFunction( + (a, b, msg) => this.sendAsyncMessage("is", { a, b, msg }), + cw, + { defineAs: "is" } + ); + + let FissionTestHelper = Cu.createObjectIn(cw, { + defineAs: "FissionTestHelper", + }); + FissionTestHelper.startTestPromise = new cw.Promise( + Cu.exportFunction(resolve => { + this._startTestPromiseResolver = resolve; + }, cw) + ); + + Cu.exportFunction(this.subtestDone.bind(this), FissionTestHelper, { + defineAs: "subtestDone", + }); + + Cu.exportFunction(this.subtestFailed.bind(this), FissionTestHelper, { + defineAs: "subtestFailed", + }); + + Cu.exportFunction(this.sendToOopif.bind(this), FissionTestHelper, { + defineAs: "sendToOopif", + }); + Cu.exportFunction(this.fireEventInEmbedder.bind(this), FissionTestHelper, { + defineAs: "fireEventInEmbedder", + }); + } + + // Called by the subtest to indicate completion to the top-level browser-chrome + // mochitest. + subtestDone() { + let cw = this.cw(); + if (cw.ApzCleanup) { + cw.ApzCleanup.execute(); + } + this.sendAsyncMessage("Test:Complete", {}); + } + + // Called by the subtest to indicate subtest failure. Only one of subtestDone + // or subtestFailed should be called. + subtestFailed(msg) { + this.sendAsyncMessage("ok", { cond: false, msg }); + this.subtestDone(); + } + + // Called by the subtest to eval some code in the OOP iframe. This returns + // a promise that resolves to the return value from the eval. + sendToOopif(iframeElement, stringToEval) { + let browsingContextId = iframeElement.browsingContext.id; + let msgId = ++this._msgCounter; + let cw = this.cw(); + let responsePromise = new cw.Promise( + Cu.exportFunction(resolve => { + this._oopifResponsePromiseResolvers[msgId] = resolve; + }, cw) + ); + this.sendAsyncMessage("EmbedderToOopif", { + browsingContextId, + msgId, + stringToEval, + }); + return responsePromise; + } + + // Called by OOP iframes to dispatch an event in the embedder window. This + // can be used by the OOP iframe to asynchronously notify the embedder of + // things that happen. The embedder can use promiseOneEvent from + // helper_fission_utils.js to listen for these events. + fireEventInEmbedder(eventType, data) { + this.sendAsyncMessage("OopifToEmbedder", { eventType, data }); + } + + handleEvent(evt) { + switch (evt.type) { + case "FissionTestHelper:Init": + this.initialize(); + break; + } + } + + receiveMessage(msg) { + switch (msg.name) { + case "Test:Start": + this._startTestPromiseResolver(); + delete this._startTestPromiseResolver; + break; + case "FromEmbedder": + let evalResult = this.contentWindow.eval(msg.data.stringToEval); + this.sendAsyncMessage("OopifToEmbedder", { + msgId: msg.data.msgId, + evalResult, + }); + break; + case "FromOopif": + if (typeof msg.data.msgId == "number") { + if (!(msg.data.msgId in this._oopifResponsePromiseResolvers)) { + dump( + "Error: FromOopif got a message with unknown numeric msgId in " + + this.contentWindow.location.href + + "\n" + ); + } + this._oopifResponsePromiseResolvers[msg.data.msgId]( + msg.data.evalResult + ); + delete this._oopifResponsePromiseResolvers[msg.data.msgId]; + } else if (typeof msg.data.eventType == "string") { + let cw = this.cw(); + let event = new cw.Event(msg.data.eventType); + event.data = Cu.cloneInto(msg.data.data, cw); + this.contentWindow.dispatchEvent(event); + } else { + dump( + "Warning: Unrecognized FromOopif message received in " + + this.contentWindow.location.href + + "\n" + ); + } + break; + } + } +} diff --git a/gfx/layers/apz/test/mochitest/FissionTestHelperParent.sys.mjs b/gfx/layers/apz/test/mochitest/FissionTestHelperParent.sys.mjs new file mode 100644 index 0000000000..d71de3b2ad --- /dev/null +++ b/gfx/layers/apz/test/mochitest/FissionTestHelperParent.sys.mjs @@ -0,0 +1,103 @@ +// This code always runs in the parent process. There is one instance of +// this class for each "inner window" (should be one per content document, +// including iframes/nested iframes). +// There is a 1:1 relationship between instances of this class and +// FissionTestHelperChild instances, and the pair are entangled such +// that they can communicate with each other regardless of which process +// they live in. + +export class FissionTestHelperParent extends JSWindowActorParent { + constructor() { + super(); + this._testCompletePromise = new Promise(resolve => { + this._testCompletePromiseResolver = resolve; + }); + } + + embedderWindow() { + let embedder = this.manager.browsingContext.embedderWindowGlobal; + // embedder is of type WindowGlobalParent, defined in WindowGlobalActors.webidl + if (!embedder) { + dump("ERROR: no embedder found in FissionTestHelperParent\n"); + } + return embedder; + } + + docURI() { + return this.manager.documentURI.spec; + } + + // Returns a promise that is resolved when this parent actor receives a + // "Test:Complete" message from the child. + getTestCompletePromise() { + return this._testCompletePromise; + } + + startTest() { + this.sendAsyncMessage("Test:Start", {}); + } + + receiveMessage(msg) { + switch (msg.name) { + case "ok": + FissionTestHelperParent.SimpleTest.ok( + msg.data.cond, + this.docURI() + " | " + msg.data.msg + ); + break; + + case "is": + FissionTestHelperParent.SimpleTest.is( + msg.data.a, + msg.data.b, + this.docURI() + " | " + msg.data.msg + ); + break; + + case "Test:Complete": + this._testCompletePromiseResolver(); + break; + + case "EmbedderToOopif": + // This relays messages from the embedder to an OOP-iframe. The browsing + // context id in the message data identifies the OOP-iframe. + let oopifBrowsingContext = BrowsingContext.get( + msg.data.browsingContextId + ); + if (oopifBrowsingContext == null) { + FissionTestHelperParent.SimpleTest.ok( + false, + "EmbedderToOopif couldn't find oopif" + ); + break; + } + let oopifActor = + oopifBrowsingContext.currentWindowGlobal.getActor( + "FissionTestHelper" + ); + if (!oopifActor) { + FissionTestHelperParent.SimpleTest.ok( + false, + "EmbedderToOopif couldn't find oopif actor" + ); + break; + } + oopifActor.sendAsyncMessage("FromEmbedder", msg.data); + break; + + case "OopifToEmbedder": + // This relays messages from the OOP-iframe to the top-level content + // window which is embedding it. + let embedderActor = this.embedderWindow().getActor("FissionTestHelper"); + if (!embedderActor) { + FissionTestHelperParent.SimpleTest.ok( + false, + "OopifToEmbedder couldn't find embedder" + ); + break; + } + embedderActor.sendAsyncMessage("FromOopif", msg.data); + break; + } + } +} diff --git a/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js b/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js new file mode 100644 index 0000000000..1b1ae8db26 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js @@ -0,0 +1,1881 @@ +// ownerGlobal isn't defined in content privileged windows. +/* eslint-disable mozilla/use-ownerGlobal */ + +// Utilities for synthesizing of native events. + +async function getResolution() { + let resolution = -1; // bogus value in case DWU fails us + // Use window.top to get the root content window which is what has + // the resolution. + resolution = await SpecialPowers.spawn(window.top, [], () => { + return SpecialPowers.getDOMWindowUtils(content.window).getResolution(); + }); + return resolution; +} + +function getPlatform() { + if (navigator.platform.indexOf("Win") == 0) { + return "windows"; + } + if (navigator.platform.indexOf("Mac") == 0) { + return "mac"; + } + // Check for Android before Linux + if (navigator.appVersion.includes("Android")) { + return "android"; + } + if (navigator.platform.indexOf("Linux") == 0) { + return "linux"; + } + return "unknown"; +} + +function nativeVerticalWheelEventMsg() { + switch (getPlatform()) { + case "windows": + return 0x020a; // WM_MOUSEWHEEL + case "mac": + var useWheelCodepath = SpecialPowers.getBoolPref( + "apz.test.mac.synth_wheel_input", + false + ); + // Default to 1 (kCGScrollPhaseBegan) to trigger PanGestureInput events + // from widget code. Allow setting a pref to override this behaviour and + // trigger ScrollWheelInput events instead. + return useWheelCodepath ? 0 : 1; + case "linux": + return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway + } + throw new Error( + "Native wheel events not supported on platform " + getPlatform() + ); +} + +function nativeHorizontalWheelEventMsg() { + switch (getPlatform()) { + case "windows": + return 0x020e; // WM_MOUSEHWHEEL + case "mac": + return 0; // value is unused, can be anything + case "linux": + return 4; // value is unused, pass GDK_SCROLL_SMOOTH anyway + } + throw new Error( + "Native wheel events not supported on platform " + getPlatform() + ); +} + +function nativeArrowDownKey() { + switch (getPlatform()) { + case "windows": + return WIN_VK_DOWN; + case "mac": + return MAC_VK_DownArrow; + } + throw new Error( + "Native key events not supported on platform " + getPlatform() + ); +} + +function nativeArrowUpKey() { + switch (getPlatform()) { + case "windows": + return WIN_VK_UP; + case "mac": + return MAC_VK_UpArrow; + } + throw new Error( + "Native key events not supported on platform " + getPlatform() + ); +} + +function targetIsWindow(aTarget) { + return aTarget.Window && aTarget instanceof aTarget.Window; +} + +function targetIsTopWindow(aTarget) { + if (!targetIsWindow(aTarget)) { + return false; + } + return aTarget == aTarget.top; +} + +// Given an event target which may be a window or an element, get the associated window. +function windowForTarget(aTarget) { + if (targetIsWindow(aTarget)) { + return aTarget; + } + return aTarget.ownerDocument.defaultView; +} + +// Given an event target which may be a window or an element, get the associated element. +function elementForTarget(aTarget) { + if (targetIsWindow(aTarget)) { + return aTarget.document.documentElement; + } + return aTarget; +} + +// Given an event target which may be a window or an element, get the associatd nsIDOMWindowUtils. +function utilsForTarget(aTarget) { + return SpecialPowers.getDOMWindowUtils(windowForTarget(aTarget)); +} + +// Given a pixel scrolling delta, converts it to the platform's native units. +function nativeScrollUnits(aTarget, aDimen) { + switch (getPlatform()) { + case "linux": { + // GTK deltas are treated as line height divided by 3 by gecko. + var targetWindow = windowForTarget(aTarget); + var targetElement = elementForTarget(aTarget); + var lineHeight = + targetWindow.getComputedStyle(targetElement)["font-size"]; + return aDimen / (parseInt(lineHeight) * 3); + } + } + return aDimen; +} + +function parseNativeModifiers(aModifiers, aWindow = window) { + let modifiers = 0; + if (aModifiers.capsLockKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CAPS_LOCK; + } + if (aModifiers.numLockKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUM_LOCK; + } + if (aModifiers.shiftKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_LEFT; + } + if (aModifiers.shiftRightKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_SHIFT_RIGHT; + } + if (aModifiers.ctrlKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT; + } + if (aModifiers.ctrlRightKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT; + } + if (aModifiers.altKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT; + } + if (aModifiers.altRightKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_RIGHT; + } + if (aModifiers.metaKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT; + } + if (aModifiers.metaRightKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT; + } + if (aModifiers.helpKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_HELP; + } + if (aModifiers.fnKey) { + modifiers |= SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_FUNCTION; + } + if (aModifiers.numericKeyPadKey) { + modifiers |= + SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_NUMERIC_KEY_PAD; + } + + if (aModifiers.accelKey) { + modifiers |= _EU_isMac(aWindow) + ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_LEFT + : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_LEFT; + } + if (aModifiers.accelRightKey) { + modifiers |= _EU_isMac(aWindow) + ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_COMMAND_RIGHT + : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_CONTROL_RIGHT; + } + if (aModifiers.altGrKey) { + modifiers |= _EU_isMac(aWindow) + ? SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_LEFT + : SpecialPowers.Ci.nsIDOMWindowUtils.NATIVE_MODIFIER_ALT_GRAPH; + } + return modifiers; +} + +// Several event sythesization functions below (and their helpers) take a "target" +// parameter which may be either an element or a window. For such functions, +// the target's "bounding rect" refers to the bounding client rect for an element, +// and the window's origin for a window. +// Not all functions have been "upgraded" to allow a window argument yet; feel +// free to upgrade others as necessary. + +// Get the origin of |aTarget| relative to the root content document's +// visual viewport in CSS coordinates. +// |aTarget| may be an element (contained in the root content document or +// a subdocument) or, as a special case, the root content window. +// FIXME: Support iframe windows as targets. +function _getTargetRect(aTarget) { + let rect = { left: 0, top: 0, width: 0, height: 0 }; + + // If the target is the root content window, its origin relative + // to the visual viewport is (0, 0). + if (aTarget instanceof Window) { + return rect; + } + if (aTarget.Window && aTarget instanceof aTarget.Window) { + // iframe window + // FIXME: Compute proper rect against the root content window + return rect; + } + + // Otherwise, we have an element. Start with the origin of + // its bounding client rect which is relative to the enclosing + // document's layout viewport. Note that for iframes, the + // layout viewport is also the visual viewport. + const boundingClientRect = aTarget.getBoundingClientRect(); + rect.left = boundingClientRect.left; + rect.top = boundingClientRect.top; + rect.width = boundingClientRect.width; + rect.height = boundingClientRect.height; + + // Iterate up the window hierarchy until we reach the root + // content window, adding the offsets of any iframe windows + // relative to their parent window. + while (aTarget.ownerDocument.defaultView.frameElement) { + const iframe = aTarget.ownerDocument.defaultView.frameElement; + // The offset of the iframe window relative to the parent window + // includes the iframe's border, and the iframe's origin in its + // containing document. + const style = iframe.ownerDocument.defaultView.getComputedStyle(iframe); + const borderLeft = parseFloat(style.borderLeftWidth) || 0; + const borderTop = parseFloat(style.borderTopWidth) || 0; + const borderRight = parseFloat(style.borderRightWidth) || 0; + const borderBottom = parseFloat(style.borderBottomWidth) || 0; + const paddingLeft = parseFloat(style.paddingLeft) || 0; + const paddingTop = parseFloat(style.paddingTop) || 0; + const paddingRight = parseFloat(style.paddingRight) || 0; + const paddingBottom = parseFloat(style.paddingBottom) || 0; + const iframeRect = iframe.getBoundingClientRect(); + rect.left += iframeRect.left + borderLeft + paddingLeft; + rect.top += iframeRect.top + borderTop + paddingTop; + if ( + rect.left + rect.width > + iframeRect.right - borderRight - paddingRight + ) { + rect.width = Math.max( + iframeRect.right - borderRight - paddingRight - rect.left, + 0 + ); + } + if ( + rect.top + rect.height > + iframeRect.bottom - borderBottom - paddingBottom + ) { + rect.height = Math.max( + iframeRect.bottom - borderBottom - paddingBottom - rect.top, + 0 + ); + } + aTarget = iframe; + } + + return rect; +} + +// Returns the in-process root window for the given |aWindow|. +function getInProcessRootWindow(aWindow) { + let window = aWindow; + while (window.frameElement) { + window = window.frameElement.ownerDocument.defaultView; + } + return window; +} + +// Convert (offsetX, offsetY) of target or center of it, in CSS pixels to device +// pixels relative to the screen. +// TODO: this function currently does not incorporate some CSS transforms on +// elements enclosing target, e.g. scale transforms. +async function coordinatesRelativeToScreen(aParams) { + const { + target, // The target element or window + offsetX, // X offset relative to `target` + offsetY, // Y offset relative to `target` + atCenter, // Instead of offsetX/offsetY, return center of `target` + } = aParams; + // Note that |window| might not be the root content window, for two + // possible reasons: + // 1. The mochitest that's calling into this function is not using a mechanism + // like runSubtestsSeriallyInFreshWindows() to load the test page in + // a top-level context, so it's loaded into an iframe by the mochitest + // harness. + // 2. The mochitest itself creates an iframe and calls this function from + // script running in the context of the iframe. + // Since the resolution applies to the top level content document, below we + // use the mozInnerScreen{X,Y} of the top level content window (window.top) + // only for the case where this function gets called in the top level content + // document. In other cases we use nsIDOMWindowUtils.toScreenRect(). + + // We do often specify `window` as the target, if it's the top level window, + // `nsIDOMWindowUtils.toScreenRect` isn't suitable because the function is + // supposed to be called with values in the document coords, so for example + // if desktop zoom is being applied, (0, 0) in the document coords might be + // outside of the visual viewport, i.e. it's going to be negative with the + // `toScreenRect` conversion, whereas the call sites with `window` of this + // function expect (0, 0) position should be the visual viport's offset. So + // in such cases we simply use mozInnerScreen{X,Y} to convert the given value + // to the screen coords. + if (target instanceof Window && window.parent == window) { + const resolution = await getResolution(); + const deviceScale = window.devicePixelRatio; + return { + x: + window.mozInnerScreenX * deviceScale + + (atCenter ? 0 : offsetX) * resolution * deviceScale, + y: + window.mozInnerScreenY * deviceScale + + (atCenter ? 0 : offsetY) * resolution * deviceScale, + }; + } + + const rect = _getTargetRect(target); + + const utils = SpecialPowers.getDOMWindowUtils(getInProcessRootWindow(window)); + const positionInScreenCoords = utils.toScreenRect( + rect.left + (atCenter ? rect.width / 2 : offsetX), + rect.top + (atCenter ? rect.height / 2 : offsetY), + 0, + 0 + ); + + return { + x: positionInScreenCoords.x, + y: positionInScreenCoords.y, + }; +} + +// Get the bounding box of aElement, and return it in device pixels +// relative to the screen. +// TODO: This function should probably take into account the resolution and +// the relative viewport rect like coordinatesRelativeToScreen() does. +function rectRelativeToScreen(aElement) { + var targetWindow = aElement.ownerDocument.defaultView; + var scale = targetWindow.devicePixelRatio; + var rect = aElement.getBoundingClientRect(); + return { + x: (targetWindow.mozInnerScreenX + rect.left) * scale, + y: (targetWindow.mozInnerScreenY + rect.top) * scale, + width: rect.width * scale, + height: rect.height * scale, + }; +} + +// Synthesizes a native mousewheel event and returns immediately. This does not +// guarantee anything; you probably want to use one of the other functions below +// which actually wait for results. +// aX and aY are relative to the top-left of |aTarget|'s bounding rect. +// aDeltaX and aDeltaY are pixel deltas, and aObserver can be left undefined +// if not needed. +async function synthesizeNativeWheel( + aTarget, + aX, + aY, + aDeltaX, + aDeltaY, + aObserver +) { + var pt = await coordinatesRelativeToScreen({ + offsetX: aX, + offsetY: aY, + target: aTarget, + }); + if (aDeltaX && aDeltaY) { + throw new Error( + "Simultaneous wheeling of horizontal and vertical is not supported on all platforms." + ); + } + aDeltaX = nativeScrollUnits(aTarget, aDeltaX); + aDeltaY = nativeScrollUnits(aTarget, aDeltaY); + var msg = aDeltaX + ? nativeHorizontalWheelEventMsg() + : nativeVerticalWheelEventMsg(); + var utils = utilsForTarget(aTarget); + var element = elementForTarget(aTarget); + utils.sendNativeMouseScrollEvent( + pt.x, + pt.y, + msg, + aDeltaX, + aDeltaY, + 0, + 0, + // Specify MOUSESCROLL_SCROLL_LINES if the test wants to run through wheel + // input code path on Mac since it's normal mouse wheel inputs. + SpecialPowers.getBoolPref("apz.test.mac.synth_wheel_input", false) + ? SpecialPowers.DOMWindowUtils.MOUSESCROLL_SCROLL_LINES + : 0, + element, + aObserver + ); + return true; +} + +// Synthesizes a native pan gesture event and returns immediately. +// NOTE: This works only on Mac. +// You can specify kCGScrollPhaseBegan = 1, kCGScrollPhaseChanged = 2 and +// kCGScrollPhaseEnded = 4 for |aPhase|. +async function synthesizeNativePanGestureEvent( + aTarget, + aX, + aY, + aDeltaX, + aDeltaY, + aPhase, + aObserver +) { + if (getPlatform() != "mac") { + throw new Error( + `synthesizeNativePanGestureEvent doesn't work on ${getPlatform()}` + ); + } + + var pt = await coordinatesRelativeToScreen({ + offsetX: aX, + offsetY: aY, + target: aTarget, + }); + if (aDeltaX && aDeltaY) { + throw new Error( + "Simultaneous panning of horizontal and vertical is not supported." + ); + } + + aDeltaX = nativeScrollUnits(aTarget, aDeltaX); + aDeltaY = nativeScrollUnits(aTarget, aDeltaY); + + var element = elementForTarget(aTarget); + var utils = utilsForTarget(aTarget); + utils.sendNativeMouseScrollEvent( + pt.x, + pt.y, + aPhase, + aDeltaX, + aDeltaY, + 0 /* deltaZ */, + 0 /* modifiers */, + 0 /* scroll event unit pixel */, + element, + aObserver + ); + + return true; +} + +// Sends a native touchpad pan event and resolve the returned promise once the +// request has been successfully made to the OS. +// NOTE: This works only on Windows and Linux. +// You can specify nsIDOMWindowUtils.PHASE_BEGIN, PHASE_UPDATE and PHASE_END +// for |aPhase|. +async function promiseNativeTouchpadPanEventAndWaitForObserver( + aTarget, + aX, + aY, + aDeltaX, + aDeltaY, + aPhase +) { + if (getPlatform() != "windows" && getPlatform() != "linux") { + throw new Error( + `promiseNativeTouchpadPanEventAndWaitForObserver doesn't work on ${getPlatform()}` + ); + } + + let pt = await coordinatesRelativeToScreen({ + offsetX: aX, + offsetY: aY, + target: aTarget, + }); + + const utils = utilsForTarget(aTarget); + + return new Promise(resolve => { + var observer = { + observe(aSubject, aTopic, aData) { + if (aTopic == "touchpadpanevent") { + resolve(); + } + }, + }; + + utils.sendNativeTouchpadPan( + aPhase, + pt.x, + pt.y, + aDeltaX, + aDeltaY, + 0, + observer + ); + }); +} + +async function synthesizeSimpleGestureEvent( + aElement, + aType, + aX, + aY, + aDirection, + aDelta, + aModifiers, + aClickCount +) { + let pt = await coordinatesRelativeToScreen({ + offsetX: aX, + offsetY: aY, + target: aElement, + }); + + let utils = utilsForTarget(aElement); + utils.sendSimpleGestureEvent( + aType, + pt.x, + pt.y, + aDirection, + aDelta, + aModifiers, + aClickCount + ); +} + +// Synthesizes a native pan gesture event and resolve the returned promise once the +// request has been successfully made to the OS. +function promiseNativePanGestureEventAndWaitForObserver( + aElement, + aX, + aY, + aDeltaX, + aDeltaY, + aPhase +) { + return new Promise(resolve => { + var observer = { + observe(aSubject, aTopic, aData) { + if (aTopic == "mousescrollevent") { + resolve(); + } + }, + }; + synthesizeNativePanGestureEvent( + aElement, + aX, + aY, + aDeltaX, + aDeltaY, + aPhase, + observer + ); + }); +} + +// Synthesizes a native mousewheel event and resolve the returned promise once the +// request has been successfully made to the OS. This does not necessarily +// guarantee that the OS generates the event we requested. See +// synthesizeNativeWheel for details on the parameters. +function promiseNativeWheelAndWaitForObserver( + aElement, + aX, + aY, + aDeltaX, + aDeltaY +) { + return new Promise(resolve => { + var observer = { + observe(aSubject, aTopic, aData) { + if (aTopic == "mousescrollevent") { + resolve(); + } + }, + }; + synthesizeNativeWheel(aElement, aX, aY, aDeltaX, aDeltaY, observer); + }); +} + +// Synthesizes a native mousewheel event and resolve the returned promise once the +// wheel event is dispatched to |aTarget|'s containing window. If the event +// targets content in a subdocument, |aTarget| should be inside the +// subdocument (or the subdocument's window). See synthesizeNativeWheel for +// details on the other parameters. +function promiseNativeWheelAndWaitForWheelEvent( + aTarget, + aX, + aY, + aDeltaX, + aDeltaY +) { + return new Promise((resolve, reject) => { + var targetWindow = windowForTarget(aTarget); + targetWindow.addEventListener( + "wheel", + function (e) { + setTimeout(resolve, 0); + }, + { once: true } + ); + try { + synthesizeNativeWheel(aTarget, aX, aY, aDeltaX, aDeltaY); + } catch (e) { + reject(e); + } + }); +} + +// Synthesizes a native mousewheel event and resolves the returned promise once the +// first resulting scroll event is dispatched to |aTarget|'s containing window. +// If the event targets content in a subdocument, |aTarget| should be inside +// the subdocument (or the subdocument's window). See synthesizeNativeWheel +// for details on the other parameters. +function promiseNativeWheelAndWaitForScrollEvent( + aTarget, + aX, + aY, + aDeltaX, + aDeltaY +) { + return new Promise((resolve, reject) => { + var targetWindow = windowForTarget(aTarget); + targetWindow.addEventListener( + "scroll", + function () { + setTimeout(resolve, 0); + }, + { capture: true, once: true } + ); // scroll events don't always bubble + try { + synthesizeNativeWheel(aTarget, aX, aY, aDeltaX, aDeltaY); + } catch (e) { + reject(e); + } + }); +} + +async function synthesizeTouchpadPinch(scales, focusX, focusY, options) { + var scalesAndFoci = []; + + for (let i = 0; i < scales.length; i++) { + scalesAndFoci.push([scales[i], focusX, focusY]); + } + + await synthesizeTouchpadGesture(scalesAndFoci, options); +} + +// scalesAndFoci is an array of [scale, focusX, focuxY] tuples. +async function synthesizeTouchpadGesture(scalesAndFoci, options) { + // Check for options, fill in defaults if appropriate. + let waitForTransformEnd = + options.waitForTransformEnd !== undefined + ? options.waitForTransformEnd + : true; + let waitForFrames = + options.waitForFrames !== undefined ? options.waitForFrames : false; + + // Register the listener for the TransformEnd observer topic + let transformEndPromise = promiseTransformEnd(); + + var modifierFlags = 0; + var utils = utilsForTarget(document.body); + for (let i = 0; i < scalesAndFoci.length; i++) { + var pt = await coordinatesRelativeToScreen({ + offsetX: scalesAndFoci[i][1], + offsetY: scalesAndFoci[i][2], + target: document.body, + }); + var phase; + if (i === 0) { + phase = SpecialPowers.DOMWindowUtils.PHASE_BEGIN; + } else if (i === scalesAndFoci.length - 1) { + phase = SpecialPowers.DOMWindowUtils.PHASE_END; + } else { + phase = SpecialPowers.DOMWindowUtils.PHASE_UPDATE; + } + utils.sendNativeTouchpadPinch( + phase, + scalesAndFoci[i][0], + pt.x, + pt.y, + modifierFlags + ); + if (waitForFrames) { + await promiseFrame(); + } + } + + // Wait for TransformEnd to fire. + if (waitForTransformEnd) { + await transformEndPromise; + } +} + +async function synthesizeTouchpadPan( + focusX, + focusY, + deltaXs, + deltaYs, + options +) { + // Check for options, fill in defaults if appropriate. + let waitForTransformEnd = + options.waitForTransformEnd !== undefined + ? options.waitForTransformEnd + : true; + let waitForFrames = + options.waitForFrames !== undefined ? options.waitForFrames : false; + + // Register the listener for the TransformEnd observer topic + let transformEndPromise = promiseTransformEnd(); + + var modifierFlags = 0; + var pt = await coordinatesRelativeToScreen({ + offsetX: focusX, + offsetY: focusY, + target: document.body, + }); + var utils = utilsForTarget(document.body); + for (let i = 0; i < deltaXs.length; i++) { + var phase; + if (i === 0) { + phase = SpecialPowers.DOMWindowUtils.PHASE_BEGIN; + } else if (i === deltaXs.length - 1) { + phase = SpecialPowers.DOMWindowUtils.PHASE_END; + } else { + phase = SpecialPowers.DOMWindowUtils.PHASE_UPDATE; + } + utils.sendNativeTouchpadPan( + phase, + pt.x, + pt.y, + deltaXs[i], + deltaYs[i], + modifierFlags + ); + if (waitForFrames) { + await promiseFrame(); + } + } + + // Wait for TransformEnd to fire. + if (waitForTransformEnd) { + await transformEndPromise; + } +} + +// Synthesizes a native touch event and dispatches it. aX and aY in CSS pixels +// relative to the top-left of |aTarget|'s bounding rect. +async function synthesizeNativeTouch( + aTarget, + aX, + aY, + aType, + aObserver = null, + aTouchId = 0 +) { + var pt = await coordinatesRelativeToScreen({ + offsetX: aX, + offsetY: aY, + target: aTarget, + }); + var utils = utilsForTarget(aTarget); + utils.sendNativeTouchPoint(aTouchId, aType, pt.x, pt.y, 1, 90, aObserver); + return true; +} + +function sendBasicNativePointerInput( + utils, + aId, + aPointerType, + aState, + aX, + aY, + aObserver, + { pressure = 1, twist = 0, tiltX = 0, tiltY = 0, button = 0 } = {} +) { + switch (aPointerType) { + case "touch": + utils.sendNativeTouchPoint(aId, aState, aX, aY, pressure, 90, aObserver); + break; + case "pen": + utils.sendNativePenInput( + aId, + aState, + aX, + aY, + pressure, + twist, + tiltX, + tiltY, + button, + aObserver + ); + break; + default: + throw new Error(`Not supported: ${aPointerType}`); + } +} + +async function promiseNativePointerInput( + aTarget, + aPointerType, + aState, + aX, + aY, + options +) { + const pt = await coordinatesRelativeToScreen({ + offsetX: aX, + offsetY: aY, + target: aTarget, + }); + const utils = utilsForTarget(aTarget); + return new Promise(resolve => { + sendBasicNativePointerInput( + utils, + options?.pointerId ?? 0, + aPointerType, + aState, + pt.x, + pt.y, + resolve, + options + ); + }); +} + +/** + * Function to generate native pointer events as a sequence. + * @param aTarget is the element or window whose bounding rect the coordinates are + * relative to. + * @param aPointerType "touch" or "pen". + * @param aPositions is a 2D array of position data. It is indexed as [row][column], + * where advancing the row counter moves forward in time, and each column + * represents a single pointer. Each row must have exactly + * the same number of columns, and the number of columns must match the length + * of the aPointerIds parameter. + * For each row, each entry is either an object with x and y fields, + * or a null. A null value indicates that the pointer should be "lifted" + * (i.e. send a touchend for that touch input). A non-null value therefore + * indicates the position of the pointer input. + * This function takes care of the state tracking necessary to send + * pointerup/pointerdown inputs as necessary as the pointers go up and down. + * @param aObserver is the observer that will get registered on the very last + * native pointer synthesis call this function makes. + * @param aPointerIds is an array holding the pointer ID values. + */ +async function synthesizeNativePointerSequences( + aTarget, + aPointerType, + aPositions, + aObserver = null, + aPointerIds = [0], + options +) { + // We use lastNonNullValue to figure out which synthesizeNativeTouch call + // will be the last one we make, so that we can register aObserver on it. + var lastNonNullValue = -1; + for (let i = 0; i < aPositions.length; i++) { + if (aPositions[i] == null) { + throw new Error(`aPositions[${i}] was unexpectedly null`); + } + if (aPositions[i].length != aPointerIds.length) { + throw new Error( + `aPositions[${i}] did not have the expected number of positions; ` + + `expected ${aPointerIds.length} pointers but found ${aPositions[i].length}` + ); + } + for (let j = 0; j < aPointerIds.length; j++) { + if (aPositions[i][j] != null) { + lastNonNullValue = i * aPointerIds.length + j; + // Do the conversion to screen space before actually synthesizing + // the events, otherwise the screen space may change as a result of + // the touch inputs and the conversion may not work as intended. + aPositions[i][j] = await coordinatesRelativeToScreen({ + offsetX: aPositions[i][j].x, + offsetY: aPositions[i][j].y, + target: aTarget, + }); + } + } + } + if (lastNonNullValue < 0) { + throw new Error("All values in positions array were null!"); + } + + // Insert a row of nulls at the end of aPositions, to ensure that all + // touches get removed. If the touches have already been removed this will + // just add an extra no-op iteration in the aPositions loop below. + var allNullRow = new Array(aPointerIds.length); + allNullRow.fill(null); + aPositions.push(allNullRow); + + // The last sendNativeTouchPoint call will be the TOUCH_REMOVE which happens + // one iteration of aPosition after the last non-null value. + var lastSynthesizeCall = lastNonNullValue + aPointerIds.length; + + // track which touches are down and which are up. start with all up + var currentPositions = new Array(aPointerIds.length); + currentPositions.fill(null); + + var utils = utilsForTarget(aTarget); + // Iterate over the position data now, and generate the touches requested + for (let i = 0; i < aPositions.length; i++) { + for (let j = 0; j < aPointerIds.length; j++) { + if (aPositions[i][j] == null) { + // null means lift the finger + if (currentPositions[j] == null) { + // it's already lifted, do nothing + } else { + // synthesize the touch-up. If this is the last call we're going to + // make, pass the observer as well + var thisIndex = i * aPointerIds.length + j; + var observer = lastSynthesizeCall == thisIndex ? aObserver : null; + sendBasicNativePointerInput( + utils, + aPointerIds[j], + aPointerType, + SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, + currentPositions[j].x, + currentPositions[j].y, + observer, + options + ); + currentPositions[j] = null; + } + } else { + sendBasicNativePointerInput( + utils, + aPointerIds[j], + aPointerType, + SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, + aPositions[i][j].x, + aPositions[i][j].y, + null, + options + ); + currentPositions[j] = aPositions[i][j]; + } + } + } + return true; +} + +async function synthesizeNativeTouchSequences( + aTarget, + aPositions, + aObserver = null, + aTouchIds = [0] +) { + await synthesizeNativePointerSequences( + aTarget, + "touch", + aPositions, + aObserver, + aTouchIds + ); +} + +async function synthesizeNativePointerDrag( + aTarget, + aPointerType, + aX, + aY, + aDeltaX, + aDeltaY, + aObserver = null, + aPointerId = 0, + options +) { + var steps = Math.max(Math.abs(aDeltaX), Math.abs(aDeltaY)); + var positions = [[{ x: aX, y: aY }]]; + for (var i = 1; i < steps; i++) { + var dx = i * (aDeltaX / steps); + var dy = i * (aDeltaY / steps); + var pos = { x: aX + dx, y: aY + dy }; + positions.push([pos]); + } + positions.push([{ x: aX + aDeltaX, y: aY + aDeltaY }]); + return synthesizeNativePointerSequences( + aTarget, + aPointerType, + positions, + aObserver, + [aPointerId], + options + ); +} + +// Note that when calling this function you'll want to make sure that the pref +// "apz.touch_start_tolerance" is set to 0, or some of the touchmove will get +// consumed to overcome the panning threshold. +async function synthesizeNativeTouchDrag( + aTarget, + aX, + aY, + aDeltaX, + aDeltaY, + aObserver = null, + aTouchId = 0 +) { + return synthesizeNativePointerDrag( + aTarget, + "touch", + aX, + aY, + aDeltaX, + aDeltaY, + aObserver, + aTouchId + ); +} + +function promiseNativePointerDrag( + aTarget, + aPointerType, + aX, + aY, + aDeltaX, + aDeltaY, + aPointerId = 0, + options +) { + return new Promise(resolve => { + synthesizeNativePointerDrag( + aTarget, + aPointerType, + aX, + aY, + aDeltaX, + aDeltaY, + resolve, + aPointerId, + options + ); + }); +} + +// Promise-returning variant of synthesizeNativeTouchDrag +function promiseNativeTouchDrag( + aTarget, + aX, + aY, + aDeltaX, + aDeltaY, + aTouchId = 0 +) { + return new Promise(resolve => { + synthesizeNativeTouchDrag( + aTarget, + aX, + aY, + aDeltaX, + aDeltaY, + resolve, + aTouchId + ); + }); +} + +// Tapping is essentially a dragging with no move +function promiseNativePointerTap(aTarget, aPointerType, aX, aY, options) { + return promiseNativePointerDrag( + aTarget, + aPointerType, + aX, + aY, + 0, + 0, + options?.pointerId ?? 0, + options + ); +} + +async function synthesizeNativeTap(aTarget, aX, aY, aObserver = null) { + var pt = await coordinatesRelativeToScreen({ + offsetX: aX, + offsetY: aY, + target: aTarget, + }); + let utils = utilsForTarget(aTarget); + utils.sendNativeTouchTap(pt.x, pt.y, false, aObserver); + return true; +} + +// only currently implemented on macOS +async function synthesizeNativeTouchpadDoubleTap(aTarget, aX, aY) { + ok( + getPlatform() == "mac", + "only implemented on mac. implement sendNativeTouchpadDoubleTap for this platform," + + " see bug 1696802 for how it was done on macOS" + ); + let pt = await coordinatesRelativeToScreen({ + offsetX: aX, + offsetY: aY, + target: aTarget, + }); + let utils = utilsForTarget(aTarget); + utils.sendNativeTouchpadDoubleTap(pt.x, pt.y, 0); + return true; +} + +// If the event targets content in a subdocument, |aTarget| should be inside the +// subdocument (or the subdocument window). +async function synthesizeNativeMouseEventWithAPZ(aParams, aObserver = null) { + if (aParams.win !== undefined) { + throw Error( + "Are you trying to use EventUtils' API? `win` won't be used with synthesizeNativeMouseClickWithAPZ." + ); + } + if (aParams.scale !== undefined) { + throw Error( + "Are you trying to use EventUtils' API? `scale` won't be used with synthesizeNativeMouseClickWithAPZ." + ); + } + if (aParams.elementOnWidget !== undefined) { + throw Error( + "Are you trying to use EventUtils' API? `elementOnWidget` won't be used with synthesizeNativeMouseClickWithAPZ." + ); + } + const { + type, // "click", "mousedown", "mouseup" or "mousemove" + target, // Origin of offsetX and offsetY, must be an element + offsetX, // X offset in `target` in CSS Pixels + offsetY, // Y offset in `target` in CSS pixels + atCenter, // Instead of offsetX/Y, synthesize the event at center of `target` + screenX, // X offset in screen in device pixels, offsetX/Y nor atCenter must not be set if this is set + screenY, // Y offset in screen in device pixels, offsetX/Y nor atCenter must not be set if this is set + button = 0, // if "click", "mousedown", "mouseup", set same value as DOM MouseEvent.button + modifiers = {}, // Active modifiers, see `parseNativeModifiers` + } = aParams; + if (atCenter) { + if (offsetX != undefined || offsetY != undefined) { + throw Error( + `atCenter is specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified` + ); + } + if (screenX != undefined || screenY != undefined) { + throw Error( + `atCenter is specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified` + ); + } + } else if (offsetX != undefined && offsetY != undefined) { + if (screenX != undefined || screenY != undefined) { + throw Error( + `offsetX/Y are specified, but screenX (${screenX}) and/or screenY (${screenY}) are also specified` + ); + } + } else if (screenX != undefined && screenY != undefined) { + if (offsetX != undefined || offsetY != undefined) { + throw Error( + `screenX/Y are specified, but offsetX (${offsetX}) and/or offsetY (${offsetY}) are also specified` + ); + } + } + const pt = await (async () => { + if (screenX != undefined) { + return { x: screenX, y: screenY }; + } + return coordinatesRelativeToScreen({ + offsetX, + offsetY, + atCenter, + target, + }); + })(); + const utils = utilsForTarget(target); + const element = elementForTarget(target); + const modifierFlags = parseNativeModifiers(modifiers); + if (type === "click") { + utils.sendNativeMouseEvent( + pt.x, + pt.y, + utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN, + button, + modifierFlags, + element, + function () { + utils.sendNativeMouseEvent( + pt.x, + pt.y, + utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP, + button, + modifierFlags, + element, + aObserver + ); + } + ); + return; + } + + utils.sendNativeMouseEvent( + pt.x, + pt.y, + (() => { + switch (type) { + case "mousedown": + return utils.NATIVE_MOUSE_MESSAGE_BUTTON_DOWN; + case "mouseup": + return utils.NATIVE_MOUSE_MESSAGE_BUTTON_UP; + case "mousemove": + return utils.NATIVE_MOUSE_MESSAGE_MOVE; + default: + throw Error(`Invalid type is specified: ${type}`); + } + })(), + button, + modifierFlags, + element, + aObserver + ); +} + +function promiseNativeMouseEventWithAPZ(aParams) { + return new Promise(resolve => + synthesizeNativeMouseEventWithAPZ(aParams, resolve) + ); +} + +// See synthesizeNativeMouseEventWithAPZ for the detail of aParams. +function promiseNativeMouseEventWithAPZAndWaitForEvent(aParams) { + return new Promise(resolve => { + const targetWindow = windowForTarget(aParams.target); + const eventType = aParams.eventTypeToWait || aParams.type; + targetWindow.addEventListener(eventType, resolve, { + once: true, + }); + synthesizeNativeMouseEventWithAPZ(aParams); + }); +} + +// Move the mouse to (dx, dy) relative to |target|, and scroll the wheel +// at that location. +// Moving the mouse is necessary to avoid wheel events from two consecutive +// promiseMoveMouseAndScrollWheelOver() calls on different elements being incorrectly +// considered as part of the same wheel transaction. +// We also wait for the mouse move event to be processed before sending the +// wheel event, otherwise there is a chance they might get reordered, and +// we have the transaction problem again. +// This function returns a promise that is resolved when the resulting wheel +// (if waitForScroll = false) or scroll (if waitForScroll = true) event is +// received. +function promiseMoveMouseAndScrollWheelOver( + target, + dx, + dy, + waitForScroll = true, + scrollDelta = 10 +) { + let p = promiseNativeMouseEventWithAPZAndWaitForEvent({ + type: "mousemove", + target, + offsetX: dx, + offsetY: dy, + }); + if (waitForScroll) { + p = p.then(() => { + return promiseNativeWheelAndWaitForScrollEvent( + target, + dx, + dy, + 0, + -scrollDelta + ); + }); + } else { + p = p.then(() => { + return promiseNativeWheelAndWaitForWheelEvent( + target, + dx, + dy, + 0, + -scrollDelta + ); + }); + } + return p; +} + +async function scrollbarDragStart(aTarget, aScaleFactor) { + var targetElement = elementForTarget(aTarget); + var w = {}, + h = {}; + utilsForTarget(aTarget).getScrollbarSizes(targetElement, w, h); + var verticalScrollbarWidth = w.value; + if (verticalScrollbarWidth == 0) { + return null; + } + + var upArrowHeight = verticalScrollbarWidth; // assume square scrollbar buttons + var startX = targetElement.clientWidth + verticalScrollbarWidth / 2; + var startY = upArrowHeight + 5; // start dragging somewhere in the thumb + startX *= aScaleFactor; + startY *= aScaleFactor; + + // targetElement.clientWidth is unaffected by the zoom, but if the target + // is the root content window, the distance from the window origin to the + // scrollbar in CSS pixels does decrease proportionally to the zoom, + // so the CSS coordinates we return need to be scaled accordingly. + if (targetIsTopWindow(aTarget)) { + var resolution = await getResolution(); + startX /= resolution; + startY /= resolution; + } + + return { x: startX, y: startY }; +} + +// Synthesizes events to drag |target|'s vertical scrollbar by the distance +// specified, synthesizing a mousemove for each increment as specified. +// Returns null if the element doesn't have a vertical scrollbar. Otherwise, +// returns an async function that should be invoked after the mousemoves have been +// processed by the widget code, to end the scrollbar drag. Mousemoves being +// processed by the widget code can be detected by listening for the mousemove +// events in the caller, or for some other event that is triggered by the +// mousemove, such as the scroll event resulting from the scrollbar drag. +// The aScaleFactor argument should be provided if the scrollframe has been +// scaled by an enclosing CSS transform. (TODO: this is a workaround for the +// fact that coordinatesRelativeToScreen is supposed to do this automatically +// but it currently does not). +// Note: helper_scrollbar_snap_bug1501062.html contains a copy of this code +// with modifications. Fixes here should be copied there if appropriate. +// |target| can be an element (for subframes) or a window (for root frames). +async function promiseVerticalScrollbarDrag( + aTarget, + aDistance = 20, + aIncrement = 5, + aScaleFactor = 1 +) { + var startPoint = await scrollbarDragStart(aTarget, aScaleFactor); + var targetElement = elementForTarget(aTarget); + if (startPoint == null) { + return null; + } + + dump( + "Starting drag at " + + startPoint.x + + ", " + + startPoint.y + + " from top-left of #" + + targetElement.id + + "\n" + ); + + // Move the mouse to the scrollbar thumb and drag it down + await promiseNativeMouseEventWithAPZ({ + target: aTarget, + offsetX: startPoint.x, + offsetY: startPoint.y, + type: "mousemove", + }); + // mouse down + await promiseNativeMouseEventWithAPZ({ + target: aTarget, + offsetX: startPoint.x, + offsetY: startPoint.y, + type: "mousedown", + }); + // drag vertically by |aIncrement| until we reach the specified distance + for (var y = aIncrement; y < aDistance; y += aIncrement) { + await promiseNativeMouseEventWithAPZ({ + target: aTarget, + offsetX: startPoint.x, + offsetY: startPoint.y + y, + type: "mousemove", + }); + } + await promiseNativeMouseEventWithAPZ({ + target: aTarget, + offsetX: startPoint.x, + offsetY: startPoint.y + aDistance, + type: "mousemove", + }); + + // and return an async function to call afterwards to finish up the drag + return async function () { + dump("Finishing drag of #" + targetElement.id + "\n"); + await promiseNativeMouseEventWithAPZ({ + target: aTarget, + offsetX: startPoint.x, + offsetY: startPoint.y + aDistance, + type: "mouseup", + }); + }; +} + +// This is similar to promiseVerticalScrollbarDrag except this triggers +// the vertical scrollbar drag with a touch drag input. This function +// returns true if a scrollbar was present and false if no scrollbar +// was found for the given element. +async function promiseVerticalScrollbarTouchDrag( + aTarget, + aDistance = 20, + aScaleFactor = 1 +) { + var startPoint = await scrollbarDragStart(aTarget, aScaleFactor); + var targetElement = elementForTarget(aTarget); + if (startPoint == null) { + return false; + } + + dump( + "Starting touch drag at " + + startPoint.x + + ", " + + startPoint.y + + " from top-left of #" + + targetElement.id + + "\n" + ); + + await promiseNativeTouchDrag( + aTarget, + startPoint.x, + startPoint.y, + 0, + aDistance + ); + + return true; +} + +// Synthesizes a native mouse drag, starting at offset (mouseX, mouseY) from +// the given target. The drag occurs in the given number of steps, to a final +// destination of (mouseX + distanceX, mouseY + distanceY) from the target. +// Returns a promise (wrapped in a function, so it doesn't execute immediately) +// that should be awaited after the mousemoves have been processed by the widget +// code, to end the drag. This is important otherwise the OS can sometimes +// reorder the events and the drag doesn't have the intended effect (see +// bug 1368603). +// Example usage: +// let dragFinisher = await promiseNativeMouseDrag(myElement, 0, 0); +// await myIndicationThatDragHadAnEffect; +// await dragFinisher(); +async function promiseNativeMouseDrag( + target, + mouseX, + mouseY, + distanceX = 20, + distanceY = 20, + steps = 20 +) { + var targetElement = elementForTarget(target); + dump( + "Starting drag at " + + mouseX + + ", " + + mouseY + + " from top-left of #" + + targetElement.id + + "\n" + ); + + // Move the mouse to the target position + await promiseNativeMouseEventWithAPZ({ + target, + offsetX: mouseX, + offsetY: mouseY, + type: "mousemove", + }); + // mouse down + await promiseNativeMouseEventWithAPZ({ + target, + offsetX: mouseX, + offsetY: mouseY, + type: "mousedown", + }); + // drag vertically by |increment| until we reach the specified distance + for (var s = 1; s <= steps; s++) { + let dx = distanceX * (s / steps); + let dy = distanceY * (s / steps); + dump(`Dragging to ${mouseX + dx}, ${mouseY + dy} from target\n`); + await promiseNativeMouseEventWithAPZ({ + target, + offsetX: mouseX + dx, + offsetY: mouseY + dy, + type: "mousemove", + }); + } + + // and return a function-wrapped promise to call afterwards to finish the drag + return function () { + return promiseNativeMouseEventWithAPZ({ + target, + offsetX: mouseX + distanceX, + offsetY: mouseY + distanceY, + type: "mouseup", + }); + }; +} + +// Synthesizes a native touch sequence of events corresponding to a pinch-zoom-in +// at the given focus point. The focus point must be specified in CSS coordinates +// relative to the document body. +async function pinchZoomInTouchSequence(focusX, focusY) { + // prettier-ignore + var zoom_in = [ + [ { x: focusX - 25, y: focusY - 50 }, { x: focusX + 25, y: focusY + 50 } ], + [ { x: focusX - 30, y: focusY - 80 }, { x: focusX + 30, y: focusY + 80 } ], + [ { x: focusX - 35, y: focusY - 110 }, { x: focusX + 40, y: focusY + 110 } ], + [ { x: focusX - 40, y: focusY - 140 }, { x: focusX + 45, y: focusY + 140 } ], + [ { x: focusX - 45, y: focusY - 170 }, { x: focusX + 50, y: focusY + 170 } ], + [ { x: focusX - 50, y: focusY - 200 }, { x: focusX + 55, y: focusY + 200 } ], + ]; + + var touchIds = [0, 1]; + return synthesizeNativeTouchSequences(document.body, zoom_in, null, touchIds); +} + +// Returns a promise that is resolved when the observer service dispatches a +// message with the given topic. +function promiseTopic(aTopic) { + return new Promise((resolve, reject) => { + SpecialPowers.Services.obs.addObserver(function observer( + subject, + topic, + data + ) { + try { + SpecialPowers.Services.obs.removeObserver(observer, topic); + resolve([subject, data]); + } catch (ex) { + SpecialPowers.Services.obs.removeObserver(observer, topic); + reject(ex); + } + }, + aTopic); + }); +} + +// Returns a promise that is resolved when a APZ transform ends. +function promiseTransformEnd() { + return promiseTopic("APZ:TransformEnd"); +} + +function promiseScrollend(aTarget = window) { + return promiseOneEvent(aTarget, "scrollend"); +} + +// Returns a promise that resolves after the indicated number +// of touchend events have fired on the given target element. +function promiseTouchEnd(element, count = 1) { + return new Promise(resolve => { + var eventCount = 0; + var counterFunction = function (e) { + eventCount++; + if (eventCount == count) { + element.removeEventListener("touchend", counterFunction, { + passive: true, + }); + resolve(); + } + }; + element.addEventListener("touchend", counterFunction, { passive: true }); + }); +} + +// This generates a touch-based pinch zoom-in gesture that is expected +// to succeed. It returns after APZ has completed the zoom and reaches the end +// of the transform. The focus point is expected to be in CSS coordinates +// relative to the document body. +async function pinchZoomInWithTouch(focusX, focusY) { + // Register the listener for the TransformEnd observer topic + let transformEndPromise = promiseTopic("APZ:TransformEnd"); + + // Dispatch all the touch events + await pinchZoomInTouchSequence(focusX, focusY); + + // Wait for TransformEnd to fire. + await transformEndPromise; +} +// This generates a touchpad pinch zoom-in gesture that is expected +// to succeed. It returns after APZ has completed the zoom and reaches the end +// of the transform. The focus point is expected to be in CSS coordinates +// relative to the document body. +async function pinchZoomInWithTouchpad(focusX, focusY, options = {}) { + var zoomIn = [ + 1.0, 1.019531, 1.035156, 1.037156, 1.039156, 1.054688, 1.056688, 1.070312, + 1.072312, 1.089844, 1.091844, 1.109375, 1.128906, 1.144531, 1.160156, + 1.175781, 1.191406, 1.207031, 1.222656, 1.234375, 1.246094, 1.261719, + 1.273438, 1.285156, 1.296875, 1.3125, 1.328125, 1.347656, 1.363281, + 1.382812, 1.402344, 1.421875, 1.0, + ]; + await synthesizeTouchpadPinch(zoomIn, focusX, focusY, options); +} + +async function pinchZoomInAndPanWithTouchpad(options = {}) { + var x = 584; + var y = 347; + var scalesAndFoci = []; + // Zoom + for (var scale = 1.0; scale <= 2.0; scale += 0.2) { + scalesAndFoci.push([scale, x, y]); + } + // Pan (due to a limitation of the current implementation, events + // for which the scale doesn't change are dropped, so vary the + // scale slightly as well). + for (var i = 1; i <= 20; i++) { + x -= 4; + y -= 5; + scalesAndFoci.push([scale + 0.01 * i, x, y]); + } + await synthesizeTouchpadGesture(scalesAndFoci, options); +} + +async function pinchZoomOutWithTouchpad(focusX, focusY, options = {}) { + // The last item equal one to indicate scale end + var zoomOut = [ + 1.0, 1.375, 1.359375, 1.339844, 1.316406, 1.296875, 1.277344, 1.257812, + 1.238281, 1.21875, 1.199219, 1.175781, 1.15625, 1.132812, 1.101562, + 1.078125, 1.054688, 1.03125, 1.011719, 0.992188, 0.972656, 0.953125, + 0.933594, 1.0, + ]; + await synthesizeTouchpadPinch(zoomOut, focusX, focusY, options); +} + +async function pinchZoomInOutWithTouchpad(focusX, focusY, options = {}) { + // Use the same scale for two events in a row to make sure the code handles this properly. + var zoomInOut = [ + 1.0, 1.082031, 1.089844, 1.097656, 1.101562, 1.109375, 1.121094, 1.128906, + 1.128906, 1.125, 1.097656, 1.074219, 1.054688, 1.035156, 1.015625, 1.0, 1.0, + ]; + await synthesizeTouchpadPinch(zoomInOut, focusX, focusY, options); +} +// This generates a touch-based pinch gesture that is expected to succeed +// and trigger an APZ:TransformEnd observer notification. +// It returns after that notification has been dispatched. +// The coordinates of touch events in `touchSequence` are expected to be +// in CSS coordinates relative to the document body. +async function synthesizeNativeTouchAndWaitForTransformEnd( + touchSequence, + touchIds +) { + // Register the listener for the TransformEnd observer topic + let transformEndPromise = promiseTopic("APZ:TransformEnd"); + + // Dispatch all the touch events + await synthesizeNativeTouchSequences( + document.body, + touchSequence, + null, + touchIds + ); + + // Wait for TransformEnd to fire. + await transformEndPromise; +} + +// Returns a touch sequence for a pinch-zoom-out operation in the center +// of the visual viewport. The touch sequence returned is in CSS coordinates +// relative to the document body. +function pinchZoomOutTouchSequenceAtCenter() { + // Divide the half of visual viewport size by 8, then cause touch events + // starting from the 7th furthest away from the center towards the center. + const deltaX = window.visualViewport.width / 16; + const deltaY = window.visualViewport.height / 16; + const centerX = + window.visualViewport.pageLeft + window.visualViewport.width / 2; + const centerY = + window.visualViewport.pageTop + window.visualViewport.height / 2; + // prettier-ignore + var zoom_out = [ + [ { x: centerX - (deltaX * 6), y: centerY - (deltaY * 6) }, + { x: centerX + (deltaX * 6), y: centerY + (deltaY * 6) } ], + [ { x: centerX - (deltaX * 5), y: centerY - (deltaY * 5) }, + { x: centerX + (deltaX * 5), y: centerY + (deltaY * 5) } ], + [ { x: centerX - (deltaX * 4), y: centerY - (deltaY * 4) }, + { x: centerX + (deltaX * 4), y: centerY + (deltaY * 4) } ], + [ { x: centerX - (deltaX * 3), y: centerY - (deltaY * 3) }, + { x: centerX + (deltaX * 3), y: centerY + (deltaY * 3) } ], + [ { x: centerX - (deltaX * 2), y: centerY - (deltaY * 2) }, + { x: centerX + (deltaX * 2), y: centerY + (deltaY * 2) } ], + [ { x: centerX - (deltaX * 1), y: centerY - (deltaY * 1) }, + { x: centerX + (deltaX * 1), y: centerY + (deltaY * 1) } ], + ]; + return zoom_out; +} + +// This generates a touch-based pinch zoom-out gesture that is expected +// to succeed. It returns after APZ has completed the zoom and reaches the end +// of the transform. The touch inputs are directed to the center of the +// current visual viewport. +async function pinchZoomOutWithTouchAtCenter() { + var zoom_out = pinchZoomOutTouchSequenceAtCenter(); + var touchIds = [0, 1]; + await synthesizeNativeTouchAndWaitForTransformEnd(zoom_out, touchIds); +} + +// useTouchpad is only currently implemented on macOS +async function synthesizeDoubleTap(element, x, y, useTouchpad) { + if (useTouchpad) { + await synthesizeNativeTouchpadDoubleTap(element, x, y); + } else { + await synthesizeNativeTap(element, x, y); + await synthesizeNativeTap(element, x, y); + } +} +// useTouchpad is only currently implemented on macOS +async function doubleTapOn(element, x, y, useTouchpad) { + let transformEndPromise = promiseTransformEnd(); + + await synthesizeDoubleTap(element, x, y, useTouchpad); + + // Wait for the APZ:TransformEnd to fire + await transformEndPromise; + + // Flush state so we can query an accurate resolution + await promiseApzFlushedRepaints(); +} + +const NativePanHandlerForLinux = { + beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN, + updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE, + endPhase: SpecialPowers.DOMWindowUtils.PHASE_END, + promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver, + delta: -50, +}; + +const NativePanHandlerForWindows = { + beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN, + updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE, + endPhase: SpecialPowers.DOMWindowUtils.PHASE_END, + promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver, + delta: 50, +}; + +const NativePanHandlerForMac = { + // From https://developer.apple.com/documentation/coregraphics/cgscrollphase/kcgscrollphasebegan?language=occ , etc. + beginPhase: 1, // kCGScrollPhaseBegan + updatePhase: 2, // kCGScrollPhaseChanged + endPhase: 4, // kCGScrollPhaseEnded + promiseNativePanEvent: promiseNativePanGestureEventAndWaitForObserver, + delta: -50, +}; + +const NativePanHandlerForHeadless = { + beginPhase: SpecialPowers.DOMWindowUtils.PHASE_BEGIN, + updatePhase: SpecialPowers.DOMWindowUtils.PHASE_UPDATE, + endPhase: SpecialPowers.DOMWindowUtils.PHASE_END, + promiseNativePanEvent: promiseNativeTouchpadPanEventAndWaitForObserver, + delta: 50, +}; + +function getPanHandler() { + if (SpecialPowers.isHeadless) { + return NativePanHandlerForHeadless; + } + + switch (getPlatform()) { + case "linux": + return NativePanHandlerForLinux; + case "windows": + return NativePanHandlerForWindows; + case "mac": + return NativePanHandlerForMac; + default: + throw new Error( + "There's no native pan handler on platform " + getPlatform() + ); + } +} + +// Lazily get `NativePanHandler` to avoid an exception where we don't support +// native pan events (e.g. Android). +if (!window.hasOwnProperty("NativePanHandler")) { + Object.defineProperty(window, "NativePanHandler", { + get() { + return getPanHandler(); + }, + }); +} + +async function panRightToLeftBegin(aElement, aX, aY, aMultiplier) { + await NativePanHandler.promiseNativePanEvent( + aElement, + aX, + aY, + NativePanHandler.delta * aMultiplier, + 0, + NativePanHandler.beginPhase + ); +} + +async function panRightToLeftUpdate(aElement, aX, aY, aMultiplier) { + await NativePanHandler.promiseNativePanEvent( + aElement, + aX, + aY, + NativePanHandler.delta * aMultiplier, + 0, + NativePanHandler.updatePhase + ); +} + +async function panRightToLeftEnd(aElement, aX, aY, aMultiplier) { + await NativePanHandler.promiseNativePanEvent( + aElement, + aX, + aY, + 0, + 0, + NativePanHandler.endPhase + ); +} + +async function panRightToLeft(aElement, aX, aY, aMultiplier) { + await panRightToLeftBegin(aElement, aX, aY, aMultiplier); + await panRightToLeftUpdate(aElement, aX, aY, aMultiplier); + await panRightToLeftEnd(aElement, aX, aY, aMultiplier); +} + +async function panLeftToRight(aElement, aX, aY, aMultiplier) { + await panLeftToRightBegin(aElement, aX, aY, aMultiplier); + await panLeftToRightUpdate(aElement, aX, aY, aMultiplier); + await panLeftToRightEnd(aElement, aX, aY, aMultiplier); +} + +async function panLeftToRightBegin(aElement, aX, aY, aMultiplier) { + await NativePanHandler.promiseNativePanEvent( + aElement, + aX, + aY, + -NativePanHandler.delta * aMultiplier, + 0, + NativePanHandler.beginPhase + ); +} + +async function panLeftToRightUpdate(aElement, aX, aY, aMultiplier) { + await NativePanHandler.promiseNativePanEvent( + aElement, + aX, + aY, + -NativePanHandler.delta * aMultiplier, + 0, + NativePanHandler.updatePhase + ); + await NativePanHandler.promiseNativePanEvent( + aElement, + aX, + aY, + -NativePanHandler.delta * aMultiplier, + 0, + NativePanHandler.updatePhase + ); +} + +async function panLeftToRightEnd(aElement, aX, aY, aMultiplier) { + await NativePanHandler.promiseNativePanEvent( + aElement, + aX, + aY, + 0, + 0, + NativePanHandler.endPhase + ); +} diff --git a/gfx/layers/apz/test/mochitest/apz_test_utils.js b/gfx/layers/apz/test/mochitest/apz_test_utils.js new file mode 100644 index 0000000000..1004f8a3d5 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/apz_test_utils.js @@ -0,0 +1,1287 @@ +// Utilities for writing APZ tests using the framework added in bug 961289 + +// ---------------------------------------------------------------------- +// Functions that convert the APZ test data into a more usable form. +// Every place we have a WebIDL sequence whose elements are dictionaries +// with two elements, a key, and a value, we convert this into a JS +// object with a property for each key/value pair. (This is the structure +// we really want, but we can't express in directly in WebIDL.) +// ---------------------------------------------------------------------- + +// getHitTestConfig() expects apz_test_native_event_utils.js to be loaded as well. +/* import-globals-from apz_test_native_event_utils.js */ + +function convertEntries(entries) { + var result = {}; + for (var i = 0; i < entries.length; ++i) { + result[entries[i].key] = entries[i].value; + } + return result; +} + +function parsePoint(str) { + var pieces = str.replace(/[()\s]+/g, "").split(","); + SimpleTest.is(pieces.length, 2, "expected string of form (x,y)"); + for (var i = 0; i < 2; i++) { + var eq = pieces[i].indexOf("="); + if (eq >= 0) { + pieces[i] = pieces[i].substring(eq + 1); + } + } + return { + x: parseInt(pieces[0]), + y: parseInt(pieces[1]), + }; +} + +// Given a VisualViewport object, return the visual viewport +// rect relative to the page. +function getVisualViewportRect(vv) { + return { + x: vv.pageLeft, + y: vv.pageTop, + width: vv.width, + height: vv.height, + }; +} + +// Return the offset of the visual viewport relative to the layout viewport. +function getRelativeViewportOffset(window) { + const offsetX = {}; + const offsetY = {}; + const utils = SpecialPowers.getDOMWindowUtils(window); + utils.getVisualViewportOffsetRelativeToLayoutViewport(offsetX, offsetY); + return { + x: offsetX.value, + y: offsetY.value, + }; +} + +function parseRect(str) { + var pieces = str.replace(/[()\s]+/g, "").split(","); + SimpleTest.is(pieces.length, 4, "expected string of form (x,y,w,h)"); + for (var i = 0; i < 4; i++) { + var eq = pieces[i].indexOf("="); + if (eq >= 0) { + pieces[i] = pieces[i].substring(eq + 1); + } + } + return { + x: parseInt(pieces[0]), + y: parseInt(pieces[1]), + width: parseInt(pieces[2]), + height: parseInt(pieces[3]), + }; +} + +// These functions expect rects with fields named x/y/width/height, such as +// that returned by parseRect(). +function rectContains(haystack, needle) { + return ( + haystack.x <= needle.x && + haystack.y <= needle.y && + haystack.x + haystack.width >= needle.x + needle.width && + haystack.y + haystack.height >= needle.y + needle.height + ); +} +function rectToString(rect) { + return ( + "(" + rect.x + "," + rect.y + "," + rect.width + "," + rect.height + ")" + ); +} +function assertRectContainment( + haystackRect, + haystackDesc, + needleRect, + needleDesc +) { + SimpleTest.ok( + rectContains(haystackRect, needleRect), + haystackDesc + + " " + + rectToString(haystackRect) + + " should contain " + + needleDesc + + " " + + rectToString(needleRect) + ); +} + +function getPropertyAsRect(scrollFrames, scrollId, prop) { + SimpleTest.ok( + scrollId in scrollFrames, + "expected scroll frame data for scroll id " + scrollId + ); + var scrollFrameData = scrollFrames[scrollId]; + SimpleTest.ok( + "displayport" in scrollFrameData, + "expected a " + prop + " for scroll id " + scrollId + ); + var value = scrollFrameData[prop]; + return parseRect(value); +} + +function convertScrollFrameData(scrollFrames) { + var result = {}; + for (var i = 0; i < scrollFrames.length; ++i) { + result[scrollFrames[i].scrollId] = convertEntries(scrollFrames[i].entries); + } + return result; +} + +function convertBuckets(buckets) { + var result = {}; + for (var i = 0; i < buckets.length; ++i) { + result[buckets[i].sequenceNumber] = convertScrollFrameData( + buckets[i].scrollFrames + ); + } + return result; +} + +function convertTestData(testData) { + var result = {}; + result.paints = convertBuckets(testData.paints); + result.repaintRequests = convertBuckets(testData.repaintRequests); + return result; +} + +// Returns the last bucket that has at least one scrollframe. This +// is useful for skipping over buckets that are from empty transactions, +// because those don't contain any useful data. +function getLastNonemptyBucket(buckets) { + for (var i = buckets.length - 1; i >= 0; --i) { + if (buckets[i].scrollFrames.length) { + return buckets[i]; + } + } + return null; +} + +// Takes something like "matrix(1, 0, 0, 1, 234.024, 528.29023)"" and returns a number array +function parseTransform(transform) { + return /matrix\((.*),(.*),(.*),(.*),(.*),(.*)\)/ + .exec(transform) + .slice(1) + .map(parseFloat); +} + +function isTransformClose(a, b, name) { + is( + a.length, + b.length, + `expected transforms ${a} and ${b} to be the same length` + ); + for (let i = 0; i < a.length; i++) { + ok(Math.abs(a[i] - b[i]) < 0.01, name); + } +} + +// Given APZ test data for a single paint on the compositor side, +// reconstruct the APZC tree structure from the 'parentScrollId' +// entries that were logged. More specifically, the subset of the +// APZC tree structure corresponding to the layer subtree for the +// content process that triggered the paint, is reconstructed (as +// the APZ test data only contains information abot this subtree). +function buildApzcTree(paint) { + // The APZC tree can potentially have multiple root nodes, + // so we invent a node that is the parent of all roots. + // This 'root' does not correspond to an APZC. + var root = { scrollId: -1, children: [] }; + for (let scrollId in paint) { + paint[scrollId].children = []; + paint[scrollId].scrollId = scrollId; + } + for (let scrollId in paint) { + var parentNode = null; + if ("hasNoParentWithSameLayersId" in paint[scrollId]) { + parentNode = root; + } else if ("parentScrollId" in paint[scrollId]) { + parentNode = paint[paint[scrollId].parentScrollId]; + } + parentNode.children.push(paint[scrollId]); + } + return root; +} + +// Given an APZC tree produced by buildApzcTree, return the RCD node in +// the tree, or null if there was none. +function findRcdNode(apzcTree) { + // isRootContent will be undefined or "1" + if (apzcTree.isRootContent) { + return apzcTree; + } + for (var i = 0; i < apzcTree.children.length; i++) { + var rcd = findRcdNode(apzcTree.children[i]); + if (rcd != null) { + return rcd; + } + } + return null; +} + +// Return whether an element whose id includes |elementId| has been layerized. +// Assumes |elementId| will be present in the content description for the +// element, and not in the content descriptions of other elements. +function isLayerized(elementId) { + var contentTestData = + SpecialPowers.getDOMWindowUtils(window).getContentAPZTestData(); + var nonEmptyBucket = getLastNonemptyBucket(contentTestData.paints); + ok(nonEmptyBucket != null, "expected at least one nonempty paint"); + var seqno = nonEmptyBucket.sequenceNumber; + contentTestData = convertTestData(contentTestData); + var paint = contentTestData.paints[seqno]; + for (var scrollId in paint) { + if ("contentDescription" in paint[scrollId]) { + if (paint[scrollId].contentDescription.includes(elementId)) { + return true; + } + } + } + return false; +} + +// Return a rect (or null) that holds the last known content-side displayport +// for a given element. (The element selection works the same way, and with +// the same assumptions as the isLayerized function above). +function getLastContentDisplayportFor(elementId, expectPainted = true) { + var contentTestData = + SpecialPowers.getDOMWindowUtils(window).getContentAPZTestData(); + if (contentTestData == undefined) { + ok(!expectPainted, "expected to have apz test data (1)"); + return null; + } + var nonEmptyBucket = getLastNonemptyBucket(contentTestData.paints); + if (nonEmptyBucket == null) { + ok(!expectPainted, "expected to have apz test data (2)"); + return null; + } + var seqno = nonEmptyBucket.sequenceNumber; + contentTestData = convertTestData(contentTestData); + var paint = contentTestData.paints[seqno]; + for (var scrollId in paint) { + if ("contentDescription" in paint[scrollId]) { + if (paint[scrollId].contentDescription.includes(elementId)) { + if ("displayport" in paint[scrollId]) { + return parseRect(paint[scrollId].displayport); + } + } + } + } + return null; +} + +// Return the APZC tree (as produced by buildApzcTree) for the last +// non-empty paint received by the compositor. +function getLastApzcTree() { + let data = SpecialPowers.getDOMWindowUtils(window).getCompositorAPZTestData(); + if (data == undefined) { + ok(false, "expected to have compositor apz test data"); + return null; + } + if (!data.paints.length) { + ok(false, "expected to have at least one compositor paint bucket"); + return null; + } + var seqno = data.paints[data.paints.length - 1].sequenceNumber; + data = convertTestData(data); + return buildApzcTree(data.paints[seqno]); +} + +// Return a promise that is resolved on the next rAF callback +function promiseFrame(aWindow = window) { + return new Promise(resolve => { + aWindow.requestAnimationFrame(resolve); + }); +} + +// Return a promise that is resolved on the next MozAfterPaint event +function promiseAfterPaint() { + return new Promise(resolve => { + window.addEventListener("MozAfterPaint", resolve, { once: true }); + }); +} + +// This waits until any pending events on the APZ controller thread are +// processed, and any resulting repaint requests are received by the main +// thread. Note that while the repaint requests do get processed by the +// APZ handler on the main thread, the repaints themselves may not have +// occurred by the the returned promise resolves. If you want to wait +// for those repaints, consider using promiseApzFlushedRepaints instead. +function promiseOnlyApzControllerFlushedWithoutSetTimeout(aWindow = window) { + return new Promise(function (resolve, reject) { + var repaintDone = function () { + dump("PromiseApzRepaintsFlushed: APZ flush done\n"); + SpecialPowers.Services.obs.removeObserver( + repaintDone, + "apz-repaints-flushed" + ); + resolve(); + }; + SpecialPowers.Services.obs.addObserver(repaintDone, "apz-repaints-flushed"); + if (SpecialPowers.getDOMWindowUtils(aWindow).flushApzRepaints()) { + dump( + "PromiseApzRepaintsFlushed: Flushed APZ repaints, waiting for callback...\n" + ); + } else { + dump( + "PromiseApzRepaintsFlushed: Flushing APZ repaints was a no-op, triggering callback directly...\n" + ); + repaintDone(); + } + }); +} + +// Another variant of the above promiseOnlyApzControllerFlushedWithoutSetTimeout +// but with a setTimeout(0) callback. +function promiseOnlyApzControllerFlushed(aWindow = window) { + return new Promise(resolve => { + promiseOnlyApzControllerFlushedWithoutSetTimeout(aWindow).then(() => { + setTimeout(resolve, 0); + }); + }); +} + +// Flush repaints, APZ pending repaints, and any repaints resulting from that +// flush. This is particularly useful if the test needs to reach some sort of +// "idle" state in terms of repaints. Usually just waiting for all paints +// followed by flushApzRepaints is sufficient to flush all APZ state back to +// the main thread, but it can leave a paint scheduled which will get triggered +// at some later time. For tests that specifically test for painting at +// specific times, this method is the way to go. Even if in doubt, this is the +// preferred method as the extra step is "safe" and shouldn't interfere with +// most tests. +async function promiseApzFlushedRepaints() { + await promiseAllPaintsDone(); + await promiseOnlyApzControllerFlushed(); + await promiseAllPaintsDone(); +} + +// This function takes a set of subtests to run one at a time in new top-level +// windows, and returns a Promise that is resolved once all the subtests are +// done running. +// +// The aSubtests array is an array of objects with the following keys: +// file: required, the filename of the subtest. +// prefs: optional, an array of arrays containing key-value prefs to set. +// dp_suppression: optional, a boolean on whether or not to respect displayport +// suppression during the test. +// onload: optional, a function that will be registered as a load event listener +// for the child window that will hold the subtest. the function will be +// passed exactly one argument, which will be the child window. +// An example of an array is: +// aSubtests = [ +// { 'file': 'test_file_name.html' }, +// { 'file': 'test_file_2.html', 'prefs': [['pref.name', true], ['other.pref', 1000]], 'dp_suppression': false } +// { 'file': 'file_3.html', 'onload': function(w) { w.subtestDone(); } } +// ]; +// +// Each subtest should call one of the subtestDone() or subtestFailed() +// functions when it is done, to indicate that the window should be torn +// down and the next test should run. +// These functions are injected into the subtest's window by this +// function prior to loading the subtest. For convenience, the |is| and |ok| +// functions provided by SimpleTest are also mapped into the subtest's window. +// For other things from the parent, the subtest can use window.opener. +// to access objects. +function runSubtestsSeriallyInFreshWindows(aSubtests) { + return new Promise(function (resolve, reject) { + var testIndex = -1; + var w = null; + + // If the "apz.subtest" pref has been set, only a single subtest whose name matches + // the pref's value (if any) will be run. + var onlyOneSubtest = SpecialPowers.getCharPref( + "apz.subtest", + /* default = */ "" + ); + + function advanceSubtestExecutionWithFailure(msg) { + SimpleTest.ok(false, msg); + advanceSubtestExecution(); + } + + async function advanceSubtestExecution() { + var test = aSubtests[testIndex]; + if (w) { + // Run any cleanup functions registered in the subtest + // Guard against the subtest not loading apz_test_utils.js + if (w.ApzCleanup) { + w.ApzCleanup.execute(); + } + if (typeof test.dp_suppression != "undefined") { + // We modified the suppression when starting the test, so now undo that. + SpecialPowers.getDOMWindowUtils(window).respectDisplayPortSuppression( + !test.dp_suppression + ); + } + + if (test.prefs) { + // We pushed some prefs for this test, pop them, and re-invoke + // advanceSubtestExecution() after that's been processed + SpecialPowers.popPrefEnv(function () { + w.close(); + w = null; + advanceSubtestExecution(); + }); + return; + } + + w.close(); + } + + testIndex++; + if (testIndex >= aSubtests.length) { + resolve(); + return; + } + + await SimpleTest.promiseFocus(window); + + test = aSubtests[testIndex]; + + let recognizedProps = ["file", "prefs", "dp_suppression", "onload"]; + for (let prop in test) { + if (!recognizedProps.includes(prop)) { + SimpleTest.ok( + false, + "Subtest " + test.file + " has unrecognized property '" + prop + "'" + ); + setTimeout(function () { + advanceSubtestExecution(); + }, 0); + return; + } + } + + if (onlyOneSubtest && onlyOneSubtest != test.file) { + SimpleTest.ok( + true, + "Skipping " + + test.file + + " because only " + + onlyOneSubtest + + " is being run" + ); + setTimeout(function () { + advanceSubtestExecution(); + }, 0); + return; + } + + SimpleTest.ok(true, "Starting subtest " + test.file); + + if (typeof test.dp_suppression != "undefined") { + // Normally during a test, the displayport will get suppressed during page + // load, and unsuppressed at a non-deterministic time during the test. The + // unsuppression can trigger a repaint which interferes with the test, so + // to avoid that we can force the displayport to be unsuppressed for the + // entire test which is more deterministic. + SpecialPowers.getDOMWindowUtils(window).respectDisplayPortSuppression( + test.dp_suppression + ); + } + + function spawnTest(aFile) { + w = window.open("", "_blank"); + w.subtestDone = advanceSubtestExecution; + w.subtestFailed = advanceSubtestExecutionWithFailure; + w.isApzSubtest = true; + w.SimpleTest = SimpleTest; + w.dump = function (msg) { + return dump(aFile + " | " + msg); + }; + w.info = function (msg) { + return info(aFile + " | " + msg); + }; + w.is = function (a, b, msg) { + return is(a, b, aFile + " | " + msg); + }; + w.isnot = function (a, b, msg) { + return isnot(a, b, aFile + " | " + msg); + }; + w.isfuzzy = function (a, b, eps, msg) { + return isfuzzy(a, b, eps, aFile + " | " + msg); + }; + w.ok = function (cond, msg) { + arguments[1] = aFile + " | " + msg; + // Forward all arguments to SimpleTest.ok where we will check that ok() was + // called with at most 2 arguments. + return SimpleTest.ok.apply(SimpleTest, arguments); + }; + w.todo_is = function (a, b, msg) { + return todo_is(a, b, aFile + " | " + msg); + }; + w.todo = function (cond, msg) { + return todo(cond, aFile + " | " + msg); + }; + if (test.onload) { + w.addEventListener( + "load", + function (e) { + test.onload(w); + }, + { once: true } + ); + } + var subtestUrl = + location.href.substring(0, location.href.lastIndexOf("/") + 1) + + aFile; + function urlResolves(url) { + var request = new XMLHttpRequest(); + request.open("GET", url, false); + request.send(); + return request.status !== 404; + } + if (!urlResolves(subtestUrl)) { + SimpleTest.ok( + false, + "Subtest URL " + + subtestUrl + + " does not resolve. " + + "Be sure it's present in the support-files section of mochitest.ini." + ); + reject(); + return undefined; + } + w.location = subtestUrl; + return w; + } + + if (test.prefs) { + // Got some prefs for this subtest, push them + await SpecialPowers.pushPrefEnv({ set: test.prefs }); + } + w = spawnTest(test.file); + } + + advanceSubtestExecution(); + }).catch(function (e) { + SimpleTest.ok(false, "Error occurred while running subtests: " + e); + }); +} + +function pushPrefs(prefs) { + return SpecialPowers.pushPrefEnv({ set: prefs }); +} + +async function waitUntilApzStable() { + if (!SpecialPowers.isMainProcess()) { + // We use this waitUntilApzStable function during test initialization + // and for those scenarios we want to flush the parent-process layer + // tree to the compositor and wait for that as well. That way we know + // that not only is the content-process layer tree ready in the compositor, + // the parent-process layer tree in the compositor has the appropriate + // RefLayer pointing to the content-process layer tree. + + // Sadly this helper function cannot reuse any code from other places because + // it must be totally self-contained to be shipped over to the parent process. + function parentProcessFlush() { + /* eslint-env mozilla/chrome-script */ + function apzFlush() { + var topWin = Services.wm.getMostRecentWindow("navigator:browser"); + if (!topWin) { + topWin = Services.wm.getMostRecentWindow("navigator:geckoview"); + } + var topUtils = topWin.windowUtils; + + var repaintDone = function () { + dump("WaitUntilApzStable: APZ flush done in parent proc\n"); + Services.obs.removeObserver(repaintDone, "apz-repaints-flushed"); + // send message back to content process + sendAsyncMessage("apz-flush-done", null); + }; + var flushRepaint = function () { + if (topUtils.isMozAfterPaintPending) { + topWin.addEventListener("MozAfterPaint", flushRepaint, { + once: true, + }); + return; + } + + Services.obs.addObserver(repaintDone, "apz-repaints-flushed"); + if (topUtils.flushApzRepaints()) { + dump( + "WaitUntilApzStable: flushed APZ repaints in parent proc, waiting for callback...\n" + ); + } else { + dump( + "WaitUntilApzStable: flushing APZ repaints in parent proc was a no-op, triggering callback directly...\n" + ); + repaintDone(); + } + }; + + // Flush APZ repaints, but wait until all the pending paints have been + // sent. + flushRepaint(); + } + function cleanup() { + removeMessageListener("apz-flush", apzFlush); + removeMessageListener("cleanup", cleanup); + } + addMessageListener("apz-flush", apzFlush); + addMessageListener("cleanup", cleanup); + } + + // This is the first time waitUntilApzStable is being called, do initialization + if (typeof waitUntilApzStable.chromeHelper == "undefined") { + waitUntilApzStable.chromeHelper = + SpecialPowers.loadChromeScript(parentProcessFlush); + ApzCleanup.register(() => { + waitUntilApzStable.chromeHelper.sendAsyncMessage("cleanup", null); + waitUntilApzStable.chromeHelper.destroy(); + delete waitUntilApzStable.chromeHelper; + }); + } + + // Actually trigger the parent-process flush and wait for it to finish + waitUntilApzStable.chromeHelper.sendAsyncMessage("apz-flush", null); + await waitUntilApzStable.chromeHelper.promiseOneMessage("apz-flush-done"); + dump("WaitUntilApzStable: got apz-flush-done in child proc\n"); + } + + await SimpleTest.promiseFocus(window); + dump("WaitUntilApzStable: done promiseFocus\n"); + await promiseAllPaintsDone(); + dump("WaitUntilApzStable: done promiseAllPaintsDone\n"); + await promiseOnlyApzControllerFlushed(); + dump("WaitUntilApzStable: all done\n"); +} + +// This function returns a promise that is resolved after at least one paint +// has been sent and processed by the compositor. This function can force +// such a paint to happen if none are pending. This is useful to run after +// the waitUntilApzStable() but before reading the compositor-side APZ test +// data, because the test data for the content layers id only gets populated +// on content layer tree updates *after* the root layer tree has a RefLayer +// pointing to the contnet layer tree. waitUntilApzStable itself guarantees +// that the root layer tree is pointing to the content layer tree, but does +// not guarantee the subsequent paint; this function does that job. +async function forceLayerTreeToCompositor() { + // Modify a style property to force a layout flush + document.body.style.boxSizing = "border-box"; + var utils = SpecialPowers.getDOMWindowUtils(window); + if (!utils.isMozAfterPaintPending) { + dump("Forcing a paint since none was pending already...\n"); + var testMode = utils.isTestControllingRefreshes; + utils.advanceTimeAndRefresh(0); + if (!testMode) { + utils.restoreNormalRefresh(); + } + } + await promiseAllPaintsDone(null, true); + await promiseOnlyApzControllerFlushed(); +} + +function isApzEnabled() { + var enabled = SpecialPowers.getDOMWindowUtils(window).asyncPanZoomEnabled; + if (!enabled) { + // All tests are required to have at least one assertion. Since APZ is + // disabled, and the main test is presumably not going to run, we stick in + // a dummy assertion here to keep the test passing. + SimpleTest.ok(true, "APZ is not enabled; this test will be skipped"); + } + return enabled; +} + +function isKeyApzEnabled() { + return isApzEnabled() && SpecialPowers.getBoolPref("apz.keyboard.enabled"); +} + +// Take a snapshot of the given rect, *including compositor transforms* (i.e. +// includes async scroll transforms applied by APZ). If you don't need the +// compositor transforms, you can probably get away with using +// SpecialPowers.snapshotWindowWithOptions or one of the friendlier wrappers. +// The rect provided is expected to be relative to the screen, for example as +// returned by rectRelativeToScreen in apz_test_native_event_utils.js. +// Example usage: +// var snapshot = getSnapshot(rectRelativeToScreen(myDiv)); +// which will take a snapshot of the 'myDiv' element. Note that if part of the +// element is obscured by other things on top, the snapshot will include those +// things. If it is clipped by a scroll container, the snapshot will include +// that area anyway, so you will probably get parts of the scroll container in +// the snapshot. If the rect extends outside the browser window then the +// results are undefined. +// The snapshot is returned in the form of a data URL. +function getSnapshot(rect) { + function parentProcessSnapshot() { + /* eslint-env mozilla/chrome-script */ + addMessageListener("snapshot", function (parentRect) { + var topWin = Services.wm.getMostRecentWindow("navigator:browser"); + if (!topWin) { + topWin = Services.wm.getMostRecentWindow("navigator:geckoview"); + } + + // reposition the rect relative to the top-level browser window + parentRect = JSON.parse(parentRect); + parentRect.x -= topWin.mozInnerScreenX; + parentRect.y -= topWin.mozInnerScreenY; + + // take the snapshot + var canvas = topWin.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + canvas.width = parentRect.width; + canvas.height = parentRect.height; + var ctx = canvas.getContext("2d"); + ctx.drawWindow( + topWin, + parentRect.x, + parentRect.y, + parentRect.width, + parentRect.height, + "rgb(255,255,255)", + ctx.DRAWWINDOW_DRAW_VIEW | + ctx.DRAWWINDOW_USE_WIDGET_LAYERS | + ctx.DRAWWINDOW_DRAW_CARET + ); + return canvas.toDataURL(); + }); + } + + if (typeof getSnapshot.chromeHelper == "undefined") { + // This is the first time getSnapshot is being called; do initialization + getSnapshot.chromeHelper = SpecialPowers.loadChromeScript( + parentProcessSnapshot + ); + ApzCleanup.register(function () { + getSnapshot.chromeHelper.destroy(); + }); + } + + return getSnapshot.chromeHelper.sendQuery("snapshot", JSON.stringify(rect)); +} + +// Takes the document's query string and parses it, assuming the query string +// is composed of key-value pairs where the value is in JSON format. The object +// returned contains the various values indexed by their respective keys. In +// case of duplicate keys, the last value be used. +// Examples: +// ?key="value"&key2=false&key3=500 +// produces { "key": "value", "key2": false, "key3": 500 } +// ?key={"x":0,"y":50}&key2=[1,2,true] +// produces { "key": { "x": 0, "y": 0 }, "key2": [1, 2, true] } +function getQueryArgs() { + var args = {}; + if (location.search.length) { + var params = location.search.substr(1).split("&"); + for (var p of params) { + var [k, v] = p.split("="); + args[k] = JSON.parse(v); + } + } + return args; +} + +// An async function that inserts a script element with the given URI into +// the head of the document of the given window. This function returns when +// the load or error event fires on the script element, indicating completion. +async function injectScript(aScript, aWindow = window) { + var e = aWindow.document.createElement("script"); + e.type = "text/javascript"; + let loadPromise = new Promise((resolve, reject) => { + e.onload = function () { + resolve(); + }; + e.onerror = function () { + dump("Script [" + aScript + "] errored out\n"); + reject(); + }; + }); + e.src = aScript; + aWindow.document.getElementsByTagName("head")[0].appendChild(e); + await loadPromise; +} + +// Compute some configuration information used for hit testing. +// The computed information is cached to avoid recomputing it +// each time this function is called. +// The computed information is an object with three fields: +// utils: the nsIDOMWindowUtils instance for this window +// isWindow: true if the platform is Windows +// activateAllScrollFrames: true if prefs indicate all scroll frames are +// activated with at least a minimal display port +function getHitTestConfig() { + if (!("hitTestConfig" in window)) { + var utils = SpecialPowers.getDOMWindowUtils(window); + var isWindows = getPlatform() == "windows"; + let activateAllScrollFrames = + SpecialPowers.getBoolPref("apz.wr.activate_all_scroll_frames") || + (SpecialPowers.getBoolPref( + "apz.wr.activate_all_scroll_frames_when_fission" + ) && + SpecialPowers.Services.appinfo.fissionAutostart); + + window.hitTestConfig = { + utils, + isWindows, + activateAllScrollFrames, + }; + } + return window.hitTestConfig; +} + +// Compute the coordinates of the center of the given element. The argument +// can either be a string (the id of the element desired) or the element +// itself. +function centerOf(element) { + if (typeof element === "string") { + element = document.getElementById(element); + } + var bounds = element.getBoundingClientRect(); + return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 }; +} + +// Peform a compositor hit test at the given point and return the result. +// |point| is expected to be in CSS coordinates relative to the layout +// viewport, since this is what sendMouseEvent() expects. (Note that this +// is different from sendNativeMouseEvent() which expects screen coordinates +// relative to the screen.) +// The returned object has two fields: +// hitInfo: a combination of APZHitResultFlags +// scrollId: the view-id of the scroll frame that was hit +function hitTest(point) { + var utils = getHitTestConfig().utils; + dump("Hit-testing point (" + point.x + ", " + point.y + ")\n"); + utils.sendMouseEvent( + "MozMouseHittest", + point.x, + point.y, + 0, + 0, + 0, + true, + 0, + 0, + true, + true + ); + var data = utils.getCompositorAPZTestData(); + ok( + data.hitResults.length >= 1, + "Expected at least one hit result in the APZTestData" + ); + var result = data.hitResults[data.hitResults.length - 1]; + return { + hitInfo: result.hitResult, + scrollId: result.scrollId, + layersId: result.layersId, + }; +} + +// Returns a canonical stringification of the hitInfo bitfield. +function hitInfoToString(hitInfo) { + var strs = []; + for (var flag in APZHitResultFlags) { + if ((hitInfo & APZHitResultFlags[flag]) != 0) { + strs.push(flag); + } + } + if (!strs.length) { + return "INVISIBLE"; + } + strs.sort(function (a, b) { + return APZHitResultFlags[a] - APZHitResultFlags[b]; + }); + return strs.join(" | "); +} + +// Takes an object returned by hitTest, along with the expected values, and +// asserts that they match. Notably, it uses hitInfoToString to provide a +// more useful message for the case that the hit info doesn't match +function checkHitResult( + hitResult, + expectedHitInfo, + expectedScrollId, + expectedLayersId, + desc +) { + is( + hitInfoToString(hitResult.hitInfo), + hitInfoToString(expectedHitInfo), + desc + " hit info" + ); + is(hitResult.scrollId, expectedScrollId, desc + " scrollid"); + is(hitResult.layersId, expectedLayersId, desc + " layersid"); +} + +// Symbolic constants used by hitTestScrollbar(). +var ScrollbarTrackLocation = { + START: 1, + END: 2, +}; +var LayerState = { + ACTIVE: 1, + INACTIVE: 2, +}; + +// Perform a hit test on the scrollbar(s) of a scroll frame. +// This function takes a single argument which is expected to be +// an object with the following fields: +// element: The scroll frame to perform the hit test on. +// directions: The direction(s) of scrollbars to test. +// If directions.vertical is true, the vertical scrollbar will be tested. +// If directions.horizontal is true, the horizontal scrollbar will be tested. +// Both may be true in a single call (in which case two tests are performed). +// expectedScrollId: The scroll id that is expected to be hit, if activateAllScrollFrames is false. +// expectedLayersId: The layers id that is expected to be hit. +// trackLocation: One of ScrollbarTrackLocation.{START, END}. +// Determines which end of the scrollbar track is targeted. +// expectThumb: Whether the scrollbar thumb is expected to be present +// at the targeted end of the scrollbar track. +// layerState: Whether the scroll frame is active or inactive. +// The function performs the hit tests and asserts that the returned +// hit test information is consistent with the passed parameters. +// There is no return value. +// Tests that use this function must set the pref +// "layout.scrollbars.always-layerize-track". +function hitTestScrollbar(params) { + var config = getHitTestConfig(); + + var elem = params.element; + + var boundingClientRect = elem.getBoundingClientRect(); + + var verticalScrollbarWidth = boundingClientRect.width - elem.clientWidth; + var horizontalScrollbarHeight = boundingClientRect.height - elem.clientHeight; + + // On windows, the scrollbar tracks have buttons on the end. When computing + // coordinates for hit-testing we need to account for this. We assume the + // buttons are square, and so can use the scrollbar width/height to estimate + // the size of the buttons + var scrollbarArrowButtonHeight = config.isWindows + ? verticalScrollbarWidth + : 0; + var scrollbarArrowButtonWidth = config.isWindows + ? horizontalScrollbarHeight + : 0; + + // Compute the expected hit result flags. + // The direction flag (APZHitResultFlags.SCROLLBAR_VERTICAL) is added in + // later, for the vertical test only. + // The APZHitResultFlags.SCROLLBAR flag will be present regardless of whether + // the layer is active or inactive because we force layerization of scrollbar + // tracks. Unfortunately not forcing the layerization results in different + // behaviour on different platforms which makes testing harder. + var expectedHitInfo = APZHitResultFlags.VISIBLE | APZHitResultFlags.SCROLLBAR; + if (params.expectThumb) { + // The thumb has listeners which are APZ-aware. + expectedHitInfo |= APZHitResultFlags.APZ_AWARE_LISTENERS; + var expectActive = + config.activateAllScrollFrames || params.layerState == LayerState.ACTIVE; + if (!expectActive) { + expectedHitInfo |= APZHitResultFlags.INACTIVE_SCROLLFRAME; + } + // We do not generate the layers for thumbs on inactive scrollframes. + if (expectActive) { + expectedHitInfo |= APZHitResultFlags.SCROLLBAR_THUMB; + } + } + + var expectedScrollId = params.expectedScrollId; + if (config.activateAllScrollFrames) { + expectedScrollId = config.utils.getViewId(params.element); + if (params.layerState == LayerState.ACTIVE) { + is( + expectedScrollId, + params.expectedScrollId, + "Expected scrollId for active scrollframe should match" + ); + } + } + + var scrollframeMsg = + params.layerState == LayerState.ACTIVE + ? "active scrollframe" + : "inactive scrollframe"; + + // Hit-test the targeted areas, assuming we don't have overlay scrollbars + // with zero dimensions. + if (params.directions.vertical && verticalScrollbarWidth > 0) { + var verticalScrollbarPoint = { + x: boundingClientRect.right - verticalScrollbarWidth / 2, + y: + params.trackLocation == ScrollbarTrackLocation.START + ? boundingClientRect.y + scrollbarArrowButtonHeight + 5 + : boundingClientRect.bottom - + horizontalScrollbarHeight - + scrollbarArrowButtonHeight - + 5, + }; + checkHitResult( + hitTest(verticalScrollbarPoint), + expectedHitInfo | APZHitResultFlags.SCROLLBAR_VERTICAL, + expectedScrollId, + params.expectedLayersId, + scrollframeMsg + " - vertical scrollbar" + ); + } + + if (params.directions.horizontal && horizontalScrollbarHeight > 0) { + var horizontalScrollbarPoint = { + x: + params.trackLocation == ScrollbarTrackLocation.START + ? boundingClientRect.x + scrollbarArrowButtonWidth + 5 + : boundingClientRect.right - + verticalScrollbarWidth - + scrollbarArrowButtonWidth - + 5, + y: boundingClientRect.bottom - horizontalScrollbarHeight / 2, + }; + checkHitResult( + hitTest(horizontalScrollbarPoint), + expectedHitInfo, + expectedScrollId, + params.expectedLayersId, + scrollframeMsg + " - horizontal scrollbar" + ); + } +} + +// Return a list of prefs for the given test identifier. +function getPrefs(ident) { + switch (ident) { + case "TOUCH_EVENTS:PAN": + return [ + // Dropping the touch slop to 0 makes the tests easier to write because + // we can just do a one-pixel drag to get over the pan threshold rather + // than having to hard-code some larger value. + ["apz.touch_start_tolerance", "0.0"], + // The touchstart from the drag can turn into a long-tap if the touch-move + // events get held up. Try to prevent that by making long-taps require + // a 10 second hold. Note that we also cannot enable chaos mode on this + // test for this reason, since chaos mode can cause the long-press timer + // to fire sooner than the pref dictates. + ["ui.click_hold_context_menus.delay", 10000], + // The subtests in this test do touch-drags to pan the page, but we don't + // want those pans to turn into fling animations, so we increase the + // fling min velocity requirement absurdly high. + ["apz.fling_min_velocity_threshold", "10000"], + // The helper_div_pan's div gets a displayport on scroll, but if the + // test takes too long the displayport can expire before the new scroll + // position is synced back to the main thread. So we disable displayport + // expiry for these tests. + ["apz.displayport_expiry_ms", 0], + // We need to disable touch resampling during these tests because we + // rely on touch move events being processed without delay. Touch + // resampling only processes them once vsync fires. + ["android.touch_resampling.enabled", false], + ]; + case "TOUCH_ACTION": + return [ + ...getPrefs("TOUCH_EVENTS:PAN"), + ["apz.test.fails_with_native_injection", getPlatform() == "windows"], + ]; + default: + return []; + } +} + +var ApzCleanup = { + _cleanups: [], + + register(func) { + if (!this._cleanups.length) { + if (!window.isApzSubtest) { + SimpleTest.registerCleanupFunction(this.execute.bind(this)); + } // else ApzCleanup.execute is called from runSubtestsSeriallyInFreshWindows + } + this._cleanups.push(func); + }, + + execute() { + while (this._cleanups.length) { + var func = this._cleanups.pop(); + try { + func(); + } catch (ex) { + SimpleTest.ok( + false, + "Subtest cleanup function [" + + func.toString() + + "] threw exception [" + + ex + + "] on page [" + + location.href + + "]" + ); + } + } + }, +}; + +/** + * Returns a promise that will resolve if `eventTarget` receives an event of the + * given type that passes the given filter. Only the first matching message is + * used. The filter must be a function (or null); it is called with the event + * object and the call must return true to resolve the promise. + */ +function promiseOneEvent(eventTarget, eventType, filter) { + return new Promise((resolve, reject) => { + eventTarget.addEventListener(eventType, function listener(e) { + let success = false; + if (filter == null) { + success = true; + } else if (typeof filter == "function") { + try { + success = filter(e); + } catch (ex) { + dump( + `ERROR: Filter passed to promiseOneEvent threw exception: ${ex}\n` + ); + reject(); + return; + } + } else { + dump( + "ERROR: Filter passed to promiseOneEvent was neither null nor a function\n" + ); + reject(); + return; + } + if (success) { + eventTarget.removeEventListener(eventType, listener); + resolve(e); + } + }); + }); +} + +function visualViewportAsZoomedRect() { + let vv = window.visualViewport; + return { + x: vv.pageLeft, + y: vv.pageTop, + w: vv.width, + h: vv.height, + z: vv.scale, + }; +} + +// Pulls the latest compositor APZ test data and checks to see if the +// scroller with id `scrollerId` was checkerboarding. It also ensures that +// a scroller with id `scrollerId` was actually found in the test data. +// This function requires that "apz.test.logging_enabled" be set to true, +// in order for the test data to be logged. +function assertNotCheckerboarded(utils, scrollerId, msgPrefix) { + utils.advanceTimeAndRefresh(0); + var data = utils.getCompositorAPZTestData(); + //dump(JSON.stringify(data, null, 4)); + var found = false; + for (apzcData of data.additionalData) { + if (apzcData.key == scrollerId) { + var checkerboarding = apzcData.value + .split(",") + .includes("checkerboarding"); + ok(!checkerboarding, `${msgPrefix}: scroller is not checkerboarding`); + found = true; + } + } + ok(found, `${msgPrefix}: Found the scroller in the APZ data`); + utils.restoreNormalRefresh(); +} + +async function waitToClearOutAnyPotentialScrolls(aWindow) { + await promiseFrame(aWindow); + await promiseFrame(aWindow); + await promiseOnlyApzControllerFlushed(aWindow); + await promiseFrame(aWindow); + await promiseFrame(aWindow); +} + +function waitForScrollEvent(target) { + return new Promise(resolve => { + target.addEventListener("scroll", resolve, { once: true }); + }); +} + +// This is another variant of promiseApzFlushedRepaints. +// We need this function because, unfortunately, there is no easy way to use +// paint_listeners.js' functions and apz_test_utils.js' functions in popup +// contents opened by extensions either as scripts in the popup contents or +// scripts inside SpecialPowers.spawn because we can't use privileged functions +// in the popup contents' script, we can't use functions basically as it as in +// the sandboxed context either. +async function promiseApzFlushedRepaintsInPopup(popup) { + // Flush APZ repaints and waits for MozAfterPaint. + await SpecialPowers.spawn(popup, [], async () => { + const utils = SpecialPowers.getDOMWindowUtils(content.window); + + async function promiseAllPaintsDone() { + return new Promise(resolve => { + function waitForPaints() { + if (utils.isMozAfterPaintPending) { + dump("Waits for a MozAfterPaint event\n"); + content.window.addEventListener( + "MozAfterPaint", + () => { + dump("Got a MozAfterPaint event\n"); + waitForPaints(); + }, + { once: true } + ); + } else { + dump("No more pending MozAfterPaint\n"); + content.window.setTimeout(resolve, 0); + } + } + waitForPaints(); + }); + } + await promiseAllPaintsDone(); + + await new Promise(resolve => { + var repaintDone = function () { + dump("APZ flush done\n"); + SpecialPowers.Services.obs.removeObserver( + repaintDone, + "apz-repaints-flushed" + ); + content.window.setTimeout(resolve, 0); + }; + SpecialPowers.Services.obs.addObserver( + repaintDone, + "apz-repaints-flushed" + ); + if (utils.flushApzRepaints()) { + dump("Flushed APZ repaints, waiting for callback...\n"); + } else { + dump( + "Flushing APZ repaints was a no-op, triggering callback directly...\n" + ); + repaintDone(); + } + }); + + await promiseAllPaintsDone(); + }); +} + +// A utility function to make sure there's no scroll animation on the given +// |aElement|. +async function cancelScrollAnimation(aElement, aWindow = window) { + // In fact there's no good way to directly cancel the active animation on the + // element, so we destroy the corresponding scrollable frame then reconstruct + // a new scrollable frame so that it clobbers the animation. + const originalStyle = aElement.style.display; + aElement.style.display = "none"; + await aWindow.promiseApzFlushedRepaints(); + aElement.style.display = originalStyle; + await aWindow.promiseApzFlushedRepaints(); +} + +function collectSampledScrollOffsets(aElement) { + let data = SpecialPowers.DOMWindowUtils.getCompositorAPZTestData(); + let sampledResults = data.sampledResults; + + const layersId = SpecialPowers.DOMWindowUtils.getLayersId(); + const scrollId = SpecialPowers.DOMWindowUtils.getViewId(aElement); + + return sampledResults.filter( + result => + SpecialPowers.wrap(result).layersId == layersId && + SpecialPowers.wrap(result).scrollId == scrollId + ); +} diff --git a/gfx/layers/apz/test/mochitest/browser.ini b/gfx/layers/apz/test/mochitest/browser.ini new file mode 100644 index 0000000000..577c663fab --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser.ini @@ -0,0 +1,64 @@ +[DEFAULT] +support-files = + apz_test_native_event_utils.js + apz_test_utils.js + helper_browser_test_utils.js + !/browser/base/content/test/forms/head.js + !/browser/components/extensions/test/browser/head.js + !/browser/components/extensions/test/browser/head_browserAction.js + +[browser_test_group_fission.js] +skip-if = + (os == 'win' && bits == 32) # Some subtests fail intermittently on Win7. + (os == 'linux' && bits == 64) # Bug 1773830 +support-files = + FissionTestHelperParent.sys.mjs + FissionTestHelperChild.sys.mjs + helper_fission_*.* + !/dom/animation/test/testcommon.js +[browser_test_select_zoom.js] +skip-if = (os == 'win') # bug 1495580 +support-files = + helper_test_select_zoom.html +[browser_test_select_popup_position.js] +support-files = + helper_test_select_popup_position.html + helper_test_select_popup_position_transformed_in_parent.html + helper_test_select_popup_position_zoomed.html +[browser_test_background_tab_load_scroll.js] +support-files = + helper_background_tab_load_scroll.html +[browser_test_background_tab_scroll.js] +skip-if = (toolkit == 'android') # wheel events not supported on mobile +support-files = + helper_background_tab_scroll.html +[browser_test_reset_scaling_zoom.js] +support-files = + helper_test_reset_scaling_zoom.html +[browser_test_scrollbar_in_extension_popup_window.js] +skip-if = + verify || os == 'linux' # Bug 1713052 +[browser_test_scrolling_in_extension_popup_window.js] +skip-if = + os == "mac" # Bug 1784759 +[browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js] +run-if = (os == 'mac') # bug 1700805 +[browser_test_scroll_thumb_dragging.js] +support-files = + helper_scroll_thumb_dragging.html +[browser_test_autoscrolling_in_extension_popup_window.js] +[browser_test_autoscrolling_in_oop_frame.js] +skip-if = !fission +support-files = + helper_test_autoscrolling_in_oop_frame.html +[browser_test_animations_without_apz_sampler.js] +[browser_test_position_sticky.js] +support-files = + helper_position_sticky_flicker.html +[browser_test_tab_drag_zoom.js] +skip-if = (os == 'win') # Our Windows touch injection test code doesn't support pinch gestures (bug 1495580) +support-files = + helper_test_tab_drag_zoom.html +[browser_test_content_response_timeout.js] +support-files = + helper_content_response_timeout.html diff --git a/gfx/layers/apz/test/mochitest/browser_test_animations_without_apz_sampler.js b/gfx/layers/apz/test/mochitest/browser_test_animations_without_apz_sampler.js new file mode 100644 index 0000000000..8fdd20887a --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_animations_without_apz_sampler.js @@ -0,0 +1,134 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js", + this +); + +add_task(async () => { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_action: { + default_popup: "popup.html", + browser_style: true, + }, + }, + + files: { + "popup.html": ` + + + + + + +
+
+ + `, + }, + }); + + await extension.startup(); + + async function takeSnapshot(browserWin, callback) { + let browser = await openBrowserActionPanel(extension, browserWin, true); + + if (callback) { + await SpecialPowers.spawn(browser, [], callback); + } + + // Ensure there's no pending paint requests. + // The below code is a simplified version of promiseAllPaintsDone in + // paint_listener.js. + await SpecialPowers.spawn(browser, [], async () => { + return new Promise(resolve => { + function waitForPaints() { + // Wait until paint suppression has ended + if (SpecialPowers.DOMWindowUtils.paintingSuppressed) { + dump`waiting for paint suppression to end...`; + content.window.setTimeout(waitForPaints, 0); + return; + } + + if (SpecialPowers.DOMWindowUtils.isMozAfterPaintPending) { + dump`waiting for paint...`; + content.window.addEventListener("MozAfterPaint", waitForPaints, { + once: true, + }); + return; + } + resolve(); + } + waitForPaints(); + }); + }); + + const snapshot = await SpecialPowers.spawn(browser, [], async () => { + return SpecialPowers.snapshotWindowWithOptions( + content.window, + undefined /* use the default rect */, + undefined /* use the default bgcolor */, + { DRAWWINDOW_DRAW_VIEW: true } /* to capture scrollbars */ + ) + .toDataURL() + .toString(); + }); + + const popup = getBrowserActionPopup(extension, browserWin); + await closeBrowserAction(extension, browserWin); + is(popup.state, "closed", "browserAction popup has been closed"); + + return snapshot; + } + + // Test without apz sampler. + await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", false]] }); + + // Reference + const newWin = await BrowserTestUtils.openNewBrowserWindow(); + const reference = await takeSnapshot(newWin); + await BrowserTestUtils.closeWindow(newWin); + + // Test target + const testWin = await BrowserTestUtils.openNewBrowserWindow(); + const result = await takeSnapshot(testWin, async () => { + let div = content.window.document.getElementById("target"); + const anim = div.animate({ opacity: [1, 0.5] }, 10); + await anim.finished; + const anim2 = div.animate( + { transform: ["translateX(10px)", "translateX(20px)"] }, + 10 + ); + await anim2.finished; + + let div2 = content.window.document.getElementById("target2"); + const anim3 = div2.animate( + { transform: ["translateX(10px)", "translateX(20px)"] }, + 10 + ); + await anim3.finished; + }); + await BrowserTestUtils.closeWindow(testWin); + + is(result, reference, "The omta property value should be reset"); + + await extension.unload(); +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_extension_popup_window.js b/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_extension_popup_window.js new file mode 100644 index 0000000000..911af7548d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_extension_popup_window.js @@ -0,0 +1,189 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js", + this +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js", + this +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", + this +); + +add_task(async () => { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_action: { + default_popup: "popup.html", + browser_style: true, + }, + }, + + files: { + "popup.html": ` + + + + + + + +
    +
  • 1
  • +
  • 2
  • +
  • 3
  • +
  • 4
  • +
  • 5
  • +
  • 6
  • +
  • 7
  • +
  • 8
  • +
  • 9
  • +
  • 10
  • +
+ + `, + "popup.js": function () { + window.addEventListener( + "mousemove", + () => { + dump("Got a mousemove event in the popup content document\n"); + browser.test.sendMessage("received-mousemove"); + }, + { once: true } + ); + window.addEventListener( + "scroll", + () => { + dump("Got a scroll event in the popup content document\n"); + browser.test.sendMessage("received-scroll"); + }, + { once: true } + ); + }, + }, + }); + + await extension.startup(); + + await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", true]] }); + + // Open the popup window of the extension. + const browserForPopup = await openBrowserActionPanel( + extension, + undefined, + true + ); + + if (!browserForPopup.isRemoteBrowser) { + await closeBrowserAction(extension); + await extension.unload(); + ok( + true, + "Skipping this test since the popup window doesn't have remote contents" + ); + return; + } + + // Flush APZ repaints and waits for MozAfterPaint to make sure APZ state is + // stable. + await promiseApzFlushedRepaintsInPopup(browserForPopup); + + const { screenX, screenY, viewId, presShellId } = await SpecialPowers.spawn( + browserForPopup, + [], + () => { + const winUtils = SpecialPowers.getDOMWindowUtils(content.window); + return { + screenX: content.window.mozInnerScreenX * content.devicePixelRatio, + screenY: content.window.mozInnerScreenY * content.devicePixelRatio, + viewId: winUtils.getViewId(content.document.documentElement), + presShellId: winUtils.getPresShellId(), + }; + } + ); + + // Before starting autoscroll we need to make sure a mousemove event has been + // processed in the popup content so that subsequent mousemoves for autoscroll + // will be properly processed in autoscroll animation. + const mousemoveEventPromise = extension.awaitMessage("received-mousemove"); + + const nativeMouseEventPromise = promiseNativeMouseEventWithAPZ({ + type: "mousemove", + target: browserForPopup, + offsetX: 100, + offsetY: 50, + }); + + await Promise.all([nativeMouseEventPromise, mousemoveEventPromise]); + + const scrollEventPromise = extension.awaitMessage("received-scroll"); + + // Start autoscrolling. + ok( + browserForPopup.browsingContext.startApzAutoscroll( + screenX + 100, + screenY + 50, + viewId, + presShellId + ) + ); + + // Send sequential mousemove events to cause autoscrolling. + for (let i = 0; i < 10; i++) { + await promiseNativeMouseEventWithAPZ({ + type: "mousemove", + target: browserForPopup, + offsetX: 100, + offsetY: 50 + i * 10, + }); + } + + // Flush APZ repaints and waits for MozAfterPaint to make sure the scroll has + // been reflected on the main thread. + const apzPromise = promiseApzFlushedRepaintsInPopup(browserForPopup); + + await Promise.all([apzPromise, scrollEventPromise]); + + const scrollY = await SpecialPowers.spawn(browserForPopup, [], () => { + return content.window.scrollY; + }); + ok(scrollY > 0, "Autoscrolling works in the popup window"); + + browserForPopup.browsingContext.stopApzAutoscroll(viewId, presShellId); + + await closeBrowserAction(extension); + + await extension.unload(); +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_oop_frame.js b/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_oop_frame.js new file mode 100644 index 0000000000..26d0ff6109 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_autoscrolling_in_oop_frame.js @@ -0,0 +1,120 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js", + this +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", + this +); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["general.autoScroll", true], + ["middlemouse.contentLoadURL", false], + ["test.events.async.enabled", true], + ], + }); +}); + +async function doTest() { + function httpURL(filename) { + const chromeURL = getRootDirectory(gTestPath) + filename; + return chromeURL.replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + } + + function getScrollY(context) { + return SpecialPowers.spawn(context, [], () => content.scrollY); + } + + const pageUrl = httpURL("helper_test_autoscrolling_in_oop_frame.html"); + + await BrowserTestUtils.withNewTab(pageUrl, async function (browser) { + await promiseApzFlushedRepaintsInPopup(browser); + + const iframeContext = browser.browsingContext.children[0]; + await promiseApzFlushedRepaintsInPopup(iframeContext); + + const { screenX, screenY, viewId, presShellId } = await SpecialPowers.spawn( + iframeContext, + [], + () => { + const winUtils = SpecialPowers.getDOMWindowUtils(content); + return { + screenX: content.mozInnerScreenX * content.devicePixelRatio, + screenY: content.mozInnerScreenY * content.devicePixelRatio, + viewId: winUtils.getViewId(content.document.documentElement), + presShellId: winUtils.getPresShellId(), + }; + } + ); + + ok( + iframeContext.startApzAutoscroll( + screenX + 100, + screenY + 50, + viewId, + presShellId + ), + "Started autscroll" + ); + + const scrollEventPromise = SpecialPowers.spawn( + iframeContext, + [], + async () => { + return new Promise(resolve => { + content.addEventListener( + "scroll", + event => { + dump("Got a scroll event in the iframe\n"); + resolve(); + }, + { once: true } + ); + }); + } + ); + + // Send sequential mousemove events to cause autoscrolling. + for (let i = 0; i < 10; i++) { + await promiseNativeMouseEventWithAPZ({ + type: "mousemove", + target: browser, + offsetX: 100, + offsetY: 50 + i * 10, + }); + } + + // Flush APZ repaints and waits for MozAfterPaint to make sure the scroll has + // been reflected on the main thread. + const apzPromise = promiseApzFlushedRepaintsInPopup(browser); + + await Promise.all([apzPromise, scrollEventPromise]); + + const frameScrollY = await getScrollY(iframeContext); + ok(frameScrollY > 0, "Autoscrolled the iframe"); + + const rootScrollY = await getScrollY(browser); + ok(rootScrollY == 0, "Didn't scroll the root document"); + + iframeContext.stopApzAutoscroll(viewId, presShellId); + }); +} + +add_task(async function test_autoscroll_in_oop_iframe() { + await doTest(); +}); + +add_task(async function test_autoscroll_in_oop_iframe_with_os_zoom() { + await SpecialPowers.pushPrefEnv({ set: [["ui.textScaleFactor", 200]] }); + await doTest(); +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_background_tab_load_scroll.js b/gfx/layers/apz/test/mochitest/browser_test_background_tab_load_scroll.js new file mode 100644 index 0000000000..9878907603 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_background_tab_load_scroll.js @@ -0,0 +1,117 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js", + this +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", + this +); + +add_task(async function test_main() { + // Open a specific page in a background tab, then switch to the tab, check if + // the visual and layout scroll offsets have diverged. + // Then change to another tab so it's background again. Then reload it. Then + // change back to it and check again if the visual and layout scroll offsets + // have diverged. + // The page has a couple important properties to trigger the bug. We need to + // be restoring a non-zero scroll position so that we call ScrollToImpl with + // origin restore so that we do not set the visual viewport offset. We then + // need to call ScrollToImpl with a origin that does not get clobber by apz + // so that we (wrongly) set the visual viewport offset. + + requestLongerTimeout(2); + + async function twoRafsInContent(browser) { + await SpecialPowers.spawn(browser, [], async function () { + await new Promise(r => + content.requestAnimationFrame(() => content.requestAnimationFrame(r)) + ); + }); + } + + async function waitForApzInContent(browser) { + await SpecialPowers.spawn(browser, [], async () => { + await content.wrappedJSObject.waitUntilApzStable(); + await content.wrappedJSObject.promiseApzFlushedRepaints(); + }); + } + + async function checkScrollPosInContent(browser, iter, num) { + let visualScrollPos = await SpecialPowers.spawn(browser, [], function () { + const offsetX = {}; + const offsetY = {}; + SpecialPowers.getDOMWindowUtils(content).getVisualViewportOffset( + offsetX, + offsetY + ); + return offsetY.value; + }); + + let scrollPos = await SpecialPowers.spawn(browser, [], function () { + return content.window.scrollY; + }); + + // When this fails the difference is at least 10000. + ok( + Math.abs(scrollPos - visualScrollPos) < 2, + "expect scroll position and visual scroll position to be the same: visual " + + visualScrollPos + + " scroll " + + scrollPos + + " (" + + iter + + "," + + num + + ")" + ); + } + + for (let i = 0; i < 5; i++) { + let blankurl = "about:blank"; + let blankTab = BrowserTestUtils.addTab(gBrowser, blankurl); + let blankbrowser = blankTab.linkedBrowser; + await BrowserTestUtils.browserLoaded(blankbrowser, false, blankurl); + + let url = + "http://mochi.test:8888/browser/gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html"; + let backgroundTab = BrowserTestUtils.addTab(gBrowser, url); + let browser = backgroundTab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser, false, url); + dump("Done loading background tab\n"); + + await twoRafsInContent(browser); + + // Switch to the foreground. + await BrowserTestUtils.switchTab(gBrowser, backgroundTab); + dump("Switched background tab to foreground\n"); + + await waitForApzInContent(browser); + + await checkScrollPosInContent(browser, i, 1); + + await BrowserTestUtils.switchTab(gBrowser, blankTab); + + browser.reload(); + await BrowserTestUtils.browserLoaded(browser, false, url); + + await twoRafsInContent(browser); + + // Switch to the foreground. + await BrowserTestUtils.switchTab(gBrowser, backgroundTab); + dump("Switched background tab to foreground\n"); + + await waitForApzInContent(browser); + + await checkScrollPosInContent(browser, i, 2); + + // Cleanup + let tabClosed = BrowserTestUtils.waitForTabClosing(backgroundTab); + BrowserTestUtils.removeTab(backgroundTab); + await tabClosed; + + let blanktabClosed = BrowserTestUtils.waitForTabClosing(blankTab); + BrowserTestUtils.removeTab(blankTab); + await blanktabClosed; + } +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_background_tab_scroll.js b/gfx/layers/apz/test/mochitest/browser_test_background_tab_scroll.js new file mode 100644 index 0000000000..4ce8200199 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_background_tab_scroll.js @@ -0,0 +1,66 @@ +add_task(async function test_main() { + // Load page in the background. This will cause the first-paint of the + // tab (which has ScrollPositionUpdate instances) to get sent to the + // compositor, but the parent process RefLayer won't be pointing to this + // tab so APZ never sees the ScrollPositionUpdate instances. + + let url = + "http://mochi.test:8888/browser/gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html#scrolltarget"; + let backgroundTab = BrowserTestUtils.addTab(gBrowser, url); + let browser = backgroundTab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser, false, url); + dump("Done loading background tab\n"); + + // Switch to the foreground, to let the APZ tree get built. + await BrowserTestUtils.switchTab(gBrowser, backgroundTab); + dump("Switched background tab to foreground\n"); + + // Verify main-thread scroll position is where we expect + let scrollPos = await ContentTask.spawn(browser, null, function () { + return content.window.scrollY; + }); + is(scrollPos, 5000, "Expected background tab to be at scroll pos 5000"); + + // Trigger an APZ-side scroll via native wheel event, followed by some code + // to ensure APZ's repaint requests to arrive at the main-thread. If + // things are working properly, the main thread will accept the repaint + // requests and update the main-thread scroll position. If the APZ side + // is sending incorrect scroll generations in the repaint request, then + // the main thread will fail to clear the main-thread scroll origin (which + // was set by the scroll to the #scrolltarget anchor), and so will not + // accept APZ's scroll position updates. + let contentScrollFunction = async function () { + await content.window.wrappedJSObject.promiseNativeWheelAndWaitForWheelEvent( + content.window, + 100, + 100, + 0, + 200 + ); + + // Advance some/all frames of the APZ wheel animation + let utils = content.window.SpecialPowers.getDOMWindowUtils(content.window); + for (var i = 0; i < 10; i++) { + utils.advanceTimeAndRefresh(16); + } + utils.restoreNormalRefresh(); + // Flush pending APZ repaints, then read the main-thread scroll + // position + await content.window.wrappedJSObject.promiseOnlyApzControllerFlushed( + content.window + ); + return content.window.scrollY; + }; + scrollPos = await ContentTask.spawn(browser, null, contentScrollFunction); + + // Verify main-thread scroll position has changed + ok( + scrollPos < 5000, + `Expected background tab to have scrolled up, is at ${scrollPos}` + ); + + // Cleanup + let tabClosed = BrowserTestUtils.waitForTabClosing(backgroundTab); + BrowserTestUtils.removeTab(backgroundTab); + await tabClosed; +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_content_response_timeout.js b/gfx/layers/apz/test/mochitest/browser_test_content_response_timeout.js new file mode 100644 index 0000000000..a80fd77c17 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_content_response_timeout.js @@ -0,0 +1,88 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js", + this +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", + this +); + +add_task(async () => { + // Use pan gesture events for Mac. + await SpecialPowers.pushPrefEnv({ + set: [ + // Set a relatively shorter timeout value. + ["apz.content_response_timeout", 100], + ], + }); + + const URL_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + // Load a content having an APZ-aware listener causing 500ms busy state and + // a scroll event listener changing the background color of an element. + // The reason why we change the background color in a scroll listener rather + // than setting up a Promise resolved in a scroll event handler and waiting + // for the Promise is SpecialPowers.spawn doesn't allow it (bug 1743857). + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + URL_ROOT + "helper_content_response_timeout.html" + ); + + let scrollPromise = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "scroll" + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.promiseApzFlushedRepaints(); + await content.wrappedJSObject.waitUntilApzStable(); + }); + + // Note that below function uses `WaitForObserver` version of sending a + // pan-start event function so that the notification can be sent in the parent + // process, thus we can get the notification even if the content process is + // busy. + await NativePanHandler.promiseNativePanEvent( + tab.linkedBrowser, + 100, + 100, + 0, + NativePanHandler.delta, + NativePanHandler.beginPhase + ); + + await new Promise(resolve => { + setTimeout(resolve, 200); + }); + + await NativePanHandler.promiseNativePanEvent( + tab.linkedBrowser, + 100, + 100, + 0, + NativePanHandler.delta, + NativePanHandler.updatePhase + ); + await NativePanHandler.promiseNativePanEvent( + tab.linkedBrowser, + 100, + 100, + 0, + 0, + NativePanHandler.endPhase + ); + + await scrollPromise; + ok(true, "We got at least one scroll event"); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_group_fission.js b/gfx/layers/apz/test/mochitest/browser_test_group_fission.js new file mode 100644 index 0000000000..43bbcbe444 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_group_fission.js @@ -0,0 +1,150 @@ +add_task(async function setup_pref() { + await SpecialPowers.pushPrefEnv({ + set: [ + // To avoid throttling requestAnimationFrame callbacks in invisible + // iframes + ["layout.throttled_frame_rate", 60], + ["dom.animations-api.getAnimations.enabled", true], + ["dom.animations-api.timelines.enabled", true], + // Next two prefs are needed for hit-testing to work + ["test.events.async.enabled", true], + ["apz.test.logging_enabled", true], + ], + }); +}); + +add_task(async function test_main() { + function httpURL(filename) { + let chromeURL = getRootDirectory(gTestPath) + filename; + return chromeURL.replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + } + + // Each of these subtests is a dictionary that contains: + // file (required): filename of the subtest that will get opened in a new tab + // in the top-level fission-enabled browser window. + // setup (optional): function that takes the top-level fission window and is + // run once after the subtest is loaded but before it is started. + var subtests = [ + { file: "helper_fission_basic.html" }, + { file: "helper_fission_transforms.html" }, + { file: "helper_fission_scroll_oopif.html" }, + { + file: "helper_fission_event_region_override.html", + setup(win) { + win.document.addEventListener("wheel", e => e.preventDefault(), { + once: true, + passive: false, + }); + }, + }, + { file: "helper_fission_animation_styling_in_oopif.html" }, + { file: "helper_fission_force_empty_hit_region.html" }, + { file: "helper_fission_touch.html" }, + { + file: "helper_fission_tap.html", + prefs: [["apz.max_tap_time", 10000]], + }, + { file: "helper_fission_inactivescroller_under_oopif.html" }, + { + file: "helper_fission_tap_on_zoomed.html", + prefs: [["apz.max_tap_time", 10000]], + }, + { + file: "helper_fission_tap_in_nested_iframe_on_zoomed.html", + prefs: [["apz.max_tap_time", 10000]], + }, + { file: "helper_fission_scroll_handoff.html" }, + { file: "helper_fission_large_subframe.html" }, + { file: "helper_fission_initial_displayport.html" }, + { file: "helper_fission_checkerboard_severity.html" }, + { file: "helper_fission_setResolution.html" }, + { file: "helper_fission_inactivescroller_positionedcontent.html" }, + { file: "helper_fission_irregular_areas.html" }, + { file: "helper_fission_animation_styling_in_transformed_oopif.html" }, + // add additional tests here + ]; + + // ccov builds run slower and need longer, so let's scale up the timeout + // by the number of tests we're running. + requestLongerTimeout(subtests.length); + + let fissionWindow = await BrowserTestUtils.openNewBrowserWindow({ + fission: true, + }); + + // We import the ESM here so that we can install functions on the class + // below. + const { FissionTestHelperParent } = ChromeUtils.importESModule( + getRootDirectory(gTestPath) + "FissionTestHelperParent.sys.mjs" + ); + FissionTestHelperParent.SimpleTest = SimpleTest; + + ChromeUtils.registerWindowActor("FissionTestHelper", { + parent: { + esModuleURI: + getRootDirectory(gTestPath) + "FissionTestHelperParent.sys.mjs", + }, + child: { + esModuleURI: + getRootDirectory(gTestPath) + "FissionTestHelperChild.sys.mjs", + events: { + "FissionTestHelper:Init": { capture: true, wantUntrusted: true }, + }, + }, + allFrames: true, + }); + + try { + var onlyOneSubtest = SpecialPowers.getCharPref( + "apz.subtest", + /*default = */ "" + ); + + for (var subtest of subtests) { + if (onlyOneSubtest && onlyOneSubtest != subtest.file) { + SimpleTest.ok( + true, + "Skipping " + + subtest.file + + " because only " + + onlyOneSubtest + + " is being run" + ); + continue; + } + let url = httpURL(subtest.file); + dump(`Starting test ${url}\n`); + + // Load the test URL and tell it to get started, and wait until it reports + // completion. + await BrowserTestUtils.withNewTab( + { gBrowser: fissionWindow.gBrowser, url }, + async browser => { + let tabActor = + browser.browsingContext.currentWindowGlobal.getActor( + "FissionTestHelper" + ); + let donePromise = tabActor.getTestCompletePromise(); + if (subtest.setup) { + subtest.setup(fissionWindow); + } + tabActor.startTest(); + await donePromise; + } + ); + + dump(`Finished test ${url}\n`); + } + } finally { + // Delete stuff we added to FissionTestHelperParent, beacuse the object will + // outlive this test, and leaving stuff on it may leak the things reachable + // from it. + delete FissionTestHelperParent.SimpleTest; + // Teardown + ChromeUtils.unregisterWindowActor("FissionTestHelper"); + await BrowserTestUtils.closeWindow(fissionWindow); + } +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_position_sticky.js b/gfx/layers/apz/test/mochitest/browser_test_position_sticky.js new file mode 100644 index 0000000000..ce6b093a80 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_position_sticky.js @@ -0,0 +1,105 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js", + this +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", + this +); + +add_task(async () => { + function httpURL(filename) { + let chromeURL = getRootDirectory(gTestPath) + filename; + return chromeURL.replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + } + + const url = httpURL("helper_position_sticky_flicker.html"); + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + const { rect, scrollbarWidth } = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async () => { + const sticky = content.document.getElementById("sticky"); + + // Get the area in the screen coords where the position:sticky element is. + let stickyRect = sticky.getBoundingClientRect(); + stickyRect.x += content.window.mozInnerScreenX; + stickyRect.y += content.window.mozInnerScreenY; + + // generate some DIVs to make the page complex enough. + for (let i = 1; i <= 120000; i++) { + const div = content.document.createElement("div"); + div.innerText = `${i}`; + content.document.body.appendChild(div); + } + + await content.wrappedJSObject.promiseApzFlushedRepaints(); + await content.wrappedJSObject.waitUntilApzStable(); + + let w = {}, + h = {}; + SpecialPowers.DOMWindowUtils.getScrollbarSizes( + content.document.documentElement, + w, + h + ); + + // Reduce the scrollbar width from the sticky area. + stickyRect.width -= w.value; + return { + rect: stickyRect, + scrollbarWidth: w.value, + }; + } + ); + + // Take a snapshot where the position:sticky element is initially painted. + const reference = await getSnapshot(rect); + + let mouseX = window.innerWidth - scrollbarWidth / 2; + let mouseY = tab.linkedBrowser.getBoundingClientRect().y + 5; + + // Scroll fast to cause checkerboarding multiple times. + const dragFinisher = await promiseNativeMouseDrag( + window, + mouseX, + mouseY, + 0, + window.innerHeight, + 100 + ); + + // On debug builds there seems to be no chance that the content process gets + // painted during above promiseNativeMouseDrag call, wait two frames to make + // sure it happens so that this test is likely able to fail without proper + // fix. + if (AppConstants.DEBUG) { + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.promiseFrame(content.window); + await content.wrappedJSObject.promiseFrame(content.window); + }); + } + + // Take a snapshot again where the position:sticky element should be painted. + const snapshot = await getSnapshot(rect); + + await dragFinisher(); + + is( + snapshot, + reference, + "The position:sticky element should stay at the " + + "same place after scrolling on heavy load" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_reset_scaling_zoom.js b/gfx/layers/apz/test/mochitest/browser_test_reset_scaling_zoom.js new file mode 100644 index 0000000000..168d358fcb --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_reset_scaling_zoom.js @@ -0,0 +1,44 @@ +add_task(async function setup_pref() { + let isWindows = navigator.platform.indexOf("Win") == 0; + await SpecialPowers.pushPrefEnv({ + set: [["apz.test.fails_with_native_injection", isWindows]], + }); +}); + +add_task(async function test_main() { + function httpURL(filename) { + let chromeURL = getRootDirectory(gTestPath) + filename; + return chromeURL.replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + } + + const pageUrl = httpURL("helper_test_reset_scaling_zoom.html"); + + await BrowserTestUtils.withNewTab(pageUrl, async function (browser) { + let getResolution = function () { + return content.window.SpecialPowers.getDOMWindowUtils( + content.window + ).getResolution(); + }; + + let doZoomIn = async function () { + await content.window.wrappedJSObject.doZoomIn(); + }; + + let resolution = await ContentTask.spawn(browser, null, getResolution); + is(resolution, 1.0, "Initial page resolution should be 1.0"); + + await ContentTask.spawn(browser, null, doZoomIn); + resolution = await ContentTask.spawn(browser, null, getResolution); + isnot(resolution, 1.0, "Expected resolution to be bigger than 1.0"); + + document.getElementById("cmd_fullZoomReset").doCommand(); + // Spin the event loop once just to make sure the message gets through + await new Promise(resolve => setTimeout(resolve, 0)); + + resolution = await ContentTask.spawn(browser, null, getResolution); + is(resolution, 1.0, "Expected resolution to be reset to 1.0"); + }); +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_scroll_thumb_dragging.js b/gfx/layers/apz/test/mochitest/browser_test_scroll_thumb_dragging.js new file mode 100644 index 0000000000..bd2d733a32 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_scroll_thumb_dragging.js @@ -0,0 +1,77 @@ +add_task(async function () { + function httpURL(filename) { + let chromeURL = getRootDirectory(gTestPath) + filename; + return chromeURL.replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + } + + const newWin = await BrowserTestUtils.openNewBrowserWindow(); + + const pageUrl = httpURL("helper_scroll_thumb_dragging.html"); + const tab = await BrowserTestUtils.openNewForegroundTab( + newWin.gBrowser, + pageUrl + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.promiseApzFlushedRepaints(); + await content.wrappedJSObject.waitUntilApzStable(); + }); + + // Send an explicit click event to make sure the new window accidentally + // doesn't get an "enter-notify-event" on Linux during dragging, the event + // forcibly cancels the dragging state. + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + // Creating an object in this content privilege so that the object + // properties can be accessed in below + // promiseNativeMouseEventWithAPZAndWaitForEvent function. + const moveParams = content.window.eval(`({ + target: window, + type: "mousemove", + offsetX: 10, + offsetY: 10 + })`); + const clickParams = content.window.eval(`({ + target: window, + type: "click", + offsetX: 10, + offsetY: 10 + })`); + // Send a mouse move event first to make sure the "enter-notify-event" + // happens. + await content.wrappedJSObject.promiseNativeMouseEventWithAPZAndWaitForEvent( + moveParams + ); + await content.wrappedJSObject.promiseNativeMouseEventWithAPZAndWaitForEvent( + clickParams + ); + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + const scrollPromise = new Promise(resolve => { + content.window.addEventListener("scroll", resolve, { once: true }); + }); + const dragFinisher = + await content.wrappedJSObject.promiseVerticalScrollbarDrag( + content.window, + 10, + 10 + ); + + await scrollPromise; + await dragFinisher(); + + await content.wrappedJSObject.promiseApzFlushedRepaints(); + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + ok( + content.window.scrollY < 100, + "The root scrollable content shouldn't be scrolled too much" + ); + }); + + await BrowserTestUtils.closeWindow(newWin); +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_scrollbar_in_extension_popup_window.js b/gfx/layers/apz/test/mochitest/browser_test_scrollbar_in_extension_popup_window.js new file mode 100644 index 0000000000..6e18129845 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_scrollbar_in_extension_popup_window.js @@ -0,0 +1,138 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js", + this +); + +add_task(async () => { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_action: { + default_popup: "popup.html", + browser_style: true, + }, + }, + + files: { + "popup.html": ` + + + + + + +
    +
  • 1
  • +
  • 2
  • +
  • 3
  • +
  • 4
  • +
  • 5
  • +
  • 6
  • +
  • 7
  • +
  • 8
  • +
  • 9
  • +
  • 10
  • +
+ + `, + }, + }); + + await extension.startup(); + + async function takeSnapshot(browserWin) { + let browser = await openBrowserActionPanel(extension, browserWin, true); + + // Ensure there's no pending paint requests. + // The below code is a simplified version of promiseAllPaintsDone in + // paint_listener.js. + await SpecialPowers.spawn(browser, [], async () => { + return new Promise(resolve => { + function waitForPaints() { + // Wait until paint suppression has ended + if (SpecialPowers.DOMWindowUtils.paintingSuppressed) { + dump`waiting for paint suppression to end...`; + content.window.setTimeout(waitForPaints, 0); + return; + } + + if (SpecialPowers.DOMWindowUtils.isMozAfterPaintPending) { + dump`waiting for paint...`; + content.window.addEventListener("MozAfterPaint", waitForPaints, { + once: true, + }); + return; + } + resolve(); + } + waitForPaints(); + }); + }); + + const snapshot = await SpecialPowers.spawn(browser, [], async () => { + return SpecialPowers.snapshotWindowWithOptions( + content.window, + undefined /* use the default rect */, + undefined /* use the default bgcolor */, + { DRAWWINDOW_DRAW_VIEW: true } /* to capture scrollbars */ + ) + .toDataURL() + .toString(); + }); + + const popup = getBrowserActionPopup(extension, browserWin); + await closeBrowserAction(extension, browserWin); + is(popup.state, "closed", "browserAction popup has been closed"); + + return snapshot; + } + + // First, take a snapshot with disabling APZ in the popup window, we assume + // scrollbars are rendered properly there. + await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", false]] }); + const newWin = await BrowserTestUtils.openNewBrowserWindow(); + const reference = await takeSnapshot(newWin); + await BrowserTestUtils.closeWindow(newWin); + + // Then take a snapshot with enabling APZ. + await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", true]] }); + const anotherWin = await BrowserTestUtils.openNewBrowserWindow(); + const test = await takeSnapshot(anotherWin); + await BrowserTestUtils.closeWindow(anotherWin); + + is( + test, + reference, + "Contents in popup window opened by extension should be same regardless of the APZ state in the window" + ); + + await extension.unload(); +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_scrolling_in_extension_popup_window.js b/gfx/layers/apz/test/mochitest/browser_test_scrolling_in_extension_popup_window.js new file mode 100644 index 0000000000..6da3f3311b --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_scrolling_in_extension_popup_window.js @@ -0,0 +1,128 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js", + this +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js", + this +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", + this +); + +add_task(async () => { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_action: { + default_popup: "popup.html", + browser_style: true, + }, + }, + + files: { + "popup.html": ` + + + + + + +
    +
  • 1
  • +
  • 2
  • +
  • 3
  • +
  • 4
  • +
  • 5
  • +
  • 6
  • +
  • 7
  • +
  • 8
  • +
  • 9
  • +
  • 10
  • +
+ + `, + }, + }); + + await extension.startup(); + + await SpecialPowers.pushPrefEnv({ set: [["apz.popups.enabled", true]] }); + + // Open the popup window of the extension. + const browserForPopup = await openBrowserActionPanel( + extension, + undefined, + true + ); + + // Flush APZ repaints and waits for MozAfterPaint to make sure APZ state is + // stable. + await promiseApzFlushedRepaintsInPopup(browserForPopup); + + const scrollEventPromise = SpecialPowers.spawn( + browserForPopup, + [], + async () => { + return new Promise(resolve => { + content.window.addEventListener( + "scroll", + event => { + dump("Got a scroll event in the popup content document\n"); + resolve(); + }, + { once: true } + ); + }); + } + ); + + // Send native mouse wheel to scroll the content in the popup. + await promiseNativeWheelAndWaitForObserver(browserForPopup, 50, 50, 0, -100); + + // Flush APZ repaints and waits for MozAfterPaint to make sure the scroll has + // been reflected on the main thread. + const apzPromise = promiseApzFlushedRepaintsInPopup(browserForPopup); + + await Promise.all([apzPromise, scrollEventPromise]); + + const scrollY = await SpecialPowers.spawn(browserForPopup, [], () => { + return content.window.scrollY; + }); + ok(scrollY > 0, "Mouse wheel scrolling works in the popup window"); + + await closeBrowserAction(extension); + + await extension.unload(); +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js b/gfx/layers/apz/test/mochitest/browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js new file mode 100644 index 0000000000..99dccd458c --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_scrolling_on_inactive_scroller_in_extension_popup_window.js @@ -0,0 +1,137 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/extensions/test/browser/head_browserAction.js", + this +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js", + this +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", + this +); + +add_task(async () => { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_action: { + default_popup: "popup.html", + browser_style: true, + }, + }, + + files: { + "popup.html": ` + + + + + + +
+
123
+
123
+
+ + `, + }, + }); + + await extension.startup(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["apz.popups.enabled", true], + ["apz.wr.activate_all_scroll_frames", false], + ["apz.wr.activate_all_scroll_frames_when_fission", false], + ], + }); + + // Open the popup window of the extension. + const browserForPopup = await openBrowserActionPanel( + extension, + undefined, + true + ); + + // Flush APZ repaints and waits for MozAfterPaint to make sure APZ state is + // stable. + await promiseApzFlushedRepaintsInPopup(browserForPopup); + + // A Promise to wait for one scroll event for each scrollable element. + const scrollEventsPromise = SpecialPowers.spawn(browserForPopup, [], () => { + let promises = []; + content.document.querySelectorAll(".overflow").forEach(element => { + let promise = new Promise(resolve => { + element.addEventListener( + "scroll", + () => { + resolve(); + }, + { once: true } + ); + }); + promises.push(promise); + }); + return Promise.all(promises); + }); + + // Send two native mouse wheel events to scroll each scrollable element in the + // popup. + await promiseNativeWheelAndWaitForObserver(browserForPopup, 50, 50, 0, -100); + await promiseNativeWheelAndWaitForObserver(browserForPopup, 150, 50, 0, -100); + + // Flush APZ repaints and waits for MozAfterPaint to make sure the scroll has + // been reflected on the main thread. + const apzPromise = promiseApzFlushedRepaintsInPopup(browserForPopup); + + await Promise.all([apzPromise, scrollEventsPromise]); + + const scrollTops = await SpecialPowers.spawn(browserForPopup, [], () => { + let result = []; + content.document.querySelectorAll(".overflow").forEach(element => { + result.push(element.scrollTop); + }); + return result; + }); + + ok(scrollTops[0] > 0, "Mouse wheel scrolling works in the popup window"); + ok(scrollTops[1] > 0, "Mouse wheel scrolling works in the popup window"); + + await closeBrowserAction(extension); + + await extension.unload(); +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_select_popup_position.js b/gfx/layers/apz/test/mochitest/browser_test_select_popup_position.js new file mode 100644 index 0000000000..08a6ec9b93 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_select_popup_position.js @@ -0,0 +1,130 @@ +/* This test is a a mash up of + https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/gfx/layers/apz/test/mochitest/browser_test_group_fission.js + https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/gfx/layers/apz/test/mochitest/helper_basic_zoom.html + https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/browser/base/content/test/forms/browser_selectpopup.js +*/ + +/* import-globals-from helper_browser_test_utils.js */ +Services.scriptloader.loadSubScript( + new URL("helper_browser_test_utils.js", gTestPath).href, + this +); + +async function runPopupPositionTest(parentDocumentFileName) { + function httpURL(filename) { + let chromeURL = getRootDirectory(gTestPath) + filename; + return chromeURL.replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + } + + function httpCrossOriginURL(filename) { + let chromeURL = getRootDirectory(gTestPath) + filename; + return chromeURL.replace( + "chrome://mochitests/content/", + "http://example.com/" + ); + } + + const pageUrl = httpURL(parentDocumentFileName); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + + // Load the OOP iframe. + const iframeUrl = httpCrossOriginURL( + "helper_test_select_popup_position.html" + ); + const iframe = await SpecialPowers.spawn( + tab.linkedBrowser, + [iframeUrl], + async url => { + const target = content.document.querySelector("iframe"); + target.src = url; + await new Promise(resolve => { + target.addEventListener("load", resolve, { once: true }); + }); + return target.browsingContext; + } + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.promiseApzFlushedRepaints(); + await content.wrappedJSObject.waitUntilApzStable(); + }); + + const selectRect = await SpecialPowers.spawn(iframe, [], () => { + return content.document.querySelector("select").getBoundingClientRect(); + }); + + // Get focus on the select element. + await SpecialPowers.spawn(iframe, [], async () => { + const select = content.document.querySelector("select"); + const focusPromise = new Promise(resolve => { + select.addEventListener("focus", resolve, { once: true }); + }); + select.focus(); + await focusPromise; + }); + + const selectPopup = await openSelectPopup(); + + const popupRect = selectPopup.getBoundingClientRect(); + const popupMarginTop = parseFloat(getComputedStyle(selectPopup).marginTop); + const popupMarginLeft = parseFloat(getComputedStyle(selectPopup).marginLeft); + + info( + `popup rect: (${popupRect.x}, ${popupRect.y}) ${popupRect.width}x${popupRect.height}` + ); + info(`popup margins: ${popupMarginTop} / ${popupMarginLeft}`); + info( + `select rect: (${selectRect.x}, ${selectRect.y}) ${selectRect.width}x${selectRect.height}` + ); + + is( + popupRect.left - popupMarginLeft, + selectRect.x * 2.0, + "select popup position x should be scaled by the desktop zoom" + ); + + // On platforms other than MaxOSX the popup menu is positioned below the + // option element. + if (!navigator.platform.includes("Mac")) { + is( + popupRect.top - popupMarginTop, + tab.linkedBrowser.getBoundingClientRect().top + + (selectRect.y + selectRect.height) * 2.0, + "select popup position y should be scaled by the desktop zoom" + ); + } else { + // On mac it's aligned to the selected menulist option. + const offsetToSelectedItem = + selectPopup.querySelector("menuitem[selected]").getBoundingClientRect() + .top - popupRect.top; + is( + popupRect.top - popupMarginTop + offsetToSelectedItem, + tab.linkedBrowser.getBoundingClientRect().top + selectRect.y * 2.0, + "select popup position y should be scaled by the desktop zoom" + ); + } + + await hideSelectPopup(); + + BrowserTestUtils.removeTab(tab); +} + +add_task(async function () { + if (!SpecialPowers.useRemoteSubframes) { + ok( + true, + "popup window position in non OOP iframe will be fixed by bug 1691346" + ); + return; + } + await runPopupPositionTest( + "helper_test_select_popup_position_transformed_in_parent.html" + ); +}); + +add_task(async function () { + await runPopupPositionTest("helper_test_select_popup_position_zoomed.html"); +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js b/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js new file mode 100644 index 0000000000..84baf8e5ac --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js @@ -0,0 +1,195 @@ +/* This test is a a mash up of + https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/gfx/layers/apz/test/mochitest/browser_test_group_fission.js + https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/gfx/layers/apz/test/mochitest/helper_basic_zoom.html + https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/browser/base/content/test/forms/browser_selectpopup.js +*/ + +/* import-globals-from helper_browser_test_utils.js */ +Services.scriptloader.loadSubScript( + new URL("helper_browser_test_utils.js", gTestPath).href, + this +); + +add_task(async function setup_pref() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Dropping the touch slop to 0 makes the tests easier to write because + // we can just do a one-pixel drag to get over the pan threshold rather + // than having to hard-code some larger value. + ["apz.touch_start_tolerance", "0.0"], + // The subtests in this test do touch-drags to pan the page, but we don't + // want those pans to turn into fling animations, so we increase the + // fling-min threshold velocity to an arbitrarily large value. + ["apz.fling_min_velocity_threshold", "10000"], + ], + }); +}); + +// This test opens a select popup after pinch (apz) zooming has happened. +add_task(async function () { + function httpURL(filename) { + let chromeURL = getRootDirectory(gTestPath) + filename; + //return chromeURL; + return chromeURL.replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + } + + const pageUrl = httpURL("helper_test_select_zoom.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + const input = content.document.getElementById("select"); + const focusPromise = new Promise(resolve => { + input.addEventListener("focus", resolve, { once: true }); + }); + input.focus(); + await focusPromise; + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.waitUntilApzStable(); + }); + + const initial_resolution = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => { + return content.window.windowUtils.getResolution(); + } + ); + + const initial_rect = await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + return content.wrappedJSObject.getSelectRect(); + }); + + ok( + initial_resolution > 0, + "The initial_resolution is " + + initial_resolution + + ", which is some sane value" + ); + + // First, get the position of the select popup when no translations have been applied. + const selectPopup = await openSelectPopup(); + + let popup_initial_rect = selectPopup.getBoundingClientRect(); + let popupInitialX = popup_initial_rect.left; + let popupInitialY = popup_initial_rect.top; + + await hideSelectPopup(); + + ok(popupInitialX > 0, "select position before zooming (x) " + popupInitialX); + ok(popupInitialY > 0, "select position before zooming (y) " + popupInitialY); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.pinchZoomInWithTouch(150, 300); + }); + + // Flush state and get the resolution we're at now + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.promiseApzFlushedRepaints(); + }); + + const final_resolution = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + () => { + return content.window.windowUtils.getResolution(); + } + ); + + ok( + final_resolution > initial_resolution, + "The final resolution (" + + final_resolution + + ") is greater after zooming in" + ); + + const final_rect = await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + return content.wrappedJSObject.getSelectRect(); + }); + + await openSelectPopup(); + + let popupRect = selectPopup.getBoundingClientRect(); + ok( + Math.abs(popupRect.left - popupInitialX) > 1, + "popup should have moved by more than one pixel (x) " + + popupRect.left + + " " + + popupInitialX + ); + ok( + Math.abs(popupRect.top - popupInitialY) > 1, + "popup should have moved by more than one pixel (y) " + + popupRect.top + + " " + + popupInitialY + ); + + ok( + Math.abs( + final_rect.left - initial_rect.left - (popupRect.left - popupInitialX) + ) < 1, + "popup should have moved approximately the same as the element (x)" + ); + let tolerance = navigator.platform.includes("Linux") ? final_rect.height : 1; + ok( + Math.abs( + final_rect.top - initial_rect.top - (popupRect.top - popupInitialY) + ) < tolerance, + "popup should have moved approximately the same as the element (y)" + ); + + ok( + true, + "initial " + + initial_rect.left + + " " + + initial_rect.top + + " " + + initial_rect.width + + " " + + initial_rect.height + ); + ok( + true, + "final " + + final_rect.left + + " " + + final_rect.top + + " " + + final_rect.width + + " " + + final_rect.height + ); + + ok( + true, + "initial popup " + + popup_initial_rect.left + + " " + + popup_initial_rect.top + + " " + + popup_initial_rect.width + + " " + + popup_initial_rect.height + ); + ok( + true, + "final popup " + + popupRect.left + + " " + + popupRect.top + + " " + + popupRect.width + + " " + + popupRect.height + ); + + await hideSelectPopup(); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/gfx/layers/apz/test/mochitest/browser_test_tab_drag_zoom.js b/gfx/layers/apz/test/mochitest/browser_test_tab_drag_zoom.js new file mode 100644 index 0000000000..e421e7bd3c --- /dev/null +++ b/gfx/layers/apz/test/mochitest/browser_test_tab_drag_zoom.js @@ -0,0 +1,103 @@ +/* This test is a a mash up of + https://searchfox.org/mozilla-central/rev/016925857e2f81a9425de9e03021dcf4251cafcc/gfx/layers/apz/test/mochitest/browser_test_select_zoom.js + https://searchfox.org/mozilla-central/rev/016925857e2f81a9425de9e03021dcf4251cafcc/browser/base/content/test/general/browser_tab_drag_drop_perwindow.js +*/ + +const EVENTUTILS_URL = + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js"; +var EventUtils = {}; + +Services.scriptloader.loadSubScript(EVENTUTILS_URL, EventUtils); + +add_task(async function test_dragging_zoom_handling() { + function httpURL(filename) { + let chromeURL = getRootDirectory(gTestPath) + filename; + //return chromeURL; + return chromeURL.replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" + ); + } + + const pageUrl = httpURL("helper_test_tab_drag_zoom.html"); + + let win1 = await BrowserTestUtils.openNewBrowserWindow(); + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + + let tab1 = await BrowserTestUtils.openNewForegroundTab(win1.gBrowser); + let tab2 = await BrowserTestUtils.openNewForegroundTab( + win2.gBrowser, + pageUrl + ); + + await SpecialPowers.spawn(tab2.linkedBrowser, [], async () => { + await content.wrappedJSObject.waitUntilApzStable(); + }); + + const initial_resolution = await SpecialPowers.spawn( + tab2.linkedBrowser, + [], + () => { + return content.window.windowUtils.getResolution(); + } + ); + + ok( + initial_resolution > 0, + "The initial_resolution is " + + initial_resolution + + ", which is some sane value" + ); + + let effect = EventUtils.synthesizeDrop( + tab2, + tab1, + [[{ type: TAB_DROP_TYPE, data: tab2 }]], + null, + win2, + win1 + ); + is(effect, "move", "Tab should be moved from win2 to win1."); + + await SpecialPowers.spawn(win1.gBrowser.selectedBrowser, [], async () => { + await content.wrappedJSObject.waitUntilApzStable(); + }); + + let resolution = await SpecialPowers.spawn( + win1.gBrowser.selectedBrowser, + [], + () => { + return content.window.windowUtils.getResolution(); + } + ); + + ok( + resolution == initial_resolution, + "The resolution (" + resolution + ") is the same after tab dragging" + ); + + await SpecialPowers.spawn(win1.gBrowser.selectedBrowser, [], async () => { + await content.wrappedJSObject.pinchZoomInWithTouch(150, 300); + }); + + // Flush state and get the resolution we're at now + await SpecialPowers.spawn(win1.gBrowser.selectedBrowser, [], async () => { + await content.wrappedJSObject.promiseApzFlushedRepaints(); + }); + + resolution = await SpecialPowers.spawn( + win1.gBrowser.selectedBrowser, + [], + () => { + return content.window.windowUtils.getResolution(); + } + ); + + ok( + resolution > initial_resolution, + "The resolution (" + resolution + ") is greater after zooming in" + ); + + await BrowserTestUtils.closeWindow(win1); + await BrowserTestUtils.closeWindow(win2); +}); diff --git a/gfx/layers/apz/test/mochitest/green100x100.png b/gfx/layers/apz/test/mochitest/green100x100.png new file mode 100644 index 0000000000..7df25f33bd Binary files /dev/null and b/gfx/layers/apz/test/mochitest/green100x100.png differ diff --git a/gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html b/gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html new file mode 100644 index 0000000000..4769861b2a --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_background_tab_load_scroll.html @@ -0,0 +1,147 @@ + + + + + + + + + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html b/gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html new file mode 100644 index 0000000000..f55a55f0fc --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_background_tab_scroll.html @@ -0,0 +1,9 @@ + + + + +
#scrolltarget
diff --git a/gfx/layers/apz/test/mochitest/helper_basic_onetouchpinch.html b/gfx/layers/apz/test/mochitest/helper_basic_onetouchpinch.html new file mode 100644 index 0000000000..1eb1d3dd03 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_basic_onetouchpinch.html @@ -0,0 +1,90 @@ + + + + + + Sanity check for one-touch pinch zooming + + + + + + + + Here is some text to stare at as the test runs. It serves no functional + purpose, but gives you an idea of the zoom level. It's harder to tell what + the zoom level is when the page is just solid white. + + diff --git a/gfx/layers/apz/test/mochitest/helper_basic_pan.html b/gfx/layers/apz/test/mochitest/helper_basic_pan.html new file mode 100644 index 0000000000..db96e34a70 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_basic_pan.html @@ -0,0 +1,73 @@ + + + + + + Sanity panning test + + + + + + + +
+ This div makes the page scrollable. +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_basic_scrollend.html b/gfx/layers/apz/test/mochitest/helper_basic_scrollend.html new file mode 100644 index 0000000000..9d71fe6251 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_basic_scrollend.html @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_basic_zoom.html b/gfx/layers/apz/test/mochitest/helper_basic_zoom.html new file mode 100644 index 0000000000..55717b31a4 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_basic_zoom.html @@ -0,0 +1,71 @@ + + + + + + Sanity check for zooming + + + + + + + + Here is some text to stare at as the test runs. It serves no functional + purpose, but gives you an idea of the zoom level. It's harder to tell what + the zoom level is when the page is just solid white. + + diff --git a/gfx/layers/apz/test/mochitest/helper_browser_test_utils.js b/gfx/layers/apz/test/mochitest/helper_browser_test_utils.js new file mode 100644 index 0000000000..ac68c9b1d4 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_browser_test_utils.js @@ -0,0 +1,11 @@ +// For hideSelectPopup. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/base/content/test/forms/head.js", + this +); + +function openSelectPopup(selector = "select", win = window) { + let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(win); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win); + return popupShownPromise; +} diff --git a/gfx/layers/apz/test/mochitest/helper_bug1162771.html b/gfx/layers/apz/test/mochitest/helper_bug1162771.html new file mode 100644 index 0000000000..3503341d41 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1162771.html @@ -0,0 +1,107 @@ + + + + + + Test for touchend on media elements + + + + + + + +

Tap on the colored boxes to hide them.

+ +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1271432.html b/gfx/layers/apz/test/mochitest/helper_bug1271432.html new file mode 100644 index 0000000000..1e40421a8f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1271432.html @@ -0,0 +1,573 @@ + + Ensure that the hit region doesn't get unexpectedly expanded + + + + + + + +Some text + +
+Scrolling on the very left edge of this div will work. +Scrolling on the right side of this div (starting with the left edge of the orange box above) should work, but doesn't.
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+
+
this div makes the page scrollable
+ diff --git a/gfx/layers/apz/test/mochitest/helper_bug1280013.html b/gfx/layers/apz/test/mochitest/helper_bug1280013.html new file mode 100644 index 0000000000..6b7d5cf4c3 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1280013.html @@ -0,0 +1,73 @@ + + + + + + Test for bug 1280013 + + + + + + + The iframe below is at (0, 400). Scroll it into view, and then scroll the contents. The content should be fully rendered in high-resolution. + + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1285070.html b/gfx/layers/apz/test/mochitest/helper_bug1285070.html new file mode 100644 index 0000000000..0df3a77f4a --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1285070.html @@ -0,0 +1,44 @@ + + + + + + Test pointer events are dispatched once for touch tap + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1299195.html b/gfx/layers/apz/test/mochitest/helper_bug1299195.html new file mode 100644 index 0000000000..b1aad7ef11 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1299195.html @@ -0,0 +1,47 @@ + + + + + + Test pointer events are dispatched once for touch tap + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1326290.html b/gfx/layers/apz/test/mochitest/helper_bug1326290.html new file mode 100644 index 0000000000..17b5a36eaa --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1326290.html @@ -0,0 +1,63 @@ + + + + + + Dragging the mouse on a inactive scrollframe's scrollbar + + + + + + + +
+
Some content inside the inactive scrollframe
+
+
Some content to ensure the root scrollframe is scrollable and the overflow:scroll div remains inactive
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1331693.html b/gfx/layers/apz/test/mochitest/helper_bug1331693.html new file mode 100644 index 0000000000..6dd0de13cb --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1331693.html @@ -0,0 +1,71 @@ + + + + + + Dragging the mouse on a scrollframe inside an SVGEffects + + + + + + + +
A div that generate an svg effects display item +
+
Some content inside the scrollframe
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1346632.html b/gfx/layers/apz/test/mochitest/helper_bug1346632.html new file mode 100644 index 0000000000..f91f8159b5 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1346632.html @@ -0,0 +1,89 @@ + + + + + + Dragging the scrollbar on a page with a fixed-positioned element just past the right edge of the content + + + + + + + +
+

+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1414336.html b/gfx/layers/apz/test/mochitest/helper_bug1414336.html new file mode 100644 index 0000000000..636328b7e4 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1414336.html @@ -0,0 +1,97 @@ + + + + + + Test for Bug 1414336 + + + + + + + + +Mozilla Bug 1414336 +

+
+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+

Test bug1414336

+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1462961.html b/gfx/layers/apz/test/mochitest/helper_bug1462961.html new file mode 100644 index 0000000000..d37d041800 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1462961.html @@ -0,0 +1,74 @@ + + + + + + Dragging the mouse on a transformed scrollframe inside a fixed-pos element + + + + + + + +
+
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1473108.html b/gfx/layers/apz/test/mochitest/helper_bug1473108.html new file mode 100644 index 0000000000..118ac3fc54 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1473108.html @@ -0,0 +1,50 @@ + + + + + + + Test for Bug 1473108 + + + + + + + + Mozilla Bug 1473108 + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1490393-2.html b/gfx/layers/apz/test/mochitest/helper_bug1490393-2.html new file mode 100644 index 0000000000..749110449e --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1490393-2.html @@ -0,0 +1,65 @@ + + + + + + Dragging the mouse on a scrollbar for a scrollframe inside nested transforms + + + + + + +
+
+
+
+
Yay text
+
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1490393.html b/gfx/layers/apz/test/mochitest/helper_bug1490393.html new file mode 100644 index 0000000000..6c18a2d24e --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1490393.html @@ -0,0 +1,64 @@ + + + + + + Dragging the mouse on a scrollbar for a scrollframe inside nested transforms + + + + + + +
+
+
+
+
Yay text
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1502010_unconsumed_pan.html b/gfx/layers/apz/test/mochitest/helper_bug1502010_unconsumed_pan.html new file mode 100644 index 0000000000..73badf4bc7 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1502010_unconsumed_pan.html @@ -0,0 +1,76 @@ + + + + + + Test pointercancel doesn't get sent for horizontal panning on a pan-y element + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1506497_touch_action_fixed_on_fixed.html b/gfx/layers/apz/test/mochitest/helper_bug1506497_touch_action_fixed_on_fixed.html new file mode 100644 index 0000000000..cc73fe99ea --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1506497_touch_action_fixed_on_fixed.html @@ -0,0 +1,96 @@ + + + + + + Test for Bug 1506497 + + + + + + + + +
+
+
Touch here and drag up
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1509575.html b/gfx/layers/apz/test/mochitest/helper_bug1509575.html new file mode 100644 index 0000000000..4c85d1db42 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1509575.html @@ -0,0 +1,71 @@ + + + + + + + Test for Bug 1509575 + + + + + + +
+ Now you're scrolled, now you're not? +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1519339_hidden_smoothscroll.html b/gfx/layers/apz/test/mochitest/helper_bug1519339_hidden_smoothscroll.html new file mode 100644 index 0000000000..225182f749 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1519339_hidden_smoothscroll.html @@ -0,0 +1,61 @@ + + + + + Test for bug 1519339 + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1544966_zoom_on_touch_action_none.html b/gfx/layers/apz/test/mochitest/helper_bug1544966_zoom_on_touch_action_none.html new file mode 100644 index 0000000000..7adfd5ba0f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1544966_zoom_on_touch_action_none.html @@ -0,0 +1,89 @@ + + + + + + Test for Bug 1544966 + + + + + + + + +
+ Put down two fingers at the same time and do a pinch action. +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1550510.html b/gfx/layers/apz/test/mochitest/helper_bug1550510.html new file mode 100644 index 0000000000..2f3be5dd2c --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1550510.html @@ -0,0 +1,66 @@ + + + + + + Dragging the mouse on a scrollbar for a transformed, filtered scrollframe + + + + + + +
+
+
+
+
+ yay text +
+
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1637113_main_thread_hit_test.html b/gfx/layers/apz/test/mochitest/helper_bug1637113_main_thread_hit_test.html new file mode 100644 index 0000000000..c58c7e40b6 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1637113_main_thread_hit_test.html @@ -0,0 +1,70 @@ + + + + + + + Test for Bug 1637113 + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1637135_narrow_viewport.html b/gfx/layers/apz/test/mochitest/helper_bug1637135_narrow_viewport.html new file mode 100644 index 0000000000..7b57416c04 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1637135_narrow_viewport.html @@ -0,0 +1,60 @@ + + + + + + + Test for Bug 1637135 + + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1638441_fixed_pos_hit_test.html b/gfx/layers/apz/test/mochitest/helper_bug1638441_fixed_pos_hit_test.html new file mode 100644 index 0000000000..3665ef5a31 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1638441_fixed_pos_hit_test.html @@ -0,0 +1,67 @@ + + + + + + + Test for Bug 1638441 + + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1638458_contextmenu.html b/gfx/layers/apz/test/mochitest/helper_bug1638458_contextmenu.html new file mode 100644 index 0000000000..a5a43f7ca1 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1638458_contextmenu.html @@ -0,0 +1,82 @@ + + + + + + + Test for Bug 1638458 + + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1648491_no_pointercancel_with_dtc.html b/gfx/layers/apz/test/mochitest/helper_bug1648491_no_pointercancel_with_dtc.html new file mode 100644 index 0000000000..3d8fdcf76e --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1648491_no_pointercancel_with_dtc.html @@ -0,0 +1,89 @@ + + + + + + Test for Bug 1648491 + + + + + + + + +
+ A two-finger pinch action here should send pointer events to content. +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1662800.html b/gfx/layers/apz/test/mochitest/helper_bug1662800.html new file mode 100644 index 0000000000..eb804a40f6 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1662800.html @@ -0,0 +1,61 @@ + + + + + + Dragging the mouse on a scrollbar for a scrollframe inside nested transforms with a scale component + + + + + + +
+
+
+
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1663731_no_pointercancel_on_second_touchstart.html b/gfx/layers/apz/test/mochitest/helper_bug1663731_no_pointercancel_on_second_touchstart.html new file mode 100644 index 0000000000..e0690c12c6 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1663731_no_pointercancel_on_second_touchstart.html @@ -0,0 +1,82 @@ + + + + + + Test for Bug 1663731 + + + + + + + + + A two-finger pinch action here should send pointer events to content and not do browser zooming. + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1669625.html b/gfx/layers/apz/test/mochitest/helper_bug1669625.html new file mode 100644 index 0000000000..95d2a4bc2c --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1669625.html @@ -0,0 +1,79 @@ + + + + + + Scrolling doesn't cause extra SchedulePaint calls + + + + + + + +
spacer
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1674935.html b/gfx/layers/apz/test/mochitest/helper_bug1674935.html new file mode 100644 index 0000000000..f5efa16d5f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1674935.html @@ -0,0 +1,76 @@ + + + + + Tests that keyboard arrow keys scroll a very specific page + + + + + + + + +
+
+ + + + + +
+
+
+

Firefox

+
+ + + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html b/gfx/layers/apz/test/mochitest/helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html new file mode 100644 index 0000000000..b9c31dfe89 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1682170_pointercancel_on_touchaction_pinchzoom.html @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + A two-finger pinch action here should trigger browser zoom and trigger a pointercancel to content. + Note that the code does a zoom-out and the page is already at min zoom, so + the zoom doesn't produce any visual effect. But the DOM events should be the + same either way. + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1695598.html b/gfx/layers/apz/test/mochitest/helper_bug1695598.html new file mode 100644 index 0000000000..fb6102e33d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1695598.html @@ -0,0 +1,123 @@ + + + Test for bug 1695598 + + + + + + + + +
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1714934_mouseevent_buttons.html b/gfx/layers/apz/test/mochitest/helper_bug1714934_mouseevent_buttons.html new file mode 100644 index 0000000000..35cea7de4f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1714934_mouseevent_buttons.html @@ -0,0 +1,40 @@ + + + + + + + Test for Bug 1714934 + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1719330.html b/gfx/layers/apz/test/mochitest/helper_bug1719330.html new file mode 100644 index 0000000000..c99b6e1012 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1719330.html @@ -0,0 +1,65 @@ + + + + + Tests that the arrow down key does not scroll by more than 1 element + + + + + + + +
+ + + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1756529.html b/gfx/layers/apz/test/mochitest/helper_bug1756529.html new file mode 100644 index 0000000000..e1767f4f57 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1756529.html @@ -0,0 +1,226 @@ + + + + + + Page scrolling bug test, helper page + + + + + + + + SmoothScrollPage not honored with MSD physics bug. + +
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tellus in metus vulputate eu. Vestibulum morbi blandit cursus risus at ultrices mi tempus imperdiet. Congue quisque egestas diam in. Pretium vulputate sapien nec sagittis aliquam malesuada bibendum arcu. Eleifend mi in nulla posuere. Proin libero nunc consequat interdum varius. Risus pretium quam vulputate dignissim suspendisse in est. Lacus vel facilisis volutpat est. Donec pretium vulputate sapien nec. Feugiat sed lectus vestibulum mattis. Platea dictumst quisque sagittis purus. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel. Enim facilisis gravida neque convallis a cras semper auctor. Placerat orci nulla pellentesque dignissim enim sit.

+

Augue neque gravida in fermentum et sollicitudin ac. Mattis enim ut tellus elementum sagittis vitae et. Malesuada nunc vel risus commodo viverra maecenas accumsan. Viverra nibh cras pulvinar mattis nunc sed. Lectus nulla at volutpat diam ut venenatis tellus in. Non tellus orci ac auctor. Magna etiam tempor orci eu lobortis. Malesuada nunc vel risus commodo viverra maecenas accumsan lacus vel. Sagittis orci a scelerisque purus. Tellus pellentesque eu tincidunt tortor. Vulputate dignissim suspendisse in est ante in. Tristique et egestas quis ipsum suspendisse. Quisque egestas diam in arcu cursus. Massa massa ultricies mi quis hendrerit dolor magna eget. Mattis nunc sed blandit libero volutpat sed. Consectetur purus ut faucibus pulvinar elementum integer enim.

+

Vestibulum lorem sed risus ultricies tristique nulla. Imperdiet nulla malesuada pellentesque elit eget gravida. Feugiat nisl pretium fusce id velit ut tortor pretium. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Id nibh tortor id aliquet lectus proin nibh nisl condimentum. Amet volutpat consequat mauris nunc congue nisi vitae suscipit tellus. Neque ornare aenean euismod elementum. Semper quis lectus nulla at. Massa sed elementum tempus egestas. Praesent elementum facilisis leo vel fringilla est ullamcorper eget nulla. Pellentesque elit eget gravida cum sociis natoque penatibus et. Massa enim nec dui nunc mattis enim. Laoreet suspendisse interdum consectetur libero id faucibus nisl. Fusce ut placerat orci nulla.

+

Vitae tempus quam pellentesque nec nam aliquam. Vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Nam libero justo laoreet sit amet. Arcu non sodales neque sodales. Nec ultrices dui sapien eget mi proin sed. Parturient montes nascetur ridiculus mus mauris vitae ultricies. Lacus sed viverra tellus in hac habitasse. Orci phasellus egestas tellus rutrum. Leo a diam sollicitudin tempor id eu nisl. Diam phasellus vestibulum lorem sed risus ultricies tristique nulla. Lectus nulla at volutpat diam ut venenatis tellus in. Cursus metus aliquam eleifend mi in nulla. Et ultrices neque ornare aenean euismod. Sit amet aliquam id diam maecenas ultricies mi. Volutpat diam ut venenatis tellus in metus vulputate eu.

+

Pellentesque elit ullamcorper dignissim cras tincidunt. Morbi tincidunt augue interdum velit euismod. Diam vel quam elementum pulvinar etiam non quam. Eget duis at tellus at urna. Posuere ac ut consequat semper viverra nam libero justo laoreet. Ac turpis egestas maecenas pharetra convallis posuere. Ultrices tincidunt arcu non sodales neque sodales ut etiam sit. In eu mi bibendum neque egestas. Pellentesque sit amet porttitor eget dolor morbi. Ac tortor dignissim convallis aenean et tortor at. Elementum tempus egestas sed sed risus pretium quam. Nisi scelerisque eu ultrices vitae auctor eu augue. Urna duis convallis convallis tellus id interdum velit laoreet id. Auctor eu augue ut lectus arcu bibendum at varius vel.

+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1780701.html b/gfx/layers/apz/test/mochitest/helper_bug1780701.html new file mode 100644 index 0000000000..f445f9abe6 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1780701.html @@ -0,0 +1,70 @@ + + + + + + Test that scroll snap wont't happen on zoomed content + + + + + + +
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug1783936.html b/gfx/layers/apz/test/mochitest/helper_bug1783936.html new file mode 100644 index 0000000000..8aec5eafef --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug1783936.html @@ -0,0 +1,74 @@ + + + + + + Test that scroll snap happens on pan end without fling + + + + + + +
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_bug982141.html b/gfx/layers/apz/test/mochitest/helper_bug982141.html new file mode 100644 index 0000000000..8ffda2dd2f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_bug982141.html @@ -0,0 +1,130 @@ + + + + + + + Test for Bug 982141, helper page + + + + + + Mozilla Bug 982141 + +
+
+ Wide content so that the vertical scrollbar for the parent div + doesn't eat into the 50px width and reduce the width of the + displayport. +
+ Line 1
+ Line 2
+ Line 3
+ Line 4
+ Line 5
+ Line 6
+ Line 7
+ Line 8
+ Line 9
+ Line 10
+ Line 11
+ Line 12
+ Line 13
+ Line 14
+ Line 15
+ Line 16
+ Line 17
+ Line 18
+ Line 19
+ Line 20
+ Line 21
+ Line 22
+ Line 23
+ Line 24
+ Line 25
+ Line 26
+ Line 27
+ Line 28
+ Line 29
+ Line 30
+ Line 31
+ Line 32
+ Line 33
+ Line 34
+ Line 35
+ Line 36
+ Line 37
+ Line 38
+ Line 39
+ Line 40
+ Line 41
+ Line 42
+ Line 43
+ Line 44
+ Line 45
+ Line 46
+ Line 40
+ Line 48
+ Line 49
+ Line 50
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_check_dp_size.html b/gfx/layers/apz/test/mochitest/helper_check_dp_size.html new file mode 100644 index 0000000000..0a81a69958 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_check_dp_size.html @@ -0,0 +1,124 @@ + + + + + + Test for Bug 1689492, helper page + + + + + + + Mozilla Bug 1689492 + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_apzforcedisabled.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_apzforcedisabled.html new file mode 100644 index 0000000000..404368a803 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_apzforcedisabled.html @@ -0,0 +1,93 @@ + + + + + + Checkerboarding while root scrollframe async-scrolls and a + subframe has APZ force disabled + + + + + + + +
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_no_multiplier.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_no_multiplier.html new file mode 100644 index 0000000000..149ef9fbba --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_no_multiplier.html @@ -0,0 +1,57 @@ + + + +Testcase for checkerboarding with displayport multipliers dropped to zero + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_scrollinfo.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_scrollinfo.html new file mode 100644 index 0000000000..2e718ad44b --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_scrollinfo.html @@ -0,0 +1,91 @@ + + + + Scrolling a scrollinfo layer and making sure it doesn't checkerboard + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_zoom_during_load.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_zoom_during_load.html new file mode 100644 index 0000000000..650b1c265d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_zoom_during_load.html @@ -0,0 +1,55 @@ + + + +Testcase for checkerboarding after zooming during page load + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_checkerboard_zoomoverflowhidden.html b/gfx/layers/apz/test/mochitest/helper_checkerboard_zoomoverflowhidden.html new file mode 100644 index 0000000000..63a07ebe9b --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_checkerboard_zoomoverflowhidden.html @@ -0,0 +1,150 @@ + + + + + + Checkerboarding in while scrolling a subframe when root scrollframe has + overflow hidden and pinch zoomed in + + + + + + + +
+
+

STR:

+
    +
  1. set apz.allow_zoom to true
  2. +
  3. visit any bugzilla site (like this one)
  4. +
  5. zoom into the page and observe the left edge of the viewport
  6. +
+

ER: content should be shown
+ AR: foreground content seems to disappear, looks like it's being cut off +

+

I attached a video of the STR to show the problem a little bit better. So far, I could only reproduce this on bugzilla. Words words words words words words words words words words words words words words words words words words words words words words.

+ +
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_click.html b/gfx/layers/apz/test/mochitest/helper_click.html new file mode 100644 index 0000000000..7c4501cb46 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_click.html @@ -0,0 +1,42 @@ + + + + + + Sanity mouse-clicking test + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_click_interrupt_animation.html b/gfx/layers/apz/test/mochitest/helper_click_interrupt_animation.html new file mode 100644 index 0000000000..adba0d90ea --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_click_interrupt_animation.html @@ -0,0 +1,96 @@ + + + + + + Clicking on the content (not scrollbar) should interrupt animations + + + + + + + + +
+
+ The above div is sized to 3x screen height so the linear gradient is more steep in terms of + color/pixel. We only scroll a few pages worth so we don't need the gradient all the way down. + And then we use a bottom-margin to make the page really big so the scrollthumb is + relatively small, giving us lots of space to click on the scrolltrack. + + diff --git a/gfx/layers/apz/test/mochitest/helper_content_response_timeout.html b/gfx/layers/apz/test/mochitest/helper_content_response_timeout.html new file mode 100644 index 0000000000..41a0319699 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_content_response_timeout.html @@ -0,0 +1,26 @@ + + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_disallow_doubletap_zoom_inside_oopif.html b/gfx/layers/apz/test/mochitest/helper_disallow_doubletap_zoom_inside_oopif.html new file mode 100644 index 0000000000..2c83cb1a1f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_disallow_doubletap_zoom_inside_oopif.html @@ -0,0 +1,58 @@ + + + + + + Check that double tapping inside an oop iframe doesn't work if the top + level content document doesn't allow zooming + + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_displayport_expiry.html b/gfx/layers/apz/test/mochitest/helper_displayport_expiry.html new file mode 100644 index 0000000000..023786a270 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_displayport_expiry.html @@ -0,0 +1,77 @@ + + + + Test for DisplayPort Expiry + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_div_pan.html b/gfx/layers/apz/test/mochitest/helper_div_pan.html new file mode 100644 index 0000000000..b740b2f16a --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_div_pan.html @@ -0,0 +1,43 @@ + + + + + + Sanity panning test for scrollable div + + + + + + +
+
+ This div makes the |outer| div scrollable. +
+
+
+ This div makes the top-level page scrollable. +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_dommousescroll.html b/gfx/layers/apz/test/mochitest/helper_dommousescroll.html new file mode 100644 index 0000000000..390db367f5 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_dommousescroll.html @@ -0,0 +1,33 @@ + + + Test that Direct Manipulation generated pan gesture events generate DOMMouseScroll events with reasonable line scroll amounts + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom.html new file mode 100644 index 0000000000..4cd613edbb --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom.html @@ -0,0 +1,50 @@ + + + + + + Sanity check for double-tap zooming + + + + + + + +
Text before the div.
+
+
Text after the div.
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_bug1702464.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_bug1702464.html new file mode 100644 index 0000000000..34d06fc039 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_bug1702464.html @@ -0,0 +1,90 @@ + + + + + + Check that double tapping internal calculations correctly convert the tap point + + + + + + + +
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos.html new file mode 100644 index 0000000000..1da5607d7e --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos.html @@ -0,0 +1,88 @@ + + + + + + Check that double tapping active scrollable elements in fixed pos work + + + + + + + + +
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos_overflow.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos_overflow.html new file mode 100644 index 0000000000..0658c562c1 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_fixedpos_overflow.html @@ -0,0 +1,113 @@ + + + + + + Check that double tapping elements with large overflow inside active scrollable elements in fixed pos work + + + + + + + + +
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_gencon.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_gencon.html new file mode 100644 index 0000000000..01b1f060d8 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_gencon.html @@ -0,0 +1,101 @@ + + + + + + Check that on generated content works + + + + + + + + +
some text
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_horizontal_center.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_horizontal_center.html new file mode 100644 index 0000000000..ab945fab1f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_horizontal_center.html @@ -0,0 +1,50 @@ + + + + + + Check that double tapping an element that doesn't fill the width of the viewport as maximum zoom centers it + + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable.html new file mode 100644 index 0000000000..21c3fb7e70 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable.html @@ -0,0 +1,85 @@ + + + + + + Check that tall element wider than the viewport doesn't scroll to the top + + + + + + + +
+
+ +
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable2.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable2.html new file mode 100644 index 0000000000..fc74ff1c89 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_hscrollable2.html @@ -0,0 +1,109 @@ + + + + + + Check that tall element wider than the viewport after zooming in doesn't scroll up + + + + + + + +
+
+ +
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_htmlelement.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_htmlelement.html new file mode 100644 index 0000000000..8fadc4eb3e --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_htmlelement.html @@ -0,0 +1,76 @@ + + + + + + Check that double tapping on a scrollbar does not scroll to top + + + + + + + + +
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_img.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_img.html new file mode 100644 index 0000000000..2559a3dd23 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_img.html @@ -0,0 +1,43 @@ + + + + + + Check that double tapping img works + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_large_overflow.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_large_overflow.html new file mode 100644 index 0000000000..02c4ca52f8 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_large_overflow.html @@ -0,0 +1,300 @@ + + + + + + Check that double tapping on overflow centers the zoom where we double tap + + + + + + + + +
+Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text +Text text text text text text text text text text text text text text text text + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_noscroll.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_noscroll.html new file mode 100644 index 0000000000..00e3638ba7 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_noscroll.html @@ -0,0 +1,59 @@ + + + + + + Check that double tapping something tall that we are already zoomed to doesn't scroll (it zooms out) + + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing.html new file mode 100644 index 0000000000..12005b3552 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing.html @@ -0,0 +1,46 @@ + + + + + + Check that double tapping when zoomed out and there is nothing to zoom to zooms in + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing_listener.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing_listener.html new file mode 100644 index 0000000000..82805fe321 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_nothing_listener.html @@ -0,0 +1,47 @@ + + + + + + Check that double tapping when zoomed out and there is nothing to zoom to does not zoom in if this is a non-passive wheel listener + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif.html new file mode 100644 index 0000000000..b6a51e8229 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_oopif.html @@ -0,0 +1,50 @@ + + + + + + Check that double tapping inside an oop iframe works + + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_scrolled_overflowhidden.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_scrolled_overflowhidden.html new file mode 100644 index 0000000000..bba8eeec08 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_scrolled_overflowhidden.html @@ -0,0 +1,82 @@ + + + + + + Check that double tapping when the page is overflow hidden and has been scrolled down by js works + + + + + + + + +
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_shadowdom.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_shadowdom.html new file mode 100644 index 0000000000..eed45028e2 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_shadowdom.html @@ -0,0 +1,69 @@ + + + + + + Check that double tapping shadow dom works + + + + + + + + +
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_small.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_small.html new file mode 100644 index 0000000000..1a2a52aff8 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_small.html @@ -0,0 +1,43 @@ + + + + + + Check that double tapping a small element works + + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_smooth.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_smooth.html new file mode 100644 index 0000000000..65b4c6698f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_smooth.html @@ -0,0 +1,161 @@ + + + + + + Check that double tapping zoom out animation is smooth + + + + + + + + +
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_square.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_square.html new file mode 100644 index 0000000000..c8278093af --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_square.html @@ -0,0 +1,61 @@ + + + + + + Check that double tapping on a square img doesn't cut off parts of the image + + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tablecell.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tablecell.html new file mode 100644 index 0000000000..f578ccc592 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tablecell.html @@ -0,0 +1,110 @@ + + + + + + Check that double tapping small table cells does not zoom + + + + + + + + + + + + + + + + + + + +
The table header
The table body
with two columns
+ + + + + + + + + + + + + +
The table header
The table body
with two columns
+ + + + + + + + + + + + + +
The table header
The table bodywith two columns
+ + + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tallwide.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tallwide.html new file mode 100644 index 0000000000..438c63b0b0 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_tallwide.html @@ -0,0 +1,85 @@ + + + + + + Check that double tapping on a tall element that is >90% width of viewport doesn't scroll to the top of it when scrolled down + + + + + + + + +
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_textarea.html b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_textarea.html new file mode 100644 index 0000000000..99616d9834 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_doubletap_zoom_textarea.html @@ -0,0 +1,43 @@ + + + + + + Check that double tapping textarea works + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_drag_bug1719913.html b/gfx/layers/apz/test/mochitest/helper_drag_bug1719913.html new file mode 100644 index 0000000000..36192aed6d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_drag_bug1719913.html @@ -0,0 +1,91 @@ + + + + + + Test for bug 1719913 + + + + + + + +
+
+
+
+
+
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_drag_click.html b/gfx/layers/apz/test/mochitest/helper_drag_click.html new file mode 100644 index 0000000000..01722c798d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_drag_click.html @@ -0,0 +1,69 @@ + + + + + + Sanity mouse-drag click test + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_drag_root_scrollbar.html b/gfx/layers/apz/test/mochitest/helper_drag_root_scrollbar.html new file mode 100644 index 0000000000..1665e168cb --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_drag_root_scrollbar.html @@ -0,0 +1,61 @@ + + + + + + Dragging the mouse on the viewport's scrollbar + + + + + + + +
Some content to ensure the root scrollframe is scrollable
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_drag_scroll.html b/gfx/layers/apz/test/mochitest/helper_drag_scroll.html new file mode 100644 index 0000000000..15ca680ad6 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_drag_scroll.html @@ -0,0 +1,653 @@ + + + + + + Dragging the mouse on a content-implemented scrollbar + + + + + + + + +
Drag up and down on this bar. The background/scrollbar shouldn't glitch
+This is a tall page
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_drag_scrollbar_hittest.html b/gfx/layers/apz/test/mochitest/helper_drag_scrollbar_hittest.html new file mode 100644 index 0000000000..7a1cab6dba --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_drag_scrollbar_hittest.html @@ -0,0 +1,100 @@ + + + + + + Test that the scrollbar thumb remains under the cursor during scrollbar dragging + + + + + + + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_empty.html b/gfx/layers/apz/test/mochitest/helper_empty.html new file mode 100644 index 0000000000..68cd9179f5 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_empty.html @@ -0,0 +1,4 @@ + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_oopif.html b/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_oopif.html new file mode 100644 index 0000000000..12221d0703 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_oopif.html @@ -0,0 +1,166 @@ + + + + + Test for scrolled out of view animation optimization in an OOPIF + + + + + + +
+
+ + + +
+ diff --git a/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_transformed_oopif.html b/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_transformed_oopif.html new file mode 100644 index 0000000000..d85d77f552 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_animation_styling_in_transformed_oopif.html @@ -0,0 +1,130 @@ + + + + + Test for scrolled out of view animation optimization in an OOPIF transformed by rotate(45deg) + + + + + + +
+
+
+ +
+
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_fission_basic.html b/gfx/layers/apz/test/mochitest/helper_fission_basic.html new file mode 100644 index 0000000000..dbc41477b9 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_basic.html @@ -0,0 +1,40 @@ + + + + + Basic sanity test that runs inside a fission-enabled window + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_checkerboard_severity.html b/gfx/layers/apz/test/mochitest/helper_fission_checkerboard_severity.html new file mode 100644 index 0000000000..ef7943b29f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_checkerboard_severity.html @@ -0,0 +1,138 @@ + + + + + + A test to make sure checkerboard severity isn't reported for non-scrollable + OOP iframe + + + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_empty.html b/gfx/layers/apz/test/mochitest/helper_fission_empty.html new file mode 100644 index 0000000000..6a95f76339 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_empty.html @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_event_region_override.html b/gfx/layers/apz/test/mochitest/helper_fission_event_region_override.html new file mode 100644 index 0000000000..82f529bebd --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_event_region_override.html @@ -0,0 +1,84 @@ + + + + + Ensure the event region override flags work properly + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_force_empty_hit_region.html b/gfx/layers/apz/test/mochitest/helper_fission_force_empty_hit_region.html new file mode 100644 index 0000000000..7baa552c37 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_force_empty_hit_region.html @@ -0,0 +1,82 @@ + + + + + Ensure the ForceEmptyHitRegion flag works properly + + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_positionedcontent.html b/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_positionedcontent.html new file mode 100644 index 0000000000..fc43ace0f1 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_positionedcontent.html @@ -0,0 +1,120 @@ + + + + + Ensure positioned content inside inactive scollframes but on top of OOPIFs hit-test properly + + + + + + + + + +
+
inside scroller
+
abspos inside scroller
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_under_oopif.html b/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_under_oopif.html new file mode 100644 index 0000000000..c3099f52ef --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_inactivescroller_under_oopif.html @@ -0,0 +1,88 @@ + + + + + Ensure inactive scollframes under OOPIFs hit-test properly + + + + + + + + + +
+
inside scroller
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_initial_displayport.html b/gfx/layers/apz/test/mochitest/helper_fission_initial_displayport.html new file mode 100644 index 0000000000..bb03ad4eb7 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_initial_displayport.html @@ -0,0 +1,105 @@ + + + + + Test that OOP iframe's displayport is initially set + + + + + + + + + +
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_irregular_areas.html b/gfx/layers/apz/test/mochitest/helper_fission_irregular_areas.html new file mode 100644 index 0000000000..8ef3367c06 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_irregular_areas.html @@ -0,0 +1,101 @@ + + + + + Ensure irregular areas on top of OOPIFs hit-test properly + + + + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_large_subframe.html b/gfx/layers/apz/test/mochitest/helper_fission_large_subframe.html new file mode 100644 index 0000000000..3d5595f48e --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_large_subframe.html @@ -0,0 +1,67 @@ + + + + + Test that large OOPIF does not get a too-large displayport + + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_scroll_handoff.html b/gfx/layers/apz/test/mochitest/helper_fission_scroll_handoff.html new file mode 100644 index 0000000000..3f75706778 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_scroll_handoff.html @@ -0,0 +1,50 @@ + + + + + scroll handoff + + + + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_scroll_oopif.html b/gfx/layers/apz/test/mochitest/helper_fission_scroll_oopif.html new file mode 100644 index 0000000000..2911b1eaf0 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_scroll_oopif.html @@ -0,0 +1,158 @@ + + + + + Test for async-scrolling an OOPIF and ensuring hit-testing still works + + + + + + + + + +
tall div to make the page scrollable
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_setResolution.html b/gfx/layers/apz/test/mochitest/helper_fission_setResolution.html new file mode 100644 index 0000000000..6bcf3fa2ce --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_setResolution.html @@ -0,0 +1,59 @@ + + + + + setResolutionAndScaleTo is properly delivered to OOP iframes + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_tap.html b/gfx/layers/apz/test/mochitest/helper_fission_tap.html new file mode 100644 index 0000000000..fab04bab26 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_tap.html @@ -0,0 +1,87 @@ + + + + + Test to ensure events get untransformed properly for OOP iframes + + + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_tap_in_nested_iframe_on_zoomed.html b/gfx/layers/apz/test/mochitest/helper_fission_tap_in_nested_iframe_on_zoomed.html new file mode 100644 index 0000000000..47a9c4bf8e --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_tap_in_nested_iframe_on_zoomed.html @@ -0,0 +1,106 @@ + + + + + Test to ensure events get delivered properly for a nested OOP iframe + + + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_tap_on_zoomed.html b/gfx/layers/apz/test/mochitest/helper_fission_tap_on_zoomed.html new file mode 100644 index 0000000000..5d99b972c2 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_tap_on_zoomed.html @@ -0,0 +1,93 @@ + + + + + Test to ensure events get delivered properly for an OOP iframe + + + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_touch.html b/gfx/layers/apz/test/mochitest/helper_fission_touch.html new file mode 100644 index 0000000000..fa317b9f1f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_touch.html @@ -0,0 +1,99 @@ + + + + + Test to ensure touch events for OOP iframes + + + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_transforms.html b/gfx/layers/apz/test/mochitest/helper_fission_transforms.html new file mode 100644 index 0000000000..193b5650fd --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_transforms.html @@ -0,0 +1,89 @@ + + + + + Test to ensure events get untransformed properly for OOP iframes + + + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_fission_utils.js b/gfx/layers/apz/test/mochitest/helper_fission_utils.js new file mode 100644 index 0000000000..ddcab62253 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fission_utils.js @@ -0,0 +1,130 @@ +// loadOOPIFrame expects apz_test_utils.js to be loaded as well, for promiseOneEvent. +/* import-globals-from apz_test_utils.js */ + +function fission_subtest_init() { + // Silence SimpleTest warning about missing assertions by having it wait + // indefinitely. We don't need to give it an explicit finish because the + // entire window this test runs in will be closed after subtestDone is called. + SimpleTest.waitForExplicitFinish(); + + // This is the point at which we inject the ok, is, subtestDone, etc. functions + // into this window. In particular this function should run after SimpleTest.js + // is imported, otherwise SimpleTest.js will clobber the functions with its + // own versions. This is implicitly enforced because if we call this function + // before SimpleTest.js is imported, the above line will throw an exception. + window.dispatchEvent(new Event("FissionTestHelper:Init")); +} + +/** + * Starts loading the given `iframePage` in the iframe element with the given + * id, and waits for it to load. + * Note that calling this function doesn't do the load directly; instead it + * returns an async function which can be added to a thenable chain. + */ +function loadOOPIFrame(iframeElementId, iframePage) { + return async function () { + if (window.location.href.startsWith("https://example.com/")) { + dump( + `WARNING: Calling loadOOPIFrame from ${window.location.href} so the iframe may not be OOP\n` + ); + ok(false, "Current origin is not example.com:443"); + } + + let url = + "https://example.com/browser/gfx/layers/apz/test/mochitest/" + iframePage; + let loadPromise = promiseOneEvent(window, "OOPIF:Load", function (e) { + return typeof e.data.content == "string" && e.data.content == url; + }); + let elem = document.getElementById(iframeElementId); + elem.src = url; + await loadPromise; + }; +} + +/** + * This is similar to the hitTest function in apz_test_utils.js, in that it + * does a hit-test for a point and returns the result. The difference is that + * in the fission world, the hit-test may land on an OOPIF, which means the + * result information will be in the APZ test data for the OOPIF process. This + * function checks both the current process and OOPIF process to see which one + * got a hit result, and returns the result regardless of which process got it. + * The caller is expected to check the layers id which will allow distinguishing + * the two cases. + */ +async function fissionHitTest(point, iframeElement) { + let get_iframe_compositor_test_data = function () { + let utils = SpecialPowers.getDOMWindowUtils(window); + return JSON.stringify(utils.getCompositorAPZTestData()); + }; + + let utils = SpecialPowers.getDOMWindowUtils(window); + + // Get the test data before doing the actual hit-test, to get a baseline + // of what we can ignore. + let oldParentTestData = utils.getCompositorAPZTestData(); + let oldIframeTestData = JSON.parse( + await FissionTestHelper.sendToOopif( + iframeElement, + `(${get_iframe_compositor_test_data})()` + ) + ); + + // Now do the hit-test + dump(`Hit-testing point (${point.x}, ${point.y}) in fission context\n`); + utils.sendMouseEvent( + "MozMouseHittest", + point.x, + point.y, + 0, + 0, + 0, + true, + 0, + 0, + true, + true + ); + + // Collect the new test data + let newParentTestData = utils.getCompositorAPZTestData(); + let newIframeTestData = JSON.parse( + await FissionTestHelper.sendToOopif( + iframeElement, + `(${get_iframe_compositor_test_data})()` + ) + ); + + // See which test data has new hit results + let hitResultCount = function (testData) { + return Object.keys(testData.hitResults).length; + }; + + let hitIframe = + hitResultCount(newIframeTestData) > hitResultCount(oldIframeTestData); + let hitParent = + hitResultCount(newParentTestData) > hitResultCount(oldParentTestData); + + // Extract the results from the appropriate test data + let lastHitResult = function (testData) { + let lastHit = + testData.hitResults[Object.keys(testData.hitResults).length - 1]; + return { + hitInfo: lastHit.hitResult, + scrollId: lastHit.scrollId, + layersId: lastHit.layersId, + }; + }; + if (hitIframe && hitParent) { + throw new Error( + "Both iframe and parent got hit-results, that is unexpected!" + ); + } else if (hitIframe) { + return lastHitResult(newIframeTestData); + } else if (hitParent) { + return lastHitResult(newParentTestData); + } else { + throw new Error( + "Neither iframe nor parent got the hit-result, that is unexpected!" + ); + } +} diff --git a/gfx/layers/apz/test/mochitest/helper_fixed_html_hittest.html b/gfx/layers/apz/test/mochitest/helper_fixed_html_hittest.html new file mode 100644 index 0000000000..27add6debb --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fixed_html_hittest.html @@ -0,0 +1,61 @@ + + + + + + Hittest position:fixed zoomed scroll + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_fixed_pos_displayport.html b/gfx/layers/apz/test/mochitest/helper_fixed_pos_displayport.html new file mode 100644 index 0000000000..adae691096 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fixed_pos_displayport.html @@ -0,0 +1,101 @@ + + + + + + position:fixed display port sizing + + + + + + +
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_fixed_position_scroll_hittest.html b/gfx/layers/apz/test/mochitest/helper_fixed_position_scroll_hittest.html new file mode 100644 index 0000000000..05cc2d0262 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fixed_position_scroll_hittest.html @@ -0,0 +1,51 @@ + + + + + + Hittest position:fixed zoomed scroll + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_fullscreen.html b/gfx/layers/apz/test/mochitest/helper_fullscreen.html new file mode 100644 index 0000000000..32de4979f2 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_fullscreen.html @@ -0,0 +1,53 @@ + + + + + + Tests that layout viewport is not larger than visual viewport on fullscreen + + + + + +
+
overflowed element
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_backface_hidden.html b/gfx/layers/apz/test/mochitest/helper_hittest_backface_hidden.html new file mode 100644 index 0000000000..0e84282e54 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_backface_hidden.html @@ -0,0 +1,67 @@ + + + + APZ hit-testing with backface-visibility:hidden + + + + + + + +
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_basic.html b/gfx/layers/apz/test/mochitest/helper_hittest_basic.html new file mode 100644 index 0000000000..a9e9f0c07f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_basic.html @@ -0,0 +1,141 @@ + + + + Various tests to exercise the APZ hit-testing codepaths + + + + + + +
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1119497.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1119497.html new file mode 100644 index 0000000000..1d1e0a922f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1119497.html @@ -0,0 +1,54 @@ + + + + A hit testing test for the scenario in bug 1119497 + + + + + + + +
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1257288.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1257288.html new file mode 100644 index 0000000000..d9b813dddf --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1257288.html @@ -0,0 +1,74 @@ + + + + A hit testing test for the scenario in bug 1257288 + + + + + + + +
+
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187.html new file mode 100644 index 0000000000..6cb58b6413 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187.html @@ -0,0 +1,69 @@ + + + + + + Check hittesting fission oop iframe with transform and pinch zoom works bug 1715187 + + + + + + + +
+ +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html new file mode 100644 index 0000000000..c0f5baf467 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715187_oopif.html @@ -0,0 +1,13 @@ + +
+
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369.html new file mode 100644 index 0000000000..d1128fa946 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369.html @@ -0,0 +1,74 @@ + + + + + + Check hittesting fission oop iframe with transform works bug 1715369 + + + + + + + + + +
+
+
+ +
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_iframe.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_iframe.html new file mode 100644 index 0000000000..034eb0429f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_iframe.html @@ -0,0 +1,13 @@ + + + +
diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html new file mode 100644 index 0000000000..1a6582ce60 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1715369_oopif.html @@ -0,0 +1,13 @@ + +
+
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-1.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-1.html new file mode 100644 index 0000000000..b84cb6b634 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-1.html @@ -0,0 +1,124 @@ + + + + A simple hit testing test that doesn't involve any transforms + + + + + + + +
+
+
+
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-2.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-2.html new file mode 100644 index 0000000000..8cd3388534 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-2.html @@ -0,0 +1,157 @@ + + + + A more involved hit testing test that involves css and async transforms + + + + + + + +
+
+
+
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-3.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-3.html new file mode 100644 index 0000000000..0ae01864a5 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-3.html @@ -0,0 +1,56 @@ + + + + A hit testing test involving a scenario with a scale transform + + + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-4.html b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-4.html new file mode 100644 index 0000000000..26ec487b3e --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_bug1730606-4.html @@ -0,0 +1,194 @@ + + + + A hit testing test involving a scenario with a scale transform + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_checkerboard.html b/gfx/layers/apz/test/mochitest/helper_hittest_checkerboard.html new file mode 100644 index 0000000000..eb2d583276 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_checkerboard.html @@ -0,0 +1,57 @@ + + + + APZ hit-testing over a checkerboarded area + + + + + + +
+ +
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_clippath.html b/gfx/layers/apz/test/mochitest/helper_hittest_clippath.html new file mode 100644 index 0000000000..0f8cfca519 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_clippath.html @@ -0,0 +1,118 @@ + + + + Hit-testing an iframe covered by an element with a clip-path + + + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_clipped_fixed_modal.html b/gfx/layers/apz/test/mochitest/helper_hittest_clipped_fixed_modal.html new file mode 100644 index 0000000000..122863967a --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_clipped_fixed_modal.html @@ -0,0 +1,85 @@ + + + + Hit-testing on content covered by a fullscreen fixed-position item clipped away + + + + + + + +
+
+ Filler to make the content div scrollable +
+
+ + + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_deep_scene_stack.html b/gfx/layers/apz/test/mochitest/helper_hittest_deep_scene_stack.html new file mode 100644 index 0000000000..a04b1d3e83 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_deep_scene_stack.html @@ -0,0 +1,57 @@ + + + + Exercising the APZ/WR hit-test with a deep scene that produces many results + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed-2.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed-2.html new file mode 100644 index 0000000000..0f20719d46 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed-2.html @@ -0,0 +1,74 @@ + + + + APZ hit-testing of fixed content when async-scrolled + + + + + + + +
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed-3.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed-3.html new file mode 100644 index 0000000000..2004ea9ae4 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed-3.html @@ -0,0 +1,113 @@ + + + + APZ hit-testing of fixed content when async-scrolled + + + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed.html new file mode 100644 index 0000000000..530c53fd7a --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed.html @@ -0,0 +1,82 @@ + + + + APZ hit-testing of fixed content when async-scrolled + + + + + + + +
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed_bg.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_bg.html new file mode 100644 index 0000000000..1eae84305d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_bg.html @@ -0,0 +1,53 @@ + + + + + + Hit-testing of fixed background image + + + + + + +
+
+ + + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed_in_scrolled_transform.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_in_scrolled_transform.html new file mode 100644 index 0000000000..93d1e6064d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_in_scrolled_transform.html @@ -0,0 +1,91 @@ + + + + Hit-testing on the special setup from fixed-pos-scrolled-clip-3.html + + + + + + + + +
+
+
+
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_fixed_item_over_oop_iframe.html b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_item_over_oop_iframe.html new file mode 100644 index 0000000000..a8b66cb835 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_fixed_item_over_oop_iframe.html @@ -0,0 +1,61 @@ + + + +Hit-testing of positioned item on top of oop iframe + + + + + +
+ Link +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1434846.html b/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1434846.html new file mode 100644 index 0000000000..986c7b8477 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1434846.html @@ -0,0 +1,56 @@ + + + + APZ hit-testing with floated subframe + + + + + + + +
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1443518.html b/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1443518.html new file mode 100644 index 0000000000..0e2e754375 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_float_bug1443518.html @@ -0,0 +1,56 @@ + + + + APZ hit-testing with floated subframe + + + + + + + +
+
+
+
A line of text that overflows because it's sufficiently long
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_hidden_inactive_scrollframe.html b/gfx/layers/apz/test/mochitest/helper_hittest_hidden_inactive_scrollframe.html new file mode 100644 index 0000000000..0abed82156 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_hidden_inactive_scrollframe.html @@ -0,0 +1,55 @@ + + + + APZ hit-testing with an inactive scrollframe that is visibility:hidden (bug 1673505) + + + + + + +
+
+
+ The body of this document is scrollable and is the main scrollable + element. On top of that we have a hidden fixed-pos item containing another + scrollframe, but this nested scrollframe is inactive. + Since the fixed-pos item is hidden, the nested scrollframe is hidden + too and shouldn't be the target of hit-testing. However, because it is + an inactive scrollframe, code to generate the "this is an inactive + scrollframe" area was marking it as hit-testable. This bug led to hit- + tests being mis-targeted to the nested scrollframe's layers id instead + of whatever was underneath. +
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_hoisted_scrollinfo.html b/gfx/layers/apz/test/mochitest/helper_hittest_hoisted_scrollinfo.html new file mode 100644 index 0000000000..3427c8da47 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_hoisted_scrollinfo.html @@ -0,0 +1,81 @@ + + + + Hit-testing on a scrollframe forced to be inactive by being inside a filter + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-2.html b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-2.html new file mode 100644 index 0000000000..9838a02aa9 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-2.html @@ -0,0 +1,69 @@ + + + Test that events are delivered to the correct document near an iframe inide a perspective transform + + + + + + +
+
+ +
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-3.html b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-3.html new file mode 100644 index 0000000000..7fb423ca1c --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective-3.html @@ -0,0 +1,70 @@ + + + Test that events are delivered with correct coordinates to an iframe inide a perspective transform + + + + + + +
+
+ +
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective.html b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective.html new file mode 100644 index 0000000000..d7858d4b00 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective.html @@ -0,0 +1,60 @@ + + + Test that events are delivered with correct coordinates to an iframe inide a perspective transform + + + + + + +
+ +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html new file mode 100644 index 0000000000..37f20ad725 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_iframe_perspective_child.html @@ -0,0 +1,13 @@ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_nested_transforms_bug1459696.html b/gfx/layers/apz/test/mochitest/helper_hittest_nested_transforms_bug1459696.html new file mode 100644 index 0000000000..9d0a9fa50c --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_nested_transforms_bug1459696.html @@ -0,0 +1,80 @@ + + + + APZ hit-testing with nested inactive transforms (bug 1459696) + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_obscuration.html b/gfx/layers/apz/test/mochitest/helper_hittest_obscuration.html new file mode 100644 index 0000000000..377b191359 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_obscuration.html @@ -0,0 +1,77 @@ + + + + Test hit-testing on content which is revealed by async scrolling + + + + + + + +
+
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html new file mode 100644 index 0000000000..c245258b68 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html @@ -0,0 +1,249 @@ + + + + Test APZ hit-testing while overscrolled + + + + + + + +
+
+ +
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_contextmenu.html b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_contextmenu.html new file mode 100644 index 0000000000..8aff3103dd --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_contextmenu.html @@ -0,0 +1,129 @@ + + + + Test APZ hit-testing while overscrolled + + + + + + + +
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_subframe.html b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_subframe.html new file mode 100644 index 0000000000..36918b3682 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_overscroll_subframe.html @@ -0,0 +1,132 @@ + + + + Test APZ hit-testing while overscrolled + + + + + + + +
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_pointerevents_svg.html b/gfx/layers/apz/test/mochitest/helper_hittest_pointerevents_svg.html new file mode 100644 index 0000000000..22b880736d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_pointerevents_svg.html @@ -0,0 +1,177 @@ + + + + Hit-testing a scrollframe covered by nonrectangular and pointer-events:none things + + + + + + + + +
+
+ + + +
+
+
+ + + +
+
+ +
+
+ + + +
+
+
+ + + +
+
+ +
+
+ + + +
+
+
+ + + +
+
+ +
+
+ + + +
+
+
+ + + +
+
+ +
+ Each of the gradients should be scrollable, except where the black stuff on the right cover them. + The brown square should not prevent scrolling. Similarly, the content on the left (which goes + underneath the scroller) shouldn't matter. +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_spam.html b/gfx/layers/apz/test/mochitest/helper_hittest_spam.html new file mode 100644 index 0000000000..cace5491a5 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_spam.html @@ -0,0 +1,100 @@ + + + + Test doing lots of hit-testing on a rapidly changing page + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_sticky_bug1478304.html b/gfx/layers/apz/test/mochitest/helper_hittest_sticky_bug1478304.html new file mode 100644 index 0000000000..395c688a12 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_sticky_bug1478304.html @@ -0,0 +1,58 @@ + + + + APZ hit-testing with sticky element inside a transform (bug 1478304) + + + + + + + +
+
+
sticky with transformed parent (click me or hover me and try a scroll)
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_hittest_touchaction.html b/gfx/layers/apz/test/mochitest/helper_hittest_touchaction.html new file mode 100644 index 0000000000..acabfcc07b --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_hittest_touchaction.html @@ -0,0 +1,353 @@ + + + + Testing APZ hit-test with touch-action boxes + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_horizontal_checkerboard.html b/gfx/layers/apz/test/mochitest/helper_horizontal_checkerboard.html new file mode 100644 index 0000000000..0355243738 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_horizontal_checkerboard.html @@ -0,0 +1,65 @@ + + + +Testcase for checkerboarding during horizontal scrolling + + + + + + +
+ + + + diff --git a/gfx/layers/apz/test/mochitest/helper_iframe1.html b/gfx/layers/apz/test/mochitest/helper_iframe1.html new file mode 100644 index 0000000000..047da96bd4 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_iframe1.html @@ -0,0 +1,14 @@ + + + + + + + +
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_iframe2.html b/gfx/layers/apz/test/mochitest/helper_iframe2.html new file mode 100644 index 0000000000..fee3883e95 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_iframe2.html @@ -0,0 +1,14 @@ + + + + + + + +
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_iframe_pan.html b/gfx/layers/apz/test/mochitest/helper_iframe_pan.html new file mode 100644 index 0000000000..032133c255 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_iframe_pan.html @@ -0,0 +1,49 @@ + + + + + + Sanity panning test for scrollable div + + + + + + + +
+ This div makes the top-level page scrollable. +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_iframe_textarea.html b/gfx/layers/apz/test/mochitest/helper_iframe_textarea.html new file mode 100644 index 0000000000..f4a934db74 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_iframe_textarea.html @@ -0,0 +1,12 @@ + + + + + + +
ABC
+ + +
ABC
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_interrupted_reflow.html b/gfx/layers/apz/test/mochitest/helper_interrupted_reflow.html new file mode 100644 index 0000000000..0dcaf1d67d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_interrupted_reflow.html @@ -0,0 +1,712 @@ + + + + + Test for bug 1292781 + + + + + + + + +Mozilla Bug 1292781 +

+
+

The frame reconstruction should not leave this scrollframe in a bad state

+
+
+ this is the top of the scrollframe. +
this is a box
+
this is a box
+
this is a box
+
this is a box
+ this is near the top of the scrollframe. +
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+ this is near the bottom of the scrollframe. +
this is a box
+
this is a box
+
this is a box
+
this is a box
+
this is a box
+ this is the bottom of the scrollframe. +
+
+
+ +
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/helper_key_scroll.html b/gfx/layers/apz/test/mochitest/helper_key_scroll.html
new file mode 100644
index 0000000000..021e2803b7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_key_scroll.html
@@ -0,0 +1,109 @@
+
+
+
+
+  
+  Async key scrolling test, helper page
+  
+  
+  
+  
+
+
+  Async key scrolling test
+  
+  
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_long_tap.html b/gfx/layers/apz/test/mochitest/helper_long_tap.html new file mode 100644 index 0000000000..2fdb2472ec --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_long_tap.html @@ -0,0 +1,166 @@ + + + + + + Ensure we get a touch-cancel after a contextmenu comes up + + + + + + + Link to nowhere + + diff --git a/gfx/layers/apz/test/mochitest/helper_main_thread_smooth_scroll_scrollend.html b/gfx/layers/apz/test/mochitest/helper_main_thread_smooth_scroll_scrollend.html new file mode 100644 index 0000000000..4f07db516e --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_main_thread_smooth_scroll_scrollend.html @@ -0,0 +1,47 @@ + + + + + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_mainthread_scroll_bug1662379.html b/gfx/layers/apz/test/mochitest/helper_mainthread_scroll_bug1662379.html new file mode 100644 index 0000000000..bef8f05f17 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_mainthread_scroll_bug1662379.html @@ -0,0 +1,168 @@ + + + + + + +
+
+
    +
  • Test item 1
  • +
  • Test item 2
  • +
  • Test item 3
  • +
  • Test item 4
  • +
  • Test item 5
  • +
  • Test item 6
  • +
  • Test item 7
  • +
  • Test item 8
  • +
  • Test item 9
  • +
  • Test item 10
  • +
  • Test item 11
  • +
  • Test item 13
  • +
  • Test item 14
  • +
  • Test item 15
  • +
  • Test item 16
  • +
  • Test item 17
  • +
  • Test item 18
  • +
  • Test item 19
  • +
+
+
+
+ Steps to reproduce: +
    +
  1. Scroll the list of "test items" all the way to the bottom +
  2. Click on the reparent button below +
  3. Click on one of the test items +
  4. The `clickTarget` JS variable should match the thing you clicked on +
+
+ +
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_minimum_scale_1_0.html b/gfx/layers/apz/test/mochitest/helper_minimum_scale_1_0.html new file mode 100644 index 0000000000..17ccb3a54d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_minimum_scale_1_0.html @@ -0,0 +1,46 @@ + + + + + + Tests that the layout viewport is expanted to the minimum scale size (minimim-scale >= 1.0) + + + + + +
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_no_scalable_with_initial_scale.html b/gfx/layers/apz/test/mochitest/helper_no_scalable_with_initial_scale.html new file mode 100644 index 0000000000..7280a26006 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_no_scalable_with_initial_scale.html @@ -0,0 +1,48 @@ + + + + + + Tests that the layout viewport is not expanted to the minimum scale size if user-scalable=no is specified + + + + + +
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_onetouchpinch_nested.html b/gfx/layers/apz/test/mochitest/helper_onetouchpinch_nested.html new file mode 100644 index 0000000000..54b578b496 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_onetouchpinch_nested.html @@ -0,0 +1,103 @@ + + + + + + One-touch pinch zooming while on a non-root scroller + + + + + + + + + Here is some text outside the scrollable div. +
+ Here is some text inside the scrollable div. +
This div actually makes it overflow.
+
+
This div makes the body scrollable.
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_overflowhidden_zoom.html b/gfx/layers/apz/test/mochitest/helper_overflowhidden_zoom.html new file mode 100644 index 0000000000..6c12008e4b --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_overflowhidden_zoom.html @@ -0,0 +1,83 @@ + + + + + + Tests that zooming in and out doesn't change the scroll position on an overflow hidden document + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_override_root.html b/gfx/layers/apz/test/mochitest/helper_override_root.html new file mode 100644 index 0000000000..81c1b34938 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_override_root.html @@ -0,0 +1,62 @@ + + + + + + Simple wheel scroll cancellation + + + + + + + This page should not be wheel-scrollable. + + diff --git a/gfx/layers/apz/test/mochitest/helper_override_subdoc.html b/gfx/layers/apz/test/mochitest/helper_override_subdoc.html new file mode 100644 index 0000000000..910f36ddc3 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_override_subdoc.html @@ -0,0 +1,15 @@ + + + + + + Wheel scroll cancellation inside iframe + + + + + This just loads helper_override_root in an iframe, so that we test event + regions overriding on in-process subdocuments. + + + diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425573.html b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425573.html new file mode 100644 index 0000000000..817522f9c3 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425573.html @@ -0,0 +1,44 @@ + + + Wheel-scrolling over inactive subframe with overscroll-behavior + + + + + + + +
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425603.html b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425603.html new file mode 100644 index 0000000000..8324530c95 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1425603.html @@ -0,0 +1,76 @@ + + + + Scrolling over checkerboarded area respects overscroll-behavior + + + + + + + +
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1494440.html b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1494440.html new file mode 100644 index 0000000000..3f12e36102 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_overscroll_behavior_bug1494440.html @@ -0,0 +1,50 @@ + + + Inactive iframe with overscroll-behavior + + + + + + +
+ + + + diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_in_apz_test_data.html b/gfx/layers/apz/test/mochitest/helper_overscroll_in_apz_test_data.html new file mode 100644 index 0000000000..ed05f25819 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_overscroll_in_apz_test_data.html @@ -0,0 +1,29 @@ + + + +A simple test checks "overscrolled" info in APZTestData +Tests scroll anchoring updates in-progress wheel scrolling __relatively__ + + + +
+ diff --git a/gfx/layers/apz/test/mochitest/helper_overscroll_in_subscroller.html b/gfx/layers/apz/test/mochitest/helper_overscroll_in_subscroller.html new file mode 100644 index 0000000000..5936de97f7 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_overscroll_in_subscroller.html @@ -0,0 +1,165 @@ + + + + + Tests that the overscroll gutter in a sub scroll container is restored if it's + no longer target scroll container + + + + + + +
+
+
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-1.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-1.html new file mode 100644 index 0000000000..6c986b0ca1 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-1.html @@ -0,0 +1,88 @@ + + + + APZ overscroll handoff for fixed elements + + + + + + + +
+
+
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-2.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-2.html new file mode 100644 index 0000000000..29b11072ca --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-2.html @@ -0,0 +1,65 @@ + + + APZ overscroll handoff for fixed elements + + + + + + +
+
+
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-3.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-3.html new file mode 100644 index 0000000000..4a0687ba20 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-3.html @@ -0,0 +1,77 @@ + + + APZ overscroll handoff for fixed elements + + + + + + +
+
+
+
+
+
+
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-4.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-4.html new file mode 100644 index 0000000000..7394984ce3 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-4.html @@ -0,0 +1,79 @@ + + + APZ overscroll handoff for fixed elements + + + + + + +
+
+
+
+
+
+
+
+
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-5.html b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-5.html new file mode 100644 index 0000000000..3d62287c7c --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_position_fixed_scroll_handoff-5.html @@ -0,0 +1,110 @@ + + + + APZ overscroll handoff for fixed elements in a subdoc + + + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_position_sticky_flicker.html b/gfx/layers/apz/test/mochitest/helper_position_sticky_flicker.html new file mode 100644 index 0000000000..28495a7122 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_position_sticky_flicker.html @@ -0,0 +1,25 @@ + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_position_sticky_scroll_handoff.html b/gfx/layers/apz/test/mochitest/helper_position_sticky_scroll_handoff.html new file mode 100644 index 0000000000..ae7815a2fc --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_position_sticky_scroll_handoff.html @@ -0,0 +1,88 @@ + + + + APZ overscroll handoff for sticky elements + + + + + + + +
+
+
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_programmatic_scroll_behavior.html b/gfx/layers/apz/test/mochitest/helper_programmatic_scroll_behavior.html new file mode 100644 index 0000000000..721ce7e538 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_programmatic_scroll_behavior.html @@ -0,0 +1,81 @@ + + + + + + + + + + + +
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_relative_scroll_smoothness.html b/gfx/layers/apz/test/mochitest/helper_relative_scroll_smoothness.html new file mode 100644 index 0000000000..c8907c6d5d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_relative_scroll_smoothness.html @@ -0,0 +1,141 @@ + + + + + + + + +What happens if main thread scrolls? + + + diff --git a/gfx/layers/apz/test/mochitest/helper_reset_zoom_bug1818967.html b/gfx/layers/apz/test/mochitest/helper_reset_zoom_bug1818967.html new file mode 100644 index 0000000000..4a4d7e34ca --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_reset_zoom_bug1818967.html @@ -0,0 +1,55 @@ + + + + +Test that we do not checkerboard after resetting the pinch-zoom scale + + + + + +
+ + + + diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_on_wheel.html b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_on_wheel.html new file mode 100644 index 0000000000..12d307fe57 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_on_wheel.html @@ -0,0 +1,59 @@ + + + +Tests scroll anchoring updates in-progress wheel scrolling __relatively__ + + + + +
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll.html b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll.html new file mode 100644 index 0000000000..7bdfa83f24 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll.html @@ -0,0 +1,54 @@ + + + +Tests scroll anchoring interaction with smooth visual scrolling. + + + + +
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll_with_set_timeout.html b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll_with_set_timeout.html new file mode 100644 index 0000000000..07ba816c36 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_anchoring_smooth_scroll_with_set_timeout.html @@ -0,0 +1,56 @@ + + + +Tests scroll anchoring interaction with smooth visual scrolling with set timeout. + + + + +
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html new file mode 100644 index 0000000000..727d0e4fd1 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_perspective.html @@ -0,0 +1,45 @@ + + + Wheel-scrolling over inactive subframe with perspective + + + + + + + +
+
+
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html new file mode 100644 index 0000000000..44c3cf3217 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_inactive_zindex.html @@ -0,0 +1,46 @@ + + + Wheel-scrolling over inactive subframe with z-index + + + + + + + +
+
+
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1516056.html b/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1516056.html new file mode 100644 index 0000000000..99952dddd4 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1516056.html @@ -0,0 +1,62 @@ + + + + + + Test for bug 1516056: "scroll into view" respects bounds on layout scroll position + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1562757.html b/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1562757.html new file mode 100644 index 0000000000..4aff2901d7 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_into_view_bug1562757.html @@ -0,0 +1,64 @@ + + + + + + Test for bug 1562757: "scroll into view" in iframe respects bounds on layout scroll position + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_by_wheel.html b/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_by_wheel.html new file mode 100644 index 0000000000..f9303253b8 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_by_wheel.html @@ -0,0 +1,65 @@ + + + + + + + +A scroll linked effect scrolled by wheel events + +
+ diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_detector.html b/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_detector.html new file mode 100644 index 0000000000..aaa4b43829 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_linked_effect_detector.html @@ -0,0 +1,108 @@ + + + + + + +ScrollLinkedEffectDetector tests + +
+ diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html b/gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html new file mode 100644 index 0000000000..5fbbc1437f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_on_position_fixed.html @@ -0,0 +1,60 @@ + + + Wheel-scrolling over position:fixed and position:sticky elements, in the top-level document as well as iframes + + + + + + +
sticky
+
fixed
+ + +
+
scrollable content inside a fixed-pos item
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_over_scrollbar.html b/gfx/layers/apz/test/mochitest/helper_scroll_over_scrollbar.html new file mode 100644 index 0000000000..b7a8698cf8 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_over_scrollbar.html @@ -0,0 +1,48 @@ + + + Wheel-scrolling over scrollbar + + + + + + + +
+
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_no_valid_snap_position.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_no_valid_snap_position.html new file mode 100644 index 0000000000..00f5e7d344 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_no_valid_snap_position.html @@ -0,0 +1,45 @@ + + + + + + No snapping occurs if there is no valid snap position + + + + + + +
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_panning.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_panning.html new file mode 100644 index 0000000000..f28a2f9396 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_panning.html @@ -0,0 +1,93 @@ + + + + + + Skip re-snapping during pan gesture + + + + + + +
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_scrollbar_dragging.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_scrollbar_dragging.html new file mode 100644 index 0000000000..ca2be3916f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_not_resnap_during_scrollbar_dragging.html @@ -0,0 +1,105 @@ + + + + + + Skip re-snapping during scrollbar dragging + + + + + + +
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_on_page_down_scroll.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_on_page_down_scroll.html new file mode 100644 index 0000000000..d0779cdb2d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_on_page_down_scroll.html @@ -0,0 +1,84 @@ + + + + + + Page scroll snaps a snap point in the same page rather than the one in the next page + + + + + + + +
1
+
2
+
3
+
4
+
5
+ + + + diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scroll.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scroll.html new file mode 100644 index 0000000000..ff1c78bef5 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scroll.html @@ -0,0 +1,81 @@ + + + + + + Re-snapping to the last snapped element on APZ + + + + + + +
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scrollBy.html b/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scrollBy.html new file mode 100644 index 0000000000..2b034da933 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_snap_resnap_after_async_scrollBy.html @@ -0,0 +1,72 @@ + + + + + + Re-snapping to the last snapped element on APZ + + + + + + +
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_tables_perspective.html b/gfx/layers/apz/test/mochitest/helper_scroll_tables_perspective.html new file mode 100644 index 0000000000..404274d3f4 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_tables_perspective.html @@ -0,0 +1,66 @@ + + + + + + + + + +
+
+
+
+
+ A
+ B
+ C
+ D
+ E
+ f
+ g
+ h
+ i
+ j
+
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_scroll_thumb_dragging.html b/gfx/layers/apz/test/mochitest/helper_scroll_thumb_dragging.html new file mode 100644 index 0000000000..821cf00be7 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scroll_thumb_dragging.html @@ -0,0 +1,20 @@ + + + + + + +
+ +
+ diff --git a/gfx/layers/apz/test/mochitest/helper_scrollbar_snap_bug1501062.html b/gfx/layers/apz/test/mochitest/helper_scrollbar_snap_bug1501062.html new file mode 100644 index 0000000000..ec18fd856d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scrollbar_snap_bug1501062.html @@ -0,0 +1,135 @@ + + + + + + Exercising the slider.snapMultiplier code + + + + + +
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_scrollbarbutton_repeat.html b/gfx/layers/apz/test/mochitest/helper_scrollbarbutton_repeat.html new file mode 100644 index 0000000000..723b250bd8 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scrollbarbutton_repeat.html @@ -0,0 +1,101 @@ + + + + + + Basic test that click and hold on a scrollbar button works as expected + + + + + + + +
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_scrollbarbuttonclick_checkerboard.html b/gfx/layers/apz/test/mochitest/helper_scrollbarbuttonclick_checkerboard.html new file mode 100644 index 0000000000..e7b7895966 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scrollbarbuttonclick_checkerboard.html @@ -0,0 +1,75 @@ + + + + + + Test that repeated scrollbar button clicks do not cause checkerboarding + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_scrollby_bug1531796.html b/gfx/layers/apz/test/mochitest/helper_scrollby_bug1531796.html new file mode 100644 index 0000000000..5e6f0e1833 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scrollby_bug1531796.html @@ -0,0 +1,36 @@ + + + + + + Test that scrollBy() doesn't scroll more than it should + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_scrollend_bubbles.html b/gfx/layers/apz/test/mochitest/helper_scrollend_bubbles.html new file mode 100644 index 0000000000..d0d763b474 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scrollend_bubbles.html @@ -0,0 +1,99 @@ + + + + + + + + + + + +
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_scrollframe_activation_on_load.html b/gfx/layers/apz/test/mochitest/helper_scrollframe_activation_on_load.html new file mode 100644 index 0000000000..1947a89a8f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scrollframe_activation_on_load.html @@ -0,0 +1,89 @@ + + + + + + Test for Bug 1151663, helper page + + + + + + + Mozilla Bug 1151663 +
+ +
+
+ +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_scrollto_tap.html b/gfx/layers/apz/test/mochitest/helper_scrollto_tap.html new file mode 100644 index 0000000000..a1fb3f67a6 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_scrollto_tap.html @@ -0,0 +1,59 @@ + + + + + + Sanity touch-tapping test + + + + + + +
spacer
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_self_closer.html b/gfx/layers/apz/test/mochitest/helper_self_closer.html new file mode 100644 index 0000000000..09a9286c06 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_self_closer.html @@ -0,0 +1,12 @@ + + + + + + +See ya! (This window will close itself) diff --git a/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam.html b/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam.html new file mode 100644 index 0000000000..154ed3cafe --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam.html @@ -0,0 +1,51 @@ + + + + + + Test for scenario in bug 1228407 + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam_interleaved.html b/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam_interleaved.html new file mode 100644 index 0000000000..003ae49ea5 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_smoothscroll_spam_interleaved.html @@ -0,0 +1,57 @@ + + + + + + Test for scenario in bug 1228407 with two scrollframes + + + + + + + +
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_subframe_style.css b/gfx/layers/apz/test/mochitest/helper_subframe_style.css new file mode 100644 index 0000000000..5af9640802 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_subframe_style.css @@ -0,0 +1,15 @@ +body { + height: 500px; +} + +.inner-frame { + margin-top: 50px; /* this should be at least 30px */ + height: 200%; + width: 75%; + overflow: scroll; +} +.inner-content { + height: 200%; + width: 200%; + background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px); +} diff --git a/gfx/layers/apz/test/mochitest/helper_tall.html b/gfx/layers/apz/test/mochitest/helper_tall.html new file mode 100644 index 0000000000..7fde795fdc --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_tall.html @@ -0,0 +1,504 @@ + + +This is a tall page
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_tap.html b/gfx/layers/apz/test/mochitest/helper_tap.html new file mode 100644 index 0000000000..f987299447 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_tap.html @@ -0,0 +1,32 @@ + + + + + + Sanity touch-tapping test + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_tap_default_passive.html b/gfx/layers/apz/test/mochitest/helper_tap_default_passive.html new file mode 100644 index 0000000000..a1f276224b --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_tap_default_passive.html @@ -0,0 +1,81 @@ + + + + + + Ensure APZ doesn't wait for passive listeners + + + + + + + Link to nowhere + + + diff --git a/gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html b/gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html new file mode 100644 index 0000000000..7d739924f0 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_tap_fullzoom.html @@ -0,0 +1,33 @@ + + + + + + Sanity touch-tapping test with fullzoom + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_tap_passive.html b/gfx/layers/apz/test/mochitest/helper_tap_passive.html new file mode 100644 index 0000000000..647564c08a --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_tap_passive.html @@ -0,0 +1,66 @@ + + + + + + Ensure APZ doesn't wait for passive listeners + + + + + + + Link to nowhere + + diff --git a/gfx/layers/apz/test/mochitest/helper_test_autoscrolling_in_oop_frame.html b/gfx/layers/apz/test/mochitest/helper_test_autoscrolling_in_oop_frame.html new file mode 100644 index 0000000000..c1826f583f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_test_autoscrolling_in_oop_frame.html @@ -0,0 +1,9 @@ + + + + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_test_reset_scaling_zoom.html b/gfx/layers/apz/test/mochitest/helper_test_reset_scaling_zoom.html new file mode 100644 index 0000000000..31779410da --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_test_reset_scaling_zoom.html @@ -0,0 +1,23 @@ + + + + + + + + +Here is some text to stare at as the test runs. It serves no functional +purpose, but gives you an idea of the zoom level. It's harder to tell what +the zoom level is when the page is just solid white. + diff --git a/gfx/layers/apz/test/mochitest/helper_test_select_popup_position.html b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position.html new file mode 100644 index 0000000000..c810751b2f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position.html @@ -0,0 +1,23 @@ + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_transformed_in_parent.html b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_transformed_in_parent.html new file mode 100644 index 0000000000..860ca079de --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_transformed_in_parent.html @@ -0,0 +1,25 @@ + + + + + +
+ +
+ diff --git a/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_zoomed.html b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_zoomed.html new file mode 100644 index 0000000000..2ec079369e --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_test_select_popup_position_zoomed.html @@ -0,0 +1,25 @@ + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_test_select_zoom.html b/gfx/layers/apz/test/mochitest/helper_test_select_zoom.html new file mode 100644 index 0000000000..d3fc5fcb67 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_test_select_zoom.html @@ -0,0 +1,43 @@ + + + + + + + + +Here is some text to stare at as the test runs. It serves no functional +purpose, but gives you an idea of the zoom level. It's harder to tell what +the zoom level is when the page is just solid white. + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_test_tab_drag_zoom.html b/gfx/layers/apz/test/mochitest/helper_test_tab_drag_zoom.html new file mode 100644 index 0000000000..9f0175468c --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_test_tab_drag_zoom.html @@ -0,0 +1,18 @@ + + + + + + + +Here is some text to stare at as the test runs. It serves no functional +purpose, but gives you an idea of the zoom level. It's harder to tell what +the zoom level is when the page is just solid white. + + + diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action.html b/gfx/layers/apz/test/mochitest/helper_touch_action.html new file mode 100644 index 0000000000..10038de29f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_touch_action.html @@ -0,0 +1,123 @@ + + + + + + Sanity touch-action test + + + + + + +
+ This div makes the page scrollable on both axes.
+ This is the second line of text.
+ This is the third line of text.
+ This is the fourth line of text. +
+ +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_complex.html b/gfx/layers/apz/test/mochitest/helper_touch_action_complex.html new file mode 100644 index 0000000000..b8df34bfca --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_touch_action_complex.html @@ -0,0 +1,137 @@ + + + + + + Complex touch-action test + + + + + + +
+
+
+ + + + +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_block.html b/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_block.html new file mode 100644 index 0000000000..ca593c0db5 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_block.html @@ -0,0 +1,39 @@ + + + + + + Touch-action with sorted element + + + + + + +
+
+ +
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_zindex.html b/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_zindex.html new file mode 100644 index 0000000000..5b0d57a18d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_touch_action_ordering_zindex.html @@ -0,0 +1,37 @@ + + + + + + Touch-action with sorted element + + + + + + +
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html b/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html new file mode 100644 index 0000000000..6a8a09e55a --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_touch_action_regions.html @@ -0,0 +1,345 @@ + + + + + + Test to ensure APZ doesn't always wait for touch-action + + + + + + +
+
+ This is a colored div that will move on the screen as the scroller scrolls. +
+
+ This is a large div to make the scroller scrollable. +
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html b/gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html new file mode 100644 index 0000000000..d31483f4f6 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_touch_action_zero_opacity_bug1500864.html @@ -0,0 +1,45 @@ + + + + + + Touch-action on a zero-opacity element + + + + + + +
+ Inside the black border is a zero-opacity touch-action none. +
+
+
+
this text shouldn't be visible
+
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_touch_drag_root_scrollbar.html b/gfx/layers/apz/test/mochitest/helper_touch_drag_root_scrollbar.html new file mode 100644 index 0000000000..018ef78087 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_touch_drag_root_scrollbar.html @@ -0,0 +1,51 @@ + + + + + + Touch Drag on the viewport's scrollbar + + + + + + + +
Some content to ensure the root scrollframe is scrollable
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_touchpad_pinch_and_pan.html b/gfx/layers/apz/test/mochitest/helper_touchpad_pinch_and_pan.html new file mode 100644 index 0000000000..24eb920a74 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_touchpad_pinch_and_pan.html @@ -0,0 +1,49 @@ + + + + + + Sanity check for Touchpad pinch zooming and panning + + + + + + + + Here is some text to stare at as the test runs. It serves no functional + purpose, but gives you an idea of the zoom level. It's harder to tell what + the zoom level is when the page is just solid white. + + diff --git a/gfx/layers/apz/test/mochitest/helper_transform_end_on_keyboard_scroll.html b/gfx/layers/apz/test/mochitest/helper_transform_end_on_keyboard_scroll.html new file mode 100644 index 0000000000..20bee3cefd --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_transform_end_on_keyboard_scroll.html @@ -0,0 +1,58 @@ + + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_transform_end_on_wheel_scroll.html b/gfx/layers/apz/test/mochitest/helper_transform_end_on_wheel_scroll.html new file mode 100644 index 0000000000..af4f72cf44 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_transform_end_on_wheel_scroll.html @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_visual_scrollbars_pagescroll.html b/gfx/layers/apz/test/mochitest/helper_visual_scrollbars_pagescroll.html new file mode 100644 index 0000000000..ae3025930f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_visual_scrollbars_pagescroll.html @@ -0,0 +1,119 @@ + + + + + + Clicking on the scrollbar track in quick succession should scroll the right amount + + + + + + + + +
+
+ The above div is sized to 3x screen height so the linear gradient is more steep in terms of + color/pixel. We only scroll a few pages worth so we don't need the gradient all the way down. + And then we use a bottom-margin to make the page really big so the scrollthumb is + relatively small, giving us lots of space to click on the scrolltrack. + + diff --git a/gfx/layers/apz/test/mochitest/helper_visual_smooth_scroll.html b/gfx/layers/apz/test/mochitest/helper_visual_smooth_scroll.html new file mode 100644 index 0000000000..9d278ee23c --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_visual_smooth_scroll.html @@ -0,0 +1,53 @@ + + + + + + Tests that the (internal) visual smooth scrolling API is not restricted to the layout scroll range + + + + + + +
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_visualscroll_clamp_restore.html b/gfx/layers/apz/test/mochitest/helper_visualscroll_clamp_restore.html new file mode 100644 index 0000000000..69c0590a56 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_visualscroll_clamp_restore.html @@ -0,0 +1,63 @@ + + + +Tests scroll position is properly synchronized when visual position is temporarily clamped on the main thread + + + +
This test runs automatically in automation. To run manually, follow the steps: 1. scroll all the way down
+
3. move the mouse. this div should have a hover effect exactly when the mouse is on top of it
+
+ diff --git a/gfx/layers/apz/test/mochitest/helper_visualscroll_nonrcd_rsf.html b/gfx/layers/apz/test/mochitest/helper_visualscroll_nonrcd_rsf.html new file mode 100644 index 0000000000..9955e49e2a --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_visualscroll_nonrcd_rsf.html @@ -0,0 +1,88 @@ + + + +Tests that pending visual scroll positions on RSFs of non-RCDs get cleared properly + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe.html b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe.html new file mode 100644 index 0000000000..79101b2ca5 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe.html @@ -0,0 +1,52 @@ + + + Test that wheel events on an unscrollable OOP iframe are handoff-ed + + + + + + +
+ +
+
+ + diff --git a/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html new file mode 100644 index 0000000000..aca9dfdbd4 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_iframe_child.html @@ -0,0 +1,11 @@ + + +
overflow: hidden on html
diff --git a/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_non_scrollable_iframe.html b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_non_scrollable_iframe.html new file mode 100644 index 0000000000..22963459a0 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_wheelevents_handoff_on_non_scrollable_iframe.html @@ -0,0 +1,113 @@ + + + + + scroll handoff on non scrollable iframe document with overscroll-behavior: none + + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe.html b/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe.html new file mode 100644 index 0000000000..bdcef59229 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe.html @@ -0,0 +1,33 @@ + + + + +Test cross origin fission iframes get displayport that covers whole width + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html b/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html new file mode 100644 index 0000000000..edf3ad4728 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_wide_crossorigin_iframe_child.html @@ -0,0 +1,71 @@ + + + + + + + + +
diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_fixed_bug1673511.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_fixed_bug1673511.html new file mode 100644 index 0000000000..c63794fdb1 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_fixed_bug1673511.html @@ -0,0 +1,42 @@ + + + + + Checking zoomToFocusedInput scrolls on position: fixed + + + + + +
+
+
ABC
+ +
+ +
ABC
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe.html new file mode 100644 index 0000000000..d916c35efb --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_iframe.html @@ -0,0 +1,68 @@ + + + + + Checking zoomToFocusedInput scrolls that focused element is into iframe + + + + + +
ABC
+ +
+ +
ABC
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_multiline.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_multiline.html new file mode 100644 index 0000000000..ff5912dcee --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_multiline.html @@ -0,0 +1,94 @@ + + + + Checking zoomToFocusedInput scrolls that focused non-input element is visible position + + + + + +
ABC
+
+
+ +
ABC
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom.html new file mode 100644 index 0000000000..5edd181a2d --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom.html @@ -0,0 +1,39 @@ + + + + Checking zoomToFocusedInput does not zoom if meta viewport does not allow it + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom_bug1738696.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom_bug1738696.html new file mode 100644 index 0000000000..4320e391b7 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_nozoom_bug1738696.html @@ -0,0 +1,51 @@ + + + + Checking zoomToFocusedInput does not zoom is meta viewport does not allow it + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_scroll.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_scroll.html new file mode 100644 index 0000000000..4054b51657 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_scroll.html @@ -0,0 +1,51 @@ + + + + Checking zoomToFocusedInput scrolls that focused input element is visible position + + + + + +
ABC
+ + +
ABC
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_touch-action.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_touch-action.html new file mode 100644 index 0000000000..bdb49f4b84 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_touch-action.html @@ -0,0 +1,67 @@ + + + + Checking zoomToFocusedInput zooms if touch-action allows it + + + + + + + +
+ +
+
+
+ +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom.html b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom.html new file mode 100644 index 0000000000..c74fe521b4 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoomToFocusedInput_zoom.html @@ -0,0 +1,39 @@ + + + + Checking zoomToFocusedInput zooms if meta viewport allows it + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_after_gpu_process_restart.html b/gfx/layers/apz/test/mochitest/helper_zoom_after_gpu_process_restart.html new file mode 100644 index 0000000000..0ce6113710 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoom_after_gpu_process_restart.html @@ -0,0 +1,63 @@ + + + + + + Sanity check for pinch zooming after GPU process restart + + + + + + + Here is some text to stare at as the test runs. It serves no functional + purpose, but gives you an idea of the zoom level. It's harder to tell what + the zoom level is when the page is just solid white. + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_keyboardscroll.html b/gfx/layers/apz/test/mochitest/helper_zoom_keyboardscroll.html new file mode 100644 index 0000000000..1a7b38fabd --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoom_keyboardscroll.html @@ -0,0 +1,74 @@ + + + + + + Tests that keyboard arrow keys scroll after zooming in when there was no scrollable overflow before zooming + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_oopif.html b/gfx/layers/apz/test/mochitest/helper_zoom_oopif.html new file mode 100644 index 0000000000..788fa31bbb --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoom_oopif.html @@ -0,0 +1,54 @@ + + + + + + Sanity check for pinch zooming oop iframe + + + + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_out_clamped_scrollpos.html b/gfx/layers/apz/test/mochitest/helper_zoom_out_clamped_scrollpos.html new file mode 100644 index 0000000000..2948df9ae3 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoom_out_clamped_scrollpos.html @@ -0,0 +1,85 @@ + + + + + + Tests that zooming out with an unchanging scroll pos still works properly + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_out_with_mainthread_clamping.html b/gfx/layers/apz/test/mochitest/helper_zoom_out_with_mainthread_clamping.html new file mode 100644 index 0000000000..c15622872a --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoom_out_with_mainthread_clamping.html @@ -0,0 +1,110 @@ + + + + + + Tests that zooming out in a way that triggers main-thread scroll re-clamping works properly + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_prevented.html b/gfx/layers/apz/test/mochitest/helper_zoom_prevented.html new file mode 100644 index 0000000000..b756c873f2 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoom_prevented.html @@ -0,0 +1,75 @@ + + + + + + Checking prevent-default for zooming + + + + + + + Here is some text to stare at as the test runs. It serves no functional + purpose, but gives you an idea of the zoom level. It's harder to tell what + the zoom level is when the page is just solid white. + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_restore_position_tabswitch.html b/gfx/layers/apz/test/mochitest/helper_zoom_restore_position_tabswitch.html new file mode 100644 index 0000000000..0373b321da --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoom_restore_position_tabswitch.html @@ -0,0 +1,74 @@ + + + + + + Switching tabs back to a zoomed page should restore visual offset + + + + + + + + Here is some text to stare at as the test runs. It serves no functional + purpose, but gives you an idea of the zoom level. It's harder to tell what + the zoom level is when the page is just solid white. + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_with_dynamic_toolbar.html b/gfx/layers/apz/test/mochitest/helper_zoom_with_dynamic_toolbar.html new file mode 100644 index 0000000000..261bca1377 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoom_with_dynamic_toolbar.html @@ -0,0 +1,45 @@ + + + + + + Zooming out to the initial scale with the dynamic toolbar + + + + + + + + + + + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoom_with_touchpad.html b/gfx/layers/apz/test/mochitest/helper_zoom_with_touchpad.html new file mode 100644 index 0000000000..6836864964 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoom_with_touchpad.html @@ -0,0 +1,110 @@ + + + + + + Sanity check for Touchpad pinch zooming + + + + + + + + Here is some text to stare at as the test runs. It serves no functional + purpose, but gives you an idea of the zoom level. It's harder to tell what + the zoom level is when the page is just solid white. + + diff --git a/gfx/layers/apz/test/mochitest/helper_zoomed_pan.html b/gfx/layers/apz/test/mochitest/helper_zoomed_pan.html new file mode 100644 index 0000000000..98547fb73f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/helper_zoomed_pan.html @@ -0,0 +1,79 @@ + + + + + + Ensure layout viewport responds to panning while pinched + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/mochitest.ini b/gfx/layers/apz/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..af357a0682 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/mochitest.ini @@ -0,0 +1,125 @@ +[DEFAULT] + prefs = + gfx.font_loader.delay=0 + support-files = + apz_test_native_event_utils.js + apz_test_utils.js + green100x100.png + helper_*.* + tags = apz +[test_abort_smooth_scroll_by_instant_scroll.html] +[test_bug1151667.html] + skip-if = + os == 'android' # wheel events not supported on mobile +[test_bug1253683.html] + skip-if = + os == 'android' # wheel events not supported on mobile +[test_bug1277814.html] + skip-if = + os == 'android' # wheel events not supported on mobile +[test_bug1304689-2.html] +[test_bug1304689.html] +[test_frame_reconstruction.html] +[test_group_bug1534549.html] +[test_group_checkerboarding.html] + skip-if = + http3 +[test_group_displayport.html] +[test_group_double_tap_zoom-2.html] + run-if = ((os == 'android') || (os == 'mac')) # FIXME: enable on more desktop platforms (see bug 1608506 comment 4) +[test_group_double_tap_zoom.html] + run-if = ((os == 'android') || (os == 'mac')) # FIXME: enable on more desktop platforms (see bug 1608506 comment 4) +[test_group_fullscreen.html] + run-if = (os == 'android') +[test_group_hittest-1.html] + skip-if = + toolkit == 'android' # mouse events not supported on mobile +[test_group_hittest-2.html] + skip-if = + toolkit == 'android' # mouse events not supported on mobile + os == 'win' && (bits == 32 || asan) + os == 'linux' && asan # stack is not large enough for the test + http3 +[test_group_hittest-3.html] + skip-if = + toolkit == 'android' # mouse events not supported on mobile + os == 'win' && (bits == 32 || asan) + http3 +[test_group_hittest-overscroll.html] + skip-if = + toolkit == 'android' # mouse events not supported on mobile +[test_group_keyboard-2.html] +[test_group_keyboard.html] +[test_group_mainthread.html] +[test_group_minimum_scale_size.html] + run-if = (os == 'android') +[test_group_mouseevents.html] + skip-if = + toolkit == 'android' # mouse events not supported on mobile +[test_group_overrides.html] + skip-if = + toolkit == 'android' # wheel events not supported on mobile +[test_group_overscroll.html] + skip-if = + toolkit == 'android' # wheel events not supported on mobile +[test_group_overscroll_handoff.html] + skip-if = + toolkit == 'android' # wheel events not supported on mobile + http3 +[test_group_pointerevents.html] + skip-if = (os == 'win' && os_version == '10.0') # Bug 1404836 +[test_group_programmatic_scroll_behavior.html] +[test_group_scroll_linked_effect.html] + skip-if = + (toolkit == 'android') # wheel events not supported on mobile + http3 +[test_group_scroll_snap.html] + skip-if = (os == 'android') # wheel events not supported on mobile +[test_group_scrollend.html] + skip-if = (toolkit == 'android') # wheel events not supported on mobile +[test_group_scrollframe_activation.html] +[test_group_touchevents-2.html] +[test_group_touchevents-3.html] +[test_group_touchevents-4.html] +[test_group_touchevents-5.html] +[test_group_touchevents.html] +[test_group_wheelevents.html] + skip-if = (toolkit == 'android') # wheel events not supported on mobile +[test_group_zoom-2.html] + skip-if = (os == 'win') # see bug 1495580 for Windows +[test_group_zoom.html] + skip-if = + os == 'win' # see bug 1495580 for Windows +[test_group_zoomToFocusedInput.html] +[test_interrupted_reflow.html] +[test_layerization.html] + skip-if = + os == 'android' # wheel events not supported on mobile + os == 'linux' && fission && headless # Bug 1722907 +[test_relative_update.html] + skip-if = + os == 'android' # wheel events not supported on mobile +[test_scroll_inactive_bug1190112.html] + skip-if = (os == 'android') # wheel events not supported on mobile +[test_scroll_inactive_flattened_frame.html] + skip-if = (os == 'android') # wheel events not supported on mobile +[test_scroll_subframe_scrollbar.html] + skip-if = (os == 'android') # wheel events not supported on mobile +[test_smoothness.html] + # hardware vsync only on win/mac + # Frame Uniformity recording is not implemented for webrender + skip-if = + debug + (os != 'mac' && os != 'win') + verify + true # Don't run in CI yet, see bug 1657477 +[test_touch_listeners_impacting_wheel.html] + skip-if = + toolkit == 'android' # wheel events not supported on mobile + toolkit == 'cocoa' # synthesized wheel smooth-scrolling not supported on OS X +[test_wheel_scroll.html] + skip-if = + os == 'android' # wheel events not supported on mobile +[test_wheel_transactions.html] + skip-if = + toolkit == 'android' # wheel events not supported on mobile diff --git a/gfx/layers/apz/test/mochitest/test_abort_smooth_scroll_by_instant_scroll.html b/gfx/layers/apz/test/mochitest/test_abort_smooth_scroll_by_instant_scroll.html new file mode 100644 index 0000000000..650c21cac7 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_abort_smooth_scroll_by_instant_scroll.html @@ -0,0 +1,51 @@ + + + + Test to make sure an on-going smooth scroll is aborted by a new + instant absolute scroll operation + + + + + + + +
+ + + diff --git a/gfx/layers/apz/test/mochitest/test_bug1151667.html b/gfx/layers/apz/test/mochitest/test_bug1151667.html new file mode 100644 index 0000000000..12a46b9094 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_bug1151667.html @@ -0,0 +1,63 @@ + + + + + Test for Bug 1151667 + + + + + + + + + +Mozilla Bug 1151667 +

+
+ +
+
+ +
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/test_bug1253683.html b/gfx/layers/apz/test/mochitest/test_bug1253683.html new file mode 100644 index 0000000000..f12455fb3a --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_bug1253683.html @@ -0,0 +1,59 @@ + + + + + Test to ensure non-scrollable frames don't get layerized + + + + + + + + +

+
+
sample code here
+
spacer to make the 'container' div the root scrollable element
+
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/test_bug1277814.html b/gfx/layers/apz/test/mochitest/test_bug1277814.html new file mode 100644 index 0000000000..28c4619e4e --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_bug1277814.html @@ -0,0 +1,105 @@ + + + + + + Test for Bug 1277814 + + + + + + + + + + +
+ CoolCmd
CoolCmd
CoolCmd
CoolCmd
+ CoolCmd
CoolCmd
CoolCmd
CoolCmd
+ CoolCmd
CoolCmd
CoolCmd
CoolCmd
+ CoolCmd
CoolCmd
CoolCmd
CoolCmd
+ CoolCmd
CoolCmd
CoolCmd
CoolCmd
+ + + diff --git a/gfx/layers/apz/test/mochitest/test_bug1304689-2.html b/gfx/layers/apz/test/mochitest/test_bug1304689-2.html new file mode 100644 index 0000000000..1c7b1255d9 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_bug1304689-2.html @@ -0,0 +1,130 @@ + + + + + + Test for Bug 1285070 + + + + + + + + +
+
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/test_bug1304689.html b/gfx/layers/apz/test/mochitest/test_bug1304689.html new file mode 100644 index 0000000000..85ca3d5503 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_bug1304689.html @@ -0,0 +1,134 @@ + + + + + + Test for Bug 1285070 + + + + + + + + +
+
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+ this is some scrollable text.
+ this is a second line to make the scrolling more obvious.
+ and a third for good measure.
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/test_frame_reconstruction.html b/gfx/layers/apz/test/mochitest/test_frame_reconstruction.html new file mode 100644 index 0000000000..1031701a3b --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_frame_reconstruction.html @@ -0,0 +1,218 @@ + + + + + Test for bug 1235899 + + + + + + + + + +Mozilla Bug 1235899 +

+
+

You should be able to fling this list without it stopping abruptly

+
+
+
    +
  1. Some text
  2. +
  3. Some text
  4. +
  5. Some text
  6. +
  7. Some text
  8. +
  9. Some text
  10. +
  11. Some text
  12. +
  13. Some text
  14. +
  15. Some text
  16. +
  17. Some text
  18. +
  19. Some text
  20. +
  21. Some text
  22. +
  23. Some text
  24. +
  25. Some text
  26. +
  27. Some text
  28. +
  29. Some text
  30. +
  31. Some text
  32. +
  33. Some text
  34. +
  35. Some text
  36. +
  37. Some text
  38. +
  39. Some text
  40. +
  41. Some text
  42. +
  43. Some text
  44. +
  45. Some text
  46. +
  47. Some text
  48. +
  49. Some text
  50. +
  51. Some text
  52. +
  53. Some text
  54. +
  55. Some text
  56. +
  57. Some text
  58. +
  59. Some text
  60. +
  61. Some text
  62. +
  63. Some text
  64. +
  65. Some text
  66. +
  67. Some text
  68. +
  69. Some text
  70. +
  71. Some text
  72. +
  73. Some text
  74. +
  75. Some text
  76. +
  77. Some text
  78. +
  79. Some text
  80. +
  81. Some text
  82. +
  83. Some text
  84. +
  85. Some text
  86. +
  87. Some text
  88. +
  89. Some text
  90. +
  91. Some text
  92. +
  93. Some text
  94. +
  95. Some text
  96. +
  97. Some text
  98. +
  99. Some text
  100. +
  101. Some text
  102. +
  103. Some text
  104. +
  105. Some text
  106. +
  107. Some text
  108. +
  109. Some text
  110. +
  111. Some text
  112. +
  113. Some text
  114. +
  115. Some text
  116. +
  117. Some text
  118. +
  119. Some text
  120. +
  121. Some text
  122. +
  123. Some text
  124. +
  125. Some text
  126. +
  127. Some text
  128. +
  129. Some text
  130. +
  131. Some text
  132. +
  133. Some text
  134. +
  135. Some text
  136. +
  137. Some text
  138. +
  139. Some text
  140. +
  141. Some text
  142. +
  143. Some text
  144. +
  145. Some text
  146. +
  147. Some text
  148. +
  149. Some text
  150. +
  151. Some text
  152. +
  153. Some text
  154. +
  155. Some text
  156. +
  157. Some text
  158. +
  159. Some text
  160. +
  161. Some text
  162. +
  163. Some text
  164. +
  165. Some text
  166. +
  167. Some text
  168. +
  169. Some text
  170. +
  171. Some text
  172. +
  173. Some text
  174. +
  175. Some text
  176. +
  177. Some text
  178. +
  179. Some text
  180. +
  181. Some text
  182. +
  183. Some text
  184. +
  185. Some text
  186. +
  187. Some text
  188. +
  189. Some text
  190. +
  191. Some text
  192. +
  193. Some text
  194. +
  195. Some text
  196. +
  197. Some text
  198. +
  199. Some text
  200. +
  201. Some text
  202. +
  203. Some text
  204. +
+
+
+
+ +
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_bug1534549.html b/gfx/layers/apz/test/mochitest/test_group_bug1534549.html
new file mode 100644
index 0000000000..6ab8afa6f7
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_bug1534549.html
@@ -0,0 +1,37 @@
+
+
+
+    
+    Tests for bug 1534549
+    
+    
+    
+    
+    
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_checkerboarding.html b/gfx/layers/apz/test/mochitest/test_group_checkerboarding.html
new file mode 100644
index 0000000000..5386ffd740
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_checkerboarding.html
@@ -0,0 +1,83 @@
+
+
+
+  
+  
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_displayport.html b/gfx/layers/apz/test/mochitest/test_group_displayport.html
new file mode 100644
index 0000000000..9ff8c524ad
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_displayport.html
@@ -0,0 +1,31 @@
+
+
+
+    
+    Tests for DisplayPorts
+    
+    
+    
+    
+    
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html b/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html
new file mode 100644
index 0000000000..3fb4f9163c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html
@@ -0,0 +1,89 @@
+
+
+
+  
+  Various zoom-related tests that spawn in new windows
+  
+  
+    
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom.html b/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom.html
new file mode 100644
index 0000000000..fe4a0784a9
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_double_tap_zoom.html
@@ -0,0 +1,66 @@
+
+
+
+  
+  Various zoom-related tests that spawn in new windows
+  
+  
+    
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_fullscreen.html b/gfx/layers/apz/test/mochitest/test_group_fullscreen.html
new file mode 100644
index 0000000000..c31a3abffb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_fullscreen.html
@@ -0,0 +1,32 @@
+
+
+
+  
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_hittest-1.html b/gfx/layers/apz/test/mochitest/test_group_hittest-1.html
new file mode 100644
index 0000000000..34bf245d6c
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_hittest-1.html
@@ -0,0 +1,59 @@
+
+
+
+  
+  Various hit-testing tests that spawn in new windows - Part 1
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_hittest-2.html b/gfx/layers/apz/test/mochitest/test_group_hittest-2.html
new file mode 100644
index 0000000000..d144aab840
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_hittest-2.html
@@ -0,0 +1,72 @@
+
+
+
+  
+  Various hit-testing tests that spawn in new windows - Part 2
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_hittest-3.html b/gfx/layers/apz/test/mochitest/test_group_hittest-3.html
new file mode 100644
index 0000000000..f5675ee790
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_hittest-3.html
@@ -0,0 +1,50 @@
+
+
+
+  
+  Various hit-testing tests that spawn in new windows - Part 3
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_hittest-overscroll.html b/gfx/layers/apz/test/mochitest/test_group_hittest-overscroll.html
new file mode 100644
index 0000000000..ee40c5dcdb
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_hittest-overscroll.html
@@ -0,0 +1,54 @@
+
+
+
+  
+  Various hit-testing tests relevant with overscroll that spawn in new windows
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_keyboard-2.html b/gfx/layers/apz/test/mochitest/test_group_keyboard-2.html
new file mode 100644
index 0000000000..c07607e1dc
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_keyboard-2.html
@@ -0,0 +1,46 @@
+
+
+
+  
+  Various keyboard scrolling tests
+  
+  
+  
+  
+  
+
+
+  Async key scrolling test
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_keyboard.html b/gfx/layers/apz/test/mochitest/test_group_keyboard.html
new file mode 100644
index 0000000000..4f1bc0d869
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_keyboard.html
@@ -0,0 +1,60 @@
+
+
+
+  
+  Various keyboard scrolling tests
+  
+  
+  
+  
+  
+
+
+  Async key scrolling test
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_mainthread.html b/gfx/layers/apz/test/mochitest/test_group_mainthread.html
new file mode 100644
index 0000000000..3e1225cadf
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_mainthread.html
@@ -0,0 +1,51 @@
+
+
+
+  
+  Tests that perform main-thread scrolling
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_minimum_scale_size.html b/gfx/layers/apz/test/mochitest/test_group_minimum_scale_size.html
new file mode 100644
index 0000000000..2de924d6bd
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_minimum_scale_size.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_mouseevents.html b/gfx/layers/apz/test/mochitest/test_group_mouseevents.html
new file mode 100644
index 0000000000..c7c86b76b5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_mouseevents.html
@@ -0,0 +1,82 @@
+
+
+
+  
+  Various mouse tests that spawn in new windows
+  
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_overrides.html b/gfx/layers/apz/test/mochitest/test_group_overrides.html
new file mode 100644
index 0000000000..434c32d24e
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_overrides.html
@@ -0,0 +1,37 @@
+
+
+
+  
+  Various tests for event regions overrides
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_overscroll.html b/gfx/layers/apz/test/mochitest/test_group_overscroll.html
new file mode 100644
index 0000000000..d94cd3b65f
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_overscroll.html
@@ -0,0 +1,35 @@
+
+
+
+  
+  Tests for overscroll
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_overscroll_handoff.html b/gfx/layers/apz/test/mochitest/test_group_overscroll_handoff.html
new file mode 100644
index 0000000000..2df4ee37b8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_overscroll_handoff.html
@@ -0,0 +1,46 @@
+
+
+
+  
+  Tests for overscroll handoff
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_pointerevents.html b/gfx/layers/apz/test/mochitest/test_group_pointerevents.html
new file mode 100644
index 0000000000..21fa4585de
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_pointerevents.html
@@ -0,0 +1,43 @@
+
+
+
+
+  
+  Test for Bug 1285070
+  
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_programmatic_scroll_behavior.html b/gfx/layers/apz/test/mochitest/test_group_programmatic_scroll_behavior.html
new file mode 100644
index 0000000000..13bdb4efc4
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_programmatic_scroll_behavior.html
@@ -0,0 +1,38 @@
+
+
+
+  
+  Various programmatic scroll tests that spawn in new windows
+  
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_scroll_linked_effect.html b/gfx/layers/apz/test/mochitest/test_group_scroll_linked_effect.html
new file mode 100644
index 0000000000..f29a382fb8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_scroll_linked_effect.html
@@ -0,0 +1,33 @@
+
+
+
+  
+  Tests for scroll linked effect
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_scroll_snap.html b/gfx/layers/apz/test/mochitest/test_group_scroll_snap.html
new file mode 100644
index 0000000000..9a1341c503
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_scroll_snap.html
@@ -0,0 +1,67 @@
+
+
+
+  
+  Various tests for scroll snap
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_scrollend.html b/gfx/layers/apz/test/mochitest/test_group_scrollend.html
new file mode 100644
index 0000000000..9c2ee71a45
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_scrollend.html
@@ -0,0 +1,58 @@
+
+
+
+  
+  Various scrollend tests that spawn in new windows
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_scrollframe_activation.html b/gfx/layers/apz/test/mochitest/test_group_scrollframe_activation.html
new file mode 100644
index 0000000000..2e75f45fc8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_scrollframe_activation.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+  
+  Tests related to scrollframe activation
+  
+  
+  
+  
+  
+
+
+
+  Mozilla Bug 1151663
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents-2.html b/gfx/layers/apz/test/mochitest/test_group_touchevents-2.html
new file mode 100644
index 0000000000..decd615795
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents-2.html
@@ -0,0 +1,69 @@
+
+
+
+  
+  Various touch tests that spawn in new windows (2)
+  
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents-3.html b/gfx/layers/apz/test/mochitest/test_group_touchevents-3.html
new file mode 100644
index 0000000000..a86c80a2ec
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents-3.html
@@ -0,0 +1,47 @@
+
+
+
+  
+  Various touch tests that spawn in new windows (3)
+  
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents-4.html b/gfx/layers/apz/test/mochitest/test_group_touchevents-4.html
new file mode 100644
index 0000000000..266fc72ee8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents-4.html
@@ -0,0 +1,54 @@
+
+
+
+  
+  Various touch tests that spawn in new windows (4)
+  
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents-5.html b/gfx/layers/apz/test/mochitest/test_group_touchevents-5.html
new file mode 100644
index 0000000000..d4ae624a69
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents-5.html
@@ -0,0 +1,51 @@
+
+
+
+  
+  Various touch tests that spawn in new windows (5)
+  
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_touchevents.html b/gfx/layers/apz/test/mochitest/test_group_touchevents.html
new file mode 100644
index 0000000000..df24e24f3d
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents.html
@@ -0,0 +1,55 @@
+
+
+
+  
+  Various touch tests that spawn in new windows
+  
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_wheelevents.html b/gfx/layers/apz/test/mochitest/test_group_wheelevents.html
new file mode 100644
index 0000000000..34e243f679
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_wheelevents.html
@@ -0,0 +1,84 @@
+
+
+
+  
+  Various wheel-scrolling tests that spawn in new windows
+  
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_zoom-2.html b/gfx/layers/apz/test/mochitest/test_group_zoom-2.html
new file mode 100644
index 0000000000..1f94b9b5b6
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_zoom-2.html
@@ -0,0 +1,81 @@
+
+
+
+  
+  Various zoom-related tests that spawn in new windows
+  
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_zoom.html b/gfx/layers/apz/test/mochitest/test_group_zoom.html
new file mode 100644
index 0000000000..03f9bbebf8
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_zoom.html
@@ -0,0 +1,80 @@
+
+
+
+  
+  Various zoom-related tests that spawn in new windows
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_group_zoomToFocusedInput.html b/gfx/layers/apz/test/mochitest/test_group_zoomToFocusedInput.html
new file mode 100644
index 0000000000..72fd9dcc11
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_group_zoomToFocusedInput.html
@@ -0,0 +1,49 @@
+
+
+
+  
+  Various zoomToFocusedInput tests
+  
+  
+  
+  
+  
+
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_interrupted_reflow.html b/gfx/layers/apz/test/mochitest/test_interrupted_reflow.html
new file mode 100644
index 0000000000..8fc72e05a5
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_interrupted_reflow.html
@@ -0,0 +1,38 @@
+
+
+ 
+ 
+  Test for bug 1292781
+  
+  
+  
+ 
+ 
+
+
+
diff --git a/gfx/layers/apz/test/mochitest/test_layerization.html b/gfx/layers/apz/test/mochitest/test_layerization.html
new file mode 100644
index 0000000000..0ff76de317
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_layerization.html
@@ -0,0 +1,312 @@
+
+
+
+
+  Test for layerization
+  
+  
+  
+  
+  
+  
+  
+  
+
+
+APZ layerization tests
+

+
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ + diff --git a/gfx/layers/apz/test/mochitest/test_relative_update.html b/gfx/layers/apz/test/mochitest/test_relative_update.html new file mode 100644 index 0000000000..01c0ee1f9b --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_relative_update.html @@ -0,0 +1,92 @@ + + + + + Test for relative scroll offset updates (Bug 1453425) + + + + + + + + + +
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html b/gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html new file mode 100644 index 0000000000..de54cf93fe --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_scroll_inactive_bug1190112.html @@ -0,0 +1,553 @@ + + + + Test scrolling flattened inactive frames + + + + + + + + +
+
+
+
+

+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+ +

+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+ +

+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+ +

+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+ +

+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+ +

+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+ +

+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+ +

+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+ +

+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+ +

+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+ +

+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+ +

+
+ + + diff --git a/gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html b/gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html new file mode 100644 index 0000000000..47207cbb9f --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_scroll_inactive_flattened_frame.html @@ -0,0 +1,50 @@ + + + + Test scrolling flattened inactive frames + + + + + + + +
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html b/gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html new file mode 100644 index 0000000000..10d53e9d04 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_scroll_subframe_scrollbar.html @@ -0,0 +1,116 @@ + + + + Test scrolling subframe scrollbars + + + + + + + + +

+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+

+ + + diff --git a/gfx/layers/apz/test/mochitest/test_smoothness.html b/gfx/layers/apz/test/mochitest/test_smoothness.html new file mode 100644 index 0000000000..6db80d365e --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_smoothness.html @@ -0,0 +1,71 @@ + + + Test Frame Uniformity While Scrolling + + + + + + + + + + + +
+
+ + diff --git a/gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html b/gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html new file mode 100644 index 0000000000..71147d5238 --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_touch_listeners_impacting_wheel.html @@ -0,0 +1,119 @@ + + + + + Test for Bug 1203140 + + + + + + + + + +Mozilla Bug 1203140 +

+
+

The box below has a touch listener and a passive wheel listener. With touch events disabled, APZ shouldn't wait for any listeners.

+
+
Div to make 'content' scrollable
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/test_wheel_scroll.html b/gfx/layers/apz/test/mochitest/test_wheel_scroll.html new file mode 100644 index 0000000000..1b50c223ed --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_wheel_scroll.html @@ -0,0 +1,109 @@ + + + + + Test for Bug 1013412 and 1168182 + + + + + + + + + +Mozilla Bug 1013412 +Mozilla Bug 1168182 +

+
+

Scrolling the page should be async, but scrolling over the dark circle should not scroll the page and instead rotate the white ball.

+
+
+
+
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/mochitest/test_wheel_transactions.html b/gfx/layers/apz/test/mochitest/test_wheel_transactions.html new file mode 100644 index 0000000000..f015ea20be --- /dev/null +++ b/gfx/layers/apz/test/mochitest/test_wheel_transactions.html @@ -0,0 +1,150 @@ + + + + + Test for Bug 1175585 + + + + + + + + + +APZ wheel transactions test +

+
+
+
+
+
+
+
+
+ + + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-h-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-ref.html new file mode 100644 index 0000000000..62d99b6dfe --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-ref.html @@ -0,0 +1,8 @@ + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl-ref.html new file mode 100644 index 0000000000..e40ac8debb --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl-ref.html @@ -0,0 +1,9 @@ + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl.html new file mode 100644 index 0000000000..81f7f77817 --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-h-rtl.html @@ -0,0 +1,13 @@ + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-h.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-h.html new file mode 100644 index 0000000000..5d30584acd --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-h.html @@ -0,0 +1,12 @@ + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom-ref.html new file mode 100644 index 0000000000..6226a95070 --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom-ref.html @@ -0,0 +1,8 @@ + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom.html new file mode 100644 index 0000000000..50c6d0854d --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-fullzoom.html @@ -0,0 +1,14 @@ + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-ref.html new file mode 100644 index 0000000000..aec5f89cbc --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-ref.html @@ -0,0 +1,8 @@ + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl-ref.html new file mode 100644 index 0000000000..81be67146f --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl-ref.html @@ -0,0 +1,9 @@ + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl.html new file mode 100644 index 0000000000..24e7705723 --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v-rtl.html @@ -0,0 +1,13 @@ + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-v.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-v.html new file mode 100644 index 0000000000..268f3b92e3 --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-v.html @@ -0,0 +1,12 @@ + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-ref.html new file mode 100644 index 0000000000..35922e3253 --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-ref.html @@ -0,0 +1,8 @@ + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl-ref.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl-ref.html new file mode 100644 index 0000000000..22bf3cf1c8 --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl-ref.html @@ -0,0 +1,9 @@ + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl.html new file mode 100644 index 0000000000..09fce0bbe9 --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh-rtl.html @@ -0,0 +1,13 @@ + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/async-scrollbar-1-vh.html b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh.html new file mode 100644 index 0000000000..a8d28ec414 --- /dev/null +++ b/gfx/layers/apz/test/reftest/async-scrollbar-1-vh.html @@ -0,0 +1,12 @@ + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping-ref.html b/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping-ref.html new file mode 100644 index 0000000000..3db9f2969e --- /dev/null +++ b/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping-ref.html @@ -0,0 +1,27 @@ + + + + +
+ This is the top of the page. +
+ This is the bottom of the page. + diff --git a/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping.html b/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping.html new file mode 100644 index 0000000000..479363f3fb --- /dev/null +++ b/gfx/layers/apz/test/reftest/frame-reconstruction-scroll-clamping.html @@ -0,0 +1,53 @@ + + + + + +
+ This is the top of the page. +
+ This is the bottom of the page. + diff --git a/gfx/layers/apz/test/reftest/iframe-zoomed-child.html b/gfx/layers/apz/test/reftest/iframe-zoomed-child.html new file mode 100644 index 0000000000..4d51f46399 --- /dev/null +++ b/gfx/layers/apz/test/reftest/iframe-zoomed-child.html @@ -0,0 +1,12 @@ + +
+
+
+
diff --git a/gfx/layers/apz/test/reftest/iframe-zoomed-ref.html b/gfx/layers/apz/test/reftest/iframe-zoomed-ref.html new file mode 100644 index 0000000000..2c98a7eb6a --- /dev/null +++ b/gfx/layers/apz/test/reftest/iframe-zoomed-ref.html @@ -0,0 +1,20 @@ + + + +
+ +
+ diff --git a/gfx/layers/apz/test/reftest/iframe-zoomed.html b/gfx/layers/apz/test/reftest/iframe-zoomed.html new file mode 100644 index 0000000000..d3d5d914ba --- /dev/null +++ b/gfx/layers/apz/test/reftest/iframe-zoomed.html @@ -0,0 +1,25 @@ + + + +
+ + +
+ diff --git a/gfx/layers/apz/test/reftest/initial-scale-1-ref.html b/gfx/layers/apz/test/reftest/initial-scale-1-ref.html new file mode 100644 index 0000000000..cb51966a28 --- /dev/null +++ b/gfx/layers/apz/test/reftest/initial-scale-1-ref.html @@ -0,0 +1,9 @@ + + + + + +This tests that an initial-scale of 0 (i.e. garbage) is overridden
+with something a little more sane. + + diff --git a/gfx/layers/apz/test/reftest/initial-scale-1.html b/gfx/layers/apz/test/reftest/initial-scale-1.html new file mode 100644 index 0000000000..58babe9403 --- /dev/null +++ b/gfx/layers/apz/test/reftest/initial-scale-1.html @@ -0,0 +1,9 @@ + + + + + +This tests that an initial-scale of 0 (i.e. garbage) is overridden
+with something a little more sane. + + diff --git a/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed-ref.html b/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed-ref.html new file mode 100644 index 0000000000..f7d485c509 --- /dev/null +++ b/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed-ref.html @@ -0,0 +1,23 @@ + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed.html b/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed.html new file mode 100644 index 0000000000..c4476f4872 --- /dev/null +++ b/gfx/layers/apz/test/reftest/pinch-zoom-position-fixed.html @@ -0,0 +1,37 @@ + + + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky-ref.html b/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky-ref.html new file mode 100644 index 0000000000..c430b532df --- /dev/null +++ b/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky-ref.html @@ -0,0 +1,27 @@ + + + + + + + +
+
+ + diff --git a/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky.html b/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky.html new file mode 100644 index 0000000000..245e0d775e --- /dev/null +++ b/gfx/layers/apz/test/reftest/pinch-zoom-position-sticky.html @@ -0,0 +1,30 @@ + + + + + + + + +
+
+ + diff --git a/gfx/layers/apz/test/reftest/reftest.list b/gfx/layers/apz/test/reftest/reftest.list new file mode 100644 index 0000000000..b346f54057 --- /dev/null +++ b/gfx/layers/apz/test/reftest/reftest.list @@ -0,0 +1,50 @@ +# The following tests test the async positioning of the scrollbars. +# Basic root-frame scrollbar with async scrolling +# First make sure that we are actually drawing scrollbars +skip-if(!asyncPan) pref(apz.allow_zooming,true) != async-scrollbar-1-v.html about:blank +skip-if(!asyncPan) pref(apz.allow_zooming,true) != async-scrollbar-1-v-ref.html about:blank +fuzzy-if(Android,0-5,0-6) fuzzy-if(gtkWidget,1-8,8-32) fuzzy-if(cocoaWidget,16-22,20-44) skip-if(!asyncPan) pref(apz.allow_zooming,true) == async-scrollbar-1-v.html async-scrollbar-1-v-ref.html +fuzzy-if(Android,0-13,0-10) fuzzy-if(gtkWidget,1-30,4-32) fuzzy-if(cocoaWidget,14-22,20-44) skip-if(!asyncPan) pref(apz.allow_zooming,true) == async-scrollbar-1-h.html async-scrollbar-1-h-ref.html +fuzzy-if(Android,0-13,0-21) fuzzy-if(gtkWidget,1-4,4-24) fuzzy-if(cocoaWidget,11-18,44-88) skip-if(!asyncPan) pref(apz.allow_zooming,true) == async-scrollbar-1-vh.html async-scrollbar-1-vh-ref.html +fuzzy-if(Android,0-5,0-6) fuzzy-if(gtkWidget,1-8,8-32) fuzzy-if(cocoaWidget,16-22,20-44) skip-if(!asyncPan) pref(apz.allow_zooming,true) == async-scrollbar-1-v-rtl.html async-scrollbar-1-v-rtl-ref.html +fuzzy-if(Android,0-14,0-10) fuzzy-if(gtkWidget,1-30,12-32) fuzzy-if(cocoaWidget,14-22,20-44) skip-if(!asyncPan) pref(apz.allow_zooming,true) == async-scrollbar-1-h-rtl.html async-scrollbar-1-h-rtl-ref.html +fuzzy-if(Android,0-43,0-26) fuzzy-if(gtkWidget,0-14,12-32) fuzzy-if(cocoaWidget,11-18,26-76) skip-if(!asyncPan) pref(apz.allow_zooming,true) == async-scrollbar-1-vh-rtl.html async-scrollbar-1-vh-rtl-ref.html + +# Different async zoom levels. Since the scrollthumb gets async-scaled in the +# compositor, the border-radius ends of the scrollthumb are going to be a little +# off, hence the fuzzy-if clauses. +skip-if(Android) fuzzy(0-107,0-72) pref(apz.allow_zooming,true) pref(apz.scrollthumb.recalc,true) == root-scrollbar-async-zoomed-in.html root-scrollbar-async-zoomed-in-ref.html +skip-if(Android) fuzzy(0-107,0-167) pref(apz.allow_zooming,true) pref(apz.scrollthumb.recalc,true) == root-scrollbar-async-zoomed-out.html root-scrollbar-async-zoomed-out-ref.html +skip-if(!Android) fuzzy(0-54,0-33) pref(apz.allow_zooming,true) == root-scrollbar-async-zoomed-in.html root-scrollbar-async-zoomed-in-ref.html +skip-if(!Android) fuzzy(0-53,0-30) pref(apz.allow_zooming,true) == root-scrollbar-async-zoomed-out.html root-scrollbar-async-zoomed-out-ref.html + +# Test that the compositor thumb sizing calculations handle a non-default device scale correctly +fuzzy-if(Android,0-31,0-29) fuzzy-if(gtkWidget,0-18,0-49) fuzzy-if(cocoaWidget,0-21,0-53) == async-scrollbar-1-v-fullzoom.html async-scrollbar-1-v-fullzoom-ref.html + +# Test scrollbars working properly with pinch-zooming, i.e. different document resolutions. +# As above, the end of the scrollthumb won't match perfectly, but the bulk of the scrollbar should be present and identical. +# On desktop, even more fuzz is needed because thumb scaling is not exactly proportional: making the page twice as long +# won't make the thumb exactly twice as short, which is what the test expects. That's fine, as the purpose of the test is +# to catch more fundamental scrollbar rendering bugs such as the entire track being mispositioned or the thumb being +# clipped away. +fuzzy-if(Android,0-54,0-22) fuzzy-if(!Android,0-128,0-137) pref(apz.allow_zooming,true) == root-scrollbar-zoomed-in.html root-scrollbar-zoomed-in-ref.html +fuzzy-if(Android,0-54,0-22) fuzzy-if(!Android,0-128,0-137) pref(apz.allow_zooming,true) pref(apz.allow_zooming_out,true) == root-scrollbar-zoomed-out.html root-scrollbar-zoomed-out-ref.html +fuzzy-if(Android,0-54,0-27) fuzzy-if(!Android,0-128,0-137) pref(apz.allow_zooming,true) == root-scrollbar-zoomed-in-async-scroll.html root-scrollbar-zoomed-in-ref.html +fuzzy-if(Android,0-54,0-25) fuzzy-if(!Android,0-128,0-137) pref(apz.allow_zooming,true) pref(apz.allow_zooming_out,true) == root-scrollbar-zoomed-out-async-scroll.html root-scrollbar-zoomed-out-ref.html +fuzzy-if(Android,0-51,0-50) fuzzy-if(!Android,0-128,0-137) pref(apz.allow_zooming,true) == subframe-scrollbar-zoomed-in-async-scroll.html subframe-scrollbar-zoomed-in-async-scroll-ref.html +fuzzy-if(Android,0-28,0-23) fuzzy-if(!Android,0-107,0-34) pref(apz.allow_zooming,true) pref(apz.allow_zooming_out,true) == subframe-scrollbar-zoomed-out-async-scroll.html subframe-scrollbar-zoomed-out-async-scroll-ref.html + +# Meta-viewport tag support +skip-if(!Android) pref(apz.allow_zooming,true) == initial-scale-1.html initial-scale-1-ref.html + +skip-if(!asyncPan) == frame-reconstruction-scroll-clamping.html frame-reconstruction-scroll-clamping-ref.html + +# Test that position:fixed and position:sticky elements are attached to the +# layout viewport. +skip-if(winWidget&&isCoverageBuild) pref(apz.allow_zooming,true) == pinch-zoom-position-fixed.html pinch-zoom-position-fixed-ref.html +skip-if(winWidget&&isCoverageBuild) pref(apz.allow_zooming,true) == pinch-zoom-position-sticky.html pinch-zoom-position-sticky-ref.html + +pref(apz.allow_zooming,true) == iframe-zoomed.html iframe-zoomed-ref.html +pref(apz.allow_zooming,true) == scaled-iframe-zoomed.html scaled-iframe-zoomed-ref.html + +== root-scrollbars-1.html root-scrollbars-1-ref.html diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in-ref.html b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in-ref.html new file mode 100644 index 0000000000..9568836459 --- /dev/null +++ b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in-ref.html @@ -0,0 +1,8 @@ + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in.html b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in.html new file mode 100644 index 0000000000..31e1e99a3d --- /dev/null +++ b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-in.html @@ -0,0 +1,13 @@ + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out-ref.html b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out-ref.html new file mode 100644 index 0000000000..9568836459 --- /dev/null +++ b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out-ref.html @@ -0,0 +1,8 @@ + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out.html b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out.html new file mode 100644 index 0000000000..4032c3c638 --- /dev/null +++ b/gfx/layers/apz/test/reftest/root-scrollbar-async-zoomed-out.html @@ -0,0 +1,13 @@ + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-async-scroll.html b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-async-scroll.html new file mode 100644 index 0000000000..04c829d427 --- /dev/null +++ b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-async-scroll.html @@ -0,0 +1,12 @@ + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-ref.html b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-ref.html new file mode 100644 index 0000000000..9568836459 --- /dev/null +++ b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in-ref.html @@ -0,0 +1,8 @@ + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in.html b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in.html new file mode 100644 index 0000000000..c9cb6e80a7 --- /dev/null +++ b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-in.html @@ -0,0 +1,8 @@ + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-async-scroll.html b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-async-scroll.html new file mode 100644 index 0000000000..465fac6211 --- /dev/null +++ b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-async-scroll.html @@ -0,0 +1,12 @@ + + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-ref.html b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-ref.html new file mode 100644 index 0000000000..9568836459 --- /dev/null +++ b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out-ref.html @@ -0,0 +1,8 @@ + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out.html b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out.html new file mode 100644 index 0000000000..0e3ec7173d --- /dev/null +++ b/gfx/layers/apz/test/reftest/root-scrollbar-zoomed-out.html @@ -0,0 +1,8 @@ + + + + + +
+ + diff --git a/gfx/layers/apz/test/reftest/root-scrollbars-1-ref.html b/gfx/layers/apz/test/reftest/root-scrollbars-1-ref.html new file mode 100644 index 0000000000..435609f8a3 --- /dev/null +++ b/gfx/layers/apz/test/reftest/root-scrollbars-1-ref.html @@ -0,0 +1,14 @@ + + + + +In this file the scrollbars that appear are non-root scrollbars + + +
+ + diff --git a/gfx/layers/apz/test/reftest/root-scrollbars-1.html b/gfx/layers/apz/test/reftest/root-scrollbars-1.html new file mode 100644 index 0000000000..e2560d48a9 --- /dev/null +++ b/gfx/layers/apz/test/reftest/root-scrollbars-1.html @@ -0,0 +1,14 @@ + + + + +In this file the scrollbars that appear are the root scrollbars + + +
+ + diff --git a/gfx/layers/apz/test/reftest/scaled-iframe-zoomed-ref.html b/gfx/layers/apz/test/reftest/scaled-iframe-zoomed-ref.html new file mode 100644 index 0000000000..39847320e2 --- /dev/null +++ b/gfx/layers/apz/test/reftest/scaled-iframe-zoomed-ref.html @@ -0,0 +1,21 @@ + + + +
+ +
+ diff --git a/gfx/layers/apz/test/reftest/scaled-iframe-zoomed.html b/gfx/layers/apz/test/reftest/scaled-iframe-zoomed.html new file mode 100644 index 0000000000..89b09047f7 --- /dev/null +++ b/gfx/layers/apz/test/reftest/scaled-iframe-zoomed.html @@ -0,0 +1,26 @@ + + + +
+ + +
+ diff --git a/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll-ref.html b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll-ref.html new file mode 100644 index 0000000000..f2d640bc2e --- /dev/null +++ b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll-ref.html @@ -0,0 +1,10 @@ + + + + + +
+
+
+ + diff --git a/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll.html b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll.html new file mode 100644 index 0000000000..2aa2a2627c --- /dev/null +++ b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-in-async-scroll.html @@ -0,0 +1,15 @@ + + + + + + +
+
+
+ + diff --git a/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll-ref.html b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll-ref.html new file mode 100644 index 0000000000..4283952f78 --- /dev/null +++ b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll-ref.html @@ -0,0 +1,10 @@ + + + + + +
+
+
+ + diff --git a/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll.html b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll.html new file mode 100644 index 0000000000..a0f1e08cf9 --- /dev/null +++ b/gfx/layers/apz/test/reftest/subframe-scrollbar-zoomed-out-async-scroll.html @@ -0,0 +1,15 @@ + + + + + + +
+
+
+ + diff --git a/gfx/layers/apz/testutil/APZTestData.cpp b/gfx/layers/apz/testutil/APZTestData.cpp new file mode 100644 index 0000000000..4154607d75 --- /dev/null +++ b/gfx/layers/apz/testutil/APZTestData.cpp @@ -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/. */ + +#include "APZTestData.h" +#include "mozilla/dom/APZTestDataBinding.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsString.h" + +namespace mozilla { +namespace layers { + +struct APZTestDataToJSConverter { + template + static void ConvertMap(const std::map& aFrom, + dom::Sequence& aOutTo, + void (*aElementConverter)(const Key&, const Value&, + KeyValuePair&)) { + for (auto it = aFrom.begin(); it != aFrom.end(); ++it) { + if (!aOutTo.AppendElement(fallible)) { + // XXX(Bug 1632090) Instead of extending the array 1-by-1 (which might + // involve multiple reallocations) and potentially crashing here, + // SetCapacity could be called outside the loop once. + mozalloc_handle_oom(0); + } + aElementConverter(it->first, it->second, aOutTo.LastElement()); + } + } + + template + static void ConvertList(const nsTArray& aFrom, + dom::Sequence& aOutTo, + void (*aElementConverter)(const Src&, Target&)) { + for (auto it = aFrom.begin(); it != aFrom.end(); ++it) { + if (!aOutTo.AppendElement(fallible)) { + // XXX(Bug 1632090) Instead of extending the array 1-by-1 (which might + // involve multiple reallocations) and potentially crashing here, + // SetCapacity could be called outside the loop once. + mozalloc_handle_oom(0); + } + aElementConverter(*it, aOutTo.LastElement()); + } + } + + static void ConvertAPZTestData(const APZTestData& aFrom, + dom::APZTestData& aOutTo) { + ConvertMap(aFrom.mPaints, aOutTo.mPaints.Construct(), ConvertBucket); + ConvertMap(aFrom.mRepaintRequests, aOutTo.mRepaintRequests.Construct(), + ConvertBucket); + ConvertList(aFrom.mHitResults, aOutTo.mHitResults.Construct(), + ConvertHitResult); + ConvertList(aFrom.mSampledResults, aOutTo.mSampledResults.Construct(), + ConvertSampledResult); + ConvertMap(aFrom.mAdditionalData, aOutTo.mAdditionalData.Construct(), + ConvertAdditionalDataEntry); + } + + static void ConvertBucket(const SequenceNumber& aKey, + const APZTestData::Bucket& aValue, + dom::APZBucket& aOutKeyValuePair) { + aOutKeyValuePair.mSequenceNumber.Construct() = aKey; + ConvertMap(aValue, aOutKeyValuePair.mScrollFrames.Construct(), + ConvertScrollFrameData); + } + + static void ConvertScrollFrameData(const APZTestData::ViewID& aKey, + const APZTestData::ScrollFrameData& aValue, + dom::ScrollFrameData& aOutKeyValuePair) { + aOutKeyValuePair.mScrollId.Construct() = aKey; + ConvertMap(aValue, aOutKeyValuePair.mEntries.Construct(), ConvertEntry); + } + + static void ConvertEntry(const std::string& aKey, const std::string& aValue, + dom::ScrollFrameDataEntry& aOutKeyValuePair) { + ConvertString(aKey, aOutKeyValuePair.mKey.Construct()); + ConvertString(aValue, aOutKeyValuePair.mValue.Construct()); + } + + static void ConvertAdditionalDataEntry( + const std::string& aKey, const std::string& aValue, + dom::AdditionalDataEntry& aOutKeyValuePair) { + ConvertString(aKey, aOutKeyValuePair.mKey.Construct()); + ConvertString(aValue, aOutKeyValuePair.mValue.Construct()); + } + + static void ConvertString(const std::string& aFrom, nsString& aOutTo) { + CopyUTF8toUTF16(aFrom, aOutTo); + } + + static void ConvertHitResult(const APZTestData::HitResult& aResult, + dom::APZHitResult& aOutHitResult) { + aOutHitResult.mScreenX.Construct() = aResult.point.x; + aOutHitResult.mScreenY.Construct() = aResult.point.y; + static_assert(MaxEnumValue::value < + std::numeric_limits::digits, + "CompositorHitTestFlags MAX value have to be less than " + "number of bits in uint16_t"); + aOutHitResult.mHitResult.Construct() = + static_cast(aResult.result.serialize()); + aOutHitResult.mLayersId.Construct() = aResult.layersId.mId; + aOutHitResult.mScrollId.Construct() = aResult.scrollId; + } + + static void ConvertSampledResult(const APZTestData::SampledResult& aResult, + dom::APZSampledResult& aOutSampledResult) { + aOutSampledResult.mScrollOffsetX.Construct() = aResult.scrollOffset.x; + aOutSampledResult.mScrollOffsetY.Construct() = aResult.scrollOffset.y; + aOutSampledResult.mLayersId.Construct() = aResult.layersId.mId; + aOutSampledResult.mScrollId.Construct() = aResult.scrollId; + aOutSampledResult.mSampledTimeStamp.Construct() = aResult.sampledTimeStamp; + } +}; + +bool APZTestData::ToJS(JS::MutableHandle aOutValue, + JSContext* aContext) const { + dom::APZTestData result; + APZTestDataToJSConverter::ConvertAPZTestData(*this, result); + return dom::ToJSValue(aContext, result, aOutValue); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/testutil/APZTestData.h b/gfx/layers/apz/testutil/APZTestData.h new file mode 100644 index 0000000000..e4a73c80cc --- /dev/null +++ b/gfx/layers/apz/testutil/APZTestData.h @@ -0,0 +1,252 @@ +/* -*- 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_APZTestData_h +#define mozilla_layers_APZTestData_h + +#include + +#include "nsDebug.h" // for NS_WARNING +#include "nsDOMNavigationTiming.h" // for DOMHighResTimeStamp +#include "nsTArray.h" +#include "mozilla/Assertions.h" // for MOZ_ASSERT +#include "mozilla/DebugOnly.h" // for DebugOnly +#include "mozilla/GfxMessageUtils.h" // for ParamTraits specializations +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/ToString.h" // for ToString +#include "mozilla/gfx/CompositorHitTestInfo.h" +#include "mozilla/layers/LayersMessageUtils.h" // for ParamTraits specializations +#include "mozilla/layers/ScrollableLayerGuid.h" +#include "ipc/IPCMessageUtils.h" +#include "js/TypeDecls.h" + +namespace mozilla { +namespace layers { + +typedef uint32_t SequenceNumber; + +/** + * This structure is used to store information logged by various gecko + * components for later examination by test code. + * It contains a bucket for every paint (initiated on the client side), + * and every repaint request (initiated on the compositor side by + * AsyncPanZoomController::RequestContentRepait), which are identified by + * sequence numbers, and within that, a set of arbitrary string key/value + * pairs for every scrollable frame, identified by a scroll id. + * There are two instances of this data structure for every content thread: + * one on the client side and one of the compositor side. + * It also contains a list of hit-test results for MozMouseHittest events + * dispatched during testing. This list is only populated on the compositor + * instance of this class. + */ +// TODO(botond): +// - Improve warnings/asserts. +// - Add ability to associate a repaint request triggered during a layers +// update with the sequence number of the paint that caused the layers +// update. +class APZTestData { + typedef ScrollableLayerGuid::ViewID ViewID; + friend struct IPC::ParamTraits; + friend struct APZTestDataToJSConverter; + + public: + void StartNewPaint(SequenceNumber aSequenceNumber) { + // We should never get more than one paint with the same sequence number. + MOZ_ASSERT(mPaints.find(aSequenceNumber) == mPaints.end()); + mPaints.insert(DataStore::value_type(aSequenceNumber, Bucket())); + } + void LogTestDataForPaint(SequenceNumber aSequenceNumber, ViewID aScrollId, + const std::string& aKey, const std::string& aValue) { + LogTestDataImpl(mPaints, aSequenceNumber, aScrollId, aKey, aValue); + } + + void StartNewRepaintRequest(SequenceNumber aSequenceNumber) { + typedef std::pair InsertResultT; + DebugOnly insertResult = mRepaintRequests.insert( + DataStore::value_type(aSequenceNumber, Bucket())); + MOZ_ASSERT(((InsertResultT&)insertResult).second, + "Already have a repaint request with this sequence number"); + } + void LogTestDataForRepaintRequest(SequenceNumber aSequenceNumber, + ViewID aScrollId, const std::string& aKey, + const std::string& aValue) { + LogTestDataImpl(mRepaintRequests, aSequenceNumber, aScrollId, aKey, aValue); + } + void RecordHitResult(const ScreenPoint& aPoint, + const mozilla::gfx::CompositorHitTestInfo& aResult, + const LayersId& aLayersId, const ViewID& aScrollId) { + mHitResults.AppendElement(HitResult{aPoint, aResult, aLayersId, aScrollId}); + } + void RecordSampledResult(const CSSPoint& aScrollOffset, + DOMHighResTimeStamp aSampledTimeStamp, + const LayersId& aLayersId, const ViewID& aScrollId) { + mSampledResults.AppendElement( + SampledResult{aScrollOffset, aSampledTimeStamp, aLayersId, aScrollId}); + } + void RecordAdditionalData(const std::string& aKey, + const std::string& aValue) { + mAdditionalData[aKey] = aValue; + } + + // Convert this object to a JS representation. + bool ToJS(JS::MutableHandle aOutValue, JSContext* aContext) const; + + // Use dummy derived structures wrapping the typedefs to work around a type + // name length limit in MSVC. + typedef std::map ScrollFrameDataBase; + struct ScrollFrameData : ScrollFrameDataBase {}; + typedef std::map BucketBase; + struct Bucket : BucketBase {}; + typedef std::map DataStoreBase; + struct DataStore : DataStoreBase {}; + struct HitResult { + ScreenPoint point; + mozilla::gfx::CompositorHitTestInfo result; + LayersId layersId; + ViewID scrollId; + }; + struct SampledResult { + CSSPoint scrollOffset; + DOMHighResTimeStamp sampledTimeStamp; + LayersId layersId; + ViewID scrollId; + }; + + private: + DataStore mPaints; + DataStore mRepaintRequests; + CopyableTArray mHitResults; + CopyableTArray mSampledResults; + // Additional free-form data that's not grouped paint or scroll frame. + std::map mAdditionalData; + + void LogTestDataImpl(DataStore& aDataStore, SequenceNumber aSequenceNumber, + ViewID aScrollId, const std::string& aKey, + const std::string& aValue) { + auto bucketIterator = aDataStore.find(aSequenceNumber); + if (bucketIterator == aDataStore.end()) { + MOZ_ASSERT(false, + "LogTestDataImpl called with nonexistent sequence number"); + return; + } + Bucket& bucket = bucketIterator->second; + ScrollFrameData& scrollFrameData = + bucket[aScrollId]; // create if doesn't exist + MOZ_ASSERT(scrollFrameData.find(aKey) == scrollFrameData.end() || + scrollFrameData[aKey] == aValue); + scrollFrameData.insert(ScrollFrameData::value_type(aKey, aValue)); + } +}; + +// A helper class for logging data for a paint. +class APZPaintLogHelper { + public: + APZPaintLogHelper(APZTestData* aTestData, SequenceNumber aPaintSequenceNumber) + : mTestData(aTestData), mPaintSequenceNumber(aPaintSequenceNumber) { + MOZ_ASSERT(!aTestData || StaticPrefs::apz_test_logging_enabled(), + "don't call me"); + } + + template + void LogTestData(ScrollableLayerGuid::ViewID aScrollId, + const std::string& aKey, const Value& aValue) const { + if (mTestData) { // avoid stringifying if mTestData == nullptr + LogTestData(aScrollId, aKey, ToString(aValue)); + } + } + + void LogTestData(ScrollableLayerGuid::ViewID aScrollId, + const std::string& aKey, const std::string& aValue) const { + if (mTestData) { + mTestData->LogTestDataForPaint(mPaintSequenceNumber, aScrollId, aKey, + aValue); + } + } + + private: + APZTestData* mTestData; + SequenceNumber mPaintSequenceNumber; +}; + +} // namespace layers +} // namespace mozilla + +namespace IPC { + +template <> +struct ParamTraits { + typedef mozilla::layers::APZTestData paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mPaints); + WriteParam(aWriter, aParam.mRepaintRequests); + WriteParam(aWriter, aParam.mHitResults); + WriteParam(aWriter, aParam.mSampledResults); + WriteParam(aWriter, aParam.mAdditionalData); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return (ReadParam(aReader, &aResult->mPaints) && + ReadParam(aReader, &aResult->mRepaintRequests) && + ReadParam(aReader, &aResult->mHitResults) && + ReadParam(aReader, &aResult->mSampledResults) && + ReadParam(aReader, &aResult->mAdditionalData)); + } +}; + +template <> +struct ParamTraits + : ParamTraits {}; + +template <> +struct ParamTraits + : ParamTraits {}; + +template <> +struct ParamTraits + : ParamTraits {}; + +template <> +struct ParamTraits { + typedef mozilla::layers::APZTestData::HitResult paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.point); + WriteParam(aWriter, aParam.result); + WriteParam(aWriter, aParam.layersId); + WriteParam(aWriter, aParam.scrollId); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return (ReadParam(aReader, &aResult->point) && + ReadParam(aReader, &aResult->result) && + ReadParam(aReader, &aResult->layersId) && + ReadParam(aReader, &aResult->scrollId)); + } +}; + +template <> +struct ParamTraits { + typedef mozilla::layers::APZTestData::SampledResult paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.scrollOffset); + WriteParam(aWriter, aParam.sampledTimeStamp); + WriteParam(aWriter, aParam.layersId); + WriteParam(aWriter, aParam.scrollId); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return (ReadParam(aReader, &aResult->scrollOffset) && + ReadParam(aReader, &aResult->sampledTimeStamp) && + ReadParam(aReader, &aResult->layersId) && + ReadParam(aReader, &aResult->scrollId)); + } +}; + +} // namespace IPC + +#endif /* mozilla_layers_APZTestData_h */ diff --git a/gfx/layers/apz/util/APZCCallbackHelper.cpp b/gfx/layers/apz/util/APZCCallbackHelper.cpp new file mode 100644 index 0000000000..8b67baf316 --- /dev/null +++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp @@ -0,0 +1,940 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "APZCCallbackHelper.h" + +#include "gfxPlatform.h" // For gfxPlatform::UseTiling + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/EventForwards.h" +#include "mozilla/dom/CustomEvent.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/layers/RepaintRequest.h" +#include "mozilla/layers/WebRenderLayerManager.h" +#include "mozilla/layers/WebRenderBridgeChild.h" +#include "mozilla/DisplayPortUtils.h" +#include "mozilla/PresShell.h" +#include "mozilla/ToString.h" +#include "mozilla/ViewportUtils.h" +#include "nsContainerFrame.h" +#include "nsContentUtils.h" +#include "nsIContent.h" +#include "nsIDOMWindowUtils.h" +#include "mozilla/dom/Document.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIScrollableFrame.h" +#include "nsLayoutUtils.h" +#include "nsPrintfCString.h" +#include "nsPIDOMWindow.h" +#include "nsRefreshDriver.h" +#include "nsString.h" +#include "nsView.h" + +static mozilla::LazyLogModule sApzHlpLog("apz.helper"); +#define APZCCH_LOG(...) MOZ_LOG(sApzHlpLog, LogLevel::Debug, (__VA_ARGS__)) +static mozilla::LazyLogModule sDisplayportLog("apz.displayport"); + +namespace mozilla { +namespace layers { + +using dom::BrowserParent; + +uint64_t APZCCallbackHelper::sLastTargetAPZCNotificationInputBlock = + uint64_t(-1); + +static ScreenMargin RecenterDisplayPort(const ScreenMargin& aDisplayPort) { + ScreenMargin margins = aDisplayPort; + margins.right = margins.left = margins.LeftRight() / 2; + margins.top = margins.bottom = margins.TopBottom() / 2; + return margins; +} + +static PresShell* GetPresShell(const nsIContent* aContent) { + if (dom::Document* doc = aContent->GetComposedDoc()) { + return doc->GetPresShell(); + } + return nullptr; +} + +static CSSPoint ScrollFrameTo(nsIScrollableFrame* aFrame, + const RepaintRequest& aRequest, + bool& aSuccessOut) { + aSuccessOut = false; + CSSPoint targetScrollPosition = aRequest.GetLayoutScrollOffset(); + + if (!aFrame) { + return targetScrollPosition; + } + + CSSPoint geckoScrollPosition = + CSSPoint::FromAppUnits(aFrame->GetScrollPosition()); + + // If the repaint request was triggered due to a previous main-thread scroll + // offset update sent to the APZ, then we don't need to do another scroll here + // and we can just return. + if (!aRequest.GetScrollOffsetUpdated()) { + return geckoScrollPosition; + } + + // If this frame is overflow:hidden, then the expectation is that it was + // sized in a way that respects its scrollable boundaries. For the root + // frame, this means that it cannot be scrolled in such a way that it moves + // the layout viewport. For a non-root frame, this means that it cannot be + // scrolled at all. + // + // In either case, |targetScrollPosition| should be the same as + // |geckoScrollPosition| here. + // + // However, this is slightly racy. We query the overflow property of the + // scroll frame at the time the repaint request arrives at the main thread + // (i.e., right now), but APZ made the decision of whether or not to allow + // scrolling based on the information it had at the time it processed the + // scroll event. The overflow property could have changed at some time + // between the two events and so APZ may have computed a scrollable region + // that is larger than what is actually allowed. + // + // Currently, we allow the scroll position to change even though the frame is + // overflow:hidden (that is, we take |targetScrollPosition|). If this turns + // out to be problematic, an alternative solution would be to ignore the + // scroll position change (that is, use |geckoScrollPosition|). + if (aFrame->GetScrollStyles().mVertical == StyleOverflow::Hidden && + targetScrollPosition.y != geckoScrollPosition.y) { + NS_WARNING( + nsPrintfCString( + "APZCCH: targetScrollPosition.y (%f) != geckoScrollPosition.y (%f)", + targetScrollPosition.y.value, geckoScrollPosition.y.value) + .get()); + } + if (aFrame->GetScrollStyles().mHorizontal == StyleOverflow::Hidden && + targetScrollPosition.x != geckoScrollPosition.x) { + NS_WARNING( + nsPrintfCString( + "APZCCH: targetScrollPosition.x (%f) != geckoScrollPosition.x (%f)", + targetScrollPosition.x.value, geckoScrollPosition.x.value) + .get()); + } + + // If the scrollable frame is currently in the middle of an async or smooth + // scroll then we don't want to interrupt it (see bug 961280). + // Also if the scrollable frame got a scroll request from a higher priority + // origin since the last layers update, then we don't want to push our scroll + // request because we'll clobber that one, which is bad. + bool scrollInProgress = APZCCallbackHelper::IsScrollInProgress(aFrame); + if (!scrollInProgress) { + ScrollSnapTargetIds snapTargetIds = aRequest.GetLastSnapTargetIds(); + aFrame->ScrollToCSSPixelsForApz(targetScrollPosition, + std::move(snapTargetIds)); + geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition()); + aSuccessOut = true; + } + // Return the final scroll position after setting it so that anything that + // relies on it can have an accurate value. Note that even if we set it above + // re-querying it is a good idea because it may have gotten clamped or + // rounded. + return geckoScrollPosition; +} + +/** + * Scroll the scroll frame associated with |aContent| to the scroll position + * requested in |aRequest|. + * + * Any difference between the requested and actual scroll positions is used to + * update the callback-transform stored on the content, and return a new + * display port. + */ +static DisplayPortMargins ScrollFrame(nsIContent* aContent, + const RepaintRequest& aRequest) { + // Scroll the window to the desired spot + nsIScrollableFrame* sf = + nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId()); + if (sf) { + sf->ResetScrollInfoIfNeeded(aRequest.GetScrollGeneration(), + aRequest.GetScrollGenerationOnApz(), + aRequest.GetScrollAnimationType(), + nsIScrollableFrame::InScrollingGesture( + aRequest.IsInScrollingGesture())); + sf->SetScrollableByAPZ(!aRequest.IsScrollInfoLayer()); + if (sf->IsRootScrollFrameOfDocument()) { + if (!APZCCallbackHelper::IsScrollInProgress(sf)) { + APZCCH_LOG("Setting VV offset to %s\n", + ToString(aRequest.GetVisualScrollOffset()).c_str()); + if (sf->SetVisualViewportOffset( + CSSPoint::ToAppUnits(aRequest.GetVisualScrollOffset()), + /* aRepaint = */ false)) { + // sf can't be destroyed if SetVisualViewportOffset returned true. + sf->MarkEverScrolled(); + } + } + } + } + // sf might have been destroyed by the call to SetVisualViewportOffset, so + // re-get it. + sf = nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId()); + bool scrollUpdated = false; + auto displayPortMargins = + DisplayPortMargins::ForScrollFrame(sf, aRequest.GetDisplayPortMargins()); + CSSPoint apzScrollOffset = aRequest.GetVisualScrollOffset(); + CSSPoint actualScrollOffset = ScrollFrameTo(sf, aRequest, scrollUpdated); + CSSPoint scrollDelta = apzScrollOffset - actualScrollOffset; + + if (scrollUpdated) { + if (aRequest.IsScrollInfoLayer()) { + // In cases where the APZ scroll offset is different from the content + // scroll offset, we want to interpret the margins as relative to the APZ + // scroll offset except when the frame is not scrollable by APZ. + // Therefore, if the layer is a scroll info layer, we leave the margins + // as-is and they will be interpreted as relative to the content scroll + // offset. + if (nsIFrame* frame = aContent->GetPrimaryFrame()) { + frame->SchedulePaint(); + } + } else { + // Correct the display port due to the difference between the requested + // and actual scroll offsets. + displayPortMargins = + DisplayPortMargins::FromAPZ(aRequest.GetDisplayPortMargins(), + apzScrollOffset, actualScrollOffset); + } + } else if (aRequest.IsRootContent() && + apzScrollOffset != aRequest.GetLayoutScrollOffset()) { + // APZ uses the visual viewport's offset to calculate where to place the + // display port, so the display port is misplaced when a pinch zoom occurs. + // + // We need to force a display port adjustment in the following paint to + // account for a difference between the requested and actual scroll + // offsets in repaints requested by + // AsyncPanZoomController::NotifyLayersUpdated. + displayPortMargins = DisplayPortMargins::FromAPZ( + aRequest.GetDisplayPortMargins(), apzScrollOffset, actualScrollOffset); + } else { + // For whatever reason we couldn't update the scroll offset on the scroll + // frame, which means the data APZ used for its displayport calculation is + // stale. Fall back to a sane default behaviour. Note that we don't + // tile-align the recentered displayport because tile-alignment depends on + // the scroll position, and the scroll position here is out of our control. + // See bug 966507 comment 21 for a more detailed explanation. + displayPortMargins = DisplayPortMargins::ForScrollFrame( + sf, RecenterDisplayPort(aRequest.GetDisplayPortMargins())); + } + + // APZ transforms inputs assuming we applied the exact scroll offset it + // requested (|apzScrollOffset|). Since we may not have, record the difference + // between what APZ asked for and what we actually applied, and apply it to + // input events to compensate. + // Note that if the main-thread had a change in its scroll position, we don't + // want to record that difference here, because it can be large and throw off + // input events by a large amount. It is also going to be transient, because + // any main-thread scroll position change will be synced to APZ and we will + // get another repaint request when APZ confirms. In the interval while this + // is happening we can just leave the callback transform as it was. + bool mainThreadScrollChanged = + sf && sf->CurrentScrollGeneration() != aRequest.GetScrollGeneration() && + nsLayoutUtils::CanScrollOriginClobberApz(sf->LastScrollOrigin()); + if (aContent && !mainThreadScrollChanged) { + aContent->SetProperty(nsGkAtoms::apzCallbackTransform, + new CSSPoint(scrollDelta), + nsINode::DeleteProperty); + } + + 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 diff --git a/gfx/layers/apz/util/APZCCallbackHelper.h b/gfx/layers/apz/util/APZCCallbackHelper.h new file mode 100644 index 0000000000..7b1f7cb88b --- /dev/null +++ b/gfx/layers/apz/util/APZCCallbackHelper.h @@ -0,0 +1,195 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layers_APZCCallbackHelper_h +#define mozilla_layers_APZCCallbackHelper_h + +#include "InputData.h" +#include "LayersTypes.h" +#include "Units.h" +#include "mozilla/EventForwards.h" +#include "mozilla/layers/MatrixMessage.h" +#include "nsRefreshObservers.h" + +#include + +class nsIContent; +class nsIScrollableFrame; +class nsIWidget; +class nsPresContext; +template +struct already_AddRefed; +template +class nsCOMPtr; + +namespace mozilla { + +class PresShell; +enum class PreventDefaultResult : uint8_t; + +namespace layers { + +struct RepaintRequest; + +/* Refer to documentation on SendSetTargetAPZCNotification for this class */ +class DisplayportSetListener : public ManagedPostRefreshObserver { + public: + DisplayportSetListener(nsIWidget* aWidget, nsPresContext*, + const uint64_t& aInputBlockId, + nsTArray&& aTargets); + virtual ~DisplayportSetListener(); + void Register(); + + private: + RefPtr mWidget; + uint64_t mInputBlockId; + nsTArray mTargets; + + void OnPostRefresh(); +}; + +/* This class contains some helper methods that facilitate implementing the + GeckoContentController callback interface required by the + AsyncPanZoomController. Since different platforms need to implement this + interface in similar-but- not-quite-the-same ways, this utility class + provides some helpful methods to hold code that can be shared across the + different platform implementations. + */ +class APZCCallbackHelper { + typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid; + + public: + static void NotifyLayerTransforms(const nsTArray& aTransforms); + + /* Applies the scroll and zoom parameters from the given RepaintRequest object + to the root frame for the given metrics' scrollId. If tiled thebes layers + are enabled, this will align the displayport to tile boundaries. Setting + the scroll position can cause some small adjustments to be made to the + actual scroll position. */ + static void UpdateRootFrame(const RepaintRequest& aRequest); + + /* Applies the scroll parameters from the given RepaintRequest object to the + subframe corresponding to given metrics' scrollId. If tiled thebes + layers are enabled, this will align the displayport to tile boundaries. + Setting the scroll position can cause some small adjustments to be made + to the actual scroll position. */ + static void UpdateSubFrame(const RepaintRequest& aRequest); + + /* Get the presShellId and view ID for the given content element. + * If the view ID does not exist, one is created. + * The pres shell ID should generally already exist; if it doesn't for some + * reason, false is returned. */ + static bool GetOrCreateScrollIdentifiers( + nsIContent* aContent, uint32_t* aPresShellIdOut, + ScrollableLayerGuid::ViewID* aViewIdOut); + + /* Initialize a zero-margin displayport on the root document element of the + given presShell. */ + static void InitializeRootDisplayport(PresShell* aPresShell); + + /* Get the pres context associated with the document enclosing |aContent|. */ + static nsPresContext* GetPresContextForContent(nsIContent* aContent); + + /* Get the pres shell associated with the root content document enclosing + * |aContent|. */ + static PresShell* GetRootContentDocumentPresShellForContent( + nsIContent* aContent); + + /* Dispatch a widget event via the widget stored in the event, if any. + * In a child process, allows the BrowserParent event-capture mechanism to + * intercept the event. */ + static nsEventStatus DispatchWidgetEvent(WidgetGUIEvent& aEvent); + + /* Synthesize a mouse event with the given parameters, and dispatch it + * via the given widget. */ + static nsEventStatus DispatchSynthesizedMouseEvent( + EventMessage aMsg, const LayoutDevicePoint& aRefPoint, + Modifiers aModifiers, int32_t aClickCount, nsIWidget* aWidget); + + /* Dispatch a mouse event with the given parameters. + * Return whether or not any listeners have called preventDefault on the + * event. + * This is a lightweight wrapper around nsContentUtils::SendMouseEvent() + * and as such expects |aPoint| to be in layout coordinates. */ + MOZ_CAN_RUN_SCRIPT + static PreventDefaultResult DispatchMouseEvent( + PresShell* aPresShell, const nsString& aType, const CSSPoint& aPoint, + int32_t aButton, int32_t aClickCount, int32_t aModifiers, + unsigned short aInputSourceArg, uint32_t aPointerId); + + /* Fire a single-tap event at the given point. The event is dispatched + * via the given widget. */ + static void FireSingleTapEvent(const LayoutDevicePoint& aPoint, + Modifiers aModifiers, int32_t aClickCount, + nsIWidget* aWidget); + + /* Perform hit-testing on the touch points of |aEvent| to determine + * which scrollable frames they target. If any of these frames don't have + * a displayport, set one. + * + * If any displayports need to be set, this function returns a heap-allocated + * object. The caller is responsible for calling Register() on that object. + * + * The object registers itself as a post-refresh observer on the presShell + * and ensures that notifications get sent to APZ correctly after the + * refresh. + * + * Having the caller manage this object is desirable in case they want to + * (a) know about the fact that a displayport needs to be set, and + * (b) register a post-refresh observer of their own that will run in + * a defined ordering relative to the APZ messages. + */ + static already_AddRefed SendSetTargetAPZCNotification( + nsIWidget* aWidget, mozilla::dom::Document* aDocument, + const WidgetGUIEvent& aEvent, const LayersId& aLayersId, + uint64_t aInputBlockId); + + /* Notify content of a mouse scroll testing event. */ + static void NotifyMozMouseScrollEvent( + const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent); + + /* Notify content that the repaint flush is complete. */ + static void NotifyFlushComplete(PresShell* aPresShell); + + static void NotifyAsyncScrollbarDragInitiated( + uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId, + ScrollDirection aDirection); + static void NotifyAsyncScrollbarDragRejected( + const ScrollableLayerGuid::ViewID& aScrollId); + static void NotifyAsyncAutoscrollRejected( + const ScrollableLayerGuid::ViewID& aScrollId); + + static void CancelAutoscroll(const ScrollableLayerGuid::ViewID& aScrollId); + static void NotifyScaleGestureComplete(const nsCOMPtr& aWidget, + float aScale); + + /* + * Check if the scrollable frame is currently in the middle of a main thread + * async or smooth scroll, or has already requested some other apz scroll that + * hasn't been acknowledged by apz. + * + * We want to discard apz updates to the main-thread scroll offset if this is + * true to prevent clobbering higher priority origins. + */ + static bool IsScrollInProgress(nsIScrollableFrame* aFrame); + + /* Notify content of the progress of a pinch gesture that APZ won't do + * zooming for (because the apz.allow_zooming pref is false). This function + * will dispatch appropriate WidgetSimpleGestureEvent events to gecko. + */ + static void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType, + const LayoutDevicePoint& aFocusPoint, + LayoutDeviceCoord aSpanChange, + Modifiers aModifiers, + const nsCOMPtr& aWidget); + + private: + static uint64_t sLastTargetAPZCNotificationInputBlock; +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_APZCCallbackHelper_h */ diff --git a/gfx/layers/apz/util/APZEventState.cpp b/gfx/layers/apz/util/APZEventState.cpp new file mode 100644 index 0000000000..c2bf624dda --- /dev/null +++ b/gfx/layers/apz/util/APZEventState.cpp @@ -0,0 +1,603 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "APZEventState.h" + +#include + +#include "APZCCallbackHelper.h" +#include "ActiveElementManager.h" +#include "TouchManager.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/dom/Document.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/PositionedEventTargeting.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/ToString.h" +#include "mozilla/TouchEvents.h" +#include "mozilla/ViewportUtils.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/layers/APZCCallbackHelper.h" +#include "mozilla/layers/IAPZCTreeManager.h" +#include "mozilla/widget/nsAutoRollup.h" +#include "nsCOMPtr.h" +#include "nsDocShell.h" +#include "nsIDOMWindowUtils.h" +#include "nsINamed.h" +#include "nsIScrollableFrame.h" +#include "nsIScrollbarMediator.h" +#include "nsIWeakReferenceUtils.h" +#include "nsIWidget.h" +#include "nsLayoutUtils.h" +#include "nsQueryFrame.h" + +static mozilla::LazyLogModule sApzEvtLog("apz.eventstate"); +#define APZES_LOG(...) MOZ_LOG(sApzEvtLog, LogLevel::Debug, (__VA_ARGS__)) + +// Static helper functions +namespace { + +int32_t WidgetModifiersToDOMModifiers(mozilla::Modifiers aModifiers) { + int32_t result = 0; + if (aModifiers & mozilla::MODIFIER_SHIFT) { + result |= nsIDOMWindowUtils::MODIFIER_SHIFT; + } + if (aModifiers & mozilla::MODIFIER_CONTROL) { + result |= nsIDOMWindowUtils::MODIFIER_CONTROL; + } + if (aModifiers & mozilla::MODIFIER_ALT) { + result |= nsIDOMWindowUtils::MODIFIER_ALT; + } + if (aModifiers & mozilla::MODIFIER_META) { + result |= nsIDOMWindowUtils::MODIFIER_META; + } + if (aModifiers & mozilla::MODIFIER_ALTGRAPH) { + result |= nsIDOMWindowUtils::MODIFIER_ALTGRAPH; + } + if (aModifiers & mozilla::MODIFIER_CAPSLOCK) { + result |= nsIDOMWindowUtils::MODIFIER_CAPSLOCK; + } + if (aModifiers & mozilla::MODIFIER_FN) { + result |= nsIDOMWindowUtils::MODIFIER_FN; + } + if (aModifiers & mozilla::MODIFIER_FNLOCK) { + result |= nsIDOMWindowUtils::MODIFIER_FNLOCK; + } + if (aModifiers & mozilla::MODIFIER_NUMLOCK) { + result |= nsIDOMWindowUtils::MODIFIER_NUMLOCK; + } + if (aModifiers & mozilla::MODIFIER_SCROLLLOCK) { + result |= nsIDOMWindowUtils::MODIFIER_SCROLLLOCK; + } + if (aModifiers & mozilla::MODIFIER_SYMBOL) { + result |= nsIDOMWindowUtils::MODIFIER_SYMBOL; + } + if (aModifiers & mozilla::MODIFIER_SYMBOLLOCK) { + result |= nsIDOMWindowUtils::MODIFIER_SYMBOLLOCK; + } + if (aModifiers & mozilla::MODIFIER_OS) { + result |= nsIDOMWindowUtils::MODIFIER_OS; + } + return result; +} + +} // namespace + +namespace mozilla { +namespace layers { + +APZEventState::APZEventState(nsIWidget* aWidget, + ContentReceivedInputBlockCallback&& aCallback) + : mWidget(nullptr) // initialized in constructor body + , + mActiveElementManager(new ActiveElementManager()), + mContentReceivedInputBlockCallback(std::move(aCallback)), + mPendingTouchPreventedResponse(false), + mPendingTouchPreventedBlockId(0), + mEndTouchIsClick(false), + mFirstTouchCancelled(false), + mTouchEndCancelled(false), + mSingleTapsPendingTargetInfo(), + mLastTouchIdentifier(0) { + nsresult rv; + mWidget = do_GetWeakReference(aWidget, &rv); + MOZ_ASSERT(NS_SUCCEEDED(rv), + "APZEventState constructed with a widget that" + " does not support weak references. APZ will NOT work!"); +} + +APZEventState::~APZEventState() = default; + +RefPtr DelayedFireSingleTapEvent::Create( + Maybe&& aTargetInfo) { + nsCOMPtr timer = NS_NewTimer(); + RefPtr event = + new DelayedFireSingleTapEvent(std::move(aTargetInfo), timer); + nsresult rv = timer->InitWithCallback( + event, StaticPrefs::ui_touch_activation_duration_ms(), + nsITimer::TYPE_ONE_SHOT); + if (NS_FAILED(rv)) { + event->ClearTimer(); + event = nullptr; + } + return event; +} + +NS_IMETHODIMP DelayedFireSingleTapEvent::Notify(nsITimer*) { + APZES_LOG("DelayedFireSingeTapEvent notification ready=%d", + mTargetInfo.isSome()); + // If the required information to fire the synthesized events has not + // been populated yet, we have not received the touch-end. In this case + // we should not fire the synthesized events here. The synthesized events + // will be fired on touch-end in this case. + if (mTargetInfo.isSome()) { + FireSingleTapEvent(); + } + mTimer = nullptr; + return NS_OK; +} + +NS_IMETHODIMP DelayedFireSingleTapEvent::GetName(nsACString& aName) { + aName.AssignLiteral("DelayedFireSingleTapEvent"); + return NS_OK; +} + +void DelayedFireSingleTapEvent::PopulateTargetInfo( + SingleTapTargetInfo&& aTargetInfo) { + MOZ_ASSERT(!mTargetInfo.isSome()); + mTargetInfo = Some(std::move(aTargetInfo)); + // If the timer no longer exists, we have surpassed the minimum elapsed + // time to delay the synthesized click. We can immediately fire the + // synthesized events in this case. + if (!mTimer) { + FireSingleTapEvent(); + } +} + +void DelayedFireSingleTapEvent::FireSingleTapEvent() { + MOZ_ASSERT(mTargetInfo.isSome()); + nsCOMPtr widget = do_QueryReferent(mTargetInfo->mWidget); + if (widget) { + widget::nsAutoRollup rollup(mTargetInfo->mTouchRollup.get()); + APZCCallbackHelper::FireSingleTapEvent(mTargetInfo->mPoint, + mTargetInfo->mModifiers, + mTargetInfo->mClickCount, widget); + } +} + +NS_IMPL_ISUPPORTS(DelayedFireSingleTapEvent, nsITimerCallback, nsINamed) + +void APZEventState::ProcessSingleTap(const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers, int32_t aClickCount, + uint64_t aInputBlockId) { + APZES_LOG("Handling single tap at %s with %d\n", ToString(aPoint).c_str(), + mTouchEndCancelled); + + RefPtr touchRollup = GetTouchRollup(); + mTouchRollup = nullptr; + + nsCOMPtr widget = GetWidget(); + if (!widget) { + return; + } + + if (mTouchEndCancelled) { + return; + } + + SingleTapTargetInfo targetInfo(mWidget, aPoint * aScale, aModifiers, + aClickCount, touchRollup); + + auto delayedEvent = mSingleTapsPendingTargetInfo.find(aInputBlockId); + if (delayedEvent != mSingleTapsPendingTargetInfo.end()) { + APZES_LOG("Found tap for block=%" PRIu64, aInputBlockId); + + // With the target info populated, the event will be fired as + // soon as the delay timer expires (or now, if it has already expired). + delayedEvent->second->PopulateTargetInfo(std::move(targetInfo)); + mSingleTapsPendingTargetInfo.erase(delayedEvent); + } else { + APZES_LOG("Scheduling timer for click event\n"); + + // We don't need to keep a reference to the event, because the + // event and its timer keep each other alive until the timer expires + DelayedFireSingleTapEvent::Create(Some(std::move(targetInfo))); + } +} + +PreventDefaultResult APZEventState::FireContextmenuEvents( + PresShell* aPresShell, const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, Modifiers aModifiers, + const nsCOMPtr& aWidget) { + // Suppress retargeting for mouse events generated by a long-press + EventRetargetSuppression suppression; + + // Synthesize mousemove event for allowing users to emulate to move mouse + // cursor over the element. As a result, users can open submenu UI which + // is opened when mouse cursor is moved over a link (i.e., it's a case that + // users cannot stay in the page after tapping it). So, this improves + // accessibility in websites which are designed for desktop. + // Note that we don't need to check whether mousemove event is consumed or + // not because Chrome also ignores the result. + APZCCallbackHelper::DispatchSynthesizedMouseEvent( + eMouseMove, aPoint * aScale, aModifiers, 0 /* clickCount */, aWidget); + + // Converting the modifiers to DOM format for the DispatchMouseEvent call + // is the most useless thing ever because nsDOMWindowUtils::SendMouseEvent + // just converts them back to widget format, but that API has many callers, + // including in JS code, so it's not trivial to change. + CSSPoint point = CSSPoint::FromAppUnits( + ViewportUtils::VisualToLayout(CSSPoint::ToAppUnits(aPoint), aPresShell)); + PreventDefaultResult preventDefaultResult = + APZCCallbackHelper::DispatchMouseEvent( + aPresShell, u"contextmenu"_ns, point, 2, 1, + WidgetModifiersToDOMModifiers(aModifiers), + dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH, + 0 /* Use the default value here. */); + + APZES_LOG("Contextmenu event %s\n", ToString(preventDefaultResult).c_str()); + if (preventDefaultResult != PreventDefaultResult::No) { + // If the contextmenu event was handled then we're showing a contextmenu, + // and so we should remove any activation + mActiveElementManager->ClearActivation(); +#ifndef XP_WIN + } else { + // If the contextmenu wasn't consumed, fire the eMouseLongTap event. + nsEventStatus status = APZCCallbackHelper::DispatchSynthesizedMouseEvent( + eMouseLongTap, aPoint * aScale, aModifiers, + /*clickCount*/ 1, aWidget); + if (status == nsEventStatus_eConsumeNoDefault) { + // Assuming no JS actor listens eMouseLongTap events. + preventDefaultResult = PreventDefaultResult::ByContent; + } else { + preventDefaultResult = PreventDefaultResult::No; + } + APZES_LOG("eMouseLongTap event %s\n", + ToString(preventDefaultResult).c_str()); +#endif + } + + return preventDefaultResult; +} + +void APZEventState::ProcessLongTap(PresShell* aPresShell, + const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers, + uint64_t aInputBlockId) { + APZES_LOG("Handling long tap at %s\n", ToString(aPoint).c_str()); + + nsCOMPtr widget = GetWidget(); + if (!widget) { + return; + } + + SendPendingTouchPreventedResponse(false); + +#ifdef XP_WIN + // On Windows, we fire the contextmenu events when the user lifts their + // finger, in keeping with the platform convention. This happens in the + // ProcessLongTapUp function. However, we still fire the eMouseLongTap event + // at this time, because things like text selection or dragging may want + // to know about it. + nsEventStatus status = APZCCallbackHelper::DispatchSynthesizedMouseEvent( + eMouseLongTap, aPoint * aScale, aModifiers, /*clickCount*/ 1, widget); + + PreventDefaultResult preventDefaultResult = + (status == nsEventStatus_eConsumeNoDefault) + ? PreventDefaultResult::ByContent + : PreventDefaultResult::No; +#else + PreventDefaultResult preventDefaultResult = + FireContextmenuEvents(aPresShell, aPoint, aScale, aModifiers, widget); +#endif + mContentReceivedInputBlockCallback( + aInputBlockId, preventDefaultResult != PreventDefaultResult::No); + + const bool eventHandled = +#ifdef MOZ_WIDGET_ANDROID + // On Android, GeckoView calls preventDefault() in a JSActor + // (ContentDelegateChild.jsm) when opening context menu so that we can + // tell whether contextmenu opens in response to the contextmenu event by + // checking where preventDefault() got called. + preventDefaultResult == PreventDefaultResult::ByChrome; +#else + // Unfortunately on desktop platforms other than Windows we can't use + // the same approach for Android since we no longer call preventDefault() + // since bug 1558506. So for now, we keep the current behavior that is + // sending a touchcancel event if the contextmenu event was + // preventDefault-ed in an event handler in the content itself. + preventDefaultResult == PreventDefaultResult::ByContent; +#endif + if (eventHandled) { + // Also send a touchcancel to content + // a) on Android if browser's contextmenu is open + // b) on Windows if the long tap event was consumed + // c) on other platforms if preventDefault() was called for the contextmenu + // event + // so that listeners that might be waiting for a touchend don't trigger. + WidgetTouchEvent cancelTouchEvent(true, eTouchCancel, widget.get()); + cancelTouchEvent.mModifiers = aModifiers; + auto ldPoint = LayoutDeviceIntPoint::Round(aPoint * aScale); + cancelTouchEvent.mTouches.AppendElement(new mozilla::dom::Touch( + mLastTouchIdentifier, ldPoint, LayoutDeviceIntPoint(), 0, 0)); + APZCCallbackHelper::DispatchWidgetEvent(cancelTouchEvent); + } +} + +void APZEventState::ProcessLongTapUp(PresShell* aPresShell, + const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers) { +#ifdef XP_WIN + nsCOMPtr 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&& aAllowedTouchBehaviors) { + if (aEvent.mMessage == eTouchStart && aEvent.mTouches.Length() > 0) { + mActiveElementManager->SetTargetElement( + aEvent.mTouches[0]->GetOriginalTarget()); + mLastTouchIdentifier = aEvent.mTouches[0]->Identifier(); + } + if (aEvent.mMessage == eTouchStart) { + // We get the allowed touch behaviors on a touchstart, but may not actually + // use them until the first touchmove, so we stash them in a member + // variable. + mTouchBlockAllowedBehaviors = std::move(aAllowedTouchBehaviors); + } + + bool isTouchPrevented = aContentResponse == nsEventStatus_eConsumeNoDefault; + bool sentContentResponse = false; + APZES_LOG("Handling event type %d isPrevented=%d\n", aEvent.mMessage, + isTouchPrevented); + switch (aEvent.mMessage) { + case eTouchStart: { + mTouchEndCancelled = false; + mTouchRollup = do_GetWeakReference(widget::nsAutoRollup::GetLastRollup()); + + SendPendingTouchPreventedResponse(false); + // The above call may have sent a message to APZ if we get two + // TOUCH_STARTs in a row and just responded to the first one. + + // We're about to send a response back to APZ, but we should only do it + // for events that went through APZ (which should be all of them). + MOZ_ASSERT(aEvent.mFlags.mHandledByAPZ); + + // If the first touchstart event was preventDefaulted, ensure that any + // subsequent additional touchstart events also get preventDefaulted. This + // ensures that e.g. pinch zooming is prevented even if just the first + // touchstart was prevented by content. + if (mTouchCounter.GetActiveTouchCount() == 0) { + mFirstTouchCancelled = isTouchPrevented; + } else { + if (mFirstTouchCancelled && !isTouchPrevented) { + APZES_LOG( + "Propagating prevent-default from first-touch for block %" PRIu64 + "\n", + aInputBlockId); + } + isTouchPrevented |= mFirstTouchCancelled; + } + + if (isTouchPrevented) { + mContentReceivedInputBlockCallback(aInputBlockId, isTouchPrevented); + sentContentResponse = true; + } else { + APZES_LOG("Event not prevented; pending response for %" PRIu64 " %s\n", + aInputBlockId, ToString(aGuid).c_str()); + mPendingTouchPreventedResponse = true; + mPendingTouchPreventedGuid = aGuid; + mPendingTouchPreventedBlockId = aInputBlockId; + } + break; + } + + case eTouchEnd: + if (isTouchPrevented) { + mTouchEndCancelled = true; + mEndTouchIsClick = false; + } + [[fallthrough]]; + case eTouchCancel: + mActiveElementManager->HandleTouchEndEvent(mEndTouchIsClick); + [[fallthrough]]; + case eTouchMove: { + if (mPendingTouchPreventedResponse) { + MOZ_ASSERT(aGuid == mPendingTouchPreventedGuid); + } + sentContentResponse = SendPendingTouchPreventedResponse(isTouchPrevented); + break; + } + + default: + MOZ_ASSERT_UNREACHABLE("Unknown touch event type"); + break; + } + + mTouchCounter.Update(aEvent); + if (mTouchCounter.GetActiveTouchCount() == 0) { + mFirstTouchCancelled = false; + } + + APZES_LOG("Pointercancel if %d %d %d %d\n", sentContentResponse, + !isTouchPrevented, aApzResponse == nsEventStatus_eConsumeDoDefault, + MainThreadAgreesEventsAreConsumableByAPZ()); + if (sentContentResponse && !isTouchPrevented && + aApzResponse == nsEventStatus_eConsumeDoDefault && + MainThreadAgreesEventsAreConsumableByAPZ()) { + WidgetTouchEvent cancelEvent(aEvent); + cancelEvent.mMessage = eTouchPointerCancel; + cancelEvent.mFlags.mCancelable = false; // mMessage != eTouchCancel; + for (uint32_t i = 0; i < cancelEvent.mTouches.Length(); ++i) { + if (mozilla::dom::Touch* touch = cancelEvent.mTouches[i]) { + touch->convertToPointer = true; + } + } + nsEventStatus status; + cancelEvent.mWidget->DispatchEvent(&cancelEvent, status); + } +} + +bool APZEventState::MainThreadAgreesEventsAreConsumableByAPZ() const { + // APZ errs on the side of saying it can consume touch events to perform + // default user-agent behaviours. In particular it may say this if it hasn't + // received accurate touch-action information. Here we double-check using + // accurate touch-action information. This code is kinda-sorta the main + // thread equivalent of AsyncPanZoomController::ArePointerEventsConsumable(). + + switch (mTouchBlockAllowedBehaviors.Length()) { + case 0: + // If we don't have any touch-action (e.g. because it is disabled) then + // APZ has no restrictions. + return true; + + case 1: { + // If there's one touch point in this touch block, then check the pan-x + // and pan-y flags. If neither is allowed, then we disagree with APZ and + // say that it can't do anything with this touch block. Note that it would + // be even better if we could check the allowed scroll directions of the + // scrollframe at this point and refine this further. + TouchBehaviorFlags flags = mTouchBlockAllowedBehaviors[0]; + return (flags & AllowedTouchBehavior::HORIZONTAL_PAN) || + (flags & AllowedTouchBehavior::VERTICAL_PAN); + } + + case 2: { + // If there's two touch points in this touch block, check that they both + // allow zooming. + for (const auto& allowed : mTouchBlockAllowedBehaviors) { + if (!(allowed & AllowedTouchBehavior::PINCH_ZOOM)) { + return false; + } + } + return true; + } + + default: + // More than two touch points? APZ shouldn't be doing anything with this, + // so APZ shouldn't be consuming them. + return false; + } +} + +void APZEventState::ProcessWheelEvent(const WidgetWheelEvent& aEvent, + uint64_t aInputBlockId) { + // If this event starts a swipe, indicate that it shouldn't result in a + // scroll by setting defaultPrevented to true. + bool defaultPrevented = aEvent.DefaultPrevented() || aEvent.TriggersSwipe(); + mContentReceivedInputBlockCallback(aInputBlockId, defaultPrevented); +} + +void APZEventState::ProcessMouseEvent(const WidgetMouseEvent& aEvent, + uint64_t aInputBlockId) { + bool defaultPrevented = false; + mContentReceivedInputBlockCallback(aInputBlockId, defaultPrevented); +} + +void APZEventState::ProcessAPZStateChange(ViewID aViewId, + APZStateChange aChange, int aArg, + Maybe aInputBlockId) { + switch (aChange) { + case APZStateChange::eTransformBegin: { + nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aViewId); + if (sf) { + sf->SetTransformingByAPZ(true); + sf->ScrollbarActivityStarted(); + } + + nsIContent* content = nsLayoutUtils::FindContentFor(aViewId); + dom::Document* doc = content ? content->GetComposedDoc() : nullptr; + nsCOMPtr docshell(doc ? doc->GetDocShell() : nullptr); + if (docshell && sf) { + nsDocShell* nsdocshell = static_cast(docshell.get()); + nsdocshell->NotifyAsyncPanZoomStarted(); + } + break; + } + case APZStateChange::eTransformEnd: { + nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aViewId); + if (sf) { + sf->SetTransformingByAPZ(false); + sf->ScrollbarActivityStopped(); + } + + nsIContent* content = nsLayoutUtils::FindContentFor(aViewId); + dom::Document* doc = content ? content->GetComposedDoc() : nullptr; + nsCOMPtr docshell(doc ? doc->GetDocShell() : nullptr); + if (docshell && sf) { + nsDocShell* nsdocshell = static_cast(docshell.get()); + nsdocshell->NotifyAsyncPanZoomStopped(); + } + break; + } + case APZStateChange::eStartTouch: { + bool canBePan = aArg; + mActiveElementManager->HandleTouchStart(canBePan); + // If this is a non-scrollable content, set a timer for the amount of + // time specified by ui.touch_activation.duration_ms to fire the + // synthesized click and mouse events. + APZES_LOG("%s: can-be-pan=%d", __FUNCTION__, aArg); + if (!canBePan) { + MOZ_ASSERT(aInputBlockId.isSome()); + RefPtr delayedEvent = + DelayedFireSingleTapEvent::Create(Nothing()); + DebugOnly insertResult = + mSingleTapsPendingTargetInfo.emplace(*aInputBlockId, delayedEvent) + .second; + MOZ_ASSERT(insertResult, "Failed to insert delayed tap event."); + } + break; + } + case APZStateChange::eStartPanning: { + // The user started to pan, so we don't want anything to be :active. + mActiveElementManager->ClearActivation(); + break; + } + case APZStateChange::eEndTouch: { + mEndTouchIsClick = aArg; + mActiveElementManager->HandleTouchEnd(); + break; + } + } +} + +bool APZEventState::SendPendingTouchPreventedResponse(bool aPreventDefault) { + if (mPendingTouchPreventedResponse) { + APZES_LOG("Sending response %d for pending guid: %s\n", aPreventDefault, + ToString(mPendingTouchPreventedGuid).c_str()); + mContentReceivedInputBlockCallback(mPendingTouchPreventedBlockId, + aPreventDefault); + mPendingTouchPreventedResponse = false; + return true; + } + return false; +} + +already_AddRefed APZEventState::GetWidget() const { + nsCOMPtr result = do_QueryReferent(mWidget); + return result.forget(); +} + +already_AddRefed APZEventState::GetTouchRollup() const { + nsCOMPtr result = do_QueryReferent(mTouchRollup); + return result.forget(); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/util/APZEventState.h b/gfx/layers/apz/util/APZEventState.h new file mode 100644 index 0000000000..e4f6c98303 --- /dev/null +++ b/gfx/layers/apz/util/APZEventState.h @@ -0,0 +1,190 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layers_APZEventState_h +#define mozilla_layers_APZEventState_h + +#include + +#include "Units.h" +#include "mozilla/EventForwards.h" +#include "mozilla/layers/GeckoContentControllerTypes.h" // for APZStateChange +#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid +#include "mozilla/layers/TouchCounter.h" // for TouchCounter +#include "mozilla/RefPtr.h" +#include "mozilla/StaticPrefs_ui.h" +#include "nsCOMPtr.h" +#include "nsISupportsImpl.h" // for NS_INLINE_DECL_REFCOUNTING +#include "nsITimer.h" +#include "nsIWeakReferenceUtils.h" // for nsWeakPtr + +#include +#include + +template +class nsCOMPtr; +class nsIContent; +class nsIWidget; + +namespace mozilla { + +class PresShell; +enum class PreventDefaultResult : uint8_t; + +namespace layers { + +class ActiveElementManager; + +typedef std::function + ContentReceivedInputBlockCallback; + +struct SingleTapTargetInfo { + nsWeakPtr mWidget; + LayoutDevicePoint mPoint; + Modifiers mModifiers; + int32_t mClickCount; + RefPtr mTouchRollup; + + explicit SingleTapTargetInfo(nsWeakPtr aWidget, LayoutDevicePoint aPoint, + Modifiers aModifiers, int32_t aClickCount, + RefPtr aTouchRollup) + : mWidget(std::move(aWidget)), + mPoint(aPoint), + mModifiers(aModifiers), + mClickCount(aClickCount), + mTouchRollup(std::move(aTouchRollup)) {} + + SingleTapTargetInfo(SingleTapTargetInfo&&) = default; + SingleTapTargetInfo& operator=(SingleTapTargetInfo&&) = default; +}; + +class DelayedFireSingleTapEvent final : public nsITimerCallback, + public nsINamed { + private: + explicit DelayedFireSingleTapEvent(Maybe&& aTargetInfo, + const nsCOMPtr& aTimer) + : mTargetInfo(std::move(aTargetInfo)) + // Hold the reference count until we are called back. + , + mTimer(aTimer) {} + + public: + NS_DECL_ISUPPORTS + + static RefPtr Create( + Maybe&& aTargetInfo); + + NS_IMETHOD Notify(nsITimer*) override; + + NS_IMETHOD GetName(nsACString& aName) override; + + void PopulateTargetInfo(SingleTapTargetInfo&& aTargetInfo); + + void FireSingleTapEvent(); + + void ClearTimer() { mTimer = nullptr; } + + private: + ~DelayedFireSingleTapEvent() = default; + + Maybe mTargetInfo; + nsCOMPtr mTimer; +}; + +/** + * A content-side component that keeps track of state for handling APZ + * gestures and sending APZ notifications. + */ +class APZEventState final { + typedef GeckoContentController_APZStateChange APZStateChange; + typedef ScrollableLayerGuid::ViewID ViewID; + + public: + APZEventState(nsIWidget* aWidget, + ContentReceivedInputBlockCallback&& aCallback); + + NS_INLINE_DECL_REFCOUNTING(APZEventState); + + void ProcessSingleTap(const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers, int32_t aClickCount, + uint64_t aInputBlockId); + MOZ_CAN_RUN_SCRIPT + void ProcessLongTap(PresShell* aPresShell, const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers, uint64_t aInputBlockId); + MOZ_CAN_RUN_SCRIPT + void ProcessLongTapUp(PresShell* aPresShell, const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, + Modifiers aModifiers); + void ProcessTouchEvent(const WidgetTouchEvent& aEvent, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId, nsEventStatus aApzResponse, + nsEventStatus aContentResponse, + nsTArray&& aAllowedTouchBehaviors); + void ProcessWheelEvent(const WidgetWheelEvent& aEvent, + uint64_t aInputBlockId); + void ProcessMouseEvent(const WidgetMouseEvent& aEvent, + uint64_t aInputBlockId); + void ProcessAPZStateChange(ViewID aViewId, APZStateChange aChange, int aArg, + Maybe aInputBlockId); + + private: + ~APZEventState(); + bool SendPendingTouchPreventedResponse(bool aPreventDefault); + MOZ_CAN_RUN_SCRIPT + PreventDefaultResult FireContextmenuEvents( + PresShell* aPresShell, const CSSPoint& aPoint, + const CSSToLayoutDeviceScale& aScale, Modifiers aModifiers, + const nsCOMPtr& aWidget); + already_AddRefed GetWidget() const; + already_AddRefed GetTouchRollup() const; + bool MainThreadAgreesEventsAreConsumableByAPZ() const; + + private: + nsWeakPtr mWidget; + RefPtr mActiveElementManager; + ContentReceivedInputBlockCallback mContentReceivedInputBlockCallback; + TouchCounter mTouchCounter; + bool mPendingTouchPreventedResponse; + ScrollableLayerGuid mPendingTouchPreventedGuid; + uint64_t mPendingTouchPreventedBlockId; + bool mEndTouchIsClick; + bool mFirstTouchCancelled; + bool mTouchEndCancelled; + + // Store pending single tap event dispatch tasks keyed on the + // tap gesture's input block id. In the case where multiple taps + // occur in quick succession, we may receive a later tap while the + // dispatch for an earlier tap is still pending. + std::unordered_map> + mSingleTapsPendingTargetInfo; + + int32_t mLastTouchIdentifier; + nsTArray mTouchBlockAllowedBehaviors; + + // Because touch-triggered mouse events (e.g. mouse events from a tap + // gesture) happen asynchronously from the touch events themselves, we + // need to stash and replicate some of the state from the touch events + // to the mouse events. One piece of state is the rollup content, which + // is the content for which a popup window was recently closed. If we + // don't replicate this state properly during the mouse events, the + // synthetic click might reopen a popup window that was just closed by + // the touch event, which is undesirable. See also documentation in + // nsAutoRollup.h + // Note that in cases where we get multiple touch blocks interleaved with + // their single-tap event notifications, mTouchRollup may hold an incorrect + // value. This is kind of an edge case, and falls in the same category of + // problems as bug 1227241. I intend that fixing that bug will also take + // care of this potential problem. + nsWeakPtr mTouchRollup; +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_APZEventState_h */ diff --git a/gfx/layers/apz/util/APZTaskRunnable.cpp b/gfx/layers/apz/util/APZTaskRunnable.cpp new file mode 100644 index 0000000000..6192f385a7 --- /dev/null +++ b/gfx/layers/apz/util/APZTaskRunnable.cpp @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "APZTaskRunnable.h" + +#include "mozilla/PresShell.h" +#include "nsRefreshDriver.h" + +namespace mozilla::layers { + +NS_IMETHODIMP +APZTaskRunnable::Run() { + if (!mController) { + mRegisteredPresShellId = 0; + return NS_OK; + } + + // Move these variables first since below RequestContentPaint and + // NotifyFlushComplete might spin event loop so that any new incoming requests + // will be properly queued and run in the next refresh driver's tick. + const bool needsFlushCompleteNotification = mNeedsFlushCompleteNotification; + auto requests = std::move(mPendingRepaintRequestQueue); + mPendingRepaintRequestMap.clear(); + mNeedsFlushCompleteNotification = false; + mRegisteredPresShellId = 0; + RefPtr controller = mController; + + // We need to process pending RepaintRequests first. + while (!requests.empty()) { + controller->RequestContentRepaint(requests.front()); + requests.pop_front(); + } + + if (needsFlushCompleteNotification) { + // Then notify "apz-repaints-flushed" so that we can ensure that all pending + // scroll position updates have finished when the "apz-repaints-flushed" + // arrives. + controller->NotifyFlushComplete(); + } + + return NS_OK; +} + +void APZTaskRunnable::QueueRequest(const RepaintRequest& aRequest) { + // If we are in test-controlled refreshes mode, process this |aRequest| + // synchronously. + if (IsTestControllingRefreshesEnabled()) { + // Flush all pending requests and notification just in case the refresh + // driver mode was changed before flushing them. + RefPtr controller = mController; + Run(); + controller->RequestContentRepaint(aRequest); + return; + } + EnsureRegisterAsEarlyRunner(); + + RepaintRequestKey key{aRequest.GetScrollId(), aRequest.GetScrollUpdateType()}; + + auto lastDiscardableRequest = mPendingRepaintRequestMap.find(key); + // If there's an existing request with the same key, we can discard it and we + // push the incoming one into the queue's tail so that we can ensure the order + // of processing requests. + if (lastDiscardableRequest != mPendingRepaintRequestMap.end()) { + for (auto it = mPendingRepaintRequestQueue.begin(); + it != mPendingRepaintRequestQueue.end(); it++) { + if (RepaintRequestKey{it->GetScrollId(), it->GetScrollUpdateType()} == + key) { + mPendingRepaintRequestQueue.erase(it); + break; + } + } + } + mPendingRepaintRequestMap.insert(key); + mPendingRepaintRequestQueue.push_back(aRequest); +} + +void APZTaskRunnable::QueueFlushCompleteNotification() { + // If we are in test-controlled refreshes mode, notify apz-repaints-flushed + // synchronously. + if (IsTestControllingRefreshesEnabled()) { + // Flush all pending requests and notification just in case the refresh + // driver mode was changed before flushing them. + RefPtr controller = mController; + Run(); + controller->NotifyFlushComplete(); + return; + } + + EnsureRegisterAsEarlyRunner(); + + mNeedsFlushCompleteNotification = true; +} + +bool APZTaskRunnable::IsRegisteredWithCurrentPresShell() const { + MOZ_ASSERT(mController); + + uint32_t current = 0; + if (PresShell* presShell = mController->GetTopLevelPresShell()) { + current = presShell->GetPresShellId(); + } + return mRegisteredPresShellId == current; +} + +void APZTaskRunnable::EnsureRegisterAsEarlyRunner() { + if (IsRegisteredWithCurrentPresShell()) { + return; + } + + // If the registered presshell id has been changed, we need to discard pending + // requests and notification since all of them are for documents which + // have been torn down. + if (mRegisteredPresShellId) { + mPendingRepaintRequestMap.clear(); + mPendingRepaintRequestQueue.clear(); + mNeedsFlushCompleteNotification = false; + } + + if (PresShell* presShell = mController->GetTopLevelPresShell()) { + if (nsRefreshDriver* driver = presShell->GetRefreshDriver()) { + driver->AddEarlyRunner(this); + mRegisteredPresShellId = presShell->GetPresShellId(); + } + } +} + +bool APZTaskRunnable::IsTestControllingRefreshesEnabled() const { + if (!mController) { + return false; + } + + if (PresShell* presShell = mController->GetTopLevelPresShell()) { + if (nsRefreshDriver* driver = presShell->GetRefreshDriver()) { + return driver->IsTestControllingRefreshesEnabled(); + } + } + return false; +} + +} // namespace mozilla::layers diff --git a/gfx/layers/apz/util/APZTaskRunnable.h b/gfx/layers/apz/util/APZTaskRunnable.h new file mode 100644 index 0000000000..f5de21abd4 --- /dev/null +++ b/gfx/layers/apz/util/APZTaskRunnable.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layers_RepaintRequestRunnable_h +#define mozilla_layers_RepaintRequestRunnable_h + +#include +#include + +#include "mozilla/layers/GeckoContentController.h" +#include "mozilla/layers/RepaintRequest.h" +#include "mozilla/layers/ScrollableLayerGuid.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace layers { + +class GeckoContentController; + +// A runnable invoked in nsRefreshDriver::Tick as an early runnable. +class APZTaskRunnable final : public Runnable { + public: + explicit APZTaskRunnable(GeckoContentController* aController) + : Runnable("RepaintRequestRunnable"), + mController(aController), + mRegisteredPresShellId(0), + mNeedsFlushCompleteNotification(false) {} + + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_DECL_NSIRUNNABLE + + // Queue a RepaintRequest. + // If there's already a RepaintRequest having the same scroll id, the old + // one will be discarded. + void + QueueRequest(const RepaintRequest& aRequest); + void QueueFlushCompleteNotification(); + void Revoke() { + mController = nullptr; + mRegisteredPresShellId = 0; + } + + private: + void EnsureRegisterAsEarlyRunner(); + bool IsRegisteredWithCurrentPresShell() const; + bool IsTestControllingRefreshesEnabled() const; + + // Use a GeckoContentController raw pointer here since the owner of the + // GeckoContentController instance (an APZChild instance) holds a strong + // reference of this APZTaskRunnable instance and will call Revoke() before + // the GeckoContentController gets destroyed in the dtor of the APZChild + // instance. + GeckoContentController* mController; + + struct RepaintRequestKey { + ScrollableLayerGuid::ViewID mScrollId; + RepaintRequest::ScrollOffsetUpdateType mScrollUpdateType; + bool operator==(const RepaintRequestKey& aOther) const { + return mScrollId == aOther.mScrollId && + mScrollUpdateType == aOther.mScrollUpdateType; + } + struct HashFn { + std::size_t operator()(const RepaintRequestKey& aKey) const { + return HashGeneric(aKey.mScrollId, aKey.mScrollUpdateType); + } + }; + }; + using RepaintRequests = + std::unordered_set; + // We have an unordered_map and a deque for pending RepaintRequests. The + // unordered_map is for quick lookup and the deque is for processing the + // pending RepaintRequests in the order we queued. + RepaintRequests mPendingRepaintRequestMap; + std::deque mPendingRepaintRequestQueue; + // This APZTaskRunnable instance is per APZChild instance, which means its + // lifetime is tied to the APZChild instance, thus this APZTaskRunnable + // instance will be (re-)used for different pres shells so we'd need to + // have to remember the pres shell which is currently tied to the APZChild + // to deliver queued requests and notifications to the proper pres shell. + uint32_t mRegisteredPresShellId; + bool mNeedsFlushCompleteNotification; +}; + +} // namespace layers +} // namespace mozilla + +#endif // mozilla_layers_RepaintRequestRunnable_h diff --git a/gfx/layers/apz/util/APZThreadUtils.cpp b/gfx/layers/apz/util/APZThreadUtils.cpp new file mode 100644 index 0000000000..d3bf43e61d --- /dev/null +++ b/gfx/layers/apz/util/APZThreadUtils.cpp @@ -0,0 +1,119 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "APZThreadUtils.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ProfilerRunnable.h" +#include "mozilla/StaticMutex.h" + +#include "nsISerialEventTarget.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" + +namespace mozilla { +namespace layers { + +static bool sThreadAssertionsEnabled = true; +static StaticRefPtr sControllerThread; +static StaticMutex sControllerThreadMutex MOZ_UNANNOTATED; + +/*static*/ +void APZThreadUtils::SetThreadAssertionsEnabled(bool aEnabled) { + StaticMutexAutoLock lock(sControllerThreadMutex); + sThreadAssertionsEnabled = aEnabled; +} + +/*static*/ +bool APZThreadUtils::GetThreadAssertionsEnabled() { + StaticMutexAutoLock lock(sControllerThreadMutex); + return sThreadAssertionsEnabled; +} + +/*static*/ +void APZThreadUtils::SetControllerThread(nsISerialEventTarget* aThread) { + // We must either be setting the initial controller thread, or removing it, + // or re-using an existing controller thread. + StaticMutexAutoLock lock(sControllerThreadMutex); + MOZ_ASSERT(!sControllerThread || !aThread || sControllerThread == aThread); + if (aThread != sControllerThread) { + // This can only happen once, on startup. + sControllerThread = aThread; + ClearOnShutdown(&sControllerThread); + } +} + +/*static*/ +void APZThreadUtils::AssertOnControllerThread() { +#if DEBUG + if (!GetThreadAssertionsEnabled()) { + return; + } + StaticMutexAutoLock lock(sControllerThreadMutex); + MOZ_ASSERT(sControllerThread && sControllerThread->IsOnCurrentThread()); +#endif +} + +/*static*/ +void APZThreadUtils::RunOnControllerThread(RefPtr&& aTask, + uint32_t flags) { + RefPtr thread; + { + StaticMutexAutoLock lock(sControllerThreadMutex); + thread = sControllerThread; + } + RefPtr task = std::move(aTask); + + if (!thread) { + // Could happen on startup or if Shutdown() got called. + NS_WARNING("Dropping task posted to controller thread"); + return; + } + + if (thread->IsOnCurrentThread()) { + AUTO_PROFILE_FOLLOWING_RUNNABLE(task); + task->Run(); + } else { + thread->Dispatch(task.forget(), flags); + } +} + +/*static*/ +bool APZThreadUtils::IsControllerThread() { + StaticMutexAutoLock lock(sControllerThreadMutex); + return sControllerThread && sControllerThread->IsOnCurrentThread(); +} + +/*static*/ +bool APZThreadUtils::IsControllerThreadAlive() { + StaticMutexAutoLock lock(sControllerThreadMutex); + return !!sControllerThread; +} + +/*static*/ +void APZThreadUtils::DelayedDispatch(already_AddRefed aRunnable, + int aDelayMs) { + MOZ_ASSERT(!XRE_IsContentProcess(), + "ContentProcessController should only be used remotely."); + RefPtr thread; + { + StaticMutexAutoLock lock(sControllerThreadMutex); + thread = sControllerThread; + } + if (!thread) { + // Could happen on startup + NS_WARNING("Dropping task posted to controller thread"); + return; + } + if (aDelayMs) { + thread->DelayedDispatch(std::move(aRunnable), aDelayMs); + } else { + thread->Dispatch(std::move(aRunnable)); + } +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/util/APZThreadUtils.h b/gfx/layers/apz/util/APZThreadUtils.h new file mode 100644 index 0000000000..f1560eee8c --- /dev/null +++ b/gfx/layers/apz/util/APZThreadUtils.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layers_APZThreadUtils_h +#define mozilla_layers_APZThreadUtils_h + +#include "nsIEventTarget.h" +#include "nsINamed.h" +#include "nsITimer.h" +#include "nsString.h" + +class nsISerialEventTarget; + +namespace mozilla { + +class Runnable; + +namespace layers { + +class APZThreadUtils { + public: + /** + * In the gtest environment everything runs on one thread, so we + * shouldn't assert that we're on a particular thread. This enables + * that behaviour. + */ + static void SetThreadAssertionsEnabled(bool aEnabled); + static bool GetThreadAssertionsEnabled(); + + /** + * Set the controller thread. + */ + static void SetControllerThread(nsISerialEventTarget* aThread); + + /** + * This can be used to assert that the current thread is the + * controller/UI thread (on which input events are received). + * This does nothing if thread assertions are disabled. + */ + static void AssertOnControllerThread(); + + /** + * Run the given task on the APZ "controller thread" for this platform. If + * this function is called from the controller thread itself then the task is + * run immediately without getting queued. + */ + static void RunOnControllerThread( + RefPtr&& aTask, + uint32_t flags = nsIEventTarget::DISPATCH_NORMAL); + + /** + * Returns true if currently on APZ "controller thread". + */ + static bool IsControllerThread(); + + /** + * Returns true if the controller thread is still alive. + */ + static bool IsControllerThreadAlive(); + + /** + * Schedules a runnable to run on the controller thread at some time + * in the future. + */ + static void DelayedDispatch(already_AddRefed aRunnable, + int aDelayMs); +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_APZThreadUtils_h */ diff --git a/gfx/layers/apz/util/ActiveElementManager.cpp b/gfx/layers/apz/util/ActiveElementManager.cpp new file mode 100644 index 0000000000..8da36bb2c0 --- /dev/null +++ b/gfx/layers/apz/util/ActiveElementManager.cpp @@ -0,0 +1,178 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ActiveElementManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Document.h" + +static mozilla::LazyLogModule sApzAemLog("apz.activeelement"); +#define AEM_LOG(...) MOZ_LOG(sApzAemLog, LogLevel::Debug, (__VA_ARGS__)) + +namespace mozilla { +namespace layers { + +ActiveElementManager::ActiveElementManager() + : mCanBePan(false), mCanBePanSet(false), mSetActiveTask(nullptr) {} + +ActiveElementManager::~ActiveElementManager() = default; + +void ActiveElementManager::SetTargetElement(dom::EventTarget* aTarget) { + if (mTarget) { + // Multiple fingers on screen (since HandleTouchEnd clears mTarget). + AEM_LOG("Multiple fingers on-screen, clearing target element\n"); + CancelTask(); + ResetActive(); + ResetTouchBlockState(); + return; + } + + mTarget = dom::Element::FromEventTargetOrNull(aTarget); + AEM_LOG("Setting target element to %p\n", mTarget.get()); + TriggerElementActivation(); +} + +void ActiveElementManager::HandleTouchStart(bool aCanBePan) { + AEM_LOG("Touch start, aCanBePan: %d\n", aCanBePan); + if (mCanBePanSet) { + // Multiple fingers on screen (since HandleTouchEnd clears mCanBePanSet). + AEM_LOG("Multiple fingers on-screen, clearing touch block state\n"); + CancelTask(); + ResetActive(); + ResetTouchBlockState(); + return; + } + + mCanBePan = aCanBePan; + mCanBePanSet = true; + TriggerElementActivation(); +} + +void ActiveElementManager::TriggerElementActivation() { + // Both HandleTouchStart() and SetTargetElement() call this. They can be + // called in either order. One will set mCanBePanSet, and the other, mTarget. + // We want to actually trigger the activation once both are set. + if (!(mTarget && mCanBePanSet)) { + return; + } + + // If the touch cannot be a pan, make mTarget :active right away. + // Otherwise, wait a bit to see if the user will pan or not. + if (!mCanBePan) { + SetActive(mTarget); + } else { + CancelTask(); // this is only needed because of bug 1169802. Fixing that + // bug properly should make this unnecessary. + MOZ_ASSERT(mSetActiveTask == nullptr); + + RefPtr task = + NewCancelableRunnableMethod>( + "layers::ActiveElementManager::SetActiveTask", this, + &ActiveElementManager::SetActiveTask, mTarget); + mSetActiveTask = task; + NS_GetCurrentThread()->DelayedDispatch( + task.forget(), StaticPrefs::ui_touch_activation_delay_ms()); + AEM_LOG("Scheduling mSetActiveTask %p\n", mSetActiveTask.get()); + } +} + +void ActiveElementManager::ClearActivation() { + AEM_LOG("Clearing element activation\n"); + CancelTask(); + ResetActive(); +} + +void ActiveElementManager::HandleTouchEndEvent(bool aWasClick) { + AEM_LOG("Touch end event, aWasClick: %d\n", aWasClick); + + // If the touch was a click, make mTarget :active right away. + // nsEventStateManager will reset the active element when processing + // the mouse-down event generated by the click. + CancelTask(); + if (aWasClick) { + // Scrollbar thumbs use a different mechanism for their active + // highlight (the "active" attribute), so don't set the active state + // on them because nothing will clear it. + if (!(mTarget && mTarget->IsXULElement(nsGkAtoms::thumb))) { + SetActive(mTarget); + } + } else { + // We might reach here if mCanBePan was false on touch-start and + // so we set the element active right away. Now it turns out the + // action was not a click so we need to reset the active element. + ResetActive(); + } + + ResetTouchBlockState(); +} + +void ActiveElementManager::HandleTouchEnd() { + AEM_LOG("Touch end, clearing pan state\n"); + mCanBePanSet = false; +} + +static nsPresContext* GetPresContextFor(nsIContent* aContent) { + if (!aContent) { + return nullptr; + } + PresShell* presShell = aContent->OwnerDoc()->GetPresShell(); + if (!presShell) { + return nullptr; + } + return presShell->GetPresContext(); +} + +void ActiveElementManager::SetActive(dom::Element* aTarget) { + AEM_LOG("Setting active %p\n", aTarget); + + if (nsPresContext* pc = GetPresContextFor(aTarget)) { + pc->EventStateManager()->SetContentState(aTarget, + dom::ElementState::ACTIVE); + } +} + +void ActiveElementManager::ResetActive() { + AEM_LOG("Resetting active from %p\n", mTarget.get()); + + // Clear the :active flag from mTarget by setting it on the document root. + if (mTarget) { + dom::Element* root = mTarget->OwnerDoc()->GetDocumentElement(); + if (root) { + AEM_LOG("Found root %p, making active\n", root); + SetActive(root); + } + } +} + +void ActiveElementManager::ResetTouchBlockState() { + mTarget = nullptr; + mCanBePanSet = false; +} + +void ActiveElementManager::SetActiveTask( + const nsCOMPtr& 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 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 mSetActiveTask; + + // Helpers + void TriggerElementActivation(); + void SetActive(dom::Element* aTarget); + void ResetActive(); + void ResetTouchBlockState(); + void SetActiveTask(const nsCOMPtr& aTarget); + void CancelTask(); +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_ActiveElementManager_h */ diff --git a/gfx/layers/apz/util/CheckerboardReportService.cpp b/gfx/layers/apz/util/CheckerboardReportService.cpp new file mode 100644 index 0000000000..b6ae712b3f --- /dev/null +++ b/gfx/layers/apz/util/CheckerboardReportService.cpp @@ -0,0 +1,217 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "CheckerboardReportService.h" + +#include "jsapi.h" // for JS_Now +#include "MainThreadUtils.h" // for NS_IsMainThread +#include "mozilla/Assertions.h" // for MOZ_ASSERT +#include "mozilla/ClearOnShutdown.h" // for ClearOnShutdown +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_apz.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/CheckerboardReportServiceBinding.h" // for dom::CheckerboardReports +#include "mozilla/gfx/GPUParent.h" +#include "mozilla/gfx/GPUProcessManager.h" +#include "nsContentUtils.h" // for nsContentUtils +#include "nsIObserverService.h" +#include "nsXULAppAPI.h" + +namespace mozilla { +namespace layers { + +/*static*/ +StaticRefPtr CheckerboardEventStorage::sInstance; + +/*static*/ +already_AddRefed +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 instance = sInstance.get(); + return instance.forget(); +} + +void CheckerboardEventStorage::Report(uint32_t aSeverity, + const std::string& aLog) { + if (!NS_IsMainThread()) { + RefPtr 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 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& aOutReports) { + MOZ_ASSERT(NS_IsMainThread()); + + for (int i = 0; i < RECENT_MAX_INDEX; i++) { + CheckerboardReport& r = mCheckerboardReports[i]; + if (r.mSeverity == 0) { + continue; + } + dom::CheckerboardReport report; + report.mSeverity.Construct() = r.mSeverity; + report.mTimestamp.Construct() = r.mTimestamp / 1000; // micros to millis + report.mLog.Construct() = + NS_ConvertUTF8toUTF16(r.mLog.c_str(), r.mLog.size()); + report.mReason.Construct() = (i < SEVERITY_MAX_INDEX) + ? dom::CheckerboardReason::Severe + : dom::CheckerboardReason::Recent; + aOutReports.AppendElement(report); + } +} + +} // namespace layers + +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CheckerboardReportService, mParent) + +/*static*/ +bool CheckerboardReportService::IsEnabled(JSContext* aCtx, JSObject* aGlobal) { + // Only allow this in the parent process + if (!XRE_IsParentProcess()) { + return false; + } + // Allow privileged code or about:checkerboard (unprivileged) to access this. + return nsContentUtils::IsSystemCaller(aCtx) || + nsContentUtils::IsSpecificAboutPage(aGlobal, "about:checkerboard"); +} + +/*static*/ +already_AddRefed +CheckerboardReportService::Constructor(const dom::GlobalObject& aGlobal) { + RefPtr ces = + new CheckerboardReportService(aGlobal.GetAsSupports()); + return ces.forget(); +} + +CheckerboardReportService::CheckerboardReportService(nsISupports* aParent) + : mParent(aParent) {} + +JSObject* CheckerboardReportService::WrapObject( + JSContext* aCtx, JS::Handle aGivenProto) { + return CheckerboardReportService_Binding::Wrap(aCtx, this, aGivenProto); +} + +nsISupports* CheckerboardReportService::GetParentObject() { return mParent; } + +void CheckerboardReportService::GetReports( + nsTArray& aOutReports) { + RefPtr 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 obsSvc = mozilla::services::GetObserverService(); + MOZ_ASSERT(obsSvc); + if (obsSvc) { + obsSvc->NotifyObservers(nullptr, "APZ:FlushActiveCheckerboard", nullptr); + } +} + +} // namespace dom +} // namespace mozilla diff --git a/gfx/layers/apz/util/CheckerboardReportService.h b/gfx/layers/apz/util/CheckerboardReportService.h new file mode 100644 index 0000000000..d9b37509c5 --- /dev/null +++ b/gfx/layers/apz/util/CheckerboardReportService.h @@ -0,0 +1,138 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_CheckerboardReportService_h +#define mozilla_dom_CheckerboardReportService_h + +#include + +#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 GetInstance(); + + /** + * Get the stored checkerboard reports. + */ + void GetReports(nsTArray& 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 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 Constructor( + const dom::GlobalObject& aGlobal); + + explicit CheckerboardReportService(nsISupports* aSupports); + + JSObject* WrapObject(JSContext* aCtx, + JS::Handle aGivenProto) override; + + nsISupports* GetParentObject(); + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(CheckerboardReportService) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(CheckerboardReportService) + + public: + /* + * The methods exposed via the webidl. + */ + void GetReports(nsTArray& aOutReports); + bool IsRecordingEnabled() const; + void SetRecordingEnabled(bool aEnabled); + void FlushActiveReports(); + + private: + virtual ~CheckerboardReportService() = default; + + nsCOMPtr mParent; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_layers_CheckerboardReportService_h */ diff --git a/gfx/layers/apz/util/ChromeProcessController.cpp b/gfx/layers/apz/util/ChromeProcessController.cpp new file mode 100644 index 0000000000..d16466ad7a --- /dev/null +++ b/gfx/layers/apz/util/ChromeProcessController.cpp @@ -0,0 +1,356 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ChromeProcessController.h" + +#include "MainThreadUtils.h" // for NS_IsMainThread() +#include "base/task.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/Element.h" +#include "mozilla/layers/CompositorBridgeParent.h" +#include "mozilla/layers/APZCCallbackHelper.h" +#include "mozilla/layers/APZEventState.h" +#include "mozilla/layers/APZThreadUtils.h" +#include "mozilla/layers/IAPZCTreeManager.h" +#include "mozilla/layers/InputAPZContext.h" +#include "mozilla/layers/DoubleTapToZoom.h" +#include "mozilla/layers/RepaintRequest.h" +#include "mozilla/dom/Document.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsLayoutUtils.h" +#include "nsView.h" + +static mozilla::LazyLogModule sApzChromeLog("apz.cc.chrome"); + +using namespace mozilla; +using namespace mozilla::layers; +using namespace mozilla::widget; + +ChromeProcessController::ChromeProcessController( + nsIWidget* aWidget, APZEventState* aAPZEventState, + IAPZCTreeManager* aAPZCTreeManager) + : mWidget(aWidget), + mAPZEventState(aAPZEventState), + mAPZCTreeManager(aAPZCTreeManager), + mUIThread(NS_GetCurrentThread()) { + // Otherwise we're initializing mUIThread incorrectly. + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aAPZEventState); + MOZ_ASSERT(aAPZCTreeManager); + + mUIThread->Dispatch( + NewRunnableMethod("layers::ChromeProcessController::InitializeRoot", this, + &ChromeProcessController::InitializeRoot)); +} + +ChromeProcessController::~ChromeProcessController() = default; + +void ChromeProcessController::InitializeRoot() { + APZCCallbackHelper::InitializeRootDisplayport(GetPresShell()); +} + +void ChromeProcessController::NotifyLayerTransforms( + nsTArray&& aTransforms) { + if (!mUIThread->IsOnCurrentThread()) { + mUIThread->Dispatch( + NewRunnableMethod>>( + "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 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 document = GetRootContentDocument(aGuid.mScrollId); + if (!document.get()) { + return; + } + + ZoomTarget zoomTarget = CalculateRectToZoomTo(document, aPoint); + + uint32_t presShellId; + ScrollableLayerGuid::ViewID viewId; + if (APZCCallbackHelper::GetOrCreateScrollIdentifiers( + document->GetDocumentElement(), &presShellId, &viewId)) { + mAPZCTreeManager->ZoomToRect( + ScrollableLayerGuid(aGuid.mLayersId, presShellId, viewId), zoomTarget, + ZoomToRectBehavior::DEFAULT_BEHAVIOR); + } +} + +void ChromeProcessController::HandleTap( + TapType aType, const mozilla::LayoutDevicePoint& aPoint, + Modifiers aModifiers, const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId) { + MOZ_LOG(sApzChromeLog, LogLevel::Debug, + ("HandleTap called with %d\n", (int)aType)); + if (!mUIThread->IsOnCurrentThread()) { + MOZ_LOG(sApzChromeLog, LogLevel::Debug, ("HandleTap redispatching\n")); + mUIThread->Dispatch( + NewRunnableMethod( + "layers::ChromeProcessController::HandleTap", this, + &ChromeProcessController::HandleTap, aType, aPoint, aModifiers, + aGuid, aInputBlockId)); + return; + } + + if (!mAPZEventState) { + return; + } + + RefPtr presShell = GetPresShell(); + if (!presShell) { + return; + } + if (!presShell->GetPresContext()) { + return; + } + CSSToLayoutDeviceScale scale( + presShell->GetPresContext()->CSSToDevPixelScale()); + + CSSPoint point = aPoint / scale; + + // Stash the guid in InputAPZContext so that when the visual-to-layout + // transform is applied to the event's coordinates, we use the right transform + // based on the scroll frame being targeted. + // The other values don't really matter. + InputAPZContext context(aGuid, aInputBlockId, nsEventStatus_eSentinel); + + switch (aType) { + case TapType::eSingleTap: + mAPZEventState->ProcessSingleTap(point, scale, aModifiers, 1, + aInputBlockId); + break; + case TapType::eDoubleTap: + HandleDoubleTap(point, aModifiers, aGuid); + break; + case TapType::eSecondTap: + mAPZEventState->ProcessSingleTap(point, scale, aModifiers, 2, + aInputBlockId); + break; + case TapType::eLongTap: { + RefPtr eventState(mAPZEventState); + eventState->ProcessLongTap(presShell, point, scale, aModifiers, + aInputBlockId); + break; + } + case TapType::eLongTapUp: { + RefPtr 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( + "layers::ChromeProcessController::NotifyPinchGesture", this, + &ChromeProcessController::NotifyPinchGesture, aType, aGuid, + aFocusPoint, aSpanChange, aModifiers)); + return; + } + + if (mWidget) { + // Dispatch the call to APZCCallbackHelper::NotifyPinchGesture to the main + // thread so that it runs asynchronously from the current call. This is + // because the call can run arbitrary JS code, which can also spin the event + // loop and cause undesirable re-entrancy in APZ. + mUIThread->Dispatch(NewRunnableFunction( + "layers::ChromeProcessController::NotifyPinchGestureAsync", + &APZCCallbackHelper::NotifyPinchGesture, aType, aFocusPoint, + aSpanChange, aModifiers, mWidget)); + } +} + +void ChromeProcessController::NotifyAPZStateChange( + const ScrollableLayerGuid& aGuid, APZStateChange aChange, int aArg, + Maybe aInputBlockId) { + if (!mUIThread->IsOnCurrentThread()) { + mUIThread->Dispatch(NewRunnableMethod>( + "layers::ChromeProcessController::NotifyAPZStateChange", this, + &ChromeProcessController::NotifyAPZStateChange, aGuid, aChange, aArg, + aInputBlockId)); + return; + } + + if (!mAPZEventState) { + return; + } + + mAPZEventState->ProcessAPZStateChange(aGuid.mScrollId, aChange, aArg, + aInputBlockId); +} + +void ChromeProcessController::NotifyMozMouseScrollEvent( + const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent) { + if (!mUIThread->IsOnCurrentThread()) { + mUIThread->Dispatch( + NewRunnableMethod( + "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( + "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( + "layers::ChromeProcessController::NotifyAsyncScrollbarDragRejected", + this, &ChromeProcessController::NotifyAsyncScrollbarDragRejected, + aScrollId)); + return; + } + + APZCCallbackHelper::NotifyAsyncScrollbarDragRejected(aScrollId); +} + +void ChromeProcessController::NotifyAsyncAutoscrollRejected( + const ScrollableLayerGuid::ViewID& aScrollId) { + if (!mUIThread->IsOnCurrentThread()) { + mUIThread->Dispatch(NewRunnableMethod( + "layers::ChromeProcessController::NotifyAsyncAutoscrollRejected", this, + &ChromeProcessController::NotifyAsyncAutoscrollRejected, aScrollId)); + return; + } + + APZCCallbackHelper::NotifyAsyncAutoscrollRejected(aScrollId); +} + +void ChromeProcessController::CancelAutoscroll( + const ScrollableLayerGuid& aGuid) { + if (!mUIThread->IsOnCurrentThread()) { + mUIThread->Dispatch(NewRunnableMethod( + "layers::ChromeProcessController::CancelAutoscroll", this, + &ChromeProcessController::CancelAutoscroll, aGuid)); + return; + } + + APZCCallbackHelper::CancelAutoscroll(aGuid.mScrollId); +} + +void ChromeProcessController::NotifyScaleGestureComplete( + const ScrollableLayerGuid& aGuid, float aScale) { + if (!mUIThread->IsOnCurrentThread()) { + mUIThread->Dispatch(NewRunnableMethod( + "layers::ChromeProcessController::NotifyScaleGestureComplete", this, + &ChromeProcessController::NotifyScaleGestureComplete, aGuid, aScale)); + return; + } + + if (mWidget) { + // Dispatch the call to APZCCallbackHelper::NotifyScaleGestureComplete + // to the main thread so that it runs asynchronously from the current call. + // This is because the call can run arbitrary JS code, which can also spin + // the event loop and cause undesirable re-entrancy in APZ. + mUIThread->Dispatch(NewRunnableFunction( + "layers::ChromeProcessController::NotifyScaleGestureComplete", + &APZCCallbackHelper::NotifyScaleGestureComplete, mWidget, aScale)); + } +} diff --git a/gfx/layers/apz/util/ChromeProcessController.h b/gfx/layers/apz/util/ChromeProcessController.h new file mode 100644 index 0000000000..7328d62adb --- /dev/null +++ b/gfx/layers/apz/util/ChromeProcessController.h @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layers_ChromeProcessController_h +#define mozilla_layers_ChromeProcessController_h + +#include "mozilla/layers/GeckoContentController.h" +#include "nsCOMPtr.h" +#include "mozilla/RefPtr.h" +#include "mozilla/layers/MatrixMessage.h" + +class nsIDOMWindowUtils; +class nsISerialEventTarget; +class nsIWidget; + +namespace mozilla { +class PresShell; +namespace dom { +class Document; +} + +namespace layers { + +class IAPZCTreeManager; +class APZEventState; + +/** + * ChromeProcessController is a GeckoContentController attached to the root of + * a compositor's layer tree. It's used directly by APZ by default, and remoted + * using PAPZ if there is a gpu process. + * + * If ChromeProcessController needs to implement a new method on + * GeckoContentController PAPZ, APZChild, and RemoteContentController must be + * updated to handle it. + */ +class ChromeProcessController : public mozilla::layers::GeckoContentController { + protected: + typedef mozilla::layers::FrameMetrics FrameMetrics; + typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid; + + public: + explicit ChromeProcessController(nsIWidget* aWidget, + APZEventState* aAPZEventState, + IAPZCTreeManager* aAPZCTreeManager); + virtual ~ChromeProcessController(); + void Destroy() override; + + // GeckoContentController interface + void NotifyLayerTransforms(nsTArray&& aTransforms) override; + void RequestContentRepaint(const RepaintRequest& aRequest) override; + bool IsRepaintThread() override; + void DispatchToRepaintThread(already_AddRefed aTask) override; + MOZ_CAN_RUN_SCRIPT + void HandleTap(TapType aType, const mozilla::LayoutDevicePoint& aPoint, + Modifiers aModifiers, const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId) override; + void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType, + const ScrollableLayerGuid& aGuid, + const LayoutDevicePoint& aFocusPoint, + LayoutDeviceCoord aSpanChange, + Modifiers aModifiers) override; + void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid, + APZStateChange aChange, int aArg, + Maybe aInputBlockId) override; + void NotifyMozMouseScrollEvent(const ScrollableLayerGuid::ViewID& aScrollId, + const nsString& aEvent) override; + void NotifyFlushComplete() override; + void NotifyAsyncScrollbarDragInitiated( + uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId, + ScrollDirection aDirection) override; + void NotifyAsyncScrollbarDragRejected( + const ScrollableLayerGuid::ViewID& aScrollId) override; + void NotifyAsyncAutoscrollRejected( + const ScrollableLayerGuid::ViewID& aScrollId) override; + void CancelAutoscroll(const ScrollableLayerGuid& aGuid) override; + void NotifyScaleGestureComplete(const ScrollableLayerGuid& aGuid, + float aScale) override; + + PresShell* GetTopLevelPresShell() const override { return GetPresShell(); } + + private: + nsCOMPtr mWidget; + RefPtr mAPZEventState; + RefPtr mAPZCTreeManager; + nsCOMPtr mUIThread; + + void InitializeRoot(); + PresShell* GetPresShell() const; + dom::Document* GetRootDocument() const; + dom::Document* GetRootContentDocument( + const ScrollableLayerGuid::ViewID& aScrollId) const; + void HandleDoubleTap(const mozilla::CSSPoint& aPoint, Modifiers aModifiers, + const ScrollableLayerGuid& aGuid); +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_ChromeProcessController_h */ diff --git a/gfx/layers/apz/util/ContentProcessController.cpp b/gfx/layers/apz/util/ContentProcessController.cpp new file mode 100644 index 0000000000..332d0523fe --- /dev/null +++ b/gfx/layers/apz/util/ContentProcessController.cpp @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ContentProcessController.h" + +#include "mozilla/PresShell.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/layers/APZCCallbackHelper.h" +#include "mozilla/layers/APZChild.h" +#include "nsIContentInlines.h" + +#include "InputData.h" // for InputData + +namespace mozilla { +namespace layers { + +ContentProcessController::ContentProcessController( + const RefPtr& aBrowser) + : mBrowser(aBrowser) { + MOZ_ASSERT(mBrowser); +} + +void ContentProcessController::NotifyLayerTransforms( + nsTArray&& aTransforms) { + // This should never get called + MOZ_ASSERT(false); +} + +void ContentProcessController::RequestContentRepaint( + const RepaintRequest& aRequest) { + if (mBrowser) { + mBrowser->UpdateFrame(aRequest); + } +} + +void ContentProcessController::HandleTap(TapType aType, + const LayoutDevicePoint& aPoint, + Modifiers aModifiers, + const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId) { + // This should never get called + MOZ_ASSERT(false); +} + +void ContentProcessController::NotifyPinchGesture( + PinchGestureInput::PinchGestureType aType, const ScrollableLayerGuid& aGuid, + const LayoutDevicePoint& aFocusPoint, LayoutDeviceCoord aSpanChange, + Modifiers aModifiers) { + // This should never get called + MOZ_ASSERT_UNREACHABLE("Unexpected message to content process"); +} + +void ContentProcessController::NotifyAPZStateChange( + const ScrollableLayerGuid& aGuid, APZStateChange aChange, int aArg, + Maybe aInputBlockId) { + if (mBrowser) { + mBrowser->NotifyAPZStateChange(aGuid.mScrollId, aChange, aArg, + aInputBlockId); + } +} + +void ContentProcessController::NotifyMozMouseScrollEvent( + const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent) { + if (mBrowser) { + APZCCallbackHelper::NotifyMozMouseScrollEvent(aScrollId, aEvent); + } +} + +void ContentProcessController::NotifyFlushComplete() { + if (mBrowser) { + RefPtr presShell = mBrowser->GetTopLevelPresShell(); + APZCCallbackHelper::NotifyFlushComplete(presShell); + } +} + +void ContentProcessController::NotifyAsyncScrollbarDragInitiated( + uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId, + ScrollDirection aDirection) { + APZCCallbackHelper::NotifyAsyncScrollbarDragInitiated(aDragBlockId, aScrollId, + aDirection); +} + +void ContentProcessController::NotifyAsyncScrollbarDragRejected( + const ScrollableLayerGuid::ViewID& aScrollId) { + APZCCallbackHelper::NotifyAsyncScrollbarDragRejected(aScrollId); +} + +void ContentProcessController::NotifyAsyncAutoscrollRejected( + const ScrollableLayerGuid::ViewID& aScrollId) { + APZCCallbackHelper::NotifyAsyncAutoscrollRejected(aScrollId); +} + +void ContentProcessController::CancelAutoscroll( + const ScrollableLayerGuid& aGuid) { + // This should never get called + MOZ_ASSERT_UNREACHABLE("Unexpected message to content process"); +} + +void ContentProcessController::NotifyScaleGestureComplete( + const ScrollableLayerGuid& aGuid, float aScale) { + // This should never get called + MOZ_ASSERT_UNREACHABLE("Unexpected message to content process"); +} + +bool ContentProcessController::IsRepaintThread() { return NS_IsMainThread(); } + +void ContentProcessController::DispatchToRepaintThread( + already_AddRefed aTask) { + NS_DispatchToMainThread(std::move(aTask)); +} + +PresShell* ContentProcessController::GetTopLevelPresShell() const { + if (!mBrowser) { + return nullptr; + } + return mBrowser->GetTopLevelPresShell(); +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/util/ContentProcessController.h b/gfx/layers/apz/util/ContentProcessController.h new file mode 100644 index 0000000000..02d32df72e --- /dev/null +++ b/gfx/layers/apz/util/ContentProcessController.h @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layers_ContentProcessController_h +#define mozilla_layers_ContentProcessController_h + +#include "mozilla/layers/GeckoContentController.h" + +class nsIObserver; + +namespace mozilla { + +namespace dom { +class BrowserChild; +} // namespace dom + +namespace layers { + +class APZChild; + +/** + * ContentProcessController is a GeckoContentController for a BrowserChild, and + * is always remoted using PAPZ/APZChild. + * + * ContentProcessController is created in ContentChild when a layer tree id has + * been allocated for a PBrowser that lives in that content process, and is + * destroyed when the Destroy message is received, or when the tab dies. + * + * If ContentProcessController needs to implement a new method on + * GeckoContentController PAPZ, APZChild, and RemoteContentController must be + * updated to handle it. + */ +class ContentProcessController final : public GeckoContentController { + public: + explicit ContentProcessController(const RefPtr& aBrowser); + + // GeckoContentController + + void NotifyLayerTransforms(nsTArray&& aTransforms) override; + + void RequestContentRepaint(const RepaintRequest& aRequest) override; + + void HandleTap(TapType aType, const LayoutDevicePoint& aPoint, + Modifiers aModifiers, const ScrollableLayerGuid& aGuid, + uint64_t aInputBlockId) override; + + void NotifyPinchGesture(PinchGestureInput::PinchGestureType aType, + const ScrollableLayerGuid& aGuid, + const LayoutDevicePoint& aFocusPoint, + LayoutDeviceCoord aSpanChange, + Modifiers aModifiers) override; + + void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid, + APZStateChange aChange, int aArg, + Maybe aInputBlockId) override; + + void NotifyMozMouseScrollEvent(const ScrollableLayerGuid::ViewID& aScrollId, + const nsString& aEvent) override; + + void NotifyFlushComplete() override; + + void NotifyAsyncScrollbarDragInitiated( + uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId, + ScrollDirection aDirection) override; + void NotifyAsyncScrollbarDragRejected( + const ScrollableLayerGuid::ViewID& aScrollId) override; + + void NotifyAsyncAutoscrollRejected( + const ScrollableLayerGuid::ViewID& aScrollId) override; + + void CancelAutoscroll(const ScrollableLayerGuid& aGuid) override; + + void NotifyScaleGestureComplete(const ScrollableLayerGuid& aGuid, + float aScale) override; + + bool IsRepaintThread() override; + + void DispatchToRepaintThread(already_AddRefed aTask) override; + + PresShell* GetTopLevelPresShell() const override; + + private: + RefPtr mBrowser; +}; + +} // namespace layers + +} // namespace mozilla + +#endif // mozilla_layers_ContentProcessController_h diff --git a/gfx/layers/apz/util/DoubleTapToZoom.cpp b/gfx/layers/apz/util/DoubleTapToZoom.cpp new file mode 100644 index 0000000000..c40d79ced3 --- /dev/null +++ b/gfx/layers/apz/util/DoubleTapToZoom.cpp @@ -0,0 +1,376 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "DoubleTapToZoom.h" + +#include // for std::min, std::max + +#include "mozilla/PresShell.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/dom/Element.h" +#include "nsCOMPtr.h" +#include "nsIContent.h" +#include "mozilla/dom/Document.h" +#include "nsIFrame.h" +#include "nsIFrameInlines.h" +#include "nsIScrollableFrame.h" +#include "nsTableCellFrame.h" +#include "nsLayoutUtils.h" +#include "nsStyleConsts.h" +#include "mozilla/ViewportUtils.h" +#include "mozilla/EventListenerManager.h" + +namespace mozilla { +namespace layers { + +namespace { + +using FrameForPointOption = nsLayoutUtils::FrameForPointOption; + +static bool IsGeneratedContent(nsIContent* aContent) { + // We exclude marks because making them double tap targets does not seem + // desirable. + return aContent->IsGeneratedContentContainerForBefore() || + aContent->IsGeneratedContentContainerForAfter(); +} + +// Returns the DOM element found at |aPoint|, interpreted as being relative to +// the root frame of |aPresShell| in visual coordinates. If the point is inside +// a subdocument, returns an element inside the subdocument, rather than the +// subdocument element (and does so recursively). The implementation was adapted +// from DocumentOrShadowRoot::ElementFromPoint(), with the notable exception +// that we don't pass nsLayoutUtils::IGNORE_CROSS_DOC to GetFrameForPoint(), so +// as to get the behaviour described above in the presence of subdocuments. +static already_AddRefed ElementFromPoint( + const RefPtr& aPresShell, const CSSPoint& aPoint) { + nsIFrame* rootFrame = aPresShell->GetRootFrame(); + if (!rootFrame) { + return nullptr; + } + nsIFrame* frame = nsLayoutUtils::GetFrameForPoint( + RelativeTo{rootFrame, ViewportType::Visual}, CSSPoint::ToAppUnits(aPoint), + {{FrameForPointOption::IgnorePaintSuppression}}); + while (frame && (!frame->GetContent() || + (frame->GetContent()->IsInNativeAnonymousSubtree() && + !IsGeneratedContent(frame->GetContent())))) { + frame = nsLayoutUtils::GetParentOrPlaceholderFor(frame); + } + if (!frame) { + return nullptr; + } + // FIXME(emilio): This should probably use the flattened tree, GetParent() is + // not guaranteed to be an element in presence of shadow DOM. + nsIContent* content = frame->GetContent(); + if (!content) { + return nullptr; + } + if (dom::Element* element = content->GetAsElementOrParentElement()) { + return do_AddRef(element); + } + return nullptr; +} + +// Get table cell from element, parent or grand parent. +static dom::Element* GetNearbyTableCell( + const nsCOMPtr& aElement) { + nsTableCellFrame* tableCell = do_QueryFrame(aElement->GetPrimaryFrame()); + if (tableCell) { + return aElement.get(); + } + if (dom::Element* parent = aElement->GetFlattenedTreeParentElement()) { + nsTableCellFrame* tableCell = do_QueryFrame(parent->GetPrimaryFrame()); + if (tableCell) { + return parent; + } + if (dom::Element* grandParent = parent->GetFlattenedTreeParentElement()) { + tableCell = do_QueryFrame(grandParent->GetPrimaryFrame()); + if (tableCell) { + return grandParent; + } + } + } + return nullptr; +} + +static bool ShouldZoomToElement( + const nsCOMPtr& aElement, + const RefPtr& aRootContentDocument, + nsIScrollableFrame* aRootScrollFrame, const FrameMetrics& aMetrics) { + if (nsIFrame* frame = aElement->GetPrimaryFrame()) { + if (frame->StyleDisplay()->IsInlineFlow() && + // Replaced elements are suitable zoom targets because they act like + // inline-blocks instead of inline. (textarea's are the specific reason + // we do this) + !frame->IsFrameOfType(nsIFrame::eReplaced)) { + return false; + } + } + // Trying to zoom to the html element will just end up scrolling to the start + // of the document, return false and we'll run out of elements and just + // zoomout (without scrolling to the start). + if (aElement->OwnerDoc() == aRootContentDocument && + aElement->IsHTMLElement(nsGkAtoms::html)) { + return false; + } + if (aElement->IsAnyOfHTMLElements(nsGkAtoms::li, nsGkAtoms::q)) { + return false; + } + + // Ignore elements who are table cells or their parents are table cells, and + // they take up less than 30% of page rect width because they are likely cells + // in data tables (as opposed to tables used for layout purposes), and we + // don't want to zoom to them. This heuristic is quite naive and leaves a lot + // to be desired. + if (dom::Element* tableCell = GetNearbyTableCell(aElement)) { + CSSRect rect = + nsLayoutUtils::GetBoundingContentRect(tableCell, aRootScrollFrame); + if (rect.width < 0.3 * aMetrics.GetScrollableRect().width) { + return false; + } + } + + return true; +} + +// Calculates if zooming to aRect would have almost the same zoom level as +// aCompositedArea currently has. If so we would want to zoom out instead. +static bool RectHasAlmostSameZoomLevel(const CSSRect& aRect, + const CSSRect& aCompositedArea) { + // This functions checks to see if the area of the rect visible in the + // composition bounds (i.e. the overlapArea variable below) is approximately + // the max area of the rect we can show. + + // AsyncPanZoomController::ZoomToRect will adjust the zoom and scroll offset + // so that the zoom to rect fills the composited area. If after adjusting the + // scroll offset _only_ the rect would fill the composited area we want to + // zoom out (we don't want to _just_ scroll, we want to do some amount of + // zooming, either in or out it doesn't matter which). So translate both rects + // to the same origin and then compute their overlap, which is what the + // following calculation does. + + float overlapArea = std::min(aRect.width, aCompositedArea.width) * + std::min(aRect.height, aCompositedArea.height); + float availHeight = std::min( + aRect.Width() * aCompositedArea.Height() / aCompositedArea.Width(), + aRect.Height()); + float showing = overlapArea / (aRect.Width() * availHeight); + float ratioW = aRect.Width() / aCompositedArea.Width(); + float ratioH = aRect.Height() / aCompositedArea.Height(); + + return showing > 0.9 && (ratioW > 0.9 || ratioH > 0.9); +} + +} // namespace + +static CSSRect AddHMargin(const CSSRect& aRect, const CSSCoord& aMargin, + const FrameMetrics& aMetrics) { + CSSRect rect = + CSSRect(std::max(aMetrics.GetScrollableRect().X(), aRect.X() - aMargin), + aRect.Y(), aRect.Width() + 2 * aMargin, aRect.Height()); + // Constrict the rect to the screen's right edge + rect.SetWidth( + std::min(rect.Width(), aMetrics.GetScrollableRect().XMost() - rect.X())); + return rect; +} + +static CSSRect AddVMargin(const CSSRect& aRect, const CSSCoord& aMargin, + const FrameMetrics& aMetrics) { + CSSRect rect = + CSSRect(aRect.X(), + std::max(aMetrics.GetScrollableRect().Y(), aRect.Y() - aMargin), + aRect.Width(), aRect.Height() + 2 * aMargin); + // Constrict the rect to the screen's bottom edge + rect.SetHeight( + std::min(rect.Height(), aMetrics.GetScrollableRect().YMost() - rect.Y())); + return rect; +} + +static bool IsReplacedElement(const nsCOMPtr& aElement) { + if (nsIFrame* frame = aElement->GetPrimaryFrame()) { + if (frame->IsFrameOfType(nsIFrame::eReplaced)) { + return true; + } + } + return false; +} + +static bool HasNonPassiveWheelListenerOnAncestor(nsIContent* aContent) { + for (nsIContent* content = aContent; content; + content = content->GetFlattenedTreeParent()) { + EventListenerManager* elm = content->GetExistingListenerManager(); + if (elm && elm->HasNonPassiveWheelListener()) { + return true; + } + } + return false; +} + +ZoomTarget CalculateRectToZoomTo( + const RefPtr& 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 = aRootContentDocument->GetPresShell(); + if (!presShell) { + return ZoomTarget{zoomOut, CantZoomOutBehavior::ZoomIn}; + } + + nsIScrollableFrame* rootScrollFrame = + presShell->GetRootScrollFrameAsScrollable(); + if (!rootScrollFrame) { + return ZoomTarget{zoomOut, CantZoomOutBehavior::ZoomIn}; + } + + CSSPoint documentRelativePoint = + CSSPoint::FromAppUnits(ViewportUtils::VisualToLayout( + CSSPoint::ToAppUnits(aPoint), presShell)) + + CSSPoint::FromAppUnits(rootScrollFrame->GetScrollPosition()); + + nsCOMPtr element = ElementFromPoint(presShell, aPoint); + if (!element) { + return ZoomTarget{zoomOut, CantZoomOutBehavior::ZoomIn, Nothing(), + Some(documentRelativePoint)}; + } + + CantZoomOutBehavior cantZoomOutBehavior = + HasNonPassiveWheelListenerOnAncestor(element) + ? CantZoomOutBehavior::Nothing + : CantZoomOutBehavior::ZoomIn; + + FrameMetrics metrics = + nsLayoutUtils::CalculateBasicFrameMetrics(rootScrollFrame); + + while (element && !ShouldZoomToElement(element, aRootContentDocument, + rootScrollFrame, metrics)) { + element = element->GetFlattenedTreeParentElement(); + } + + if (!element) { + return ZoomTarget{zoomOut, cantZoomOutBehavior, Nothing(), + Some(documentRelativePoint)}; + } + + CSSPoint visualScrollOffset = metrics.GetVisualScrollOffset(); + CSSRect compositedArea(visualScrollOffset, + metrics.CalculateCompositedSizeInCssPixels()); + Maybe nearestScrollClip; + CSSRect rect = nsLayoutUtils::GetBoundingContentRect(element, rootScrollFrame, + &nearestScrollClip); + + // In some cases, like overflow: visible and overflowing content, the bounding + // client rect of the targeted element won't contain the point the user double + // tapped on. In that case we use the scrollable overflow rect if it contains + // the user point. + if (!rect.Contains(documentRelativePoint)) { + if (nsIFrame* scrolledFrame = rootScrollFrame->GetScrolledFrame()) { + if (nsIFrame* f = element->GetPrimaryFrame()) { + nsRect overflowRect = f->ScrollableOverflowRect(); + nsLayoutUtils::TransformResult res = + nsLayoutUtils::TransformRect(f, scrolledFrame, overflowRect); + MOZ_ASSERT(res == nsLayoutUtils::TRANSFORM_SUCCEEDED || + res == nsLayoutUtils::NONINVERTIBLE_TRANSFORM); + if (res == nsLayoutUtils::TRANSFORM_SUCCEEDED) { + CSSRect overflowRectCSS = CSSRect::FromAppUnits(overflowRect); + if (nearestScrollClip.isSome()) { + overflowRectCSS = nearestScrollClip->Intersect(overflowRectCSS); + } + if (overflowRectCSS.Contains(documentRelativePoint)) { + rect = overflowRectCSS; + } + } + } + } + } + + CSSRect elementBoundingRect = rect; + + // Generally we zoom to the width of some element, but sometimes we zoom to + // the height. We set this to true when that happens so that we can add a + // vertical margin to the rect, otherwise it looks weird. + bool heightConstrained = false; + + // If the element is taller than the visible area of the page scale + // the height of the |rect| so that it has the same aspect ratio as + // the root frame. The clipped |rect| is centered on the y value of + // the touch point. This allows tall narrow elements to be zoomed. + if (!rect.IsEmpty() && compositedArea.Width() > 0.0f && + compositedArea.Height() > 0.0f) { + // Calculate the height of the rect if it had the same aspect ratio as + // compositedArea. + const float widthRatio = rect.Width() / compositedArea.Width(); + float targetHeight = compositedArea.Height() * widthRatio; + + // We don't want to cut off the top or bottoms of replaced elements that are + // square or wider in aspect ratio. + + // If it's a replaced element and we would otherwise trim it's height below + if (IsReplacedElement(element) && targetHeight < rect.Height() && + // If the target rect is at most 1.1x away from being square or wider + // aspect ratio + rect.Height() < 1.1 * rect.Width() && + // and our compositedArea is wider than it is tall + compositedArea.Width() >= compositedArea.Height()) { + heightConstrained = true; + // Expand the width of the rect so that it fills compositedArea so that if + // we are already zoomed to this element then the IsRectZoomedIn call + // below returns true so that we zoom out. This won't change what we + // actually zoom to as we are just making the rect the same aspect ratio + // as compositedArea. + float targetWidth = + rect.Height() * compositedArea.Width() / compositedArea.Height(); + MOZ_ASSERT(targetWidth > rect.Width()); + if (targetWidth > rect.Width()) { + rect.x -= (targetWidth - rect.Width()) / 2; + rect.SetWidth(targetWidth); + // keep elementBoundingRect containing rect + elementBoundingRect = rect; + } + + } else if (targetHeight < rect.Height()) { + // Trim the height so that the target rect has the same aspect ratio as + // compositedArea, centering it around the user tap point. + float newY = documentRelativePoint.y - (targetHeight * 0.5f); + if ((newY + targetHeight) > rect.YMost()) { + rect.MoveByY(rect.Height() - targetHeight); + } else if (newY > rect.Y()) { + rect.MoveToY(newY); + } + rect.SetHeight(targetHeight); + } + } + + const CSSCoord margin = 15; + rect = AddHMargin(rect, margin, metrics); + + if (heightConstrained) { + rect = AddVMargin(rect, margin, metrics); + } + + // If the rect is already taking up most of the visible area and is + // stretching the width of the page, then we want to zoom out instead. + if (RectHasAlmostSameZoomLevel(rect, compositedArea)) { + return ZoomTarget{zoomOut, cantZoomOutBehavior, Nothing(), + Some(documentRelativePoint)}; + } + + elementBoundingRect = AddHMargin(elementBoundingRect, margin, metrics); + + // Unlike rect, elementBoundingRect is the full height of the element we are + // zooming to. If we zoom to it without a margin it can look a weird, so give + // it a vertical margin. + elementBoundingRect = AddVMargin(elementBoundingRect, margin, metrics); + + rect.Round(); + elementBoundingRect.Round(); + return ZoomTarget{rect, cantZoomOutBehavior, Some(elementBoundingRect), + Some(documentRelativePoint)}; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/util/DoubleTapToZoom.h b/gfx/layers/apz/util/DoubleTapToZoom.h new file mode 100644 index 0000000000..91264deef9 --- /dev/null +++ b/gfx/layers/apz/util/DoubleTapToZoom.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layers_DoubleTapToZoom_h +#define mozilla_layers_DoubleTapToZoom_h + +#include "Units.h" + +template +class RefPtr; + +namespace mozilla { +namespace dom { +class Document; +} + +namespace layers { + +enum class CantZoomOutBehavior : int8_t { Nothing = 0, ZoomIn }; + +struct ZoomTarget { + // The preferred target rect that we'd like to zoom in on, if possible. An + // empty rect means the browser should zoom out. + CSSRect targetRect; + + // If we are asked to zoom out but cannot (due to zoom constraints, etc), then + // zoom in some small amount to provide feedback to the user. + CantZoomOutBehavior cantZoomOutBehavior = CantZoomOutBehavior::Nothing; + + // If zooming all the way in on |targetRect| is not possible (for example, due + // to a max zoom constraint), |elementBoundingRect| may be used to inform a + // more optimal target scroll position (for example, we may try to maximize + // the area of |elementBoundingRect| that's showing, while keeping + // |targetRect| in view and keeping the zoom as close to the desired zoom as + // possible). + Maybe elementBoundingRect; + + // The document relative (ie if the content inside the root scroll frame + // existed without that scroll frame) pointer position at the time of the + // double tap or location of the double tap if we can compute it. Only used if + // the rest of this ZoomTarget is asking to zoom out but we are already at the + // minimum zoom. In which case we zoom in a small amount on this point. + Maybe documentRelativePointerPosition; +}; + +/** + * For a double tap at |aPoint|, return a ZoomTarget struct with contains a rect + * to which the browser should zoom in response (see ZoomTarget definition for + * more details). An empty rect means the browser should zoom out. |aDocument| + * should be the root content document for the content that was tapped. + */ +ZoomTarget CalculateRectToZoomTo( + const RefPtr& aRootContentDocument, + const CSSPoint& aPoint); + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_DoubleTapToZoom_h */ diff --git a/gfx/layers/apz/util/InputAPZContext.cpp b/gfx/layers/apz/util/InputAPZContext.cpp new file mode 100644 index 0000000000..77573221ff --- /dev/null +++ b/gfx/layers/apz/util/InputAPZContext.cpp @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "InputAPZContext.h" + +namespace mozilla { +namespace layers { + +ScrollableLayerGuid InputAPZContext::sGuid; +uint64_t InputAPZContext::sBlockId = 0; +nsEventStatus InputAPZContext::sApzResponse = nsEventStatus_eSentinel; +bool InputAPZContext::sPendingLayerization = false; +bool InputAPZContext::sRoutedToChildProcess = false; + +/*static*/ +ScrollableLayerGuid InputAPZContext::GetTargetLayerGuid() { return sGuid; } + +/*static*/ +uint64_t InputAPZContext::GetInputBlockId() { return sBlockId; } + +/*static*/ +nsEventStatus InputAPZContext::GetApzResponse() { return sApzResponse; } + +/*static*/ +bool InputAPZContext::HavePendingLayerization() { return sPendingLayerization; } + +/*static*/ +bool InputAPZContext::WasRoutedToChildProcess() { + return sRoutedToChildProcess; +} + +InputAPZContext::InputAPZContext(const ScrollableLayerGuid& aGuid, + const uint64_t& aBlockId, + const nsEventStatus& aApzResponse, + bool aPendingLayerization) + : mOldGuid(sGuid), + mOldBlockId(sBlockId), + mOldApzResponse(sApzResponse), + mOldPendingLayerization(sPendingLayerization), + mOldRoutedToChildProcess(sRoutedToChildProcess) { + sGuid = aGuid; + sBlockId = aBlockId; + sApzResponse = aApzResponse; + sPendingLayerization = aPendingLayerization; + sRoutedToChildProcess = false; +} + +InputAPZContext::~InputAPZContext() { + sGuid = mOldGuid; + sBlockId = mOldBlockId; + sApzResponse = mOldApzResponse; + sPendingLayerization = mOldPendingLayerization; + sRoutedToChildProcess = mOldRoutedToChildProcess; +} + +/*static*/ +void InputAPZContext::SetRoutedToChildProcess() { + sRoutedToChildProcess = true; +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/util/InputAPZContext.h b/gfx/layers/apz/util/InputAPZContext.h new file mode 100644 index 0000000000..928359ab1d --- /dev/null +++ b/gfx/layers/apz/util/InputAPZContext.h @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layers_InputAPZContext_h +#define mozilla_layers_InputAPZContext_h + +#include "mozilla/EventForwards.h" +#include "mozilla/layers/ScrollableLayerGuid.h" + +namespace mozilla { +namespace layers { + +// InputAPZContext is used to communicate various pieces of information +// around the codebase without having to plumb it through lots of functions +// and codepaths. Conceptually it is attached to a WidgetInputEvent that is +// relevant to APZ. +// +// There are two types of information bits propagated using this class. One +// type is propagated "downwards" (from a process entry point like nsBaseWidget +// or BrowserChild) into deeper code that is run during complicated operations +// like event dispatch. The other type is information that is propagated +// "upwards", from the deeper code back to the entry point. +class MOZ_STACK_CLASS InputAPZContext { + private: + // State that is propagated downwards from InputAPZContext creation into + // "deeper" code. + static ScrollableLayerGuid sGuid; + static uint64_t sBlockId; + static nsEventStatus sApzResponse; + static bool sPendingLayerization; + + // State that is set in deeper code and propagated upwards. + static bool sRoutedToChildProcess; + + public: + // Functions to access downwards-propagated data + static ScrollableLayerGuid GetTargetLayerGuid(); + static uint64_t GetInputBlockId(); + static nsEventStatus GetApzResponse(); + static bool HavePendingLayerization(); + + // Functions to access upwards-propagated data + static bool WasRoutedToChildProcess(); + + // Constructor sets the data to be propagated downwards + InputAPZContext(const ScrollableLayerGuid& aGuid, const uint64_t& aBlockId, + const nsEventStatus& aApzResponse, + bool aPendingLayerization = false); + ~InputAPZContext(); + + // Functions to set data to be propagated upwards + static void SetRoutedToChildProcess(); + + private: + ScrollableLayerGuid mOldGuid; + uint64_t mOldBlockId; + nsEventStatus mOldApzResponse; + bool mOldPendingLayerization; + + bool mOldRoutedToChildProcess; +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_InputAPZContext_h */ diff --git a/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp b/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp new file mode 100644 index 0000000000..eb456fa243 --- /dev/null +++ b/gfx/layers/apz/util/ScrollLinkedEffectDetector.cpp @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ScrollLinkedEffectDetector.h" + +#include "mozilla/dom/Document.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace layers { + +uint32_t ScrollLinkedEffectDetector::sDepth = 0; +bool ScrollLinkedEffectDetector::sFoundScrollLinkedEffect = false; + +/* static */ +void ScrollLinkedEffectDetector::PositioningPropertyMutated() { + MOZ_ASSERT(NS_IsMainThread()); + + if (sDepth > 0) { + // We are inside a scroll event dispatch + sFoundScrollLinkedEffect = true; + } +} + +ScrollLinkedEffectDetector::ScrollLinkedEffectDetector( + dom::Document* aDoc, const TimeStamp& aTimeStamp) + : mDocument(aDoc), mTimeStamp(aTimeStamp) { + MOZ_ASSERT(NS_IsMainThread()); + sDepth++; +} + +ScrollLinkedEffectDetector::~ScrollLinkedEffectDetector() { + sDepth--; + if (sDepth == 0) { + // We have exited all (possibly-nested) scroll event dispatches, + // record whether or not we found an effect, and reset state + if (sFoundScrollLinkedEffect) { + mDocument->ReportHasScrollLinkedEffect(mTimeStamp); + sFoundScrollLinkedEffect = false; + } + } +} + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/util/ScrollLinkedEffectDetector.h b/gfx/layers/apz/util/ScrollLinkedEffectDetector.h new file mode 100644 index 0000000000..4568fe649b --- /dev/null +++ b/gfx/layers/apz/util/ScrollLinkedEffectDetector.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layers_ScrollLinkedEffectDetector_h +#define mozilla_layers_ScrollLinkedEffectDetector_h + +#include "mozilla/RefPtr.h" +#include "mozilla/TimeStamp.h" + +namespace mozilla { + +namespace dom { +class Document; +} + +namespace layers { + +// ScrollLinkedEffectDetector is used to detect the existence of a scroll-linked +// effect on a webpage. Generally speaking, a scroll-linked effect is something +// on the page that animates or changes with respect to the scroll position. +// Content authors usually rely on running some JS in response to the scroll +// event in order to implement such effects, and therefore it tends to be laggy +// or work improperly with APZ enabled. This class helps us detect such an +// effect so that we can warn the author and/or take other preventative +// measures. +class MOZ_STACK_CLASS ScrollLinkedEffectDetector final { + private: + static uint32_t sDepth; + static bool sFoundScrollLinkedEffect; + + public: + static void PositioningPropertyMutated(); + + ScrollLinkedEffectDetector(dom::Document*, const TimeStamp& aTimeStamp); + ~ScrollLinkedEffectDetector(); + + private: + RefPtr mDocument; + TimeStamp mTimeStamp; +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_ScrollLinkedEffectDetector_h */ diff --git a/gfx/layers/apz/util/ScrollingInteractionContext.cpp b/gfx/layers/apz/util/ScrollingInteractionContext.cpp new file mode 100644 index 0000000000..1a92a9eb07 --- /dev/null +++ b/gfx/layers/apz/util/ScrollingInteractionContext.cpp @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ScrollingInteractionContext.h" + +namespace mozilla::layers { + +/*static*/ +bool ScrollingInteractionContext::sScrollingToAnchor = false; + +/*static*/ +bool ScrollingInteractionContext::IsScrollingToAnchor() { + return sScrollingToAnchor; +} + +ScrollingInteractionContext::ScrollingInteractionContext( + bool aScrollingToAnchor) + : mOldScrollingToAnchor(sScrollingToAnchor) { + sScrollingToAnchor = aScrollingToAnchor; +} + +ScrollingInteractionContext::~ScrollingInteractionContext() { + sScrollingToAnchor = mOldScrollingToAnchor; +} + +} // namespace mozilla::layers diff --git a/gfx/layers/apz/util/ScrollingInteractionContext.h b/gfx/layers/apz/util/ScrollingInteractionContext.h new file mode 100644 index 0000000000..cae953008b --- /dev/null +++ b/gfx/layers/apz/util/ScrollingInteractionContext.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layers_ScrollingInteractionContext_h +#define mozilla_layers_ScrollingInteractionContext_h + +#include "mozilla/EventForwards.h" +#include "mozilla/layers/ScrollableLayerGuid.h" + +namespace mozilla { +namespace layers { + +// The ScrollingInteractionContext is used to store minor details of the +// current scrolling interaction on the stack to avoid having to pass them +// though the callstack +class MOZ_STACK_CLASS ScrollingInteractionContext { + private: + static bool sScrollingToAnchor; + + public: + // Functions to access downwards-propagated data + static bool IsScrollingToAnchor(); + + // Constructor sets the data to be propagated downwards + explicit ScrollingInteractionContext(bool aScrollingToAnchor); + + // Destructor restores the previous state + ~ScrollingInteractionContext(); + + private: + bool mOldScrollingToAnchor; +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_ScrollingInteractionContext_h */ diff --git a/gfx/layers/apz/util/TouchActionHelper.cpp b/gfx/layers/apz/util/TouchActionHelper.cpp new file mode 100644 index 0000000000..4598e30a6a --- /dev/null +++ b/gfx/layers/apz/util/TouchActionHelper.cpp @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TouchActionHelper.h" + +#include "mozilla/layers/IAPZCTreeManager.h" +#include "mozilla/PresShell.h" +#include "mozilla/TouchEvents.h" +#include "nsContainerFrame.h" +#include "nsIFrameInlines.h" +#include "nsIScrollableFrame.h" +#include "nsLayoutUtils.h" + +namespace mozilla::layers { + +static void UpdateAllowedBehavior(StyleTouchAction aTouchActionValue, + bool aConsiderPanning, + TouchBehaviorFlags& aOutBehavior) { + if (aTouchActionValue != StyleTouchAction::AUTO) { + // Double-tap-zooming need property value AUTO + aOutBehavior &= ~AllowedTouchBehavior::ANIMATING_ZOOM; + if (aTouchActionValue != StyleTouchAction::MANIPULATION && + !(aTouchActionValue & StyleTouchAction::PINCH_ZOOM)) { + // Pinch-zooming needs value AUTO or MANIPULATION, or the PINCH_ZOOM bit + // set + aOutBehavior &= ~AllowedTouchBehavior::PINCH_ZOOM; + } + } + + if (aConsiderPanning) { + if (aTouchActionValue == StyleTouchAction::NONE) { + aOutBehavior &= ~AllowedTouchBehavior::VERTICAL_PAN; + aOutBehavior &= ~AllowedTouchBehavior::HORIZONTAL_PAN; + } + + // Values pan-x and pan-y set at the same time to the same element do not + // affect panning constraints. Therefore we need to check whether pan-x is + // set without pan-y and the same for pan-y. + if ((aTouchActionValue & StyleTouchAction::PAN_X) && + !(aTouchActionValue & StyleTouchAction::PAN_Y)) { + aOutBehavior &= ~AllowedTouchBehavior::VERTICAL_PAN; + } else if ((aTouchActionValue & StyleTouchAction::PAN_Y) && + !(aTouchActionValue & StyleTouchAction::PAN_X)) { + aOutBehavior &= ~AllowedTouchBehavior::HORIZONTAL_PAN; + } + } +} + +static TouchBehaviorFlags GetAllowedTouchBehaviorForPoint( + nsIWidget* aWidget, RelativeTo aRootFrame, + const LayoutDeviceIntPoint& aPoint) { + nsPoint relativePoint = + nsLayoutUtils::GetEventCoordinatesRelativeTo(aWidget, aPoint, aRootFrame); + + nsIFrame* target = nsLayoutUtils::GetFrameForPoint(aRootFrame, relativePoint); + + return TouchActionHelper::GetAllowedTouchBehaviorForFrame(target); +} + +nsTArray TouchActionHelper::GetAllowedTouchBehavior( + nsIWidget* aWidget, dom::Document* aDocument, + const WidgetTouchEvent& aEvent) { + nsTArray flags; + if (!aWidget || !aDocument) { + return flags; + } + if (PresShell* presShell = aDocument->GetPresShell()) { + if (nsIFrame* rootFrame = presShell->GetRootFrame()) { + for (const auto& touch : aEvent.mTouches) { + flags.AppendElement(GetAllowedTouchBehaviorForPoint( + aWidget, RelativeTo{rootFrame, ViewportType::Visual}, + touch->mRefPoint)); + } + } + } + return flags; +} + +TouchBehaviorFlags TouchActionHelper::GetAllowedTouchBehaviorForFrame( + nsIFrame* aFrame) { + TouchBehaviorFlags behavior = AllowedTouchBehavior::VERTICAL_PAN | + AllowedTouchBehavior::HORIZONTAL_PAN | + AllowedTouchBehavior::PINCH_ZOOM | + AllowedTouchBehavior::ANIMATING_ZOOM; + + if (!aFrame) { + return behavior; + } + + nsIScrollableFrame* nearestScrollableParent = + nsLayoutUtils::GetNearestScrollableFrame(aFrame, 0); + nsIFrame* nearestScrollableFrame = do_QueryFrame(nearestScrollableParent); + + // We're walking up the DOM tree until we meet the element with touch behavior + // and accumulating touch-action restrictions of all elements in this chain. + // The exact quote from the spec, that clarifies more: + // To determine the effect of a touch, find the nearest ancestor (starting + // from the element itself) that has a default touch behavior. Then examine + // the touch-action property of each element between the hit tested element + // and the element with the default touch behavior (including both the hit + // tested element and the element with the default touch behavior). If the + // touch-action property of any of those elements disallows the default touch + // behavior, do nothing. Otherwise allow the element to start considering the + // touch for the purposes of executing a default touch behavior. + + // Currently we support only two touch behaviors: panning and zooming. + // For panning we walk up until we meet the first scrollable element (the + // element that supports panning) or root element. For zooming we walk up + // until the root element since Firefox currently supports only zooming of the + // root frame but not the subframes. + + bool considerPanning = true; + + for (nsIFrame* frame = aFrame; frame && frame->GetContent() && behavior; + frame = frame->GetInFlowParent()) { + UpdateAllowedBehavior(frame->UsedTouchAction(), considerPanning, behavior); + + if (frame == nearestScrollableFrame) { + // We met the scrollable element, after it we shouldn't consider + // touch-action values for the purpose of panning but only for zooming. + considerPanning = false; + } + } + + return behavior; +} + +} // namespace mozilla::layers diff --git a/gfx/layers/apz/util/TouchActionHelper.h b/gfx/layers/apz/util/TouchActionHelper.h new file mode 100644 index 0000000000..b83d0d9ecb --- /dev/null +++ b/gfx/layers/apz/util/TouchActionHelper.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __mozilla_layers_TouchActionHelper_h__ +#define __mozilla_layers_TouchActionHelper_h__ + +#include "mozilla/layers/LayersTypes.h" // for TouchBehaviorFlags +#include "RelativeTo.h" // for RelativeTo + +class nsIWidget; +namespace mozilla { + +namespace dom { +class Document; +} // namespace dom + +class WidgetTouchEvent; +} // namespace mozilla + +namespace mozilla::layers { + +/* + * Helper class to figure out the allowed touch behavior for frames, as per + * the touch-action spec. + */ +class TouchActionHelper { + public: + /* + * Performs hit testing on content, finds frame that corresponds to the touch + * points of aEvent and retrieves touch-action CSS property value from it + * according the rules specified in the spec: + * http://www.w3.org/TR/pointerevents/#the-touch-action-css-property. + */ + static nsTArray GetAllowedTouchBehavior( + nsIWidget* aWidget, dom::Document* aDocument, + const WidgetTouchEvent& aPoint); + + static TouchBehaviorFlags GetAllowedTouchBehaviorForFrame(nsIFrame* aFrame); +}; + +} // namespace mozilla::layers + +#endif /*__mozilla_layers_TouchActionHelper_h__ */ diff --git a/gfx/layers/apz/util/TouchCounter.cpp b/gfx/layers/apz/util/TouchCounter.cpp new file mode 100644 index 0000000000..9e4d6f1ba6 --- /dev/null +++ b/gfx/layers/apz/util/TouchCounter.cpp @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TouchCounter.h" + +#include "InputData.h" +#include "mozilla/TouchEvents.h" + +namespace mozilla { +namespace layers { + +TouchCounter::TouchCounter() : mActiveTouchCount(0) {} + +void TouchCounter::Update(const MultiTouchInput& aInput) { + switch (aInput.mType) { + case MultiTouchInput::MULTITOUCH_START: + // touch-start event contains all active touches of the current session + mActiveTouchCount = aInput.mTouches.Length(); + break; + case MultiTouchInput::MULTITOUCH_END: + if (mActiveTouchCount >= aInput.mTouches.Length()) { + // touch-end event contains only released touches + mActiveTouchCount -= aInput.mTouches.Length(); + } else { + NS_WARNING("Got an unexpected touchend"); + mActiveTouchCount = 0; + } + break; + case MultiTouchInput::MULTITOUCH_CANCEL: + mActiveTouchCount = 0; + break; + case MultiTouchInput::MULTITOUCH_MOVE: + break; + } +} + +void TouchCounter::Update(const WidgetTouchEvent& aEvent) { + switch (aEvent.mMessage) { + case eTouchStart: + // touch-start event contains all active touches of the current session + mActiveTouchCount = aEvent.mTouches.Length(); + break; + case eTouchEnd: { + // touch-end contains all touches, but ones being lifted are marked as + // changed + uint32_t liftedTouches = 0; + for (const auto& touch : aEvent.mTouches) { + if (touch->mChanged) { + liftedTouches++; + } + } + if (mActiveTouchCount >= liftedTouches) { + mActiveTouchCount -= liftedTouches; + } else { + NS_WARNING("Got an unexpected touchend"); + mActiveTouchCount = 0; + } + break; + } + case eTouchCancel: + mActiveTouchCount = 0; + break; + default: + break; + } +} + +uint32_t TouchCounter::GetActiveTouchCount() const { return mActiveTouchCount; } + +} // namespace layers +} // namespace mozilla diff --git a/gfx/layers/apz/util/TouchCounter.h b/gfx/layers/apz/util/TouchCounter.h new file mode 100644 index 0000000000..c13f475355 --- /dev/null +++ b/gfx/layers/apz/util/TouchCounter.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_layers_TouchCounter_h +#define mozilla_layers_TouchCounter_h + +#include "mozilla/EventForwards.h" + +namespace mozilla { + +class MultiTouchInput; + +namespace layers { + +// TouchCounter simply tracks the number of active touch points. Feed it +// your input events to update the internal state. Generally you should +// only be calling one of the Update functions, depending on which type +// of touch inputs you have access to. +class TouchCounter { + public: + TouchCounter(); + void Update(const MultiTouchInput& aInput); + void Update(const WidgetTouchEvent& aEvent); + uint32_t GetActiveTouchCount() const; + + private: + uint32_t mActiveTouchCount; +}; + +} // namespace layers +} // namespace mozilla + +#endif /* mozilla_layers_TouchCounter_h */ -- cgit v1.2.3