Bug 1026023 - Part 3: Integrate MSD movement with nsGfxScrollFrame. r=mattwoodrow

- Added nsIScrollableFrame::ScrollMode::SMOOTH_MSD to differentiate
  existing smooth scrolls used by keyboard and mousewheel events from the
  CSSOM-View scroll-behavior's MSD motion scrolling.
- Implemented ScrollFrameHelper::AsyncSmoothMSDScroll, which takes the role
  of ScrollFrameHelper::AsyncScroll when SMOOTH_MSD scrolls are requested.
- Implemented glue code to handle callbacks from AsyncSmoothMSDScroll and
  to hand off velocity between the classes when one scroll animation is
  interrupted by another.
This commit is contained in:
Kearwood (Kip) Gilbert 2014-07-09 10:02:31 -07:00
parent 8062fdbcec
commit 1453cb535f
3 changed files with 302 additions and 59 deletions

View File

@ -53,6 +53,8 @@
#include "StickyScrollContainer.h"
#include "nsIFrameInlines.h"
#include "gfxPrefs.h"
#include <mozilla/layers/AxisPhysicsModel.h>
#include <mozilla/layers/AxisPhysicsMSDModel.h>
#include <algorithm>
#include <cstdlib> // for std::abs(int/long)
#include <cmath> // for std::abs(float/double)
@ -1244,6 +1246,136 @@ NS_QUERYFRAME_TAIL_INHERITING(nsBoxFrame)
const double kCurrentVelocityWeighting = 0.25;
const double kStopDecelerationWeighting = 0.4;
// AsyncSmoothMSDScroll has ref counting.
class ScrollFrameHelper::AsyncSmoothMSDScroll MOZ_FINAL : public nsARefreshObserver {
public:
AsyncSmoothMSDScroll(const nsPoint &aInitialPosition,
const nsPoint &aInitialDestination,
const nsSize &aInitialVelocity,
const nsRect &aRange,
const mozilla::TimeStamp &aStartTime)
: mXAxisModel(aInitialPosition.x, aInitialDestination.x,
aInitialVelocity.width,
gfxPrefs::ScrollBehaviorSpringConstant(),
gfxPrefs::ScrollBehaviorDampingRatio())
, mYAxisModel(aInitialPosition.y, aInitialDestination.y,
aInitialVelocity.height,
gfxPrefs::ScrollBehaviorSpringConstant(),
gfxPrefs::ScrollBehaviorDampingRatio())
, mRange(aRange)
, mLastRefreshTime(aStartTime)
, mCallee(nullptr)
{
}
NS_INLINE_DECL_REFCOUNTING(AsyncSmoothMSDScroll)
nsSize GetVelocity() {
// In nscoords per second
return nsSize(mXAxisModel.GetVelocity(), mYAxisModel.GetVelocity());
}
nsPoint GetPosition() {
// In nscoords
return nsPoint(NSToCoordRound(mXAxisModel.GetPosition()), NSToCoordRound(mYAxisModel.GetPosition()));
}
void SetDestination(const nsPoint &aDestination) {
mXAxisModel.SetDestination(static_cast<int32_t>(aDestination.x));
mYAxisModel.SetDestination(static_cast<int32_t>(aDestination.y));
}
void SetRange(const nsRect &aRange)
{
mRange = aRange;
}
nsRect GetRange()
{
return mRange;
}
void Simulate(const TimeDuration& aDeltaTime)
{
mXAxisModel.Simulate(aDeltaTime);
mYAxisModel.Simulate(aDeltaTime);
nsPoint desired = GetPosition();
nsPoint clamped = mRange.ClampPoint(desired);
if(desired.x != clamped.x) {
// The scroll has hit the "wall" at the left or right edge of the allowed
// scroll range.
// Absorb the impact to avoid bounceback effect.
mXAxisModel.SetVelocity(0.0);
mXAxisModel.SetPosition(clamped.x);
}
if(desired.y != clamped.y) {
// The scroll has hit the "wall" at the left or right edge of the allowed
// scroll range.
// Absorb the impact to avoid bounceback effect.
mYAxisModel.SetVelocity(0.0);
mYAxisModel.SetPosition(clamped.y);
}
}
bool IsFinished()
{
return mXAxisModel.IsFinished() && mYAxisModel.IsFinished();
}
virtual void WillRefresh(mozilla::TimeStamp aTime) MOZ_OVERRIDE {
mozilla::TimeDuration deltaTime = aTime - mLastRefreshTime;
mLastRefreshTime = aTime;
// The callback may release "this".
// We don't access members after returning, so no need for KungFuDeathGrip.
ScrollFrameHelper::AsyncSmoothMSDScrollCallback(mCallee, deltaTime);
}
/*
* Set a refresh observer for smooth scroll iterations (and start observing).
* Should be used at most once during the lifetime of this object.
* Return value: true on success, false otherwise.
*/
bool SetRefreshObserver(ScrollFrameHelper *aCallee) {
NS_ASSERTION(aCallee && !mCallee, "AsyncSmoothMSDScroll::SetRefreshObserver - Invalid usage.");
if (!RefreshDriver(aCallee)->AddRefreshObserver(this, Flush_Style)) {
return false;
}
mCallee = aCallee;
return true;
}
private:
// Private destructor, to discourage deletion outside of Release():
~AsyncSmoothMSDScroll() {
RemoveObserver();
}
nsRefreshDriver* RefreshDriver(ScrollFrameHelper* aCallee) {
return aCallee->mOuter->PresContext()->RefreshDriver();
}
/*
* The refresh driver doesn't hold a reference to its observers,
* so releasing this object can (and is) used to remove the observer on DTOR.
* Currently, this object is released once the scrolling ends.
*/
void RemoveObserver() {
if (mCallee) {
RefreshDriver(mCallee)->RemoveRefreshObserver(this, Flush_Style);
}
}
mozilla::layers::AxisPhysicsMSDModel mXAxisModel, mYAxisModel;
nsRect mRange;
mozilla::TimeStamp mLastRefreshTime;
ScrollFrameHelper *mCallee;
};
// AsyncScroll has ref counting.
class ScrollFrameHelper::AsyncScroll MOZ_FINAL : public nsARefreshObserver {
public:
@ -1267,7 +1399,8 @@ public:
nsSize VelocityAt(TimeStamp aTime); // In nscoords per second
void InitSmoothScroll(TimeStamp aTime, nsPoint aDestination,
nsIAtom *aOrigin, const nsRect& aRange);
nsIAtom *aOrigin, const nsRect& aRange,
const nsSize& aCurrentVelocity);
void Init(const nsRect& aRange) {
mRange = aRange;
}
@ -1466,10 +1599,11 @@ void
ScrollFrameHelper::AsyncScroll::InitSmoothScroll(TimeStamp aTime,
nsPoint aDestination,
nsIAtom *aOrigin,
const nsRect& aRange) {
const nsRect& aRange,
const nsSize& aCurrentVelocity) {
mRange = aRange;
TimeDuration duration = CalcDurationForEventTime(aTime, aOrigin);
nsSize currentVelocity(0, 0);
nsSize currentVelocity = aCurrentVelocity;
if (!mIsFirstIteration) {
// If an additional event has not changed the destination, then do not let
// another minimum duration reset slow things down. If it would then
@ -1569,6 +1703,7 @@ ScrollFrameHelper::ScrollFrameHelper(nsContainerFrame* aOuter,
, mResizerBox(nullptr)
, mOuter(aOuter)
, mAsyncScroll(nullptr)
, mAsyncSmoothMSDScroll(nullptr)
, mOriginOfLastScroll(nsGkAtoms::other)
, mScrollGeneration(++sScrollGenerationCounter)
, mDestination(0, 0)
@ -1641,45 +1776,84 @@ ScrollFrameHelper::~ScrollFrameHelper()
}
}
/*
* Callback function from AsyncSmoothMSDScroll, used in ScrollFrameHelper::ScrollTo
*/
void
ScrollFrameHelper::AsyncSmoothMSDScrollCallback(ScrollFrameHelper* aInstance,
mozilla::TimeDuration aDeltaTime)
{
NS_ASSERTION(aInstance != nullptr, "aInstance must not be null");
NS_ASSERTION(aInstance->mAsyncSmoothMSDScroll,
"Did not expect AsyncSmoothMSDScrollCallback without an active MSD scroll.");
nsRect range = aInstance->mAsyncSmoothMSDScroll->GetRange();
aInstance->mAsyncSmoothMSDScroll->Simulate(aDeltaTime);
if (!aInstance->mAsyncSmoothMSDScroll->IsFinished()) {
nsPoint destination = aInstance->mAsyncSmoothMSDScroll->GetPosition();
// Allow this scroll operation to land on any pixel boundary within the
// allowed scroll range for this frame.
// If the MSD is under-dampened or the destination is changed rapidly,
// it is expected (and desired) that the scrolling may overshoot.
nsRect intermediateRange =
nsRect(destination, nsSize()).UnionEdges(range);
aInstance->ScrollToImpl(destination, intermediateRange);
// 'aInstance' might be destroyed here
return;
}
aInstance->CompleteAsyncScroll(range);
}
/*
* Callback function from AsyncScroll, used in ScrollFrameHelper::ScrollTo
*/
void
ScrollFrameHelper::AsyncScrollCallback(void* anInstance, mozilla::TimeStamp aTime)
ScrollFrameHelper::AsyncScrollCallback(ScrollFrameHelper* aInstance,
mozilla::TimeStamp aTime)
{
ScrollFrameHelper* self = static_cast<ScrollFrameHelper*>(anInstance);
if (!self || !self->mAsyncScroll)
return;
NS_ASSERTION(aInstance != nullptr, "aInstance must not be null");
NS_ASSERTION(aInstance->mAsyncScroll,
"Did not expect AsyncScrollCallback without an active async scroll.");
nsRect range = self->mAsyncScroll->mRange;
if (self->mAsyncScroll->mIsSmoothScroll) {
if (!self->mAsyncScroll->IsFinished(aTime)) {
nsPoint destination = self->mAsyncScroll->PositionAt(aTime);
nsRect range = aInstance->mAsyncScroll->mRange;
if (aInstance->mAsyncScroll->mIsSmoothScroll) {
if (!aInstance->mAsyncScroll->IsFinished(aTime)) {
nsPoint destination = aInstance->mAsyncScroll->PositionAt(aTime);
// Allow this scroll operation to land on any pixel boundary between the
// current position and the final allowed range. (We don't want
// intermediate steps to be more constrained than the final step!)
nsRect intermediateRange =
nsRect(self->GetScrollPosition(), nsSize()).UnionEdges(range);
self->ScrollToImpl(destination, intermediateRange);
// 'self' might be destroyed here
nsRect(aInstance->GetScrollPosition(), nsSize()).UnionEdges(range);
aInstance->ScrollToImpl(destination, intermediateRange);
// 'aInstance' might be destroyed here
return;
}
}
aInstance->CompleteAsyncScroll(range);
}
void
ScrollFrameHelper::CompleteAsyncScroll(const nsRect &aRange, nsIAtom* aOrigin)
{
// Apply desired destination range since this is the last step of scrolling.
self->mAsyncScroll = nullptr;
nsWeakFrame weakFrame(self->mOuter);
self->ScrollToImpl(self->mDestination, range);
mAsyncSmoothMSDScroll = nullptr;
mAsyncScroll = nullptr;
nsWeakFrame weakFrame(mOuter);
ScrollToImpl(mDestination, aRange, aOrigin);
if (!weakFrame.IsAlive()) {
return;
}
// We are done scrolling, set our destination to wherever we actually ended
// up scrolling to.
self->mDestination = self->GetScrollPosition();
mDestination = GetScrollPosition();
}
void
ScrollFrameHelper::ScrollToCSSPixels(const CSSIntPoint& aScrollPosition)
ScrollFrameHelper::ScrollToCSSPixels(const CSSIntPoint& aScrollPosition,
nsIScrollableFrame::ScrollMode aMode)
{
nsPoint current = GetScrollPosition();
CSSIntPoint currentCSSPixels = GetScrollPositionCSSPixels();
@ -1699,7 +1873,7 @@ ScrollFrameHelper::ScrollToCSSPixels(const CSSIntPoint& aScrollPosition)
range.y = pt.y;
range.height = 0;
}
ScrollTo(pt, nsIScrollableFrame::INSTANT, &range);
ScrollTo(pt, aMode, &range);
// 'this' might be destroyed here
}
@ -1738,15 +1912,7 @@ ScrollFrameHelper::ScrollToWithOrigin(nsPoint aScrollPosition,
if (aMode == nsIScrollableFrame::INSTANT) {
// Asynchronous scrolling is not allowed, so we'll kill any existing
// async-scrolling process and do an instant scroll.
mAsyncScroll = nullptr;
nsWeakFrame weakFrame(mOuter);
ScrollToImpl(mDestination, range, aOrigin);
if (!weakFrame.IsAlive()) {
return;
}
// We are done scrolling, set our destination to wherever we actually ended
// up scrolling to.
mDestination = GetScrollPosition();
CompleteAsyncScroll(range, aOrigin);
return;
}
@ -1754,19 +1920,48 @@ ScrollFrameHelper::ScrollToWithOrigin(nsPoint aScrollPosition,
bool isSmoothScroll = (aMode == nsIScrollableFrame::SMOOTH) &&
IsSmoothScrollingEnabled();
nsSize currentVelocity(0, 0);
if (gfxPrefs::ScrollBehaviorEnabled()) {
if (aMode == nsIScrollableFrame::SMOOTH_MSD) {
if (!mAsyncSmoothMSDScroll) {
if (mAsyncScroll) {
if (mAsyncScroll->mIsSmoothScroll) {
currentVelocity = mAsyncScroll->VelocityAt(now);
}
mAsyncScroll = nullptr;
}
mAsyncSmoothMSDScroll =
new AsyncSmoothMSDScroll(GetScrollPosition(), mDestination,
currentVelocity, GetScrollRangeForClamping(),
now);
if (!mAsyncSmoothMSDScroll->SetRefreshObserver(this)) {
// Observer setup failed. Scroll the normal way.
CompleteAsyncScroll(range, aOrigin);
return;
}
} else {
// A previous smooth MSD scroll is still in progress, so we just need to
// update its destination.
mAsyncSmoothMSDScroll->SetDestination(mDestination);
}
return;
} else {
if (mAsyncSmoothMSDScroll) {
currentVelocity = mAsyncSmoothMSDScroll->GetVelocity();
mAsyncSmoothMSDScroll = nullptr;
}
}
}
if (!mAsyncScroll) {
mAsyncScroll = new AsyncScroll(GetScrollPosition());
if (!mAsyncScroll->SetRefreshObserver(this)) {
mAsyncScroll = nullptr;
// Observer setup failed. Scroll the normal way.
nsWeakFrame weakFrame(mOuter);
ScrollToImpl(mDestination, range, aOrigin);
if (!weakFrame.IsAlive()) {
return;
}
// We are done scrolling, set our destination to wherever we actually
// ended up scrolling to.
mDestination = GetScrollPosition();
CompleteAsyncScroll(range, aOrigin);
return;
}
}
@ -1774,7 +1969,7 @@ ScrollFrameHelper::ScrollToWithOrigin(nsPoint aScrollPosition,
mAsyncScroll->mIsSmoothScroll = isSmoothScroll;
if (isSmoothScroll) {
mAsyncScroll->InitSmoothScroll(now, mDestination, aOrigin, range);
mAsyncScroll->InitSmoothScroll(now, mDestination, aOrigin, range, currentVelocity);
} else {
mAsyncScroll->Init(range);
}
@ -2826,6 +3021,13 @@ ScrollFrameHelper::ScrollBy(nsIntPoint aDelta,
nsIntPoint* aOverflow,
nsIAtom *aOrigin)
{
if (mAsyncSmoothMSDScroll != nullptr) {
// When CSSOM-View scroll-behavior smooth scrolling is interrupted,
// the scroll is not completed to avoid non-smooth snapping to the
// prior smooth scroll's destination.
mDestination = GetScrollPosition();
}
nsSize deltaMultiplier;
float negativeTolerance;
float positiveTolerance;
@ -3966,7 +4168,7 @@ ScrollFrameHelper::ReflowFinished()
// do anything.
nsPoint currentScrollPos = GetScrollPosition();
ScrollToImpl(currentScrollPos, nsRect(currentScrollPos, nsSize(0, 0)));
if (!mAsyncScroll) {
if (!mAsyncScroll && !mAsyncSmoothMSDScroll) {
// We need to have mDestination track the current scroll position,
// in case it falls outside the new reflow area. mDestination is used
// by ScrollBy as its starting position.

View File

@ -44,6 +44,7 @@ public:
typedef mozilla::layout::ScrollbarActivity ScrollbarActivity;
class AsyncScroll;
class AsyncSmoothMSDScroll;
ScrollFrameHelper(nsContainerFrame* aOuter, bool aIsRoot);
~ScrollFrameHelper();
@ -172,7 +173,10 @@ protected:
nsRect GetScrollRangeForClamping() const;
public:
static void AsyncScrollCallback(void* anInstance, mozilla::TimeStamp aTime);
static void AsyncScrollCallback(ScrollFrameHelper* aInstance,
mozilla::TimeStamp aTime);
static void AsyncSmoothMSDScrollCallback(ScrollFrameHelper* aInstance,
mozilla::TimeDuration aDeltaTime);
/**
* @note This method might destroy the frame, pres shell and other objects.
* aRange is the range of allowable scroll positions around the desired
@ -186,7 +190,9 @@ public:
/**
* @note This method might destroy the frame, pres shell and other objects.
*/
void ScrollToCSSPixels(const CSSIntPoint& aScrollPosition);
void ScrollToCSSPixels(const CSSIntPoint& aScrollPosition,
nsIScrollableFrame::ScrollMode aMode
= nsIScrollableFrame::INSTANT);
/**
* @note This method might destroy the frame, pres shell and other objects.
*/
@ -269,7 +275,9 @@ public:
bool IsLTR() const;
bool IsScrollbarOnRight() const;
bool IsScrollingActive() const { return mScrollingActive || mShouldBuildScrollableLayer; }
bool IsProcessingAsyncScroll() const { return mAsyncScroll != nullptr; }
bool IsProcessingAsyncScroll() const {
return mAsyncScroll != nullptr || mAsyncSmoothMSDScroll != nullptr;
}
void ResetScrollPositionForLayerPixelAlignment()
{
mScrollPosForLayerPixelAlignment = GetScrollPosition();
@ -332,6 +340,7 @@ public:
nsIFrame* mResizerBox;
nsContainerFrame* mOuter;
nsRefPtr<AsyncScroll> mAsyncScroll;
nsRefPtr<AsyncSmoothMSDScroll> mAsyncSmoothMSDScroll;
nsRefPtr<ScrollbarActivity> mScrollbarActivity;
nsTArray<nsIScrollPositionListener*> mListeners;
nsIAtom* mOriginOfLastScroll;
@ -422,6 +431,8 @@ protected:
nsIAtom *aOrigin, // nullptr indicates "other" origin
const nsRect* aRange);
void CompleteAsyncScroll(const nsRect &aRange, nsIAtom* aOrigin = nullptr);
static void EnsureImageVisPrefsCached();
static bool sImageVisPrefsCached;
// The number of scrollports wide/high to expand when looking for images.
@ -609,8 +620,10 @@ public:
/**
* @note This method might destroy the frame, pres shell and other objects.
*/
virtual void ScrollToCSSPixels(const CSSIntPoint& aScrollPosition) MOZ_OVERRIDE {
mHelper.ScrollToCSSPixels(aScrollPosition);
virtual void ScrollToCSSPixels(const CSSIntPoint& aScrollPosition,
nsIScrollableFrame::ScrollMode aMode
= nsIScrollableFrame::INSTANT) MOZ_OVERRIDE {
mHelper.ScrollToCSSPixels(aScrollPosition, aMode);
}
virtual void ScrollToCSSPixelsApproximate(const mozilla::CSSPoint& aScrollPosition,
nsIAtom* aOrigin = nullptr) MOZ_OVERRIDE {
@ -931,8 +944,10 @@ public:
/**
* @note This method might destroy the frame, pres shell and other objects.
*/
virtual void ScrollToCSSPixels(const CSSIntPoint& aScrollPosition) MOZ_OVERRIDE {
mHelper.ScrollToCSSPixels(aScrollPosition);
virtual void ScrollToCSSPixels(const CSSIntPoint& aScrollPosition,
nsIScrollableFrame::ScrollMode aMode
= nsIScrollableFrame::INSTANT) MOZ_OVERRIDE {
mHelper.ScrollToCSSPixels(aScrollPosition, aMode);
}
virtual void ScrollToCSSPixelsApproximate(const mozilla::CSSPoint& aScrollPosition,
nsIAtom* aOrigin = nullptr) MOZ_OVERRIDE {

View File

@ -154,14 +154,33 @@ public:
virtual nsSize GetPageScrollAmount() const = 0;
/**
* When a scroll operation is requested, we ask for instant, smooth or normal
* scrolling. SMOOTH will only be smooth if smooth scrolling is actually
* enabled. INSTANT is always synchronous, NORMAL can be asynchronous.
* If an INSTANT request happens while a smooth or async scroll is already in
* progress, the async scroll is interrupted and we instantly scroll to the
* destination.
* When a scroll operation is requested, we ask for instant, smooth,
* smooth msd, or normal scrolling.
*
* SMOOTH scrolls have a symmetrical acceleration and deceleration curve
* modeled with a set of splines that guarantee that the destination will be
* reached over a fixed time interval. SMOOTH will only be smooth if smooth
* scrolling is actually enabled. This behavior is utilized by keyboard and
* mouse wheel scrolling events.
*
* SMOOTH_MSD implements a physically based model that approximates the
* behavior of a mass-spring-damper system. SMOOTH_MSD scrolls have a
* non-symmetrical acceleration and deceleration curve, can potentially
* overshoot the destination on intermediate frames, and complete over a
* variable time interval. SMOOTH_MSD will only be smooth if cssom-view
* smooth-scrolling is enabled.
*
* INSTANT is always synchronous, NORMAL can be asynchronous.
*
* If an INSTANT scroll request happens while a SMOOTH or async scroll is
* already in progress, the async scroll is interrupted and we instantly
* scroll to the destination.
*
* If an INSTANT or SMOOTH scroll request happens while a SMOOTH_MSD scroll
* is already in progress, the SMOOTH_MSD scroll is interrupted without
* first scrolling to the destination.
*/
enum ScrollMode { INSTANT, SMOOTH, NORMAL };
enum ScrollMode { INSTANT, SMOOTH, SMOOTH_MSD, NORMAL };
/**
* @note This method might destroy the frame, pres shell and other objects.
* Clamps aScrollPosition to GetScrollRange and sets the scroll position
@ -180,11 +199,19 @@ public:
* position, rounded to CSS pixels, matches aScrollPosition. If
* aScrollPosition.x/y is different from the current CSS pixel position,
* makes sure we only move in the direction given by the difference.
* Ensures that GetScrollPositionCSSPixels (the scroll position after
* rounding to CSS pixels) will be exactly aScrollPosition.
* The scroll mode is INSTANT.
*
* When aMode is SMOOTH, INSTANT, or NORMAL, GetScrollPositionCSSPixels (the
* scroll position after rounding to CSS pixels) will be exactly
* aScrollPosition at the end of the scroll animation.
*
* When aMode is SMOOTH_MSD, intermediate animation frames may be outside the
* range and / or moving in any direction; GetScrollPositionCSSPixels will be
* exactly aScrollPosition at the end of the scroll animation unless the
* SMOOTH_MSD animation is interrupted.
*/
virtual void ScrollToCSSPixels(const CSSIntPoint& aScrollPosition) = 0;
virtual void ScrollToCSSPixels(const CSSIntPoint& aScrollPosition,
nsIScrollableFrame::ScrollMode aMode
= nsIScrollableFrame::INSTANT) = 0;
/**
* @note This method might destroy the frame, pres shell and other objects.
* Scrolls to a particular position in float CSS pixels.
@ -192,7 +219,6 @@ public:
* aScrollPosition afterward. It tries to scroll as close to
* aScrollPosition as possible while scrolling by an integer
* number of layer pixels (so the operation is fast and looks clean).
* The scroll mode is INSTANT.
*/
virtual void ScrollToCSSPixelsApproximate(const mozilla::CSSPoint& aScrollPosition,
nsIAtom *aOrigin = nullptr) = 0;