Bug 1715179 - Allow double-tap-to-zoom to zoom in contents inside OOP iframes. r...
[gecko.git] / gfx / layers / apz / src / AsyncPanZoomController.cpp
bloba471dee48526249120d782ab08cbe150eb833ecb
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "AsyncPanZoomController.h" // for AsyncPanZoomController, etc
9 #include <math.h> // for fabsf, fabs, atan2
10 #include <stdint.h> // for uint32_t, uint64_t
11 #include <sys/types.h> // for int32_t
12 #include <algorithm> // for max, min
13 #include <utility> // for std::make_pair
15 #include "APZCTreeManager.h" // for APZCTreeManager
16 #include "AsyncPanZoomAnimation.h" // for AsyncPanZoomAnimation
17 #include "AutoDirWheelDeltaAdjuster.h" // for APZAutoDirWheelDeltaAdjuster
18 #include "AutoscrollAnimation.h" // for AutoscrollAnimation
19 #include "Axis.h" // for AxisX, AxisY, Axis, etc
20 #include "CheckerboardEvent.h" // for CheckerboardEvent
21 #include "Compositor.h" // for Compositor
22 #include "DesktopFlingPhysics.h" // for DesktopFlingPhysics
23 #include "FrameMetrics.h" // for FrameMetrics, etc
24 #include "GenericFlingAnimation.h" // for GenericFlingAnimation
25 #include "GestureEventListener.h" // for GestureEventListener
26 #include "HitTestingTreeNode.h" // for HitTestingTreeNode
27 #include "InputData.h" // for MultiTouchInput, etc
28 #include "InputBlockState.h" // for InputBlockState, TouchBlockState
29 #include "InputQueue.h" // for InputQueue
30 #include "Overscroll.h" // for OverscrollAnimation
31 #include "OverscrollHandoffState.h" // for OverscrollHandoffState
32 #include "SimpleVelocityTracker.h" // for SimpleVelocityTracker
33 #include "Units.h" // for CSSRect, CSSPoint, etc
34 #include "UnitTransforms.h" // for TransformTo
35 #include "base/message_loop.h" // for MessageLoop
36 #include "base/task.h" // for NewRunnableMethod, etc
37 #include "gfxTypes.h" // for gfxFloat
38 #include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
39 #include "mozilla/BasicEvents.h" // for Modifiers, MODIFIER_*
40 #include "mozilla/ClearOnShutdown.h" // for ClearOnShutdown
41 #include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction
42 #include "mozilla/EventForwards.h" // for nsEventStatus_*
43 #include "mozilla/EventStateManager.h" // for EventStateManager
44 #include "mozilla/MouseEvents.h" // for WidgetWheelEvent
45 #include "mozilla/Preferences.h" // for Preferences
46 #include "mozilla/RecursiveMutex.h" // for RecursiveMutexAutoLock, etc
47 #include "mozilla/RefPtr.h" // for RefPtr
48 #include "mozilla/ScrollTypes.h"
49 #include "mozilla/StaticPrefs_apz.h"
50 #include "mozilla/StaticPrefs_general.h"
51 #include "mozilla/StaticPrefs_gfx.h"
52 #include "mozilla/StaticPrefs_mousewheel.h"
53 #include "mozilla/StaticPrefs_layers.h"
54 #include "mozilla/StaticPrefs_layout.h"
55 #include "mozilla/StaticPrefs_slider.h"
56 #include "mozilla/StaticPrefs_test.h"
57 #include "mozilla/StaticPrefs_toolkit.h"
58 #include "mozilla/Telemetry.h" // for Telemetry
59 #include "mozilla/TimeStamp.h" // for TimeDuration, TimeStamp
60 #include "mozilla/dom/CheckerboardReportService.h" // for CheckerboardEventStorage
61 // note: CheckerboardReportService.h actually lives in gfx/layers/apz/util/
62 #include "mozilla/dom/Touch.h" // for Touch
63 #include "mozilla/gfx/gfxVars.h" // for gfxVars
64 #include "mozilla/gfx/BasePoint.h" // for BasePoint
65 #include "mozilla/gfx/BaseRect.h" // for BaseRect
66 #include "mozilla/gfx/Point.h" // for Point, RoundedToInt, etc
67 #include "mozilla/gfx/Rect.h" // for RoundedIn
68 #include "mozilla/gfx/ScaleFactor.h" // for ScaleFactor
69 #include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread, etc
70 #include "mozilla/layers/APZUtils.h" // for AsyncTransform
71 #include "mozilla/layers/CompositorController.h" // for CompositorController
72 #include "mozilla/layers/DirectionUtils.h" // for GetAxis{Start,End,Length,Scale}
73 #include "mozilla/layers/APZPublicUtils.h" // for GetScrollMode
74 #include "mozilla/mozalloc.h" // for operator new, etc
75 #include "mozilla/Unused.h" // for unused
76 #include "nsAlgorithm.h" // for clamped
77 #include "nsCOMPtr.h" // for already_AddRefed
78 #include "nsDebug.h" // for NS_WARNING
79 #include "nsLayoutUtils.h"
80 #include "nsMathUtils.h" // for NS_hypot
81 #include "nsPoint.h" // for nsIntPoint
82 #include "nsStyleConsts.h"
83 #include "nsTArray.h" // for nsTArray, nsTArray_Impl, etc
84 #include "nsThreadUtils.h" // for NS_IsMainThread
85 #include "nsViewportInfo.h" // for ViewportMinScale(), ViewportMaxScale()
86 #include "prsystem.h" // for PR_GetPhysicalMemorySize
87 #include "mozilla/ipc/SharedMemoryBasic.h" // for SharedMemoryBasic
88 #include "ScrollSnap.h" // for ScrollSnapUtils
89 #include "ScrollAnimationPhysics.h" // for ComputeAcceleratedWheelDelta
90 #include "SmoothMsdScrollAnimation.h"
91 #include "SmoothScrollAnimation.h"
92 #include "WheelScrollAnimation.h"
93 #if defined(MOZ_WIDGET_ANDROID)
94 # include "AndroidAPZ.h"
95 #endif // defined(MOZ_WIDGET_ANDROID)
97 static mozilla::LazyLogModule sApzCtlLog("apz.controller");
98 #define APZC_LOG(...) MOZ_LOG(sApzCtlLog, LogLevel::Debug, (__VA_ARGS__))
99 #define APZC_LOGV(...) MOZ_LOG(sApzCtlLog, LogLevel::Verbose, (__VA_ARGS__))
101 // Log to the apz.controller log with additional info from the APZC
102 #define APZC_LOG_DETAIL(fmt, apzc, ...) \
103 APZC_LOG("%p(%s scrollId=%" PRIu64 "): " fmt, (apzc), \
104 (apzc)->IsRootContent() ? "root" : "subframe", \
105 (apzc)->GetScrollId(), ##__VA_ARGS__)
107 #define APZC_LOG_FM_COMMON(fm, prefix, level, ...) \
108 if (MOZ_LOG_TEST(sApzCtlLog, level)) { \
109 std::stringstream ss; \
110 ss << nsPrintfCString(prefix, __VA_ARGS__).get() << ":" << fm; \
111 MOZ_LOG(sApzCtlLog, level, ("%s\n", ss.str().c_str())); \
113 #define APZC_LOG_FM(fm, prefix, ...) \
114 APZC_LOG_FM_COMMON(fm, prefix, LogLevel::Debug, __VA_ARGS__)
115 #define APZC_LOGV_FM(fm, prefix, ...) \
116 APZC_LOG_FM_COMMON(fm, prefix, LogLevel::Verbose, __VA_ARGS__)
118 namespace mozilla {
119 namespace layers {
121 typedef mozilla::layers::AllowedTouchBehavior AllowedTouchBehavior;
122 typedef GeckoContentController::APZStateChange APZStateChange;
123 typedef GeckoContentController::TapType TapType;
124 typedef mozilla::gfx::Point Point;
125 typedef mozilla::gfx::Matrix4x4 Matrix4x4;
127 // Choose between platform-specific implementations.
128 #ifdef MOZ_WIDGET_ANDROID
129 typedef WidgetOverscrollEffect OverscrollEffect;
130 typedef AndroidSpecificState PlatformSpecificState;
131 #else
132 typedef GenericOverscrollEffect OverscrollEffect;
133 typedef PlatformSpecificStateBase
134 PlatformSpecificState; // no extra state, just use the base class
135 #endif
138 * \page APZCPrefs APZ preferences
140 * The following prefs are used to control the behaviour of the APZC.
141 * The default values are provided in StaticPrefList.yaml.
143 * \li\b apz.allow_double_tap_zooming
144 * Pref that allows or disallows double tap to zoom
146 * \li\b apz.allow_immediate_handoff
147 * If set to true, scroll can be handed off from one APZC to another within
148 * a single input block. If set to false, a single input block can only
149 * scroll one APZC.
151 * \li\b apz.allow_zooming_out
152 * If set to true, APZ will allow zooming out past the initial scale on
153 * desktop. This is false by default to match Chrome's behaviour.
155 * \li\b apz.android.chrome_fling_physics.friction
156 * A tunable parameter for Chrome fling physics on Android that governs
157 * how quickly a fling animation slows down due to friction (and therefore
158 * also how far it reaches). Should be in the range [0-1].
160 * \li\b apz.android.chrome_fling_physics.inflexion
161 * A tunable parameter for Chrome fling physics on Android that governs
162 * the shape of the fling curve. Should be in the range [0-1].
164 * \li\b apz.android.chrome_fling_physics.stop_threshold
165 * A tunable parameter for Chrome fling physics on Android that governs
166 * how close the fling animation has to get to its target destination
167 * before it stops.
168 * Units: ParentLayer pixels
170 * \li\b apz.autoscroll.enabled
171 * If set to true, autoscrolling is driven by APZ rather than the content
172 * process main thread.
174 * \li\b apz.axis_lock.mode
175 * The preferred axis locking style. See AxisLockMode for possible values.
177 * \li\b apz.axis_lock.lock_angle
178 * Angle from axis within which we stay axis-locked.\n
179 * Units: radians
181 * \li\b apz.axis_lock.breakout_threshold
182 * Distance in inches the user must pan before axis lock can be broken.\n
183 * Units: (real-world, i.e. screen) inches
185 * \li\b apz.axis_lock.breakout_angle
186 * Angle at which axis lock can be broken.\n
187 * Units: radians
189 * \li\b apz.axis_lock.direct_pan_angle
190 * If the angle from an axis to the line drawn by a pan move is less than
191 * this value, we can assume that panning can be done in the allowed direction
192 * (horizontal or vertical).\n
193 * Currently used only for touch-action css property stuff and was addded to
194 * keep behaviour consistent with IE.\n
195 * Units: radians
197 * \li\b apz.content_response_timeout
198 * Amount of time before we timeout response from content. For example, if
199 * content is being unruly/slow and we don't get a response back within this
200 * time, we will just pretend that content did not preventDefault any touch
201 * events we dispatched to it.\n
202 * Units: milliseconds
204 * \li\b apz.danger_zone_x
205 * \li\b apz.danger_zone_y
206 * When drawing high-res tiles, we drop down to drawing low-res tiles
207 * when we know we can't keep up with the scrolling. The way we determine
208 * this is by checking if we are entering the "danger zone", which is the
209 * boundary of the painted content. For example, if the painted content
210 * goes from y=0...1000 and the visible portion is y=250...750 then
211 * we're far from checkerboarding. If we get to y=490...990 though then we're
212 * only 10 pixels away from showing checkerboarding so we are probably in
213 * a state where we can't keep up with scrolling. The danger zone prefs specify
214 * how wide this margin is; in the above example a y-axis danger zone of 10
215 * pixels would make us drop to low-res at y=490...990.\n
216 * This value is in screen pixels.
218 * \li\b apz.disable_for_scroll_linked_effects
219 * Setting this pref to true will disable APZ scrolling on documents where
220 * scroll-linked effects are detected. A scroll linked effect is detected if
221 * positioning or transform properties are updated inside a scroll event
222 * dispatch; we assume that such an update is in response to the scroll event
223 * and is therefore a scroll-linked effect which will be laggy with APZ
224 * scrolling.
226 * \li\b apz.displayport_expiry_ms
227 * While a scrollable frame is scrolling async, we set a displayport on it
228 * to make sure it is layerized. However this takes up memory, so once the
229 * scrolling stops we want to remove the displayport. This pref controls how
230 * long after scrolling stops the displayport is removed. A value of 0 will
231 * disable the expiry behavior entirely.
232 * Units: milliseconds
234 * \li\b apz.drag.enabled
235 * Setting this pref to true will cause APZ to handle mouse-dragging of
236 * scrollbar thumbs.
238 * \li\b apz.drag.initial.enabled
239 * Setting this pref to true will cause APZ to try to handle mouse-dragging
240 * of scrollbar thumbs without an initial round-trip to content to start it
241 * if possible. Only has an effect if apz.drag.enabled is also true.
243 * \li\b apz.drag.touch.enabled
244 * Setting this pref to true will cause APZ to handle touch-dragging of
245 * scrollbar thumbs. Only has an effect if apz.drag.enabled is also true.
247 * \li\b apz.enlarge_displayport_when_clipped
248 * Pref that enables enlarging of the displayport along one axis when the
249 * generated displayport's size is beyond that of the scrollable rect on the
250 * opposite axis.
252 * \li\b apz.fling_accel_min_fling_velocity
253 * The minimum velocity of the second fling, and the minimum velocity of the
254 * previous fling animation at the point of interruption, for the new fling to
255 * be considered for fling acceleration.
256 * Units: screen pixels per milliseconds
258 * \li\b apz.fling_accel_min_pan_velocity
259 * The minimum velocity during the pan gesture that causes a fling for that
260 * fling to be considered for fling acceleration.
261 * Units: screen pixels per milliseconds
263 * \li\b apz.fling_accel_max_pause_interval_ms
264 * The maximum time that is allowed to elapse between the touch start event that
265 * interrupts the previous fling, and the touch move that initiates panning for
266 * the current fling, for that fling to be considered for fling acceleration.
267 * Units: milliseconds
269 * \li\b apz.fling_accel_base_mult
270 * \li\b apz.fling_accel_supplemental_mult
271 * When applying an acceleration on a fling, the new computed velocity is
272 * (new_fling_velocity * base_mult) + (old_velocity * supplemental_mult).
273 * The base_mult and supplemental_mult multiplier values are controlled by
274 * these prefs. Note that "old_velocity" here is the initial velocity of the
275 * previous fling _after_ acceleration was applied to it (if applicable).
277 * \li\b apz.fling_curve_function_x1
278 * \li\b apz.fling_curve_function_y1
279 * \li\b apz.fling_curve_function_x2
280 * \li\b apz.fling_curve_function_y2
281 * \li\b apz.fling_curve_threshold_inches_per_ms
282 * These five parameters define a Bezier curve function and threshold used to
283 * increase the actual velocity relative to the user's finger velocity. When the
284 * finger velocity is below the threshold (or if the threshold is not positive),
285 * the velocity is used as-is. If the finger velocity exceeds the threshold
286 * velocity, then the function defined by the curve is applied on the part of
287 * the velocity that exceeds the threshold. Note that the upper bound of the
288 * velocity is still specified by the \b apz.max_velocity_inches_per_ms pref,
289 * and the function will smoothly curve the velocity from the threshold to the
290 * max. In general the function parameters chosen should define an ease-out
291 * curve in order to increase the velocity in this range, or an ease-in curve to
292 * decrease the velocity. A straight-line curve is equivalent to disabling the
293 * curve entirely by setting the threshold to -1. The max velocity pref must
294 * also be set in order for the curving to take effect, as it defines the upper
295 * bound of the velocity curve.\n
296 * The points (x1, y1) and (x2, y2) used as the two intermediate control points
297 * in the cubic bezier curve; the first and last points are (0,0) and (1,1).\n
298 * Some example values for these prefs can be found at\n
299 * https://searchfox.org/mozilla-central/rev/f82d5c549f046cb64ce5602bfd894b7ae807c8f8/dom/animation/ComputedTimingFunction.cpp#27-33
301 * \li\b apz.fling_friction
302 * Amount of friction applied during flings. This is used in the following
303 * formula: v(t1) = v(t0) * (1 - f)^(t1 - t0), where v(t1) is the velocity
304 * for a new sample, v(t0) is the velocity at the previous sample, f is the
305 * value of this pref, and (t1 - t0) is the amount of time, in milliseconds,
306 * that has elapsed between the two samples.\n
307 * NOTE: Not currently used in Android fling calculations.
309 * \li\b apz.fling_min_velocity_threshold
310 * Minimum velocity for a fling to actually kick off. If the user pans and lifts
311 * their finger such that the velocity is smaller than or equal to this amount,
312 * no fling is initiated.\n
313 * Units: screen pixels per millisecond
315 * \li\b apz.fling_stop_on_tap_threshold
316 * When flinging, if the velocity is above this number, then a tap on the
317 * screen will stop the fling without dispatching a tap to content. If the
318 * velocity is below this threshold a tap will also be dispatched.
319 * Note: when modifying this pref be sure to run the APZC gtests as some of
320 * them depend on the value of this pref.\n
321 * Units: screen pixels per millisecond
323 * \li\b apz.fling_stopped_threshold
324 * When flinging, if the velocity goes below this number, we just stop the
325 * animation completely. This is to prevent asymptotically approaching 0
326 * velocity and rerendering unnecessarily.\n
327 * Units: screen pixels per millisecond.\n
328 * NOTE: Should not be set to anything
329 * other than 0.0 for Android except for tests to disable flings.
331 * \li\b apz.keyboard.enabled
332 * Determines whether scrolling with the keyboard will be allowed to be handled
333 * by APZ.
335 * \li\b apz.keyboard.passive-listeners
336 * When enabled, APZ will interpret the passive event listener flag to mean
337 * that the event listener won't change the focused element or selection of
338 * the page. With this, web content can use passive key listeners and not have
339 * keyboard APZ disabled.
341 * \li\b apz.max_tap_time
342 * Maximum time for a touch on the screen and corresponding lift of the finger
343 * to be considered a tap. This also applies to double taps, except that it is
344 * used both for the interval between the first touchdown and first touchup,
345 * and for the interval between the first touchup and the second touchdown.\n
346 * Units: milliseconds.
348 * \li\b apz.max_velocity_inches_per_ms
349 * Maximum velocity. Velocity will be capped at this value if a faster fling
350 * occurs. Negative values indicate unlimited velocity.\n
351 * Units: (real-world, i.e. screen) inches per millisecond
353 * \li\b apz.max_velocity_queue_size
354 * Maximum size of velocity queue. The queue contains last N velocity records.
355 * On touch end we calculate the average velocity in order to compensate
356 * touch/mouse drivers misbehaviour.
358 * \li\b apz.min_skate_speed
359 * Minimum amount of speed along an axis before we switch to "skate" multipliers
360 * rather than using the "stationary" multipliers.\n
361 * Units: CSS pixels per millisecond
363 * \li\b apz.one_touch_pinch.enabled
364 * Whether or not the "one-touch-pinch" gesture (for zooming with one finger)
365 * is enabled or not.
367 * \li\b apz.overscroll.enabled
368 * Pref that enables overscrolling. If this is disabled, excess scroll that
369 * cannot be handed off is discarded.
371 * \li\b apz.overscroll.min_pan_distance_ratio
372 * The minimum ratio of the pan distance along one axis to the pan distance
373 * along the other axis needed to initiate overscroll along the first axis
374 * during panning.
376 * \li\b apz.overscroll.stretch_factor
377 * How much overscrolling can stretch content along an axis.
378 * The maximum stretch along an axis is a factor of (1 + kStretchFactor).
379 * (So if kStretchFactor is 0, you can't stretch at all; if kStretchFactor
380 * is 1, you can stretch at most by a factor of 2).
382 * \li\b apz.overscroll.stop_distance_threshold
383 * \li\b apz.overscroll.stop_velocity_threshold
384 * Thresholds for stopping the overscroll animation. When both the distance
385 * and the velocity fall below their thresholds, we stop oscillating.\n
386 * Units: screen pixels (for distance)
387 * screen pixels per millisecond (for velocity)
389 * \li\b apz.overscroll.spring_stiffness
390 * The spring stiffness constant for the overscroll mass-spring-damper model.
392 * \li\b apz.overscroll.damping
393 * The damping constant for the overscroll mass-spring-damper model.
395 * \li\b apz.overscroll.max_velocity
396 * The maximum velocity (in ParentLayerPixels per millisecond) allowed when
397 * initiating the overscroll snap-back animation.
399 * \li\b apz.paint_skipping.enabled
400 * When APZ is scrolling and sending repaint requests to the main thread, often
401 * the main thread doesn't actually need to do a repaint. This pref allows the
402 * main thread to skip doing those repaints in cases where it doesn't need to.
404 * \li\b apz.pinch_lock.mode
405 * The preferred pinch locking style. See PinchLockMode for possible values.
407 * \li\b apz.pinch_lock.scroll_lock_threshold
408 * Pinch locking is triggered if the user scrolls more than this distance
409 * and pinches less than apz.pinch_lock.span_lock_threshold.\n
410 * Units: (real-world, i.e. screen) inches
412 * \li\b apz.pinch_lock.span_breakout_threshold
413 * Distance in inches the user must pinch before lock can be broken.\n
414 * Units: (real-world, i.e. screen) inches measured between two touch points
416 * \li\b apz.pinch_lock.span_lock_threshold
417 * Pinch locking is triggered if the user pinches less than this distance
418 * and scrolls more than apz.pinch_lock.scroll_lock_threshold.\n
419 * Units: (real-world, i.e. screen) inches measured between two touch points
421 * \li\b apz.pinch_lock.buffer_max_age
422 * To ensure that pinch locking threshold calculations are not affected by
423 * variations in touch screen sensitivity, calculations draw from a buffer of
424 * recent events. This preference specifies the maximum time that events are
425 * held in this buffer.
426 * Units: milliseconds
428 * \li\b apz.popups.enabled
429 * Determines whether APZ is used for XUL popup widgets with remote content.
430 * Ideally, this should always be true, but it is currently not well tested, and
431 * has known issues, so needs to be prefable.
433 * \li\b apz.record_checkerboarding
434 * Whether or not to record detailed info on checkerboarding events.
436 * \li\b apz.second_tap_tolerance
437 * Constant describing the tolerance in distance we use, multiplied by the
438 * device DPI, within which a second tap is counted as part of a gesture
439 * continuing from the first tap. Making this larger allows the user more
440 * distance between the first and second taps in a "double tap" or "one touch
441 * pinch" gesture.\n
442 * Units: (real-world, i.e. screen) inches
444 * \li\b apz.test.logging_enabled
445 * Enable logging of APZ test data (see bug 961289).
447 * \li\b apz.touch_move_tolerance
448 * See the description for apz.touch_start_tolerance below. This is a similar
449 * threshold, except it is used to suppress touchmove events from being
450 * delivered to content for NON-scrollable frames (or more precisely, for APZCs
451 * where ArePointerEventsConsumable returns false).\n Units: (real-world, i.e.
452 * screen) inches
454 * \li\b apz.touch_start_tolerance
455 * Constant describing the tolerance in distance we use, multiplied by the
456 * device DPI, before we start panning the screen. This is to prevent us from
457 * accidentally processing taps as touch moves, and from very short/accidental
458 * touches moving the screen. touchmove events are also not delivered to content
459 * within this distance on scrollable frames.\n
460 * Units: (real-world, i.e. screen) inches
462 * \li\b apz.velocity_bias
463 * How much to adjust the displayport in the direction of scrolling. This value
464 * is multiplied by the velocity and added to the displayport offset.
466 * \li\b apz.velocity_relevance_time_ms
467 * When computing a fling velocity from the most recently stored velocity
468 * information, only velocities within the most X milliseconds are used.
469 * This pref controls the value of X.\n
470 * Units: ms
472 * \li\b apz.x_skate_size_multiplier
473 * \li\b apz.y_skate_size_multiplier
474 * The multiplier we apply to the displayport size if it is skating (current
475 * velocity is above \b apz.min_skate_speed). We prefer to increase the size of
476 * the Y axis because it is more natural in the case that a user is reading a
477 * page page that scrolls up/down. Note that one, both or neither of these may
478 * be used at any instant.\n In general we want \b
479 * apz.[xy]_skate_size_multiplier to be smaller than the corresponding
480 * stationary size multiplier because when panning fast we would like to paint
481 * less and get faster, more predictable paint times. When panning slowly we
482 * can afford to paint more even though it's slower.
484 * \li\b apz.x_stationary_size_multiplier
485 * \li\b apz.y_stationary_size_multiplier
486 * The multiplier we apply to the displayport size if it is not skating (see
487 * documentation for the skate size multipliers above).
489 * \li\b apz.x_skate_highmem_adjust
490 * \li\b apz.y_skate_highmem_adjust
491 * On high memory systems, we adjust the displayport during skating
492 * to be larger so we can reduce checkerboarding.
494 * \li\b apz.zoom_animation_duration_ms
495 * This controls how long the zoom-to-rect animation takes.\n
496 * Units: ms
498 * \li\b apz.scale_repaint_delay_ms
499 * How long to delay between repaint requests during a scale.
500 * A negative number prevents repaint requests during a scale.\n
501 * Units: ms
505 * Computed time function used for sampling frames of a zoom to animation.
507 StaticAutoPtr<StyleComputedTimingFunction> gZoomAnimationFunction;
510 * Computed time function used for curving up velocity when it gets high.
512 StaticAutoPtr<StyleComputedTimingFunction> gVelocityCurveFunction;
515 * The estimated duration of a paint for the purposes of calculating a new
516 * displayport, in milliseconds.
518 static const double kDefaultEstimatedPaintDurationMs = 50;
521 * Returns true if this is a high memory system and we can use
522 * extra memory for a larger displayport to reduce checkerboarding.
524 static bool gIsHighMemSystem = false;
525 static bool IsHighMemSystem() { return gIsHighMemSystem; }
527 AsyncPanZoomAnimation* PlatformSpecificStateBase::CreateFlingAnimation(
528 AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState,
529 float aPLPPI) {
530 return new GenericFlingAnimation<DesktopFlingPhysics>(aApzc, aHandoffState,
531 aPLPPI);
534 UniquePtr<VelocityTracker> PlatformSpecificStateBase::CreateVelocityTracker(
535 Axis* aAxis) {
536 return MakeUnique<SimpleVelocityTracker>(aAxis);
539 SampleTime AsyncPanZoomController::GetFrameTime() const {
540 APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
541 return treeManagerLocal ? treeManagerLocal->GetFrameTime()
542 : SampleTime::FromNow();
545 bool AsyncPanZoomController::IsZero(const ParentLayerPoint& aPoint) const {
546 RecursiveMutexAutoLock lock(mRecursiveMutex);
548 const auto zoom = Metrics().GetZoom();
550 if (zoom == CSSToParentLayerScale(0)) {
551 return true;
554 return layers::IsZero(aPoint / zoom);
557 bool AsyncPanZoomController::IsZero(ParentLayerCoord aCoord) const {
558 RecursiveMutexAutoLock lock(mRecursiveMutex);
560 const auto zoom = Metrics().GetZoom();
562 if (zoom == CSSToParentLayerScale(0)) {
563 return true;
566 return FuzzyEqualsAdditive((aCoord / zoom), CSSCoord(), COORDINATE_EPSILON);
569 bool AsyncPanZoomController::FuzzyGreater(ParentLayerCoord aCoord1,
570 ParentLayerCoord aCoord2) const {
571 RecursiveMutexAutoLock lock(mRecursiveMutex);
573 const auto zoom = Metrics().GetZoom();
575 if (zoom == CSSToParentLayerScale(0)) {
576 return false;
579 return (aCoord1 - aCoord2) / zoom > COORDINATE_EPSILON;
582 class MOZ_STACK_CLASS StateChangeNotificationBlocker final {
583 public:
584 explicit StateChangeNotificationBlocker(AsyncPanZoomController* aApzc)
585 : mApzc(aApzc) {
586 RecursiveMutexAutoLock lock(mApzc->mRecursiveMutex);
587 mInitialState = mApzc->mState;
588 mApzc->mNotificationBlockers++;
591 ~StateChangeNotificationBlocker() {
592 AsyncPanZoomController::PanZoomState newState;
594 RecursiveMutexAutoLock lock(mApzc->mRecursiveMutex);
595 mApzc->mNotificationBlockers--;
596 newState = mApzc->mState;
598 mApzc->DispatchStateChangeNotification(mInitialState, newState);
601 private:
602 AsyncPanZoomController* mApzc;
603 AsyncPanZoomController::PanZoomState mInitialState;
607 * An RAII class to temporarily apply async test attributes to the provided
608 * AsyncPanZoomController.
610 * This class should be used in the implementation of any AsyncPanZoomController
611 * method that queries the async scroll offset or async zoom (this includes
612 * the async layout viewport offset, since modifying the async scroll offset
613 * may result in the layout viewport moving as well).
615 class MOZ_RAII AutoApplyAsyncTestAttributes final {
616 public:
617 explicit AutoApplyAsyncTestAttributes(
618 const AsyncPanZoomController*,
619 const RecursiveMutexAutoLock& aProofOfLock);
620 ~AutoApplyAsyncTestAttributes();
622 private:
623 AsyncPanZoomController* mApzc;
624 FrameMetrics mPrevFrameMetrics;
625 ParentLayerPoint mPrevOverscroll;
626 const RecursiveMutexAutoLock& mProofOfLock;
629 AutoApplyAsyncTestAttributes::AutoApplyAsyncTestAttributes(
630 const AsyncPanZoomController* aApzc,
631 const RecursiveMutexAutoLock& aProofOfLock)
632 // Having to use const_cast here seems less ugly than the alternatives
633 // of making several members of AsyncPanZoomController that
634 // ApplyAsyncTestAttributes() modifies |mutable|, or several methods that
635 // query the async transforms non-const.
636 : mApzc(const_cast<AsyncPanZoomController*>(aApzc)),
637 mPrevFrameMetrics(aApzc->Metrics()),
638 mPrevOverscroll(aApzc->GetOverscrollAmountInternal()),
639 mProofOfLock(aProofOfLock) {
640 mApzc->ApplyAsyncTestAttributes(aProofOfLock);
643 AutoApplyAsyncTestAttributes::~AutoApplyAsyncTestAttributes() {
644 mApzc->UnapplyAsyncTestAttributes(mProofOfLock, mPrevFrameMetrics,
645 mPrevOverscroll);
648 class ZoomAnimation : public AsyncPanZoomAnimation {
649 public:
650 ZoomAnimation(AsyncPanZoomController& aApzc, const CSSPoint& aStartOffset,
651 const CSSToParentLayerScale& aStartZoom,
652 const CSSPoint& aEndOffset,
653 const CSSToParentLayerScale& aEndZoom)
654 : mApzc(aApzc),
655 mTotalDuration(TimeDuration::FromMilliseconds(
656 StaticPrefs::apz_zoom_animation_duration_ms())),
657 mStartOffset(aStartOffset),
658 mStartZoom(aStartZoom),
659 mEndOffset(aEndOffset),
660 mEndZoom(aEndZoom) {}
662 virtual bool DoSample(FrameMetrics& aFrameMetrics,
663 const TimeDuration& aDelta) override {
664 mDuration += aDelta;
665 double animPosition = mDuration / mTotalDuration;
667 if (animPosition >= 1.0) {
668 aFrameMetrics.SetZoom(mEndZoom);
669 mApzc.SetVisualScrollOffset(mEndOffset);
670 return false;
673 // Sample the zoom at the current time point. The sampled zoom
674 // will affect the final computed resolution.
675 float sampledPosition =
676 gZoomAnimationFunction->At(animPosition, /* aBeforeFlag = */ false);
678 // We scale the scrollOffset linearly with sampledPosition, so the zoom
679 // needs to scale inversely to match.
680 if (mStartZoom == CSSToParentLayerScale(0) ||
681 mEndZoom == CSSToParentLayerScale(0)) {
682 return false;
685 aFrameMetrics.SetZoom(
686 CSSToParentLayerScale(1 / (sampledPosition / mEndZoom.scale +
687 (1 - sampledPosition) / mStartZoom.scale)));
689 mApzc.SetVisualScrollOffset(CSSPoint::FromUnknownPoint(gfx::Point(
690 mEndOffset.x * sampledPosition + mStartOffset.x * (1 - sampledPosition),
691 mEndOffset.y * sampledPosition +
692 mStartOffset.y * (1 - sampledPosition))));
693 return true;
696 virtual bool WantsRepaints() override { return true; }
698 private:
699 AsyncPanZoomController& mApzc;
701 TimeDuration mDuration;
702 const TimeDuration mTotalDuration;
704 // Old metrics from before we started a zoom animation. This is only valid
705 // when we are in the "ANIMATED_ZOOM" state. This is used so that we can
706 // interpolate between the start and end frames. We only use the
707 // |mViewportScrollOffset| and |mResolution| fields on this.
708 CSSPoint mStartOffset;
709 CSSToParentLayerScale mStartZoom;
711 // Target metrics for a zoom to animation. This is only valid when we are in
712 // the "ANIMATED_ZOOM" state. We only use the |mViewportScrollOffset| and
713 // |mResolution| fields on this.
714 CSSPoint mEndOffset;
715 CSSToParentLayerScale mEndZoom;
718 /*static*/
719 void AsyncPanZoomController::InitializeGlobalState() {
720 static bool sInitialized = false;
721 if (sInitialized) return;
722 sInitialized = true;
724 MOZ_ASSERT(NS_IsMainThread());
726 gZoomAnimationFunction = new StyleComputedTimingFunction(
727 StyleComputedTimingFunction::Keyword(StyleTimingKeyword::Ease));
728 ClearOnShutdown(&gZoomAnimationFunction);
729 gVelocityCurveFunction =
730 new StyleComputedTimingFunction(StyleComputedTimingFunction::CubicBezier(
731 StaticPrefs::apz_fling_curve_function_x1_AtStartup(),
732 StaticPrefs::apz_fling_curve_function_y1_AtStartup(),
733 StaticPrefs::apz_fling_curve_function_x2_AtStartup(),
734 StaticPrefs::apz_fling_curve_function_y2_AtStartup()));
735 ClearOnShutdown(&gVelocityCurveFunction);
737 uint64_t sysmem = PR_GetPhysicalMemorySize();
738 uint64_t threshold = 1LL << 32; // 4 GB in bytes
739 gIsHighMemSystem = sysmem >= threshold;
741 PlatformSpecificState::InitializeGlobalState();
744 AsyncPanZoomController::AsyncPanZoomController(
745 LayersId aLayersId, APZCTreeManager* aTreeManager,
746 const RefPtr<InputQueue>& aInputQueue,
747 GeckoContentController* aGeckoContentController, GestureBehavior aGestures)
748 : mLayersId(aLayersId),
749 mGeckoContentController(aGeckoContentController),
750 mRefPtrMonitor("RefPtrMonitor"),
751 // mTreeManager must be initialized before GetFrameTime() is called
752 mTreeManager(aTreeManager),
753 mRecursiveMutex("AsyncPanZoomController"),
754 mLastContentPaintMetrics(mLastContentPaintMetadata.GetMetrics()),
755 mPanDirRestricted(false),
756 mPinchLocked(false),
757 mPinchEventBuffer(TimeDuration::FromMilliseconds(
758 StaticPrefs::apz_pinch_lock_buffer_max_age_AtStartup())),
759 mZoomConstraints(false, false,
760 mScrollMetadata.GetMetrics().GetDevPixelsPerCSSPixel() *
761 ViewportMinScale() / ParentLayerToScreenScale(1),
762 mScrollMetadata.GetMetrics().GetDevPixelsPerCSSPixel() *
763 ViewportMaxScale() / ParentLayerToScreenScale(1)),
764 mLastSampleTime(GetFrameTime()),
765 mLastCheckerboardReport(GetFrameTime()),
766 mOverscrollEffect(MakeUnique<OverscrollEffect>(*this)),
767 mState(NOTHING),
768 mX(this),
769 mY(this),
770 mNotificationBlockers(0),
771 mInputQueue(aInputQueue),
772 mPinchPaintTimerSet(false),
773 mDelayedTransformEnd(false),
774 mTestAttributeAppliers(0),
775 mTestHasAsyncKeyScrolled(false),
776 mCheckerboardEventLock("APZCBELock") {
777 if (aGestures == USE_GESTURE_DETECTOR) {
778 mGestureEventListener = new GestureEventListener(this);
780 // Put one default-constructed sampled state in the queue.
781 RecursiveMutexAutoLock lock(mRecursiveMutex);
782 mSampledState.emplace_back();
785 AsyncPanZoomController::~AsyncPanZoomController() { MOZ_ASSERT(IsDestroyed()); }
787 PlatformSpecificStateBase* AsyncPanZoomController::GetPlatformSpecificState() {
788 if (!mPlatformSpecificState) {
789 mPlatformSpecificState = MakeUnique<PlatformSpecificState>();
791 return mPlatformSpecificState.get();
794 already_AddRefed<GeckoContentController>
795 AsyncPanZoomController::GetGeckoContentController() const {
796 MonitorAutoLock lock(mRefPtrMonitor);
797 RefPtr<GeckoContentController> controller = mGeckoContentController;
798 return controller.forget();
801 already_AddRefed<GestureEventListener>
802 AsyncPanZoomController::GetGestureEventListener() const {
803 MonitorAutoLock lock(mRefPtrMonitor);
804 RefPtr<GestureEventListener> listener = mGestureEventListener;
805 return listener.forget();
808 const RefPtr<InputQueue>& AsyncPanZoomController::GetInputQueue() const {
809 return mInputQueue;
812 void AsyncPanZoomController::Destroy() {
813 AssertOnUpdaterThread();
815 CancelAnimation(CancelAnimationFlags::ScrollSnap);
817 { // scope the lock
818 MonitorAutoLock lock(mRefPtrMonitor);
819 mGeckoContentController = nullptr;
820 mGestureEventListener = nullptr;
822 mParent = nullptr;
823 mTreeManager = nullptr;
826 bool AsyncPanZoomController::IsDestroyed() const {
827 return mTreeManager == nullptr;
830 float AsyncPanZoomController::GetDPI() const {
831 if (APZCTreeManager* localPtr = mTreeManager) {
832 return localPtr->GetDPI();
834 // If this APZC has been destroyed then this value is not going to be
835 // used for anything that the user will end up seeing, so we can just
836 // return 0.
837 return 0.0;
840 ScreenCoord AsyncPanZoomController::GetTouchStartTolerance() const {
841 return (StaticPrefs::apz_touch_start_tolerance() * GetDPI());
844 ScreenCoord AsyncPanZoomController::GetTouchMoveTolerance() const {
845 return (StaticPrefs::apz_touch_move_tolerance() * GetDPI());
848 ScreenCoord AsyncPanZoomController::GetSecondTapTolerance() const {
849 return (StaticPrefs::apz_second_tap_tolerance() * GetDPI());
852 /* static */ AsyncPanZoomController::AxisLockMode
853 AsyncPanZoomController::GetAxisLockMode() {
854 return static_cast<AxisLockMode>(StaticPrefs::apz_axis_lock_mode());
857 bool AsyncPanZoomController::UsingStatefulAxisLock() const {
858 return (GetAxisLockMode() == STANDARD || GetAxisLockMode() == STICKY);
861 /* static */ AsyncPanZoomController::PinchLockMode
862 AsyncPanZoomController::GetPinchLockMode() {
863 return static_cast<PinchLockMode>(StaticPrefs::apz_pinch_lock_mode());
866 PointerEventsConsumableFlags AsyncPanZoomController::ArePointerEventsConsumable(
867 TouchBlockState* aBlock, const MultiTouchInput& aInput) {
868 uint32_t touchPoints = aInput.mTouches.Length();
869 if (touchPoints == 0) {
870 // Cant' do anything with zero touch points
871 return {false, false};
874 // This logic is simplified, erring on the side of returning true if we're
875 // not sure. It's safer to pretend that we can consume the event and then
876 // not be able to than vice-versa. But at the same time, we should try hard
877 // to return an accurate result, because returning true can trigger a
878 // pointercancel event to web content, which can break certain features
879 // that are using touch-action and handling the pointermove events.
881 // Note that in particular this function can return true if APZ is waiting on
882 // the main thread for touch-action information. In this scenario, the
883 // APZEventState::MainThreadAgreesEventsAreConsumableByAPZ() function tries
884 // to use the main-thread touch-action information to filter out false
885 // positives.
887 // We could probably enhance this logic to determine things like "we're
888 // not pannable, so we can only zoom in, and the zoom is already maxed
889 // out, so we're not zoomable either" but no need for that at this point.
891 bool pannableX = aBlock->GetOverscrollHandoffChain()->CanScrollInDirection(
892 this, ScrollDirection::eHorizontal);
893 bool touchActionAllowsX = aBlock->TouchActionAllowsPanningX();
894 bool pannableY = (aBlock->GetOverscrollHandoffChain()->CanScrollInDirection(
895 this, ScrollDirection::eVertical) ||
896 // In the case of the root APZC with any dynamic toolbar, it
897 // shoule be pannable if there is room moving the dynamic
898 // toolbar.
899 (IsRootContent() && CanVerticalScrollWithDynamicToolbar()));
900 bool touchActionAllowsY = aBlock->TouchActionAllowsPanningY();
902 bool pannable;
903 bool touchActionAllowsPanning;
905 Maybe<ScrollDirection> panDirection =
906 aBlock->GetBestGuessPanDirection(aInput);
907 if (panDirection == Some(ScrollDirection::eVertical)) {
908 pannable = pannableY;
909 touchActionAllowsPanning = touchActionAllowsY;
910 } else if (panDirection == Some(ScrollDirection::eHorizontal)) {
911 pannable = pannableX;
912 touchActionAllowsPanning = touchActionAllowsX;
913 } else {
914 // If we don't have a guessed pan direction, err on the side of returning
915 // true.
916 pannable = pannableX || pannableY;
917 touchActionAllowsPanning = touchActionAllowsX || touchActionAllowsY;
920 if (touchPoints == 1) {
921 return {pannable, touchActionAllowsPanning};
924 bool zoomable = ZoomConstraintsAllowZoom();
925 bool touchActionAllowsZoom = aBlock->TouchActionAllowsPinchZoom();
927 return {pannable || zoomable,
928 touchActionAllowsPanning || touchActionAllowsZoom};
931 nsEventStatus AsyncPanZoomController::HandleDragEvent(
932 const MouseInput& aEvent, const AsyncDragMetrics& aDragMetrics,
933 OuterCSSCoord aInitialThumbPos, const CSSRect& aInitialScrollableRect) {
934 // RDM is a special case where touch events will be synthesized in response
935 // to mouse events, and APZ will receive both even though RDM prevent-defaults
936 // the mouse events. This is because mouse events don't opt into APZ waiting
937 // to check if the event has been prevent-defaulted and are still processed
938 // as a result. To handle this, have APZ ignore mouse events when RDM and
939 // touch simulation are active.
940 bool isRDMTouchSimulationActive = false;
942 RecursiveMutexAutoLock lock(mRecursiveMutex);
943 isRDMTouchSimulationActive =
944 mScrollMetadata.GetIsRDMTouchSimulationActive();
947 if (!StaticPrefs::apz_drag_enabled() || isRDMTouchSimulationActive) {
948 return nsEventStatus_eIgnore;
951 if (!GetApzcTreeManager()) {
952 return nsEventStatus_eConsumeNoDefault;
956 RecursiveMutexAutoLock lock(mRecursiveMutex);
958 if (aEvent.mType == MouseInput::MouseType::MOUSE_UP) {
959 if (mState == SCROLLBAR_DRAG) {
960 APZC_LOG("%p ending drag\n", this);
961 SetState(NOTHING);
964 SnapBackIfOverscrolled();
966 return nsEventStatus_eConsumeNoDefault;
970 HitTestingTreeNodeAutoLock node;
971 GetApzcTreeManager()->FindScrollThumbNode(aDragMetrics, mLayersId, node);
972 if (!node) {
973 APZC_LOG("%p unable to find scrollthumb node with viewid %" PRIu64 "\n",
974 this, aDragMetrics.mViewId);
975 return nsEventStatus_eConsumeNoDefault;
978 if (aEvent.mType == MouseInput::MouseType::MOUSE_DOWN) {
979 APZC_LOG("%p starting scrollbar drag\n", this);
980 SetState(SCROLLBAR_DRAG);
983 if (aEvent.mType != MouseInput::MouseType::MOUSE_MOVE) {
984 APZC_LOG("%p discarding event of type %d\n", this, aEvent.mType);
985 return nsEventStatus_eConsumeNoDefault;
988 const ScrollbarData& scrollbarData = node->GetScrollbarData();
989 MOZ_ASSERT(scrollbarData.mScrollbarLayerType ==
990 layers::ScrollbarLayerType::Thumb);
991 MOZ_ASSERT(scrollbarData.mDirection.isSome());
992 ScrollDirection direction = *scrollbarData.mDirection;
994 bool isMouseAwayFromThumb = false;
995 if (int snapMultiplier = StaticPrefs::slider_snapMultiplier()) {
996 // It's fine to ignore the async component of the thumb's transform,
997 // because any async transform of the thumb will be in the direction of
998 // scrolling, but here we're interested in the other direction.
999 ParentLayerRect thumbRect =
1000 (node->GetTransform() * AsyncTransformMatrix())
1001 .TransformBounds(LayerRect(node->GetVisibleRegion().GetBounds()));
1002 ScrollDirection otherDirection = GetPerpendicularDirection(direction);
1003 ParentLayerCoord distance =
1004 GetAxisStart(otherDirection, thumbRect.DistanceTo(aEvent.mLocalOrigin));
1005 ParentLayerCoord thumbWidth = GetAxisLength(otherDirection, thumbRect);
1006 // Avoid triggering this condition spuriously when the thumb is
1007 // offscreen and its visible region is therefore empty.
1008 if (thumbWidth > 0 && thumbWidth * snapMultiplier < distance) {
1009 isMouseAwayFromThumb = true;
1010 APZC_LOG("%p determined mouse is away from thumb, will snap\n", this);
1014 RecursiveMutexAutoLock lock(mRecursiveMutex);
1015 OuterCSSCoord thumbPosition;
1016 if (isMouseAwayFromThumb) {
1017 thumbPosition = aInitialThumbPos;
1018 } else {
1019 thumbPosition = ConvertScrollbarPoint(aEvent.mLocalOrigin, scrollbarData) -
1020 aDragMetrics.mScrollbarDragOffset;
1023 OuterCSSCoord maxThumbPos = scrollbarData.mScrollTrackLength;
1024 maxThumbPos -= scrollbarData.mThumbLength;
1026 float scrollPercent =
1027 maxThumbPos.value == 0.0f ? 0.0f : (float)(thumbPosition / maxThumbPos);
1028 APZC_LOG("%p scrollbar dragged to %f percent\n", this, scrollPercent);
1030 CSSCoord minScrollPosition =
1031 GetAxisStart(direction, aInitialScrollableRect.TopLeft());
1032 CSSCoord maxScrollPosition =
1033 GetAxisStart(direction, aInitialScrollableRect.BottomRight()) -
1034 GetAxisLength(direction, Metrics().CalculateCompositedSizeInCssPixels());
1035 CSSCoord scrollPosition =
1036 minScrollPosition +
1037 (scrollPercent * (maxScrollPosition - minScrollPosition));
1039 scrollPosition = std::max(scrollPosition, minScrollPosition);
1040 scrollPosition = std::min(scrollPosition, maxScrollPosition);
1042 CSSPoint scrollOffset = Metrics().GetVisualScrollOffset();
1043 if (direction == ScrollDirection::eHorizontal) {
1044 scrollOffset.x = scrollPosition;
1045 } else {
1046 scrollOffset.y = scrollPosition;
1048 APZC_LOG("%p set scroll offset to %s from scrollbar drag\n", this,
1049 ToString(scrollOffset).c_str());
1050 SetVisualScrollOffset(scrollOffset);
1051 ScheduleCompositeAndMaybeRepaint();
1053 return nsEventStatus_eConsumeNoDefault;
1056 nsEventStatus AsyncPanZoomController::HandleInputEvent(
1057 const InputData& aEvent,
1058 const ScreenToParentLayerMatrix4x4& aTransformToApzc) {
1059 APZThreadUtils::AssertOnControllerThread();
1061 nsEventStatus rv = nsEventStatus_eIgnore;
1063 switch (aEvent.mInputType) {
1064 case MULTITOUCH_INPUT: {
1065 MultiTouchInput multiTouchInput = aEvent.AsMultiTouchInput();
1066 RefPtr<GestureEventListener> listener = GetGestureEventListener();
1067 if (listener) {
1068 // We only care about screen coordinates in the gesture listener,
1069 // so we don't bother transforming the event to parent layer coordinates
1070 rv = listener->HandleInputEvent(multiTouchInput);
1071 if (rv == nsEventStatus_eConsumeNoDefault) {
1072 return rv;
1076 if (!multiTouchInput.TransformToLocal(aTransformToApzc)) {
1077 return rv;
1080 switch (multiTouchInput.mType) {
1081 case MultiTouchInput::MULTITOUCH_START:
1082 rv = OnTouchStart(multiTouchInput);
1083 break;
1084 case MultiTouchInput::MULTITOUCH_MOVE:
1085 rv = OnTouchMove(multiTouchInput);
1086 break;
1087 case MultiTouchInput::MULTITOUCH_END:
1088 rv = OnTouchEnd(multiTouchInput);
1089 break;
1090 case MultiTouchInput::MULTITOUCH_CANCEL:
1091 rv = OnTouchCancel(multiTouchInput);
1092 break;
1094 break;
1096 case PANGESTURE_INPUT: {
1097 PanGestureInput panGestureInput = aEvent.AsPanGestureInput();
1098 if (!panGestureInput.TransformToLocal(aTransformToApzc)) {
1099 return rv;
1102 switch (panGestureInput.mType) {
1103 case PanGestureInput::PANGESTURE_MAYSTART:
1104 rv = OnPanMayBegin(panGestureInput);
1105 break;
1106 case PanGestureInput::PANGESTURE_CANCELLED:
1107 rv = OnPanCancelled(panGestureInput);
1108 break;
1109 case PanGestureInput::PANGESTURE_START:
1110 rv = OnPanBegin(panGestureInput);
1111 break;
1112 case PanGestureInput::PANGESTURE_PAN:
1113 rv = OnPan(panGestureInput, FingersOnTouchpad::Yes);
1114 break;
1115 case PanGestureInput::PANGESTURE_END:
1116 rv = OnPanEnd(panGestureInput);
1117 break;
1118 case PanGestureInput::PANGESTURE_MOMENTUMSTART:
1119 rv = OnPanMomentumStart(panGestureInput);
1120 break;
1121 case PanGestureInput::PANGESTURE_MOMENTUMPAN:
1122 rv = OnPan(panGestureInput, FingersOnTouchpad::No);
1123 break;
1124 case PanGestureInput::PANGESTURE_MOMENTUMEND:
1125 rv = OnPanMomentumEnd(panGestureInput);
1126 break;
1127 case PanGestureInput::PANGESTURE_INTERRUPTED:
1128 rv = OnPanInterrupted(panGestureInput);
1129 break;
1131 break;
1133 case MOUSE_INPUT: {
1134 MouseInput mouseInput = aEvent.AsMouseInput();
1135 if (!mouseInput.TransformToLocal(aTransformToApzc)) {
1136 return rv;
1138 break;
1140 case SCROLLWHEEL_INPUT: {
1141 ScrollWheelInput scrollInput = aEvent.AsScrollWheelInput();
1142 if (!scrollInput.TransformToLocal(aTransformToApzc)) {
1143 return rv;
1146 rv = OnScrollWheel(scrollInput);
1147 break;
1149 case PINCHGESTURE_INPUT: {
1150 // The APZCTreeManager should take care of ensuring that only root-content
1151 // APZCs get pinch inputs.
1152 MOZ_ASSERT(IsRootContent());
1153 PinchGestureInput pinchInput = aEvent.AsPinchGestureInput();
1154 if (!pinchInput.TransformToLocal(aTransformToApzc)) {
1155 return rv;
1158 rv = HandleGestureEvent(pinchInput);
1159 break;
1161 case TAPGESTURE_INPUT: {
1162 TapGestureInput tapInput = aEvent.AsTapGestureInput();
1163 if (!tapInput.TransformToLocal(aTransformToApzc)) {
1164 return rv;
1167 rv = HandleGestureEvent(tapInput);
1168 break;
1170 case KEYBOARD_INPUT: {
1171 const KeyboardInput& keyInput = aEvent.AsKeyboardInput();
1172 rv = OnKeyboard(keyInput);
1173 break;
1177 return rv;
1180 nsEventStatus AsyncPanZoomController::HandleGestureEvent(
1181 const InputData& aEvent) {
1182 APZThreadUtils::AssertOnControllerThread();
1184 nsEventStatus rv = nsEventStatus_eIgnore;
1186 switch (aEvent.mInputType) {
1187 case PINCHGESTURE_INPUT: {
1188 // This may be invoked via a one-touch-pinch gesture from
1189 // GestureEventListener. In that case we want redirect it to the enclosing
1190 // root-content APZC.
1191 if (!IsRootContent()) {
1192 if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
1193 if (RefPtr<AsyncPanZoomController> root =
1194 treeManagerLocal->FindZoomableApzc(this)) {
1195 rv = root->HandleGestureEvent(aEvent);
1198 break;
1200 PinchGestureInput pinchGestureInput = aEvent.AsPinchGestureInput();
1201 pinchGestureInput.TransformToLocal(GetTransformToThis());
1202 switch (pinchGestureInput.mType) {
1203 case PinchGestureInput::PINCHGESTURE_START:
1204 rv = OnScaleBegin(pinchGestureInput);
1205 break;
1206 case PinchGestureInput::PINCHGESTURE_SCALE:
1207 rv = OnScale(pinchGestureInput);
1208 break;
1209 case PinchGestureInput::PINCHGESTURE_FINGERLIFTED:
1210 case PinchGestureInput::PINCHGESTURE_END:
1211 rv = OnScaleEnd(pinchGestureInput);
1212 break;
1214 break;
1216 case TAPGESTURE_INPUT: {
1217 TapGestureInput tapGestureInput = aEvent.AsTapGestureInput();
1218 tapGestureInput.TransformToLocal(GetTransformToThis());
1219 switch (tapGestureInput.mType) {
1220 case TapGestureInput::TAPGESTURE_LONG:
1221 rv = OnLongPress(tapGestureInput);
1222 break;
1223 case TapGestureInput::TAPGESTURE_LONG_UP:
1224 rv = OnLongPressUp(tapGestureInput);
1225 break;
1226 case TapGestureInput::TAPGESTURE_UP:
1227 rv = OnSingleTapUp(tapGestureInput);
1228 break;
1229 case TapGestureInput::TAPGESTURE_CONFIRMED:
1230 rv = OnSingleTapConfirmed(tapGestureInput);
1231 break;
1232 case TapGestureInput::TAPGESTURE_DOUBLE:
1233 if (!IsRootContent()) {
1234 if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
1235 if (AsyncPanZoomController* apzc =
1236 treeManagerLocal->FindRootApzcFor(GetLayersId())) {
1237 rv = apzc->OnDoubleTap(tapGestureInput);
1240 break;
1242 rv = OnDoubleTap(tapGestureInput);
1243 break;
1244 case TapGestureInput::TAPGESTURE_SECOND:
1245 rv = OnSecondTap(tapGestureInput);
1246 break;
1247 case TapGestureInput::TAPGESTURE_CANCEL:
1248 rv = OnCancelTap(tapGestureInput);
1249 break;
1251 break;
1253 default:
1254 MOZ_ASSERT_UNREACHABLE("Unhandled input event");
1255 break;
1258 return rv;
1261 void AsyncPanZoomController::StartAutoscroll(const ScreenPoint& aPoint) {
1262 // Cancel any existing animation.
1263 CancelAnimation();
1265 SetState(AUTOSCROLL);
1266 StartAnimation(do_AddRef(new AutoscrollAnimation(*this, aPoint)));
1269 void AsyncPanZoomController::StopAutoscroll() {
1270 if (mState == AUTOSCROLL) {
1271 CancelAnimation(TriggeredExternally);
1275 nsEventStatus AsyncPanZoomController::OnTouchStart(
1276 const MultiTouchInput& aEvent) {
1277 APZC_LOG_DETAIL("got a touch-start in state %s\n", this,
1278 ToString(mState).c_str());
1279 mPanDirRestricted = false;
1281 switch (mState) {
1282 case FLING:
1283 case ANIMATING_ZOOM:
1284 case SMOOTH_SCROLL:
1285 case SMOOTHMSD_SCROLL:
1286 case OVERSCROLL_ANIMATION:
1287 case WHEEL_SCROLL:
1288 case KEYBOARD_SCROLL:
1289 case PAN_MOMENTUM:
1290 case AUTOSCROLL:
1291 MOZ_ASSERT(GetCurrentTouchBlock());
1292 GetCurrentTouchBlock()->GetOverscrollHandoffChain()->CancelAnimations(
1293 ExcludeOverscroll);
1294 [[fallthrough]];
1295 case SCROLLBAR_DRAG:
1296 case NOTHING: {
1297 ParentLayerPoint point = GetFirstTouchPoint(aEvent);
1298 mLastTouch.mPosition = mStartTouch = GetFirstExternalTouchPoint(aEvent);
1299 StartTouch(point, aEvent.mTimeStamp);
1300 if (RefPtr<GeckoContentController> controller =
1301 GetGeckoContentController()) {
1302 MOZ_ASSERT(GetCurrentTouchBlock());
1303 controller->NotifyAPZStateChange(
1304 GetGuid(), APZStateChange::eStartTouch,
1305 GetCurrentTouchBlock()->GetOverscrollHandoffChain()->CanBePanned(
1306 this),
1307 Some(GetCurrentTouchBlock()->GetBlockId()));
1309 mLastTouch.mTimeStamp = mTouchStartTime = aEvent.mTimeStamp;
1310 SetState(TOUCHING);
1311 break;
1313 case TOUCHING:
1314 case PANNING:
1315 case PANNING_LOCKED_X:
1316 case PANNING_LOCKED_Y:
1317 case PINCHING:
1318 NS_WARNING("Received impossible touch in OnTouchStart");
1319 break;
1322 return nsEventStatus_eConsumeNoDefault;
1325 nsEventStatus AsyncPanZoomController::OnTouchMove(
1326 const MultiTouchInput& aEvent) {
1327 APZC_LOG_DETAIL("got a touch-move in state %s\n", this,
1328 ToString(mState).c_str());
1329 switch (mState) {
1330 case FLING:
1331 case SMOOTHMSD_SCROLL:
1332 case NOTHING:
1333 case ANIMATING_ZOOM:
1334 // May happen if the user double-taps and drags without lifting after the
1335 // second tap. Ignore the move if this happens.
1336 return nsEventStatus_eIgnore;
1338 case TOUCHING: {
1339 ScreenCoord panThreshold = GetTouchStartTolerance();
1340 ExternalPoint extPoint = GetFirstExternalTouchPoint(aEvent);
1341 Maybe<std::pair<MultiTouchInput, MultiTouchInput>> splitEvent;
1343 // We intentionally skip the UpdateWithTouchAtDevicePoint call when the
1344 // panThreshold is zero. This ensures more deterministic behaviour during
1345 // testing. If we call that, Axis::mPos gets updated to the point of this
1346 // touchmove event, but we "consume" the move to overcome the
1347 // panThreshold, so it's hard to pan a specific amount reliably from a
1348 // mochitest.
1349 if (panThreshold > 0.0f) {
1350 const float vectorLength = PanVector(extPoint).Length();
1352 if (vectorLength < panThreshold) {
1353 UpdateWithTouchAtDevicePoint(aEvent);
1354 mLastTouch = {extPoint, aEvent.mTimeStamp};
1356 return nsEventStatus_eIgnore;
1359 splitEvent = MaybeSplitTouchMoveEvent(aEvent, panThreshold,
1360 vectorLength, extPoint);
1362 UpdateWithTouchAtDevicePoint(splitEvent ? splitEvent->first : aEvent);
1365 nsEventStatus result;
1366 const MultiTouchInput& firstEvent =
1367 splitEvent ? splitEvent->first : aEvent;
1369 MOZ_ASSERT(GetCurrentTouchBlock());
1370 if (GetCurrentTouchBlock()->TouchActionAllowsPanningXY()) {
1371 // In the calls to StartPanning() below, the first argument needs to be
1372 // the External position of |firstEvent|.
1373 // However, instead of computing that using
1374 // GetFirstExternalTouchPoint(firstEvent), we pass |extPoint| which
1375 // has been modified by MaybeSplitTouchMoveEvent() to the desired
1376 // value. This is a workaround for the fact that recomputing the
1377 // External point would require a round-trip through |mScreenPoint|
1378 // which is an integer.
1380 // User tries to trigger a touch behavior. If allowed touch behavior is
1381 // vertical pan + horizontal pan (touch-action value is equal to AUTO)
1382 // we can return ConsumeNoDefault status immediately to trigger cancel
1383 // event further.
1384 // It should happen independent of the parent type (whether it is
1385 // scrolling or not).
1386 StartPanning(extPoint, firstEvent.mTimeStamp);
1387 result = nsEventStatus_eConsumeNoDefault;
1388 } else {
1389 result = StartPanning(extPoint, firstEvent.mTimeStamp);
1392 if (splitEvent && IsInPanningState()) {
1393 TrackTouch(splitEvent->second);
1394 return nsEventStatus_eConsumeNoDefault;
1397 return result;
1400 case PANNING:
1401 case PANNING_LOCKED_X:
1402 case PANNING_LOCKED_Y:
1403 case PAN_MOMENTUM:
1404 TrackTouch(aEvent);
1405 return nsEventStatus_eConsumeNoDefault;
1407 case PINCHING:
1408 // The scale gesture listener should have handled this.
1409 NS_WARNING(
1410 "Gesture listener should have handled pinching in OnTouchMove.");
1411 return nsEventStatus_eIgnore;
1413 case SMOOTH_SCROLL:
1414 case WHEEL_SCROLL:
1415 case KEYBOARD_SCROLL:
1416 case OVERSCROLL_ANIMATION:
1417 case AUTOSCROLL:
1418 case SCROLLBAR_DRAG:
1419 // Should not receive a touch-move in the OVERSCROLL_ANIMATION state
1420 // as touch blocks that begin in an overscrolled state cancel the
1421 // animation. The same is true for wheel scroll animations.
1422 NS_WARNING("Received impossible touch in OnTouchMove");
1423 break;
1426 return nsEventStatus_eConsumeNoDefault;
1429 nsEventStatus AsyncPanZoomController::OnTouchEnd(
1430 const MultiTouchInput& aEvent) {
1431 APZC_LOG_DETAIL("got a touch-end in state %s\n", this,
1432 ToString(mState).c_str());
1433 OnTouchEndOrCancel();
1435 // In case no touch behavior triggered previously we can avoid sending
1436 // scroll events or requesting content repaint. This condition is added
1437 // to make tests consistent - in case touch-action is NONE (and therefore
1438 // no pans/zooms can be performed) we expected neither scroll or repaint
1439 // events.
1440 if (mState != NOTHING) {
1441 RecursiveMutexAutoLock lock(mRecursiveMutex);
1444 switch (mState) {
1445 case FLING:
1446 // Should never happen.
1447 NS_WARNING("Received impossible touch end in OnTouchEnd.");
1448 [[fallthrough]];
1449 case ANIMATING_ZOOM:
1450 case SMOOTHMSD_SCROLL:
1451 case NOTHING:
1452 // May happen if the user double-taps and drags without lifting after the
1453 // second tap. Ignore if this happens.
1454 return nsEventStatus_eIgnore;
1456 case TOUCHING:
1457 // We may have some velocity stored on the axis from move events
1458 // that were not big enough to trigger scrolling. Clear that out.
1459 SetVelocityVector(ParentLayerPoint(0, 0));
1460 MOZ_ASSERT(GetCurrentTouchBlock());
1461 APZC_LOG("%p still has %u touch points active\n", this,
1462 GetCurrentTouchBlock()->GetActiveTouchCount());
1463 // In cases where the user is panning, then taps the second finger without
1464 // entering a pinch, we will arrive here when the second finger is lifted.
1465 // However the first finger is still down so we want to remain in state
1466 // TOUCHING.
1467 if (GetCurrentTouchBlock()->GetActiveTouchCount() == 0) {
1468 // It's possible we may be overscrolled if the user tapped during a
1469 // previous overscroll pan. Make sure to snap back in this situation.
1470 // An ancestor APZC could be overscrolled instead of this APZC, so
1471 // walk the handoff chain as well.
1472 GetCurrentTouchBlock()
1473 ->GetOverscrollHandoffChain()
1474 ->SnapBackOverscrolledApzc(this);
1475 mFlingAccelerator.Reset();
1476 // SnapBackOverscrolledApzc() will put any APZC it causes to snap back
1477 // into the OVERSCROLL_ANIMATION state. If that's not us, since we're
1478 // done TOUCHING enter the NOTHING state.
1479 if (mState != OVERSCROLL_ANIMATION) {
1480 SetState(NOTHING);
1483 return nsEventStatus_eIgnore;
1485 case PANNING:
1486 case PANNING_LOCKED_X:
1487 case PANNING_LOCKED_Y:
1488 case PAN_MOMENTUM: {
1489 MOZ_ASSERT(GetCurrentTouchBlock());
1490 EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::Yes);
1491 return HandleEndOfPan();
1493 case PINCHING:
1494 SetState(NOTHING);
1495 // Scale gesture listener should have handled this.
1496 NS_WARNING(
1497 "Gesture listener should have handled pinching in OnTouchEnd.");
1498 return nsEventStatus_eIgnore;
1500 case SMOOTH_SCROLL:
1501 case WHEEL_SCROLL:
1502 case KEYBOARD_SCROLL:
1503 case OVERSCROLL_ANIMATION:
1504 case AUTOSCROLL:
1505 case SCROLLBAR_DRAG:
1506 // Should not receive a touch-end in the OVERSCROLL_ANIMATION state
1507 // as touch blocks that begin in an overscrolled state cancel the
1508 // animation. The same is true for WHEEL_SCROLL.
1509 NS_WARNING("Received impossible touch in OnTouchEnd");
1510 break;
1513 return nsEventStatus_eConsumeNoDefault;
1516 nsEventStatus AsyncPanZoomController::OnTouchCancel(
1517 const MultiTouchInput& aEvent) {
1518 APZC_LOG_DETAIL("got a touch-cancel in state %s\n", this,
1519 ToString(mState).c_str());
1520 OnTouchEndOrCancel();
1521 CancelAnimationAndGestureState();
1522 return nsEventStatus_eConsumeNoDefault;
1525 nsEventStatus AsyncPanZoomController::OnScaleBegin(
1526 const PinchGestureInput& aEvent) {
1527 APZC_LOG_DETAIL("got a scale-begin in state %s\n", this,
1528 ToString(mState).c_str());
1530 mPinchLocked = false;
1531 mPinchPaintTimerSet = false;
1532 // Note that there may not be a touch block at this point, if we received the
1533 // PinchGestureEvent directly from widget code without any touch events.
1534 if (HasReadyTouchBlock() &&
1535 !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
1536 return nsEventStatus_eIgnore;
1539 // For platforms that don't support APZ zooming, dispatch a message to the
1540 // content controller, it may want to do something else with this gesture.
1541 // FIXME: bug 1525793 -- this may need to handle zooming or not on a
1542 // per-document basis.
1543 if (!StaticPrefs::apz_allow_zooming()) {
1544 if (RefPtr<GeckoContentController> controller =
1545 GetGeckoContentController()) {
1546 APZC_LOG("%p notifying controller of pinch gesture start\n", this);
1547 controller->NotifyPinchGesture(
1548 aEvent.mType, GetGuid(),
1549 ViewAs<LayoutDevicePixel>(
1550 aEvent.mFocusPoint,
1551 PixelCastJustification::
1552 LayoutDeviceIsScreenForUntransformedEvent),
1553 0, aEvent.modifiers);
1557 SetState(PINCHING);
1558 Telemetry::Accumulate(Telemetry::APZ_ZOOM_PINCHSOURCE, (int)aEvent.mSource);
1559 SetVelocityVector(ParentLayerPoint(0, 0));
1560 RecursiveMutexAutoLock lock(mRecursiveMutex);
1561 mLastZoomFocus =
1562 aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
1564 mPinchEventBuffer.push(aEvent);
1566 return nsEventStatus_eConsumeNoDefault;
1569 nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) {
1570 APZC_LOG_DETAIL("got a scale in state %s\n", this, ToString(mState).c_str());
1572 if (HasReadyTouchBlock() &&
1573 !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
1574 return nsEventStatus_eIgnore;
1577 if (mState != PINCHING) {
1578 return nsEventStatus_eConsumeNoDefault;
1581 mPinchEventBuffer.push(aEvent);
1582 HandlePinchLocking(aEvent);
1583 bool allowZoom = ZoomConstraintsAllowZoom() && !mPinchLocked;
1585 // If we are pinch-locked, this is a two-finger pan.
1586 // Tracking panning distance and velocity.
1587 // UpdateWithTouchAtDevicePoint() acquires the tree lock, so
1588 // it cannot be called while the mRecursiveMutex lock is held.
1589 if (mPinchLocked) {
1590 mX.UpdateWithTouchAtDevicePoint(aEvent.mLocalFocusPoint.x,
1591 aEvent.mTimeStamp);
1592 mY.UpdateWithTouchAtDevicePoint(aEvent.mLocalFocusPoint.y,
1593 aEvent.mTimeStamp);
1596 // FIXME: bug 1525793 -- this may need to handle zooming or not on a
1597 // per-document basis.
1598 if (!StaticPrefs::apz_allow_zooming()) {
1599 if (RefPtr<GeckoContentController> controller =
1600 GetGeckoContentController()) {
1601 APZC_LOG("%p notifying controller of pinch gesture\n", this);
1602 controller->NotifyPinchGesture(
1603 aEvent.mType, GetGuid(),
1604 ViewAs<LayoutDevicePixel>(
1605 aEvent.mFocusPoint,
1606 PixelCastJustification::
1607 LayoutDeviceIsScreenForUntransformedEvent),
1608 ViewAs<LayoutDevicePixel>(
1609 aEvent.mCurrentSpan - aEvent.mPreviousSpan,
1610 PixelCastJustification::
1611 LayoutDeviceIsScreenForUntransformedEvent),
1612 aEvent.modifiers);
1617 RecursiveMutexAutoLock lock(mRecursiveMutex);
1618 // Only the root APZC is zoomable, and the root APZC is not allowed to have
1619 // different x and y scales. If it did, the calculations in this function
1620 // would have to be adjusted (as e.g. it would no longer be valid to take
1621 // the minimum or maximum of the ratios of the widths and heights of the
1622 // page rect and the composition bounds).
1623 MOZ_ASSERT(Metrics().IsRootContent());
1625 CSSToParentLayerScale userZoom = Metrics().GetZoom();
1626 ParentLayerPoint focusPoint =
1627 aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
1628 CSSPoint cssFocusPoint;
1629 if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
1630 cssFocusPoint = focusPoint / Metrics().GetZoom();
1633 ParentLayerPoint focusChange = mLastZoomFocus - focusPoint;
1634 mLastZoomFocus = focusPoint;
1635 // If displacing by the change in focus point will take us off page bounds,
1636 // then reduce the displacement such that it doesn't.
1637 focusChange.x -= mX.DisplacementWillOverscrollAmount(focusChange.x);
1638 focusChange.y -= mY.DisplacementWillOverscrollAmount(focusChange.y);
1639 if (userZoom != CSSToParentLayerScale(0)) {
1640 ScrollBy(focusChange / userZoom);
1643 // If the span is zero or close to it, we don't want to process this zoom
1644 // change because we're going to get wonky numbers for the spanRatio. So
1645 // let's bail out here. Note that we do this after the focus-change-scroll
1646 // above, so that if we have a pinch with zero span but changing focus,
1647 // such as generated by some Synaptics touchpads on Windows, we still
1648 // scroll properly.
1649 float prevSpan = aEvent.mPreviousSpan;
1650 if (fabsf(prevSpan) <= EPSILON || fabsf(aEvent.mCurrentSpan) <= EPSILON) {
1651 // We might have done a nonzero ScrollBy above, so update metrics and
1652 // repaint/recomposite
1653 ScheduleCompositeAndMaybeRepaint();
1654 return nsEventStatus_eConsumeNoDefault;
1656 float spanRatio = aEvent.mCurrentSpan / aEvent.mPreviousSpan;
1658 // When we zoom in with focus, we can zoom too much towards the boundaries
1659 // that we actually go over them. These are the needed displacements along
1660 // either axis such that we don't overscroll the boundaries when zooming.
1661 CSSPoint neededDisplacement;
1663 CSSToParentLayerScale realMinZoom = mZoomConstraints.mMinZoom;
1664 CSSToParentLayerScale realMaxZoom = mZoomConstraints.mMaxZoom;
1665 realMinZoom.scale =
1666 std::max(realMinZoom.scale, Metrics().GetCompositionBounds().Width() /
1667 Metrics().GetScrollableRect().Width());
1668 realMinZoom.scale =
1669 std::max(realMinZoom.scale, Metrics().GetCompositionBounds().Height() /
1670 Metrics().GetScrollableRect().Height());
1671 if (realMaxZoom < realMinZoom) {
1672 realMaxZoom = realMinZoom;
1675 bool doScale = allowZoom && ((spanRatio > 1.0 && userZoom < realMaxZoom) ||
1676 (spanRatio < 1.0 && userZoom > realMinZoom));
1678 if (doScale) {
1679 spanRatio = clamped(spanRatio, realMinZoom.scale / userZoom.scale,
1680 realMaxZoom.scale / userZoom.scale);
1682 // Note that the spanRatio here should never put us into OVERSCROLL_BOTH
1683 // because up above we clamped it.
1684 neededDisplacement.x =
1685 -mX.ScaleWillOverscrollAmount(spanRatio, cssFocusPoint.x);
1686 neededDisplacement.y =
1687 -mY.ScaleWillOverscrollAmount(spanRatio, cssFocusPoint.y);
1689 ScaleWithFocus(spanRatio, cssFocusPoint);
1691 if (neededDisplacement != CSSPoint()) {
1692 ScrollBy(neededDisplacement);
1695 // We don't want to redraw on every scale, so throttle it.
1696 if (!mPinchPaintTimerSet) {
1697 const int delay = StaticPrefs::apz_scale_repaint_delay_ms();
1698 if (delay >= 0) {
1699 if (RefPtr<GeckoContentController> controller =
1700 GetGeckoContentController()) {
1701 mPinchPaintTimerSet = true;
1702 controller->PostDelayedTask(
1703 NewRunnableMethod(
1704 "layers::AsyncPanZoomController::"
1705 "DoDelayedRequestContentRepaint",
1706 this,
1707 &AsyncPanZoomController::DoDelayedRequestContentRepaint),
1708 delay);
1711 } else if (apz::AboutToCheckerboard(mLastContentPaintMetrics,
1712 Metrics())) {
1713 // If we already scheduled a throttled repaint request but are also
1714 // in danger of checkerboarding soon, trigger the repaint request to
1715 // go out immediately. This should reduce the amount of time we spend
1716 // checkerboarding.
1718 // Note that if we remain in this "about to
1719 // checkerboard" state over a period of time with multiple pinch input
1720 // events (which is quite likely), then we will flip-flop between taking
1721 // the above branch (!mPinchPaintTimerSet) and this branch (which will
1722 // flush the repaint request and reset mPinchPaintTimerSet to false).
1723 // This is sort of desirable because it halves the number of repaint
1724 // requests we send, and therefore reduces IPC traffic.
1725 // Keep in mind that many of these repaint requests will be ignored on
1726 // the main-thread anyway due to the resolution mismatch - the first
1727 // repaint request will be honored because APZ's notion of the painted
1728 // resolution matches the actual main thread resolution, but that first
1729 // repaint request will change the resolution on the main thread.
1730 // Subsequent repaint requests will be ignored in APZCCallbackHelper, at
1731 // https://searchfox.org/mozilla-central/rev/e0eb861a187f0bb6d994228f2e0e49b2c9ee455e/gfx/layers/apz/util/APZCCallbackHelper.cpp#331-338,
1732 // until we receive a NotifyLayersUpdated call that re-syncs APZ's
1733 // notion of the painted resolution to the main thread. These ignored
1734 // repaint requests are contributing to IPC traffic needlessly, and so
1735 // halving the number of repaint requests (as mentioned above) seems
1736 // desirable.
1737 DoDelayedRequestContentRepaint();
1739 } else {
1740 // Trigger a repaint request after scrolling.
1741 RequestContentRepaint();
1744 // We did a ScrollBy call above even if we didn't do a scale, so we
1745 // should composite for that.
1746 ScheduleComposite();
1749 return nsEventStatus_eConsumeNoDefault;
1752 nsEventStatus AsyncPanZoomController::OnScaleEnd(
1753 const PinchGestureInput& aEvent) {
1754 APZC_LOG_DETAIL("got a scale-end in state %s\n", this,
1755 ToString(mState).c_str());
1757 mPinchPaintTimerSet = false;
1759 if (HasReadyTouchBlock() &&
1760 !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
1761 return nsEventStatus_eIgnore;
1764 // FIXME: bug 1525793 -- this may need to handle zooming or not on a
1765 // per-document basis.
1766 if (!StaticPrefs::apz_allow_zooming()) {
1767 if (RefPtr<GeckoContentController> controller =
1768 GetGeckoContentController()) {
1769 controller->NotifyPinchGesture(
1770 aEvent.mType, GetGuid(),
1771 ViewAs<LayoutDevicePixel>(
1772 aEvent.mFocusPoint,
1773 PixelCastJustification::
1774 LayoutDeviceIsScreenForUntransformedEvent),
1775 0, aEvent.modifiers);
1780 RecursiveMutexAutoLock lock(mRecursiveMutex);
1781 ScheduleComposite();
1782 RequestContentRepaint();
1785 mPinchEventBuffer.clear();
1787 if (aEvent.mType == PinchGestureInput::PINCHGESTURE_FINGERLIFTED) {
1788 // One finger is still down, so transition to a TOUCHING state
1789 if (!mPinchLocked) {
1790 mPanDirRestricted = false;
1791 mLastTouch.mPosition = mStartTouch =
1792 ToExternalPoint(aEvent.mScreenOffset, aEvent.mFocusPoint);
1793 mLastTouch.mTimeStamp = mTouchStartTime = aEvent.mTimeStamp;
1794 StartTouch(aEvent.mLocalFocusPoint, aEvent.mTimeStamp);
1795 SetState(TOUCHING);
1796 } else {
1797 // If we are pinch locked, StartTouch() was already called
1798 // when we entered the pinch lock.
1799 StartPanning(ToExternalPoint(aEvent.mScreenOffset, aEvent.mFocusPoint),
1800 aEvent.mTimeStamp);
1802 } else {
1803 // Otherwise, handle the gesture being completely done.
1805 // Some of the code paths below, like ScrollSnap() or HandleEndOfPan(),
1806 // may start an animation, but otherwise we want to end up in the NOTHING
1807 // state. To avoid state change notification churn, we use a
1808 // notification blocker.
1809 bool stateWasPinching = (mState == PINCHING);
1810 StateChangeNotificationBlocker blocker(this);
1811 SetState(NOTHING);
1813 if (ZoomConstraintsAllowZoom()) {
1814 RecursiveMutexAutoLock lock(mRecursiveMutex);
1816 // We can get into a situation where we are overscrolled at the end of a
1817 // pinch if we go into overscroll with a two-finger pan, and then turn
1818 // that into a pinch by increasing the span sufficiently. In such a case,
1819 // there is no snap-back animation to get us out of overscroll, so we need
1820 // to get out of it somehow.
1821 // Moreover, in cases of scroll handoff, the overscroll can be on an APZC
1822 // further up in the handoff chain rather than on the current APZC, so
1823 // we need to clear overscroll along the entire handoff chain.
1824 if (HasReadyTouchBlock()) {
1825 GetCurrentTouchBlock()->GetOverscrollHandoffChain()->ClearOverscroll();
1826 } else {
1827 ClearOverscroll();
1829 // Along with clearing the overscroll, we also want to snap to the nearest
1830 // snap point as appropriate.
1831 ScrollSnap(ScrollSnapFlags::IntendedEndPosition);
1832 } else {
1833 // when zoom is not allowed
1834 EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::Yes);
1835 if (stateWasPinching) {
1836 // still pinching
1837 if (HasReadyTouchBlock()) {
1838 return HandleEndOfPan();
1843 return nsEventStatus_eConsumeNoDefault;
1846 nsEventStatus AsyncPanZoomController::HandleEndOfPan() {
1847 MOZ_ASSERT(!mAnimation);
1848 MOZ_ASSERT(GetCurrentTouchBlock() || GetCurrentPanGestureBlock());
1849 GetCurrentInputBlock()->GetOverscrollHandoffChain()->FlushRepaints();
1850 ParentLayerPoint flingVelocity = GetVelocityVector();
1852 // Clear our velocities; if DispatchFling() gives the fling to us,
1853 // the fling velocity gets *added* to our existing velocity in
1854 // AcceptFling().
1855 SetVelocityVector(ParentLayerPoint(0, 0));
1856 // Clear our state so that we don't stay in the PANNING state
1857 // if DispatchFling() gives the fling to somone else. However,
1858 // don't send the state change notification until we've determined
1859 // what our final state is to avoid notification churn.
1860 StateChangeNotificationBlocker blocker(this);
1861 SetState(NOTHING);
1863 APZC_LOG("%p starting a fling animation if %f > %f\n", this,
1864 flingVelocity.Length().value,
1865 StaticPrefs::apz_fling_min_velocity_threshold());
1867 if (flingVelocity.Length() <=
1868 StaticPrefs::apz_fling_min_velocity_threshold()) {
1869 // Relieve overscroll now if needed, since we will not transition to a fling
1870 // animation and then an overscroll animation, and relieve it then.
1871 GetCurrentInputBlock()
1872 ->GetOverscrollHandoffChain()
1873 ->SnapBackOverscrolledApzc(this);
1874 mFlingAccelerator.Reset();
1875 return nsEventStatus_eConsumeNoDefault;
1878 // Make a local copy of the tree manager pointer and check that it's not
1879 // null before calling DispatchFling(). This is necessary because Destroy(),
1880 // which nulls out mTreeManager, could be called concurrently.
1881 if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
1882 const FlingHandoffState handoffState{
1883 flingVelocity,
1884 GetCurrentInputBlock()->GetOverscrollHandoffChain(),
1885 Some(mTouchStartRestingTimeBeforePan),
1886 mMinimumVelocityDuringPan.valueOr(0),
1887 false /* not handoff */,
1888 GetCurrentInputBlock()->GetScrolledApzc()};
1889 treeManagerLocal->DispatchFling(this, handoffState);
1891 return nsEventStatus_eConsumeNoDefault;
1894 Maybe<LayoutDevicePoint> AsyncPanZoomController::ConvertToGecko(
1895 const ScreenIntPoint& aPoint) {
1896 if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
1897 if (Maybe<ScreenIntPoint> layoutPoint =
1898 treeManagerLocal->ConvertToGecko(aPoint, this)) {
1899 return Some(LayoutDevicePoint(ViewAs<LayoutDevicePixel>(
1900 *layoutPoint,
1901 PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent)));
1904 return Nothing();
1907 OuterCSSCoord AsyncPanZoomController::ConvertScrollbarPoint(
1908 const ParentLayerPoint& aScrollbarPoint,
1909 const ScrollbarData& aThumbData) const {
1910 RecursiveMutexAutoLock lock(mRecursiveMutex);
1912 CSSPoint scrollbarPoint;
1913 if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
1914 // First, get it into the right coordinate space.
1915 scrollbarPoint = aScrollbarPoint / Metrics().GetZoom();
1918 // The scrollbar can be transformed with the frame but the pres shell
1919 // resolution is only applied to the scroll frame.
1920 OuterCSSPoint outerScrollbarPoint =
1921 scrollbarPoint * Metrics().GetCSSToOuterCSSScale();
1923 // Now, get it to be relative to the beginning of the scroll track.
1924 OuterCSSRect cssCompositionBound =
1925 Metrics().CalculateCompositionBoundsInOuterCssPixels();
1926 return GetAxisStart(*aThumbData.mDirection, outerScrollbarPoint) -
1927 GetAxisStart(*aThumbData.mDirection, cssCompositionBound) -
1928 aThumbData.mScrollTrackStart;
1931 static bool AllowsScrollingMoreThanOnePage(double aMultiplier) {
1932 return Abs(aMultiplier) >=
1933 EventStateManager::MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL;
1936 ParentLayerPoint AsyncPanZoomController::GetScrollWheelDelta(
1937 const ScrollWheelInput& aEvent) const {
1938 return GetScrollWheelDelta(aEvent, aEvent.mDeltaX, aEvent.mDeltaY,
1939 aEvent.mUserDeltaMultiplierX,
1940 aEvent.mUserDeltaMultiplierY);
1943 ParentLayerPoint AsyncPanZoomController::GetScrollWheelDelta(
1944 const ScrollWheelInput& aEvent, double aDeltaX, double aDeltaY,
1945 double aMultiplierX, double aMultiplierY) const {
1946 ParentLayerSize scrollAmount;
1947 ParentLayerSize pageScrollSize;
1950 // Grab the lock to access the frame metrics.
1951 RecursiveMutexAutoLock lock(mRecursiveMutex);
1952 LayoutDeviceIntSize scrollAmountLD = mScrollMetadata.GetLineScrollAmount();
1953 LayoutDeviceIntSize pageScrollSizeLD =
1954 mScrollMetadata.GetPageScrollAmount();
1955 scrollAmount = scrollAmountLD / Metrics().GetDevPixelsPerCSSPixel() *
1956 Metrics().GetZoom();
1957 pageScrollSize = pageScrollSizeLD / Metrics().GetDevPixelsPerCSSPixel() *
1958 Metrics().GetZoom();
1961 ParentLayerPoint delta;
1962 switch (aEvent.mDeltaType) {
1963 case ScrollWheelInput::SCROLLDELTA_LINE: {
1964 delta.x = aDeltaX * scrollAmount.width;
1965 delta.y = aDeltaY * scrollAmount.height;
1966 break;
1968 case ScrollWheelInput::SCROLLDELTA_PAGE: {
1969 delta.x = aDeltaX * pageScrollSize.width;
1970 delta.y = aDeltaY * pageScrollSize.height;
1971 break;
1973 case ScrollWheelInput::SCROLLDELTA_PIXEL: {
1974 delta = ToParentLayerCoordinates(ScreenPoint(aDeltaX, aDeltaY),
1975 aEvent.mOrigin);
1976 break;
1980 // Apply user-set multipliers.
1981 delta.x *= aMultiplierX;
1982 delta.y *= aMultiplierY;
1984 // For the conditions under which we allow system scroll overrides, see
1985 // WidgetWheelEvent::OverriddenDelta{X,Y}.
1986 // Note that we do *not* restrict this to the root content, see bug 1217715
1987 // for discussion on this.
1988 if (StaticPrefs::mousewheel_system_scroll_override_enabled() &&
1989 !aEvent.IsCustomizedByUserPrefs() &&
1990 aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_LINE &&
1991 aEvent.mAllowToOverrideSystemScrollSpeed) {
1992 delta.x = WidgetWheelEvent::ComputeOverriddenDelta(delta.x, false);
1993 delta.y = WidgetWheelEvent::ComputeOverriddenDelta(delta.y, true);
1996 // If this is a line scroll, and this event was part of a scroll series, then
1997 // it might need extra acceleration. See WheelHandlingHelper.cpp.
1998 if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_LINE &&
1999 aEvent.mScrollSeriesNumber > 0) {
2000 int32_t start = StaticPrefs::mousewheel_acceleration_start();
2001 if (start >= 0 && aEvent.mScrollSeriesNumber >= uint32_t(start)) {
2002 int32_t factor = StaticPrefs::mousewheel_acceleration_factor();
2003 if (factor > 0) {
2004 delta.x = ComputeAcceleratedWheelDelta(
2005 delta.x, aEvent.mScrollSeriesNumber, factor);
2006 delta.y = ComputeAcceleratedWheelDelta(
2007 delta.y, aEvent.mScrollSeriesNumber, factor);
2012 // We shouldn't scroll more than one page at once except when the
2013 // user preference is large.
2014 if (!AllowsScrollingMoreThanOnePage(aMultiplierX) &&
2015 Abs(delta.x) > pageScrollSize.width) {
2016 delta.x = (delta.x >= 0) ? pageScrollSize.width : -pageScrollSize.width;
2018 if (!AllowsScrollingMoreThanOnePage(aMultiplierY) &&
2019 Abs(delta.y) > pageScrollSize.height) {
2020 delta.y = (delta.y >= 0) ? pageScrollSize.height : -pageScrollSize.height;
2023 return delta;
2026 nsEventStatus AsyncPanZoomController::OnKeyboard(const KeyboardInput& aEvent) {
2027 // Mark that this APZC has async key scrolled
2028 mTestHasAsyncKeyScrolled = true;
2030 // Calculate the destination for this keyboard scroll action
2031 CSSPoint destination = GetKeyboardDestination(aEvent.mAction);
2032 ScrollOrigin scrollOrigin =
2033 SmoothScrollAnimation::GetScrollOriginForAction(aEvent.mAction.mType);
2034 Maybe<CSSSnapDestination> snapDestination =
2035 MaybeAdjustDestinationForScrollSnapping(
2036 aEvent, destination,
2037 GetScrollSnapFlagsForKeyboardAction(aEvent.mAction));
2038 ScrollMode scrollMode = apz::GetScrollModeForOrigin(scrollOrigin);
2040 RecordScrollPayload(aEvent.mTimeStamp);
2041 // If the scrolling is instant, then scroll immediately to the destination
2042 if (scrollMode == ScrollMode::Instant) {
2043 CancelAnimation();
2045 ParentLayerPoint startPoint, endPoint;
2048 RecursiveMutexAutoLock lock(mRecursiveMutex);
2050 // CallDispatchScroll interprets the start and end points as the start and
2051 // end of a touch scroll so they need to be reversed.
2052 startPoint = destination * Metrics().GetZoom();
2053 endPoint = Metrics().GetVisualScrollOffset() * Metrics().GetZoom();
2056 ParentLayerPoint delta = endPoint - startPoint;
2058 ScreenPoint distance = ToScreenCoordinates(
2059 ParentLayerPoint(fabs(delta.x), fabs(delta.y)), startPoint);
2061 OverscrollHandoffState handoffState(
2062 *mInputQueue->GetCurrentKeyboardBlock()->GetOverscrollHandoffChain(),
2063 distance, ScrollSource::Keyboard);
2065 CallDispatchScroll(startPoint, endPoint, handoffState);
2066 ParentLayerPoint remainingDelta = endPoint - startPoint;
2067 if (remainingDelta != delta) {
2068 // If any scrolling happened, set KEYBOARD_SCROLL explicitly so that it
2069 // will trigger a TransformEnd notification.
2070 SetState(KEYBOARD_SCROLL);
2073 if (snapDestination) {
2075 RecursiveMutexAutoLock lock(mRecursiveMutex);
2076 mLastSnapTargetIds = std::move(snapDestination->mTargetIds);
2079 SetState(NOTHING);
2081 return nsEventStatus_eConsumeDoDefault;
2084 // The lock must be held across the entire update operation, so the
2085 // compositor doesn't end the animation before we get a chance to
2086 // update it.
2087 RecursiveMutexAutoLock lock(mRecursiveMutex);
2089 if (snapDestination) {
2090 // If we're scroll snapping, use a smooth scroll animation to get
2091 // the desired physics. Note that SmoothMsdScrollTo() will re-use an
2092 // existing smooth scroll animation if there is one.
2093 APZC_LOG("%p keyboard scrolling to snap point %s\n", this,
2094 ToString(destination).c_str());
2095 SmoothMsdScrollTo(std::move(*snapDestination), ScrollTriggeredByScript::No);
2096 return nsEventStatus_eConsumeDoDefault;
2099 // Use a keyboard scroll animation to scroll, reusing an existing one if it
2100 // exists
2101 if (mState != KEYBOARD_SCROLL) {
2102 CancelAnimation();
2104 // Keyboard input that does not change the scroll position should not
2105 // cause a TransformBegin state change, in order to avoid firing a
2106 // scrollend event when no scrolling occurred.
2107 if (!CanScroll(ConvertDestinationToDelta(destination))) {
2108 return nsEventStatus_eConsumeDoDefault;
2110 SetState(KEYBOARD_SCROLL);
2112 nsPoint initialPosition =
2113 CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset());
2114 StartAnimation(do_AddRef(
2115 new SmoothScrollAnimation(*this, initialPosition, scrollOrigin)));
2118 // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and then
2119 // to appunits/second.
2120 nsPoint velocity;
2121 if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
2122 velocity =
2123 CSSPoint::ToAppUnits(ParentLayerPoint(mX.GetVelocity() * 1000.0f,
2124 mY.GetVelocity() * 1000.0f) /
2125 Metrics().GetZoom());
2128 SmoothScrollAnimation* animation = mAnimation->AsSmoothScrollAnimation();
2129 MOZ_ASSERT(animation);
2131 animation->UpdateDestination(aEvent.mTimeStamp,
2132 CSSPixel::ToAppUnits(destination),
2133 nsSize(velocity.x, velocity.y));
2135 return nsEventStatus_eConsumeDoDefault;
2138 CSSPoint AsyncPanZoomController::GetKeyboardDestination(
2139 const KeyboardScrollAction& aAction) const {
2140 CSSSize lineScrollSize;
2141 CSSSize pageScrollSize;
2142 CSSPoint scrollOffset;
2143 CSSRect scrollRect;
2144 ParentLayerRect compositionBounds;
2147 // Grab the lock to access the frame metrics.
2148 RecursiveMutexAutoLock lock(mRecursiveMutex);
2150 lineScrollSize = mScrollMetadata.GetLineScrollAmount() /
2151 Metrics().GetDevPixelsPerCSSPixel();
2152 pageScrollSize = mScrollMetadata.GetPageScrollAmount() /
2153 Metrics().GetDevPixelsPerCSSPixel();
2155 scrollOffset = GetCurrentAnimationDestination(lock).valueOr(
2156 Metrics().GetVisualScrollOffset());
2158 scrollRect = Metrics().GetScrollableRect();
2159 compositionBounds = Metrics().GetCompositionBounds();
2162 // Calculate the scroll destination based off of the scroll type and direction
2163 CSSPoint scrollDestination = scrollOffset;
2165 switch (aAction.mType) {
2166 case KeyboardScrollAction::eScrollCharacter: {
2167 int32_t scrollDistance =
2168 StaticPrefs::toolkit_scrollbox_horizontalScrollDistance();
2170 if (aAction.mForward) {
2171 scrollDestination.x += scrollDistance * lineScrollSize.width;
2172 } else {
2173 scrollDestination.x -= scrollDistance * lineScrollSize.width;
2175 break;
2177 case KeyboardScrollAction::eScrollLine: {
2178 int32_t scrollDistance =
2179 StaticPrefs::toolkit_scrollbox_verticalScrollDistance();
2180 if (scrollDistance * lineScrollSize.height <=
2181 compositionBounds.Height()) {
2182 if (aAction.mForward) {
2183 scrollDestination.y += scrollDistance * lineScrollSize.height;
2184 } else {
2185 scrollDestination.y -= scrollDistance * lineScrollSize.height;
2187 break;
2189 [[fallthrough]];
2191 case KeyboardScrollAction::eScrollPage: {
2192 if (aAction.mForward) {
2193 scrollDestination.y += pageScrollSize.height;
2194 } else {
2195 scrollDestination.y -= pageScrollSize.height;
2197 break;
2199 case KeyboardScrollAction::eScrollComplete: {
2200 if (aAction.mForward) {
2201 scrollDestination.y = scrollRect.YMost();
2202 } else {
2203 scrollDestination.y = scrollRect.Y();
2205 break;
2209 return scrollDestination;
2212 ScrollSnapFlags AsyncPanZoomController::GetScrollSnapFlagsForKeyboardAction(
2213 const KeyboardScrollAction& aAction) const {
2214 switch (aAction.mType) {
2215 case KeyboardScrollAction::eScrollCharacter:
2216 case KeyboardScrollAction::eScrollLine:
2217 return ScrollSnapFlags::IntendedDirection;
2218 case KeyboardScrollAction::eScrollPage:
2219 return ScrollSnapFlags::IntendedDirection |
2220 ScrollSnapFlags::IntendedEndPosition;
2221 case KeyboardScrollAction::eScrollComplete:
2222 return ScrollSnapFlags::IntendedEndPosition;
2224 return ScrollSnapFlags::Disabled;
2227 ParentLayerPoint AsyncPanZoomController::GetDeltaForEvent(
2228 const InputData& aEvent) const {
2229 ParentLayerPoint delta;
2230 if (aEvent.mInputType == SCROLLWHEEL_INPUT) {
2231 delta = GetScrollWheelDelta(aEvent.AsScrollWheelInput());
2232 } else if (aEvent.mInputType == PANGESTURE_INPUT) {
2233 const PanGestureInput& panInput = aEvent.AsPanGestureInput();
2234 delta = ToParentLayerCoordinates(panInput.UserMultipliedPanDisplacement(),
2235 panInput.mPanStartPoint);
2237 return delta;
2240 CSSRect AsyncPanZoomController::GetCurrentScrollRangeInCssPixels() const {
2241 RecursiveMutexAutoLock lock(mRecursiveMutex);
2242 return Metrics().CalculateScrollRange();
2245 // Return whether or not the underlying layer can be scrolled on either axis.
2246 bool AsyncPanZoomController::CanScroll(const InputData& aEvent) const {
2247 ParentLayerPoint delta = GetDeltaForEvent(aEvent);
2248 if (!delta.x && !delta.y) {
2249 return false;
2252 if (SCROLLWHEEL_INPUT == aEvent.mInputType) {
2253 const ScrollWheelInput& scrollWheelInput = aEvent.AsScrollWheelInput();
2254 // If it's a wheel scroll, we first check if it is an auto-dir scroll.
2255 // 1. For an auto-dir scroll, check if it's delta should be adjusted, if it
2256 // is, then we can conclude it must be scrollable; otherwise, fall back
2257 // to checking if it is scrollable without adjusting its delta.
2258 // 2. For a non-auto-dir scroll, simply check if it is scrollable without
2259 // adjusting its delta.
2260 RecursiveMutexAutoLock lock(mRecursiveMutex);
2261 if (scrollWheelInput.IsAutoDir(mScrollMetadata.ForceMousewheelAutodir())) {
2262 auto deltaX = scrollWheelInput.mDeltaX;
2263 auto deltaY = scrollWheelInput.mDeltaY;
2264 bool isRTL =
2265 IsContentOfHonouredTargetRightToLeft(scrollWheelInput.HonoursRoot(
2266 mScrollMetadata.ForceMousewheelAutodirHonourRoot()));
2267 APZAutoDirWheelDeltaAdjuster adjuster(deltaX, deltaY, mX, mY, isRTL);
2268 if (adjuster.ShouldBeAdjusted()) {
2269 // If we detect that the delta values should be adjusted for an auto-dir
2270 // wheel scroll, then it is impossible to be an unscrollable scroll.
2271 return true;
2274 return CanScrollWithWheel(delta);
2276 return CanScroll(delta);
2279 ScrollDirections AsyncPanZoomController::GetAllowedHandoffDirections() const {
2280 ScrollDirections result;
2281 RecursiveMutexAutoLock lock(mRecursiveMutex);
2283 // In Fission there can be non-scrollable APZCs. It's unclear whether
2284 // overscroll-behavior should be respected for these
2285 // (see https://github.com/w3c/csswg-drafts/issues/6523) but
2286 // we currently don't, to match existing practice.
2287 const bool isScrollable = mX.CanScroll() || mY.CanScroll();
2288 const bool isRoot = IsRootContent();
2289 if ((!isScrollable && !isRoot) || mX.OverscrollBehaviorAllowsHandoff()) {
2290 result += ScrollDirection::eHorizontal;
2292 if ((!isScrollable && !isRoot) || mY.OverscrollBehaviorAllowsHandoff()) {
2293 result += ScrollDirection::eVertical;
2295 return result;
2298 bool AsyncPanZoomController::CanScroll(const ParentLayerPoint& aDelta) const {
2299 RecursiveMutexAutoLock lock(mRecursiveMutex);
2300 return mX.CanScroll(ParentLayerCoord(aDelta.x)) ||
2301 mY.CanScroll(ParentLayerCoord(aDelta.y));
2304 bool AsyncPanZoomController::CanScrollWithWheel(
2305 const ParentLayerPoint& aDelta) const {
2306 RecursiveMutexAutoLock lock(mRecursiveMutex);
2308 // For more details about the concept of a disregarded direction, refer to the
2309 // code in struct ScrollMetadata which defines mDisregardedDirection.
2310 Maybe<ScrollDirection> disregardedDirection =
2311 mScrollMetadata.GetDisregardedDirection();
2312 if (mX.CanScroll(ParentLayerCoord(aDelta.x)) &&
2313 disregardedDirection != Some(ScrollDirection::eHorizontal)) {
2314 return true;
2316 if (mY.CanScroll(ParentLayerCoord(aDelta.y)) &&
2317 disregardedDirection != Some(ScrollDirection::eVertical)) {
2318 return true;
2320 return false;
2323 bool AsyncPanZoomController::CanScroll(ScrollDirection aDirection) const {
2324 RecursiveMutexAutoLock lock(mRecursiveMutex);
2325 switch (aDirection) {
2326 case ScrollDirection::eHorizontal:
2327 return mX.CanScroll();
2328 case ScrollDirection::eVertical:
2329 return mY.CanScroll();
2331 MOZ_ASSERT_UNREACHABLE("Invalid value");
2332 return false;
2335 bool AsyncPanZoomController::CanVerticalScrollWithDynamicToolbar() const {
2336 MOZ_ASSERT(IsRootContent());
2338 RecursiveMutexAutoLock lock(mRecursiveMutex);
2339 return mY.CanVerticalScrollWithDynamicToolbar();
2342 bool AsyncPanZoomController::CanOverscrollUpwards() const {
2343 RecursiveMutexAutoLock lock(mRecursiveMutex);
2344 return !mY.CanScrollTo(eSideTop) && mY.OverscrollBehaviorAllowsHandoff();
2347 bool AsyncPanZoomController::CanScrollDownwards() const {
2348 RecursiveMutexAutoLock lock(mRecursiveMutex);
2349 return mY.CanScrollTo(eSideBottom);
2352 SideBits AsyncPanZoomController::ScrollableDirections() const {
2353 SideBits result;
2354 { // scope lock to respect lock ordering with APZCTreeManager::mTreeLock
2355 // which will be acquired in the `GetCompositorFixedLayerMargins` below.
2356 RecursiveMutexAutoLock lock(mRecursiveMutex);
2357 result = mX.ScrollableDirections() | mY.ScrollableDirections();
2360 if (IsRootContent()) {
2361 if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
2362 ScreenMargin fixedLayerMargins =
2363 treeManagerLocal->GetCompositorFixedLayerMargins();
2365 RecursiveMutexAutoLock lock(mRecursiveMutex);
2366 result |= mY.ScrollableDirectionsWithDynamicToolbar(fixedLayerMargins);
2371 return result;
2374 bool AsyncPanZoomController::IsContentOfHonouredTargetRightToLeft(
2375 bool aHonoursRoot) const {
2376 if (aHonoursRoot) {
2377 return mScrollMetadata.IsAutoDirRootContentRTL();
2379 RecursiveMutexAutoLock lock(mRecursiveMutex);
2380 return Metrics().IsHorizontalContentRightToLeft();
2383 bool AsyncPanZoomController::AllowScrollHandoffInCurrentBlock() const {
2384 bool result = mInputQueue->AllowScrollHandoff();
2385 if (!StaticPrefs::apz_allow_immediate_handoff()) {
2386 if (InputBlockState* currentBlock = GetCurrentInputBlock()) {
2387 // Do not allow handoff beyond the first APZC to scroll.
2388 if (currentBlock->GetScrolledApzc() == this) {
2389 result = false;
2390 APZC_LOG("%p dropping handoff; AllowImmediateHandoff=false\n", this);
2394 return result;
2397 void AsyncPanZoomController::DoDelayedRequestContentRepaint() {
2398 if (!IsDestroyed() && mPinchPaintTimerSet) {
2399 RecursiveMutexAutoLock lock(mRecursiveMutex);
2400 RequestContentRepaint();
2402 mPinchPaintTimerSet = false;
2405 void AsyncPanZoomController::DoDelayedTransformEndNotification(
2406 PanZoomState aOldState) {
2407 if (!IsDestroyed() && IsDelayedTransformEndSet()) {
2408 DispatchStateChangeNotification(aOldState, NOTHING);
2410 SetDelayedTransformEnd(false);
2413 static void AdjustDeltaForAllowedScrollDirections(
2414 ParentLayerPoint& aDelta,
2415 const ScrollDirections& aAllowedScrollDirections) {
2416 if (!aAllowedScrollDirections.contains(ScrollDirection::eHorizontal)) {
2417 aDelta.x = 0;
2419 if (!aAllowedScrollDirections.contains(ScrollDirection::eVertical)) {
2420 aDelta.y = 0;
2424 nsEventStatus AsyncPanZoomController::OnScrollWheel(
2425 const ScrollWheelInput& aEvent) {
2426 // Get the scroll wheel's delta values in parent-layer pixels. But before
2427 // getting the values, we need to check if it is an auto-dir scroll and if it
2428 // should be adjusted, if both answers are yes, let's adjust X and Y values
2429 // first, and then get the delta values in parent-layer pixels based on the
2430 // adjusted values.
2431 bool adjustedByAutoDir = false;
2432 auto deltaX = aEvent.mDeltaX;
2433 auto deltaY = aEvent.mDeltaY;
2434 ParentLayerPoint delta;
2436 RecursiveMutexAutoLock lock(mRecursiveMutex);
2437 if (aEvent.IsAutoDir(mScrollMetadata.ForceMousewheelAutodir())) {
2438 // It's an auto-dir scroll, so check if its delta should be adjusted, if
2439 // so, adjust it.
2440 bool isRTL = IsContentOfHonouredTargetRightToLeft(aEvent.HonoursRoot(
2441 mScrollMetadata.ForceMousewheelAutodirHonourRoot()));
2442 APZAutoDirWheelDeltaAdjuster adjuster(deltaX, deltaY, mX, mY, isRTL);
2443 if (adjuster.ShouldBeAdjusted()) {
2444 adjuster.Adjust();
2445 adjustedByAutoDir = true;
2449 // Ensure the calls to GetScrollWheelDelta are outside the mRecursiveMutex
2450 // lock since these calls may acquire the APZ tree lock. Holding
2451 // mRecursiveMutex while acquiring the APZ tree lock is lock ordering
2452 // violation.
2453 if (adjustedByAutoDir) {
2454 // If the original delta values have been adjusted, we pass them to
2455 // replace the original delta values in |aEvent| so that the delta values
2456 // in parent-layer pixels are caculated based on the adjusted values, not
2457 // the original ones.
2458 // Pay special attention to the last two parameters. They are in a swaped
2459 // order so that they still correspond to their delta after adjustment.
2460 delta = GetScrollWheelDelta(aEvent, deltaX, deltaY,
2461 aEvent.mUserDeltaMultiplierY,
2462 aEvent.mUserDeltaMultiplierX);
2463 } else {
2464 // If the original delta values haven't been adjusted by auto-dir, just pass
2465 // the |aEvent| and caculate the delta values in parent-layer pixels based
2466 // on the original delta values from |aEvent|.
2467 delta = GetScrollWheelDelta(aEvent);
2470 APZC_LOG("%p got a scroll-wheel with delta in parent-layer pixels: %s\n",
2471 this, ToString(delta).c_str());
2473 if (adjustedByAutoDir) {
2474 MOZ_ASSERT(delta.x || delta.y,
2475 "Adjusted auto-dir delta values can never be all-zero.");
2476 APZC_LOG("%p got a scroll-wheel with adjusted auto-dir delta values\n",
2477 this);
2478 } else if ((delta.x || delta.y) && !CanScrollWithWheel(delta)) {
2479 // We can't scroll this apz anymore, so we simply drop the event.
2480 if (mInputQueue->GetActiveWheelTransaction() &&
2481 StaticPrefs::test_mousescroll()) {
2482 if (RefPtr<GeckoContentController> controller =
2483 GetGeckoContentController()) {
2484 controller->NotifyMozMouseScrollEvent(GetScrollId(),
2485 u"MozMouseScrollFailed"_ns);
2488 return nsEventStatus_eConsumeNoDefault;
2491 MOZ_ASSERT(mInputQueue->GetCurrentWheelBlock());
2492 AdjustDeltaForAllowedScrollDirections(
2493 delta, mInputQueue->GetCurrentWheelBlock()->GetAllowedScrollDirections());
2495 if (delta.x == 0 && delta.y == 0) {
2496 // Avoid spurious state changes and unnecessary work
2497 return nsEventStatus_eIgnore;
2500 switch (aEvent.mScrollMode) {
2501 case ScrollWheelInput::SCROLLMODE_INSTANT: {
2502 // Wheel events from "clicky" mouse wheels trigger scroll snapping to the
2503 // next snap point. Check for this, and adjust the delta to take into
2504 // account the snap point.
2505 CSSPoint startPosition;
2507 RecursiveMutexAutoLock lock(mRecursiveMutex);
2508 startPosition = Metrics().GetVisualScrollOffset();
2510 Maybe<CSSSnapDestination> snapDestination =
2511 MaybeAdjustDeltaForScrollSnappingOnWheelInput(aEvent, delta,
2512 startPosition);
2514 ScreenPoint distance = ToScreenCoordinates(
2515 ParentLayerPoint(fabs(delta.x), fabs(delta.y)), aEvent.mLocalOrigin);
2517 CancelAnimation();
2519 OverscrollHandoffState handoffState(
2520 *mInputQueue->GetCurrentWheelBlock()->GetOverscrollHandoffChain(),
2521 distance, ScrollSource::Wheel);
2522 ParentLayerPoint startPoint = aEvent.mLocalOrigin;
2523 ParentLayerPoint endPoint = aEvent.mLocalOrigin - delta;
2524 RecordScrollPayload(aEvent.mTimeStamp);
2526 CallDispatchScroll(startPoint, endPoint, handoffState);
2527 ParentLayerPoint remainingDelta = endPoint - startPoint;
2528 if (remainingDelta != delta) {
2529 // If any scrolling happened, set WHEEL_SCROLL explicitly so that it
2530 // will trigger a TransformEnd notification.
2531 SetState(WHEEL_SCROLL);
2534 if (snapDestination) {
2536 RecursiveMutexAutoLock lock(mRecursiveMutex);
2537 mLastSnapTargetIds = std::move(snapDestination->mTargetIds);
2540 SetState(NOTHING);
2542 // The calls above handle their own locking; moreover,
2543 // ToScreenCoordinates() and CallDispatchScroll() can grab the tree lock.
2544 RecursiveMutexAutoLock lock(mRecursiveMutex);
2545 RequestContentRepaint();
2547 break;
2550 case ScrollWheelInput::SCROLLMODE_SMOOTH: {
2551 // The lock must be held across the entire update operation, so the
2552 // compositor doesn't end the animation before we get a chance to
2553 // update it.
2554 RecursiveMutexAutoLock lock(mRecursiveMutex);
2556 RecordScrollPayload(aEvent.mTimeStamp);
2557 // Perform scroll snapping if appropriate.
2558 // If we're already in a wheel scroll or smooth scroll animation,
2559 // the delta is applied to its destination, not to the current
2560 // scroll position. Take this into account when finding a snap point.
2561 CSSPoint startPosition = GetCurrentAnimationDestination(lock).valueOr(
2562 Metrics().GetVisualScrollOffset());
2564 if (Maybe<CSSSnapDestination> snapDestination =
2565 MaybeAdjustDeltaForScrollSnappingOnWheelInput(aEvent, delta,
2566 startPosition)) {
2567 // If we're scroll snapping, use a smooth scroll animation to get
2568 // the desired physics. Note that SmoothMsdScrollTo() will re-use an
2569 // existing smooth scroll animation if there is one.
2570 APZC_LOG("%p wheel scrolling to snap point %s\n", this,
2571 ToString(startPosition).c_str());
2572 SmoothMsdScrollTo(std::move(*snapDestination),
2573 ScrollTriggeredByScript::No);
2574 break;
2577 // Otherwise, use a wheel scroll animation, also reusing one if possible.
2578 if (mState != WHEEL_SCROLL) {
2579 CancelAnimation();
2580 SetState(WHEEL_SCROLL);
2582 nsPoint initialPosition =
2583 CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset());
2584 StartAnimation(do_AddRef(new WheelScrollAnimation(
2585 *this, initialPosition, aEvent.mDeltaType)));
2587 // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and
2588 // then to appunits/second.
2590 nsPoint deltaInAppUnits;
2591 nsPoint velocity;
2592 if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
2593 deltaInAppUnits = CSSPoint::ToAppUnits(delta / Metrics().GetZoom());
2594 velocity =
2595 CSSPoint::ToAppUnits(ParentLayerPoint(mX.GetVelocity() * 1000.0f,
2596 mY.GetVelocity() * 1000.0f) /
2597 Metrics().GetZoom());
2600 WheelScrollAnimation* animation = mAnimation->AsWheelScrollAnimation();
2601 animation->UpdateDelta(aEvent.mTimeStamp, deltaInAppUnits,
2602 nsSize(velocity.x, velocity.y));
2603 break;
2607 return nsEventStatus_eConsumeNoDefault;
2610 void AsyncPanZoomController::NotifyMozMouseScrollEvent(
2611 const nsString& aString) const {
2612 RefPtr<GeckoContentController> controller = GetGeckoContentController();
2613 if (!controller) {
2614 return;
2616 controller->NotifyMozMouseScrollEvent(GetScrollId(), aString);
2619 nsEventStatus AsyncPanZoomController::OnPanMayBegin(
2620 const PanGestureInput& aEvent) {
2621 APZC_LOG_DETAIL("got a pan-maybegin in state %s\n", this,
2622 ToString(mState).c_str());
2624 StartTouch(aEvent.mLocalPanStartPoint, aEvent.mTimeStamp);
2625 MOZ_ASSERT(GetCurrentPanGestureBlock());
2626 GetCurrentPanGestureBlock()->GetOverscrollHandoffChain()->CancelAnimations();
2628 return nsEventStatus_eConsumeNoDefault;
2631 nsEventStatus AsyncPanZoomController::OnPanCancelled(
2632 const PanGestureInput& aEvent) {
2633 APZC_LOG_DETAIL("got a pan-cancelled in state %s\n", this,
2634 ToString(mState).c_str());
2636 mX.CancelGesture();
2637 mY.CancelGesture();
2639 return nsEventStatus_eConsumeNoDefault;
2642 nsEventStatus AsyncPanZoomController::OnPanBegin(
2643 const PanGestureInput& aEvent) {
2644 APZC_LOG_DETAIL("got a pan-begin in state %s\n", this,
2645 ToString(mState).c_str());
2647 if (mState == SMOOTHMSD_SCROLL) {
2648 // SMOOTHMSD_SCROLL scrolls are cancelled by pan gestures.
2649 CancelAnimation();
2652 StartTouch(aEvent.mLocalPanStartPoint, aEvent.mTimeStamp);
2654 if (!UsingStatefulAxisLock()) {
2655 SetState(PANNING);
2656 } else {
2657 float dx = aEvent.mPanDisplacement.x, dy = aEvent.mPanDisplacement.y;
2659 if (dx != 0.0f || dy != 0.0f) {
2660 double angle = atan2(dy, dx); // range [-pi, pi]
2661 angle = fabs(angle); // range [0, pi]
2662 HandlePanning(angle);
2663 } else {
2664 SetState(PANNING);
2668 // Call into OnPan in order to process any delta included in this event.
2669 OnPan(aEvent, FingersOnTouchpad::Yes);
2671 return nsEventStatus_eConsumeNoDefault;
2674 std::tuple<ParentLayerPoint, ScreenPoint>
2675 AsyncPanZoomController::GetDisplacementsForPanGesture(
2676 const PanGestureInput& aEvent) {
2677 // Note that there is a multiplier that applies onto the "physical" pan
2678 // displacement (how much the user's fingers moved) that produces the
2679 // "logical" pan displacement (how much the page should move). For some of the
2680 // code below it makes more sense to use the physical displacement rather than
2681 // the logical displacement, and vice-versa.
2682 ScreenPoint physicalPanDisplacement = aEvent.mPanDisplacement;
2683 ParentLayerPoint logicalPanDisplacement =
2684 aEvent.UserMultipliedLocalPanDisplacement();
2685 if (aEvent.mDeltaType == PanGestureInput::PANDELTA_PAGE) {
2686 // Pan events with page units are used by Gtk, so this replicates Gtk:
2687 // https://gitlab.gnome.org/GNOME/gtk/blob/c734c7e9188b56f56c3a504abee05fa40c5475ac/gtk/gtkrange.c#L3065-3073
2688 CSSSize pageScrollSize;
2689 CSSToParentLayerScale zoom;
2691 // Grab the lock to access the frame metrics.
2692 RecursiveMutexAutoLock lock(mRecursiveMutex);
2693 pageScrollSize = mScrollMetadata.GetPageScrollAmount() /
2694 Metrics().GetDevPixelsPerCSSPixel();
2695 zoom = Metrics().GetZoom();
2697 // scrollUnit* is in units of "ParentLayer pixels per page proportion"...
2698 auto scrollUnitWidth = std::min(std::pow(pageScrollSize.width, 2.0 / 3.0),
2699 pageScrollSize.width / 2.0) *
2700 zoom.scale;
2701 auto scrollUnitHeight = std::min(std::pow(pageScrollSize.height, 2.0 / 3.0),
2702 pageScrollSize.height / 2.0) *
2703 zoom.scale;
2704 // ... and pan displacements are in units of "page proportion count"
2705 // here, so the products of them and scrollUnit* are in ParentLayer pixels
2706 ParentLayerPoint physicalPanDisplacementPL(
2707 physicalPanDisplacement.x * scrollUnitWidth,
2708 physicalPanDisplacement.y * scrollUnitHeight);
2709 physicalPanDisplacement = ToScreenCoordinates(physicalPanDisplacementPL,
2710 aEvent.mLocalPanStartPoint);
2711 logicalPanDisplacement.x *= scrollUnitWidth;
2712 logicalPanDisplacement.y *= scrollUnitHeight;
2714 // Accelerate (decelerate) any pans by raising it to a user configurable
2715 // power (apz.touch_acceleration_factor_x, apz.touch_acceleration_factor_y)
2717 // Confine input for pow() to greater than or equal to 0 to avoid domain
2718 // errors with non-integer exponents
2719 if (mX.GetVelocity() != 0) {
2720 float absVelocity = std::abs(mX.GetVelocity());
2721 logicalPanDisplacement.x *=
2722 std::pow(absVelocity,
2723 StaticPrefs::apz_touch_acceleration_factor_x()) /
2724 absVelocity;
2727 if (mY.GetVelocity() != 0) {
2728 float absVelocity = std::abs(mY.GetVelocity());
2729 logicalPanDisplacement.y *=
2730 std::pow(absVelocity,
2731 StaticPrefs::apz_touch_acceleration_factor_y()) /
2732 absVelocity;
2736 MOZ_ASSERT(GetCurrentPanGestureBlock());
2737 AdjustDeltaForAllowedScrollDirections(
2738 logicalPanDisplacement,
2739 GetCurrentPanGestureBlock()->GetAllowedScrollDirections());
2741 if (GetAxisLockMode() == DOMINANT_AXIS) {
2742 // Given a pan gesture and both directions have a delta, implement
2743 // dominant axis scrolling and only use the delta for the larger
2744 // axis.
2745 if (logicalPanDisplacement.y != 0 && logicalPanDisplacement.x != 0) {
2746 if (fabs(logicalPanDisplacement.y) >= fabs(logicalPanDisplacement.x)) {
2747 logicalPanDisplacement.x = 0;
2748 physicalPanDisplacement.x = 0;
2749 } else {
2750 logicalPanDisplacement.y = 0;
2751 physicalPanDisplacement.y = 0;
2756 return {logicalPanDisplacement, physicalPanDisplacement};
2759 nsEventStatus AsyncPanZoomController::OnPan(
2760 const PanGestureInput& aEvent, FingersOnTouchpad aFingersOnTouchpad) {
2761 APZC_LOG_DETAIL("got a pan-pan in state %s\n", this,
2762 ToString(GetState()).c_str());
2764 if (GetState() == SMOOTHMSD_SCROLL) {
2765 if (aFingersOnTouchpad == FingersOnTouchpad::No) {
2766 // When a SMOOTHMSD_SCROLL scroll is being processed on a frame, mouse
2767 // wheel and trackpad momentum scroll position updates will not cancel the
2768 // SMOOTHMSD_SCROLL scroll animations, enabling scripts that depend on
2769 // them to be responsive without forcing the user to wait for the momentum
2770 // scrolling to completely stop.
2771 return nsEventStatus_eConsumeNoDefault;
2774 // SMOOTHMSD_SCROLL scrolls are cancelled by pan gestures.
2775 CancelAnimation();
2778 if (GetState() == NOTHING) {
2779 // This event block was interrupted by something else. If the user's fingers
2780 // are still on on the touchpad we want to resume scrolling, otherwise we
2781 // ignore the rest of the scroll gesture.
2782 if (aFingersOnTouchpad == FingersOnTouchpad::No) {
2783 return nsEventStatus_eConsumeNoDefault;
2785 // Resume / restart the pan.
2786 // PanBegin will call back into this function with mState == PANNING.
2787 return OnPanBegin(aEvent);
2790 auto [logicalPanDisplacement, physicalPanDisplacement] =
2791 GetDisplacementsForPanGesture(aEvent);
2794 // Grab the lock to protect the animation from being canceled on the updater
2795 // thread.
2796 RecursiveMutexAutoLock lock(mRecursiveMutex);
2797 MOZ_ASSERT_IF(GetState() == OVERSCROLL_ANIMATION, mAnimation);
2799 if (GetState() == OVERSCROLL_ANIMATION && mAnimation &&
2800 aFingersOnTouchpad == FingersOnTouchpad::No) {
2801 // If there is an on-going overscroll animation, we tell the animation
2802 // whether the displacements should be handled by the animation or not.
2803 MOZ_ASSERT(mAnimation->AsOverscrollAnimation());
2804 if (RefPtr<OverscrollAnimation> overscrollAnimation =
2805 mAnimation->AsOverscrollAnimation()) {
2806 overscrollAnimation->HandlePanMomentum(logicalPanDisplacement);
2807 // And then as a result of the above call, if the animation is currently
2808 // affecting on the axis, drop the displacement value on the axis so
2809 // that we stop further oversrolling on the axis.
2810 if (overscrollAnimation->IsManagingXAxis()) {
2811 logicalPanDisplacement.x = 0;
2812 physicalPanDisplacement.x = 0;
2814 if (overscrollAnimation->IsManagingYAxis()) {
2815 logicalPanDisplacement.y = 0;
2816 physicalPanDisplacement.y = 0;
2822 HandlePanningUpdate(physicalPanDisplacement);
2824 MOZ_ASSERT(GetCurrentPanGestureBlock());
2825 ScreenPoint panDistance(fabs(physicalPanDisplacement.x),
2826 fabs(physicalPanDisplacement.y));
2827 OverscrollHandoffState handoffState(
2828 *GetCurrentPanGestureBlock()->GetOverscrollHandoffChain(), panDistance,
2829 ScrollSource::Touchpad);
2831 // Create fake "touch" positions that will result in the desired scroll
2832 // motion. Note that the pan displacement describes the change in scroll
2833 // position: positive displacement values mean that the scroll position
2834 // increases. However, an increase in scroll position means that the scrolled
2835 // contents are moved to the left / upwards. Since our simulated "touches"
2836 // determine the motion of the scrolled contents, not of the scroll position,
2837 // they need to move in the opposite direction of the pan displacement.
2838 ParentLayerPoint startPoint = aEvent.mLocalPanStartPoint;
2839 ParentLayerPoint endPoint =
2840 aEvent.mLocalPanStartPoint - logicalPanDisplacement;
2841 if (logicalPanDisplacement != ParentLayerPoint()) {
2842 // Don't expect a composite to be triggered if the displacement is zero
2843 RecordScrollPayload(aEvent.mTimeStamp);
2846 const ParentLayerPoint velocity = GetVelocityVector();
2847 bool consumed = CallDispatchScroll(startPoint, endPoint, handoffState);
2849 const ParentLayerPoint visualDisplacement = ToParentLayerCoordinates(
2850 handoffState.mTotalMovement, aEvent.mPanStartPoint);
2851 // We need to update the axis velocity in order to get a useful display port
2852 // size and position. We need to do so even if this is a momentum pan (i.e.
2853 // aFingersOnTouchpad == No); in that case the "with touch" part is not
2854 // really appropriate, so we may want to rethink this at some point.
2855 // Note that we have to make all simulated positions relative to
2856 // Axis::GetPos(), because the current position is an invented position, and
2857 // because resetting the position to the mouse position (e.g.
2858 // aEvent.mLocalStartPoint) would mess up velocity calculation. (This is
2859 // the only caller of UpdateWithTouchAtDevicePoint() for pan events, so
2860 // there is no risk of other calls resetting the position.)
2861 // Also note that if there is an on-going overscroll animation in the axis,
2862 // we shouldn't call UpdateWithTouchAtDevicePoint because the call changes
2863 // the velocity which should be managed by the overscroll animation.
2864 // Finally, note that we do this *after* CallDispatchScroll(), so that the
2865 // position we use reflects the actual amount of movement that occurred
2866 // (in particular, if we're in overscroll, if reflects the amount of movement
2867 // *after* applying resistance). This is important because we want the axis
2868 // velocity to track the visual movement speed of the page.
2869 if (visualDisplacement.x != 0) {
2870 mX.UpdateWithTouchAtDevicePoint(mX.GetPos() - visualDisplacement.x,
2871 aEvent.mTimeStamp);
2873 if (visualDisplacement.y != 0) {
2874 mY.UpdateWithTouchAtDevicePoint(mY.GetPos() - visualDisplacement.y,
2875 aEvent.mTimeStamp);
2878 if (aFingersOnTouchpad == FingersOnTouchpad::No) {
2879 if (IsOverscrolled() && GetState() != OVERSCROLL_ANIMATION) {
2880 StartOverscrollAnimation(velocity, GetOverscrollSideBits());
2881 } else if (!consumed) {
2882 // If there is unconsumed scroll and we're in the momentum part of the
2883 // pan gesture, terminate the momentum scroll. This prevents momentum
2884 // scroll events from unexpectedly causing scrolling later if somehow
2885 // the APZC becomes scrollable again in this direction (e.g. if the user
2886 // uses some other input method to scroll in the opposite direction).
2887 SetState(NOTHING);
2891 return nsEventStatus_eConsumeNoDefault;
2894 nsEventStatus AsyncPanZoomController::OnPanEnd(const PanGestureInput& aEvent) {
2895 APZC_LOG_DETAIL("got a pan-end in state %s\n", this,
2896 ToString(mState).c_str());
2898 // This can happen if the OS sends a second pan-end event after the first one
2899 // has already started an overscroll animation or entered a fling state.
2900 // This has been observed on some Wayland versions.
2901 PanZoomState currentState = GetState();
2902 if (currentState == OVERSCROLL_ANIMATION || currentState == NOTHING ||
2903 currentState == FLING) {
2904 return nsEventStatus_eIgnore;
2907 if (aEvent.mPanDisplacement != ScreenPoint{}) {
2908 // Call into OnPan in order to process the delta included in this event.
2909 OnPan(aEvent, FingersOnTouchpad::Yes);
2912 // Do not unlock the axis lock at the end of a pan gesture. The axis lock
2913 // should extend into the momentum scroll.
2914 EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::No);
2916 // Use HandleEndOfPan for fling on platforms that don't
2917 // emit momentum events (Gtk).
2918 if (aEvent.mSimulateMomentum) {
2919 return HandleEndOfPan();
2922 MOZ_ASSERT(GetCurrentPanGestureBlock());
2923 RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
2924 GetCurrentPanGestureBlock()->GetOverscrollHandoffChain();
2926 // Call SnapBackOverscrolledApzcForMomentum regardless whether this APZC is
2927 // overscrolled or not since overscroll animations for ancestor APZCs in this
2928 // overscroll handoff chain might have been cancelled by the current pan
2929 // gesture block.
2930 overscrollHandoffChain->SnapBackOverscrolledApzcForMomentum(
2931 this, GetVelocityVector());
2932 // If this APZC is overscrolled, the above SnapBackOverscrolledApzcForMomentum
2933 // triggers an overscroll animation. When we're finished with the overscroll
2934 // animation, the state will be reset and a TransformEnd will be sent to the
2935 // main thread.
2936 currentState = GetState();
2937 if (currentState != OVERSCROLL_ANIMATION) {
2938 // Do not send a state change notification to the content controller here.
2939 // Instead queue a delayed task to dispatch the notification if no
2940 // momentum pan or scroll snap follows the pan-end.
2941 RefPtr<GeckoContentController> controller = GetGeckoContentController();
2942 if (controller) {
2943 SetDelayedTransformEnd(true);
2944 controller->PostDelayedTask(
2945 NewRunnableMethod<PanZoomState>(
2946 "layers::AsyncPanZoomController::"
2947 "DoDelayedTransformEndNotification",
2948 this, &AsyncPanZoomController::DoDelayedTransformEndNotification,
2949 currentState),
2950 StaticPrefs::apz_scrollend_event_content_delay_ms());
2951 SetStateNoContentControllerDispatch(NOTHING);
2952 } else {
2953 SetState(NOTHING);
2957 // Drop any velocity on axes where we don't have room to scroll anyways
2958 // (in this APZC, or an APZC further in the handoff chain).
2959 // This ensures that we don't enlarge the display port unnecessarily.
2961 RecursiveMutexAutoLock lock(mRecursiveMutex);
2962 if (!overscrollHandoffChain->CanScrollInDirection(
2963 this, ScrollDirection::eHorizontal)) {
2964 mX.SetVelocity(0);
2966 if (!overscrollHandoffChain->CanScrollInDirection(
2967 this, ScrollDirection::eVertical)) {
2968 mY.SetVelocity(0);
2972 RequestContentRepaint();
2973 ScrollSnapToDestination();
2975 return nsEventStatus_eConsumeNoDefault;
2978 nsEventStatus AsyncPanZoomController::OnPanMomentumStart(
2979 const PanGestureInput& aEvent) {
2980 APZC_LOG_DETAIL("got a pan-momentumstart in state %s\n", this,
2981 ToString(mState).c_str());
2983 if (mState == SMOOTHMSD_SCROLL || mState == OVERSCROLL_ANIMATION) {
2984 return nsEventStatus_eConsumeNoDefault;
2987 if (IsDelayedTransformEndSet()) {
2988 // Do not send another TransformBegin notification if we have not
2989 // delivered a corresponding TransformEnd. Also ensure that any
2990 // queued transform-end due to a pan-end is not sent. Instead rely
2991 // on the transform-end sent due to the momentum pan.
2992 SetDelayedTransformEnd(false);
2993 SetStateNoContentControllerDispatch(PAN_MOMENTUM);
2994 } else {
2995 SetState(PAN_MOMENTUM);
2998 // Call into OnPan in order to process any delta included in this event.
2999 OnPan(aEvent, FingersOnTouchpad::No);
3001 return nsEventStatus_eConsumeNoDefault;
3004 nsEventStatus AsyncPanZoomController::OnPanMomentumEnd(
3005 const PanGestureInput& aEvent) {
3006 APZC_LOG_DETAIL("got a pan-momentumend in state %s\n", this,
3007 ToString(mState).c_str());
3009 if (mState == OVERSCROLL_ANIMATION) {
3010 return nsEventStatus_eConsumeNoDefault;
3013 // Call into OnPan in order to process any delta included in this event.
3014 OnPan(aEvent, FingersOnTouchpad::No);
3016 // We need to reset the velocity to zero. We don't really have a "touch"
3017 // here because the touch has already ended long before the momentum
3018 // animation started, but I guess it doesn't really matter for now.
3019 mX.CancelGesture();
3020 mY.CancelGesture();
3021 SetState(NOTHING);
3023 RequestContentRepaint();
3025 return nsEventStatus_eConsumeNoDefault;
3028 nsEventStatus AsyncPanZoomController::OnPanInterrupted(
3029 const PanGestureInput& aEvent) {
3030 APZC_LOG_DETAIL("got a pan-interrupted in state %s\n", this,
3031 ToString(mState).c_str());
3033 CancelAnimation();
3035 return nsEventStatus_eIgnore;
3038 nsEventStatus AsyncPanZoomController::OnLongPress(
3039 const TapGestureInput& aEvent) {
3040 APZC_LOG_DETAIL("got a long-press in state %s\n", this,
3041 ToString(mState).c_str());
3042 RefPtr<GeckoContentController> controller = GetGeckoContentController();
3043 if (controller) {
3044 if (Maybe<LayoutDevicePoint> geckoScreenPoint =
3045 ConvertToGecko(aEvent.mPoint)) {
3046 TouchBlockState* touch = GetCurrentTouchBlock();
3047 if (!touch) {
3048 APZC_LOG(
3049 "%p dropping long-press because some non-touch block interrupted "
3050 "it\n",
3051 this);
3052 return nsEventStatus_eIgnore;
3054 if (touch->IsDuringFastFling()) {
3055 APZC_LOG("%p dropping long-press because of fast fling\n", this);
3056 return nsEventStatus_eIgnore;
3058 uint64_t blockId = GetInputQueue()->InjectNewTouchBlock(this);
3059 controller->HandleTap(TapType::eLongTap, *geckoScreenPoint,
3060 aEvent.modifiers, GetGuid(), blockId, Nothing());
3061 return nsEventStatus_eConsumeNoDefault;
3064 return nsEventStatus_eIgnore;
3067 nsEventStatus AsyncPanZoomController::OnLongPressUp(
3068 const TapGestureInput& aEvent) {
3069 APZC_LOG_DETAIL("got a long-tap-up in state %s\n", this,
3070 ToString(mState).c_str());
3071 return GenerateSingleTap(TapType::eLongTapUp, aEvent.mPoint,
3072 aEvent.modifiers);
3075 nsEventStatus AsyncPanZoomController::GenerateSingleTap(
3076 TapType aType, const ScreenIntPoint& aPoint,
3077 mozilla::Modifiers aModifiers) {
3078 RefPtr<GeckoContentController> controller = GetGeckoContentController();
3079 if (controller) {
3080 if (Maybe<LayoutDevicePoint> geckoScreenPoint = ConvertToGecko(aPoint)) {
3081 TouchBlockState* touch = GetCurrentTouchBlock();
3082 // |touch| may be null in the case where this function is
3083 // invoked by GestureEventListener on a timeout. In that case we already
3084 // verified that the single tap is allowed so we let it through.
3085 // XXX there is a bug here that in such a case the touch block that
3086 // generated this tap will not get its mSingleTapOccurred flag set.
3087 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1256344#c6
3088 if (touch) {
3089 if (touch->IsDuringFastFling()) {
3090 APZC_LOG(
3091 "%p dropping single-tap because it was during a fast-fling\n",
3092 this);
3093 return nsEventStatus_eIgnore;
3095 touch->SetSingleTapOccurred();
3097 // Because this may be being running as part of
3098 // APZCTreeManager::ReceiveInputEvent, calling controller->HandleTap
3099 // directly might mean that content receives the single tap message before
3100 // the corresponding touch-up. To avoid that we schedule the singletap
3101 // message to run on the next spin of the event loop. See bug 965381 for
3102 // the issue this was causing.
3103 APZC_LOG("posting runnable for HandleTap from GenerateSingleTap");
3104 RefPtr<Runnable> runnable =
3105 NewRunnableMethod<TapType, LayoutDevicePoint, mozilla::Modifiers,
3106 ScrollableLayerGuid, uint64_t,
3107 Maybe<DoubleTapToZoomMetrics>>(
3108 "layers::GeckoContentController::HandleTap", controller,
3109 &GeckoContentController::HandleTap, aType, *geckoScreenPoint,
3110 aModifiers, GetGuid(), touch ? touch->GetBlockId() : 0,
3111 Nothing());
3113 controller->PostDelayedTask(runnable.forget(), 0);
3114 return nsEventStatus_eConsumeNoDefault;
3117 return nsEventStatus_eIgnore;
3120 void AsyncPanZoomController::OnTouchEndOrCancel() {
3121 if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
3122 MOZ_ASSERT(GetCurrentTouchBlock());
3123 controller->NotifyAPZStateChange(
3124 GetGuid(), APZStateChange::eEndTouch,
3125 GetCurrentTouchBlock()->SingleTapOccurred(),
3126 Some(GetCurrentTouchBlock()->GetBlockId()));
3130 nsEventStatus AsyncPanZoomController::OnSingleTapUp(
3131 const TapGestureInput& aEvent) {
3132 APZC_LOG_DETAIL("got a single-tap-up in state %s\n", this,
3133 ToString(mState).c_str());
3134 // If mZoomConstraints.mAllowDoubleTapZoom is true we wait for a call to
3135 // OnSingleTapConfirmed before sending event to content
3136 MOZ_ASSERT(GetCurrentTouchBlock());
3137 if (!(ZoomConstraintsAllowDoubleTapZoom() &&
3138 GetCurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) {
3139 return GenerateSingleTap(TapType::eSingleTap, aEvent.mPoint,
3140 aEvent.modifiers);
3142 return nsEventStatus_eIgnore;
3145 nsEventStatus AsyncPanZoomController::OnSingleTapConfirmed(
3146 const TapGestureInput& aEvent) {
3147 APZC_LOG_DETAIL("got a single-tap-confirmed in state %s\n", this,
3148 ToString(mState).c_str());
3149 return GenerateSingleTap(TapType::eSingleTap, aEvent.mPoint,
3150 aEvent.modifiers);
3153 nsEventStatus AsyncPanZoomController::OnDoubleTap(
3154 const TapGestureInput& aEvent) {
3155 APZC_LOG_DETAIL("got a double-tap in state %s\n", this,
3156 ToString(mState).c_str());
3158 MOZ_ASSERT(IsRootForLayersId(),
3159 "This function should be called for the root content APZC or "
3160 "OOPIF root APZC");
3162 CSSToCSSMatrix4x4 transformToRootContentApzc;
3163 RefPtr<AsyncPanZoomController> rootContentApzc;
3164 if (IsRootContent()) {
3165 rootContentApzc = RefPtr{this};
3166 } else {
3167 if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
3168 rootContentApzc = treeManagerLocal->FindZoomableApzc(this);
3169 if (rootContentApzc) {
3170 MOZ_ASSERT(rootContentApzc->GetLayersId() != GetLayersId());
3171 MOZ_ASSERT(this == treeManagerLocal->FindRootApzcFor(GetLayersId()));
3172 transformToRootContentApzc =
3173 treeManagerLocal->GetOopifToRootContentTransform(this);
3175 CSSPoint rootScrollPosition = rootContentApzc->GetLayoutScrollOffset();
3176 transformToRootContentApzc.PostTranslate(rootScrollPosition.x,
3177 rootScrollPosition.y, 0);
3182 if (!rootContentApzc) {
3183 return nsEventStatus_eIgnore;
3186 RefPtr<GeckoContentController> controller = GetGeckoContentController();
3187 if (controller) {
3188 if (rootContentApzc->ZoomConstraintsAllowDoubleTapZoom() &&
3189 (!GetCurrentTouchBlock() ||
3190 GetCurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) {
3191 if (Maybe<LayoutDevicePoint> geckoScreenPoint =
3192 ConvertToGecko(aEvent.mPoint)) {
3193 controller->HandleTap(
3194 TapType::eDoubleTap, *geckoScreenPoint, aEvent.modifiers, GetGuid(),
3195 GetCurrentTouchBlock() ? GetCurrentTouchBlock()->GetBlockId() : 0,
3196 Some(DoubleTapToZoomMetrics{rootContentApzc->GetVisualViewport(),
3197 rootContentApzc->GetScrollableRect(),
3198 transformToRootContentApzc}));
3201 return nsEventStatus_eConsumeNoDefault;
3203 return nsEventStatus_eIgnore;
3206 nsEventStatus AsyncPanZoomController::OnSecondTap(
3207 const TapGestureInput& aEvent) {
3208 APZC_LOG_DETAIL("got a second-tap in state %s\n", this,
3209 ToString(mState).c_str());
3210 return GenerateSingleTap(TapType::eSecondTap, aEvent.mPoint,
3211 aEvent.modifiers);
3214 nsEventStatus AsyncPanZoomController::OnCancelTap(
3215 const TapGestureInput& aEvent) {
3216 APZC_LOG_DETAIL("got a cancel-tap in state %s\n", this,
3217 ToString(mState).c_str());
3218 // XXX: Implement this.
3219 return nsEventStatus_eIgnore;
3222 ScreenToParentLayerMatrix4x4 AsyncPanZoomController::GetTransformToThis()
3223 const {
3224 if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
3225 return treeManagerLocal->GetScreenToApzcTransform(this);
3227 return ScreenToParentLayerMatrix4x4();
3230 ScreenPoint AsyncPanZoomController::ToScreenCoordinates(
3231 const ParentLayerPoint& aVector, const ParentLayerPoint& aAnchor) const {
3232 return TransformVector(GetTransformToThis().Inverse(), aVector, aAnchor);
3235 // TODO: figure out a good way to check the w-coordinate is positive and return
3236 // the result
3237 ParentLayerPoint AsyncPanZoomController::ToParentLayerCoordinates(
3238 const ScreenPoint& aVector, const ScreenPoint& aAnchor) const {
3239 return TransformVector(GetTransformToThis(), aVector, aAnchor);
3242 ParentLayerPoint AsyncPanZoomController::ToParentLayerCoordinates(
3243 const ScreenPoint& aVector, const ExternalPoint& aAnchor) const {
3244 return ToParentLayerCoordinates(
3245 aVector,
3246 ViewAs<ScreenPixel>(aAnchor, PixelCastJustification::ExternalIsScreen));
3249 ExternalPoint AsyncPanZoomController::ToExternalPoint(
3250 const ExternalPoint& aScreenOffset, const ScreenPoint& aScreenPoint) {
3251 return aScreenOffset +
3252 ViewAs<ExternalPixel>(aScreenPoint,
3253 PixelCastJustification::ExternalIsScreen);
3256 ScreenPoint AsyncPanZoomController::PanVector(const ExternalPoint& aPos) const {
3257 return ScreenPoint(fabs(aPos.x - mStartTouch.x),
3258 fabs(aPos.y - mStartTouch.y));
3261 bool AsyncPanZoomController::Contains(const ScreenIntPoint& aPoint) const {
3262 ScreenToParentLayerMatrix4x4 transformToThis = GetTransformToThis();
3263 Maybe<ParentLayerIntPoint> point = UntransformBy(transformToThis, aPoint);
3264 if (!point) {
3265 return false;
3268 ParentLayerIntRect cb;
3270 RecursiveMutexAutoLock lock(mRecursiveMutex);
3271 GetFrameMetrics().GetCompositionBounds().ToIntRect(&cb);
3273 return cb.Contains(*point);
3276 bool AsyncPanZoomController::IsInOverscrollGutter(
3277 const ScreenPoint& aHitTestPoint) const {
3278 if (!IsPhysicallyOverscrolled()) {
3279 return false;
3282 Maybe<ParentLayerPoint> apzcPoint =
3283 UntransformBy(GetTransformToThis(), aHitTestPoint);
3284 if (!apzcPoint) return false;
3285 return IsInOverscrollGutter(*apzcPoint);
3288 bool AsyncPanZoomController::IsInOverscrollGutter(
3289 const ParentLayerPoint& aHitTestPoint) const {
3290 ParentLayerRect compositionBounds;
3292 RecursiveMutexAutoLock lock(mRecursiveMutex);
3293 compositionBounds = GetFrameMetrics().GetCompositionBounds();
3295 if (!compositionBounds.Contains(aHitTestPoint)) {
3296 // Point is outside of scrollable element's bounds altogether.
3297 return false;
3299 auto overscrollTransform = GetOverscrollTransform(eForEventHandling);
3300 ParentLayerPoint overscrollUntransformed =
3301 overscrollTransform.Inverse().TransformPoint(aHitTestPoint);
3303 if (compositionBounds.Contains(overscrollUntransformed)) {
3304 // Point is over scrollable content.
3305 return false;
3308 // Point is in gutter.
3309 return true;
3312 bool AsyncPanZoomController::IsOverscrolled() const {
3313 return mOverscrollEffect->IsOverscrolled();
3316 bool AsyncPanZoomController::IsPhysicallyOverscrolled() const {
3317 // As an optimization, avoid calling Apply/UnapplyAsyncTestAttributes
3318 // unless we're in a test environment where we need it.
3319 if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) {
3320 RecursiveMutexAutoLock lock(mRecursiveMutex);
3321 AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
3322 return mX.IsOverscrolled() || mY.IsOverscrolled();
3324 RecursiveMutexAutoLock lock(mRecursiveMutex);
3325 return mX.IsOverscrolled() || mY.IsOverscrolled();
3328 bool AsyncPanZoomController::IsInInvalidOverscroll() const {
3329 return mX.IsInInvalidOverscroll() || mY.IsInInvalidOverscroll();
3332 ParentLayerPoint AsyncPanZoomController::PanStart() const {
3333 return ParentLayerPoint(mX.PanStart(), mY.PanStart());
3336 const ParentLayerPoint AsyncPanZoomController::GetVelocityVector() const {
3337 RecursiveMutexAutoLock lock(mRecursiveMutex);
3338 return ParentLayerPoint(mX.GetVelocity(), mY.GetVelocity());
3341 void AsyncPanZoomController::SetVelocityVector(
3342 const ParentLayerPoint& aVelocityVector) {
3343 RecursiveMutexAutoLock lock(mRecursiveMutex);
3344 mX.SetVelocity(aVelocityVector.x);
3345 mY.SetVelocity(aVelocityVector.y);
3348 void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle) {
3349 // Handling of cross sliding will need to be added in this method after
3350 // touch-action released enabled by default.
3351 MOZ_ASSERT(GetCurrentTouchBlock());
3352 RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
3353 GetCurrentInputBlock()->GetOverscrollHandoffChain();
3354 bool canScrollHorizontal =
3355 !mX.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
3356 this, ScrollDirection::eHorizontal);
3357 bool canScrollVertical =
3358 !mY.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
3359 this, ScrollDirection::eVertical);
3360 if (GetCurrentTouchBlock()->TouchActionAllowsPanningXY()) {
3361 if (canScrollHorizontal && canScrollVertical) {
3362 if (apz::IsCloseToHorizontal(aAngle,
3363 StaticPrefs::apz_axis_lock_lock_angle())) {
3364 mY.SetAxisLocked(true);
3365 SetState(PANNING_LOCKED_X);
3366 } else if (apz::IsCloseToVertical(
3367 aAngle, StaticPrefs::apz_axis_lock_lock_angle())) {
3368 mX.SetAxisLocked(true);
3369 SetState(PANNING_LOCKED_Y);
3370 } else {
3371 SetState(PANNING);
3373 } else if (canScrollHorizontal || canScrollVertical) {
3374 SetState(PANNING);
3375 } else {
3376 SetState(NOTHING);
3378 } else if (GetCurrentTouchBlock()->TouchActionAllowsPanningX()) {
3379 // Using bigger angle for panning to keep behavior consistent
3380 // with IE.
3381 if (apz::IsCloseToHorizontal(
3382 aAngle, StaticPrefs::apz_axis_lock_direct_pan_angle())) {
3383 mY.SetAxisLocked(true);
3384 SetState(PANNING_LOCKED_X);
3385 mPanDirRestricted = true;
3386 } else {
3387 // Don't treat these touches as pan/zoom movements since 'touch-action'
3388 // value requires it.
3389 SetState(NOTHING);
3391 } else if (GetCurrentTouchBlock()->TouchActionAllowsPanningY()) {
3392 if (apz::IsCloseToVertical(aAngle,
3393 StaticPrefs::apz_axis_lock_direct_pan_angle())) {
3394 mX.SetAxisLocked(true);
3395 SetState(PANNING_LOCKED_Y);
3396 mPanDirRestricted = true;
3397 } else {
3398 SetState(NOTHING);
3400 } else {
3401 SetState(NOTHING);
3403 if (!IsInPanningState()) {
3404 // If we didn't enter a panning state because touch-action disallowed it,
3405 // make sure to clear any leftover velocity from the pre-threshold
3406 // touchmoves.
3407 mX.SetVelocity(0);
3408 mY.SetVelocity(0);
3412 void AsyncPanZoomController::HandlePanning(double aAngle) {
3413 RecursiveMutexAutoLock lock(mRecursiveMutex);
3414 MOZ_ASSERT(GetCurrentInputBlock());
3415 RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
3416 GetCurrentInputBlock()->GetOverscrollHandoffChain();
3417 bool canScrollHorizontal =
3418 !mX.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
3419 this, ScrollDirection::eHorizontal);
3420 bool canScrollVertical =
3421 !mY.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
3422 this, ScrollDirection::eVertical);
3424 MOZ_ASSERT(UsingStatefulAxisLock());
3426 if (!canScrollHorizontal || !canScrollVertical) {
3427 SetState(PANNING);
3428 } else if (apz::IsCloseToHorizontal(
3429 aAngle, StaticPrefs::apz_axis_lock_lock_angle())) {
3430 mY.SetAxisLocked(true);
3431 if (canScrollHorizontal) {
3432 SetState(PANNING_LOCKED_X);
3434 } else if (apz::IsCloseToVertical(aAngle,
3435 StaticPrefs::apz_axis_lock_lock_angle())) {
3436 mX.SetAxisLocked(true);
3437 if (canScrollVertical) {
3438 SetState(PANNING_LOCKED_Y);
3440 } else {
3441 SetState(PANNING);
3445 void AsyncPanZoomController::HandlePanningUpdate(
3446 const ScreenPoint& aPanDistance) {
3447 // If we're axis-locked, check if the user is trying to break the lock
3448 if (GetAxisLockMode() == STICKY && !mPanDirRestricted) {
3449 ParentLayerPoint vector =
3450 ToParentLayerCoordinates(aPanDistance, mStartTouch);
3452 float angle = atan2f(vector.y, vector.x); // range [-pi, pi]
3453 angle = fabsf(angle); // range [0, pi]
3455 float breakThreshold =
3456 StaticPrefs::apz_axis_lock_breakout_threshold() * GetDPI();
3458 if (fabs(aPanDistance.x) > breakThreshold ||
3459 fabs(aPanDistance.y) > breakThreshold) {
3460 switch (mState) {
3461 case PANNING_LOCKED_X:
3462 if (!apz::IsCloseToHorizontal(
3463 angle, StaticPrefs::apz_axis_lock_breakout_angle())) {
3464 mY.SetAxisLocked(false);
3465 // If we are within the lock angle from the Y axis, lock
3466 // onto the Y axis.
3467 if (apz::IsCloseToVertical(
3468 angle, StaticPrefs::apz_axis_lock_lock_angle())) {
3469 mX.SetAxisLocked(true);
3470 SetState(PANNING_LOCKED_Y);
3471 } else {
3472 SetState(PANNING);
3475 break;
3477 case PANNING_LOCKED_Y:
3478 if (!apz::IsCloseToVertical(
3479 angle, StaticPrefs::apz_axis_lock_breakout_angle())) {
3480 mX.SetAxisLocked(false);
3481 // If we are within the lock angle from the X axis, lock
3482 // onto the X axis.
3483 if (apz::IsCloseToHorizontal(
3484 angle, StaticPrefs::apz_axis_lock_lock_angle())) {
3485 mY.SetAxisLocked(true);
3486 SetState(PANNING_LOCKED_X);
3487 } else {
3488 SetState(PANNING);
3491 break;
3493 case PANNING:
3494 HandlePanning(angle);
3495 break;
3497 default:
3498 break;
3504 void AsyncPanZoomController::HandlePinchLocking(
3505 const PinchGestureInput& aEvent) {
3506 // Focus change and span distance calculated from an event buffer
3507 // Used to handle pinch locking irrespective of touch screen sensitivity
3508 // Note: both values fall back to the same value as
3509 // their un-buffered counterparts if there is only one (the latest)
3510 // event in the buffer. ie: when the touch screen is dispatching
3511 // events slower than the lifetime of the buffer
3512 ParentLayerCoord bufferedSpanDistance;
3513 ParentLayerPoint focusPoint, bufferedFocusChange;
3515 RecursiveMutexAutoLock lock(mRecursiveMutex);
3517 focusPoint = mPinchEventBuffer.back().mLocalFocusPoint -
3518 Metrics().GetCompositionBounds().TopLeft();
3519 ParentLayerPoint bufferedLastZoomFocus =
3520 (mPinchEventBuffer.size() > 1)
3521 ? mPinchEventBuffer.front().mLocalFocusPoint -
3522 Metrics().GetCompositionBounds().TopLeft()
3523 : mLastZoomFocus;
3525 bufferedFocusChange = bufferedLastZoomFocus - focusPoint;
3526 bufferedSpanDistance = fabsf(mPinchEventBuffer.front().mPreviousSpan -
3527 mPinchEventBuffer.back().mCurrentSpan);
3530 // Convert to screen coordinates
3531 ScreenCoord spanDistance =
3532 ToScreenCoordinates(ParentLayerPoint(0, bufferedSpanDistance), focusPoint)
3533 .Length();
3534 ScreenPoint focusChange =
3535 ToScreenCoordinates(bufferedFocusChange, focusPoint);
3537 if (mPinchLocked) {
3538 if (GetPinchLockMode() == PINCH_STICKY) {
3539 ScreenCoord spanBreakoutThreshold =
3540 StaticPrefs::apz_pinch_lock_span_breakout_threshold() * GetDPI();
3541 mPinchLocked = !(spanDistance > spanBreakoutThreshold);
3543 } else {
3544 if (GetPinchLockMode() != PINCH_FREE) {
3545 ScreenCoord spanLockThreshold =
3546 StaticPrefs::apz_pinch_lock_span_lock_threshold() * GetDPI();
3547 ScreenCoord scrollLockThreshold =
3548 StaticPrefs::apz_pinch_lock_scroll_lock_threshold() * GetDPI();
3550 if (spanDistance < spanLockThreshold &&
3551 focusChange.Length() > scrollLockThreshold) {
3552 mPinchLocked = true;
3554 // We are transitioning to a two-finger pan that could trigger
3555 // a fling at its end, so start tracking velocity.
3556 StartTouch(aEvent.mLocalFocusPoint, aEvent.mTimeStamp);
3562 nsEventStatus AsyncPanZoomController::StartPanning(
3563 const ExternalPoint& aStartPoint, const TimeStamp& aEventTime) {
3564 ParentLayerPoint vector =
3565 ToParentLayerCoordinates(PanVector(aStartPoint), mStartTouch);
3566 double angle = atan2(vector.y, vector.x); // range [-pi, pi]
3567 angle = fabs(angle); // range [0, pi]
3569 RecursiveMutexAutoLock lock(mRecursiveMutex);
3570 HandlePanningWithTouchAction(angle);
3572 if (IsInPanningState()) {
3573 mTouchStartRestingTimeBeforePan = aEventTime - mTouchStartTime;
3574 mMinimumVelocityDuringPan = Nothing();
3576 if (RefPtr<GeckoContentController> controller =
3577 GetGeckoContentController()) {
3578 controller->NotifyAPZStateChange(GetGuid(),
3579 APZStateChange::eStartPanning);
3581 return nsEventStatus_eConsumeNoDefault;
3583 // Don't consume an event that didn't trigger a panning.
3584 return nsEventStatus_eIgnore;
3587 void AsyncPanZoomController::UpdateWithTouchAtDevicePoint(
3588 const MultiTouchInput& aEvent) {
3589 const SingleTouchData& touchData = aEvent.mTouches[0];
3590 // Take historical touch data into account in order to improve the accuracy
3591 // of the velocity estimate. On many Android devices, the touch screen samples
3592 // at a higher rate than vsync (e.g. 100Hz vs 60Hz), and the historical data
3593 // lets us take advantage of those high-rate samples.
3594 for (const auto& historicalData : touchData.mHistoricalData) {
3595 ParentLayerPoint historicalPoint = historicalData.mLocalScreenPoint;
3596 mX.UpdateWithTouchAtDevicePoint(historicalPoint.x,
3597 historicalData.mTimeStamp);
3598 mY.UpdateWithTouchAtDevicePoint(historicalPoint.y,
3599 historicalData.mTimeStamp);
3601 ParentLayerPoint point = touchData.mLocalScreenPoint;
3602 mX.UpdateWithTouchAtDevicePoint(point.x, aEvent.mTimeStamp);
3603 mY.UpdateWithTouchAtDevicePoint(point.y, aEvent.mTimeStamp);
3606 Maybe<CompositionPayload> AsyncPanZoomController::NotifyScrollSampling() {
3607 RecursiveMutexAutoLock lock(mRecursiveMutex);
3608 return mSampledState.front().TakeScrollPayload();
3611 bool AsyncPanZoomController::AttemptScroll(
3612 ParentLayerPoint& aStartPoint, ParentLayerPoint& aEndPoint,
3613 OverscrollHandoffState& aOverscrollHandoffState) {
3614 // "start - end" rather than "end - start" because e.g. moving your finger
3615 // down (*positive* direction along y axis) causes the vertical scroll offset
3616 // to *decrease* as the page follows your finger.
3617 ParentLayerPoint displacement = aStartPoint - aEndPoint;
3619 ParentLayerPoint overscroll; // will be used outside monitor block
3621 // If the direction of panning is reversed within the same input block,
3622 // a later event in the block could potentially scroll an APZC earlier
3623 // in the handoff chain, than an earlier event in the block (because
3624 // the earlier APZC was scrolled to its extent in the original direction).
3625 // We want to disallow this.
3626 bool scrollThisApzc = false;
3627 if (InputBlockState* block = GetCurrentInputBlock()) {
3628 scrollThisApzc =
3629 !block->GetScrolledApzc() || block->IsDownchainOfScrolledApzc(this);
3632 ParentLayerPoint adjustedDisplacement;
3633 if (scrollThisApzc) {
3634 RecursiveMutexAutoLock lock(mRecursiveMutex);
3635 bool respectDisregardedDirections =
3636 ScrollSourceRespectsDisregardedDirections(
3637 aOverscrollHandoffState.mScrollSource);
3638 bool forcesVerticalOverscroll = respectDisregardedDirections &&
3639 mScrollMetadata.GetDisregardedDirection() ==
3640 Some(ScrollDirection::eVertical);
3641 bool forcesHorizontalOverscroll =
3642 respectDisregardedDirections &&
3643 mScrollMetadata.GetDisregardedDirection() ==
3644 Some(ScrollDirection::eHorizontal);
3646 bool yChanged =
3647 mY.AdjustDisplacement(displacement.y, adjustedDisplacement.y,
3648 overscroll.y, forcesVerticalOverscroll);
3649 bool xChanged =
3650 mX.AdjustDisplacement(displacement.x, adjustedDisplacement.x,
3651 overscroll.x, forcesHorizontalOverscroll);
3652 if (xChanged || yChanged) {
3653 ScheduleComposite();
3656 if (!IsZero(adjustedDisplacement) &&
3657 Metrics().GetZoom() != CSSToParentLayerScale(0)) {
3658 ScrollBy(adjustedDisplacement / Metrics().GetZoom());
3659 if (InputBlockState* block = GetCurrentInputBlock()) {
3660 bool displacementIsUserVisible = true;
3662 { // Release the APZC lock before calling ToScreenCoordinates which
3663 // acquires the APZ tree lock. Note that this just unlocks the mutex
3664 // once, so if we're locking it multiple times on the callstack then
3665 // this will be insufficient.
3666 RecursiveMutexAutoUnlock unlock(mRecursiveMutex);
3668 ScreenIntPoint screenDisplacement = RoundedToInt(
3669 ToScreenCoordinates(adjustedDisplacement, aStartPoint));
3670 // If the displacement we just applied rounds to zero in screen space,
3671 // then it's probably not going to be visible to the user. In that
3672 // case let's not mark this APZC as scrolled, so that even if the
3673 // immediate handoff pref is disabled, we'll allow doing the handoff
3674 // to the next APZC.
3675 if (screenDisplacement == ScreenIntPoint()) {
3676 displacementIsUserVisible = false;
3679 if (displacementIsUserVisible) {
3680 block->SetScrolledApzc(this);
3683 // Note that in the case of instant scrolling, the last snap target ids
3684 // will be set after AttemptScroll call so that we can clobber them
3685 // unconditionally here.
3686 mLastSnapTargetIds = ScrollSnapTargetIds{};
3687 ScheduleCompositeAndMaybeRepaint();
3690 // Adjust the start point to reflect the consumed portion of the scroll.
3691 aStartPoint = aEndPoint + overscroll;
3692 } else {
3693 overscroll = displacement;
3696 // Accumulate the amount of actual scrolling that occurred into the handoff
3697 // state. Note that ToScreenCoordinates() needs to be called outside the
3698 // mutex.
3699 if (!IsZero(adjustedDisplacement)) {
3700 aOverscrollHandoffState.mTotalMovement +=
3701 ToScreenCoordinates(adjustedDisplacement, aEndPoint);
3704 // If we consumed the entire displacement as a normal scroll, great.
3705 if (IsZero(overscroll)) {
3706 return true;
3709 if (AllowScrollHandoffInCurrentBlock()) {
3710 // If there is overscroll, first try to hand it off to an APZC later
3711 // in the handoff chain to consume (either as a normal scroll or as
3712 // overscroll).
3713 // Note: "+ overscroll" rather than "- overscroll" because "overscroll"
3714 // is what's left of "displacement", and "displacement" is "start - end".
3715 ++aOverscrollHandoffState.mChainIndex;
3716 bool consumed =
3717 CallDispatchScroll(aStartPoint, aEndPoint, aOverscrollHandoffState);
3718 if (consumed) {
3719 return true;
3722 overscroll = aStartPoint - aEndPoint;
3723 MOZ_ASSERT(!IsZero(overscroll));
3726 // If there is no APZC later in the handoff chain that accepted the
3727 // overscroll, try to accept it ourselves. We only accept it if we
3728 // are pannable.
3729 if (ScrollSourceAllowsOverscroll(aOverscrollHandoffState.mScrollSource)) {
3730 APZC_LOG("%p taking overscroll during panning\n", this);
3732 ParentLayerPoint prevVisualOverscroll = GetOverscrollAmount();
3734 OverscrollForPanning(overscroll, aOverscrollHandoffState.mPanDistance);
3736 // Accumulate the amount of change to the overscroll that occurred into the
3737 // handoff state. Note that the input amount, |overscroll|, is turned into
3738 // some smaller visual overscroll amount (queried via GetOverscrollAmount())
3739 // by applying resistance (Axis::ApplyResistance()), and it's the latter we
3740 // want to count towards OverscrollHandoffState::mTotalMovement.
3741 ParentLayerPoint visualOverscrollChange =
3742 GetOverscrollAmount() - prevVisualOverscroll;
3743 if (!IsZero(visualOverscrollChange)) {
3744 aOverscrollHandoffState.mTotalMovement +=
3745 ToScreenCoordinates(visualOverscrollChange, aEndPoint);
3749 aStartPoint = aEndPoint + overscroll;
3751 return IsZero(overscroll);
3754 void AsyncPanZoomController::OverscrollForPanning(
3755 ParentLayerPoint& aOverscroll, const ScreenPoint& aPanDistance) {
3756 // Only allow entering overscroll along an axis if the pan distance along
3757 // that axis is greater than the pan distance along the other axis by a
3758 // configurable factor. If we are already overscrolled, don't check this.
3759 if (!IsOverscrolled()) {
3760 if (aPanDistance.x <
3761 StaticPrefs::apz_overscroll_min_pan_distance_ratio() * aPanDistance.y) {
3762 aOverscroll.x = 0;
3764 if (aPanDistance.y <
3765 StaticPrefs::apz_overscroll_min_pan_distance_ratio() * aPanDistance.x) {
3766 aOverscroll.y = 0;
3770 OverscrollBy(aOverscroll);
3773 ScrollDirections AsyncPanZoomController::GetOverscrollableDirections() const {
3774 ScrollDirections result;
3776 RecursiveMutexAutoLock lock(mRecursiveMutex);
3778 // If the target has the disregarded direction, it means it's single line
3779 // text control, thus we don't want to overscroll in both directions.
3780 if (mScrollMetadata.GetDisregardedDirection()) {
3781 return result;
3784 if (mX.CanScroll() && mX.OverscrollBehaviorAllowsOverscrollEffect()) {
3785 result += ScrollDirection::eHorizontal;
3788 if (mY.CanScroll() && mY.OverscrollBehaviorAllowsOverscrollEffect()) {
3789 result += ScrollDirection::eVertical;
3792 return result;
3795 void AsyncPanZoomController::OverscrollBy(ParentLayerPoint& aOverscroll) {
3796 if (!StaticPrefs::apz_overscroll_enabled()) {
3797 return;
3800 RecursiveMutexAutoLock lock(mRecursiveMutex);
3801 // Do not go into overscroll in a direction in which we have no room to
3802 // scroll to begin with.
3803 ScrollDirections overscrollableDirections = GetOverscrollableDirections();
3804 if (IsZero(aOverscroll.x)) {
3805 overscrollableDirections -= ScrollDirection::eHorizontal;
3807 if (IsZero(aOverscroll.y)) {
3808 overscrollableDirections -= ScrollDirection::eVertical;
3811 mOverscrollEffect->ConsumeOverscroll(aOverscroll, overscrollableDirections);
3814 RefPtr<const OverscrollHandoffChain>
3815 AsyncPanZoomController::BuildOverscrollHandoffChain() {
3816 if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
3817 return treeManagerLocal->BuildOverscrollHandoffChain(this);
3820 // This APZC IsDestroyed(). To avoid callers having to special-case this
3821 // scenario, just build a 1-element chain containing ourselves.
3822 OverscrollHandoffChain* result = new OverscrollHandoffChain;
3823 result->Add(this);
3824 return result;
3827 ParentLayerPoint AsyncPanZoomController::AttemptFling(
3828 const FlingHandoffState& aHandoffState) {
3829 // The PLPPI computation acquires the tree lock, so it needs to be performed
3830 // on the controller thread, and before the APZC lock is acquired.
3831 APZThreadUtils::AssertOnControllerThread();
3832 float PLPPI = ComputePLPPI(PanStart(), aHandoffState.mVelocity);
3834 RecursiveMutexAutoLock lock(mRecursiveMutex);
3836 if (!IsPannable()) {
3837 return aHandoffState.mVelocity;
3840 // We may have a pre-existing velocity for whatever reason (for example,
3841 // a previously handed off fling). We don't want to clobber that.
3842 APZC_LOG("%p accepting fling with velocity %s\n", this,
3843 ToString(aHandoffState.mVelocity).c_str());
3844 ParentLayerPoint residualVelocity = aHandoffState.mVelocity;
3845 if (mX.CanScroll()) {
3846 mX.SetVelocity(mX.GetVelocity() + aHandoffState.mVelocity.x);
3847 residualVelocity.x = 0;
3849 if (mY.CanScroll()) {
3850 mY.SetVelocity(mY.GetVelocity() + aHandoffState.mVelocity.y);
3851 residualVelocity.y = 0;
3854 // If we're not scrollable in at least one of the directions in which we
3855 // were handed velocity, don't start a fling animation.
3856 // The |IsFinite()| condition should only fail when running some tests
3857 // that generate events faster than the clock resolution.
3858 ParentLayerPoint velocity = GetVelocityVector();
3859 if (!velocity.IsFinite() ||
3860 velocity.Length() <= StaticPrefs::apz_fling_min_velocity_threshold()) {
3861 // Relieve overscroll now if needed, since we will not transition to a fling
3862 // animation and then an overscroll animation, and relieve it then.
3863 aHandoffState.mChain->SnapBackOverscrolledApzc(this);
3864 return residualVelocity;
3867 // If there's a scroll snap point near the predicted fling destination,
3868 // scroll there using a smooth scroll animation. Otherwise, start a
3869 // fling animation.
3870 ScrollSnapToDestination();
3871 if (mState != SMOOTHMSD_SCROLL) {
3872 SetState(FLING);
3873 RefPtr<AsyncPanZoomAnimation> fling =
3874 GetPlatformSpecificState()->CreateFlingAnimation(*this, aHandoffState,
3875 PLPPI);
3876 StartAnimation(fling.forget());
3879 return residualVelocity;
3882 float AsyncPanZoomController::ComputePLPPI(ParentLayerPoint aPoint,
3883 ParentLayerPoint aDirection) const {
3884 // Avoid division-by-zero.
3885 if (aDirection == ParentLayerPoint()) {
3886 return GetDPI();
3889 // Convert |aDirection| into a unit vector.
3890 aDirection = aDirection / aDirection.Length();
3892 // Place the vector at |aPoint| and convert to screen coordinates.
3893 // The length of the resulting vector is the number of Screen coordinates
3894 // that equal 1 ParentLayer coordinate in the given direction.
3895 float screenPerParent = ToScreenCoordinates(aDirection, aPoint).Length();
3897 // Finally, factor in the DPI scale.
3898 return GetDPI() / screenPerParent;
3901 Maybe<CSSPoint> AsyncPanZoomController::GetCurrentAnimationDestination(
3902 const RecursiveMutexAutoLock& aProofOfLock) const {
3903 if (mState == WHEEL_SCROLL) {
3904 return Some(mAnimation->AsWheelScrollAnimation()->GetDestination());
3906 if (mState == SMOOTH_SCROLL) {
3907 return Some(mAnimation->AsSmoothScrollAnimation()->GetDestination());
3909 if (mState == SMOOTHMSD_SCROLL) {
3910 return Some(mAnimation->AsSmoothMsdScrollAnimation()->GetDestination());
3912 if (mState == KEYBOARD_SCROLL) {
3913 return Some(mAnimation->AsSmoothScrollAnimation()->GetDestination());
3916 return Nothing();
3919 ParentLayerPoint
3920 AsyncPanZoomController::AdjustHandoffVelocityForOverscrollBehavior(
3921 ParentLayerPoint& aHandoffVelocity) const {
3922 ParentLayerPoint residualVelocity;
3923 ScrollDirections handoffDirections = GetAllowedHandoffDirections();
3924 if (!handoffDirections.contains(ScrollDirection::eHorizontal)) {
3925 residualVelocity.x = aHandoffVelocity.x;
3926 aHandoffVelocity.x = 0;
3928 if (!handoffDirections.contains(ScrollDirection::eVertical)) {
3929 residualVelocity.y = aHandoffVelocity.y;
3930 aHandoffVelocity.y = 0;
3932 return residualVelocity;
3935 bool AsyncPanZoomController::OverscrollBehaviorAllowsSwipe() const {
3936 // Swipe navigation is a "non-local" overscroll behavior like handoff.
3937 return GetAllowedHandoffDirections().contains(ScrollDirection::eHorizontal);
3940 void AsyncPanZoomController::HandleFlingOverscroll(
3941 const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits,
3942 const RefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
3943 const RefPtr<const AsyncPanZoomController>& aScrolledApzc) {
3944 APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
3945 if (treeManagerLocal) {
3946 const FlingHandoffState handoffState{
3947 aVelocity, aOverscrollHandoffChain, Nothing(),
3948 0, true /* handoff */, aScrolledApzc};
3949 ParentLayerPoint residualVelocity =
3950 treeManagerLocal->DispatchFling(this, handoffState);
3951 FLING_LOG("APZC %p left with residual velocity %s\n", this,
3952 ToString(residualVelocity).c_str());
3953 if (!IsZero(residualVelocity) && IsPannable() &&
3954 StaticPrefs::apz_overscroll_enabled()) {
3955 // Obey overscroll-behavior.
3956 RecursiveMutexAutoLock lock(mRecursiveMutex);
3957 if (!mX.OverscrollBehaviorAllowsOverscrollEffect()) {
3958 residualVelocity.x = 0;
3960 if (!mY.OverscrollBehaviorAllowsOverscrollEffect()) {
3961 residualVelocity.y = 0;
3964 if (!IsZero(residualVelocity)) {
3965 mOverscrollEffect->RelieveOverscroll(residualVelocity,
3966 aOverscrollSideBits);
3972 void AsyncPanZoomController::HandleSmoothScrollOverscroll(
3973 const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits) {
3974 // We must call BuildOverscrollHandoffChain from this deferred callback
3975 // function in order to avoid a deadlock when acquiring the tree lock.
3976 HandleFlingOverscroll(aVelocity, aOverscrollSideBits,
3977 BuildOverscrollHandoffChain(), nullptr);
3980 ParentLayerPoint AsyncPanZoomController::ConvertDestinationToDelta(
3981 CSSPoint& aDestination) const {
3982 ParentLayerPoint startPoint, endPoint;
3985 RecursiveMutexAutoLock lock(mRecursiveMutex);
3987 startPoint = aDestination * Metrics().GetZoom();
3988 endPoint = Metrics().GetVisualScrollOffset() * Metrics().GetZoom();
3991 return startPoint - endPoint;
3994 void AsyncPanZoomController::SmoothScrollTo(
3995 CSSSnapDestination&& aDestination,
3996 ScrollTriggeredByScript aTriggeredByScript, const ScrollOrigin& aOrigin) {
3997 // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and then
3998 // to appunits/second.
3999 nsPoint destination = CSSPoint::ToAppUnits(aDestination.mPosition);
4000 nsSize velocity;
4001 if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
4002 velocity = CSSSize::ToAppUnits(ParentLayerSize(mX.GetVelocity() * 1000.0f,
4003 mY.GetVelocity() * 1000.0f) /
4004 Metrics().GetZoom());
4007 if (mState == SMOOTH_SCROLL && mAnimation) {
4008 RefPtr<SmoothScrollAnimation> animation(
4009 mAnimation->AsSmoothScrollAnimation());
4010 if (animation->GetScrollOrigin() == aOrigin) {
4011 APZC_LOG("%p updating destination on existing animation\n", this);
4012 animation->UpdateDestinationAndSnapTargets(
4013 GetFrameTime().Time(), destination, velocity,
4014 std::move(aDestination.mTargetIds), aTriggeredByScript);
4015 return;
4019 CancelAnimation();
4021 // If no scroll is required, we should exit early to avoid triggering
4022 // a scrollend event when no scrolling occurred.
4023 if (ConvertDestinationToDelta(aDestination.mPosition) == ParentLayerPoint()) {
4024 return;
4027 SetState(SMOOTH_SCROLL);
4028 nsPoint initialPosition =
4029 CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset());
4030 RefPtr<SmoothScrollAnimation> animation =
4031 new SmoothScrollAnimation(*this, initialPosition, aOrigin);
4032 animation->UpdateDestinationAndSnapTargets(
4033 GetFrameTime().Time(), destination, velocity,
4034 std::move(aDestination.mTargetIds), aTriggeredByScript);
4035 StartAnimation(animation.forget());
4038 void AsyncPanZoomController::SmoothMsdScrollTo(
4039 CSSSnapDestination&& aDestination,
4040 ScrollTriggeredByScript aTriggeredByScript) {
4041 if (mState == SMOOTHMSD_SCROLL && mAnimation) {
4042 APZC_LOG("%p updating destination on existing animation\n", this);
4043 RefPtr<SmoothMsdScrollAnimation> animation(
4044 static_cast<SmoothMsdScrollAnimation*>(mAnimation.get()));
4045 animation->SetDestination(aDestination.mPosition,
4046 std::move(aDestination.mTargetIds),
4047 aTriggeredByScript);
4048 return;
4051 // If no scroll is required, we should exit early to avoid triggering
4052 // a scrollend event when no scrolling occurred.
4053 if (ConvertDestinationToDelta(aDestination.mPosition) == ParentLayerPoint()) {
4054 return;
4056 CancelAnimation();
4057 SetState(SMOOTHMSD_SCROLL);
4058 // Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s.
4059 CSSPoint initialVelocity;
4060 if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
4061 initialVelocity = ParentLayerPoint(mX.GetVelocity() * 1000.0f,
4062 mY.GetVelocity() * 1000.0f) /
4063 Metrics().GetZoom();
4066 StartAnimation(do_AddRef(new SmoothMsdScrollAnimation(
4067 *this, Metrics().GetVisualScrollOffset(), initialVelocity,
4068 aDestination.mPosition,
4069 StaticPrefs::layout_css_scroll_behavior_spring_constant(),
4070 StaticPrefs::layout_css_scroll_behavior_damping_ratio(),
4071 std::move(aDestination.mTargetIds), aTriggeredByScript)));
4074 void AsyncPanZoomController::StartOverscrollAnimation(
4075 const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits) {
4076 MOZ_ASSERT(mState != OVERSCROLL_ANIMATION);
4078 SetState(OVERSCROLL_ANIMATION);
4080 ParentLayerPoint velocity = aVelocity;
4081 AdjustDeltaForAllowedScrollDirections(velocity,
4082 GetOverscrollableDirections());
4083 StartAnimation(
4084 do_AddRef(new OverscrollAnimation(*this, velocity, aOverscrollSideBits)));
4087 bool AsyncPanZoomController::CallDispatchScroll(
4088 ParentLayerPoint& aStartPoint, ParentLayerPoint& aEndPoint,
4089 OverscrollHandoffState& aOverscrollHandoffState) {
4090 // Make a local copy of the tree manager pointer and check if it's not
4091 // null before calling DispatchScroll(). This is necessary because
4092 // Destroy(), which nulls out mTreeManager, could be called concurrently.
4093 APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
4094 if (!treeManagerLocal) {
4095 return false;
4098 // Obey overscroll-behavior.
4099 ParentLayerPoint endPoint = aEndPoint;
4100 if (aOverscrollHandoffState.mChainIndex > 0) {
4101 ScrollDirections handoffDirections = GetAllowedHandoffDirections();
4102 if (!handoffDirections.contains(ScrollDirection::eHorizontal)) {
4103 endPoint.x = aStartPoint.x;
4105 if (!handoffDirections.contains(ScrollDirection::eVertical)) {
4106 endPoint.y = aStartPoint.y;
4108 if (aStartPoint == endPoint) {
4109 // Handoff not allowed in either direction - don't even bother.
4110 return false;
4114 return treeManagerLocal->DispatchScroll(this, aStartPoint, endPoint,
4115 aOverscrollHandoffState);
4118 void AsyncPanZoomController::RecordScrollPayload(const TimeStamp& aTimeStamp) {
4119 RecursiveMutexAutoLock lock(mRecursiveMutex);
4120 if (!mScrollPayload) {
4121 mScrollPayload = Some(
4122 CompositionPayload{CompositionPayloadType::eAPZScroll, aTimeStamp});
4126 void AsyncPanZoomController::StartTouch(const ParentLayerPoint& aPoint,
4127 TimeStamp aTimestamp) {
4128 RecursiveMutexAutoLock lock(mRecursiveMutex);
4129 mX.StartTouch(aPoint.x, aTimestamp);
4130 mY.StartTouch(aPoint.y, aTimestamp);
4133 void AsyncPanZoomController::EndTouch(TimeStamp aTimestamp,
4134 Axis::ClearAxisLock aClearAxisLock) {
4135 RecursiveMutexAutoLock lock(mRecursiveMutex);
4136 mX.EndTouch(aTimestamp, aClearAxisLock);
4137 mY.EndTouch(aTimestamp, aClearAxisLock);
4140 void AsyncPanZoomController::TrackTouch(const MultiTouchInput& aEvent) {
4141 ExternalPoint extPoint = GetFirstExternalTouchPoint(aEvent);
4142 ScreenPoint panVector = PanVector(extPoint);
4143 HandlePanningUpdate(panVector);
4145 ParentLayerPoint prevTouchPoint(mX.GetPos(), mY.GetPos());
4146 ParentLayerPoint touchPoint = GetFirstTouchPoint(aEvent);
4148 UpdateWithTouchAtDevicePoint(aEvent);
4150 auto velocity = GetVelocityVector().Length();
4151 if (mMinimumVelocityDuringPan) {
4152 mMinimumVelocityDuringPan =
4153 Some(std::min(*mMinimumVelocityDuringPan, velocity));
4154 } else {
4155 mMinimumVelocityDuringPan = Some(velocity);
4158 if (prevTouchPoint != touchPoint) {
4159 MOZ_ASSERT(GetCurrentTouchBlock());
4160 OverscrollHandoffState handoffState(
4161 *GetCurrentTouchBlock()->GetOverscrollHandoffChain(), panVector,
4162 ScrollSource::Touchscreen);
4163 RecordScrollPayload(aEvent.mTimeStamp);
4164 CallDispatchScroll(prevTouchPoint, touchPoint, handoffState);
4168 ParentLayerPoint AsyncPanZoomController::GetFirstTouchPoint(
4169 const MultiTouchInput& aEvent) {
4170 return ((SingleTouchData&)aEvent.mTouches[0]).mLocalScreenPoint;
4173 ExternalPoint AsyncPanZoomController::GetFirstExternalTouchPoint(
4174 const MultiTouchInput& aEvent) {
4175 return ToExternalPoint(aEvent.mScreenOffset,
4176 ((SingleTouchData&)aEvent.mTouches[0]).mScreenPoint);
4179 ParentLayerPoint AsyncPanZoomController::GetOverscrollAmount() const {
4180 if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) {
4181 RecursiveMutexAutoLock lock(mRecursiveMutex);
4182 AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
4183 return GetOverscrollAmountInternal();
4185 RecursiveMutexAutoLock lock(mRecursiveMutex);
4186 return GetOverscrollAmountInternal();
4189 ParentLayerPoint AsyncPanZoomController::GetOverscrollAmountInternal() const {
4190 return {mX.GetOverscroll(), mY.GetOverscroll()};
4193 SideBits AsyncPanZoomController::GetOverscrollSideBits() const {
4194 return apz::GetOverscrollSideBits({mX.GetOverscroll(), mY.GetOverscroll()});
4197 void AsyncPanZoomController::RestoreOverscrollAmount(
4198 const ParentLayerPoint& aOverscroll) {
4199 mX.RestoreOverscroll(aOverscroll.x);
4200 mY.RestoreOverscroll(aOverscroll.y);
4203 void AsyncPanZoomController::StartAnimation(
4204 already_AddRefed<AsyncPanZoomAnimation> aAnimation) {
4205 RecursiveMutexAutoLock lock(mRecursiveMutex);
4206 mAnimation = aAnimation;
4207 mLastSampleTime = GetFrameTime();
4208 ScheduleComposite();
4211 void AsyncPanZoomController::CancelAnimation(CancelAnimationFlags aFlags) {
4212 RecursiveMutexAutoLock lock(mRecursiveMutex);
4213 APZC_LOG_DETAIL("running CancelAnimation(0x%x) in state %s\n", this, aFlags,
4214 ToString(mState).c_str());
4216 if ((aFlags & ExcludeWheel) && mState == WHEEL_SCROLL) {
4217 return;
4220 if (mAnimation) {
4221 mAnimation->Cancel(aFlags);
4224 SetState(NOTHING);
4225 mLastSnapTargetIds = ScrollSnapTargetIds{};
4226 mAnimation = nullptr;
4227 // Since there is no animation in progress now the axes should
4228 // have no velocity either. If we are dropping the velocity from a non-zero
4229 // value we should trigger a repaint as the displayport margins are dependent
4230 // on the velocity and the last repaint request might not have good margins
4231 // any more.
4232 bool repaint = !IsZero(GetVelocityVector());
4233 mX.SetVelocity(0);
4234 mY.SetVelocity(0);
4235 mX.SetAxisLocked(false);
4236 mY.SetAxisLocked(false);
4237 // Setting the state to nothing and cancelling the animation can
4238 // preempt normal mechanisms for relieving overscroll, so we need to clear
4239 // overscroll here.
4240 if (!(aFlags & ExcludeOverscroll) && IsOverscrolled()) {
4241 ClearOverscroll();
4242 repaint = true;
4244 // Similar to relieving overscroll, we also need to snap to any snap points
4245 // if appropriate.
4246 if (aFlags & CancelAnimationFlags::ScrollSnap) {
4247 ScrollSnap(ScrollSnapFlags::IntendedEndPosition);
4249 if (repaint) {
4250 RequestContentRepaint();
4251 ScheduleComposite();
4255 void AsyncPanZoomController::ClearOverscroll() {
4256 mOverscrollEffect->ClearOverscroll();
4259 void AsyncPanZoomController::ClearPhysicalOverscroll() {
4260 RecursiveMutexAutoLock lock(mRecursiveMutex);
4261 mX.ClearOverscroll();
4262 mY.ClearOverscroll();
4265 void AsyncPanZoomController::SetCompositorController(
4266 CompositorController* aCompositorController) {
4267 mCompositorController = aCompositorController;
4270 void AsyncPanZoomController::SetVisualScrollOffset(const CSSPoint& aOffset) {
4271 Metrics().SetVisualScrollOffset(aOffset);
4272 Metrics().RecalculateLayoutViewportOffset();
4275 void AsyncPanZoomController::ClampAndSetVisualScrollOffset(
4276 const CSSPoint& aOffset) {
4277 Metrics().ClampAndSetVisualScrollOffset(aOffset);
4278 Metrics().RecalculateLayoutViewportOffset();
4281 void AsyncPanZoomController::ScrollBy(const CSSPoint& aOffset) {
4282 SetVisualScrollOffset(Metrics().GetVisualScrollOffset() + aOffset);
4285 void AsyncPanZoomController::ScrollByAndClamp(const CSSPoint& aOffset) {
4286 ClampAndSetVisualScrollOffset(Metrics().GetVisualScrollOffset() + aOffset);
4289 void AsyncPanZoomController::ScaleWithFocus(float aScale,
4290 const CSSPoint& aFocus) {
4291 Metrics().ZoomBy(aScale);
4292 // We want to adjust the scroll offset such that the CSS point represented by
4293 // aFocus remains at the same position on the screen before and after the
4294 // change in zoom. The below code accomplishes this; see
4295 // https://bugzilla.mozilla.org/show_bug.cgi?id=923431#c6 for an in-depth
4296 // explanation of how.
4297 SetVisualScrollOffset((Metrics().GetVisualScrollOffset() + aFocus) -
4298 (aFocus / aScale));
4301 /*static*/
4302 gfx::IntSize AsyncPanZoomController::GetDisplayportAlignmentMultiplier(
4303 const ScreenSize& aBaseSize) {
4304 gfx::IntSize multiplier(1, 1);
4305 float baseWidth = aBaseSize.width;
4306 while (baseWidth > 500) {
4307 baseWidth /= 2;
4308 multiplier.width *= 2;
4309 if (multiplier.width >= 8) {
4310 break;
4313 float baseHeight = aBaseSize.height;
4314 while (baseHeight > 500) {
4315 baseHeight /= 2;
4316 multiplier.height *= 2;
4317 if (multiplier.height >= 8) {
4318 break;
4321 return multiplier;
4325 * Enlarges the displayport along both axes based on the velocity.
4327 static CSSSize CalculateDisplayPortSize(
4328 const CSSSize& aCompositionSize, const CSSPoint& aVelocity,
4329 AsyncPanZoomController::ZoomInProgress aZoomInProgress,
4330 const CSSToScreenScale2D& aDpPerCSS) {
4331 bool xIsStationarySpeed =
4332 fabsf(aVelocity.x) < StaticPrefs::apz_min_skate_speed();
4333 bool yIsStationarySpeed =
4334 fabsf(aVelocity.y) < StaticPrefs::apz_min_skate_speed();
4335 float xMultiplier = xIsStationarySpeed
4336 ? StaticPrefs::apz_x_stationary_size_multiplier()
4337 : StaticPrefs::apz_x_skate_size_multiplier();
4338 float yMultiplier = yIsStationarySpeed
4339 ? StaticPrefs::apz_y_stationary_size_multiplier()
4340 : StaticPrefs::apz_y_skate_size_multiplier();
4342 if (IsHighMemSystem() && !xIsStationarySpeed) {
4343 xMultiplier += StaticPrefs::apz_x_skate_highmem_adjust();
4346 if (IsHighMemSystem() && !yIsStationarySpeed) {
4347 yMultiplier += StaticPrefs::apz_y_skate_highmem_adjust();
4350 if (aZoomInProgress == AsyncPanZoomController::ZoomInProgress::Yes) {
4351 // If a zoom is in progress, we will be making content visible on the
4352 // x and y axes in equal proportion, because the zoom operation scales
4353 // equally on the x and y axes. The default multipliers computed above are
4354 // biased towards the y-axis since that's where most scrolling occurs, but
4355 // in the case of zooming, we should really use equal multipliers on both
4356 // axes. This does that while preserving the total displayport area
4357 // quantity (aCompositionSize.Area() * xMultiplier * yMultiplier).
4358 // Note that normally changing the shape of the displayport is expensive
4359 // and should be avoided, but if a zoom is in progress the displayport
4360 // is likely going to be fully repainted anyway due to changes in resolution
4361 // so there should be no marginal cost to also changing the shape of it.
4362 float areaMultiplier = xMultiplier * yMultiplier;
4363 xMultiplier = sqrt(areaMultiplier);
4364 yMultiplier = xMultiplier;
4367 // Scale down the margin multipliers by the alignment multiplier because
4368 // the alignment code will expand the displayport outward to the multiplied
4369 // alignment. This is not necessary for correctness, but for performance;
4370 // if we don't do this the displayport can end up much larger. The math here
4371 // is actually just scaling the part of the multipler that is > 1, so that
4372 // we never end up with xMultiplier or yMultiplier being less than 1 (that
4373 // would result in a guaranteed checkerboarding situation). Note that the
4374 // calculation doesn't cancel exactly the increased margin from applying
4375 // the alignment multiplier, but this is simple and should provide
4376 // reasonable behaviour in most cases.
4377 gfx::IntSize alignmentMultipler =
4378 AsyncPanZoomController::GetDisplayportAlignmentMultiplier(
4379 aCompositionSize * aDpPerCSS);
4380 if (xMultiplier > 1) {
4381 xMultiplier = ((xMultiplier - 1) / alignmentMultipler.width) + 1;
4383 if (yMultiplier > 1) {
4384 yMultiplier = ((yMultiplier - 1) / alignmentMultipler.height) + 1;
4387 return aCompositionSize * CSSSize(xMultiplier, yMultiplier);
4391 * Ensures that the displayport is at least as large as the visible area
4392 * inflated by the danger zone. If this is not the case then the
4393 * "AboutToCheckerboard" function in TiledContentClient.cpp will return true
4394 * even in the stable state.
4396 static CSSSize ExpandDisplayPortToDangerZone(
4397 const CSSSize& aDisplayPortSize, const FrameMetrics& aFrameMetrics) {
4398 CSSSize dangerZone(0.0f, 0.0f);
4399 if (aFrameMetrics.DisplayportPixelsPerCSSPixel().xScale != 0 &&
4400 aFrameMetrics.DisplayportPixelsPerCSSPixel().yScale != 0) {
4401 dangerZone = ScreenSize(StaticPrefs::apz_danger_zone_x(),
4402 StaticPrefs::apz_danger_zone_y()) /
4403 aFrameMetrics.DisplayportPixelsPerCSSPixel();
4405 const CSSSize compositionSize =
4406 aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels();
4408 const float xSize = std::max(aDisplayPortSize.width,
4409 compositionSize.width + (2 * dangerZone.width));
4411 const float ySize =
4412 std::max(aDisplayPortSize.height,
4413 compositionSize.height + (2 * dangerZone.height));
4415 return CSSSize(xSize, ySize);
4419 * Attempts to redistribute any area in the displayport that would get clipped
4420 * by the scrollable rect, or be inaccessible due to disabled scrolling, to the
4421 * other axis, while maintaining total displayport area.
4423 static void RedistributeDisplayPortExcess(CSSSize& aDisplayPortSize,
4424 const CSSRect& aScrollableRect) {
4425 // As aDisplayPortSize.height * aDisplayPortSize.width does not change,
4426 // we are just scaling by the ratio and its inverse.
4427 if (aDisplayPortSize.height > aScrollableRect.Height()) {
4428 aDisplayPortSize.width *=
4429 (aDisplayPortSize.height / aScrollableRect.Height());
4430 aDisplayPortSize.height = aScrollableRect.Height();
4431 } else if (aDisplayPortSize.width > aScrollableRect.Width()) {
4432 aDisplayPortSize.height *=
4433 (aDisplayPortSize.width / aScrollableRect.Width());
4434 aDisplayPortSize.width = aScrollableRect.Width();
4438 /* static */
4439 const ScreenMargin AsyncPanZoomController::CalculatePendingDisplayPort(
4440 const FrameMetrics& aFrameMetrics, const ParentLayerPoint& aVelocity,
4441 ZoomInProgress aZoomInProgress) {
4442 if (aFrameMetrics.IsScrollInfoLayer()) {
4443 // Don't compute margins. Since we can't asynchronously scroll this frame,
4444 // we don't want to paint anything more than the composition bounds.
4445 return ScreenMargin();
4448 CSSSize compositionSize =
4449 aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels();
4450 CSSPoint velocity;
4451 if (aFrameMetrics.GetZoom() != CSSToParentLayerScale(0)) {
4452 velocity = aVelocity / aFrameMetrics.GetZoom(); // avoid division by zero
4454 CSSRect scrollableRect = aFrameMetrics.GetExpandedScrollableRect();
4456 // Calculate the displayport size based on how fast we're moving along each
4457 // axis.
4458 CSSSize displayPortSize =
4459 CalculateDisplayPortSize(compositionSize, velocity, aZoomInProgress,
4460 aFrameMetrics.DisplayportPixelsPerCSSPixel());
4462 displayPortSize =
4463 ExpandDisplayPortToDangerZone(displayPortSize, aFrameMetrics);
4465 if (StaticPrefs::apz_enlarge_displayport_when_clipped()) {
4466 RedistributeDisplayPortExcess(displayPortSize, scrollableRect);
4469 // We calculate a "displayport" here which is relative to the scroll offset.
4470 // Note that the scroll offset we have here in the APZ code may not be the
4471 // same as the base rect that gets used on the layout side when the
4472 // displayport margins are actually applied, so it is important to only
4473 // consider the displayport as margins relative to a scroll offset rather than
4474 // relative to something more unchanging like the scrollable rect origin.
4476 // Center the displayport based on its expansion over the composition size.
4477 CSSRect displayPort((compositionSize.width - displayPortSize.width) / 2.0f,
4478 (compositionSize.height - displayPortSize.height) / 2.0f,
4479 displayPortSize.width, displayPortSize.height);
4481 // Offset the displayport, depending on how fast we're moving and the
4482 // estimated time it takes to paint, to try to minimise checkerboarding.
4483 float paintFactor = kDefaultEstimatedPaintDurationMs;
4484 displayPort.MoveBy(velocity * paintFactor * StaticPrefs::apz_velocity_bias());
4486 APZC_LOGV_FM(aFrameMetrics,
4487 "Calculated displayport as %s from velocity %s zooming %d paint "
4488 "time %f metrics",
4489 ToString(displayPort).c_str(), ToString(aVelocity).c_str(),
4490 (int)aZoomInProgress, paintFactor);
4492 CSSMargin cssMargins;
4493 cssMargins.left = -displayPort.X();
4494 cssMargins.top = -displayPort.Y();
4495 cssMargins.right =
4496 displayPort.Width() - compositionSize.width - cssMargins.left;
4497 cssMargins.bottom =
4498 displayPort.Height() - compositionSize.height - cssMargins.top;
4500 return cssMargins * aFrameMetrics.DisplayportPixelsPerCSSPixel();
4503 void AsyncPanZoomController::ScheduleComposite() {
4504 if (mCompositorController) {
4505 mCompositorController->ScheduleRenderOnCompositorThread(
4506 wr::RenderReasons::APZ);
4510 void AsyncPanZoomController::ScheduleCompositeAndMaybeRepaint() {
4511 ScheduleComposite();
4512 RequestContentRepaint();
4515 void AsyncPanZoomController::FlushRepaintForOverscrollHandoff() {
4516 RecursiveMutexAutoLock lock(mRecursiveMutex);
4517 RequestContentRepaint();
4520 void AsyncPanZoomController::FlushRepaintForNewInputBlock() {
4521 APZC_LOG("%p flushing repaint for new input block\n", this);
4523 RecursiveMutexAutoLock lock(mRecursiveMutex);
4524 RequestContentRepaint();
4527 bool AsyncPanZoomController::SnapBackIfOverscrolled() {
4528 RecursiveMutexAutoLock lock(mRecursiveMutex);
4529 if (SnapBackIfOverscrolledForMomentum(ParentLayerPoint(0, 0))) {
4530 return true;
4532 // If we don't kick off an overscroll animation, we still need to snap to any
4533 // nearby snap points, assuming we haven't already done so when we started
4534 // this fling
4535 if (mState != FLING) {
4536 ScrollSnap(ScrollSnapFlags::IntendedEndPosition);
4538 return false;
4541 bool AsyncPanZoomController::SnapBackIfOverscrolledForMomentum(
4542 const ParentLayerPoint& aVelocity) {
4543 RecursiveMutexAutoLock lock(mRecursiveMutex);
4544 // It's possible that we're already in the middle of an overscroll
4545 // animation - if so, don't start a new one.
4546 if (IsOverscrolled() && mState != OVERSCROLL_ANIMATION) {
4547 APZC_LOG("%p is overscrolled, starting snap-back\n", this);
4548 mOverscrollEffect->RelieveOverscroll(aVelocity, GetOverscrollSideBits());
4549 return true;
4551 return false;
4554 bool AsyncPanZoomController::IsFlingingFast() const {
4555 RecursiveMutexAutoLock lock(mRecursiveMutex);
4556 if (mState == FLING && GetVelocityVector().Length() >
4557 StaticPrefs::apz_fling_stop_on_tap_threshold()) {
4558 APZC_LOG("%p is moving fast\n", this);
4559 return true;
4561 return false;
4564 bool AsyncPanZoomController::IsPannable() const {
4565 RecursiveMutexAutoLock lock(mRecursiveMutex);
4566 return mX.CanScroll() || mY.CanScroll();
4569 bool AsyncPanZoomController::IsScrollInfoLayer() const {
4570 RecursiveMutexAutoLock lock(mRecursiveMutex);
4571 return Metrics().IsScrollInfoLayer();
4574 int32_t AsyncPanZoomController::GetLastTouchIdentifier() const {
4575 RefPtr<GestureEventListener> listener = GetGestureEventListener();
4576 return listener ? listener->GetLastTouchIdentifier() : -1;
4579 void AsyncPanZoomController::RequestContentRepaint(
4580 RepaintUpdateType aUpdateType) {
4581 // Reinvoke this method on the repaint thread if it's not there already. It's
4582 // important to do this before the call to CalculatePendingDisplayPort, so
4583 // that CalculatePendingDisplayPort uses the most recent available version of
4584 // Metrics(). just before the paint request is dispatched to content.
4585 RefPtr<GeckoContentController> controller = GetGeckoContentController();
4586 if (!controller) {
4587 return;
4589 if (!controller->IsRepaintThread()) {
4590 // Even though we want to do the actual repaint request on the repaint
4591 // thread, we want to update the expected gecko metrics synchronously.
4592 // Otherwise we introduce a race condition where we might read from the
4593 // expected gecko metrics on the controller thread before or after it gets
4594 // updated on the repaint thread, when in fact we always want the updated
4595 // version when reading.
4596 { // scope lock
4597 RecursiveMutexAutoLock lock(mRecursiveMutex);
4598 mExpectedGeckoMetrics.UpdateFrom(Metrics());
4601 // use the local variable to resolve the function overload.
4602 auto func =
4603 static_cast<void (AsyncPanZoomController::*)(RepaintUpdateType)>(
4604 &AsyncPanZoomController::RequestContentRepaint);
4605 controller->DispatchToRepaintThread(NewRunnableMethod<RepaintUpdateType>(
4606 "layers::AsyncPanZoomController::RequestContentRepaint", this, func,
4607 aUpdateType));
4608 return;
4611 MOZ_ASSERT(controller->IsRepaintThread());
4613 RecursiveMutexAutoLock lock(mRecursiveMutex);
4614 ParentLayerPoint velocity = GetVelocityVector();
4615 ScreenMargin displayportMargins = CalculatePendingDisplayPort(
4616 Metrics(), velocity,
4617 (mState == PINCHING || mState == ANIMATING_ZOOM) ? ZoomInProgress::Yes
4618 : ZoomInProgress::No);
4619 Metrics().SetPaintRequestTime(TimeStamp::Now());
4620 RequestContentRepaint(velocity, displayportMargins, aUpdateType);
4623 static CSSRect GetDisplayPortRect(const FrameMetrics& aFrameMetrics,
4624 const ScreenMargin& aDisplayportMargins) {
4625 // This computation is based on what happens in CalculatePendingDisplayPort.
4626 // If that changes then this might need to change too.
4627 // Note that the display port rect APZ computes is relative to the visual
4628 // scroll offset. It's adjusted to be relative to the layout scroll offset
4629 // when the main thread processes a repaint request (in
4630 // APZCCallbackHelper::AdjustDisplayPortForScrollDelta()) and ultimately
4631 // applied (in DisplayPortUtils::GetDisplayPort()) in this adjusted form.
4632 CSSRect baseRect(aFrameMetrics.GetVisualScrollOffset(),
4633 aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels());
4634 baseRect.Inflate(aDisplayportMargins /
4635 aFrameMetrics.DisplayportPixelsPerCSSPixel());
4636 return baseRect;
4639 void AsyncPanZoomController::RequestContentRepaint(
4640 const ParentLayerPoint& aVelocity, const ScreenMargin& aDisplayportMargins,
4641 RepaintUpdateType aUpdateType) {
4642 mRecursiveMutex.AssertCurrentThreadIn();
4644 RefPtr<GeckoContentController> controller = GetGeckoContentController();
4645 if (!controller) {
4646 return;
4648 MOZ_ASSERT(controller->IsRepaintThread());
4650 APZScrollAnimationType animationType = APZScrollAnimationType::No;
4651 if (mAnimation) {
4652 animationType = mAnimation->WasTriggeredByScript()
4653 ? APZScrollAnimationType::TriggeredByScript
4654 : APZScrollAnimationType::TriggeredByUserInput;
4656 RepaintRequest request(Metrics(), aDisplayportMargins, aUpdateType,
4657 animationType, mScrollGeneration, mLastSnapTargetIds,
4658 IsInScrollingGesture());
4660 if (request.IsRootContent() && request.GetZoom() != mLastNotifiedZoom &&
4661 mState != PINCHING && mState != ANIMATING_ZOOM) {
4662 controller->NotifyScaleGestureComplete(
4663 GetGuid(),
4664 (request.GetZoom() / request.GetDevPixelsPerCSSPixel()).scale);
4665 mLastNotifiedZoom = request.GetZoom();
4668 // If we're trying to paint what we already think is painted, discard this
4669 // request since it's a pointless paint.
4670 if (request.GetDisplayPortMargins().WithinEpsilonOf(
4671 mLastPaintRequestMetrics.GetDisplayPortMargins(), EPSILON) &&
4672 request.GetVisualScrollOffset().WithinEpsilonOf(
4673 mLastPaintRequestMetrics.GetVisualScrollOffset(), EPSILON) &&
4674 request.GetPresShellResolution() ==
4675 mLastPaintRequestMetrics.GetPresShellResolution() &&
4676 request.GetZoom() == mLastPaintRequestMetrics.GetZoom() &&
4677 request.GetLayoutViewport().WithinEpsilonOf(
4678 mLastPaintRequestMetrics.GetLayoutViewport(), EPSILON) &&
4679 request.GetScrollGeneration() ==
4680 mLastPaintRequestMetrics.GetScrollGeneration() &&
4681 request.GetScrollUpdateType() ==
4682 mLastPaintRequestMetrics.GetScrollUpdateType() &&
4683 request.GetScrollAnimationType() ==
4684 mLastPaintRequestMetrics.GetScrollAnimationType() &&
4685 request.GetLastSnapTargetIds() ==
4686 mLastPaintRequestMetrics.GetLastSnapTargetIds()) {
4687 return;
4690 APZC_LOGV("%p requesting content repaint %s", this,
4691 ToString(request).c_str());
4692 { // scope lock
4693 MutexAutoLock lock(mCheckerboardEventLock);
4694 if (mCheckerboardEvent && mCheckerboardEvent->IsRecordingTrace()) {
4695 std::stringstream info;
4696 info << " velocity " << aVelocity;
4697 std::string str = info.str();
4698 mCheckerboardEvent->UpdateRendertraceProperty(
4699 CheckerboardEvent::RequestedDisplayPort,
4700 GetDisplayPortRect(Metrics(), aDisplayportMargins), str);
4704 controller->RequestContentRepaint(request);
4705 mExpectedGeckoMetrics.UpdateFrom(Metrics());
4706 mLastPaintRequestMetrics = request;
4708 // We're holding the APZC lock here, so redispatch this so we can get
4709 // the tree lock without the APZC lock.
4710 controller->DispatchToRepaintThread(
4711 NewRunnableMethod<AsyncPanZoomController*>(
4712 "layers::APZCTreeManager::SendSubtreeTransformsToChromeMainThread",
4713 GetApzcTreeManager(),
4714 &APZCTreeManager::SendSubtreeTransformsToChromeMainThread, this));
4717 bool AsyncPanZoomController::UpdateAnimation(
4718 const RecursiveMutexAutoLock& aProofOfLock, const SampleTime& aSampleTime,
4719 nsTArray<RefPtr<Runnable>>* aOutDeferredTasks) {
4720 AssertOnSamplerThread();
4722 // This function may get called multiple with the same sample time, if we
4723 // composite multiple times at the same timestamp.
4724 // However we only want to do one animation step per composition so we need
4725 // to deduplicate these calls first.
4726 // Even if there's no animation, if we have a scroll offset change pending due
4727 // to the frame delay, we need to keep compositing.
4728 if (mLastSampleTime == aSampleTime) {
4729 return !!mAnimation || HavePendingFrameDelayedOffset();
4732 // We're at a new timestamp, so advance to the next sample in the deque, if
4733 // there is one. That one will be used for all the code that reads the
4734 // eForCompositing transforms in this vsync interval.
4735 AdvanceToNextSample();
4737 // And then create a new sample, which will be used in the *next* vsync
4738 // interval. We do the sample at this point and not later in order to try
4739 // and enforce one frame delay between computing the async transform and
4740 // compositing it to the screen. This one-frame delay gives code running on
4741 // the main thread a chance to try and respond to the scroll position change,
4742 // so that e.g. a main-thread animation can stay in sync with user-driven
4743 // scrolling or a compositor animation.
4744 bool needComposite = SampleCompositedAsyncTransform(aProofOfLock);
4746 TimeDuration sampleTimeDelta = aSampleTime - mLastSampleTime;
4747 mLastSampleTime = aSampleTime;
4749 if (needComposite || mAnimation) {
4750 // Bump the scroll generation before we call RequestContentRepaint below
4751 // so that the RequestContentRepaint call will surely use the new
4752 // generation.
4753 if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
4754 mScrollGeneration = treeManagerLocal->NewAPZScrollGeneration();
4758 if (mAnimation) {
4759 bool continueAnimation = mAnimation->Sample(Metrics(), sampleTimeDelta);
4760 bool wantsRepaints = mAnimation->WantsRepaints();
4761 *aOutDeferredTasks = mAnimation->TakeDeferredTasks();
4762 if (!continueAnimation) {
4763 SetState(NOTHING);
4764 if (mAnimation->AsSmoothMsdScrollAnimation()) {
4766 RecursiveMutexAutoLock lock(mRecursiveMutex);
4767 mLastSnapTargetIds =
4768 mAnimation->AsSmoothMsdScrollAnimation()->TakeSnapTargetIds();
4770 } else if (mAnimation->AsSmoothScrollAnimation()) {
4771 RecursiveMutexAutoLock lock(mRecursiveMutex);
4772 mLastSnapTargetIds =
4773 mAnimation->AsSmoothScrollAnimation()->TakeSnapTargetIds();
4775 mAnimation = nullptr;
4777 // Request a repaint at the end of the animation in case something such as a
4778 // call to NotifyLayersUpdated was invoked during the animation and Gecko's
4779 // current state is some intermediate point of the animation.
4780 if (!continueAnimation || wantsRepaints) {
4781 RequestContentRepaint();
4783 needComposite = true;
4785 return needComposite;
4788 AsyncTransformComponentMatrix AsyncPanZoomController::GetOverscrollTransform(
4789 AsyncTransformConsumer aMode) const {
4790 RecursiveMutexAutoLock lock(mRecursiveMutex);
4791 AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
4793 if (aMode == eForCompositing && mScrollMetadata.IsApzForceDisabled()) {
4794 return AsyncTransformComponentMatrix();
4797 if (!IsPhysicallyOverscrolled()) {
4798 return AsyncTransformComponentMatrix();
4801 // The overscroll effect is a simple translation by the overscroll offset.
4802 ParentLayerPoint overscrollOffset(-mX.GetOverscroll(), -mY.GetOverscroll());
4803 return AsyncTransformComponentMatrix().PostTranslate(overscrollOffset.x,
4804 overscrollOffset.y, 0);
4807 bool AsyncPanZoomController::AdvanceAnimations(const SampleTime& aSampleTime) {
4808 AssertOnSamplerThread();
4810 // Don't send any state-change notifications until the end of the function,
4811 // because we may go through some intermediate states while we finish
4812 // animations and start new ones.
4813 StateChangeNotificationBlocker blocker(this);
4815 // The eventual return value of this function. The compositor needs to know
4816 // whether or not to advance by a frame as soon as it can. For example, if a
4817 // fling is happening, it has to keep compositing so that the animation is
4818 // smooth. If an animation frame is requested, it is the compositor's
4819 // responsibility to schedule a composite.
4820 bool requestAnimationFrame = false;
4821 nsTArray<RefPtr<Runnable>> deferredTasks;
4824 RecursiveMutexAutoLock lock(mRecursiveMutex);
4825 { // scope lock
4826 CSSRect visibleRect = GetVisibleRect(lock);
4827 MutexAutoLock lock2(mCheckerboardEventLock);
4828 // Update RendertraceProperty before UpdateAnimation() call, since
4829 // the UpdateAnimation() updates effective ScrollOffset for next frame
4830 // if APZFrameDelay is enabled.
4831 if (mCheckerboardEvent) {
4832 mCheckerboardEvent->UpdateRendertraceProperty(
4833 CheckerboardEvent::UserVisible, visibleRect);
4837 requestAnimationFrame = UpdateAnimation(lock, aSampleTime, &deferredTasks);
4839 // Execute any deferred tasks queued up by mAnimation's Sample() (called by
4840 // UpdateAnimation()). This needs to be done after the monitor is released
4841 // since the tasks are allowed to call APZCTreeManager methods which can grab
4842 // the tree lock.
4843 for (uint32_t i = 0; i < deferredTasks.Length(); ++i) {
4844 APZThreadUtils::RunOnControllerThread(std::move(deferredTasks[i]));
4847 // If any of the deferred tasks starts a new animation, it will request a
4848 // new composite directly, so we can just return requestAnimationFrame here.
4849 return requestAnimationFrame;
4852 ParentLayerPoint AsyncPanZoomController::GetCurrentAsyncScrollOffset(
4853 AsyncTransformConsumer aMode) const {
4854 RecursiveMutexAutoLock lock(mRecursiveMutex);
4855 AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
4857 return GetEffectiveScrollOffset(aMode, lock) * GetEffectiveZoom(aMode, lock);
4860 CSSRect AsyncPanZoomController::GetCurrentAsyncVisualViewport(
4861 AsyncTransformConsumer aMode) const {
4862 RecursiveMutexAutoLock lock(mRecursiveMutex);
4863 AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
4865 return CSSRect(
4866 GetEffectiveScrollOffset(aMode, lock),
4867 FrameMetrics::CalculateCompositedSizeInCssPixels(
4868 Metrics().GetCompositionBounds(), GetEffectiveZoom(aMode, lock)));
4871 AsyncTransform AsyncPanZoomController::GetCurrentAsyncTransform(
4872 AsyncTransformConsumer aMode, AsyncTransformComponents aComponents,
4873 std::size_t aSampleIndex) const {
4874 RecursiveMutexAutoLock lock(mRecursiveMutex);
4875 AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
4877 CSSToParentLayerScale effectiveZoom;
4878 if (aComponents.contains(AsyncTransformComponent::eVisual)) {
4879 effectiveZoom = GetEffectiveZoom(aMode, lock, aSampleIndex);
4880 } else {
4881 effectiveZoom =
4882 Metrics().LayersPixelsPerCSSPixel() * LayerToParentLayerScale(1.0f);
4885 LayerToParentLayerScale compositedAsyncZoom =
4886 effectiveZoom / Metrics().LayersPixelsPerCSSPixel();
4888 ParentLayerPoint translation;
4889 if (aComponents.contains(AsyncTransformComponent::eVisual)) {
4890 // There is no "lastPaintVisualOffset" to subtract here; the visual offset
4891 // is entirely async.
4893 CSSPoint currentVisualOffset =
4894 GetEffectiveScrollOffset(aMode, lock, aSampleIndex) -
4895 GetEffectiveLayoutViewport(aMode, lock, aSampleIndex).TopLeft();
4897 translation += currentVisualOffset * effectiveZoom;
4899 if (aComponents.contains(AsyncTransformComponent::eLayout)) {
4900 CSSPoint lastPaintLayoutOffset;
4901 if (mLastContentPaintMetrics.IsScrollable()) {
4902 lastPaintLayoutOffset = mLastContentPaintMetrics.GetLayoutScrollOffset();
4905 CSSPoint currentLayoutOffset =
4906 GetEffectiveLayoutViewport(aMode, lock, aSampleIndex).TopLeft();
4908 translation +=
4909 (currentLayoutOffset - lastPaintLayoutOffset) * effectiveZoom;
4912 return AsyncTransform(compositedAsyncZoom, -translation);
4915 AsyncTransformComponentMatrix
4916 AsyncPanZoomController::GetAsyncTransformForInputTransformation(
4917 AsyncTransformComponents aComponents, LayersId aForLayersId) const {
4918 AsyncTransformComponentMatrix result;
4919 // If we are the root, and |aForLayersId| is different from our LayersId,
4920 // |aForLayersId| must be in a remote subdocument.
4921 if (IsRootContent() && aForLayersId != GetLayersId()) {
4922 result =
4923 ViewAs<AsyncTransformComponentMatrix>(GetPaintedResolutionTransform());
4925 // Order of transforms: the painted resolution (if any) applies first, and
4926 // any async transform on top of that.
4927 result = result * AsyncTransformComponentMatrix(GetCurrentAsyncTransform(
4928 eForEventHandling, aComponents));
4929 // The overscroll transform is considered part of the layout component of
4930 // the async transform, because it should not apply to fixed content.
4931 if (aComponents.contains(AsyncTransformComponent::eLayout)) {
4932 result = result * GetOverscrollTransform(eForEventHandling);
4934 return result;
4937 Matrix4x4 AsyncPanZoomController::GetPaintedResolutionTransform() const {
4938 RecursiveMutexAutoLock lock(mRecursiveMutex);
4939 MOZ_ASSERT(IsRootContent());
4940 float resolution = mLastContentPaintMetrics.GetPresShellResolution();
4941 return Matrix4x4::Scaling(resolution, resolution, 1.f);
4944 LayoutDeviceToParentLayerScale AsyncPanZoomController::GetCurrentPinchZoomScale(
4945 AsyncTransformConsumer aMode) const {
4946 RecursiveMutexAutoLock lock(mRecursiveMutex);
4947 AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
4948 CSSToParentLayerScale scale = GetEffectiveZoom(aMode, lock);
4949 return scale / Metrics().GetDevPixelsPerCSSPixel();
4952 AutoTArray<wr::SampledScrollOffset, 2>
4953 AsyncPanZoomController::GetSampledScrollOffsets() const {
4954 AssertOnSamplerThread();
4956 RecursiveMutexAutoLock lock(mRecursiveMutex);
4958 const AsyncTransformComponents asyncTransformComponents =
4959 GetZoomAnimationId()
4960 ? AsyncTransformComponents{AsyncTransformComponent::eLayout}
4961 : LayoutAndVisual;
4963 // If layerTranslation includes only the layout component of the async
4964 // transform then it has not been scaled by the async zoom, so we want to
4965 // divide it by the resolution. If layerTranslation includes the visual
4966 // component, then we should use the pinch zoom scale, which includes the
4967 // async zoom. However, we only use LayoutAndVisual for non-zoomable APZCs,
4968 // so it makes no difference.
4969 LayoutDeviceToParentLayerScale resolution =
4970 GetCumulativeResolution() * LayerToParentLayerScale(1.0f);
4972 AutoTArray<wr::SampledScrollOffset, 2> sampledOffsets;
4974 for (std::deque<SampledAPZCState>::size_type index = 0;
4975 index < mSampledState.size(); index++) {
4976 ParentLayerPoint layerTranslation =
4977 GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing,
4978 asyncTransformComponents, index)
4979 .mTranslation;
4981 // Include the overscroll transform here in scroll offsets transform
4982 // to ensure that we do not overscroll fixed content.
4983 layerTranslation =
4984 GetOverscrollTransform(AsyncPanZoomController::eForCompositing)
4985 .TransformPoint(layerTranslation);
4986 // The positive translation means the painted content is supposed to
4987 // move down (or to the right), and that corresponds to a reduction in
4988 // the scroll offset. Since we are effectively giving WR the async
4989 // scroll delta here, we want to negate the translation.
4990 LayoutDevicePoint asyncScrollDelta = -layerTranslation / resolution;
4991 sampledOffsets.AppendElement(wr::SampledScrollOffset{
4992 wr::ToLayoutVector2D(asyncScrollDelta),
4993 wr::ToWrAPZScrollGeneration(mSampledState[index].Generation())});
4996 return sampledOffsets;
4999 bool AsyncPanZoomController::SuppressAsyncScrollOffset() const {
5000 return mScrollMetadata.IsApzForceDisabled() ||
5001 (Metrics().IsMinimalDisplayPort() &&
5002 StaticPrefs::apz_prefer_jank_minimal_displayports());
5005 CSSRect AsyncPanZoomController::GetEffectiveLayoutViewport(
5006 AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
5007 std::size_t aSampleIndex) const {
5008 if (aMode == eForCompositing && SuppressAsyncScrollOffset()) {
5009 return mLastContentPaintMetrics.GetLayoutViewport();
5011 if (aMode == eForCompositing) {
5012 return mSampledState[aSampleIndex].GetLayoutViewport();
5014 return Metrics().GetLayoutViewport();
5017 CSSPoint AsyncPanZoomController::GetEffectiveScrollOffset(
5018 AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
5019 std::size_t aSampleIndex) const {
5020 if (aMode == eForCompositing && SuppressAsyncScrollOffset()) {
5021 return mLastContentPaintMetrics.GetVisualScrollOffset();
5023 if (aMode == eForCompositing) {
5024 return mSampledState[aSampleIndex].GetVisualScrollOffset();
5026 return Metrics().GetVisualScrollOffset();
5029 CSSToParentLayerScale AsyncPanZoomController::GetEffectiveZoom(
5030 AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
5031 std::size_t aSampleIndex) const {
5032 if (aMode == eForCompositing && SuppressAsyncScrollOffset()) {
5033 return mLastContentPaintMetrics.GetZoom();
5035 if (aMode == eForCompositing) {
5036 return mSampledState[aSampleIndex].GetZoom();
5038 return Metrics().GetZoom();
5041 void AsyncPanZoomController::AdvanceToNextSample() {
5042 AssertOnSamplerThread();
5043 RecursiveMutexAutoLock lock(mRecursiveMutex);
5044 // Always keep at least one state in mSampledState.
5045 if (mSampledState.size() > 1) {
5046 mSampledState.pop_front();
5050 bool AsyncPanZoomController::HavePendingFrameDelayedOffset() const {
5051 AssertOnSamplerThread();
5052 RecursiveMutexAutoLock lock(mRecursiveMutex);
5054 const bool nextFrameWillChange =
5055 mSampledState.size() >= 2 && mSampledState[0] != mSampledState[1];
5056 const bool frameAfterThatWillChange =
5057 mSampledState.back() != SampledAPZCState(Metrics());
5058 return nextFrameWillChange || frameAfterThatWillChange;
5061 bool AsyncPanZoomController::SampleCompositedAsyncTransform(
5062 const RecursiveMutexAutoLock& aProofOfLock) {
5063 MOZ_ASSERT(mSampledState.size() <= 2);
5064 bool sampleChanged = (mSampledState.back() != SampledAPZCState(Metrics()));
5065 mSampledState.emplace_back(Metrics(), std::move(mScrollPayload),
5066 mScrollGeneration);
5067 return sampleChanged;
5070 void AsyncPanZoomController::ResampleCompositedAsyncTransform(
5071 const RecursiveMutexAutoLock& aProofOfLock) {
5072 // This only gets called during testing situations, so the fact that this
5073 // drops the scroll payload from mSampledState.front() is not really a
5074 // problem.
5075 if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
5076 mScrollGeneration = treeManagerLocal->NewAPZScrollGeneration();
5078 mSampledState.front() = SampledAPZCState(Metrics(), {}, mScrollGeneration);
5081 void AsyncPanZoomController::ApplyAsyncTestAttributes(
5082 const RecursiveMutexAutoLock& aProofOfLock) {
5083 if (mTestAttributeAppliers == 0) {
5084 if (mTestAsyncScrollOffset != CSSPoint() ||
5085 mTestAsyncZoom != LayerToParentLayerScale()) {
5086 // TODO Currently we update Metrics() and resample, which will cause
5087 // the very latest user input to get immediately captured in the sample,
5088 // and may defeat our attempt at "frame delay" (i.e. delaying the user
5089 // input from affecting composition by one frame).
5090 // Instead, maybe we should just apply the mTest* stuff directly to
5091 // mSampledState.front(). We can even save/restore that SampledAPZCState
5092 // instance in the AutoApplyAsyncTestAttributes instead of Metrics().
5093 Metrics().ZoomBy(mTestAsyncZoom.scale);
5094 CSSPoint asyncScrollPosition = Metrics().GetVisualScrollOffset();
5095 CSSPoint requestedPoint =
5096 asyncScrollPosition + this->mTestAsyncScrollOffset;
5097 CSSPoint clampedPoint =
5098 Metrics().CalculateScrollRange().ClampPoint(requestedPoint);
5099 CSSPoint difference = mTestAsyncScrollOffset - clampedPoint;
5101 ScrollByAndClamp(mTestAsyncScrollOffset);
5103 if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) {
5104 ParentLayerPoint overscroll = difference * Metrics().GetZoom();
5105 OverscrollBy(overscroll);
5107 ResampleCompositedAsyncTransform(aProofOfLock);
5110 ++mTestAttributeAppliers;
5113 void AsyncPanZoomController::UnapplyAsyncTestAttributes(
5114 const RecursiveMutexAutoLock& aProofOfLock,
5115 const FrameMetrics& aPrevFrameMetrics,
5116 const ParentLayerPoint& aPrevOverscroll) {
5117 MOZ_ASSERT(mTestAttributeAppliers >= 1);
5118 --mTestAttributeAppliers;
5119 if (mTestAttributeAppliers == 0) {
5120 if (mTestAsyncScrollOffset != CSSPoint() ||
5121 mTestAsyncZoom != LayerToParentLayerScale()) {
5122 Metrics() = aPrevFrameMetrics;
5123 RestoreOverscrollAmount(aPrevOverscroll);
5124 ResampleCompositedAsyncTransform(aProofOfLock);
5129 Matrix4x4 AsyncPanZoomController::GetTransformToLastDispatchedPaint(
5130 const AsyncTransformComponents& aComponents, LayersId aForLayersId) const {
5131 RecursiveMutexAutoLock lock(mRecursiveMutex);
5132 CSSPoint componentOffset;
5134 // The computation of the componentOffset should roughly be the negation
5135 // of the translation in GetCurrentAsyncTransform() with the expected
5136 // gecko metrics substituted for the effective scroll offsets.
5137 if (aComponents.contains(AsyncTransformComponent::eVisual)) {
5138 componentOffset += mExpectedGeckoMetrics.GetLayoutScrollOffset() -
5139 mExpectedGeckoMetrics.GetVisualScrollOffset();
5142 if (aComponents.contains(AsyncTransformComponent::eLayout)) {
5143 CSSPoint lastPaintLayoutOffset;
5145 if (mLastContentPaintMetrics.IsScrollable()) {
5146 lastPaintLayoutOffset = mLastContentPaintMetrics.GetLayoutScrollOffset();
5149 componentOffset +=
5150 lastPaintLayoutOffset - mExpectedGeckoMetrics.GetLayoutScrollOffset();
5153 LayerPoint scrollChange = componentOffset *
5154 mLastContentPaintMetrics.GetDevPixelsPerCSSPixel() *
5155 mLastContentPaintMetrics.GetCumulativeResolution();
5157 // We're interested in the async zoom change. Factor out the content scale
5158 // that may change when dragging the window to a monitor with a different
5159 // content scale.
5160 LayoutDeviceToParentLayerScale lastContentZoom =
5161 mLastContentPaintMetrics.GetZoom() /
5162 mLastContentPaintMetrics.GetDevPixelsPerCSSPixel();
5163 LayoutDeviceToParentLayerScale lastDispatchedZoom =
5164 mExpectedGeckoMetrics.GetZoom() /
5165 mExpectedGeckoMetrics.GetDevPixelsPerCSSPixel();
5166 float zoomChange = 1.0;
5167 if (aComponents.contains(AsyncTransformComponent::eVisual) &&
5168 lastDispatchedZoom != LayoutDeviceToParentLayerScale(0)) {
5169 zoomChange = lastContentZoom.scale / lastDispatchedZoom.scale;
5171 Matrix4x4 result;
5172 // If we are the root, and |aForLayersId| is different from our LayersId,
5173 // |aForLayersId| must be in a remote subdocument.
5174 if (IsRootContent() && aForLayersId != GetLayersId()) {
5175 result = GetPaintedResolutionTransform();
5177 // Order of transforms: the painted resolution (if any) applies first, and
5178 // any async transform on top of that.
5179 return result * Matrix4x4::Translation(scrollChange.x, scrollChange.y, 0)
5180 .PostScale(zoomChange, zoomChange, 1);
5183 CSSRect AsyncPanZoomController::GetVisibleRect(
5184 const RecursiveMutexAutoLock& aProofOfLock) const {
5185 AutoApplyAsyncTestAttributes testAttributeApplier(this, aProofOfLock);
5186 CSSPoint currentScrollOffset = GetEffectiveScrollOffset(
5187 AsyncPanZoomController::eForCompositing, aProofOfLock);
5188 CSSRect visible = CSSRect(currentScrollOffset,
5189 Metrics().CalculateCompositedSizeInCssPixels());
5190 return visible;
5193 static CSSRect GetPaintedRect(const FrameMetrics& aFrameMetrics) {
5194 CSSRect displayPort = aFrameMetrics.GetDisplayPort();
5195 if (displayPort.IsEmpty()) {
5196 // Fallback to use the viewport if the diplayport hasn't been set.
5197 // This situation often happens non-scrollable iframe's root scroller in
5198 // Fission.
5199 return aFrameMetrics.GetVisualViewport();
5202 return displayPort + aFrameMetrics.GetLayoutScrollOffset();
5205 uint32_t AsyncPanZoomController::GetCheckerboardMagnitude(
5206 const ParentLayerRect& aClippedCompositionBounds) const {
5207 RecursiveMutexAutoLock lock(mRecursiveMutex);
5209 CSSRect painted = GetPaintedRect(mLastContentPaintMetrics);
5210 painted.Inflate(CSSMargin::FromAppUnits(
5211 nsMargin(1, 1, 1, 1))); // fuzz for rounding error
5213 CSSRect visible = GetVisibleRect(lock); // relative to scrolled frame origin
5214 if (visible.IsEmpty() || painted.Contains(visible)) {
5215 // early-exit if we're definitely not checkerboarding
5216 return 0;
5219 // aClippedCompositionBounds and Metrics().GetCompositionBounds() are both
5220 // relative to the layer tree origin.
5221 // The "*RelativeToItself*" variables are relative to the comp bounds origin
5222 ParentLayerRect visiblePartOfCompBoundsRelativeToItself =
5223 aClippedCompositionBounds - Metrics().GetCompositionBounds().TopLeft();
5224 CSSRect visiblePartOfCompBoundsRelativeToItselfInCssSpace;
5225 if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
5226 visiblePartOfCompBoundsRelativeToItselfInCssSpace =
5227 (visiblePartOfCompBoundsRelativeToItself / Metrics().GetZoom());
5230 // This one is relative to the scrolled frame origin, same as `visible`
5231 CSSRect visiblePartOfCompBoundsInCssSpace =
5232 visiblePartOfCompBoundsRelativeToItselfInCssSpace + visible.TopLeft();
5234 visible = visible.Intersect(visiblePartOfCompBoundsInCssSpace);
5236 CSSIntRegion checkerboard;
5237 // Round so as to minimize checkerboarding; if we're only showing fractional
5238 // pixels of checkerboarding it's not really worth counting
5239 checkerboard.Sub(RoundedIn(visible), RoundedOut(painted));
5240 uint32_t area = checkerboard.Area();
5241 if (area) {
5242 APZC_LOG_FM(Metrics(),
5243 "%p is currently checkerboarding (painted %s visible %s)", this,
5244 ToString(painted).c_str(), ToString(visible).c_str());
5246 return area;
5249 void AsyncPanZoomController::ReportCheckerboard(
5250 const SampleTime& aSampleTime,
5251 const ParentLayerRect& aClippedCompositionBounds) {
5252 if (mLastCheckerboardReport == aSampleTime) {
5253 // This function will get called multiple times for each APZC on a single
5254 // composite (once for each layer it is attached to). Only report the
5255 // checkerboard once per composite though.
5256 return;
5258 mLastCheckerboardReport = aSampleTime;
5260 bool recordTrace = StaticPrefs::apz_record_checkerboarding();
5261 bool forTelemetry = Telemetry::CanRecordBase();
5262 uint32_t magnitude = GetCheckerboardMagnitude(aClippedCompositionBounds);
5264 // IsInTransformingState() acquires the APZC lock and thus needs to
5265 // be called before acquiring mCheckerboardEventLock.
5266 bool inTransformingState = IsInTransformingState();
5268 MutexAutoLock lock(mCheckerboardEventLock);
5269 if (!mCheckerboardEvent && (recordTrace || forTelemetry)) {
5270 mCheckerboardEvent = MakeUnique<CheckerboardEvent>(recordTrace);
5272 mPotentialCheckerboardTracker.InTransform(inTransformingState,
5273 recordTrace || forTelemetry);
5274 if (magnitude) {
5275 mPotentialCheckerboardTracker.CheckerboardSeen();
5277 UpdateCheckerboardEvent(lock, magnitude);
5280 void AsyncPanZoomController::UpdateCheckerboardEvent(
5281 const MutexAutoLock& aProofOfLock, uint32_t aMagnitude) {
5282 if (mCheckerboardEvent && mCheckerboardEvent->RecordFrameInfo(aMagnitude)) {
5283 // This checkerboard event is done. Report some metrics to telemetry.
5284 mozilla::Telemetry::Accumulate(mozilla::Telemetry::CHECKERBOARD_SEVERITY,
5285 mCheckerboardEvent->GetSeverity());
5286 mozilla::Telemetry::Accumulate(mozilla::Telemetry::CHECKERBOARD_PEAK,
5287 mCheckerboardEvent->GetPeak());
5288 mozilla::Telemetry::Accumulate(
5289 mozilla::Telemetry::CHECKERBOARD_DURATION,
5290 (uint32_t)mCheckerboardEvent->GetDuration().ToMilliseconds());
5292 // mCheckerboardEvent only gets created if we are supposed to record
5293 // telemetry so we always pass true for aRecordTelemetry.
5294 mPotentialCheckerboardTracker.CheckerboardDone(
5295 /* aRecordTelemetry = */ true);
5297 if (StaticPrefs::apz_record_checkerboarding()) {
5298 // if the pref is enabled, also send it to the storage class. it may be
5299 // chosen for public display on about:checkerboard, the hall of fame for
5300 // checkerboard events.
5301 uint32_t severity = mCheckerboardEvent->GetSeverity();
5302 std::string log = mCheckerboardEvent->GetLog();
5303 CheckerboardEventStorage::Report(severity, log);
5305 mCheckerboardEvent = nullptr;
5309 void AsyncPanZoomController::FlushActiveCheckerboardReport() {
5310 MutexAutoLock lock(mCheckerboardEventLock);
5311 // Pretend like we got a frame with 0 pixels checkerboarded. This will
5312 // terminate the checkerboard event and flush it out
5313 UpdateCheckerboardEvent(lock, 0);
5316 void AsyncPanZoomController::NotifyLayersUpdated(
5317 const ScrollMetadata& aScrollMetadata, bool aIsFirstPaint,
5318 bool aThisLayerTreeUpdated) {
5319 AssertOnUpdaterThread();
5321 RecursiveMutexAutoLock lock(mRecursiveMutex);
5322 bool isDefault = mScrollMetadata.IsDefault();
5324 const FrameMetrics& aLayerMetrics = aScrollMetadata.GetMetrics();
5326 if ((aScrollMetadata == mLastContentPaintMetadata) && !isDefault) {
5327 // No new information here, skip it.
5328 APZC_LOGV("%p NotifyLayersUpdated short-circuit\n", this);
5329 return;
5332 // If the Metrics scroll offset is different from the last scroll offset
5333 // that the main-thread sent us, then we know that the user has been doing
5334 // something that triggers a scroll. This check is the APZ equivalent of the
5335 // check on the main-thread at
5336 // https://hg.mozilla.org/mozilla-central/file/97a52326b06a/layout/generic/nsGfxScrollFrame.cpp#l4050
5337 // There is code below (the use site of userScrolled) that prevents a
5338 // restored- scroll-position update from overwriting a user scroll, again
5339 // equivalent to how the main thread code does the same thing.
5340 // XXX Suspicious comparison between layout and visual scroll offsets.
5341 // This may not do the right thing when we're zoomed in.
5342 CSSPoint lastScrollOffset = mLastContentPaintMetrics.GetLayoutScrollOffset();
5343 bool userScrolled =
5344 !FuzzyEqualsCoordinate(Metrics().GetVisualScrollOffset().x,
5345 lastScrollOffset.x) ||
5346 !FuzzyEqualsCoordinate(Metrics().GetVisualScrollOffset().y,
5347 lastScrollOffset.y);
5349 if (aScrollMetadata.DidContentGetPainted()) {
5350 mLastContentPaintMetadata = aScrollMetadata;
5353 mScrollMetadata.SetScrollParentId(aScrollMetadata.GetScrollParentId());
5354 APZC_LOGV_FM(aLayerMetrics,
5355 "%p got a NotifyLayersUpdated with aIsFirstPaint=%d, "
5356 "aThisLayerTreeUpdated=%d",
5357 this, aIsFirstPaint, aThisLayerTreeUpdated);
5359 { // scope lock
5360 MutexAutoLock lock(mCheckerboardEventLock);
5361 if (mCheckerboardEvent && mCheckerboardEvent->IsRecordingTrace()) {
5362 std::string str;
5363 if (aThisLayerTreeUpdated) {
5364 if (!aLayerMetrics.GetPaintRequestTime().IsNull()) {
5365 // Note that we might get the paint request time as non-null, but with
5366 // aThisLayerTreeUpdated false. That can happen if we get a layer
5367 // transaction from a different process right after we get the layer
5368 // transaction with aThisLayerTreeUpdated == true. In this case we
5369 // want to ignore the paint request time because it was already dumped
5370 // in the previous layer transaction.
5371 TimeDuration paintTime =
5372 TimeStamp::Now() - aLayerMetrics.GetPaintRequestTime();
5373 std::stringstream info;
5374 info << " painttime " << paintTime.ToMilliseconds();
5375 str = info.str();
5376 } else {
5377 // This might be indicative of a wasted paint particularly if it
5378 // happens during a checkerboard event.
5379 str = " (this layertree updated)";
5382 mCheckerboardEvent->UpdateRendertraceProperty(
5383 CheckerboardEvent::Page, aLayerMetrics.GetScrollableRect());
5384 mCheckerboardEvent->UpdateRendertraceProperty(
5385 CheckerboardEvent::PaintedDisplayPort, GetPaintedRect(aLayerMetrics),
5386 str);
5390 // The main thread may send us a visual scroll offset update. This is
5391 // different from a layout viewport offset update in that the layout viewport
5392 // offset is limited to the layout scroll range, while the visual viewport
5393 // offset is not.
5394 // However, there are some conditions in which the layout update will clobber
5395 // the visual update, and we want to ignore the visual update in those cases.
5396 // This variable tracks that.
5397 bool ignoreVisualUpdate = false;
5399 // TODO if we're in a drag and scrollOffsetUpdated is set then we want to
5400 // ignore it
5402 bool needContentRepaint = false;
5403 RepaintUpdateType contentRepaintType = RepaintUpdateType::eNone;
5404 bool viewportSizeUpdated = false;
5405 bool needToReclampScroll = false;
5407 if ((aIsFirstPaint && aThisLayerTreeUpdated) || isDefault ||
5408 Metrics().IsRootContent() != aLayerMetrics.IsRootContent()) {
5409 if (Metrics().IsRootContent() && !aLayerMetrics.IsRootContent()) {
5410 // We only support zooming on root content APZCs
5411 SetZoomAnimationId(Nothing());
5414 // Initialize our internal state to something sane when the content
5415 // that was just painted is something we knew nothing about previously
5416 CancelAnimation();
5418 // Keep our existing scroll generation, if there are scroll updates. In this
5419 // case we'll update our scroll generation when processing the scroll update
5420 // array below. If there are no scroll updates, take the generation from the
5421 // incoming metrics. Bug 1662019 will simplify this later.
5422 ScrollGeneration oldScrollGeneration = Metrics().GetScrollGeneration();
5423 mScrollMetadata = aScrollMetadata;
5424 if (!aScrollMetadata.GetScrollUpdates().IsEmpty()) {
5425 Metrics().SetScrollGeneration(oldScrollGeneration);
5428 mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
5430 for (auto& sampledState : mSampledState) {
5431 sampledState.UpdateScrollProperties(Metrics());
5432 sampledState.UpdateZoomProperties(Metrics());
5435 if (aLayerMetrics.HasNonZeroDisplayPortMargins()) {
5436 // A non-zero display port margin here indicates a displayport has
5437 // been set by a previous APZC for the content at this guid. The
5438 // scrollable rect may have changed since then, making the margins
5439 // wrong, so we need to calculate a new display port.
5440 // It is important that we request a repaint here only when we need to
5441 // otherwise we will end up setting a display port on every frame that
5442 // gets a view id.
5443 APZC_LOG("%p detected non-empty margins which probably need updating\n",
5444 this);
5445 needContentRepaint = true;
5447 } else {
5448 // If we're not taking the aLayerMetrics wholesale we still need to pull
5449 // in some things into our local Metrics() because these things are
5450 // determined by Gecko and our copy in Metrics() may be stale.
5452 if (Metrics().GetLayoutViewport().Size() !=
5453 aLayerMetrics.GetLayoutViewport().Size()) {
5454 CSSRect layoutViewport = Metrics().GetLayoutViewport();
5455 // The offset will be updated if necessary via
5456 // RecalculateLayoutViewportOffset().
5457 layoutViewport.SizeTo(aLayerMetrics.GetLayoutViewport().Size());
5458 Metrics().SetLayoutViewport(layoutViewport);
5460 needContentRepaint = true;
5461 viewportSizeUpdated = true;
5464 // TODO: Rely entirely on |aScrollMetadata.IsResolutionUpdated()| to
5465 // determine which branch to take, and drop the other conditions.
5466 CSSToParentLayerScale oldZoom = Metrics().GetZoom();
5467 if (FuzzyEqualsAdditive(
5468 Metrics().GetCompositionBoundsWidthIgnoringScrollbars(),
5469 aLayerMetrics.GetCompositionBoundsWidthIgnoringScrollbars()) &&
5470 Metrics().GetDevPixelsPerCSSPixel() ==
5471 aLayerMetrics.GetDevPixelsPerCSSPixel() &&
5472 !viewportSizeUpdated && !aScrollMetadata.IsResolutionUpdated()) {
5473 // Any change to the pres shell resolution was requested by APZ and is
5474 // already included in our zoom; however, other components of the
5475 // cumulative resolution (a parent document's pres-shell resolution, or
5476 // the css-driven resolution) may have changed, and we need to update
5477 // our zoom to reflect that. Note that we can't just take
5478 // aLayerMetrics.mZoom because the APZ may have additional async zoom
5479 // since the repaint request.
5480 float totalResolutionChange = 1.0;
5482 if (Metrics().GetCumulativeResolution() != LayoutDeviceToLayerScale(0)) {
5483 totalResolutionChange = aLayerMetrics.GetCumulativeResolution().scale /
5484 Metrics().GetCumulativeResolution().scale;
5487 float presShellResolutionChange = aLayerMetrics.GetPresShellResolution() /
5488 Metrics().GetPresShellResolution();
5489 if (presShellResolutionChange != 1.0f) {
5490 needContentRepaint = true;
5492 Metrics().ZoomBy(totalResolutionChange / presShellResolutionChange);
5493 for (auto& sampledState : mSampledState) {
5494 sampledState.ZoomBy(totalResolutionChange / presShellResolutionChange);
5496 } else {
5497 // Take the new zoom as either device scale or composition width or
5498 // viewport size got changed (e.g. due to orientation change, or content
5499 // changing the meta-viewport tag), or the main thread originated a
5500 // resolution change for another reason (e.g. Ctrl+0 was pressed to
5501 // reset the zoom).
5502 Metrics().SetZoom(aLayerMetrics.GetZoom());
5503 for (auto& sampledState : mSampledState) {
5504 sampledState.UpdateZoomProperties(aLayerMetrics);
5506 Metrics().SetDevPixelsPerCSSPixel(
5507 aLayerMetrics.GetDevPixelsPerCSSPixel());
5510 if (Metrics().GetZoom() != oldZoom) {
5511 // If the zoom changed, the scroll range in CSS pixels may have changed
5512 // even if the composition bounds didn't.
5513 needToReclampScroll = true;
5516 mExpectedGeckoMetrics.UpdateZoomFrom(aLayerMetrics);
5518 if (!Metrics().GetScrollableRect().IsEqualEdges(
5519 aLayerMetrics.GetScrollableRect())) {
5520 Metrics().SetScrollableRect(aLayerMetrics.GetScrollableRect());
5521 needContentRepaint = true;
5522 needToReclampScroll = true;
5524 if (!Metrics().GetCompositionBounds().IsEqualEdges(
5525 aLayerMetrics.GetCompositionBounds())) {
5526 Metrics().SetCompositionBounds(aLayerMetrics.GetCompositionBounds());
5527 needToReclampScroll = true;
5529 Metrics().SetCompositionBoundsWidthIgnoringScrollbars(
5530 aLayerMetrics.GetCompositionBoundsWidthIgnoringScrollbars());
5532 if (Metrics().IsRootContent() &&
5533 Metrics().GetCompositionSizeWithoutDynamicToolbar() !=
5534 aLayerMetrics.GetCompositionSizeWithoutDynamicToolbar()) {
5535 Metrics().SetCompositionSizeWithoutDynamicToolbar(
5536 aLayerMetrics.GetCompositionSizeWithoutDynamicToolbar());
5537 needToReclampScroll = true;
5539 Metrics().SetBoundingCompositionSize(
5540 aLayerMetrics.GetBoundingCompositionSize());
5541 Metrics().SetPresShellResolution(aLayerMetrics.GetPresShellResolution());
5542 Metrics().SetCumulativeResolution(aLayerMetrics.GetCumulativeResolution());
5543 Metrics().SetTransformToAncestorScale(
5544 aLayerMetrics.GetTransformToAncestorScale());
5545 mScrollMetadata.SetHasScrollgrab(aScrollMetadata.GetHasScrollgrab());
5546 mScrollMetadata.SetLineScrollAmount(aScrollMetadata.GetLineScrollAmount());
5547 mScrollMetadata.SetPageScrollAmount(aScrollMetadata.GetPageScrollAmount());
5548 mScrollMetadata.SetSnapInfo(ScrollSnapInfo(aScrollMetadata.GetSnapInfo()));
5549 mScrollMetadata.SetIsLayersIdRoot(aScrollMetadata.IsLayersIdRoot());
5550 mScrollMetadata.SetIsAutoDirRootContentRTL(
5551 aScrollMetadata.IsAutoDirRootContentRTL());
5552 Metrics().SetIsScrollInfoLayer(aLayerMetrics.IsScrollInfoLayer());
5553 Metrics().SetHasNonZeroDisplayPortMargins(
5554 aLayerMetrics.HasNonZeroDisplayPortMargins());
5555 Metrics().SetMinimalDisplayPort(aLayerMetrics.IsMinimalDisplayPort());
5556 mScrollMetadata.SetForceDisableApz(aScrollMetadata.IsApzForceDisabled());
5557 mScrollMetadata.SetIsRDMTouchSimulationActive(
5558 aScrollMetadata.GetIsRDMTouchSimulationActive());
5559 mScrollMetadata.SetForceMousewheelAutodir(
5560 aScrollMetadata.ForceMousewheelAutodir());
5561 mScrollMetadata.SetForceMousewheelAutodirHonourRoot(
5562 aScrollMetadata.ForceMousewheelAutodirHonourRoot());
5563 mScrollMetadata.SetIsPaginatedPresentation(
5564 aScrollMetadata.IsPaginatedPresentation());
5565 mScrollMetadata.SetDisregardedDirection(
5566 aScrollMetadata.GetDisregardedDirection());
5567 mScrollMetadata.SetOverscrollBehavior(
5568 aScrollMetadata.GetOverscrollBehavior());
5571 if (needToReclampScroll) {
5572 // Whenever scrollable rect or composition bounds has changed, we need to
5573 // re-clamp the scroll offset since it may be out of bounds. Also note that
5574 // we need to re-clamp before updating new scroll offsets from content since
5575 // we will use the last scroll offset to reflect the new offsets.
5576 ClampAndSetVisualScrollOffset(Metrics().GetVisualScrollOffset());
5577 for (auto& sampledState : mSampledState) {
5578 sampledState.ClampVisualScrollOffset(Metrics());
5582 bool instantScrollMayTriggerTransform = false;
5583 bool scrollOffsetUpdated = false;
5584 bool smoothScrollRequested = false;
5585 bool didCancelAnimation = false;
5586 Maybe<CSSPoint> cumulativeRelativeDelta;
5587 for (const auto& scrollUpdate : aScrollMetadata.GetScrollUpdates()) {
5588 APZC_LOG("%p processing scroll update %s\n", this,
5589 ToString(scrollUpdate).c_str());
5590 if (!(Metrics().GetScrollGeneration() < scrollUpdate.GetGeneration())) {
5591 // This is stale, let's ignore it
5592 APZC_LOG("%p scrollupdate generation stale, dropping\n", this);
5593 continue;
5595 Metrics().SetScrollGeneration(scrollUpdate.GetGeneration());
5597 MOZ_ASSERT(scrollUpdate.GetOrigin() != ScrollOrigin::Apz);
5598 if (userScrolled &&
5599 !nsLayoutUtils::CanScrollOriginClobberApz(scrollUpdate.GetOrigin())) {
5600 APZC_LOG("%p scrollupdate cannot clobber APZ userScrolled\n", this);
5601 continue;
5603 // XXX: if we get here, |scrollUpdate| is clobbering APZ, so we may want
5604 // to reset |userScrolled| back to false so that subsequent scrollUpdates
5605 // in this loop don't get dropped by the check above. Need to add a test
5606 // that exercises this scenario, as we don't currently have one.
5608 if (scrollUpdate.GetMode() == ScrollMode::Smooth ||
5609 scrollUpdate.GetMode() == ScrollMode::SmoothMsd) {
5610 smoothScrollRequested = true;
5612 // Requests to animate the visual scroll position override requests to
5613 // simply update the visual scroll offset to a particular point. Since
5614 // we have an animation request, we set ignoreVisualUpdate to true to
5615 // indicate we don't need to apply the visual scroll update in
5616 // aLayerMetrics.
5617 ignoreVisualUpdate = true;
5619 // For relative updates we want to add the relative offset to any existing
5620 // destination, or the current visual offset if there is no existing
5621 // destination.
5622 CSSPoint base = GetCurrentAnimationDestination(lock).valueOr(
5623 Metrics().GetVisualScrollOffset());
5625 CSSPoint destination;
5626 if (scrollUpdate.GetType() == ScrollUpdateType::Relative) {
5627 CSSPoint delta =
5628 scrollUpdate.GetDestination() - scrollUpdate.GetSource();
5629 APZC_LOG("%p relative smooth scrolling from %s by %s\n", this,
5630 ToString(base).c_str(), ToString(delta).c_str());
5631 destination = Metrics().CalculateScrollRange().ClampPoint(base + delta);
5632 } else if (scrollUpdate.GetType() == ScrollUpdateType::PureRelative) {
5633 CSSPoint delta = scrollUpdate.GetDelta();
5634 APZC_LOG("%p pure-relative smooth scrolling from %s by %s\n", this,
5635 ToString(base).c_str(), ToString(delta).c_str());
5636 destination = Metrics().CalculateScrollRange().ClampPoint(base + delta);
5637 } else {
5638 APZC_LOG("%p smooth scrolling to %s\n", this,
5639 ToString(scrollUpdate.GetDestination()).c_str());
5640 destination = scrollUpdate.GetDestination();
5643 if (scrollUpdate.GetMode() == ScrollMode::SmoothMsd) {
5644 SmoothMsdScrollTo(
5645 CSSSnapDestination{destination, scrollUpdate.GetSnapTargetIds()},
5646 scrollUpdate.GetScrollTriggeredByScript());
5647 } else {
5648 MOZ_ASSERT(scrollUpdate.GetMode() == ScrollMode::Smooth);
5649 SmoothScrollTo(
5650 CSSSnapDestination{destination, scrollUpdate.GetSnapTargetIds()},
5651 scrollUpdate.GetScrollTriggeredByScript(),
5652 scrollUpdate.GetOrigin());
5654 continue;
5657 MOZ_ASSERT(scrollUpdate.GetMode() == ScrollMode::Instant ||
5658 scrollUpdate.GetMode() == ScrollMode::Normal);
5660 instantScrollMayTriggerTransform =
5661 scrollUpdate.GetMode() == ScrollMode::Instant &&
5662 scrollUpdate.GetScrollTriggeredByScript() ==
5663 ScrollTriggeredByScript::No;
5665 // If the layout update is of a higher priority than the visual update, then
5666 // we don't want to apply the visual update.
5667 // If the layout update is of a clobbering type (or a smooth scroll request,
5668 // which is handled above) then it takes precedence over an eRestore visual
5669 // update. But we also allow the possibility for the main thread to ask us
5670 // to scroll both the layout and visual viewports to distinct (but
5671 // compatible) locations (via e.g. both updates being of a non-clobbering/
5672 // eRestore type).
5673 if (nsLayoutUtils::CanScrollOriginClobberApz(scrollUpdate.GetOrigin()) &&
5674 aLayerMetrics.GetVisualScrollUpdateType() !=
5675 FrameMetrics::eMainThread) {
5676 ignoreVisualUpdate = true;
5679 Maybe<CSSPoint> relativeDelta;
5680 if (scrollUpdate.GetType() == ScrollUpdateType::Relative) {
5681 APZC_LOG(
5682 "%p relative updating scroll offset from %s by %s\n", this,
5683 ToString(Metrics().GetVisualScrollOffset()).c_str(),
5684 ToString(scrollUpdate.GetDestination() - scrollUpdate.GetSource())
5685 .c_str());
5687 scrollOffsetUpdated = true;
5689 // It's possible that the main thread has ignored an APZ scroll offset
5690 // update for the pending relative scroll that we have just received.
5691 // When this happens, we need to send a new scroll offset update with
5692 // the combined scroll offset or else the main thread may have an
5693 // incorrect scroll offset for a period of time.
5694 if (Metrics().HasPendingScroll(aLayerMetrics)) {
5695 needContentRepaint = true;
5696 contentRepaintType = RepaintUpdateType::eUserAction;
5699 relativeDelta =
5700 Some(Metrics().ApplyRelativeScrollUpdateFrom(scrollUpdate));
5701 Metrics().RecalculateLayoutViewportOffset();
5702 } else if (scrollUpdate.GetType() == ScrollUpdateType::PureRelative) {
5703 APZC_LOG("%p pure-relative updating scroll offset from %s by %s\n", this,
5704 ToString(Metrics().GetVisualScrollOffset()).c_str(),
5705 ToString(scrollUpdate.GetDelta()).c_str());
5707 scrollOffsetUpdated = true;
5709 // Always need a repaint request with a repaint type for pure relative
5710 // scrolls because apz is doing the scroll at the main thread's request.
5711 // The main thread has not updated it's scroll offset yet, it is depending
5712 // on apz to tell it where to scroll.
5713 needContentRepaint = true;
5714 contentRepaintType = RepaintUpdateType::eVisualUpdate;
5716 // We have to ignore a visual scroll offset update otherwise it will
5717 // clobber the relative scrolling we are about to do. We perform
5718 // visualScrollOffset = visualScrollOffset + delta. Then the
5719 // visualScrollOffsetUpdated block below will do visualScrollOffset =
5720 // aLayerMetrics.GetVisualDestination(). We need visual scroll offset
5721 // updates to be incorporated into this scroll update loop to properly fix
5722 // this.
5723 ignoreVisualUpdate = true;
5725 relativeDelta =
5726 Some(Metrics().ApplyPureRelativeScrollUpdateFrom(scrollUpdate));
5727 Metrics().RecalculateLayoutViewportOffset();
5728 } else if (scrollUpdate.GetType() == ScrollUpdateType::MergeableAbsolute) {
5729 APZC_LOG("%p mergeable updating scroll offset from %s to %s\n", this,
5730 ToString(Metrics().GetVisualScrollOffset()).c_str(),
5731 ToString(scrollUpdate.GetDestination()).c_str());
5732 relativeDelta =
5733 Some(Metrics().ApplyAbsoluteScrollUpdateFrom(scrollUpdate).second);
5734 Metrics().RecalculateLayoutViewportOffset();
5735 scrollOffsetUpdated = true;
5736 } else {
5737 APZC_LOG("%p updating scroll offset from %s to %s\n", this,
5738 ToString(Metrics().GetVisualScrollOffset()).c_str(),
5739 ToString(scrollUpdate.GetDestination()).c_str());
5740 auto [offsetChanged, _] =
5741 Metrics().ApplyAbsoluteScrollUpdateFrom(scrollUpdate);
5742 Metrics().RecalculateLayoutViewportOffset();
5744 if (offsetChanged || scrollUpdate.GetMode() != ScrollMode::Instant ||
5745 scrollUpdate.GetType() != ScrollUpdateType::Absolute ||
5746 scrollUpdate.GetOrigin() != ScrollOrigin::None) {
5747 // We get a NewScrollFrame update for newly created scroll frames. Only
5748 // if this was not a NewScrollFrame update or the offset changed do we
5749 // request repaint. This is important so that we don't request repaint
5750 // for every new content and set a full display port on it.
5751 scrollOffsetUpdated = true;
5755 if (relativeDelta) {
5756 cumulativeRelativeDelta =
5757 !cumulativeRelativeDelta
5758 ? relativeDelta
5759 : Some(*cumulativeRelativeDelta + *relativeDelta);
5760 } else {
5761 // If the scroll update is not relative, clobber the cumulative delta,
5762 // i.e. later updates win.
5763 cumulativeRelativeDelta.reset();
5766 // If an animation is underway, tell it about the scroll offset update.
5767 // Some animations can handle some scroll offset updates and continue
5768 // running. Those that can't will return false, and we cancel them.
5769 if (ShouldCancelAnimationForScrollUpdate(relativeDelta)) {
5770 // Cancel the animation (which might also trigger a repaint request)
5771 // after we update the scroll offset above. Otherwise we can be left
5772 // in a state where things are out of sync.
5773 CancelAnimation();
5774 didCancelAnimation = true;
5778 if (scrollOffsetUpdated) {
5779 for (auto& sampledState : mSampledState) {
5780 if (!didCancelAnimation && cumulativeRelativeDelta.isSome()) {
5781 sampledState.UpdateScrollPropertiesWithRelativeDelta(
5782 Metrics(), *cumulativeRelativeDelta);
5783 } else {
5784 sampledState.UpdateScrollProperties(Metrics());
5788 // Because of the scroll generation update, any inflight paint requests
5789 // are going to be ignored by layout, and so mExpectedGeckoMetrics becomes
5790 // incorrect for the purposes of calculating the LD transform. To correct
5791 // this we need to update mExpectedGeckoMetrics to be the last thing we
5792 // know was painted by Gecko.
5793 mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
5795 // Since the scroll offset has changed, we need to recompute the
5796 // displayport margins and send them to layout. Otherwise there might be
5797 // scenarios where for example we scroll from the top of a page (where the
5798 // top displayport margin is zero) to the bottom of a page, which will
5799 // result in a displayport that doesn't extend upwards at all.
5800 // Note that even if the CancelAnimation call above requested a repaint
5801 // this is fine because we already have repaint request deduplication.
5802 needContentRepaint = true;
5803 // Since the main-thread scroll offset changed we should trigger a
5804 // recomposite to make sure it becomes user-visible.
5805 ScheduleComposite();
5807 // If the scroll offset was updated, we're not in a transforming state,
5808 // and we are scrolling by a non-zero delta, we should ensure
5809 // TransformBegin and TransformEnd notifications are sent.
5810 if (!IsTransformingState(mState) && instantScrollMayTriggerTransform &&
5811 cumulativeRelativeDelta && *cumulativeRelativeDelta != CSSPoint() &&
5812 !didCancelAnimation) {
5813 SendTransformBeginAndEnd();
5817 // If our scroll range changed (for example, because the page dynamically
5818 // loaded new content, thereby increasing the size of the scrollable rect),
5819 // and we're overscrolled, being overscrolled may no longer be a valid
5820 // state (for example, we may no longer be at the edge of our scroll range),
5821 // so clear overscroll and discontinue any overscroll animation.
5822 // Ideas for improvements here:
5823 // - Instead of collapsing the overscroll gutter, try to "fill it"
5824 // with newly loaded content. This would basically entail checking
5825 // if (GetVisualScrollOffset() + GetOverscrollAmount()) is a valid
5826 // visual scroll offset in our new scroll range, and if so, scrolling
5827 // there.
5828 if (needToReclampScroll) {
5829 if (IsInInvalidOverscroll()) {
5830 if (mState == OVERSCROLL_ANIMATION) {
5831 CancelAnimation();
5832 } else if (IsOverscrolled()) {
5833 ClearOverscroll();
5838 if (smoothScrollRequested && !scrollOffsetUpdated) {
5839 mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
5840 // Need to acknowledge the request.
5841 needContentRepaint = true;
5844 // If `isDefault` is true, this APZC is a "new" one (this is the first time
5845 // it's getting a NotifyLayersUpdated call). In this case we want to apply the
5846 // visual scroll offset from the main thread to our scroll offset.
5847 // The main thread may also ask us to scroll the visual viewport to a
5848 // particular location. However, in all cases, we want to ignore the visual
5849 // offset update if ignoreVisualUpdate is true, because we're clobbering
5850 // the visual update with a layout update.
5851 bool visualScrollOffsetUpdated =
5852 !ignoreVisualUpdate &&
5853 (isDefault ||
5854 aLayerMetrics.GetVisualScrollUpdateType() != FrameMetrics::eNone);
5856 if (visualScrollOffsetUpdated) {
5857 APZC_LOG("%p updating visual scroll offset from %s to %s (updateType %d)\n",
5858 this, ToString(Metrics().GetVisualScrollOffset()).c_str(),
5859 ToString(aLayerMetrics.GetVisualDestination()).c_str(),
5860 (int)aLayerMetrics.GetVisualScrollUpdateType());
5861 bool offsetChanged = Metrics().ClampAndSetVisualScrollOffset(
5862 aLayerMetrics.GetVisualDestination());
5864 // If this is the first time we got metrics for this content (isDefault) and
5865 // the update type was none and the offset didn't change then we don't have
5866 // to do anything. This is important because we don't want to request
5867 // repaint on the initial NotifyLayersUpdated for every content and thus set
5868 // a full display port.
5869 if (aLayerMetrics.GetVisualScrollUpdateType() == FrameMetrics::eNone &&
5870 !offsetChanged) {
5871 visualScrollOffsetUpdated = false;
5874 if (visualScrollOffsetUpdated) {
5875 // The rest of this branch largely follows the code in the
5876 // |if (scrollOffsetUpdated)| branch above. Eventually it should get
5877 // merged into that branch.
5878 Metrics().RecalculateLayoutViewportOffset();
5879 for (auto& sampledState : mSampledState) {
5880 sampledState.UpdateScrollProperties(Metrics());
5882 mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
5883 if (ShouldCancelAnimationForScrollUpdate(Nothing())) {
5884 CancelAnimation();
5886 // The main thread did not actually paint a displayport at the target
5887 // visual offset, so we need to ask it to repaint. We need to set the
5888 // contentRepaintType to something other than eNone, otherwise the main
5889 // thread will short-circuit the repaint request.
5890 // Don't do this for eRestore visual updates as a repaint coming from APZ
5891 // breaks the scroll offset restoration mechanism.
5892 needContentRepaint = true;
5893 if (aLayerMetrics.GetVisualScrollUpdateType() ==
5894 FrameMetrics::eMainThread) {
5895 contentRepaintType = RepaintUpdateType::eVisualUpdate;
5897 ScheduleComposite();
5900 if (viewportSizeUpdated) {
5901 // While we want to accept the main thread's layout viewport _size_,
5902 // its position may be out of date in light of async scrolling, to
5903 // adjust it if necessary to make sure it continues to enclose the
5904 // visual viewport.
5905 // Note: it's important to do this _after_ we've accepted any
5906 // updated composition bounds.
5907 Metrics().RecalculateLayoutViewportOffset();
5910 if (needContentRepaint) {
5911 // This repaint request could be driven by a user action if we accept a
5912 // relative scroll offset update
5913 RequestContentRepaint(contentRepaintType);
5917 FrameMetrics& AsyncPanZoomController::Metrics() {
5918 mRecursiveMutex.AssertCurrentThreadIn();
5919 return mScrollMetadata.GetMetrics();
5922 const FrameMetrics& AsyncPanZoomController::Metrics() const {
5923 mRecursiveMutex.AssertCurrentThreadIn();
5924 return mScrollMetadata.GetMetrics();
5927 GeckoViewMetrics AsyncPanZoomController::GetGeckoViewMetrics() const {
5928 RecursiveMutexAutoLock lock(mRecursiveMutex);
5929 return GeckoViewMetrics{GetEffectiveScrollOffset(eForCompositing, lock),
5930 GetEffectiveZoom(eForCompositing, lock)};
5933 bool AsyncPanZoomController::UpdateRootFrameMetricsIfChanged(
5934 GeckoViewMetrics& aMetrics) {
5935 RecursiveMutexAutoLock lock(mRecursiveMutex);
5937 if (!Metrics().IsRootContent()) {
5938 return false;
5941 GeckoViewMetrics newMetrics = GetGeckoViewMetrics();
5942 bool hasChanged = RoundedToInt(aMetrics.mVisualScrollOffset) !=
5943 RoundedToInt(newMetrics.mVisualScrollOffset) ||
5944 aMetrics.mZoom != newMetrics.mZoom;
5946 if (hasChanged) {
5947 aMetrics = newMetrics;
5950 return hasChanged;
5953 const FrameMetrics& AsyncPanZoomController::GetFrameMetrics() const {
5954 return Metrics();
5957 const ScrollMetadata& AsyncPanZoomController::GetScrollMetadata() const {
5958 mRecursiveMutex.AssertCurrentThreadIn();
5959 return mScrollMetadata;
5962 void AsyncPanZoomController::AssertOnSamplerThread() const {
5963 if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
5964 treeManagerLocal->AssertOnSamplerThread();
5968 void AsyncPanZoomController::AssertOnUpdaterThread() const {
5969 if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
5970 treeManagerLocal->AssertOnUpdaterThread();
5974 APZCTreeManager* AsyncPanZoomController::GetApzcTreeManager() const {
5975 mRecursiveMutex.AssertNotCurrentThreadIn();
5976 return mTreeManager;
5979 void AsyncPanZoomController::ZoomToRect(const ZoomTarget& aZoomTarget,
5980 const uint32_t aFlags) {
5981 CSSRect rect = aZoomTarget.targetRect;
5982 if (!rect.IsFinite()) {
5983 NS_WARNING("ZoomToRect got called with a non-finite rect; ignoring...");
5984 return;
5987 if (rect.IsEmpty() && (aFlags & DISABLE_ZOOM_OUT)) {
5988 // Double-tap-to-zooming uses an empty rect to mean "zoom out".
5989 // If zooming out is disabled, an empty rect is nonsensical
5990 // and will produce undesirable scrolling.
5991 NS_WARNING(
5992 "ZoomToRect got called with an empty rect and zoom out disabled; "
5993 "ignoring...");
5994 return;
5997 SetState(ANIMATING_ZOOM);
6000 RecursiveMutexAutoLock lock(mRecursiveMutex);
6002 MOZ_ASSERT(Metrics().IsRootContent());
6004 const float defaultZoomInAmount =
6005 StaticPrefs::apz_doubletapzoom_defaultzoomin();
6007 ParentLayerRect compositionBounds = Metrics().GetCompositionBounds();
6008 CSSRect cssPageRect = Metrics().GetScrollableRect();
6009 CSSPoint scrollOffset = Metrics().GetVisualScrollOffset();
6010 CSSSize sizeBeforeZoom = Metrics().CalculateCompositedSizeInCssPixels();
6011 CSSToParentLayerScale currentZoom = Metrics().GetZoom();
6012 CSSToParentLayerScale targetZoom;
6014 // The minimum zoom to prevent over-zoom-out.
6015 // If the zoom factor is lower than this (i.e. we are zoomed more into the
6016 // page), then the CSS content rect, in layers pixels, will be smaller than
6017 // the composition bounds. If this happens, we can't fill the target
6018 // composited area with this frame.
6019 const CSSRect cssExpandedPageRect = Metrics().GetExpandedScrollableRect();
6020 CSSToParentLayerScale localMinZoom(
6021 std::max(compositionBounds.Width() / cssExpandedPageRect.Width(),
6022 compositionBounds.Height() / cssExpandedPageRect.Height()));
6024 localMinZoom.scale =
6025 clamped(localMinZoom.scale, mZoomConstraints.mMinZoom.scale,
6026 mZoomConstraints.mMaxZoom.scale);
6028 localMinZoom = std::max(mZoomConstraints.mMinZoom, localMinZoom);
6029 CSSToParentLayerScale localMaxZoom =
6030 std::max(localMinZoom, mZoomConstraints.mMaxZoom);
6032 if (!rect.IsEmpty()) {
6033 // Intersect the zoom-to-rect to the CSS rect to make sure it fits.
6034 rect = rect.Intersect(cssPageRect);
6035 targetZoom = CSSToParentLayerScale(
6036 std::min(compositionBounds.Width() / rect.Width(),
6037 compositionBounds.Height() / rect.Height()));
6038 if (aFlags & DISABLE_ZOOM_OUT) {
6039 targetZoom = std::max(targetZoom, currentZoom);
6043 // 1. If the rect is empty, the content-side logic for handling a double-tap
6044 // requested that we zoom out.
6045 // 2. currentZoom is equal to mZoomConstraints.mMaxZoom and user still
6046 // double-tapping it
6047 // Treat these cases as a request to zoom out as much as possible
6048 // unless cantZoomOutBehavior == ZoomIn and currentZoom
6049 // is equal to localMinZoom and user still double-tapping it, then try to
6050 // zoom in a small amount to provide feedback to the user.
6051 bool zoomOut = false;
6052 // True if we are already zoomed out and we are asked to either stay there
6053 // or zoom out more and cantZoomOutBehavior == ZoomIn.
6054 bool zoomInDefaultAmount = false;
6055 if (aFlags & DISABLE_ZOOM_OUT) {
6056 zoomOut = false;
6057 } else {
6058 if (rect.IsEmpty()) {
6059 if (currentZoom == localMinZoom &&
6060 aZoomTarget.cantZoomOutBehavior == CantZoomOutBehavior::ZoomIn &&
6061 (defaultZoomInAmount != 1.f)) {
6062 zoomInDefaultAmount = true;
6063 } else {
6064 zoomOut = true;
6066 } else if (currentZoom == localMaxZoom && targetZoom >= localMaxZoom) {
6067 zoomOut = true;
6071 // already at min zoom and asked to zoom out further
6072 if (!zoomOut && currentZoom == localMinZoom && targetZoom <= localMinZoom &&
6073 aZoomTarget.cantZoomOutBehavior == CantZoomOutBehavior::ZoomIn &&
6074 (defaultZoomInAmount != 1.f)) {
6075 zoomInDefaultAmount = true;
6077 MOZ_ASSERT(!(zoomInDefaultAmount && zoomOut));
6079 if (zoomInDefaultAmount) {
6080 targetZoom =
6081 CSSToParentLayerScale(currentZoom.scale * defaultZoomInAmount);
6084 if (zoomOut) {
6085 targetZoom = localMinZoom;
6088 if (aFlags & PAN_INTO_VIEW_ONLY) {
6089 targetZoom = currentZoom;
6090 } else if (aFlags & ONLY_ZOOM_TO_DEFAULT_SCALE) {
6091 CSSToParentLayerScale zoomAtDefaultScale =
6092 Metrics().GetDevPixelsPerCSSPixel() *
6093 LayoutDeviceToParentLayerScale(1.0);
6094 if (targetZoom.scale > zoomAtDefaultScale.scale) {
6095 // Only change the zoom if we are less than the default zoom
6096 if (currentZoom.scale < zoomAtDefaultScale.scale) {
6097 targetZoom = zoomAtDefaultScale;
6098 } else {
6099 targetZoom = currentZoom;
6104 targetZoom.scale =
6105 clamped(targetZoom.scale, localMinZoom.scale, localMaxZoom.scale);
6107 FrameMetrics endZoomToMetrics = Metrics();
6108 endZoomToMetrics.SetZoom(CSSToParentLayerScale(targetZoom));
6109 CSSSize sizeAfterZoom =
6110 endZoomToMetrics.CalculateCompositedSizeInCssPixels();
6112 if (zoomInDefaultAmount || zoomOut) {
6113 // For the zoom out case we should always center what was visible
6114 // otherwise it feels like we are scrolling as well as zooming out. For
6115 // the non-zoomOut case, if we've been provided a pointer location, zoom
6116 // around that, otherwise just zoom in to the center of what's currently
6117 // visible.
6118 if (!zoomOut && aZoomTarget.documentRelativePointerPosition.isSome()) {
6119 rect = CSSRect(aZoomTarget.documentRelativePointerPosition->x -
6120 sizeAfterZoom.width / 2,
6121 aZoomTarget.documentRelativePointerPosition->y -
6122 sizeAfterZoom.height / 2,
6123 sizeAfterZoom.Width(), sizeAfterZoom.Height());
6124 } else {
6125 rect = CSSRect(
6126 scrollOffset.x + (sizeBeforeZoom.width - sizeAfterZoom.width) / 2,
6127 scrollOffset.y + (sizeBeforeZoom.height - sizeAfterZoom.height) / 2,
6128 sizeAfterZoom.Width(), sizeAfterZoom.Height());
6131 rect = rect.Intersect(cssPageRect);
6134 // Check if we can fit the full elementBoundingRect.
6135 if (!aZoomTarget.targetRect.IsEmpty() && !zoomOut &&
6136 aZoomTarget.elementBoundingRect.isSome()) {
6137 MOZ_ASSERT(aZoomTarget.elementBoundingRect->Contains(rect));
6138 CSSRect elementBoundingRect =
6139 aZoomTarget.elementBoundingRect->Intersect(cssPageRect);
6140 if (elementBoundingRect.width <= sizeAfterZoom.width &&
6141 elementBoundingRect.height <= sizeAfterZoom.height) {
6142 rect = elementBoundingRect;
6146 // Vertically center the zoomed element in the screen.
6147 if (!zoomOut && (sizeAfterZoom.height > rect.Height())) {
6148 rect.MoveByY(-(sizeAfterZoom.height - rect.Height()) * 0.5f);
6149 if (rect.Y() < 0.0f) {
6150 rect.MoveToY(0.0f);
6154 // Horizontally center the zoomed element in the screen.
6155 if (!zoomOut && (sizeAfterZoom.width > rect.Width())) {
6156 rect.MoveByX(-(sizeAfterZoom.width - rect.Width()) * 0.5f);
6157 if (rect.X() < 0.0f) {
6158 rect.MoveToX(0.0f);
6162 bool intersectRectAgain = false;
6163 // If we can't zoom out enough to show the full rect then shift the rect we
6164 // are able to show to center what was visible.
6165 // Note that this calculation works no matter the relation of sizeBeforeZoom
6166 // to sizeAfterZoom, ie whether we are increasing or decreasing zoom.
6167 if (!zoomOut && (sizeAfterZoom.height < rect.Height())) {
6168 rect.y =
6169 scrollOffset.y + (sizeBeforeZoom.height - sizeAfterZoom.height) / 2;
6170 rect.height = sizeAfterZoom.Height();
6172 intersectRectAgain = true;
6175 if (!zoomOut && (sizeAfterZoom.width < rect.Width())) {
6176 rect.x =
6177 scrollOffset.x + (sizeBeforeZoom.width - sizeAfterZoom.width) / 2;
6178 rect.width = sizeAfterZoom.Width();
6180 intersectRectAgain = true;
6182 if (intersectRectAgain) {
6183 rect = rect.Intersect(cssPageRect);
6186 // If any of these conditions are met, the page will be overscrolled after
6187 // zoomed. Attempting to scroll outside of the valid scroll range will cause
6188 // problems.
6189 if (rect.Y() + sizeAfterZoom.height > cssPageRect.YMost()) {
6190 rect.MoveToY(std::max(cssPageRect.Y(),
6191 cssPageRect.YMost() - sizeAfterZoom.height));
6193 if (rect.Y() < cssPageRect.Y()) {
6194 rect.MoveToY(cssPageRect.Y());
6196 if (rect.X() + sizeAfterZoom.width > cssPageRect.XMost()) {
6197 rect.MoveToX(
6198 std::max(cssPageRect.X(), cssPageRect.XMost() - sizeAfterZoom.width));
6200 if (rect.X() < cssPageRect.X()) {
6201 rect.MoveToY(cssPageRect.X());
6204 endZoomToMetrics.SetVisualScrollOffset(rect.TopLeft());
6205 endZoomToMetrics.RecalculateLayoutViewportOffset();
6207 StartAnimation(do_AddRef(new ZoomAnimation(
6208 *this, Metrics().GetVisualScrollOffset(), Metrics().GetZoom(),
6209 endZoomToMetrics.GetVisualScrollOffset(), endZoomToMetrics.GetZoom())));
6211 RequestContentRepaint(RepaintUpdateType::eUserAction);
6215 InputBlockState* AsyncPanZoomController::GetCurrentInputBlock() const {
6216 return GetInputQueue()->GetCurrentBlock();
6219 TouchBlockState* AsyncPanZoomController::GetCurrentTouchBlock() const {
6220 return GetInputQueue()->GetCurrentTouchBlock();
6223 PanGestureBlockState* AsyncPanZoomController::GetCurrentPanGestureBlock()
6224 const {
6225 return GetInputQueue()->GetCurrentPanGestureBlock();
6228 PinchGestureBlockState* AsyncPanZoomController::GetCurrentPinchGestureBlock()
6229 const {
6230 return GetInputQueue()->GetCurrentPinchGestureBlock();
6233 void AsyncPanZoomController::ResetTouchInputState() {
6234 MultiTouchInput cancel(MultiTouchInput::MULTITOUCH_CANCEL, 0,
6235 TimeStamp::Now(), 0);
6236 RefPtr<GestureEventListener> listener = GetGestureEventListener();
6237 if (listener) {
6238 listener->HandleInputEvent(cancel);
6240 CancelAnimationAndGestureState();
6241 // Clear overscroll along the entire handoff chain, in case an APZC
6242 // later in the chain is overscrolled.
6243 if (TouchBlockState* block = GetCurrentTouchBlock()) {
6244 block->GetOverscrollHandoffChain()->ClearOverscroll();
6248 void AsyncPanZoomController::ResetPanGestureInputState() {
6249 // No point sending a PANGESTURE_INTERRUPTED as all it does is
6250 // call CancelAnimation(), which we also do here.
6251 CancelAnimationAndGestureState();
6252 // Clear overscroll along the entire handoff chain, in case an APZC
6253 // later in the chain is overscrolled.
6254 if (PanGestureBlockState* block = GetCurrentPanGestureBlock()) {
6255 block->GetOverscrollHandoffChain()->ClearOverscroll();
6259 void AsyncPanZoomController::CancelAnimationAndGestureState() {
6260 mX.CancelGesture();
6261 mY.CancelGesture();
6262 CancelAnimation(CancelAnimationFlags::ScrollSnap);
6265 bool AsyncPanZoomController::HasReadyTouchBlock() const {
6266 return GetInputQueue()->HasReadyTouchBlock();
6269 bool AsyncPanZoomController::CanHandleScrollOffsetUpdate(PanZoomState aState) {
6270 return aState == NOTHING || aState == PAN_MOMENTUM || aState == TOUCHING ||
6271 IsPanningState(aState);
6274 bool AsyncPanZoomController::ShouldCancelAnimationForScrollUpdate(
6275 const Maybe<CSSPoint>& aRelativeDelta) {
6276 // Never call CancelAnimation() for a no-op relative update.
6277 if (aRelativeDelta == Some(CSSPoint())) {
6278 return false;
6281 if (mAnimation) {
6282 return !mAnimation->HandleScrollOffsetUpdate(aRelativeDelta);
6285 return !CanHandleScrollOffsetUpdate(mState);
6288 AsyncPanZoomController::PanZoomState
6289 AsyncPanZoomController::SetStateNoContentControllerDispatch(
6290 PanZoomState aNewState) {
6291 RecursiveMutexAutoLock lock(mRecursiveMutex);
6292 APZC_LOG_DETAIL("changing from state %s to %s\n", this,
6293 ToString(mState).c_str(), ToString(aNewState).c_str());
6294 PanZoomState oldState = mState;
6295 mState = aNewState;
6296 return oldState;
6299 void AsyncPanZoomController::SetState(PanZoomState aNewState) {
6300 // When a state transition to a transforming state is occuring and a delayed
6301 // transform end notification exists, send the TransformEnd notification
6302 // before the TransformBegin notification is sent for the input state change.
6303 if (IsTransformingState(aNewState) && IsDelayedTransformEndSet()) {
6304 MOZ_ASSERT(!IsTransformingState(mState));
6305 SetDelayedTransformEnd(false);
6306 DispatchStateChangeNotification(PANNING, NOTHING);
6309 PanZoomState oldState = SetStateNoContentControllerDispatch(aNewState);
6311 DispatchStateChangeNotification(oldState, aNewState);
6314 auto AsyncPanZoomController::GetState() const -> PanZoomState {
6315 RecursiveMutexAutoLock lock(mRecursiveMutex);
6316 return mState;
6319 void AsyncPanZoomController::DispatchStateChangeNotification(
6320 PanZoomState aOldState, PanZoomState aNewState) {
6321 { // scope the lock
6322 RecursiveMutexAutoLock lock(mRecursiveMutex);
6323 if (mNotificationBlockers > 0) {
6324 return;
6328 if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
6329 if (!IsTransformingState(aOldState) && IsTransformingState(aNewState)) {
6330 controller->NotifyAPZStateChange(GetGuid(),
6331 APZStateChange::eTransformBegin);
6332 } else if (IsTransformingState(aOldState) &&
6333 !IsTransformingState(aNewState)) {
6334 controller->NotifyAPZStateChange(GetGuid(),
6335 APZStateChange::eTransformEnd);
6339 void AsyncPanZoomController::SendTransformBeginAndEnd() {
6340 RefPtr<GeckoContentController> controller = GetGeckoContentController();
6341 if (controller) {
6342 controller->NotifyAPZStateChange(GetGuid(),
6343 APZStateChange::eTransformBegin);
6344 controller->NotifyAPZStateChange(GetGuid(), APZStateChange::eTransformEnd);
6348 bool AsyncPanZoomController::IsInTransformingState() const {
6349 RecursiveMutexAutoLock lock(mRecursiveMutex);
6350 return IsTransformingState(mState);
6353 bool AsyncPanZoomController::IsTransformingState(PanZoomState aState) {
6354 return !(aState == NOTHING || aState == TOUCHING);
6357 bool AsyncPanZoomController::IsPanningState(PanZoomState aState) {
6358 return (aState == PANNING || aState == PANNING_LOCKED_X ||
6359 aState == PANNING_LOCKED_Y);
6362 bool AsyncPanZoomController::IsInPanningState() const {
6363 return IsPanningState(mState);
6366 bool AsyncPanZoomController::IsInScrollingGesture() const {
6367 return IsPanningState(mState) || mState == SCROLLBAR_DRAG ||
6368 mState == TOUCHING || mState == PINCHING;
6371 bool AsyncPanZoomController::IsDelayedTransformEndSet() {
6372 RecursiveMutexAutoLock lock(mRecursiveMutex);
6373 return mDelayedTransformEnd;
6376 void AsyncPanZoomController::SetDelayedTransformEnd(bool aDelayedTransformEnd) {
6377 RecursiveMutexAutoLock lock(mRecursiveMutex);
6378 mDelayedTransformEnd = aDelayedTransformEnd;
6381 void AsyncPanZoomController::UpdateZoomConstraints(
6382 const ZoomConstraints& aConstraints) {
6383 if ((MOZ_LOG_TEST(sApzCtlLog, LogLevel::Debug) &&
6384 (aConstraints != mZoomConstraints)) ||
6385 MOZ_LOG_TEST(sApzCtlLog, LogLevel::Verbose)) {
6386 APZC_LOG("%p updating zoom constraints to %d %d %f %f\n", this,
6387 aConstraints.mAllowZoom, aConstraints.mAllowDoubleTapZoom,
6388 aConstraints.mMinZoom.scale, aConstraints.mMaxZoom.scale);
6391 if (std::isnan(aConstraints.mMinZoom.scale) ||
6392 std::isnan(aConstraints.mMaxZoom.scale)) {
6393 NS_WARNING("APZC received zoom constraints with NaN values; dropping...");
6394 return;
6397 RecursiveMutexAutoLock lock(mRecursiveMutex);
6398 CSSToParentLayerScale min = Metrics().GetDevPixelsPerCSSPixel() *
6399 ViewportMinScale() / ParentLayerToScreenScale(1);
6400 CSSToParentLayerScale max = Metrics().GetDevPixelsPerCSSPixel() *
6401 ViewportMaxScale() / ParentLayerToScreenScale(1);
6403 // inf float values and other bad cases should be sanitized by the code below.
6404 mZoomConstraints.mAllowZoom = aConstraints.mAllowZoom;
6405 mZoomConstraints.mAllowDoubleTapZoom = aConstraints.mAllowDoubleTapZoom;
6406 mZoomConstraints.mMinZoom =
6407 (min > aConstraints.mMinZoom ? min : aConstraints.mMinZoom);
6408 mZoomConstraints.mMaxZoom =
6409 (max > aConstraints.mMaxZoom ? aConstraints.mMaxZoom : max);
6410 if (mZoomConstraints.mMaxZoom < mZoomConstraints.mMinZoom) {
6411 mZoomConstraints.mMaxZoom = mZoomConstraints.mMinZoom;
6415 bool AsyncPanZoomController::ZoomConstraintsAllowZoom() const {
6416 RecursiveMutexAutoLock lock(mRecursiveMutex);
6417 return mZoomConstraints.mAllowZoom;
6420 bool AsyncPanZoomController::ZoomConstraintsAllowDoubleTapZoom() const {
6421 RecursiveMutexAutoLock lock(mRecursiveMutex);
6422 return mZoomConstraints.mAllowDoubleTapZoom;
6425 void AsyncPanZoomController::PostDelayedTask(already_AddRefed<Runnable> aTask,
6426 int aDelayMs) {
6427 APZThreadUtils::AssertOnControllerThread();
6428 RefPtr<Runnable> task = aTask;
6429 RefPtr<GeckoContentController> controller = GetGeckoContentController();
6430 if (controller) {
6431 controller->PostDelayedTask(task.forget(), aDelayMs);
6433 // If there is no controller, that means this APZC has been destroyed, and
6434 // we probably don't need to run the task. It will get destroyed when the
6435 // RefPtr goes out of scope.
6438 bool AsyncPanZoomController::Matches(const ScrollableLayerGuid& aGuid) {
6439 return aGuid == GetGuid();
6442 bool AsyncPanZoomController::HasTreeManager(
6443 const APZCTreeManager* aTreeManager) const {
6444 return GetApzcTreeManager() == aTreeManager;
6447 void AsyncPanZoomController::GetGuid(ScrollableLayerGuid* aGuidOut) const {
6448 if (aGuidOut) {
6449 *aGuidOut = GetGuid();
6453 ScrollableLayerGuid AsyncPanZoomController::GetGuid() const {
6454 RecursiveMutexAutoLock lock(mRecursiveMutex);
6455 return ScrollableLayerGuid(mLayersId, Metrics().GetPresShellId(),
6456 Metrics().GetScrollId());
6459 void AsyncPanZoomController::SetTestAsyncScrollOffset(const CSSPoint& aPoint) {
6460 RecursiveMutexAutoLock lock(mRecursiveMutex);
6461 mTestAsyncScrollOffset = aPoint;
6462 ScheduleComposite();
6465 void AsyncPanZoomController::SetTestAsyncZoom(
6466 const LayerToParentLayerScale& aZoom) {
6467 RecursiveMutexAutoLock lock(mRecursiveMutex);
6468 mTestAsyncZoom = aZoom;
6469 ScheduleComposite();
6472 Maybe<CSSSnapDestination> AsyncPanZoomController::FindSnapPointNear(
6473 const CSSPoint& aDestination, ScrollUnit aUnit,
6474 ScrollSnapFlags aSnapFlags) {
6475 mRecursiveMutex.AssertCurrentThreadIn();
6476 APZC_LOG("%p scroll snapping near %s\n", this,
6477 ToString(aDestination).c_str());
6478 CSSRect scrollRange = Metrics().CalculateScrollRange();
6479 if (auto snapDestination = ScrollSnapUtils::GetSnapPointForDestination(
6480 mScrollMetadata.GetSnapInfo(), aUnit, aSnapFlags,
6481 CSSRect::ToAppUnits(scrollRange),
6482 CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset()),
6483 CSSPoint::ToAppUnits(aDestination))) {
6484 CSSPoint cssSnapPoint = CSSPoint::FromAppUnits(snapDestination->mPosition);
6485 // GetSnapPointForDestination() can produce a destination that's outside
6486 // of the scroll frame's scroll range. Clamp it here (this matches the
6487 // behaviour of the main-thread code path, which clamps it in
6488 // nsGfxScrollFrame::ScrollTo()).
6489 return Some(CSSSnapDestination{scrollRange.ClampPoint(cssSnapPoint),
6490 snapDestination->mTargetIds});
6492 return Nothing();
6495 Maybe<std::pair<MultiTouchInput, MultiTouchInput>>
6496 AsyncPanZoomController::MaybeSplitTouchMoveEvent(
6497 const MultiTouchInput& aOriginalEvent, ScreenCoord aPanThreshold,
6498 float aVectorLength, ExternalPoint& aExtPoint) {
6499 if (aVectorLength <= aPanThreshold) {
6500 return Nothing();
6503 auto splitEvent = std::make_pair(aOriginalEvent, aOriginalEvent);
6505 SingleTouchData& firstTouchData = splitEvent.first.mTouches[0];
6506 SingleTouchData& secondTouchData = splitEvent.second.mTouches[0];
6508 firstTouchData.mHistoricalData.Clear();
6509 secondTouchData.mHistoricalData.Clear();
6511 ExternalPoint destination = aExtPoint;
6512 ExternalPoint thresholdPosition;
6514 const float ratio = aPanThreshold / aVectorLength;
6515 thresholdPosition.x = mStartTouch.x + ratio * (destination.x - mStartTouch.x);
6516 thresholdPosition.y = mStartTouch.y + ratio * (destination.y - mStartTouch.y);
6518 TouchSample start{mLastTouch};
6519 // To compute the timestamp of the first event (which is at the threshold),
6520 // use linear interpolation with the starting point |start| being the last
6521 // event that's before the threshold, and the end point |end| being the first
6522 // event after the threshold.
6524 // The initial choice for |start| is the last touch event before
6525 // |aOriginalEvent|, and the initial choice for |end| is |aOriginalEvent|.
6527 // However, the historical data points stored in |aOriginalEvent| may contain
6528 // intermediate positions that can serve as tighter bounds for the
6529 // interpolation.
6530 TouchSample end{destination, aOriginalEvent.mTimeStamp};
6532 for (const auto& historicalData :
6533 aOriginalEvent.mTouches[0].mHistoricalData) {
6534 ExternalPoint histExtPoint = ToExternalPoint(aOriginalEvent.mScreenOffset,
6535 historicalData.mScreenPoint);
6537 if (PanVector(histExtPoint).Length() <
6538 PanVector(thresholdPosition).Length()) {
6539 start = {histExtPoint, historicalData.mTimeStamp};
6540 } else {
6541 break;
6545 for (const SingleTouchData::HistoricalTouchData& histData :
6546 Reversed(aOriginalEvent.mTouches[0].mHistoricalData)) {
6547 ExternalPoint histExtPoint =
6548 ToExternalPoint(aOriginalEvent.mScreenOffset, histData.mScreenPoint);
6550 if (PanVector(histExtPoint).Length() >
6551 PanVector(thresholdPosition).Length()) {
6552 end = {histExtPoint, histData.mTimeStamp};
6553 } else {
6554 break;
6558 const float totalLength =
6559 ScreenPoint(fabs(end.mPosition.x - start.mPosition.x),
6560 fabs(end.mPosition.y - start.mPosition.y))
6561 .Length();
6562 const float thresholdLength =
6563 ScreenPoint(fabs(thresholdPosition.x - start.mPosition.x),
6564 fabs(thresholdPosition.y - start.mPosition.y))
6565 .Length();
6566 const float splitRatio = thresholdLength / totalLength;
6568 splitEvent.first.mTimeStamp =
6569 start.mTimeStamp +
6570 (end.mTimeStamp - start.mTimeStamp).MultDouble(splitRatio);
6572 for (const auto& historicalData :
6573 aOriginalEvent.mTouches[0].mHistoricalData) {
6574 if (historicalData.mTimeStamp > splitEvent.first.mTimeStamp) {
6575 secondTouchData.mHistoricalData.AppendElement(historicalData);
6576 } else {
6577 firstTouchData.mHistoricalData.AppendElement(historicalData);
6581 firstTouchData.mScreenPoint = RoundedToInt(
6582 ViewAs<ScreenPixel>(thresholdPosition - splitEvent.first.mScreenOffset,
6583 PixelCastJustification::ExternalIsScreen));
6585 // Recompute firstTouchData.mLocalScreenPoint.
6586 splitEvent.first.TransformToLocal(GetTransformToThis());
6588 // Pass |thresholdPosition| back out to the caller via |aExtPoint|
6589 aExtPoint = thresholdPosition;
6591 return Some(splitEvent);
6594 void AsyncPanZoomController::ScrollSnapNear(const CSSPoint& aDestination,
6595 ScrollSnapFlags aSnapFlags) {
6596 if (Maybe<CSSSnapDestination> snapDestination = FindSnapPointNear(
6597 aDestination, ScrollUnit::DEVICE_PIXELS, aSnapFlags)) {
6598 if (snapDestination->mPosition != Metrics().GetVisualScrollOffset()) {
6599 APZC_LOG("%p smooth scrolling to snap point %s\n", this,
6600 ToString(snapDestination->mPosition).c_str());
6601 SmoothMsdScrollTo(std::move(*snapDestination),
6602 ScrollTriggeredByScript::No);
6607 void AsyncPanZoomController::ScrollSnap(ScrollSnapFlags aSnapFlags) {
6608 RecursiveMutexAutoLock lock(mRecursiveMutex);
6609 ScrollSnapNear(Metrics().GetVisualScrollOffset(), aSnapFlags);
6612 void AsyncPanZoomController::ScrollSnapToDestination() {
6613 RecursiveMutexAutoLock lock(mRecursiveMutex);
6615 float friction = StaticPrefs::apz_fling_friction();
6616 ParentLayerPoint velocity(mX.GetVelocity(), mY.GetVelocity());
6617 ParentLayerPoint predictedDelta;
6618 // "-velocity / log(1.0 - friction)" is the integral of the deceleration
6619 // curve modeled for flings in the "Axis" class.
6620 if (velocity.x != 0.0f && friction != 0.0f) {
6621 predictedDelta.x = -velocity.x / log(1.0 - friction);
6623 if (velocity.y != 0.0f && friction != 0.0f) {
6624 predictedDelta.y = -velocity.y / log(1.0 - friction);
6627 // If the fling will overscroll, don't scroll snap, because then the user
6628 // user would not see any overscroll animation.
6629 bool flingWillOverscroll =
6630 IsOverscrolled() && ((velocity.x.value * mX.GetOverscroll() >= 0) ||
6631 (velocity.y.value * mY.GetOverscroll() >= 0));
6632 if (flingWillOverscroll) {
6633 return;
6636 CSSPoint startPosition = Metrics().GetVisualScrollOffset();
6637 ScrollSnapFlags snapFlags = ScrollSnapFlags::IntendedEndPosition;
6638 if (predictedDelta != ParentLayerPoint()) {
6639 snapFlags |= ScrollSnapFlags::IntendedDirection;
6641 if (Maybe<CSSSnapDestination> snapDestination =
6642 MaybeAdjustDeltaForScrollSnapping(ScrollUnit::DEVICE_PIXELS,
6643 snapFlags, predictedDelta,
6644 startPosition)) {
6645 APZC_LOG(
6646 "%p fling snapping. friction: %f velocity: %f, %f "
6647 "predictedDelta: %f, %f position: %f, %f "
6648 "snapDestination: %f, %f\n",
6649 this, friction, velocity.x.value, velocity.y.value,
6650 predictedDelta.x.value, predictedDelta.y.value,
6651 Metrics().GetVisualScrollOffset().x.value,
6652 Metrics().GetVisualScrollOffset().y.value, startPosition.x.value,
6653 startPosition.y.value);
6655 // Ensure that any queued transform-end due to a pan-end is not
6656 // sent. Instead rely on the transform-end sent due to the
6657 // scroll snap animation.
6658 SetDelayedTransformEnd(false);
6660 SmoothMsdScrollTo(std::move(*snapDestination), ScrollTriggeredByScript::No);
6664 Maybe<CSSSnapDestination>
6665 AsyncPanZoomController::MaybeAdjustDeltaForScrollSnapping(
6666 ScrollUnit aUnit, ScrollSnapFlags aSnapFlags, ParentLayerPoint& aDelta,
6667 CSSPoint& aStartPosition) {
6668 RecursiveMutexAutoLock lock(mRecursiveMutex);
6669 CSSToParentLayerScale zoom = Metrics().GetZoom();
6670 if (zoom == CSSToParentLayerScale(0)) {
6671 return Nothing();
6673 CSSPoint destination = Metrics().CalculateScrollRange().ClampPoint(
6674 aStartPosition + (aDelta / zoom));
6676 if (Maybe<CSSSnapDestination> snapDestination =
6677 FindSnapPointNear(destination, aUnit, aSnapFlags)) {
6678 aDelta = (snapDestination->mPosition - aStartPosition) * zoom;
6679 aStartPosition = snapDestination->mPosition;
6680 return snapDestination;
6682 return Nothing();
6685 Maybe<CSSSnapDestination>
6686 AsyncPanZoomController::MaybeAdjustDeltaForScrollSnappingOnWheelInput(
6687 const ScrollWheelInput& aEvent, ParentLayerPoint& aDelta,
6688 CSSPoint& aStartPosition) {
6689 // Don't scroll snap for pixel scrolls. This matches the main thread
6690 // behaviour in EventStateManager::DoScrollText().
6691 if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_PIXEL) {
6692 return Nothing();
6695 // Note that this MaybeAdjustDeltaForScrollSnappingOnWheelInput also gets
6696 // called for pan gestures at least on older Mac and Windows. In such cases
6697 // `aEvent.mDeltaType` is `SCROLLDELTA_PIXEL` which should be filtered out by
6698 // the above `if` block, so we assume all incoming `aEvent` are purely wheel
6699 // events, thus we basically use `IntendedDirection` here.
6700 // If we want to change the behavior, i.e. we want to do scroll snap for
6701 // such cases as well, we need to use `IntendedEndPoint`.
6702 ScrollSnapFlags snapFlags = ScrollSnapFlags::IntendedDirection;
6703 if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_PAGE) {
6704 // On Windows there are a couple of cases where scroll events happen with
6705 // SCROLLDELTA_PAGE, in such case we consider it's a page scroll.
6706 snapFlags |= ScrollSnapFlags::IntendedEndPosition;
6708 return MaybeAdjustDeltaForScrollSnapping(
6709 ScrollWheelInput::ScrollUnitForDeltaType(aEvent.mDeltaType),
6710 ScrollSnapFlags::IntendedDirection, aDelta, aStartPosition);
6713 Maybe<CSSSnapDestination>
6714 AsyncPanZoomController::MaybeAdjustDestinationForScrollSnapping(
6715 const KeyboardInput& aEvent, CSSPoint& aDestination,
6716 ScrollSnapFlags aSnapFlags) {
6717 RecursiveMutexAutoLock lock(mRecursiveMutex);
6718 ScrollUnit unit = KeyboardScrollAction::GetScrollUnit(aEvent.mAction.mType);
6720 if (Maybe<CSSSnapDestination> snapPoint =
6721 FindSnapPointNear(aDestination, unit, aSnapFlags)) {
6722 aDestination = snapPoint->mPosition;
6723 return snapPoint;
6725 return Nothing();
6728 void AsyncPanZoomController::SetZoomAnimationId(
6729 const Maybe<uint64_t>& aZoomAnimationId) {
6730 RecursiveMutexAutoLock lock(mRecursiveMutex);
6731 mZoomAnimationId = aZoomAnimationId;
6734 Maybe<uint64_t> AsyncPanZoomController::GetZoomAnimationId() const {
6735 RecursiveMutexAutoLock lock(mRecursiveMutex);
6736 return mZoomAnimationId;
6739 std::ostream& operator<<(std::ostream& aOut,
6740 const AsyncPanZoomController::PanZoomState& aState) {
6741 switch (aState) {
6742 case AsyncPanZoomController::PanZoomState::NOTHING:
6743 aOut << "NOTHING";
6744 break;
6745 case AsyncPanZoomController::PanZoomState::FLING:
6746 aOut << "FLING";
6747 break;
6748 case AsyncPanZoomController::PanZoomState::TOUCHING:
6749 aOut << "TOUCHING";
6750 break;
6751 case AsyncPanZoomController::PanZoomState::PANNING:
6752 aOut << "PANNING";
6753 break;
6754 case AsyncPanZoomController::PanZoomState::PANNING_LOCKED_X:
6755 aOut << "PANNING_LOCKED_X";
6756 break;
6757 case AsyncPanZoomController::PanZoomState::PANNING_LOCKED_Y:
6758 aOut << "PANNING_LOCKED_Y";
6759 break;
6760 case AsyncPanZoomController::PanZoomState::PAN_MOMENTUM:
6761 aOut << "PAN_MOMENTUM";
6762 break;
6763 case AsyncPanZoomController::PanZoomState::PINCHING:
6764 aOut << "PINCHING";
6765 break;
6766 case AsyncPanZoomController::PanZoomState::ANIMATING_ZOOM:
6767 aOut << "ANIMATING_ZOOM";
6768 break;
6769 case AsyncPanZoomController::PanZoomState::OVERSCROLL_ANIMATION:
6770 aOut << "OVERSCROLL_ANIMATION";
6771 break;
6772 case AsyncPanZoomController::PanZoomState::SMOOTH_SCROLL:
6773 aOut << "SMOOTH_SCROLL";
6774 break;
6775 case AsyncPanZoomController::PanZoomState::SMOOTHMSD_SCROLL:
6776 aOut << "SMOOTHMSD_SCROLL";
6777 break;
6778 case AsyncPanZoomController::PanZoomState::WHEEL_SCROLL:
6779 aOut << "WHEEL_SCROLL";
6780 break;
6781 case AsyncPanZoomController::PanZoomState::KEYBOARD_SCROLL:
6782 aOut << "KEYBOARD_SCROLL";
6783 break;
6784 case AsyncPanZoomController::PanZoomState::AUTOSCROLL:
6785 aOut << "AUTOSCROLL";
6786 break;
6787 case AsyncPanZoomController::PanZoomState::SCROLLBAR_DRAG:
6788 aOut << "SCROLLBAR_DRAG";
6789 break;
6790 default:
6791 aOut << "UNKNOWN_STATE";
6792 break;
6794 return aOut;
6797 bool operator==(const PointerEventsConsumableFlags& aLhs,
6798 const PointerEventsConsumableFlags& aRhs) {
6799 return (aLhs.mHasRoom == aRhs.mHasRoom) &&
6800 (aLhs.mAllowedByTouchAction == aRhs.mAllowedByTouchAction);
6803 std::ostream& operator<<(std::ostream& aOut,
6804 const PointerEventsConsumableFlags& aFlags) {
6805 aOut << std::boolalpha << "{ hasRoom: " << aFlags.mHasRoom
6806 << ", allowedByTouchAction: " << aFlags.mAllowedByTouchAction << "}";
6807 return aOut;
6810 } // namespace layers
6811 } // namespace mozilla