Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
#include "AsyncPanZoomController.h" // for AsyncPanZoomController, etc
#include <math.h> // for fabsf, fabs, atan2
#include <stdint.h> // for uint32_t, uint64_t
#include <sys/types.h> // for int32_t
#include <algorithm> // for max, min
#include <utility> // for std::make_pair
#include "APZCTreeManager.h" // for APZCTreeManager
#include "AsyncPanZoomAnimation.h" // for AsyncPanZoomAnimation
#include "AutoDirWheelDeltaAdjuster.h" // for APZAutoDirWheelDeltaAdjuster
#include "AutoscrollAnimation.h" // for AutoscrollAnimation
#include "Axis.h" // for AxisX, AxisY, Axis, etc
#include "CheckerboardEvent.h" // for CheckerboardEvent
#include "Compositor.h" // for Compositor
#include "DesktopFlingPhysics.h" // for DesktopFlingPhysics
#include "FrameMetrics.h" // for FrameMetrics, etc
#include "GenericFlingAnimation.h" // for GenericFlingAnimation
#include "GestureEventListener.h" // for GestureEventListener
#include "HitTestingTreeNode.h" // for HitTestingTreeNode
#include "InputData.h" // for MultiTouchInput, etc
#include "InputBlockState.h" // for InputBlockState, TouchBlockState
#include "InputQueue.h" // for InputQueue
#include "Overscroll.h" // for OverscrollAnimation
#include "OverscrollHandoffState.h" // for OverscrollHandoffState
#include "SimpleVelocityTracker.h" // for SimpleVelocityTracker
#include "Units.h" // for CSSRect, CSSPoint, etc
#include "UnitTransforms.h" // for TransformTo
#include "base/message_loop.h" // for MessageLoop
#include "base/task.h" // for NewRunnableMethod, etc
#include "gfxTypes.h" // for gfxFloat
#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
#include "mozilla/BasicEvents.h" // for Modifiers, MODIFIER_*
#include "mozilla/ClearOnShutdown.h" // for ClearOnShutdown
#include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction
#include "mozilla/EventForwards.h" // for nsEventStatus_*
#include "mozilla/EventStateManager.h" // for EventStateManager
#include "mozilla/glean/GleanMetrics.h"
#include "mozilla/MouseEvents.h" // for WidgetWheelEvent
#include "mozilla/Preferences.h" // for Preferences
#include "mozilla/RecursiveMutex.h" // for RecursiveMutexAutoLock, etc
#include "mozilla/RefPtr.h" // for RefPtr
#include "mozilla/ScrollTypes.h"
#include "mozilla/StaticPrefs_apz.h"
#include "mozilla/StaticPrefs_general.h"
#include "mozilla/StaticPrefs_gfx.h"
#include "mozilla/StaticPrefs_mousewheel.h"
#include "mozilla/StaticPrefs_layers.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/StaticPrefs_slider.h"
#include "mozilla/StaticPrefs_test.h"
#include "mozilla/StaticPrefs_toolkit.h"
#include "mozilla/Telemetry.h" // for Telemetry
#include "mozilla/TimeStamp.h" // for TimeDuration, TimeStamp
#include "mozilla/dom/CheckerboardReportService.h" // for CheckerboardEventStorage
// note: CheckerboardReportService.h actually lives in gfx/layers/apz/util/
#include "mozilla/dom/Touch.h" // for Touch
#include "mozilla/gfx/gfxVars.h" // for gfxVars
#include "mozilla/gfx/BasePoint.h" // for BasePoint
#include "mozilla/gfx/BaseRect.h" // for BaseRect
#include "mozilla/gfx/Point.h" // for Point, RoundedToInt, etc
#include "mozilla/gfx/Rect.h" // for RoundedIn
#include "mozilla/gfx/ScaleFactor.h" // for ScaleFactor
#include "mozilla/layers/APZThreadUtils.h" // for AssertOnControllerThread, etc
#include "mozilla/layers/APZUtils.h" // for AsyncTransform
#include "mozilla/layers/CompositorController.h" // for CompositorController
#include "mozilla/layers/DirectionUtils.h" // for GetAxis{Start,End,Length,Scale}
#include "mozilla/layers/APZPublicUtils.h" // for GetScrollMode
#include "mozilla/webrender/WebRenderAPI.h" // for MinimapData
#include "mozilla/mozalloc.h" // for operator new, etc
#include "mozilla/Unused.h" // for unused
#include "mozilla/webrender/WebRenderTypes.h"
#include "nsCOMPtr.h" // for already_AddRefed
#include "nsDebug.h" // for NS_WARNING
#include "nsLayoutUtils.h"
#include "nsMathUtils.h" // for NS_hypot
#include "nsPoint.h" // for nsIntPoint
#include "nsStyleConsts.h"
#include "nsTArray.h" // for nsTArray, nsTArray_Impl, etc
#include "nsThreadUtils.h" // for NS_IsMainThread
#include "nsViewportInfo.h" // for ViewportMinScale(), ViewportMaxScale()
#include "prsystem.h" // for PR_GetPhysicalMemorySize
#include "mozilla/ipc/SharedMemory.h" // for SharedMemory
#include "ScrollSnap.h" // for ScrollSnapUtils
#include "ScrollAnimationPhysics.h" // for ComputeAcceleratedWheelDelta
#include "SmoothMsdScrollAnimation.h"
#include "SmoothScrollAnimation.h"
#include "WheelScrollAnimation.h"
#if defined(MOZ_WIDGET_ANDROID)
# include "AndroidAPZ.h"
#endif // defined(MOZ_WIDGET_ANDROID)
static mozilla::LazyLogModule sApzCtlLog("apz.controller");
#define APZC_LOG(...) MOZ_LOG(sApzCtlLog, LogLevel::Debug, (__VA_ARGS__))
#define APZC_LOGV(...) MOZ_LOG(sApzCtlLog, LogLevel::Verbose, (__VA_ARGS__))
// Log to the apz.controller log with additional info from the APZC
#define APZC_LOG_DETAIL(fmt, apzc, ...) \
APZC_LOG("%p(%s scrollId=%" PRIu64 "): " fmt, (apzc), \
(apzc)->IsRootContent() ? "root" : "subframe", \
(apzc)->GetScrollId(), ##__VA_ARGS__)
#define APZC_LOGV_DETAIL(fmt, apzc, ...) \
APZC_LOGV("%p(%s scrollId=%" PRIu64 "): " fmt, (apzc), \
(apzc)->IsRootContent() ? "root" : "subframe", \
(apzc)->GetScrollId(), ##__VA_ARGS__)
#define APZC_LOG_FM_COMMON(fm, prefix, level, ...) \
if (MOZ_LOG_TEST(sApzCtlLog, level)) { \
std::stringstream ss; \
ss << nsPrintfCString(prefix, __VA_ARGS__).get() << ":" << fm; \
MOZ_LOG(sApzCtlLog, level, ("%s\n", ss.str().c_str())); \
}
#define APZC_LOG_FM(fm, prefix, ...) \
APZC_LOG_FM_COMMON(fm, prefix, LogLevel::Debug, __VA_ARGS__)
#define APZC_LOGV_FM(fm, prefix, ...) \
APZC_LOG_FM_COMMON(fm, prefix, LogLevel::Verbose, __VA_ARGS__)
namespace mozilla {
namespace layers {
typedef mozilla::layers::AllowedTouchBehavior AllowedTouchBehavior;
typedef GeckoContentController::APZStateChange APZStateChange;
typedef GeckoContentController::TapType TapType;
typedef mozilla::gfx::Point Point;
typedef mozilla::gfx::Matrix4x4 Matrix4x4;
// Choose between platform-specific implementations.
#ifdef MOZ_WIDGET_ANDROID
typedef WidgetOverscrollEffect OverscrollEffect;
typedef AndroidSpecificState PlatformSpecificState;
#else
typedef GenericOverscrollEffect OverscrollEffect;
typedef PlatformSpecificStateBase
PlatformSpecificState; // no extra state, just use the base class
#endif
/**
* \page APZCPrefs APZ preferences
*
* The following prefs are used to control the behaviour of the APZC.
* The default values are provided in StaticPrefList.yaml.
*
* \li\b apz.allow_double_tap_zooming
* Pref that allows or disallows double tap to zoom
*
* \li\b apz.allow_immediate_handoff
* If set to true, scroll can be handed off from one APZC to another within
* a single input block. If set to false, a single input block can only
* scroll one APZC.
*
* \li\b apz.allow_zooming_out
* If set to true, APZ will allow zooming out past the initial scale on
* desktop. This is false by default to match Chrome's behaviour.
*
* \li\b apz.android.chrome_fling_physics.friction
* A tunable parameter for Chrome fling physics on Android that governs
* how quickly a fling animation slows down due to friction (and therefore
* also how far it reaches). Should be in the range [0-1].
*
* \li\b apz.android.chrome_fling_physics.inflexion
* A tunable parameter for Chrome fling physics on Android that governs
* the shape of the fling curve. Should be in the range [0-1].
*
* \li\b apz.android.chrome_fling_physics.stop_threshold
* A tunable parameter for Chrome fling physics on Android that governs
* how close the fling animation has to get to its target destination
* before it stops.
* Units: ParentLayer pixels
*
* \li\b apz.autoscroll.enabled
* If set to true, autoscrolling is driven by APZ rather than the content
* process main thread.
*
* \li\b apz.axis_lock.mode
* The preferred axis locking style. See AxisLockMode for possible values.
*
* \li\b apz.axis_lock.lock_angle
* Angle from axis within which we stay axis-locked.\n
* Units: radians
*
* \li\b apz.axis_lock.breakout_threshold
* Distance in inches the user must pan before axis lock can be broken.\n
* Units: (real-world, i.e. screen) inches
*
* \li\b apz.axis_lock.breakout_angle
* Angle at which axis lock can be broken.\n
* Units: radians
*
* \li\b apz.axis_lock.direct_pan_angle
* If the angle from an axis to the line drawn by a pan move is less than
* this value, we can assume that panning can be done in the allowed direction
* (horizontal or vertical).\n
* Currently used only for touch-action css property stuff and was addded to
* keep behaviour consistent with IE.\n
* Units: radians
*
* \li\b apz.content_response_timeout
* Amount of time before we timeout response from content. For example, if
* content is being unruly/slow and we don't get a response back within this
* time, we will just pretend that content did not preventDefault any touch
* events we dispatched to it.\n
* Units: milliseconds
*
* \li\b apz.danger_zone_x
* \li\b apz.danger_zone_y
* When drawing high-res tiles, we drop down to drawing low-res tiles
* when we know we can't keep up with the scrolling. The way we determine
* this is by checking if we are entering the "danger zone", which is the
* boundary of the painted content. For example, if the painted content
* goes from y=0...1000 and the visible portion is y=250...750 then
* we're far from checkerboarding. If we get to y=490...990 though then we're
* only 10 pixels away from showing checkerboarding so we are probably in
* a state where we can't keep up with scrolling. The danger zone prefs specify
* how wide this margin is; in the above example a y-axis danger zone of 10
* pixels would make us drop to low-res at y=490...990.\n
* This value is in screen pixels.
*
* \li\b apz.disable_for_scroll_linked_effects
* Setting this pref to true will disable APZ scrolling on documents where
* scroll-linked effects are detected. A scroll linked effect is detected if
* positioning or transform properties are updated inside a scroll event
* dispatch; we assume that such an update is in response to the scroll event
* and is therefore a scroll-linked effect which will be laggy with APZ
* scrolling.
*
* \li\b apz.displayport_expiry_ms
* While a scrollable frame is scrolling async, we set a displayport on it
* to make sure it is layerized. However this takes up memory, so once the
* scrolling stops we want to remove the displayport. This pref controls how
* long after scrolling stops the displayport is removed. A value of 0 will
* disable the expiry behavior entirely.
* Units: milliseconds
*
* \li\b apz.drag.enabled
* Setting this pref to true will cause APZ to handle mouse-dragging of
* scrollbar thumbs.
*
* \li\b apz.drag.touch.enabled
* Setting this pref to true will cause APZ to handle touch-dragging of
* scrollbar thumbs. Only has an effect if apz.drag.enabled is also true.
*
* \li\b apz.enlarge_displayport_when_clipped
* Pref that enables enlarging of the displayport along one axis when the
* generated displayport's size is beyond that of the scrollable rect on the
* opposite axis.
*
* \li\b apz.fling_accel_min_fling_velocity
* The minimum velocity of the second fling, and the minimum velocity of the
* previous fling animation at the point of interruption, for the new fling to
* be considered for fling acceleration.
* Units: screen pixels per milliseconds
*
* \li\b apz.fling_accel_min_pan_velocity
* The minimum velocity during the pan gesture that causes a fling for that
* fling to be considered for fling acceleration.
* Units: screen pixels per milliseconds
*
* \li\b apz.fling_accel_max_pause_interval_ms
* The maximum time that is allowed to elapse between the touch start event that
* interrupts the previous fling, and the touch move that initiates panning for
* the current fling, for that fling to be considered for fling acceleration.
* Units: milliseconds
*
* \li\b apz.fling_accel_base_mult
* \li\b apz.fling_accel_supplemental_mult
* When applying an acceleration on a fling, the new computed velocity is
* (new_fling_velocity * base_mult) + (old_velocity * supplemental_mult).
* The base_mult and supplemental_mult multiplier values are controlled by
* these prefs. Note that "old_velocity" here is the initial velocity of the
* previous fling _after_ acceleration was applied to it (if applicable).
*
* \li\b apz.fling_curve_function_x1
* \li\b apz.fling_curve_function_y1
* \li\b apz.fling_curve_function_x2
* \li\b apz.fling_curve_function_y2
* \li\b apz.fling_curve_threshold_inches_per_ms
* These five parameters define a Bezier curve function and threshold used to
* increase the actual velocity relative to the user's finger velocity. When the
* finger velocity is below the threshold (or if the threshold is not positive),
* the velocity is used as-is. If the finger velocity exceeds the threshold
* velocity, then the function defined by the curve is applied on the part of
* the velocity that exceeds the threshold. Note that the upper bound of the
* velocity is still specified by the \b apz.max_velocity_inches_per_ms pref,
* and the function will smoothly curve the velocity from the threshold to the
* max. In general the function parameters chosen should define an ease-out
* curve in order to increase the velocity in this range, or an ease-in curve to
* decrease the velocity. A straight-line curve is equivalent to disabling the
* curve entirely by setting the threshold to -1. The max velocity pref must
* also be set in order for the curving to take effect, as it defines the upper
* bound of the velocity curve.\n
* The points (x1, y1) and (x2, y2) used as the two intermediate control points
* in the cubic bezier curve; the first and last points are (0,0) and (1,1).\n
* Some example values for these prefs can be found at\n
*
* \li\b apz.fling_friction
* Amount of friction applied during flings. This is used in the following
* formula: v(t1) = v(t0) * (1 - f)^(t1 - t0), where v(t1) is the velocity
* for a new sample, v(t0) is the velocity at the previous sample, f is the
* value of this pref, and (t1 - t0) is the amount of time, in milliseconds,
* that has elapsed between the two samples.\n
* NOTE: Not currently used in Android fling calculations.
*
* \li\b apz.fling_min_velocity_threshold
* Minimum velocity for a fling to actually kick off. If the user pans and lifts
* their finger such that the velocity is smaller than or equal to this amount,
* no fling is initiated.\n
* Units: screen pixels per millisecond
*
* \li\b apz.fling_stop_on_tap_threshold
* When flinging, if the velocity is above this number, then a tap on the
* screen will stop the fling without dispatching a tap to content. If the
* velocity is below this threshold a tap will also be dispatched.
* Note: when modifying this pref be sure to run the APZC gtests as some of
* them depend on the value of this pref.\n
* Units: screen pixels per millisecond
*
* \li\b apz.fling_stopped_threshold
* When flinging, if the velocity goes below this number, we just stop the
* animation completely. This is to prevent asymptotically approaching 0
* velocity and rerendering unnecessarily.\n
* Units: screen pixels per millisecond.\n
* NOTE: Should not be set to anything
* other than 0.0 for Android except for tests to disable flings.
*
* \li\b apz.keyboard.enabled
* Determines whether scrolling with the keyboard will be allowed to be handled
* by APZ.
*
* \li\b apz.keyboard.passive-listeners
* When enabled, APZ will interpret the passive event listener flag to mean
* that the event listener won't change the focused element or selection of
* the page. With this, web content can use passive key listeners and not have
* keyboard APZ disabled.
*
* \li\b apz.max_tap_time
* Maximum time for a touch on the screen and corresponding lift of the finger
* to be considered a tap. This also applies to double taps, except that it is
* used both for the interval between the first touchdown and first touchup,
* and for the interval between the first touchup and the second touchdown.\n
* Units: milliseconds.
*
* \li\b apz.max_velocity_inches_per_ms
* Maximum velocity. Velocity will be capped at this value if a faster fling
* occurs. Negative values indicate unlimited velocity.\n
* Units: (real-world, i.e. screen) inches per millisecond
*
* \li\b apz.max_velocity_queue_size
* Maximum size of velocity queue. The queue contains last N velocity records.
* On touch end we calculate the average velocity in order to compensate
* touch/mouse drivers misbehaviour.
*
* \li\b apz.min_skate_speed
* Minimum amount of speed along an axis before we switch to "skate" multipliers
* rather than using the "stationary" multipliers.\n
* Units: CSS pixels per millisecond
*
* \li\b apz.one_touch_pinch.enabled
* Whether or not the "one-touch-pinch" gesture (for zooming with one finger)
* is enabled or not.
*
* \li\b apz.overscroll.enabled
* Pref that enables overscrolling. If this is disabled, excess scroll that
* cannot be handed off is discarded.
*
* \li\b apz.overscroll.min_pan_distance_ratio
* The minimum ratio of the pan distance along one axis to the pan distance
* along the other axis needed to initiate overscroll along the first axis
* during panning.
*
* \li\b apz.overscroll.stretch_factor
* How much overscrolling can stretch content along an axis.
* The maximum stretch along an axis is a factor of (1 + kStretchFactor).
* (So if kStretchFactor is 0, you can't stretch at all; if kStretchFactor
* is 1, you can stretch at most by a factor of 2).
*
* \li\b apz.overscroll.stop_distance_threshold
* \li\b apz.overscroll.stop_velocity_threshold
* Thresholds for stopping the overscroll animation. When both the distance
* and the velocity fall below their thresholds, we stop oscillating.\n
* Units: screen pixels (for distance)
* screen pixels per millisecond (for velocity)
*
* \li\b apz.overscroll.spring_stiffness
* The spring stiffness constant for the overscroll mass-spring-damper model.
*
* \li\b apz.overscroll.damping
* The damping constant for the overscroll mass-spring-damper model.
*
* \li\b apz.overscroll.max_velocity
* The maximum velocity (in ParentLayerPixels per millisecond) allowed when
* initiating the overscroll snap-back animation.
*
* \li\b apz.paint_skipping.enabled
* When APZ is scrolling and sending repaint requests to the main thread, often
* the main thread doesn't actually need to do a repaint. This pref allows the
* main thread to skip doing those repaints in cases where it doesn't need to.
*
* \li\b apz.pinch_lock.mode
* The preferred pinch locking style. See PinchLockMode for possible values.
*
* \li\b apz.pinch_lock.scroll_lock_threshold
* Pinch locking is triggered if the user scrolls more than this distance
* and pinches less than apz.pinch_lock.span_lock_threshold.\n
* Units: (real-world, i.e. screen) inches
*
* \li\b apz.pinch_lock.span_breakout_threshold
* Distance in inches the user must pinch before lock can be broken.\n
* Units: (real-world, i.e. screen) inches measured between two touch points
*
* \li\b apz.pinch_lock.span_lock_threshold
* Pinch locking is triggered if the user pinches less than this distance
* and scrolls more than apz.pinch_lock.scroll_lock_threshold.\n
* Units: (real-world, i.e. screen) inches measured between two touch points
*
* \li\b apz.pinch_lock.buffer_max_age
* To ensure that pinch locking threshold calculations are not affected by
* variations in touch screen sensitivity, calculations draw from a buffer of
* recent events. This preference specifies the maximum time that events are
* held in this buffer.
* Units: milliseconds
*
* \li\b apz.popups.enabled
* Determines whether APZ is used for XUL popup widgets with remote content.
* Ideally, this should always be true, but it is currently not well tested, and
* has known issues, so needs to be prefable.
*
* \li\b apz.record_checkerboarding
* Whether or not to record detailed info on checkerboarding events.
*
* \li\b apz.second_tap_tolerance
* Constant describing the tolerance in distance we use, multiplied by the
* device DPI, within which a second tap is counted as part of a gesture
* continuing from the first tap. Making this larger allows the user more
* distance between the first and second taps in a "double tap" or "one touch
* pinch" gesture.\n
* Units: (real-world, i.e. screen) inches
*
* \li\b apz.test.logging_enabled
*
* \li\b apz.touch_move_tolerance
* See the description for apz.touch_start_tolerance below. This is a similar
* threshold, except it is used to suppress touchmove events from being
* delivered to content for NON-scrollable frames (or more precisely, for APZCs
* where ArePointerEventsConsumable returns false).\n Units: (real-world, i.e.
* screen) inches
*
* \li\b apz.touch_start_tolerance
* Constant describing the tolerance in distance we use, multiplied by the
* device DPI, before we start panning the screen. This is to prevent us from
* accidentally processing taps as touch moves, and from very short/accidental
* touches moving the screen. touchmove events are also not delivered to content
* within this distance on scrollable frames.\n
* Units: (real-world, i.e. screen) inches
*
* \li\b apz.velocity_bias
* How much to adjust the displayport in the direction of scrolling. This value
* is multiplied by the velocity and added to the displayport offset.
*
* \li\b apz.velocity_relevance_time_ms
* When computing a fling velocity from the most recently stored velocity
* information, only velocities within the most X milliseconds are used.
* This pref controls the value of X.\n
* Units: ms
*
* \li\b apz.x_skate_size_multiplier
* \li\b apz.y_skate_size_multiplier
* The multiplier we apply to the displayport size if it is skating (current
* velocity is above \b apz.min_skate_speed). We prefer to increase the size of
* the Y axis because it is more natural in the case that a user is reading a
* page page that scrolls up/down. Note that one, both or neither of these may
* be used at any instant.\n In general we want \b
* apz.[xy]_skate_size_multiplier to be smaller than the corresponding
* stationary size multiplier because when panning fast we would like to paint
* less and get faster, more predictable paint times. When panning slowly we
* can afford to paint more even though it's slower.
*
* \li\b apz.x_stationary_size_multiplier
* \li\b apz.y_stationary_size_multiplier
* The multiplier we apply to the displayport size if it is not skating (see
* documentation for the skate size multipliers above).
*
* \li\b apz.x_skate_highmem_adjust
* \li\b apz.y_skate_highmem_adjust
* On high memory systems, we adjust the displayport during skating
* to be larger so we can reduce checkerboarding.
*
* \li\b apz.zoom_animation_duration_ms
* This controls how long the zoom-to-rect animation takes.\n
* Units: ms
*
* \li\b apz.scale_repaint_delay_ms
* How long to delay between repaint requests during a scale.
* A negative number prevents repaint requests during a scale.\n
* Units: ms
*/
/**
* Computed time function used for sampling frames of a zoom to animation.
*/
StaticAutoPtr<StyleComputedTimingFunction> gZoomAnimationFunction;
/**
* Computed time function used for curving up velocity when it gets high.
*/
StaticAutoPtr<StyleComputedTimingFunction> gVelocityCurveFunction;
/**
* The estimated duration of a paint for the purposes of calculating a new
* displayport, in milliseconds.
*/
static const double kDefaultEstimatedPaintDurationMs = 50;
/**
* Returns true if this is a high memory system and we can use
* extra memory for a larger displayport to reduce checkerboarding.
*/
static bool gIsHighMemSystem = false;
static bool IsHighMemSystem() { return gIsHighMemSystem; }
// An RAII class to hide the dynamic toolbar on Android.
class MOZ_RAII AutoDynamicToolbarHider final {
public:
explicit AutoDynamicToolbarHider(AsyncPanZoomController* aApzc)
: mApzc(aApzc) {
MOZ_ASSERT(mApzc);
}
~AutoDynamicToolbarHider() {
if (mHideDynamicToolbar) {
RefPtr<GeckoContentController> controller =
mApzc->GetGeckoContentController();
controller->HideDynamicToolbar(mApzc->GetGuid());
}
}
void Hide() { mHideDynamicToolbar = true; }
friend class AsyncPanZoomController;
private:
AsyncPanZoomController* mApzc;
bool mHideDynamicToolbar = false;
};
AsyncPanZoomAnimation* PlatformSpecificStateBase::CreateFlingAnimation(
AsyncPanZoomController& aApzc, const FlingHandoffState& aHandoffState,
float aPLPPI) {
return new GenericFlingAnimation<DesktopFlingPhysics>(aApzc, aHandoffState,
aPLPPI);
}
UniquePtr<VelocityTracker> PlatformSpecificStateBase::CreateVelocityTracker(
Axis* aAxis) {
return MakeUnique<SimpleVelocityTracker>(aAxis);
}
SampleTime AsyncPanZoomController::GetFrameTime() const {
APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
return treeManagerLocal ? treeManagerLocal->GetFrameTime()
: SampleTime::FromNow();
}
bool AsyncPanZoomController::IsZero(const ParentLayerPoint& aPoint) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return layers::IsZero(ToCSSPixels(aPoint));
}
bool AsyncPanZoomController::IsZero(ParentLayerCoord aCoord) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return FuzzyEqualsAdditive(ToCSSPixels(aCoord), CSSCoord(),
COORDINATE_EPSILON);
}
bool AsyncPanZoomController::FuzzyGreater(ParentLayerCoord aCoord1,
ParentLayerCoord aCoord2) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return ToCSSPixels(aCoord1 - aCoord2) > COORDINATE_EPSILON;
}
class StateChangeNotificationBlocker final {
public:
explicit StateChangeNotificationBlocker(AsyncPanZoomController* aApzc)
: mApzc(aApzc) {
RecursiveMutexAutoLock lock(mApzc->mRecursiveMutex);
mInitialState = mApzc->mState;
mApzc->mNotificationBlockers++;
}
StateChangeNotificationBlocker(const StateChangeNotificationBlocker&) =
delete;
StateChangeNotificationBlocker(StateChangeNotificationBlocker&& aOther)
: mApzc(aOther.mApzc), mInitialState(aOther.mInitialState) {
aOther.mApzc = nullptr;
}
~StateChangeNotificationBlocker() {
if (!mApzc) { // moved-from
return;
}
AsyncPanZoomController::PanZoomState newState;
{
RecursiveMutexAutoLock lock(mApzc->mRecursiveMutex);
mApzc->mNotificationBlockers--;
newState = mApzc->mState;
}
mApzc->DispatchStateChangeNotification(mInitialState, newState);
}
private:
AsyncPanZoomController* mApzc;
AsyncPanZoomController::PanZoomState mInitialState;
};
/**
* An RAII class to temporarily apply async test attributes to the provided
* AsyncPanZoomController.
*
* This class should be used in the implementation of any AsyncPanZoomController
* method that queries the async scroll offset or async zoom (this includes
* the async layout viewport offset, since modifying the async scroll offset
* may result in the layout viewport moving as well).
*/
class MOZ_RAII AutoApplyAsyncTestAttributes final {
public:
explicit AutoApplyAsyncTestAttributes(
const AsyncPanZoomController*,
const RecursiveMutexAutoLock& aProofOfLock);
~AutoApplyAsyncTestAttributes();
private:
AsyncPanZoomController* mApzc;
FrameMetrics mPrevFrameMetrics;
ParentLayerPoint mPrevOverscroll;
const RecursiveMutexAutoLock& mProofOfLock;
};
AutoApplyAsyncTestAttributes::AutoApplyAsyncTestAttributes(
const AsyncPanZoomController* aApzc,
const RecursiveMutexAutoLock& aProofOfLock)
// Having to use const_cast here seems less ugly than the alternatives
// of making several members of AsyncPanZoomController that
// ApplyAsyncTestAttributes() modifies |mutable|, or several methods that
// query the async transforms non-const.
: mApzc(const_cast<AsyncPanZoomController*>(aApzc)),
mPrevFrameMetrics(aApzc->Metrics()),
mPrevOverscroll(aApzc->GetOverscrollAmountInternal()),
mProofOfLock(aProofOfLock) {
mApzc->ApplyAsyncTestAttributes(aProofOfLock);
}
AutoApplyAsyncTestAttributes::~AutoApplyAsyncTestAttributes() {
mApzc->UnapplyAsyncTestAttributes(mProofOfLock, mPrevFrameMetrics,
mPrevOverscroll);
}
class ZoomAnimation : public AsyncPanZoomAnimation {
public:
ZoomAnimation(AsyncPanZoomController& aApzc, const CSSPoint& aStartOffset,
const CSSToParentLayerScale& aStartZoom,
const CSSPoint& aEndOffset,
const CSSToParentLayerScale& aEndZoom)
: mApzc(aApzc),
mTotalDuration(TimeDuration::FromMilliseconds(
StaticPrefs::apz_zoom_animation_duration_ms())),
mStartOffset(aStartOffset),
mStartZoom(aStartZoom),
mEndOffset(aEndOffset),
mEndZoom(aEndZoom) {}
virtual bool DoSample(FrameMetrics& aFrameMetrics,
const TimeDuration& aDelta) override {
mDuration += aDelta;
double animPosition = mDuration / mTotalDuration;
if (animPosition >= 1.0) {
aFrameMetrics.SetZoom(mEndZoom);
mApzc.SetVisualScrollOffset(mEndOffset);
return false;
}
// Sample the zoom at the current time point. The sampled zoom
// will affect the final computed resolution.
float sampledPosition =
gZoomAnimationFunction->At(animPosition, /* aBeforeFlag = */ false);
// We scale the scrollOffset linearly with sampledPosition, so the zoom
// needs to scale inversely to match.
if (mStartZoom == CSSToParentLayerScale(0) ||
mEndZoom == CSSToParentLayerScale(0)) {
return false;
}
aFrameMetrics.SetZoom(
CSSToParentLayerScale(1 / (sampledPosition / mEndZoom.scale +
(1 - sampledPosition) / mStartZoom.scale)));
mApzc.SetVisualScrollOffset(CSSPoint::FromUnknownPoint(gfx::Point(
mEndOffset.x * sampledPosition + mStartOffset.x * (1 - sampledPosition),
mEndOffset.y * sampledPosition +
mStartOffset.y * (1 - sampledPosition))));
return true;
}
virtual bool WantsRepaints() override { return true; }
private:
AsyncPanZoomController& mApzc;
TimeDuration mDuration;
const TimeDuration mTotalDuration;
// Old metrics from before we started a zoom animation. This is only valid
// when we are in the "ANIMATED_ZOOM" state. This is used so that we can
// interpolate between the start and end frames. We only use the
// |mViewportScrollOffset| and |mResolution| fields on this.
CSSPoint mStartOffset;
CSSToParentLayerScale mStartZoom;
// Target metrics for a zoom to animation. This is only valid when we are in
// the "ANIMATED_ZOOM" state. We only use the |mViewportScrollOffset| and
// |mResolution| fields on this.
CSSPoint mEndOffset;
CSSToParentLayerScale mEndZoom;
};
/*static*/
void AsyncPanZoomController::InitializeGlobalState() {
static bool sInitialized = false;
if (sInitialized) return;
sInitialized = true;
MOZ_ASSERT(NS_IsMainThread());
gZoomAnimationFunction = new StyleComputedTimingFunction(
StyleComputedTimingFunction::Keyword(StyleTimingKeyword::Ease));
ClearOnShutdown(&gZoomAnimationFunction);
gVelocityCurveFunction =
new StyleComputedTimingFunction(StyleComputedTimingFunction::CubicBezier(
StaticPrefs::apz_fling_curve_function_x1_AtStartup(),
StaticPrefs::apz_fling_curve_function_y1_AtStartup(),
StaticPrefs::apz_fling_curve_function_x2_AtStartup(),
StaticPrefs::apz_fling_curve_function_y2_AtStartup()));
ClearOnShutdown(&gVelocityCurveFunction);
uint64_t sysmem = PR_GetPhysicalMemorySize();
uint64_t threshold = 1LL << 32; // 4 GB in bytes
gIsHighMemSystem = sysmem >= threshold;
PlatformSpecificState::InitializeGlobalState();
}
AsyncPanZoomController::AsyncPanZoomController(
LayersId aLayersId, APZCTreeManager* aTreeManager,
const RefPtr<InputQueue>& aInputQueue,
GeckoContentController* aGeckoContentController, GestureBehavior aGestures)
: mLayersId(aLayersId),
mGeckoContentController(aGeckoContentController),
mRefPtrMonitor("RefPtrMonitor"),
// mTreeManager must be initialized before GetFrameTime() is called
mTreeManager(aTreeManager),
mRecursiveMutex("AsyncPanZoomController"),
mLastContentPaintMetrics(mLastContentPaintMetadata.GetMetrics()),
mPanDirRestricted(false),
mPinchLocked(false),
mPinchEventBuffer(TimeDuration::FromMilliseconds(
StaticPrefs::apz_pinch_lock_buffer_max_age_AtStartup())),
mTouchScrollEventBuffer(
TimeDuration::FromMilliseconds(
StaticPrefs::apz_touch_scroll_buffer_max_age_AtStartup()),
2),
mZoomConstraints(false, false,
mScrollMetadata.GetMetrics().GetDevPixelsPerCSSPixel() *
ViewportMinScale() / ParentLayerToScreenScale(1),
mScrollMetadata.GetMetrics().GetDevPixelsPerCSSPixel() *
ViewportMaxScale() / ParentLayerToScreenScale(1)),
mLastSampleTime(GetFrameTime()),
mLastCheckerboardReport(GetFrameTime()),
mOverscrollEffect(MakeUnique<OverscrollEffect>(*this)),
mState(NOTHING),
mX(this),
mY(this),
mNotificationBlockers(0),
mInputQueue(aInputQueue),
mPinchPaintTimerSet(false),
mDelayedTransformEnd(false),
mTestAttributeAppliers(0),
mTestHasAsyncKeyScrolled(false),
mCheckerboardEventLock("APZCBELock") {
if (aGestures == USE_GESTURE_DETECTOR) {
mGestureEventListener = new GestureEventListener(this);
}
// Put one default-constructed sampled state in the queue.
RecursiveMutexAutoLock lock(mRecursiveMutex);
mSampledState.emplace_back();
}
AsyncPanZoomController::~AsyncPanZoomController() { MOZ_ASSERT(IsDestroyed()); }
PlatformSpecificStateBase* AsyncPanZoomController::GetPlatformSpecificState() {
if (!mPlatformSpecificState) {
mPlatformSpecificState = MakeUnique<PlatformSpecificState>();
}
return mPlatformSpecificState.get();
}
already_AddRefed<GeckoContentController>
AsyncPanZoomController::GetGeckoContentController() const {
MonitorAutoLock lock(mRefPtrMonitor);
RefPtr<GeckoContentController> controller = mGeckoContentController;
return controller.forget();
}
already_AddRefed<GestureEventListener>
AsyncPanZoomController::GetGestureEventListener() const {
MonitorAutoLock lock(mRefPtrMonitor);
RefPtr<GestureEventListener> listener = mGestureEventListener;
return listener.forget();
}
const RefPtr<InputQueue>& AsyncPanZoomController::GetInputQueue() const {
return mInputQueue;
}
void AsyncPanZoomController::Destroy() {
AssertOnUpdaterThread();
CancelAnimation(CancelAnimationFlags::ScrollSnap);
{ // scope the lock
MonitorAutoLock lock(mRefPtrMonitor);
mGeckoContentController = nullptr;
mGestureEventListener = nullptr;
}
mParent = nullptr;
mTreeManager = nullptr;
}
bool AsyncPanZoomController::IsDestroyed() const {
return mTreeManager == nullptr;
}
float AsyncPanZoomController::GetDPI() const {
if (APZCTreeManager* localPtr = mTreeManager) {
return localPtr->GetDPI();
}
// If this APZC has been destroyed then this value is not going to be
// used for anything that the user will end up seeing, so we can just
// return 0.
return 0.0;
}
ScreenCoord AsyncPanZoomController::GetTouchStartTolerance() const {
return (StaticPrefs::apz_touch_start_tolerance() * GetDPI());
}
ScreenCoord AsyncPanZoomController::GetTouchMoveTolerance() const {
return (StaticPrefs::apz_touch_move_tolerance() * GetDPI());
}
ScreenCoord AsyncPanZoomController::GetSecondTapTolerance() const {
return (StaticPrefs::apz_second_tap_tolerance() * GetDPI());
}
/* static */ AsyncPanZoomController::AxisLockMode
AsyncPanZoomController::GetAxisLockMode() {
return static_cast<AxisLockMode>(StaticPrefs::apz_axis_lock_mode());
}
bool AsyncPanZoomController::UsingStatefulAxisLock() const {
return (GetAxisLockMode() == AxisLockMode::STANDARD ||
GetAxisLockMode() == AxisLockMode::STICKY ||
GetAxisLockMode() == AxisLockMode::BREAKABLE);
}
/* static */ AsyncPanZoomController::PinchLockMode
AsyncPanZoomController::GetPinchLockMode() {
return static_cast<PinchLockMode>(StaticPrefs::apz_pinch_lock_mode());
}
PointerEventsConsumableFlags AsyncPanZoomController::ArePointerEventsConsumable(
const TouchBlockState* aBlock, const MultiTouchInput& aInput) const {
uint32_t touchPoints = aInput.mTouches.Length();
if (touchPoints == 0) {
// Cant' do anything with zero touch points
return {false, false};
}
// This logic is simplified, erring on the side of returning true if we're
// not sure. It's safer to pretend that we can consume the event and then
// not be able to than vice-versa. But at the same time, we should try hard
// to return an accurate result, because returning true can trigger a
// pointercancel event to web content, which can break certain features
// that are using touch-action and handling the pointermove events.
//
// Note that in particular this function can return true if APZ is waiting on
// the main thread for touch-action information. In this scenario, the
// APZEventState::MainThreadAgreesEventsAreConsumableByAPZ() function tries
// to use the main-thread touch-action information to filter out false
// positives.
//
// We could probably enhance this logic to determine things like "we're
// not pannable, so we can only zoom in, and the zoom is already maxed
// out, so we're not zoomable either" but no need for that at this point.
bool pannableX = aBlock->GetOverscrollHandoffChain()->CanScrollInDirection(
this, ScrollDirection::eHorizontal);
bool touchActionAllowsX = aBlock->TouchActionAllowsPanningX();
bool pannableY = (aBlock->GetOverscrollHandoffChain()->CanScrollInDirection(
this, ScrollDirection::eVertical) ||
// In the case of the root APZC with any dynamic toolbar, it
// shoule be pannable if there is room moving the dynamic
// toolbar.
(IsRootContent() && CanVerticalScrollWithDynamicToolbar()));
bool touchActionAllowsY = aBlock->TouchActionAllowsPanningY();
bool pannable;
bool touchActionAllowsPanning;
Maybe<ScrollDirection> panDirection =
aBlock->GetBestGuessPanDirection(aInput);
if (panDirection == Some(ScrollDirection::eVertical)) {
pannable = pannableY;
touchActionAllowsPanning = touchActionAllowsY;
} else if (panDirection == Some(ScrollDirection::eHorizontal)) {
pannable = pannableX;
touchActionAllowsPanning = touchActionAllowsX;
} else {
// If we don't have a guessed pan direction, err on the side of returning
// true.
pannable = pannableX || pannableY;
touchActionAllowsPanning = touchActionAllowsX || touchActionAllowsY;
}
if (touchPoints == 1) {
return {pannable, touchActionAllowsPanning};
}
bool zoomable = ZoomConstraintsAllowZoom();
bool touchActionAllowsZoom = aBlock->TouchActionAllowsPinchZoom();
return {pannable || zoomable,
touchActionAllowsPanning || touchActionAllowsZoom};
}
nsEventStatus AsyncPanZoomController::HandleDragEvent(
const MouseInput& aEvent, const AsyncDragMetrics& aDragMetrics,
OuterCSSCoord aInitialThumbPos, const CSSRect& aInitialScrollableRect) {
// RDM is a special case where touch events will be synthesized in response
// to mouse events, and APZ will receive both even though RDM prevent-defaults
// the mouse events. This is because mouse events don't opt into APZ waiting
// to check if the event has been prevent-defaulted and are still processed
// as a result. To handle this, have APZ ignore mouse events when RDM and
// touch simulation are active.
bool isRDMTouchSimulationActive = false;
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
isRDMTouchSimulationActive =
mScrollMetadata.GetIsRDMTouchSimulationActive();
}
if (!StaticPrefs::apz_drag_enabled() || isRDMTouchSimulationActive) {
return nsEventStatus_eIgnore;
}
if (!GetApzcTreeManager()) {
return nsEventStatus_eConsumeNoDefault;
}
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
if (aEvent.mType == MouseInput::MouseType::MOUSE_UP) {
if (mState == SCROLLBAR_DRAG) {
APZC_LOG("%p ending drag\n", this);
SetState(NOTHING);
}
SnapBackIfOverscrolled();
return nsEventStatus_eConsumeNoDefault;
}
}
HitTestingTreeNodeAutoLock node;
GetApzcTreeManager()->FindScrollThumbNode(aDragMetrics, mLayersId, node);
if (!node) {
APZC_LOG("%p unable to find scrollthumb node with viewid %" PRIu64 "\n",
this, aDragMetrics.mViewId);
return nsEventStatus_eConsumeNoDefault;
}
if (aEvent.mType == MouseInput::MouseType::MOUSE_DOWN) {
APZC_LOG("%p starting scrollbar drag\n", this);
SetState(SCROLLBAR_DRAG);
}
if (aEvent.mType != MouseInput::MouseType::MOUSE_MOVE) {
APZC_LOG("%p discarding event of type %d\n", this, aEvent.mType);
return nsEventStatus_eConsumeNoDefault;
}
const ScrollbarData& scrollbarData = node->GetScrollbarData();
MOZ_ASSERT(scrollbarData.mScrollbarLayerType ==
layers::ScrollbarLayerType::Thumb);
MOZ_ASSERT(scrollbarData.mDirection.isSome());
ScrollDirection direction = *scrollbarData.mDirection;
bool isMouseAwayFromThumb = false;
if (int snapMultiplier = StaticPrefs::slider_snapMultiplier()) {
// It's fine to ignore the async component of the thumb's transform,
// because any async transform of the thumb will be in the direction of
// scrolling, but here we're interested in the other direction.
ParentLayerRect thumbRect =
(node->GetTransform() * AsyncTransformMatrix())
.TransformBounds(LayerRect(node->GetVisibleRect()));
ScrollDirection otherDirection = GetPerpendicularDirection(direction);
ParentLayerCoord distance =
GetAxisStart(otherDirection, thumbRect.DistanceTo(aEvent.mLocalOrigin));
ParentLayerCoord thumbWidth = GetAxisLength(otherDirection, thumbRect);
// Avoid triggering this condition spuriously when the thumb is
// offscreen and its visible region is therefore empty.
if (thumbWidth > 0 && thumbWidth * snapMultiplier < distance) {
isMouseAwayFromThumb = true;
APZC_LOG("%p determined mouse is away from thumb, will snap\n", this);
}
}
RecursiveMutexAutoLock lock(mRecursiveMutex);
OuterCSSCoord thumbPosition;
if (isMouseAwayFromThumb) {
thumbPosition = aInitialThumbPos;
} else {
thumbPosition = ConvertScrollbarPoint(aEvent.mLocalOrigin, scrollbarData) -
aDragMetrics.mScrollbarDragOffset;
}
OuterCSSCoord maxThumbPos = scrollbarData.mScrollTrackLength;
maxThumbPos -= scrollbarData.mThumbLength;
float scrollPercent =
maxThumbPos.value == 0.0f ? 0.0f : (float)(thumbPosition / maxThumbPos);
APZC_LOG("%p scrollbar dragged to %f percent\n", this, scrollPercent);
CSSCoord minScrollPosition =
GetAxisStart(direction, aInitialScrollableRect.TopLeft());
CSSCoord maxScrollPosition =
GetAxisStart(direction, aInitialScrollableRect.BottomRight()) -
GetAxisLength(direction, Metrics().CalculateCompositedSizeInCssPixels());
CSSCoord scrollPosition =
minScrollPosition +
(scrollPercent * (maxScrollPosition - minScrollPosition));
scrollPosition = std::max(scrollPosition, minScrollPosition);
scrollPosition = std::min(scrollPosition, maxScrollPosition);
CSSPoint scrollOffset = Metrics().GetVisualScrollOffset();
if (direction == ScrollDirection::eHorizontal) {
scrollOffset.x = scrollPosition;
} else {
scrollOffset.y = scrollPosition;
}
APZC_LOG("%p set scroll offset to %s from scrollbar drag\n", this,
ToString(scrollOffset).c_str());
// Since the scroll position was calculated based on the scrollable rect at
// the start of the drag, we need to clamp the scroll position in case the
// scrollable rect has since shrunk.
ClampAndSetVisualScrollOffset(scrollOffset);
ScheduleCompositeAndMaybeRepaint();
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::HandleInputEvent(
const InputData& aEvent,
const ScreenToParentLayerMatrix4x4& aTransformToApzc) {
APZThreadUtils::AssertOnControllerThread();
nsEventStatus rv = nsEventStatus_eIgnore;
switch (aEvent.mInputType) {
case MULTITOUCH_INPUT: {
MultiTouchInput multiTouchInput = aEvent.AsMultiTouchInput();
RefPtr<GestureEventListener> listener = GetGestureEventListener();
if (listener) {
// We only care about screen coordinates in the gesture listener,
// so we don't bother transforming the event to parent layer coordinates
rv = listener->HandleInputEvent(multiTouchInput);
if (rv == nsEventStatus_eConsumeNoDefault) {
return rv;
}
}
if (!multiTouchInput.TransformToLocal(aTransformToApzc)) {
return rv;
}
switch (multiTouchInput.mType) {
case MultiTouchInput::MULTITOUCH_START:
rv = OnTouchStart(multiTouchInput);
break;
case MultiTouchInput::MULTITOUCH_MOVE:
rv = OnTouchMove(multiTouchInput);
break;
case MultiTouchInput::MULTITOUCH_END:
rv = OnTouchEnd(multiTouchInput);
break;
case MultiTouchInput::MULTITOUCH_CANCEL:
rv = OnTouchCancel(multiTouchInput);
break;
}
break;
}
case PANGESTURE_INPUT: {
PanGestureInput panGestureInput = aEvent.AsPanGestureInput();
if (!panGestureInput.TransformToLocal(aTransformToApzc)) {
return rv;
}
switch (panGestureInput.mType) {
case PanGestureInput::PANGESTURE_MAYSTART:
rv = OnPanMayBegin(panGestureInput);
break;
case PanGestureInput::PANGESTURE_CANCELLED:
rv = OnPanCancelled(panGestureInput);
break;
case PanGestureInput::PANGESTURE_START:
rv = OnPanBegin(panGestureInput);
break;
case PanGestureInput::PANGESTURE_PAN:
rv = OnPan(panGestureInput, FingersOnTouchpad::Yes);
break;
case PanGestureInput::PANGESTURE_END:
rv = OnPanEnd(panGestureInput);
break;
case PanGestureInput::PANGESTURE_MOMENTUMSTART:
rv = OnPanMomentumStart(panGestureInput);
break;
case PanGestureInput::PANGESTURE_MOMENTUMPAN:
rv = OnPan(panGestureInput, FingersOnTouchpad::No);
break;
case PanGestureInput::PANGESTURE_MOMENTUMEND:
rv = OnPanMomentumEnd(panGestureInput);
break;
case PanGestureInput::PANGESTURE_INTERRUPTED:
rv = OnPanInterrupted(panGestureInput);
break;
}
break;
}
case MOUSE_INPUT: {
MouseInput mouseInput = aEvent.AsMouseInput();
if (!mouseInput.TransformToLocal(aTransformToApzc)) {
return rv;
}
break;
}
case SCROLLWHEEL_INPUT: {
ScrollWheelInput scrollInput = aEvent.AsScrollWheelInput();
if (!scrollInput.TransformToLocal(aTransformToApzc)) {
return rv;
}
rv = OnScrollWheel(scrollInput);
break;
}
case PINCHGESTURE_INPUT: {
// The APZCTreeManager should take care of ensuring that only root-content
// APZCs get pinch inputs.
MOZ_ASSERT(IsRootContent());
PinchGestureInput pinchInput = aEvent.AsPinchGestureInput();
if (!pinchInput.TransformToLocal(aTransformToApzc)) {
return rv;
}
rv = HandleGestureEvent(pinchInput);
break;
}
case TAPGESTURE_INPUT: {
TapGestureInput tapInput = aEvent.AsTapGestureInput();
if (!tapInput.TransformToLocal(aTransformToApzc)) {
return rv;
}
rv = HandleGestureEvent(tapInput);
break;
}
case KEYBOARD_INPUT: {
const KeyboardInput& keyInput = aEvent.AsKeyboardInput();
rv = OnKeyboard(keyInput);
break;
}
}
return rv;
}
nsEventStatus AsyncPanZoomController::HandleGestureEvent(
const InputData& aEvent) {
APZThreadUtils::AssertOnControllerThread();
nsEventStatus rv = nsEventStatus_eIgnore;
switch (aEvent.mInputType) {
case PINCHGESTURE_INPUT: {
// This may be invoked via a one-touch-pinch gesture from
// GestureEventListener. In that case we want redirect it to the enclosing
// root-content APZC.
if (!IsRootContent()) {
if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
if (RefPtr<AsyncPanZoomController> root =
treeManagerLocal->FindZoomableApzc(this)) {
rv = root->HandleGestureEvent(aEvent);
}
}
break;
}
PinchGestureInput pinchGestureInput = aEvent.AsPinchGestureInput();
pinchGestureInput.TransformToLocal(GetTransformToThis());
switch (pinchGestureInput.mType) {
case PinchGestureInput::PINCHGESTURE_START:
rv = OnScaleBegin(pinchGestureInput);
break;
case PinchGestureInput::PINCHGESTURE_SCALE:
rv = OnScale(pinchGestureInput);
break;
case PinchGestureInput::PINCHGESTURE_FINGERLIFTED:
case PinchGestureInput::PINCHGESTURE_END:
rv = OnScaleEnd(pinchGestureInput);
break;
}
break;
}
case TAPGESTURE_INPUT: {
TapGestureInput tapGestureInput = aEvent.AsTapGestureInput();
tapGestureInput.TransformToLocal(GetTransformToThis());
switch (tapGestureInput.mType) {
case TapGestureInput::TAPGESTURE_LONG:
rv = OnLongPress(tapGestureInput);
break;
case TapGestureInput::TAPGESTURE_LONG_UP:
rv = OnLongPressUp(tapGestureInput);
break;
case TapGestureInput::TAPGESTURE_UP:
rv = OnSingleTapUp(tapGestureInput);
break;
case TapGestureInput::TAPGESTURE_CONFIRMED:
rv = OnSingleTapConfirmed(tapGestureInput);
break;
case TapGestureInput::TAPGESTURE_DOUBLE:
if (!IsRootContent()) {
if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
if (AsyncPanZoomController* apzc =
treeManagerLocal->FindRootApzcFor(GetLayersId())) {
rv = apzc->OnDoubleTap(tapGestureInput);
}
}
break;
}
rv = OnDoubleTap(tapGestureInput);
break;
case TapGestureInput::TAPGESTURE_SECOND:
rv = OnSecondTap(tapGestureInput);
break;
case TapGestureInput::TAPGESTURE_CANCEL:
rv = OnCancelTap(tapGestureInput);
break;
}
break;
}
default:
MOZ_ASSERT_UNREACHABLE("Unhandled input event");
break;
}
return rv;
}
void AsyncPanZoomController::StartAutoscroll(const ScreenPoint& aPoint) {
// Cancel any existing animation.
CancelAnimation();
SetState(AUTOSCROLL);
StartAnimation(do_AddRef(new AutoscrollAnimation(*this, aPoint)));
}
void AsyncPanZoomController::StopAutoscroll() {
if (mState == AUTOSCROLL) {
CancelAnimation(TriggeredExternally);
}
}
nsEventStatus AsyncPanZoomController::OnTouchStart(
const MultiTouchInput& aEvent) {
APZC_LOG_DETAIL("got a touch-start in state %s\n", this,
ToString(mState).c_str());
mPanDirRestricted = false;
switch (mState) {
case FLING:
case ANIMATING_ZOOM:
case SMOOTH_SCROLL:
case SMOOTHMSD_SCROLL:
case OVERSCROLL_ANIMATION:
case WHEEL_SCROLL:
case KEYBOARD_SCROLL:
case PAN_MOMENTUM:
case AUTOSCROLL:
MOZ_ASSERT(GetCurrentTouchBlock());
GetCurrentTouchBlock()->GetOverscrollHandoffChain()->CancelAnimations(
ExcludeOverscroll);
[[fallthrough]];
case SCROLLBAR_DRAG:
case NOTHING: {
ParentLayerPoint point = GetFirstTouchPoint(aEvent);
mLastTouch.mPosition = mStartTouch = GetFirstExternalTouchPoint(aEvent);
StartTouch(point, aEvent.mTimeStamp);
if (RefPtr<GeckoContentController> controller =
GetGeckoContentController()) {
MOZ_ASSERT(GetCurrentTouchBlock());
const bool canBePanOrZoom =
GetCurrentTouchBlock()->GetOverscrollHandoffChain()->CanBePanned(
this) ||
(ZoomConstraintsAllowDoubleTapZoom() &&
GetCurrentTouchBlock()->TouchActionAllowsDoubleTapZoom());
controller->NotifyAPZStateChange(
GetGuid(), APZStateChange::eStartTouch, canBePanOrZoom,
Some(GetCurrentTouchBlock()->GetBlockId()));
}
mLastTouch.mTimeStamp = mTouchStartTime = aEvent.mTimeStamp;
SetState(TOUCHING);
mTouchScrollEventBuffer.push(aEvent);
break;
}
case TOUCHING:
case PANNING:
case PANNING_LOCKED_X:
case PANNING_LOCKED_Y:
case PINCHING:
NS_WARNING("Received impossible touch in OnTouchStart");
break;
}
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::OnTouchMove(
const MultiTouchInput& aEvent) {
APZC_LOG_DETAIL("got a touch-move in state %s\n", this,
ToString(mState).c_str());
switch (mState) {
case FLING:
case SMOOTHMSD_SCROLL:
case NOTHING:
case ANIMATING_ZOOM:
// May happen if the user double-taps and drags without lifting after the
// second tap. Ignore the move if this happens.
return nsEventStatus_eIgnore;
case TOUCHING: {
ScreenCoord panThreshold = GetTouchStartTolerance();
ExternalPoint extPoint = GetFirstExternalTouchPoint(aEvent);
Maybe<std::pair<MultiTouchInput, MultiTouchInput>> splitEvent;
// We intentionally skip the UpdateWithTouchAtDevicePoint call when the
// panThreshold is zero. This ensures more deterministic behaviour during
// testing. If we call that, Axis::mPos gets updated to the point of this
// touchmove event, but we "consume" the move to overcome the
// panThreshold, so it's hard to pan a specific amount reliably from a
// mochitest.
if (panThreshold > 0.0f) {
const float vectorLength = PanVector(extPoint).Length();
if (vectorLength < panThreshold) {
UpdateWithTouchAtDevicePoint(aEvent);
mLastTouch = {extPoint, aEvent.mTimeStamp};
return nsEventStatus_eIgnore;
}
splitEvent = MaybeSplitTouchMoveEvent(aEvent, panThreshold,
vectorLength, extPoint);
UpdateWithTouchAtDevicePoint(splitEvent ? splitEvent->first : aEvent);
}
nsEventStatus result;
const MultiTouchInput& firstEvent =
splitEvent ? splitEvent->first : aEvent;
mTouchScrollEventBuffer.push(firstEvent);
MOZ_ASSERT(GetCurrentTouchBlock());
if (GetCurrentTouchBlock()->TouchActionAllowsPanningXY()) {
// In the calls to StartPanning() below, the first argument needs to be
// the External position of |firstEvent|.
// However, instead of computing that using
// GetFirstExternalTouchPoint(firstEvent), we pass |extPoint| which
// has been modified by MaybeSplitTouchMoveEvent() to the desired
// value. This is a workaround for the fact that recomputing the
// External point would require a round-trip through |mScreenPoint|
// which is an integer.
// User tries to trigger a touch behavior. If allowed touch behavior is
// vertical pan + horizontal pan (touch-action value is equal to AUTO)
// we can return ConsumeNoDefault status immediately to trigger cancel
// event further.
// It should happen independent of the parent type (whether it is
// scrolling or not).
StartPanning(extPoint, firstEvent.mTimeStamp);
result = nsEventStatus_eConsumeNoDefault;
} else {
result = StartPanning(extPoint, firstEvent.mTimeStamp);
}
if (splitEvent && IsInPanningState()) {
TrackTouch(splitEvent->second);
return nsEventStatus_eConsumeNoDefault;
}
return result;
}
case PANNING:
case PANNING_LOCKED_X:
case PANNING_LOCKED_Y:
case PAN_MOMENTUM:
TrackTouch(aEvent);
return nsEventStatus_eConsumeNoDefault;
case PINCHING:
// The scale gesture listener should have handled this.
NS_WARNING(
"Gesture listener should have handled pinching in OnTouchMove.");
return nsEventStatus_eIgnore;
case SMOOTH_SCROLL:
case WHEEL_SCROLL:
case KEYBOARD_SCROLL:
case OVERSCROLL_ANIMATION:
case AUTOSCROLL:
case SCROLLBAR_DRAG:
// Should not receive a touch-move in the OVERSCROLL_ANIMATION state
// as touch blocks that begin in an overscrolled state cancel the
// animation. The same is true for wheel scroll animations.
NS_WARNING("Received impossible touch in OnTouchMove");
break;
}
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::OnTouchEnd(
const MultiTouchInput& aEvent) {
APZC_LOG_DETAIL("got a touch-end in state %s\n", this,
ToString(mState).c_str());
OnTouchEndOrCancel();
// In case no touch behavior triggered previously we can avoid sending
// scroll events or requesting content repaint. This condition is added
// to make tests consistent - in case touch-action is NONE (and therefore
// no pans/zooms can be performed) we expected neither scroll or repaint
// events.
if (mState != NOTHING) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
}
switch (mState) {
case FLING:
// Should never happen.
NS_WARNING("Received impossible touch end in OnTouchEnd.");
[[fallthrough]];
case ANIMATING_ZOOM:
case SMOOTHMSD_SCROLL:
case NOTHING:
// May happen if the user double-taps and drags without lifting after the
// second tap. Ignore if this happens.
return nsEventStatus_eIgnore;
case TOUCHING:
// We may have some velocity stored on the axis from move events
// that were not big enough to trigger scrolling. Clear that out.
SetVelocityVector(ParentLayerPoint(0, 0));
MOZ_ASSERT(GetCurrentTouchBlock());
APZC_LOG("%p still has %u touch points active\n", this,
GetCurrentTouchBlock()->GetActiveTouchCount());
// In cases where the user is panning, then taps the second finger without
// entering a pinch, we will arrive here when the second finger is lifted.
// However the first finger is still down so we want to remain in state
// TOUCHING.
if (GetCurrentTouchBlock()->GetActiveTouchCount() == 0) {
// It's possible we may be overscrolled if the user tapped during a
// previous overscroll pan. Make sure to snap back in this situation.
// An ancestor APZC could be overscrolled instead of this APZC, so
// walk the handoff chain as well.
GetCurrentTouchBlock()
->GetOverscrollHandoffChain()
->SnapBackOverscrolledApzc(this);
mFlingAccelerator.Reset();
// SnapBackOverscrolledApzc() will put any APZC it causes to snap back
// into the OVERSCROLL_ANIMATION state. If that's not us, since we're
// done TOUCHING enter the NOTHING state.
if (mState != OVERSCROLL_ANIMATION) {
SetState(NOTHING);
}
}
return nsEventStatus_eIgnore;
case PANNING:
case PANNING_LOCKED_X:
case PANNING_LOCKED_Y:
case PAN_MOMENTUM: {
MOZ_ASSERT(GetCurrentTouchBlock());
EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::Yes);
return HandleEndOfPan();
}
case PINCHING:
SetState(NOTHING);
// Scale gesture listener should have handled this.
NS_WARNING(
"Gesture listener should have handled pinching in OnTouchEnd.");
return nsEventStatus_eIgnore;
case SMOOTH_SCROLL:
case WHEEL_SCROLL:
case KEYBOARD_SCROLL:
case OVERSCROLL_ANIMATION:
case AUTOSCROLL:
case SCROLLBAR_DRAG:
// Should not receive a touch-end in the OVERSCROLL_ANIMATION state
// as touch blocks that begin in an overscrolled state cancel the
// animation. The same is true for WHEEL_SCROLL.
NS_WARNING("Received impossible touch in OnTouchEnd");
break;
}
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::OnTouchCancel(
const MultiTouchInput& aEvent) {
APZC_LOG_DETAIL("got a touch-cancel in state %s\n", this,
ToString(mState).c_str());
OnTouchEndOrCancel();
CancelAnimationAndGestureState();
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::OnScaleBegin(
const PinchGestureInput& aEvent) {
APZC_LOG_DETAIL("got a scale-begin in state %s\n", this,
ToString(mState).c_str());
mPinchLocked = false;
mPinchPaintTimerSet = false;
// Note that there may not be a touch block at this point, if we received the
// PinchGestureEvent directly from widget code without any touch events.
if (HasReadyTouchBlock() &&
!GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
return nsEventStatus_eIgnore;
}
// For platforms that don't support APZ zooming, dispatch a message to the
// content controller, it may want to do something else with this gesture.
// per-document basis.
if (!StaticPrefs::apz_allow_zooming()) {
if (RefPtr<GeckoContentController> controller =
GetGeckoContentController()) {
APZC_LOG("%p notifying controller of pinch gesture start\n", this);
controller->NotifyPinchGesture(
aEvent.mType, GetGuid(),
ViewAs<LayoutDevicePixel>(
aEvent.mFocusPoint,
PixelCastJustification::
LayoutDeviceIsScreenForUntransformedEvent),
0, aEvent.modifiers);
}
}
SetState(PINCHING);
Telemetry::Accumulate(Telemetry::APZ_ZOOM_PINCHSOURCE, (int)aEvent.mSource);
SetVelocityVector(ParentLayerPoint(0, 0));
RecursiveMutexAutoLock lock(mRecursiveMutex);
mLastZoomFocus =
aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
mPinchEventBuffer.push(aEvent);
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) {
APZC_LOG_DETAIL("got a scale in state %s\n", this, ToString(mState).c_str());
if (HasReadyTouchBlock() &&
!GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
return nsEventStatus_eIgnore;
}
if (mState != PINCHING) {
return nsEventStatus_eConsumeNoDefault;
}
mPinchEventBuffer.push(aEvent);
HandlePinchLocking(aEvent);
bool allowZoom = ZoomConstraintsAllowZoom() && !mPinchLocked;
// If we are pinch-locked, this is a two-finger pan.
// Tracking panning distance and velocity.
// UpdateWithTouchAtDevicePoint() acquires the tree lock, so
// it cannot be called while the mRecursiveMutex lock is held.
if (mPinchLocked) {
mX.UpdateWithTouchAtDevicePoint(aEvent.mLocalFocusPoint.x,
aEvent.mTimeStamp);
mY.UpdateWithTouchAtDevicePoint(aEvent.mLocalFocusPoint.y,
aEvent.mTimeStamp);
}
// per-document basis.
if (!StaticPrefs::apz_allow_zooming()) {
if (RefPtr<GeckoContentController> controller =
GetGeckoContentController()) {
APZC_LOG("%p notifying controller of pinch gesture\n", this);
controller->NotifyPinchGesture(
aEvent.mType, GetGuid(),
ViewAs<LayoutDevicePixel>(
aEvent.mFocusPoint,
PixelCastJustification::
LayoutDeviceIsScreenForUntransformedEvent),
ViewAs<LayoutDevicePixel>(
aEvent.mCurrentSpan - aEvent.mPreviousSpan,
PixelCastJustification::
LayoutDeviceIsScreenForUntransformedEvent),
aEvent.modifiers);
}
}
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
// Only the root APZC is zoomable, and the root APZC is not allowed to have
// different x and y scales. If it did, the calculations in this function
// would have to be adjusted (as e.g. it would no longer be valid to take
// the minimum or maximum of the ratios of the widths and heights of the
// page rect and the composition bounds).
MOZ_ASSERT(Metrics().IsRootContent());
CSSToParentLayerScale userZoom = Metrics().GetZoom();
ParentLayerPoint focusPoint =
aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
CSSPoint cssFocusPoint;
if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
cssFocusPoint = focusPoint / Metrics().GetZoom();
}
ParentLayerPoint focusChange = mLastZoomFocus - focusPoint;
mLastZoomFocus = focusPoint;
// If displacing by the change in focus point will take us off page bounds,
// then reduce the displacement such that it doesn't.
focusChange.x -= mX.DisplacementWillOverscrollAmount(focusChange.x);
focusChange.y -= mY.DisplacementWillOverscrollAmount(focusChange.y);
if (userZoom != CSSToParentLayerScale(0)) {
ScrollBy(focusChange / userZoom);
}
// If the span is zero or close to it, we don't want to process this zoom
// change because we're going to get wonky numbers for the spanRatio. So
// let's bail out here. Note that we do this after the focus-change-scroll
// above, so that if we have a pinch with zero span but changing focus,
// such as generated by some Synaptics touchpads on Windows, we still
// scroll properly.
float prevSpan = aEvent.mPreviousSpan;
if (fabsf(prevSpan) <= EPSILON || fabsf(aEvent.mCurrentSpan) <= EPSILON) {
// We might have done a nonzero ScrollBy above, so update metrics and
// repaint/recomposite
ScheduleCompositeAndMaybeRepaint();
return nsEventStatus_eConsumeNoDefault;
}
float spanRatio = aEvent.mCurrentSpan / aEvent.mPreviousSpan;
// When we zoom in with focus, we can zoom too much towards the boundaries
// that we actually go over them. These are the needed displacements along
// either axis such that we don't overscroll the boundaries when zooming.
CSSPoint neededDisplacement;
CSSToParentLayerScale realMinZoom = mZoomConstraints.mMinZoom;
CSSToParentLayerScale realMaxZoom = mZoomConstraints.mMaxZoom;
realMinZoom.scale =
std::max(realMinZoom.scale, Metrics().GetCompositionBounds().Width() /
Metrics().GetScrollableRect().Width());
realMinZoom.scale =
std::max(realMinZoom.scale, Metrics().GetCompositionBounds().Height() /
Metrics().GetScrollableRect().Height());
if (realMaxZoom < realMinZoom) {
realMaxZoom = realMinZoom;
}
bool doScale = allowZoom && ((spanRatio > 1.0 && userZoom < realMaxZoom) ||
(spanRatio < 1.0 && userZoom > realMinZoom));
if (doScale) {
spanRatio = std::clamp(spanRatio, realMinZoom.scale / userZoom.scale,
realMaxZoom.scale / userZoom.scale);
// Note that the spanRatio here should never put us into OVERSCROLL_BOTH
// because up above we clamped it.
neededDisplacement.x =
-mX.ScaleWillOverscrollAmount(spanRatio, cssFocusPoint.x);
neededDisplacement.y =
-mY.ScaleWillOverscrollAmount(spanRatio, cssFocusPoint.y);
ScaleWithFocus(spanRatio, cssFocusPoint);
if (neededDisplacement != CSSPoint()) {
ScrollBy(neededDisplacement);
}
// We don't want to redraw on every scale, so throttle it.
if (!mPinchPaintTimerSet) {
const int delay = StaticPrefs::apz_scale_repaint_delay_ms();
if (delay >= 0) {
if (RefPtr<GeckoContentController> controller =
GetGeckoContentController()) {
mPinchPaintTimerSet = true;
controller->PostDelayedTask(
NewRunnableMethod(
"layers::AsyncPanZoomController::"
"DoDelayedRequestContentRepaint",
this,
&AsyncPanZoomController::DoDelayedRequestContentRepaint),
delay);
}
}
} else if (apz::AboutToCheckerboard(mLastContentPaintMetrics,
Metrics())) {
// If we already scheduled a throttled repaint request but are also
// in danger of checkerboarding soon, trigger the repaint request to
// go out immediately. This should reduce the amount of time we spend
// checkerboarding.
//
// Note that if we remain in this "about to
// checkerboard" state over a period of time with multiple pinch input
// events (which is quite likely), then we will flip-flop between taking
// the above branch (!mPinchPaintTimerSet) and this branch (which will
// flush the repaint request and reset mPinchPaintTimerSet to false).
// This is sort of desirable because it halves the number of repaint
// requests we send, and therefore reduces IPC traffic.
// Keep in mind that many of these repaint requests will be ignored on
// the main-thread anyway due to the resolution mismatch - the first
// repaint request will be honored because APZ's notion of the painted
// resolution matches the actual main thread resolution, but that first
// repaint request will change the resolution on the main thread.
// Subsequent repaint requests will be ignored in APZCCallbackHelper, at
// until we receive a NotifyLayersUpdated call that re-syncs APZ's
// notion of the painted resolution to the main thread. These ignored
// repaint requests are contributing to IPC traffic needlessly, and so
// halving the number of repaint requests (as mentioned above) seems
// desirable.
DoDelayedRequestContentRepaint();
}
} else {
// Trigger a repaint request after scrolling.
RequestContentRepaint();
}
// We did a ScrollBy call above even if we didn't do a scale, so we
// should composite for that.
ScheduleComposite();
}
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::OnScaleEnd(
const PinchGestureInput& aEvent) {
APZC_LOG_DETAIL("got a scale-end in state %s\n", this,
ToString(mState).c_str());
mPinchPaintTimerSet = false;
if (HasReadyTouchBlock() &&
!GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
return nsEventStatus_eIgnore;
}
// per-document basis.
if (!StaticPrefs::apz_allow_zooming()) {
if (RefPtr<GeckoContentController> controller =
GetGeckoContentController()) {
controller->NotifyPinchGesture(
aEvent.mType, GetGuid(),
ViewAs<LayoutDevicePixel>(
aEvent.mFocusPoint,
PixelCastJustification::
LayoutDeviceIsScreenForUntransformedEvent),
0, aEvent.modifiers);
}
}
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
ScheduleComposite();
RequestContentRepaint();
}
mPinchEventBuffer.clear();
if (aEvent.mType == PinchGestureInput::PINCHGESTURE_FINGERLIFTED) {
// One finger is still down, so transition to a TOUCHING state
if (!mPinchLocked) {
mPanDirRestricted = false;
mLastTouch.mPosition = mStartTouch =
ToExternalPoint(aEvent.mScreenOffset, aEvent.mFocusPoint);
mLastTouch.mTimeStamp = mTouchStartTime = aEvent.mTimeStamp;
StartTouch(aEvent.mLocalFocusPoint, aEvent.mTimeStamp);
SetState(TOUCHING);
} else {
// If we are pinch locked, StartTouch() was already called
// when we entered the pinch lock.
StartPanning(ToExternalPoint(aEvent.mScreenOffset, aEvent.mFocusPoint),
aEvent.mTimeStamp);
}
} else {
// Otherwise, handle the gesture being completely done.
// Some of the code paths below, like ScrollSnap() or HandleEndOfPan(),
// may start an animation, but otherwise we want to end up in the NOTHING
// state. To avoid state change notification churn, we use a
// notification blocker.
bool stateWasPinching = (mState == PINCHING);
StateChangeNotificationBlocker blocker(this);
SetState(NOTHING);
if (ZoomConstraintsAllowZoom()) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
// We can get into a situation where we are overscrolled at the end of a
// pinch if we go into overscroll with a two-finger pan, and then turn
// that into a pinch by increasing the span sufficiently. In such a case,
// there is no snap-back animation to get us out of overscroll, so we need
// to get out of it somehow.
// Moreover, in cases of scroll handoff, the overscroll can be on an APZC
// further up in the handoff chain rather than on the current APZC, so
// we need to clear overscroll along the entire handoff chain.
if (HasReadyTouchBlock()) {
GetCurrentTouchBlock()->GetOverscrollHandoffChain()->ClearOverscroll();
} else {
ClearOverscroll();
}
// Along with clearing the overscroll, we also want to snap to the nearest
// snap point as appropriate.
ScrollSnap(ScrollSnapFlags::IntendedEndPosition);
} else {
// when zoom is not allowed
EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::Yes);
if (stateWasPinching) {
// still pinching
if (HasReadyTouchBlock()) {
return HandleEndOfPan();
}
}
}
}
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::HandleEndOfPan() {
MOZ_ASSERT(!mAnimation);
MOZ_ASSERT(GetCurrentTouchBlock() || GetCurrentPanGestureBlock());
GetCurrentInputBlock()->GetOverscrollHandoffChain()->FlushRepaints();
ParentLayerPoint flingVelocity = GetVelocityVector();
// Clear our velocities; if DispatchFling() gives the fling to us,
// the fling velocity gets *added* to our existing velocity in
// AcceptFling().
SetVelocityVector(ParentLayerPoint(0, 0));
// Clear our state so that we don't stay in the PANNING state
// if DispatchFling() gives the fling to somone else. However,
// don't send the state change notification until we've determined
// what our final state is to avoid notification churn.
StateChangeNotificationBlocker blocker(this);
SetState(NOTHING);
APZC_LOG("%p starting a fling animation if %f > %f\n", this,
flingVelocity.Length().value,
StaticPrefs::apz_fling_min_velocity_threshold());
if (flingVelocity.Length() <=
StaticPrefs::apz_fling_min_velocity_threshold()) {
// Relieve overscroll now if needed, since we will not transition to a fling
// animation and then an overscroll animation, and relieve it then.
GetCurrentInputBlock()
->GetOverscrollHandoffChain()
->SnapBackOverscrolledApzc(this);
mFlingAccelerator.Reset();
return nsEventStatus_eConsumeNoDefault;
}
// Make a local copy of the tree manager pointer and check that it's not
// null before calling DispatchFling(). This is necessary because Destroy(),
// which nulls out mTreeManager, could be called concurrently.
if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
const FlingHandoffState handoffState{
flingVelocity,
GetCurrentInputBlock()->GetOverscrollHandoffChain(),
Some(mTouchStartRestingTimeBeforePan),
mMinimumVelocityDuringPan.valueOr(0),
false /* not handoff */,
GetCurrentInputBlock()->GetScrolledApzc()};
treeManagerLocal->DispatchFling(this, handoffState);
}
return nsEventStatus_eConsumeNoDefault;
}
Maybe<LayoutDevicePoint> AsyncPanZoomController::ConvertToGecko(
const ScreenIntPoint& aPoint) {
if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
if (Maybe<ScreenIntPoint> layoutPoint =
treeManagerLocal->ConvertToGecko(aPoint, this)) {
return Some(LayoutDevicePoint(ViewAs<LayoutDevicePixel>(
*layoutPoint,
PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent)));
}
}
return Nothing();
}
OuterCSSCoord AsyncPanZoomController::ConvertScrollbarPoint(
const ParentLayerPoint& aScrollbarPoint,
const ScrollbarData& aThumbData) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
CSSPoint scrollbarPoint;
if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
// First, get it into the right coordinate space.
scrollbarPoint = aScrollbarPoint / Metrics().GetZoom();
}
// The scrollbar can be transformed with the frame but the pres shell
// resolution is only applied to the scroll frame.
OuterCSSPoint outerScrollbarPoint =
scrollbarPoint * Metrics().GetCSSToOuterCSSScale();
// Now, get it to be relative to the beginning of the scroll track.
OuterCSSRect cssCompositionBound =
Metrics().CalculateCompositionBoundsInOuterCssPixels();
return GetAxisStart(*aThumbData.mDirection, outerScrollbarPoint) -
GetAxisStart(*aThumbData.mDirection, cssCompositionBound) -
aThumbData.mScrollTrackStart;
}
static bool AllowsScrollingMoreThanOnePage(double aMultiplier) {
return Abs(aMultiplier) >=
EventStateManager::MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL;
}
ParentLayerPoint AsyncPanZoomController::GetScrollWheelDelta(
const ScrollWheelInput& aEvent) const {
return GetScrollWheelDelta(aEvent, aEvent.mDeltaX, aEvent.mDeltaY,
aEvent.mUserDeltaMultiplierX,
aEvent.mUserDeltaMultiplierY);
}
ParentLayerPoint AsyncPanZoomController::GetScrollWheelDelta(
const ScrollWheelInput& aEvent, double aDeltaX, double aDeltaY,
double aMultiplierX, double aMultiplierY) const {
ParentLayerSize scrollAmount;
ParentLayerSize pageScrollSize;
{
// Grab the lock to access the frame metrics.
RecursiveMutexAutoLock lock(mRecursiveMutex);
LayoutDeviceIntSize scrollAmountLD = mScrollMetadata.GetLineScrollAmount();
LayoutDeviceIntSize pageScrollSizeLD =
mScrollMetadata.GetPageScrollAmount();
scrollAmount = scrollAmountLD / Metrics().GetDevPixelsPerCSSPixel() *
Metrics().GetZoom();
pageScrollSize = pageScrollSizeLD / Metrics().GetDevPixelsPerCSSPixel() *
Metrics().GetZoom();
}
ParentLayerPoint delta;
switch (aEvent.mDeltaType) {
case ScrollWheelInput::SCROLLDELTA_LINE: {
delta.x = aDeltaX * scrollAmount.width;
delta.y = aDeltaY * scrollAmount.height;
break;
}
case ScrollWheelInput::SCROLLDELTA_PAGE: {
delta.x = aDeltaX * pageScrollSize.width;
delta.y = aDeltaY * pageScrollSize.height;
break;
}
case ScrollWheelInput::SCROLLDELTA_PIXEL: {
delta = ToParentLayerCoordinates(ScreenPoint(aDeltaX, aDeltaY),
aEvent.mOrigin);
break;
}
}
// Apply user-set multipliers.
delta.x *= aMultiplierX;
delta.y *= aMultiplierY;
APZC_LOGV(
"user-multiplied delta is %s (deltaType %d, line size %s, page size %s)",
ToString(delta).c_str(), (int)aEvent.mDeltaType,
ToString(scrollAmount).c_str(), ToString(pageScrollSize).c_str());
// For the conditions under which we allow system scroll overrides, see
// WidgetWheelEvent::OverriddenDelta{X,Y}.
// for discussion on this.
if (StaticPrefs::mousewheel_system_scroll_override_enabled() &&
!aEvent.IsCustomizedByUserPrefs() &&
aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_LINE &&
aEvent.mAllowToOverrideSystemScrollSpeed) {
delta.x = WidgetWheelEvent::ComputeOverriddenDelta(delta.x, false);
delta.y = WidgetWheelEvent::ComputeOverriddenDelta(delta.y, true);
APZC_LOGV("overridden delta is %s", ToString(delta).c_str());
}
// If this is a line scroll, and this event was part of a scroll series, then
// it might need extra acceleration. See WheelHandlingHelper.cpp.
if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_LINE &&
aEvent.mScrollSeriesNumber > 0) {
int32_t start = StaticPrefs::mousewheel_acceleration_start();
if (start >= 0 && aEvent.mScrollSeriesNumber >= uint32_t(start)) {
int32_t factor = StaticPrefs::mousewheel_acceleration_factor();
if (factor > 0) {
delta.x = ComputeAcceleratedWheelDelta(
delta.x, aEvent.mScrollSeriesNumber, factor);
delta.y = ComputeAcceleratedWheelDelta(
delta.y, aEvent.mScrollSeriesNumber, factor);
}
}
}
// We shouldn't scroll more than one page at once except when the
// user preference is large.
if (!AllowsScrollingMoreThanOnePage(aMultiplierX) &&
Abs(delta.x) > pageScrollSize.width) {
delta.x = (delta.x >= 0) ? pageScrollSize.width : -pageScrollSize.width;
}
if (!AllowsScrollingMoreThanOnePage(aMultiplierY) &&
Abs(delta.y) > pageScrollSize.height) {
delta.y = (delta.y >= 0) ? pageScrollSize.height : -pageScrollSize.height;
}
return delta;
}
nsEventStatus AsyncPanZoomController::OnKeyboard(const KeyboardInput& aEvent) {
// Mark that this APZC has async key scrolled
mTestHasAsyncKeyScrolled = true;
// Calculate the destination for this keyboard scroll action
CSSPoint destination = GetKeyboardDestination(aEvent.mAction);
ScrollOrigin scrollOrigin =
SmoothScrollAnimation::GetScrollOriginForAction(aEvent.mAction.mType);
Maybe<CSSSnapDestination> snapDestination =
MaybeAdjustDestinationForScrollSnapping(
aEvent, destination,
GetScrollSnapFlagsForKeyboardAction(aEvent.mAction));
ScrollMode scrollMode = apz::GetScrollModeForOrigin(scrollOrigin);
RecordScrollPayload(aEvent.mTimeStamp);
// If the scrolling is instant, then scroll immediately to the destination
if (scrollMode == ScrollMode::Instant) {
CancelAnimation();
ParentLayerPoint startPoint, endPoint;
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
// CallDispatchScroll interprets the start and end points as the start and
// end of a touch scroll so they need to be reversed.
startPoint = destination * Metrics().GetZoom();
endPoint = Metrics().GetVisualScrollOffset() * Metrics().GetZoom();
}
ParentLayerPoint delta = endPoint - startPoint;
ScreenPoint distance = ToScreenCoordinates(
ParentLayerPoint(fabs(delta.x), fabs(delta.y)), startPoint);
OverscrollHandoffState handoffState(
*mInputQueue->GetCurrentKeyboardBlock()->GetOverscrollHandoffChain(),
distance, ScrollSource::Keyboard);
CallDispatchScroll(startPoint, endPoint, handoffState);
ParentLayerPoint remainingDelta = endPoint - startPoint;
if (remainingDelta != delta) {
// If any scrolling happened, set KEYBOARD_SCROLL explicitly so that it
// will trigger a TransformEnd notification.
SetState(KEYBOARD_SCROLL);
}
if (snapDestination) {
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
mLastSnapTargetIds = std::move(snapDestination->mTargetIds);
}
}
SetState(NOTHING);
return nsEventStatus_eConsumeDoDefault;
}
// The lock must be held across the entire update operation, so the
// compositor doesn't end the animation before we get a chance to
// update it.
RecursiveMutexAutoLock lock(mRecursiveMutex);
if (snapDestination) {
// If we're scroll snapping, use a smooth scroll animation to get
// the desired physics. Note that SmoothMsdScrollTo() will re-use an
// existing smooth scroll animation if there is one.
APZC_LOG("%p keyboard scrolling to snap point %s\n", this,
ToString(destination).c_str());
SmoothMsdScrollTo(std::move(*snapDestination), ScrollTriggeredByScript::No);
return nsEventStatus_eConsumeDoDefault;
}
// Use a keyboard scroll animation to scroll, reusing an existing one if it
// exists
if (mState != KEYBOARD_SCROLL) {
CancelAnimation();
// Keyboard input that does not change the scroll position should not
// cause a TransformBegin state change, in order to avoid firing a
// scrollend event when no scrolling occurred.
if (!CanScroll(ConvertDestinationToDelta(destination))) {
return nsEventStatus_eConsumeDoDefault;
}
SetState(KEYBOARD_SCROLL);
nsPoint initialPosition =
CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset());
StartAnimation(do_AddRef(
new SmoothScrollAnimation(*this, initialPosition, scrollOrigin)));
}
// Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and then
// to appunits/second.
nsPoint velocity;
if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
velocity =
CSSPoint::ToAppUnits(ParentLayerPoint(mX.GetVelocity() * 1000.0f,
mY.GetVelocity() * 1000.0f) /
Metrics().GetZoom());
}
SmoothScrollAnimation* animation = mAnimation->AsSmoothScrollAnimation();
MOZ_ASSERT(animation);
animation->UpdateDestination(aEvent.mTimeStamp,
CSSPixel::ToAppUnits(destination),
nsSize(velocity.x, velocity.y));
return nsEventStatus_eConsumeDoDefault;
}
CSSPoint AsyncPanZoomController::GetKeyboardDestination(
const KeyboardScrollAction& aAction) const {
CSSSize lineScrollSize;
CSSSize pageScrollSize;
CSSPoint scrollOffset;
CSSRect scrollRect;
ParentLayerRect compositionBounds;
{
// Grab the lock to access the frame metrics.
RecursiveMutexAutoLock lock(mRecursiveMutex);
lineScrollSize = mScrollMetadata.GetLineScrollAmount() /
Metrics().GetDevPixelsPerCSSPixel();
pageScrollSize = mScrollMetadata.GetPageScrollAmount() /
Metrics().GetDevPixelsPerCSSPixel();
scrollOffset = GetCurrentAnimationDestination(lock).valueOr(
Metrics().GetVisualScrollOffset());
scrollRect = Metrics().GetScrollableRect();
compositionBounds = Metrics().GetCompositionBounds();
}
// Calculate the scroll destination based off of the scroll type and direction
CSSPoint scrollDestination = scrollOffset;
switch (aAction.mType) {
case KeyboardScrollAction::eScrollCharacter: {
int32_t scrollDistance =
StaticPrefs::toolkit_scrollbox_horizontalScrollDistance();
if (aAction.mForward) {
scrollDestination.x += scrollDistance * lineScrollSize.width;
} else {
scrollDestination.x -= scrollDistance * lineScrollSize.width;
}
break;
}
case KeyboardScrollAction::eScrollLine: {
int32_t scrollDistance =
StaticPrefs::toolkit_scrollbox_verticalScrollDistance();
if (scrollDistance * lineScrollSize.height <=
compositionBounds.Height()) {
if (aAction.mForward) {
scrollDestination.y += scrollDistance * lineScrollSize.height;
} else {
scrollDestination.y -= scrollDistance * lineScrollSize.height;
}
break;
}
[[fallthrough]];
}
case KeyboardScrollAction::eScrollPage: {
if (aAction.mForward) {
scrollDestination.y += pageScrollSize.height;
} else {
scrollDestination.y -= pageScrollSize.height;
}
break;
}
case KeyboardScrollAction::eScrollComplete: {
if (aAction.mForward) {
scrollDestination.y = scrollRect.YMost();
} else {
scrollDestination.y = scrollRect.Y();
}
break;
}
}
return scrollDestination;
}
ScrollSnapFlags AsyncPanZoomController::GetScrollSnapFlagsForKeyboardAction(
const KeyboardScrollAction& aAction) const {
switch (aAction.mType) {
case KeyboardScrollAction::eScrollCharacter:
case KeyboardScrollAction::eScrollLine:
return ScrollSnapFlags::IntendedDirection;
case KeyboardScrollAction::eScrollPage:
return ScrollSnapFlags::IntendedDirection |
ScrollSnapFlags::IntendedEndPosition;
case KeyboardScrollAction::eScrollComplete:
return ScrollSnapFlags::IntendedEndPosition;
}
return ScrollSnapFlags::Disabled;
}
ParentLayerPoint AsyncPanZoomController::GetDeltaForEvent(
const InputData& aEvent) const {
ParentLayerPoint delta;
if (aEvent.mInputType == SCROLLWHEEL_INPUT) {
delta = GetScrollWheelDelta(aEvent.AsScrollWheelInput());
} else if (aEvent.mInputType == PANGESTURE_INPUT) {
const PanGestureInput& panInput = aEvent.AsPanGestureInput();
delta = ToParentLayerCoordinates(panInput.UserMultipliedPanDisplacement(),
panInput.mPanStartPoint);
}
return delta;
}
CSSRect AsyncPanZoomController::GetCurrentScrollRangeInCssPixels() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return Metrics().CalculateScrollRange();
}
bool AsyncPanZoomController::AllowOneTouchPinch() const {
return StaticPrefs::apz_one_touch_pinch_enabled() &&
ZoomConstraintsAllowZoom();
}
// Return whether or not the underlying layer can be scrolled on either axis.
bool AsyncPanZoomController::CanScroll(const InputData& aEvent) const {
ParentLayerPoint delta = GetDeltaForEvent(aEvent);
APZC_LOGV_DETAIL("CanScroll: event delta is %s", this,
ToString(delta).c_str());
if (!delta.x && !delta.y) {
return false;
}
if (SCROLLWHEEL_INPUT == aEvent.mInputType) {
const ScrollWheelInput& scrollWheelInput = aEvent.AsScrollWheelInput();
// If it's a wheel scroll, we first check if it is an auto-dir scroll.
// 1. For an auto-dir scroll, check if it's delta should be adjusted, if it
// is, then we can conclude it must be scrollable; otherwise, fall back
// to checking if it is scrollable without adjusting its delta.
// 2. For a non-auto-dir scroll, simply check if it is scrollable without
// adjusting its delta.
RecursiveMutexAutoLock lock(mRecursiveMutex);
if (scrollWheelInput.IsAutoDir(mScrollMetadata.ForceMousewheelAutodir())) {
auto deltaX = scrollWheelInput.mDeltaX;
auto deltaY = scrollWheelInput.mDeltaY;
bool isRTL =
IsContentOfHonouredTargetRightToLeft(scrollWheelInput.HonoursRoot(
mScrollMetadata.ForceMousewheelAutodirHonourRoot()));
APZAutoDirWheelDeltaAdjuster adjuster(deltaX, deltaY, mX, mY, isRTL);
if (adjuster.ShouldBeAdjusted()) {
// If we detect that the delta values should be adjusted for an auto-dir
// wheel scroll, then it is impossible to be an unscrollable scroll.
return true;
}
}
return CanScrollWithWheel(delta);
}
return CanScroll(delta);
}
ScrollDirections AsyncPanZoomController::GetAllowedHandoffDirections(
HandoffConsumer aConsumer) const {
ScrollDirections result;
RecursiveMutexAutoLock lock(mRecursiveMutex);
// In Fission there can be non-scrollable APZCs. It's unclear whether
// overscroll-behavior should be respected for these
// we currently don't, to match existing practice.
const bool isScrollable = mX.CanScroll() || mY.CanScroll();
const bool isRoot = IsRootContent();
if ((!isScrollable && !isRoot) || mX.OverscrollBehaviorAllowsHandoff()) {
result += ScrollDirection::eHorizontal;
}
if ((!isScrollable && !isRoot) || mY.OverscrollBehaviorAllowsHandoff()) {
// to match Chrome behaviour.
bool blockPullToRefreshForOverflowHidden =
isRoot && aConsumer == HandoffConsumer::PullToRefresh &&
GetScrollMetadata().GetOverflow().mOverflowY == StyleOverflow::Hidden;
if (!blockPullToRefreshForOverflowHidden) {
result += ScrollDirection::eVertical;
}
}
return result;
}
bool AsyncPanZoomController::CanScroll(const ParentLayerPoint& aDelta) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return mX.CanScroll(ParentLayerCoord(aDelta.x)) ||
mY.CanScroll(ParentLayerCoord(aDelta.y));
}
bool AsyncPanZoomController::CanScrollWithWheel(
const ParentLayerPoint& aDelta) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
// For more details about the concept of a disregarded direction, refer to the
// code in struct ScrollMetadata which defines mDisregardedDirection.
Maybe<ScrollDirection> disregardedDirection =
mScrollMetadata.GetDisregardedDirection();
if (mX.CanScroll(ParentLayerCoord(aDelta.x)) &&
disregardedDirection != Some(ScrollDirection::eHorizontal)) {
return true;
}
if (mY.CanScroll(ParentLayerCoord(aDelta.y)) &&
disregardedDirection != Some(ScrollDirection::eVertical)) {
return true;
}
APZC_LOGV_FM(Metrics(),
"cannot scroll with wheel (disregarded direction is %s)",
ToString(disregardedDirection).c_str());
return false;
}
bool AsyncPanZoomController::CanScroll(ScrollDirection aDirection) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
switch (aDirection) {
case ScrollDirection::eHorizontal:
return mX.CanScroll();
case ScrollDirection::eVertical:
return mY.CanScroll();
}
MOZ_ASSERT_UNREACHABLE("Invalid value");
return false;
}
bool AsyncPanZoomController::CanVerticalScrollWithDynamicToolbar() const {
MOZ_ASSERT(IsRootContent());
RecursiveMutexAutoLock lock(mRecursiveMutex);
return mY.CanVerticalScrollWithDynamicToolbar();
}
bool AsyncPanZoomController::CanOverscrollUpwards() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return !mY.CanScrollTo(eSideTop) && mY.OverscrollBehaviorAllowsHandoff();
}
bool AsyncPanZoomController::CanScrollDownwards() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return mY.CanScrollTo(eSideBottom);
}
SideBits AsyncPanZoomController::ScrollableDirections() const {
SideBits result;
{ // scope lock to respect lock ordering with APZCTreeManager::mTreeLock
// which will be acquired in the `GetCompositorFixedLayerMargins` below.
RecursiveMutexAutoLock lock(mRecursiveMutex);
result = mX.ScrollableDirections() | mY.ScrollableDirections();
}
if (IsRootContent()) {
if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
ScreenMargin fixedLayerMargins =
treeManagerLocal->GetCompositorFixedLayerMargins();
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
result |= mY.ScrollableDirectionsWithDynamicToolbar(fixedLayerMargins);
}
}
}
return result;
}
bool AsyncPanZoomController::IsContentOfHonouredTargetRightToLeft(
bool aHonoursRoot) const {
if (aHonoursRoot) {
return mScrollMetadata.IsAutoDirRootContentRTL();
}
RecursiveMutexAutoLock lock(mRecursiveMutex);
return Metrics().IsHorizontalContentRightToLeft();
}
bool AsyncPanZoomController::AllowScrollHandoffInCurrentBlock() const {
bool result = mInputQueue->AllowScrollHandoff();
if (!StaticPrefs::apz_allow_immediate_handoff()) {
if (InputBlockState* currentBlock = GetCurrentInputBlock()) {
// Do not allow handoff beyond the first APZC to scroll.
if (currentBlock->GetScrolledApzc() == this) {
result = false;
APZC_LOG("%p dropping handoff; AllowImmediateHandoff=false\n", this);
}
}
}
return result;
}
void AsyncPanZoomController::DoDelayedRequestContentRepaint() {
if (!IsDestroyed() && mPinchPaintTimerSet) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
RequestContentRepaint();
}
mPinchPaintTimerSet = false;
}
void AsyncPanZoomController::DoDelayedTransformEndNotification(
PanZoomState aOldState) {
if (!IsDestroyed() && IsDelayedTransformEndSet()) {
DispatchStateChangeNotification(aOldState, NOTHING);
}
SetDelayedTransformEnd(false);
}
static void AdjustDeltaForAllowedScrollDirections(
ParentLayerPoint& aDelta,
const ScrollDirections& aAllowedScrollDirections) {
if (!aAllowedScrollDirections.contains(ScrollDirection::eHorizontal)) {
aDelta.x = 0;
}
if (!aAllowedScrollDirections.contains(ScrollDirection::eVertical)) {
aDelta.y = 0;
}
}
nsEventStatus AsyncPanZoomController::OnScrollWheel(
const ScrollWheelInput& aEvent) {
// Get the scroll wheel's delta values in parent-layer pixels. But before
// getting the values, we need to check if it is an auto-dir scroll and if it
// should be adjusted, if both answers are yes, let's adjust X and Y values
// first, and then get the delta values in parent-layer pixels based on the
// adjusted values.
bool adjustedByAutoDir = false;
auto deltaX = aEvent.mDeltaX;
auto deltaY = aEvent.mDeltaY;
ParentLayerPoint delta;
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
if (aEvent.IsAutoDir(mScrollMetadata.ForceMousewheelAutodir())) {
// It's an auto-dir scroll, so check if its delta should be adjusted, if
// so, adjust it.
bool isRTL = IsContentOfHonouredTargetRightToLeft(aEvent.HonoursRoot(
mScrollMetadata.ForceMousewheelAutodirHonourRoot()));
APZAutoDirWheelDeltaAdjuster adjuster(deltaX, deltaY, mX, mY, isRTL);
if (adjuster.ShouldBeAdjusted()) {
adjuster.Adjust();
adjustedByAutoDir = true;
}
}
}
// Ensure the calls to GetScrollWheelDelta are outside the mRecursiveMutex
// lock since these calls may acquire the APZ tree lock. Holding
// mRecursiveMutex while acquiring the APZ tree lock is lock ordering
// violation.
if (adjustedByAutoDir) {
// If the original delta values have been adjusted, we pass them to
// replace the original delta values in |aEvent| so that the delta values
// in parent-layer pixels are caculated based on the adjusted values, not
// the original ones.
// Pay special attention to the last two parameters. They are in a swaped
// order so that they still correspond to their delta after adjustment.
delta = GetScrollWheelDelta(aEvent, deltaX, deltaY,
aEvent.mUserDeltaMultiplierY,
aEvent.mUserDeltaMultiplierX);
} else {
// If the original delta values haven't been adjusted by auto-dir, just pass
// the |aEvent| and caculate the delta values in parent-layer pixels based
// on the original delta values from |aEvent|.
delta = GetScrollWheelDelta(aEvent);
}
APZC_LOG("%p got a scroll-wheel with delta in parent-layer pixels: %s\n",
this, ToString(delta).c_str());
if (adjustedByAutoDir) {
MOZ_ASSERT(delta.x || delta.y,
"Adjusted auto-dir delta values can never be all-zero.");
APZC_LOG("%p got a scroll-wheel with adjusted auto-dir delta values\n",
this);
} else if ((delta.x || delta.y) && !CanScrollWithWheel(delta)) {
// We can't scroll this apz anymore, so we simply drop the event.
if (mInputQueue->GetActiveWheelTransaction() &&
StaticPrefs::test_mousescroll()) {
if (RefPtr<GeckoContentController> controller =
GetGeckoContentController()) {
controller->NotifyMozMouseScrollEvent(GetScrollId(),
u"MozMouseScrollFailed"_ns);
}
}
return nsEventStatus_eConsumeNoDefault;
}
MOZ_ASSERT(mInputQueue->GetCurrentWheelBlock());
AdjustDeltaForAllowedScrollDirections(
delta, mInputQueue->GetCurrentWheelBlock()->GetAllowedScrollDirections());
if (delta.x == 0 && delta.y == 0) {
// Avoid spurious state changes and unnecessary work
return nsEventStatus_eIgnore;
}
switch (aEvent.mScrollMode) {
case ScrollWheelInput::SCROLLMODE_INSTANT: {
// Wheel events from "clicky" mouse wheels trigger scroll snapping to the
// next snap point. Check for this, and adjust the delta to take into
// account the snap point.
CSSPoint startPosition;
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
startPosition = Metrics().GetVisualScrollOffset();
}
Maybe<CSSSnapDestination> snapDestination =
MaybeAdjustDeltaForScrollSnappingOnWheelInput(aEvent, delta,
startPosition);
ScreenPoint distance = ToScreenCoordinates(
ParentLayerPoint(fabs(delta.x), fabs(delta.y)), aEvent.mLocalOrigin);
CancelAnimation();
OverscrollHandoffState handoffState(
*mInputQueue->GetCurrentWheelBlock()->GetOverscrollHandoffChain(),
distance, ScrollSource::Wheel);
ParentLayerPoint startPoint = aEvent.mLocalOrigin;
ParentLayerPoint endPoint = aEvent.mLocalOrigin - delta;
RecordScrollPayload(aEvent.mTimeStamp);
CallDispatchScroll(startPoint, endPoint, handoffState);
ParentLayerPoint remainingDelta = endPoint - startPoint;
if (remainingDelta != delta) {
// If any scrolling happened, set WHEEL_SCROLL explicitly so that it
// will trigger a TransformEnd notification.
SetState(WHEEL_SCROLL);
}
if (snapDestination) {
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
mLastSnapTargetIds = std::move(snapDestination->mTargetIds);
}
}
SetState(NOTHING);
// The calls above handle their own locking; moreover,
// ToScreenCoordinates() and CallDispatchScroll() can grab the tree lock.
RecursiveMutexAutoLock lock(mRecursiveMutex);
RequestContentRepaint();
break;
}
case ScrollWheelInput::SCROLLMODE_SMOOTH: {
// The lock must be held across the entire update operation, so the
// compositor doesn't end the animation before we get a chance to
// update it.
RecursiveMutexAutoLock lock(mRecursiveMutex);
RecordScrollPayload(aEvent.mTimeStamp);
// Perform scroll snapping if appropriate.
// If we're already in a wheel scroll or smooth scroll animation,
// the delta is applied to its destination, not to the current
// scroll position. Take this into account when finding a snap point.
CSSPoint startPosition = GetCurrentAnimationDestination(lock).valueOr(
Metrics().GetVisualScrollOffset());
if (Maybe<CSSSnapDestination> snapDestination =
MaybeAdjustDeltaForScrollSnappingOnWheelInput(aEvent, delta,
startPosition)) {
// If we're scroll snapping, use a smooth scroll animation to get
// the desired physics. Note that SmoothMsdScrollTo() will re-use an
// existing smooth scroll animation if there is one.
APZC_LOG("%p wheel scrolling to snap point %s\n", this,
ToString(startPosition).c_str());
SmoothMsdScrollTo(std::move(*snapDestination),
ScrollTriggeredByScript::No);
break;
}
// Otherwise, use a wheel scroll animation, also reusing one if possible.
if (mState != WHEEL_SCROLL) {
CancelAnimation();
SetState(WHEEL_SCROLL);
nsPoint initialPosition =
CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset());
StartAnimation(do_AddRef(new WheelScrollAnimation(
*this, initialPosition, aEvent.mDeltaType)));
}
// Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and
// then to appunits/second.
nsPoint deltaInAppUnits;
nsPoint velocity;
if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
deltaInAppUnits = CSSPoint::ToAppUnits(delta / Metrics().GetZoom());
velocity =
CSSPoint::ToAppUnits(ParentLayerPoint(mX.GetVelocity() * 1000.0f,
mY.GetVelocity() * 1000.0f) /
Metrics().GetZoom());
}
WheelScrollAnimation* animation = mAnimation->AsWheelScrollAnimation();
animation->UpdateDelta(aEvent.mTimeStamp, deltaInAppUnits,
nsSize(velocity.x, velocity.y));
break;
}
}
return nsEventStatus_eConsumeNoDefault;
}
void AsyncPanZoomController::NotifyMozMouseScrollEvent(
const nsString& aString) const {
RefPtr<GeckoContentController> controller = GetGeckoContentController();
if (!controller) {
return;
}
controller->NotifyMozMouseScrollEvent(GetScrollId(), aString);
}
nsEventStatus AsyncPanZoomController::OnPanMayBegin(
const PanGestureInput& aEvent) {
APZC_LOG_DETAIL("got a pan-maybegin in state %s\n", this,
ToString(mState).c_str());
StartTouch(aEvent.mLocalPanStartPoint, aEvent.mTimeStamp);
MOZ_ASSERT(GetCurrentPanGestureBlock());
GetCurrentPanGestureBlock()->GetOverscrollHandoffChain()->CancelAnimations(
ExcludeOverscroll);
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::OnPanCancelled(
const PanGestureInput& aEvent) {
APZC_LOG_DETAIL("got a pan-cancelled in state %s\n", this,
ToString(mState).c_str());
mX.CancelGesture();
mY.CancelGesture();
MOZ_ASSERT(GetCurrentPanGestureBlock());
GetCurrentPanGestureBlock()
->GetOverscrollHandoffChain()
->SnapBackOverscrolledApzc(this);
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::OnPanBegin(
const PanGestureInput& aEvent) {
APZC_LOG_DETAIL("got a pan-begin in state %s\n", this,
ToString(mState).c_str());
if (mState == SMOOTHMSD_SCROLL) {
// SMOOTHMSD_SCROLL scrolls are cancelled by pan gestures.
CancelAnimation();
}
StartTouch(aEvent.mLocalPanStartPoint, aEvent.mTimeStamp);
if (!UsingStatefulAxisLock()) {
SetState(PANNING);
} else {
float dx = aEvent.mPanDisplacement.x, dy = aEvent.mPanDisplacement.y;
if (dx != 0.0f || dy != 0.0f) {
double angle = atan2(dy, dx); // range [-pi, pi]
angle = fabs(angle); // range [0, pi]
HandlePanning(angle);
} else {
SetState(PANNING);
}
}
// Call into OnPan in order to process any delta included in this event.
OnPan(aEvent, FingersOnTouchpad::Yes);
return nsEventStatus_eConsumeNoDefault;
}
std::tuple<ParentLayerPoint, ScreenPoint>
AsyncPanZoomController::GetDisplacementsForPanGesture(
const PanGestureInput& aEvent) {
// Note that there is a multiplier that applies onto the "physical" pan
// displacement (how much the user's fingers moved) that produces the
// "logical" pan displacement (how much the page should move). For some of the
// code below it makes more sense to use the physical displacement rather than
// the logical displacement, and vice-versa.
ScreenPoint physicalPanDisplacement = aEvent.mPanDisplacement;
ParentLayerPoint logicalPanDisplacement =
aEvent.UserMultipliedLocalPanDisplacement();
if (aEvent.mDeltaType == PanGestureInput::PANDELTA_PAGE) {
// Pan events with page units are used by Gtk, so this replicates Gtk:
CSSSize pageScrollSize;
CSSToParentLayerScale zoom;
{
// Grab the lock to access the frame metrics.
RecursiveMutexAutoLock lock(mRecursiveMutex);
pageScrollSize = mScrollMetadata.GetPageScrollAmount() /
Metrics().GetDevPixelsPerCSSPixel();
zoom = Metrics().GetZoom();
}
// scrollUnit* is in units of "ParentLayer pixels per page proportion"...
auto scrollUnitWidth = std::min(std::pow(pageScrollSize.width, 2.0 / 3.0),
pageScrollSize.width / 2.0) *
zoom.scale;
auto scrollUnitHeight = std::min(std::pow(pageScrollSize.height, 2.0 / 3.0),
pageScrollSize.height / 2.0) *
zoom.scale;
// ... and pan displacements are in units of "page proportion count"
// here, so the products of them and scrollUnit* are in ParentLayer pixels
ParentLayerPoint physicalPanDisplacementPL(
physicalPanDisplacement.x * scrollUnitWidth,
physicalPanDisplacement.y * scrollUnitHeight);
physicalPanDisplacement = ToScreenCoordinates(physicalPanDisplacementPL,
aEvent.mLocalPanStartPoint);
logicalPanDisplacement.x *= scrollUnitWidth;
logicalPanDisplacement.y *= scrollUnitHeight;
// Accelerate (decelerate) any pans by raising it to a user configurable
// power (apz.touch_acceleration_factor_x, apz.touch_acceleration_factor_y)
//
// Confine input for pow() to greater than or equal to 0 to avoid domain
// errors with non-integer exponents
if (mX.GetVelocity() != 0) {
float absVelocity = std::abs(mX.GetVelocity());
logicalPanDisplacement.x *=
std::pow(absVelocity,
StaticPrefs::apz_touch_acceleration_factor_x()) /
absVelocity;
}
if (mY.GetVelocity() != 0) {
float absVelocity = std::abs(mY.GetVelocity());
logicalPanDisplacement.y *=
std::pow(absVelocity,
StaticPrefs::apz_touch_acceleration_factor_y()) /
absVelocity;
}
}
MOZ_ASSERT(GetCurrentPanGestureBlock());
AdjustDeltaForAllowedScrollDirections(
logicalPanDisplacement,
GetCurrentPanGestureBlock()->GetAllowedScrollDirections());
if (GetAxisLockMode() == AxisLockMode::DOMINANT_AXIS) {
// Given a pan gesture and both directions have a delta, implement
// dominant axis scrolling and only use the delta for the larger
// axis.
if (logicalPanDisplacement.y != 0 && logicalPanDisplacement.x != 0) {
if (fabs(logicalPanDisplacement.y) >= fabs(logicalPanDisplacement.x)) {
logicalPanDisplacement.x = 0;
physicalPanDisplacement.x = 0;
} else {
logicalPanDisplacement.y = 0;
physicalPanDisplacement.y = 0;
}
}
}
return {logicalPanDisplacement, physicalPanDisplacement};
}
CSSPoint AsyncPanZoomController::ToCSSPixels(ParentLayerPoint value) const {
if (this->Metrics().GetZoom() == CSSToParentLayerScale(0)) {
return CSSPoint{0, 0};
}
return (value / this->Metrics().GetZoom());
}
CSSCoord AsyncPanZoomController::ToCSSPixels(ParentLayerCoord value) const {
if (this->Metrics().GetZoom() == CSSToParentLayerScale(0)) {
return CSSCoord{0};
}
return (value / this->Metrics().GetZoom());
}
nsEventStatus AsyncPanZoomController::OnPan(
const PanGestureInput& aEvent, FingersOnTouchpad aFingersOnTouchpad) {
APZC_LOG_DETAIL("got a pan-pan in state %s\n", this,
ToString(GetState()).c_str());
if (GetState() == SMOOTHMSD_SCROLL) {
if (aFingersOnTouchpad == FingersOnTouchpad::No) {
// When a SMOOTHMSD_SCROLL scroll is being processed on a frame, mouse
// wheel and trackpad momentum scroll position updates will not cancel the
// SMOOTHMSD_SCROLL scroll animations, enabling scripts that depend on
// them to be responsive without forcing the user to wait for the momentum
// scrolling to completely stop.
return nsEventStatus_eConsumeNoDefault;
}
// SMOOTHMSD_SCROLL scrolls are cancelled by pan gestures.
CancelAnimation();
}
if (GetState() == NOTHING) {
// This event block was interrupted by something else. If the user's fingers
// are still on on the touchpad we want to resume scrolling, otherwise we
// ignore the rest of the scroll gesture.
if (aFingersOnTouchpad == FingersOnTouchpad::No) {
return nsEventStatus_eConsumeNoDefault;
}
// Resume / restart the pan.
// PanBegin will call back into this function with mState == PANNING.
return OnPanBegin(aEvent);
}
auto [logicalPanDisplacement, physicalPanDisplacement] =
GetDisplacementsForPanGesture(aEvent);
{
// Grab the lock to protect the animation from being canceled on the updater
// thread.
RecursiveMutexAutoLock lock(mRecursiveMutex);
MOZ_ASSERT_IF(GetState() == OVERSCROLL_ANIMATION, mAnimation);
if (GetState() == OVERSCROLL_ANIMATION && mAnimation &&
aFingersOnTouchpad == FingersOnTouchpad::No) {
// If there is an on-going overscroll animation, we tell the animation
// whether the displacements should be handled by the animation or not.
MOZ_ASSERT(mAnimation->AsOverscrollAnimation());
if (RefPtr<OverscrollAnimation> overscrollAnimation =
mAnimation->AsOverscrollAnimation()) {
overscrollAnimation->HandlePanMomentum(logicalPanDisplacement);
// And then as a result of the above call, if the animation is currently
// affecting on the axis, drop the displacement value on the axis so
// that we stop further oversrolling on the axis.
if (overscrollAnimation->IsManagingXAxis()) {
logicalPanDisplacement.x = 0;
physicalPanDisplacement.x = 0;
}
if (overscrollAnimation->IsManagingYAxis()) {
logicalPanDisplacement.y = 0;
physicalPanDisplacement.y = 0;
}
}
}
}
HandlePanningUpdate(physicalPanDisplacement);
MOZ_ASSERT(GetCurrentPanGestureBlock());
ScreenPoint panDistance(fabs(physicalPanDisplacement.x),
fabs(physicalPanDisplacement.y));
OverscrollHandoffState handoffState(
*GetCurrentPanGestureBlock()->GetOverscrollHandoffChain(), panDistance,
ScrollSource::Touchpad);
// Create fake "touch" positions that will result in the desired scroll
// motion. Note that the pan displacement describes the change in scroll
// position: positive displacement values mean that the scroll position
// increases. However, an increase in scroll position means that the scrolled
// contents are moved to the left / upwards. Since our simulated "touches"
// determine the motion of the scrolled contents, not of the scroll position,
// they need to move in the opposite direction of the pan displacement.
ParentLayerPoint startPoint = aEvent.mLocalPanStartPoint;
ParentLayerPoint endPoint =
aEvent.mLocalPanStartPoint - logicalPanDisplacement;
if (logicalPanDisplacement != ParentLayerPoint()) {
// Don't expect a composite to be triggered if the displacement is zero
RecordScrollPayload(aEvent.mTimeStamp);
}
const ParentLayerPoint velocity = GetVelocityVector();
bool consumed = CallDispatchScroll(startPoint, endPoint, handoffState);
const ParentLayerPoint visualDisplacement = ToParentLayerCoordinates(
handoffState.mTotalMovement, aEvent.mPanStartPoint);
// We need to update the axis velocity in order to get a useful display port
// size and position. We need to do so even if this is a momentum pan (i.e.
// aFingersOnTouchpad == No); in that case the "with touch" part is not
// really appropriate, so we may want to rethink this at some point.
// Note that we have to make all simulated positions relative to
// Axis::GetPos(), because the current position is an invented position, and
// because resetting the position to the mouse position (e.g.
// aEvent.mLocalStartPoint) would mess up velocity calculation. (This is
// the only caller of UpdateWithTouchAtDevicePoint() for pan events, so
// there is no risk of other calls resetting the position.)
// Also note that if there is an on-going overscroll animation in the axis,
// we shouldn't call UpdateWithTouchAtDevicePoint because the call changes
// the velocity which should be managed by the overscroll animation.
// Finally, note that we do this *after* CallDispatchScroll(), so that the
// position we use reflects the actual amount of movement that occurred
// (in particular, if we're in overscroll, if reflects the amount of movement
// *after* applying resistance). This is important because we want the axis
// velocity to track the visual movement speed of the page.
if (visualDisplacement.x != 0) {
mX.UpdateWithTouchAtDevicePoint(mX.GetPos() - visualDisplacement.x,
aEvent.mTimeStamp);
}
if (visualDisplacement.y != 0) {
mY.UpdateWithTouchAtDevicePoint(mY.GetPos() - visualDisplacement.y,
aEvent.mTimeStamp);
}
if (aFingersOnTouchpad == FingersOnTouchpad::No) {
if (IsOverscrolled() && GetState() != OVERSCROLL_ANIMATION) {
StartOverscrollAnimation(velocity, GetOverscrollSideBits());
} else if (!consumed) {
// If there is unconsumed scroll and we're in the momentum part of the
// pan gesture, terminate the momentum scroll. This prevents momentum
// scroll events from unexpectedly causing scrolling later if somehow
// the APZC becomes scrollable again in this direction (e.g. if the user
// uses some other input method to scroll in the opposite direction).
SetState(NOTHING);
}
}
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::OnPanEnd(const PanGestureInput& aEvent) {
APZC_LOG_DETAIL("got a pan-end in state %s\n", this,
ToString(mState).c_str());
// This can happen if the OS sends a second pan-end event after the first one
// has already started an overscroll animation or entered a fling state.
// This has been observed on some Wayland versions.
PanZoomState currentState = GetState();
if (currentState == OVERSCROLL_ANIMATION || currentState == NOTHING ||
currentState == FLING) {
return nsEventStatus_eIgnore;
}
if (aEvent.mPanDisplacement != ScreenPoint{}) {
// Call into OnPan in order to process the delta included in this event.
OnPan(aEvent, FingersOnTouchpad::Yes);
}
// Do not unlock the axis lock at the end of a pan gesture. The axis lock
// should extend into the momentum scroll.
EndTouch(aEvent.mTimeStamp, Axis::ClearAxisLock::No);
// Use HandleEndOfPan for fling on platforms that don't
// emit momentum events (Gtk).
if (aEvent.mSimulateMomentum) {
return HandleEndOfPan();
}
MOZ_ASSERT(GetCurrentPanGestureBlock());
RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
GetCurrentPanGestureBlock()->GetOverscrollHandoffChain();
// Call SnapBackOverscrolledApzcForMomentum regardless whether this APZC is
// overscrolled or not since overscroll animations for ancestor APZCs in this
// overscroll handoff chain might have been cancelled by the current pan
// gesture block.
overscrollHandoffChain->SnapBackOverscrolledApzcForMomentum(
this, GetVelocityVector());
// If this APZC is overscrolled, the above SnapBackOverscrolledApzcForMomentum
// triggers an overscroll animation. When we're finished with the overscroll
// animation, the state will be reset and a TransformEnd will be sent to the
// main thread.
currentState = GetState();
if (currentState != OVERSCROLL_ANIMATION) {
// Do not send a state change notification to the content controller here.
// Instead queue a delayed task to dispatch the notification if no
// momentum pan or scroll snap follows the pan-end.
RefPtr<GeckoContentController> controller = GetGeckoContentController();
if (controller) {
SetDelayedTransformEnd(true);
controller->PostDelayedTask(
NewRunnableMethod<PanZoomState>(
"layers::AsyncPanZoomController::"
"DoDelayedTransformEndNotification",
this, &AsyncPanZoomController::DoDelayedTransformEndNotification,
currentState),
StaticPrefs::apz_scrollend_event_content_delay_ms());
SetStateNoContentControllerDispatch(NOTHING);
} else {
SetState(NOTHING);
}
}
// Drop any velocity on axes where we don't have room to scroll anyways
// (in this APZC, or an APZC further in the handoff chain).
// This ensures that we don't enlarge the display port unnecessarily.
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
if (!overscrollHandoffChain->CanScrollInDirection(
this, ScrollDirection::eHorizontal)) {
mX.SetVelocity(0);
}
if (!overscrollHandoffChain->CanScrollInDirection(
this, ScrollDirection::eVertical)) {
mY.SetVelocity(0);
}
}
RequestContentRepaint();
ScrollSnapToDestination();
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::OnPanMomentumStart(
const PanGestureInput& aEvent) {
APZC_LOG_DETAIL("got a pan-momentumstart in state %s\n", this,
ToString(mState).c_str());
if (mState == SMOOTHMSD_SCROLL || mState == OVERSCROLL_ANIMATION) {
return nsEventStatus_eConsumeNoDefault;
}
if (IsDelayedTransformEndSet()) {
// Do not send another TransformBegin notification if we have not
// delivered a corresponding TransformEnd. Also ensure that any
// queued transform-end due to a pan-end is not sent. Instead rely
// on the transform-end sent due to the momentum pan.
SetDelayedTransformEnd(false);
SetStateNoContentControllerDispatch(PAN_MOMENTUM);
} else {
SetState(PAN_MOMENTUM);
}
// Call into OnPan in order to process any delta included in this event.
OnPan(aEvent, FingersOnTouchpad::No);
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::OnPanMomentumEnd(
const PanGestureInput& aEvent) {
APZC_LOG_DETAIL("got a pan-momentumend in state %s\n", this,
ToString(mState).c_str());
if (mState == OVERSCROLL_ANIMATION) {
return nsEventStatus_eConsumeNoDefault;
}
// Call into OnPan in order to process any delta included in this event.
OnPan(aEvent, FingersOnTouchpad::No);
// We need to reset the velocity to zero. We don't really have a "touch"
// here because the touch has already ended long before the momentum
// animation started, but I guess it doesn't really matter for now.
mX.CancelGesture();
mY.CancelGesture();
SetState(NOTHING);
RequestContentRepaint();
return nsEventStatus_eConsumeNoDefault;
}
nsEventStatus AsyncPanZoomController::OnPanInterrupted(
const PanGestureInput& aEvent) {
APZC_LOG_DETAIL("got a pan-interrupted in state %s\n", this,
ToString(mState).c_str());
CancelAnimation();
return nsEventStatus_eIgnore;
}
nsEventStatus AsyncPanZoomController::OnLongPress(
const TapGestureInput& aEvent) {
APZC_LOG_DETAIL("got a long-press in state %s\n", this,
ToString(mState).c_str());
RefPtr<GeckoContentController> controller = GetGeckoContentController();
if (controller) {
if (Maybe<LayoutDevicePoint> geckoScreenPoint =
ConvertToGecko(aEvent.mPoint)) {
TouchBlockState* touch = GetCurrentTouchBlock();
if (!touch) {
APZC_LOG(
"%p dropping long-press because some non-touch block interrupted "
"it\n",
this);
return nsEventStatus_eIgnore;
}
if (touch->IsDuringFastFling()) {
APZC_LOG("%p dropping long-press because of fast fling\n", this);
return nsEventStatus_eIgnore;
}
uint64_t blockId = GetInputQueue()->InjectNewTouchBlock(this);
controller->HandleTap(TapType::eLongTap, *geckoScreenPoint,
aEvent.modifiers, GetGuid(), blockId, Nothing());
return nsEventStatus_eConsumeNoDefault;
}
}
return nsEventStatus_eIgnore;
}
nsEventStatus AsyncPanZoomController::OnLongPressUp(
const TapGestureInput& aEvent) {
APZC_LOG_DETAIL("got a long-tap-up in state %s\n", this,
ToString(mState).c_str());
return GenerateSingleTap(TapType::eLongTapUp, aEvent.mPoint,
aEvent.modifiers);
}
nsEventStatus AsyncPanZoomController::GenerateSingleTap(
TapType aType, const ScreenIntPoint& aPoint,
mozilla::Modifiers aModifiers) {
RefPtr<GeckoContentController> controller = GetGeckoContentController();
if (controller) {
if (Maybe<LayoutDevicePoint> geckoScreenPoint = ConvertToGecko(aPoint)) {
TouchBlockState* touch = GetCurrentTouchBlock();
// |touch| may be null in the case where this function is
// invoked by GestureEventListener on a timeout. In that case we already
// verified that the single tap is allowed so we let it through.
// XXX there is a bug here that in such a case the touch block that
// generated this tap will not get its mSingleTapOccurred flag set.
if (touch) {
if (touch->IsDuringFastFling()) {
APZC_LOG(
"%p dropping single-tap because it was during a fast-fling\n",
this);
return nsEventStatus_eIgnore;
}
// The below `single-tap-occurred` flag is only used to tell whether the
// touch block caused a `click` event or not, thus for long-tap events,
// it's not necessary.
if (aType != TapType::eLongTapUp) {
touch->SetSingleTapState(apz::SingleTapState::WasClick);
}
}
// Because this may be being running as part of
// APZCTreeManager::ReceiveInputEvent, calling controller->HandleTap
// directly might mean that content receives the single tap message before
// the corresponding touch-up. To avoid that we schedule the singletap
// the issue this was causing.
APZC_LOG("posting runnable for HandleTap from GenerateSingleTap");
RefPtr<Runnable> runnable =
NewRunnableMethod<TapType, LayoutDevicePoint, mozilla::Modifiers,
ScrollableLayerGuid, uint64_t,
Maybe<DoubleTapToZoomMetrics>>(
"layers::GeckoContentController::HandleTap", controller,
&GeckoContentController::HandleTap, aType, *geckoScreenPoint,
aModifiers, GetGuid(), touch ? touch->GetBlockId() : 0,
Nothing());
controller->PostDelayedTask(runnable.forget(), 0);
return nsEventStatus_eConsumeNoDefault;
}
}
return nsEventStatus_eIgnore;
}
void AsyncPanZoomController::OnTouchEndOrCancel() {
mTouchScrollEventBuffer.clear();
if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
MOZ_ASSERT(GetCurrentTouchBlock());
controller->NotifyAPZStateChange(
GetGuid(), APZStateChange::eEndTouch,
static_cast<int>(GetCurrentTouchBlock()->SingleTapState()),
Some(GetCurrentTouchBlock()->GetBlockId()));
}
}
nsEventStatus AsyncPanZoomController::OnSingleTapUp(
const TapGestureInput& aEvent) {
APZC_LOG_DETAIL("got a single-tap-up in state %s\n", this,
ToString(mState).c_str());
// If mZoomConstraints.mAllowDoubleTapZoom is true we wait for a call to
// OnSingleTapConfirmed before sending event to content
MOZ_ASSERT(GetCurrentTouchBlock());
if (!(ZoomConstraintsAllowDoubleTapZoom() &&
GetCurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) {
return GenerateSingleTap(TapType::eSingleTap, aEvent.mPoint,
aEvent.modifiers);
}
// Ignore the event if it does not have valid local coordinates.
// GenerateSingleTap will not send a tap in this case.
if (!ConvertToGecko(aEvent.mPoint)) {
return nsEventStatus_eIgnore;
}
// Here we need to wait for the call to OnSingleTapConfirmed, we need to tell
// it to ActiveElementManager so that we can do element activation once
// ActiveElementManager got a single tap event later.
if (TouchBlockState* touch = GetCurrentTouchBlock()) {
if (!touch->IsDuringFastFling()) {
touch->SetSingleTapState(apz::SingleTapState::NotYetDetermined);
}
}
return nsEventStatus_eIgnore;
}
nsEventStatus AsyncPanZoomController::OnSingleTapConfirmed(
const TapGestureInput& aEvent) {
APZC_LOG_DETAIL("got a single-tap-confirmed in state %s\n", this,
ToString(mState).c_str());
return GenerateSingleTap(TapType::eSingleTap, aEvent.mPoint,
aEvent.modifiers);
}
nsEventStatus AsyncPanZoomController::OnDoubleTap(
const TapGestureInput& aEvent) {
APZC_LOG_DETAIL("got a double-tap in state %s\n", this,
ToString(mState).c_str());
MOZ_ASSERT(IsRootForLayersId(),
"This function should be called for the root content APZC or "
"OOPIF root APZC");
CSSToCSSMatrix4x4 transformToRootContentApzc;
RefPtr<AsyncPanZoomController> rootContentApzc;
if (IsRootContent()) {
rootContentApzc = RefPtr{this};
} else {
if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
rootContentApzc = treeManagerLocal->FindZoomableApzc(this);
if (rootContentApzc) {
MOZ_ASSERT(rootContentApzc->GetLayersId() != GetLayersId());
MOZ_ASSERT(this == treeManagerLocal->FindRootApzcFor(GetLayersId()));
transformToRootContentApzc =
treeManagerLocal->GetOopifToRootContentTransform(this);
}
}
}
if (!rootContentApzc) {
return nsEventStatus_eIgnore;
}
RefPtr<GeckoContentController> controller = GetGeckoContentController();
if (controller) {
if (rootContentApzc->ZoomConstraintsAllowDoubleTapZoom() &&
(!GetCurrentTouchBlock() ||
GetCurrentTouchBlock()->TouchActionAllowsDoubleTapZoom())) {
if (Maybe<LayoutDevicePoint> geckoScreenPoint =
ConvertToGecko(aEvent.mPoint)) {
controller->HandleTap(
TapType::eDoubleTap, *geckoScreenPoint, aEvent.modifiers, GetGuid(),
GetCurrentTouchBlock() ? GetCurrentTouchBlock()->GetBlockId() : 0,
Some(DoubleTapToZoomMetrics{rootContentApzc->GetVisualViewport(),
rootContentApzc->GetScrollableRect(),
transformToRootContentApzc}));
}
}
return nsEventStatus_eConsumeNoDefault;
}
return nsEventStatus_eIgnore;
}
nsEventStatus AsyncPanZoomController::OnSecondTap(
const TapGestureInput& aEvent) {
APZC_LOG_DETAIL("got a second-tap in state %s\n", this,
ToString(mState).c_str());
return GenerateSingleTap(TapType::eSecondTap, aEvent.mPoint,
aEvent.modifiers);
}
nsEventStatus AsyncPanZoomController::OnCancelTap(
const TapGestureInput& aEvent) {
APZC_LOG_DETAIL("got a cancel-tap in state %s\n", this,
ToString(mState).c_str());
// XXX: Implement this.
return nsEventStatus_eIgnore;
}
ScreenToParentLayerMatrix4x4 AsyncPanZoomController::GetTransformToThis()
const {
if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
return treeManagerLocal->GetScreenToApzcTransform(this);
}
return ScreenToParentLayerMatrix4x4();
}
ScreenPoint AsyncPanZoomController::ToScreenCoordinates(
const ParentLayerPoint& aVector, const ParentLayerPoint& aAnchor) const {
return TransformVector(GetTransformToThis().Inverse(), aVector, aAnchor);
}
// TODO: figure out a good way to check the w-coordinate is positive and return
// the result
ParentLayerPoint AsyncPanZoomController::ToParentLayerCoordinates(
const ScreenPoint& aVector, const ScreenPoint& aAnchor) const {
return TransformVector(GetTransformToThis(), aVector, aAnchor);
}
ParentLayerPoint AsyncPanZoomController::ToParentLayerCoordinates(
const ScreenPoint& aVector, const ExternalPoint& aAnchor) const {
return ToParentLayerCoordinates(
aVector,
ViewAs<ScreenPixel>(aAnchor, PixelCastJustification::ExternalIsScreen));
}
ExternalPoint AsyncPanZoomController::ToExternalPoint(
const ExternalPoint& aScreenOffset, const ScreenPoint& aScreenPoint) {
return aScreenOffset +
ViewAs<ExternalPixel>(aScreenPoint,
PixelCastJustification::ExternalIsScreen);
}
ScreenPoint AsyncPanZoomController::PanVector(const ExternalPoint& aPos) const {
return ScreenPoint(fabs(aPos.x - mStartTouch.x),
fabs(aPos.y - mStartTouch.y));
}
bool AsyncPanZoomController::Contains(const ScreenIntPoint& aPoint) const {
ScreenToParentLayerMatrix4x4 transformToThis = GetTransformToThis();
Maybe<ParentLayerIntPoint> point = UntransformBy(transformToThis, aPoint);
if (!point) {
return false;
}
ParentLayerIntRect cb;
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
GetFrameMetrics().GetCompositionBounds().ToIntRect(&cb);
}
return cb.Contains(*point);
}
bool AsyncPanZoomController::IsInOverscrollGutter(
const ScreenPoint& aHitTestPoint) const {
if (!IsPhysicallyOverscrolled()) {
return false;
}
Maybe<ParentLayerPoint> apzcPoint =
UntransformBy(GetTransformToThis(), aHitTestPoint);
if (!apzcPoint) return false;
return IsInOverscrollGutter(*apzcPoint);
}
bool AsyncPanZoomController::IsInOverscrollGutter(
const ParentLayerPoint& aHitTestPoint) const {
ParentLayerRect compositionBounds;
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
compositionBounds = GetFrameMetrics().GetCompositionBounds();
}
if (!compositionBounds.Contains(aHitTestPoint)) {
// Point is outside of scrollable element's bounds altogether.
return false;
}
auto overscrollTransform = GetOverscrollTransform(eForEventHandling);
ParentLayerPoint overscrollUntransformed =
overscrollTransform.Inverse().TransformPoint(aHitTestPoint);
if (compositionBounds.Contains(overscrollUntransformed)) {
// Point is over scrollable content.
return false;
}
// Point is in gutter.
return true;
}
bool AsyncPanZoomController::IsOverscrolled() const {
return mOverscrollEffect->IsOverscrolled();
}
bool AsyncPanZoomController::IsPhysicallyOverscrolled() const {
// As an optimization, avoid calling Apply/UnapplyAsyncTestAttributes
// unless we're in a test environment where we need it.
if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
return mX.IsOverscrolled() || mY.IsOverscrolled();
}
RecursiveMutexAutoLock lock(mRecursiveMutex);
return mX.IsOverscrolled() || mY.IsOverscrolled();
}
bool AsyncPanZoomController::IsInInvalidOverscroll() const {
return mX.IsInInvalidOverscroll() || mY.IsInInvalidOverscroll();
}
ParentLayerPoint AsyncPanZoomController::PanStart() const {
return ParentLayerPoint(mX.PanStart(), mY.PanStart());
}
const ParentLayerPoint AsyncPanZoomController::GetVelocityVector() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return ParentLayerPoint(mX.GetVelocity(), mY.GetVelocity());
}
void AsyncPanZoomController::SetVelocityVector(
const ParentLayerPoint& aVelocityVector) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
mX.SetVelocity(aVelocityVector.x);
mY.SetVelocity(aVelocityVector.y);
}
void AsyncPanZoomController::HandlePanningWithTouchAction(double aAngle) {
// Handling of cross sliding will need to be added in this method after
// touch-action released enabled by default.
MOZ_ASSERT(GetCurrentTouchBlock());
RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
GetCurrentInputBlock()->GetOverscrollHandoffChain();
bool canScrollHorizontal =
!mX.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
this, ScrollDirection::eHorizontal);
bool canScrollVertical =
!mY.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
this, ScrollDirection::eVertical);
if (GetCurrentTouchBlock()->TouchActionAllowsPanningXY()) {
if (canScrollHorizontal && canScrollVertical) {
if (apz::IsCloseToHorizontal(aAngle,
StaticPrefs::apz_axis_lock_lock_angle())) {
mY.SetAxisLocked(true);
SetState(PANNING_LOCKED_X);
} else if (apz::IsCloseToVertical(
aAngle, StaticPrefs::apz_axis_lock_lock_angle())) {
mX.SetAxisLocked(true);
SetState(PANNING_LOCKED_Y);
} else {
SetState(PANNING);
}
} else if (canScrollHorizontal || canScrollVertical) {
SetState(PANNING);
} else {
SetState(NOTHING);
}
} else if (GetCurrentTouchBlock()->TouchActionAllowsPanningX()) {
// Using bigger angle for panning to keep behavior consistent
// with IE.
if (apz::IsCloseToHorizontal(
aAngle, StaticPrefs::apz_axis_lock_direct_pan_angle())) {
mY.SetAxisLocked(true);
SetState(PANNING_LOCKED_X);
mPanDirRestricted = true;
} else {
// Don't treat these touches as pan/zoom movements since 'touch-action'
// value requires it.
SetState(NOTHING);
}
} else if (GetCurrentTouchBlock()->TouchActionAllowsPanningY()) {
if (apz::IsCloseToVertical(aAngle,
StaticPrefs::apz_axis_lock_direct_pan_angle())) {
mX.SetAxisLocked(true);
SetState(PANNING_LOCKED_Y);
mPanDirRestricted = true;
} else {
SetState(NOTHING);
}
} else {
SetState(NOTHING);
}
if (!IsInPanningState()) {
// If we didn't enter a panning state because touch-action disallowed it,
// make sure to clear any leftover velocity from the pre-threshold
// touchmoves.
mX.SetVelocity(0);
mY.SetVelocity(0);
}
}
void AsyncPanZoomController::HandlePanning(double aAngle) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
MOZ_ASSERT(GetCurrentInputBlock());
RefPtr<const OverscrollHandoffChain> overscrollHandoffChain =
GetCurrentInputBlock()->GetOverscrollHandoffChain();
bool canScrollHorizontal =
!mX.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
this, ScrollDirection::eHorizontal);
bool canScrollVertical =
!mY.IsAxisLocked() && overscrollHandoffChain->CanScrollInDirection(
this, ScrollDirection::eVertical);
MOZ_ASSERT(UsingStatefulAxisLock());
if (!canScrollHorizontal || !canScrollVertical) {
SetState(PANNING);
} else if (apz::IsCloseToHorizontal(
aAngle, StaticPrefs::apz_axis_lock_lock_angle())) {
mY.SetAxisLocked(true);
if (canScrollHorizontal) {
SetState(PANNING_LOCKED_X);
}
} else if (apz::IsCloseToVertical(aAngle,
StaticPrefs::apz_axis_lock_lock_angle())) {
mX.SetAxisLocked(true);
if (canScrollVertical) {
SetState(PANNING_LOCKED_Y);
}
} else {
SetState(PANNING);
}
}
void AsyncPanZoomController::HandlePanningUpdate(
const ScreenPoint& aPanDistance) {
// If we're axis-locked, check if the user is trying to break the lock
if ((GetAxisLockMode() == AxisLockMode::STICKY ||
GetAxisLockMode() == AxisLockMode::BREAKABLE) &&
!mPanDirRestricted) {
ParentLayerPoint vector =
ToParentLayerCoordinates(aPanDistance, mStartTouch);
float angle = atan2f(vector.y, vector.x); // range [-pi, pi]
angle = fabsf(angle); // range [0, pi]
float breakThreshold =
StaticPrefs::apz_axis_lock_breakout_threshold() * GetDPI();
if (fabs(aPanDistance.x) > breakThreshold ||
fabs(aPanDistance.y) > breakThreshold) {
switch (mState) {
case PANNING_LOCKED_X:
if (!apz::IsCloseToHorizontal(
angle, StaticPrefs::apz_axis_lock_breakout_angle())) {
mY.SetAxisLocked(false);
// If we are within the lock angle from the Y axis and STICKY,
// lock onto the Y axis. BREAKABLE should not re-acquire the lock.
if (apz::IsCloseToVertical(
angle, StaticPrefs::apz_axis_lock_lock_angle()) &&
GetAxisLockMode() != AxisLockMode::BREAKABLE) {
mX.SetAxisLocked(true);
SetState(PANNING_LOCKED_Y);
} else {
SetState(PANNING);
}
}
break;
case PANNING_LOCKED_Y:
if (!apz::IsCloseToVertical(
angle, StaticPrefs::apz_axis_lock_breakout_angle())) {
mX.SetAxisLocked(false);
// If we are within the lock angle from the X axis and STICKY,
// lock onto the X axis. BREAKABLE should not re-acquire the lock.
if (apz::IsCloseToHorizontal(
angle, StaticPrefs::apz_axis_lock_lock_angle()) &&
GetAxisLockMode() != AxisLockMode::BREAKABLE) {
mY.SetAxisLocked(true);
SetState(PANNING_LOCKED_X);
} else {
SetState(PANNING);
}
}
break;
case PANNING:
// `HandlePanning` can re-acquire the axis lock, which we don't want
// to do if the lock is BREAKABLE
if (GetAxisLockMode() != AxisLockMode::BREAKABLE) {
HandlePanning(angle);
}
break;
default:
break;
}
}
}
}
void AsyncPanZoomController::HandlePinchLocking(
const PinchGestureInput& aEvent) {
// Focus change and span distance calculated from an event buffer
// Used to handle pinch locking irrespective of touch screen sensitivity
// Note: both values fall back to the same value as
// their un-buffered counterparts if there is only one (the latest)
// event in the buffer. ie: when the touch screen is dispatching
// events slower than the lifetime of the buffer
ParentLayerCoord bufferedSpanDistance;
ParentLayerPoint focusPoint, bufferedFocusChange;
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
focusPoint = mPinchEventBuffer.back().mLocalFocusPoint -
Metrics().GetCompositionBounds().TopLeft();
ParentLayerPoint bufferedLastZoomFocus =
(mPinchEventBuffer.size() > 1)
? mPinchEventBuffer.front().mLocalFocusPoint -
Metrics().GetCompositionBounds().TopLeft()
: mLastZoomFocus;
bufferedFocusChange = bufferedLastZoomFocus - focusPoint;
bufferedSpanDistance = fabsf(mPinchEventBuffer.front().mPreviousSpan -
mPinchEventBuffer.back().mCurrentSpan);
}
// Convert to screen coordinates
ScreenCoord spanDistance =
ToScreenCoordinates(ParentLayerPoint(0, bufferedSpanDistance), focusPoint)
.Length();
ScreenPoint focusChange =
ToScreenCoordinates(bufferedFocusChange, focusPoint);
if (mPinchLocked) {
if (GetPinchLockMode() == PINCH_STICKY) {
ScreenCoord spanBreakoutThreshold =
StaticPrefs::apz_pinch_lock_span_breakout_threshold() * GetDPI();
mPinchLocked = !(spanDistance > spanBreakoutThreshold);
}
} else {
if (GetPinchLockMode() != PINCH_FREE) {
ScreenCoord spanLockThreshold =
StaticPrefs::apz_pinch_lock_span_lock_threshold() * GetDPI();
ScreenCoord scrollLockThreshold =
StaticPrefs::apz_pinch_lock_scroll_lock_threshold() * GetDPI();
if (spanDistance < spanLockThreshold &&
focusChange.Length() > scrollLockThreshold) {
mPinchLocked = true;
// We are transitioning to a two-finger pan that could trigger
// a fling at its end, so start tracking velocity.
StartTouch(aEvent.mLocalFocusPoint, aEvent.mTimeStamp);
}
}
}
}
nsEventStatus AsyncPanZoomController::StartPanning(
const ExternalPoint& aStartPoint, const TimeStamp& aEventTime) {
ParentLayerPoint vector =
ToParentLayerCoordinates(PanVector(aStartPoint), mStartTouch);
double angle = atan2(vector.y, vector.x); // range [-pi, pi]
angle = fabs(angle); // range [0, pi]
RecursiveMutexAutoLock lock(mRecursiveMutex);
HandlePanningWithTouchAction(angle);
if (IsInPanningState()) {
mTouchStartRestingTimeBeforePan = aEventTime - mTouchStartTime;
mMinimumVelocityDuringPan = Nothing();
if (RefPtr<GeckoContentController> controller =
GetGeckoContentController()) {
controller->NotifyAPZStateChange(GetGuid(),
APZStateChange::eStartPanning);
}
return nsEventStatus_eConsumeNoDefault;
}
// Don't consume an event that didn't trigger a panning.
return nsEventStatus_eIgnore;
}
void AsyncPanZoomController::UpdateWithTouchAtDevicePoint(
const MultiTouchInput& aEvent) {
const SingleTouchData& touchData = aEvent.mTouches[0];
// Take historical touch data into account in order to improve the accuracy
// of the velocity estimate. On many Android devices, the touch screen samples
// at a higher rate than vsync (e.g. 100Hz vs 60Hz), and the historical data
// lets us take advantage of those high-rate samples.
for (const auto& historicalData : touchData.mHistoricalData) {
ParentLayerPoint historicalPoint = historicalData.mLocalScreenPoint;
mX.UpdateWithTouchAtDevicePoint(historicalPoint.x,
historicalData.mTimeStamp);
mY.UpdateWithTouchAtDevicePoint(historicalPoint.y,
historicalData.mTimeStamp);
}
ParentLayerPoint point = touchData.mLocalScreenPoint;
mX.UpdateWithTouchAtDevicePoint(point.x, aEvent.mTimeStamp);
mY.UpdateWithTouchAtDevicePoint(point.y, aEvent.mTimeStamp);
}
Maybe<CompositionPayload> AsyncPanZoomController::NotifyScrollSampling() {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return mSampledState.front().TakeScrollPayload();
}
bool AsyncPanZoomController::AttemptScroll(
ParentLayerPoint& aStartPoint, ParentLayerPoint& aEndPoint,
OverscrollHandoffState& aOverscrollHandoffState) {
// "start - end" rather than "end - start" because e.g. moving your finger
// down (*positive* direction along y axis) causes the vertical scroll offset
// to *decrease* as the page follows your finger.
ParentLayerPoint displacement = aStartPoint - aEndPoint;
ParentLayerPoint overscroll; // will be used outside monitor block
// If the direction of panning is reversed within the same input block,
// a later event in the block could potentially scroll an APZC earlier
// in the handoff chain, than an earlier event in the block (because
// the earlier APZC was scrolled to its extent in the original direction).
// We want to disallow this.
bool scrollThisApzc = false;
if (InputBlockState* block = GetCurrentInputBlock()) {
scrollThisApzc =
!block->GetScrolledApzc() || block->IsDownchainOfScrolledApzc(this);
}
ParentLayerPoint adjustedDisplacement;
if (scrollThisApzc) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
bool respectDisregardedDirections =
ScrollSourceRespectsDisregardedDirections(
aOverscrollHandoffState.mScrollSource);
bool forcesVerticalOverscroll = respectDisregardedDirections &&
mScrollMetadata.GetDisregardedDirection() ==
Some(ScrollDirection::eVertical);
bool forcesHorizontalOverscroll =
respectDisregardedDirections &&
mScrollMetadata.GetDisregardedDirection() ==
Some(ScrollDirection::eHorizontal);
bool yChanged =
mY.AdjustDisplacement(displacement.y, adjustedDisplacement.y,
overscroll.y, forcesVerticalOverscroll);
bool xChanged =
mX.AdjustDisplacement(displacement.x, adjustedDisplacement.x,
overscroll.x, forcesHorizontalOverscroll);
if (xChanged || yChanged) {
ScheduleComposite();
}
if (!IsZero(adjustedDisplacement) &&
Metrics().GetZoom() != CSSToParentLayerScale(0)) {
ScrollBy(adjustedDisplacement / Metrics().GetZoom());
if (InputBlockState* block = GetCurrentInputBlock()) {
bool displacementIsUserVisible = true;
{ // Release the APZC lock before calling ToScreenCoordinates which
// acquires the APZ tree lock. Note that this just unlocks the mutex
// once, so if we're locking it multiple times on the callstack then
// this will be insufficient.
RecursiveMutexAutoUnlock unlock(mRecursiveMutex);
ScreenIntPoint screenDisplacement = RoundedToInt(
ToScreenCoordinates(adjustedDisplacement, aStartPoint));
// If the displacement we just applied rounds to zero in screen space,
// then it's probably not going to be visible to the user. In that
// case let's not mark this APZC as scrolled, so that even if the
// immediate handoff pref is disabled, we'll allow doing the handoff
// to the next APZC.
if (screenDisplacement == ScreenIntPoint()) {
displacementIsUserVisible = false;
}
}
if (displacementIsUserVisible) {
block->SetScrolledApzc(this);
}
}
// Note that in the case of instant scrolling, the last snap target ids
// will be set after AttemptScroll call so that we can clobber them
// unconditionally here.
mLastSnapTargetIds = ScrollSnapTargetIds{};
ScheduleCompositeAndMaybeRepaint();
}
// Adjust the start point to reflect the consumed portion of the scroll.
aStartPoint = aEndPoint + overscroll;
} else {
overscroll = displacement;
}
// Accumulate the amount of actual scrolling that occurred into the handoff
// state. Note that ToScreenCoordinates() needs to be called outside the
// mutex.
if (!IsZero(adjustedDisplacement)) {
aOverscrollHandoffState.mTotalMovement +=
ToScreenCoordinates(adjustedDisplacement, aEndPoint);
}
// If we consumed the entire displacement as a normal scroll, great.
if (IsZero(overscroll)) {
return true;
}
if (AllowScrollHandoffInCurrentBlock()) {
// If there is overscroll, first try to hand it off to an APZC later
// in the handoff chain to consume (either as a normal scroll or as
// overscroll).
// Note: "+ overscroll" rather than "- overscroll" because "overscroll"
// is what's left of "displacement", and "displacement" is "start - end".
++aOverscrollHandoffState.mChainIndex;
bool consumed =
CallDispatchScroll(aStartPoint, aEndPoint, aOverscrollHandoffState);
if (consumed) {
return true;
}
overscroll = aStartPoint - aEndPoint;
MOZ_ASSERT(!IsZero(overscroll));
}
// If there is no APZC later in the handoff chain that accepted the
// overscroll, try to accept it ourselves. We only accept it if we
// are pannable.
if (ScrollSourceAllowsOverscroll(aOverscrollHandoffState.mScrollSource)) {
APZC_LOG("%p taking overscroll during panning\n", this);
ParentLayerPoint prevVisualOverscroll = GetOverscrollAmount();
OverscrollForPanning(overscroll, aOverscrollHandoffState.mPanDistance);
// Accumulate the amount of change to the overscroll that occurred into the
// handoff state. Note that the input amount, |overscroll|, is turned into
// some smaller visual overscroll amount (queried via GetOverscrollAmount())
// by applying resistance (Axis::ApplyResistance()), and it's the latter we
// want to count towards OverscrollHandoffState::mTotalMovement.
ParentLayerPoint visualOverscrollChange =
GetOverscrollAmount() - prevVisualOverscroll;
if (!IsZero(visualOverscrollChange)) {
aOverscrollHandoffState.mTotalMovement +=
ToScreenCoordinates(visualOverscrollChange, aEndPoint);
}
}
aStartPoint = aEndPoint + overscroll;
return IsZero(overscroll);
}
void AsyncPanZoomController::OverscrollForPanning(
ParentLayerPoint& aOverscroll, const ScreenPoint& aPanDistance) {
// Only allow entering overscroll along an axis if the pan distance along
// that axis is greater than the pan distance along the other axis by a
// configurable factor. If we are already overscrolled, don't check this.
if (!IsOverscrolled()) {
if (aPanDistance.x <
StaticPrefs::apz_overscroll_min_pan_distance_ratio() * aPanDistance.y) {
aOverscroll.x = 0;
}
if (aPanDistance.y <
StaticPrefs::apz_overscroll_min_pan_distance_ratio() * aPanDistance.x) {
aOverscroll.y = 0;
}
}
OverscrollBy(aOverscroll);
}
ScrollDirections AsyncPanZoomController::GetOverscrollableDirections() const {
ScrollDirections result;
RecursiveMutexAutoLock lock(mRecursiveMutex);
// If the target has the disregarded direction, it means it's single line
// text control, thus we don't want to overscroll in both directions.
if (mScrollMetadata.GetDisregardedDirection()) {
return result;
}
if (mX.CanScroll() && mX.OverscrollBehaviorAllowsOverscrollEffect()) {
result += ScrollDirection::eHorizontal;
}
if (mY.CanScroll() && mY.OverscrollBehaviorAllowsOverscrollEffect()) {
result += ScrollDirection::eVertical;
}
return result;
}
void AsyncPanZoomController::OverscrollBy(ParentLayerPoint& aOverscroll) {
if (!StaticPrefs::apz_overscroll_enabled()) {
return;
}
RecursiveMutexAutoLock lock(mRecursiveMutex);
// Do not go into overscroll in a direction in which we have no room to
// scroll to begin with.
ScrollDirections overscrollableDirections = GetOverscrollableDirections();
if (IsZero(aOverscroll.x)) {
overscrollableDirections -= ScrollDirection::eHorizontal;
}
if (IsZero(aOverscroll.y)) {
overscrollableDirections -= ScrollDirection::eVertical;
}
mOverscrollEffect->ConsumeOverscroll(aOverscroll, overscrollableDirections);
}
RefPtr<const OverscrollHandoffChain>
AsyncPanZoomController::BuildOverscrollHandoffChain() {
if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
return treeManagerLocal->BuildOverscrollHandoffChain(this);
}
// This APZC IsDestroyed(). To avoid callers having to special-case this
// scenario, just build a 1-element chain containing ourselves.
OverscrollHandoffChain* result = new OverscrollHandoffChain;
result->Add(this);
return result;
}
ParentLayerPoint AsyncPanZoomController::AttemptFling(
const FlingHandoffState& aHandoffState) {
// The PLPPI computation acquires the tree lock, so it needs to be performed
// on the controller thread, and before the APZC lock is acquired.
APZThreadUtils::AssertOnControllerThread();
float PLPPI = ComputePLPPI(PanStart(), aHandoffState.mVelocity);
RecursiveMutexAutoLock lock(mRecursiveMutex);
if (!IsPannable()) {
return aHandoffState.mVelocity;
}
// We may have a pre-existing velocity for whatever reason (for example,
// a previously handed off fling). We don't want to clobber that.
APZC_LOG("%p accepting fling with velocity %s\n", this,
ToString(aHandoffState.mVelocity).c_str());
ParentLayerPoint residualVelocity = aHandoffState.mVelocity;
if (mX.CanScroll()) {
mX.SetVelocity(mX.GetVelocity() + aHandoffState.mVelocity.x);
residualVelocity.x = 0;
}
if (mY.CanScroll()) {
mY.SetVelocity(mY.GetVelocity() + aHandoffState.mVelocity.y);
residualVelocity.y = 0;
}
// If we're not scrollable in at least one of the directions in which we
// were handed velocity, don't start a fling animation.
// The |IsFinite()| condition should only fail when running some tests
// that generate events faster than the clock resolution.
ParentLayerPoint velocity = GetVelocityVector();
if (!velocity.IsFinite() ||
velocity.Length() <= StaticPrefs::apz_fling_min_velocity_threshold()) {
// Relieve overscroll now if needed, since we will not transition to a fling
// animation and then an overscroll animation, and relieve it then.
aHandoffState.mChain->SnapBackOverscrolledApzc(this);
return residualVelocity;
}
// If there's a scroll snap point near the predicted fling destination,
// scroll there using a smooth scroll animation. Otherwise, start a
// fling animation.
ScrollSnapToDestination();
if (mState != SMOOTHMSD_SCROLL) {
SetState(FLING);
RefPtr<AsyncPanZoomAnimation> fling =
GetPlatformSpecificState()->CreateFlingAnimation(*this, aHandoffState,
PLPPI);
StartAnimation(fling.forget());
}
return residualVelocity;
}
float AsyncPanZoomController::ComputePLPPI(ParentLayerPoint aPoint,
ParentLayerPoint aDirection) const {
// Avoid division-by-zero.
if (aDirection == ParentLayerPoint()) {
return GetDPI();
}
// Convert |aDirection| into a unit vector.
aDirection = aDirection / aDirection.Length();
// Place the vector at |aPoint| and convert to screen coordinates.
// The length of the resulting vector is the number of Screen coordinates
// that equal 1 ParentLayer coordinate in the given direction.
float screenPerParent = ToScreenCoordinates(aDirection, aPoint).Length();
// Finally, factor in the DPI scale.
return GetDPI() / screenPerParent;
}
Maybe<CSSPoint> AsyncPanZoomController::GetCurrentAnimationDestination(
const RecursiveMutexAutoLock& aProofOfLock) const {
if (mState == WHEEL_SCROLL) {
return Some(mAnimation->AsWheelScrollAnimation()->GetDestination());
}
if (mState == SMOOTH_SCROLL) {
return Some(mAnimation->AsSmoothScrollAnimation()->GetDestination());
}
if (mState == SMOOTHMSD_SCROLL) {
return Some(mAnimation->AsSmoothMsdScrollAnimation()->GetDestination());
}
if (mState == KEYBOARD_SCROLL) {
return Some(mAnimation->AsSmoothScrollAnimation()->GetDestination());
}
return Nothing();
}
ParentLayerPoint
AsyncPanZoomController::AdjustHandoffVelocityForOverscrollBehavior(
ParentLayerPoint& aHandoffVelocity) const {
ParentLayerPoint residualVelocity;
ScrollDirections handoffDirections = GetAllowedHandoffDirections();
if (!handoffDirections.contains(ScrollDirection::eHorizontal)) {
residualVelocity.x = aHandoffVelocity.x;
aHandoffVelocity.x = 0;
}
if (!handoffDirections.contains(ScrollDirection::eVertical)) {
residualVelocity.y = aHandoffVelocity.y;
aHandoffVelocity.y = 0;
}
return residualVelocity;
}
bool AsyncPanZoomController::OverscrollBehaviorAllowsSwipe() const {
// Swipe navigation is a "non-local" overscroll behavior like handoff.
return GetAllowedHandoffDirections().contains(ScrollDirection::eHorizontal);
}
void AsyncPanZoomController::HandleFlingOverscroll(
const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits,
const RefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain,
const RefPtr<const AsyncPanZoomController>& aScrolledApzc) {
APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
if (treeManagerLocal) {
const FlingHandoffState handoffState{
aVelocity, aOverscrollHandoffChain, Nothing(),
0, true /* handoff */, aScrolledApzc};
ParentLayerPoint residualVelocity =
treeManagerLocal->DispatchFling(this, handoffState);
FLING_LOG("APZC %p left with residual velocity %s\n", this,
ToString(residualVelocity).c_str());
if (!IsZero(residualVelocity) && IsPannable() &&
StaticPrefs::apz_overscroll_enabled()) {
// Obey overscroll-behavior.
RecursiveMutexAutoLock lock(mRecursiveMutex);
if (!mX.OverscrollBehaviorAllowsOverscrollEffect()) {
residualVelocity.x = 0;
}
if (!mY.OverscrollBehaviorAllowsOverscrollEffect()) {
residualVelocity.y = 0;
}
// If there is velocity left over from the fling which could not
// be handed off to another other APZC in the handoff chain,
// start an overscroll animation which will enter overscroll
// and then relieve it.
if (!IsZero(residualVelocity)) {
mOverscrollEffect->RelieveOverscroll(residualVelocity,
aOverscrollSideBits);
}
// Additionally snap back any other APZC in the handoff chain
// which may be overscrolled (e.g. an ancestor whose overscroll
// animation may have been interrupted by the input gesture which
// triggered the fling).
aOverscrollHandoffChain->SnapBackOverscrolledApzcForMomentum(
this, residualVelocity);
}
}
}
ParentLayerPoint AsyncPanZoomController::ConvertDestinationToDelta(
CSSPoint& aDestination) const {
ParentLayerPoint startPoint, endPoint;
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
startPoint = aDestination * Metrics().GetZoom();
endPoint = Metrics().GetVisualScrollOffset() * Metrics().GetZoom();
}
return startPoint - endPoint;
}
void AsyncPanZoomController::SmoothScrollTo(
CSSSnapDestination&& aDestination,
ScrollTriggeredByScript aTriggeredByScript, const ScrollOrigin& aOrigin) {
// Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s and then
// to appunits/second.
nsPoint destination = CSSPoint::ToAppUnits(aDestination.mPosition);
nsSize velocity;
if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
velocity = CSSSize::ToAppUnits(ParentLayerSize(mX.GetVelocity() * 1000.0f,
mY.GetVelocity() * 1000.0f) /
Metrics().GetZoom());
}
if (mState == SMOOTH_SCROLL && mAnimation) {
RefPtr<SmoothScrollAnimation> animation(
mAnimation->AsSmoothScrollAnimation());
if (animation->GetScrollOrigin() == aOrigin) {
APZC_LOG("%p updating destination on existing animation\n", this);
animation->UpdateDestinationAndSnapTargets(
GetFrameTime().Time(), destination, velocity,
std::move(aDestination.mTargetIds), aTriggeredByScript);
return;
}
}
CancelAnimation();
// If no scroll is required, we should exit early to avoid triggering
// a scrollend event when no scrolling occurred.
if (ConvertDestinationToDelta(aDestination.mPosition) == ParentLayerPoint()) {
return;
}
SetState(SMOOTH_SCROLL);
nsPoint initialPosition =
CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset());
RefPtr<SmoothScrollAnimation> animation =
new SmoothScrollAnimation(*this, initialPosition, aOrigin);
animation->UpdateDestinationAndSnapTargets(
GetFrameTime().Time(), destination, velocity,
std::move(aDestination.mTargetIds), aTriggeredByScript);
StartAnimation(animation.forget());
}
void AsyncPanZoomController::SmoothMsdScrollTo(
CSSSnapDestination&& aDestination,
ScrollTriggeredByScript aTriggeredByScript) {
if (mState == SMOOTHMSD_SCROLL && mAnimation) {
APZC_LOG("%p updating destination on existing animation\n", this);
RefPtr<SmoothMsdScrollAnimation> animation(
static_cast<SmoothMsdScrollAnimation*>(mAnimation.get()));
animation->SetDestination(aDestination.mPosition,
std::move(aDestination.mTargetIds),
aTriggeredByScript);
return;
}
// If no scroll is required, we should exit early to avoid triggering
// a scrollend event when no scrolling occurred.
if (ConvertDestinationToDelta(aDestination.mPosition) == ParentLayerPoint()) {
return;
}
CancelAnimation();
SetState(SMOOTHMSD_SCROLL);
// Convert velocity from ParentLayerPoints/ms to ParentLayerPoints/s.
CSSPoint initialVelocity;
if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
initialVelocity = ParentLayerPoint(mX.GetVelocity() * 1000.0f,
mY.GetVelocity() * 1000.0f) /
Metrics().GetZoom();
}
StartAnimation(do_AddRef(new SmoothMsdScrollAnimation(
*this, Metrics().GetVisualScrollOffset(), initialVelocity,
aDestination.mPosition,
StaticPrefs::layout_css_scroll_behavior_spring_constant(),
StaticPrefs::layout_css_scroll_behavior_damping_ratio(),
std::move(aDestination.mTargetIds), aTriggeredByScript)));
}
void AsyncPanZoomController::StartOverscrollAnimation(
const ParentLayerPoint& aVelocity, SideBits aOverscrollSideBits) {
MOZ_ASSERT(mState != OVERSCROLL_ANIMATION);
SetState(OVERSCROLL_ANIMATION);
ParentLayerPoint velocity = aVelocity;
AdjustDeltaForAllowedScrollDirections(velocity,
GetOverscrollableDirections());
StartAnimation(
do_AddRef(new OverscrollAnimation(*this, velocity, aOverscrollSideBits)));
}
bool AsyncPanZoomController::CallDispatchScroll(
ParentLayerPoint& aStartPoint, ParentLayerPoint& aEndPoint,
OverscrollHandoffState& aOverscrollHandoffState) {
// Make a local copy of the tree manager pointer and check if it's not
// null before calling DispatchScroll(). This is necessary because
// Destroy(), which nulls out mTreeManager, could be called concurrently.
APZCTreeManager* treeManagerLocal = GetApzcTreeManager();
if (!treeManagerLocal) {
return false;
}
// Obey overscroll-behavior.
ParentLayerPoint endPoint = aEndPoint;
if (aOverscrollHandoffState.mChainIndex > 0) {
ScrollDirections handoffDirections = GetAllowedHandoffDirections();
if (!handoffDirections.contains(ScrollDirection::eHorizontal)) {
endPoint.x = aStartPoint.x;
}
if (!handoffDirections.contains(ScrollDirection::eVertical)) {
endPoint.y = aStartPoint.y;
}
if (aStartPoint == endPoint) {
// Handoff not allowed in either direction - don't even bother.
return false;
}
}
return treeManagerLocal->DispatchScroll(this, aStartPoint, endPoint,
aOverscrollHandoffState);
}
void AsyncPanZoomController::RecordScrollPayload(const TimeStamp& aTimeStamp) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
if (!mScrollPayload) {
mScrollPayload = Some(
CompositionPayload{CompositionPayloadType::eAPZScroll, aTimeStamp});
}
}
void AsyncPanZoomController::StartTouch(const ParentLayerPoint& aPoint,
TimeStamp aTimestamp) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
mX.StartTouch(aPoint.x, aTimestamp);
mY.StartTouch(aPoint.y, aTimestamp);
}
void AsyncPanZoomController::EndTouch(TimeStamp aTimestamp,
Axis::ClearAxisLock aClearAxisLock) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
mX.EndTouch(aTimestamp, aClearAxisLock);
mY.EndTouch(aTimestamp, aClearAxisLock);
}
void AsyncPanZoomController::TrackTouch(const MultiTouchInput& aEvent) {
mTouchScrollEventBuffer.push(aEvent);
ExternalPoint extPoint = GetFirstExternalTouchPoint(aEvent);
ExternalPoint refPoint;
if (mTouchScrollEventBuffer.size() > 1) {
refPoint = GetFirstExternalTouchPoint(mTouchScrollEventBuffer.front());
} else {
refPoint = mStartTouch;
}
ScreenPoint panVector = ViewAs<ScreenPixel>(
extPoint - refPoint, PixelCastJustification::ExternalIsScreen);
HandlePanningUpdate(panVector);
ParentLayerPoint prevTouchPoint(mX.GetPos(), mY.GetPos());
ParentLayerPoint touchPoint = GetFirstTouchPoint(aEvent);
UpdateWithTouchAtDevicePoint(aEvent);
auto velocity = GetVelocityVector().Length();
if (mMinimumVelocityDuringPan) {
mMinimumVelocityDuringPan =
Some(std::min(*mMinimumVelocityDuringPan, velocity));
} else {
mMinimumVelocityDuringPan = Some(velocity);
}
if (prevTouchPoint != touchPoint) {
MOZ_ASSERT(GetCurrentTouchBlock());
OverscrollHandoffState handoffState(
*GetCurrentTouchBlock()->GetOverscrollHandoffChain(),
PanVector(extPoint), ScrollSource::Touchscreen);
RecordScrollPayload(aEvent.mTimeStamp);
CallDispatchScroll(prevTouchPoint, touchPoint, handoffState);
}
}
ParentLayerPoint AsyncPanZoomController::GetFirstTouchPoint(
const MultiTouchInput& aEvent) {
return ((SingleTouchData&)aEvent.mTouches[0]).mLocalScreenPoint;
}
ExternalPoint AsyncPanZoomController::GetFirstExternalTouchPoint(
const MultiTouchInput& aEvent) {
return ToExternalPoint(aEvent.mScreenOffset,
((SingleTouchData&)aEvent.mTouches[0]).mScreenPoint);
}
ParentLayerPoint AsyncPanZoomController::GetOverscrollAmount() const {
if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
return GetOverscrollAmountInternal();
}
RecursiveMutexAutoLock lock(mRecursiveMutex);
return GetOverscrollAmountInternal();
}
ParentLayerPoint AsyncPanZoomController::GetOverscrollAmountInternal() const {
return {mX.GetOverscroll(), mY.GetOverscroll()};
}
SideBits AsyncPanZoomController::GetOverscrollSideBits() const {
return apz::GetOverscrollSideBits({mX.GetOverscroll(), mY.GetOverscroll()});
}
void AsyncPanZoomController::RestoreOverscrollAmount(
const ParentLayerPoint& aOverscroll) {
mX.RestoreOverscroll(aOverscroll.x);
mY.RestoreOverscroll(aOverscroll.y);
}
void AsyncPanZoomController::StartAnimation(
already_AddRefed<AsyncPanZoomAnimation> aAnimation) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
mAnimation = aAnimation;
mLastSampleTime = GetFrameTime();
ScheduleComposite();
}
void AsyncPanZoomController::CancelAnimation(CancelAnimationFlags aFlags) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
APZC_LOG_DETAIL("running CancelAnimation(0x%x) in state %s\n", this, aFlags,
ToString(mState).c_str());
if ((aFlags & ExcludeWheel) && mState == WHEEL_SCROLL) {
return;
}
if (mAnimation) {
mAnimation->Cancel(aFlags);
}
SetState(NOTHING);
mLastSnapTargetIds = ScrollSnapTargetIds{};
mAnimation = nullptr;
// Since there is no animation in progress now the axes should
// have no velocity either. If we are dropping the velocity from a non-zero
// value we should trigger a repaint as the displayport margins are dependent
// on the velocity and the last repaint request might not have good margins
// any more.
bool repaint = !IsZero(GetVelocityVector());
mX.SetVelocity(0);
mY.SetVelocity(0);
mX.SetAxisLocked(false);
mY.SetAxisLocked(false);
// Setting the state to nothing and cancelling the animation can
// preempt normal mechanisms for relieving overscroll, so we need to clear
// overscroll here.
if (!(aFlags & ExcludeOverscroll) && IsOverscrolled()) {
ClearOverscroll();
repaint = true;
}
// Similar to relieving overscroll, we also need to snap to any snap points
// if appropriate.
if (aFlags & CancelAnimationFlags::ScrollSnap) {
ScrollSnap(ScrollSnapFlags::IntendedEndPosition);
}
if (repaint) {
RequestContentRepaint();
ScheduleComposite();
}
}
void AsyncPanZoomController::ClearOverscroll() {
mOverscrollEffect->ClearOverscroll();
}
void AsyncPanZoomController::ClearPhysicalOverscroll() {
RecursiveMutexAutoLock lock(mRecursiveMutex);
mX.ClearOverscroll();
mY.ClearOverscroll();
}
void AsyncPanZoomController::SetCompositorController(
CompositorController* aCompositorController) {
mCompositorController = aCompositorController;
}
void AsyncPanZoomController::SetVisualScrollOffset(const CSSPoint& aOffset) {
Metrics().SetVisualScrollOffset(aOffset);
Metrics().RecalculateLayoutViewportOffset();
}
void AsyncPanZoomController::ClampAndSetVisualScrollOffset(
const CSSPoint& aOffset) {
Metrics().ClampAndSetVisualScrollOffset(aOffset);
Metrics().RecalculateLayoutViewportOffset();
}
void AsyncPanZoomController::ScrollBy(const CSSPoint& aOffset) {
SetVisualScrollOffset(Metrics().GetVisualScrollOffset() + aOffset);
}
void AsyncPanZoomController::ScrollByAndClamp(const CSSPoint& aOffset) {
ClampAndSetVisualScrollOffset(Metrics().GetVisualScrollOffset() + aOffset);
}
void AsyncPanZoomController::ScaleWithFocus(float aScale,
const CSSPoint& aFocus) {
Metrics().ZoomBy(aScale);
// We want to adjust the scroll offset such that the CSS point represented by
// aFocus remains at the same position on the screen before and after the
// change in zoom. The below code accomplishes this; see
// explanation of how.
SetVisualScrollOffset((Metrics().GetVisualScrollOffset() + aFocus) -
(aFocus / aScale));
}
/*static*/
gfx::IntSize AsyncPanZoomController::GetDisplayportAlignmentMultiplier(
const ScreenSize& aBaseSize) {
gfx::IntSize multiplier(1, 1);
float baseWidth = aBaseSize.width;
while (baseWidth > 500) {
baseWidth /= 2;
multiplier.width *= 2;
if (multiplier.width >= 8) {
break;
}
}
float baseHeight = aBaseSize.height;
while (baseHeight > 500) {
baseHeight /= 2;
multiplier.height *= 2;
if (multiplier.height >= 8) {
break;
}
}
return multiplier;
}
/**
* Enlarges the displayport along both axes based on the velocity.
*/
static CSSSize CalculateDisplayPortSize(
const CSSSize& aCompositionSize, const CSSPoint& aVelocity,
AsyncPanZoomController::ZoomInProgress aZoomInProgress,
const CSSToScreenScale2D& aDpPerCSS) {
bool xIsStationarySpeed =
fabsf(aVelocity.x) < StaticPrefs::apz_min_skate_speed();
bool yIsStationarySpeed =
fabsf(aVelocity.y) < StaticPrefs::apz_min_skate_speed();
float xMultiplier = xIsStationarySpeed
? StaticPrefs::apz_x_stationary_size_multiplier()
: StaticPrefs::apz_x_skate_size_multiplier();
float yMultiplier = yIsStationarySpeed
? StaticPrefs::apz_y_stationary_size_multiplier()
: StaticPrefs::apz_y_skate_size_multiplier();
if (IsHighMemSystem() && !xIsStationarySpeed) {
xMultiplier += StaticPrefs::apz_x_skate_highmem_adjust();
}
if (IsHighMemSystem() && !yIsStationarySpeed) {
yMultiplier += StaticPrefs::apz_y_skate_highmem_adjust();
}
if (aZoomInProgress == AsyncPanZoomController::ZoomInProgress::Yes) {
// If a zoom is in progress, we will be making content visible on the
// x and y axes in equal proportion, because the zoom operation scales
// equally on the x and y axes. The default multipliers computed above are
// biased towards the y-axis since that's where most scrolling occurs, but
// in the case of zooming, we should really use equal multipliers on both
// axes. This does that while preserving the total displayport area
// quantity (aCompositionSize.Area() * xMultiplier * yMultiplier).
// Note that normally changing the shape of the displayport is expensive
// and should be avoided, but if a zoom is in progress the displayport
// is likely going to be fully repainted anyway due to changes in resolution
// so there should be no marginal cost to also changing the shape of it.
float areaMultiplier = xMultiplier * yMultiplier;
xMultiplier = sqrt(areaMultiplier);
yMultiplier = xMultiplier;
}
// Scale down the margin multipliers by the alignment multiplier because
// the alignment code will expand the displayport outward to the multiplied
// alignment. This is not necessary for correctness, but for performance;
// if we don't do this the displayport can end up much larger. The math here
// is actually just scaling the part of the multipler that is > 1, so that
// we never end up with xMultiplier or yMultiplier being less than 1 (that
// would result in a guaranteed checkerboarding situation). Note that the
// calculation doesn't cancel exactly the increased margin from applying
// the alignment multiplier, but this is simple and should provide
// reasonable behaviour in most cases.
gfx::IntSize alignmentMultipler =
AsyncPanZoomController::GetDisplayportAlignmentMultiplier(
aCompositionSize * aDpPerCSS);
if (xMultiplier > 1) {
xMultiplier = ((xMultiplier - 1) / alignmentMultipler.width) + 1;
}
if (yMultiplier > 1) {
yMultiplier = ((yMultiplier - 1) / alignmentMultipler.height) + 1;
}
return aCompositionSize * CSSSize(xMultiplier, yMultiplier);
}
/**
* Ensures that the displayport is at least as large as the visible area
* inflated by the danger zone. If this is not the case then the
* "AboutToCheckerboard" function in TiledContentClient.cpp will return true
* even in the stable state.
*/
static CSSSize ExpandDisplayPortToDangerZone(
const CSSSize& aDisplayPortSize, const FrameMetrics& aFrameMetrics) {
CSSSize dangerZone(0.0f, 0.0f);
if (aFrameMetrics.DisplayportPixelsPerCSSPixel().xScale != 0 &&
aFrameMetrics.DisplayportPixelsPerCSSPixel().yScale != 0) {
dangerZone = ScreenSize(StaticPrefs::apz_danger_zone_x(),
StaticPrefs::apz_danger_zone_y()) /
aFrameMetrics.DisplayportPixelsPerCSSPixel();
}
const CSSSize compositionSize =
aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels();
const float xSize = std::max(aDisplayPortSize.width,
compositionSize.width + (2 * dangerZone.width));
const float ySize =
std::max(aDisplayPortSize.height,
compositionSize.height + (2 * dangerZone.height));
return CSSSize(xSize, ySize);
}
/**
* Attempts to redistribute any area in the displayport that would get clipped
* by the scrollable rect, or be inaccessible due to disabled scrolling, to the
* other axis, while maintaining total displayport area.
*/
static void RedistributeDisplayPortExcess(CSSSize& aDisplayPortSize,
const CSSRect& aScrollableRect) {
// As aDisplayPortSize.height * aDisplayPortSize.width does not change,
// we are just scaling by the ratio and its inverse.
if (aDisplayPortSize.height > aScrollableRect.Height()) {
aDisplayPortSize.width *=
(aDisplayPortSize.height / aScrollableRect.Height());
aDisplayPortSize.height = aScrollableRect.Height();
} else if (aDisplayPortSize.width > aScrollableRect.Width()) {
aDisplayPortSize.height *=
(aDisplayPortSize.width / aScrollableRect.Width());
aDisplayPortSize.width = aScrollableRect.Width();
}
}
/* static */
const ScreenMargin AsyncPanZoomController::CalculatePendingDisplayPort(
const FrameMetrics& aFrameMetrics, const ParentLayerPoint& aVelocity,
ZoomInProgress aZoomInProgress) {
if (aFrameMetrics.IsScrollInfoLayer()) {
// Don't compute margins. Since we can't asynchronously scroll this frame,
// we don't want to paint anything more than the composition bounds.
return ScreenMargin();
}
CSSSize compositionSize =
aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels();
CSSPoint velocity;
if (aFrameMetrics.GetZoom() != CSSToParentLayerScale(0)) {
velocity = aVelocity / aFrameMetrics.GetZoom(); // avoid division by zero
}
CSSRect scrollableRect = aFrameMetrics.GetExpandedScrollableRect();
// Calculate the displayport size based on how fast we're moving along each
// axis.
CSSSize displayPortSize =
CalculateDisplayPortSize(compositionSize, velocity, aZoomInProgress,
aFrameMetrics.DisplayportPixelsPerCSSPixel());
displayPortSize =
ExpandDisplayPortToDangerZone(displayPortSize, aFrameMetrics);
if (StaticPrefs::apz_enlarge_displayport_when_clipped()) {
RedistributeDisplayPortExcess(displayPortSize, scrollableRect);
}
// We calculate a "displayport" here which is relative to the scroll offset.
// Note that the scroll offset we have here in the APZ code may not be the
// same as the base rect that gets used on the layout side when the
// displayport margins are actually applied, so it is important to only
// consider the displayport as margins relative to a scroll offset rather than
// relative to something more unchanging like the scrollable rect origin.
// Center the displayport based on its expansion over the composition size.
CSSRect displayPort((compositionSize.width - displayPortSize.width) / 2.0f,
(compositionSize.height - displayPortSize.height) / 2.0f,
displayPortSize.width, displayPortSize.height);
// Offset the displayport, depending on how fast we're moving and the
// estimated time it takes to paint, to try to minimise checkerboarding.
float paintFactor = kDefaultEstimatedPaintDurationMs;
displayPort.MoveBy(velocity * paintFactor * StaticPrefs::apz_velocity_bias());
APZC_LOGV_FM(aFrameMetrics,
"Calculated displayport as %s from velocity %s zooming %d paint "
"time %f metrics",
ToString(displayPort).c_str(), ToString(aVelocity).c_str(),
(int)aZoomInProgress, paintFactor);
CSSMargin cssMargins;
cssMargins.left = -displayPort.X();
cssMargins.top = -displayPort.Y();
cssMargins.right =
displayPort.Width() - compositionSize.width - cssMargins.left;
cssMargins.bottom =
displayPort.Height() - compositionSize.height - cssMargins.top;
return cssMargins * aFrameMetrics.DisplayportPixelsPerCSSPixel();
}
void AsyncPanZoomController::ScheduleComposite() {
if (mCompositorController) {
mCompositorController->ScheduleRenderOnCompositorThread(
wr::RenderReasons::APZ);
}
}
void AsyncPanZoomController::ScheduleCompositeAndMaybeRepaint() {
ScheduleComposite();
RequestContentRepaint();
}
void AsyncPanZoomController::FlushRepaintForOverscrollHandoff() {
RecursiveMutexAutoLock lock(mRecursiveMutex);
RequestContentRepaint();
}
void AsyncPanZoomController::FlushRepaintForNewInputBlock() {
APZC_LOG("%p flushing repaint for new input block\n", this);
RecursiveMutexAutoLock lock(mRecursiveMutex);
RequestContentRepaint();
}
bool AsyncPanZoomController::SnapBackIfOverscrolled() {
RecursiveMutexAutoLock lock(mRecursiveMutex);
if (SnapBackIfOverscrolledForMomentum(ParentLayerPoint(0, 0))) {
return true;
}
// If we don't kick off an overscroll animation, we still need to snap to any
// nearby snap points, assuming we haven't already done so when we started
// this fling
if (mState != FLING) {
ScrollSnap(ScrollSnapFlags::IntendedEndPosition);
}
return false;
}
bool AsyncPanZoomController::SnapBackIfOverscrolledForMomentum(
const ParentLayerPoint& aVelocity) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
// It's possible that we're already in the middle of an overscroll
// animation - if so, don't start a new one.
if (IsOverscrolled() && mState != OVERSCROLL_ANIMATION) {
APZC_LOG("%p is overscrolled, starting snap-back\n", this);
mOverscrollEffect->RelieveOverscroll(aVelocity, GetOverscrollSideBits());
return true;
}
return false;
}
bool AsyncPanZoomController::IsFlingingFast() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
if (mState == FLING && GetVelocityVector().Length() >
StaticPrefs::apz_fling_stop_on_tap_threshold()) {
APZC_LOG("%p is moving fast\n", this);
return true;
}
return false;
}
bool AsyncPanZoomController::IsPannable() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return mX.CanScroll() || mY.CanScroll();
}
bool AsyncPanZoomController::IsScrollInfoLayer() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return Metrics().IsScrollInfoLayer();
}
int32_t AsyncPanZoomController::GetLastTouchIdentifier() const {
RefPtr<GestureEventListener> listener = GetGestureEventListener();
return listener ? listener->GetLastTouchIdentifier() : -1;
}
void AsyncPanZoomController::RequestContentRepaint(
RepaintUpdateType aUpdateType) {
// Reinvoke this method on the repaint thread if it's not there already. It's
// important to do this before the call to CalculatePendingDisplayPort, so
// that CalculatePendingDisplayPort uses the most recent available version of
// Metrics(). just before the paint request is dispatched to content.
RefPtr<GeckoContentController> controller = GetGeckoContentController();
if (!controller) {
return;
}
if (!controller->IsRepaintThread()) {
// Even though we want to do the actual repaint request on the repaint
// thread, we want to update the expected gecko metrics synchronously.
// Otherwise we introduce a race condition where we might read from the
// expected gecko metrics on the controller thread before or after it gets
// updated on the repaint thread, when in fact we always want the updated
// version when reading.
{ // scope lock
RecursiveMutexAutoLock lock(mRecursiveMutex);
mExpectedGeckoMetrics.UpdateFrom(Metrics());
}
// use the local variable to resolve the function overload.
auto func =
static_cast<void (AsyncPanZoomController::*)(RepaintUpdateType)>(
&AsyncPanZoomController::RequestContentRepaint);
controller->DispatchToRepaintThread(NewRunnableMethod<RepaintUpdateType>(
"layers::AsyncPanZoomController::RequestContentRepaint", this, func,
aUpdateType));
return;
}
MOZ_ASSERT(controller->IsRepaintThread());
RecursiveMutexAutoLock lock(mRecursiveMutex);
ParentLayerPoint velocity = GetVelocityVector();
ScreenMargin displayportMargins = CalculatePendingDisplayPort(
Metrics(), velocity,
(mState == PINCHING || mState == ANIMATING_ZOOM) ? ZoomInProgress::Yes
: ZoomInProgress::No);
Metrics().SetPaintRequestTime(TimeStamp::Now());
RequestContentRepaint(velocity, displayportMargins, aUpdateType);
}
static CSSRect GetDisplayPortRect(const FrameMetrics& aFrameMetrics,
const ScreenMargin& aDisplayportMargins) {
// This computation is based on what happens in CalculatePendingDisplayPort.
// If that changes then this might need to change too.
// Note that the display port rect APZ computes is relative to the visual
// scroll offset. It's adjusted to be relative to the layout scroll offset
// when the main thread processes a repaint request (in
// APZCCallbackHelper::AdjustDisplayPortForScrollDelta()) and ultimately
// applied (in DisplayPortUtils::GetDisplayPort()) in this adjusted form.
CSSRect baseRect(aFrameMetrics.GetVisualScrollOffset(),
aFrameMetrics.CalculateBoundedCompositedSizeInCssPixels());
baseRect.Inflate(aDisplayportMargins /
aFrameMetrics.DisplayportPixelsPerCSSPixel());
return baseRect;
}
void AsyncPanZoomController::RequestContentRepaint(
const ParentLayerPoint& aVelocity, const ScreenMargin& aDisplayportMargins,
RepaintUpdateType aUpdateType) {
mRecursiveMutex.AssertCurrentThreadIn();
RefPtr<GeckoContentController> controller = GetGeckoContentController();
if (!controller) {
return;
}
MOZ_ASSERT(controller->IsRepaintThread());
APZScrollAnimationType animationType = APZScrollAnimationType::No;
if (mAnimation) {
animationType = mAnimation->WasTriggeredByScript()
? APZScrollAnimationType::TriggeredByScript
: APZScrollAnimationType::TriggeredByUserInput;
}
RepaintRequest request(Metrics(), aDisplayportMargins, aUpdateType,
animationType, mScrollGeneration, mLastSnapTargetIds,
IsInScrollingGesture());
if (request.IsRootContent() && request.GetZoom() != mLastNotifiedZoom &&
mState != PINCHING && mState != ANIMATING_ZOOM) {
controller->NotifyScaleGestureComplete(
GetGuid(),
(request.GetZoom() / request.GetDevPixelsPerCSSPixel()).scale);
mLastNotifiedZoom = request.GetZoom();
}
// If we're trying to paint what we already think is painted, discard this
// request since it's a pointless paint.
if (request.GetDisplayPortMargins().WithinEpsilonOf(
mLastPaintRequestMetrics.GetDisplayPortMargins(), EPSILON) &&
request.GetVisualScrollOffset().WithinEpsilonOf(
mLastPaintRequestMetrics.GetVisualScrollOffset(), EPSILON) &&
request.GetPresShellResolution() ==
mLastPaintRequestMetrics.GetPresShellResolution() &&
request.GetZoom() == mLastPaintRequestMetrics.GetZoom() &&
request.GetLayoutViewport().WithinEpsilonOf(
mLastPaintRequestMetrics.GetLayoutViewport(), EPSILON) &&
request.GetScrollGeneration() ==
mLastPaintRequestMetrics.GetScrollGeneration() &&
request.GetScrollUpdateType() ==
mLastPaintRequestMetrics.GetScrollUpdateType() &&
request.GetScrollAnimationType() ==
mLastPaintRequestMetrics.GetScrollAnimationType() &&
request.GetLastSnapTargetIds() ==
mLastPaintRequestMetrics.GetLastSnapTargetIds()) {
return;
}
APZC_LOGV("%p requesting content repaint %s", this,
ToString(request).c_str());
{ // scope lock
MutexAutoLock lock(mCheckerboardEventLock);
if (mCheckerboardEvent && mCheckerboardEvent->IsRecordingTrace()) {
std::stringstream info;
info << " velocity " << aVelocity;
std::string str = info.str();
mCheckerboardEvent->UpdateRendertraceProperty(
CheckerboardEvent::RequestedDisplayPort,
GetDisplayPortRect(Metrics(), aDisplayportMargins), str);
}
}
controller->RequestContentRepaint(request);
mExpectedGeckoMetrics.UpdateFrom(Metrics());
mLastPaintRequestMetrics = request;
// We're holding the APZC lock here, so redispatch this so we can get
// the tree lock without the APZC lock.
controller->DispatchToRepaintThread(
NewRunnableMethod<AsyncPanZoomController*>(
"layers::APZCTreeManager::SendSubtreeTransformsToChromeMainThread",
GetApzcTreeManager(),
&APZCTreeManager::SendSubtreeTransformsToChromeMainThread, this));
}
bool AsyncPanZoomController::UpdateAnimation(
const RecursiveMutexAutoLock& aProofOfLock, const SampleTime& aSampleTime,
nsTArray<RefPtr<Runnable>>* aOutDeferredTasks) {
AssertOnSamplerThread();
// This function may get called multiple with the same sample time, if we
// composite multiple times at the same timestamp.
// However we only want to do one animation step per composition so we need
// to deduplicate these calls first.
// Even if there's no animation, if we have a scroll offset change pending due
// to the frame delay, we need to keep compositing.
if (mLastSampleTime == aSampleTime) {
APZC_LOGV_DETAIL(
"UpdateAnimation short-circuit, animation=%p, pending frame-delayed "
"offset=%d\n",
this, mAnimation.get(), HavePendingFrameDelayedOffset());
return !!mAnimation || HavePendingFrameDelayedOffset();
}
// We're at a new timestamp, so advance to the next sample in the deque, if
// there is one. That one will be used for all the code that reads the
// eForCompositing transforms in this vsync interval.
AdvanceToNextSample();
// And then create a new sample, which will be used in the *next* vsync
// interval. We do the sample at this point and not later in order to try
// and enforce one frame delay between computing the async transform and
// compositing it to the screen. This one-frame delay gives code running on
// the main thread a chance to try and respond to the scroll position change,
// so that e.g. a main-thread animation can stay in sync with user-driven
// scrolling or a compositor animation.
bool needComposite = SampleCompositedAsyncTransform(aProofOfLock);
APZC_LOGV_DETAIL("UpdateAnimation needComposite=%d mAnimation=%p\n", this,
needComposite, mAnimation.get());
TimeDuration sampleTimeDelta = aSampleTime - mLastSampleTime;
mLastSampleTime = aSampleTime;
if (needComposite || mAnimation) {
// Bump the scroll generation before we call RequestContentRepaint below
// so that the RequestContentRepaint call will surely use the new
// generation.
if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
mScrollGeneration = treeManagerLocal->NewAPZScrollGeneration();
}
}
if (mAnimation) {
bool continueAnimation = mAnimation->Sample(Metrics(), sampleTimeDelta);
bool wantsRepaints = mAnimation->WantsRepaints();
*aOutDeferredTasks = mAnimation->TakeDeferredTasks();
if (!continueAnimation) {
SetState(NOTHING);
if (mAnimation->AsSmoothMsdScrollAnimation()) {
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
mLastSnapTargetIds =
mAnimation->AsSmoothMsdScrollAnimation()->TakeSnapTargetIds();
}
} else if (mAnimation->AsSmoothScrollAnimation()) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
mLastSnapTargetIds =
mAnimation->AsSmoothScrollAnimation()->TakeSnapTargetIds();
}
mAnimation = nullptr;
}
// Request a repaint at the end of the animation in case something such as a
// call to NotifyLayersUpdated was invoked during the animation and Gecko's
// current state is some intermediate point of the animation.
if (!continueAnimation || wantsRepaints) {
RequestContentRepaint();
}
needComposite = true;
}
return needComposite;
}
AsyncTransformComponentMatrix AsyncPanZoomController::GetOverscrollTransform(
AsyncTransformConsumer aMode) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
if (aMode == eForCompositing && mScrollMetadata.IsApzForceDisabled()) {
return AsyncTransformComponentMatrix();
}
if (!IsPhysicallyOverscrolled()) {
return AsyncTransformComponentMatrix();
}
// The overscroll effect is a simple translation by the overscroll offset.
ParentLayerPoint overscrollOffset(-mX.GetOverscroll(), -mY.GetOverscroll());
return AsyncTransformComponentMatrix().PostTranslate(overscrollOffset.x,
overscrollOffset.y, 0);
}
bool AsyncPanZoomController::AdvanceAnimations(const SampleTime& aSampleTime) {
AssertOnSamplerThread();
// Don't send any state-change notifications until the end of the function,
// because we may go through some intermediate states while we finish
// animations and start new ones.
StateChangeNotificationBlocker blocker(this);
// The eventual return value of this function. The compositor needs to know
// whether or not to advance by a frame as soon as it can. For example, if a
// fling is happening, it has to keep compositing so that the animation is
// smooth. If an animation frame is requested, it is the compositor's
// responsibility to schedule a composite.
bool requestAnimationFrame = false;
nsTArray<RefPtr<Runnable>> deferredTasks;
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
{ // scope lock
CSSRect visibleRect = GetVisibleRect(lock);
MutexAutoLock lock2(mCheckerboardEventLock);
// Update RendertraceProperty before UpdateAnimation() call, since
// the UpdateAnimation() updates effective ScrollOffset for next frame
// if APZFrameDelay is enabled.
if (mCheckerboardEvent) {
mCheckerboardEvent->UpdateRendertraceProperty(
CheckerboardEvent::UserVisible, visibleRect);
}
}
requestAnimationFrame = UpdateAnimation(lock, aSampleTime, &deferredTasks);
}
// Execute any deferred tasks queued up by mAnimation's Sample() (called by
// UpdateAnimation()). This needs to be done after the monitor is released
// since the tasks are allowed to call APZCTreeManager methods which can grab
// the tree lock.
// Move the StateChangeNotificationBlocker into the task so that notifications
// continue to be blocked until the deferred tasks have run.
// Additionally store a RefPtr(this) in the lambda since
// StateChangeNotificationBlocker keeps a raw pointer to the APZC.
if (!deferredTasks.IsEmpty()) {
APZThreadUtils::RunOnControllerThread(NS_NewRunnableFunction(
"AsyncPanZoomController::AdvanceAnimations deferred tasks",
[keepApzcAlive = RefPtr(this), blocker = std::move(blocker),
deferredTasks = std::move(deferredTasks)]() {
for (uint32_t i = 0; i < deferredTasks.Length(); ++i) {
deferredTasks[i]->Run();
}
}));
}
// If any of the deferred tasks starts a new animation, it will request a
// new composite directly, so we can just return requestAnimationFrame here.
return requestAnimationFrame;
}
ParentLayerPoint AsyncPanZoomController::GetCurrentAsyncScrollOffset(
AsyncTransformConsumer aMode) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
return GetEffectiveScrollOffset(aMode, lock) * GetEffectiveZoom(aMode, lock);
}
CSSRect AsyncPanZoomController::GetCurrentAsyncVisualViewport(
AsyncTransformConsumer aMode) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
return CSSRect(
GetEffectiveScrollOffset(aMode, lock),
FrameMetrics::CalculateCompositedSizeInCssPixels(
Metrics().GetCompositionBounds(), GetEffectiveZoom(aMode, lock)));
}
AsyncTransform AsyncPanZoomController::GetCurrentAsyncTransform(
AsyncTransformConsumer aMode, AsyncTransformComponents aComponents,
std::size_t aSampleIndex) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
CSSToParentLayerScale effectiveZoom;
if (aComponents.contains(AsyncTransformComponent::eVisual)) {
effectiveZoom = GetEffectiveZoom(aMode, lock, aSampleIndex);
} else {
effectiveZoom =
Metrics().LayersPixelsPerCSSPixel() * LayerToParentLayerScale(1.0f);
}
LayerToParentLayerScale compositedAsyncZoom =
effectiveZoom / Metrics().LayersPixelsPerCSSPixel();
ParentLayerPoint translation;
if (aComponents.contains(AsyncTransformComponent::eVisual)) {
// There is no "lastPaintVisualOffset" to subtract here; the visual offset
// is entirely async.
CSSPoint currentVisualOffset =
GetEffectiveScrollOffset(aMode, lock, aSampleIndex) -
GetEffectiveLayoutViewport(aMode, lock, aSampleIndex).TopLeft();
translation += currentVisualOffset * effectiveZoom;
}
if (aComponents.contains(AsyncTransformComponent::eLayout)) {
CSSPoint lastPaintLayoutOffset;
if (mLastContentPaintMetrics.IsScrollable()) {
lastPaintLayoutOffset = mLastContentPaintMetrics.GetLayoutScrollOffset();
}
CSSPoint currentLayoutOffset =
GetEffectiveLayoutViewport(aMode, lock, aSampleIndex).TopLeft();
translation +=
(currentLayoutOffset - lastPaintLayoutOffset) * effectiveZoom;
}
return AsyncTransform(compositedAsyncZoom, -translation);
}
AsyncTransformComponentMatrix
AsyncPanZoomController::GetAsyncTransformForInputTransformation(
AsyncTransformComponents aComponents, LayersId aForLayersId) const {
AsyncTransformComponentMatrix result;
// If we are the root, and |aForLayersId| is different from our LayersId,
// |aForLayersId| must be in a remote subdocument.
if (IsRootContent() && aForLayersId != GetLayersId()) {
result =
ViewAs<AsyncTransformComponentMatrix>(GetPaintedResolutionTransform());
}
// Order of transforms: the painted resolution (if any) applies first, and
// any async transform on top of that.
result = result * AsyncTransformComponentMatrix(GetCurrentAsyncTransform(
eForEventHandling, aComponents));
// The overscroll transform is considered part of the layout component of
// the async transform, because it should not apply to fixed content.
if (aComponents.contains(AsyncTransformComponent::eLayout)) {
result = result * GetOverscrollTransform(eForEventHandling);
}
return result;
}
Matrix4x4 AsyncPanZoomController::GetPaintedResolutionTransform() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
MOZ_ASSERT(IsRootContent());
float resolution = mLastContentPaintMetrics.GetPresShellResolution();
return Matrix4x4::Scaling(resolution, resolution, 1.f);
}
LayoutDeviceToParentLayerScale AsyncPanZoomController::GetCurrentPinchZoomScale(
AsyncTransformConsumer aMode) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
AutoApplyAsyncTestAttributes testAttributeApplier(this, lock);
CSSToParentLayerScale scale = GetEffectiveZoom(aMode, lock);
return scale / Metrics().GetDevPixelsPerCSSPixel();
}
AutoTArray<wr::SampledScrollOffset, 2>
AsyncPanZoomController::GetSampledScrollOffsets() const {
AssertOnSamplerThread();
RecursiveMutexAutoLock lock(mRecursiveMutex);
const AsyncTransformComponents asyncTransformComponents =
GetZoomAnimationId()
? AsyncTransformComponents{AsyncTransformComponent::eLayout}
: LayoutAndVisual;
// If layerTranslation includes only the layout component of the async
// transform then it has not been scaled by the async zoom, so we want to
// divide it by the resolution. If layerTranslation includes the visual
// component, then we should use the pinch zoom scale, which includes the
// async zoom. However, we only use LayoutAndVisual for non-zoomable APZCs,
// so it makes no difference.
LayoutDeviceToParentLayerScale resolution =
GetCumulativeResolution() * LayerToParentLayerScale(1.0f);
AutoTArray<wr::SampledScrollOffset, 2> sampledOffsets;
for (std::deque<SampledAPZCState>::size_type index = 0;
index < mSampledState.size(); index++) {
ParentLayerPoint layerTranslation =
GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing,
asyncTransformComponents, index)
.mTranslation;
// Include the overscroll transform here in scroll offsets transform
// to ensure that we do not overscroll fixed content.
layerTranslation =
GetOverscrollTransform(AsyncPanZoomController::eForCompositing)
.TransformPoint(layerTranslation);
// The positive translation means the painted content is supposed to
// move down (or to the right), and that corresponds to a reduction in
// the scroll offset. Since we are effectively giving WR the async
// scroll delta here, we want to negate the translation.
LayoutDevicePoint asyncScrollDelta = -layerTranslation / resolution;
sampledOffsets.AppendElement(wr::SampledScrollOffset{
wr::ToLayoutVector2D(asyncScrollDelta),
wr::ToWrAPZScrollGeneration(mSampledState[index].Generation())});
}
return sampledOffsets;
}
bool AsyncPanZoomController::SuppressAsyncScrollOffset() const {
return mScrollMetadata.IsApzForceDisabled() ||
(Metrics().IsMinimalDisplayPort() &&
StaticPrefs::apz_prefer_jank_minimal_displayports());
}
CSSRect AsyncPanZoomController::GetEffectiveLayoutViewport(
AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
std::size_t aSampleIndex) const {
if (aMode == eForCompositing && SuppressAsyncScrollOffset()) {
return mLastContentPaintMetrics.GetLayoutViewport();
}
if (aMode == eForCompositing) {
return mSampledState[aSampleIndex].GetLayoutViewport();
}
return Metrics().GetLayoutViewport();
}
CSSPoint AsyncPanZoomController::GetEffectiveScrollOffset(
AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
std::size_t aSampleIndex) const {
if (aMode == eForCompositing && SuppressAsyncScrollOffset()) {
return mLastContentPaintMetrics.GetVisualScrollOffset();
}
if (aMode == eForCompositing) {
return mSampledState[aSampleIndex].GetVisualScrollOffset();
}
return Metrics().GetVisualScrollOffset();
}
CSSToParentLayerScale AsyncPanZoomController::GetEffectiveZoom(
AsyncTransformConsumer aMode, const RecursiveMutexAutoLock& aProofOfLock,
std::size_t aSampleIndex) const {
if (aMode == eForCompositing && SuppressAsyncScrollOffset()) {
return mLastContentPaintMetrics.GetZoom();
}
if (aMode == eForCompositing) {
return mSampledState[aSampleIndex].GetZoom();
}
return Metrics().GetZoom();
}
void AsyncPanZoomController::AdvanceToNextSample() {
AssertOnSamplerThread();
RecursiveMutexAutoLock lock(mRecursiveMutex);
// Always keep at least one state in mSampledState.
if (mSampledState.size() > 1) {
mSampledState.pop_front();
}
}
bool AsyncPanZoomController::HavePendingFrameDelayedOffset() const {
AssertOnSamplerThread();
RecursiveMutexAutoLock lock(mRecursiveMutex);
const bool nextFrameWillChange =
mSampledState.size() >= 2 && mSampledState[0] != mSampledState[1];
const bool frameAfterThatWillChange =
mSampledState.back() != SampledAPZCState(Metrics());
return nextFrameWillChange || frameAfterThatWillChange;
}
bool AsyncPanZoomController::SampleCompositedAsyncTransform(
const RecursiveMutexAutoLock& aProofOfLock) {
MOZ_ASSERT(mSampledState.size() <= 2);
bool sampleChanged = (mSampledState.back() != SampledAPZCState(Metrics()));
mSampledState.emplace_back(Metrics(), std::move(mScrollPayload),
mScrollGeneration);
return sampleChanged;
}
void AsyncPanZoomController::ResampleCompositedAsyncTransform(
const RecursiveMutexAutoLock& aProofOfLock) {
// This only gets called during testing situations, so the fact that this
// drops the scroll payload from mSampledState.front() is not really a
// problem.
if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
mScrollGeneration = treeManagerLocal->NewAPZScrollGeneration();
}
mSampledState.front() = SampledAPZCState(Metrics(), {}, mScrollGeneration);
}
void AsyncPanZoomController::ApplyAsyncTestAttributes(
const RecursiveMutexAutoLock& aProofOfLock) {
if (mTestAttributeAppliers == 0) {
if (mTestAsyncScrollOffset != CSSPoint() ||
mTestAsyncZoom != LayerToParentLayerScale()) {
// TODO Currently we update Metrics() and resample, which will cause
// the very latest user input to get immediately captured in the sample,
// and may defeat our attempt at "frame delay" (i.e. delaying the user
// input from affecting composition by one frame).
// Instead, maybe we should just apply the mTest* stuff directly to
// mSampledState.front(). We can even save/restore that SampledAPZCState
// instance in the AutoApplyAsyncTestAttributes instead of Metrics().
Metrics().ZoomBy(mTestAsyncZoom.scale);
CSSPoint asyncScrollPosition = Metrics().GetVisualScrollOffset();
CSSPoint requestedPoint =
asyncScrollPosition + this->mTestAsyncScrollOffset;
CSSPoint clampedPoint =
Metrics().CalculateScrollRange().ClampPoint(requestedPoint);
CSSPoint difference = mTestAsyncScrollOffset - clampedPoint;
ScrollByAndClamp(mTestAsyncScrollOffset);
if (StaticPrefs::apz_overscroll_test_async_scroll_offset_enabled()) {
ParentLayerPoint overscroll = difference * Metrics().GetZoom();
OverscrollBy(overscroll);
}
ResampleCompositedAsyncTransform(aProofOfLock);
}
}
++mTestAttributeAppliers;
}
void AsyncPanZoomController::UnapplyAsyncTestAttributes(
const RecursiveMutexAutoLock& aProofOfLock,
const FrameMetrics& aPrevFrameMetrics,
const ParentLayerPoint& aPrevOverscroll) {
MOZ_ASSERT(mTestAttributeAppliers >= 1);
--mTestAttributeAppliers;
if (mTestAttributeAppliers == 0) {
if (mTestAsyncScrollOffset != CSSPoint() ||
mTestAsyncZoom != LayerToParentLayerScale()) {
Metrics() = aPrevFrameMetrics;
RestoreOverscrollAmount(aPrevOverscroll);
ResampleCompositedAsyncTransform(aProofOfLock);
}
}
}
Matrix4x4 AsyncPanZoomController::GetTransformToLastDispatchedPaint(
const AsyncTransformComponents& aComponents, LayersId aForLayersId) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
CSSPoint componentOffset;
// The computation of the componentOffset should roughly be the negation
// of the translation in GetCurrentAsyncTransform() with the expected
// gecko metrics substituted for the effective scroll offsets.
if (aComponents.contains(AsyncTransformComponent::eVisual)) {
componentOffset += mExpectedGeckoMetrics.GetLayoutScrollOffset() -
mExpectedGeckoMetrics.GetVisualScrollOffset();
}
if (aComponents.contains(AsyncTransformComponent::eLayout)) {
CSSPoint lastPaintLayoutOffset;
if (mLastContentPaintMetrics.IsScrollable()) {
lastPaintLayoutOffset = mLastContentPaintMetrics.GetLayoutScrollOffset();
}
componentOffset +=
lastPaintLayoutOffset - mExpectedGeckoMetrics.GetLayoutScrollOffset();
}
LayerPoint scrollChange = componentOffset *
mLastContentPaintMetrics.GetDevPixelsPerCSSPixel() *
mLastContentPaintMetrics.GetCumulativeResolution();
// We're interested in the async zoom change. Factor out the content scale
// that may change when dragging the window to a monitor with a different
// content scale.
LayoutDeviceToParentLayerScale lastContentZoom =
mLastContentPaintMetrics.GetZoom() /
mLastContentPaintMetrics.GetDevPixelsPerCSSPixel();
LayoutDeviceToParentLayerScale lastDispatchedZoom =
mExpectedGeckoMetrics.GetZoom() /
mExpectedGeckoMetrics.GetDevPixelsPerCSSPixel();
float zoomChange = 1.0;
if (aComponents.contains(AsyncTransformComponent::eVisual) &&
lastDispatchedZoom != LayoutDeviceToParentLayerScale(0)) {
zoomChange = lastContentZoom.scale / lastDispatchedZoom.scale;
}
Matrix4x4 result;
// If we are the root, and |aForLayersId| is different from our LayersId,
// |aForLayersId| must be in a remote subdocument.
if (IsRootContent() && aForLayersId != GetLayersId()) {
result = GetPaintedResolutionTransform();
}
// Order of transforms: the painted resolution (if any) applies first, and
// any async transform on top of that.
return result * Matrix4x4::Translation(scrollChange.x, scrollChange.y, 0)
.PostScale(zoomChange, zoomChange, 1);
}
CSSRect AsyncPanZoomController::GetVisibleRect(
const RecursiveMutexAutoLock& aProofOfLock) const {
AutoApplyAsyncTestAttributes testAttributeApplier(this, aProofOfLock);
CSSPoint currentScrollOffset = GetEffectiveScrollOffset(
AsyncPanZoomController::eForCompositing, aProofOfLock);
CSSRect visible = CSSRect(currentScrollOffset,
Metrics().CalculateCompositedSizeInCssPixels());
return visible;
}
static CSSRect GetPaintedRect(const FrameMetrics& aFrameMetrics) {
CSSRect displayPort = aFrameMetrics.GetDisplayPort();
if (displayPort.IsEmpty()) {
// Fallback to use the viewport if the diplayport hasn't been set.
// This situation often happens non-scrollable iframe's root scroller in
// Fission.
return aFrameMetrics.GetVisualViewport();
}
return displayPort + aFrameMetrics.GetLayoutScrollOffset();
}
uint32_t AsyncPanZoomController::GetCheckerboardMagnitude(
const ParentLayerRect& aClippedCompositionBounds) const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
CSSRect painted = GetPaintedRect(mLastContentPaintMetrics);
painted.Inflate(CSSMargin::FromAppUnits(
nsMargin(1, 1, 1, 1))); // fuzz for rounding error
CSSRect visible = GetVisibleRect(lock); // relative to scrolled frame origin
if (visible.IsEmpty() || painted.Contains(visible)) {
// early-exit if we're definitely not checkerboarding
return 0;
}
// aClippedCompositionBounds and Metrics().GetCompositionBounds() are both
// relative to the layer tree origin.
// The "*RelativeToItself*" variables are relative to the comp bounds origin
ParentLayerRect visiblePartOfCompBoundsRelativeToItself =
aClippedCompositionBounds - Metrics().GetCompositionBounds().TopLeft();
CSSRect visiblePartOfCompBoundsRelativeToItselfInCssSpace;
if (Metrics().GetZoom() != CSSToParentLayerScale(0)) {
visiblePartOfCompBoundsRelativeToItselfInCssSpace =
(visiblePartOfCompBoundsRelativeToItself / Metrics().GetZoom());
}
// This one is relative to the scrolled frame origin, same as `visible`
CSSRect visiblePartOfCompBoundsInCssSpace =
visiblePartOfCompBoundsRelativeToItselfInCssSpace + visible.TopLeft();
visible = visible.Intersect(visiblePartOfCompBoundsInCssSpace);
CSSIntRegion checkerboard;
// Round so as to minimize checkerboarding; if we're only showing fractional
// pixels of checkerboarding it's not really worth counting
checkerboard.Sub(RoundedIn(visible), RoundedOut(painted));
uint32_t area = checkerboard.Area();
if (area) {
APZC_LOG_FM(Metrics(),
"%p is currently checkerboarding (painted %s visible %s)", this,
ToString(painted).c_str(), ToString(visible).c_str());
}
return area;
}
void AsyncPanZoomController::ReportCheckerboard(
const SampleTime& aSampleTime,
const ParentLayerRect& aClippedCompositionBounds) {
if (mLastCheckerboardReport == aSampleTime) {
// This function will get called multiple times for each APZC on a single
// composite (once for each layer it is attached to). Only report the
// checkerboard once per composite though.
return;
}
mLastCheckerboardReport = aSampleTime;
bool recordTrace = StaticPrefs::apz_record_checkerboarding();
bool forTelemetry = Telemetry::CanRecordBase();
uint32_t magnitude = GetCheckerboardMagnitude(aClippedCompositionBounds);
// IsInTransformingState() acquires the APZC lock and thus needs to
// be called before acquiring mCheckerboardEventLock.
bool inTransformingState = IsInTransformingState();
MutexAutoLock lock(mCheckerboardEventLock);
if (!mCheckerboardEvent && (recordTrace || forTelemetry)) {
mCheckerboardEvent = MakeUnique<CheckerboardEvent>(recordTrace);
}
mPotentialCheckerboardTracker.InTransform(inTransformingState,
recordTrace || forTelemetry);
if (magnitude) {
mPotentialCheckerboardTracker.CheckerboardSeen();
}
UpdateCheckerboardEvent(lock, magnitude);
}
void AsyncPanZoomController::UpdateCheckerboardEvent(
const MutexAutoLock& aProofOfLock, uint32_t aMagnitude) {
if (mCheckerboardEvent && mCheckerboardEvent->RecordFrameInfo(aMagnitude)) {
// This checkerboard event is done. Report some metrics to telemetry.
mozilla::glean::gfx_checkerboard::severity.AccumulateSingleSample(
mCheckerboardEvent->GetSeverity());
mozilla::glean::gfx_checkerboard::peak_pixel_count.AccumulateSingleSample(
mCheckerboardEvent->GetPeak());
mozilla::glean::gfx_checkerboard::duration.AccumulateRawDuration(
mCheckerboardEvent->GetDuration());
// mCheckerboardEvent only gets created if we are supposed to record
// telemetry so we always pass true for aRecordTelemetry.
mPotentialCheckerboardTracker.CheckerboardDone(
/* aRecordTelemetry = */ true);
if (StaticPrefs::apz_record_checkerboarding()) {
// if the pref is enabled, also send it to the storage class. it may be
// chosen for public display on about:checkerboard, the hall of fame for
// checkerboard events.
uint32_t severity = mCheckerboardEvent->GetSeverity();
std::string log = mCheckerboardEvent->GetLog();
CheckerboardEventStorage::Report(severity, log);
}
mCheckerboardEvent = nullptr;
}
}
void AsyncPanZoomController::FlushActiveCheckerboardReport() {
MutexAutoLock lock(mCheckerboardEventLock);
// Pretend like we got a frame with 0 pixels checkerboarded. This will
// terminate the checkerboard event and flush it out
UpdateCheckerboardEvent(lock, 0);
}
void AsyncPanZoomController::NotifyLayersUpdated(
const ScrollMetadata& aScrollMetadata, bool aIsFirstPaint,
bool aThisLayerTreeUpdated) {
AssertOnUpdaterThread();
RecursiveMutexAutoLock lock(mRecursiveMutex);
bool isDefault = mScrollMetadata.IsDefault();
const FrameMetrics& aLayerMetrics = aScrollMetadata.GetMetrics();
if ((aScrollMetadata == mLastContentPaintMetadata) && !isDefault) {
// No new information here, skip it.
APZC_LOGV("%p NotifyLayersUpdated short-circuit\n", this);
return;
}
// If the Metrics scroll offset is different from the last scroll offset
// that the main-thread sent us, then we know that the user has been doing
// something that triggers a scroll. This check is the APZ equivalent of the
// check on the main-thread at
// https://hg.mozilla.org/mozilla-central/file/97a52326b06a/layout/generic/nsGfxScrollFrame.cpp#l4050
// There is code below (the use site of userScrolled) that prevents a
// restored- scroll-position update from overwriting a user scroll, again
// equivalent to how the main thread code does the same thing.
// XXX Suspicious comparison between layout and visual scroll offsets.
// This may not do the right thing when we're zoomed in.
CSSPoint lastScrollOffset = mLastContentPaintMetrics.GetLayoutScrollOffset();
bool userScrolled =
!FuzzyEqualsCoordinate(Metrics().GetVisualScrollOffset().x,
lastScrollOffset.x) ||
!FuzzyEqualsCoordinate(Metrics().GetVisualScrollOffset().y,
lastScrollOffset.y);
if (aScrollMetadata.DidContentGetPainted()) {
mLastContentPaintMetadata = aScrollMetadata;
}
mScrollMetadata.SetScrollParentId(aScrollMetadata.GetScrollParentId());
APZC_LOGV_FM(aLayerMetrics,
"%p got a NotifyLayersUpdated with aIsFirstPaint=%d, "
"aThisLayerTreeUpdated=%d",
this, aIsFirstPaint, aThisLayerTreeUpdated);
{ // scope lock
MutexAutoLock lock(mCheckerboardEventLock);
if (mCheckerboardEvent && mCheckerboardEvent->IsRecordingTrace()) {
std::string str;
if (aThisLayerTreeUpdated) {
if (!aLayerMetrics.GetPaintRequestTime().IsNull()) {
// Note that we might get the paint request time as non-null, but with
// aThisLayerTreeUpdated false. That can happen if we get a layer
// transaction from a different process right after we get the layer
// transaction with aThisLayerTreeUpdated == true. In this case we
// want to ignore the paint request time because it was already dumped
// in the previous layer transaction.
TimeDuration paintTime =
TimeStamp::Now() - aLayerMetrics.GetPaintRequestTime();
std::stringstream info;
info << " painttime " << paintTime.ToMilliseconds();
str = info.str();
} else {
// This might be indicative of a wasted paint particularly if it
// happens during a checkerboard event.
str = " (this layertree updated)";
}
}
mCheckerboardEvent->UpdateRendertraceProperty(
CheckerboardEvent::Page, aLayerMetrics.GetScrollableRect());
mCheckerboardEvent->UpdateRendertraceProperty(
CheckerboardEvent::PaintedDisplayPort, GetPaintedRect(aLayerMetrics),
str);
}
}
// The main thread may send us a visual scroll offset update. This is
// different from a layout viewport offset update in that the layout viewport
// offset is limited to the layout scroll range, while the visual viewport
// offset is not.
// However, there are some conditions in which the layout update will clobber
// the visual update, and we want to ignore the visual update in those cases.
// This variable tracks that.
bool ignoreVisualUpdate = false;
// TODO if we're in a drag and scrollOffsetUpdated is set then we want to
// ignore it
bool needContentRepaint = false;
RepaintUpdateType contentRepaintType = RepaintUpdateType::eNone;
bool viewportSizeUpdated = false;
bool needToReclampScroll = false;
if ((aIsFirstPaint && aThisLayerTreeUpdated) || isDefault ||
Metrics().IsRootContent() != aLayerMetrics.IsRootContent()) {
if (Metrics().IsRootContent() && !aLayerMetrics.IsRootContent()) {
// We only support zooming on root content APZCs
SetZoomAnimationId(Nothing());
}
// Initialize our internal state to something sane when the content
// that was just painted is something we knew nothing about previously
CancelAnimation();
// Keep our existing scroll generation, if there are scroll updates. In this
// case we'll update our scroll generation when processing the scroll update
// array below. If there are no scroll updates, take the generation from the
ScrollGeneration oldScrollGeneration = Metrics().GetScrollGeneration();
mScrollMetadata = aScrollMetadata;
if (!aScrollMetadata.GetScrollUpdates().IsEmpty()) {
Metrics().SetScrollGeneration(oldScrollGeneration);
}
mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
for (auto& sampledState : mSampledState) {
sampledState.UpdateScrollProperties(Metrics());
sampledState.UpdateZoomProperties(Metrics());
}
if (aLayerMetrics.HasNonZeroDisplayPortMargins()) {
// A non-zero display port margin here indicates a displayport has
// been set by a previous APZC for the content at this guid. The
// scrollable rect may have changed since then, making the margins
// wrong, so we need to calculate a new display port.
// It is important that we request a repaint here only when we need to
// otherwise we will end up setting a display port on every frame that
// gets a view id.
APZC_LOG("%p detected non-empty margins which probably need updating\n",
this);
needContentRepaint = true;
}
} else {
// If we're not taking the aLayerMetrics wholesale we still need to pull
// in some things into our local Metrics() because these things are
// determined by Gecko and our copy in Metrics() may be stale.
if (Metrics().GetLayoutViewport().Size() !=
aLayerMetrics.GetLayoutViewport().Size()) {
CSSRect layoutViewport = Metrics().GetLayoutViewport();
// The offset will be updated if necessary via
// RecalculateLayoutViewportOffset().
layoutViewport.SizeTo(aLayerMetrics.GetLayoutViewport().Size());
Metrics().SetLayoutViewport(layoutViewport);
needContentRepaint = true;
viewportSizeUpdated = true;
}
// TODO: Rely entirely on |aScrollMetadata.IsResolutionUpdated()| to
// determine which branch to take, and drop the other conditions.
CSSToParentLayerScale oldZoom = Metrics().GetZoom();
if (FuzzyEqualsAdditive(
Metrics().GetCompositionBoundsWidthIgnoringScrollbars(),
aLayerMetrics.GetCompositionBoundsWidthIgnoringScrollbars()) &&
Metrics().GetDevPixelsPerCSSPixel() ==
aLayerMetrics.GetDevPixelsPerCSSPixel() &&
!viewportSizeUpdated && !aScrollMetadata.IsResolutionUpdated()) {
// Any change to the pres shell resolution was requested by APZ and is
// already included in our zoom; however, other components of the
// cumulative resolution (a parent document's pres-shell resolution, or
// the css-driven resolution) may have changed, and we need to update
// our zoom to reflect that. Note that we can't just take
// aLayerMetrics.mZoom because the APZ may have additional async zoom
// since the repaint request.
float totalResolutionChange = 1.0;
if (Metrics().GetCumulativeResolution() != LayoutDeviceToLayerScale(0)) {
totalResolutionChange = aLayerMetrics.GetCumulativeResolution().scale /
Metrics().GetCumulativeResolution().scale;
}
float presShellResolutionChange = aLayerMetrics.GetPresShellResolution() /
Metrics().GetPresShellResolution();
if (presShellResolutionChange != 1.0f) {
needContentRepaint = true;
}
Metrics().ZoomBy(totalResolutionChange / presShellResolutionChange);
for (auto& sampledState : mSampledState) {
sampledState.ZoomBy(totalResolutionChange / presShellResolutionChange);
}
} else {
// Take the new zoom as either device scale or composition width or
// viewport size got changed (e.g. due to orientation change, or content
// changing the meta-viewport tag), or the main thread originated a
// resolution change for another reason (e.g. Ctrl+0 was pressed to
// reset the zoom).
Metrics().SetZoom(aLayerMetrics.GetZoom());
for (auto& sampledState : mSampledState) {
sampledState.UpdateZoomProperties(aLayerMetrics);
}
Metrics().SetDevPixelsPerCSSPixel(
aLayerMetrics.GetDevPixelsPerCSSPixel());
}
if (Metrics().GetZoom() != oldZoom) {
// If the zoom changed, the scroll range in CSS pixels may have changed
// even if the composition bounds didn't.
needToReclampScroll = true;
}
mExpectedGeckoMetrics.UpdateZoomFrom(aLayerMetrics);
if (!Metrics().GetScrollableRect().IsEqualEdges(
aLayerMetrics.GetScrollableRect())) {
Metrics().SetScrollableRect(aLayerMetrics.GetScrollableRect());
needContentRepaint = true;
needToReclampScroll = true;
}
if (!Metrics().GetCompositionBounds().IsEqualEdges(
aLayerMetrics.GetCompositionBounds())) {
Metrics().SetCompositionBounds(aLayerMetrics.GetCompositionBounds());
needToReclampScroll = true;
}
Metrics().SetCompositionBoundsWidthIgnoringScrollbars(
aLayerMetrics.GetCompositionBoundsWidthIgnoringScrollbars());
if (Metrics().IsRootContent() &&
Metrics().GetCompositionSizeWithoutDynamicToolbar() !=
aLayerMetrics.GetCompositionSizeWithoutDynamicToolbar()) {
Metrics().SetCompositionSizeWithoutDynamicToolbar(
aLayerMetrics.GetCompositionSizeWithoutDynamicToolbar());
needToReclampScroll = true;
}
Metrics().SetBoundingCompositionSize(
aLayerMetrics.GetBoundingCompositionSize());
Metrics().SetPresShellResolution(aLayerMetrics.GetPresShellResolution());
Metrics().SetCumulativeResolution(aLayerMetrics.GetCumulativeResolution());
Metrics().SetTransformToAncestorScale(
aLayerMetrics.GetTransformToAncestorScale());
mScrollMetadata.SetHasScrollgrab(aScrollMetadata.GetHasScrollgrab());
mScrollMetadata.SetLineScrollAmount(aScrollMetadata.GetLineScrollAmount());
mScrollMetadata.SetPageScrollAmount(aScrollMetadata.GetPageScrollAmount());
mScrollMetadata.SetSnapInfo(ScrollSnapInfo(aScrollMetadata.GetSnapInfo()));
mScrollMetadata.SetIsLayersIdRoot(aScrollMetadata.IsLayersIdRoot());
mScrollMetadata.SetIsAutoDirRootContentRTL(
aScrollMetadata.IsAutoDirRootContentRTL());
Metrics().SetIsScrollInfoLayer(aLayerMetrics.IsScrollInfoLayer());
Metrics().SetHasNonZeroDisplayPortMargins(
aLayerMetrics.HasNonZeroDisplayPortMargins());
Metrics().SetMinimalDisplayPort(aLayerMetrics.IsMinimalDisplayPort());
mScrollMetadata.SetForceDisableApz(aScrollMetadata.IsApzForceDisabled());
mScrollMetadata.SetIsRDMTouchSimulationActive(
aScrollMetadata.GetIsRDMTouchSimulationActive());
mScrollMetadata.SetForceMousewheelAutodir(
aScrollMetadata.ForceMousewheelAutodir());
mScrollMetadata.SetForceMousewheelAutodirHonourRoot(
aScrollMetadata.ForceMousewheelAutodirHonourRoot());
mScrollMetadata.SetIsPaginatedPresentation(
aScrollMetadata.IsPaginatedPresentation());
mScrollMetadata.SetDisregardedDirection(
aScrollMetadata.GetDisregardedDirection());
mScrollMetadata.SetOverscrollBehavior(
aScrollMetadata.GetOverscrollBehavior());
mScrollMetadata.SetOverflow(aScrollMetadata.GetOverflow());
}
bool instantScrollMayTriggerTransform = false;
bool scrollOffsetUpdated = false;
bool smoothScrollRequested = false;
bool didCancelAnimation = false;
Maybe<CSSPoint> cumulativeRelativeDelta;
for (const auto& scrollUpdate : aScrollMetadata.GetScrollUpdates()) {
APZC_LOG("%p processing scroll update %s\n", this,
ToString(scrollUpdate).c_str());
if (!(Metrics().GetScrollGeneration() < scrollUpdate.GetGeneration())) {
// This is stale, let's ignore it
APZC_LOG("%p scrollupdate generation stale, dropping\n", this);
continue;
}
Metrics().SetScrollGeneration(scrollUpdate.GetGeneration());
MOZ_ASSERT(scrollUpdate.GetOrigin() != ScrollOrigin::Apz);
if (userScrolled &&
!nsLayoutUtils::CanScrollOriginClobberApz(scrollUpdate.GetOrigin())) {
APZC_LOG("%p scrollupdate cannot clobber APZ userScrolled\n", this);
continue;
}
// XXX: if we get here, |scrollUpdate| is clobbering APZ, so we may want
// to reset |userScrolled| back to false so that subsequent scrollUpdates
// in this loop don't get dropped by the check above. Need to add a test
// that exercises this scenario, as we don't currently have one.
if (scrollUpdate.GetMode() == ScrollMode::Smooth ||
scrollUpdate.GetMode() == ScrollMode::SmoothMsd) {
smoothScrollRequested = true;
// Requests to animate the visual scroll position override requests to
// simply update the visual scroll offset to a particular point. Since
// we have an animation request, we set ignoreVisualUpdate to true to
// indicate we don't need to apply the visual scroll update in
// aLayerMetrics.
ignoreVisualUpdate = true;
// For relative updates we want to add the relative offset to any existing
// destination, or the current visual offset if there is no existing
// destination.
CSSPoint base = GetCurrentAnimationDestination(lock).valueOr(
Metrics().GetVisualScrollOffset());
CSSPoint destination;
if (scrollUpdate.GetType() == ScrollUpdateType::Relative) {
CSSPoint delta =
scrollUpdate.GetDestination() - scrollUpdate.GetSource();
APZC_LOG("%p relative smooth scrolling from %s by %s\n", this,
ToString(base).c_str(), ToString(delta).c_str());
destination = Metrics().CalculateScrollRange().ClampPoint(base + delta);
} else if (scrollUpdate.GetType() == ScrollUpdateType::PureRelative) {
CSSPoint delta = scrollUpdate.GetDelta();
APZC_LOG("%p pure-relative smooth scrolling from %s by %s\n", this,
ToString(base).c_str(), ToString(delta).c_str());
destination = Metrics().CalculateScrollRange().ClampPoint(base + delta);
} else {
APZC_LOG("%p smooth scrolling to %s\n", this,
ToString(scrollUpdate.GetDestination()).c_str());
destination = scrollUpdate.GetDestination();
}
if (scrollUpdate.GetMode() == ScrollMode::SmoothMsd) {
SmoothMsdScrollTo(
CSSSnapDestination{destination, scrollUpdate.GetSnapTargetIds()},
scrollUpdate.GetScrollTriggeredByScript());
} else {
MOZ_ASSERT(scrollUpdate.GetMode() == ScrollMode::Smooth);
SmoothScrollTo(
CSSSnapDestination{destination, scrollUpdate.GetSnapTargetIds()},
scrollUpdate.GetScrollTriggeredByScript(),
scrollUpdate.GetOrigin());
}
continue;
}
MOZ_ASSERT(scrollUpdate.GetMode() == ScrollMode::Instant ||
scrollUpdate.GetMode() == ScrollMode::Normal);
instantScrollMayTriggerTransform =
scrollUpdate.GetMode() == ScrollMode::Instant &&
scrollUpdate.GetScrollTriggeredByScript() ==
ScrollTriggeredByScript::No;
// If the layout update is of a higher priority than the visual update, then
// we don't want to apply the visual update.
// If the layout update is of a clobbering type (or a smooth scroll request,
// which is handled above) then it takes precedence over an eRestore visual
// update. But we also allow the possibility for the main thread to ask us
// to scroll both the layout and visual viewports to distinct (but
// compatible) locations (via e.g. both updates being of a non-clobbering/
// eRestore type).
if (nsLayoutUtils::CanScrollOriginClobberApz(scrollUpdate.GetOrigin()) &&
aLayerMetrics.GetVisualScrollUpdateType() !=
FrameMetrics::eMainThread) {
ignoreVisualUpdate = true;
}
Maybe<CSSPoint> relativeDelta;
if (scrollUpdate.GetType() == ScrollUpdateType::Relative) {
APZC_LOG(
"%p relative updating scroll offset from %s by %s\n", this,
ToString(Metrics().GetVisualScrollOffset()).c_str(),
ToString(scrollUpdate.GetDestination() - scrollUpdate.GetSource())
.c_str());
scrollOffsetUpdated = true;
// It's possible that the main thread has ignored an APZ scroll offset
// update for the pending relative scroll that we have just received.
// When this happens, we need to send a new scroll offset update with
// the combined scroll offset or else the main thread may have an
// incorrect scroll offset for a period of time.
if (Metrics().HasPendingScroll(aLayerMetrics)) {
needContentRepaint = true;
contentRepaintType = RepaintUpdateType::eUserAction;
}
relativeDelta =
Some(Metrics().ApplyRelativeScrollUpdateFrom(scrollUpdate));
Metrics().RecalculateLayoutViewportOffset();
} else if (scrollUpdate.GetType() == ScrollUpdateType::PureRelative) {
APZC_LOG("%p pure-relative updating scroll offset from %s by %s\n", this,
ToString(Metrics().GetVisualScrollOffset()).c_str(),
ToString(scrollUpdate.GetDelta()).c_str());
scrollOffsetUpdated = true;
// Always need a repaint request with a repaint type for pure relative
// scrolls because apz is doing the scroll at the main thread's request.
// The main thread has not updated it's scroll offset yet, it is depending
// on apz to tell it where to scroll.
needContentRepaint = true;
contentRepaintType = RepaintUpdateType::eVisualUpdate;
// We have to ignore a visual scroll offset update otherwise it will
// clobber the relative scrolling we are about to do. We perform
// visualScrollOffset = visualScrollOffset + delta. Then the
// visualScrollOffsetUpdated block below will do visualScrollOffset =
// aLayerMetrics.GetVisualDestination(). We need visual scroll offset
// updates to be incorporated into this scroll update loop to properly fix
// this.
ignoreVisualUpdate = true;
relativeDelta =
Some(Metrics().ApplyPureRelativeScrollUpdateFrom(scrollUpdate));
Metrics().RecalculateLayoutViewportOffset();
} else {
APZC_LOG("%p updating scroll offset from %s to %s\n", this,
ToString(Metrics().GetVisualScrollOffset()).c_str(),
ToString(scrollUpdate.GetDestination()).c_str());
bool offsetChanged = Metrics().ApplyScrollUpdateFrom(scrollUpdate);
Metrics().RecalculateLayoutViewportOffset();
if (offsetChanged || scrollUpdate.GetMode() != ScrollMode::Instant ||
scrollUpdate.GetType() != ScrollUpdateType::Absolute ||
scrollUpdate.GetOrigin() != ScrollOrigin::None) {
// We get a NewScrollFrame update for newly created scroll frames. Only
// if this was not a NewScrollFrame update or the offset changed do we
// request repaint. This is important so that we don't request repaint
// for every new content and set a full display port on it.
scrollOffsetUpdated = true;
}
}
if (relativeDelta) {
cumulativeRelativeDelta =
!cumulativeRelativeDelta
? relativeDelta
: Some(*cumulativeRelativeDelta + *relativeDelta);
} else {
// If the scroll update is not relative, clobber the cumulative delta,
// i.e. later updates win.
cumulativeRelativeDelta.reset();
}
// If an animation is underway, tell it about the scroll offset update.
// Some animations can handle some scroll offset updates and continue
// running. Those that can't will return false, and we cancel them.
if (ShouldCancelAnimationForScrollUpdate(relativeDelta)) {
// Cancel the animation (which might also trigger a repaint request)
// after we update the scroll offset above. Otherwise we can be left
// in a state where things are out of sync.
CancelAnimation();
didCancelAnimation = true;
}
}
if (aIsFirstPaint || needToReclampScroll) {
// The scrollable rect or composition bounds may have changed in a way that
// makes our local scroll offset out of bounds, so clamp it.
ClampAndSetVisualScrollOffset(Metrics().GetVisualScrollOffset());
for (auto& sampledState : mSampledState) {
sampledState.ClampVisualScrollOffset(Metrics());
}
}
// If our scroll range changed (for example, because the page dynamically
// loaded new content, thereby increasing the size of the scrollable rect),
// and we're overscrolled, being overscrolled may no longer be a valid
// state (for example, we may no longer be at the edge of our scroll range),
// then try to fill it out with the new content if the overscroll amount is
// inside the new scroll range.
if (needToReclampScroll && IsInInvalidOverscroll()) {
if (!cumulativeRelativeDelta) {
// TODO: If we have a cumulative delta, can we combine the overscroll
// change with it?
CSSPoint scrollPositionChange = MaybeFillOutOverscrollGutter(lock);
if (scrollPositionChange != CSSPoint()) {
cumulativeRelativeDelta = Some(scrollPositionChange);
}
}
if (mState == OVERSCROLL_ANIMATION) {
CancelAnimation();
didCancelAnimation = true;
} else if (IsOverscrolled()) {
ClearOverscroll();
}
}
if (scrollOffsetUpdated) {
for (auto& sampledState : mSampledState) {
if (!didCancelAnimation && cumulativeRelativeDelta.isSome()) {
sampledState.UpdateScrollPropertiesWithRelativeDelta(
Metrics(), *cumulativeRelativeDelta);
} else {
sampledState.UpdateScrollProperties(Metrics());
}
}
// Because of the scroll generation update, any inflight paint requests
// are going to be ignored by layout, and so mExpectedGeckoMetrics becomes
// incorrect for the purposes of calculating the LD transform. To correct
// this we need to update mExpectedGeckoMetrics to be the last thing we
// know was painted by Gecko.
mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
// Since the scroll offset has changed, we need to recompute the
// displayport margins and send them to layout. Otherwise there might be
// scenarios where for example we scroll from the top of a page (where the
// top displayport margin is zero) to the bottom of a page, which will
// result in a displayport that doesn't extend upwards at all.
// Note that even if the CancelAnimation call above requested a repaint
// this is fine because we already have repaint request deduplication.
needContentRepaint = true;
// Since the main-thread scroll offset changed we should trigger a
// recomposite to make sure it becomes user-visible.
ScheduleComposite();
// If the scroll offset was updated, we're not in a transforming state,
// and we are scrolling by a non-zero delta, we should ensure
// TransformBegin and TransformEnd notifications are sent.
if (!IsTransformingState(mState) && instantScrollMayTriggerTransform &&
cumulativeRelativeDelta && *cumulativeRelativeDelta != CSSPoint() &&
!didCancelAnimation) {
SendTransformBeginAndEnd();
}
}
if (smoothScrollRequested && !scrollOffsetUpdated) {
mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
// Need to acknowledge the request.
needContentRepaint = true;
}
// If `isDefault` is true, this APZC is a "new" one (this is the first time
// it's getting a NotifyLayersUpdated call). In this case we want to apply the
// visual scroll offset from the main thread to our scroll offset.
// The main thread may also ask us to scroll the visual viewport to a
// particular location. However, in all cases, we want to ignore the visual
// offset update if ignoreVisualUpdate is true, because we're clobbering
// the visual update with a layout update.
bool visualScrollOffsetUpdated =
!ignoreVisualUpdate &&
(isDefault ||
aLayerMetrics.GetVisualScrollUpdateType() != FrameMetrics::eNone);
if (visualScrollOffsetUpdated) {
APZC_LOG("%p updating visual scroll offset from %s to %s (updateType %d)\n",
this, ToString(Metrics().GetVisualScrollOffset()).c_str(),
ToString(aLayerMetrics.GetVisualDestination()).c_str(),
(int)aLayerMetrics.GetVisualScrollUpdateType());
bool offsetChanged = Metrics().ClampAndSetVisualScrollOffset(
aLayerMetrics.GetVisualDestination());
// If this is the first time we got metrics for this content (isDefault) and
// the update type was none and the offset didn't change then we don't have
// to do anything. This is important because we don't want to request
// repaint on the initial NotifyLayersUpdated for every content and thus set
// a full display port.
if (aLayerMetrics.GetVisualScrollUpdateType() == FrameMetrics::eNone &&
!offsetChanged) {
visualScrollOffsetUpdated = false;
}
}
if (visualScrollOffsetUpdated) {
// The rest of this branch largely follows the code in the
// |if (scrollOffsetUpdated)| branch above. Eventually it should get
// merged into that branch.
Metrics().RecalculateLayoutViewportOffset();
for (auto& sampledState : mSampledState) {
sampledState.UpdateScrollProperties(Metrics());
}
mExpectedGeckoMetrics.UpdateFrom(aLayerMetrics);
if (ShouldCancelAnimationForScrollUpdate(Nothing())) {
CancelAnimation();
}
// The main thread did not actually paint a displayport at the target
// visual offset, so we need to ask it to repaint. We need to set the
// contentRepaintType to something other than eNone, otherwise the main
// thread will short-circuit the repaint request.
// Don't do this for eRestore visual updates as a repaint coming from APZ
// breaks the scroll offset restoration mechanism.
needContentRepaint = true;
if (aLayerMetrics.GetVisualScrollUpdateType() ==
FrameMetrics::eMainThread) {
contentRepaintType = RepaintUpdateType::eVisualUpdate;
}
ScheduleComposite();
}
if (viewportSizeUpdated) {
// While we want to accept the main thread's layout viewport _size_,
// its position may be out of date in light of async scrolling, to
// adjust it if necessary to make sure it continues to enclose the
// visual viewport.
// Note: it's important to do this _after_ we've accepted any
// updated composition bounds.
Metrics().RecalculateLayoutViewportOffset();
}
if (needContentRepaint) {
// This repaint request could be driven by a user action if we accept a
// relative scroll offset update
RequestContentRepaint(contentRepaintType);
}
}
FrameMetrics& AsyncPanZoomController::Metrics() {
mRecursiveMutex.AssertCurrentThreadIn();
return mScrollMetadata.GetMetrics();
}
const FrameMetrics& AsyncPanZoomController::Metrics() const {
mRecursiveMutex.AssertCurrentThreadIn();
return mScrollMetadata.GetMetrics();
}
GeckoViewMetrics AsyncPanZoomController::GetGeckoViewMetrics() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return GeckoViewMetrics{GetEffectiveScrollOffset(eForCompositing, lock),
GetEffectiveZoom(eForCompositing, lock)};
}
wr::MinimapData AsyncPanZoomController::GetMinimapData() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
wr::MinimapData result;
result.is_root_content = IsRootContent();
// We want the minimap to reflect the scroll offset actually composited,
// which could be older than the latest one in Metrics() due to the frame
// delay.
CSSRect visualViewport = GetCurrentAsyncVisualViewport(eForCompositing);
result.visual_viewport = wr::ToLayoutRect(visualViewport.ToUnknownRect());
CSSRect layoutViewport = GetEffectiveLayoutViewport(eForCompositing, lock);
result.layout_viewport = wr::ToLayoutRect(layoutViewport.ToUnknownRect());
result.scrollable_rect =
wr::ToLayoutRect(Metrics().GetScrollableRect().ToUnknownRect());
// The display port is stored relative to the layout viewport origin.
// Translate it to be relative to the document origin, like the other rects.
CSSRect displayPort = mLastContentPaintMetrics.GetDisplayPort() +
mLastContentPaintMetrics.GetLayoutScrollOffset();
result.displayport = wr::ToLayoutRect(displayPort.ToUnknownRect());
// Remaining fields (zoom_transform, root_content_scroll_id,
// root_content_pipeline_id) will be populated by the caller, since they
// require information from other APZCs to compute.
return result;
}
bool AsyncPanZoomController::UpdateRootFrameMetricsIfChanged(
GeckoViewMetrics& aMetrics) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
if (!Metrics().IsRootContent()) {
return false;
}
GeckoViewMetrics newMetrics = GetGeckoViewMetrics();
bool hasChanged = RoundedToInt(aMetrics.mVisualScrollOffset) !=
RoundedToInt(newMetrics.mVisualScrollOffset) ||
aMetrics.mZoom != newMetrics.mZoom;
if (hasChanged) {
aMetrics = newMetrics;
}
return hasChanged;
}
const FrameMetrics& AsyncPanZoomController::GetFrameMetrics() const {
return Metrics();
}
const ScrollMetadata& AsyncPanZoomController::GetScrollMetadata() const {
mRecursiveMutex.AssertCurrentThreadIn();
return mScrollMetadata;
}
void AsyncPanZoomController::AssertOnSamplerThread() const {
if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
treeManagerLocal->AssertOnSamplerThread();
}
}
void AsyncPanZoomController::AssertOnUpdaterThread() const {
if (APZCTreeManager* treeManagerLocal = GetApzcTreeManager()) {
treeManagerLocal->AssertOnUpdaterThread();
}
}
APZCTreeManager* AsyncPanZoomController::GetApzcTreeManager() const {
mRecursiveMutex.AssertNotCurrentThreadIn();
return mTreeManager;
}
void AsyncPanZoomController::ZoomToRect(const ZoomTarget& aZoomTarget,
const uint32_t aFlags) {
CSSRect rect = aZoomTarget.targetRect;
if (!rect.IsFinite()) {
NS_WARNING("ZoomToRect got called with a non-finite rect; ignoring...");
return;
}
if (rect.IsEmpty() && (aFlags & DISABLE_ZOOM_OUT)) {
// Double-tap-to-zooming uses an empty rect to mean "zoom out".
// If zooming out is disabled, an empty rect is nonsensical
// and will produce undesirable scrolling.
NS_WARNING(
"ZoomToRect got called with an empty rect and zoom out disabled; "
"ignoring...");
return;
}
AutoDynamicToolbarHider dynamicToolbarHider(this);
{
RecursiveMutexAutoLock lock(mRecursiveMutex);
// If we are zooming to focus an input element near the bottom of the
// scrollable rect, it may be covered up by the dynamic toolbar and we may
// not have room to scroll it into view. In such cases, trigger hiding of
// the dynamic toolbar to ensure the input element is visible.
if (aFlags & ZOOM_TO_FOCUSED_INPUT) {
// Long and short viewport heights, corresponding to CSS length values of
// 100lvh and 100svh.
const CSSCoord lvh = ToCSSPixels(Metrics().GetCompositionBounds().height);
const CSSCoord svh = ToCSSPixels(
Metrics().GetCompositionSizeWithoutDynamicToolbar().height);
const CSSCoord scrollableRectHeight =
Metrics().GetScrollableRect().height;
if (scrollableRectHeight > svh && scrollableRectHeight < lvh) {
const CSSCoord targetDistanceFromBottom =
(Metrics().GetScrollableRect().YMost() -
aZoomTarget.targetRect.YMost());
const CSSCoord dynamicToolbarHeight = (lvh - svh);
if (targetDistanceFromBottom < dynamicToolbarHeight) {
dynamicToolbarHider.Hide();
}
}
}
MOZ_ASSERT(Metrics().IsRootContent());
const float defaultZoomInAmount =
StaticPrefs::apz_doubletapzoom_defaultzoomin();
ParentLayerRect compositionBounds = Metrics().GetCompositionBounds();
CSSRect cssPageRect = Metrics().GetScrollableRect();
CSSPoint scrollOffset = Metrics().GetVisualScrollOffset();
CSSSize sizeBeforeZoom = Metrics().CalculateCompositedSizeInCssPixels();
CSSToParentLayerScale currentZoom = Metrics().GetZoom();
CSSToParentLayerScale targetZoom;
// The minimum zoom to prevent over-zoom-out.
// If the zoom factor is lower than this (i.e. we are zoomed more into the
// page), then the CSS content rect, in layers pixels, will be smaller than
// the composition bounds. If this happens, we can't fill the target
// composited area with this frame.
const CSSRect cssExpandedPageRect = Metrics().GetExpandedScrollableRect();
CSSToParentLayerScale localMinZoom(
std::max(compositionBounds.Width() / cssExpandedPageRect.Width(),
compositionBounds.Height() / cssExpandedPageRect.Height()));
localMinZoom.scale =
std::clamp(localMinZoom.scale, mZoomConstraints.mMinZoom.scale,
mZoomConstraints.mMaxZoom.scale);
localMinZoom = std::max(mZoomConstraints.mMinZoom, localMinZoom);
CSSToParentLayerScale localMaxZoom =
std::max(localMinZoom, mZoomConstraints.mMaxZoom);
if (!rect.IsEmpty()) {
// Intersect the zoom-to-rect to the CSS rect to make sure it fits.
rect = rect.Intersect(cssPageRect);
targetZoom = CSSToParentLayerScale(
std::min(compositionBounds.Width() / rect.Width(),
compositionBounds.Height() / rect.Height()));
if (aFlags & DISABLE_ZOOM_OUT) {
targetZoom = std::max(targetZoom, currentZoom);
}
}
// 1. If the rect is empty, the content-side logic for handling a double-tap
// requested that we zoom out.
// 2. currentZoom is equal to mZoomConstraints.mMaxZoom and user still
// double-tapping it
// Treat these cases as a request to zoom out as much as possible
// unless cantZoomOutBehavior == ZoomIn and currentZoom
// is equal to localMinZoom and user still double-tapping it, then try to
// zoom in a small amount to provide feedback to the user.
bool zoomOut = false;
// True if we are already zoomed out and we are asked to either stay there
// or zoom out more and cantZoomOutBehavior == ZoomIn.
bool zoomInDefaultAmount = false;
if (aFlags & DISABLE_ZOOM_OUT) {
zoomOut = false;
} else {
if (rect.IsEmpty()) {
if (currentZoom == localMinZoom &&
aZoomTarget.cantZoomOutBehavior == CantZoomOutBehavior::ZoomIn &&
(defaultZoomInAmount != 1.f)) {
zoomInDefaultAmount = true;
} else {
zoomOut = true;
}
} else if (currentZoom == localMaxZoom && targetZoom >= localMaxZoom) {
zoomOut = true;
}
}
// already at min zoom and asked to zoom out further
if (!zoomOut && currentZoom == localMinZoom && targetZoom <= localMinZoom &&
aZoomTarget.cantZoomOutBehavior == CantZoomOutBehavior::ZoomIn &&
(defaultZoomInAmount != 1.f)) {
zoomInDefaultAmount = true;
}
MOZ_ASSERT(!(zoomInDefaultAmount && zoomOut));
if (zoomInDefaultAmount) {
targetZoom =
CSSToParentLayerScale(currentZoom.scale * defaultZoomInAmount);
}
if (zoomOut) {
targetZoom = localMinZoom;
}
if (aFlags & PAN_INTO_VIEW_ONLY) {
targetZoom = currentZoom;
} else if (aFlags & ONLY_ZOOM_TO_DEFAULT_SCALE) {
CSSToParentLayerScale zoomAtDefaultScale =
Metrics().GetDevPixelsPerCSSPixel() *
LayoutDeviceToParentLayerScale(1.0);
if (targetZoom.scale > zoomAtDefaultScale.scale) {
// Only change the zoom if we are less than the default zoom
if (currentZoom.scale < zoomAtDefaultScale.scale) {
targetZoom = zoomAtDefaultScale;
} else {
targetZoom = currentZoom;
}
}
}
targetZoom.scale =
std::clamp(targetZoom.scale, localMinZoom.scale, localMaxZoom.scale);
// For zoom-to-focused-input, we've already centered the given focused
// element in nsDOMWindowUtils::ZoomToFocusedInput() so that if the target
// zoom scale would be same we don't need to trigger a ZoomAnimation.
if ((aFlags & ZOOM_TO_FOCUSED_INPUT) && targetZoom == currentZoom) {
return;
}
FrameMetrics endZoomToMetrics = Metrics();
endZoomToMetrics.SetZoom(CSSToParentLayerScale(targetZoom));
CSSSize sizeAfterZoom =
endZoomToMetrics.CalculateCompositedSizeInCssPixels();
if (zoomInDefaultAmount || zoomOut) {
// For the zoom out case we should always center what was visible
// otherwise it feels like we are scrolling as well as zooming out. For
// the non-zoomOut case, if we've been provided a pointer location, zoom
// around that, otherwise just zoom in to the center of what's currently
// visible.
if (!zoomOut && aZoomTarget.documentRelativePointerPosition.isSome()) {
rect = CSSRect(aZoomTarget.documentRelativePointerPosition->x -
sizeAfterZoom.width / 2,
aZoomTarget.documentRelativePointerPosition->y -
sizeAfterZoom.height / 2,
sizeAfterZoom.Width(), sizeAfterZoom.Height());
} else {
rect = CSSRect(
scrollOffset.x + (sizeBeforeZoom.width - sizeAfterZoom.width) / 2,
scrollOffset.y + (sizeBeforeZoom.height - sizeAfterZoom.height) / 2,
sizeAfterZoom.Width(), sizeAfterZoom.Height());
}
rect = rect.Intersect(cssPageRect);
}
// Check if we can fit the full elementBoundingRect.
if (!aZoomTarget.targetRect.IsEmpty() && !zoomOut &&
aZoomTarget.elementBoundingRect.isSome()) {
MOZ_ASSERT(aZoomTarget.elementBoundingRect->Contains(rect));
CSSRect elementBoundingRect =
aZoomTarget.elementBoundingRect->Intersect(cssPageRect);
if (elementBoundingRect.width <= sizeAfterZoom.width &&
elementBoundingRect.height <= sizeAfterZoom.height) {
rect = elementBoundingRect;
}
}
// Vertically center the zoomed element in the screen.
if (!zoomOut && (sizeAfterZoom.height > rect.Height())) {
rect.MoveByY(-(sizeAfterZoom.height - rect.Height()) * 0.5f);
if (rect.Y() < 0.0f) {
rect.MoveToY(0.0f);
}
}
// Horizontally center the zoomed element in the screen.
if (!zoomOut && (sizeAfterZoom.width > rect.Width())) {
rect.MoveByX(-(sizeAfterZoom.width - rect.Width()) * 0.5f);
if (rect.X() < 0.0f) {
rect.MoveToX(0.0f);
}
}
bool intersectRectAgain = false;
// If we can't zoom out enough to show the full rect then shift the rect we
// are able to show to center what was visible.
// Note that this calculation works no matter the relation of sizeBeforeZoom
// to sizeAfterZoom, ie whether we are increasing or decreasing zoom.
if (!zoomOut && (sizeAfterZoom.height < rect.Height())) {
rect.y =
scrollOffset.y + (sizeBeforeZoom.height - sizeAfterZoom.height) / 2;
rect.height = sizeAfterZoom.Height();
intersectRectAgain = true;
}
if (!zoomOut && (sizeAfterZoom.width < rect.Width())) {
rect.x =
scrollOffset.x + (sizeBeforeZoom.width - sizeAfterZoom.width) / 2;
rect.width = sizeAfterZoom.Width();
intersectRectAgain = true;
}
if (intersectRectAgain) {
rect = rect.Intersect(cssPageRect);
}
// If any of these conditions are met, the page will be overscrolled after
// zoomed. Attempting to scroll outside of the valid scroll range will cause
// problems.
if (rect.Y() + sizeAfterZoom.height > cssPageRect.YMost()) {
rect.MoveToY(std::max(cssPageRect.Y(),
cssPageRect.YMost() - sizeAfterZoom.height));
}
if (rect.Y() < cssPageRect.Y()) {
rect.MoveToY(cssPageRect.Y());
}
if (rect.X() + sizeAfterZoom.width > cssPageRect.XMost()) {
rect.MoveToX(
std::max(cssPageRect.X(), cssPageRect.XMost() - sizeAfterZoom.width));
}
if (rect.X() < cssPageRect.X()) {
rect.MoveToY(cssPageRect.X());
}
endZoomToMetrics.SetVisualScrollOffset(rect.TopLeft());
endZoomToMetrics.RecalculateLayoutViewportOffset();
SetState(ANIMATING_ZOOM);
StartAnimation(do_AddRef(new ZoomAnimation(
*this, Metrics().GetVisualScrollOffset(), Metrics().GetZoom(),
endZoomToMetrics.GetVisualScrollOffset(), endZoomToMetrics.GetZoom())));
RequestContentRepaint(RepaintUpdateType::eUserAction);
}
}
InputBlockState* AsyncPanZoomController::GetCurrentInputBlock() const {
return GetInputQueue()->GetCurrentBlock();
}
TouchBlockState* AsyncPanZoomController::GetCurrentTouchBlock() const {
return GetInputQueue()->GetCurrentTouchBlock();
}
PanGestureBlockState* AsyncPanZoomController::GetCurrentPanGestureBlock()
const {
return GetInputQueue()->GetCurrentPanGestureBlock();
}
PinchGestureBlockState* AsyncPanZoomController::GetCurrentPinchGestureBlock()
const {
return GetInputQueue()->GetCurrentPinchGestureBlock();
}
void AsyncPanZoomController::ResetTouchInputState() {
MultiTouchInput cancel(MultiTouchInput::MULTITOUCH_CANCEL, 0,
TimeStamp::Now(), 0);
RefPtr<GestureEventListener> listener = GetGestureEventListener();
if (listener) {
listener->HandleInputEvent(cancel);
}
CancelAnimationAndGestureState();
// Clear overscroll along the entire handoff chain, in case an APZC
// later in the chain is overscrolled.
if (TouchBlockState* block = GetCurrentTouchBlock()) {
block->GetOverscrollHandoffChain()->ClearOverscroll();
}
}
void AsyncPanZoomController::ResetPanGestureInputState() {
// No point sending a PANGESTURE_INTERRUPTED as all it does is
// call CancelAnimation(), which we also do here.
CancelAnimationAndGestureState();
// Clear overscroll along the entire handoff chain, in case an APZC
// later in the chain is overscrolled.
if (PanGestureBlockState* block = GetCurrentPanGestureBlock()) {
block->GetOverscrollHandoffChain()->ClearOverscroll();
}
}
void AsyncPanZoomController::CancelAnimationAndGestureState() {
mX.CancelGesture();
mY.CancelGesture();
CancelAnimation(CancelAnimationFlags::ScrollSnap);
}
bool AsyncPanZoomController::HasReadyTouchBlock() const {
return GetInputQueue()->HasReadyTouchBlock();
}
bool AsyncPanZoomController::CanHandleScrollOffsetUpdate(PanZoomState aState) {
return aState == NOTHING || aState == PAN_MOMENTUM || aState == TOUCHING ||
IsPanningState(aState);
}
bool AsyncPanZoomController::ShouldCancelAnimationForScrollUpdate(
const Maybe<CSSPoint>& aRelativeDelta) {
// Never call CancelAnimation() for a no-op relative update.
if (aRelativeDelta == Some(CSSPoint())) {
return false;
}
if (mAnimation) {
return !mAnimation->HandleScrollOffsetUpdate(aRelativeDelta);
}
return !CanHandleScrollOffsetUpdate(mState);
}
AsyncPanZoomController::PanZoomState
AsyncPanZoomController::SetStateNoContentControllerDispatch(
PanZoomState aNewState) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
APZC_LOG_DETAIL("changing from state %s to %s\n", this,
ToString(mState).c_str(), ToString(aNewState).c_str());
PanZoomState oldState = mState;
mState = aNewState;
return oldState;
}
void AsyncPanZoomController::SetState(PanZoomState aNewState) {
// When a state transition to a transforming state is occuring and a delayed
// transform end notification exists, send the TransformEnd notification
// before the TransformBegin notification is sent for the input state change.
if (IsTransformingState(aNewState) && IsDelayedTransformEndSet()) {
MOZ_ASSERT(!IsTransformingState(mState));
SetDelayedTransformEnd(false);
DispatchStateChangeNotification(PANNING, NOTHING);
}
PanZoomState oldState = SetStateNoContentControllerDispatch(aNewState);
DispatchStateChangeNotification(oldState, aNewState);
}
auto AsyncPanZoomController::GetState() const -> PanZoomState {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return mState;
}
void AsyncPanZoomController::DispatchStateChangeNotification(
PanZoomState aOldState, PanZoomState aNewState) {
{ // scope the lock
RecursiveMutexAutoLock lock(mRecursiveMutex);
if (mNotificationBlockers > 0) {
return;
}
}
if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
if (!IsTransformingState(aOldState) && IsTransformingState(aNewState)) {
controller->NotifyAPZStateChange(GetGuid(),
APZStateChange::eTransformBegin);
} else if (IsTransformingState(aOldState) &&
!IsTransformingState(aNewState)) {
controller->NotifyAPZStateChange(GetGuid(),
APZStateChange::eTransformEnd);
}
}
}
void AsyncPanZoomController::SendTransformBeginAndEnd() {
RefPtr<GeckoContentController> controller = GetGeckoContentController();
if (controller) {
controller->NotifyAPZStateChange(GetGuid(),
APZStateChange::eTransformBegin);
controller->NotifyAPZStateChange(GetGuid(), APZStateChange::eTransformEnd);
}
}
bool AsyncPanZoomController::IsInTransformingState() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return IsTransformingState(mState);
}
bool AsyncPanZoomController::IsTransformingState(PanZoomState aState) {
return !(aState == NOTHING || aState == TOUCHING);
}
bool AsyncPanZoomController::IsPanningState(PanZoomState aState) {
return (aState == PANNING || aState == PANNING_LOCKED_X ||
aState == PANNING_LOCKED_Y);
}
bool AsyncPanZoomController::IsInPanningState() const {
return IsPanningState(mState);
}
bool AsyncPanZoomController::IsInScrollingGesture() const {
return IsPanningState(mState) || mState == SCROLLBAR_DRAG ||
mState == TOUCHING || mState == PINCHING;
}
bool AsyncPanZoomController::IsDelayedTransformEndSet() {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return mDelayedTransformEnd;
}
void AsyncPanZoomController::SetDelayedTransformEnd(bool aDelayedTransformEnd) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
mDelayedTransformEnd = aDelayedTransformEnd;
}
void AsyncPanZoomController::UpdateZoomConstraints(
const ZoomConstraints& aConstraints) {
if ((MOZ_LOG_TEST(sApzCtlLog, LogLevel::Debug) &&
(aConstraints != mZoomConstraints)) ||
MOZ_LOG_TEST(sApzCtlLog, LogLevel::Verbose)) {
APZC_LOG("%p updating zoom constraints to %d %d %f %f\n", this,
aConstraints.mAllowZoom, aConstraints.mAllowDoubleTapZoom,
aConstraints.mMinZoom.scale, aConstraints.mMaxZoom.scale);
}
if (std::isnan(aConstraints.mMinZoom.scale) ||
std::isnan(aConstraints.mMaxZoom.scale)) {
NS_WARNING("APZC received zoom constraints with NaN values; dropping...");
return;
}
RecursiveMutexAutoLock lock(mRecursiveMutex);
CSSToParentLayerScale min = Metrics().GetDevPixelsPerCSSPixel() *
ViewportMinScale() / ParentLayerToScreenScale(1);
CSSToParentLayerScale max = Metrics().GetDevPixelsPerCSSPixel() *
ViewportMaxScale() / ParentLayerToScreenScale(1);
// inf float values and other bad cases should be sanitized by the code below.
mZoomConstraints.mAllowZoom = aConstraints.mAllowZoom;
mZoomConstraints.mAllowDoubleTapZoom = aConstraints.mAllowDoubleTapZoom;
mZoomConstraints.mMinZoom =
(min > aConstraints.mMinZoom ? min : aConstraints.mMinZoom);
mZoomConstraints.mMaxZoom =
(max > aConstraints.mMaxZoom ? aConstraints.mMaxZoom : max);
if (mZoomConstraints.mMaxZoom < mZoomConstraints.mMinZoom) {
mZoomConstraints.mMaxZoom = mZoomConstraints.mMinZoom;
}
}
bool AsyncPanZoomController::ZoomConstraintsAllowZoom() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return mZoomConstraints.mAllowZoom;
}
bool AsyncPanZoomController::ZoomConstraintsAllowDoubleTapZoom() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return mZoomConstraints.mAllowDoubleTapZoom;
}
void AsyncPanZoomController::PostDelayedTask(already_AddRefed<Runnable> aTask,
int aDelayMs) {
APZThreadUtils::AssertOnControllerThread();
RefPtr<Runnable> task = aTask;
RefPtr<GeckoContentController> controller = GetGeckoContentController();
if (controller) {
controller->PostDelayedTask(task.forget(), aDelayMs);
}
// If there is no controller, that means this APZC has been destroyed, and
// we probably don't need to run the task. It will get destroyed when the
// RefPtr goes out of scope.
}
bool AsyncPanZoomController::Matches(const ScrollableLayerGuid& aGuid) {
return aGuid == GetGuid();
}
bool AsyncPanZoomController::HasTreeManager(
const APZCTreeManager* aTreeManager) const {
return GetApzcTreeManager() == aTreeManager;
}
void AsyncPanZoomController::GetGuid(ScrollableLayerGuid* aGuidOut) const {
if (aGuidOut) {
*aGuidOut = GetGuid();
}
}
ScrollableLayerGuid AsyncPanZoomController::GetGuid() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return ScrollableLayerGuid(mLayersId, Metrics().GetPresShellId(),
Metrics().GetScrollId());
}
void AsyncPanZoomController::SetTestAsyncScrollOffset(const CSSPoint& aPoint) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
mTestAsyncScrollOffset = aPoint;
ScheduleComposite();
}
void AsyncPanZoomController::SetTestAsyncZoom(
const LayerToParentLayerScale& aZoom) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
mTestAsyncZoom = aZoom;
ScheduleComposite();
}
Maybe<CSSSnapDestination> AsyncPanZoomController::FindSnapPointNear(
const CSSPoint& aDestination, ScrollUnit aUnit,
ScrollSnapFlags aSnapFlags) {
mRecursiveMutex.AssertCurrentThreadIn();
APZC_LOG("%p scroll snapping near %s\n", this,
ToString(aDestination).c_str());
CSSRect scrollRange = Metrics().CalculateScrollRange();
if (auto snapDestination = ScrollSnapUtils::GetSnapPointForDestination(
mScrollMetadata.GetSnapInfo(), aUnit, aSnapFlags,
CSSRect::ToAppUnits(scrollRange),
CSSPoint::ToAppUnits(Metrics().GetVisualScrollOffset()),
CSSPoint::ToAppUnits(aDestination))) {
CSSPoint cssSnapPoint = CSSPoint::FromAppUnits(snapDestination->mPosition);
// GetSnapPointForDestination() can produce a destination that's outside
// of the scroll frame's scroll range. Clamp it here (this matches the
// behaviour of the main-thread code path, which clamps it in
// ScrollContainerFrame::ScrollTo()).
return Some(CSSSnapDestination{scrollRange.ClampPoint(cssSnapPoint),
snapDestination->mTargetIds});
}
return Nothing();
}
Maybe<std::pair<MultiTouchInput, MultiTouchInput>>
AsyncPanZoomController::MaybeSplitTouchMoveEvent(
const MultiTouchInput& aOriginalEvent, ScreenCoord aPanThreshold,
float aVectorLength, ExternalPoint& aExtPoint) {
if (aVectorLength <= aPanThreshold) {
return Nothing();
}
auto splitEvent = std::make_pair(aOriginalEvent, aOriginalEvent);
SingleTouchData& firstTouchData = splitEvent.first.mTouches[0];
SingleTouchData& secondTouchData = splitEvent.second.mTouches[0];
firstTouchData.mHistoricalData.Clear();
secondTouchData.mHistoricalData.Clear();
ExternalPoint destination = aExtPoint;
ExternalPoint thresholdPosition;
const float ratio = aPanThreshold / aVectorLength;
thresholdPosition.x = mStartTouch.x + ratio * (destination.x - mStartTouch.x);
thresholdPosition.y = mStartTouch.y + ratio * (destination.y - mStartTouch.y);
TouchSample start{mLastTouch};
// To compute the timestamp of the first event (which is at the threshold),
// use linear interpolation with the starting point |start| being the last
// event that's before the threshold, and the end point |end| being the first
// event after the threshold.
// The initial choice for |start| is the last touch event before
// |aOriginalEvent|, and the initial choice for |end| is |aOriginalEvent|.
// However, the historical data points stored in |aOriginalEvent| may contain
// intermediate positions that can serve as tighter bounds for the
// interpolation.
TouchSample end{destination, aOriginalEvent.mTimeStamp};
for (const auto& historicalData :
aOriginalEvent.mTouches[0].mHistoricalData) {
ExternalPoint histExtPoint = ToExternalPoint(aOriginalEvent.mScreenOffset,
historicalData.mScreenPoint);
if (PanVector(histExtPoint).Length() <
PanVector(thresholdPosition).Length()) {
start = {histExtPoint, historicalData.mTimeStamp};
} else {
break;
}
}
for (const SingleTouchData::HistoricalTouchData& histData :
Reversed(aOriginalEvent.mTouches[0].mHistoricalData)) {
ExternalPoint histExtPoint =
ToExternalPoint(aOriginalEvent.mScreenOffset, histData.mScreenPoint);
if (PanVector(histExtPoint).Length() >
PanVector(thresholdPosition).Length()) {
end = {histExtPoint, histData.mTimeStamp};
} else {
break;
}
}
const float totalLength =
ScreenPoint(fabs(end.mPosition.x - start.mPosition.x),
fabs(end.mPosition.y - start.mPosition.y))
.Length();
const float thresholdLength =
ScreenPoint(fabs(thresholdPosition.x - start.mPosition.x),
fabs(thresholdPosition.y - start.mPosition.y))
.Length();
const float splitRatio = thresholdLength / totalLength;
splitEvent.first.mTimeStamp =
start.mTimeStamp +
(end.mTimeStamp - start.mTimeStamp).MultDouble(splitRatio);
for (const auto& historicalData :
aOriginalEvent.mTouches[0].mHistoricalData) {
if (historicalData.mTimeStamp > splitEvent.first.mTimeStamp) {
secondTouchData.mHistoricalData.AppendElement(historicalData);
} else {
firstTouchData.mHistoricalData.AppendElement(historicalData);
}
}
firstTouchData.mScreenPoint = RoundedToInt(
ViewAs<ScreenPixel>(thresholdPosition - splitEvent.first.mScreenOffset,
PixelCastJustification::ExternalIsScreen));
// Recompute firstTouchData.mLocalScreenPoint.
splitEvent.first.TransformToLocal(
GetCurrentTouchBlock()->GetTransformToApzc());
// Pass |thresholdPosition| back out to the caller via |aExtPoint|
aExtPoint = thresholdPosition;
return Some(splitEvent);
}
void AsyncPanZoomController::ScrollSnapNear(const CSSPoint& aDestination,
ScrollSnapFlags aSnapFlags) {
if (Maybe<CSSSnapDestination> snapDestination = FindSnapPointNear(
aDestination, ScrollUnit::DEVICE_PIXELS, aSnapFlags)) {
if (snapDestination->mPosition != Metrics().GetVisualScrollOffset()) {
APZC_LOG("%p smooth scrolling to snap point %s\n", this,
ToString(snapDestination->mPosition).c_str());
SmoothMsdScrollTo(std::move(*snapDestination),
ScrollTriggeredByScript::No);
}
}
}
void AsyncPanZoomController::ScrollSnap(ScrollSnapFlags aSnapFlags) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
ScrollSnapNear(Metrics().GetVisualScrollOffset(), aSnapFlags);
}
void AsyncPanZoomController::ScrollSnapToDestination() {
RecursiveMutexAutoLock lock(mRecursiveMutex);
float friction = StaticPrefs::apz_fling_friction();
ParentLayerPoint velocity(mX.GetVelocity(), mY.GetVelocity());
ParentLayerPoint predictedDelta;
// "-velocity / log(1.0 - friction)" is the integral of the deceleration
// curve modeled for flings in the "Axis" class.
if (velocity.x != 0.0f && friction != 0.0f) {
predictedDelta.x = -velocity.x / log(1.0 - friction);
}
if (velocity.y != 0.0f && friction != 0.0f) {
predictedDelta.y = -velocity.y / log(1.0 - friction);
}
// If the fling will overscroll, don't scroll snap, because then the user
// user would not see any overscroll animation.
bool flingWillOverscroll =
IsOverscrolled() && ((velocity.x.value * mX.GetOverscroll() >= 0) ||
(velocity.y.value * mY.GetOverscroll() >= 0));
if (flingWillOverscroll) {
return;
}
CSSPoint startPosition = Metrics().GetVisualScrollOffset();
ScrollSnapFlags snapFlags = ScrollSnapFlags::IntendedEndPosition;
if (predictedDelta != ParentLayerPoint()) {
snapFlags |= ScrollSnapFlags::IntendedDirection;
}
if (Maybe<CSSSnapDestination> snapDestination =
MaybeAdjustDeltaForScrollSnapping(ScrollUnit::DEVICE_PIXELS,
snapFlags, predictedDelta,
startPosition)) {
APZC_LOG(
"%p fling snapping. friction: %f velocity: %f, %f "
"predictedDelta: %f, %f position: %f, %f "
"snapDestination: %f, %f\n",
this, friction, velocity.x.value, velocity.y.value,
predictedDelta.x.value, predictedDelta.y.value,
Metrics().GetVisualScrollOffset().x.value,
Metrics().GetVisualScrollOffset().y.value, startPosition.x.value,
startPosition.y.value);
// Ensure that any queued transform-end due to a pan-end is not
// sent. Instead rely on the transform-end sent due to the
// scroll snap animation.
SetDelayedTransformEnd(false);
SmoothMsdScrollTo(std::move(*snapDestination), ScrollTriggeredByScript::No);
}
}
Maybe<CSSSnapDestination>
AsyncPanZoomController::MaybeAdjustDeltaForScrollSnapping(
ScrollUnit aUnit, ScrollSnapFlags aSnapFlags, ParentLayerPoint& aDelta,
CSSPoint& aStartPosition) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
CSSToParentLayerScale zoom = Metrics().GetZoom();
if (zoom == CSSToParentLayerScale(0)) {
return Nothing();
}
CSSPoint destination = Metrics().CalculateScrollRange().ClampPoint(
aStartPosition + ToCSSPixels(aDelta));
if (Maybe<CSSSnapDestination> snapDestination =
FindSnapPointNear(destination, aUnit, aSnapFlags)) {
aDelta = (snapDestination->mPosition - aStartPosition) * zoom;
aStartPosition = snapDestination->mPosition;
return snapDestination;
}
return Nothing();
}
Maybe<CSSSnapDestination>
AsyncPanZoomController::MaybeAdjustDeltaForScrollSnappingOnWheelInput(
const ScrollWheelInput& aEvent, ParentLayerPoint& aDelta,
CSSPoint& aStartPosition) {
// Don't scroll snap for pixel scrolls. This matches the main thread
// behaviour in EventStateManager::DoScrollText().
if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_PIXEL) {
return Nothing();
}
// Note that this MaybeAdjustDeltaForScrollSnappingOnWheelInput also gets
// called for pan gestures at least on older Mac and Windows. In such cases
// `aEvent.mDeltaType` is `SCROLLDELTA_PIXEL` which should be filtered out by
// the above `if` block, so we assume all incoming `aEvent` are purely wheel
// events, thus we basically use `IntendedDirection` here.
// If we want to change the behavior, i.e. we want to do scroll snap for
// such cases as well, we need to use `IntendedEndPoint`.
ScrollSnapFlags snapFlags = ScrollSnapFlags::IntendedDirection;
if (aEvent.mDeltaType == ScrollWheelInput::SCROLLDELTA_PAGE) {
// On Windows there are a couple of cases where scroll events happen with
// SCROLLDELTA_PAGE, in such case we consider it's a page scroll.
snapFlags |= ScrollSnapFlags::IntendedEndPosition;
}
return MaybeAdjustDeltaForScrollSnapping(
ScrollWheelInput::ScrollUnitForDeltaType(aEvent.mDeltaType),
ScrollSnapFlags::IntendedDirection, aDelta, aStartPosition);
}
Maybe<CSSSnapDestination>
AsyncPanZoomController::MaybeAdjustDestinationForScrollSnapping(
const KeyboardInput& aEvent, CSSPoint& aDestination,
ScrollSnapFlags aSnapFlags) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
ScrollUnit unit = KeyboardScrollAction::GetScrollUnit(aEvent.mAction.mType);
if (Maybe<CSSSnapDestination> snapPoint =
FindSnapPointNear(aDestination, unit, aSnapFlags)) {
aDestination = snapPoint->mPosition;
return snapPoint;
}
return Nothing();
}
void AsyncPanZoomController::SetZoomAnimationId(
const Maybe<uint64_t>& aZoomAnimationId) {
RecursiveMutexAutoLock lock(mRecursiveMutex);
mZoomAnimationId = aZoomAnimationId;
}
Maybe<uint64_t> AsyncPanZoomController::GetZoomAnimationId() const {
RecursiveMutexAutoLock lock(mRecursiveMutex);
return mZoomAnimationId;
}
CSSPoint AsyncPanZoomController::MaybeFillOutOverscrollGutter(
const RecursiveMutexAutoLock& aProofOfLock) {
CSSPoint delta = ToCSSPixels(GetOverscrollAmount());
CSSPoint origin = Metrics().GetVisualScrollOffset();
CSSRect scrollRange = Metrics().CalculateScrollRange();
if (!scrollRange.ContainsInclusively(origin + delta)) {
return CSSPoint();
}
SetVisualScrollOffset(origin + delta);
Metrics().RecalculateLayoutViewportOffset();
return Metrics().GetVisualScrollOffset() - origin;
}
std::ostream& operator<<(std::ostream& aOut,
const AsyncPanZoomController::PanZoomState& aState) {
switch (aState) {
case AsyncPanZoomController::PanZoomState::NOTHING:
aOut << "NOTHING";
break;
case AsyncPanZoomController::PanZoomState::FLING:
aOut << "FLING";
break;
case AsyncPanZoomController::PanZoomState::TOUCHING:
aOut << "TOUCHING";
break;
case AsyncPanZoomController::PanZoomState::PANNING:
aOut << "PANNING";
break;
case AsyncPanZoomController::PanZoomState::PANNING_LOCKED_X:
aOut << "PANNING_LOCKED_X";
break;
case AsyncPanZoomController::PanZoomState::PANNING_LOCKED_Y:
aOut << "PANNING_LOCKED_Y";
break;
case AsyncPanZoomController::PanZoomState::PAN_MOMENTUM:
aOut << "PAN_MOMENTUM";
break;
case AsyncPanZoomController::PanZoomState::PINCHING:
aOut << "PINCHING";
break;
case AsyncPanZoomController::PanZoomState::ANIMATING_ZOOM:
aOut << "ANIMATING_ZOOM";
break;
case AsyncPanZoomController::PanZoomState::OVERSCROLL_ANIMATION:
aOut << "OVERSCROLL_ANIMATION";
break;
case AsyncPanZoomController::PanZoomState::SMOOTH_SCROLL:
aOut << "SMOOTH_SCROLL";
break;
case AsyncPanZoomController::PanZoomState::SMOOTHMSD_SCROLL:
aOut << "SMOOTHMSD_SCROLL";
break;
case AsyncPanZoomController::PanZoomState::WHEEL_SCROLL:
aOut << "WHEEL_SCROLL";
break;
case AsyncPanZoomController::PanZoomState::KEYBOARD_SCROLL:
aOut << "KEYBOARD_SCROLL";
break;
case AsyncPanZoomController::PanZoomState::AUTOSCROLL:
aOut << "AUTOSCROLL";
break;
case AsyncPanZoomController::PanZoomState::SCROLLBAR_DRAG:
aOut << "SCROLLBAR_DRAG";
break;
default:
aOut << "UNKNOWN_STATE";
break;
}
return aOut;
}
bool operator==(const PointerEventsConsumableFlags& aLhs,
const PointerEventsConsumableFlags& aRhs) {
return (aLhs.mHasRoom == aRhs.mHasRoom) &&
(aLhs.mAllowedByTouchAction == aRhs.mAllowedByTouchAction);
}
std::ostream& operator<<(std::ostream& aOut,
const PointerEventsConsumableFlags& aFlags) {
aOut << std::boolalpha << "{ hasRoom: " << aFlags.mHasRoom
<< ", allowedByTouchAction: " << aFlags.mAllowedByTouchAction << "}";
return aOut;
}
} // namespace layers
} // namespace mozilla